O SQL Server divide A <> B em A <B OR A> B, produzindo resultados estranhos se B for não determinístico

26

Encontramos um problema interessante com o SQL Server. Considere o seguinte exemplo de reprodução:

CREATE TABLE #test (s_guid uniqueidentifier PRIMARY KEY);
INSERT INTO #test (s_guid) VALUES ('7E28EFF8-A80A-45E4-BFE0-C13989D69618');

SELECT s_guid FROM #test
WHERE s_guid = '7E28EFF8-A80A-45E4-BFE0-C13989D69618'
  AND s_guid <> NEWID();

DROP TABLE #test;

violino

Por favor, esqueça por um momento que a s_guid <> NEWID()condição parece totalmente inútil - este é apenas um exemplo mínimo de reprodução. Como a probabilidade de NEWID()corresponder a um determinado valor constante é extremamente pequena, ela deve ser avaliada como TRUE sempre.

Mas isso não acontece. A execução dessa consulta geralmente retorna 1 linha, mas às vezes (com bastante frequência, mais de 1 vez em cada 10) retorna 0 linhas. Eu o reproduzi com o SQL Server 2008 no meu sistema e você pode reproduzi-lo on-line com o violino vinculado acima (SQL Server 2014).

Examinar o plano de execução revela que o analisador de consultas aparentemente divide a condição em s_guid < NEWID() OR s_guid > NEWID():

captura de tela do plano de consulta

... o que explica completamente por que às vezes falha (se o primeiro ID gerado for menor e o segundo maior que o ID fornecido).

O SQL Server pode avaliar A <> Bcomo A < B OR A > B, mesmo que uma das expressões seja não determinística? Se sim, onde está documentado? Ou encontramos um bug?

Curiosamente, AND NOT (s_guid = NEWID())produz o mesmo plano de execução (e o mesmo resultado aleatório).

Encontramos esse problema quando um desenvolvedor queria excluir opcionalmente uma linha específica e usou:

s_guid <> ISNULL(@someParameter, NEWID())

como um "atalho" para:

(@someParameter IS NULL OR s_guid <> @someParameter)

Estou procurando documentação e / ou confirmação de um bug. O código não é tão relevante, portanto, as soluções alternativas não são necessárias.

Heinzi
fonte
4
Parece semelhante a esta pergunta: resultados inesperados com números aleatórios e tipos de junção
Erik Darling

Respostas:

22

O SQL Server pode avaliar A <> Bcomo A < B OR A > B, mesmo que uma das expressões seja não determinística?

Este é um ponto um tanto controverso, e a resposta é um "sim" qualificado.

A melhor discussão que eu conheço foi dada em resposta ao relatório de bug do Itzik Ben-Gan do Connect Bug with NEWID e Table Expressions , que foi fechado como não será corrigido. O Connect foi desativado, então o link existe para um arquivo da web. Infelizmente, muito material útil foi perdido (ou dificultado de encontrar) pelo fim do Connect. De qualquer forma, as citações mais úteis de Jim Hogg da Microsoft são:

Isso atinge o cerne da questão - a otimização pode alterar a semântica de um programa? Ou seja: se um programa fornece certas respostas, mas é executado lentamente, é legítimo que um Query Optimizer faça com que o programa seja executado mais rapidamente, mas também altera os resultados fornecidos?

Antes de gritar "NÃO!" (minha própria inclinação pessoal também :-), considere: a boa notícia é que, em 99% dos casos, as respostas são as mesmas. Portanto, a otimização de consultas é uma vitória clara. A má notícia é que, se a consulta contiver código com efeito colateral, planos diferentes PODEM realmente produzir resultados diferentes. E NEWID () é uma dessas 'funções' com efeito colateral (não determinística) que expõe a diferença. [Na verdade, se você experimentar, poderá criar outras - por exemplo, avaliação de curto-circuito das cláusulas AND: faça a segunda cláusula gerar uma divisão aritmética por zero - otimizações diferentes podem executar essa segunda cláusula ANTES da primeira cláusula] Isso reflete A explicação de Craig, em outra parte deste segmento, que o SqlServer não garante quando os operadores escalares são executados.

Portanto, temos uma escolha: se queremos garantir um certo comportamento na presença de código não determinístico (efeito colateral) - para que os resultados de JOINs, por exemplo, sigam a semântica de uma execução de loop aninhado - então nós pode usar OPÇÕES apropriadas para forçar esse comportamento - como aponta a UC. Mas o código resultante ficará lento - esse é o custo de, de fato, prejudicar o Query Optimizer.

Tudo isso dito, estamos movendo o Query Optimizer na direção do comportamento "conforme o esperado" para NEWID () - trocando desempenho por "resultados conforme o esperado".

Um exemplo da mudança de comportamento nesse sentido ao longo do tempo é o NULLIF funciona incorretamente com funções não determinísticas, como RAND () . Também existem outros casos semelhantes usando, por exemplo, COALESCEuma subconsulta que pode produzir resultados inesperados e que também estão sendo tratados gradualmente.

Jim continua:

Fechando o loop. . . Eu discuti essa questão com a equipe de desenvolvimento. E, finalmente, decidimos não mudar o comportamento atual, pelos seguintes motivos:

1) O otimizador não garante tempo ou número de execuções de funções escalares. Esse é um princípio estabelecido há muito tempo. É a 'margem de manobra' fundamental que permite ao otimizador liberdade suficiente para obter melhorias significativas na execução do plano de consulta.

2) Esse "comportamento de uma vez por linha" não é um problema novo, embora não seja amplamente discutido. Começamos a ajustar seu comportamento de volta no lançamento do Yukon. Mas é muito difícil definir com precisão, em todos os casos, exatamente o que isso significa! Por exemplo, isso se aplica a linhas intermediárias calculadas 'a caminho' do resultado final? - nesse caso, depende claramente do plano escolhido. Ou se aplica apenas às linhas que aparecerão no resultado concluído? - Há uma recursão desagradável acontecendo aqui, como eu tenho certeza que você concorda!

3) Como mencionei anteriormente, o padrão é "otimizar o desempenho" - o que é bom para 99% dos casos. O 1% dos casos em que isso pode alterar os resultados é bastante fácil de detectar - 'funções' com efeito colateral, como NEWID - e fácil de 'consertar' (desempenho da negociação, como conseqüência). Esse padrão para "otimizar o desempenho" novamente, é estabelecido há muito tempo e aceito. (Sim, não é a postura escolhida pelos compiladores para linguagens de programação convencionais, mas que seja).

Portanto, nossas recomendações são:

a) Evite confiar no tempo não garantido e na semântica do número de execuções. b) Evite usar NEWID () nas expressões da tabela. c) Use OPTION para forçar um comportamento específico (desempenho da negociação)

Espero que esta explicação ajude a esclarecer nossas razões para fechar esse bug como "não será corrigido".


Curiosamente, AND NOT (s_guid = NEWID())produz o mesmo plano de execução

Isso é consequência da normalização, que ocorre muito cedo durante a compilação de consultas. Ambas as expressões são compiladas exatamente da mesma forma normalizada, para que o mesmo plano de execução seja produzido.

Paul White diz que a GoFundMonica
fonte
Nesse caso, se queremos forçar um plano específico que parece evitar o problema, podemos usar WITH (FORCESCAN). Para ter certeza, devemos usar uma variável para armazenar o resultado de NEWID () antes de executar a consulta.
Razvan Socol
11

Isso está documentado (mais ou menos) aqui:

O número de vezes que uma função especificada em uma consulta é realmente executada pode variar entre os planos de execução criados pelo otimizador. Um exemplo é uma função invocada por uma subconsulta em uma cláusula WHERE. O número de vezes que a subconsulta e sua função são executadas podem variar de acordo com os diferentes caminhos de acesso escolhidos pelo otimizador.

Funções definidas pelo usuário

Este não é o único formulário de consulta em que o plano de consulta executará NEWID () várias vezes e alterará o resultado. Isso é confuso, mas é realmente crítico que NEWID () seja útil para geração de chave e classificação aleatória.

O mais confuso é que nem todas as funções não determinísticas realmente se comportam assim. Por exemplo, RAND () e GETDATE () serão executados apenas uma vez por consulta.

David Browne - Microsoft
fonte
Existe algum post de blog ou similar que explique por que / quando o mecanismo converterá "não é igual" em um intervalo?
Mister Magoo
3
Não que eu saiba. Pode ser de rotina, pois =, <e >podem ser eficientemente avaliada contra um BTree.
David Browne - Microsoft
5

Pelo que vale a pena, se você olhar para este antigo documento padrão do SQL 92 , os requisitos em torno da desigualdade são descritos na seção " 8.2 <comparison predicate>" da seguinte maneira:

1) Seja X e Y quaisquer dois <elemento construtor do valor da linha> s correspondentes. Sejam XV e YV os valores representados por X e Y, respectivamente.

[...]

ii) "X <> Y" é verdadeiro se e somente se XV e YV não forem iguais.

[...]

7) Seja Rx e Ry os dois <construtores de valor de linha> s do <predicado de comparação> e RXi e RYi sejam o i-ésimo <elemento construtor de valor de linha> s de Rx e Ry, respectivamente. "Rx <comp op> Ry" é verdadeiro, falso ou desconhecido da seguinte maneira:

[...]

b) "x <> Ry" é verdadeiro se e somente se RXi <> RYi para alguns i.

[...]

h) "x <> Ry" é falso se e somente se "Rx = Ry" for verdadeiro.

Nota: Incluí 7b e 7h para completude, pois eles falam sobre <>comparação - não acho que a comparação de construtores de valor de linha com vários valores seja implementada no T-SQL, a menos que eu esteja apenas entendendo mal o que isso diz - o que é bem possível

Isso é um monte de lixo confuso. Mas se você quiser continuar mergulhando no lixo ...

Eu acho que 1.ii é o item que se aplica nesse cenário, pois estamos comparando os valores de "elementos construtores de valor de linha".

ii) "X <> Y" é verdadeiro se e somente se XV e YV não forem iguais.

Basicamente, X <> Yé verdade que os valores representados por X e Y não são iguais. Como X < Y OR X > Yé uma reescrita logicamente equivalente desse predicado, é totalmente legal para o otimizador usá-lo.

O padrão não impõe nenhuma restrição a essa definição relacionada à determinística (ou seja o que for que você entende) dos elementos construtores do valor da linha em ambos os lados do <>operador de comparação. É responsabilidade do código do usuário lidar com o fato de que uma expressão de valor de um lado pode ser não determinística.

Josh Darnell
fonte
11
Vou recusar a votação (para cima ou para baixo), mas não estou convencido. As aspas que você fornece mencionam "valor" . Meu entendimento é que a comparação é entre dois valores, um de cada lado. Não entre duas (ou mais) instanciações de um valor em cada lado. Além disso, o padrão (pelo menos os 92 citados) não menciona todas as funções não determinísticas. Por um raciocínio semelhante ao seu, podemos assumir que um produto SQL que esteja em conformidade com o padrão não forneça nenhuma função não determinística, mas apenas as mencionadas no padrão.
precisa saber é o seguinte
@ yper obrigado pelo feedback! Eu acho que sua interpretação é definitivamente válida. Esta é a primeira vez que li esse documento. Ele está mencionando valores no contexto do valor representado por um "construtor de valor de linha", que em outras partes do documento pode ser uma subconsulta escalar (entre muitas outras coisas). A subconsulta escalar em particular parece que pode ser não determinística. Mas eu realmente não sei o que eu estou falando =)
Josh Darnell