When one of my clients asked me for a reason to upgrade to SQL 2019, the first thing that came to mind is Scalar-valued function inlining. Reason? Well, for some reason developers like stuffing their SQL with SVFs, and that… makes… it… so… slow. Are SVFs really that bad? And is scalar-valued function inlining useful at all? Let’s walk through it together and find out!
First off, I’m using the opensource database StackOverflow2013. I’m going to start off with creating a SVF, which’ll just count the amount of votes in the Votes table for a specific UserId.
USE tempdb
CREATE OR ALTER FUNCTION dbo.svf_YesWeCan(@UserId INT)
RETURNS INT
WITH RETURNS NULL ON NULL INPUT
AS
BEGIN
DECLARE @Count INT
SELECT @Count = COUNT(*)
FROM dbo.Votes
WHERE UserId = @UserID
RETURN @Count
END
GO
Make sure we’re using an older compat level, I’ll be using 130 for this example.
ALTER DATABASE StackOverflow2013
SET COMPATIBILITY_LEVEL = 130
And I also created this index to support my SVF.
CREATE INDEX NCI_UserId ON dbo.Votes( UserId )
And I’m adding a data filter, just so I don’t have to wait for too long.
SET STATISTICS TIME,IO ON
SELECT dbo.svf_YesWeCan(Id),*
FROM dbo.Users
WHERE CreationDate > '20130501' AND CreationDate < '20130601'
GO
If we look at the query plan afterwards:
(98144 rows affected)
Table ‘Users’. Scan count 1, logical reads 44492, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 4, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
(1 row affected)
SQL Server Execution Times:
CPU time = 6860 ms, elapsed time = 7153 ms.
But…, that doesn’t even look that bad? So are scalar-valued functions not really an issue? Well, a careful eye would have noticed we’re missing something here. The SVF I made queries data from the Votes table, yet nowhere in the stats or in the plan we can see the votes.
Let’s go over some downsides of Scalar valued functions.
So, in short, you’ll have a single core query, plowing through all your rows, and then, not admitting what it did in the plan. Now, how can this be fixed? Currently one of the most used fixes for this (if you can change the code) is to turn the SVF into an inlined table valued function, like this:
CREATE OR ALTER FUNCTION dbo.TVF_YesWeCan
(
@UserId INT
)
RETURNS TABLE
AS
RETURN
(
SELECT COUNT(*) Total
FROM dbo.Votes
WHERE UserId = @UserID
)GO
And then, we can use it in our query:
SELECT TVF.Total,*
FROM dbo.Users
CROSS APPLY dbo.TVF_YesWeCan(Id) TVF
WHERE CreationDate > '20130501'
(98144 rows affected)
Table ‘Votes’. Scan count 9, logical reads 92352, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table ‘Users’. Scan count 9, logical reads 44994, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 4, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
(1 row affected)
SQL Server Execution Times:
CPU time = 15175 ms, elapsed time = 3545 ms.
While this plan looks more complicated, it’s a lot more transparent to us. We can also see that the query went parallel, which is good for this query because it allows it to go much faster.
“But I’m not allowed to change any code in my current environment” Fair point! This is where scalar-valued function inlining comes in. We alter the compat level to SQL 2019.
ALTER DATABASE StackOverflow2013
SET COMPATIBILITY_LEVEL = 150
We run the same query as before with the SVF:
SET STATISTICS TIME,IO ON
SELECT dbo.svf_YesWeCan(Id),*
FROM dbo.Users
WHERE CreationDate > '20130501' AND CreationDate < '20130601'
GO
(98144 rows affected)
Table ‘Votes’. Scan count 98144, logical reads 294690, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
Table ‘Users’. Scan count 1, logical reads 44492, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 4, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
(1 row affected)
SQL Server Execution Times:
CPU time = 718 ms, elapsed time = 1496 ms.
What kind of magic is this? While the plan looks more complicated, the SQL optimizer knows what to do and creates the best ‘good-enough-plan’ for execution!
If we compare the different execution times for the queries:
SVF compat 130 | 7153 ms |
iTVF | 3545 ms |
SVF compat 150 | 1496 ms |
“Does this mean I can use SVF everywhere now without my DBAs complaining?” Probably not. The feature isn’t perfect and it definitely has some limitations, but that’s a bit too much for this post. However, I’m a big fan of trialing your current workload on a SQL2019, and perhaps this magic will do wonders for you as well!
Cookie | Duration | Description |
---|---|---|
ARRAffinity | session | ARRAffinity cookie is set by Azure app service, and allows the service to choose the right instance established by a user to deliver subsequent requests made by that user. |
ARRAffinitySameSite | session | This cookie is set by Windows Azure cloud, and is used for load balancing to make sure the visitor page requests are routed to the same server in any browsing session. |
cookielawinfo-checkbox-advertisement | 1 year | Set by the GDPR Cookie Consent plugin, this cookie records the user consent for the cookies in the "Advertisement" category. |
cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
CookieLawInfoConsent | 1 year | CookieYes sets this cookie to record the default button state of the corresponding category and the status of CCPA. It works only in coordination with the primary cookie. |
elementor | never | The website's WordPress theme uses this cookie. It allows the website owner to implement or change the website's content in real-time. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | Cloudflare set the cookie to support Cloudflare Bot Management. |
pll_language | 1 year | Polylang sets this cookie to remember the language the user selects when returning to the website and get the language information when unavailable in another way. |
Cookie | Duration | Description |
---|---|---|
_ga | 1 year 1 month 4 days | Google Analytics sets this cookie to calculate visitor, session and campaign data and track site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognise unique visitors. |
_ga_* | 1 year 1 month 4 days | Google Analytics sets this cookie to store and count page views. |
_gat_gtag_UA_* | 1 minute | Google Analytics sets this cookie to store a unique user ID. |
_gid | 1 day | Google Analytics sets this cookie to store information on how visitors use a website while also creating an analytics report of the website's performance. Some of the collected data includes the number of visitors, their source, and the pages they visit anonymously. |
ai_session | 30 minutes | This is a unique anonymous session identifier cookie set by Microsoft Application Insights software to gather statistical usage and telemetry data for apps built on the Azure cloud platform. |
CONSENT | 2 years | YouTube sets this cookie via embedded YouTube videos and registers anonymous statistical data. |
vuid | 1 year 1 month 4 days | Vimeo installs this cookie to collect tracking information by setting a unique ID to embed videos on the website. |
Cookie | Duration | Description |
---|---|---|
ai_user | 1 year | Microsoft Azure sets this cookie as a unique user identifier cookie, enabling counting of the number of users accessing the application over time. |
VISITOR_INFO1_LIVE | 5 months 27 days | YouTube sets this cookie to measure bandwidth, determining whether the user gets the new or old player interface. |
YSC | session | Youtube sets this cookie to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt-remote-device-id | never | YouTube sets this cookie to store the user's video preferences using embedded YouTube videos. |
yt.innertube::nextId | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen. |
Cookie | Duration | Description |
---|---|---|
WFESessionId | session | No description available. |