SQL Server · Index options
WITH (IGNORE_DUP_KEY = ON)It's not new syntax. It's an old index option that quietly drops duplicate rows on insert instead of failing the whole statement — handy for idempotency, but with one sharp performance edge worth knowing about.
Is it new?
No — it's ancient
The option predates SQL Server 2005. Only the WITH (…) wrapper around it is "2005-new".
What is it?
An index option
A property of a UNIQUE index — not a query hint, not a table feature you toggle per insert.
The catch
~18× slower on a
clustered index
On a clustered unique index with many duplicates it can be brutally slow. Nonclustered is fine.
IGNORE_DUP_KEY is an option on a UNIQUE index (or the unique index behind a PRIMARY KEY / UNIQUE constraint). It changes one thing: how the engine reacts when an INSERT would create a duplicate key.
You set it when you create the index, not at insert time:
-- modern form (SQL Server 2005+) CREATE UNIQUE INDEX IX_Order_NaturalKey ON dbo.[Order] (CustomerId, ExternalRef) WITH (IGNORE_DUP_KEY = ON); -- on a constraint, same idea ALTER TABLE dbo.[Order] ADD CONSTRAINT UQ_Order UNIQUE (CustomerId, ExternalRef) WITH (IGNORE_DUP_KEY = ON);
WHERE NOT EXISTS or catching error 2627. That's the genuinely useful part. The sharp edge is below.
The behaviour itself is one of the oldest knobs in the product — it shipped in the early SQL Server / Sybase lineage and was already standard by SQL Server 2000. What changed over the years is only the syntax and a 2017 performance escape hatch.
WITH IGNORE_DUP_KEY, and it couldn't be applied to a primary key.
WITH (option = ON|OFF) syntax arrives
SQL Server 2005 introduced the parenthesised index-options grammar — this is where WITH (IGNORE_DUP_KEY = ON) comes from. The old bare WITH IGNORE_DUP_KEY still works and means the same as = ON.
SUPPRESS_MESSAGES = ON added
SQL Server 2017 added a sub-option that also fixes the clustered-index performance trap (see §4). Lets you skip the per-dupe warning chatter and force the fast plan.
So if a plan or teammate is presenting WITH (IGNORE_DUP_KEY = ON) as a modern trick, the honest framing is: old feature, two-decade-old syntax. Nothing to wait for or worry about on version grounds.
ignore_dup_key column in sys.indexes.
This is the one genuinely surprising thing about the option. Where the unique index lives changes performance by more than an order of magnitude when duplicates are common. Paul White's benchmark — 1,000,000 rows, only 1,000 distinct values (so ~999k dupes) — makes it vivid:
Lower is faster. The clustered case is ~18× the nonclustered case here — and ~92% of its runtime is raising and catching internal exceptions, one per discarded duplicate.
| Aspect | Nonclustered (fast) | Clustered (slow) |
|---|---|---|
| Who handles it | The query processor, up front in the plan | The storage engine, row by row |
| How dupes are removed | A merge semi-join + Assert + Segment/Top eliminate dupes before any insert is attempted | Each row navigates the b-tree, takes latches/locks, builds the row & log record, then hits the dupe and throws |
| Cost driver | One set-based pass; no exceptions | An exception raised & caught per duplicate — the dominant cost |
The asymmetry is architectural: the clustered index is the table, and it's updated before the nonclustered indexes. By the time a nonclustered uniqueness check would fire, the base row already exists — so SQL Server moves that dedup work to the query processor for nonclustered, but the clustered path is stuck doing it the expensive way.
If you're on SQL Server 2017 or later and you do want it on a clustered index, add SUPPRESS_MESSAGES = ON. It silences the per-dupe warnings and makes the optimizer use the fast query-processor plan even for the clustered case:
CREATE UNIQUE CLUSTERED INDEX CIX_Order ON dbo.[Order] (CustomerId, ExternalRef) WITH (IGNORE_DUP_KEY = ON (SUPPRESS_MESSAGES = ON));
And the honest comparison — when duplicates are the norm, plain set-based dedup often beats IGNORE_DUP_KEY on a clustered index anyway. In the same benchmark, a SELECT DISTINCT before the insert ran in ~400 ms vs the 15,900 ms clustered case.
SUPPRESS_MESSAGES = ON for a clustered key.INSERT … SELECT DISTINCT or WHERE NOT EXISTS / MERGE.