How to import a CSV file into MySQL, PostgreSQL, SQL Server and SQLite
By Hammad Rasool · October 13, 2026
You have a customers.csv from a spreadsheet, a CRM export or a client, and it needs to go into a database table. Every database has a fast built-in loader for this, but each has its own syntax and its own permission error waiting for you. Below are the four native loaders, the fixes for the errors they usually throw, and the portable fallback for when you can't use any of them.
The sample file and table
Every example below loads this file:
id,name,email,signup_date,active
1,"Khan, Sara",sara@example.com,2026-03-14,true
2,Ali Raza,ali@example.com,2026-04-02,falseNote the quoted "Khan, Sara". A comma inside a value is the most common thing that breaks a naive import, because a loader that is not told about quotes splits that row into six columns instead of five. The target table:
CREATE TABLE customers (
id INT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255),
signup_date DATE,
active BOOLEAN
);1. MySQL and MariaDB: LOAD DATA LOCAL INFILE
LOAD DATA LOCAL INFILE '/path/to/customers.csv'
INTO TABLE customers
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(id, name, email, signup_date, @active)
SET active = (@active = 'true');OPTIONALLY ENCLOSED BY '"'handles the quoted comma.IGNORE 1 LINESskips the header row.- MySQL does not turn the text
true/falseinto a boolean on load (in strict mode it raises an error; otherwise it silently stores 0). Reading the column into the user variable@activeand converting it inSETsolves that.
Error: “Loading local data is disabled”
LOCAL has to be allowed on both the server and the client. Enable it on the server:
SET GLOBAL local_infile = 1; -- on the server, needs admin rightsand start the client with the flag:
mysql --local-infile=1 -u user -p mydbIf the file was saved by Excel on Windows, the lines end in \r\n. Use LINES TERMINATED BY '\r\n', otherwise the last column of every row gets a stray carriage return.
2. PostgreSQL: \copy
\copy customers FROM 'customers.csv' WITH (FORMAT csv, HEADER true)Run this inside psql. The backslash version, \copy, reads the file from your computer and streams it to the server. PostgreSQL parses true, false and ISO dates like 2026-03-14 on its own, so no conversion is needed.
Error: “must be superuser or have privileges of the pg_read_server_files role”
You used COPY without the backslash. Plain COPY … FROM '/path' reads the file from the database server's disk, which needs special rights and is almost never what you want on a managed database such as RDS, Supabase or Neon. Switch to \copy.
3. SQL Server: BULK INSERT
BULK INSERT dbo.customers
FROM 'C:\data\customers.csv'
WITH (
FORMAT = 'CSV', -- SQL Server 2017+; understands quoted fields
FIRSTROW = 2, -- skip the header
FIELDQUOTE = '"',
ROWTERMINATOR = '0x0a'
);- The path is on the SQL Server machine, not on your laptop. With a remote server, copy the file there first or use the Import Flat File wizard in SSMS.
BOOLEANdoes not exist in SQL Server; the column is aBIT. Converttrue/falseto1/0beforehand, or load into a staging table withVARCHARcolumns and convert withINSERT … SELECT.FORMAT = 'CSV'arrived in SQL Server 2017. On older versions quoted commas split columns, so use the wizard or pre-generated INSERT statements instead.
4. SQLite: .import
sqlite3 app.db
sqlite> .import --csv --skip 1 customers.csv customers--skip 1 drops the header when the table already exists. If the table does not exist yet, leave --skip out: SQLite creates the table from the header row, but every column becomes TEXT.
When you can't use a bulk loader: generate INSERT statements
The native loaders are fastest, but sometimes they are not an option:
- a managed database gives you no file access and no command-line client,
- you need a migration or seed file that lives in the repository and goes through code review,
- the only access you have is a web UI such as phpMyAdmin, Adminer or a cloud console,
- or it is fifty rows and you just want something you can read and paste.
Plain INSERT statements run everywhere. For the sample file they look like this:
INSERT INTO customers (id, name, email, signup_date, active) VALUES
(1, 'Khan, Sara', 'sara@example.com', '2026-03-14', TRUE),
(2, 'Ali Raza', 'ali@example.com', '2026-04-02', FALSE);Writing these by hand is error-prone: every string needs quotes, an apostrophe in a name like O'Neil has to be doubled, empty cells should become NULL, and SQL Server wants 1/0 where MySQL accepts TRUE/FALSE. The CSV to SQL converter does all of that for MySQL, PostgreSQL, SQL Server, SQLite and Oracle. You can open the .csv file directly, and it keeps values with leading zeros (zip codes, phone numbers) as text. If your data is JSON rather than CSV, the JSON to SQL converter works the same way.
Three rules for generated INSERTs
- Batch the rows. One
INSERTwith 100–1,000 rows runs many times faster than the same number of single-row statements. SQL Server caps a singleVALUESlist at 1,000 rows. - Wrap the script in a transaction so a failure halfway through doesn't leave half the data behind:
BEGIN; -- INSERT statements… COMMIT; - Oracle (before 23c) has no multi-row
VALUESlist. Use oneINSERTper row orINSERT ALL.
Quick comparison
| Database | Command | File must be on | Skip header | Watch out for |
|---|---|---|---|---|
| MySQL / MariaDB | LOAD DATA LOCAL INFILE | your machine | IGNORE 1 LINES | local_infile disabled; booleans |
| PostgreSQL | \copy … (FORMAT csv) | your machine | HEADER true | COPY vs \copy |
| SQL Server | BULK INSERT … FORMAT = 'CSV' | the server | FIRSTROW = 2 | needs 2017+; BIT columns |
| SQLite | .import --csv --skip 1 | your machine | --skip 1 | all TEXT if auto-created |
| Any database | generated INSERTs | anywhere | n/a | escaping, batching |
Going the other way
Need the data back out as a spreadsheet, and all you have is a .sql dump? The SQL to CSV converter reads the INSERT statements directly, so you don't have to restore the database just to export one table.