SQL Server divide A <> B in A <B OR A> B, producendo strani risultati se B non è deterministico


26

Abbiamo riscontrato un problema interessante con SQL Server. Considera il seguente esempio di riproduzione:

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

Per favore, dimentica per un momento che la s_guid <> NEWID()condizione sembra del tutto inutile - questo è solo un esempio di riproduzione minima. Poiché la probabilità di NEWID()abbinare un dato valore costante è estremamente ridotta, dovrebbe sempre essere VERO.

Ma non lo fa. L'esecuzione di questa query di solito restituisce 1 riga, ma a volte (abbastanza frequentemente, più di 1 volta su 10) restituisce 0 righe. L'ho riprodotto con SQL Server 2008 sul mio sistema e puoi riprodurlo online con il violino collegato sopra (SQL Server 2014).

L'esame del piano di esecuzione rivela che l'analizzatore di query suddivide apparentemente la condizione in s_guid < NEWID() OR s_guid > NEWID():

screenshot del piano di query

... il che spiega completamente perché a volte fallisce (se il primo ID generato è più piccolo e il secondo più grande dell'ID specificato).

SQL Server può valutare A <> Bcome A < B OR A > B, anche se una delle espressioni non è deterministica? Se sì, dove è documentato? O abbiamo trovato un bug?

È interessante notare che AND NOT (s_guid = NEWID())produce lo stesso piano di esecuzione (e lo stesso risultato casuale).

Abbiamo riscontrato questo problema quando uno sviluppatore voleva facoltativamente escludere una determinata riga e ha utilizzato:

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

come "scorciatoia" per:

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

Sto cercando documentazione e / o conferma di un bug. Il codice non è poi così rilevante, quindi non sono necessarie soluzioni alternative.


Risposte:


22

SQL Server può valutare A <> Bcome A < B OR A > B, anche se una delle espressioni non è deterministica?

Questo è un punto alquanto controverso e la risposta è un "sì" qualificato.

La migliore discussione di cui sono a conoscenza è stata data in risposta alla segnalazione di bug di Itzik Ben-Gan su Bug con NEWID ed espressioni di tabella , che è stata chiusa perché non risolvibile. Da allora Connect è stato ritirato, quindi il collegamento è a un archivio web. Purtroppo, un sacco di materiale utile è stato perso (o reso più difficile da trovare) dalla fine di Connect. Ad ogni modo, le citazioni più utili di Jim Hogg di Microsoft ci sono:

Ciò colpisce il cuore del problema: l'ottimizzazione è autorizzata a modificare la semantica di un programma? Vale a dire: se un programma fornisce determinate risposte, ma viene eseguito lentamente, è legittimo per uno Strumento per ottimizzare le query rendere quel programma più veloce, ma anche cambiare i risultati forniti?

Prima di gridare "NO!" (Anche la mia inclinazione personale :-), considera: la buona notizia è che, nel 99% dei casi, le risposte SONO le stesse. Quindi l'ottimizzazione delle query è una chiara vittoria. La cattiva notizia è che, se la query contiene codice con effetti collaterali, piani diversi possono effettivamente produrre risultati diversi. E NEWID () è una di queste "funzioni" con effetti collaterali (non deterministici) che espone la differenza. [In realtà, se si sperimenta, è possibile escogitare altri - ad esempio, la valutazione di cortocircuito delle clausole AND: fare in modo che la seconda clausola divida per zero una divisione aritmetica - diverse ottimizzazioni possono eseguire quella seconda clausola PRIMA della prima clausola] Ciò riflette Spiegazione di Craig, altrove in questo thread, che SqlServer non garantisce quando vengono eseguiti operatori scalari.

Quindi, abbiamo una scelta: se vogliamo garantire un determinato comportamento in presenza di codice non deterministico (ad effetto collaterale) - in modo che i risultati dei JOIN, ad esempio, seguano la semantica di un'esecuzione a ciclo nidificato - allora noi può utilizzare OPZIONI appropriate per forzare quel comportamento - come sottolinea UC. Ma il codice risultante funzionerà lentamente - questo è il costo, in effetti, di ottimizzare lo Strumento per ottimizzare le query.

Detto questo, stiamo spostando lo Strumento per ottimizzare le query nella direzione del comportamento "come previsto" per NEWID (), scambiando le prestazioni per "risultati come previsto".

Un esempio del cambiamento di comportamento in questo senso nel tempo è che NULLIF funziona in modo errato con funzioni non deterministiche come RAND () . Esistono anche altri casi simili che utilizzano, ad esempio, COALESCEcon una sottoquery che può produrre risultati imprevisti e che vengono anche affrontati gradualmente.

Jim continua:

Chiusura del ciclo. . . Ho discusso questa domanda con il team Dev. E alla fine abbiamo deciso di non modificare il comportamento attuale, per i seguenti motivi:

1) L'ottimizzatore non garantisce tempi o numero di esecuzioni di funzioni scalari. Questo è un principio di lunga data. È il "margine di manovra" fondamentale che consente all'ottimizzatore abbastanza libertà per ottenere miglioramenti significativi nell'esecuzione del piano di query.

2) Questo "comportamento una volta per fila" non è un nuovo problema, sebbene non sia ampiamente discusso. Abbiamo iniziato a modificare il suo comportamento nella versione Yukon. Ma è abbastanza difficile stabilire con precisione, in tutti i casi, esattamente cosa significa! Ad esempio, si applica alle righe intermedie calcolate "lungo la strada" per il risultato finale? - nel qual caso dipende chiaramente dal piano scelto. O si applica solo alle righe che appariranno alla fine nel risultato completato? - c'è una brutta ricorsione in corso qui, poiché sono sicuro che sarai d'accordo!

3) Come accennato in precedenza, per impostazione predefinita "ottimizziamo le prestazioni", il che è positivo per il 99% dei casi. L'1% dei casi in cui potrebbe cambiare i risultati è abbastanza facile da individuare - "funzioni" con effetti collaterali come NEWID - e facili da "correggere" (il trading di perf, di conseguenza). L'impostazione predefinita per "ottimizzare le prestazioni" è di nuovo consolidata e accettata. (Sì, non è la posizione scelta dai compilatori per i linguaggi di programmazione convenzionali, ma così sia).

Quindi, i nostri consigli sono:

a) Evitare di fare affidamento su tempi non garantiti e semantica del numero di esecuzioni. b) Evitare di utilizzare NEWID () in profondità nelle espressioni di tabella. c) Usa OPTION per forzare un comportamento particolare (trading perf)

Spero che questa spiegazione ci aiuti a chiarire le nostre ragioni per chiudere questo bug come "non risolverà".


È interessante notare che AND NOT (s_guid = NEWID())produce lo stesso piano di esecuzione

Questa è una conseguenza della normalizzazione, che si verifica molto presto durante la compilazione delle query. Entrambe le espressioni vengono compilate esattamente nella stessa forma normalizzata, quindi viene prodotto lo stesso piano di esecuzione.


In questo caso, se vogliamo forzare un piano particolare che sembra evitare il problema, possiamo usare WITH (FORCESCAN). Per essere sicuri, dovremmo usare una variabile per memorizzare il risultato di NEWID () prima di eseguire la query.
Razvan Socol,

11

Questo è documentato (una specie di) qui:

Il numero di volte in cui una funzione specificata in una query viene effettivamente eseguita può variare tra i piani di esecuzione creati dall'ottimizzatore. Un esempio è una funzione richiamata da una sottoquery in una clausola WHERE. Il numero di volte in cui viene eseguita la query secondaria e la sua funzione può variare a seconda dei percorsi di accesso scelti dall'ottimizzatore.

Funzioni definite dall'utente

Questo non è l'unico modulo di query in cui il piano di query eseguirà NEWID () più volte e modificherà il risultato. Questo è confuso, ma in realtà è fondamentale per NEWID () per essere utile per la generazione di chiavi e l'ordinamento casuale.

La cosa più confusione è che non tutte le funzioni non-deterministiche in realtà si comportano come questo. Ad esempio RAND () e GETDATE () verranno eseguiti una sola volta per query.


Esistono post di blog o simili che spiegano perché / quando il motore convertirà "non è uguale" in un intervallo?
Mago Magoo,

3
Non che io sappia. Può essere di routine perché =, <e >può essere valutato in modo efficiente rispetto a un BTree.
David Browne - Microsoft

5

Per quello che vale, se guardi questo vecchio documento standard di SQL 92 , i requisiti sulla disuguaglianza sono descritti nella sezione " 8.2 <comparison predicate>" come segue:

1) Sia X e Y due qualsiasi <elemento del costruttore del valore di riga> s corrispondenti. Sia XV e YV i valori rappresentati da X e Y, rispettivamente.

[...]

ii) "X <> Y" è vero se e solo se XV e YV non sono uguali.

[...]

7) Sia Rx e Ry i due <costruttori del valore di riga> s del <predicato di confronto> e sia RXi e RYi i-i <elementi del costruttore del valore di riga> s rispettivamente di Rx e Ry. "Rx <comp op> Ry" è vero, falso o sconosciuto come segue:

[...]

b) "x <> Ry" è vero se e solo se RXi <> RYi per alcuni i.

[...]

h) "x <> Ry" è falso se e solo se "Rx = Ry" è vero.

Nota: ho incluso 7b e 7h per completezza poiché parlano di <>confronto - non penso che il confronto di costruttori di valori di riga con più valori sia implementato in T-SQL, a meno che non stia semplicemente fraintendendo in modo massiccio ciò che dice - il che è del tutto possibile

Questo è un mucchio di immondizia confusa. Ma se vuoi continuare a immergerti nel cassonetto ...

Io penso che 1.II è l'elemento che si applica in questo scenario, dal momento che stiamo confrontando i valori di "elementi costruttore valore di riga."

ii) "X <> Y" è vero se e solo se XV e YV non sono uguali.

Fondamentalmente sta dicendo che X <> Yè vero se i valori rappresentati da X e Y non sono uguali. Poiché X < Y OR X > Yè una riscrittura logicamente equivalente di quel predicato, è assolutamente fantastico che l'ottimizzatore lo usi.

Lo standard non pone alcun vincolo su questa definizione in relazione alla determinismo (o qualunque altra cosa, si ottiene) degli elementi del costruttore del valore di riga su entrambi i lati <>dell'operatore di confronto. È responsabilità del codice utente gestire il fatto che un'espressione di valore su un lato potrebbe non essere deterministica.


1
Mi limiterò a votare (su o giù) ma non sono convinto. Le citazioni fornite indicano "valore" . La mia comprensione è che il confronto è tra due valori, uno su ciascun lato. Non tra due (o più) istanze di un valore su ciascun lato. Inoltre, lo standard (almeno il 92 che citi) non menziona tutte le funzioni non deterministiche. Con un ragionamento simile al tuo, possiamo supporre che un prodotto SQL conforme allo standard non fornisca alcuna funzione non deterministica ma solo quelle menzionate nello standard.
ypercubeᵀᴹ

@per grazie per il feedback! Penso che la tua interpretazione sia decisamente valida. Questa è la prima volta che leggo quel documento. Parla di valori nel contesto del valore rappresentato da un "costruttore di valori di riga", che altrove nel documento che dice può essere una subquery scalare (tra le altre cose). La subquery scalare in particolare sembra che potrebbe essere non deterministica. Ma davvero non so di cosa sto parlando =)
Josh Darnell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.