⚙️ File Operations · 9 min read
CSV to SQL: INSERT Statements That Survive Quotes and Commas
Decide first whether you need a file of INSERT statements or the data inside the database, because the two jobs have different answers. For actual loading, use the engine's bulk loader: PostgreSQL COPY, MySQL LOAD DATA INFILE, SQL Server BULK INSERT, SQLite .import. Reach for generated INSERTs only when you have no file access to the server or the file is small. Either way, never build a statement by pasting raw CSV fields into it, because CSV quoting and SQL quoting are not the same thing. Everything that breaks follows from that one mismatch.
The frustration is well documented. On r/SQLServer the question was blunt: “How are people creating tables for new CSVs without raging in frustration?” A blog post on the same problem names the cause precisely: “the people who filled the CSV files put all kinds of strange characters in them, like quotation marks and carriage returns.” Quotes and line breaks are exactly the two characters that a CSV is allowed to hide inside a field and that a naive INSERT script cannot survive.
Two different jobs disguised as one
“CSV to SQL” covers two tasks that need different tools.
The first is producing a script: a .sql file full of CREATE TABLE and INSERT statements that a colleague can run, review, or keep in version control. That is a text-generation job, and its hard part is escaping.
The second is getting rows into a database that is already running. That is a data-loading job, and its hard part is throughput and partial failure, not text. The engine has a loader built for it and it will be faster than anything you generate.
Which route fits
| Route | Best for | Watch out for |
|---|---|---|
| Generated INSERT script | Handing a .sql file to someone, reviewing changes in git | Escaping, statement size, 1,000-row batch limits |
| Client import wizard (SSMS, Workbench, pgAdmin) | One-off loads where you can see the mapping before running | Choosing “keep nulls” vs empty strings, correct types |
PostgreSQL COPY | Large files, server has access to the path | Needs server-side file permissions or STDIN |
MySQL LOAD DATA INFILE | Large files on a server you control | Session flags vary between servers |
SQL Server BULK INSERT | Loading into an existing table at speed | Field and row terminators, code page of the file |
SQLite .import | Local files, quick analysis, no server at all | Table must exist first unless you use the CSV import mode |
Code with a driver (to_sql, parameter binding) | Repeating loads inside a pipeline | Chunk size, transaction handling |
Why naive scripts break
Take one row, from a real customer list:
id,name,note
1,O'Brien,"Said ""ship it"" on Friday, then left"The CSV rules come from RFC 4180. The field is wrapped in double quotes because it contains a comma, and the double quotes inside it are doubled. Correct CSV, and it parses back to exactly what the customer typed.
The SQL rules are different. ISO/IEC 9075, the SQL standard, marks a string literal with single quotes and expects a literal single quote inside to be doubled. So the value O'Brien has to be written as 'O''Brien'. Nothing in the CSV told you that, which is why the generated statement dies at the apostrophe.
Then there is the line break. A CSV field may legally contain a newline, and the note above could have been spread over three lines. A generator that splits the file on newlines will see three malformed rows where the parser sees one clean field. This is the failure mode that produces the near-useless error message about a syntax error near a comma, when the real problem started two lines earlier.
A third trap is MySQL specifically. In MySQL, a backslash inside a string literal is an escape character by default, so a Windows file path like C:\temp\new does something unexpected to the statement. Enabling NO_BACKSLASH_ESCAPES in the session removes that behaviour, but it changes how every literal in the same script is read, so set it deliberately rather than adding it while debugging something else.
Generating the script in code
Let the CSV parser and the database driver do the quoting. This is the shortest correct version, and it produces a file of INSERTs you can review:
import csv
def sql_literal(value):
if value == "":
return "NULL"
return "'" + value.replace("'", "''") + "'"
with open("customers.csv", newline="", encoding="utf-8") as f:
rows = list(csv.reader(f))
header = rows[0]
cols = ", ".join(f'"{c}"' for c in header)
batch = 500
with open("load.sql", "w", encoding="utf-8") as out:
for i in range(1, len(rows), batch):
chunk = rows[i:i + batch]
values = ",\n".join(
"(" + ", ".join(sql_literal(v) for v in r) + ")" for r in chunk
)
out.write(f"INSERT INTO customers ({cols}) VALUES\n{values};\n")Three details matter more than the rest. csv.reader is given the file object, so quoted fields containing commas and newlines are handled by the parser rather than by string splitting. The column list is written out explicitly, so the statement does not depend on column order. And the output is chunked at 500 rows, which keeps each statement inside the limits described below.
The replace call is the only escaping rule the script needs for standard SQL. If you are targeting MySQL with default settings, backslashes need attention too, and the safest answer is to use a parameterised insert through the driver instead of writing literals by hand. That also removes any chance of SQL injection from a file you did not produce.
The limits that force chunking
| Engine | Documented limit | What it means for your script |
|---|---|---|
| SQL Server | 1,000 rows maximum in a table value constructor | A 50,000-row file needs at least 50 statements |
| SQL Server | 8,060 bytes per row | Long text columns in a wide CSV can overflow a row, so check the widest fields |
| SQLite | SQLITE_MAX_VARIABLE_NUMBER caps parameters per statement | Batch sizes depend on the build, so stay conservative |
| PostgreSQL | Statement size bounded by available memory | Prefer COPY over giant INSERTs |
None of these limits is visible until a script fails halfway through, which is worse than failing at the start. Chunk from the beginning and wrap the chunks in a transaction so a failure leaves the table empty rather than half filled.
Loading it instead of inserting it
For anything above a few thousand rows, skip generation entirely.
PostgreSQL: COPY customers FROM '/path/customers.csv' WITH (FORMAT csv, HEADER true); — the loader reads the CSV format directly, header and all, and runs in one pass.
MySQL: LOAD DATA INFILE with FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' and a line terminator. Set the escapes to match how the file was written rather than assuming.
SQL Server: BULK INSERT into a table that already exists, with the field terminator and row terminator declared. For JSON that arrived in a column instead of a file, OPENJSON reads the structure without a second tool.
SQLite: the sqlite3 CLI takes .mode csv followed by .import file.csv table_name. Files that are too big for Excel are unremarkable here, because SQLite is not holding a window. It is also public domain, which makes it the least complicated way to answer a question about a large file without asking anyone for permission. There is a full walkthrough of importing CSV into SQLite.
If your question is about reading a file with SQL rather than loading it into a database, that is a different and shorter path, covered in querying a CSV with SQL directly.
NULL, empty string and type inference
A CSV cannot distinguish an empty field from a missing one. Both are nothing between two commas, and everything downstream has to guess. The guess is often wrong.
| In the CSV | Common default | What to check |
|---|---|---|
1,,3 | pandas reads it as missing; a loader may write '' | Set the flag explicitly rather than accepting the default |
| Zeroes at the start of an ID | Read as a number, digits dropped | Declare the column as text. This is the same problem as leading zeros in CSV. |
| Dates in a local format | Guessed, or read as text | Parse with an explicit format and store as a real date type |
| Numbers with thousands separators | Treated as text, or silently truncated | Strip separators during load, not afterwards |
| Non-ASCII names in a Windows-exported file | Mojibake, or a load that stops at row one | Confirm the encoding before loading. Encoding problems in CSV are worth reading before you retry the load a third time. |
Type inference only sees the rows it sampled. A column that holds three-digit numbers for the first thousand rows and the text N/A at row 1,200 will be created as an integer and then fail on row 1,200, or worse, load as text in a table that expected numbers. When you generate the schema, scan the whole file for the widest value in each column, or declare the column as text and cast in SQL. It is a dull answer that prevents a dull afternoon.
Before you run the script
Three checks catch most failures before they reach a database. Confirm the row count you expect against the row count the CSV file actually contains, because a file with embedded line breaks will report more rows to a naive counter than it has records. Confirm the delimiter, since a semicolon-separated export loaded with a comma setting produces one very wide column, and changing a CSV delimiter first is simpler than fixing it in the schema. Then check the header against your table columns by name rather than by position.
If the script is going into version control, that also settles a practical question: split large files with something like splitting a large CSV into per-table files, so a reviewer can see what changed instead of scrolling past ten thousand identical-looking rows.
Frequently asked questions
How do I convert a CSV file to a SQL INSERT statement?
Read the CSV with a real parser, then build the statement one row at a time and escape every value. Never build the string by joining raw fields with commas. In Python, csv.reader or pandas reads the file, and the database driver's parameter binding handles the quoting for you. If you need a text file of INSERTs, quote each value as a SQL string literal and double any single quote inside it, because that is the escape defined by the SQL standard ISO/IEC 9075.
Why does my generated INSERT fail with a syntax error near a quote?
Because CSV quoting and SQL quoting are different systems. CSV wraps a field in double quotes so it survives a comma; SQL marks a string with single quotes and expects a literal single quote inside it to be doubled. A value like O'Brien is perfectly valid CSV and invalid SQL if you paste it in unchanged. Commas are not the problem; apostrophes, line breaks inside quoted fields, and stray backslashes are.
What is the fastest way to load a large CSV into a database?
Use the database's own bulk loader rather than INSERT statements. PostgreSQL has COPY, MySQL has LOAD DATA INFILE, SQL Server has BULK INSERT, SQLite has the .import command in its CLI. These read the file directly and skip the statement parser, which is the difference between seconds and minutes on a file with millions of rows. INSERT-per-row is for small files, for fixing a handful of records, or for when you have no file access to the server.
How many rows can I put in one INSERT statement?
It depends on the engine and there are real limits to respect. SQL Server caps a table value constructor at 1,000 rows, so a 100,000-row file needs at least 100 statements. SQLite caps the number of parameters in a single statement, and that value is compiled in, so batch sizes should be conservative. SQL Server also limits a row to 8,060 bytes, which long text columns in a CSV can exceed. Chunk every generated script into batches rather than trusting one giant statement.
How do I import NULL values from a CSV?
Carefully, because a CSV cannot tell an empty field from a missing one. Both look like nothing between two commas. Loaders make a choice for you: pandas treats a list of strings as missing by default, while a database LOAD DATA statement may write an empty string instead of NULL. Decide which one your schema expects and set the option explicitly. If the difference matters, keep a sentinel value such as the literal text NULL in the file and convert it during load.
Can I create the table automatically from the CSV?
Yes, and it is a reasonable first pass. Tools that infer a schema from a sample save typing: pandas to_sql creates a table from a DataFrame, DuckDB's read_csv_auto detects column types on its own, and most database import wizards propose a schema you can edit before running it. Treat the result as a draft. Type inference looks only at the rows it sampled, so a column that is numeric in the first 1,000 rows and contains N/A later needs a text type from the start.
Does the column order in the CSV have to match the table?
Not if you name the columns. An INSERT statement with an explicit column list is bound by name, so the CSV can be reordered without breaking anything. The position-only form relies on the order matching the table exactly, which is a silent failure mode rather than a loud one: values land in the wrong columns and nothing errors. Name the columns in generated INSERTs, and map CSV headers to table columns in your loader config.
Is a CSV file or a SQL dump better for backup?
They answer different questions. A SQL dump preserves types, constraints, indexes and relationships, so it restores the database as it was. A CSV preserves only the values, which makes it portable, readable in any spreadsheet, and unable to carry a schema with it. For backups, use the dump. For moving data between systems, or handing a table to someone who does not have your database, use the CSV.
Tools mentioned in this guide
The loaders themselves are free. These three matter when the CSV arrives on a schedule and the database has to be right without anyone watching:
- OpenCode Go — the escape function above is ten lines until a client sends a file with line breaks inside quoted fields, and then it is an afternoon; a $10-a-month subscription covering 19+ models is the cheaper way to get that script written and reviewed. Try OpenCode Go
- Stack AI— when the same CSV lands in a shared drive every morning, a workflow can validate it, load it and flag the bad rows, which beats discovering on Friday that Tuesday's file loaded half a table. Try Stack AI
- Softr — if the reason for loading the CSV is that colleagues want to look things up in it, publishing the data as a searchable page answers their question and leaves the database alone. Try Softr
Some links above are affiliate links — if you buy through them we may earn a commission at no extra cost to you. Links currently point to each vendor's official page until our dedicated tracking links are registered. OpenCode Go uses our referral link.
Check the File Before You Load It
Row counts, duplicates, blanks and the columns with mixed types. Find them now instead of at row 1,200.
Related reading
File Operations — other guides that pair well with this one.
- Cheap USB Drives & Portable SSDs Under $50 for CSV
- Wireless Keyboard & Mouse for Excel Under $30
- Open a CSV Too Big for Excel
- Excel Slow With Large Data
Browse all guides in the NoCodeCSV blog.