SQL Server error 2627
Violation of PRIMARY KEY constraint. Cannot insert duplicate key in object
A row would give a primary key, UNIQUE constraint (2627) or unique index (2601) a value another row already has, so the statement fails and writes nothing. If the value is an id you didn’t supply, the IDENTITY or sequence is behind the data.
Violation of PRIMARY KEY constraint 'PK_plans'. Cannot insert duplicate key in object 'seo_sqlerr_data.plans'. The duplicate key value is (1).
Tested on SQL Server 2022 RTM-CU27 (16.0.4295.3); same messages on 2019 RTM-CU32-GDR and 2025 RTM-CU9 · Updated 9 October 2026
What it means
A primary key, a UNIQUE constraint and a unique index each allow a value (or a combination of
values) only once. Your INSERT, UPDATE or MERGE would have created a second row with the same
key, so SQL Server stopped the statement and undid all of it: none of its rows were written.
The number tells you which kind of object refused, and both messages end with the clashing value:
| Error | Raised by | Message |
|---|---|---|
| 2627 | a PRIMARY KEY or UNIQUE constraint | Violation of PRIMARY KEY constraint 'PK_plans'. Cannot insert duplicate key in object 'dbo.plans'. The duplicate key value is (1). |
| 2601 | an index made with CREATE UNIQUE INDEX | Cannot insert duplicate key row in object 'dbo.customers' with unique index 'IX_customers_name'. The duplicate key value is (Ada). |
Only the statement fails. If you’re inside a transaction, it stays open and the earlier statements
in it can still be committed, unless SET XACT_ABORT ON is in effect: then the whole transaction is
rolled back.
Common causes
- A real duplicate: the same email signed up twice, a retried request, a job that ran twice, or the same key twice in one batch of rows you’re loading.
- Check-then-insert under load.
IF NOT EXISTS (SELECT …) INSERT …isn’t atomic: two sessions both find no row, and both insert. - The id generator is behind the data. The duplicate value is an id you didn’t supply. Someone
ran
DBCC CHECKIDENT (…, RESEED, 0)after deleting rows, or the id comes from aSEQUENCEdefault and rows were loaded with explicit ids, which a sequence never notices. Code that picksMAX(id) + 1itself clashes as soon as two sessions run it at once. - An upsert written as a plain insert, or a
MERGEwhose source has the same key twice. - A second NULL in a unique column. SQL Server allows only one NULL in a
UNIQUEconstraint or unique index, unlike PostgreSQL and MySQL; the message then readsThe duplicate key value is (<NULL>).
Adding a unique constraint or index to data that already has duplicates fails with a different
number, 1505 (The CREATE UNIQUE INDEX statement terminated because a duplicate key was found…).
How to fix it
Find the duplicates
The message names the constraint or index and the value. To list every clash before you add a constraint or load data:
SELECT email, COUNT(*) AS copies
FROM dbo.customers
GROUP BY email
HAVING COUNT(*) > 1;
Check the IDENTITY or sequence
NORESEED only reports; it changes nothing:
DBCC CHECKIDENT ('dbo.plans', NORESEED);
Checking identity information: current identity value '1', current column value '2'.
When the current identity value is below the highest id in the column, RESEED with no value moves
it up to that maximum:
DBCC CHECKIDENT ('dbo.plans', RESEED);
It needs db_owner, db_ddladmin, sysadmin or ownership of the schema. Never reseed below the
highest id to “close gaps”: that is how the clash starts. Failed inserts use up identity values too,
so gaps are normal.
Inserting explicit ids with SET IDENTITY_INSERT dbo.plans ON moves the IDENTITY past them by
itself. A SEQUENCE doesn’t: after loading rows with their own ids, restart it past the highest one.
SELECT current_value FROM sys.sequences WHERE name = 'order_ids';
SELECT MAX(id) FROM dbo.orders;
ALTER SEQUENCE dbo.order_ids RESTART WITH 3; -- MAX(id) + 1
Make insert-or-update safe
Hold the lock from the check until the insert, inside one transaction. UPDLOCK, HOLDLOCK makes a
second session wait at the SELECT until the first commits, and then it sees the row:
BEGIN TRAN;
IF NOT EXISTS (SELECT 1 FROM dbo.customers WITH (UPDLOCK, HOLDLOCK) WHERE email = @email)
INSERT INTO dbo.customers (email, name) VALUES (@email, @name);
ELSE
UPDATE dbo.customers SET name = @name WHERE email = @email;
COMMIT;
Or use MERGE with HOLDLOCK, which Microsoft’s documentation says protects against unique key
violations when keys are both inserted and updated:
MERGE dbo.customers WITH (HOLDLOCK) AS t
USING (VALUES (@email, @name)) AS s (email, name)
ON t.email = s.email
WHEN MATCHED THEN UPDATE SET name = s.name
WHEN NOT MATCHED THEN INSERT (email, name) VALUES (s.email, s.name);
MERGE compares each source row with the table as it was, so a source with the same new key twice
inserts it twice and fails with 2627. Remove duplicates from the source first (GROUP BY, or keep
ROW_NUMBER() = 1 per key).
To add only the rows that are missing, filter them in the INSERT:
INSERT INTO dbo.customers (email, name)
SELECT s.email, s.name
FROM staging.customers AS s
WHERE NOT EXISTS (SELECT 1 FROM dbo.customers AS c WITH (UPDLOCK, HOLDLOCK) WHERE c.email = s.email);
Skip duplicates on purpose: IGNORE_DUP_KEY
A unique index created WITH (IGNORE_DUP_KEY = ON) drops the duplicate rows of an INSERT with a
warning (Duplicate key was ignored.) and inserts the rest. It applies to inserts only, not to
UPDATE, and can’t be used on a filtered index. It suits a staging table you’re loading; on a real
table it hides the duplicates you may want to know about.
CREATE UNIQUE INDEX IX_tags_tag ON dbo.tags (tag) WITH (IGNORE_DUP_KEY = ON);
Allow many NULLs with a filtered index
To keep values unique but allow any number of NULLs, replace the constraint with a filtered unique index:
ALTER TABLE dbo.members DROP CONSTRAINT UQ_members_phone;
CREATE UNIQUE INDEX UX_members_phone ON dbo.members (phone) WHERE phone IS NOT NULL;
Reproduce it
On SQL Server 2022 (16.0.4295.3) with sqlcmd, in a scratch schema. A UNIQUE constraint on
email and a unique index on name:
Msg 2627, Level 14, State 1, Server 7732422b7f56, Line 1
Violation of UNIQUE KEY constraint 'UQ_customers_email'. Cannot insert duplicate key in object 'seo_sqlerr_data.customers'. The duplicate key value is (ada@example.com).
The statement has been terminated.
Msg 2601, Level 14, State 1, Server 7732422b7f56, Line 1
Cannot insert duplicate key row in object 'seo_sqlerr_data.customers' with unique index 'IX_customers_name'. The duplicate key value is (Ada).
The statement has been terminated.
A table with id int IDENTITY(1,1). Ids 1 and 2 loaded with IDENTITY_INSERT ON moved the identity
to 2, and the next insert got 3. Then id 3 was deleted and the table “reset”:
DBCC CHECKIDENT ('seo_sqlerr_data.plans', RESEED, 0);
INSERT INTO seo_sqlerr_data.plans (name) VALUES (N'team');
Msg 2627, Level 14, State 1, Server 7732422b7f56, Line 1
Violation of PRIMARY KEY constraint 'PK_plans'. Cannot insert duplicate key in object 'seo_sqlerr_data.plans'. The duplicate key value is (1).
The statement has been terminated.
DBCC CHECKIDENT … NORESEED then reported current identity value '1', current column value '2'
(the failed insert used up 1). After DBCC CHECKIDENT ('seo_sqlerr_data.plans', RESEED) it reported
'2', '2', and the insert returned id 3. A table whose id defaults to NEXT VALUE FOR a sequence,
after rows 1 and 2 were inserted with explicit ids, failed the same way on (1) and worked after
ALTER SEQUENCE … RESTART WITH 3.
Two sessions running the check-then-insert at once (a two-second WAITFOR between the check and the
insert): the second got 2627 on lin@example.com. With WITH (UPDLOCK, HOLDLOCK) on the check, the
second session waited and neither failed. A second NULL in a UNIQUE column:
Msg 2627, Level 14, State 1, Server 7732422b7f56, Line 1
Violation of UNIQUE KEY constraint 'UQ_members_phone'. Cannot insert duplicate key in object 'seo_sqlerr_data.members'. The duplicate key value is (<NULL>).
The statement has been terminated.
With the filtered index instead, two NULLs went in, and a repeated phone number got 2601. Unlike
PostgreSQL, SQL Server checks uniqueness at the end of the statement, so
UPDATE seats SET seat_no = seat_no + 1 on seats 1 and 2 worked. With SET XACT_ABORT ON, a 2627
inside a transaction rolled back the rows inserted before it; without it, they were committed.
SQL Server 2019 and 2025 printed the same messages.
In Inlet
Grid edits are staged until you commit (⌘S), and Review shows what will run first, so you can spot
a key you’re about to repeat. When SQL Server refuses, Inlet shows its message with
SQL Server error 2627, state 1, severity 14 and links to this page. A table’s definition (its
CREATE TABLE with constraints and indexes) shows which columns UQ_customers_email covers. With your own Anthropic API key, Ask Claude (⌘L) can write the MERGE for your table; it
sends the schema, never rows.
Related
- The INSERT statement conflicted with the FOREIGN KEY constraint
- Cannot insert the value NULL into column; column does not allow nulls. INSERT fails.
- The INSERT statement conflicted with the CHECK constraint
- duplicate key value violates unique constraint
- ERROR 1062 (23000): Duplicate entry for key
- UNIQUE constraint failed
- E11000 duplicate key error collection
Sources
- learn.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors-2000-to-2999
- learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-checkident-transact-sql
- learn.microsoft.com/en-us/sql/t-sql/statements/set-identity-insert-transact-sql
- learn.microsoft.com/en-us/sql/t-sql/statements/create-sequence-transact-sql
- learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql
- learn.microsoft.com/en-us/sql/t-sql/statements/create-index-transact-sql
- learn.microsoft.com/en-us/sql/relational-databases/indexes/create-filtered-indexes
- learn.microsoft.com/en-us/sql/relational-databases/tables/unique-constraints-and-check-constraints