Index Tuning Report – RockyPC / SQLStorm – 2026-09-20 15:34:25 UTC

Tuning goal: Index Tuning

Server: RockyPC Database: SQLStorm Engine: SQL Server 2022 Enterprise Engine, Developer Edition Build: 16.0.1200.5 RTM-GDR

Executive summary

Immediate action: Create five workload-focused nonclustered indexes, beginning with PostHistory, Votes, and Posts; then update statistics and re-trust foreign keys only where validation finds no violating rows.
16.3 GBApproximate current clustered-index storage reported for the principal tables.
5Recommended nonclustered indexes, estimated combined size about 78.8–86.8 MB.
0Duplicate or left-prefix overlapping indexes detected.
17Untrusted foreign keys; no disabled constraints reported.
ONFORCE_LAST_GOOD_PLAN desired and actual state.

Top priorities

  1. PostHistory access path: add (PostId, CreationDate) with narrow analytical includes. The table has 847,593 rows, 28 missing-index seeks, a 99.56% estimated impact, and repeated windowing by PostId/CreationDate.
  2. Votes aggregation paths: add separate indexes for PostId and UserId. Votes has 926,084 rows, 216 reported PostId seeks/scans, 72 UserId seeks/scans, and query patterns aggregate by both relationships.
  3. Posts join and ranking path: add (OwnerUserId, CreationDate) including PostTypeId, Score, ViewCount. This is not in the missing-index report, but it directly supports frequent Query Store and plan-cache joins and window functions; the table has only four reported updates.
  4. Badges consolidation: add one (UserId) index including Class, Name, covering three overlapping missing-index requests without creating three separate indexes. Badges has 439,352 rows and no reported writes.
  5. Statistics and constraints: enable or compensate for disabled automatic statistics, update affected statistics, and conditionally re-trust all untrusted foreign keys. Untrusted relationships prevent join elimination and some predicate simplification.

Confidence: high for PostHistory, Votes, Badges, and statistics actions; medium-high for the Posts index because nonclustered usage counters and exact predicate selectivity were not supplied. Validate each change against representative plans before retaining it.

Scope and evidence

The analysis covers user database SQLStorm only. No system-database changes are recommended. The supplied workload contains very expensive analytical CTEs, repeated joins from Users to Posts, Votes, Comments, and Badges, and frequent aggregation or ranking by relationship and date columns.

EvidenceFindingImplication
Missing-index impactPostHistory 99.56%, Votes UserId 99.59%, Votes PostId 64.03%, Comments PostId 28.64%.Prioritize selective relationship indexes; defer low-impact alternatives.
Writes versus readsVotes and Comments report only 4 index updates, while reads are 72/216 and 360 respectively. Posts reports 4 updates and extensive query usage.Read benefit materially outweighs observed write maintenance for the recommended indexes.
Table sizesBadges 439,352; PostHistory 847,593; Votes 926,084; Comments 351,440; Posts 246,672; Users 267,193 rows.These tables are large enough for selective indexing; no table is under the approximately 1,000-row avoidance threshold.
Column cardinalityBadges.UserId is high cardinality; Votes.PostId is high cardinality; Users.Reputation has only about 1,755 distinct values; PostHistory.PostHistoryTypeId has about 29.Favor relationship keys and dates. Do not lead with low-cardinality type/class columns.
ColumnstoreNo large tables were reported by the supplied large-table analysis; the largest reported table is below 1,000,000 rows.Do not add clustered or nonclustered columnstore indexes in this run.

Detailed prioritized recommendations

1. Add a consolidated Badges user lookup index High confidence

Create IX_Badges_UserId_Class_Name on dbo.Badges(UserId) including Class, Name. It covers the three UserId candidates: UserId alone, UserId plus Class, and UserId plus Name. UserId has approximately 154,016 distinct values among 439,352 rows, whereas Class has only about two distinct values and should remain an included column.

Usage justification48 + 12 + 18 missing-index seeks/scans across UserId candidates; no reported writes to the Badges clustered index.
Estimated size10.1 MB reported candidate estimate for UserId plus Class or Name; the consolidated UserId plus Class plus Name index is estimated at 12–16 MB, 439,352 rows. Assumption: 4-byte key, approximately 13–25 bytes of included payload plus row/page overhead, PAGE compression, fill factor 95.
Expected benefitFewer clustered scans and lookups for user badge counts and class summaries in Query Store statements.
RiskLow write risk based on zero reported writes; medium storage uncertainty because column widths were not supplied.

2. Add a covering PostHistory chronological access path High confidence

Create IX_PostHistory_PostId_CreationDate on dbo.PostHistory(PostId, CreationDate) including PostHistoryTypeId, UserId, Comment. This supports the dominant PostId lookup and recent-history window functions. It also subsumes a narrower PostId-leading index if one is later created; no such existing index was reported.

Usage justification28 missing-index operations with 99.56% impact; repeated Query Store and plan-cache statements use PostId, CreationDate, PostHistoryTypeId, UserId, and Comment.
Estimated size38.8 MB reported candidate estimate for PostId plus CreationDate with UserId and Comment; adding PostHistoryTypeId is expected to keep the range about 38.8–45.3 MB, 847,593 rows. Assumption: PAGE compression, fill factor 95, and the reported candidate estimates are used without recomputation.
Expected benefitSeekable recent-history retrieval, lower sort and scan work, and fewer spills in queries using ROW_NUMBER() OVER (PARTITION BY PostId ORDER BY CreationDate DESC).
RiskMedium storage and write-maintenance risk because Comment can be wide, although no PostHistory writes were reported in the supplied counters.

3. Add Votes indexes for PostId and UserId aggregations High confidence

Create IX_Votes_PostId_VoteTypeId and IX_Votes_UserId_VoteTypeId_BountyAmount. Separate leading keys are necessary because a PostId-leading index cannot efficiently seek UserId predicates and vice versa. Do not lead with low-cardinality VoteTypeId.

IndexEvidenceEstimated size and assumptions
IX_Votes_PostId_VoteTypeId216 operations, 64.03% impact, and repeated PostId grouping and VoteTypeId conditional aggregation.21.2 MB reported estimate; 926,084 rows. PAGE compression, fill factor 95.
IX_Votes_UserId_VoteTypeId_BountyAmount76 UserId operations across two candidates, 97.68–99.59% impact, and frequent UserId bounty/vote aggregation. Only 4 clustered-index updates were reported.28.3 MB reported estimate for the wider candidate; 926,084 rows. PAGE compression, fill factor 95.

Expected benefit: lower scans and aggregation cost in Query Store statements such as the UserVoteStats and UserActivity CTEs. Risk: medium because two additional indexes add write and storage overhead; retain both only if post-change read reduction is demonstrated.

4. Add Posts owner/date index for frequent joins and ranking Medium-high confidence

Create IX_Posts_OwnerUserId_CreationDate on dbo.Posts(OwnerUserId, CreationDate) including PostTypeId, Score, ViewCount. This is intentionally recommended despite not appearing in the missing-index list because plan-cache and Query Store statements repeatedly join Posts by OwnerUserId and rank or filter by CreationDate. The clustered index reports only four updates and 3,220 scans.

Estimated size3.8–8.5 MB, 246,672 rows. The supplied OwnerUserId candidate estimate is 3.8 MB; the range allows for CreationDate and the three included columns. Assumption: PAGE compression, fill factor 95.
Expected benefitFaster user-post aggregation and recent-post ranking in queries with WHERE p.CreationDate > ... and PARTITION BY p.OwnerUserId.
RiskMedium: the index is inferred from workload patterns rather than a direct missing-index recommendation. Verify that it replaces scans rather than merely adding another scan option.

5. Defer the Users.Reputation and Comments.PostId alternatives until measured Conditional

Users.Reputation is used frequently for ranking and filtering, but its approximately 1,755 distinct values and missing-index impact of only 8% make a permanent index less certain. The supplied candidate is 4.1 MB for 267,193 rows. Consider a filtered index on Reputation IS NOT NULL only if actual plans show a selective range seek and the table is not scanned for the ranking.

Comments(PostId) has 360 reported operations but only 28.64% impact and a 5.4 MB candidate estimate. It is a reasonable second-wave index if post-change plans still scan Comments. Do not add both conditional indexes preemptively.

6. Do not drop or rebuild existing indexes now High confidence

  • No duplicate or left-prefix overlapping indexes were detected.
  • All reported primary keys enforce constraints and must not be dropped without replacement.
  • Page density is healthy: all reported indexes are above 93.65%, well above the 75% threshold.
  • Posts fragmentation is 11.52% across 3,316 pages, below a compelling rebuild threshold for this scope; page density is still 93.65%.
  • Comments, PostHistory, Users, Votes, and Badges show low fragmentation and high density. Rebuilds would add cost without a demonstrated benefit.

Workload and plan analysis

The plan cache shows severe analytical workload cost. Examples include query hash 8E0DB20F8C836D8C with average CPU time about 1,139,186,444 and 13,117,331 logical reads, query hash 563E43D720E73157 with about 7,553,111 reads and 725,895 spills, and query hash 82536F6F37257B12 with about 11,400,594 reads and 4,068 spills.

The recommended indexes directly support the repeated operators behind these statements: joins on Votes.UserId, Votes.PostId, Badges.UserId, Posts.OwnerUserId, and recent PostHistory rows. Indexes will not fix row multiplication caused by joining several one-to-many tables before aggregation. Rewrite those queries to aggregate each child table separately before joining to Users or Posts, and replace repeated LIKE '%tag%' predicates with a normalized post-tag bridge table or full-text/search design.

Before/after expectation: current plans frequently perform clustered scans, large hash joins, sorts, and spills. After indexing, the expected shape is selective nonclustered seeks or ordered scans into child aggregates, followed by smaller hash or stream aggregates. Exact costs and row counts must be confirmed from actual plans because the supplied plan text is truncated and Query Store metrics contain inconsistent units.

Query Store reports one plan per listed workload item and no evidence of parameter sensitivity variation: distinct parameter sets are one with zero standard deviation in the supplied samples. The workload is therefore more consistent with broad scans, join multiplication, missing statistics, and query shape than with parameter-sensitive plan instability.

Constraints, statistics, and automatic tuning

Untrusted foreign keys

All 17 reported foreign keys are enabled but untrusted. An untrusted foreign key can block optimizer join elimination and reduce confidence in relationship-based simplification. Re-trust only after validating existing data. The implementation script below tests each relationship and executes WITH CHECK CHECK CONSTRAINT only when no violating rows exist; otherwise it prints a skipped message. Validation scans can be expensive on the larger tables.

Statistics configuration

AUTO_CREATE_STATISTICS and AUTO_UPDATE_STATISTICS are both OFF. This is a significant risk for the supplied analytical workload, especially after adding indexes. Enable both options if operational policy permits, then update statistics on affected tables. If policy requires them to remain OFF, schedule equivalent full-scan or sampled updates after data loads and index changes.

Automatic plan correction

FORCE_LAST_GOOD_PLAN is desired ON and actual ON, so no corrective configuration action is needed. No Query Store forced-plan failures were detected; therefore there are no affected query_id/plan_id pairs, failure counts, timestamps, or failure reasons to classify. Automatic tuning should remain enabled while index changes are staged.

Maintenance, compression, and storage impact

ChangeBeforeAfterNet change
Badges consolidated index0 MB new index12–16 MB+12–16 MB
PostHistory covering index0 MB new index38.8–45.3 MB+38.8–45.3 MB
Votes PostId index0 MB new index21.2 MB+21.2 MB
Votes UserId index0 MB new index28.3 MB+28.3 MB
Posts OwnerUserId/date index0 MB new index3.8–8.5 MB+3.8–8.5 MB
Total recommended set—104.1–119.3 MB+104.1–119.3 MB

The total range uses the supplied candidate estimates where available and a conservative range where included-column widths were not supplied. The earlier five-index estimate excluding the wider Badges and PostHistory uncertainty is approximately 78.8–86.8 MB; the complete implementation estimate is 104.1–119.3 MB. No dropped index reclaims storage in this run. Existing clustered indexes use PAGE compression except the small lookup tables; recommended indexes use PAGE compression and fill factor 95. Reassess fill factor only if page-split telemetry increases.

Scripts

Create the consolidated Badges index

USE [SQLStorm];
GO
IF NOT EXISTS
(
    SELECT 1
    FROM sys.indexes
    WHERE object_id = OBJECT_ID(N'dbo.Badges')
      AND name = N'IX_Badges_UserId_Class_Name'
)
BEGIN
    CREATE NONCLUSTERED INDEX [IX_Badges_UserId_Class_Name]
    ON [dbo].[Badges] ([UserId])
    INCLUDE ([Class], [Name])
    WITH (SORT_IN_TEMPDB = ON, ONLINE = ON, DATA_COMPRESSION = PAGE, FILLFACTOR = 95);
END;
GO

Create the PostHistory chronological covering index

USE [SQLStorm];
GO
IF NOT EXISTS
(
    SELECT 1
    FROM sys.indexes
    WHERE object_id = OBJECT_ID(N'dbo.PostHistory')
      AND name = N'IX_PostHistory_PostId_CreationDate'
)
BEGIN
    CREATE NONCLUSTERED INDEX [IX_PostHistory_PostId_CreationDate]
    ON [dbo].[PostHistory] ([PostId], [CreationDate])
    INCLUDE ([PostHistoryTypeId], [UserId], [Comment])
    WITH (SORT_IN_TEMPDB = ON, ONLINE = ON, DATA_COMPRESSION = PAGE, FILLFACTOR = 95);
END;
GO

Create the Votes PostId aggregation index

USE [SQLStorm];
GO
IF NOT EXISTS
(
    SELECT 1
    FROM sys.indexes
    WHERE object_id = OBJECT_ID(N'dbo.Votes')
      AND name = N'IX_Votes_PostId_VoteTypeId'
)
BEGIN
    CREATE NONCLUSTERED INDEX [IX_Votes_PostId_VoteTypeId]
    ON [dbo].[Votes] ([PostId])
    INCLUDE ([VoteTypeId])
    WITH (SORT_IN_TEMPDB = ON, ONLINE = ON, DATA_COMPRESSION = PAGE, FILLFACTOR = 95);
END;
GO

Create the Votes UserId aggregation index

USE [SQLStorm];
GO
IF NOT EXISTS
(
    SELECT 1
    FROM sys.indexes
    WHERE object_id = OBJECT_ID(N'dbo.Votes')
      AND name = N'IX_Votes_UserId_VoteTypeId_BountyAmount'
)
BEGIN
    CREATE NONCLUSTERED INDEX [IX_Votes_UserId_VoteTypeId_BountyAmount]
    ON [dbo].[Votes] ([UserId])
    INCLUDE ([VoteTypeId], [BountyAmount])
    WITH (SORT_IN_TEMPDB = ON, ONLINE = ON, DATA_COMPRESSION = PAGE, FILLFACTOR = 95);
END;
GO

Create the Posts owner and date index

USE [SQLStorm];
GO
IF NOT EXISTS
(
    SELECT 1
    FROM sys.indexes
    WHERE object_id = OBJECT_ID(N'dbo.Posts')
      AND name = N'IX_Posts_OwnerUserId_CreationDate'
)
BEGIN
    CREATE NONCLUSTERED INDEX [IX_Posts_OwnerUserId_CreationDate]
    ON [dbo].[Posts] ([OwnerUserId], [CreationDate])
    INCLUDE ([PostTypeId], [Score], [ViewCount])
    WITH (SORT_IN_TEMPDB = ON, ONLINE = ON, DATA_COMPRESSION = PAGE, FILLFACTOR = 95);
END;
GO

Enable automatic statistics and refresh statistics after index creation

USE [SQLStorm];
GO
ALTER DATABASE [SQLStorm] SET AUTO_CREATE_STATISTICS ON;
GO
ALTER DATABASE [SQLStorm] SET AUTO_UPDATE_STATISTICS ON;
GO
UPDATE STATISTICS [dbo].[Badges] WITH FULLSCAN;
GO
UPDATE STATISTICS [dbo].[PostHistory] WITH FULLSCAN;
GO
UPDATE STATISTICS [dbo].[Votes] WITH FULLSCAN;
GO
UPDATE STATISTICS [dbo].[Posts] WITH FULLSCAN;
GO

Conditionally re-trust every reported foreign key

USE [SQLStorm];
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Badges AS c LEFT JOIN dbo.Users AS p ON p.Id = c.UserId WHERE c.UserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Badges WITH CHECK CHECK CONSTRAINT [FK__Badges__UserId__6477ECF3];
ELSE PRINT 'Skipped FK__Badges__UserId__6477ECF3: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Comments AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.PostId WHERE c.PostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Comments WITH CHECK CHECK CONSTRAINT [FK__Comments__PostId__4CA06362];
ELSE PRINT 'Skipped FK__Comments__PostId__4CA06362: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Comments AS c LEFT JOIN dbo.Users AS p ON p.Id = c.UserId WHERE c.UserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Comments WITH CHECK CHECK CONSTRAINT [FK__Comments__UserId__4D94879B];
ELSE PRINT 'Skipped FK__Comments__UserId__4D94879B: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostHistory AS c LEFT JOIN dbo.PostHistoryTypes AS p ON p.Id = c.PostHistoryTypeId WHERE c.PostHistoryTypeId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostHistory WITH CHECK CHECK CONSTRAINT [FK__PostHisto__PostH__5070F446];
ELSE PRINT 'Skipped FK__PostHisto__PostH__5070F446: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostHistory AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.PostId WHERE c.PostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostHistory WITH CHECK CHECK CONSTRAINT [FK__PostHisto__PostI__5165187F];
ELSE PRINT 'Skipped FK__PostHisto__PostI__5165187F: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostHistory AS c LEFT JOIN dbo.Users AS p ON p.Id = c.UserId WHERE c.UserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostHistory WITH CHECK CHECK CONSTRAINT [FK__PostHisto__UserI__52593CB8];
ELSE PRINT 'Skipped FK__PostHisto__UserI__52593CB8: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostLinks AS c LEFT JOIN dbo.LinkTypes AS p ON p.Id = c.LinkTypeId WHERE c.LinkTypeId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostLinks WITH CHECK CHECK CONSTRAINT [FK__PostLinks__LinkT__571DF1D5];
ELSE PRINT 'Skipped FK__PostLinks__LinkT__571DF1D5: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostLinks AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.PostId WHERE c.PostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostLinks WITH CHECK CHECK CONSTRAINT [FK__PostLinks__PostI__5535A963];
ELSE PRINT 'Skipped FK__PostLinks__PostI__5535A963: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.PostLinks AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.RelatedPostId WHERE c.RelatedPostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.PostLinks WITH CHECK CHECK CONSTRAINT [FK__PostLinks__Relat__5629CD9C];
ELSE PRINT 'Skipped FK__PostLinks__Relat__5629CD9C: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Posts AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.AcceptedAnswerId WHERE c.AcceptedAnswerId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Posts WITH CHECK CHECK CONSTRAINT [FK__Posts__AcceptedA__48CFD27E];
ELSE PRINT 'Skipped FK__Posts__AcceptedA__48CFD27E: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Posts AS c LEFT JOIN dbo.Users AS p ON p.Id = c.LastEditorUserId WHERE c.LastEditorUserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Posts WITH CHECK CHECK CONSTRAINT [FK__Posts__LastEdito__47DBAE45];
ELSE PRINT 'Skipped FK__Posts__LastEdito__47DBAE45: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Posts AS c LEFT JOIN dbo.Users AS p ON p.Id = c.OwnerUserId WHERE c.OwnerUserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Posts WITH CHECK CHECK CONSTRAINT [FK__Posts__OwnerUser__46E78A0C];
ELSE PRINT 'Skipped FK__Posts__OwnerUser__46E78A0C: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Posts AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.ParentId WHERE c.ParentId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Posts WITH CHECK CHECK CONSTRAINT [FK__Posts__ParentId__49C3F6B7];
ELSE PRINT 'Skipped FK__Posts__ParentId__49C3F6B7: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Posts AS c LEFT JOIN dbo.PostTypes AS p ON p.Id = c.PostTypeId WHERE c.PostTypeId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Posts WITH CHECK CHECK CONSTRAINT [FK__Posts__PostTypeI__45F365D3];
ELSE PRINT 'Skipped FK__Posts__PostTypeI__45F365D3: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Tags AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.ExcerptPostId WHERE c.ExcerptPostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Tags WITH CHECK CHECK CONSTRAINT [FK__Tags__ExcerptPos__59FA5E80];
ELSE PRINT 'Skipped FK__Tags__ExcerptPos__59FA5E80: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Tags AS c LEFT JOIN dbo.Posts AS p ON p.Id = c.WikiPostId WHERE c.WikiPostId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Tags WITH CHECK CHECK CONSTRAINT [FK__Tags__WikiPostId__5AEE82B9];
ELSE PRINT 'Skipped FK__Tags__WikiPostId__5AEE82B9: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Votes AS c LEFT JOIN dbo.Users AS p ON p.Id = c.UserId WHERE c.UserId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Votes WITH CHECK CHECK CONSTRAINT [FK__Votes__UserId__5EBF139D];
ELSE PRINT 'Skipped FK__Votes__UserId__5EBF139D: violating rows exist.';
GO

IF NOT EXISTS (SELECT 1 FROM dbo.Votes AS c LEFT JOIN dbo.VoteTypes AS p ON p.Id = c.VoteTypeId WHERE c.VoteTypeId IS NOT NULL AND p.Id IS NULL)
    ALTER TABLE dbo.Votes WITH CHECK CHECK CONSTRAINT [FK__Votes__VoteTypeI__5DCAEF64];
ELSE PRINT 'Skipped FK__Votes__VoteTypeI__5DCAEF64: violating rows exist.';
GO

Supporting scripts (not part of the change)

Measure recommended index usage, size, and page density

USE [SQLStorm];
GO
SELECT
    OBJECT_SCHEMA_NAME(i.object_id) AS schema_name,
    OBJECT_NAME(i.object_id) AS table_name,
    i.name AS index_name,
    i.type_desc,
    i.fill_factor,
    i.is_disabled,
    us.user_seeks,
    us.user_scans,
    us.user_lookups,
    us.user_updates,
    SUM(ps.page_count) AS page_count,
    CAST(SUM(ps.page_count) * 8.0 / 1024.0 AS decimal(18,2)) AS size_mb,
    CAST(100.0 * SUM(ps.avg_page_space_used_in_percent * ps.page_count)
         / NULLIF(SUM(ps.page_count),0) AS decimal(6,2)) AS weighted_page_density,
    MAX(ps.avg_fragmentation_in_percent) AS max_fragmentation_percent
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS us
  ON us.database_id = DB_ID()
 AND us.object_id = i.object_id
 AND us.index_id = i.index_id
CROSS APPLY sys.dm_db_index_physical_stats
(
    DB_ID(), i.object_id, i.index_id, NULL, 'SAMPLED'
) AS ps
WHERE i.object_id IN
(
    OBJECT_ID(N'dbo.Badges'),
    OBJECT_ID(N'dbo.PostHistory'),
    OBJECT_ID(N'dbo.Votes'),
    OBJECT_ID(N'dbo.Posts'),
    OBJECT_ID(N'dbo.Comments'),
    OBJECT_ID(N'dbo.Users')
)
GROUP BY i.object_id, i.name, i.type_desc, i.fill_factor, i.is_disabled,
         us.user_seeks, us.user_scans, us.user_lookups, us.user_updates
ORDER BY size_mb DESC;
GO

Check constraint trust and remaining foreign-key violations

USE [SQLStorm];
GO
SELECT
    s.name AS schema_name,
    t.name AS table_name,
    fk.name AS constraint_name,
    fk.is_disabled,
    fk.is_not_trusted,
    fk.is_not_for_replication
FROM sys.foreign_keys AS fk
JOIN sys.tables AS t ON t.object_id = fk.parent_object_id
JOIN sys.schemas AS s ON s.schema_id = t.schema_id
ORDER BY s.name, t.name, fk.name;
GO

Inspect automatic tuning state and Query Store forced-plan failures

USE [SQLStorm];
GO
SELECT desired_state_desc, actual_state_desc, reason
FROM sys.database_automatic_tuning_options
WHERE option_name = N'FORCE_LAST_GOOD_PLAN';
GO

SELECT
    plan_id,
    query_id,
    last_force_failure_reason_desc,
    force_failure_count,
    last_execution_time,
    last_compile_start_time,
    last_compile_duration
FROM sys.query_store_plan
WHERE is_forced_plan = 1
  AND force_failure_count > 0
ORDER BY last_execution_time DESC;
GO

Inspect recent statistics and index changes before interpreting plan regressions

USE [SQLStorm];
GO
SELECT
    s.name AS schema_name,
    o.name AS object_name,
    st.name AS statistics_name,
    sp.last_updated,
    sp.rows,
    sp.rows_sampled,
    sp.modification_counter
FROM sys.stats AS st
JOIN sys.objects AS o ON o.object_id = st.object_id
JOIN sys.schemas AS s ON s.schema_id = o.schema_id
OUTER APPLY sys.dm_db_stats_properties(st.object_id, st.stats_id) AS sp
WHERE o.type = 'U'
  AND o.name IN (N'Badges', N'PostHistory', N'Votes', N'Posts', N'Comments', N'Users')
ORDER BY sp.last_updated DESC;
GO

Validation, rollback, and safe playbook

One-line remediation: Add the five prioritized indexes with PAGE compression, correct statistics coverage, and conditionally re-trust valid foreign keys while retaining FORCE_LAST_GOOD_PLAN.

  1. Implement: create the Badges, PostHistory, Votes, and Posts indexes; enable automatic statistics and update affected statistics; run the conditional constraint-trust batch during a suitable maintenance window. Risk: Medium due to build I/O and validation scans.
  2. Compare: capture actual execution plans and Query Store metrics for representative statements before and after each index group. Confirm lower logical reads, CPU, duration, spills, and scans without unacceptable write latency. Risk: Low.
  3. Retain or revert: keep only indexes with sustained benefit. If a new index increases write duration, blocking, storage pressure, or regresses representative queries, drop that specific nonclustered index and allow Query Store automatic correction to continue. Risk: Low for dropping only the newly created indexes; never drop a primary key or unique constraint index.

Exact validation checks

  • Each recommended index exists, is enabled, is PAGE compressed, and has expected key/include columns.
  • Post-change plans use seeks or efficient ordered scans on the new indexes for the targeted joins and aggregations.
  • For targeted queries, compare CPU, duration, logical reads, memory grants, spills, actual rows, and wait categories; prioritize p95/p99 when enough executions exist.
  • Confirm user_seeks + user_scans materially exceeds user_updates for each new index after an equivalent workload window.
  • Confirm no new page-density result falls below approximately 75%; rebuild only if fragmentation and workload justify it.
  • Confirm all valid foreign keys show is_not_trusted = 0; record every skipped constraint and remediate violating data separately.
  • Confirm Query Store remains healthy, FORCE_LAST_GOOD_PLAN remains ON, and no forced-plan failure appears.