I love presenting sessions because you get so many interesting questions. For example, what happens with Optimize for Ad Hoc when Query Store is enabled? Great question. I didn’t have the answer, so, on to testing.
For those who don’t know, Optimize for Ad Hoc is a mechanism for dealing with lots and lots of ad hoc queries. When this is enabled, instead of storing an execution plan the first time a query is called, a plan stub, basically the identifying mechanisms, for the plan is stored in cache. This reduces the amount of space wasted in your cache. The second time the query is called, the plan is then stored in cache.
I’m going to set up Optimize for Ad Hoc and Query Store and, to clean the slate, I’ll remove everything from cache and clear out the Query Store, just in case:
EXEC sys.sp_configure N'show advanced options', N'1'; RECONFIGURE WITH OVERRIDE; GO EXEC sys.sp_configure N'optimize for ad hoc workloads', N'1'; GO RECONFIGURE WITH OVERRIDE; GO EXEC sys.sp_configure N'show advanced options', N'0'; RECONFIGURE WITH OVERRIDE; GO DBCC FREEPROCCACHE(); GO USE AdventureWorks2014; GO ALTER DATABASE AdventureWorks2014 SET QUERY_STORE = ON; GO ALTER DATABASE AdventureWorks2014 SET QUERY_STORE CLEAR; GO
Then, we just need an ad hoc query:
SELECT p.Name, soh.OrderDate, sod.OrderQty FROM Sales.SalesOrderHeader AS soh JOIN Sales.SalesOrderDetail AS sod ON sod.SalesOrderID = soh.SalesOrderID JOIN Production.Product AS p ON p.ProductID = sod.ProductID WHERE p.Name = 'Road-750 Black, 48' AND sod.OrderQty > 10;
If I run this query one time in my cleaned up environment, I can check to see if Optimize For Ad Hoc is working by querying the cache:
SELECT dest.text,
deqs.execution_count,
deqp.query_plan
FROM sys.dm_exec_query_stats AS deqs
CROSS APPLY sys.dm_exec_sql_text(deqs.sql_handle) AS dest
CROSS APPLY sys.dm_exec_query_plan(deqs.plan_handle) AS deqp
WHERE dest.text LIKE ‘SELECT p.Name,%’;
The results look like this:
So, what’s in the Query Store. We’ll use this query:
SELECT qsqt.query_sql_text, qsq.count_compiles, CAST(qsp.query_plan AS XML) FROM sys.query_store_query AS qsq JOIN sys.query_store_query_text AS qsqt ON qsqt.query_text_id = qsq.query_text_id JOIN sys.query_store_plan AS qsp ON qsp.query_id = qsq.query_id WHERE qsqt.query_sql_text LIKE 'SELECT p.Name,%';
The results look like this:
In short, the plan is stored in the query store, even though the plan isn’t stored in cache. Now, this has implications. I’m not saying they’re good and I’m not saying they’re bad, but there are implications. If you’re in a situation where you need to use Optimize For Ad Hoc to help manage your cache, now, you’re going to possibly see negative impacts on your Query Store since it’s going to capture all the plans that you avoided. There are mechanisms for managing Query Store behavior.
I’m going to modify my own Query Store to change the capture behavior from “All” to “Automatic.” This enables an internal filtering mechanism, defined by Microsoft, to eliminate some captures. When I reset everything and run the example ad hoc query one time, I get the plan stub in cache, but nothing in the query store (that I can see). I run the ad hoc query again and now I get a plan in the cache, but nothing in the query store. If I run the ad hoc query for a third time, there’s a counter somewhere (I haven’t found it yet)Â because I suddenly get a query in the Query Store.
For a bit more information, let’s modify the Query Store query to include some runtime stats:
SELECT qsqt.query_sql_text, qsq.count_compiles, CAST(qsp.query_plan AS XML), qsrs.count_executions FROM sys.query_store_query AS qsq JOIN sys.query_store_query_text AS qsqt ON qsqt.query_text_id = qsq.query_text_id JOIN sys.query_store_plan AS qsp ON qsp.query_id = qsq.query_id JOIN sys.query_store_runtime_stats AS qsrs ON qsrs.plan_id = qsp.plan_id WHERE qsqt.query_sql_text LIKE 'SELECT p.Name,%';
Now, I run this query and the one from above against the cache, I get the following information:
(you might have to click on that to make it bigger).
Here’s the interesting bit. The execution_count from cache, the top set of results, is 2, even though I ran the query three times. What happens in cache is that the plan stub is removed and the count is reset. The bottom set of results, from Query Store, shows only a single execution.
What does all this mean? Just that as we add additional behaviors to our systems, we have additional management worries. With the ability to modify the Query Store behavior, you won’t need to necessarily worry that you’re going to get hurt by your need to use Optimize for Ad Hoc.
[…] Grant Fritchey investigates the combination of using Query Store and turning on Optimize For Ad Hoc…: […]
Hey Grant,
Great post! Just one thing—I’m unable to click on the bottom image to make it bigger… any chance you can provide that link?
Thanks!
Sorry about that. It looks to be clickable now.
What versions of SQL server is this for? I get an error when I test with 2012.
Optimize for Ad Hoc goes back to SQL Server 2008, so you shouldn’t hit issues there: https://msdn.microsoft.com/en-us/library/cc645587(v=sql.100).aspx
Query Store on the other hand only exists in Azure SQL Database and SQL Server 2016. I have it marked in the tag and in the category for the post, but I didn’t mention it in the text. Sorry about that.
[…] I was pretty sure I’d read a post from Grant Fritchey about this, and sure enough, he covers the behavior here. […]
We are working with a Biztalk instance where QS is somehow using large amounts of memory. ~3/4 of the queries are ad hoc so I’m going to try turning on optimize for ad hoc and change the QS to automatic. I’ll let you know how it goes.
You might try upping the frequency that it writes to disk in order to reduce the memory footprint (at the cost of some I/O).