Query rewriteIdentity-verification SaaS company, anonymized × red9CS-0468
Rewriting NOT IN as NOT EXISTS took a 53-minute query to 92 milliseconds
The problem. An identity-verification SaaS company had an auto-ship customer query running for 53 minutes, or 3,200,290 ms, per execution. It read 942,222,862 pages doing so, and with 12 sessions running it concurrently the instance sat at around 70% CPU. The plan said the logic itself was the problem.
What we did. We read the statement rather than the wait stats. It used a NOT IN operator against a view, and in this plan that produced an expensive anti-join across the whole view result. We rewrote it as NOT EXISTS, which lets the engine stop at the first match per row. Testing ran against a restored copy of the database so a change of this size could be validated safely.
Red9 · Performance Impact
Per-run duration
53 min → 92 ms
No new index; the logic itself was the fix.
Total logical reads
31,084x
942,222,862 down to 30,312.
Concurrent sessions
12
All running the same statement, holding CPU near 70% on the instance.
Duration
3,200,290 ms → 92 ms
34,786x shorter
Logical reads
942,222,862 → 30,312
31,084x fewer
CPU during the run
~70%
before the rewrite
Auto-ship query
duration, per run
How the math works. The 34,786x is 3,200,290 ms over 92 ms; the 31,084x is 942,222,862 reads over 30,312. The ~70% CPU is what was observed on the instance while 12 sessions ran the old version. The measurement was taken on a restored copy of the production database, which is how the client chose to validate a rewrite of this size.
The result. A statement that occupied a server for the better part of an hour now returns in under a tenth of a second, and it reads 30,312 pages instead of 942 million. On the restored copy, 12 concurrent sessions of the new version cost a fraction of what the old one did.
The technical detail
What the review found. In this plan the NOT IN form produced a costly anti-join, and nullable values make NOT IN behave differently from NOT EXISTS. The NOT EXISTS form gave the engine an anti-semi-join it could short-circuit, and the read count fell from 942,222,862 to 30,312.
What we changed (identifiers generalized for privacy):
-- Auto-ship customer query: 3,200,290 ms, 942,222,862 reads, 12 concurrent sessions.
-- before: WHERE customerId NOT IN (SELECT customerId FROM dbo.v_excluded_customers)
-- after:
WHERE NOT EXISTS (SELECT 1 FROM dbo.v_excluded_customers x
WHERE x.customerId = c.customerId)
-- rollback: prior statement text retained
One operator change, no schema change, and the query went from 53 minutes to 92 milliseconds.