Code Review – RockyPC/SQLStorm – 2026-09-27 17:05:17 UTC
Tuning goal: Code Review
Server: RockyPC · Database: SQLStorm · SQL Server 2022 (16.x), RTM-GDR 16.0.1200.5 · Developer Edition with Enterprise engine capabilities
Executive summary
Only the most recent objects provided in the input were reviewed. All 100 supplied stored-procedure definitions were labelled complete. No functions, triggers, or views were provided. No objects were marked NOT INCLUDED or NOT AVAILABLE.
- Correct the high-impact data and cardinality errors in
dbo.sp00060. Its line 79 join compares a user ID to a post ID, which can produce incorrect results. The procedure also performs a non-sargable tag search at lines 21–25 and has an expensive multi-join aggregate at lines 36–45. Confidence: High. - Remove the five-second transaction-held lock interval in
dbo.usp_UpdateTablesWithDelay. Lines 13–36 keep locks from the first update while executingWAITFOR DELAYat line 25. Move the delay beforeBEGIN TRANSACTION. Confidence: High. - Do not use the supplied procedures as production workload evidence without correcting their fixed dates and cross joins. Many procedures use literal dates based on
2024-10-01, and several intentionally or accidentally create Cartesian-style joins. Query Store shows zero executions for most supplied objects, so prioritization is based primarily on code risk rather than measured runtime. Confidence: High. - Defer index changes. Every displayed index has zero seeks, scans, lookups, and updates during the 37.5-hour observation window. The window is too short and workload coverage is not established; no index should be dropped solely because of these counters. Confidence: High.
Review scope and inventory
| Object category | Count | Review status |
|---|---|---|
| Stored procedures | 100 | All supplied definitions complete and reviewed |
| Functions | 0 | None supplied |
| Triggers | 0 | None supplied |
| Views | 0 | None supplied |
| Tables over 1,000 rows | 8 | Reviewed as context for access paths and aggregation cost |
| Index entries in usage summary | 16 | Reviewed; all counters are zero in the supplied interval |
The largest tables are dbo.Votes with 926,084 rows, dbo.PostHistory with 847,593 rows, dbo.Badges with 439,352 rows, dbo.Comments with 351,440 rows, dbo.Users with 267,193 rows, and dbo.Posts with 246,672 rows. These sizes make repeated full-table aggregation particularly important in procedures that scan votes, history, comments, posts, or badges.
Detailed prioritized recommendations
-
Priority 1 — Correct
dbo.sp00060user/post join and reduce duplicate aggregationAt lines 36–45 of
dbo.sp00060,Posts,Votes, andBadgesare joined before aggregation. This multiplies rows when a user has multiple posts, votes, and badges;COUNT(DISTINCT p.Id)masks only one part of the multiplication, while the vote sums can be overstated. At line 79,tp.UserId = rp.PostIdjoins unrelated key domains and is almost certainly incorrect.Lines 21–25 also match tags using
p.Tags LIKE '%' + t.TagName + '%'. A leading wildcard prevents a normal B-tree seek and can scan the 246,672-rowdbo.Poststable and compare against the 1,232-rowdbo.Tagstable. The supplied indexIX_Posts_Question_TagScanis not an effective solution for this predicate because its key is onlyPostTypeIdand the tag predicate remains non-sargable.The revised definition in the Scripts section pre-aggregates posts, votes, and badges independently, includes
OwnerUserIdin the ranked-post CTE, and joins users to their own posts. It preserves the original result columns and read-only behavior while correcting the key mismatch.Confidence: High for the join and aggregation defects; medium for the intended tag result semantics because the original description and query shape do not fully specify tag ownership behavior.
-
Priority 2 — Never hold update locks during
WAITFORdbo.usp_UpdateTablesWithDelaybegins a transaction at line 13, updatesdbo.Usersat lines 16–19, and then waits five seconds at line 25 before updatingdbo.Postsat lines 28–31. Under normal read-committed locking, the first row/key locks remain held until line 36 commits. Concurrent sessions can therefore wait on the user row for the entire delay, and sessions that update the tables in the opposite order can deadlock.The safest code-only change is to move the deliberate delay before the transaction begins. The revised procedure in the Scripts section retains the same two updates, transaction atomicity, error handling, result sets, and five-second delay, but does not hold transaction locks while waiting.
Confidence: High. No actual blocking or deadlock graph was supplied, so the observed impact cannot be quantified.
-
Priority 3 — Validate loop parameters and avoid wasteful random scans
dbo.usp_UpdateTablesInLoopusesORDER BY NEWID()at lines 27–29 and 31–33. This generally requires reading and sorting a large portion ofdbo.Usersanddbo.Postsfor every iteration. With 267,193 users and 246,672 posts, repeated executions can be expensive even though Query Store reports zero executions in the last 30 days.Line 77 divides by
@LoopCount. A caller passing zero causes a divide-by-zero error, while a negative count silently skips the loop and still reaches the percentage calculation. The revised definition validates that@LoopCountis positive and returns a safe zero success rate when appropriate. This is a correctness and defensive-programming improvement; a truly efficient random-row strategy requires statistics or a known contiguous key range that were not supplied.Confidence: High for parameter validation; medium for the exact cost of
ORDER BY NEWID()because no execution plan or duration was provided. -
Priority 4 — Replace hard-coded historical dates with runtime semantics where the report is intended to be relative
Numerous procedures use fixed expressions such as
DATEADD(YEAR, -1, '2024-10-01 12:34:56')orDATEADD(DAY, -30, '2024-10-01'). Examples includedbo.sp09995lines 22–24,dbo.sp09994lines 24–26,dbo.sp09993lines 20–22, anddbo.sp09900lines 21–23. These reports become stale and may return no current data.Do not mechanically replace every literal: some may be intentional test fixtures. Where the report is intended to be relative, use a deterministic local variable initialized once, for example inline
@AsOfDate = SYSUTCDATETIME(), and calculate all boundaries from that value. No scripts are supplied for this broad change because each procedure's intended reporting period and contract must be preserved independently.Confidence: High that the dates are stale for current reporting; low-to-medium regarding the intended business semantics.
-
Priority 5 — Eliminate accidental Cartesian joins and joins on display names
Several procedures join unrelated row sets using predicates that are not relational keys. Examples include
dbo.sp09990lines 69–74, where popular tags are joined to users onUA.PostsChanged > 0;dbo.sp09974lines 48–50, which cross joins every qualifying user to every popular post;dbo.sp09895lines 48–65, which cross joins top users to popular posts; anddbo.sp09881lines 69–74, which cross joins user statistics and popular tags before joining history by display name.Display-name joins also occur in procedures such as
dbo.sp09909lines 65–68,dbo.sp09828lines 46–49, anddbo.sp09827lines 53–58. Display names are not guaranteed to be unique or immutable. UseUsers.Id,Posts.OwnerUserId, andPostHistory.UserIdwherever the intended relationship is available.These procedures are marked unused in the supplied 30-day Query Store snapshot, so no replacement definitions are included. They should be corrected before being enabled for production use.
Confidence: High for the cardinality and key-quality risks.
-
Priority 6 — Pre-aggregate one-to-many relationships before joining
A recurring pattern joins posts to comments, votes, badges, and sometimes post history in one query, then aggregates afterward. Examples include
dbo.sp09991lines 6–21,dbo.sp09941lines 6–27,dbo.sp09982lines 6–27, anddbo.sp09883lines 51–85. Because comments, votes, badges, and history are independent one-to-many relationships, the join can create a multiplicative intermediate result.COUNT(DISTINCT ...)may repair counts but does not avoid the memory, sort, and spill cost.Use separate grouped CTEs or derived tables keyed by the parent ID, then join the compact aggregates. This is the approach used in the revised
dbo.sp00060definition.Confidence: High as a general code-review finding; exact benefit is not measurable without plans and runtime statistics.
-
Priority 7 — Treat non-sargable tag searches as a schema-design issue
Tag searches using expressions such as
LIKE '%' + t.TagName + '%'appear indbo.sp00060lines 21–25,dbo.sp09990lines 29–36,dbo.sp09982lines 47–57, anddbo.sp09881lines 25–36.STRING_SPLITusage appears indbo.sp09915lines 26–36 anddbo.sp09886lines 13–20.The durable fix is a normalized bridge table such as
PostTags(PostId, TagId), populated transactionally with the post. That object was not supplied, and introducing it would require a data migration and application changes, so no executable script is recommended in this report.Confidence: High for the access-path limitation; medium for migration suitability.
Locking and blocking analysis
No blocking-chain data, wait statistics, deadlock graph, transaction log evidence, or execution plans were supplied. Consequently, no blocking diagram can be rendered and no wait duration or deadlock frequency can be stated from the input.
dbo.usp_UpdateTablesWithDelay holds the transaction open across line 25's five-second delay. This is a deterministic code-path risk even though the input does not prove that a blocking incident occurred.
The procedure's SET XACT_ABORT ON at line 10 is appropriate for an all-or-nothing update. The issue is transaction duration, not the use of an explicit transaction.
Indexes and storage impact
The usage summary covers only the 37.5 hours since startup. All 16 displayed indexes report zero seeks, scans, lookups, and updates during that interval. The largest reported indexes are the clustered PostHistory primary key at 820.3 MB, the clustered Posts primary key at 400.3 MB, and the clustered Comments primary key at 78.1 MB.
| Index observation | Interpretation | Recommendation |
|---|---|---|
PK__PostHist__3214EC078EEC0060, 820.3 MB | Zero usage counters in the sampled interval | Do not drop; it is the clustered primary key and supports row identity and foreign-key access patterns. |
PK__Posts__3214EC07AF21AC3E, 400.3 MB | Zero usage counters in the sampled interval | Do not drop; it is the clustered primary key. |
IX_Posts_Question_TagScan, 10.5 MB | Key is PostTypeId; tag predicate remains leading-wildcard | Do not modify solely from this snapshot. Reassess after representative workload capture. |
IX_Votes_PostId_VoteType, 9.8 MB | Zero usage counters, but many reviewed procedures join votes by PostId | Retain pending a longer observation period; zero usage may reflect the restart boundary. |
IX_Posts_OwnerUserId_Cover, 2.4 MB | Supports frequent owner-based post access in the supplied code | Retain pending representative workload evidence. |
New-index estimate: no new index is recommended in this run. Therefore, estimated new storage is 0 MB.
Dropped-index estimate: no index is recommended for removal. Therefore, estimated reclaimed storage is 0 MB.
Modified-index estimate: no index is recommended for modification. Therefore, before size, after size, and net change are not applicable.
Net storage change for all recommendations: 0 MB. This is based on making no physical index changes because the input lacks a representative observation period, index definitions for all possible access paths, column-width statistics for missing candidates, and measured query demand.
Object-specific findings
| Object or pattern | Finding | Priority |
|---|---|---|
dbo.sp00060, lines 21–25 | Leading-wildcard tag matching; likely scan-heavy | High |
dbo.sp00060, lines 36–45 | Independent one-to-many joins multiply aggregates | High |
dbo.sp00060, line 79 | User ID joined to post ID | Critical correctness |
dbo.usp_UpdateTablesWithDelay, lines 13–36 | Five-second wait occurs inside transaction | High |
dbo.usp_UpdateTablesInLoop, lines 27–33 | ORDER BY NEWID() repeated against large tables | Medium |
dbo.usp_UpdateTablesInLoop, line 77 | Possible divide-by-zero for zero loop count | Medium |
dbo.sp09995, lines 51–53 | Ordering and pagination inside a CTE is unnecessary complexity; vote aggregation scans all votes | Medium |
dbo.sp09994, lines 21–28 | Comments and votes joined before aggregation, causing multiplication | Medium |
dbo.sp09971, lines 14–18 | Grouping by post makes row numbering by post redundant | Low |
dbo.sp09974, lines 19–31 and 48–50 | Independent top-user and top-post result sets are cross joined | High |
dbo.sp09990, lines 28–40 and 71–74 | Popular tags are calculated using non-sargable matching and then broadly joined | High |
dbo.sp09991, lines 15–21 | Posts and votes are joined before user-level aggregation | Medium |
dbo.sp09898, lines 15–21 | Posts and badges are joined before aggregation, risking inflated counts | Medium |
dbo.sp09883, lines 33–37 | Post types are joined only on rp.PostId IS NOT NULL, creating a Cartesian-style relationship | High |
dbo.sp09812, lines 36–48 | Comments and history are independently joined to posts before owner aggregation | Medium |
| Procedures marked unused | Most supplied procedures have zero executions in the 30-day Query Store snapshot | Operational |
The remaining supplied procedures were reviewed for the same classes of issues: non-sargable predicates, repeated full-table aggregates, one-to-many join multiplication, display-name joins, fixed dates, unnecessary ranking, and broad cross joins. They are not individually rewritten because the input provides no measured plans or execution metrics and changing their output semantics safely would require a procedure-by-procedure contract.
Scripts
Correct dbo.sp00060 aggregation and user-to-post relationship — Recommendation 1
USE [SQLStorm];
GO
CREATE OR ALTER PROCEDURE [dbo].[sp00060]
AS
BEGIN
SET NOCOUNT ON;
;WITH RankedPosts AS
(
SELECT
p.Id AS PostId,
p.OwnerUserId,
p.Title,
p.CreationDate,
ROW_NUMBER() OVER
(
PARTITION BY t.Id
ORDER BY p.CreationDate DESC, p.Id DESC
) AS rn,
t.TagName
FROM dbo.Posts AS p
INNER JOIN dbo.Tags AS t
ON p.Tags LIKE '%' + t.TagName + '%'
WHERE p.PostTypeId = 1
),
PostCounts AS
(
SELECT
p.OwnerUserId AS UserId,
COUNT_BIG(*) AS TotalPosts
FROM dbo.Posts AS p
WHERE p.OwnerUserId IS NOT NULL
GROUP BY p.OwnerUserId
),
VoteCounts AS
(
SELECT
p.OwnerUserId AS UserId,
SUM(CASE WHEN v.VoteTypeId = 2 THEN CONVERT(bigint, 1) ELSE CONVERT(bigint, 0) END) AS UpVotes,
SUM(CASE WHEN v.VoteTypeId = 3 THEN CONVERT(bigint, 1) ELSE CONVERT(bigint, 0) END) AS DownVotes
FROM dbo.Posts AS p
LEFT JOIN dbo.Votes AS v
ON v.PostId = p.Id
AND v.VoteTypeId IN (2, 3)
WHERE p.OwnerUserId IS NOT NULL
GROUP BY p.OwnerUserId
),
BadgeCounts AS
(
SELECT
b.UserId,
COUNT_BIG(*) AS BadgeCount
FROM dbo.Badges AS b
GROUP BY b.UserId
),
TopUsers AS
(
SELECT
u.Id AS UserId,
u.DisplayName,
CONVERT(int, ISNULL(pc.TotalPosts, 0)) AS TotalPosts,
CONVERT(int, ISNULL(vc.UpVotes, 0)) AS UpVotes,
CONVERT(int, ISNULL(vc.DownVotes, 0)) AS DownVotes,
CONVERT(int, ISNULL(bc.BadgeCount, 0)) AS BadgeCount,
DENSE_RANK() OVER
(
ORDER BY
ISNULL(pc.TotalPosts, 0) DESC,
ISNULL(vc.UpVotes, 0) - ISNULL(vc.DownVotes, 0) DESC
) AS UserRank
FROM dbo.Users AS u
LEFT JOIN PostCounts AS pc
ON pc.UserId = u.Id
LEFT JOIN VoteCounts AS vc
ON vc.UserId = u.Id
LEFT JOIN BadgeCounts AS bc
ON bc.UserId = u.Id
WHERE ISNULL(pc.TotalPosts, 0) > 10
)
SELECT
tu.UserRank,
tu.DisplayName,
tu.TotalPosts,
tu.UpVotes,
tu.DownVotes,
tu.BadgeCount,
rp.Title AS LatestPostTitle,
rp.CreationDate AS LatestPostDate,
CASE
WHEN rp.rn = 1 THEN 'Latest'
ELSE 'Older'
END AS PostStatus
FROM TopUsers AS tu
LEFT JOIN RankedPosts AS rp
ON rp.OwnerUserId = tu.UserId
ORDER BY
tu.UserRank,
rp.CreationDate DESC,
rp.PostId DESC;
END;
GO
Move the deliberate delay outside the transaction — Recommendation 2
USE [SQLStorm];
GO
CREATE OR ALTER PROCEDURE dbo.usp_UpdateTablesWithDelay
@UserId INT,
@NewReputation INT,
@PostId INT,
@NewScore INT
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
WAITFOR DELAY '00:00:05';
BEGIN TRANSACTION;
UPDATE dbo.Users
SET Reputation = @NewReputation,
LastAccessDate = GETDATE()
WHERE Id = @UserId;
DECLARE @RowsAffected1 INT = @@ROWCOUNT;
UPDATE dbo.Posts
SET Score = @NewScore,
LastActivityDate = GETDATE()
WHERE Id = @PostId;
DECLARE @RowsAffected2 INT = @@ROWCOUNT;
COMMIT TRANSACTION;
SELECT
'Success' AS Status,
'Both tables updated successfully' AS Message,
@RowsAffected1 AS UsersRowsAffected,
@RowsAffected2 AS PostsRowsAffected;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
SELECT
'Error' AS Status,
ERROR_NUMBER() AS ErrorNumber,
ERROR_MESSAGE() AS ErrorMessage,
ERROR_LINE() AS ErrorLine,
ERROR_PROCEDURE() AS ErrorProcedure;
THROW;
END CATCH
END;
GO
Validate loop count and make success-rate calculation safe — Recommendation 3
USE [SQLStorm];
GO
CREATE OR ALTER PROCEDURE dbo.usp_UpdateTablesInLoop
@LoopCount INT = 10,
@DelayBetweenCalls VARCHAR(8) = '00:00:01'
AS
BEGIN
SET NOCOUNT ON;
IF @LoopCount < 1
THROW 50001, 'LoopCount must be greater than zero.', 1;
DECLARE @Counter INT = 0;
DECLARE @RandomUserId INT;
DECLARE @RandomPostId INT;
DECLARE @NewReputation INT;
DECLARE @NewScore INT;
DECLARE @SuccessCount INT = 0;
DECLARE @ErrorCount INT = 0;
PRINT 'Starting loop execution at ' + CONVERT(varchar(50), GETDATE());
PRINT 'Will execute ' + CONVERT(varchar(10), @LoopCount) + ' iterations';
PRINT REPLICATE('-', 80);
WHILE @Counter < @LoopCount
BEGIN
SET @Counter += 1;
BEGIN TRY
SET @RandomUserId = NULL;
SET @RandomPostId = NULL;
SELECT TOP (1)
@RandomUserId = Id
FROM dbo.Users
ORDER BY NEWID();
SELECT TOP (1)
@RandomPostId = Id
FROM dbo.Posts
ORDER BY NEWID();
SET @NewReputation = ABS(CHECKSUM(NEWID())) % 10000;
SET @NewScore = ABS(CHECKSUM(NEWID())) % 100 - 10;
EXEC dbo.usp_UpdateTablesWithDelay
@UserId = @RandomUserId,
@NewReputation = @NewReputation,
@PostId = @RandomPostId,
@NewScore = @NewScore;
SET @SuccessCount += 1;
END TRY
BEGIN CATCH
SET @ErrorCount += 1;
PRINT 'Iteration ' + CONVERT(varchar(10), @Counter) +
' failed: ' + ERROR_MESSAGE();
END CATCH;
IF @Counter < @LoopCount
WAITFOR DELAY @DelayBetweenCalls;
END;
SELECT
@LoopCount AS TotalIterations,
@SuccessCount AS Successful,
@ErrorCount AS Errors,
CONVERT(decimal(5,2), @SuccessCount * 100.0 / NULLIF(@LoopCount, 0)) AS SuccessRate_Percent;
END;
GO
Confidence and limitations
- High confidence:
dbo.sp00060contains an invalid user-ID-to-post-ID relationship at line 79 and multiplies independent one-to-many joins at lines 36–45. - High confidence:
dbo.usp_UpdateTablesWithDelayholds locks across the five-second wait at line 25. - High confidence: leading-wildcard tag predicates are not ordinarily seekable through the supplied B-tree indexes.
- Medium confidence: the precise runtime cost of the reviewed procedures, because Query Store durations, plans, reads, CPU, memory grants, spills, and wait data are absent.
- Low-to-medium confidence: the intended business output of procedures containing cross joins or fixed dates; those procedures are not rewritten without a reliable contract.
No functions, triggers, views, NOT INCLUDED objects, or NOT AVAILABLE objects were supplied. Recommendations are limited to code that was shown. The report does not recommend changes to unseen code.