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 Bo Ceffettivamente contenere alcun NULLvalore (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 EXISTSdichiarazioni separate . Ciò avrebbe il vantaggio di consentire alle query di interrompere la scansione non appena NULLviene trovata una. Ma se entrambe le colonne in realtà non contengono NULLs, 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 NULLmolto 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 3per TOP 2consentirgli di uscire prima. Per impostazione predefinita, ho ricevuto un piano parallelo per quella risposta, quindi l'ho provato anche con un MAXDOP 1suggerimento 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 GROUPsuggerimento, 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 NULLvalori (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 NULLabbia un NULL in entrambe le colonne Be 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 NULLma rispetto ai precedenti dati di test che uno non mi dà un piano con un Flow Distinct, mentre NullExists IS NOT NULLquello lo fa (piano sotto).

TOP 3potrebbe essere proprioTOP 2come 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