CSV to SQL Converter

Drop a CSV in, get a .sql file you can paste into a client and run. A CREATE TABLE with types read from your data, then INSERT statements in batches, spelled the way Postgres, MySQL, SQLite or SQL Server wants them. It all happens in this tab, so nothing uploads and no counter is ticking down.

Columns to drop or rename before the load? Open the app

When a CSV has to become a script

Every database has a bulk loader, and half the time you cannot use it. That is the whole reason this page exists.

  • The loader wants a file the server can see. Postgres COPY reads a path on the database host. MySQL LOAD DATA INFILE is off by default on managed instances. On RDS, Cloud SQL or a client's locked-down box you often have a SQL console and nothing else. INSERT statements travel down any connection that will take a query.
  • Seeding a dev or test database. Reference data belongs in the repository next to the migrations, and a .sql file is what migration tooling runs. Product keeps the list in a spreadsheet, you regenerate the script when it changes.
  • A one-off export somebody needs to query. The analytics team sends 40,000 rows. Nobody wants a new pipeline. They want the rows in a table for an afternoon.
  • Handing data to a person, not a machine. A DBA reviewing a change would rather read a script than open your attachment, and a script is reviewable in a pull request the way a spreadsheet never is.

Worked example: the same CSV in two dialects

This is the file behind the Sample button, saved as customers.csv. Four things in it are awkward on purpose: a comma inside a quoted name, an apostrophe, a ZIP code with a leading zero, and a boolean column.

id,name,zip,signed_up,active
1,Ada Lovelace,01730,2024-01-15,true
2,"O'Hara, Grace",94043,2024-02-02,false
3,Alan Turing,02139,2024-03-11,true

Postgres, which is what the dialect control starts on:

CREATE TABLE "customers" (
  "id" NUMERIC,
  "name" TEXT,
  "zip" TEXT,
  "signed_up" TEXT,
  "active" BOOLEAN
);

INSERT INTO "customers" ("id", "name", "zip", "signed_up", "active") VALUES
  (1, 'Ada Lovelace', '01730', '2024-01-15', TRUE),
  (2, 'O''Hara, Grace', '94043', '2024-02-02', FALSE),
  (3, 'Alan Turing', '02139', '2024-03-11', TRUE);

Now click MySQL. Same file, same button, four differences:

CREATE TABLE `customers` (
  `id` DECIMAL(20,0),
  `name` TEXT,
  `zip` TEXT,
  `signed_up` TEXT,
  `active` TINYINT(1)
);

INSERT INTO `customers` (`id`, `name`, `zip`, `signed_up`, `active`) VALUES
  (1, 'Ada Lovelace', '01730', '2024-01-15', 1),
  (2, 'O''Hara, Grace', '94043', '2024-02-02', 0),
  (3, 'Alan Turing', '02139', '2024-03-11', 1);

Backticks instead of double quotes. DECIMAL(20,0) instead of NUMERIC. TINYINT(1) instead of BOOLEAN, and the values written as 1 and 0 rather than TRUE and FALSE. None of that is decoration. A Postgres script pasted into MySQL fails on line 1, because MySQL reads "customers" as a string, not a table.

SQL Server is further away again, and here is its table definition for the same file:

CREATE TABLE [customers] (
  [id] NUMERIC(20,0),
  [name] VARCHAR(MAX),
  [zip] VARCHAR(MAX),
  [signed_up] VARCHAR(MAX),
  [active] BIT
);

What does not change is the part people get wrong by hand. O'Hara, Grace comes out as 'O''Hara, Grace' in every dialect, because doubling the quote is the only escape SQL has. And 01730 keeps its zero in all four, because the column was typed as text before a single row was written.

What the dialect switch actually changes

  • Identifier quoting. Double quotes for Postgres and SQLite, backticks for MySQL, square brackets for SQL Server. The closing character is doubled inside a name, so a column someone called order "x" cannot end the quoting early and turn the rest of the line into syntax.
  • Boolean literals. Postgres gets TRUE and FALSE. The other three get 1 and 0, which is what their boolean-ish types actually store.
  • The text type. TEXT everywhere except SQL Server, where it is VARCHAR(MAX). SQL Server's own TEXT type has been deprecated for years.
  • The exact number type. NUMERIC in Postgres and SQLite, NUMERIC(20,0) in SQL Server, and DECIMAL(20,0) in MySQL. That last one is deliberate: a bare NUMERIC in MySQL means DECIMAL(10,0), which quietly refuses an eleven-digit id.
  • The batch ceiling. SQL Server rejects an INSERT carrying more than 1,000 rows. Ask for more and the batch is reduced with a message saying so: SQL Server takes at most 1,000 rows in one INSERT, so the batch size was reduced to that. The other three go up to 5,000.

Three more controls sit next to the dialect. Script chooses between a full script and INSERTs only. Table name is blank by default, which means the filename is used with anything awkward turned into underscores, so Q1 sales (final).csv becomes Q1_sales_final, and pasted text, which has no filename, becomes data. Rows per INSERT takes 1 to 5,000 and starts at 500.

How the column types are chosen

Every column is read all the way to the last row before it gets a type. Sampling the first hundred rows is how a converter decides a column is an integer and then hits row 4,000, which holds a hyphen.

  • A column is numeric only when every filled cell in it survives a round trip: the parsed number has to print back as the same text. 42.5 qualifies. 42.50 does not, because it would print as 42.5, so that column lands in TEXT with the trailing zero intact.
  • A numeric column with no fractional part anywhere gets the exact type, NUMERIC or DECIMAL(20,0). One cent anywhere in it and the whole column becomes DOUBLE PRECISION, DOUBLE, REAL or FLOAT depending on the dialect.
  • Leading zeros keep a column in TEXT. ZIP codes, product codes, phone numbers and anything else that only looks like a number stay exactly as typed.
  • Booleans are strictly true and false, in any case. Yes, No, Y and 1 are left as text, because guessing at those is how a survey column ends up half converted.
  • An empty cell in a numeric or boolean column becomes NULL. In a text column it stays an empty string, because that is a value somebody may have meant.
  • A column that is empty in every row is TEXT, which is the only honest answer.

Gotchas worth knowing

  • The table name is one identifier, not two. Type public.customers and you get a table literally named public.customers, dot included, because the whole string is quoted. Pick the schema with a search_path or a USE statement instead, and leave the name plain.
  • Ragged rows are padded, not dropped. A row with more fields than the header widens the table, and the extra column arrives as column_3 or similar. The widget says how many rows it padded, which is usually a sign the export is broken upstream.
  • A cell containing a line break stays one value. The literal keeps its newline, so a single INSERT can span several lines of the file. Valid SQL, but do not write a script that assumes one row per line.
  • No keys, no indexes, no IF NOT EXISTS. The CREATE TABLE has column names and types and nothing else. If the table might already be there, run the INSERTs only version.
  • Column order follows the CSV. Reorder the columns in the file if you want them in a different order in the table.
  • The preview is capped at 200 lines. Copy and Download hand you the entire script. Big files stay fast because the browser is not asked to paint a hundred thousand lines of SQL.
  • Tab and semicolon files work too. The delimiter is sniffed from the text, so a selection pasted straight out of Excel or Google Sheets converts without being saved first.

Frequently Asked Questions

Which dialect do I pick for MariaDB, Redshift or DuckDB?

MariaDB reads the MySQL script, backticks and all. Redshift and DuckDB take the Postgres one for a plain load, though both have their own opinions about column types, so read the CREATE TABLE before you run it. If the target is fussy, choose INSERTs only and write the table definition yourself.

Can I get the INSERT statements without a CREATE TABLE?

Yes. Set Script to INSERTs only and the CREATE TABLE is left out. The column list in each INSERT is taken from the CSV header and quoted for the dialect, so those names have to match the columns already in the table.

Why is my ZIP code column TEXT instead of a number?

Because at least one value in it has a leading zero. 01730 loaded into a numeric column comes back as 1730, and that is a silent data loss nobody catches until a mailing goes out. A digit string only gets a numeric type when the number prints back as the identical text, so 01730 stays TEXT and 94043 does not.

Why did I get several INSERT statements instead of one?

Rows per INSERT defaults to 500, so 2,500 rows come out as five statements. Batching is what keeps a big load from blowing a packet limit or a statement size limit, and it means a failure names a batch rather than the whole file. You can set anything from 1 to 5,000.

Does the script include keys, indexes or constraints?

No. You get a CREATE TABLE with column names and types, and the INSERTs. Primary keys, foreign keys, NOT NULL, defaults and indexes are decisions about your schema, not facts in the CSV, so they are left to you. Adding them after the load is also faster than inserting through an index.

Is the CSV uploaded anywhere?

No. The file is parsed and the SQL is written by JavaScript in your tab, and this page has no upload endpoint. Files up to 100 MB are handled here, with no row cap and no daily quota, and a reload leaves you with an empty box.

Turn your CSV into a .sql file

Free, no account, no upload, no row cap. Pick the dialect, check the preview, copy the script or download it.

Back to the converter