Convert CSV to SQL

Turn a CSV or Excel file into CREATE TABLE and INSERT statements for MySQL, PostgreSQL or SQLite. Runs in your browser tab, so the file is never uploaded.

🌐 Español

Drop your file here (.csv, .xlsx, .xls)

🔒 Private by design: your files are processed locally in your browser and never uploaded to any server.

Running the conversion, step by step

  1. Drop a .csv, .xlsx or .xls file onto the box above, or click Choose a file. It handles one file per run, and any other extension is turned away before anything is read.
  2. Set Output to “CREATE TABLE + INSERT statements” when the table does not exist yet, or to “INSERT statements only (table already exists)” when you are topping up a table you have already created.
  3. Choose the SQL dialect. MySQL is the default, and this single choice decides identifier quoting, the column type names and one escaping rule.
  4. Leave Rows per INSERT statement at 500 unless your server rejects large statements, in which case lower it. Values below 1 are pulled up to 1 and anything above 5000 is pulled down to 5000.
  5. Click Convert CSV to SQL, then download the file. It reuses your original file name with the extension swapped for .sql.

A worked example, in all three dialects

Say the file is called Q3 Products (final).csv and looks like this:

sku,Product Name,price,zip,notes
A-19,Widget,3.14,00501,"O'Brien's pick"
B-02,Gadget,12,90210,
C-77,Doo-dad,7.5,10001,C:\temp\new

With the default options and the MySQL dialect, the output begins with a CREATE TABLE whose identifiers are wrapped in backticks, then a single INSERT holding all three rows:

CREATE TABLE `q3_products_final` (
  `sku` VARCHAR(255),
  `Product Name` VARCHAR(255),
  `price` DECIMAL(18,4),
  `zip` VARCHAR(255),
  `notes` VARCHAR(255)
);

INSERT INTO `q3_products_final` (`sku`, `Product Name`, `price`, `zip`, `notes`) VALUES
('A-19', 'Widget', 3.14, '00501', 'O''Brien''s pick'),
('B-02', 'Gadget', 12, 90210, NULL),
('C-77', 'Doo-dad', 7.5, 10001, 'C:\\temp\\new');

Switch the dialect to PostgreSQL and the backticks become double quotes, DECIMAL(18,4) becomes NUMERIC(18,4), and the Windows path in the last row keeps its single backslashes. Switch to SQLite and the text columns become TEXT, the decimal column becomes REAL, and the backslashes again stay as they are. Nothing else about the data changes between the three.

That small sample already contains most of the decisions the converter has to make, so it is worth walking through them.

Per-cell typing, and the leading-zero problem

Two separate judgements happen on every run. The declared column type is decided once per column, from every non-blank cell in it: all clean integers gives you an integer column, a mix of integers and decimals gives you the decimal type, and anything else gives you text. Whether an individual cell is written with quotes around it is decided again, on its own, for that one cell.

The rule for “looks like a number” is deliberately narrow. A value qualifies only if the entire trimmed cell is an optional minus sign, then either a lone zero or a digit from one to nine with any number of digits after it, optionally followed by a dot and at least one more digit. 1e5 does not qualify. 1,234 does not qualify. Neither does 00501, and that exclusion is the whole point: a postal code written as the bare number 501 has quietly lost the information that made it a postal code. Blank and whitespace-only cells become the unquoted keyword NULL rather than an empty string pretending to be one.

The trade-off in the sample above is visible. The zip column is declared VARCHAR(255) because of 00501, yet 90210 and 10001 are still emitted as bare numbers, since each cell is judged on its own. Read the generated CREATE TABLE before you run it. Type inference from a sample of text can never be more than a good guess, and this one is honest about being a guess.

Quoting rules, and the one place MySQL differs

Every value written as a string is single-quoted, and every single quote inside it is doubled. That is the SQL standard rule and it holds in all three dialects, which is why O'Brien's pick comes out as 'O''Brien''s pick' and cannot break out of its literal.

Backslashes are the exception. MySQL, in its usual configuration, reads a backslash inside a string literal as the start of an escape sequence, so the two characters that make up \n in a Windows path would be stored as a newline. For the MySQL dialect only, every backslash is therefore doubled first. PostgreSQL and SQLite treat a backslash as an ordinary character, and doubling it for them would insert a second backslash into your data, so nothing is done there. Table and column names are quoted too, which is why a header like Product Name survives the trip intact and a column named after a reserved word still works.

Table names, sheet names and file names

There is no free-text box for the table name, so it is derived. For a CSV, and for a workbook with a single sheet, the name comes from the file name with the extension removed, punctuation and spaces collapsed into underscores, and the whole thing lowercased. That is how Q3 Products (final).csv became q3_products_final. A file called 2024 sales.csv becomes _2024_sales, because a bare leading digit is not a valid identifier.

A workbook with more than one sheet is treated differently: each sheet becomes its own table named after that sheet tab, so a three-tab workbook produces three CREATE TABLE statements and three sets of INSERTs in the same download. Only the table names get this cleanup. Column names are taken from your header row exactly as written and simply quoted, so spacing and capitalisation are preserved.

Limits worth knowing before you run the SQL

The output is a text file, not a migration. There are no primary keys, no indexes, no foreign keys, no NOT NULL constraints and no length tuning beyond the fixed VARCHAR(255) that MySQL and PostgreSQL text columns get, so treat the CREATE TABLE as a first draft you edit rather than a schema you ship. Nothing runs against a database here either; you get a file to review and execute yourself.

Because the whole file is parsed and turned into text in the tab’s own memory, the ceiling is your device rather than an upload quota, and a genuinely huge export will make the tab work for it. If you only need to look at the data first, open it in the CSV Viewer and sort or search it there. If your source is an Excel workbook and JSON is the real destination, Excel to JSON skips the SQL step entirely and accepts several workbooks at once. To paste CSV text straight from the clipboard instead of picking a file, CSV to JSON takes pasted input. And once you have the statements, the SQL Formatter will re-indent them, which is handy after you have edited the CREATE TABLE by hand. More conversion utilities live on the developer tools hub.

See it in action

Screenshot of the Convert CSV to SQL tool with sysfenix-sample.csv (140 B) loaded, Output set to CREATE TABLE + INSERT statements, SQL dialect set to MySQL
Convert CSV to SQL mid-process: sysfenix-sample.csv (140 B) loaded, Output set to CREATE TABLE + INSERT statements, SQL dialect set to MySQL.
Screenshot of the Convert CSV to SQL result screen showing sysfenix-sample.sql ready to download (380 B, 171% larger)
The finished result: sysfenix-sample.sql ready to download (380 B, 171% larger). The download link is a local blob URL — the file never leaves your device.

Frequently asked questions

My database is MariaDB (or Redshift). Which dialect should I choose?

Choose MySQL for MariaDB, because MariaDB inherited MySQL's backtick identifier quoting and its backslash-escaping behaviour inside string literals. Choose PostgreSQL for Redshift and for most other Postgres-derived warehouses, since those quote identifiers with double quotes and treat a backslash as an ordinary character. The identifier quoting and the string escaping will then be right; the column types are still a starting point you should review, because warehouse type systems are not identical to plain Postgres.

A column of phone numbers came out as text instead of a number. Is that wrong?

It is deliberate. A column is only declared as a whole-number type when every non-blank cell in it is a plain integer with no leading zero, no plus sign, no spaces and no separators, so a real phone number list almost always lands on the text type. That is the safer outcome, because storing a phone number as an integer throws away the leading zero and the country prefix that make it dialable.

The generated file put a bare number inside a column that was declared as text. Is that a problem?

The column type and the value formatting are two separate decisions. The declared type is chosen once for the whole column, while quoting is decided cell by cell, so a mixed column such as postal codes can end up declared as text while the cells that happen to look like clean integers are still written unquoted. MySQL and SQLite accept a numeric literal in a text column without complaint. PostgreSQL is the strictest of the three about that mismatch, so read that statement before you run it there.

Can I set a different table name without renaming the file first?

Not from the options panel, because the table name is derived from the file name rather than typed in. Every run of characters that is not a letter, digit or underscore becomes a single underscore, the result is lowercased, and a name that would start with a digit gets an underscore in front of it. Renaming the file before you pick it is the quickest route; a find and replace on the downloaded file works just as well.

What happens to an Excel workbook that has a blank sheet in the middle of it?

A sheet with no rows at all is skipped, and every other sheet becomes its own table named after that sheet tab. If the whole workbook turns out to be empty, the conversion stops and the page shows its general failure message rather than handing you an empty file.

Does the first row of my file have to be a header row?

Yes, in the sense that the first row is always read as the column names, whatever it contains. If your export has no header line, the first record silently becomes your column names and disappears from the data. Add a header row before converting. A header cell that is blank is filled in as Column 2, Column 3 and so on, counting from the left, so a stray empty heading does not break the statement.

Related tools