Skip to content
PostgreSQL

PostgreSQL Error 23505

duplicate key value violates unique constraint

Applies to: PostgreSQL 13 to 18 (examples run on PostgreSQL 18.6 in PGlite)
Last technically reviewed 24 October 2026 on PostgreSQL 18.6 (PGlite 0.5.11)
The full message
ERROR:  duplicate key value violates unique constraint "customers_email_key"
DETAIL:  Key (email)=(sara@example.com) already exists.
Other forms of this message
Primary key
ERROR:  duplicate key value violates unique constraint "customers_pkey"
DETAIL:  Key (id)=(1) already exists.
Multi-column key
ERROR:  duplicate key value violates unique constraint "order_lines_pkey"
DETAIL:  Key (order_id, line_no)=(10, 1) already exists.
Creating a unique index or constraint over existing duplicates (same SQLSTATE)
ERROR:  could not create unique index "ux_people_name"
DETAIL:  Key (name)=(Sara) is duplicated.
psql also prints the statement position
LINE 1: ... (a LINE line with a caret under the failing part of the statement)
SQLSTATE
23505
Condition name
unique_violation
Product
PostgreSQL
Versions
PostgreSQL 13 to 18 (examples run on PostgreSQL 18.6 in PGlite)
Last reviewed
24 October 2026

What it means

SQLSTATE 23505 means a row would store a value that already exists in a PRIMARY KEY, UNIQUE constraint or unique index, so PostgreSQL rejected the statement.

A PRIMARY KEY or UNIQUE constraint is enforced by a unique index, and each key value may appear only once. When an INSERT, UPDATE or COPY would create a second row with an existing value, PostgreSQL stops the statement and raises 23505 (unique_violation). The message names the constraint or index, and the DETAIL line shows the exact key and value: Key (email)=(sara@example.com) already exists. For a key over several columns, DETAIL lists every column and value.

Inside a transaction, the error also aborts the transaction: every later statement fails with 25P02 until you ROLLBACK (or roll back to a savepoint). Building a unique index or adding a UNIQUE constraint over data that already has duplicates raises the same SQLSTATE with a different first line, "could not create unique index".

Common causes

  • The same row is inserted twice: a retried request, a double submit or an import that runs again.
  • The sequence behind a serial or identity column is behind the data: rows were inserted with explicit ids (an import, a restore of data only, a manual fix), so the next generated id is one that already exists.
  • An UPDATE changes a unique column, such as an email address, to a value another row already has.
  • A unique index or constraint is added to a column that already holds duplicate values.
  • An INSERT ... SELECT or COPY brings in a source with duplicates, among its own rows or against existing ones.

Diagnose it

Read-only These queries only read the system catalogs (pg_catalog, information_schema) and statistics views such as pg_stat_activity and pg_locks. They do not change data, roles or settings, so they are safe to run first.

Which columns does the constraint cover?

Use the constraint name from the message.

SELECT conname, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'customers'::regclass
  AND contype IN ('p', 'u')
ORDER BY conname;
What to look for

Each primary key and unique constraint with its columns, for example UNIQUE (email). A unique index created with CREATE UNIQUE INDEX is not a constraint: find it in pg_indexes instead.

Find the row that already holds the value

SELECT *
FROM customers
WHERE email = 'sara@example.com';  -- the value from the DETAIL line
What to look for

The existing row. Compare it with the data you were writing to decide whether to skip, update or reject the new row.

Is the id sequence behind the data?

When the duplicate is on a serial or identity id, compare the sequence with the highest id in the table.

SELECT pg_get_serial_sequence('customers', 'id') AS sequence_name;

SELECT last_value, is_called FROM customers_id_seq;
SELECT max(id) AS highest_id FROM customers;
What to look for

If last_value is lower than highest_id, the next generated id will collide with an existing row. That is the classic cause after an import or a data-only restore.

Find duplicates before adding a unique index

SELECT name, count(*) AS copies
FROM people
GROUP BY name
HAVING count(*) > 1
ORDER BY copies DESC;
What to look for

Each value that appears more than once; CREATE UNIQUE INDEX fails until these are resolved.

How to fix it

Each fix is labelled with what it changes. Try read-only checks and the least invasive fix first, and test changes outside production.

Update the existing row instead (upsert)

Changes data

When a matching key should update the row rather than fail, use INSERT ... ON CONFLICT (column) DO UPDATE. EXCLUDED is the row you tried to insert. Name the conflict column (or constraint) so only that key is handled. ON CONFLICT is safe under concurrency, unlike a separate SELECT followed by INSERT.

INSERT INTO customers (email, name)
VALUES ('sara@example.com', 'Sara Ahmed')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;

Skip rows that already exist

Changes data

For re-runs and imports, ON CONFLICT DO NOTHING skips the conflicting rows and inserts the rest.

INSERT INTO customers (email, name)
SELECT email, name FROM staging_customers
ON CONFLICT (email) DO NOTHING;

Move the sequence past the highest id

Changes database state

When the sequence is behind the data, set it to the current maximum so the next generated id is free. For an identity column, pg_get_serial_sequence returns its sequence too.

Caution: Run it when nothing else is inserting into the table, or take a lock first; otherwise a concurrent insert can still pick a value between the check and the change. Never set the sequence lower than the highest id.

SELECT setval(pg_get_serial_sequence('customers', 'id'),
              (SELECT max(id) FROM customers));

Resolve existing duplicates, then add the unique index

Changes schema

Find the duplicates with the read-only query above, agree which row to keep, then create the index. On a busy table, CREATE UNIQUE INDEX CONCURRENTLY avoids blocking writes; if it fails it leaves an invalid index that you must drop before retrying.

CREATE UNIQUE INDEX CONCURRENTLY ux_people_name ON people (name);

Examples

Inserting an email address that is already registered

CREATE TABLE customers (
    id    serial PRIMARY KEY,
    email text UNIQUE,
    name  text
);

INSERT INTO customers (email, name) VALUES ('sara@example.com', 'Sara');
INSERT INTO customers (email, name) VALUES ('sara@example.com', 'Sara B');
Result
ERROR:  duplicate key value violates unique constraint "customers_email_key"
DETAIL:  Key (email)=(sara@example.com) already exists.

Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.

A sequence that is behind the data

The failed INSERT still used up sequence value 1, so retrying it gets id 2 and succeeds here. With many imported rows the collisions continue until the sequence passes the highest id, which is why the fix is setval.

CREATE TABLE customers (
    id    serial PRIMARY KEY,
    email text UNIQUE,
    name  text
);

-- rows loaded with explicit ids, for example from an import
INSERT INTO customers (id, email, name) VALUES (1, 'sara@example.com', 'Sara');

-- the sequence still starts at 1
INSERT INTO customers (email, name) VALUES ('ali@example.com', 'Ali');
Result
ERROR:  duplicate key value violates unique constraint "customers_pkey"
DETAIL:  Key (id)=(1) already exists.

Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.

A duplicate in a multi-column key

CREATE TABLE order_lines (
    order_id int,
    line_no  int,
    PRIMARY KEY (order_id, line_no)
);

INSERT INTO order_lines (order_id, line_no) VALUES (10, 1), (10, 1);
Result
ERROR:  duplicate key value violates unique constraint "order_lines_pkey"
DETAIL:  Key (order_id, line_no)=(10, 1) already exists.

Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.

Adding a unique index over existing duplicates

CREATE TABLE people (id int, name text);
INSERT INTO people VALUES (1, 'Sara'), (2, 'Sara');

CREATE UNIQUE INDEX ux_people_name ON people (name);
Result
ERROR:  could not create unique index "ux_people_name"
DETAIL:  Key (name)=(Sara) is duplicated.

Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.

How to prevent it

Let PostgreSQL generate surrogate keys with an identity column (GENERATED ALWAYS AS IDENTITY) or serial, and do not insert explicit ids into them; after any import that does, run setval as shown above. Make writes that may run twice idempotent with ON CONFLICT.

Keep the unique constraint even when the application checks first, and handle SQLSTATE 23505 in your code (most drivers expose the SQLSTATE and the constraint name) so a duplicate becomes a clear message such as "this email is already registered". Remember that the error aborts the current transaction.

Common questions

How do I fix "duplicate key value violates unique constraint" in PostgreSQL?

Read the constraint name and the Key (...)=(...) part of the DETAIL line, find the existing row, then skip the new row (ON CONFLICT DO NOTHING) or update the existing one (ON CONFLICT DO UPDATE). If the duplicate is on a generated id, move the sequence past the highest id with setval.

Why does my serial or identity id collide after importing data?

The import inserted explicit ids but did not advance the sequence, so the next generated value is one that already exists. Set the sequence to the highest id with setval(pg_get_serial_sequence(...), max(id)).

Why are there gaps in my ids after this error?

Sequence values are not rolled back: a failed INSERT still uses up the value it took, so the next successful row gets a higher id. Gaps are normal and do not need fixing.

Learn more on CodeWithSQL

Official documentation

Last technically reviewed 24 October 2026; examples and diagnostics run on PostgreSQL 18.6 (PGlite 0.5.11). SQL-level examples were run in PGlite (PostgreSQL compiled to WebAssembly); examples that need a running server are shown as documented in the PostgreSQL documentation.

Still stuck on a PostgreSQL error?

Browse the full PostgreSQL error library, or get hands-on help from an experienced SQL professional.