SQL to CSV Converter
Turn a dump file back into a spreadsheet. Every INSERT in the text is read, grouped by the table it targets, and written out as rows you can open anywhere.
To convert SQL to CSV, paste your INSERT statements or drop a .sql file into the box above. The reader tokenizes the script, pulls the column list from each INSERT INTO or from a matching CREATE TABLE, collects every VALUES tuple, and writes the result as CSV. NULL becomes an empty cell, quoted strings keep their apostrophes, and nothing leaves your browser.
Need to filter or join before you export? Open the app
What a dump actually looks like when it reaches you
Nobody sits down intending to convert SQL to CSV. What happens is that someone in support emails you a fragment of a nightly dump, or a colleague pastes forty INSERT lines into a ticket, or you inherit a seed file from a repository whose database no longer exists. The rows are right there in the text and there is no server to load them into.
The awkward part is that the format is a program, not a data file. A single row can span three lines, a comma inside a customer name looks exactly like the comma that separates two columns, and an apostrophe in O'Hara is written two different ways depending on which database produced the file. Splitting on commas gets you nonsense, and it gets you nonsense quietly.
So this reads the script the way a database would: character by character into tokens, then tokens into statements. Comments are discarded, strings are read to their real closing quote, and only then are the values pulled out.
Worked example: two tables in one file
Here is a fragment shaped like the ones that actually arrive, with a lookup table, a data table, backtick identifiers and an apostrophe in a customer name:
CREATE TABLE regions (
region_code TEXT,
region_name TEXT,
opened_on DATE
);
INSERT INTO regions VALUES
('01', 'North Coast', '2019-03-04'),
('02', 'Lakeside', '2020-07-19');
INSERT INTO `orders` (`order_id`, `region_code`, `customer`, `revenue`, `note`) VALUES
(4101, '01', 'Ada Lovelace', 1840.50, NULL),
(4102, '02', 'O''Hara Supplies', 275.00, 'PO 8841');
Pick the orders table and this is the CSV:
order_id,region_code,customer,revenue,note
4101,01,Ada Lovelace,1840.50,
4102,02,"O'Hara Supplies",275.00,PO 8841
Three things happened there that are worth naming. The regions table had no column list on its INSERT, so the names came from the CREATE TABLE above it. O''Hara became one apostrophe and the field picked up CSV quoting because it now contains a comma-free but quote-bearing value the writer must protect. And NULL became an empty cell rather than the four letters N, U, L, L, which is the difference between a blank and a customer whose note literally says "NULL".
Every table, not the first one
A dump with eight tables in it is normal. Most free converters read until they find a VALUES clause and stop there, so you get whichever table the exporter happened to write first, usually a schema-migration log.
Here the whole file is scanned. Statements are grouped by target table, so three separate INSERT batches into orders become one table of rows rather than three fragments, and every table that has rows appears in a picker under the result with its row and column counts. Switching tables re-runs the conversion; nothing is thrown away.
If two INSERT statements into the same table name different column lists, which happens when a dump was produced across a schema change, the wider list wins and the shorter rows are padded. You get a note saying so rather than a silently narrow table.
The syntax it reads
- Multi-row VALUES.
VALUES (1,'a'),(2,'b'),(3,'c')is one statement and three rows. So is the same thing spread over sixty lines. - Both escape conventions.
'O''Hara'is the standard doubling and'O\'Hara'is MySQL's. Either produces one apostrophe. Backslash escapes for newline, tab and NUL are decoded too. - Every identifier quoting style. Backticks for MySQL, square brackets for SQL Server, double quotes for Postgres, and bare words.
`my db`.`orders`resolves to the tableorders. - NULL and DEFAULT both become an empty cell. A quoted
'NULL'stays the text NULL, because somebody meant that. - TRUE and FALSE become
trueandfalse, so a boolean column stays a boolean column on the way out. - Comments. Line comments with
--or#, and block comments, are skipped even when they contain a semicolon or a decoy INSERT. - Expressions. A value like
NOW()or1 + 2cannot be evaluated without a database, so the cell holds the expression text. A blank there would be a lie. - INSERT ... SELECT carries no literal rows, so it is skipped. If a file has nothing but those, you get a message that says exactly that.
Leading zeros, and why the whole column decides
The region_code column in the example holds '01' and '02'. Those are text in the database and they must be text on the way out, or the region codes become 1 and 2 and the join against your other file stops matching.
CSV has no types, so this only matters for the columns underneath: a leading zero is preserved because the value is written back exactly as it was read. Where the type does matter is when you keep going, to JSON or to Excel or to Parquet. There the decision is made once for the whole column, never per cell, and a column is only numeric if every value in it round trips exactly.
That rule has one consequence worth knowing about in advance: a price column holding 275.00 stays text, because writing 275 back is not the same string. Change the data to 275 and the column becomes numbers. It is a deliberately conservative rule, and it is why you never get a column that is half strings and half numbers.
What it will not do
- It does not run the SQL. There is no database here. A
SELECT, aCREATE INDEXor a stored procedure is read past, not executed. - It does not resolve foreign keys. If
orders.region_codepoints atregions.region_code, you get two tables, not a joined one. Take both to the app and join them there. - It does not decode a binary blob. An
X'4d5a'literal comes through as that text, because the alternative is a cell full of control characters. - It does not guess a header from your first row. If the INSERT has no column list and there is no CREATE TABLE, the columns are named by position and the result says so.
Frequently Asked Questions
Does it handle a dump with several tables in it?
Yes, and that is the main reason to use this one. The whole file is scanned, statements are grouped by the table they target, and every table with rows appears in a picker under the result with its row and column counts. Most converters take whichever table appears first and say nothing about the rest.
What happens to NULL?
NULL and DEFAULT both become an empty cell. A quoted 'NULL' stays the four-letter text, because someone deliberately stored that. When you carry on to JSON or YAML the empty cell becomes a real null rather than an empty string.
My INSERT statements have no column list. Do I lose the headers?
Not if the file also has a CREATE TABLE for that table, which most dumps do. The column names are taken from it. Failing that the columns are named column_1, column_2 and so on, and a note under the result tells you that is what happened.
Does it read MySQL backslash escapes as well as doubled quotes?
Both. 'O''Hara' and 'O\'Hara' each produce one apostrophe, and backslash sequences for newline, tab, carriage return and NUL are decoded. A file only ever uses one convention, and accepting both means you do not have to know which tool produced it.
What about a value like NOW() or CURRENT_TIMESTAMP?
The cell holds the expression as text. Evaluating it would need a database and a clock, and guessing a value would put a number in your file that was never in the database. The expression is visible, so you can see exactly which rows need attention.
Is my dump uploaded anywhere?
No. The parsing runs in your browser tab and nothing is sent, stored or logged. That matters more here than on most pages, because a production dump is usually the most sensitive file in the building. Files up to 100 MB work in the widget, and the full editor handles more.
Get the rows out of that dump
Paste the INSERT statements, pick your table, download the CSV. No account, no upload, no row cap.
Back to the converter