Questo è un problema che mi viene incontro periodicamente e non ho ancora trovato una buona soluzione.
Supponendo la seguente struttura della tabella
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
e il requisito è determinare se una delle colonne nullable B
o C
effettivamente contenere alcun NULL
valore (e in tal caso quale (i)).
Supponiamo inoltre che la tabella contenga milioni di righe (e che non siano disponibili statistiche di colonne che potrebbero essere visualizzate in quanto sono interessato a una soluzione più generica per questa classe di query).
Mi vengono in mente alcuni modi per affrontarlo, ma tutti hanno punti deboli.
Due EXISTS
dichiarazioni separate . Ciò avrebbe il vantaggio di consentire alle query di interrompere la scansione non appena NULL
viene trovata una. Ma se entrambe le colonne in realtà non contengono NULL
s, ne risulteranno due scansioni complete.
Query aggregata singola
SELECT
MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
Questo potrebbe elaborare entrambe le colonne contemporaneamente, quindi avere il caso peggiore di una scansione completa. Lo svantaggio è che anche se si incontra NULL
molto presto in una delle due colonne della query, la scansione finirà comunque per scansionare l'intero resto della tabella.
Variabili utente
Io riesco a pensare ad un terzo modo di fare questo
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT
@B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
@C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
/*Divide by zero error if both @B and @C are 1.
Might happen next row as no guarantee of order of
assignments*/
@D = 1 / (2 - (@B + @C))
FROM T
OPTION (MAXDOP 1)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
BEGIN
SELECT 'B,C both contain NULLs'
RETURN;
END
ELSE
RETURN;
END CATCH
SELECT ISNULL(@B,0),
ISNULL(@C,0)
ma questo non è adatto per il codice di produzione poiché il comportamento corretto per una query di concatenazione aggregata non è definito. e terminare la scansione lanciando un errore è comunque una soluzione piuttosto orribile.
C'è un'altra opzione che combina i punti di forza degli approcci sopra?
modificare
Solo per aggiornare questo con i risultati che ottengo in termini di letture per le risposte inviate finora (usando i dati di test di @ ypercube)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | 2 * EXISTS | CASE | Kejser | Kejser | Kejser | ypercube | 8kb |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | | | | MAXDOP 1 | HASH GROUP, MAXDOP 1 | | |
| No Nulls | 15208 | 7604 | 8343 | 7604 | 7604 | 15208 | 8346 (8343+3) |
| One Null | 7613 | 7604 | 8343 | 7604 | 7604 | 7620 | 7630 (25+7602+3) |
| Two Null | 23 | 7604 | 8343 | 7604 | 7604 | 30 | 30 (18+12) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Per la risposta di @Thomas ho cambiato TOP 3
per TOP 2
consentirgli di uscire prima. Per impostazione predefinita, ho ricevuto un piano parallelo per quella risposta, quindi l'ho provato anche con un MAXDOP 1
suggerimento per rendere il numero di letture più paragonabile agli altri piani. Sono stato un po 'sorpreso dai risultati, poiché nel mio test precedente avevo visto quel corto circuito di query senza leggere l'intera tabella.
Il piano per i miei dati di test secondo cui i cortocircuiti è inferiore
Il piano per i dati di ypercube è
Quindi aggiunge un operatore di ordinamento bloccante al piano. Ho anche provato con il HASH GROUP
suggerimento, ma quello finisce per leggere tutte le righe
Quindi la chiave sembra essere quella di convincere un hash match (flow distinct)
operatore a consentire questo piano in corto circuito poiché le altre alternative bloccheranno e consumeranno comunque tutte le file. Non credo che ci sia un suggerimento per forzare questo in modo specifico ma apparentemente "in generale, l'ottimizzatore sceglie un Flow Distinct dove determina che sono necessarie meno righe di output rispetto a valori distinti nel set di input". .
I dati di @ ypercube hanno solo 1 riga in ogni colonna con NULL
valori (cardinalità tabella = 30300) e le righe stimate in entrata e in uscita dall'operatore sono entrambe 1
. Rendendo il predicato un po 'più opaco per l'ottimizzatore, ha generato un piano con l'operatore Flow Distinct.
SELECT TOP 2 *
FROM (SELECT DISTINCT
CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
, CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
FROM test T
WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT
Modifica 2
Un ultimo accenno che mi è venuto in mente è che la query sopra potrebbe finire per elaborare più righe del necessario nel caso in cui la prima riga che incontra NULL
abbia un NULL in entrambe le colonne B
e C
. Continuerà la scansione anziché uscire immediatamente. Un modo per evitarlo sarebbe di annullare la rotazione delle righe durante la scansione. Quindi la mia ultima modifica alla risposta di Thomas Kejser è di seguito
SELECT DISTINCT TOP 2 NullExists
FROM test T
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
(CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
Probabilmente sarebbe meglio per il predicato essere, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL
ma rispetto ai precedenti dati di test che uno non mi dà un piano con un Flow Distinct, mentre NullExists IS NOT NULL
quello lo fa (piano sotto).
TOP 3
potrebbe essere proprioTOP 2
come attualmente si esegue una scansione finché non trova uno di ciascuno dei seguenti(NOT_NULL,NULL)
,(NULL,NOT_NULL)
,(NULL,NULL)
. Qualsiasi 2 su quei 3 sarebbe sufficiente - e se trova(NULL,NULL)
prima, non sarebbe nemmeno necessario il secondo. Anche per cortocircuitare il piano dovrebbe implementare il distinto tramite unhash match (flow distinct)
operatore anzichéhash match (aggregate)
odistinct sort