tutorials/postgresql/import-data

Import CSV into PostgreSQL with COPY, \copy or a GUI

There are two ways to get a CSV file into PostgreSQL. Import it visually in 1bench by picking the file and the target table, or run COPY or psql's \copy, which add delimiters, NULL handling, bad-row skipping and upserts.

Updated Nov 2, 2026Tested against PostgreSQL 16.13 and 18.6 (ON_ERROR also checked on 17.10)

Quick answer

In 1bench
Right-click the table in the sidebar, choose Import Data, browse to the CSV file and click Import.
In SQL
sql
\copy products (name, sku, price, category, stock_quantity) FROM 'products.csv' WITH (FORMAT csv, HEADER)

Run it in psql. \copy reads the file from your computer; plain COPY reads it from the database server and needs extra privileges.

1bench
No SQL needed

Import a CSV visually in 1bench

1bench imports CSV, JSON, NDJSON and SQL files into PostgreSQL from the sidebar. It reads the CSV header, matches each column to the table by name and loads the whole file in one transaction.

Prerequisite: download 1bench. Free 7-day trial, no sign-up.

  1. 1

    Create the table first

    Import Data fills a table that already exists; it does not create one from the file. If you need a new table, right-click the schema, choose New Table, or run CREATE TABLE in the Query panel.

  2. 2

    Choose Import Data

    Right-click the table in the sidebar and choose Import Data. If the table is already open, the Import button at the bottom of the window does the same. The item is not shown for views and is greyed out on a read-only connection.

  3. 3

    Pick the file

    Click Browse and choose the file. The Format list (SQL, NDJSON, CSV, JSON) switches to match the extension, so a .csv file selects CSV (.csv). Target Table shows the table you clicked.

  4. 4

    Clear the table (optional)

    Tick Clear target table before importing to replace the current rows. 1bench runs TRUNCATE TABLE inside the same transaction as the inserts, so a failed import leaves the old rows in place.

  5. 5

    Import

    Click Import. The dialog counts the rows as they go in (Inserted 1500 rows) and Stop import cancels and rolls back. When it finishes, a toast reports Imported 4 rows and the open grid refreshes.

Good to know
  • The CSV must start with a header row. Columns are matched by name in any order, columns left out of the file get their DEFAULT, and a header name the table does not have stops the import with PostgreSQL's error, such as column "internal_note" of relation "products" does not exist.
  • 1bench reads comma-separated, double-quoted, UTF-8 files (a leading BOM from Excel is fine). For tab or semicolon files, other quote characters or another encoding, use COPY with DELIMITER, QUOTE or ENCODING from the Query panel.
  • An unquoted empty field becomes NULL. A quoted empty field ("") stays an empty string in text columns and becomes NULL in number and date columns. Rows with the wrong number of fields are skipped, and the toast says how many.
  • One bad value, a duplicate key or a failed check rolls back the whole file. Generated columns in the file are skipped with a warning, and GENERATED ALWAYS identity values are kept as written.
  • SQL files run as written, in one transaction. Plain pg_dump output, including its COPY ... FROM stdin blocks, loads fine. Other psql commands such as \connect are refused, and a custom-format .dump file needs pg_restore. Disable foreign key constraints during import sets session_replication_role to replica for the run.
The manual way

Import with COPY and psql

loads a file into a table that already exists. The examples use a table with , , (unique), , and , and a CSV whose header is . List the columns after the table name so the id is generated for you.

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products.csv'
WITH (FORMAT csv, HEADER);

Every statement below was run on PostgreSQL 16 and 18. Where an option needs a newer release, the section says which one.

COPY vs \copy

runs on the database server and opens the file there. is a psql command that reads the file on your own computer and streams it to the server, so it is the one to use for a remote database, a Docker container or a managed service such as RDS.

text
\copy products (name, sku, price, category, stock_quantity) FROM 'products.csv' WITH (FORMAT csv, HEADER)

Point COPY at a file that only exists on your laptop and the server cannot find it. The hint tells you to switch:

text
ERROR:  could not open file "/tmp/client_only.csv" for reading: No such file or directory
HINT:  COPY FROM instructs the PostgreSQL server process to read a file. You may want a client-side facility such as psql's \copy.

Relative paths in COPY are resolved from the server's data directory, not from your shell, so fails the same way. With , a relative path is relative to the folder you started psql in.

There are two different permission errors. The first means your role may not read server files at all:

text
ERROR:  permission denied to COPY from a file
DETAIL:  Only roles with privileges of the "pg_read_server_files" role may COPY from a file.
HINT:  Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.

Use , or have a superuser run . The second comes from the operating system: the role is allowed, but the user cannot read the file or its folder (a home directory, , or a file with mode 600).

text
ERROR:  could not open file "/root/tutdata/products.csv" for reading: Permission denied

Move the file somewhere the server can read, such as , fix its permissions, or use , which reads the file as you.

Example: import from the shell in one line, or feed the file to COPY through standard input.

bash
psql -d shop -c "\copy products (name, sku, price, category, stock_quantity) FROM 'products.csv' WITH (FORMAT csv, HEADER)"

psql -d shop -c "COPY products (name, sku, price, category, stock_quantity) FROM STDIN WITH (FORMAT csv, HEADER)" < products.csv

With and without a header

skips the first line. COPY does not read the names in it; it fills the listed columns from left to right. Forget and the column names are parsed as data:

text
ERROR:  invalid input syntax for type numeric: "price"
CONTEXT:  COPY products, line 1, column price: "price"

For a file without a header, leave the option out. (PostgreSQL 15 and later) checks that the header names match the column list in order, which catches a file whose columns were rearranged:

sql
COPY products (sku, name, price, category, stock_quantity)
FROM '/tmp/products.csv' WITH (FORMAT csv, HEADER MATCH);
text
ERROR:  column name mismatch in header line field 1: got "name", expected "sku"

Leaving out the column list makes COPY expect every table column in order, starting with , so the first name lands in the integer column: .

Delimiters: tab and semicolon

takes one character. Without it, a semicolon file is read as one long field per line and fails with .

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products_semicolon.csv' WITH (FORMAT csv, HEADER, DELIMITER ';');

Files from European spreadsheets often pair the semicolon with a decimal comma, which numeric columns reject: . Load the column as text into a staging table and convert it with .

For tab-separated files, use with , or the default text format, which expects tabs and writes NULL as :

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products.tsv' WITH (FORMAT text, HEADER);

NULL values and empty strings

In CSV format, an unquoted empty field is NULL and a quoted empty field () is an empty string. The word NULL in the file is just text unless you say otherwise.

OptionWhat it doesExample
String that means NULL (CSV default is an unquoted empty field)
Treat quoted empty strings in these columns as NULL too
Never NULL in these columns; empty fields become ''
String that means "use the column default" (PostgreSQL 16+)

Example: treat empty fields, quoted or not, as NULL in the category column.

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products.csv' WITH (FORMAT csv, HEADER, FORCE_NULL (category));

and for every column need PostgreSQL 17. On 16 the star is refused with .

An empty field does not fall back to the column default. It is NULL, so a NOT NULL column with a default still fails:

text
ERROR:  null value in column "stock_quantity" of relation "products" violates not-null constraint

Either leave the column out of the column list, so every row gets the default, or mark the cells with a placeholder and the option. is not allowed in CSV mode because it clashes with the NULL marker ().

Quotes and commas inside values

CSV format already handles double-quoted values. stays one field and loads as . Change when the file uses another character, and when quotes inside values are escaped with a backslash instead of doubled.

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products_single_quote.csv' WITH (FORMAT csv, HEADER, QUOTE '''');

has no quoting at all, so do not use it for a file with quoted fields.

Dates and timestamps

COPY parses dates with the session's , which is by default. A day-first date such as 25/12/2024 fails, and worse, 05/03/2024 loads quietly as May 3.

text
ERROR:  date/time field value out of range: "25/12/2024"
HINT:  Perhaps you need a different "DateStyle" setting.

Set the order for the session before the load:

sql
SET datestyle = 'ISO, DMY';
COPY products_launches (sku, launched_on, updated_at)
FROM '/tmp/dates.csv' WITH (FORMAT csv, HEADER);

For any other layout, load the column as text and convert it with or .

Encoding

COPY assumes the file uses the client encoding, which is usually UTF-8. A Latin-1 or Windows-1252 export fails on the first accented letter:

text
ERROR:  invalid byte sequence for encoding "UTF8": 0xe9 0x20 0x54

Name the file's encoding and PostgreSQL converts it on the way in:

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products_latin1.csv' WITH (FORMAT csv, HEADER, ENCODING 'LATIN1');

Use for files saved by older versions of Excel on Windows.

Import into a new table or a temp table

COPY never creates a table. Write the CREATE TABLE first, with one column per CSV column in the same order.

sql
CREATE TABLE products_import (
  name text,
  sku text,
  price numeric(10,2),
  category text,
  stock_quantity integer
);
\copy products_import FROM 'products.csv' WITH (FORMAT csv, HEADER)

copies an existing table's columns and defaults without its indexes. For a one-off check, use ; it disappears when the session ends, or at COMMIT with .

Import specific columns or skip columns

List the target columns after the table name, in the order they appear in the file. Columns you leave out, such as , get their defaults.

text
\copy products (name, sku, price) FROM 'prices.csv' WITH (FORMAT csv, HEADER)

COPY has no option to skip a column in the file. If the file has more columns than the list, it fails with . Load the full file into an all-text staging table and pick the columns you want:

sql
CREATE TEMP TABLE products_raw (
  id text, name text, sku text, price text,
  category text, internal_note text, stock_quantity text
);
\copy products_raw FROM 'products_extra.csv' WITH (FORMAT csv, HEADER)

INSERT INTO products (name, sku, price, category, stock_quantity)
SELECT name, sku, price::numeric, category, stock_quantity::int
FROM products_raw;

On macOS or Linux, can cut the columns out before they reach the server. It is fine for simple files, but splits on every comma, including commas inside quoted values.

text
\copy products (name, sku, price, category, stock_quantity) FROM PROGRAM 'cut -d, -f2-5,7 products_extra.csv' WITH (FORMAT csv, HEADER)

filters rows during the load (PostgreSQL 12 and later), for example .

Skip bad rows with ON_ERROR

By default one bad value stops the whole COPY and nothing is kept. PostgreSQL 17 added , which skips rows whose values cannot be converted to the column type:

sql
COPY products (name, sku, price, category, stock_quantity)
FROM '/tmp/products_bad.csv'
WITH (FORMAT csv, HEADER, ON_ERROR ignore, LOG_VERBOSITY verbose);
text
NOTICE:  skipping row due to data type incompatibility at line 3 for column "price": "ten"
NOTICE:  1 row was skipped due to data type incompatibility
COPY 2

PostgreSQL 18 adds , which still fails the COPY once more than n rows have been skipped. Version differences found in testing:

  • PostgreSQL 16: .
  • PostgreSQL 17: ON_ERROR and LOG_VERBOSITY work; .
  • PostgreSQL 18: all three work. is refused with .

ON_ERROR only covers type conversion. A row with too few fields (), a NOT NULL or CHECK failure, or a duplicate key still stops the load. For those, or on PostgreSQL 16, load into an all-text staging table and filter with SQL.

Duplicates and upserts

COPY has no ON CONFLICT clause. A row that repeats a unique key fails the load:

text
ERROR:  duplicate key value violates unique constraint "products_sku_key"
DETAIL:  Key (sku)=(SKU-HOM-WDL001) already exists.

Load into a staging table first, then insert from it with . skips rows that already exist; turns the import into an upsert.

Example: update prices and stock for existing SKUs and add the new ones, all in one transaction.

sql
BEGIN;
CREATE TEMP TABLE products_stage (LIKE products INCLUDING DEFAULTS) ON COMMIT DROP;
\copy products_stage (name, sku, price, category, stock_quantity) FROM 'products_update.csv' WITH (FORMAT csv, HEADER)

INSERT INTO products (name, sku, price, category, stock_quantity)
SELECT DISTINCT ON (sku) name, sku, price, category, stock_quantity
FROM products_stage
ORDER BY sku
ON CONFLICT (sku) DO UPDATE
  SET price = EXCLUDED.price,
      stock_quantity = EXCLUDED.stock_quantity;
COMMIT;

matters when the file itself repeats a key. Without it, the upsert fails with . Add a column to the ORDER BY (a line number or an updated_at) to choose which duplicate wins.

Import a large CSV file

COPY is already the fastest way in. In testing, loaded 2 million rows (79 MB) in under a second on both versions. For files in the tens of gigabytes:

  • Load into a table without indexes or foreign keys, then add them after. One index build at the end is much faster than updating it for every row.
  • writes rows already frozen, which saves a later VACUUM pass. It only works when the table was created or truncated in the same transaction; otherwise you get .
  • An staging table skips the write-ahead log. Its contents are lost after a crash, so copy the rows into the real table once they are clean.
  • Raise for the session before on the loaded table.

Example: load a fresh table with FREEZE.

sql
BEGIN;
TRUNCATE products_import;
COPY products_import FROM '/tmp/products.csv' WITH (FORMAT csv, HEADER, FREEZE);
COMMIT;

Watch a running COPY from a second session (PostgreSQL 14 and later). With the type is PIPE and is 0, because the server cannot see the file size.

sql
SELECT relid::regclass, type, bytes_processed, bytes_total, tuples_processed
FROM pg_stat_progress_copy;

Import a SQL file or dump

A plain file, including a default dump, runs with . A custom-format dump () or a directory dump needs . Each tool tells you when you picked the wrong one:

text
The input is a PostgreSQL custom-format dump.
Use the pg_restore command-line client to restore this dump to a database.

pg_restore: error: input file appears to be a text format dump. Please use psql.

Example: restore a plain dump so that it stops at the first error and keeps nothing if it fails, then restore a custom dump over existing tables.

bash
psql -d shop -v ON_ERROR_STOP=1 --single-transaction -f shop.sql

pg_restore -d shop --clean --if-exists --no-owner shop.dump

psql's default is to report an error and carry on with the next statement, which can leave a half-restored database. skips the ALTER ... OWNER statements, which fail when the original owner role does not exist on the new server.

Dumps made with pg_dump 17 or 18 start with . Loading one into PostgreSQL 16 or older prints . Without ON_ERROR_STOP, psql and pg_restore skip that line and the rest loads; with ON_ERROR_STOP, psql stops right there, so delete the line from a plain dump first. Version checks run the other way too: pg_restore 16 cannot read a custom dump from pg_dump 18 (), and pg_dump refuses a newer server (). Use the client tools from the newest version involved.

Import JSON

COPY has no JSON input format, so load each document into a column and expand it with SQL. For NDJSON (one object per line), read the file as single-column CSV with quote and delimiter characters that never appear in JSON. The default text format would treat backslashes as escapes and fail with .

sql
CREATE TEMP TABLE products_json (doc jsonb);
\copy products_json FROM 'products.ndjson' WITH (FORMAT csv, QUOTE E'\x01', DELIMITER E'\x02')

INSERT INTO products (name, sku, price, category, stock_quantity)
SELECT r.name, r.sku, r.price, r.category, r.stock_quantity
FROM products_json, jsonb_populate_record(NULL::products, doc) AS r;

For a file holding one JSON array, read it whole and expand it with . reads on the server and needs superuser or pg_read_server_files; from psql, a variable reads it on your machine.

text
\set content `cat products.json`
INSERT INTO products (name, sku, price, category, stock_quantity)
SELECT name, sku, price, category, stock_quantity
FROM jsonb_populate_recordset(NULL::products, :'content'::jsonb);

On PostgreSQL 17 and later, maps nested paths to columns, for example . PostgreSQL 16 does not have it and reports a syntax error at COLUMNS.

Work with PostgreSQL in one window

1bench is a desktop GUI for PostgreSQL. Browse tables, filter rows, run SQL and export what you see, with the SQL still there when you want it.

Open PostgreSQL in 1bench

Frequently asked questions

How do I import a CSV file into PostgreSQL with pgAdmin?
Right-click the table in pgAdmin 4, choose Import/Export Data, switch the dialog to Import, pick the file and turn on Header in the Options tab. pgAdmin runs psql's \copy for you, so a failure with exit code 1 means the \copy itself failed. Open the process details to read the real PostgreSQL error, which is usually a column count, NULL or date format problem covered on this page.
Can PostgreSQL create a table from a CSV file automatically?
No. COPY, \copy, pgAdmin and 1bench all load into a table that already exists. DuckDB can do it in one step: ATTACH the Postgres database with the postgres extension, then run CREATE TABLE pg.public.products_raw AS FROM read_csv('products.csv'). DuckDB guesses the types (text as varchar, decimals as double precision, whole numbers as bigint), so tighten them with ALTER TABLE afterwards.
How do I import an Excel file into PostgreSQL?
PostgreSQL cannot read .xlsx files. In Excel, use Save As and pick CSV UTF-8 (Comma delimited), then load the file with \copy ... WITH (FORMAT csv, HEADER). Excel writes dates in your locale's format and may add thousands separators, so check the date and number columns against the sections above before a large load.
How do I import a .gz file with psql?
For a gzipped CSV, let psql run the decompressor on your machine: \copy products FROM PROGRAM 'gunzip -c products.csv.gz' WITH (FORMAT csv, HEADER). For a gzipped SQL dump, pipe it in from the shell with gunzip -c shop.sql.gz | psql -d shop. The server-side COPY ... FROM PROGRAM form also works, but it needs superuser or the pg_execute_server_program role.