<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://blogs.msdn.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Craig Freedman's SQL Server Blog : Conversions</title><link>http://blogs.msdn.com/craigfr/archive/tags/Conversions/default.aspx</link><description>Tags: Conversions</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP1 (Build: 61025.2)</generator><item><title>Implicit Conversions</title><link>http://blogs.msdn.com/craigfr/archive/2008/06/05/implicit-conversions.aspx</link><pubDate>Thu, 05 Jun 2008 20:15:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8575794</guid><dc:creator>craigfr</dc:creator><slash:comments>1</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8575794.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8575794</wfw:commentRss><description>&lt;P&gt;In my last couple of posts, I wrote about how explicit conversions can lead to errors.&amp;nbsp; In this post, I'm going to take a look at some issues involving implicit conversions.&amp;nbsp; SQL Server adds implicit conversions whenever you mix columns, variables, and/or parameters with different (but compatible) data types in a single expression.&amp;nbsp; For example, if you try to compare INT and FLOAT columns, the INT must be converted to a FLOAT.&amp;nbsp; If you write "C_INT = C_FLOAT", SQL Server rewrites this expressions as "CONVERT(FLOAT,C_INT) = C_FLOAT".&lt;/P&gt;
&lt;P&gt;When performing implicit conversions, SQL Server will try to choose the conversion that is least likely either to fail due to an overflow or to lose precision.&amp;nbsp; For example, a SMALLINT will be converted to an INT since all SMALLINTs can be converted to INTs without any data loss.&amp;nbsp; On the other hand, an INT will be converted to a REAL since all INTs can be converted to REALs but not vice-versa.&amp;nbsp; However, the conversion is potentially lossy since some INTs contain more digits than can be represented by a REAL.&lt;/P&gt;
&lt;P&gt;Note that some data types can only be combined in a single expression by using an explicit conversion and that some data types are not compatible at all with or without an explicit conversion.&amp;nbsp; This &lt;A title="CAST and CONVERT (Transact-SQL)" href="http://msdn.microsoft.com/en-us/library/ms187928.aspx" mce_href="http://msdn.microsoft.com/en-us/library/ms187928.aspx"&gt;Books Online page&lt;/A&gt; includes a compatibility matrix showing all of the possible data type combinations.&amp;nbsp; For the remainder of this post, I'm going to focus only on implicit conversions.&lt;/P&gt;
&lt;P&gt;To get started, I'd like to show an example that did not work so well on SQL Server 2000:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T (C INT)&lt;BR&gt;CREATE UNIQUE CLUSTERED INDEX TC ON T(C)&lt;/P&gt;
&lt;P mce_keep="true"&gt;INSERT T VALUES (1000000000)&lt;BR&gt;INSERT T VALUES (1000000001)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;First, let's consider a query that uses a clustered index scan:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @V REAL&lt;BR&gt;SET @V = 1E9&lt;BR&gt;SELECT * FROM T WITH (INDEX(0)) WHERE C = @V&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;PRE&gt;C           
----------- 
1000000000
1000000001&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;At first glance, this may seem like an incorrect result.&amp;nbsp; The second row does not appear to match the predicate.&amp;nbsp; However, if we check the plan, we see that SQL added an implicit convert to the predicate:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T].[TC]), WHERE:(&lt;B&gt;Convert&lt;/B&gt;([T].[C])=[@V]))&lt;/P&gt;
&lt;P mce_keep="true"&gt;Moreover, if we check the results of the conversion, we can see that it is lossy.&amp;nbsp; Both INTs convert to the same REAL value.&amp;nbsp; The above results now make more sense:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT *, CONVERT(REAL,C) REAL_C FROM T&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;PRE&gt;C           REAL_C                   
----------- ------------------------ 
1000000000  1.0E+9
1000000001  1.0E+9&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;Now, let's consider the same query with a clustered index seek:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @V REAL&lt;BR&gt;SET @V = 1E9&lt;BR&gt;SELECT * FROM T WITH (INDEX(1)) WHERE C = @V&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;PRE&gt;C           
----------- 
1000000000&lt;/PRE&gt;
&lt;P mce_keep="true"&gt;Hold it!&amp;nbsp; What happened to the second row?&amp;nbsp; Let's check the plan:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1002], [Expr1003], [Expr1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1002]=Convert([@V])-1, [Expr1003]=Convert([@V])+1, [Expr1004]=If (Convert([@V])-1=NULL) then 0 else 6|If (Convert([@V])+1=NULL) then 0 else 10))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T].[TC]), SEEK:([T].[C] &amp;gt; [Expr1002] AND [T].[C] &amp;lt; [Expr1003]),&amp;nbsp; WHERE:(Convert([T].[C])=[@V]) ORDERED FORWARD)&lt;/P&gt;
&lt;P mce_keep="true"&gt;This plan looks rather complicated, but aside from unfortunately getting a wrong answer (at least according to the conversion semantics we just discussed), it is really quite simple.&amp;nbsp; To perform an index seek, SQL Server must have key values that match the data type stored in the index.&amp;nbsp; SQL Server cannot perform an index seek on an INT index using a REAL key.&amp;nbsp; So, SQL Server must convert the REAL variable to an INT.&amp;nbsp; Because, as we've already seen, conversions can be lossy, SQL Server also expands the range of values returned by the index seek by subtracting and adding one to the result of the conversion.&amp;nbsp; Basically, after substituting the value of the parameter, this plan is the same as running the following query:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT * FROM T WHERE (C &amp;gt; 999999999 AND C &amp;lt; 1000000001) AND CONVERT(REAL,C) = 1E9&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;Notice that the original predicate with the convert is retained in case the index seek returns too many rows and some must be filtered out.&amp;nbsp; In most cases, this algorithm works just fine; I've deliberately selected a scenario that does not work so well.&amp;nbsp; In addition to potentially omitting results, this algorithm may also harm performance.&amp;nbsp; For instance, consider this example:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T (C NUMERIC(12,4))&lt;BR&gt;CREATE UNIQUE CLUSTERED INDEX TC ON T(C)&lt;/P&gt;
&lt;P mce_keep="true"&gt;INSERT T VALUES (0.0001)&lt;BR&gt;INSERT T VALUES (0.0002)&lt;BR&gt;INSERT T VALUES (0.0003)&lt;BR&gt;...&lt;/P&gt;
&lt;P mce_keep="true"&gt;DECLARE @P REAL&lt;BR&gt;SET @P = 1.0000&lt;BR&gt;SELECT * FROM T WITH (INDEX(1)) WHERE C = @P&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Suppose that table T includes many values in the range 0.0000 to 2.0000.&amp;nbsp; In this example, the conversion is lossless.&amp;nbsp; Only rows with the value 1.0000 should be returned.&amp;nbsp; Unfortunately, the index seek still returns all of the rows in the range and the filter with the convert discards all of them except for those with the value 1.0000.&lt;/P&gt;
&lt;P&gt;Now let's see how SQL Server 2005 (and 2008) handle these same queries.&amp;nbsp; Let's start with the clustered index scan:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @P REAL&lt;BR&gt;SET @P = 1E9&lt;BR&gt;SELECT * FROM T WITH (INDEX(0)) WHERE C = @P&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T].[TC]), WHERE:(&lt;B&gt;CONVERT_IMPLICIT&lt;/B&gt;(real(24),[T].[C],0)=[@P]))&lt;/P&gt;
&lt;P&gt;This is the same plan that we got from SQL Server 2000 and we get the same result (both rows).&amp;nbsp; The only difference is that SQL Server 2005 indicates that the convert is implicit and provides the type of the conversion.&amp;nbsp; Next let's try the clustered index seek:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;DECLARE @P REAL&lt;BR&gt;SET @P = 1E9&lt;BR&gt;SELECT * FROM T WITH (INDEX(1)) WHERE C = @P&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1005], [Expr1006], [Expr1004]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:(([Expr1005],[Expr1006],[Expr1004])=&lt;B&gt;GetRangeThroughConvert&lt;/B&gt;([@P],[@P],(62))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T].[TC]), SEEK:([T].[C] &amp;gt; [Expr1005] AND [T].[C] &amp;lt; [Expr1006]),&amp;nbsp; WHERE:(CONVERT_IMPLICIT(real(24),[T].[C],0)=[@P]) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;This plan is similar to the SQL Server 2000 plan with one key difference.&amp;nbsp; The conversion of the seek key from REAL to INT is performed by an internal function GetRangeThroughConvert which correctly determines the exact range of values to seek.&amp;nbsp; A little experimentation will show that, in this example, the range works out to all values between 999,999,968 and 1,000,000,479.&amp;nbsp; On the other hand, for the example with the numeric column, the "range" works out to exactly 1.0000.&amp;nbsp; This solution is both more accurate and more efficient than the SQL Server 2000 algorithm.&lt;/P&gt;
&lt;P&gt;Finally, let's take a look at the performance implications of the new plan:&amp;nbsp; &lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T1 (C_INT INT, C_REAL REAL)&lt;BR&gt;CREATE CLUSTERED INDEX T1_C_REAL ON T1(C_REAL)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;CREATE TABLE T2 (C_INT INT, C_REAL REAL)&lt;BR&gt;CREATE CLUSTERED INDEX T2_C_INT ON T2(C_INT)&lt;/P&gt;
&lt;P mce_keep="true"&gt;SET NOCOUNT ON&lt;BR&gt;DECLARE @I INT&lt;BR&gt;SET @I = 0&lt;BR&gt;WHILE @I &amp;lt; 100000&lt;BR&gt;&amp;nbsp; BEGIN&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; INSERT T1 VALUES (@I, @I)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; INSERT T2 VALUES (@I, @I)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SET @I = @I + 1&lt;BR&gt;&amp;nbsp; END &lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Here are three join plans.&amp;nbsp; I'm using a join hint to ensure that I do not get a hash join which uses a completely different algorithm.&amp;nbsp; I'm using COUNT(*) to eliminate the overhead of transferring output rows to the client and an OPTION hint to avoid parallelism.&amp;nbsp; The first plan joins the two INT columns.&amp;nbsp; The second plan joins the two REAL columns.&amp;nbsp; The third plan joins the INT and REAL columns and uses the same algorithm with the GetRangeThroughConvert function as the simpler query above.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT COUNT(*)&lt;BR&gt;FROM T1 INNER LOOP JOIN T2 ON T1.C_INT = T2.C_INT&lt;BR&gt;OPTION(MAXDOP 1)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Stream Aggregate(DEFINE:([Expr1012]=Count(*)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T1].[C_INT], [Expr1011]) WITH UNORDERED PREFETCH)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T1].[T1_C_REAL]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T2].[T2_C_INT]), SEEK:([T2].[C_INT]=[T1].[C_INT]) ORDERED FORWARD)&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT COUNT(*)&lt;BR&gt;FROM T2 INNER LOOP JOIN T1 ON T2.C_REAL = T1.C_REAL&lt;BR&gt;OPTION(MAXDOP 1)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Stream Aggregate(DEFINE:([Expr1012]=Count(*)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T2].[C_REAL], [Expr1011]) WITH UNORDERED PREFETCH)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Scan(OBJECT:([T2].[T2_C_INT]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T1].[T1_C_REAL]), SEEK:([T1].[C_REAL]=[T2].[C_REAL]) ORDERED FORWARD)&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT COUNT(*)&lt;BR&gt;FROM T1 INNER LOOP JOIN T2 ON T1.C_REAL = T2.C_INT&lt;BR&gt;OPTION(MAXDOP 1)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1014],0)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Stream Aggregate(DEFINE:([Expr1014]=Count(*)))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([T1].[C_REAL]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;|--Clustered Index Scan(OBJECT:([T1].[T1_C_REAL]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1012], [Expr1013], [Expr1011]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:(([Expr1012],[Expr1013],[Expr1011])=GetRangeThroughConvert([T1].[C_REAL],[T1].[C_REAL],(62))))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Constant Scan&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Clustered Index Seek(OBJECT:([T2].[T2_C_INT]), SEEK:([T2].[C_INT] &amp;gt; [Expr1012] AND [T2].[C_INT] &amp;lt; [Expr1013]),&amp;nbsp; WHERE:([T1].[C_REAL]=CONVERT_IMPLICIT(real(24),[T2].[C_INT],0)) ORDERED FORWARD)&lt;/P&gt;
&lt;P&gt;I measured the times using SET STATISTICS TIME ON.&amp;nbsp; Here are the results I got on my system:&lt;/P&gt;
&lt;P&gt;SQL Server Execution Times:&amp;nbsp;&amp;nbsp; CPU time = 750 ms,&amp;nbsp; elapsed time = 741 ms.&lt;/P&gt;
&lt;P mce_keep="true"&gt;SQL Server Execution Times:&amp;nbsp;&amp;nbsp; CPU time = 735 ms,&amp;nbsp; elapsed time = 742 ms.&lt;/P&gt;
&lt;P mce_keep="true"&gt;SQL Server Execution Times:&amp;nbsp;&amp;nbsp; CPU time = 1609 ms,&amp;nbsp; elapsed time = 1605 ms.&lt;/P&gt;
&lt;P mce_keep="true"&gt;Notice that each of first two queries with the joins on the matching types complete in 740ms while the third query with the join on the mismatched types takes more than twice as long at 1600ms!&amp;nbsp; The moral of this story?&amp;nbsp; Try to stick to matching types whenever possible.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8575794" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Conversions/default.aspx">Conversions</category></item><item><title>Conversion and Arithmetic Errors: Change between SQL Server 2000 and 2005</title><link>http://blogs.msdn.com/craigfr/archive/2008/05/06/conversion-and-arithmetic-errors-change-between-sql-server-2000-and-2005.aspx</link><pubDate>Tue, 06 May 2008 21:54:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8463682</guid><dc:creator>craigfr</dc:creator><slash:comments>0</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8463682.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8463682</wfw:commentRss><description>&lt;P&gt;In &lt;A title="Conversion and Arithmetic Errors" href="http://blogs.msdn.com/craigfr/archive/2008/04/28/conversion-and-arithmetic-errors.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2008/04/28/conversion-and-arithmetic-errors.aspx"&gt;this post&lt;/A&gt; from last week, I gave an example of a query with a conversion where the optimizer pushes the conversion below a join.&amp;nbsp; The result is that the conversion may be run on rows that do not join which could lead to avoidable failures.&amp;nbsp; I ran this query on SQL Server 2005.&amp;nbsp; After I published that post, a reader pointed out to me that my example query generates a different plan on SQL Server 2000:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T1 (A INT, B CHAR(8))&lt;BR&gt;INSERT T1 VALUES (0, '0')&lt;BR&gt;INSERT T1 VALUES (1, '1')&lt;BR&gt;INSERT T1 VALUES (99, 'Error')&lt;/P&gt;
&lt;P mce_keep="true"&gt;CREATE TABLE T2 (X INT)&lt;BR&gt;INSERT T2 VALUES (1)&lt;BR&gt;INSERT T2 VALUES (2)&lt;BR&gt;INSERT T2 VALUES (3)&lt;BR&gt;INSERT T2 VALUES (4)&lt;BR&gt;INSERT T2 VALUES (5)&lt;/P&gt;
&lt;P mce_keep="true"&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;Here is the SQL Server 2000 plan:&lt;/P&gt;
&lt;P&gt;&lt;B&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1004]=Convert([T1].[B])))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T1].[A]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P mce_keep="true"&gt;And here is the SQL Server 2005 plan:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T1].[A]))&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;The result is that the SQL Server 2000 plan runs without any errors - the "bad" row is filtered out by the join - while the SQL Server 2005 plan fails when it tries to convert the "bad" row.&amp;nbsp; It turns out that this change in behavior is documented in &lt;A title="Behavior Changes to Database Engine Features in SQL Server 2005" href="http://msdn.microsoft.com/en-us/library/ms143359.aspx" mce_href="http://msdn.microsoft.com/en-us/library/ms143359.aspx"&gt;this Books Online entry&lt;/A&gt; under the Transact-SQL section and "Expressions in queries" feature.&amp;nbsp; This entry states that queries that worked on SQL Server 2000 could fail on SQL Server 2005.&amp;nbsp; It also states that the change enables two benefits: "the ability to match indexes on computed columns" and "the prevention of redundant computation of expression results."&amp;nbsp; Let's investigate these two benefits. &lt;/P&gt;
&lt;P&gt;To understand the first benefit ("the ability to match indexes on computed columns"), suppose we create a new table with a computed column that matches the conversion in our query and then create an index on that computed column:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T3 (A INT, B CHAR(8), C AS CONVERT(INT, B)&lt;BR&gt;CREATE INDEX T3C ON T3 (C, A)&lt;/P&gt;
&lt;P mce_keep="true"&gt;INSERT T3 VALUES (0, '0')&lt;BR&gt;INSERT T3 VALUES (1, '1')&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P mce_keep="true"&gt;This computed column is sometimes referred to as a "persisted" computed column because the index stores the actual values of the computed column.&amp;nbsp; For computed columns on complex or expensive expressions, using such an index could be faster than evaluating the expression anew.&amp;nbsp; Note that, because the computed column must be evaluated to maintain the index, we cannot insert a "bad" row into T3.&amp;nbsp; Any attempt to insert a "bad" row immediately fails with a conversion error.&lt;/P&gt;
&lt;P&gt;I've deliberately included column A as part of the index so that the index covers all of the columns in the query.&amp;nbsp; If we omit column A from the index, SQL Server cannot use the index without performing a &lt;A title="Bookmark Lookup" href="http://blogs.msdn.com/craigfr/archive/2006/06/30/652639.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/06/30/652639.aspx"&gt;bookmark lookup&lt;/A&gt;.&amp;nbsp; In this particular example, that would prevent SQL Server from choosing the plan that I'm trying to demonstrate.&amp;nbsp; Also, note that, on SQL Server 2005 we could use the INCLUDE keyword instead of making column A an index key, but I wanted this example to work on both SQL Server 2000 and SQL Server 2005.&lt;/P&gt;
&lt;P&gt;Now let's compare the plans using the new table.&amp;nbsp; First, the SQL Server 2000 plan:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1004]=Convert([T3].[B])))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T3].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T3].[A]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T3]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P mce_keep="true"&gt;Compare that to the SQL Server 2005 plan:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T3].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T3].[A]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([T3].[C]=[T3].[C]))&lt;BR&gt;&lt;STRONG&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Index Scan(OBJECT:([T3].[T3C]))&lt;BR&gt;&lt;/STRONG&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;Notice that SQL Server 2000 uses the same plan as we saw above while SQL Server 2005 uses the new index and eliminates the conversion.&amp;nbsp; This type of index matching is not limited to conversions and works for many other expressions and computed columns.&lt;/P&gt;
&lt;P&gt;I researched the second benefit ("the prevention of redundant computation of expression results") but I'm unable to come up with a compelling explanation for it.&amp;nbsp; In some cases, it is certainly true that pushing the conversion below a join reduces the number of times that the conversion is evaluated.&amp;nbsp; For instance, returning to the original example above, suppose we add many duplicate values to T2 such that each T1 row joins multiple times.&amp;nbsp; In the SQL Server 2005 plan, the conversion is evaluated before the join and so it is evaluated exactly once for each T1 row regardless of how many times each row joins.&amp;nbsp; In the SQL Server 2000 plan, the conversion is evaluated after the join and so it is evaluated multiple times for each T1 row - once for each time that the row joins.&amp;nbsp; Of course, it is also possible that none of the rows from T1 join.&amp;nbsp; In this case, the SQL Server 2005 plan unnecessarily evaluates the conversion on every T1 row while the SQL Server 2000 plan does not evaluate the conversion at all.&lt;/P&gt;
&lt;P&gt;Moreover, you may recall from my last post, that SQL Server defers the execution of most scalar expressions until they are actually evaluated.&amp;nbsp; I used the following nested loops join example to illustrate this point:&lt;/P&gt;
&lt;P&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;BR&gt;OPTION (LOOP JOIN)&lt;/P&gt;
&lt;P mce_keep="true"&gt;&amp;nbsp; |--Nested Loops(Inner Join, WHERE:([T2].[X]=[T1].[A]))&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;In this example, the compute scalar is below the join in the plan but the conversion is still executed after the join.&amp;nbsp; Thus, pushing the conversion below the join does not change the number of times that it is executed.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8463682" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Joins/default.aspx">Joins</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Conversions/default.aspx">Conversions</category></item><item><title>Conversion and Arithmetic Errors</title><link>http://blogs.msdn.com/craigfr/archive/2008/04/28/conversion-and-arithmetic-errors.aspx</link><pubDate>Tue, 29 Apr 2008 02:23:00 GMT</pubDate><guid isPermaLink="false">91d46819-8472-40ad-a661-2c78acb4018c:8436318</guid><dc:creator>craigfr</dc:creator><slash:comments>4</slash:comments><comments>http://blogs.msdn.com/craigfr/comments/8436318.aspx</comments><wfw:commentRss>http://blogs.msdn.com/craigfr/commentrss.aspx?PostID=8436318</wfw:commentRss><description>&lt;P mce_keep="true"&gt;Let's take a look at a simple query:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T1 (A INT, B CHAR(8))&lt;BR&gt;INSERT T1 VALUES (0, '0')&lt;BR&gt;INSERT T1 VALUES (1, '1')&lt;BR&gt;INSERT T1 VALUES (99, 'Error')&lt;/P&gt;
&lt;P&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT FROM T1&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;There is no way to convert the string "Error" into an integer, so it should come as no surprise that this query fails with a conversion error:&lt;/P&gt;&lt;PRE&gt;A           B_INT
----------- -----------
1           1
Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error   ' to data type int.&lt;/PRE&gt;
&lt;P&gt;Now, let's create a second table and try a join:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE TABLE T2 (X INT)&lt;BR&gt;INSERT T2 VALUES (1)&lt;BR&gt;INSERT T2 VALUES (2)&lt;BR&gt;INSERT T2 VALUES (3)&lt;BR&gt;INSERT T2 VALUES (4)&lt;BR&gt;INSERT T2 VALUES (5)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Although this join query includes the same conversion as the first query, the row with T1.A = 99 does not join with any rows in T2, so at first glance, we might conclude that the conversion should never be executed.&amp;nbsp; However, if we execute the query, we still get the error:&lt;/P&gt;&lt;PRE&gt;A           B_INT
----------- -----------
Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the varchar value 'Error   ' to data type int.&lt;/PRE&gt;
&lt;P&gt;What happened?&amp;nbsp; The SQL Server query optimizer is free to move expressions up and down within a query plan.&amp;nbsp; Since the expression with the conversion depends only on T1, the optimizer can (and does) push the conversion below the join and close to the scan of T1:&lt;/P&gt;
&lt;P&gt;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T1].[A]))&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;Now, let's see what happens if we force a nested loops join:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;BR&gt;OPTION (LOOP JOIN)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp; |--Nested Loops(Inner Join, WHERE:([T2].[X]=[T1].[A]))&lt;BR&gt;&lt;B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))&lt;BR&gt;&lt;/B&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;Once again, the optimizer pushes the conversion below the join, so we might conclude that this query will also fail.&amp;nbsp; Surprisingly, it does not:&lt;/P&gt;&lt;PRE&gt;A           B_INT
----------- -----------
1           1&lt;/PRE&gt;
&lt;P&gt;What's going on here?&amp;nbsp; SQL Server includes a runtime optimization that defers the evaluation of most scalar expressions until they are actually referenced.&amp;nbsp; We can see this optimization in action if we check the output of SET STATISTICS PROFILE ON:&lt;/P&gt;&lt;PRE&gt;1      1        |--Nested Loops(Inner Join, WHERE:([T2].[X]=[T1].[A]))
0      0             |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))
3      1             |    |--Table Scan(OBJECT:([T1]))
15     3             |--Table Scan(OBJECT:([T2])) &lt;/PRE&gt;
&lt;P&gt;Notice that the compute scalar is executed zero times.&amp;nbsp; The result of the conversion is not needed by the join and is computed immediately prior to returning the query result to the client.&amp;nbsp; By this time, the bad row has already been filtered by the join.&lt;/P&gt;
&lt;P&gt;SQL Server actually uses the same optimization both for &lt;A title="Nested Loops Join" href="http://blogs.msdn.com/craigfr/archive/2006/07/26/679319.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/07/26/679319.aspx"&gt;the nested loops join&lt;/A&gt; and for &lt;A title="Hash Join" href="http://blogs.msdn.com/craigfr/archive/2006/08/10/687630.aspx" mce_href="http://blogs.msdn.com/craigfr/archive/2006/08/10/687630.aspx"&gt;the hash join&lt;/A&gt;.&amp;nbsp; However, recall that a hash join builds a hash table on its left input.&amp;nbsp; This hash table includes values for all columns that are needed to evaluate the join and values for all columns that are output by the join.&amp;nbsp; Thus, the hash join must evaluate and store the result of the conversion in the hash table.&amp;nbsp; Thus, the conversion is evaluated and fails before the hash join can determine whether the bad row actually joins.&lt;/P&gt;
&lt;P&gt;As you can see, whether or not a conversion causes a failure depends on a variety of factors including where in the plan the optimizer chooses to place the conversion and when during execution the conversion is actually evaluated.&amp;nbsp; Moreover, when the conversion is actually evaluated depends on the precise operators in the plan and the behavior of those operators.&lt;/P&gt;
&lt;P&gt;We can see similar problems with arithmetic errors such as overflows or divide by zero:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;INSERT T1 VALUES (0, '0')&lt;/P&gt;
&lt;P mce_keep="true"&gt;SELECT T1.A, 1 / T1.A AS A_Reciprocal&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;/P&gt;&lt;/BLOCKQUOTE&gt;&lt;PRE&gt;A           A_Reciprocal
----------- ------------
Msg 8134, Level 16, State 1, Line 1
Divide by zero error encountered.&lt;/PRE&gt;
&lt;P&gt;So, what can you do to work around this problem?&amp;nbsp; The best solution from a performance perspective is to avoid using the conversion at all or to ensure that the data in the column to be converted is valid (or to avoid using an expression that could fail due to an arithmetic error). &amp;nbsp;If you must use a conversion (or any other expression that could fail), you can use a CASE statement to validate the input.&amp;nbsp; For example, the following query trims away leading and trailing spaces from the CHAR column and then uses the PATINDEX function to validate that the column does not contain any non-numeric characters:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT T1.A,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CASE WHEN PATINDEX('%[^0-9]%', RTRIM(LTRIM(T1.B))) = 0&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; THEN CONVERT(INT, T1.B)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ELSE NULL&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; END AS B_INT&lt;BR&gt;FROM T1&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;This example shows how we can avoid the divide by zero error:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT T1.A,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CASE WHEN T1.A &amp;lt;&amp;gt; 0&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; THEN 1 / T1.A&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ELSE NULL&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; END AS A_Reciprocal&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Although I would not recommend it, for arithmetic errors, you can use the SET ANSI_WARNINGS, SET ARITHABORT, and SET ARITHIGNORE options to disable the error.&amp;nbsp; These settings do nothing for conversion errors.&lt;/P&gt;
&lt;P&gt;Finally, it is worth pointing out that some hints can affect the placement of the conversion:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT T1.A, CONVERT(INT, T1.B) AS B_INT&lt;BR&gt;FROM T1 JOIN T2 ON T1.A = T2.X&lt;BR&gt;OPTION (FORCE ORDER)&lt;/P&gt;&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;&amp;nbsp; |--Compute Scalar(DEFINE:([Expr1008]=CONVERT(int,[T1].[B],0)))&lt;BR&gt;&lt;/STRONG&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Hash Match(Inner Join, HASH:([T1].[A])=([T2].[X]), RESIDUAL:([T2].[X]=[T1].[A]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T1]))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; |--Table Scan(OBJECT:([T2]))&lt;/P&gt;
&lt;P&gt;Notice that the conversion is now computed after the join.&amp;nbsp; I would strongly discourage the use of this hint as a workaround except perhaps in the simplest of queries given the number of side effects it has.&amp;nbsp; Mostly I point out this particular result in case any readers are tempted to use this or other hints to explore how other join types and join orders might affect the behavior of this query.&lt;/P&gt;&lt;img src="http://blogs.msdn.com/aggbug.aspx?PostID=8436318" width="1" height="1"&gt;</description><category domain="http://blogs.msdn.com/craigfr/archive/tags/Joins/default.aspx">Joins</category><category domain="http://blogs.msdn.com/craigfr/archive/tags/Conversions/default.aspx">Conversions</category></item></channel></rss>