When SQL beats an import wizard
Database import tools are powerful but fiddly: COPY needs server file access, LOAD DATA LOCAL INFILE is often disabled, and GUI wizards guess types badly. Plain SQL statements work everywhere — a migration file, a psql or sqlcmd session, a seed script in your repository, or the query window of a hosted database console. Converting a CSV into INSERTs is the quickest way to get a spreadsheet, a report export or a small reference table (countries, price lists, feature flags) into a database you control.
How the CSV is read
The first row must be a header; its cells become column names. Empty header cells are named column_1, column_2 and so on, and duplicate names get a suffix. Rows longer than the header add extra column_N columns, and missing trailing fields become NULL. Parsing follows RFC 4180, so quoted commas, doubled quotes and multi-line cells work as in Excel.
Each cell is then typed with the same cautious rules as CSV to JSON:
- An empty cell becomes
NULL, not an empty string. - Exactly
trueandfalsebecome booleans. - Plain decimals such as
42or1520.50become numbers, keeping their written digits. - Anything else stays text — including
00123,+65 9123 4567,1e5andTRUE— so identifiers never lose leading zeros.
Column types per database
The table definition is chosen by looking at every value in each column. A column of small whole numbers gets INTEGER (INT in MySQL and SQL Server); bigger ones get BIGINT. A column mixing 88.00 and 1520.50 becomes NUMERIC in PostgreSQL or DECIMAL(6,2) in MySQL, SQL Server and standard SQL, sized from the widest value. Values like 2024-03-11 produce a DATE column, and full timestamps a timestamp type; SQLite, which has neither, keeps them as TEXT.
Text columns are TEXT in PostgreSQL and SQLite, NVARCHAR(MAX) in SQL Server, and VARCHAR(n) in MySQL and standard SQL, where n is the longest value in characters. A column where one row says n/a and the rest are numbers is typed as text, because a numeric column would reject that row.
Options
Table name sets the target (default records); staging.import_2026 is quoted as schema and table. Dialect selects PostgreSQL, MySQL, SQLite, T-SQL (SQL Server) or Standard SQL. Include CREATE TABLE can be switched off to append rows to an existing table. One multi-row INSERT groups up to 1,000 rows per statement, which loads far faster than single-row inserts.
All identifiers are quoted in the dialect’s style, so header names with spaces or reserved words such as from and select are valid. Escaping follows each database’s rules for apostrophes, backslashes (MySQL) and Unicode (SQL Server’s N prefix). Review the output in the SQL formatter; nothing you paste is sent anywhere.
Examples
Customer export for PostgreSQL
joined becomes a DATE column, balance a NUMERIC column, and the quoted name keeps its comma and inner quotes.
id,name,city,joined,balance
1,"Tan, Aisha",Singapore,2024-03-11,1520.50
2,Ben Okafor,Lagos,2023-11-02,88.00
3,"Chloé ""CJ"" Martin",Paris,2025-01-20,-12.75
CREATE TABLE "customers" (
"id" INTEGER,
"name" TEXT,
"city" TEXT,
"joined" DATE,
"balance" NUMERIC
);
INSERT INTO "customers" ("id", "name", "city", "joined", "balance") VALUES (1, 'Tan, Aisha', 'Singapore', '2024-03-11', 1520.50);
INSERT INTO "customers" ("id", "name", "city", "joined", "balance") VALUES (2, 'Ben Okafor', 'Lagos', '2023-11-02', 88.00);
INSERT INTO "customers" ("id", "name", "city", "joined", "balance") VALUES (3, 'Chloé "CJ" Martin', 'Paris', '2025-01-20', -12.75);
ZIP codes and gaps into MySQL
Leading-zero ZIP codes stay text in a VARCHAR column, empty cells are inserted as NULL, and all rows share one INSERT.
store,zip,opened,manager
Downtown,00123,2019-05-01,Ana
Harbour,04567,,
Airport,10001,2023-02-14,Raj
CREATE TABLE `stores` (
`store` VARCHAR(8),
`zip` VARCHAR(5),
`opened` DATE,
`manager` VARCHAR(3)
);
INSERT INTO `stores` (`store`, `zip`, `opened`, `manager`) VALUES
('Downtown', '00123', '2019-05-01', 'Ana'),
('Harbour', '04567', NULL, NULL),
('Airport', '10001', '2023-02-14', 'Raj');
Reserved words as headers in SQLite
Only INSERT statements are produced, and the reserved column names are double-quoted so SQLite accepts them.
select,from,order
alpha,web,1
beta,mobile,2
INSERT INTO "records" ("select", "from", "order") VALUES ('alpha', 'web', 1);
INSERT INTO "records" ("select", "from", "order") VALUES ('beta', 'mobile', 2);
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
This quoted field is never closedExplained | A quote opens a field and never closes, which would swallow the rest of the file. | Close the quote; write quotes inside values as two quotes. |
There are no data rows, so no INSERT statements were written | A warning: the CSV contains only a header row. | Add data rows, or keep just the CREATE TABLE if that is all you need. |
Row has 3 fields, expected 5; the missing fields are left emptyExplained | A warning: some rows are shorter than the header, so their missing values become NULL. | Check that row for a missing comma or an accidental line break. |
Duplicate header "total" renamed to "total_2" | Two columns share a name, which a table definition cannot allow. | Rename one of the headers in the CSV to something meaningful. |
Frequently asked questions
Does the CSV need a header row?
Yes. The first row supplies the column names; if your file has none, add a header line before converting.
Why is my ZIP code column TEXT and not INTEGER?
Values with leading zeros are kept as text so 00123 is not stored as 123. Numeric columns are only chosen when every value is a plain number.
How are empty cells inserted?
As NULL. If you need empty strings instead, replace them after loading or edit the generated statements.
Can I load large files this way?
Thousands of rows are fine, especially with One multi-row INSERT. For millions of rows, a bulk loader such as COPY or LOAD DATA will be much faster.