AI SQL Tuner Recommendations

Tuning Goal: Index Tuning
Server Name
RockyPC
Database Name
SQLStorm
SQL Server Version
SQL Server 2022 (16.x), 16.0.1170.5, Developer Edition (64-bit), Engine Edition: Enterprise
Platform Notes
Online index operations, PAGE compression, columnstore, partition switching, and SQL Server 2022 IQP features available

Executive summary

  1. Highest priority: add dbo.Comments(PostId) and dbo.Votes(PostId, VoteTypeId) indexes. The dominant workload repeatedly joins and aggregates comments and votes by PostId, while current usage shows heavy clustered scans on both tables:
    • dbo.Comments clustered index: 702 scans, no seek-supporting PostId index present.
    • dbo.Votes clustered index: 1435 scans, many queries count votes by PostId and VoteTypeId.
  2. High priority: add dbo.Badges(UserId) INCLUDE(Class). Missing index data is strong, UserId is highly selective in Badges, and plan cache / Query Store show frequent badge aggregations per user.
  3. Medium priority: add dbo.Votes(UserId, VoteTypeId) INCLUDE(BountyAmount) for user-centric vote and bounty rollups. This is useful, but lower priority than the PostId-based Votes index because Votes.UserId cardinality is relatively low in the provided stats.
  4. Low priority: consider dropping dbo.PostLinks.IX_PostLinks_RelatedPostId if the observation window is representative. It has 0 seeks, 0 scans, 0 lookups.
  5. Code optimization is required: the biggest Query Store regressions are driven by query shape, not just missing indexes. Repeated patterns such as Posts.Tags LIKE '%' + TagName + '%', correlated scalar subqueries against Votes/Comments, and many-way joins before aggregation are causing massive logical reads and extreme durations.
  6. No rebuild emergency: page density is excellent across the sampled indexes (98.3% to 99.9%) and fragmentation is negligible. No index rebuild recommendation is justified from the supplied page-density data.
  7. Automatic tuning status: FORCE_LAST_GOOD_PLAN desired state is ON and actual state is ON. No corrective action needed.
  8. Forced plan failures: none detected.
Priority Recommendation Expected Benefit Confidence
1 Create IX_Comments_PostId Reduce clustered scans for post/comment joins and grouped comment counts 96%
2 Create IX_Votes_PostId_VoteTypeId Reduce scans for vote counts by post, upvotes/downvotes, and bounty aggregations 95%
3 Create IX_Badges_UserId INCLUDE(Class) Improve user badge rollups and eliminate repeated Badges scans 94%
4 Create IX_Votes_UserId_VoteTypeId INCLUDE(BountyAmount) Improve user-centric vote and bounty aggregation 82%
5 Drop IX_PostLinks_RelatedPostId if unused window is representative Reduce maintenance/storage overhead 68%

Detailed prioritized recommendations

1. Create a nonclustered index on dbo.Comments(PostId)

Priority: HighConfidence: 96%

  • Why: many top queries join Comments to Posts on c.PostId = p.Id and aggregate COUNT(c.Id).
  • Current gap: existing nonclustered index is IX_Comments_UserId_CreationDate; there is no usable index for the common PostId join pattern.
  • Evidence:
    • dbo.Comments clustered index shows 702 scans.
    • Comments.PostId has good cardinality: estimated distinct 122,541 over 351,440 rows.
    • Representative statements:
      • LEFT JOIN Comments c ON p.Id = c.PostId from Query Hash A435556D6E6DA664
      • LEFT JOIN Comments c ON p.Id = c.PostId from Query Hash CE7337517B419CA4
      • COALESCE((SELECT COUNT(*) FROM Comments c WHERE c.PostId = p.Id), 0) from Query Hash 6211E6BFDACEAE1E
  • Design: key (PostId); include Id, CreationDate, Score to cover common grouped and display queries without extra lookups.
  • Overhead assessment: justified. Table is large enough, scan volume is high, and no conflicting write-heavy evidence is present in the supplied usage snapshot.

2. Create a nonclustered index on dbo.Votes(PostId, VoteTypeId)

Priority: HighConfidence: 95%

  • Why: this is the most broadly beneficial Votes index for the workload shown. Many queries count upvotes/downvotes and bounties by PostId with VoteTypeId filters.
  • Evidence:
    • dbo.Votes clustered index shows 1435 scans.
    • Votes.PostId estimated distinct 242,634; composite (PostId, VoteTypeId) estimated distinct 347,249, which is strong selectivity.
    • Representative statements:
      • LEFT JOIN Votes v ON p.Id = v.PostId from Query Hash A435556D6E6DA664
      • COALESCE((SELECT COUNT(*) FROM Votes v WHERE v.PostId = p.Id AND v.VoteTypeId = 2), 0) from Query Hash 6211E6BFDACEAE1E
      • COALESCE((SELECT COUNT(*) FROM Votes V WHERE V.PostId = P.Id AND V.VoteTypeId = 3), 0) from Query Hash 29,987 seconds / PostStatistics
  • Design: key (PostId, VoteTypeId); include UserId, BountyAmount, CreationDate.
  • Why this before the missing-index UserId recommendation: the plan cache and Query Store show more repeated access by PostId than by UserId. This aligns better with the dominant workload.
  • Overhead assessment: worthwhile, but this table is one of the larger write candidates in a normal StackOverflow-style schema, so avoid adding more than two targeted nonclustered indexes on Votes.

3. Create a nonclustered index on dbo.Badges(UserId) INCLUDE(Class)

Priority: HighConfidence: 94%

  • Why: explicit missing-index recommendations are consistent and strong for Badges, and the workload repeatedly summarizes badges by user and class.
  • Evidence:
    • Missing index: UserId INCLUDE(Class), average impact 97.70%, total seeks/scans 57.
    • Additional missing index: UserId, average impact 96.66%, total seeks/scans 33.
    • dbo.Badges clustered index shows 707 scans.
    • Badges.UserId has high cardinality: estimated distinct 154,016 over 439,352 rows.
    • Class is low-cardinality, so it belongs as an included column, not as a leading key.
    • Representative statements:
      • LEFT JOIN Badges b ON u.Id = b.UserId from Query Hash 37B66C47D3CB2E52
      • SUM(CASE WHEN b.Class = 1 THEN 1 ELSE 0 END) from Query Hash 22097AE9F1E55919
  • Filtered index note: filtered indexes by Class are not recommended. Class has only about 3 values and would add maintenance with limited selectivity benefit.

4. Create a nonclustered index on dbo.Votes(UserId, VoteTypeId) INCLUDE(BountyAmount)

Priority: MediumConfidence: 82%

  • Why: this directly addresses the supplied missing-index request and supports user-centric vote and bounty summaries.
  • Evidence:
    • Missing index: UserId INCLUDE(VoteTypeId, BountyAmount), average impact 99.39%.
    • Representative statements:
      • SELECT v.UserId, SUM(CASE WHEN v.VoteTypeId = 2 THEN 1 ELSE 0 END)... GROUP BY v.UserId from Query Hash BC476FF89BF38787
      • LEFT JOIN Votes v ON u.Id = v.UserId from Query Hash C4678A0E114059E6
      • LEFT JOIN Votes v ON u.Id = v.UserId AND v.VoteTypeId = 8 from Query Hash F058EE4DBCA51E18
  • Caution: supplied cardinality for Votes.UserId is low (estimated distinct 929), so this index is less selective than the PostId-based Votes index.
  • Decision: recommend it, but after the (PostId, VoteTypeId) index.
  • Filtered alternative: if write overhead becomes a concern, replace this with a smaller filtered index for bounty rows only: WHERE VoteTypeId = 8 AND UserId IS NOT NULL.

5. Drop unused nonclustered index dbo.PostLinks.IX_PostLinks_RelatedPostId

Priority: LowConfidence: 68%

  • Why: current usage is 0 seeks, 0 scans, 0 lookups.
  • Size: only 0.4 MB, so savings are small.
  • Decision: optional only. Safe to defer if the sampling window may be incomplete.
  • Do not drop any clustered primary key indexes; no PK rebuild/drop recommendation is warranted.

6. Statistics and maintenance recommendations

Priority: MediumConfidence: 88%

  • No index rebuild/reorganize recommendation: page density is already excellent across major indexes:
    • Posts clustered: 99.2%
    • Votes clustered: 99.8%
    • Badges clustered: 99.9%
    • Comments clustered: 98.3%
  • Update statistics on Posts, Votes, Comments, Badges, and Users after index deployment because Query Store shows severe plan instability symptoms, extreme read counts, and multiple two-plan cases.
  • Auto statistics settings:
    • AUTO_CREATE_STATISTICS = ON — good
    • AUTO_UPDATE_STATISTICS = ON — good
    • AUTO_UPDATE_STATISTICS_ASYNC = OFF — acceptable; no change required for this report
  • Columnstore: not recommended from the supplied data. No table exceeds the report’s >1,000,000 row threshold.

7. Query and code changes with high performance impact

Priority: HighConfidence: 97%

  • Replace non-sargable tag matching: queries using Posts.Tags LIKE '%' + TagName + '%' are a major workload problem and are not fixable with a normal b-tree index on Posts.Tags.
    • Representative statements:
      • Query Hash 872C7773968C6AAF
      • Query Store entries with repeated JOIN Tags t ON p.Tags LIKE '%' + t.TagName + '%'
    • Recommendation: normalize tags into a bridge table such as dbo.PostTags(PostId, TagId) with indexes on (TagId, PostId) and (PostId, TagId).
  • Rewrite correlated scalar subqueries on Votes and Comments into pre-aggregated derived tables or CTEs. Example anti-pattern:
    • (SELECT COUNT(*) FROM Votes v WHERE v.PostId = p.Id AND v.VoteTypeId = 2)
    • (SELECT COUNT(*) FROM Comments c WHERE c.PostId = p.Id)
  • Aggregate before joining: many long-running Query Store queries join Users, Posts, Votes, Comments, and Badges first, then use COUNT(DISTINCT ...). This inflates row counts and memory grants. Aggregate each child table separately by its join key first.
  • Simplify date predicates: several queries use unnecessarily complex expressions around DATEADD and CAST. Use a direct parameter or computed date variable for better cardinality estimation.
  • Memory grant / spill note: Query Hash 9D978A0253DCE155 shows 623 spills; this strongly supports query-shape rewrite in addition to indexing.

Platform and automatic tuning review

  • Version/edition suitability: SQL Server 2022 Enterprise capabilities fully support ONLINE = ON, PAGE compression, and advanced indexing options used below.
  • Automatic tuning: FORCE_LAST_GOOD_PLAN desired state ON, actual state ON. No action required.
  • Forced plan failures: none detected; no unforce action required.
  • Constraints impact: no recommendation requires dropping or recreating any primary key or foreign key.

Implementation scripts

Create 1: dbo.Comments(PostId)

USE [SQLStorm];
GO

CREATE INDEX [IX_Comments_PostId]
ON [dbo].[Comments] ([PostId])
INCLUDE ([Id], [CreationDate], [Score])
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO

Create 2: dbo.Votes(PostId, VoteTypeId)

USE [SQLStorm];
GO

CREATE INDEX [IX_Votes_PostId_VoteTypeId]
ON [dbo].[Votes] ([PostId], [VoteTypeId])
INCLUDE ([UserId], [BountyAmount], [CreationDate])
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO

Create 3: dbo.Badges(UserId) INCLUDE(Class)

USE [SQLStorm];
GO

CREATE INDEX [IX_Badges_UserId]
ON [dbo].[Badges] ([UserId])
INCLUDE ([Class])
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO

Create 4: dbo.Votes(UserId, VoteTypeId) INCLUDE(BountyAmount)

USE [SQLStorm];
GO

CREATE INDEX [IX_Votes_UserId_VoteTypeId]
ON [dbo].[Votes] ([UserId], [VoteTypeId])
INCLUDE ([BountyAmount])
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO

Alternative smaller filtered index for user bounty workload

USE [SQLStorm];
GO

CREATE INDEX [IX_Votes_UserId_Bounty_VT8]
ON [dbo].[Votes] ([UserId])
INCLUDE ([BountyAmount], [CreationDate])
WHERE [VoteTypeId] = 8 AND [UserId] IS NOT NULL
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO

Optional drop: unused dbo.PostLinks index

USE [SQLStorm];
GO

DROP INDEX [IX_PostLinks_RelatedPostId]
ON [dbo].[PostLinks];
GO

Statistics refresh after index deployment

USE [SQLStorm];
GO

UPDATE STATISTICS [dbo].[Posts] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[Votes] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[Comments] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[Badges] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[Users] WITH FULLSCAN;
GO

Query rewrite pattern: replace correlated subqueries with pre-aggregation

/* Pattern example */

WITH VoteAgg AS
(
    SELECT
        v.PostId,
        SUM(CASE WHEN v.VoteTypeId = 2 THEN 1 ELSE 0 END) AS UpVotes,
        SUM(CASE WHEN v.VoteTypeId = 3 THEN 1 ELSE 0 END) AS DownVotes,
        SUM(CASE WHEN v.VoteTypeId = 8 THEN ISNULL(v.BountyAmount,0) ELSE 0 END) AS TotalBounty
    FROM dbo.Votes AS v
    GROUP BY v.PostId
),
CommentAgg AS
(
    SELECT
        c.PostId,
        COUNT(*) AS CommentCount
    FROM dbo.Comments AS c
    GROUP BY c.PostId
)
SELECT
    p.Id,
    p.Title,
    ISNULL(va.UpVotes,0) AS UpVotes,
    ISNULL(va.DownVotes,0) AS DownVotes,
    ISNULL(va.TotalBounty,0) AS TotalBounty,
    ISNULL(ca.CommentCount,0) AS CommentCount
FROM dbo.Posts AS p
LEFT JOIN VoteAgg AS va
    ON va.PostId = p.Id
LEFT JOIN CommentAgg AS ca
    ON ca.PostId = p.Id
WHERE p.CreationDate >= DATEADD(YEAR, -1, @AsOfDate);

Schema pattern: normalize tags to eliminate LIKE '%tag%'

/* Suggested model */

CREATE TABLE dbo.PostTags
(
    PostId int NOT NULL,
    TagId  int NOT NULL,
    CONSTRAINT PK_PostTags PRIMARY KEY CLUSTERED (PostId, TagId),
    CONSTRAINT FK_PostTags_Posts FOREIGN KEY (PostId) REFERENCES dbo.Posts(Id),
    CONSTRAINT FK_PostTags_Tags  FOREIGN KEY (TagId)  REFERENCES dbo.Tags(Id)
);
GO

CREATE INDEX IX_PostTags_TagId_PostId
ON dbo.PostTags(TagId, PostId)
WITH (
    ONLINE = ON,
    SORT_IN_TEMPDB = ON,
    DATA_COMPRESSION = PAGE,
    FILLFACTOR = 100
);
GO