ordem das cláusulas em “EXISTENTES (…) OU EXISTEM (…)”

11

Eu tenho uma classe de consultas que testam a existência de uma das duas coisas. É da forma

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

A instrução real é gerada em C e executada como uma consulta ad-hoc em uma conexão ODBC.

Recentemente, ficou claro que o segundo SELECT provavelmente será mais rápido que o primeiro SELECT na maioria dos casos e que a mudança da ordem das duas cláusulas EXISTS causou uma aceleração drástica em pelo menos um caso de teste abusivo que acabamos de criar.

A coisa mais óbvia a fazer é seguir em frente e mudar as duas cláusulas, mas eu queria ver se alguém mais familiarizado com o SQL Server se importaria em ponderar sobre isso. Parece que estou confiando na coincidência e em um "detalhe da implementação".

(Também parece que, se o SQL Server fosse mais inteligente, ele executaria as duas cláusulas EXISTS em paralelo e permitiria que qualquer uma delas concluída primeiro provoque um curto-circuito na outra.)

Existe uma maneira melhor de obter o SQL Server para melhorar consistentemente o tempo de execução dessa consulta?

Atualizar

Obrigado pelo seu tempo e interesse na minha pergunta. Eu não esperava perguntas sobre os planos de consulta reais, mas estou disposto a compartilhá-los.

Isso é para um componente de software que oferece suporte ao SQL Server 2008R2 e superior. A forma dos dados pode ser bem diferente, dependendo da configuração e uso. Meu colega de trabalho pensou em fazer essa alteração na consulta porque a dbf_1162761$z$rv$1257927703tabela (no exemplo) sempre terá maior ou igual ao número de linhas que a dbf_1162761$z$dd$1257927703tabela - às vezes significativamente mais (ordens de magnitude).

Aqui está o caso abusivo que mencionei. A primeira consulta é lenta e leva cerca de 20 segundos. A segunda consulta é concluída em um instante.

Pelo que vale a pena, o bit "OPTIMIZE FOR UNKNOWN" também foi adicionado recentemente porque o sniffing de parâmetros estava destruindo certos casos.

Consulta original:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plano original:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Consulta corrigida:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plano fixo:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
jr
fonte
1
Perguntas e respostas relacionadas: Operação física da concatenação: garante a ordem de execução?
Paul White 9

Respostas:

11

Como regra geral, o SQL Server executará as partes de uma CASEinstrução em ordem, mas pode reordenar as ORcondições. Para algumas consultas, você pode obter um desempenho consistentemente melhor alterando a ordem das WHENexpressões dentro de uma CASEinstrução. Às vezes, você também pode obter um melhor desempenho ao alterar a ordem das condições em uma ORinstrução, mas não é um comportamento garantido.

Provavelmente, é melhor seguir com um exemplo simples. Estou testando no SQL Server 2016, portanto, é possível que você não obtenha exatamente os mesmos resultados em sua máquina, mas, tanto quanto sei, os mesmos princípios se aplicam. Primeiro, colocarei um milhão de números inteiros de 1 a 1000000 em duas tabelas, uma com um índice clusterizado e outra como heap:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Considere a seguinte consulta:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Sabemos que avaliar a subconsulta X_CIserá muito mais barato que a subconsulta X_HEAP, especialmente quando não houver uma linha correspondente. Se não houver uma linha correspondente, precisamos apenas fazer algumas leituras lógicas na tabela com um índice em cluster. No entanto, precisaríamos varrer todas as linhas da pilha para saber que não há uma linha correspondente. O otimizador também sabe disso. Em termos gerais, o uso de um índice clusterizado para procurar uma linha é muito barato se comparado à varredura de uma tabela.

Para este exemplo de dados, eu escreveria a consulta assim:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Isso efetivamente força o SQL Server a executar a subconsulta na tabela com um índice clusterizado primeiro. Aqui estão os resultados de SET STATISTICS IO, TIME ON:

Tabela 'X_CI'. Contagem de varreduras 0, leituras lógicas 3, leituras físicas 0

Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms.

Observando o plano de consulta, se a busca no rótulo 1 retornar algum dado, a verificação no rótulo 2 não será necessária e não ocorrerá:

boa consulta

A consulta a seguir é muito menos eficiente:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

Observando o plano de consulta, vemos que a verificação no rótulo 2 sempre acontece. Se uma linha for encontrada, a busca no rótulo 1 será ignorada. Essa não é a ordem que queríamos:

plano de consulta incorreto

O desempenho resulta disso:

Tabela 'X_HEAP'. Contagem de varreduras 1, leituras lógicas 7247

Tempos de execução do SQL Server: tempo de CPU = 15 ms, tempo decorrido = 22 ms.

Voltando à consulta original, para esta consulta, vejo a busca e a verificação avaliadas na ordem que é boa para o desempenho:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

E nesta consulta, eles são avaliados na ordem oposta:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

No entanto, ao contrário do par de consultas anterior, não há nada que force o otimizador de consultas do SQL Server a avaliar uma antes da outra. Você não deve confiar nesse comportamento para nada importante.

Em conclusão, se você precisar que uma subconsulta seja avaliada antes da outra, use uma CASEinstrução ou outro método para forçar a solicitação. Caso contrário, sinta-se à vontade para solicitar subconsultas em uma ORcondição da maneira que desejar, mas saiba que não há garantia de que o otimizador as executará na ordem conforme escrita.

Termo aditivo:

Uma pergunta de acompanhamento natural é o que você pode fazer se você quiser que o SQL Server decida qual consulta é mais barata e execute essa pergunta primeiro? Até agora, todos os métodos parecem ter sido implementados pelo SQL Server na ordem em que a consulta é gravada, mesmo que não seja um comportamento garantido para alguns deles.

Aqui está uma opção que parece funcionar para as tabelas de demonstração simples:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Você pode encontrar uma demonstração do db fiddle aqui . Alterar a ordem das tabelas derivadas não altera o plano de consulta. Nas duas consultas, a X_HEAPtabela não é tocada. Em outras palavras, o otimizador de consulta parece executar a consulta mais barata primeiro. Não posso recomendar o uso de algo assim na produção, por isso está aqui principalmente pelo valor da curiosidade. Pode haver uma maneira muito mais simples de realizar a mesma coisa.

Joe Obbish
fonte
4
Ou CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDpoderia ser uma alternativa, embora isso ainda dependa manualmente de decidir qual consulta é mais rápida e de colocá-la em primeiro lugar. Não tenho certeza se existe uma maneira de expressá-lo, para que o SQL Server reordene automaticamente, para que o mais barato seja avaliado primeiro primeiro.
Martin Smith