Covering index on a refresh queueHospitality software company, anonymized × red9CS-0234
A daily revenue refresh went from 752 million reads to under four million
The problem. A hospitality software company ran a revenue refresh procedure about 7,007 times a day, averaging 260.74 ms per call with 1,234.76 ms of CPU. The CPU figure being nearly five times the duration meant several cores were working inside every call. Across the day the procedure drove 751,802,051 logical reads.
What we did. We captured the Query Store baseline, then created one nonclustered index keyed on the three columns the procedure filters, with the status description carried as an included column so the engine never returns to the base table, and captured the same statistics again.
Red9 · Performance Impact
Daily reads
752M → 4M
751,802,051 down to 3,965,962 across the day.
CPU per call
1,235x
1,234.76 ms down to about 1 ms.
Per-call duration
260.74 → 2.29 ms
A 114-fold cut, worth about 30 minutes of SQL processing a day.
Duration
260.74 ms → 2.29 ms
114x shorter
CPU
1,234.76 ms → 1 ms
1,235x less
Reads per call
107,293 → 566
190x less
Daily reads
for this procedure
How the math works. The 114x divides 260.74 ms by 2.29 ms. The CPU multiple of 1,235x comes from the report, which records the after-value as about 1 ms. Reads per call fell from 107,293 to 566, and the daily total from 751,802,051 to 3,965,962. CPU above duration on the original is not an error in the report: it is several cores accumulating time in parallel inside one call, and it is the clearest sign that a scan is being brute-forced.
The result. The refresh completes in 2.29 ms using about a millisecond of CPU, and the daily read volume fell by roughly 748 million. About 30 minutes of daily SQL processing went back to the platform, and the cores that had been recruited for the scan went with it.
The technical detail
What the review found. The procedure filtered a queue table on a plan, month, and segment combination with no index behind it, so each of its 7,007 daily calls scanned and parallelized. The status column it returned forced a lookup on every row it kept.
What we changed (identifiers generalized for privacy):
-- Revenue refresh: ~7,007 calls/day, 260.74 ms each, 1,234.76 ms CPU, 752M reads/day.
CREATE NONCLUSTERED INDEX IX_red9_revenue_queue_plan_month_segment
ON dbo.[revenue_refresh_queue] (planId, monthId, segmentId) INCLUDE (statusDesc);
-- before/after captured with Query Store; DROP INDEX rollback provided
Three key columns and one include, and the parallel scan turned into a seek.