Tipos de dados de parâmetro
Conforme mencionado na primeira parte desta série, uma das razões pelas quais é melhor parametrizar explicitamente é para que você tenha controle total sobre os tipos de dados de parâmetro. A parametrização simples tem uma série de peculiaridades nesta área, que podem resultar em mais planos parametrizados sendo armazenados em cache do que o esperado ou encontrando resultados diferentes em comparação com a versão não parametrizada.
Quando o SQL Server aplica parametrização simples para uma instrução ad-hoc, ele faz uma suposição sobre o tipo de dados do parâmetro de substituição. Vou cobrir as razões para a adivinhação mais tarde na série.
Por enquanto, vejamos alguns exemplos usando o banco de dados Stack Overflow 2010 no SQL Server 2019 CU 14. A compatibilidade do banco de dados está definida como 150 e o limite de custo para o paralelismo está definido como 50 para evitar o paralelismo por enquanto:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 25221; GO SELECT U.DisplayName FROM dbo.Users AS U WHERE U.Reputation = 252552;
Essas instruções resultam em seis planos em cache, três Adhoc e três Preparados :
Diferentes tipos de adivinhação
Observe os diferentes tipos de dados de parâmetro no Preparado planos.
Inferência de tipo de dados
Os detalhes de como cada tipo de dado é adivinhado são complexos e documentados de forma incompleta. Como ponto de partida, o SQL Server infere um tipo básico da representação textual do valor e, em seguida, usa o menor subtipo compatível.
Para uma sequência de números sem aspas ou ponto decimal, o SQL Server escolhe
tinyint
, smallint
, e integer
. Para esses números além do intervalo de um integer
, o SQL Server usa numeric
com a menor precisão possível. Por exemplo, o número 2.147.483.648 é digitado como numeric(10,0)
. O bigint
type não é usado para parametrização do lado do servidor. Este parágrafo explica os tipos de dados selecionados nos exemplos anteriores. Strings de números com um ponto decimal são interpretados como
numeric
, com precisão e escala grandes o suficiente para conter o valor fornecido. Strings prefixadas com um símbolo de moeda são interpretadas como money
. Strings em notação científica são traduzidas para float
. O smallmoney
e real
tipos não são empregados. O
datetime
e uniqueidentifer
tipos não podem ser inferidos de formatos de string naturais. Para obter um datetime
ou uniqueidentifer
tipo de parâmetro, o valor literal deve ser fornecido no formato de escape ODBC. Por exemplo {d '1901-01-01'}
, {ts '1900-01-01 12:34:56.790'}
, ou {guid 'F85C72AB-15F7-49E9-A949-273C55A6C393'}
. Caso contrário, a data pretendida ou o literal UUID é digitado como uma string. Tipos de data e hora diferentes de datetime
não são usados. String geral e literais binários são digitados como
varchar(8000)
, nvarchar(4000)
, ou varbinary(8000)
conforme apropriado, a menos que o literal exceda 8.000 bytes, caso em que o max
variante é usada. Esse esquema ajuda a evitar a poluição do cache e o baixo nível de reutilização que resultaria do uso de comprimentos específicos. Não é possível usar
CAST
ou CONVERT
para definir o tipo de dados para parâmetros por motivos que detalharei mais adiante nesta série. Há um exemplo disso na próxima seção. Não abordarei parametrização forçada nesta série, mas quero mencionar que as regras para inferência de tipo de dados nesse caso têm algumas diferenças importantes em comparação com a parametrização simples . A parametrização forçada não foi adicionada até o SQL Server 2005, então a Microsoft teve a oportunidade de incorporar algumas lições da parametrização simples experiência e não precisa se preocupar muito com problemas de compatibilidade com versões anteriores.
Tipos numéricos
Para números com um ponto decimal e números inteiros além do intervalo de
integer
, as regras de tipo inferido apresentam problemas especiais para reutilização de planos e poluição de cache. Considere a seguinte consulta usando decimais:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DROP TABLE IF EXISTS dbo.Test; GO CREATE TABLE dbo.Test ( SomeValue decimal(19,8) NOT NULL ); GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 987.65432 AND T.SomeValue < 123456.789;
Esta consulta se qualifica para parametrização simples . O SQL Server escolhe a menor precisão e escala para os parâmetros capazes de conter os valores fornecidos. Isso significa que ele escolhe
numeric(8,5)
para 987.65432
e numeric(9,3)
para 123456.789
:Tipos de dados numéricos inferidos
Esses tipos inferidos não correspondem ao
decimal(19,8)
type da coluna, então uma conversão em torno do parâmetro aparece no plano de execução:Conversão para tipo de coluna
Essas conversões representam apenas uma pequena ineficiência de tempo de execução neste caso específico. Em outras situações, uma incompatibilidade entre o tipo de dados da coluna e o tipo inferido de um parâmetro pode impedir uma busca de índice ou exigir que o SQL Server faça um trabalho extra para fabricar uma busca dinâmica.
Mesmo quando o plano de execução resultante parece razoável, uma incompatibilidade de tipo pode afetar facilmente a qualidade do plano devido ao efeito da incompatibilidade de tipo na estimativa de cardinalidade. É sempre melhor usar tipos de dados correspondentes e prestar muita atenção aos tipos derivados resultantes de expressões.
Planejar Reutilização
O principal problema com o plano atual são os tipos inferidos específicos que afetam a correspondência do plano em cache e, portanto, a reutilização. Vamos executar mais algumas consultas da mesma forma geral:
SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 98.76 AND T.SomeValue < 123.4567; GO SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= 1.2 AND T.SomeValue < 1234.56789; GO
Agora olhe para o cache do plano:
SELECT CP.usecounts, CP.objtype, ST.[text] FROM sys.dm_exec_cached_plans AS CP CROSS APPLY sys.dm_exec_sql_text (CP.plan_handle) AS ST WHERE ST.[text] NOT LIKE '%dm_exec_cached_plans%' AND ST.[text] LIKE '%SomeValue%Test%' ORDER BY CP.objtype ASC;
Ele mostra um AdHoc e Preparado declaração para cada consulta que enviamos:
Declarações preparadas separadas
O texto parametrizado é o mesmo, mas os tipos de dados de parâmetro são diferentes, portanto, planos separados são armazenados em cache e não ocorre reutilização de plano.
Se continuarmos a enviar consultas com diferentes combinações de escala ou precisão, um novo Preparado plano será criado e armazenado em cache a cada vez. Lembre-se de que o tipo inferido de cada parâmetro não é limitado pelo tipo de dados da coluna, portanto, podemos acabar com um grande número de planos em cache, dependendo dos literais numéricos enviados. O número de combinações de
numeric(1,0)
para numeric(38,38)
já é grande antes de pensarmos em vários parâmetros. Parametrização explícita
Esse problema não surge quando usamos parametrização explícita, escolhendo idealmente o mesmo tipo de dados da coluna com a qual o parâmetro é comparado:
ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; GO DECLARE @stmt nvarchar(4000) = N'SELECT T.SomeValue FROM dbo.Test AS T WHERE T.SomeValue >= @P1 AND T.SomeValue < @P2;', @params nvarchar(4000) = N'@P1 numeric(19,8), @P2 numeric(19,8)'; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 987.65432, @P2 = 123456.789; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 98.76, @P2 = 123.4567; EXECUTE sys.sp_executesql @stmt, @params, @P1 = 1.2, @P2 = 1234.56789;
Com parametrização explícita, a consulta de cache do plano mostra apenas um plano armazenado em cache, usado três vezes e nenhuma conversão de tipo necessária:
Parametrização explícita
Como observação final, usei
decimal
e numeric
alternadamente nesta seção. Eles são tecnicamente tipos diferentes, embora documentados como sinônimos e se comportando de forma equivalente. Este é geralmente o caso, mas nem sempre:-- Raises error 8120: -- Column 'dbo.Test.SomeValue' is invalid in the select list -- because it is not contained in either an aggregate function -- or the GROUP BY clause. SELECT CONVERT(decimal(19,8), T.SomeValue) FROM dbo.Test AS T GROUP BY CONVERT(numeric(19,8), T.SomeValue);
Provavelmente é um pequeno bug do analisador, mas ainda vale a pena ser consistente (a menos que você esteja escrevendo um artigo e queira apontar uma exceção interessante).
Operadores aritméticos
Há um outro caso extremo que quero abordar, com base em um exemplo fornecido na documentação, mas com um pouco mais de detalhes (e talvez precisão):
-- The dbo.LinkTypes table contains two rows -- Uses simple parameterization SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization due to -- constant-constant comparison SELECT r = CONVERT(float, 1./ 7) FROM dbo.LinkTypes AS LT WHERE 1 = 1;
Os resultados são diferentes, conforme documentado:
Diferentes resultados
Com parametrização simples
Quando parametrização simples ocorre, o SQL Server parametriza ambos os valores literais. O
1.
valor é digitado como numeric(1,0)
como esperado. Um pouco inconsistente, o 7
é digitado como integer
(não tinyint
). As regras de inferência de tipos foram construídas ao longo do tempo, por diferentes equipes. Os comportamentos são mantidos para evitar quebrar o código legado. A próxima etapa envolve o
/
operador aritmético. O SQL Server requer tipos compatíveis antes de realizar a divisão. Dado numeric
(decimal
) tem uma precedência de tipo de dados maior que integer
, o integer
será convertido para numeric
. O SQL Server precisa converter implicitamente o
integer
para numeric
. Mas qual precisão e escala usar? A resposta pode ser baseada no literal original, como o SQL Server faz em outras circunstâncias, mas sempre usa numeric(10)
aqui. O tipo de dados do resultado da divisão de um
numeric(1,0)
por um numeric(10,0)
é determinado por outro conjunto de regras, fornecido na documentação para precisão, escala e comprimento. Colocando os números nas fórmulas para precisão e escala do resultado, temos:- Precisão do resultado:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 10 + 1)
- =1 + max(6, 11)
- =1 + 11
- =12
- Escala de resultados:
- max(6, s1 + p2 + 1)
- =max(6, 0 + 10 + 1)
- =max(6, 11)
- =11
O tipo de dados de
1. / 7
é, portanto, numeric(12, 11)
. Este valor é então convertido para float
conforme solicitado e exibido como 0.14285714285
(com 11 dígitos após o ponto decimal). Sem parametrização simples
Quando a parametrização simples não é realizada, o
1.
literal é digitado como numeric(1,0)
como antes. O 7
é inicialmente digitado como integer
também como visto anteriormente. A principal diferença é o integer
é convertido para numeric(1,0)
, para que o operador de divisão tenha tipos comuns para trabalhar. Esta é a menor precisão e escala capaz de conter o valor 7
. Lembre-se de parametrização simples usada numeric(10,0)
aqui. As fórmulas de precisão e escala para dividir
numeric(1,0)
por numeric(1,0)
dê um tipo de dados de resultado de numeric(7,6)
:- Precisão do resultado:
- p1 – s1 + s2 + max(6, s1 + p2 + 1)
- =1 – 0 + 0 + max(6, 0 + 1 + 1)
- =1 + max(6, 2)
- =1 + 6
- =7
- Escala de resultados:
- max(6, s1 + p2 + 1)
- =max(6, 0 + 1 + 1)
- =max(6, 2)
- =6
Após a conversão final para
float
, o resultado exibido é 0.142857
(com seis dígitos após o ponto decimal). A diferença observada nos resultados é, portanto, devido à derivação de tipo provisória (
numeric(12,11)
vs. numeric(7,6)
) em vez da conversão final para float
. Se você precisar de mais evidências da conversão para
float
não é responsável, considere:-- Simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT; -- No simple parameterization SELECT r = CONVERT(decimal(13,12), 1. / 7) FROM dbo.LinkTypes AS LT OPTION (MAXDOP 1);
Resultado com decimal
Os resultados diferem em valor e escala como antes.
Esta seção não abrange todas as peculiaridades da inferência e conversão de tipos de dados com parametrização simples por qualquer meio. Como dito antes, é melhor usar parâmetros explícitos com tipos de dados conhecidos sempre que possível.
Fim da Parte 2
A próxima parte desta série descreve como a parametrização simples afeta os planos de execução.