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
@@ -39,7 +39,26 @@ public class DeploymentRecordConfiguration : IEntityTypeConfiguration<Deployment
builder.HasIndex(d => d.DeploymentId).IsUnique();
builder.HasIndex(d => d.InstanceId);
builder.HasIndex(d => d.DeployedAt);
// Deployment-list sort key. Every list read orders DeployedAt DESC, Id DESC
// (DeploymentManagerRepository.QueryDeploymentListPageAsync and
// QueryDeploymentSummariesAsync) — Id is the deterministic tie-break, because
// a bulk site deploy stamps many rows in the same DeployedAt instant and a
// non-deterministic order would let a row repeat or vanish across pages.
// Declaring both key columns descending lets a forward scan feed the paged
// Skip/Take directly instead of the server sorting the whole filtered set per
// page (deferred-work register row 35).
//
// This REPLACES the old single-column HasIndex(d => d.DeployedAt): that index
// is strictly redundant here, not merely overlapping. DeployedAt leads both, so
// any predicate the old one served the composite serves too (direction does not
// affect seekability — SQL Server scans either way), and the leaf is the same
// width, since Id is the clustered PK and was therefore already sitting in the
// old index's leaf as the row locator. Keeping it would buy nothing and cost a
// second index maintained on every insert and status transition.
builder.HasIndex(d => new { d.DeployedAt, d.Id })
.IsDescending(true, true)
.HasDatabaseName("IX_DeploymentRecords_DeployedAt_Id");
}
}
@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
{
/// <summary>
/// Replaces the single-column <c>IX_DeploymentRecords_DeployedAt</c> with the
/// composite <c>IX_DeploymentRecords_DeployedAt_Id (DeployedAt DESC, Id DESC)</c>,
/// which is the deployment list's exact sort key (deferred-work register row 35).
/// </summary>
/// <remarks>
/// The list read (<c>DeploymentManagerRepository.QueryDeploymentListPageAsync</c>)
/// joins <c>Instance</c> to make <c>SiteId</c> filterable, then orders
/// <c>DeployedAt DESC, Id DESC</c> and pages with Skip/Take. With both key columns
/// declared descending, a forward scan of this index already delivers rows in the
/// requested order, so the plan can feed the Top-N page directly rather than sorting
/// the whole filtered set on every page request.
///
/// The drop is safe rather than merely tolerable: <c>DeployedAt</c> leads both
/// indexes, so every predicate the old one served the composite serves as well
/// (index direction does not affect seekability — SQL Server scans either
/// direction), and the leaf rows are the same width, because <c>Id</c> is the
/// clustered primary key and was therefore already present in the old nonclustered
/// index's leaf as the row locator. What changes is only that <c>Id</c> is now a
/// declared, ordered key column instead of an implicit locator. Keeping both would
/// add a second index to maintain on every insert and status transition for no read
/// benefit. <c>SystemArtifactDeploymentRecords</c> has its own separate
/// <c>DeployedAt</c> index and is deliberately untouched.
/// </remarks>
public partial class AddDeploymentRecordListSortIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DeploymentRecords_DeployedAt",
table: "DeploymentRecords");
migrationBuilder.CreateIndex(
name: "IX_DeploymentRecords_DeployedAt_Id",
table: "DeploymentRecords",
columns: new[] { "DeployedAt", "Id" },
descending: new bool[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DeploymentRecords_DeployedAt_Id",
table: "DeploymentRecords");
migrationBuilder.CreateIndex(
name: "IX_DeploymentRecords_DeployedAt",
table: "DeploymentRecords",
column: "DeployedAt");
}
}
}
@@ -267,13 +267,15 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
b.HasKey("Id");
b.HasIndex("DeployedAt");
b.HasIndex("DeploymentId")
.IsUnique();
b.HasIndex("InstanceId");
b.HasIndex("DeployedAt", "Id")
.IsDescending()
.HasDatabaseName("IX_DeploymentRecords_DeployedAt_Id");
b.ToTable("DeploymentRecords");
});