DROPCLEANBUFFERS: A Better Test Of Storage Than Query Performance For SQL Server

Relics


When I’m tuning queries, people will often ask me what metrics I look at to judge efficiency. Usually, it’s just getting things to be done faster.

Sometimes it’s okay to use more CPU via a parallel plan to get your query faster. Sometimes it’s okay to do more reads to get a query faster.

Sure, it’s cool when it works out that you can reduce resources overall, but every query is special. It all sort of depends on where the bottleneck is.

One thing I’ve been asked about several times is about how important it is to clear out the plan cache and drop clean buffers between runs.

While this post is only about the dropping of cleanly buffers, let’s touch on clearing the plan cache in some way quickly.

Dusted


Clearing out the plan cache (or recompiling, or whatever) is rarely an effective query tuning mechanism, unless you’re working on a parameter sniffing issue, or trying to prove that something else about a query is causing a problem. Maybe it’s local variables, maybe it’s a bad estimate from a table variable.

You get the point.

But starting with a new plan every time is overblown — if you change things like indexes or the way the query is written, you’re gonna get a new plan anyway.

If you’re worried about long compile times, you might also want to do this to prove it’s not necessarily the query that’s slow.

Busted


Let’s look at a big picture. The script that generates this picture is as follow:

--Calgon
DBCC DROPCLEANBUFFERS;

--o boy
SELECT
    COUNT_BIG(*) AS records
FROM dbo.Posts AS p;

--table manners
SELECT
    COUNT_BIG(*) AS records
FROM dbo.Posts AS p;

--so considerate
CREATE INDEX p ON dbo.Posts(Id);

--y tho?
DBCC DROPCLEANBUFFERS;

--woah woah woah
SELECT
    COUNT_BIG(*) AS records
FROM dbo.Posts AS p;

--hey handsome
SELECT
    COUNT_BIG(*) AS records
FROM dbo.Posts AS p;

We’re gonna drop them buffferinos, run the same count query twice, add a real narrow index, then count twice again.

Great. Great. Great.

SQL Server Query Plan
different lifetime

Annalieses


For the first two executions, we performance tuned the query by about 30 seconds, just by… reading data from memory.

Hm. Okay. Unless you’re trying to prove that you don’t have enough memory, or that storage sucks, you’re not really convincing me of much.

Yes, RAM is faster than disk. Now what?

For the second two executions, query performance got way better. But reading the smaller index from disk hardly changed overall execution time.

If it’s not a strong enough argument that getting a query from 14 seconds down to half a second with a better index means you need an index, you might be working for difficult people.

Of course, Size Matters™

2021 02 04 19 36 31
brick to back

The second query finishes much faster because we have a much smaller amount of data to read, period. If we had a where clause that allowed our index to seek to a specific chunk of data, we could have done even less work. This shouldn’t surprise anyone, though. Reading 450MB is faster than reading 120,561MB.

This is not a math test.

Coasting


Starting queries out with an empty buffer pool doesn’t really offer any insights into if you’ve tuned the query. It only exaggerates a difference that is usually not a reality.

It is a useful tool if you want to prove that:

  • You need more memory
  • You need better storage
  • You need a different index

But I sure wouldn’t use it to prove that I made a query better.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Residual Predicates In SQL Server Query Plans

We Will Talk About Things And Have Fun Now


Video Summary

In this video, I share my experiences and insights on residual predicates in SQL Server queries, particularly focusing on how they can affect performance when using indexes and temporary tables. I start by recounting a series of 15 frustrating takes where I was interrupted while recording, leading to an impromptu discussion over a Monte Carlo cocktail—2.5 ounces of bourbon, about an ounce of Benedictine, and a dash of bitters—which surprisingly made me more talkative than motivated for writing. The video delves into the nuances of residual predicates by examining an index on the `post` table in the Stack Overflow database, specifically how the order of key columns impacts seek operations and overall query performance. I explore various strategies to optimize queries, including using outer applies twice and leveraging temporary tables, all while emphasizing the importance of understanding index dependencies for effective query tuning.

Full Transcript

This is the, I want to say, 15th take where I’ve started recording and been interrupted rudely, brusquely, or is it brusquely, brusquely, probably brusquely. It’s like a rude broccoli, brusquely. But I’m recording a video today. I was going to just write a blog post, but I was not so rudely interrupted by this lovely drink called the Monte Carlo, which is 2.5 ounces of bourbon, about an ounce of Benedictine, and just about whatever kind of bitters you have sitting around the house. You could do Angostura, cherry, just about anything you might have. And it’s, it’s a very nice cocktail. It’s stiff. It’s a stiff one. And, uh, it, it, it killed my motivation to write, but it did make me a bit talkative. So we’re going to chat today. We’re going to have a little chit chat. A sitting chit chat. It’s probably the best idea. About, uh, residual predicates, uh, and different ways that you can fix them, uh, or make them faster, I guess. Figure out if they’re a problem. It’s probably a good thing. All right. We could figure out if there’s an issue with this residual predicate. Figure it out. So we’ve got this index. Because where would we be without an index? Be nowhere without an index. We’ve got this index on the on the post table. The stack overflow database on the post type ID. Oh, I highlighted too much. Post type ID is score and owner user ID columns. And we have this index because it is fantastic for this query right here. Because this query has a where clause on post type ID and score and a join clause on owner user ID. Now in the grand scheme of things, this is a fairly good indexing strategy.

Because we are, we have our, our key columns up front that help us with the where clause. It helps the where clause find data. But we still have our join, our join clause column in the key of the index. So that’s a pretty good starting place. Like if you need to design an index, you’re like, I need to, I need to help this query. I need to, where this query has to find data, has to join, has to do all sorts of stuff. And that’s a pretty good way to design an index. Help it find all the data that it needs and then help it join whatever data it needs. So that’s a pretty good, pretty good strategy there. And the thing is, when we, we put that data into a temp table, right? Let’s, let’s actually run that and do it. Because I want to show you just how fantastically fast this query runs because of this professional grade index.

It finishes, my friends, in 12 milliseconds. 12 milliseconds. Fantastic. Who’s the best query tuner?

Who’s the best query tuner? The problem is, or the problem becomes, that when we need to, uh, use that temp table to derive some additional information from our database. We want, we have some figuring out to do. Uh, we have a query that just does not run as fast as we would like it to.

So we’ll hit F5 here and we’ll wait patiently for around about seven seconds for this thing to churn its wheels and do whatever it is it needs to do to, to send us data back. Okay. 6.6 seconds. Fine. I said about seven. I didn’t say exactly seven.

I don’t get on my case buster. There is our 6.6 seconds of time. And what happens in the query plan really isn’t all that important. It’s just, it’s not that important.

I mean, we, we can see that we have another semi-disaster as a repartition stream. Oh, I’m sorry. That’s over here. We have this repartition streams over here.

I don’t like. But this query takes about seven seconds. And if we look at the index seek over here, something kind of interesting shows up. We were able to seek to the post type ID that we care about.

Right? One and two. That’s good. That’s fine. We come over here. We join to that owner user ID column.

And that’s, that’s probably fine too. I’m not going to complain too much about that. But I am going to complain about the fact that this takes seven seconds. And that’s about, I don’t know, depending on how good of a query tuner you are, somewhere between four and six seconds too slow.

Now, normally, something that I enjoy getting to do in my, my life and my query tuning work with people is taking a sub query like that, a scalar sub query in the select clause and replacing it with an outer apply. Now we have to use outer apply here. If we use cross apply, we will restrict results.

One thing that this scalar sub query does, despite the fact that it has a where clause with an and in it. So it’s extra, extra where clausey. But one thing that would, might happen is we might not have a result from that sub query show up, but that wouldn’t restrict rows.

This would, this would restrict rows if we used cross apply. But if we use outer apply, something terrible will happen. My, my normal query tuning trickery will not work.

Now, if you’ve read my blog, you may be familiar with how much I hate eager index pools. Now you can’t see how terrible it is here. But if we go look at a query plan that I have saved off professionally, saved off for you, we can see that we have a query that will spend eight seconds scanning the post table and then a minute and 20 seconds building an index for us.

If you want to know more about that, you can read all my other posts about eager index pools. But what’s extra funny about this particular eager index pool, really about every eager index pool, is that even if we got it to go in parallel, it would not run faster. It actually runs a few seconds slower.

Look at that, a minute and 31 seconds. And that is because, of course, the eager index pool is a dirty, filthy liar. And all, I think, I want to say, I’m going to say off the cuff, that’s 46 million rows. And they all end up on one single, solitary thread.

So getting this query to go parallel does not provide any benefit for us whatsoever. Lovely, isn’t that? Lovely.

Lovely. Now, we could try doing all sorts of stuff with top one to get rid of the max, but unfortunately, if we do that, both of those plans are going to end up doing the same gosh darn thing with these eager index pools. Have mercy.

Have mercy on all of us. Hmm. So, this is where I started feeling personally aggrieved because I felt, in my professional query tuning opinion, that there is no way that my lovely outer apply trick should be slower, should result in an index pool. All right.

So, what I did was, rather than go with the max, I wanted to do the top one thing, but I also wanted to try separating things out a little bit, doing things a little bit different, differently. And, so, I’m going to run this select from my temp table with just the results of post type ID 1. Remember, up here, we’re looking for post type ID 1 or 2.

That’s a question or an answer right there. And if we run this, this is remarkably fast. Despite having a very lazy spool over here, it is remarkably fast.

This is not our problem. We can kind of get a feel for this because if we look at the index seek over here, right, we don’t even have to really get the properties. We can just get the tool tip.

But you can see that we read 472,310 rows. Right? It’s not too bad. I mean, maybe a lot of rows, kind of.

But this is a very fast query. Very fast. 233 milliseconds. Nearly broke the sound barrier. Definitely broke a track record.

You show me someone with a 233 second mile. Oh. Love to meet that person. That person is a spaceship. Space jokes.

Hate myself. But if we try that again with the post type ID equals two portion of the query, this will be incredibly slow. And it’s so slow, in fact, that I refuse to run this query and make you watch it run.

Because even though it has the exact same query plan shape, the time is not what it once was. This takes a full two minutes and one second, which is actually somehow a little bit worse than that eager index spool plan. Somehow, we found a way to be worse than an eager index spool.

Now, if we look at the index seek over here, we read quite a few more rows. I don’t know what this number is. It is a three, three, nine, seven, five, four, five, four, nine, oh.

That is a ten digit number of rows that we end up reading. That we end up seeking to. Because you know how seeks are always so much faster.

I kind of wish we had just scanned this thing. Because the seek is not working out for us here. So, despite all we’ve done to set up our query and our indexes to provide a nice seek, we do not get a very timely seek, do we? This is two minutes of seeking.

Now, what’s kind of interesting is we seek to this post type ID. And then we have this residual predicate on owner user ID. This is where the problem really is.

Just as we can seek to post type ID equals two, that doesn’t really buy us a whole lot. There are a lot of post type ID, post type IDs of two in the post table. There are many of them.

There are lots of answers in the post table. And the fact that we can seek to every single one of them is great. But then we can’t just immediately seek to the owner user ID that we care about. Because we have that score column in the middle, remember?

Because we’re using that index over here, that east index. And our index, of course, because it looks like this, we can seek to here. But then we have this sort of thing right here in between what we need to seek to next.

We have this score column sitting between us. And this goes back to a lot of stuff that I’ve said and talked about and written about with indexes, where when you create these rowstore indexes, the order of key columns matters quite a bit because we introduce dependencies going from right to left, right?

So we can seek to post type ID and we can seek to score. But even if we seek to post type ID, we can’t seek to owner user ID after that. We have to go through score somehow to get to owner user ID.

So we kind of get a bit stuck, don’t we? We’re stuck not being able to get through score. And of course, having score second actually works out pretty well for us generally because when we seek to this post type ID, we have score in order for the order by here.

So having score in the index there is actually a good idea. I mean, not only for this part of the query, but that first query that populated the temp table, that was a good idea there too.

That helped that query finish very quickly. So one thing we could do if we were feeling particularly ambitious is we could use outer apply twice. In the first outer apply, we can get the top one score for what we know is fast, right?

Post type ID one is fast. This finishes in 233 milliseconds. God bless.

And we can use the score that we pull out of here as an additional predicate in the second apply. And what this does is it helps us bridge that gap between post type ID and owner user ID. We’re going to use score as another predicate.

So we’re going to be able to use the full extent of our key columns. If we run the query like this, this will also finish very quickly. In fact, we 290 milliseconds is still breaking some records.

So that is absolutely lovely. But keep in mind, we have the optimizers telling us we could have we could do better. We can create an index and do do better.

Yeah, thanks. Thanks, index. I would. Thanks. You’re wrong. You’re wrong.

But that’s okay. It’s okay. You’re allowed to be wrong, optimizer. That’s why I’m here. So whenever I explain that to people, though, they get very confused, right? Because if you look at the query plan, right, you have this one seek up here that does the residual predicate thing.

It is on your owner user ID and it’s fast. It doesn’t matter because it does a very small number of reads, 473, 2,310 reads. And then when we come and look at the index seek down here, it looks a little funny, doesn’t it?

So in this one we look for that post type ID equals 2 first and then we have another seek predicate now. Where score is greater than expression 1,0003. And that’s just lovely.

We have another seek predicate and that other seek predicate helps us have another thing to help us find our data faster. That expression, that expression 1, 0, 0, 3 is what comes from here, right? So getting the score from this part is our, is expression 1, 0, 0, 3.

And one way to kind of make it a little bit easier to visualize what’s going on in your head is to take the result of that first apply operation, right? Where we get post type ID equals 1. And dump that into a temp table, right?

And that happens very quickly. And 57, this goes parallel and takes 57 milliseconds now. Whew. I’m going to have to fan myself.

I’m going to have to spritz, give myself a spritz, take a cold shower. But now, well this 2 isn’t slow. My comment is a liar. But now if we select from that second temp table and we outer apply the portion to get post type beta equals 2, what we’re going to do is take the score that’s in our temp table and use that as a predicate.

So before we took the one from the first outer apply and use it as a predicate in the second outer apply. But a slightly easier way to visualize that is to do something like this. And if we run this query, this will also be lightning fast.

This is 58 milliseconds. That further breaks our track record. Because now we’re down to like a hundred and something milliseconds between the insert into the temp table and that. So that’s actually maybe the better strategy.

Maybe. I don’t know. I might be crazy. Maybe we should take this first outer apply and put it into our initial temp table select and then get the second one after that. And we would only use one temp table instead of two, which would be wonderful and lovely.

But who knows? Who knows? Now, the whole reason why this works is because of the order of the key columns in our index. Again, post type ID and then score sort of set this thing that we can’t get past.

Right? Because the ordering of the index depends on the ordering of our key columns. So it’s ordered by post type ID.

And then within duplicates of post type ID, we have score in order. And then within the duplicates of score, we have owner user ID in order. So it’s that dependency going from left to right or right to left in the in the rowstore indexes that really kind of beat this query up because we couldn’t seek to post type ID and then seek to owner user ID. We couldn’t just like hop, skip and jump over score to get there.

Could we? Now, we could. We could. We could. Try shuffling the key, the order of the key columns, right? We could go post type ID owner user ID then score, but.

We can’t always change indexes. It’s not always easy. That index might be there for a bunch of other queries too. If we change this index, it’s going to mess up a whole bunch of other queries potentially, isn’t it? It’s going to mess a whole bunch of downstream stuff up.

Maybe other queries doing things. We don’t know. We don’t know what might happen. We don’t know. We could add another index, but then we have two indexes that have nearly the same data in them. And that’s.

It’s depressing, isn’t it? Why would we ever want that? Duplicative indexes. Another thing that we could do is. Get our max score a slightly different way by using something like row number.

So we could get row number from posts for that and then do our filtering after we get the row number. But. Ah.

Boy oh boy. In row mode. This sucks. In row mode. This is just as slow as the first query we ran. It took seven seconds. It’s not breaking any. Well, I actually guess that.

I suppose that actually is breaking track record still, isn’t it? No one has a seven second mile. But yeah, this takes this takes actually this one takes much closer to 300 milliseconds closer to seven seconds than the original query, which took 6.6 seconds. But.

If we. Get some batch mode involved. Now I have this table called T. I mean fine. I’m not the most creative person in the world.

I have this table called T in my database. And this table called T is not for Texas. It is for table. And this T table has a clustered columnstore index on it. And if we do a stupid looking left join to that table.

Ah, I messed that all up. Apologize. It’s the first mistake I’ve made all day. We do this stupid join to our T table. Something interesting will happen.

Get you out of here. But this finishes. Now in about 2.2 seconds. Because we have a bunch of stuff that’s going to happen in batch mode now.

We see this window aggregate operator. Fantastic. Fantastic. We’re going to have a sort that happens. Oh, second mistake I made today.

This sort that happens in batch mode. We have this window aggregate that happens in batch mode. And because the sort is a child operator of a window aggregate. All this this can parallelize rather nicely.

A rather nice parallelization. But batch mode sorts when they’re not the child operator of a window aggregate. All the rows end up on one thread which can sometimes be worse.

I mean this they’re still getting the batch mode efficiencies of the sort. But that whole one thread thing is a little little wonky. But anyway.

That’s another way you could potentially fix the query. It’s just by getting a row number and doing some batch modeing. Which is perfectly acceptable. But let’s make sure we clean up after ourselves.

So thank you for watching. I hope you enjoyed this video. I hope you enjoy the remainder of whatever day this is. And well, maybe in the future I’ll drink some more Monte Carlos and get talkative and record some more videos.

I do miss you. Where have you been? Huh.

Around I guess. Cheers. Thank you. I was going to say something profound. I forget what it is now though. That always happens to me.

It’s right on the tip of my tongue. Well, anyway.

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. I’m also available for consulting if you just don’t have time for that and need to solve performance problems quickly.

USE StackOverflow;

EXEC dbo.DropIndexes; 

/*
CREATE INDEX east 
    ON dbo.Posts
        (PostTypeId, Score, OwnerUserId) 
WITH ( MAXDOP = 8, 
       SORT_IN_TEMPDB = ON, 
       DATA_COMPRESSION = ROW );
*/

DROP TABLE IF EXISTS #t;
GO 

SELECT   
    u.Id,
    u.Reputation,
    u.DisplayName,
    p.Id AS PostId,
    p.Title
INTO #t
FROM dbo.Users AS u
JOIN dbo.Posts AS p
    ON p.OwnerUserId = u.Id
WHERE u.Reputation >= 1000
AND   p.PostTypeId = 1
AND   p.Score >= 1000 
ORDER BY u.Reputation DESC;



/*
CREATE INDEX east 
    ON dbo.Posts(PostTypeId, Score, OwnerUserId);
*/
SELECT 
    t.Id, 
    t.Reputation, 
    ( 
        SELECT 
            MAX(p.Score) 
        FROM dbo.Posts AS p 
        WHERE p.OwnerUserId = t.Id 
        AND   p.PostTypeId IN (1, 2) 
    ) AS TopPostScore,
    t.PostId, 
    t.Title
FROM #t AS t
ORDER BY t.Reputation DESC;


/*
Usually I love replacing select 
list subqueries with APPLY

Just show the saved plan here
*/
SELECT 
    t.Id, 
    t.Reputation, 
    pq.Score,
    t.PostId, 
    t.Title
FROM #t AS t
OUTER APPLY --We have to use outer apply to not restrict results!
(
    SELECT 
        MAX(p.Score) AS Score
    FROM dbo.Posts AS p 
    WHERE p.OwnerUserId = t.Id 
    AND   p.PostTypeId IN (1, 2)
) AS pq
ORDER BY t.Reputation DESC;


/*
TOP (1) also spools
*/
SELECT 
    t.Id, 
    t.Reputation, 
    ( 
        SELECT TOP (1) 
            p.Score
        FROM dbo.Posts AS p
        WHERE p.PostTypeId IN (1, 2)
        AND   p.OwnerUserId = t.Id
        ORDER BY p.Score DESC 
    ) AS TopPostScore,
    t.PostId, 
    t.Title
FROM #t AS t
ORDER BY t.Reputation DESC;

SELECT 
    t.Id, 
    t.Reputation, 
    pq.Score,
    t.PostId, 
    t.Title
FROM #t AS t
OUTER APPLY
(
    SELECT TOP (1) 
        p.Score
    FROM dbo.Posts AS p
    WHERE p.PostTypeId IN (1, 2)
    AND   p.OwnerUserId = t.Id
    ORDER BY p.Score DESC
) AS pq
ORDER BY t.Reputation DESC;


/*
CREATE INDEX east 
    ON dbo.Posts(PostTypeId, Score, OwnerUserId);
*/
SELECT 
    t.Id, 
    t.Reputation, 
    pq.Score,
    t.PostId, 
    t.Title
FROM #t AS t
OUTER APPLY --This one is fast
(
    SELECT TOP (1) 
        p.Score
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 1
    AND   p.OwnerUserId = t.Id
    ORDER BY p.Score DESC
) AS pq
ORDER BY t.Reputation DESC;

SELECT 
    t.Id, 
    t.Reputation, 
    pa.Score,
    t.PostId, 
    t.Title
FROM #t AS t
OUTER APPLY --This two is slow...
(
    SELECT TOP (1) 
        p.Score
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 2
    AND   p.OwnerUserId = t.Id
    ORDER BY p.Score DESC
) AS pa
ORDER BY t.Reputation DESC;


/*
Use the Score!
*/
SELECT 
    t.Id, 
    t.Reputation, 
    ISNULL(pa.Score, pq.Score) AS TopPostScore,
    t.PostId, 
    t.Title
FROM #t AS t
OUTER APPLY --This one is fast
(
    SELECT TOP (1) 
        p.Score --Let's get the top score here
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 1
    AND   p.OwnerUserId = t.Id
    ORDER BY p.Score DESC
) AS pq
OUTER APPLY --This two is slow...
(
    SELECT TOP (1) 
        p.Score
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 2
    AND   p.OwnerUserId = t.Id
    AND   pq.Score < p.Score --Then use it as a filter down here
    ORDER BY p.Score DESC
) AS pa
ORDER BY t.Reputation DESC;


SELECT 
    t.Id, 
    t.Reputation, 
    ISNULL(pq.Score, 0) AS Score,
    t.PostId, 
    t.Title
INTO #t2
FROM #t AS t
OUTER APPLY --This one is fast
(
    SELECT TOP (1) 
        p.Score --Let's get the top score here
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 1
    AND   p.OwnerUserId = t.Id
    ORDER BY p.Score DESC
) AS pq;


SELECT 
    t.Id, 
    t.Reputation, 
    ISNULL(pa.Score, t.Score) AS TopPostScore, 
    t.PostId, 
    t.Title
FROM #t2 AS t
OUTER APPLY 
(
    SELECT TOP (1) 
        p.Score
    FROM dbo.Posts AS p
    WHERE p.PostTypeId = 2
    AND   p.OwnerUserId = t.Id
    AND   t.Score < p.Score --Then use it as a filter down here
    ORDER BY p.Score DESC
) AS pa
ORDER BY t.Reputation DESC;



/*
What happened?
 * Index key column order
   * (PostTypeId, Score, OwnerUserId)

Other things we could try:
 * Shuffling index key order, or creating a new index
   * (PostTypeId, OwnerUserId, Score)
 
 * Rewriting the query to use ROW_NUMBER() instead
  * Have to be really careful here, probably use Batch Mode

*/

/*
CREATE TABLE dbo.t
(
id int NOT NULL,
INDEX c CLUSTERED COLUMNSTORE
);
*/

SELECT 
    t.Id, 
    t.Reputation, 
    pa.Score,
    t.PostId, 
    t.Title
FROM #t AS t
LEFT JOIN dbo.t AS tt ON 1 = 0
OUTER APPLY
(
    SELECT 
        rn.*
    FROM 
    (
        SELECT
            p.*,
            ROW_NUMBER()
                OVER
                (
                    PARTITION BY 
                        p.OwnerUserId
                    ORDER BY
                        p.Score DESC
                ) AS n
        FROM dbo.Posts AS p
        WHERE p.PostTypeId IN (1, 2)
    ) AS rn
    WHERE rn.OwnerUserId = t.Id
    AND   rn.n = 1
) AS pa
ORDER BY t.Reputation DESC;


DROP TABLE #t, #t2;

 

SQL Server Filtered Indexes Need Serious Help

Minorly


Filtered indexes are really interesting things. Just slap a where clause on your index definition, and you can do all sorts of helpful stuff:

  • Isolate hot data
  • Make soft delete queries faster
  • Get a histogram specific to the span of data you care about

Among other things, of course. There are some annoying things about them though.

  • They only work with specific ANSI options
  • If you don’t include the filter definition columns in the index, it might not get used
  • They only work when queries use literals, not parameters or variables

Majorly


Part of the optimizer’s process consists of expression matching, where things like computed columns, filtered indexes, and indexed views are considered for use in your query.

I mean, if you have any of them. If you don’t, it probably just stares inwardly for a few nanoseconds, wondering why you don’t care about it.

Something that this part of the process is terrible at is any sort of “advanced” expression matching. It has to be exact, or you get whacked.

Here’s an example:

DROP TABLE IF EXISTS dbo.is_deleted;

CREATE TABLE dbo.is_deleted
(
    id int PRIMARY KEY,
    dt datetime NOT NULL,
    thing1 varchar(50) NOT NULL,
    thing2 varchar(50) NOT NULL,
    is_deleted bit NOT NULL
);

INSERT dbo.is_deleted WITH(TABLOCK)
(
    id,
    dt,
    thing1,
    thing2,
    is_deleted
)
SELECT 
    x.n,
    DATEADD(MINUTE, x.n, GETDATE()),
    SUBSTRING(x.text, 0, 50),
    SUBSTRING(x.text, 0, 50),
    x.n % 2
FROM (
SELECT 
    ROW_NUMBER() OVER 
    (
        ORDER BY 1/0
    ) AS n,
    m.*
FROM sys.messages AS m
) AS x;

CREATE INDEX isd 
ON dbo.is_deleted
    (dt)
INCLUDE
    (is_deleted)
WHERE 
    (is_deleted = 0);

Overly


If you run that setup script, you’ll get yourself a table that’s ripe for a filtered index on the is_deleted column.

But it doesn’t work with every type of query pattern. Some people are super fancy and want to find NOTs!

SELECT 
    COUNT_BIG(*) AS records
FROM dbo.is_deleted AS id
WHERE id.dt >= GETDATE() + 200
AND   (NOT 1 = id.is_deleted)
AND   1 = (SELECT 1);

I have the 1 = (SELECT 1) in there for reasons. But we still get no satisfaction.

SQL Server Query Plan
hurtin

If we try to force the matter, we’ll get an error!

SELECT 
    COUNT_BIG(*) AS records
FROM dbo.is_deleted AS id WITH(INDEX = isd)
WHERE id.dt >= GETDATE() + 200
AND   (NOT 1 = id.is_deleted)
AND   1 = (SELECT 1);

The optimizer says non.

Msg 8622, Level 16, State 1, Line 84
Query processor could not produce a query plan because of the hints defined in this query. 
Resubmit the query without specifying any hints and without using SET FORCEPLAN.

It has no problem with this one, though.

SELECT
    COUNT_BIG(*) AS records
FROM dbo.is_deleted AS id
WHERE id.dt >= GETDATE() + 200
AND   (0 = id.is_deleted)
AND   1 = (SELECT 1);

Underly


It would be nice if there were some more work put into filtered indexes to make them generally more useful.

In much the same way that a general set of contradictions can be detected, simple things like this could be too.

Computed columns have a similar issue, where if the definition is col1 + col2, a query looking at col2 + col1 won’t pick it up.

It’s a darn shame that such potentially powerful tools don’t get much love.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Recompile And Nested Procedures In SQL Server

Rock Sale


While I was answering a question, I had to revisit what happens when using different flavors of recompile hints with stored procedure when they call inner stored procedures. I like when this happens, because there are so many little details I forget.

Anyway, the TL;DR is that if you have nested stored procedures, recompiling only recompiles the outer one. The inner procedures — really, I should say modules, because it includes other objects that compile query plans — but hey. Now you know what I should have said.

If you want to play around with the tests, you’ll need to grab sp_BlitzCache. I’m too lazy to write plan cache queries from scratch.

Testament


The procedures:

CREATE OR ALTER PROCEDURE dbo.inner_sp
AS
BEGIN

    SELECT
        COUNT_BIG(*) AS records
    FROM sys.master_files AS mf;
END;
GO 

CREATE OR ALTER PROCEDURE dbo.outer_sp
--WITH RECOMPILE /*toggle this to see different behavior*/
AS
BEGIN

    SELECT 
        COUNT_BIG(*) AS records
    FROM sys.databases AS d;
    
    EXEC dbo.inner_sp;

END;
GO

The tests:

--It's helpful to run this before each test to clear out clutter
DBCC FREEPROCCACHE;

--Look at this with and without 
--WITH RECOMPILE in the procedure definition
EXEC dbo.outer_sp;

--Take out the proc-level recompile and run this
EXEC dbo.outer_sp WITH RECOMPILE;

--Take out the proc-level recompile and run this
EXEC sp_recompile 'dbo.outer_sp';
EXEC dbo.outer_sp;

--You should run these between each test to verify behavior
--If you just run them here at the end, you'll be disappointed
EXEC sp_BlitzCache 
    @DatabaseName = 'Crap', 
    @QueryFilter = 'procedure', 
    @SkipAnalysis = 1, 
    @HideSummary = 1;

EXEC sp_BlitzCache 
    @DatabaseName = 'Crap', 
    @QueryFilter = 'statement', 
    @SkipAnalysis = 1, 
    @HideSummary = 1;

Whatchalookinat?


After each of these where a recompile is applied, you should see the inner proc/statement in the BlitzCache results, but not the outer procedure.

It’s important to understand behavior like this, because recompile hints are most often used to help investigate parameter sniffing issues. If it’s taking place in nested stored procedure calls, you may find yourself with a bunch of extra work to do or needing to re-focus your use of recompile hints.

Of course, this is why I much prefer option recompile hints on problem statements. You get much more reliable behavior.

And, as Paul writes:

For instances running at least SQL Server 2008 build 2746 (Service Pack 1 with Cumulative Update 5), using OPTION (RECOMPILE) has another significant advantage over WITH RECOMPILE: Only OPTION (RECOMPILE) enables the Parameter Embedding Optimization.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

SQL Server Foreign Keys Don’t Always Improve Query Plans

Short and Lousy


This is one of the most frustrating things I’ve seen from the optimizer in quite a while.

Here are a couple tables, with a foreign key between them:

CREATE TABLE dbo.ct(id int PRIMARY KEY, dt datetime);

CREATE TABLE dbo.ct_fk(id int PRIMARY KEY, dt datetime);

ALTER TABLE dbo.ct ADD CONSTRAINT
    ct_c_fk FOREIGN KEY (id) REFERENCES dbo.ct_fk(id);

When we use the EXISTS clause, join elimination occurs normally:

SELECT COUNT_BIG(*) AS [?]
FROM dbo.ct AS c
WHERE EXISTS
(
    SELECT 1/0
    FROM dbo.ct_fk AS cf
    WHERE cf.id = c.id
);
SQL Server Query Plan
all the chickens

But when we use NOT EXISTS, it… doesn’t.

SELECT COUNT_BIG(*) AS [?]
FROM dbo.ct AS c
WHERE NOT EXISTS
(
    SELECT 1/0
    FROM dbo.ct_fk AS cf
    WHERE cf.id = c.id
);
SQL Server Query Plan
?

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Indexed Views As Filtered Indexes In SQL Server

Pssst!


If you landed here from Brent’s weekly links, use this link to get my training for 90% off.

The access is for life, but this coupon code isn’t! Get it while it lasts.

Discount applies at checkout, and you have to purchase everything for it to apply.

A Persistent Frustration


SQL Server comes with some great features for tuning queries:

  • Computed Columns
  • Filtered Indexes
  • Indexed Views

But there’s an interoperability issue when you try to use things together. You can’t create a filtered index with the filter definition on a computed column, nor can you create a filtered index on an indexed view.

If you find yourself backed into a corner, you may need to consider using an indexed view without any aggregation (which is the normal use-case).

Empty Tables


If we try to do something like this, we’ll get an error.

DROP TABLE IF EXISTS dbo.indexed_view;
GO

CREATE TABLE dbo.indexed_view
(
    id int PRIMARY KEY,
    notfizzbuzz AS (id * 2)
);
GO

CREATE INDEX n 
    ON dbo.indexed_view (notfizzbuzz) 
WHERE notfizzbuzz = 0;
GO

Yes, I’m putting the error message here for SEO bucks.

Msg 10609, Level 16, State 1, Line 19
Filtered index 'nfb' cannot be created on table 'dbo.indexed_view' because the column 'notfizzbuzz' in the filter expression is a computed column. 
Rewrite the filter expression so that it does not include this column.

An Indexed View Doesn’t Help


If we run this to create an indexed view on top of our base table, we still can’t create a filtered index, but there’s a different error message.

CREATE OR ALTER VIEW dbo.computed_column
WITH SCHEMABINDING
AS
SELECT
    iv.id, 
    iv.notfizzbuzz
FROM dbo.indexed_view AS iv;
GO 

CREATE UNIQUE CLUSTERED INDEX c 
    ON dbo.computed_column(id);

CREATE INDEX nfb 
    ON dbo.computed_column(notfizzbuzz) 
WHERE notfizzbuzz = 0;
Msg 10610, Level 16, State 1, Line 37
Filtered index 'nfb' cannot be created on object 'dbo.computed_column' because it is not a user table. 
Filtered indexes are only supported on tables. 
If you are trying to create a filtered index on a view, consider creating an indexed view with the filter expression incorporated in the view definition.

But what a thoughtful error message it is! Thanks, whomever wrote that.

Still Needs Help


We can create this indexed view just fine.

CREATE OR ALTER VIEW dbo.computed_column
WITH SCHEMABINDING
AS
SELECT
    iv.id, 
    iv.notfizzbuzz
FROM dbo.indexed_view AS iv
WHERE iv.notfizzbuzz = 0;
GO 

CREATE UNIQUE CLUSTERED INDEX c 
    ON dbo.computed_column(id);

But if we try to select from it, the view is expanded.

SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc
WHERE cc.notfizzbuzz = 0;
SQL Server Query Plan
upstate

The issue here is the simple parameterization that is attempted with the trivial plan.

If we run the query like this, and look at the end of the output, we’ll see a message at the bottom that our query is safe for auto (simple) parameterization. This may still happen even if the plan doesn’t remain trivial (more detail at the link above!)

DBCC FREEPROCCACHE;
GO 
DBCC TRACEON(8607, 3604);
GO 
SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc
WHERE cc.notfizzbuzz = 0;
DBCC TRACEOFF(8607, 3604);
GO 

********************
** Query marked as Cachable
** Query marked as Safe for Auto-Param
********************

Making It Work


The two ways we can run this query to get the indexed view to be used are like so:

SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc WITH(NOEXPAND)
WHERE cc.notfizzbuzz = 0;


SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc
WHERE cc.notfizzbuzz = 0
AND   1 = (SELECT 1);
SQL Server Query Plan
thanks i guess

A Closer Look


If we put those two queries through the ringer, we’ll still see auto (simple) parameterization from the first query:

DBCC FREEPROCCACHE;
GO 
DBCC TRACEON(8607, 3604);
GO 
SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc WITH(NOEXPAND)
WHERE cc.notfizzbuzz = 0;
GO 
DBCC TRACEOFF(8607, 3604);
GO 

********************
** Query marked as Cachable
** Query marked as Safe for Auto-Param
********************

DBCC FREEPROCCACHE;
GO 
DBCC TRACEON(8607, 3604);
GO 
SELECT
    cc.id, 
    cc.notfizzbuzz
FROM dbo.computed_column AS cc
WHERE cc.notfizzbuzz = 0
AND   1 = (SELECT 1);
GO 
DBCC TRACEOFF(8607, 3604);
GO 

********************
** Query marked as Cachable
********************

It’s goofy, but it’s worth noting. Anyway, if I had to pick one of these methods to get the plan I want, it would be the NOEXPAND version.

Using that hint is the only thing that will allow for statistics to get generated on indexed views.

In case you’re wondering, marking the computed column as PERSISTED doesn’t change the outcome for any of these issues.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Considerations For Implementing Soft Deletes In SQL Server

From The Beginning


Implementing soft deletes for an app that’s been around for a while can be tough. In the same way as implementing Partitioning can be tough to add in later to get data management value from (rebuilding clustered indexes on the scheme, making sure all nonclustered indexes are aligned, and all future indexes are too, and making sure you have sufficient partitions at the beginning and end for data movement).

Boy, I really stressed those parentheses out.

If you do either one from the outset, it’s far less painful to manage. The structural stuff is there for you from the beginning, and you can test different strategies early on before data change become difficult to manage.

Queries


The first and most obvious thing is that all your queries now need to only find data that isn’t deleted.

Almost universally, it’s easier to put views on top of tables that have the appropriate bit search for deleted or not deleted rows than to expect people to remember it.

CREATE VIEW dbo.Users_Active
AS
SELECT 
    u.*
FROM dbo.Users AS u
WHERE u.is_deleted = 0;

CREATE VIEW dbo.Users_Inactive
AS
SELECT 
    u.*
FROM dbo.Users AS u
WHERE u.is_deleted = 1;

It’s not that views have any magical performance properties; they’re just queries after all, but it gives you an explicit data source.

Indexes


Depending on how your other queries search for data, you may need to start accounting for the is_deleted flag in your indexes. This could make a really big difference if the optimizer stops choosing your narrower nonclustered indexes because it hates key lookups.

Typically, other predicates will give you a selective-enough result set that a residual predicate on a bit field won’t make much difference. If you’ve already got a seek to the portion of data you’re interested in and most of it will be not-deleted, who cares?

And let’s be honest, in most implementations deleted rows will be the minority of data, and searches for it will be far less common. Usually it’s just there for an occasional audit.

In adjacent cases where instead of deleted you need to designate things as currently active, and you may have many inactive rows compared to active rows, filtered indexes can be your best friend.

Coming back to the views, I don’t think that making them indexed is necessary by default, but it might be if you’re using forced parameterization and filtered indexes.

CREATE TABLE dbo.Users(id int, is_deleted bit);
GO 

CREATE INDEX u ON dbo.Users (id) WHERE is_deleted = 0;
GO

SELECT 
    u.id, u.is_deleted
FROM dbo.Users AS u
WHERE u.is_deleted = 0;

Under simple parameterization, this can be fine. Under forced parameterization, things can get weird.

SQL Server Query Plan
tutor the tutors

Tables and Tables


In some cases, it might be easier to create tables specifically for deleted rows so you don’t have unnecessary data in your main tables. You can implement this easily enough with after triggers. Just make sure they’re designed to handle multiple rows.

If you want something out of the box, you might mess with:

  • Temporal tables
  • Change Data Capture
  • Change Tracking

However, none of those help you deal with who deleted rows. For that, you’ll need an Audit.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Simplifying Archival Processes In SQL Server Using OUTPUT

Long List Of Demands


Many suggestions for making changes or improvements to a process in a database are met like orders that mean, if not certain, then likely death.

EXT. A WEBEX MEETING

… Erik is smiling, happy to be wrapping up and moving on. Client DBAs and Developers are sweating profusely, mechanical keyboards all heavily armed with Cherry MX Blue switches in an attempt to drown out the part where he talks about NOLOCK hints. For some reason everyone is smoking cigarettes and finalizing their wills

ERIK

You should enable the remote DAC, it’ll be helpful next time you hit THREADPOOL.

There’s a script included to turn it on, and a bunch of examples on how to use it.

IT Manager

By God, there’s no telling who of us will survive this, but we’ll see’er through.

Oh, Now You Need Consistency


Well, friends, I have good news for you. This is an easy one to implement.

Let’s say that in Stack Overflow land, when a user deletes their account we also delete all their votes. That’s not how it works, but it’s how I’m going to show you how to condense what can normally be a difficult process to isolate into a single operation.

First, we need an archive table. So let it be done.

CREATE TABLE dbo.Votes_Archive
(
    Id int NOT NULL,
    PostId int NOT NULL,
    UserId int NULL,
    BountyAmount int NULL,
    VoteTypeId int NOT NULL,
    CreationDate datetime NOT NULL,
    CONSTRAINT PK_VotesArchive_Id
        PRIMARY KEY CLUSTERED (Id ASC)
);

Next, we need a query to, like, do things. I guess.

Normally, you’d insert the rows you want to keep into the archive table, and then remove them from the source table by using a delete … where exists  type query. Usually in a transaction, because you heard you need them.

Savvier practitioners might even use the output clause with their delete to dump things into a #temp table.

But the savviest practitioners will do it all in one go.

Sauvignon Blanc


What a lot of people miss about output is that the results are tabular, like many other things in a database.

Just like you can select from deleted and inserted virtual tables in a trigger, you can select from them in a… select statement.

I’m using a transaction here because I want to roll it back.

BEGIN TRAN

INSERT dbo.Votes_Archive
    (Id, PostId, UserId, BountyAmount, VoteTypeId, CreationDate)
SELECT 
    v.*
FROM 
(
    DELETE v
        OUTPUT
            Deleted.Id,
            Deleted.PostId,
            Deleted.UserId,
            Deleted.BountyAmount,
            Deleted.VoteTypeId,
            Deleted.CreationDate
    FROM dbo.Votes AS v
    WHERE v.UserId = 190597
) AS v;

--Validate stuff
SELECT * FROM dbo.Votes AS v WHERE v.UserId = 190597;

SELECT * FROM dbo.Votes_Archive AS vi;
--Stop validating stuff, nutso
ROLLBACK

So when whoever user 190597 is deletes their account, all their votes are inserted into the archive table at the same time they’re deleted.

The execution plan looks like this:

SQL Server Query Plan
polar seltzer

Which is pretty nifty, and eliminates the need for a lot of jumping around.

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Defeating Parameter Sniffing With Dynamic SQL In SQL Server

Enjoy!



Thanks for watching!

Video Summary

In this video, I delve into the world of parameter sniffing and explore how dynamic SQL can be a powerful tool in addressing it. I start by explaining what dynamic SQL is—essentially, it’s a string that you build into a query to execute—and highlight its flexibility and usefulness in various scenarios, such as making decisions based on user input or analyzing SQL Server configurations. However, I also emphasize the potential pitfalls of using dynamic SQL without proper caution, particularly when concatenating user inputs directly into your queries, which can lead to security vulnerabilities like SQL injection. To combat these issues, I walk through how to safely use dynamic SQL and introduce `sp_executesql` with parameters, ensuring that the parameter values are passed securely and reducing the risk of malicious code execution. Throughout the video, I provide practical examples and insights into when recompile hints might not be the best solution, especially in scenarios where queries execute frequently or take a long time to compile. By the end, you’ll have a clearer understanding of how dynamic SQL can help mitigate parameter sniffing issues while maintaining security and performance.

Full Transcript

Hello and welcome to defeating parameter sniffing with Dynamic SQL. I’ll be your host, I’m Erik Darling. If you want to get in touch with me about anything related to this talk or SQL Server or, I don’t know, just ask me how my day was, you can get in touch with me via any of these things. I have a website, I have an email address, and I have a Twitter handle I’m a triple threat as far as contact methods go. If you want to download the demo database and demo scripts to play around with on your own, you can go to these bit.ly links, but keep in mind they are case sensitive. And if you type them in incorrectly, I cannot be held personally responsible for wherever your browser ends up taking you because there’s no longer my links. So mind your S’s and D’s in those links right there. Now, of course, I say all that because I’m not going to use any of these things. Because this talk is given under the Creative Commons license, meaning you can do all sorts of stuff with it, and you can present it, and you can do all that as long as you attribute and you don’t change the licensing on that. But of course, I don’t have any, I’m not cool enough or rich enough to have any lawyers. So I don’t know, I’ll just have to beat you up next time I see you if you if you do any of these things, if you do any of these things dirty. So the reason why I want to give this talk is, of course, because parameter sniffing can be a really, really tough problem to solve. But the first thing we should clear up is that parameter sniffing isn’t always a problem. It gets as a very, very bad reputation, because when it is a problem, it is nuts. But it is quite often happening on many SQL servers all across the globe. And no one is complaining, it might even be happening in outer space. I don’t know exactly how far reaching Microsoft is, they might have like stuck SQL Server on like an asteroid or something and just sent it barreling into space. I don’t know.

But I’m sure that it’s out there. Parameter sniffing away. Now, most of the time, this is a good thing because SQL Server thinks up a query plan and uses it and then keeps using it. And no one’s any the wiser. No one’s complaining performance is okay. And everything is nice and stable in your server. And isn’t that wonderful? I mean, I think it’s wonderful. I think it’s wonderful. Sort of kind of makes me like the Maytag man where I’m like, you know, sitting there waiting for a phone call. Someone’s like, hey, we’re a performance problem. And then there’s no performance problem, because everything’s fine. And then I get sad and lonely. But there are of course, lots of other ways that you can fix parameter sniffing other than using dynamic SQL. But this is a kind of a fun, interesting method that I’ve used a lot with clients and that I want to share with the world. Because deep down inside, I’m a nice person and I do care about sharing with the world.

But before we get into exactly how to fix dynamic SQL, or how to fix parameter sniffing with dynamic SQL, I need to make sure everyone understands what dynamic SQL is, and how to use it safely. What a parameter is, and how they can cause performance problems. What parameter sniffing looks like, and how you can observe it happening. And of course, what information you need to reproduce it. All good things to know.

And of course, I want to teach you these things, because I want you to understand what is constantly happening on your SQL Server, when it’s good, and when it’s bad. So the first question that we’re going to answer today is, what is dynamic SQL? This is a good question, isn’t it? What is that dynamic SQL stuff? What is that wall of red text that I struggle with? Incorrect. Syntax near. Single tick. And of course, dynamic SQL is a string that you build into a query to execute. That is the most basic definition I can think of for dynamic SQL.

And of course, dynamic SQL is a very flexible coding paradigm with all sorts of cool stuff you can do with it. You can make decisions based on user input, the state of data, or where the query is executing. You might take a table name as input. You might see if a user has permission to go do something. If you are the type of crazy person who writes scripts that analyze SQL Server, well, you might need to make some decisions about which DMBs you touch and which column names you go and select from, depending on which version and edition of SQL Server you’re on.

You might need to figure out if objects exist before you run a query or do something. Like, let’s say you have a server where you have a database per client or something, right? And you want to add an index to a table, but you already added it to some tables and others. You go through every database and figure out if that index exists before you go and create it. All sorts of good stuff you can do with dynamic SQL.

Another great use for dynamic SQL is figuring out which search arguments you want to have in your where clause based on what people are searching for. Now, the problem with dynamic SQL, or one of the problems, one of the many myriad problems with dynamic SQL is that people write it in an unsafe way. Now, the easiest, for instance, to look at is when people use just exec to execute strings willy nilly. Now, of course, this isn’t always a problem.

You might ask for user input like this, but then not actually use that user input in the string that you execute. You might have a hard-coded string that you execute based on user input, and you are not going to get SQL injected when you do this, because there is no untrustworthy user ickiness drifting into your queries to run. We just get a hard-coded string that executes.

Someone would have to go to a pretty great lengths to, like, get into your code and change that hard-coded string to something malicious and then, you know, do stuff. So I’m not saying it’s impossible, but it’s certainly difficult. Now, if you look at the execution plan, of course, we’re just going to have selected from the votes table, because that is what our user instructed us to do.

Of course, the problem with exec is not when you do something like this. The problem with exec is when you do something like this, where you concatenate a value into a string, right? So you either use concat or the plus signs or whatever else, and this is where users can do all sorts of icky, nasty, ugly stuff.

Now, I hate strings in databases. I realize why they’re there, and they might have to be there for certain things. But let’s say that we have this column, which is an Envarkar 250, where every time someone asks a question in the Stack Overflow database, what happens?

They need to have a title for that question so people know what they’re getting into when they click the link. And with 250 characters available, you sure can fit a lot of nonsense into a string. You can fit, like, this entire Union All statement.

And I know what you’re thinking. You know, little Bobby tables. Go drop all our tables. I hate little Bobby tables. I hate that cartoon. Because it really does sort of distract people from what is often the intent of SQL injection, which is not to just inconvenience a DBA somewhere by dropping a table or a database or something.

It is to steal information. And if we look at what happens with the result of this query, we get a bunch of valid search results back, which match exactly what we are looking for. But we also get back a list of tables from the Stack Overflow database.

And this is probably not what we wanted users getting back. I mean, this whole list of tables here. So that’s not good. And if you look over in the Messages tab, we will see an issue.

We will see that we did search for where a title was like anything. But then we also concatenated this whole string on. And we got results back from a system table.

Now, it’s very easy, I think, to, in terms of, like, security, maybe disallow an application user from being able to drop a table or drop a database or do something crazy. But the more and more that I work with SQL Server and different applications, the more I see applications doing kind of crazy administrative stuff. Like, they might be creating indexes.

They might be creating databases. They might be creating schema. They might be creating agent jobs, taking backups, like, creating, like, security certificates, doing all sorts of things. You need a pretty elevated privilege to go out and do. Now, if you want to spend years of your life doing every grant and revoke to make that work out without, you know, someone being able to, say, select from sys.tables, well, that’s great for you.

But I think you’re going to have a really hard time with that. The other thing that’s difficult is that a lot of applications, when they do stuff like upgrades or whatever, they might need to hit those dynamic management views to see if certain things exist or not. So you’re going to have an even tougher time because no one out there in the world is writing their applications to use very specific logins for very specific tasks.

So until that happens, we’re going to have a tough time with SQL injection and Dynamic SQL. Now, of course, you could use sp-execute SQL to buy yourself a little bit of security here. But when we use sp-execute SQL, we still need to use parameters.

So in this case, we’re still being dum-dums and concatenating all this stuff into a string. And even though we’re using sp-execute SQL, we are afforded no protection here. If I run this the exact same way that I ran the last query, we are going to get the exact same results back where we get all of the tables from sys.tables.

And we are going to get, of course, that whole union all put into the select list, right? All that stuff is still in there. We did not save the day.

In order to make Dynamic SQL as safe as can be, we need to write our Dynamic SQL so that we are not concatenating the parameter into the string. The parameter has to be part of the string. See, the title is in red here, which means it’s inside of the Dynamic SQL.

The parameter is coming from inside of the Dynamic SQL. It’s terrifying. And, of course, when we do that, we need to give sp-execute SQL a little information about the parameter that exists inside of it. And we, of course, need to set that parameter to a value.

And when I run the Dynamic SQL like this, what we get back is something totally different. We get back no search results because there is no post in the post table. There is no question in the post table that has a title of UnionAllSelectYibidaBibidaBibida.

It’s just not in there. And if we go look at the Messages tab, we no longer have that whole big UnionAll as part of our string. We just have a single parameter in here.

So SQL Server went and searched for the contents of the parameter rather than taking that parameter and concatenating it into the string and getting a whole bunch of malicious code on top of it. I believe that dirty payload or something. So that is when dynamic SQL can be unsafe and backfire.

But dynamic SQL is often the only tool that you can use to fix some performance issues. Now, if you write queries like this, you should feel bad about yourself and the way that you write queries. It should feel terrible.

It should hire me to help you fix them. But I get it. If you do this stuff and you stick a recompile hint on there, everything will go swimmingly. The problem becomes if queries execute frequently or if they take a long time to compile.

If they’re very complicated queries, they might take a very long time to compile an execution plan. And these can be times when a recompile hint works against you. The other kind of bummer about the recompile hint is that, I mean, it’s a little bit less of a bummer if you have query store turned on or if you have a monitoring tool.

But a kind of unfortunate side effect of recompile is that you do not have any sort of long forensic history in the plan cache about how many times a query executed, if there’s a lot of variation in it and stuff like that. So there is some stuff missing. There’s stuff that recompile takes away too.

There’s stuff that recompile can mess with. Now, let’s look at an example of recompile and the problem that it fixes. Now, I have two indexes on the POST table.

I have an index called 1z and an index called 3z. It would have been 1z, 2z, but 2z was on a different table. So we’ll get to that.

We’ll get to 2z in a bit. But 1z is on owner user ID, score, and creation date. Keep that in mind. Owner user ID is the leading column here. And on 3z, parent ID is the leading column, and owner user ID is the second column.

Now, what I’m going to do is run three queries using slightly different techniques to figure out nullability of parameters. We’ll do this one with an OR clause. We’ll do this one with ISNULL, and we’ll do this one with COALESSE.

Now, these don’t have recompile hints on them, so things are going to be a little bit awkward for all of these queries. And if we look at the execution plans, something kind of funny happens. This first one, even though it gets a good cardinality estimate for how many rows are going to come out of the index scan, we use the index that starts with parent ID instead of the index that starts with owner user ID.

And, of course, we have to scan that index because owner user ID is not the leading column. And even more troublesome is that we need to do, in the key lookup, we need to evaluate the predicate on creation date. So that’s very, very weird.

I don’t even have an explanation for this. SQL Server just got cracked out on me. I started drinking heavily. But the other two queries sort of have different performance issues. Even though they use the right index, if you look very, very closely, whether we use ISNULL or COALESSE, we get the same bad estimate across the board.

And notice that we still scan the nonclustered index, even though it reads with owner user ID. We still have to scan it. And if we look at the predicate that gets applied here, it is gigantic.

It is unfriendly. It is not friendly to being able to seek into the index because SQL Server on the fly has to figure out if a parameter is null or if it’s not null. And then it also has to figure out a plan that’s safe for any particular parameter being null or not.

We might not be able to seek to nulls in a not nullable column if the parameter we pass in is null. And that we could just seek the entire index. Wouldn’t that be unfortunate?

If we look at the predicates for COALESSE, they’re even more unfortunate. We get these gigantic case expressions. I mean, not that this query does significantly better or worse, but it’s just aggravating to look at. I just don’t like looking at it.

So using any of these methods, we get incorrect index usage. We get bad estimates. And we maybe get like just not maybe not like the best possible execution plan for our query. Recompile will help, of course.

Right. So if we run those same three queries with recompile hints, what’s going to happen is we’re going to get very different execution plans. And regardless of which method we choose, we are going to get accurate guesses. Right.

So now we’re able to seek into our indexes. We get a good guess. We use the correct index in this first one. Yippee-ki-yay. I’m not going to finish that thought probably. But then if we look at these two, not only do we use the correct index and seek, despite those crazy functions being in there, but look what happens. We get good guesses too.

Right. So we get spot on with those. And of course, the index seek just turns into a simple case of seeking to a literal value here and applying a predicate on this value here. So that’s pretty good.

Recompile is pretty awesome for these situations. Right. I’m totally fine. Again, not anti-recompile. I’m totally fine with you using it. But I do want you to be careful with it.

Use it judiciously. Use it when you know it’s a good idea. Use it when it’s safe. Now let’s ask ourselves a related question. Let’s ask ourselves just what a parameter is.

Good question to ask. What’s a parameter? What’s going on in there? What’s the frequency, Mr. Parameter? So there are different things in SQL Server that have parameters as part of their definition. So you can create store procedures with parameters.

That’s pretty obvious. You can create functions that accept parameters. And you can pass parameters into Dynamic SQL.

Like so. Now, parameters are not the same as declared variables. I have a whole long blog post about that at my website here.

So if you want to go look at that, you can. Sometimes it’s just easier to search Erik Darling data for local variables. And you’ll come to that post rather than try to memorize that whole URL.

But you’re smart. I’m sure you could figure that out. But anyway, if you declare a variable like this. And then use it in a query like this. Like specific.

Pretty specifically in a where clause. It is not the same as if you use a parameter. It is a much different thing. However, you can magically turn variables into parameters. By passing them to store procedures or functions.

Or passing them into Dynamic SQL. You can transmogrify them into. To magical, wonderful parameters. And have them not be variables anymore.

And this distinction is important. Because local variables do not usually get you good guesses for cardinality estimates. Right?

So what I’m going to do is declare these vote type ID variables. And set them equal to for here and to here. And I’m going to run both of these queries. And we’re going to admire the devastation.

I mean the first one is fine because it’s 733 rows. If your queries have problems counting 733 rows, you have a very different problem than parameter sniffing. You might just want to turn that server off because it’s terrible.

Anyway. Or you could call me for help too. I’m not going to complain either way. But what happens here is we get the same sort of stock guess regardless of how many rows are actually going to come out of the index seek here.

So SQL Server just uses the same cardinality estimation process. And it’s not even actually a cardinality estimate. Cardinality estimates are like math equations.

This is just a magic number guess. So that’s fun for you. Right? Cool. Anyway. In a perfect world, those would get correct-ish estimates.

Right? We would just maybe get the same behavior as using a parameter there. But that’s not what we get. And of course, if we start treating that variable like a parameter, well, this is where some trouble might start. So what I’m going to do is use dynamic SQL and pass a parameter to it for vote type ID.

The first time around, we’re going to use vote type ID 4, which only has 733 rows. And the second time around, we’re going to use vote type ID 2, which has something like 37 million rows. And if I run these two queries, we’re going to see where the problem with parameters starts to kick in.

Right? So even though we return two very different counts here, the execution plans, well, the execution plan, rather, gets reused. All right?

We get a good guess for 733 rows for vote type ID 4. But we get a very bad guess for vote type ID 2, which returns a count of 37 million rows. This query ends up taking nearly five seconds to run versus the zero seconds this one takes.

Yee. That’s no good. Of course, if we run those in reverse order, something different happens that works out mostly in our favor. All right?

Both of these finish relatively quickly. All right? But the execution plans are different now. These go parallel. All right? So we get a good guess here. And SQL Server says, this looks like it’s going to be an expensive date. I’m going to go parallel.

I’m going to have my little racing stripes on all these things. All right? So SQL Server’s like, yes, more CPUs, please. But now this query down here, which returns very few rows, also does the same thing because it reuses the guess for finding a large amount of data. Now, maybe that’s okay.

Maybe that’s not a big deal. Maybe that’s just the performance improvement that we need across the board. I’m not going to argue with that.

I’m not going to tell you it’s bad. But I am going to caution you a little bit because when queries go parallel, they use more CPU. They reserve more threads to use. And if some knucklehead admin comes along and doesn’t understand parallelism or a CX packet or weight stats, they might come look at your server and they might say, ah, I found this wonderful script on the internet.

It will tell me about the weights that I have and all their percentages. And they might run that script and might say, wow, this server has 99% CX packet weights. We should set max stop to one.

And then what happens? You have a lot of slow serial queries that could benefit from parallelism because we engaged parallelism perhaps when it was inappropriate. So that’s something to just be a little careful of.

If you’re going to tune your queries to go parallel constantly, you’re going to need to tune your admins to ignore those CX packet and CX consumer weights. CX consumer is on the newer SQL Server stuff, but it’s a bit of a digression that we don’t need to get into. Of course, there are very, very good reasons to parameterize.

We just saw a case where, you know, SQL Server using different execution plans got very different performance profiles of those queries. Right? There were some changes in the queries that might not have existed if we used just a plan based on whatever parameter we passed in.

Right? Not reusing plans. But there are very, very good reasons to parameterize. Now, if you write dynamic SQL that looks like this, it, of course, will not be parameterized.

And if you run queries that look like this for different values, SQL Server is not going to reuse execution plans. SQL Server is going to regard these queries with a great amount of distrust. And we are going to get different query plans based on what values get passed in.

Now, that all finished relatively quickly, which is by design. I’m pretty good at this stuff right now because I don’t want to sit here for a long time while queries run. That’s no fun for anybody.

But let’s look at what happened. We have all these literal values. Right? And for each one of these literal values, SQL Server is going to think up its very own special execution plan. And if you look through the list of execution plans, there are going to be three main strategies that we see.

Some queries are going to choose a key lookup based on how many rows they think are going to come out of this index seek. Some are going to skip that key lookup situation. And they’re just going to scan the clustered index and then go into a stream aggregate.

And even still, some others are going to scan the clustered index and go into a hash match aggregate. This is something that is a new sort of perk of SQL Server 2019’s batch mode on rowstore. It used to be that you could only have a stream aggregate for a scalar aggregate.

Now you can also have a hash aggregate for a scalar aggregate. So fun stuff there. Thanks, 2019, for giving me a third query plan to tell people about, I suppose. Now, if we look at the plan cache, and I highly, highly recommend if you are going to go looking at the plan cache, you use SP Blitz cache to do it.

But I also highly, highly recommend turning query plans off. Do not collect query plans for SP Blitz cache or else it will run for a very long time and you will question my sanity as a presenter and a performance tuner. But if you run this with query plans turned off, we’re going to get back to the top 10 statements that executed in here.

And if we look at the query text for them that got stored when the plans were generated, we can revalidate the fact that every single one of these literal value queries got a brand new execution plan. And we can even see, if we look over here, that all of them executed exactly once because SQL Server did not trust them to be the same query.

Bummer, right? Of course, if we parameterize like this, say between start date and, well, I’m going to use between. Aaron Bertrand might yell at me later.

I don’t care. He’s Canadian. I don’t know. Maybe he’s too polite to yell at me. Who knows? But if we turn query plans back on, because now we need them again, and we run this, we’re going to get all 11 of those queries back.

But all 11 of the queries that print out are going to look like this. We have these parameters in there rather than those literal values. All of the execution plans in this case will be the same, which is really to be expected when we reuse plans.

And if we look in SP Blitzcache, of course, turning query plans off, what are we going to get? One example of the query text with the parameters in it. Here, start date and adding 11 days to the start date.

And we will see that we got 14 executions of that query. So plan reuse, if you want it, you’re going to have to parameterize for it. So parameterization can be a very, very good technique.

You know, it’s like the opposite sort of opposite end of the spectrum, really. It’s like if you have using recompile hints everywhere, you know, you’re going to have a bunch of queries that just show one execution and not a lot going on. If you don’t parameterize queries, you’re going to see a whole bunch of the same query saying one execution and maybe using slightly different execution plans.

And then if you parameterize, you’re going to see all of the executions for a query since it’s parameterized. Now, it can be a real drawback to not parameterize because if you have a bunch of single-use statements and query plans, you might have a hard time with your plan cache because if you wanted to figure out, like, just how much this query is running, you need to find different ways to identify that query and then, like, add that up and tally things up by, like, a query hash or a query plan hash or something.

And if you have optimized or ad hoc workloads turned on, then what is that? Fix it, not fix it, and makes your job harder because all of those single-use plans just end up with a stub and you really don’t have a lot of feedback about that stub.

So that’s no good at all. So let’s recap what we know so far. We know that we can write Dynamic SQL to produce different queries situationally.

We know that we have to write it in a safe way to prevent people from stealing data or otherwise defacing our databases. We know that parameters and variables are treated much differently by the optimizer. We know that parameters encourage plan reuse.

And we know that that can be great if you have a good enough execution plan for everyone and not so great if your data has a lot of skew in it. Or if you use parameters to search for different volumes of data. So, like, rather than just, like, an equality which might get hurt by there being skew in the data, you know, you might have one query that searches for, like, everything that’s over a dollar and then another query that searches for everything that’s over, like, a million dollars.

And clearly, the one dollar query is going to return a lot of results and the over a million dollar query is not going to return as many results. So you can also see disparity with range-type queries, too. Now, the first thing you want to do if you suspect parameter sniffing is rule a couple things out.

There are a couple things that happen in SQL Server that always, always, always get confused for parameter sniffing. The first one is resource contention. If you need to figure out resource contention, you can grab my script, SP Pressure Detector.

It’s available on my website. You don’t have to memorize this whole URL. That’s in there for the sake of people who download the script and click on stuff. You can also go to my site.

There’s a little tab up top that says scripts. And if you hover your beautiful, cute little mouse over that, it’ll give you the option to which scripts you want to look at. The second thing you have to rule out is blocking.

And I think just about the best tool out there for that is SP WhoisActive. You can go to a very easy-to-memorize website to get that and troubleshoot blocking. And we’ll look at how to use SP WhoisActive to evaluate parameter sniffing.

But first, let’s talk a little bit about Query Store because Query Store is very cool. The plan cache, it’s a whole lot harder to track down parameter sniffing issues with the plan cache. Mostly because what you get back in the plan cache is just the compiled parameter value for a query.

You don’t get the runtime value. Now, the reason why that stinks is because if you want to reproduce a parameter sniffing situation, you need some things.

You need the query plan. You need the text of the query. You need the indexes available. And you need the parameters that were used to both compile and run whatever query we’re looking at. Now, you don’t get the runtime values in Query Store.

But you do get a couple interesting views where you can look at regressed queries and you can look at queries with a very high variance in resource usage. So you can look at that by a whole bunch of different things, CPU, res, writes, duration, all that stuff.

So let’s look at how SP WhoisActive can help us evaluate a parameter sniffing scenario. So the first thing I want to do is walk you through these parameters that I’m going to be using because they’re very, very important to how we troubleshoot the problem.

GetFullInnerText will tell us which query is currently executing. You’re probably pretty used to seeing that output already. GetOuterCommand will tell us if that query was called in a store procedure or some other larger batch of queries.

GetPlans will go and fetch us the execution plan. And GetAverageTime will go out and look at the plan cache and look at how long a query normally runs, how long a query runs for on average so we can compare the current runtime to the current average.

Now what we’re going to be looking for when we run SP WhoisActive like this is of course queries running for a longer than average amount of time. We’re going to look at the outer command to see if we have a store procedure that’s getting hit by parameter sniffing.

If not, then we look at the inner text and see if we just have a regular ad hoc query, perhaps something generated by Dynamic SQL that’s having problems. And then we’re also going to get the query plan and runtime values for parameters.

So we’re going to look at the execution plan and along with all the other goodies that we get, we’re also going to do something that we should always be doing when we are evaluating execution plans.

We’re going to go into the properties where all of the Pro Tools live. That’s where all the real SQL Server professionals go and look at stuff. I mean, it’s where I go look at stuff, so I assume everyone else does it. I could be wrong.

They might have much smarter things that they do. Crap. Now I feel very insecure. Might need to start drinking. So if we want to reproduce a parameter sniffing situation, we need to run the procedure first with the compile time value and then again with the runtime value.

Now what I have is a store procedure that I call take a chance. And what take a chance does is does some randomization of a number. And depending on what that number is, we are either going to set parent ID to zero or use some other modulus mapped out number in here.

Now the reason we’re doing that is because in the post table, there are around about 6 million or so rows that match a parent ID of zero. But then every other row in the table has a very, very small number of rows associated with it.

So sometimes this is going to run and use a small number of row plan. And then it’s going to hit parent ID zero and run for a lot longer. Other times we might start with parent ID zero and have some other weird juggling back and forth.

But let’s go create this. And we’re going to use, I think, a great tool for this called RML Utilities. It’s distributed by Microsoft.

If you might be easier to search for RML Utilities and go download it that way, then try to memorize this whole insane link. I still can’t memorize it and I go to it frequently. It’s strange, right?

But anyway, I mean, I say Microsoft distributes it. They haven’t done work on it in a very, very long time. So maybe I just said they used to distribute it and now it’s just sort of floating out there in the either. But we’re going to run, make extra double careful extra sure that I actually copied that string.

And we’re going to run store procedure, take a chance, 10 cycles and 100 threads per cycle. So that’s good. That’s all running.

And let’s go and run SP who is active. I should have put that in a new window when I told myself to. And run that. And what we’re going to have, right? I can probably kill this off now so that I don’t set my CPUs on fire.

What we’re going to have here is the output that I was just telling you about. Isn’t that wonderful? So the first couple columns are going to be pretty indicative of the problem of parameter sniffing.

They are currently executing for around 20 seconds. But on average, they run for around 0 seconds. So obviously that’s a problem.

If a query is normally running in 0 seconds and it’s now currently running for 20 seconds, we have an issue. And up here, we can see the text of the query that’s currently running. We can see the command that called the query.

In this case, our store procedure. And if we come way over here, we will have our execution plan. Our beautiful, wonderful, lovely execution plan where things are maybe not looking so hot. Because we have this query that’s just doing a whole lot more work than it should.

We have some pretty bad guesses up here. I’m sure 6 was a good guess when this first ran, but now 6 is not so great of a guess. And if we go look at the properties of the select operator, what we’re going to see is a parameter list.

And more importantly, what we’re going to see is the value that the query plan was compiled for and the value that the query is currently executing with. So starting with 34 and ending up with 0, what does that get us? A bad execution plan for when we need to return 6 million rows, but probably a great execution plan when we only need to return 6 rows.

So that’s fun and interesting. Thank you, spwhoisactive, and thank you, AdamMechanic, for writing that. Now, that’s cool.

That’s great. We’ve learned a few things about parameter sniffing. But now we should probably learn about how to fix parameter sniffing. That’s what we’re here for.

And we’re going to learn how we can do that with our good friend, DynamicSQL. But now we have to put all the things that we’ve learned together. So we know that we have parameters that we can use to make decisions when we build a query to execute.

Right? And we know that DynamicSQL is capable of building different strings based on that. So why don’t we use DynamicSQL to build different strings based on what we know about our data, about the parameters being passed in?

Now, first, we need to understand where skew lives, of course. Is it within equality predicates? Do we have some outliers in our data that have a lot of rows associated with them?

Do we have a problem with ranges? Do we sometimes search for a small range of data and sometimes search for a very large range of data? There’s some tug of war going on with that.

And, of course, we should also take some time to evaluate our queries before we go digging in and blaming parameter sniffing. Because we might be doing a whole lot of things that are messing up query performance that have nothing to do with the parameters that we’re using. Or rather, they are maybe equally at fault as the parameters that we’re using.

So hopefully everyone has a safe place to go and reproduce these issues. If not, well, I mean, I don’t know what to tell you. I’ve got a laptop.

So first, let’s look at some skewed data in the Stack Overflow database. Now, if we look at the vote type ID frequencies in the votes table, we’re going to see some pretty big disparities in the data volume present here. If we sort of draw some lines around small, medium, and large ranges of values, even within these ranges, there’s some pretty big disparities.

We’re going from like 37 million to 3.7 million. It’s a pretty big drop. There’s not much of a drop between 3.7 million and 2 million or even 1.2 million. But that’s a huge drop.

And then if we look at down here below, it’s a pretty big drop even after that where we go way down to a very small number of rows per group. This is skew. So when people tell you to index for selectivity, you should say, okay, well, what’s selective?

Because this doesn’t look very selective to me. This stuff maybe looks a little bit selective. And of course, this stuff maybe a little bit more selective.

And this stuff probably, well, I mean, not highly selective, but a lot more selective than we’re seeing for like this. So we have these procedures right now. Or rather, we have these indexes for our procedure right now.

So we have 1sie and 2sie. We know 1sie from earlier on the post table. And now we finally get to meet 2sie on the votes table, which is on vote type ID and creation date. Good stuff.

And we have this procedure, which is going to select some stuff from votes, join to posts, join to users. And the only parameter that we’re filtering on is vote type ID. And we’re doing that, of course, because vote type ID causes all sorts of problems.

Now, there are a whole bunch of different regressions if we run this store procedure in different orders. It’s not very interesting to do all of them. It wouldn’t like show you all of them because it just, trust me, it’s not that much fun.

It’s not that interesting. But if we, say, run it for plan 7, or rather vote type ID 7, this will finish very quickly for 7. It’s about 40 milliseconds.

Good stuff there. But if we reuse 7’s execution plan for 1, things go a little bit less well for our query. Things slow down rather significantly for our query.

Not in a way that we’re going to be happy with. Of course, we’re never happy when queries slow down. Unless we just want to, like, go take lunch and say, ah, this query’s going to take a half hour.

Let’s start it running and go start drinking. When we look at the execution plan now, this runs for around 11 seconds, 11 and a half seconds. And that’s not good.

From 40 milliseconds to 11 and a half seconds. And, of course, we’re only returning a top 200. But, again, this is a data volume issue. And starting way over here, the amount of data that we have to process is not a friendly amount for the query plan that we’ve chosen. All sorts of bad things happen because of this.

We end up taking about nine and a half seconds just getting up to this nested loops join. There’s all sorts of yucky stuff happening in this query. So that’s not good.

Now, of course, we could fix this with recompile. If we just recompile our store procedure, and if we actually hit the right button, we hit F5 instead of F4, we will recompile the store procedure. And if we were to run this, say, first for one, we would get a much different execution plan, especially if we’re on SQL Server 2019.

I’m going to get all this crazy adaptive join-y stuff. Look at this craziness happening in here. Woo-hoo!

Thanks, SQL Server. And the important thing, though, is that this query finishes in 1.8 seconds. But if we rerun this now for 7, 7 faces a little bit of a regression using that bigger plan.

Remember, this used to finish in about 40 milliseconds. Now it takes just under a second for 7 to do all the stuff that it has to do. So that’s not good, right?

Going from big to small and small to big, we’re not able to very effectively share plans from either side of that. Now, if we run this in slightly different order, right? If we look at, say, plan 7.

Actually, no, we’ll do it. We’ll look at plan 9 because plan 9 is interesting. Plan 9 uses the big plan, right? It doesn’t take quite as long as when we use vote type ID 1, but watch what happens if we use plan 7 for plan 9.

All right? That finishes in about 40 milliseconds, and now plan 9 finishes very quickly, too. All right?

So it’s interesting that if we were to use recompile here, vote type ID 9 would actually get a worse execution plan made specifically, tailored exactly for it. All right?

So recompile is not only obfuscating for the plan cache, but recompile can actually make some queries worse. Because when you see here, the SQL Server made a good guess about plan 9, and it came with an execution plan for it, but plan 9 actually did better with the plan for vote type ID 7.

So that’s a very, very interesting thing to think about now, is that we might have to test some queries with different execution plans in order to figure this stuff out. So our first option is to trick the optimizer into building a different execution plan by sticking some useless logic into our WHERE clause.

Let me show you what that means. First, we’re going to clear out the plan cache, because we’re allowed to do that. And the second thing is we’re going to turn this into the safe kind of dynamic SQL.

Yes, yes. Very safe. The next thing that we’re going to do is add some of this useless, meaningless logic to it.

So if the vote type ID is one that has to process a large volume of data, then we’re going to say, add where, or add and 1 equals select 1 to the WHERE clause. If we are processing a small amount of data, we’re going to add and 2 equals select 2 to the WHERE clause.

Now, the one thing that I do, one sort of side note that I do want to point out here, is that whenever you write dynamic SQL that’s going to be generated by a store procedure, it is common, common courtesy, to add a comment to the query that gets built in the dynamic SQL to tell people where it originated from.

So please, if you’re going to write dynamic SQL, do this, so that when some handsome, young, earnest consultant comes in and wants to start working with your queries that generate dynamic SQL, they will know where to find them in the store procedures.

They’ll know which store procedure to go and look at. I thank you for that. So now let’s run this store procedure for a couple different values. So if we run this for 7 and 1, which are two plans that we experimented with earlier, we’re going to get back two different execution plans.

For vote type ID 7, we get back the fast 40 millisecond plan. And for vote type ID 1, we get back the fast big, well, I mean, I mean, let’s say fast, but 1.8 seconds, right?

It’s faster than 11 seconds. Maybe there’s some query tuning we could do here. After all, SQL Server is telling us in this fabulous green text that we need an index. Hearts racing.

So because we told SQL Server to build different strings based on what got passed in, SQL Server came up with different execution plans for them. There’s 2 equals select 2, and there’s 1 equals select 1.

And because SQL Server built slightly different strings, it built two different execution plans. But we would actually be able to reuse execution plans within all of these. So these will all get reused when we process bigger, small amounts of data.

I understand that it’s a little bit tough sometimes to put hard-coded values like this into your query plan. Sometimes you might have to run a count query and make some runtime decision about what gets returned by that count query to figure out if you want a big plan or a little plan.

And, you know, that is a little bit trickier, but it is something you can do. For me, though, you know, I like using the votes table, and I like using that vote type ID column, and I have no problem hard-coding this.

Now, another similar option to that, now, is to use an optimize for a specific value hint. Now, I’m being very specific. We are not optimizing for unknown here, because this would probably not solve the problem that we want.

It usually introduces many other problems. When people say that they fixed parameter sniffing by using optimize for unknown or declaring a local variable, well, I mean, they’re sort of right.

They did get rid of parameter sniffing, but they usually introduced some other weird plan issues along the way, because you get that wonky estimate, that density vector estimate, when you use unknown or a local variable.

But the optimizing for a specific value will work the same as the, you know, whatever equals select whatever. You will get plan reuse for each optimize for, and the only sort of warning here is that it’s probably not as safe as the one equals select one, two equals select two method if you need to replace string values in there.

So just sort of an example of what that looks like would be to stick an option optimize for hint at the end of your query. And sort of a funny tokenized looking value here.

And then you could just, in the dynamic SQL, replace that tokenized string with a different number, or with a different optimize for value. So that’s another way that you can do it.

And I’m not going to go and run that, because just about the same thing happens. But you get what you get, and you better not cry about it. So those are both fine techniques if you’ve got a pretty manageable number of values to deal with, and their overall distribution will be stable.

Right? So what I mean by that is in the votes table, like, we might have, you know, we might add 10 years of data to it, but all the most common votes would still be the big values, and all of the least common votes would still be the small values.

Right? We’d still, like, keep, we would still maintain the overall distribution of data. Just maybe the numbers would get higher, but they would still be, like, you know, relative to whatever they started at.

Equality predicates does make this easier, of course, because we can figure out if there’s skew for an equality. If there’s, like, an outlier value or set of values, we can always figure out if there’s skew there.

What’s a little bit trickier is if we have a range of values that we’re evaluating. So what I did for this is I wrote a function, and I know you’re going to, oh, no, not a function, Eric, not a function.

Please, no function. But this is the good kind of function. This is a good which. This is the kind of function that returns a table. This is the inline table-valued function that you’ve heard so much about. And what this is going to do is take a parameter, called procid.

This will make more sense in a minute. And it’s going to go out into the plan cache, and it’s going to search the plan cache for where the object ID equals the proc ID that we pass in. And we are going to get back all of the information about parameters from that query.

Good stuff. And what we’re going to do with that information is use that to help us make decisions. Now, a sort of simple demonstration of exactly how that function works within a store procedure.

This won’t work in an ad hoc query, unfortunately. But what we can do is take this parameter value, use it to find some data here, and then use the plan cache to go and get the parameter values for our store procedure.

The only thing is that this only works the first time, or rather, this only works after the first time that you execute it. So if we run this query once, we’re going to get back nothing about parameters.

But if we run this query a second time, we will get back information here. Let me say SQL Server.

What happened? Last time we executed this, or rather, the time we compiled a plan for this, we executed it with the value 8 for param 1. So good stuff there.

Now, this is what we need it for. We need it for situations where we are looking for some kind of range. So start date and end date is a pretty common one.

And here are where clause, where again, Aaron Bertrand is probably going to yell at me for using between with dates. But again, Canadians, right? What are you going to do with them?

Now, if we run this for one day in 2013, we will get back this execution plan, which finishes relatively quickly, 425 milliseconds.

But then if we go and reuse that for looking at a year of data, what’s SQL Server going to tell us? I don’t like you.

SQL Server is going to yell at us, scream at us, kick, drag its feet, poke our eyes. I don’t know. Who knows what else? But the execution plan for this, this takes about six and a half seconds. It’s a pretty painful degradation in performance.

All right? Not a good time there. And now let’s look at how we can use that function and a similar technique that we saw before to get around parameter sniffing issue.

So what we’re going to have to do is a little bit of work in order to solve a pretty big problem. We’re going to need a couple placeholder parameters to hold a compile start date and end date.

And then we’re going to go out to the plan cache once and dump all our parameter information into a temp table. And we’re going to do that because it’s better than making two trips out to the plan cache.

All right? So we’re going to set compile start date and compile end date from the previous compilations of the query here.

And then, so kind of a funny thing that I discovered while I was writing this is that if I don’t use the parameters in some way in the store procedure, they don’t actually get cached with the store procedure.

They will get cached with the dynamic SQL, but that’s far less helpful because dynamic SQL executes in a different context from the store procedure. And that’s why we need to do stuff like put the store procedure name in here.

Otherwise, it won’t be associated with it in any way. We completely detach. It’s like a headless, it’s like a procedureless code, I guess, if you wanted to use a cool hip term. Serverless is still cool, right? Could still talk about that.

But then, depending on some stuff, we will decide whether or not we want to put a recompile hint on the end of our query. So the sort of situationally appropriate circumstances that I wanted to look at are if the date diff between the current start date and end date is greater than 3, and the date diff between the previously compiled start and end date is less than 2, then we’re going to add a recompile hint.

And then sort of the inverse of that, where if the current start date and end date is less than 2, and the previous start date and end date is greater than 3, then we’ll use a recompile hint.

Otherwise, we will just add a semicolon to the end of our query so Itzik doesn’t come and yell at us. He stares at me in my sleep sometimes.

It’s very uncomfortable. The big fella is an imposing fella. Scares the dickens out of you at night. Anyway, he’s really nice.

He doesn’t actually come stare at me at night. Not that I’d be opposed to that. Maybe not because I left the semicolon off. But anyway, let’s clear the proc cache, get rid of you, and let’s run this.

Now we’re going to execute the first version twice and then the second version. And we’re going to look at both the execution plans and the query text that gets returned here. So for the first two executions, we do something very normal.

We just execute our query. No recompile hint. All right. And then for the third one, where our range changed, we do put a recompile hint on.

Now, for the life in New York, it’s always a party. Anyway, now let’s run the inverse fact. Actually, let’s look at execution plans.

I got all thrown off by the musical accompaniment. If we look at either of the first two executions, we’re going to see that 428 millisecond plan again. And if we look at the third execution, we’ll get a different execution plan that finishes in about three and a half seconds.

So that’s about three seconds better than the ineffective plan that we would have reused here, which is good, right? Three seconds faster. That’s probably a good thing. Now let’s run that in the inverse order.

First two executions, what are they going to be? The big plan. It’s more effective for processing a large volume of data. All right.

That big one takes three and a half seconds. And then on the third iteration, we use the other plan that takes about half a second. All very, very good. Good stuff there.

And if you look over at the messages tab, we’ll see the same thing as last time where the first two queries execute without recompile hints. And then the third iteration executes with our recompile hint. So we have a very, very happy time there.

So anyway, wipe the sweat off. What we learned during this session is that dynamic SQL is a string that you can build into a query to execute.

In order to use it safely, we need to parameterize, and we need to use SP execute SQL. A parameter is something that you can pass to a procedure, a function, or to dynamic SQL.

And it is not a local variable. Very important. Parameters can cause performance problems when execution plans get reused for highly skewed amounts of data.

All right. So data volume. And parameter sniffing, when we look at it, it looks like the query got a lot slower for absolutely no apparent reason.

But remember, we do need to check and see if there are resource contention issues or blocking issues before we go and firmly say we are facing a parameter sniffing problem. If you want to easily detect parameter sniffing while it’s happening, SP who is active is very, very helpful.

Remember all the parameters that I gave you for that, though, the inner text, outer command, query plan, and average time. And then in order to reproduce parameter sniffing, the four things that we need follow the Q-tip acronym, the query plan, the text of the query, the indexes available, and the parameters used to compile and run the query.

And then if we want to fix it with dynamic SQL, what we need to do is isolate skewed values or detect incompatible ranges, and that will help us figure out if we need to generate a different string or something along the way and run that.

So again, thank you for having me. Again, I’m Erik Darling with Erik Darling Data. You can reach me at any of these methods, and you can also get the scripts and database at these links.

Thanks for joining me. If you have any questions, you can ask them wherever in the chat window is. I’m not sure. They don’t tell me anything ahead of time. But anyway, if you don’t have any questions, go start drinking.

I know that’s what I’m going to do. Thanks again. Goodbye.

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.

Using Plan Guides To Get Around Query Hints In SQL Server

Prophesy As


According to Not-Australians, there used to be a trace flag that would get queries to ignore any supplied hints. It doesn’t work anymore, which sucks, kinda.

Because people do lots of stupid things with hints. Real stupid things. Things you wouldn’t believe the stupid of.

Let’s say, for example, hypothetically of course, that your front end application would add an index hint to every query.

That index hint may or not be helpful to your query in any way. But there it is.

Let’s also posit, using the very depths of our imaginations, that the front end developer was unlikely to change that behavior.

Planning Fields


We’ve got a couple indexes:

CREATE INDEX r 
    ON dbo.Users(Reputation) 
WITH(MAXDOP = 8, SORT_IN_TEMPDB = ON);

CREATE INDEX c 
    ON dbo.Users(CreationDate) 
WITH(MAXDOP = 8, SORT_IN_TEMPDB = ON);

And we’ve got a query that, via an index hint, is being forced to use the wrong index.

DECLARE @Reputation int = 2;
EXEC sp_executesql N'SELECT * FROM dbo.Users WITH (INDEX  = c) WHERE Reputation = @Reputation;',
                   N'@Reputation int',
                   @Reputation;

The ensuing query plan makes no sense whatsoever.

SQL Server Query Plan
i really mean it

The things are all backwards. We scan the entire nonclustered index, and do a lookup to the clustered index just to evaluate the @Reputation predicate.

The idea is bad. Please don’t do the idea.

Guiding Bright


There are two things we could do here. We could hint the query to use the index we want, sure.

But what if we change something about this index, or add another one to the table? We might want the optimizer to have a bit more freedom to choose.

I mean, I know. That has its own risks, but whatever.

We can add a plan guide that looks like this:

EXEC sp_create_plan_guide
@name = N'dammit',
@stmt = N'SELECT * FROM dbo.Users WITH (INDEX  = c) WHERE Reputation = @Reputation;',
@type = N'SQL',
@module_or_batch = NULL,
@params = N'@Reputation int',
@hints =  N'OPTION(TABLE HINT(dbo.Users))';

If we were writing proper queries where tables are aliased, it’d look like this:

EXEC sp_create_plan_guide
@name = N'dammit',
@stmt = N'SELECT u.* FROM dbo.Users AS u WITH (INDEX  = c) WHERE u.Reputation = @Reputation;',
@type = N'SQL',
@module_or_batch = NULL,
@params = N'@Reputation int',
@hints =  N'OPTION(TABLE HINT(u))';

When we re-run our query, things look a lot better:

SQL Server Query Plan
focus

Thanks for reading!

Going Further


If this is the kind of SQL Server stuff you love learning about, you’ll love my training. Blog readers get 25% off the Everything Bundle — over 100 hours of performance tuning content. Need hands-on help? I offer consulting engagements from targeted investigations to ongoing retainers. Want a quick sanity check before committing to a full engagement? Schedule a call — no commitment required.