In this article I will continue to suggest easy ways that one can explore the cached query plans via SQL and XML as explained in ‘Exploring Query Plans in SQL‘. I will demonstrate some more tricks, such as how to find query plan warnings inside the cached query plans.
If you are not checking for warnings in your query plans during development or when testing, then you are missing some valuable information that could make a considerable difference to the performance of your queries. You can, for example, detect implicit conversion, missing statistics, and missing indexes. The table-valued functions I show you here will allow you to see, without having to get involved in XML, all the warnings in plan cache for a particular database, or allow you to select particular types. This method would only be suitable for use in the test or development environment. In production, or where the plan cache is large, it would cause a significant performance hit.Query plan Warnings
Query plan Warnings are included inside the query plan to alert you to problems that the query optimizer can’t solve for you. For example, if the query tries to compare one string field with a decimal value, the database engine will use auto-convert to execute the query but will include a warning (PlanAffectingConvert warning) because the query could run better if the value was a string instead of a decimal value.
Until SQL Server 2008, the warnings didn’t appear in the query plan. This meant that you had to use SQL Profiler to see them. Since SQL Server 2008, the warnings are displayed in query plans.
Another warning example is MissingIndex: this warning advises us that the query would run much better with an index that doesn’t exist yet.
These query plan warnings are valuable information that points to ways that you can make your queries, applications and environment better. Let’s talk about how to find them in the query plan cache.
First things First: Filtering by Database
Before we get too deeply into selecting the Query-plan Warnings, we will need a way of selecting just the plans for a particular database. In the article ‘Exploring Query Plans in SQL‘, I demonstrated how to find details about the query plans in the cache, but the cache contains plans for all the databases in the server. What if we would like to see information about only one database? How could we filter the queries by one specific database? The query plans in the cache are in XML format following the query plan schema (http://schemas.microsoft.com/sqlserver/2004/07/showplan), so if you inspect this you’ll notice that we can use XML queries to find the ‘ColumnReference‘ element that has ‘Database' attribute. We can filter the query plan by this combination to select the pans of a particular database. The query for this will be:
|
1 2 3 4 5 6 7 |
with xmlnamespaces (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') select qp.query_plan,qt.text, total_worker_time from sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) qt CROSS APPLY sys.dm_exec_query_plan(plan_handle) qp where qp.query_plan.exist('//ColumnReference[@Database="[AdventureWorks2012]"]')=1 order by total_worker_time desc |
xmlnamespaces declaration in the beginning of the query simplifies the syntax of the xquery expression, removing the need for the namespace declaration from the xquery expression.
Creating a table-valued function to filter by database
This is an ideal query to transform into a function: The query will became easier to use and we can use the function to create more complex queries filtered on a particular database. For each function we create the need to get too familiar to XML decreases, because the function hides these details. The function will be like this:|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
-- ============================================= -- Author: Dennes Torres -- Create date: 01/23/2015 -- Description: return the query plans in cache for a specific database -- ============================================= CREATE FUNCTION [dbo].[planCachefromDatabase] ( -- Add the parameters for the function here @DatabaseName varchar(50) ) RETURNS TABLE AS RETURN ( with xmlnamespaces (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') select qp.query_plan,qt.text, statement_start_offset, statement_end_offset, creation_time, last_execution_time, execution_count, total_worker_time, last_worker_time, min_worker_time, max_worker_time, total_physical_reads, last_physical_reads, min_physical_reads, max_physical_reads, total_logical_writes, last_logical_writes, min_logical_writes, max_logical_writes, total_logical_reads, last_logical_reads, min_logical_reads, max_logical_reads, total_elapsed_time, last_elapsed_time, min_elapsed_time, max_elapsed_time, total_rows, last_rows, min_rows, max_rows from sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) qt CROSS APPLY sys.dm_exec_query_plan(plan_handle) qp where qp.query_plan.exist('//ColumnReference[fn:lower-case(@Database)=fn:lower-case(sql:variable("@DatabaseName"))]')=1 ) GO |
WITH xmlnamespaces' clause, I also used the lower-case function, so the parameter becomes case-insensitive. This can prevent all manner of confusion.
We can now adopt this function for a whole range of database-specific queries. A simple query using the function might be:
|
1 2 3 |
select query_plan,[text],total_worker_time from dbo.planCacheFromDatabase('[AdventureWorks2012]') order by total_worker_time desc |
Finding Warnings in the Plan Cache
Test Environment
I will use the AdventureWorks2012 database to test the functions, but with additional tables created by Adam Machanic in his blog post ‘Thinking Big (Adventure)’Finding PlanAffectingConvert Warning
Grant Fritchey has written aboutPlanAffectingConvert. in his article ‘The Seven Sins against TSQL Performance‘, it’s a query plan warning about there being an implicit convertion in the plan.
Implicit conversion happens when we try to compare information of different types. For example, if we try to compare a string field with an integer SQL Server will convert the information implicit, but this has a cost and can cause problems to the query, so we receive a warning about the conversion.
Let’s use the same sample query used by Grant in his article, we can notice the warning in the query plan:
|
1 2 3 4 |
SELECT e.BusinessEntityID, e.NationalIDNumber FROM HumanResources.Employee AS e WHERE e.NationalIDNumber = 112457891; |
After running this query, the plan will be in the cache and it has the PlanAffectingConvert warning in it. So, let’s create a query using our new ‘planCacheFromDatabase‘ function to find PlanAffectingConvert in the plan cache:
|
1 2 3 4 5 6 |
with xmlnamespaces (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') select query_plan,text, total_worker_time from dbo.planCacheFromDatabase('[AdventureWorks2012]') where query_plan.exist('//PlanAffectingConvert')=1 order by total_worker_time desc |
planCacheFromDatabase and filter the query plans to find which one has the PlanAffectingConvert warning.
Finding Warnings
PlanAffectingConvert is only one of many warnings that can appear in query plans. We need to retrieve from the cache the query plans that have warnings, with the information of which and how many warnings each query plan has. With this information we can analyze and change the query to solve the problems.
It’s important to notice that there are different kinds of warnings. These warnings can appear in any element of the query plan, so there are many ‘warnings’ elements in the query plan schema.
One example is the ColumnWithNoStatistics warning. While ‘PlanAffectingConvert' appeared over the ‘Select‘ element in the plan, ‘ColumnWithNoStatistics' will appear over ‘scan‘ elements if we do a query over a column with no statistics.
Statistics are very important, even in fields with no index, because they help the query optimizer to choose the best plan for the query. When we do a query over a field with no statistics and the query could be improved with them, the warning ‘ColumnWithNoStatistics' is included in the query plan.
It’s important to notice that SQL Server databases have the configuration ‘Auto Create Statistics‘ with the default as true. This means that we will never see this warning, because SQL Server will automatically create the statistics when needed.
This is the best setting in the production environment, even though it will affect all the clients while SQL Server creates the statistics over big tables.
For the Test or development environment, it is possible to disable ‘Auto Create Statistics’ and to look for ‘ColumnWithNoStatistics‘ in the cache to find where you would need to create statistics (or even indexes).
To simulate this warning we need to temporarily disable ‘Auto Create Statistics’ in AdventureWorks database, it will be like this:
|
1 |
alterdatabase adventureworks2012 setauto_Create_statistics off |
|
1 2 |
select * from bigtransactionHistory where TransactionDate >'2006/01/01' and transactiondate <'2006/02/20' |
Make sure that, once you’ve run your tests, you re-enable ‘Auto Create Statistics’ .
Now that we see the importance of warnings like ‘ColumnWithNoStatistics‘, ‘PlanAffectingQuery‘ and many others, let’s create a query to find which and how many warnings each query plan in the cache has:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
with xmlnamespaces (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan'), qry as ( select [text], cast(nos.query('local-name(.)') as varchar) warning, total_Worker_time from dbo.planCacheFromDatabase('[AdventureWorks2012]') CROSS APPLY query_plan.nodes('//Warnings/*') (nos) ) select [text],warning,count(*) qtd,max(total_worker_time) total_worker_time from qry group by [text],warning order by total_worker_time desc |
- To combine in the same query the
xmlnamespacesand one CTE (Common Table Expression) I used only one ‘with’ clause and a comma to separate thexmlnamespacesand the CTE - The xquery expression doesn’t include the path of the ‘Warnings’ element because the ‘Warnings’ element can appear in different paths in the schema.
- Below the ‘Warnings’ element each warning is a different element and the name of the element is the name of the warning. The ‘
local-name‘ function retrieves the name of the element (the warning name) for the query.
The above query is a good candidate to become a function, so here it is:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
-- ============================================= -- Author: Dennes Torres -- Create date: 01/24/2015 -- Description: Return the warnings in the query plans in cache -- ============================================= CREATE FUNCTION [dbo].[FindWarnings] ( -- Add the parameters for the function here @DatabaseName varchar(50) ) RETURNS TABLE AS RETURN ( with xmlnamespaces (default 'http://schemas.microsoft.com/sqlserver/2004/07/showplan'), qry as (select [text], cast(nos.query('local-name(.)') as varchar) warning, total_Worker_time from dbo.planCacheFromDatabase(@DatabaseName) CROSS APPLY query_plan.nodes('//Warnings/*') (nos) ) select [text],warning,count(*) qtd,max(total_worker_time) total_worker_time from qry group by [text],warning ) GO |
|
1 2 |
select * from dbo.FindWarnings('[AdventureWorks2012]') order by total_worker_time desc |
Summary
In this article we talked about the importance of warnings in query plan cache, and how we can use this information in development, or while testing, to make our environment better. You saw queries to find query plans with warnings in the plan cache and we created functions so you can use these techniques without the need to create XML queries at the time that you are investigating warnings.FAQs
1. What are query plan warnings in the SQL Server plan cache?
Query plan warnings are annotations the SQL Server query optimiser adds to a plan when it identifies issues it cannot resolve automatically. Common warnings include PlanAffectingConvert (an implicit conversion is preventing efficient index use), ConvertIssue (similar, for non-SARGable predicates), NoStatsFound (statistics are missing on referenced columns), MissingIndexes (the optimiser’s recommendation for indexes that would improve the query), and unmatched indexes (indexes the optimiser considered but couldn’t use). The warnings are stored inside the cached plan XML in sys.dm_exec_cached_plans. Since SQL Server 2008 they appear in graphical execution plans in SSMS; before 2008, only SQL Profiler exposed them.
2. What does PlanAffectingConvert mean in a SQL Server query plan?
PlanAffectingConvert means SQL Server detected an implicit data type conversion in a predicate that is forcing it to use a less efficient query plan – typically an index scan instead of an index seek. The classic case is a query comparing a string column against a numeric literal (for example, WHERE varchar_column = 12345), which forces SQL Server to convert every row’s column value to numeric before comparing. The fix is to make the predicate match the column’s actual type (use ‘12345’ as a string literal, or convert the literal to varchar in your query). PlanAffectingConvert is the warning to scan for first when investigating slow queries with no obvious cause – the impact on cardinality estimation can be enormous.
3. How do I find queries with missing statistics warnings in SQL Server?
Query the cached plans for the NoStatsFound warning. Using the table-valued function this article defines (which filters plans by database), the pattern is: SELECT plan_handle, query_text FROM dbo.GetCachedPlansForDatabase(‘MyDb’) WHERE plan_xml.exist(‘//*[local-name()=”Warnings”]/*[local-name()=”NoStatsFound”]’) = 1. The result identifies queries where SQL Server couldn’t find appropriate statistics on referenced columns – usually meaning UPDATE STATISTICS or CREATE STATISTICS is needed on those columns. NoStatsFound warnings are particularly worth fixing because missing statistics often produce wildly inaccurate cardinality estimates, which then produce poor plans across many queries that touch the affected tables.
4. Should I query the SQL Server plan cache in production?
Generally no. Querying the plan cache – especially queries that parse plan XML or scan all cached plans for a database – is computationally expensive, locks portions of the cache during the scan, and can affect concurrent query performance. For a large production cache, the scan can take minutes and cause noticeable contention. The plan-cache investigation queries in this article are designed for development and test environments where the cache is smaller and contention is acceptable. For production, capture plans selectively via Extended Events, Query Store (SQL Server 2016+), or by inspecting individual slow queries’ plans through SSMS. Query Store is particularly suitable for production warning analysis because it persists plans and metadata outside the in-memory cache.
This document contains proprietary information and is protected by copyright law.
Copyright © 2026 Red Gate Software Limited. All rights reserved
Load comments