Export a PostgreSQL Table to a CSV File

Learn how to export a PostgreSQL table or query result to a CSV file using the server-side COPY TO command and the client-side psql \copy meta-command, with examples for partial columns and filtered exports.

5 min read · Back to overview

Quick Answer

Use COPY table_name TO '/path/to/file.csv' DELIMITER ',' CSV HEADER; to export a table to CSV when the output file should be written on the database server. To save the file to your local machine, use the psql \copy command — it streams data over the client connection and requires no superuser privileges.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

PostgreSQL offers two commands for writing table data to a CSV file: the server-side COPY ... TO statement and the client-side psql \copy meta-command. COPY TO writes the file directly on the database host — ideal for large exports to a local filesystem — but requires superuser access. \copy streams the data to a file on your workstation over the psql connection and works with ordinary user privileges, making it the practical choice for scripts and CI pipelines.

Syntax

Server-side export with COPY TO:

COPY table_name [(column_list)]
TO '/absolute/path/to/output.csv'
[ DELIMITER ',' ]
[ CSV [ HEADER ] ];

Export a query result:

COPY (SELECT ...)
TO '/absolute/path/to/output.csv'
CSV HEADER;

Client-side export with \copy (run inside psql — no semicolon):

\copy table_name [(column_list)]
TO '/local/path/to/output.csv'
WITH (FORMAT csv, HEADER true)

Key options:

OptionPurpose
DELIMITER ','Field separator character
CSV HEADERInclude a header row with column names
NULL 'NULL'String to write for SQL NULL values
ENCODING 'UTF8'Output file encoding
FORCE_QUOTE (col)Always quote a specific column, even when not required

Practical Example

Create and populate an orders table:

CREATE TABLE orders (
  order_id    SERIAL PRIMARY KEY,
  product_sku VARCHAR(30) NOT NULL,
  quantity    INT         NOT NULL,
  unit_price  NUMERIC(10, 2) NOT NULL,
  status      VARCHAR(20) NOT NULL DEFAULT 'pending',
  placed_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO orders (product_sku, quantity, unit_price, status, placed_at)
VALUES
  ('SKU-001', 3, 29.99, 'shipped',  '2026-03-01 09:00:00+00'),
  ('SKU-002', 1, 149.00, 'shipped', '2026-03-02 11:30:00+00'),
  ('SKU-003', 5, 9.50,  'pending',  '2026-03-03 14:15:00+00');

Export the entire table to a CSV file with a header row:

COPY orders
TO '/var/lib/postgresql/orders_export.csv'
DELIMITER ','
CSV HEADER;
COPY 3

Export only selected columns:

COPY orders (order_id, product_sku, status)
TO '/var/lib/postgresql/orders_partial.csv'
DELIMITER ','
CSV HEADER;

Export only shipped orders using a query:

COPY (
  SELECT order_id, product_sku, quantity, unit_price
  FROM orders
  WHERE status = 'shipped'
  ORDER BY placed_at
)
TO '/var/lib/postgresql/shipped_orders.csv'
CSV HEADER;

Exporting to Your Local Machine with \copy

When you need the file on your workstation or when connecting to a managed PostgreSQL service, use \copy inside psql. It accepts the same options but resolves the file path locally:

\copy (
  SELECT order_id, product_sku, quantity, unit_price, placed_at
  FROM orders
  ORDER BY placed_at DESC
) TO '/Users/you/orders_local.csv' WITH (FORMAT csv, HEADER true)

You can also pipe the result directly to a program using PROGRAM instead of a file path on the server side:

COPY orders TO PROGRAM 'gzip > /var/lib/postgresql/orders.csv.gz' CSV HEADER;

Testing with Vela

Before exporting production data — especially for data migration or audit reports — verify that your COPY query returns exactly the rows and columns you expect. Vela’s database branching makes this safe: create a branch from production, run the export query against the branch, and inspect the output file. You can iterate on your SELECT filter logic without touching the live database or generating incorrect reports.

Production Tips

  • Use \copy in CI and deployment scripts to avoid hard-coding server-side file paths that vary between environments.
  • Wrap multiple COPY TO commands in a single BEGIN / COMMIT block with REPEATABLE READ isolation when you need a consistent snapshot across several exported files.
  • Add an ORDER BY clause inside the subquery to produce deterministic output — important for diffing exports across runs or comparing snapshots.
  • Append a timestamp to the output filename (e.g., orders_20260401.csv) to prevent accidental overwrites of earlier exports.
  • For large exports, run COPY during off-peak hours or against a read replica to avoid competing with OLTP traffic on the primary.
  • Set NULL 'NULL' (or another sentinel string) so downstream consumers can distinguish empty strings from true SQL NULLs.

Continue in Import / Export: Back to tutorial overview.

Related in this section: Import CSV File into Table

Frequently Asked Questions

What is the difference between COPY TO and \copy in PostgreSQL?
COPY TO is a server-side SQL command that writes the output file on the PostgreSQL host machine. It requires superuser privileges and a file path accessible to the server process. \copy is a psql client-side meta-command that streams data to a file on your local workstation over the existing connection. No superuser rights are needed for \copy.
Does COPY TO lock the table during export?
COPY TO acquires a ROW SHARE lock, which is compatible with concurrent reads and writes. Other transactions can INSERT, UPDATE, DELETE, and SELECT while the export is running. The export reads a consistent snapshot of the table at the start of the command.
Can I export the result of a SELECT query instead of a whole table?
Yes. Wrap any SELECT in parentheses after COPY: COPY (SELECT order_id, total FROM orders WHERE status = 'shipped') TO '/path/file.csv' CSV HEADER. The psql \copy command accepts the same syntax. This lets you filter, join, aggregate, and shape data before writing the file.
Does COPY TO overwrite an existing file?
Yes. If the destination path already exists, COPY TO overwrites it silently without any warning or confirmation prompt. Always verify the output path before running the command in production.
What is the safest way to export production data to CSV?
Run the export inside a transaction with REPEATABLE READ isolation to ensure a consistent snapshot across multiple COPY commands. Use \copy from a read replica rather than the primary to avoid any resource contention. Name output files with timestamps to avoid accidental overwrites.