Blog Post

VARCHAR = @NVARCHAR and SARGability

,

In my previous post, I mentioned the use of functions, specifically ISNULL and the effects on SARGability of the query. There is another one that often catches out developers and DBAs.

To start off I have created the following two tables and populated them with the same 3 million rows:

post99_1_TableMyUnicodeUserspost99_2_TableMyUsers

Both have clustered index on Id and a non-clustered on DisplayName

CREATE NONCLUSTERED INDEX [IX_MyUnicodeUsers_DisplayName]
ON [dbo].[MyUnicodeUsers] ([DisplayName])
GO

CREATE NONCLUSTERED INDEX [IX_MyUsers_DisplayName]
ON [dbo].[MyUsers] ([DisplayName])
GO

The tables are very similar with the only difference being that AboutMe and DisplayName columns in MyUnicodeUsers are of data type nvarchar, unlike varchar data type in MyUsers table. Before running any queries, please select “Include Actual Execution Plan”. This can be easily done using the keyboard shortcut, “Ctrl + M”.

Now let’s run the following two queries against MyUnicodeUsers table:

SELECT Id, DisplayName
FROM MyUnicodeUsers 
WHERE DisplayName = 'Paul'
SELECT Id, DisplayName
FROM MyUnicodeUsers 
WHERE DisplayName = N'Paul'

The execution plans look as follows:

post99_3_ExecutionPlan1

Nothing to worry there. Now let’s run the same two queries against MyUsers table:

SELECT Id, DisplayName
FROM MyUsers 
WHERE DisplayName = 'Paul'
SELECT Id, DisplayName
FROM MyUsers 
WHERE DisplayName = N'Paul'

And here’s the execution plan:

post99_4_ExecutionPlan2_detail

Huh? What happened there?

When the last query ran, SQL Server decided to scan the non-clustered index instead of doing a seek, like it did in the three previous queries. With one of the queries run against MyUnicodeUsers table, I did compare the varchar parameter to nvarchar data type column. And that seemed fine. So why the difference?

The answer to that lies in this MSDN article.

The nvarchar data type has higher precedence than varchar data type. So when the operator compares the DisplayName varchar column with the nvarchar parameter, it will implicitly convert the DisplayName column, making the predicate non-sargable and, thereby, hindering the seek on the non-clustered index. Well, well… that wasn’t quite what I expected.

If you looks closely, and hover your mouse over the execution plan that did a seek on MyUnicodeUsers table. even though the two data types – column and parameter – were different, you will notice:

post99_5_ExecutionPlan1_detail

There was an implicit conversion, although it did not affect the sargability of our query as it wasn’t the column that got converted, but the parameter passed.

Hmmmm, I wonder how many queries running on my database are suffering from scans due to implicit conversions on the column.

Fortunately, Jonathan Kehayias has provided a script in this article, which is as follows:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DECLARE @dbname SYSNAME 
SET @dbname = QUOTENAME(DB_NAME()); 
WITH XMLNAMESPACES 
   (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') 
SELECT 
   stmt.value('(@StatementText)[1]', 'varchar(max)'), 
   t.value('(ScalarOperator/Identifier/ColumnReference/@Schema)[1]', 'varchar(128)'), 
   t.value('(ScalarOperator/Identifier/ColumnReference/@Table)[1]', 'varchar(128)'), 
   t.value('(ScalarOperator/Identifier/ColumnReference/@Column)[1]', 'varchar(128)'), 
   ic.DATA_TYPE AS ConvertFrom, 
   ic.CHARACTER_MAXIMUM_LENGTH AS ConvertFromLength, 
   t.value('(@DataType)[1]', 'varchar(128)') AS ConvertTo, 
   t.value('(@Length)[1]', 'int') AS ConvertToLength, 
   query_plan 
FROM sys.dm_exec_cached_plans AS cp 
CROSS APPLY sys.dm_exec_query_plan(plan_handle) AS qp 
CROSS APPLY query_plan.nodes('/ShowPlanXML/BatchSequence/Batch/Statements/StmtSimple') AS batch(stmt) 
CROSS APPLY stmt.nodes('.//Convert[@Implicit="1"]') AS n(t) 
JOIN INFORMATION_SCHEMA.COLUMNS AS ic 
   ON QUOTENAME(ic.TABLE_SCHEMA) = t.value('(ScalarOperator/Identifier/ColumnReference/@Schema)[1]', 'varchar(128)') 
   AND QUOTENAME(ic.TABLE_NAME) = t.value('(ScalarOperator/Identifier/ColumnReference/@Table)[1]', 'varchar(128)') 
   AND ic.COLUMN_NAME = t.value('(ScalarOperator/Identifier/ColumnReference/@Column)[1]', 'varchar(128)') 
WHERE t.exist('ScalarOperator/Identifier/ColumnReference[@Database=sql:variable("@dbname")][@Schema!="[sys]"]') = 1
and ic.DATA_TYPE = 'varchar'

This will display details of any queries suffering from scans due to implicit conversions to a column. Fantastic!

The post VARCHAR = @NVARCHAR and SARGability appeared first on SQLYSE.

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating