Quando os predicados SARGable podem ser enviados para uma tabela CTE ou derivada?

15

Saco de areia

Enquanto trabalhava no Top Quality Blog Posts®, me deparei com um comportamento otimizador que achei realmente irritante interessante. Eu não tenho uma explicação imediata, pelo menos não uma com a qual estou feliz, então estou colocando aqui caso alguém inteligente apareça.

Se você quiser acompanhar, você pode pegar a versão 2013 do despejo de dados do Stack Overflow aqui . Estou usando a tabela Comentários, com um índice adicional.

CREATE INDEX [ix_ennui] ON [dbo].[Comments] ( [UserId], [Score] DESC );

Consulta Um

Quando consulta a tabela dessa maneira, recebo um plano de consulta ímpar .

WITH x
    AS
     (
         SELECT   TOP 101
                  c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
         ORDER BY c.Score DESC
     )
SELECT *
FROM   x
WHERE  x.Score >= 500;

NUTS

O predicado SARGable no Score não é enviado para dentro do CTE. Está em um operador de filtro muito mais tarde no plano.

NUTS

O que acho estranho, já que o ORDER BYestá na mesma coluna do filtro.

Consulta Dois

Se eu alterar a consulta, ela será enviada.

WITH x
    AS
     (
         SELECT   c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
     )
SELECT TOP 101 *
FROM   x
WHERE  x.Score >= 500
ORDER BY x.Score DESC;

O plano de consulta também muda e é executado muito mais rápido, sem derramamento no disco. Ambos produzem os mesmos resultados, com o predicado na varredura de índice não clusterizada.

NUTS

NUTS

Consulta Três

Isso é o equivalente a escrever a consulta da seguinte maneira:

SELECT   TOP 101
         c.UserId, c.Text, c.Score
FROM     dbo.Comments AS c
WHERE c.Score >= 500
ORDER BY c.Score DESC;

Consulta Quatro

O uso de uma tabela derivada obtém o mesmo plano de consulta "inválido" da consulta CTE inicial

SELECT *
FROM   (   SELECT   TOP 101
                    c.UserId, c.Text, c.Score
           FROM     dbo.Comments AS c
           ORDER BY c.Score DESC ) AS x
WHERE x.Score >= 500;

As coisas ficam ainda mais estranhas quando ...

Altero a consulta para ordenar os dados ascendentes e o filtro para <=.

Para não exagerar nessa pergunta, vou juntar tudo.

Consultas

--Derived table
SELECT *
FROM   (   SELECT   TOP 101
                    c.UserId, c.Text, c.Score
           FROM     dbo.Comments AS c
           ORDER BY c.Score ASC ) AS x
WHERE x.Score <= 500;


--TOP inside CTE
WITH x
    AS
     (
         SELECT   TOP 101
                  c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
         ORDER BY c.Score ASC
     )
SELECT *
FROM   x
WHERE  x.Score <= 500;


--Written normally
SELECT   TOP 101
         c.UserId, c.Text, c.Score
FROM     dbo.Comments AS c
WHERE c.Score <= 500
ORDER BY c.Score ASC;

--TOP outside CTE
WITH x
    AS
     (
         SELECT   c.UserId, c.Text, c.Score
         FROM     dbo.Comments AS c
     )
SELECT TOP 101 *
FROM   x
WHERE  x.Score <= 500
ORDER BY x.Score ASC;

Planos

Plano de ligação .

NUTS

Observe que nenhuma dessas consultas tira proveito do índice não clusterizado - a única coisa que muda aqui é a posição do operador de filtro. Em nenhum caso o predicado é enviado para o acesso ao índice.

Uma pergunta aparece!

Existe uma razão para que um predicado SARGable possa ser enviado em alguns cenários e não em outros? As diferenças nas consultas classificadas em ordem decrescente são interessantes, mas as diferenças entre essas e as que estão subindo são bizarras.

Para quem estiver interessado, aqui estão os planos com apenas um índice Score:

Erik Darling
fonte

Respostas:

11

Existem alguns problemas em jogo aqui.

Empurrando predicados além TOP

Atualmente, o otimizador não pode ultrapassar um predicado a TOP, mesmo nos casos limitados em que seria seguro fazê-lo *. Essa limitação explica o comportamento de todas as consultas na pergunta em que o predicado está em um escopo mais alto que o TOP.

A solução é executar a reescrita manualmente. A questão fundamental é semelhante ao caso de enviar predicados para uma função da janela , exceto que não existe uma regra especializada correspondente SelOnSeqPrj.

Minha opinião pessoal é que uma regra de exploração como essa SelOnToppermanece não implementada porque as pessoas deliberadamente escrevem consultas com o TOPobjetivo de fornecer uma espécie de "cerca de otimização".

* Geralmente, isso significa que o predicado deve aparecer na ORDER BYcláusula associada ae TOPa direção de qualquer desigualdade deve concordar com a direção da classificação. A transformação também precisaria considerar o comportamento de classificação de NULLs no SQL Server. No geral, as limitações provavelmente significam que essa transformação geralmente não seria útil o suficiente na prática para justificar os esforços adicionais de exploração.

Problemas de custo

Os demais planos de execução na pergunta podem ser explicados como opções baseadas em custo devido à distribuição de valores na Scorecoluna (muito mais linhas <= 500 que> = 500) e o efeito do objetivo da linha introduzido pelo TOP.

Por exemplo, a consulta:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score <= 500
ORDER BY
    c.Score ASC;

... produz um plano com um predicado aparentemente sem push em um filtro:

filtro atrasado devido ao objetivo da linha

Observe que a classificação é estimada para produzir 101 linhas. Esse é o efeito da meta de linha adicionada pela parte superior. Isso afeta o custo estimado da classificação e do filtro o suficiente para parecer que essa é a opção mais barata. O custo estimado deste plano é de 2401,39 unidades.

Se desativarmos as metas de linha com uma dica de consulta:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score <= 500
ORDER BY
    c.Score ASC
OPTION (USE HINT ('DISABLE_OPTIMIZER_ROWGOAL'));

... o plano de execução produzido é:

planejar sem meta de linha

O predicado foi inserido na varredura como um predicado residual não sargável e o custo de todo o plano é de 2402,32 unidades.

Observe que o <= 500predicado não deve filtrar nenhuma linha. Se você tivesse escolhido um número menor, como <= 50, por exemplo, o otimizador preferiria o plano de predicado enviado, independentemente do efeito da meta da linha.

Para a consulta com Score DESCe um Score >= 500predicado:

--Written normally
SELECT TOP (101)
    c.UserId, 
    c.[Text],
    c.Score
FROM dbo.Comments AS c
WHERE
    c.Score >= 500
ORDER BY
    c.Score DESC;

Agora, espera-se que o predicado seja muito seletivo, portanto, o otimizador escolhe enviar por push o predicado e usar o índice não clusterizado com pesquisas:

predicado seletivo

Novamente, o otimizador considerou várias alternativas e a escolheu como a opção aparentemente mais barata, como de costume.

Paul White restabelece Monica
fonte