
Автор: Craig Freedman Implicit Conversions
В нескольких статьях я уже показывал, как явные преобразования могут приводить к ошибкам. В этой статье рассмотрим некоторые проблемы, связанные с неявными преобразованиями. SQL Server добавляет неявные преобразования, когда в одном выражении запроса используются колонки, переменные и/или параметры с разными (но совместимыми) типами данных. Например, если нужно сравнить колонки с типами INT и FLOAT, INT необходимо преобразовать в FLOAT. Если вы напишете "C_INT = C_FLOAT", SQL Server перепишет это выражение как "CONVERT (FLOAT, C_INT) = C_FLOAT".
При выполнении неявных преобразований SQL Server попытается выбрать преобразование, которое с наименьшей вероятностью
приведет к сбою из-за переполнения или потере точности. Например, SMALLINT будет преобразовано в INT, поскольку все SMALLINT могут быть преобразованы в INT без потери данных. С
другой стороны, INT будет преобразовано в
REAL, поскольку все INT могут быть преобразованы в REAL, но не наоборот.
Однако преобразование потенциально может привести к потерям, поскольку
некоторые INT содержат больше цифр, чем может быть представлено REAL.
Обратите внимание, что некоторые типы данных можно
объединить только в одном выражении с помощью явного преобразования, а
некоторые типы данных вообще несовместимы с явным преобразованием или без него.
На этой странице Books Online показана матрица совместимости, показывающая все возможные комбинации преобразования
типов данных. В этой статье мы сосредоточимся только на неявных
преобразованиях.
Для начала посмотрим пример, который не очень хорошо
работал на SQL Server 2000 и тем удобен для демонстрации:
CREATE TABLE T (C INT)
CREATE UNIQUE CLUSTERED INDEX TC ON T(C)
INSERT T VALUES (1000000000)
INSERT T VALUES (1000000001)
Сначала рассмотрим запрос, использующий просмотр
кластерного индекса:
DECLARE @V REAL
SET @V = 1E9
SELECT * FROM T WITH (INDEX(0)) WHERE C = @V
C
-----------10000000001000000001
На первый взгляд может показаться что результат
неверный. Вторая строка, похоже, не соответствует предикату. Однако, если мы
проверим план, то увидим, что к предикату было добавлено неявное
преобразование:
|--Clustered Index Scan(OBJECT:([T].[TC]), WHERE:(Convert([T].[C])=[@V]))
Более того, если мы проверим результаты
преобразования, то увидим, что оно выполнилось с потерями. Оба INT преобразуются в одно и то же значение REAL. Приведенные выше результаты теперь имеют больше
смысла:
SELECT *, CONVERT(REAL,C) REAL_C FROM T
C REAL_C
----------- ------------------------1000000000 1.0E+91000000001 1.0E+9
Теперь рассмотрим тот же запрос с
поиском по кластерному индексу:
DECLARE @V REAL
SET @V = 1E9
SELECT * FROM T WITH (INDEX(1)) WHERE C = @V
C
-----------1000000000
Подождите, куда пропала вторая строка? Давайте заглянем в план:
|--Nested Loops(Inner Join, OUTER
REFERENCES:([Expr1002], [Expr1003], [Expr1004]))
|--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))
| |--Constant Scan
|--Clustered Index Seek(OBJECT:([T].[TC]),
SEEK:([T].[C] > [Expr1002] AND [T].[C] < [Expr1003]), WHERE:(Convert([T].[C])=[@V])
ORDERED FORWARD)
Этот план выглядит сложнее, но, помимо демонстрации, к
сожалению, неправильного результата (согласно семантике преобразования, которую
мы обсудили выше), на самом деле он довольно прост. Для выполнения поиска по
индексу SQL Server должен оперировать значениями ключа, которые соответствуют типу данных в
индексе. SQL Server не может выполнять поиск по индексу по колонке INT с использованием ключа с типом REAL. Поэтому SQL Server должен преобразовать переменную REAL в INT. Поскольку преобразования могут быть с потерями, SQL Server ещё и расширил диапазон значений, возвращаемых поиском по индексу, вычитая
и прибавляя единицу к результату преобразования. По сути, после подстановки
значения параметра этот план будет такой, как и если бы выполнялся следующего
запроса:
SELECT * FROM T WHERE (C > 999999999 AND C < 1000000001) AND CONVERT(REAL,C) = 1E9
Обратите внимание, что исходный предикат конвертирован
и добавлен в конце на случай, если поиск по индексу вернет слишком много строк
и какие-то из них придется отфильтровать. В обычной практике всё это работает
хорошо; тут намеренно выбран сценарий, работа которого выбивается из нормы.
Помимо потенциальной потери части результатов, этот алгоритм также может снизить
производительность. Например, рассмотрим следующий пример:
CREATE TABLE T (C NUMERIC(12,4))
CREATE UNIQUE CLUSTERED INDEX TC ON T(C)
INSERT T VALUES (0.0001)
INSERT T VALUES (0.0002)
INSERT T VALUES (0.0003)
...
DECLARE @P REAL
SET @P = 1.0000
SELECT * FROM T WITH (INDEX(1)) WHERE C = @P
Предположим, что таблица T содержит много записей со значениями в диапазоне от 0,0000 до 2,0000. В
этом примере преобразование выполняется без потерь, запросом возвращаются
только строки со значением 1,0000. К сожалению, поиск по индексу тут будет выбирать
все записи в диапазоне, а потом фильтр с преобразованием оставит строки со значением
1,0000.
Теперь давайте посмотрим, как в SQL Server с версии 2005 обрабатываются эти же запросы. Начнем со просмотра
кластерного индекса:
DECLARE @P REAL
SET @P = 1E9
SELECT * FROM T WITH (INDEX(0)) WHERE C = @P
|--Clustered Index Scan(OBJECT:([T].[TC]), WHERE:(CONVERT_IMPLICIT(real(24),[T].[C],0)=[@P]))
Это тот же план, как в SQL Server 2000, и результат тот же (обе строки). Единственное
отличие в том, что SQL Server теперь указывает, что преобразование было
неявное, и показывает тип этого преобразования. Теперь давайте попробуем
выполнить поиск по кластерному индексу:
DECLARE @P REAL
SET @P = 1E9
SELECT * FROM T WITH (INDEX(1)) WHERE C = @P
|--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1005],
[Expr1006], [Expr1004]))
|--Compute
Scalar(DEFINE:(([Expr1005],[Expr1006],[Expr1004])=GetRangeThroughConvert([@P],[@P],(62))))
| |--Constant Scan
|--Clustered Index Seek(OBJECT:([T].[TC]),
SEEK:([T].[C] > [Expr1005] AND [T].[C] < [Expr1006]), WHERE:(CONVERT_IMPLICIT(real(24),[T].[C],0)=[@P])
ORDERED FORWARD)
Этот план тоже похож на сгенерированный SQL Server
2000 с одним важным отличием: преобразование для поискового ключа из REAL в INT
выполняется внутренней функцией GetRangeThroughConvert, которая правильно определяет точный диапазон
значений для поиска. Небольшой эксперимент покажет, что в этом примере затрагивается
диапазон для значений между 999999968 и 1000 000479. С другой стороны, для
примера с числовым столбцом «диапазон» срабатывает ровно до 10000. Это решение
и точнее, и эффективнее, чем алгоритм SQL Server 2000.
Наконец, давайте рассмотрим влияние нового плана на
производительность:
CREATE TABLE T1 (C_INT INT, C_REAL REAL)
CREATE CLUSTERED INDEX T1_C_REAL ON T1(C_REAL)
CREATE TABLE T2 (C_INT INT, C_REAL REAL)
CREATE CLUSTERED INDEX T2_C_INT ON T2(C_INT)
SET NOCOUNT ON
DECLARE @I INT
SET @I = 0
WHILE @I < 100000
BEGIN
INSERT T1 VALUES (@I, @I)
INSERT T2 VALUES (@I, @I)
SET @I = @I + 1
END
Ниже три запроса и три плана с соединениями. В них
используется подсказка типа соединения, чтобы гарантированно не получить
хэш-соединение, которое использует совершенно другой алгоритм. COUNT(*) используется для
исключения накладных расходов на передачу строк клиенту, а подсказка OPTION, чтобы избежать параллелизма. Первый план соединяет
две колонки INT. Второй план соединяет два столбца REAL. Третий план соединяет колонки INT и REAL и использует тот же
алгоритм с функцией GetRangeThroughConvert, что и простой запрос
выше.
SELECT COUNT(*)
FROM T1 INNER LOOP JOIN T2 ON T1.C_INT = T2.C_INT
OPTION(MAXDOP 1)
|--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))
|--Stream
Aggregate(DEFINE:([Expr1012]=Count(*)))
|--Nested
Loops(Inner Join, OUTER REFERENCES:([T1].[C_INT], [Expr1011]) WITH UNORDERED
PREFETCH)
|--Clustered Index Scan(OBJECT:([T1].[T1_C_REAL]))
|--Clustered Index Seek(OBJECT:([T2].[T2_C_INT]),
SEEK:([T2].[C_INT]=[T1].[C_INT]) ORDERED FORWARD)
SELECT COUNT(*)
FROM T2 INNER LOOP JOIN T1 ON T2.C_REAL = T1.C_REAL
OPTION(MAXDOP 1)
|--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1012],0)))
|--Stream
Aggregate(DEFINE:([Expr1012]=Count(*)))
|--Nested
Loops(Inner Join, OUTER REFERENCES:([T2].[C_REAL], [Expr1011]) WITH UNORDERED
PREFETCH)
|--Clustered Index Scan(OBJECT:([T2].[T2_C_INT]))
|--Clustered Index Seek(OBJECT:([T1].[T1_C_REAL]),
SEEK:([T1].[C_REAL]=[T2].[C_REAL]) ORDERED FORWARD)
SELECT COUNT(*)
FROM T1 INNER LOOP JOIN T2 ON T1.C_REAL = T2.C_INT
OPTION(MAXDOP 1)
|--Compute Scalar(DEFINE:([Expr1008]=CONVERT_IMPLICIT(int,[Expr1014],0)))
|--Stream
Aggregate(DEFINE:([Expr1014]=Count(*)))
|--Nested
Loops(Inner Join, OUTER REFERENCES:([T1].[C_REAL]))
|--Clustered
Index Scan(OBJECT:([T1].[T1_C_REAL]))
|--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1012], [Expr1013],
[Expr1011]))
|--Compute Scalar(DEFINE:(([Expr1012],[Expr1013],[Expr1011])=GetRangeThroughConvert([T1].[C_REAL],[T1].[C_REAL],(62))))
| |--Constant Scan
|--Clustered Index Seek(OBJECT:([T2].[T2_C_INT]), SEEK:([T2].[C_INT] >
[Expr1012] AND [T2].[C_INT] < [Expr1013]), WHERE:([T1].[C_REAL]=CONVERT_IMPLICIT(real(24),[T2].[C_INT],0))
ORDERED FORWARD)
Время исполнения отслеживаем с помощью SET STATISTICS TIME ON. Вот результаты, которые я получил на своей системе:
·
SQL Server Execution Times: CPU time = 750
ms, elapsed time = 741 ms.
·
SQL Server Execution Times: CPU time = 735
ms, elapsed time = 742 ms.
·
SQL Server Execution Times: CPU time = 1609
ms, elapsed time = 1605 ms.
Обратите внимание, что первые два запроса с соединениями по совпадающим типам завершается за 740 ms, тогда как третий запрос с соединением по несовпадающим типам выполняется более чем в два раза дольше — 1600 ms! Мораль этой истории? Старайтесь придерживаться совпадающих типов, когда это возможно.
Комментариев нет:
Отправить комментарий