SQL Code Smells: Implicit Conversions
You run a query against a table. The column is indexed. The statistics are fresh. And yet the query does a full scan, takes ten times longer than it should, and you have no idea why. The code looks correct. The data looks correct. Nothing looks wrong.
The culprit is a single mismatched data type, and SQL Server fixed it for you without saying a word.
What is an implicit conversion?
Every comparison SQL Server makes requires both sides to be the same data type. When they are not, SQL Server silently converts one side to match the other. This is an implicit conversion. It happens automatically, without any error or warning in your query results.
An explicit conversion is one you write yourself using CONVERT() or CAST(). You can see it in the code. An implicit one is invisible. SQL Server inserts it into the execution plan behind the scenes, and you only discover it when you go looking.
The important thing to understand is that even an explicit CONVERT() or CAST() applied to an indexed column in a WHERE clause causes exactly the same problem. Whether SQL Server adds the conversion or you do, wrapping a column in a function prevents the optimizer from using the index efficiently.
Why does it exist in code?
Implicit conversions show up for a handful of very common reasons:
- An ORM or reporting tool sends parameters as the wrong type. For example, passing a date as a string to a column defined as DATE or DATETIME.
- A schema changed over time. A column that used to be VARCHAR became INT, but the application code sending the query was never updated.
- A developer did not know the exact data type of the column they were querying and made a reasonable-sounding guess.
- A query was written to work, and it does work, just not efficiently. The conversion happens silently, the results are correct, and nobody looks further.
The insidious part is that none of these cause query failures. The query runs, returns correct data, and the conversion goes unnoticed until the table grows large enough that performance becomes a problem.
Why is it a problem?
Implicit conversions cause two distinct performance problems, and either one can be enough to turn a fast query into a slow one.
Index seeks become index scans
When SQL Server needs to convert a column’s data type to make a comparison work, it cannot use the index directly. Instead of seeking to the right place in the index and reading a small number of rows, it has to scan every row in the table, apply the conversion, and then evaluate the comparison. On a small table this is fast enough to ignore. On a table with millions of rows it can be the difference between milliseconds and minutes.
Cardinality estimates go wrong
The query optimizer needs to estimate how many rows a condition will return in order to build a good execution plan. When a conversion is involved, those estimates become unreliable. The optimizer might plan for 10 rows when the actual result is 50,000. That bad estimate then cascades into poor join strategies, wrong memory grants, and parallel execution decisions that make things worse instead of better.
How to find it with Database Health Monitor
The challenge with implicit conversions is that they are invisible in the query text itself. You have to look at the execution plan, and specifically at plan warnings, to find them. Database Health Monitor surfaces this without requiring you to manually pull plans for every slow query.
Start with Long Running History
Open the Long Running History or Long Running by Hour instance report. These reports track your slowest queries over time and include a Warnings column that flags when a stored execution plan contains conversion warnings. Any row with a warning indicator is worth investigating.
Double-click to open the Query Advisor
Double-clicking any row in the Long Running History report opens the Query Advisor. This is where the detail lives. The Query Advisor shows the query text, its execution context, and when a plan is available, a parsed summary of any plan warnings found.
For implicit conversions, you will see entries in the warnings section that look like this:
<PlanAffectingConvert ConvertIssue="Cardinality Estimate" Expression="CONVERT(varchar(10),[lh].[loginDate],112)"/><PlanAffectingConvert ConvertIssue="Seek Plan" Expression="CONVERT_IMPLICIT(nvarchar(250),[ul].[user_email],0)=[lh].[user_email]"/>
Two warning types appear here, and they tell you different things:
- ConvertIssue="Cardinality Estimate" means the conversion is poisoning the optimizer’s row count estimates. The plan may be structurally valid but built on bad assumptions.
- ConvertIssue="Seek Plan" means the conversion is preventing an index seek entirely. This is typically the more damaging of the two, as the query is doing far more I/O than it needs to.
The Expression attribute tells you exactly which column and conversion is responsible, making it straightforward to track down in the code.
Tip: The Warnings column is also visible directly in the Blocking Over Time report. If a blocked query has a conversion warning, that warning may itself be contributing to the blocking by causing the query to hold locks longer than necessary.
How to fix it
Once you have identified the conversion, the fix usually falls into one of three categories.
Match the parameter type to the column type
This is the most common fix and the cleanest. If the column is INT, pass an INT parameter. If it is DATE, pass a DATE. If the query is generated by application code or an ORM, trace back to where the parameter type is set and correct it there.
-- Smell: parameter sent as string, column is INTWHERE CustomerID = '10045'-- Fix: match the typeWHERE CustomerID = 10045
Do not wrap indexed columns in functions
If you need to transform a value for comparison, transform the parameter or the literal, not the column. The column must appear bare on one side of the operator for the index to be usable.
-- Smell: function applied to indexed column prevents seekWHERE CONVERT(varchar(10), OrderDate, 112) = '20240101'-- Fix: express the condition in the column's native typeWHERE OrderDate >= '2024-01-01' AND OrderDate < '2024-01-02'
Fix the schema if the mismatch lives in the table
Sometimes the column itself is the wrong type, such as a date stored as VARCHAR or an ID stored as FLOAT. These are worth fixing at the schema level, though they require careful planning. Changing a column type in a production table requires testing application code, verifying dependent objects, and doing the migration in a controlled way.
In the meantime, you can often mitigate the impact by adding a computed column with the correct type and indexing that instead. It is not ideal, but it is a lower-risk bridge while a proper fix is planned.
Find these in your environment today
Implicit conversions are one of those performance problems that hide in plain sight. The query works, the results are correct, and nothing raises an alarm until the table is large enough that the scan cost becomes painful.
Database Health Monitor’s Long Running History report and Query Advisor give you a direct path to surfacing conversion warnings across your instance without having to manually capture and inspect execution plans one query at a time. If you have not explored the Warnings column in your long running query reports yet, it is worth a look.
