From 9a3340d98506264c77affc885129dcf7c06a6b91 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:07:24 -0800 Subject: [PATCH] parameter sensitivity training --- Presentations/Parameters/00 Demo.sql | 1668 +++++++++++++++++ .../Parameters/01 VoteTypeId2.sqlplan | Bin 0 -> 90574 bytes Presentations/Parameters/02 PSPO Oddities.sql | 287 +++ 3 files changed, 1955 insertions(+) create mode 100644 Presentations/Parameters/00 Demo.sql create mode 100644 Presentations/Parameters/01 VoteTypeId2.sqlplan create mode 100644 Presentations/Parameters/02 PSPO Oddities.sql diff --git a/Presentations/Parameters/00 Demo.sql b/Presentations/Parameters/00 Demo.sql new file mode 100644 index 00000000..fe62a35f --- /dev/null +++ b/Presentations/Parameters/00 Demo.sql @@ -0,0 +1,1668 @@ +USE StackOverflow2013; +SET NOCOUNT ON; +EXECUTE dbo.DropIndexes; +DBCC FREEPROCCACHE; +ALTER DATABASE StackOverflow2013 +SET COMPATIBILITY_LEVEL = 160; +GO + +/* +Notes for Erik: + * Start creating the below indexes + * Set font percentage to 205% + * Turn on execution plans! + * Make sure *you're* connected to Wi-Fi + * Double check VM network is using Wi-Fi + +*/ + +CREATE INDEX + VoteTypeId_UserId_PostId +ON dbo.Votes + (VoteTypeId, UserId, PostId) +INCLUDE + (BountyAmount, CreationDate) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +CREATE INDEX + Score +ON dbo.Comments + (Score) +INCLUDE + (UserId, PostId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +CREATE INDEX + UserId +ON dbo.Badges + (UserId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +CREATE INDEX + ParentId +ON dbo.Posts + (ParentId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + + +/* +██████╗ █████╗ ██████╗ █████╗ ███╗ ███╗███████╗████████╗███████╗██████╗ +██╔══██╗██╔══██╗██╔══██╗██╔══██╗████╗ ████║██╔════╝╚══██╔══╝██╔════╝██╔══██╗ +██████╔╝███████║██████╔╝███████║██╔████╔██║█████╗ ██║ █████╗ ██████╔╝ +██╔═══╝ ██╔══██║██╔══██╗██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██╔══╝ ██╔══██╗ +██║ ██║ ██║██║ ██║██║ ██║██║ ╚═╝ ██║███████╗ ██║ ███████╗██║ ██║ +╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + +███████╗███████╗███╗ ██╗███████╗██╗████████╗██╗██╗ ██╗██╗████████╗██╗ ██╗ +██╔════╝██╔════╝████╗ ██║██╔════╝██║╚══██╔══╝██║██║ ██║██║╚══██╔══╝╚██╗ ██╔╝ +███████╗█████╗ ██╔██╗ ██║███████╗██║ ██║ ██║██║ ██║██║ ██║ ╚████╔╝ +╚════██║██╔══╝ ██║╚██╗██║╚════██║██║ ██║ ██║╚██╗ ██╔╝██║ ██║ ╚██╔╝ +███████║███████╗██║ ╚████║███████║██║ ██║ ██║ ╚████╔╝ ██║ ██║ ██║ +╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝ + +████████╗██████╗ █████╗ ██╗███╗ ██╗██╗███╗ ██╗ ██████╗ +╚══██╔══╝██╔══██╗██╔══██╗██║████╗ ██║██║████╗ ██║██╔════╝ + ██║ ██████╔╝███████║██║██╔██╗ ██║██║██╔██╗ ██║██║ ███╗ + ██║ ██╔══██╗██╔══██║██║██║╚██╗██║██║██║╚██╗██║██║ ██║ + ██║ ██║ ██║██║ ██║██║██║ ╚████║██║██║ ╚████║╚██████╔╝ + ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ + + Erik Darling +(Consultare Maximus - Rationabile Pretium) + +W: https://erikdarling.com +E: mailto:erik@erikdarling.com +T: https://twitter.com/erikdarlingdata +T: https://www.tiktok.com/@darling.data +L: https://www.linkedin.com/company/darling-data/ +Y: https://www.youtube.com/@ErikDarlingData + +Demos: +Database: https://go.erikdarling.com/Stack2013 + +*/ + + +/* +Let's start by defining some important terms here: + +Parameter Sniffing: + * When the optimizer uses the value(s) assigned + to one or more formal parameters for cardinality + estimation in order to compile (and cache!) a + query's execution plan for reuse in the future. + + This is generally a good thing, as it reduces + plan compilation overhead on busy OLTP systems. + + +Parameter Sensitivity: + * When a cached execution plan involving one or + more formal parameter values has far different + cardinality estimates than the current runtime + parameter value(s) would produce, to the point + that a different query execution plan would be + more appropriate and perform better than current. + + This is the problem that we're talking about today. + + +General Performance Issues: + * When people do all manner of terrible things to + their SQL Server, and spend all day wondering if + it's parameter sniffing, including but not limited + to: local variables, table variables, scalar UDFs, + multi-statement table valued functions, non-SARGable + predicates, implicit conversions, XML, JSON, string + splitting, any ORM, and poorly conceived tables, + columns, and indexes (or lack of indexes, probably), + and not keeping statistics reasonably up to date. + + We also need to separate "sometimes slow" queries + that are "sometimes slow" for other reasons, like + being blocked, reading from disk, and many others. + +For example: + +*/ + +CHECKPOINT; +DBCC DROPCLEANBUFFERS; +GO + +EXECUTE sys.sp_executesql + N' + SELECT + c = COUNT_BIG(*) + FROM dbo.Posts AS p + WHERE p.Score = @Score; + ', + N'@Score integer', + -166; +GO + +EXECUTE sys.sp_executesql + N' + SELECT + c = COUNT_BIG(*) + FROM dbo.Posts AS p + WHERE p.Score = @Score; + ', + N'@Score integer', + -146; +GO + +/* +Query 1: + * Reads from disk, look at: + * Operator times? + * ~6.5 seconds + * Wait stats? + * ~40 seconds PAGEIOLATCH_SH + +Query 2: + * Reads from buffer pool! + * Operator times? + * ~630ms + * Wait stats? + * ~630ms CXSYNC_PORT + +Is this sort of performance difference worrying? + +Yes, of course. + +But it's not parameter sensitivity. + * There was nothing ill-fitting about the plan + * Okay there's an obvious missing index but... + * The only detriment was cached data availability + * This query really just needs an index + +What kind of code is parameter sensitive? + * Touches tables with skewed data in them, or + search arguments use some form of range search: + * >, >=, <, <=, LIKE, IN/NOT IN(list), etc. + * Uses formal parameters, not local variables + * Literal values would also qualify if forced + parameterization is enabled for the database, + or if simple parameterization is used, but... + * We're not too concerned with simple parameterization + * It's for queries that qualify for trivial plans + * There usually aren't cost-based choices in those + +Having choices is really what messes the optimizer up. + +*/ + +SELECT + c = COUNT_BIG(*) +FROM dbo.Users AS u +WHERE u.Reputation = 2; + +SELECT + c = COUNT_BIG(*) +FROM dbo.Users AS u +WHERE u.Reputation = 1; + + +/* +The two most common vehicles for parameterized code +in SQL Server are Stored Procedures and queries that +are executed via sp_executesql (dynamic or application). + +The main distinction between formal parameters & local +variables is an important one for our purposes, since +values assigned to variables are not currently sniffed +at compile or execution time (without recompiling, yes). + +*/ + +/*We've got this index.*/ +CREATE INDEX + ParentId +ON dbo.Posts + (ParentId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); + + +/*Sometimes it matters.*/ +DECLARE + @ParentId integer = 0; + +SELECT + s = SUM(p.Score), + c = COUNT_BIG(*) +FROM dbo.Posts AS p +WHERE p.ParentId = @ParentId; +GO + +/*Sometimes it doesn't.*/ +DECLARE + @ParentId integer = 184618; + +SELECT + s = SUM(p.Score), + c = COUNT_BIG(*) +FROM dbo.Posts AS p +WHERE p.ParentId = @ParentId; +GO + +/*Distribution?*/ +SELECT TOP (10) + p.ParentId, + Total = FORMAT(COUNT_BIG(*), 'N0') +FROM dbo.Posts AS p +GROUP BY + p.ParentId +ORDER BY + COUNT_BIG(*) DESC; +GO + + +/*Clean up*/ +EXECUTE dbo.DropIndexes + @TableName = N'Posts'; + +/* +How it works, in great detail: + * https://go.erikdarling.com/LocalVariables + +But Why? + +What actually happens now: + * Compile an executable plan faster, but with + an unknown estimate for the COUNT query that + uses it in the where clause (cardinality!) + +What could happen in the future: + * Defer compilation: Don't compile a plan for the + COUNT query until the variable has been assigned. + * Doing this every time: Lots of recompiling + * Doing this once: Feels like parameter sniffing + +The internals are there to do the second option, but +it has not been exposed to us yet. This does the same +general thing as table variable deferred compilation. + +People historically used local variables to avoid +sniffing, so T.P.T.B. decided not to break this. + +Despite having no problem breaking many other things. + +Before we move on, it's worth talking about the thing +everyone always talks about when talking about the +thing we're talking about: RECOMPILE! + +I am very much in favor of the recompile technique. + +But there's something people frequently mess up: + * Where to RECOMPILE + * Procedure level? + * Statement level? + * What's the difference? + +*/ + +CREATE INDEX + OwnerUserId_Score +ON dbo.Posts + (OwnerUserId, Score DESC) +INCLUDE + (PostTypeId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + + +CREATE OR ALTER PROCEDURE + dbo.WhereToRecompile +( + @OwnerUserId integer = NULL, + @CreationDate datetime = NULL, + @PostTypeId integer = NULL, + @Score integer = NULL +) +WITH + RECOMPILE /*This sucks.*/ +AS +BEGIN + SET NOCOUNT, XACT_ABORT ON; + + SELECT TOP (5000) + p.OwnerUserId, + p.Score, + Tags = ISNULL(p.Tags, N'N/A: Question'), + Title = ISNULL(p.Title, N'N/A: Question'), + p.CreationDate, + p.LastActivityDate, + p.Body + FROM dbo.Posts AS p + WHERE (p.OwnerUserId = @OwnerUserId OR @OwnerUserId IS NULL) + AND (p.CreationDate >= @CreationDate OR @CreationDate IS NULL) + AND (p.PostTypeId = @PostTypeId OR @PostTypeId IS NULL) + AND (p.Score >= @Score OR @Score IS NULL) + ORDER BY + p.Score DESC, + p.Id; + + SELECT TOP (5000) + p.OwnerUserId, + p.Score, + Tags = ISNULL(p.Tags, N'N/A: Question'), + Title = ISNULL(p.Title, N'N/A: Question'), + p.CreationDate, + p.LastActivityDate, + p.Body + FROM dbo.Posts AS p + WHERE (p.OwnerUserId = @OwnerUserId OR @OwnerUserId IS NULL) + AND (p.CreationDate >= @CreationDate OR @CreationDate IS NULL) + AND (p.PostTypeId = @PostTypeId OR @PostTypeId IS NULL) + AND (p.Score >= @Score OR @Score IS NULL) + ORDER BY + p.Score DESC, + p.Id + OPTION(RECOMPILE); /*This doesn't suck.*/ +END; +GO + +/*Example executions to get both query plans here.*/ +EXECUTE dbo.WhereToRecompile + @OwnerUserId = 22656; +GO + +/*Changes.*/ +EXECUTE dbo.WhereToRecompile + @CreationDate = '20131231'; +GO + +/* +This is the parameter embedding optimization, and it's +only available with statement level recompile. This is an +important difference, and why procedure level recompiles +(while they do recompile all plans) do not embed values. + +SQL Server 2025 has a new feature called OPPO, short for +the Optional Parameter Plan Optimization. I've had mixed +results with it in my testing, but maybe I'm just picky. + +We'll see how things go after a few cumulative updates. + +It's meant to address code patterns like the one above, +and it uses a similar framework to another feature (PSPO). + +*/ + +/*Show option recompile effect on cached values*/ +DECLARE + @start_date date = + CONVERT + ( + date, + SYSDATETIME() + ); + +EXECUTE dbo.sp_QuickieStore + @database_name = 'StackOverflow2013', + @procedure_name = 'WhereToRecompile', + @query_text_search = 'OPTION(RECOMPILE)', + @start_date = @start_date; + + +/* +Let's look at a minor parameter sensitivity example. + +What I'm going to show you how to do: + * Use Query Store to reproduce parameter problems + +*/ + +CREATE INDEX + DisplayName +ON dbo.Users + (DisplayName) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +/*Our hero*/ +CREATE OR ALTER PROCEDURE + dbo.DisplayNameSearcher +( + @DisplayName nvarchar(40) +) +AS +BEGIN + SET NOCOUNT, XACT_ABORT ON; + + SELECT + u.Id, + u.DisplayName, + TotalScore = SUM(p.Score) + FROM dbo.Users AS u + JOIN dbo.Posts AS p + ON p.OwnerUserId = u.Id + WHERE u.DisplayName LIKE @DisplayName + AND p.Score > 0 + GROUP BY + u.Id, + u.DisplayName + HAVING + SUM(p.Score) >= 5000 + ORDER BY + TotalScore DESC; +END; +GO + + +/*Resetter*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.DisplayNameSearcher'; + +/*John: 15k*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'John%'; + +/*User: 595k*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'User%'; + +/*bbum: 1*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'bbum%'; + +/*Resetter*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.DisplayNameSearcher'; + +/*User: 595k*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'User%'; + +/*John: 15k*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'John%'; + +/*bbum: 1*/ +EXECUTE dbo.DisplayNameSearcher + @DisplayName = N'bbum%'; + + +/* +This isn't possible with with plan cache + * Query Store makes it easy(er) + +*/ + +DECLARE + @start_date date = + CONVERT + ( + date, + SYSDATETIME() + ); + +EXECUTE dbo.sp_QuickieStore + @database_name = 'StackOverflow2013', + @procedure_name = 'DisplayNameSearcher', + @start_date = @start_date; + +/* +Here are some tips: + 1. Use Query Store + 2. Use Query Store + 3. Use Query Store + 4. Don't use the GUI, it's useless + * Whoever designed it hates people + 5. Use sp_QuickieStore (code.erikdarling.com) + 6. Look for the procedure/query + 7. Look at avg/min/max duration and CPU + 8. Look for different plan_ids for the same query_id + 9. Look at their compile time parameters +10. Don't test the code with local variables +11. Don't test the code with local variables +12. Don't test the code with local variables + +*/ + +DECLARE + --@DisplayName nvarchar(40) = N'John%' + @DisplayName nvarchar(40) = N'User%'; + +SELECT + u.Id, + u.DisplayName, + TotalScore = SUM(p.Score) +FROM dbo.Users AS u +JOIN dbo.Posts AS p + ON p.OwnerUserId = u.Id +WHERE u.DisplayName LIKE @DisplayName +AND p.Score > 0 +GROUP BY + u.Id, + u.DisplayName +HAVING + SUM(p.Score) >= 5000 +ORDER BY + TotalScore DESC; +GO + +/* +Getting the same plan for both values isn't helpful. + +This isn't sniffing, this is local variable behavior. +The values are just not sniffed at all. Remember that. + +Either make a copy of the procedure for testing, a +temporary stored procedure, or parameterized dynamic +SQL to test things out. Local variables <> Adequate. + +*/ + +CREATE OR ALTER PROCEDURE + #DisplayNameSearcher +( + @DisplayName nvarchar(40) +) +AS +BEGIN + SELECT + c = COUNT_BIG(*) + FROM dbo.Users AS u + WHERE u.DisplayName LIKE @DisplayName; +END; +GO + +EXECUTE #DisplayNameSearcher + @DisplayName = N'John%'; + +/*Or...*/ + +EXECUTE sys.sp_executesql +N' + SELECT + c = COUNT_BIG(*) + FROM dbo.Users AS u + WHERE u.DisplayName LIKE @DisplayName; +', +N'@DisplayName nvarchar(40)', +N'John%'; + + +/* +Get it done quickly + * code.erikdarling.com +*/ + +DECLARE + @start_date date = + CONVERT + ( + date, + SYSDATETIME() + ); + +EXECUTE dbo.sp_QueryReproBuilder + @database_name = 'StackOverflow2013', + @procedure_name = 'DisplayNameSearcher', + @start_date = @start_date; + + +/*Clean Up*/ +EXECUTE dbo.DropIndexes + @TableName = N'Posts'; + +EXECUTE dbo.DropIndexes + @TableName = N'Users'; + + +/* +We live in rather funny times for parameter sniffing. + * SQL Server 2022: + * Parameter Sensitive Plan Optimization (PSPO) + * Compatibility Level 160 + * Enterprise Only + * Equality predicates only + * Three different plans for row ranges: + * Unusually common: 3 + * Very uncommon: 1 + * Everything in between: 2 + * Per qualifying parameter (up to 3) + * Per qualifying table reference (up to 3) + * That means a total of 27 plan variants + * If you have 3 qualifying parameters + * Kicks in ~heuristically~ + * Skewness: is the most common value 100k times + more common than the least common value is? + * 1:100k ratio, so if the least common value + has 10 rows, most common needs 1 million rows. + * Many other things + * No built-in way to force the issue +*/ + +SELECT + mv.map_value +FROM sys.dm_xe_map_values AS mv +WHERE mv.name = N'psp_skipped_reason_enum' +ORDER BY + mv.map_key; + +/* +There are three kinds of queries we still need to fix: + * Ones where the sensitivity isn't an equality search + * >, >=, <, <=, LIKE, IN/NOT IN(list), etc. + * Ones where the feature does not kick in on its own + * We need to be tricky to change its mind + * Ones where the feature buckets things poorly + * No way to control the range buckets + + +First, inequality predicates: + +Here's our stored procedure, which has predicates +on Score and CreationDate. Nothing too crazy here. + +*/ + +/*Already created*/ +CREATE INDEX + Score +ON dbo.Comments + (Score) +INCLUDE + (UserId, PostId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +CREATE OR ALTER PROCEDURE + dbo.InequalitySniffing +( + @CreationDate datetime, + @Score integer +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + SELECT TOP (10000) + c.* + FROM dbo.Comments AS c + WHERE c.Score BETWEEN @Score AND @Score + AND c.CreationDate >= @CreationDate + ORDER BY + c.Score, + c.CreationDate, + c.Id; +END; +GO + +/* +This is fast, because both predicates are selective. + +But please take note of the predicate in the Lookup. + +*/ + +EXECUTE dbo.InequalitySniffing + @CreationDate = '20131231', + @Score = 6; + +/* +This is not so fast of course, because many Comments +(rightly) have a Score of zero. Hilarity ensues. + +Performance isn't too horrible at ~3.5 seconds, but +you can see the extra time spent in the Seek into +the Clustered index, ~2.9 seconds, and that the Sort +now spills because the memory grant was insufficient. + +*/ + +EXECUTE dbo.InequalitySniffing + @CreationDate = '20131231', + @Score = 0; + + +/*Create this before proceeding, ~40 seconds*/ +CREATE INDEX + Score_CreationDate +ON dbo.Comments + (Score, CreationDate) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); + + +/* +The reason why this procedure is parameter-sensitive +is that we don't have one index for both predicates. + +Lookups can often be faster than clustered index scans, +so you shouldn't focus on fixing all of them. Always +get the actual execution plan and validate slow points. + +Look at operator times. Do not trust costs. Not even +in Actual Execution Plans. They're still estimates, +and not durable performance metrics. Useless liars. + +Predicate lookups are different, because they're a sign +you have incomplete indexes for what your queries need +to accomplish. These are usually worth noting when doing +execution plan analysis for parameter sensitivity issues. + +These are now reasonably fast, even with a Lookup, +because all the filtering is done within one index. + +*/ + +EXECUTE dbo.InequalitySniffing + @Score = 6, + @CreationDate = '20131231'; + +EXECUTE dbo.InequalitySniffing + @Score = 0, + @CreationDate = '20131231'; +GO + + +/* +Now we'll look at problems with PSPO. + * This could have been cool. + * Instead we got Fabric. + +One might think that with a feature name like the + ~*~*~PARAMETER SENSITIVE PLAN OPTIMIZATION~*~*~ +that it would act sanely and rationally in its effort +to optimize parameter sensitive plans. One might +need reminding that Availability Groups rarely make +things more available. Usually they're less so. + +The two main problems we'll look at here: + * Heuristic weaknesses + * Poor bucketing practices + +*/ + + +/*Just create this*/ +CREATE INDEX + OwnerUserId_PostTypeId +ON dbo.Posts + (OwnerUserId, PostTypeId) +WHERE + (PostTypeId = 1) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); + +/*Already Created*/ +CREATE INDEX + UserId +ON dbo.Badges + (UserId) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); + +CREATE INDEX + VoteTypeId_UserId_PostId +ON dbo.Votes + (VoteTypeId, UserId, PostId) +INCLUDE + (BountyAmount, CreationDate) +WITH + (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE); +GO + +/* +This is one of my favorite parameter sensitivity demos. + +*/ + +CREATE OR ALTER PROCEDURE + dbo.VoteSniffing +( + @VoteTypeId integer +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + SELECT + UserId = ISNULL(v.UserId, 0), + Votes2013 = + SUM + ( + CASE + WHEN v.CreationDate >= CONVERT(datetime, '20130101', 112) + AND v.CreationDate < CONVERT(datetime, '20140101', 112) + THEN 1 + ELSE 0 + END + ), + TotalBounty = + SUM + ( + CASE + WHEN v.BountyAmount IS NULL + THEN 0 + ELSE 1 + END + ), + PostCount = COUNT(DISTINCT v.PostId), + VoteTypeId = @VoteTypeId + FROM dbo.Votes AS v + WHERE v.VoteTypeId = @VoteTypeId + AND NOT EXISTS + ( + SELECT + 1/0 + FROM dbo.Posts AS p + JOIN dbo.Badges AS b + ON b.UserId = p.OwnerUserId + WHERE p.OwnerUserId = v.UserId + AND p.PostTypeId = 1 + ) + GROUP BY + v.UserId + ORDER BY + PostCount DESC; +END; +GO + + +/* +This seems sensitive to me. +*/ + +DBCC SHOW_STATISTICS +( + N'dbo.Votes', + N'VoteTypeId_UserId_PostId' +) +WITH + HISTOGRAM; +GO + +/*Oh, right.*/ +SELECT + EQ_ROWS = + FORMAT + ( + CONVERT(bigint, 3.733213E+07), + 'N0' + ); + + +/* +But because the least frequent value in the histogram +is VoteTypeId 4 at 733 rows, and the most frequent is +VoteTypeId 2 at 37,332,130 rows, we do not meet the +minimum skewness threshold. Sadness ensues, as usual. + +Shorter: + 733 * 100,000 = 73,300,000 + and + 73,300,000 > 37,332,130 + +*/ + + +/*This kinda sucks, going from small to big*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffing'; + + +/*Does fine to start!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 4; + +/*For a little texture. Not bad.*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 1; + +/* +Show saved plan, totally eats it. + * That Batch Mode spill is a mess + * Microsoft hides BPSORT waits + * What are they ashamed of here? + * https://go.erikdarling.com/SlowSortSpills + * Instead we got Fabric. + +*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 2; + + +/*This really sucks*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffing'; + + +/*Much better start, I think.*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 2; + +/*For a little texture. Much better!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 1; + +/*This plan is a total waste.*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 4; +GO + +/* +It's not that it's "slow", but it takes around +100ms, when the original plan for 4 took 0ms, +and used very little resource-wise. This is a big +ol' parallel plan that uses ~3GB for a memory grant. + +We can make SQL Server change its mind about this by +adding a dummy row to the votes table! Sketchy, huh? + +Now we'll meet the skewness threshold, statistically. + + 1 * 100,000 = 100,000 + + 37,332,130 > 100,000 + +*/ + +SET IDENTITY_INSERT + dbo.Votes ON; + +INSERT + dbo.Votes +( + Id, + PostId, + UserId, + BountyAmount, + VoteTypeId, + CreationDate +) +VALUES +( + -2147483648, + -2147483648, + -2147483648, + NULL, + 0, /*VoteTypeId*/ + '99991231' +); + +SET IDENTITY_INSERT + dbo.Votes OFF; + + +/*This is to avoid anything dumb*/ +UPDATE STATISTICS + dbo.Votes + VoteTypeId_UserId_PostId +WITH + FULLSCAN; + +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffing'; + + +/*QueryVariantID = 1!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 0; + +/*QueryVariantID = 2!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 4; + +/*QueryVariantID = 3!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 2; + +/*QueryVariantID = ?!*/ +EXECUTE dbo.VoteSniffing + @VoteTypeId = 1; + + +/*Unfortunate*/ +SELECT + v.VoteTypeId, + Total = FORMAT(COUNT_BIG(*), 'N0'), + QueryVariantId = + CASE + WHEN v.VoteTypeId = 2 + THEN '3: Unusually Common' + WHEN v.VoteTypeId = 0 + THEN '1: Very Uncommon' + ELSE '2: Everything Else' + END +FROM dbo.Votes AS v +GROUP BY + v.VoteTypeId +ORDER BY + COUNT_BIG(*) DESC; + + +/*Remove Dummy*/ +DELETE + v +FROM dbo.Votes AS v +WHERE v.VoteTypeId = 0 +AND v.CreationDate >= CONVERT(datetime, '99991231', 112); + + +/*Remove Dummy*/ +UPDATE STATISTICS + dbo.Votes + VoteTypeId_UserId_PostId +WITH + FULLSCAN; +GO + +/*Dummy Removal Check*/ +DBCC SHOW_STATISTICS +( + N'dbo.Votes', + N'VoteTypeId_UserId_PostId' +) +WITH + HISTOGRAM; +GO + +/* +I know what you're thinking here! + +Erik! Query Store! Surely we can just force one of +these queries to use a different variant. Surely! + +Wrong. + +*/ + +DECLARE + @start_date date = + CONVERT + ( + date, + SYSDATETIME() + ); + +EXECUTE dbo.sp_QuickieStore + @database_name = 'StackOverflow2013', + @only_queries_with_variants = 1, + @query_text_search = 'UserId = ISNULL(v.UserId, 0)', + @start_date = @start_date; + +/* +Things to note here: + * Every query has a different query_id + * We can't force plans across query_ids + +*/ + +EXECUTE sys.sp_query_store_force_plan + @query_id = NULL, + @plan_id = NULL; +GO + + +/* +We've done a fair bit of character assassination in +our dealings with PSPO. It's not intentional, it's +just me trying to use the feature. But don't worry, +I'm sure it will work great for you! You're special. + +Most of the time with parameter sniffing, it's just a +matter of comparing different plan choices, and then +making some query or index adjustment to give the +optimizer fewer choices. After all, it's human like us. + +The more choices it has, the more likely it is to make +a bad one that it would regret if it had those feelings. + +Some common things to be aware of in sensitive plans: + * Join type + * Join order + * Seek/Scan + Lookup vs Clustered/Table Scan + * Parallel vs Serial plan + * Memory grants + +For this procedure, we have a rather convenient tactic. + +There's one parameter that causes all of our problems, +and this may be true of procedures you deal with too. +It might just be less obvious because you have more +than one parameter. Like I've said, this is a tough +problem to solve because of all the *stuff* people +do at once to their queries. But I have faith in you! + +We can isolate the parameter sensitive portion of the +query by sticking it into a #temp table... sometimes. + +More on that in a moment. + +*/ + +CREATE OR ALTER PROCEDURE + dbo.VoteSniffing +( + @VoteTypeId int +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + CREATE TABLE + #votes + ( + UserId integer NULL UNIQUE CLUSTERED, + Votes2013 integer NOT NULL, + TotalBounty integer NOT NULL, + PostCount integer NOT NULL + ); + + INSERT + #votes + ( + UserId, + Votes2013, + TotalBounty, + PostCount + ) + SELECT + UserId = v.UserId, + Votes2013 = + SUM + ( + CASE + WHEN v.CreationDate >= CONVERT(datetime, '20130101', 112) + AND v.CreationDate < CONVERT(datetime, '20140101', 112) + THEN 1 + ELSE 0 + END + ), + TotalBounty = + SUM + ( + CASE + WHEN v.BountyAmount IS NULL + THEN 0 + ELSE 1 + END + ), + PostCount = COUNT(DISTINCT v.PostId) + FROM dbo.Votes AS v + WHERE v.VoteTypeId = @VoteTypeId + GROUP BY + v.UserId; + + SELECT + UserId = ISNULL(v.UserId, 0), + v.Votes2013, + v.TotalBounty, + v.PostCount, + VoteTypeId = @VoteTypeId + FROM #votes AS v + WHERE NOT EXISTS + ( + SELECT + 1/0 + FROM dbo.Posts AS p + JOIN dbo.Badges AS b + ON b.UserId = p.OwnerUserId + WHERE p.OwnerUserId = v.UserId + AND p.PostTypeId = 1 + ) + ORDER BY + v.PostCount DESC; +END; +GO + + +/*This is fine*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffing'; + +EXECUTE dbo.VoteSniffing + @VoteTypeId = 1; + +EXECUTE dbo.VoteSniffing + @VoteTypeId = 4; + + +/* +Problems this pattern solves: + * We get stable performance from most executions + * The parameter sensitive part of the query is fenced off + * Fewer choices for the second part of the query + +Problems this pattern doesn’t solve: + * Large result sets dumped into temp tables can be bad + +*/ + + +/*This is not so great*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffing'; + +EXECUTE dbo.VoteSniffing + @VoteTypeId = 2; +GO + +/* +What do we do here? + +For an outlier of this size (37 million rows), +we probably don't want to use a #temp table anyway. + +That's a pretty big darn #temp table. + +A hybrid approach? + * For VoteTypeId 2, run the regular query + * For all other VoteTypeIds, use the #temp table + * Two Stored procedures + * One executes for VoteTypeId 2 + * One executes for all other VoteTypeIds + +*/ + +CREATE OR ALTER PROCEDURE + dbo.VoteSniffingOuter +( + @VoteTypeId integer +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + IF @VoteTypeId = 2 + BEGIN + EXECUTE dbo.VoteSniffingProcedure + @VoteTypeId = @VoteTypeId; + END; + + IF @VoteTypeId <> 2 + BEGIN + EXECUTE dbo.VoteSniffingTempTable + @VoteTypeId = @VoteTypeId; + END; +END; +GO + + +/*A normal procedure call*/ +CREATE OR ALTER PROCEDURE + dbo.VoteSniffingProcedure +( + @VoteTypeId integer +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + SELECT + UserId = ISNULL(v.UserId, 0), + Votes2013 = + SUM + ( + CASE + WHEN v.CreationDate >= CONVERT(datetime, '20130101', 112) + AND v.CreationDate < CONVERT(datetime, '20140101', 112) + THEN 1 + ELSE 0 + END + ), + TotalBounty = + SUM + ( + CASE + WHEN v.BountyAmount IS NULL + THEN 0 + ELSE 1 + END + ), + PostCount = COUNT(DISTINCT v.PostId), + VoteTypeId = @VoteTypeId + FROM dbo.Votes AS v + WHERE v.VoteTypeId = @VoteTypeId + AND NOT EXISTS + ( + SELECT + 1/0 + FROM dbo.Posts AS p + JOIN dbo.Badges AS b + ON b.UserId = p.OwnerUserId + WHERE p.OwnerUserId = v.UserId + AND p.PostTypeId = 1 + ) + GROUP BY + v.UserId + ORDER BY + PostCount DESC; +END; +GO + + +/*The temp table from before*/ +CREATE OR ALTER PROCEDURE + dbo.VoteSniffingTempTable +( + @VoteTypeId integer +) +AS +BEGIN + SET XACT_ABORT, NOCOUNT ON; + + CREATE TABLE + #votes + ( + UserId integer NULL UNIQUE CLUSTERED, + Votes2013 integer NOT NULL, + TotalBounty integer NOT NULL, + PostCount integer NOT NULL + ); + + INSERT + #votes + ( + UserId, + Votes2013, + TotalBounty, + PostCount + ) + SELECT + UserId = v.UserId, + Votes2013 = + SUM + ( + CASE + WHEN v.CreationDate >= CONVERT(datetime, '20130101', 112) + AND v.CreationDate < CONVERT(datetime, '20140101', 112) + THEN 1 + ELSE 0 + END + ), + TotalBounty = + SUM + ( + CASE + WHEN v.BountyAmount IS NULL + THEN 0 + ELSE 1 + END + ), + PostCount = COUNT(DISTINCT v.PostId) + FROM dbo.Votes AS v + WHERE v.VoteTypeId = @VoteTypeId + GROUP BY + v.UserId; + + SELECT + UserId = ISNULL(v.UserId, 0), + v.Votes2013, + v.TotalBounty, + v.PostCount, + VoteTypeId = @VoteTypeId + FROM #votes AS v + WHERE NOT EXISTS + ( + SELECT + 1/0 + FROM dbo.Posts AS p + JOIN dbo.Badges AS b + ON b.UserId = p.OwnerUserId + WHERE p.OwnerUserId = v.UserId + AND p.PostTypeId = 1 + ) + ORDER BY + v.PostCount DESC; +END; +GO + + +/*This is fine*/ +EXECUTE sys.sp_recompile + @objname = N'dbo.VoteSniffingOuter'; + +EXECUTE dbo.VoteSniffingOuter + @VoteTypeId = 1; + +EXECUTE dbo.VoteSniffingOuter + @VoteTypeId = 4; + +/*Now this is fine, too*/ +EXECUTE dbo.VoteSniffingOuter + @VoteTypeId = 2; +GO + + +/* +We have a somewhat creative solution here that gets +us good performance across the outliers in our data: + * Very big, PostTypeId 2 + * Relatively big, PostTypeId 1 + * Very small, PostTypeId 4 + +There are even more things you can do with dynamic SQL. + * It's like PSPO, but you control: + * The bucketing + * Hints applied + * Indexes used + * And more! + +The deeper you get into parameter sensitivity problems, +the more often you'll lean on dynamic SQL (usually). + +You may also learn to lean on OPTION(RECOMPILE) often. + +*/ + +DECLARE + @VoteTypeId integer = NULL, + @sql nvarchar(max) = N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Votes AS v +WHERE v.VoteTypeId = @VoteTypeId'; + +IF @VoteTypeId IN (1, 3, 5, 6, 10, 16) +BEGIN + SELECT + @sql + N' +AND 1 = (SELECT 1);'; +END; + +IF @VoteTypeId = 2 +BEGIN + SELECT + @sql + N' +AND 2 = (SELECT 2);'; +END; + +IF @VoteTypeId IN (4, 7, 8, 9, 11, 12, 15) +BEGIN + SELECT + @sql + N' +AND 3 = (SELECT 3);'; +END; +GO + + +/*Add in different hints depending on values*/ +DECLARE + @VoteTypeId integer = NULL, + @sql nvarchar(max) = N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Votes AS v +JOIN dbo.Posts AS p + ON p.Id = v.PostId +WHERE v.VoteTypeId = @VoteTypeId'; + +IF @VoteTypeId IN (1, 3, 5, 6, 10, 16) +BEGIN + SELECT + @sql + N' +OPTON(MERGE JOIN);'; +END; + +IF @VoteTypeId = 2 +BEGIN + SELECT + @sql + N' +OPTION(HASH JOIN);'; +END; + +IF @VoteTypeId IN (4, 7, 8, 9, 11, 12, 15) +BEGIN + SELECT + @sql + N' +OPTION(LOOP JOIN);'; +END; +GO + + +/*Optimize for bucketized or even specific values*/ +DECLARE + @VoteTypeId integer = NULL, + @sql nvarchar(max) = N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Votes AS v +WHERE v.VoteTypeId = @VoteTypeId'; + +IF @VoteTypeId IN (1, 3, 5, 6, 10, 16) +BEGIN + SELECT + @sql + N' +OPTION +( + OPTIMIZE FOR + ( + @VoteTypeId = 1 + ) +);'; +END; + +IF @VoteTypeId = 2 +BEGIN + SELECT + @sql + N' +OPTION +( + OPTIMIZE FOR + ( + @VoteTypeId = 2 + ) +);'; +END; + +IF @VoteTypeId IN (4, 7, 8, 9, 11, 12, 15) +BEGIN + SELECT + @sql + N' +OPTION +( + OPTIMIZE FOR + ( + @VoteTypeId = 15 + ) +);'; +END; +GO + +/*Plan per value?*/ +DECLARE + @VoteTypeId integer = NULL, + @sql nvarchar(max) = N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Votes AS v +WHERE v.VoteTypeId = @VoteTypeId +OPTION +( + OPTIMIZE FOR + ( + @VoteTypeId = {@VoteTypeId} + ) +);'; + +SELECT + @sql = + REPLACE + ( + @sql, + N'{@VoteTypeId}', + @VoteTypeId + ); +GO + + +/*Wider date ranges?*/ +DECLARE + @StartDate datetime = NULL, + @EndDate datetime = NULL, + @sql nvarchar(max) = N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Votes AS v +WHERE v.CreationDate >= @StartDate +AND v.CreationDate < @EndDate'; + +IF DATEDIFF +( + MONTH, + @StartDate, + @EndDate +) >= 3 +BEGIN + SELECT + @sql + N' +OPTION(RECOMPILE);'; +END; + + +/* +Dynamic SQL is your friend. + * Make sure it's parameterized + * Make sure objects use QUOTENAME() + * Make sure it's formatted nicely + * Make sure you add a comment with the + module name that creates/runs the query + so that you know where it comes from. + +*/ + + +/* +████████╗██╗ ██╗ █████╗ ███╗ ██╗██╗ ██╗ +╚══██╔══╝██║ ██║██╔══██╗████╗ ██║██║ ██╔╝ + ██║ ███████║███████║██╔██╗ ██║█████╔╝ + ██║ ██╔══██║██╔══██║██║╚██╗██║██╔═██╗ + ██║ ██║ ██║██║ ██║██║ ╚████║██║ ██╗ + ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ + +██╗ ██╗ ██████╗ ██╗ ██╗██╗ +╚██╗ ██╔╝██╔═══██╗██║ ██║██║ + ╚████╔╝ ██║ ██║██║ ██║██║ + ╚██╔╝ ██║ ██║██║ ██║╚═╝ + ██║ ╚██████╔╝╚██████╔╝██╗ + ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ + + Erik Darling +(Consultare Maximus - Rationabile Pretium) + +W: https://erikdarling.com +E: mailto:erik@erikdarling.com +T: https://twitter.com/erikdarlingdata +T: https://www.tiktok.com/@darling.data +L: https://www.linkedin.com/company/darling-data/ +Y: https://www.youtube.com/@ErikDarlingData + +*/ diff --git a/Presentations/Parameters/01 VoteTypeId2.sqlplan b/Presentations/Parameters/01 VoteTypeId2.sqlplan new file mode 100644 index 0000000000000000000000000000000000000000..14d6628d7ea2d3c6492f67e8738fd2338169784b GIT binary patch literal 90574 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 I{`p7r{~du>Jpcdz literal 0 HcmV?d00001 diff --git a/Presentations/Parameters/02 PSPO Oddities.sql b/Presentations/Parameters/02 PSPO Oddities.sql new file mode 100644 index 00000000..82e6d280 --- /dev/null +++ b/Presentations/Parameters/02 PSPO Oddities.sql @@ -0,0 +1,287 @@ +USE StackOverflow2013; + +/*Bye*/ +DROP TABLE IF EXISTS + #DBCCShowStatistics; + +/*Hi*/ +CREATE TABLE + #DBCCShowStatistics +( + range_high_key bigint NOT NULL, + range_rows bigint NOT NULL, + equality_rows bigint NOT NULL, + distinct_range_rows bigint NOT NULL, + average_range_rows bigint NOT NULL +) + +/*Making it easier*/ +INSERT + #DBCCShowStatistics +( + range_high_key, + range_rows, + equality_rows, + distinct_range_rows, + average_range_rows +) +EXECUTE sys.sp_executesql +N' +DBCC SHOW_STATISTICS +( + N''dbo.Comments'', + N''Score'' +) +WITH + HISTOGRAM; +'; + +/*Results for sanity*/ +SELECT + dss.* +FROM #DBCCShowStatistics AS dss +ORDER BY + dss.range_high_key; + +/* +Any value that appears fewer than 100 times (according to +statistics) is considered very uncommon. + +That means that if the *least* common value appears 100 times, +it will *not* be considered very uncommon and will get the +'everything else' variant. + +Only values that don't appear in the histogram or are otherwise +estimated (between steps) to match fewer than 100 times would +trigger the 'very uncommon' variant in that scenario. + +1. Very uncommon = fewer than 100 times +2. Very common = 100,000 * least common +3. Everything else + +*/ + + +/*Some heuristics stuff*/ +SELECT + EqualityLow = + MIN(dss.equality_rows), + EqualityHigh = + MAX(dss.equality_rows), + IsEligible = + CASE + WHEN MAX(dss.equality_rows) > + MIN(dss.equality_rows) * 100000 + THEN 'Yes' + ELSE 'No' + END, + VeryUncommon = + '< 100', + VeryCommon = + POWER + ( + 10, + FLOOR + ( + LOG10 + ( + MAX(dss.equality_rows) + ) + ) + ) +FROM #DBCCShowStatistics AS dss; + + +/*This query gets PSPO*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score; +', +N'@Score integer', +0; + +/* +Optional extra examples (might be mundane) +*/ + +-- Very uncommon +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +', +N'@Score integer', +81; + +-- Everything else +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +', +N'@Score integer', +74; + +/* +This does not because the predicate +on Id restricts things below the +threshold of 99,999 rows. + +*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +AND c.Id BETWEEN 1 AND 167991 +', +N'@Score integer', +0; + + +/* +Lone Id predicate to demonstrate +The estimate is rounded to 100k +in the graphic plan, but in the +tooltip you'll see 99,999.5 rows. + +*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Id BETWEEN 1 AND 167991 +'; + + +/* +Make sure you set this up to be repeatable 4 u. +With DOP 1 FULLSCAN stats on Comments, I found +the tipping point to be (163769, 163770) + +*/ + +UPDATE STATISTICS + dbo.Comments + PK_Comments_Id +WITH + FULLSCAN, + MAXDOP = 1; + +/*This gets PSPO, one more row*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +AND c.Id BETWEEN 1 AND 167992 +', +N'@Score integer', +0; + + +/* +This should get QueryVariantId 27. + +Note that only the first three of +the parameters (up to @Score3) get +a "plan per value". 3 * 9 = 27, so +that math all checks out. There are +27 possible plans for this query. + +*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score1 + +UNION ALL + +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score2 + +UNION ALL + +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score3 + +UNION ALL + +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score4 +', +N' +@Score1 integer, +@Score2 integer, +@Score3 integer, +@Score4 integer +', +0, +0, +0, +0; + +/* +Also variant 27, just more compact: + +*/ + +EXECUTE sys.sp_executesql +N' +SELECT c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +UNION +SELECT c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score +UNION +SELECT c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE c.Score = @Score; +', +N'@Score integer', +0; + + +/* +This query does not get PSPO +because the predicate is... +backwards! (@Score = c.Score) + +Quite silly. Foolish even. Hm. + +*/ +EXECUTE sys.sp_executesql +N' +SELECT + c = COUNT_BIG(*) +FROM dbo.Comments AS c +WHERE @Score = c.Score +', +N'@Score integer', +0; + +/* +The other main reason you might not see PSPO +is a compilation time exceeding 1000ms. + +*/