INSERT in PostgreSQL

Learn how to add rows to a PostgreSQL table with INSERT INTO, specify column lists, use expressions, and retrieve generated IDs with RETURNING.

5 min read · Back to overview

Quick Answer

Use INSERT INTO table_name (col1, col2) VALUES (val1, val2) to add a row. Always specify an explicit column list. Add RETURNING id to get back the generated primary key without a second query.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

The INSERT statement is the standard way to add rows to a PostgreSQL table. You name the table, list the columns you want to populate, and supply matching values. PostgreSQL inserts exactly one row per VALUES list and returns a command tag (INSERT 0 1) confirming the row count.

Syntax

INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);

Retrieve the auto-generated primary key with RETURNING:

INSERT INTO table_name (column1, column2)
VALUES (value1, value2)
RETURNING id;

Practical Example

Create an orders table and insert a row:

CREATE TABLE orders (
  id          SERIAL PRIMARY KEY,
  customer    VARCHAR(100) NOT NULL,
  product_sku VARCHAR(50)  NOT NULL,
  quantity    INT          NOT NULL DEFAULT 1,
  unit_price  NUMERIC(10, 2) NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO orders (customer, product_sku, quantity, unit_price)
VALUES ('Acme Corp', 'SKU-7721', 3, 49.99);

Output:

INSERT 0 1

To insert a string containing a single quote, double the quote character:

INSERT INTO orders (customer, product_sku, quantity, unit_price)
VALUES ('O''Brien LLC', 'SKU-1002', 1, 199.00);

Use RETURNING to get the generated id and creation timestamp in the same statement:

INSERT INTO orders (customer, product_sku, quantity, unit_price)
VALUES ('Northgate Inc', 'SKU-4450', 5, 25.00)
RETURNING id, created_at;
 id |          created_at
----+-------------------------------
  3 | 2026-04-07 09:15:42.123456+00
(1 row)

Inspect the table to confirm all rows:

SELECT id, customer, product_sku, quantity, unit_price
FROM orders
ORDER BY id;

RETURNING Clause

RETURNING is a PostgreSQL extension to standard SQL. It returns column values from the newly inserted row — useful for capturing auto-generated primary keys, timestamps, or computed defaults without issuing a second query.

INSERT INTO orders (customer, product_sku, quantity, unit_price)
VALUES ('Eastwick Co', 'SKU-9910', 2, 75.50)
RETURNING id AS new_order_id, created_at AS order_time;

You can return every column with RETURNING *, or rename individual columns with AS. The value is read from the row after all defaults and triggers have been applied.

Use a data-modifying CTE to chain the inserted row into a follow-up operation within the same transaction:

WITH new_order AS (
  INSERT INTO orders (customer, product_sku, quantity, unit_price)
  VALUES ('Harbor Trades', 'SKU-3310', 10, 12.00)
  RETURNING id
)
INSERT INTO order_audit (order_id, event, recorded_at)
SELECT id, 'created', now()
FROM new_order;

Testing with Vela

Before running INSERT-heavy migrations or bulk data loads in production, use Vela database branching to create an instant copy-on-write clone of your production database. Test the INSERT statements — including RETURNING logic and any trigger side-effects — on the branch, then promote to production only when the results are confirmed. This is especially valuable when inserting into tables with complex constraints or referential integrity rules.

Production Tips

  • Always specify an explicit column list. Relying on column position breaks silently when columns are added or reordered.
  • Use RETURNING id instead of a follow-up SELECT to retrieve the generated primary key — it is atomic and saves a round-trip.
  • Wrap related inserts in an explicit transaction so all succeed together or none are persisted.
  • For bulk data loads, COPY FROM is significantly faster than many individual INSERT statements.
  • Use INSERT ... ON CONFLICT DO NOTHING to skip rows that would violate a unique constraint rather than catching errors in application code.
  • SERIAL and IDENTITY columns generate values automatically — omit them from the column list and let PostgreSQL handle them.

Continue in Modifying Data: INSERT Multiple Rows.

Related in this section: INSERT Multiple Rows · UPDATE · UPDATE Join

Frequently Asked Questions

What does INSERT do in PostgreSQL?
INSERT adds one or more new rows to a table. You name the target table, list the columns to populate, and provide matching values. Columns omitted from the list receive their default value or NULL if no default is defined.
Does INSERT lock the table?
INSERT acquires a ROW EXCLUSIVE lock, which allows concurrent reads and other row-level writes. It does not block SELECT queries. Only table-level operations such as VACUUM FULL or ALTER TABLE would conflict.
What happens if I omit a NOT NULL column in an INSERT?
PostgreSQL raises an error: 'null value in column violates not-null constraint'. You must supply a value for every NOT NULL column that has no DEFAULT defined.
Can I use IF NOT EXISTS with INSERT?
There is no IF NOT EXISTS clause for INSERT. To skip rows that would conflict with a unique constraint, use INSERT ... ON CONFLICT DO NOTHING instead.
What is the safest way to INSERT in production?
Always specify an explicit column list so the statement remains correct if columns are added or reordered later. Wrap related inserts in a transaction and use RETURNING to retrieve generated IDs atomically rather than issuing a follow-up SELECT.