Skip to content

Use CREATE INDEX with DROP_EXISTING=ON for index facet changes (SQL Server)#38271

Open
m-x-shokhzod wants to merge 1 commit into
dotnet:mainfrom
m-x-shokhzod:perf/sqlserver-drop-existing-index
Open

Use CREATE INDEX with DROP_EXISTING=ON for index facet changes (SQL Server)#38271
m-x-shokhzod wants to merge 1 commit into
dotnet:mainfrom
m-x-shokhzod:perf/sqlserver-drop-existing-index

Conversation

@m-x-shokhzod
Copy link
Copy Markdown
Contributor

When an index facet changes (fill factor, sort order, uniqueness, filter, columns), MigrationsModelDiffer produces a DropIndexOperation + CreateIndexOperation pair, which the SQL Server generator emits as two statements:

DROP INDEX [IX_People_X] ON [People];
CREATE UNIQUE INDEX [IX_People_X] ON [People] ([X]);

This is suboptimal: during the drop, queries can't use the index → potentially full table scans, and on large tables the gap can be significant.

SQL Server supports CREATE INDEX ... WITH (DROP_EXISTING = ON) since SQL Server 2008, which:

  • Atomically replaces the old index in a single statement
  • Lets queries continue using the old index while the new one is being built — no un-indexed gap

After

CREATE UNIQUE INDEX [IX_People_X] ON [People] ([X]) WITH (DROP_EXISTING = ON);

Implementation

The rewrite lives entirely in the SQL Server provider:

  • New internal annotation SqlServerAnnotationNames.UseDropExisting
  • New helper SqlServerMigrationsSqlGenerator.RewriteDropAndCreateIndexAsDropExisting (called from RewriteOperations, alongside the existing FixLegacyTemporalAnnotations) — detects Drop+Create pairs for the same (Name, Table, Schema), removes the drop from the operation list, and marks the create with UseDropExisting = true
  • IndexOptions reads the annotation and adds DROP_EXISTING = ON to the WITH (...) clause alongside FILLFACTOR, ONLINE, SORT_IN_TEMPDB, DATA_COMPRESSION

Limited to standard indexes — memory-optimized, full-text, and vector indexes use different syntax/restrictions and fall back to the existing drop+create path.

MigrationsModelDiffer is unchanged; the differ still emits Drop+Create as before. Other providers (MySQL, PostgreSQL, SQLite, Cosmos) are unaffected.

Tests

Test Type Verifies
Alter_index_make_unique functional (override) Existing test — SQL expectation updated to single-statement form
Alter_index_change_sort_order functional (override) Existing test — SQL expectation updated
Alter_index_fill_factor_uses_drop_existing functional (new) FILLFACTOR change collapses to WITH (FILLFACTOR = 90, DROP_EXISTING = ON)
Alter_index_filter_uses_drop_existing functional (new) Filter change collapses to WITH (DROP_EXISTING = ON), filter rendered after WHERE

All 4 tests pass end-to-end against a real SQL Server instance (verified locally against Azure SQL Edge on Apple Silicon ARM64; the CI matrix will additionally verify against SqlServer 2019/2022/2025).

Full regression sweep: EFCore.SqlServer.Tests 1335/1335, SqlServerMigrationsSqlGeneratorTest 124/124, MigrationsSqlServerTest index tests 34/34.

Fixes #35067

…erver)

When an index facet changes (fill factor, sort order, uniqueness, filter,
columns), the migration model differ produces a DropIndexOperation +
CreateIndexOperation pair. On SQL Server this can be collapsed into a single
`CREATE INDEX ... WITH (DROP_EXISTING = ON)`, which:

- Lets queries continue using the old index while the new one is being built
  (no un-indexed gap during a long rebuild on large tables)
- Atomically replaces the old index in a single statement

The rewrite lives in SqlServerMigrationsSqlGenerator.RewriteOperations
(new helper RewriteDropAndCreateIndexAsDropExisting). The matching
DropIndexOperation is removed from the operation list and the
CreateIndexOperation is marked with a new internal annotation
SqlServerAnnotationNames.UseDropExisting, which IndexOptions reads to
emit DROP_EXISTING = ON in the WITH clause.

Limited to standard indexes — memory-optimized, full-text, and vector
indexes use different syntax/restrictions and fall back to the existing
drop+create path.

MigrationsModelDiffer is unchanged; the differ still emits Drop+Create as
before. Other providers are unaffected.

Fixes dotnet#35067
@m-x-shokhzod m-x-shokhzod requested a review from a team as a code owner May 13, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL Server: instead of dropping/creating indexes, use CREATE INDEX with DROP EXISTING=ON

1 participant