Data types are one of those core things you need to get right. Whether it’s matching types between join columns, or between parameters and predicates, not doing so can really squash application performance in quite similar ways to writing non-SARGable predicates.
That’s because — wait for it — a lot of the stuff we’ve talked about over the last week that can happen with poorly written predicates can happen with poorly matched data types, too.
The main thing to remember, is that aside from max datatypes, what matters most in a situation with implicit conversion is that it doesn’t take place on a column. If it happens on a parameter or variable it’s far less of an issue, but it can still cause oddities in query plans, and with cardinality estimation.
Speak And Spell
Most of the problems I see with implicit conversion is with other datatypes being compared to nchar/nvarchar types, but not always.
For example, this query results in an index seek despite a variable declared as nvarchar(11).
DECLARE @i nvarchar(11) = N'22656'; SELECT u.* FROM dbo.Users AS u WHERE u.AccountId = @i; GO
Note that the convert_implicit function is applied to the variable, and not the AccountId column.
To show an example of when implicit conversions act the same way was non-SARGable predicates, let’s make a crappy copy of some columns from the Users table.
DROP TABLE IF EXISTS dbo.UsersBad; SELECT u.Id, ISNULL ( CONVERT ( varchar(40), u.DisplayName ), '' ) AS DisplayName, ISNULL ( CONVERT ( nvarchar(11), u.Reputation ), '' ) AS Reputation INTO dbo.UsersBad FROM dbo.Users AS u; ALTER TABLE dbo.UsersBad ADD CONSTRAINT PK_UsersBad_Id PRIMARY KEY CLUSTERED (Id); CREATE INDEX ur ON dbo.UsersBad(Reputation); CREATE INDEX ud ON dbo.UsersBad(DisplayName);
Here, we’re converting DisplayName from nvarchar, and Reputation from an int. We’re also creating some indexes that will be rendered nearly useless by implicit conversions.
CREATE OR ALTER PROCEDURE dbo.BadUsersQuery ( @DisplayName nvarchar(40), @Reputation int ) AS BEGIN SELECT u.DisplayName FROM dbo.UsersBad AS u WHERE u.DisplayName = @DisplayName; SELECT Reputation = MAX(u.Reputation) FROM dbo.UsersBad AS u WHERE u.Reputation = @Reputation; END; GO EXEC dbo.BadUsersQuery @DisplayName = N'Eggs McLaren', @Reputation = 1787;
For both of these, the convert_implicit winds up on the column rather than on the parameter.
And that’s what makes for the type of implicit conversion that causes most performance problems I see.
This is a relatively small table, so the hit isn’t too bad, but comparatively it’s much worse, like in all the other examples of SARGability we’ve seen lately.
I think most people who do performance tuning regularly are quite aware of this problem. There’s even a query plan warning about it, for those who don’t.
It’s definitely a good problem to solve, but it often leads to finding other problems. For example, we get the seek predicate warning regardless of if there’s an index we can seek to. If I drop all the indexes off of the UsersBad table and run a query like this, we still get a warning:
DECLARE @DisplayName nvarchar(40); SELECT u.DisplayName FROM dbo.UsersBad AS u WHERE u.DisplayName = @DisplayName;
It’s also worth noting that getting rid of the implicit conversion — much like fixing other non-SARGable predicates — may reveal missing index requests that weren’t there before.
DECLARE @DisplayName nvarchar(40) = N'Eggs McLaren'; SELECT u.* FROM dbo.UsersBad AS u WHERE u.DisplayName = @DisplayName; GO DECLARE @DisplayName varchar(40) = 'Eggs McLaren'; SELECT u.* FROM dbo.UsersBad AS u WHERE u.DisplayName = @DisplayName; GO
Solving implicit conversion issues is just as important (and often easier) than solving other issues with SARGable predicates, and just as important.
Even though it’s one of the first performance problems people learn about, I still see it out there enough to write about it. I think a lot of the reason that it still crops up is because ORMs leave developers detached from the queries, and they don’t see how parameter types end up getting inferred when not strongly typed.
Anyway, that’s it for this series. Tomorrow’s post will be a wrap up with links.
Thanks for reading!