There Are Days When I Feel Like Giving Up on the Plan Cache and Query Store.
In theory, SQL Server performance monitoring is pretty simple:
- Review the server’s top wait types
- Find the queries causing those wait types
- Fix those queries, or improve the way the server reacts to them (indexes, settings, etc.)
But in practice, step 2 is awful because:
- Apps send unparameterized strings to the database server
- Entity Framework users build queries with FromSqlRaw or string.Format()
- Entity Framework users write queries with .Contains, which builds an unparameterized IN list, even when they’re only looking for a single value (which got better in EF9)
- People write sloppy dynamic SQL that just concatenates values directly into the query string
- SaaS developers put each client in their own database, and plans aren’t reused across databases
So as a result, the plan cache and Query Store are damn near useless because every query that comes in is seen as a “new” query. I wrote about this back in 2018, and since then, the problem seems like it’s gotten continuously worse. I’ve been tracking client servers over the last couple of years, and these days, 1 in 3 servers I face has these issues. My lens might be distorted since maybe people who aren’t having this problem are all solving their performance issues with conventional tools like sp_BlitzCache and Query Store, but even if that’s true, there’s a problem bigger than human query troubleshooting.
Modern versions of SQL Server are increasingly reliant on properly parameterized queries. Tools like Automatic Plan Regression (aka Automatic Tuning), Adaptive Memory Grants, Adaptive Joins, CE Feedback, DOP Feedback, Parameter-Sensitive Plan Optimization, and Optional Parameter Plan Optimization all rely on proper parameterization so that they can adapt to the same query over time. If every query comes in wearing a disguise, these features just don’t work.
There’s a database-level switch that’s supposed to help: Forced Parameterization. Turn it on, and SQL Server and Azure SQL DB examine every incoming query, and if it isn’t fully parameterized, the literals are stripped out and replaced with variables. The problem is that it doesn’t work in a lot of situations:
- Partially parameterized queries – if a query has any parameters, Microsoft assumes the whole thing is parameterized, which is especially problematic for EF’s .Contains
- Literals in the SELECT list, like SELECT 1 AS ClientId, … – which always blows me away when I see them, but strangely it’s a commonly used technique for reasons I will never understand
- Literals in HAVING, GROUP BY, and ORDER BY
- And more, as explained in
this SQL Server 2008 R2 documentation pagethat has never been updated (Update May 9: the documentation has been updated!)
Even when Forced Parameterization does work, turning it on suddenly causes parameter sniffing emergencies. Queries that used to get their own hand-crafted plans suddenly get reusable plans, and while that’s great for performance monitoring, it’s not so great for end user performance in some scenarios. Your application might have 1,000 queries, and 990 of them might be just fine with reusable queries – but those 10 others represent 10 different parameter sniffing emergency situations that are going to strike out of nowhere, and keep striking if you don’t fix the queries for good.
I don’t have any answers.
It just feels like there are two kinds of shops:
- The ones who properly parameterize everything, and can leverage the plan cache and Query Store, but suffer from parameter sniffing emergencies. So they have good monitoring, but they need it, because they face performance emergencies from time to time.
- The ones who don’t parameterize at least some of their code, so the plan cache & Query Store are largely useless at best, misleading at worst – but they don’t have parameter sniffing emergencies. So they have bad monitoring, but … they don’t care as much.
Some days, I look at that latter group and say, I get it. Not all the time! Most of the time, I want the cool features built into modern versions of SQL Server, Azure SQL DB, and Intelligent Query Processing. But some days… some days, I want to embed literals into all my queries.
Related

Hi! I’m Brent Ozar.
I make Microsoft SQL Server go faster. I love teaching, travel, cars, and laughing. I’m based out of Las Vegas. He/him. I teach SQL Server training classes, or if you haven’t got time for the pain, I’m available for consulting too.


17 Comments. Leave new
QueryStore / AutoTuning seemed like such a great opportunity on paper that year. I was amped up to see it, but disappointed getting consistency out of it.
And here we are, with the same stubborn issues a decade later.
So you’re saying we should blame the application developers. I’m okay with that. 😉
re: • SaaS developers put each client in their own database
Do they not know about row level security and/or how to build a multi tenant db?
Or are there legitimate reasons to use separate DBs? That is, apart from management directive.
There are usually compelling reasons to not use separate dbs. eg: when there are tables that are common to all tenants that require either duplicate tables or foregoing the FK constraints. From my experience there are always common tables.
Managment can often be the issue: sheet scared of leakage and don’t understand FK constraints. I get that, but are there real reasons?
And what is their MO to deal with common tables?
Yeah, there are real reasons: https://www.brentozar.com/archive/2011/06/how-design-multiclient-databases/
Good article and discussion. Thank You.
Thanks!
It looks like the Forced Parameterization article in the Query Processing Architecture section of the SQL2008R2 docs is now a topic in the current docs article called Query Processing Architecture Guide. Yes, it was difficult to find.
https://learn.microsoft.com/en-us/sql/relational-databases/query-processing-architecture-guide?view=sql-server-ver17#forced-parameterization
WHOA! Nice work! I’ve been looking for that for years! Dang.
SQL2K8 is bestest SQL (I may be a tad biased)
Will it override what WHERE 1=1 does to a query?
Came here to say this.
Great point! I should update this post to reflect the new documentation. Thanks!
[…] There Are Days When I Feel Like Giving Up on the Plan Cache and Query Store. (Brent Ozar) […]
This was fixed for real in EF Core 10!
I’m very excited for that too. I’ve just been frustrated lately with several clients telling me they’re on 2-3 year old “long term support” versions of EF, and that it’ll be years before they can get to EF10. (sigh)
Thanks for the post.
My friend said “you forgot cursors; we have a system which uses cursors to control the dataflow to the application which is more challenging to identify the active unparameterized queries causing performance issues”.
Absolutely true! Those are so painful in the plan cache.