Detailed prioritized recommendations
1. Redesign the denormalized tag relationship before adding another Posts index
Recommendation: Replace substring matching against Posts.Tags with a relational post/tag bridge or another semantically equivalent tag-search implementation. The current predicate searches for a tag name anywhere in a delimited string, so an ordinary index on Tags cannot seek to the substring.
Expected benefit: This targets the plan's dominant operator: Nested Loops Node 8, own cost 157.2936, approximately 80% of the 196.93 statement cost. The current tag branch processes 104,436 question rows and performs wildcard matching against 1,232 tag rows.
Important semantic note: A bridge table must preserve the current substring semantics if exact row equivalence is required. A tokenized bridge may differ for substrings, delimiters, escaping and case/collation behavior. Full-text search also has different tokenization semantics and should not be substituted without an approved result-set change.
Evidence: Node 8 shows Posts.Tags LIKE '%' + Tags.TagName + '%'; Node 9 reads 104,436 question rows; Node 10 has 104,435 rewinds and subtree cost 33.6225.
Risk: High implementation and data-maintenance risk because the schema/data model changes. No executable object-creation script is supplied because the input does not define a bridge table, change-capture process, or exact tag-token semantics.
2. Validate the apparent UserId/PostId join defect before any result-changing rewrite
Recommendation: Review LEFT JOIN RankedPosts rp ON tp.UserId = rp.PostId. The CTE exposes p.Id AS PostId, while the outer row is keyed by tp.UserId. This associates a user with a post whose identifier equals the user identifier, rather than associating the user's own posts.
Why no rewrite is provided: Replacing this condition with rp.OwnerUserId = tp.UserId would likely represent the intended business rule, but it would not return exactly the same rows as the supplied query. The requested tuning constraint requires result preservation.
Evidence: The plan's final Hash Match Node 4 builds on Posts.Id and probes with Users.Id. The query text explicitly specifies tp.UserId = rp.PostId.
Risk: Very high if changed without business approval; result cardinality and displayed latest-post data can change. Confidence that this is a correctness anomaly: 95%; confidence that it is unintended: 75%, because intent was not separately documented.
3. Separate the Posts/Votes/Badges aggregates in a future approved business rewrite
Recommendation: For the intended user-statistics result, aggregate Posts, Votes and Badges independently by user, then join the three compact aggregate sets to Users. This prevents the multiplication of vote rows by badge rows and avoids the need for two distinct aggregates over the multiplied stream.
Expected benefit: The current aggregate branch receives 1,677,770 estimated rows at Node 19 and uses Hash Aggregate Node 18 with two distinct aggregates. The source tables contain 1,852,168 Votes, 878,704 Badges, 1,091,124 Posts and 534,386 Users rows.
Result-preservation limitation: This is not included as a runnable “rewritten query” because it changes the current arithmetic whenever a user has multiple badges and multiple vote rows. The supplied query's SUM(CASE...) values are multiplied by badge cardinality for users with posts and badges.
Evidence: Node 18 computes COUNT(DISTINCT Posts.Id), vote sums and COUNT(DISTINCT Badges.Id) after joins; Node 19 estimates 1,677,770 rows. No plan warning indicates a missing join predicate; the multiplication follows directly from the query's join shape.
Risk: High semantic risk unless the intended counts are confirmed. Performance confidence is high for workloads with many users having both votes and badges, but the exact runtime reduction is unavailable because no actual execution metrics were supplied.
4. Refresh statistics immediately, then enable automatic statistics management
Recommendation: Run the supplied statistics-maintenance script in a controlled maintenance window. Then enable AUTO_CREATE_STATISTICS, AUTO_UPDATE_STATISTICS and, where acceptable, asynchronous updates for SQLStorm.
Expected benefit: Better cardinality estimates can reduce memory-grant errors and improve join/aggregate choices. This will not eliminate the leading-wildcard cost, but it addresses stale input information used by the compiled plan.
Evidence: Database settings report all three automatic statistics options disabled. Votes statistics were last updated on 2026-09-12 with modification count 1,600,424 and sampling 71.5712%; the plan uses those statistics.
Risk: Statistics updates consume CPU and I/O and can compile plans. FULLSCAN on the largest referenced objects may be expensive; the script uses targeted updates rather than a database-wide operation.
5. Do not add the DMV missing-index candidates for this query without workload proof
Recommendation: Do not implement the reported DMV candidates solely from their percentages. The most relevant missing-index-looking item for the current join would be Votes(UserId) INCLUDE (VoteTypeId), but this query joins Votes by PostId, and the existing IX_Votes_PostId_VoteType already supports that access.
Evidence: The plan scans IX_Votes_PostId_VoteType, reading 926,084 rows at cost 1.05937, and scans IX_Badges_UserId_Id, reading 439,352 rows at cost 0.502074. The DMV recommendation for Votes.UserId has only 10 total seeks/scans in the post-restart window.
Risk: Adding workload-specific indexes can increase DML, storage and maintenance cost without improving this statement. The supplied index usage statistics show zero reported updates during this short observation window, so write overhead cannot be quantified from the input.
6. Treat sort and memory-grant tuning as secondary
Recommendation: Do not force join algorithms, MAXDOP or memory-grant hints from this estimated plan. First address tag matching, aggregate shape and statistics. Capture an actual plan to determine whether any sort spills.
Evidence: Three Sort operators are present, but their individual own costs are only 0.0580, 0.1100 and 0.0586. Estimated desired memory is 5,783,616 KB, while granted/used memory is explicitly unknown and no spill warning is supplied.
Risk: Hints can stabilize a single estimate while harming other executions. No parameter exists in this query, so parameter sniffing remediation is not applicable.
Supporting scripts (not part of the change)
Capture actual I/O, CPU time and execution plan for before/after validation
USE [SQLStorm];
GO
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SET STATISTICS XML ON;
WITH RankedPosts AS
(
SELECT
p.Id AS PostId,
p.Title,
p.CreationDate,
ROW_NUMBER() OVER
(
PARTITION BY t.Id
ORDER BY p.CreationDate DESC
) AS rn,
t.TagName
FROM Posts AS p
JOIN Tags AS t
ON p.Tags LIKE '%' + t.TagName + '%'
WHERE p.PostTypeId = 1
),
UserStats AS
(
SELECT
u.Id AS UserId,
u.DisplayName,
COUNT(DISTINCT p.Id) AS TotalPosts,
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,
COUNT(DISTINCT b.Id) AS BadgeCount
FROM Users AS u
LEFT JOIN Posts AS p
ON u.Id = p.OwnerUserId
LEFT JOIN Votes AS v
ON p.Id = v.PostId
LEFT JOIN Badges AS b
ON u.Id = b.UserId
GROUP BY
u.Id,
u.DisplayName
),
TopUsers AS
(
SELECT
us.UserId,
us.DisplayName,
us.TotalPosts,
us.UpVotes,
us.DownVotes,
us.BadgeCount,
DENSE_RANK() OVER
(
ORDER BY
us.TotalPosts DESC,
us.UpVotes - us.DownVotes DESC
) AS UserRank
FROM UserStats AS us
WHERE us.TotalPosts > 10
)
SELECT
tp.UserRank,
tp.DisplayName,
tp.TotalPosts,
tp.UpVotes,
tp.DownVotes,
tp.BadgeCount,
rp.Title AS LatestPostTitle,
rp.CreationDate AS LatestPostDate,
CASE
WHEN rp.rn = 1 THEN 'Latest'
ELSE 'Older'
END AS PostStatus
FROM TopUsers AS tp
LEFT JOIN RankedPosts AS rp
ON tp.UserId = rp.PostId
ORDER BY
tp.UserRank,
rp.CreationDate DESC;
SET STATISTICS XML OFF;
SET STATISTICS TIME OFF;
SET STATISTICS IO OFF;
GO
Validation requirement: Compare logical reads, CPU time, elapsed time, actual rows, memory grant, spills and returned-row checksum before and after the statistics change. The supplied plan is estimated only, so actual performance cannot be claimed yet.