Reviewing the concept
A quick search will tell you that implicit conversions are pretty awful for performance, and in particular drive CPU usage. That’s not news. There is an aspect of this I think a lot of engineers don’t understand; why does it cause performance issues?
An implicit conversion occurs when you try to compare between or assign data across two different data types, without converting one yourself. If you used a CAST or CONVERT, it would be an explicit conversion. When SQL Server does it for you, it’s an implicit conversion, and these can have a real impact on your execution plans. Not all combinations of types can be converted implicitly, for a full list look here.
So let’s look at an example based on WideWorldImporters:
--CREATE INDEX IX_Sales_OrderLines_Description ON Sales.OrderLines(Description);
SELECT sol.OrderLineID, sol.Description
FROM Sales.OrderLines sol
WHERE sol.Description = 'Plush shark slippers (Gray) XL'
The Description column is an nvarchar type, but I’m comparing it to a string that is non-unicode. And for the sake of this example, I did create an index on this column.
And we see an implicit conversion. In this case, the optimizer converted our string to a nvarchar(4000) type to match the Description column. But in this case, the implicit conversion isn’t going to cause a problem. Converting one value isn’t that expensive; you can confirm that from the ~0.025 cost for this operation. We even used the index on the column.
The real problem comes when SQL Server converts the column to match the variable.
Reproducing the issue
I’ve seen implicit conversions many times, and have one example in my head I recall very clearly. So I was fairly astonished when I tried to reproduce this using WideWorldImporters. SQL Server tries very hard to convert the parameter or literal, rather than the column. It’s quite difficult to get it behave badly, in part because of the data types chosen for the tables.
Microsoft’s documentation on data type precedence indicates that SQL Server will convert from one type to another based on the priority in the chart; but I have an example that seems to counter this.
DECLARE @dec decimal(18,2) = 27.0;
SELECT
sol.OrderLineID, sol.Quantity
FROM Sales.OrderLines sol
WHERE
sol.Quantity = @dec;
The Quantity column is a int, which has a lower priority than our decimal type variable. So according to the chart, the column should be converted, meaning every value on every row; more on that later.
But looking at the plan, we don’t convert the Quantity column. Instead there is a scalar operator on the variable that appears to be obscuring the underlying conversion:
So, the behavior here doesn’t match expectations. In my testing, all the int\decimal\money types worked similarly, and the optimizer would convert the variable. One takeaway I had from writing this is that implicit conversion occur much more often than I thought, but SQL Server is smart enough to convert the variable unless it has no choice. You likely wouldn’t see the many plans that convert the column, because they aren’t causing a performance problem. Instead, you’ll see and remember the one that caused a massive issue.
another angle
An easy example would be to compare a varchar column to an nvarchar variable. This forces SQL Server’s hand, only in one direction. SQL can easily convert any varchar to an nvarchar, but the opposite is more problematic. If your nvarchar string contains kanji or something else that can’t be represented with a varchar string, the operation would fail. So, SQL Server would rather convert the varchar, even if that means converting a column.
Note, I had to change databases here, because there are no varchar columns in WideWorldImporters (thus preventing this convert).
USE AdventureWorks2014
GO
DECLARE @AcctNum NVARCHAR(20) = N'AW00000146';
SELECT
cus.AccountNumber
FROM Sales.Customer cus
WHERE cus.AccountNumber = @AcctNum;
--CREATE INDEX IX_Customer_AccountNumber ON dbo.Customer(AccountNumber);
--DROP INDEX IX_Customer_AccountNumber ON dbo.Customer;
Even though I’ve created an index on the AccountNumber column, this results in a scan.
Usually I’ve seen the CONVERT_IMPLICIT on the scan operator itself, but here we see it as part of the filter. The query only returns 1 row, but we read all 19820 rows on the table to fulfill this. Image the effect if this table had 10 million rows.
Why the Table Scan?
Reasonable question. SQL Server doesn’t really have a choice. The index is built on the AccountNumber, not the output of this function call:
CONVERT_IMPLICIT(nvarchar(10),[AdventureWorks2014].[Sales].[Customer].[AccountNumber] as [cus].[AccountNumber],0)
So, SQL Server can’t search the index for this information; it’s not in the index. And SQL Server can’t really predict what the output of the function will be before it has converted the data, so it has to look at every AccountNumber value, convert it, then compare. The most expensive part isn’t the convert itself, but the table scan it forces.
So, pay attention to your data types when writing procedures and queries to avoid these issues. The effect of an implicit conversion on a column is much more pronounced the larger the table is, and it can easily drive your CPU to 100%.
Hidden Implicit Conversions
One of my attempts to create an implicit conversion worked, but didn’t show it clearly in the plan. I tried the following example using the sql_variant type:
DECLARE @svar sql_variant = 90.1;
SELECT
sol.OrderLineID, sol.UnitPrice
FROM Sales.OrderLines sol
WHERE
sol.UnitPrice = @svar;
The plan is similar to the last one, but we don’t see the CONVERT_IMPLICIT when we mouseover any of the three operators.
However, if you hit F4 and examine the Compute Scalar operator, you can see the definition of expression Expr1001 in the Properties window.
This clearly shows the column being converted, But it’s odd to need to do this to see the convert.
This would be a good time to refer to a bit of brilliance Jonathan Kehayias posted quite some time ago. This script will allow you to see any implicit conversions on columns in the plan cache, along with the statement, plan, and other relevant details. This can be useful in a number of cases, but especially when the operation is hidden as in this example.
Also, Any Other Function CALL
This post is primarily about understanding how implicit conversions work and why the occur, but it bears mentioning that basically any function call wrapping a column you want to search on will work the same way.
For example, using SUBSTRING on AccountNumber gives largely the same result:
SELECT
cus.AccountNumber
FROM Sales.Customer cus
WHERE
SUBSTRING(cus.AccountNumber,7,4) = '0146';
If you need to use a function call in your WHERE, hopefully you have another clause that can do the majority of your filtering.
Happy New Year
Hope you had a great holiday season! I’ll keep up the posting going forward, so check back in, and reach out if you have any questions I can help answer.