Scalar function removal and indexingProperty software company, anonymized × red9CS-0291
Removing scalar functions from one query returned 12.5 hours of SQL work a day
The problem. A property software company had a query built on a scalar user-defined function that assembled person data. It ran about 700 times a day at roughly 67 seconds each, which came to something near 13 hours of SQL work daily, and the function forced single-threaded plans so the server could not use the cores it had.
What we did. We reworked the person and address objects to remove the computed columns and scalar functions in favor of persisted values, merged the address calculation into a single function, added an index on the phone lookup path, and replaced the function calls with a precalculated address field. Two further top queries were indexed at the same time.
Red9 · Performance Impact
Daily SQL work returned
~12.5 hrs / day
From one query running about 700 times a day.
Per-run duration
34x
About 67 seconds down to roughly two.
Reads per run
805,838 → 12,703
A 63-fold drop, once the row-by-row function work was gone.
Duration
~67 s → ~2 s
34x shorter
Reads
805,838 → 12,703
63x less
Second query
580 ms → 1 ms
~70,000 runs/day
Function query
duration, per run
How the math works. The 34x divides about 67,000 ms by about 2,000 ms, and the 63x divides 805,838 reads by 12,703. The 12.5 hours a day is roughly 700 daily runs multiplied by the 65 seconds each one no longer takes, and it matches the report's own figure. A second query at about 70,000 runs a day went from 580 ms to 1 ms with reads from 518,566 to five, worth another 11 hours daily on the client's figures.
The result. The function-driven query returns in about two seconds instead of over a minute, and the estate got back roughly 12.5 hours of daily SQL work from that change alone, with a further 11 hours from the second query. The application kept its behavior; only the way the values are produced changed.
The technical detail
What the review found. A scalar user-defined function was being evaluated per row, and computed columns compounded it, which both multiplied the reads and blocked parallelism. No index would have rescued that shape of query.
What we changed (identifiers generalized for privacy):
-- Function-backed query: ~700 runs/day at ~67 s, 805,838 reads, single-threaded plans.
-- computed columns and scalar UDF calls replaced with persisted / precalculated values
-- address calculation consolidated into one function, called once per set
CREATE NONCLUSTERED INDEX IX_red9_person_phone_type
ON dbo.[person_phone] (personTypeId) INCLUDE (personId, phoneNumber);
-- two further top queries indexed in the same pass; rollbacks supplied
Getting the function out of the row path is what allowed a parallel plan and dropped the reads from 805,838 to 12,703.