info@techdevops.com | 437-991-3573 | Data Engineering Services
TechDevOps.com
Resources Tools
Experts in Microsoft SQL Server on Windows, Linux, Containers | Clusters, Always On, FCI | Migrations, Cloud, Performance



Get Non-SARGable Queries in SQL Server
by BFarrell (Principal Consultant; Architecture; Engineering)
2026-03-29







A key part of query performance tuning is inspecting plans for non-SARGable predicates.

Leverage the Query Store which keeps a history of queries, plans and runtime stats.

Then analyze the SQL Server plans which show the operators and predicates the query optimizer used.


What usually makes a query non-SARGable?

Common patterns are predicates that apply an expression to the column side, such as:

WHERE YEAR(OrderDate) = 2025
WHERE LEFT(LastName,1) = 'S'
WHERE ISNULL(Col,'') = 'X'
WHERE CAST(DateCol AS date) = @d
WHERE Col + 1 = 10
WHERE Name LIKE '%abc'

Those patterns often prevent efficient index seeks. The indicator is usually more logical reads than you expected, which you can confirm with SET STATISTICS IO.

Look for:

Table Scan
Index Scan
A predicate that contains a function or expression on the column
Implicit conversions

With a re-write the goal is fewer logical reads and, often, less CPU and elapsed time.

Typical rewrite:

-- Non-SARGable
WHERE YEAR(OrderDate) = 2025

-- Re-write
WHERE OrderDate >= '20250101'
AND OrderDate < '20260101'


Query #1: Find Slow Running Queries

SELECT TOP (50)
qsq.query_id,
qsp.plan_id,
SUM(rs.count_executions) AS executions,
SUM(rs.avg_logical_io_reads * rs.count_executions) AS total_logical_reads,
SUM(rs.avg_cpu_time * rs.count_executions) AS total_cpu_time,
SUM(rs.avg_duration * rs.count_executions) AS total_duration,
qt.query_sql_text
FROM sys.query_store_query_text AS qt
JOIN sys.query_store_query AS qsq
ON qt.query_text_id = qsq.query_text_id
JOIN sys.query_store_plan AS qsp
ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats AS rs
ON qsp.plan_id = rs.plan_id
GROUP BY
qsq.query_id,
qsp.plan_id,
qt.query_sql_text
ORDER BY total_logical_reads DESC;


Query #2: Find queries that are likely the non-SARGable ones

This is useful when Query Store is off or you want a quick scan of cached plans.

;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT TOP (100)
qs.total_logical_reads,
qs.total_worker_time,
qs.execution_count,
SUBSTRING(st.text,
(qs.statement_start_offset/2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1) AS statement_text,
qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
WHERE
qp.query_plan.exist('//RelOp[@LogicalOp=("Index Scan","Clustered Index Scan","Table Scan")]') = 1
AND (
qp.query_plan.exist('//Predicate//ScalarOperator') = 1
OR qp.query_plan.exist('//SeekPredicates//ScalarOperator') = 1
)
ORDER BY qs.total_logical_reads DESC;


What //Predicate//ScalarOperator means
It’s an XPath expression used to search inside an execution plan XML.
Does this query have any expressions inside predicates?
i.e. Find any expression used inside a predicate anywhere in the execution plan.

// ? search anywhere in the XML
Predicate is a filter condition (like your WHERE clause)
The ScalarOperator is the actual expression logic:
comparisons (=, >, <)
functions (YEAR(), LEFT())
expressions (Col + 1)


Query #3: Detect implicit conversions

Implicit conversions are a very common hidden cause. They can make an otherwise good predicate behave badly. They hurt performance because they often prevent the optimizer from using indexes efficiently and add extra work at runtime. They can break index seeks(biggest issue).

;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT TOP (100)
qs.total_logical_reads,
qs.execution_count,
st.text,
qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.exist('//Convert[@Implicit="1"]') = 1
ORDER BY qs.total_logical_reads DESC;.

If this returns a query that is joining or filtering on mismatched data types, fix the data types. Then fix any code & schema data type mismatches.

Convert[@Implicit="1"] in plan happens when SQL Server automatically converts one data type to another to make a comparison work.

Example:
WHERE IntColumn = '123' -- string compared to INT

SQL Server rewrites it internally like:
WHERE IntColumn = CONVERT(INT, '123')

Sometimes it’s the column that gets converted instead - that is a the performance issue.

Why implicit conversions are bad:

Implicit conversions break index seeks (biggest issue)
If SQL Server has to convert the column, it can’t use the index efficiently.
Index scan instead of seek.
Reads way more data than needed.

Implicit conversions cause Higher CPU usage
Every row may need conversion:
On large tables:
thousands/millions of conversions
unnecessary CPU overhead

Implicit conversions cause Bad cardinality estimates
SQL Server may misestimate how many rows match:
leads to poor execution plans
wrong join types
memory issues

Implicit conversions cause more logical reads
Because seeks turn into scans:
SQL reads many more pages
slower queries, more IO pressure

Implicit conversions cause Plan instability
Same query, different parameter types ? different plans
WHERE Col = @param
Sometimes @param is INT
Sometimes VARCHAR
SQL Server may:
generate different plans
or reuse a bad one

Implicit conversions are a Performance destroyer
It’s not obvious in the query
You only see it in the execution plan:
warning icon
CONVERT_IMPLICIT

WHERE UserId = '100'
If UserId is INT:
SQL may convert column ? scan

WHERE UserId = 100
Proper type:
Index seek

Key rule (super important)
Conversions on the COLUMN = bad
Conversions on the VALUE = usually fine

Speed, Speed, Speed !!!!!