Database
 sql >> Base de Dados >  >> RDS >> Database

Parametrização Simples e Planos Triviais - Parte 2

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.