The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Import a client file\copy app.stage_order (external_id, ordered_at, amount) FROM './orders.csv' WITH (FORMAT csv, HEADER match)View examples
Import a server fileCOPY app.stage_order (external_id, ordered_at, amount) FROM '/srv/import/orders.csv' WITH (FORMAT csv, HEADER match);View examples
Stream from a clientCOPY app.stage_order (external_id, ordered_at, amount) FROM STDIN WITH (FORMAT csv, HEADER match);View examples
Verify CSV headingsCOPY app.stage_order (external_id, ordered_at, amount) FROM STDIN WITH (FORMAT csv, HEADER match);View examples
Choose a NULL markerCOPY app.stage_order FROM STDIN WITH (FORMAT csv, HEADER match, NULL 'NULL');View examples
Keep empty textCOPY app.contact FROM STDIN WITH (FORMAT csv, HEADER match, FORCE_NOT_NULL (display_name));View examples
Declare source encodingCOPY app.stage_order FROM STDIN WITH (FORMAT csv, HEADER match, ENCODING 'UTF8');View examples
Create transaction stagingCREATE TEMP TABLE stage_order (LIKE app.orders INCLUDING DEFAULTS) ON COMMIT DROP;View examples
Promote valid rowsINSERT INTO app.orders (external_id, ordered_at, amount) SELECT external_id, ordered_at, amount FROM stage_order;View examples
Bound conversion rejectsCOPY app.stage_order FROM STDIN WITH (FORMAT csv, HEADER match, ON_ERROR ignore, REJECT_LIMIT 20);View examples
Report rejected fieldsCOPY app.stage_order FROM STDIN WITH (FORMAT csv, ON_ERROR ignore, REJECT_LIMIT 20, LOG_VERBOSITY verbose);View examples
Export to a client file\copy app.orders (external_id, ordered_at, amount) TO './orders.csv' WITH (FORMAT csv, HEADER true)View examples
Export a stable projection\copy ( SELECT external_id, ordered_at, amount FROM app.orders ORDER BY external_id ) TO './orders.csv' WITH (FORMAT csv, HEADER true)View examples
Monitor COPY progressSELECT pid, command, type, bytes_processed, tuples_processed FROM pg_stat_progress_copy;View examples
Reclaim failed-load spaceVACUUM (ANALYZE) app.stage_order;View examples

Bulk transfer crosses database, filesystem, encoding, and trust boundaries at once. Prefer client-side \copy unless a controlled server-side path is genuinely required, name columns explicitly, define the CSV contract, and load untrusted data into a restricted staging table before promotion. Treat skipped rows, trigger behavior, RLS limitations, exported secrets, and spreadsheet formulas as security and correctness concerns—not cleanup details.

Step by step

Detailed examples

01

Choose the filesystem and privilege boundary deliberately

SQL COPY with a filename reads or writes on the database server as the operating-system PostgreSQL account. It requires superuser or a powerful predefined server-file role and can reach any file that account can access. psql \copy instead uses COPY FROM STDIN or TO STDOUT and accesses the client filesystem as the local user. Prefer the client path for routine transfers. Never interpolate untrusted input into COPY PROGRAM: the server invokes it through a shell. COPY FROM also cannot target an RLS-enabled table in PostgreSQL 18; use authorized INSERT statements or a separately secured staging workflow.

Use a local psql file without server-file privileges
\copy app.stage_order (external_id, ordered_at, amount) FROM './orders.csv' WITH (FORMAT csv, HEADER match, ENCODING 'UTF8')
Back to quick reference ↑
02

Make the CSV dialect part of the interface

Always list destination columns so schema additions do not shift incoming fields. In PostgreSQL 18, HEADER MATCH rejects a header whose names, number, or order differ from that list; older supported releases accept only a Boolean HEADER option. CSV's default unquoted empty field means NULL, while a quoted empty field means an empty string. Set NULL, FORCE_NULL, or FORCE_NOT_NULL only when the producer's contract requires it, and declare ENCODING when the source is known. CSV values can contain embedded line breaks, so line counts are not row counts.

Define names, null semantics, and encoding together
COPY app.stage_contact (external_id, display_name, email)
FROM STDIN
WITH (
  FORMAT csv,
  HEADER match,
  NULL 'NULL',
  FORCE_NOT_NULL (display_name),
  ENCODING 'UTF8'
);
Back to quick reference ↑
03

Load untrusted data into a narrow staging contract

COPY FROM invokes destination triggers and check constraints, but it does not invoke rules and it supplies explicit values to identity columns. A dedicated staging table lets you preserve source text, quarantine invalid rows, check uniqueness and foreign keys, and promote only reviewed columns. Keep it inaccessible to runtime roles, run the workflow in a transaction when size permits, record source metadata and row counts, and make promotion idempotent for safe retries.

Validate before an idempotent promotion
BEGIN;
CREATE TEMP TABLE stage_order (
  external_id text PRIMARY KEY,
  ordered_at timestamptz NOT NULL,
  amount numeric(12,2) CHECK (amount >= 0)
) ON COMMIT DROP;

COPY stage_order (external_id, ordered_at, amount)
FROM STDIN WITH (FORMAT csv, HEADER match);

INSERT INTO app.orders (external_id, ordered_at, amount)
SELECT external_id, ordered_at, amount
FROM stage_order
ON CONFLICT (external_id) DO UPDATE
SET ordered_at = EXCLUDED.ordered_at, amount = EXCLUDED.amount;
COMMIT;
Back to quick reference ↑
04

Treat tolerated errors as an audited exception path

PostgreSQL 18 can ignore data-type conversion errors in text or CSV COPY FROM, but ON_ERROR ignore does not make arbitrary constraint, trigger, encoding, or structural failures safe. Without REJECT_LIMIT, every conversion error may be skipped, so always set a small business-approved ceiling. LOG_VERBOSITY verbose helps diagnose rejected fields but can expose sensitive source values in notices and logs. Reconcile the COPY count against source, accepted, and quarantined counts; use fail-fast loading when you cannot account for every rejected record.

Allow a bounded number of conversion failures
COPY app.stage_order (external_id, ordered_at, amount)
FROM STDIN
WITH (
  FORMAT csv,
  HEADER match,
  ON_ERROR ignore,
  REJECT_LIMIT 20,
  LOG_VERBOSITY verbose
);
Back to quick reference ↑
05

Export an approved projection, not an entire table

COPY TO requires SELECT privilege and applies relevant SELECT policies when RLS is active, but a table owner, superuser, or BYPASSRLS exporter can still expose every row. Use a query with explicit columns and stable ordering, run as a least-privilege export role, and store the result with restrictive local permissions and retention. CSV opened in spreadsheet software can interpret cells beginning with =, +, -, or @ as formulas; neutralize or avoid spreadsheet delivery when values are user-controlled.

Export a minimal deterministic report through psql
\copy (SELECT external_id, ordered_at, amount FROM app.orders WHERE ordered_at >= DATE '2026-08-01' ORDER BY external_id) TO './orders-2026-08.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')
Back to quick reference ↑
06

Observe throughput and plan for failure cleanup

Each active COPY backend appears in pg_stat_progress_copy, though some counters can be zero when the source cannot report them. COPY FROM appends rows and takes a ROW EXCLUSIVE table lock; indexes, triggers, constraints, WAL, and replicas can dominate runtime. A failed COPY leaves its inserted tuples invisible but occupying space until vacuum can reclaim them. Benchmark realistic batches, watch WAL and replica lag, analyze after major data changes, and prefer restartable chunks over a single unbounded production load.

Inspect active transfers from a monitoring session
SELECT pid, datname, relid::regclass AS relation,
       command, type, bytes_processed, bytes_total,
       tuples_processed, tuples_excluded
FROM pg_catalog.pg_stat_progress_copy
ORDER BY pid;
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. PostgreSQL Global Development GroupPostgreSQL: COPYpostgresql.org
  2. PostgreSQL Global Development GroupPostgreSQL: psqlpostgresql.org
  3. PostgreSQL Global Development GroupPostgreSQL: Progress Reportingpostgresql.org
  4. PostgreSQL Global Development GroupPostgreSQL: Predefined Rolespostgresql.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback