sp_prepare For Mediocre
You may remember me from movies like Optimize for… Mediocre? and Why You’re Tuning Stored Procedures Wrong (the Problem with Local Variables)!
Great posts, Kendra!
Following the same theme, we found this issue while looking at queries issued from JDBC. Specifically, the prepared statement class seems to cause queries to hit the density vector rather than the histogram for cardinality estimation.
Puddin’
Let’s create an index to make our query’s work easy!
CREATE INDEX ix_whatever ON dbo.Users (Reputation) INCLUDE (DisplayName);
Now, to mimic the behavior of a JDBC query:
DECLARE @out INT; EXEC sys.sp_prepare @out OUTPUT, N'@r INT', N'SELECT COUNT(DisplayName) as records FROM dbo.Users AS u WHERE u.Reputation = @r;', 1; EXEC sys.sp_execute @out, 1; EXEC sys.sp_execute @out, 2; EXEC sys.sp_execute @out, 6; EXEC sys.sp_execute @out, 10; EXEC sys.sp_unprepare @out; GO
The query plans for all of these have something in common. They have the exact same estimate!
You might be saying to yourself that the first parameter is sniffed, and you’d be wrong.
That estimate exactly matches the density vector estimate that I’d get with a local variable or optimize for unknown: SELECT (7250739 * 5.280389E-05)
You can validate things a bit by adding a recompile hint to the demo code.
DECLARE @out INT; EXEC sys.sp_prepare @out OUTPUT, N'@r INT', N'SELECT COUNT(DisplayName) as records FROM dbo.Users AS u WHERE u.Reputation = @r OPTION(RECOMPILE);', 1; EXEC sys.sp_execute @out, 1; EXEC sys.sp_execute @out, 2; EXEC sys.sp_execute @out, 6; EXEC sys.sp_execute @out, 10; EXEC sys.sp_unprepare @out; GO
The plans for all of the recompiled queries get different estimates, and no estimate matches the 382 estimate we saw from the first round.
Am I saying you should recompile all of your queries to get around this?
No, of course not. Query compilation isn’t what you should be spending your SQL Server licensing money on.
You may want to not use JDBC anymore, but…
How Is sp_executesql Different?
Well, sp_executesql “sniffs” parameters.
DECLARE @sql NVARCHAR(MAX ) = N'' SET @sql += N'SELECT COUNT(DisplayName) as records FROM dbo.Users AS u WHERE u.Reputation = @r;' EXEC sys.sp_executesql @sql, N'@r INT', 1; EXEC sys.sp_executesql @sql, N'@r INT', 2; EXEC sys.sp_executesql @sql, N'@r INT', 6; EXEC sys.sp_executesql @sql, N'@r INT', 10; GO
If I run my demo queries in this order, the plan for Reputation = 1 gets cached and reused by all the other calls.
If I change the order so Reputation = 2 runs first, the plans change (after clearing the plan out of the cache, of course).
Now they all reuse that plan:
Why Is One better?
I put together this handy chart!
I’m not smart enough to get a formatted table like this into a web page.
I’m a bad DBA.
Thanks for reading!
UPDATE:
The admirable and honorable Joseph Gooch notes in the comments that you can configure this with the jTDS JDBC driver:
prepareSQL
(default –3
for SQL Server,1
for Sybase)This parameter specifies the mechanism used for Prepared Statements.
Value Description 0
SQL is sent to the server each time without any preparation, literals are inserted in the SQL (slower) 1
Temporary stored procedures are created for each unique SQL statement and parameter combination (faster) 2
sp_executesql is used (fast) 3
sp_prepare and sp_cursorprepare are used in conjunction with sp_execute and sp_cursorexecute (faster, SQL Server only)
Though I’m not too thrilled that sp_prepare is called “faster”.
And! That similar options are available in the 6.1.6 preview of the Microsoft JDBC drivers.
Our live class lineup has 2 new ones: Database DevOps and Practical Real World Performance Tuning in 4 Hours.