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,false

Note 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 LINES skips the header row.
  • MySQL does not turn the text true/false into a boolean on load (in strict mode it raises an error; otherwise it silently stores 0). Reading the column into the user variable @active and converting it in SET solves 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 rights

and start the client with the flag:

mysql --local-infile=1 -u user -p mydb

If 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.
  • BOOLEAN does not exist in SQL Server; the column is a BIT. Convert true/false to 1/0 beforehand, or load into a staging table with VARCHAR columns and convert with INSERT … 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

  1. Batch the rows. One INSERT with 100–1,000 rows runs many times faster than the same number of single-row statements. SQL Server caps a single VALUES list at 1,000 rows.
  2. Wrap the script in a transaction so a failure halfway through doesn't leave half the data behind:
    BEGIN;
    -- INSERT statements…
    COMMIT;
  3. Oracle (before 23c) has no multi-row VALUES list. Use one INSERT per row or INSERT ALL.

Quick comparison

DatabaseCommandFile must be onSkip headerWatch out for
MySQL / MariaDBLOAD DATA LOCAL INFILEyour machineIGNORE 1 LINESlocal_infile disabled; booleans
PostgreSQL\copy … (FORMAT csv)your machineHEADER trueCOPY vs \copy
SQL ServerBULK INSERT … FORMAT = 'CSV'the serverFIRSTROW = 2needs 2017+; BIT columns
SQLite.import --csv --skip 1your machine--skip 1all TEXT if auto-created
Any databasegenerated INSERTsanywheren/aescaping, 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.