Index Tuning Report – RockyPC / SQLStorm – 2026-09-20 15:34:25 UTC
Tuning goal: Index Tuning
Executive summary
PostHistory, Votes, and Posts; then update statistics and re-trust foreign keys only where validation finds no violating rows.
FORCE_LAST_GOOD_PLAN desired and actual state.Top priorities
- 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 byPostId/CreationDate. - Votes aggregation paths: add separate indexes for
PostIdandUserId. Votes has 926,084 rows, 216 reported PostId seeks/scans, 72 UserId seeks/scans, and query patterns aggregate by both relationships. - Posts join and ranking path: add
(OwnerUserId, CreationDate)includingPostTypeId, 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. - Badges consolidation: add one
(UserId)index includingClass, Name, covering three overlapping missing-index requests without creating three separate indexes. Badges has 439,352 rows and no reported writes. - 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.
| Evidence | Finding | Implication |
|---|---|---|
| Missing-index impact | PostHistory 99.56%, Votes UserId 99.59%, Votes PostId 64.03%, Comments PostId 28.64%. | Prioritize selective relationship indexes; defer low-impact alternatives. |
| Writes versus reads | Votes 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 sizes | Badges 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 cardinality | Badges.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. |
| Columnstore | No 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 justification | 48 + 12 + 18 missing-index seeks/scans across UserId candidates; no reported writes to the Badges clustered index. |
|---|---|
| Estimated size | 10.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 benefit | Fewer clustered scans and lookups for user badge counts and class summaries in Query Store statements. |
| Risk | Low 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 justification | 28 missing-index operations with 99.56% impact; repeated Query Store and plan-cache statements use PostId, CreationDate, PostHistoryTypeId, UserId, and Comment. |
|---|---|
| Estimated size | 38.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 benefit | Seekable recent-history retrieval, lower sort and scan work, and fewer spills in queries using ROW_NUMBER() OVER (PARTITION BY PostId ORDER BY CreationDate DESC). |
| Risk | Medium 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.
| Index | Evidence | Estimated size and assumptions |
|---|---|---|
IX_Votes_PostId_VoteTypeId | 216 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_BountyAmount | 76 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 size | 3.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 benefit | Faster user-post aggregation and recent-post ranking in queries with WHERE p.CreationDate > ... and PARTITION BY p.OwnerUserId. |
| Risk | Medium: 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.
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
| Change | Before | After | Net change |
|---|---|---|---|
| Badges consolidated index | 0 MB new index | 12–16 MB | +12–16 MB |
| PostHistory covering index | 0 MB new index | 38.8–45.3 MB | +38.8–45.3 MB |
| Votes PostId index | 0 MB new index | 21.2 MB | +21.2 MB |
| Votes UserId index | 0 MB new index | 28.3 MB | +28.3 MB |
| Posts OwnerUserId/date index | 0 MB new index | 3.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.
- 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.
- 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.
- 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_scansmaterially exceedsuser_updatesfor 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_PLANremains ON, and no forced-plan failure appears.