Esta é a décima terceira e última parte de uma série sobre expressões de tabela. Este mês continuo a discussão que comecei no mês passado sobre funções com valor de tabela inline (iTVFs).
No mês passado, expliquei que quando o SQL Server inline iTVFs que são consultados com constantes como entradas, ele aplica a otimização de incorporação de parâmetros por padrão. A incorporação de parâmetros significa que o SQL Server substitui as referências de parâmetros na consulta pelos valores de constantes literais da execução atual e, em seguida, o código com as constantes é otimizado. Esse processo permite simplificações que podem resultar em planos de consulta mais otimizados. Este mês eu elaboro o tema, cobrindo casos específicos para tais simplificações, como dobras constantes e filtragem e ordenação dinâmicas. Se você precisar de uma atualização sobre a otimização de incorporação de parâmetros, consulte o artigo do mês passado, bem como o excelente artigo de Paul White Parameter Sniffing, Embedding, and the RECOMPILE Options.
Em meus exemplos, usarei um banco de dados de exemplo chamado TSQLV5. Você pode encontrar o script que o cria e o preenche aqui e seu diagrama ER aqui.
Dobra constante
Durante os estágios iniciais do processamento da consulta, o SQL Server avalia certas expressões envolvendo constantes, dobrando-as para as constantes de resultado. Por exemplo, a expressão 40 + 2 pode ser dobrada para a constante 42. Você pode encontrar as regras para expressões dobráveis e não dobráveis aqui em “Constant Folding and Expression Evaluation”.
O que é interessante em relação aos iTVFs é que, graças à otimização de incorporação de parâmetros, as consultas envolvendo iTVFs nas quais você passa constantes como entradas podem, nas circunstâncias certas, se beneficiar do dobramento constante. Conhecer as regras para expressões dobráveis e não dobráveis pode afetar a maneira como você implementa seus iTVFs. Em alguns casos, ao aplicar alterações muito sutis em suas expressões, você pode habilitar planos mais otimizados com melhor utilização da indexação.
Como exemplo, considere a seguinte implementação de um iTVF chamado Sales.MyOrders:
USE TSQLV5;GO CREATE OR ALTER FUNCTION Sales.MyOrders ( @add AS INT, @subtract AS INT )RETURNS TABLEASRETURN SELECT orderid + @add - @subtract AS myorderid, orderdate, custid, empid FROM Sales.Orders;GO
Emita a seguinte consulta envolvendo o iTVF (vou me referir a isso como Consulta 1):
SELECT myorderid, orderdate, custid, empidFROM Sales.MyOrders(1, 10248)ORDER BY myorderid;
O plano para a Consulta 1 é mostrado na Figura 1.
Figura 1:plano para a consulta 1
O índice clusterizado PK_Orders é definido com orderid como chave. Se a dobragem constante tivesse ocorrido aqui após a incorporação do parâmetro, a expressão de ordenação orderid + 1 – 10248 teria sido dobrada para orderid – 10247. Essa expressão teria sido considerada uma expressão de preservação de ordem em relação a orderid e, como tal, teria permitido o otimizador para confiar na ordem do índice. Infelizmente, esse não é o caso, como é evidente pelo operador Sort explícito no plano. Então o que aconteceu?
As regras de dobragem constantes são complicadas. A expressão coluna1 + constante1 – constante2 é avaliada da esquerda para a direita para fins de dobra constante. A primeira parte, coluna1 + constante1 não é dobrada. Vamos chamar essa expressão1. A próxima parte avaliada é tratada como expression1 – constant2, que também não é dobrada. Sem dobrar, uma expressão no formato coluna1 + constante1 – constante2 não é considerada preservação da ordem em relação à coluna1 e, portanto, não pode depender da ordenação do índice, mesmo que você tenha um índice de suporte na coluna1. Da mesma forma, a expressão constante1 + coluna1 – constante2 não é dobrável constante. No entanto, a expressão constante1 – constante2 + coluna1 é dobrável. Mais especificamente, a primeira parte constante1 – constante2 é dobrada em uma única constante (vamos chamá-la de constante3), resultando na expressão constante3 + coluna1. Essa expressão é considerada uma expressão de preservação de ordem em relação à coluna1. Portanto, contanto que você tenha certeza de escrever sua expressão usando o último formulário, poderá habilitar o otimizador para confiar na ordenação do índice.
Considere as seguintes consultas (vou me referir a elas como Consulta 2, Consulta 3 e Consulta 4) e, antes de examinar os planos de consulta, veja se você pode dizer quais envolverão classificação explícita no plano e quais não:
-- Consulta 2SELECT orderid + 1 - 10248 AS myorderid, orderdate, custid, empidFROM Sales.OrdersORDER BY myorderid; -- Query 3SELECT 1 + orderid - 10248 AS myorderid, orderdate, custid, empidFROM Sales.OrdersORDER BY myorderid; -- Query 4SELECT 1 - 10248 + orderid AS myorderid, orderdate, custid, empidFROM Sales.OrdersORDER BY myorderid;
Agora examine os planos para essas consultas, conforme mostrado na Figura 2.
Figura 2:planos para consulta 2, consulta 3 e consulta 4
Examine os operadores Compute Scalar nos três planos. Apenas o plano para a Consulta 4 incorreu em dobras constantes, resultando em uma expressão de ordenação que é considerada preservadora de ordem em relação a orderid, evitando a classificação explícita.
Entendendo esse aspecto de dobra constante, você pode corrigir facilmente o iTVF alterando a expressão orderid + @add – @subtract para @add – @subtract + orderid, assim:
CREATE OR ALTER FUNCTION Sales.MyOrders ( @add AS INT, @subtract AS INT )RETURNS TABLEASRETURN SELECT @add - @subtract + orderid AS myorderid, orderdate, custid, empid FROM Sales.Orders;GO
Consulte a função novamente (vou me referir a isso como Consulta 5):
SELECT myorderid, orderdate, custid, empidFROM Sales.MyOrders(1, 10248)ORDER BY myorderid;
O plano para esta consulta é mostrado na Figura 3.
Figura 3:plano para a consulta 5
Como você pode ver, desta vez a consulta experimentou dobras constantes e o otimizador pôde contar com a ordenação do índice, evitando a classificação explícita.
Usei um exemplo simples para demonstrar essa técnica de otimização e, como tal, pode parecer um pouco artificial. Você pode encontrar uma aplicação prática dessa técnica no artigo Soluções de desafio do gerador de séries de números – Parte 1.
Filtragem/Ordenação Dinâmica
No mês passado, abordei a diferença entre a maneira como o SQL Server otimiza uma consulta em um iTVF versus a mesma consulta em um procedimento armazenado. O SQL Server normalmente aplicará a otimização de incorporação de parâmetros por padrão para uma consulta envolvendo um iTVF com constantes como entradas, mas otimizará a forma parametrizada de uma consulta em um procedimento armazenado. No entanto, se você adicionar OPTION(RECOMPILE) à consulta no procedimento armazenado, o SQL Server normalmente também aplicará a otimização de incorporação de parâmetros nesse caso. Os benefícios no caso do iTVF incluem o fato de que você pode envolvê-lo em uma consulta e, desde que você passe repetidas entradas constantes, há o potencial de reutilizar um plano anteriormente armazenado em cache. Com um procedimento armazenado, você não pode envolvê-lo em uma consulta e, se adicionar OPTION(RECOMPILE) para obter otimização de incorporação de parâmetros, não há possibilidade de reutilização do plano. O procedimento armazenado permite muito mais flexibilidade em termos dos elementos de código que você pode usar.
Vamos ver como tudo isso se desenrola em uma tarefa clássica de incorporação e ordenação de parâmetros. Veja a seguir um procedimento armazenado simplificado que aplica filtragem e classificação dinâmicas semelhantes ao que Paul usou em seu artigo:
CRIAR OU ALTERAR PROCEDIMENTO HR.GetEmpsP @lastnamepattern AS NVARCHAR(50), @sort AS TINYINTASSET NOCOUNT ON; SELECT empid, firstname, lastnameFROM HR.EmployeesWHERE lastname LIKE @lastnamepattern OR @lastnamepattern IS NULLORDER BY CASE WHEN @sort =1 THEN empid END, CASE WHEN @sort =2 THEN firstname END, CASE WHEN @sort =3 THEN lastname END;GO
Observe que a implementação atual do procedimento armazenado não inclui OPTION(RECOMPILE) na consulta.
Considere a seguinte execução do procedimento armazenado:
EXEC HR.GetEmpsP @lastnamepattern =N'D%', @sort =3;
O plano para esta execução é mostrado na Figura 4.
Figura 4:Plano para o procedimento HR.GetEmpsP
Há um índice definido na coluna sobrenome. Teoricamente, com as entradas atuais, o índice pode ser benéfico tanto para as necessidades de filtragem (com uma busca) quanto para a ordenação (com uma varredura ordenada:true range) da consulta. No entanto, como por padrão o SQL Server otimiza a forma parametrizada da consulta e não aplica a incorporação de parâmetros, ele não aplica as simplificações necessárias para poder se beneficiar do índice para fins de filtragem e ordenação. Portanto, o plano é reutilizável, mas não é o ideal.
Para ver como as coisas mudam com a otimização de incorporação de parâmetros, altere a consulta do procedimento armazenado adicionando OPTION(RECOMPILE), assim:
CRIAR OU ALTERAR PROCEDIMENTO HR.GetEmpsP @lastnamepattern AS NVARCHAR(50), @sort AS TINYINTASSET NOCOUNT ON; SELECT empid, firstname, lastnameFROM HR.EmployeesWHERE lastname LIKE @lastnamepattern OR @lastnamepattern IS NULLORDER BY CASE WHEN @sort =1 THEN empid END, CASE WHEN @sort =2 THEN firstname END, CASE WHEN @sort =3 THEN lastname ENDOPTION(RECOMPILE );Ir
Execute o procedimento armazenado novamente com as mesmas entradas que você usou antes:
EXEC HR.GetEmpsP @lastnamepattern =N'D%', @sort =3;
O plano para esta execução é mostrado na Figura 5.
Figura 5:Planejar o procedimento HR.GetEmpsP com OPTION(RECOMPILE)
Como você pode ver, graças à otimização de incorporação de parâmetros, o SQL Server conseguiu simplificar o predicado de filtro para o predicado sargable lastname LIKE N'D%' e a lista de ordenação para NULL, NULL, lastname. Ambos os elementos podem se beneficiar do índice no sobrenome e, portanto, o plano mostra uma busca no índice e nenhuma classificação explícita.
Teoricamente, você espera conseguir uma simplificação semelhante se implementar a consulta em um iTVF e, portanto, benefícios de otimização semelhantes, mas com a capacidade de reutilizar planos em cache quando os mesmos valores de entrada forem reutilizados. Então vamos tentar…
Aqui está uma tentativa de implementar a mesma consulta em um iTVF (não execute este código ainda):
CREATE OR ALTER FUNCTION HR.GetEmpsF( @lastnamepattern AS NVARCHAR(50), @sort AS TINYINT)RETURNS TABLEASRETURN SELECT empid, firstname, lastname FROM HR.Employees WHERE lastname LIKE @lastnamepattern OR @lastnamepattern IS NULL ORDER BY CASE WHEN @sort =1 THEN empid END, CASE WHEN @sort =2 THEN firstname END, CASE WHEN @sort =3 THEN lastname END;GO
Antes de tentar executar este código, você pode ver um problema com esta implementação? Lembre-se de que no início desta série expliquei que uma expressão de tabela é uma tabela. O corpo de uma tabela é um conjunto (ou multiconjunto) de linhas e, como tal, não tem ordem. Portanto, normalmente, uma consulta usada como expressão de tabela não pode ter uma cláusula ORDER BY. De fato, se você tentar executar este código, receberá o seguinte erro:
Msg 1033, Level 15, State 1, Procedure GetEmps, Line 16 [Batch Start Line 128]
A cláusula ORDER BY é inválida em visualizações, funções inline, tabelas derivadas, subconsultas e expressões de tabela comuns, a menos que TOP, OFFSET ou FOR XML também é especificado.
Claro, como o erro diz, o SQL Server fará uma exceção se você usar um elemento de filtragem como TOP ou OFFSET-FETCH, que depende da cláusula ORDER BY para definir o aspecto de ordenação do filtro. Mas mesmo que você inclua uma cláusula ORDER BY na consulta interna graças a essa exceção, você ainda não obtém uma garantia para a ordem do resultado em uma consulta externa em relação à expressão da tabela, a menos que tenha sua própria cláusula ORDER BY .
Se você ainda deseja implementar a consulta em um iTVF, pode fazer com que a consulta interna lide com a parte de filtragem dinâmica, mas não com a ordenação dinâmica, assim:
CREATE OR ALTER FUNCTION HR.GetEmpsF( @lastnamepattern AS NVARCHAR(50))RETURNS TABLEASRETURN SELECT empid, firstname, lastname FROM HR.Employees WHERE lastname LIKE @lastnamepattern OR @lastnamepattern IS NULL;GO
Claro, você pode fazer com que a consulta externa lide com qualquer necessidade de ordenação específica, como no código a seguir (vou me referir a isso como Consulta 6):
SELECT empid, firstname, lastnameFROM HR.GetEmpsF(N'D%')ORDER BY lastname;
O plano para esta consulta é mostrado na Figura 6.
Figura 6:planejar a consulta 6
Graças ao inlining e à incorporação de parâmetros, o plano é semelhante ao mostrado anteriormente para a consulta de procedimento armazenado na Figura 5. O plano depende eficientemente do índice para fins de filtragem e ordenação. No entanto, você não obtém a flexibilidade da entrada de ordenação dinâmica como tinha com o procedimento armazenado. Você precisa ser explícito com a ordenação na cláusula ORDER BY na consulta em relação à função.
O exemplo a seguir tem uma consulta na função sem filtragem e sem requisitos de ordenação (vou me referir a isso como Consulta 7):
SELECT empid, firstname, lastnameFROM HR.GetEmpsF(NULL);
O plano para esta consulta é mostrado na Figura 7.
Figura 7:planejar a consulta 7
Após a inserção e a incorporação de parâmetros, a consulta é simplificada para não ter predicado de filtro nem ordenação e é otimizada com uma varredura completa não ordenada do índice clusterizado.
Por fim, consulte a função com N'D%' como o padrão de filtragem de sobrenome de entrada e ordene o resultado pela coluna firstname (vou me referir a isso como Consulta 8):
SELECT empid, firstname, lastnameFROM HR.GetEmpsF(N'D%')ORDER BY firstname;
O plano para esta consulta é mostrado na Figura 8.
Figura 8:planejar a consulta 8
Após a simplificação, a consulta envolve apenas o predicado de filtragem lastname LIKE N'D%' e o elemento de ordenação firstname. Desta vez, o otimizador opta por aplicar uma varredura não ordenada do índice clusterizado, com o predicado residual sobrenome LIKE N'D%', seguido de classificação explícita. Ele optou por não aplicar uma busca no índice no sobrenome porque o índice não é abrangente, a tabela é muito pequena e a ordenação do índice não é benéfica para as necessidades atuais de ordenação da consulta. Além disso, não há índice definido na coluna firstname, portanto, uma classificação explícita deve ser aplicada de qualquer maneira.
Conclusão
A otimização de incorporação de parâmetros padrão de iTVFs também pode resultar em dobras constantes, permitindo planos mais otimizados. No entanto, você precisa estar atento às regras de dobra constante para determinar a melhor forma de formular suas expressões.
A implementação da lógica em um iTVF tem vantagens e desvantagens em comparação com a implementação da lógica em um procedimento armazenado. Se você não estiver interessado na otimização de incorporação de parâmetros, a otimização de consulta parametrizada padrão de procedimentos armazenados pode resultar em um cache de plano e comportamento de reutilização mais otimizados. Nos casos em que você está interessado na otimização de incorporação de parâmetros, normalmente você a obtém por padrão com iTVFs. Para obter essa otimização com procedimentos armazenados, você precisa adicionar a opção de consulta RECOMPILE, mas não terá a reutilização do plano. Pelo menos com iTVFs, você pode obter a reutilização do plano desde que os mesmos valores de parâmetro sejam repetidos. Por outro lado, você tem menos flexibilidade com os elementos de consulta que pode usar em um iTVF; por exemplo, você não tem permissão para ter uma cláusula ORDER BY de apresentação.
De volta a toda a série sobre expressões de tabela, acho o tópico super importante para os profissionais de banco de dados. A série mais completa inclui a subsérie no gerador de séries numéricas, que é implementado como um iTVF. No total, a série inclui as seguintes 19 partes:
- Fundamentos das expressões de tabela, Parte 1
- Fundamentos de expressões de tabela, Parte 2 – Tabelas derivadas, considerações lógicas
- Fundamentos das expressões de tabela, Parte 3 – Tabelas derivadas, considerações de otimização
- Fundamentos de expressões de tabela, Parte 4 – Tabelas derivadas, considerações de otimização, continuação
- Fundamentos de expressões de tabela, Parte 5 – CTEs, considerações lógicas
- Fundamentos de expressões de tabela, Parte 6 – CTEs recursivos
- Fundamentos de expressões de tabela, Parte 7 – CTEs, considerações de otimização
- Fundamentos de expressões de tabela, Parte 8 – CTEs, considerações de otimização continuação
- Fundamentos de expressões de tabela, Parte 9 – Visualizações, em comparação com tabelas derivadas e CTEs
- Fundamentos de expressões de tabela, Parte 10 – Visualizações, SELECT * e alterações de DDL
- Fundamentos das Expressões de Tabela, Parte 11 - Visualizações, Considerações de Modificação
- Fundamentos das Expressões de Tabela, Parte 12 – Funções Inline com Valor de Tabela
- Fundamentos das Expressões de Tabela, Parte 13 – Funções Inline com Valor de Tabela, continuação
- O desafio está lançado! Chamada da comunidade para criar o gerador de séries numéricas mais rápido
- Soluções do desafio do gerador da série numérica - Parte 1
- Soluções do desafio do gerador da série numérica - Parte 2
- Soluções do desafio do gerador da série numérica - Parte 3
- Soluções do desafio do gerador da série numérica - Parte 4
- Soluções do desafio do gerador da série numérica - Parte 5