57 COPY
COPY is PostgreSQL’s bulk-data import/export command. It moves rows between a table and a file (or standard input/output) in CSV, text, or binary format. Compared to INSERT, COPY is dramatically faster — sometimes 10–100x — because it skips much of the per-row overhead. For loading large amounts of data, COPY is what you reach for.
What you’ll learn
COPY ... TOfor export andCOPY ... FROMfor import- The difference between
COPY(server-side) and\copy(client-side) - CSV format and the
HEADER,QUOTE,DELIMITERoptions - Streaming via
STDIN/STDOUT - Why
COPYbeatsINSERTfor bulk loads
Example
COPY ... TO STDOUT writes to the connection’s output stream. When you run a SQL file with psql -f, that stream is your terminal — handy for ad-hoc exports. Redirect from the shell to save to a file: psql -f source/copy.sql postgres > out.csv.
COPY ... FROM <path> reads from a file on the server. The PostgreSQL process needs read access; on a managed cloud database, you usually do not. The client-side \copy (a psql meta-command) reads the file on your machine, streams it through the connection, and inserts it on the server — exactly the same behavior as COPY FROM STDIN, just wrapped for convenience.
For application code doing bulk loads, almost every driver exposes a “COPY FROM STREAM” API (psycopg’s copy_from, pgx’s CopyFrom, etc.). For a few thousand rows it is much faster than batched INSERTs; it is often the best starting point for millions of rows, though the benefit depends on the workload.
COPY FROM invokes triggers and enforces check constraints and foreign keys on the destination table. Trigger work therefore contributes to load cost. It does not invoke rules. A staging table is useful for validating and transforming a load before inserting into the final table.
Run the example
To run:
Common pitfalls
- Mixing up
COPYand\copy: the first runs on the server (paths are server paths, permissions are server permissions); the second runs on the client (paths are local). A common mistake is writingCOPY ... FROM '~/file.csv'and getting “permission denied” because the server cannot read your home directory. - Encoding mismatches: The file uses the
ENCODINGoption, or the current client encoding when that option is absent. Mis-encoded CSVs produce garbled text or “invalid byte sequence” errors. UseWITH (ENCODING 'utf8')or convert beforehand. - CSVs with embedded newlines need
QUOTEandESCAPEoptions. The defaults ("for both) usually work. COPYwith constraints on a huge load is slow. Trick: load into a staging table, deduplicate / validate, thenINSERT ... ON CONFLICTinto the real table.
For very large imports, drop indexes (except the PK) before COPY, then recreate. The COPY is faster, and a single index build at the end is faster than maintaining indexes per-row across millions of rows. Wrap in a transaction so failure rolls back.
Export fruits to a file: psql -f source/copy.sql postgres > out.csv. Then import it back into a staging table: create fruits_stage in one psql session, then run \copy on its own line:
Compare the counts.
Downloads
SQL file: copy.sql · All lesson files · Setup guide