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$1257927703
tabela (no exemplo) sempre terá maior ou igual ao número de linhas que a dbf_1162761$z$dd$1257927703
tabela - à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)
fonte
Respostas:
Como regra geral, o SQL Server executará as partes de uma
CASE
instrução em ordem, mas pode reordenar asOR
condições. Para algumas consultas, você pode obter um desempenho consistentemente melhor alterando a ordem dasWHEN
expressões dentro de umaCASE
instrução. Às vezes, você também pode obter um melhor desempenho ao alterar a ordem das condições em umaOR
instruçã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:
Considere a seguinte consulta:
Sabemos que avaliar a subconsulta
X_CI
será muito mais barato que a subconsultaX_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:
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
: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á:
A consulta a seguir é muito menos eficiente:
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:
O desempenho resulta disso:
Voltando à consulta original, para esta consulta, vejo a busca e a verificação avaliadas na ordem que é boa para o desempenho:
E nesta consulta, eles são avaliados na ordem oposta:
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
CASE
instrução ou outro método para forçar a solicitação. Caso contrário, sinta-se à vontade para solicitar subconsultas em umaOR
condiçã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:
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_HEAP
tabela 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.fonte
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 END
poderia 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.