All posts

From Spreadsheet Rows to SQL INSERT Statements

July 27, 2026 · DevTools

csv
sql
database
data-migration
developer-tools

Someone drops a spreadsheet in Slack: "can we just import this into the database?" The spreadsheet has 4,200 rows. Three columns have empty cells. One column has names with apostrophes. Another has dates that look like numbers. You have no ETL pipeline, and you need it done by end of day.

You could write a Python script. You could open a SQL client and try to import it manually. Or you can convert the CSV to a SQL INSERT file and load it directly — but the naive approach of f"INSERT INTO t VALUES ('{row}')" will break on your first O'Brien, your first NULL, and your first date format your database does not recognize.

The CSV to SQL Converter handles all of this automatically. Here is what it actually takes to get right.

The naive approach and why it fails

A minimal converter loops over rows and emits strings:

INSERT INTO users (name, email, active) VALUES ('Alice', 'alice@example.com', 1);
INSERT INTO users (name, email, active) VALUES ('Bob', 'bob@example.com', 0);

This works for simple data. It breaks in three predictable ways.

Strings with single quotes. O'Brien becomes O'Brien — the quote terminates the string, the SQL parser chokes, and you get a syntax error. The fix is to escape each ' as '' (standard SQL) or use your database's escape function.

Empty cells as strings. A blank email field produces '' — an empty string, not a NULL. For columns that are genuinely optional, you want the SQL keyword NULL, not a quoted empty string. These are different values: NULL means "no value supplied", '' means "an empty string was supplied".

Numbers as strings. If a numeric column receives '1' (a string), some databases will cast it silently, some will warn, and some will error. The converter should detect numeric columns and emit unquoted values.

Typed values: what a correct converter does

-- Typed values: numbers unquoted, NULL for empty cells
INSERT INTO users (id, name, email, age, active) VALUES
  (1,   'Alice',   'alice@example.com',   28,   true),
  (2,   'Bob',     'bob@example.com',      NULL,  false),
  (3,   'O''Brien','obrien@example.com',  35,   true);

The difference: age gets 28 (number, no quotes), active gets true (boolean, no quotes), Bob's missing age gets NULL, and O'Brien's name has the apostrophe escaped as ''.

Multi-row INSERT performance

The difference between one-row and multi-row INSERT statements is significant at scale. Each INSERT statement has a fixed parsing and execution overhead. Batching rows cuts that overhead dramatically:

-- One row per statement — 4,200 round-trips
INSERT INTO users (id, name) VALUES (1, 'Alice');
INSERT INTO users (id, name) VALUES (2, 'Bob');
-- ...

-- Multi-row — one round-trip for all 4,200 rows
INSERT INTO users (id, name) VALUES
  (1, 'Alice'),
  (2, 'Bob'),
  -- ...
  (4200, 'Zara');

A 100-row batch is a good default: fast enough to be worth batching, small enough that a failure corrupts a manageable number of rows.

Identifier quoting: why some column names need backticks

Column names with spaces, reserved words, or database-specific keywords need quoting:

DatabaseQuote characterExample
MySQLBacktick ``from`
PostgreSQLDouble-quote ""from"
ANSI/SQL StandardDouble-quote ""from"
SQL ServerSquare brackets [ ][from]

The CSV to SQL Converter lets you pick an identifier-quoting style — ANSI (double quotes), MySQL (backticks), or none — and applies it automatically.

-- MySQL: backtick quoting
INSERT INTO `order-items` (`product name`, `quantity`, `unit-price`) VALUES
  ('Widget', 10, 9.99);

-- PostgreSQL: double-quote quoting
INSERT INTO "order-items" ("product name", "quantity", "unit-price") VALUES
  ('Widget', 10, 9.99);

NULL vs empty string: the semantics matter

In most databases, these are not equivalent:

SELECT * FROM users WHERE email = NULL;        -- never matches (NULL ≠ NULL)
SELECT * FROM users WHERE email IS NULL;        -- matches NULL
SELECT * FROM users WHERE email = '';           -- matches empty string
SELECT * FROM users WHERE email IS NOT NULL;   -- matches non-NULL values, including ''

If your spreadsheet has empty cells for optional columns, they should become NULL in SQL. If empty cells mean "the user entered an explicit empty value," they should become ''. The CSV to SQL Converter treats empty cells as NULL.

Importing the generated file

Once you have the .sql file:

# PostgreSQL
psql -d mydb -f data.sql

# MySQL
mysql -u root -p mydb < data.sql

# SQLite
sqlite3 mydb.db < data.sql

For very large files, most databases support a COPY command that is significantly faster than running many INSERT statements — but COPY requires the CSV format, not INSERT syntax. For bulk loading, load the source CSV directly with your database's COPY (or LOAD DATA) command instead.

Validation before import

Before loading, validate:

  1. Row count: wc -l data.sql matches your CSV row count (plus a few for headers)
  2. No unbalanced quotes: a stray quote on a column name can close the string prematurely
  3. Numeric columns: check that number columns contain only digits, minus signs, and decimal points — a $ sign or comma in a "number" column will cause a type error
  4. Encoding: ensure UTF-8 throughout. Non-ASCII characters in names or addresses break silently if your terminal or database uses Latin-1

The CSV to SQL Converter does this conversion without writing any code — paste your CSV, choose your identifier-quoting style, and copy the generated SQL.