Query Parameterization and Plan Forcing Gotchas In SQL Server

5th Dimensions


Video Summary

In this video, I dive into the fascinating world of forcing query plans using Query Store and uncover an interesting gotcha along the way. Starting off with a bit of humor about changing my company name to “Grimes’ baby name,” which promises to make paperwork more exciting, I clear out Query Store and set up an index on a users table for demonstration purposes. The video then explores how forcing query plans works when using literal values in queries versus parameterized queries. By running two different queries with the same logic but different literal values, I highlight the differences in execution plans and explain why SQL Server treats them as separate queries despite their similarity. This leads to an important lesson: if you want to force a specific plan ID for a query ID using Query Store, you must use parameterized queries to avoid the limitations imposed by literal values. The video concludes with practical advice on when and how to leverage parameterized code for better SQL Server performance management.

Full Transcript

Erik Darling here with, for now, Erik Darling data. But I’ve got paperwork in to change my company name to Grimes’ baby name. So that’s going to be fun. That should make paperwork interesting, right? So I’m going to record a hopefully hit video here. I’ve got everything going for me. I have pretty good equipment. I’ve got an interesting topic. And now all I need is for you to watch it, I guess. Actually, is it interesting? I don’t know. It might be interesting. It depends on what your kink is, I guess. So what we’re going to talk about is forcing query plans in Query Store and an interesting gotcha that I ran into. So first thing I want to do is clear out Query Store. Right there. Bada bing, bada boom.

I’ve already got this index created on the users table and a column called reputation because I want to get two different query plans based on how we query the users table. Now, both of the queries that I’m going to run at first are going to use literal values in the where clause. So this is going to be the literal value one and this is going to be the literal value two. And if I run this first query, we will get back an execution plan. I promise an actual execution plan, an actual factual execution plan.

Maybe we’ll start calling them literal execution plans because they are literally what happened. And we’ll call estimated plans figurative plans because that’s just what the optimizer figured it would do. I think that makes sense, right? So right over here we have our literal execution plan where we start by scanning the clustered index on the post table, doing some hashy bitmap stuff and then down here joining off to the users table.

But the clustered index on the users table, not the nonclustered index that we created. This will all change. This will all get freaky, deaky, wikiwile, wikiwile when we run this query. That’s going to look for reputation equals two. For reputation equals two, we start with an index seek into our nonclustered index on the users table. We do a key lookup back to the clustered index to get that display name column because the display name column is not in our nonclustered index.

And then we do some hashy bitmap stuff over here and then down the bottom, well then on the inner side of the join rather, we join to the clustered index on the post table. That song remains the same, but the stuff with the users table was much different. Now in all different DMVs, all different parts of SQL Server, queries get identified in different ways.

In query store, you have a query ID and a plan ID, but in lots of the more traditional DMVs, we have like query hash, query plan hash, SQL handle, plan handle, all sorts of different hashes, different like binary values that SQL Server uses to represent execution plans. Now what’s funny to me is that if we go and look in the sys.query store query table, rolls right off the tongue, thanks whoever designed that. If we run this query and we look at what SQL Server thinks of our execution plan or other of our queries rather, we will get one query hash for both of those queries, but we will have two query IDs for it.

So SQL Server treated this at the query level like it’s one query, but query store treated it like two different queries, and I’ll show you what I mean. If we run this to get some more details on these queries, and yes we do need to join one, two, three, four different views together to get this information out, we will see that we have across the board, query ID one has plan ID one, one execution and use 3.5 seconds of CPU time on average. So that’s for reputation equals one, we can see that over here.

Query ID two down the bottom is also plan ID two, with one execution and 1.4 seconds of CPU time, and of course that is where reputation equals two. Now, if you found some super duper mega awesome script on the internet, and you wanted to make let’s say query ID one use plan ID two, because that uses less CPU, and you’re like, wow, I could totally make this query better by just having it use this different execution plan. Well, you can’t really do that. So there’s a store procedure for query store, called whatever.

So let’s say that we want to make query ID one use plan ID two, because it uses less CPU. So we’ll plug query ID one into here and plan ID two into here. And when we run this, we will get an error because the plan ID two is not associated with query ID one. Even though, if we look back into the DMVs, well, they’re nearly the same query aside from that thing.

They can’t. Query store says we can’t share an execution plan between you two. Now, this isn’t something that’s true of plan guides. Granted, plan guides have many, many, many other things that are strange and wrong with them.

But we would be able to do this. Now, the optimizer would check to make sure we weren’t doing anything completely asinine. Like if we had a query that was like select count from post and we wanted and we said, hey, use this plan guide where you select count from votes. The optimizer would be like, you’re up to no good. Not going to go through with that.

But here, even though like logically and semantically, like really every other way possible, these two queries should be able to share the same plan because they get different query IDs. Because of those literal values, they can’t. So how you can fix this or how you can get around this is if you use parameterized queries. All right. So what we’re going to do is you know, you can use a stored procedure.

I’m going to use SP execute SQL because it’s a little bit quicker, not faster like performance wise, just quicker to like have on screen and show you. But I’m going to run this and we’re going to run it for reputation equals one first. And I have a recompile hint in here because I want to get two different execution plans.

I want new execution plans here. So I’m going to run this for reputation equals one. And note that this is parameterized. This is not the crappy, hacky kind of dynamic SQL that gets people fired because hackers destroy their database.

This is the good safe kind of dynamic SQL that handsome tattooed consultants use all day long. So we’re going to that’s I believe that one comes before two. So that should be reputation equals one.

And we can look over at the query plan and see that, yes, indeed, we got that that plan that we wanted. Now, I’m going to run this for reputation equals two. And we’re going to get the key lookup plan, which is intentional. I want that to happen.

There’s a reason that recompile hint is in there. So now we see that we got that same key lookup plan again. So that’s good. That’s exactly what we wanted. Now, when I go in, I go back to sys.queryStoreQuery for some reason, and I go and I look for other query hashes that have more than one distinct plan associated with them. And I run this, we still only have that one result in there.

That’s from that’s the one from before query hash that ends in 8044 with two query IDs associated with it. But now when we look in the query, when we do we run our 70,000 join query to get four columns back. Now we have two more lines in here.

And these two lines here, they start a little bit different. These ones have little parameters at the beginning of them. All right, let’s see that reputation thing there. And if we make this column a little bit wider, we can see that there’s a difference.

So this top one is where reputation equals one literal. This one is where reputation equals two literal. But in these, this is parameterized.

So we just see that reputation parameter in there. So now, when we have one query ID across of both of them, but two different plan IDs. So this means that we could, we could tell query ID five to always use execution plan four.

So let’s go try to do that. Remember, query ID five, you want to use four because we found this awesome script on the internet and it said, hey, you know what you should do? You should force plans where you have a better one.

And you were like, okay, I’m going to do that because I don’t feel like doing actual work. So we’re going to say query ID five, use plan ID four. And we plug that in here and we plug that in here.

Oh man, I’m exhausted. Whew. Let’s start doing cardio or something. Just kidding. Just kidding.

Just kidding. So we’re going to run this and this now, now we will be allowed to associate that other plan ID with that other query ID. So if you are the type of person who gets cranky about SQL Server performance and you are the type of person who gets cranky about, I don’t know, stuff like regressions or I don’t know, things going wrong with queries. You know, you should make some attempt to use parameterized code if you are, if you wanted to use a query store to force execution plans.

Otherwise SQL Server will do what it does. Like when it compiles a lot of, when it like sees literal values and queries and keeps compiling new plans for them. Query store does the same thing.

It’s just like, I don’t know you. And it gives them new query IDs and then, and then you can’t force query plans across query IDs. And then you actually have to go tune queries.

And that sucks. It’s always, life is always a lot better when you can just hit a button. Isn’t it?

It is for me anyway. Alright. Thanks for watchin’. I don’t know. It’s always in a rescue kit. And it’s still dying if the weather is good for you.

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.