Repository navigation
sp_IndexCleanup: keeps a strict unique index over one with IGNORE_DUP_KEY = ON, so duplicate inserts start to fail #916
Description
Two unique indexes or unique constraints can have the same keys, with IGNORE_DUP_KEY = ON on only one of them. In that case, sp_IndexCleanup does not look at IGNORE_DUP_KEY when it picks the one to keep. Rules 2 and 7.5b pick by index_priority, then by name. Rule 7 keeps the nonclustered index and drops the constraint.
If the generated scripts remove the IGNORE_DUP_KEY structure, an INSERT of a duplicate key behaves differently afterwards. Before the scripts run, SQL Server skips the duplicate row with a warning. After the scripts run, the INSERT fails.
What SQL Server does when both are present
I measured this on SQL Server 2022. When a strict structure and an IGNORE_DUP_KEY structure share their keys, the IGNORE_DUP_KEY one decides the outcome. SQL Server skips the duplicate row, and the strict structure raises no error. I tested 16 combinations:
- a strict constraint with an IGNORE_DUP_KEY index, and the reverse
- two unique indexes, created in both orders
- a single-row insert, a 500-row insert, and 500-row and 50,000-row inserts with a forced wide plan (
QUERYTRACEON 8790)
All 16 skipped the duplicates with no error. So if sp_IndexCleanup keeps the IGNORE_DUP_KEY structure, nothing changes. If it keeps the strict one, inserts of duplicate keys start to fail.
What sp_IndexCleanup does
Each test table has the two structures on one key column, k. I ran each procedure with @dedupe_only = 1.
| Table | Structures | Rule | Kept | Removed by the script | Duplicate insert after the scripts run |
|---|---|---|---|---|---|
| t21 | strict constraint, IGNORE_DUP_KEY index | 7 | the index | the constraint | skipped, as before |
| t22 | IGNORE_DUP_KEY constraint, strict index | 7 | the index | the constraint | fails with error 2601 |
| t23 | strict ux23_a, IGNORE_DUP_KEY ux23_b |
2 | ux23_a |
ux23_b |
fails with error 2601 |
| t24 | IGNORE_DUP_KEY ux24_a, strict ux24_b |
2 | ux24_a |
ux24_b |
skipped, as before |
| t25 | strict uq25_a, IGNORE_DUP_KEY uq25_b |
7.5b | uq25_a |
uq25_b |
fails with error 2627 |
| t26 | IGNORE_DUP_KEY uq26_a, strict uq26_b |
7.5b | uq26_a |
uq26_b |
skipped, as before |
I ran the t22, t23 and t25 scripts inside a transaction and rolled it back. Before the scripts, a duplicate insert was skipped. After them, it failed with the error in the table. In t23 to t26, both structures get the same score, so the names decide the result.
The results are the same on dev before #915, with #915, and with #914. Rule 5 (same keys, different includes) picks its winner the same way. I did not test Rule 5.
To reproduce the Rule 2 case:
CREATE TABLE dbo.t23 (id integer NOT NULL CONSTRAINT pk_t23 PRIMARY KEY, k integer NOT NULL, x integer NULL);
CREATE UNIQUE INDEX ux23_a ON dbo.t23 (k);
CREATE UNIQUE INDEX ux23_b ON dbo.t23 (k) WITH (IGNORE_DUP_KEY = ON);sp_IndexCleanup returns ALTER INDEX [ux23_b] ON [...].[dbo].[t23] DISABLE; for this table.
The same decision as #917
In #913, stroexd suggested that the index that backs a foreign key win on purpose, not by alphabetical luck, "probably as a term in index_priority". stroexd wrote it up as #917. This issue needs the same lever. In both cases, the score does not see a property that decides which structure has to stay.
The two terms also interact. Take a strict unique index that backs a foreign key, and an IGNORE_DUP_KEY duplicate that backs nothing. If either one is removed, something changes: the foreign key is disabled, or duplicate inserts start to fail. One result removes the duplicate and changes nothing: keep the index that backs the key, and run ALTER INDEX ... SET (IGNORE_DUP_KEY = ON) on it.
I tested this on SQL Server 2022. The foreign key stayed enabled and trusted, a duplicate insert was skipped, and an orphan insert failed with error 547. We recommend one change for both terms, because the order of the two terms decides this case.
Two ways to fix it
- Make the IGNORE_DUP_KEY structure win. Add a term to
index_prioritythat outranks the usage and include terms. Then Rules 2, 5 and 7.5b keep the IGNORE_DUP_KEY structure. If Rule 7 drops a constraint that has IGNORE_DUP_KEY = ON, rebuild the replacement index with IGNORE_DUP_KEY = ON. sp_IndexCleanup: carry index options through rebuilds and skip no-op merges #914 already does this when the replacement index is not unique yet (MAKE UNIQUE). sp_IndexCleanup: carry index options through rebuilds and skip no-op merges #914 writes no script for a replacement index that is already unique. In that case, the rebuild adds no column and no uniqueness. - Do not pair two unique structures whose IGNORE_DUP_KEY settings differ. Keep both, and report nothing for the pair.
Option 1 keeps the cleanup and the insert behavior. Option 2 changes less code, but it leaves a redundant structure in place.
Activity
sp_IndexCleanup: score the foreign key and IGNORE_DUP_KEY properties (#…
Merge pull request #920 from stroexd/fix/indexcleanup-917-916-priorit…
erikdarlingdata commentedon Oct 14, 2026
Fixed by #920, merged to dev in b81754f. The IGNORE_DUP_KEY structure now wins its dedupe pair on an index_priority term, and a keeper that outranks it (a foreign key's backing index, or the Rule 7 replacement) takes the option over, in its rebuild or through a new IGNORE_DUP_KEY SCRIPT row ordered before the DISABLE.