perf(configdb): index DeploymentRecords on the deployment-list sort key (register row 35)

The deployment list orders `DeployedAt DESC, Id DESC` and joins `Instance` to
make SiteId filterable, but the only DeployedAt index was single-column and
ascending, so the server sorted the filtered set on every page request.

Adds `IX_DeploymentRecords_DeployedAt_Id (DeployedAt DESC, Id DESC)` — the exact
sort key, both columns descending, so a forward scan feeds the paged Skip/Take
directly. Id is in the key as the deterministic tie-break: a bulk site deploy
stamps many rows in the same DeployedAt instant, and without it a row can repeat
or vanish across page boundaries. Three reads share the key
(QueryDeploymentListPageAsync, QueryDeploymentSummariesAsync, and the single-row
GetCurrentDeploymentStatusAsync).

DROPS the old `IX_DeploymentRecords_DeployedAt` rather than keeping both. It is
strictly redundant, not merely overlapping: DeployedAt leads both, so every
predicate it served the composite serves too (direction does not affect
seekability — SQL Server scans either way), and the leaf rows are the same width,
because Id is the clustered PK and was therefore already in the old nonclustered
index's leaf as the row locator. All that changes is Id becoming a declared,
ordered key column. Keeping it would maintain a second index on every deployment
insert and status transition for no read benefit.

Honest caveat: because the clustering key is implicitly ordered within a
nonclustered index, the optimizer may already have been able to serve this
ordering by scanning the old index backward. The explicit composite makes it
deterministic and self-documenting rather than dependent on that inference, and
costs nothing to declare.

Migration 20260815165452_AddDeploymentRecordListSortIndex (scaffolded with a real
build, non-empty, snapshot updated) plus the hand-written production script under
docs/plans/sql/ following the AddAuditLogAndNotificationCoveringIndexes
precedent. SystemArtifactDeploymentRecords has its own separate DeployedAt index
and is deliberately untouched.
This commit is contained in:
Joseph Doherty
2026-08-15 13:22:29 -04:00
parent 0db253b55f
commit be29bf471e
6 changed files with 2264 additions and 4 deletions
@@ -0,0 +1,56 @@
-- Register row 35: the deployment-list sort key gets its own index.
--
-- Replaces the single-column [IX_DeploymentRecords_DeployedAt] with the composite
-- [IX_DeploymentRecords_DeployedAt_Id] ([DeployedAt] DESC, [Id] DESC), which is exactly
-- the order DeploymentManagerRepository.QueryDeploymentListPageAsync (and
-- QueryDeploymentSummariesAsync) asks for. With both key columns descending, a forward
-- scan delivers the requested order, so a paged Skip/Take reads a Top-N off the index
-- instead of sorting the whole filtered set on every page request.
--
-- The DROP is safe rather than merely tolerable. [DeployedAt] leads both indexes, so any
-- predicate the old one served the composite serves too (direction does not affect
-- seekability -- SQL Server can scan either way), and the leaf rows are the same width:
-- [Id] is the clustered primary key and was therefore already in the old nonclustered
-- index's leaf as the row locator. All that changes is that [Id] becomes a declared,
-- ordered key column. Keeping both would maintain a second index on every deployment
-- insert and status transition for no read benefit.
--
-- Note [SystemArtifactDeploymentRecords] is a DIFFERENT table with its own
-- [DeployedAt] index; it is deliberately untouched.
--
-- DeploymentRecords is small enough that a plain (offline) index rebuild is fine. On an
-- Enterprise edition instance where the deployment history is large and the Central UI
-- must stay responsive, add WITH (ONLINE = ON) to the CREATE INDEX below.
--
-- Run with QUOTED_IDENTIFIER ON. Several tables in this database carry filtered indexes
-- and persisted computed columns, and SQL Server refuses DDL/DML against them with error
-- 1934 when the session has QUOTED_IDENTIFIER OFF -- which is exactly what `sqlcmd` gives
-- you by default (pass -I, or keep this header). The SETs sit in their own batch so they
-- are in force when the statements after the GO parse.
SET QUOTED_IDENTIFIER ON;
SET ANSI_NULLS ON;
GO
BEGIN TRANSACTION;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260815165452_AddDeploymentRecordListSortIndex'
)
BEGIN
EXEC(N'DROP INDEX [IX_DeploymentRecords_DeployedAt] ON [DeploymentRecords]');
EXEC(N'CREATE INDEX [IX_DeploymentRecords_DeployedAt_Id]
ON [DeploymentRecords] ([DeployedAt] DESC, [Id] DESC)');
END;
IF NOT EXISTS (
SELECT * FROM [__EFMigrationsHistory]
WHERE [MigrationId] = N'20260815165452_AddDeploymentRecordListSortIndex'
)
BEGIN
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260815165452_AddDeploymentRecordListSortIndex', N'10.0.7');
END;
COMMIT;
GO