perf(site-call-audit): filtered IX_SiteCalls_NonTerminal index — KPI predicates seek the live queue instead of scanning 365 days
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM [__EFMigrationsHistory]
|
||||||
|
WHERE [MigrationId] = N'20260709110614_AddSiteCallsNonTerminalIndex'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
EXEC(N'CREATE INDEX [IX_SiteCalls_NonTerminal] ON [SiteCalls] ([CreatedAtUtc]) INCLUDE ([SourceSite], [SourceNode], [Status]) WHERE [TerminalAtUtc] IS NULL');
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT * FROM [__EFMigrationsHistory]
|
||||||
|
WHERE [MigrationId] = N'20260709110614_AddSiteCallsNonTerminalIndex'
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
|
||||||
|
VALUES (N'20260709110614_AddSiteCallsNonTerminalIndex', N'10.0.7');
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
GO
|
||||||
|
|
||||||
+14
-2
@@ -66,8 +66,9 @@ public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration<SiteCall
|
|||||||
// SourceNode (SourceNode-stamping): node-local identifier of the
|
// SourceNode (SourceNode-stamping): node-local identifier of the
|
||||||
// cluster member that produced the call (e.g. "node-a", "central-a"). NULL is
|
// cluster member that produced the call (e.g. "node-a", "central-a"). NULL is
|
||||||
// valid for rows that pre-date this feature and for reconciled rows from a
|
// valid for rows that pre-date this feature and for reconciled rows from a
|
||||||
// retired node. ASCII — varchar(64). No index — Site Call Audit KPIs are
|
// retired node. ASCII — varchar(64). Not a key column of any index, but it rides
|
||||||
// per-site, not per-node, on this table.
|
// as an INCLUDE column on IX_SiteCalls_NonTerminal below so the per-node KPI
|
||||||
|
// breakdown reads it covered off the filtered live-queue index.
|
||||||
builder.Property(s => s.SourceNode)
|
builder.Property(s => s.SourceNode)
|
||||||
.HasColumnType("varchar(64)")
|
.HasColumnType("varchar(64)")
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
@@ -85,5 +86,16 @@ public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration<SiteCall
|
|||||||
builder.HasIndex(s => new { s.Status, s.UpdatedAtUtc })
|
builder.HasIndex(s => new { s.Status, s.UpdatedAtUtc })
|
||||||
.IsDescending(false, true)
|
.IsDescending(false, true)
|
||||||
.HasDatabaseName("IX_SiteCalls_Status_Updated");
|
.HasDatabaseName("IX_SiteCalls_Status_Updated");
|
||||||
|
|
||||||
|
// NonTerminal backs every "live queue" KPI predicate (TerminalAtUtc IS NULL):
|
||||||
|
// buffered/stuck/oldest counts run every 60 s from the KPI recorder plus every
|
||||||
|
// 10 s Health-dashboard poll. Filtered: the non-terminal population is the live
|
||||||
|
// queue, not the 365-day archive — each count becomes a small seek instead of a
|
||||||
|
// clustered scan (arch-review 04, P1). SourceSite/SourceNode/Status ride as
|
||||||
|
// INCLUDE columns so the per-site + per-node KPI breakdowns stay covered.
|
||||||
|
builder.HasIndex(s => s.CreatedAtUtc)
|
||||||
|
.HasDatabaseName("IX_SiteCalls_NonTerminal")
|
||||||
|
.HasFilter("[TerminalAtUtc] IS NULL")
|
||||||
|
.IncludeProperties(nameof(SiteCall.SourceSite), nameof(SiteCall.SourceNode), nameof(SiteCall.Status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2015
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSiteCallsNonTerminalIndex : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SiteCalls_NonTerminal",
|
||||||
|
table: "SiteCalls",
|
||||||
|
column: "CreatedAtUtc",
|
||||||
|
filter: "[TerminalAtUtc] IS NULL")
|
||||||
|
.Annotation("SqlServer:Include", new[] { "SourceSite", "SourceNode", "Status" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_SiteCalls_NonTerminal",
|
||||||
|
table: "SiteCalls");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -161,6 +161,12 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations
|
|||||||
|
|
||||||
b.HasKey("TrackedOperationId");
|
b.HasKey("TrackedOperationId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAtUtc")
|
||||||
|
.HasDatabaseName("IX_SiteCalls_NonTerminal")
|
||||||
|
.HasFilter("[TerminalAtUtc] IS NULL");
|
||||||
|
|
||||||
|
SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("CreatedAtUtc"), new[] { "SourceSite", "SourceNode", "Status" });
|
||||||
|
|
||||||
b.HasIndex("SourceSite", "CreatedAtUtc")
|
b.HasIndex("SourceSite", "CreatedAtUtc")
|
||||||
.IsDescending(false, true)
|
.IsDescending(false, true)
|
||||||
.HasDatabaseName("IX_SiteCalls_Source_Created");
|
.HasDatabaseName("IX_SiteCalls_Source_Created");
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
@@ -70,6 +73,27 @@ public class SchemaConfigurationTests : IDisposable
|
|||||||
|
|
||||||
Assert.IsType<EncryptedStringConverter>(converter);
|
Assert.IsType<EncryptedStringConverter>(converter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arch-review 04 (P1): the SiteCalls "live queue" KPI predicates all filter on
|
||||||
|
// TerminalAtUtc IS NULL and run every 60 s (KPI recorder) + every 10 s (Health poll).
|
||||||
|
// A filtered index over just the non-terminal rows turns each count from a clustered
|
||||||
|
// scan of the 365-day archive into a small seek. Locked shape:
|
||||||
|
// key = CreatedAtUtc, filter = [TerminalAtUtc] IS NULL, INCLUDE SourceSite/SourceNode/Status.
|
||||||
|
[Fact]
|
||||||
|
public void SiteCalls_Has_Filtered_NonTerminal_Index()
|
||||||
|
{
|
||||||
|
// IncludeProperties is a SQL-Server-only annotation stripped from the runtime
|
||||||
|
// read-optimized model, so read the design-time model where it is preserved.
|
||||||
|
var model = _context.GetService<IDesignTimeModel>().Model;
|
||||||
|
var entity = model.FindEntityType(typeof(SiteCall))!;
|
||||||
|
var index = entity.GetIndexes().Single(i => i.GetDatabaseName() == "IX_SiteCalls_NonTerminal");
|
||||||
|
|
||||||
|
Assert.Equal(new[] { nameof(SiteCall.CreatedAtUtc) }, index.Properties.Select(p => p.Name));
|
||||||
|
Assert.Equal("[TerminalAtUtc] IS NULL", index.GetFilter());
|
||||||
|
Assert.Equal(
|
||||||
|
new[] { nameof(SiteCall.SourceSite), nameof(SiteCall.SourceNode), nameof(SiteCall.Status) },
|
||||||
|
index.GetIncludeProperties());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SplitQueryBehaviourTests : IDisposable
|
public class SplitQueryBehaviourTests : IDisposable
|
||||||
|
|||||||
Reference in New Issue
Block a user