<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LingamSuyambu</title>
    <description>The latest articles on DEV Community by LingamSuyambu (@lingamsuyambu).</description>
    <link>https://dev.to/lingamsuyambu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4215876%2F22af5bb9-86c8-4459-b5b6-4bb8f288078c.png</url>
      <title>DEV Community: LingamSuyambu</title>
      <link>https://dev.to/lingamsuyambu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lingamsuyambu"/>
    <language>en</language>
    <item>
      <title>SARGable Queries in SQL Server: Why Functions in WHERE Slow You Down</title>
      <dc:creator>LingamSuyambu</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:02:01 +0000</pubDate>
      <link>https://dev.to/lingamsuyambu/sargable-queries-in-sql-server-why-functions-in-where-slow-you-down-35m9</link>
      <guid>https://dev.to/lingamsuyambu/sargable-queries-in-sql-server-why-functions-in-where-slow-you-down-35m9</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sqlcloudlab.blogspot.com/2026/10/sargable-queries-in-sql-server-why.html" rel="noopener noreferrer"&gt;SQL &amp;amp; Cloud Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You add the perfect index, run the query, and SQL Server still scans the whole table. One of the most common reasons is a non-SARGable predicate: a WHERE clause written in a way that stops SQL Server from using the index to seek. The fix is usually a small rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SARGable means
&lt;/h2&gt;

&lt;p&gt;SARGable stands for &lt;strong&gt;Search ARGument-able&lt;/strong&gt;. A predicate is SARGable when SQL Server can use it to jump straight to the matching rows in an index (an index seek). If you wrap the indexed column in a function or calculation, SQL Server can no longer search the index for a value; it has to calculate the expression for every row, which means a scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; leave the column bare on one side of the comparison, and do any calculation on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common non-SARGable patterns and their fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functions on date columns
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Not SARGable: YEAR() runs on every row&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;YEAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;

&lt;span class="c1"&gt;-- SARGable: a range on the bare column&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;OrderDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;OrderDate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2027-01-01'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use a half-open range (&lt;code&gt;&amp;gt;=&lt;/code&gt; start and &lt;code&gt;&amp;lt;&lt;/code&gt; next start) so it works correctly for date and datetime columns alike.&lt;/p&gt;

&lt;h3&gt;
  
  
  String functions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Not SARGable&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmployeeCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ABC'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmployeeCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ABC'&lt;/span&gt;

&lt;span class="c1"&gt;-- SARGable: a prefix LIKE can seek&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;EmployeeCode&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'ABC%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that a leading wildcard, such as &lt;code&gt;LIKE '%ABC'&lt;/code&gt;, cannot seek, because the index is sorted by the start of the value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Arithmetic on the column
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Not SARGable&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

&lt;span class="c1"&gt;-- SARGable: move the arithmetic to the constant&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ISNULL and COALESCE in filters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Not SARGable&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;ISNULL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Open'&lt;/span&gt;

&lt;span class="c1"&gt;-- SARGable: NULL never equals 'Open', so the function is not needed&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Open'&lt;/span&gt;

&lt;span class="c1"&gt;-- When you really need NULLs too&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Implicit conversions
&lt;/h3&gt;

&lt;p&gt;This one is easy to miss because the function is hidden. If a VARCHAR column is compared with an NVARCHAR value, SQL Server may convert the column on every row, which can turn a seek into a scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- EmployeeCode is VARCHAR(20)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;EmployeeCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'E1001'&lt;/span&gt;  &lt;span class="c1"&gt;-- NVARCHAR literal: can force a conversion&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;EmployeeCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'E1001'&lt;/span&gt;   &lt;span class="c1"&gt;-- matching type: clean seek&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This happens a lot from application code. In .NET, string parameters default to NVARCHAR; for VARCHAR columns, set the parameter type explicitly (for example &lt;code&gt;SqlDbType.VarChar&lt;/code&gt;, or configure the column type in EF Core). The execution plan shows a warning icon for implicit conversions, so it is easy to confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about OR and NOT?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OR across different columns&lt;/strong&gt; (&lt;code&gt;WHERE A = 1 OR B = 2&lt;/code&gt;) often prevents a single seek. Rewriting it as two queries combined with &lt;code&gt;UNION&lt;/code&gt; (or &lt;code&gt;UNION ALL&lt;/code&gt; when rows cannot overlap) can let each part use its own index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; and &lt;code&gt;NOT IN&lt;/code&gt;&lt;/strong&gt; are technically SARGable, but they usually match so many rows that a scan is the right choice anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When you cannot change the expression
&lt;/h2&gt;

&lt;p&gt;If the calculation is unavoidable, for example you always filter on an expression, you can add a computed column for it and index that column. SQL Server can then seek on the indexed expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;OrderYear&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;YEAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX_Orders_OrderYear&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderYear&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Queries that use &lt;code&gt;YEAR(OrderDate) = 2026&lt;/code&gt; or &lt;code&gt;OrderYear = 2026&lt;/code&gt; can then use the new index.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your fix
&lt;/h2&gt;

&lt;p&gt;Run the query with the actual execution plan and &lt;code&gt;SET STATISTICS IO ON&lt;/code&gt;. A successful rewrite changes an &lt;strong&gt;Index Scan&lt;/strong&gt; into an &lt;strong&gt;Index Seek&lt;/strong&gt; and drops logical reads sharply, often from thousands to a handful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick rules
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep indexed columns bare in WHERE and JOIN conditions.&lt;/li&gt;
&lt;li&gt;Replace date functions with ranges, and LEFT/SUBSTRING with prefix LIKE.&lt;/li&gt;
&lt;li&gt;Move arithmetic to the constant side.&lt;/li&gt;
&lt;li&gt;Match parameter data types to column data types.&lt;/li&gt;
&lt;li&gt;Use an indexed computed column when the expression is unavoidable.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;More SQL Server guides on my blog, &lt;a href="https://sqlcloudlab.blogspot.com/" rel="noopener noreferrer"&gt;SQL &amp;amp; Cloud Lab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>sqlserver</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
