Come controllare in modo efficiente EXISTS su più colonne?


26

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

Corto circuiti

Il piano per i dati di ypercube è

Non cortocircuito

Quindi aggiunge un operatore di ordinamento bloccante al piano. Ho anche provato con il HASH GROUPsuggerimento, ma quello finisce per leggere tutte le righe

Non cortocircuito

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).

pivot

Risposte:


20

Che ne dite di:

SELECT TOP 3 *
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 T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT

Mi piace questo approccio. Tuttavia, ci sono alcuni possibili problemi che affronro nelle modifiche alla mia domanda. Come scritto TOP 3potrebbe essere proprio TOP 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 un hash match (flow distinct)operatore anziché hash match (aggregate)odistinct sort
Martin Smith,

6

Come capisco la domanda, vuoi sapere se esiste uno null in uno qualsiasi dei valori delle colonne invece di restituire effettivamente le righe in cui B o C è null. In tal caso, perché no:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

Sul mio banco di prova con SQL 2008 R2 e un milione di righe, ho ottenuto i seguenti risultati in ms dalla scheda Statistiche client:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Se aggiungi il suggerimento nolock, i risultati sono ancora più veloci:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Per riferimento ho usato il generatore SQL di Red-gate per generare i dati. Sul mio milione di righe, 9.886 righe avevano un valore B nullo e 10.019 avevano un valore C null.

In questa serie di test, ogni riga nella colonna B ha un valore:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Prima di ogni test (entrambi i set) ho corso CHECKPOINTe DBCC DROPCLEANBUFFERS.

Ecco i risultati quando non ci sono valori null nella tabella. Si noti che le 2 soluzioni fornite da ypercube sono quasi identiche alle mie in termini di letture e tempi di esecuzione. Ritengo che ciò sia dovuto ai vantaggi dell'edizione Enterprise / Developer che utilizza Scansione avanzata . Se stavi utilizzando solo la versione Standard o precedente, la soluzione di Kejser potrebbe essere la soluzione più veloce.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278

4

Sono IFconsentite dichiarazioni?

Ciò dovrebbe consentire di confermare l'esistenza di B o C in un passaggio attraverso la tabella:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      

4

Testato in SQL-Fiddle nelle versioni: 2008 r2 e 2012 con 30K righe.

  • La EXISTSquery mostra un enorme vantaggio in termini di efficienza quando trova Nulls in anticipo, il che è previsto.
  • Ottengo prestazioni migliori con la EXISTSquery, in tutti i casi nel 2012, che non posso spiegare.
  • Nel 2008R2, quando non ci sono Null, è più lento delle altre 2 query. Più presto trova i Null, più veloce diventa e quando entrambe le colonne hanno nulli in anticipo, è molto più veloce delle altre 2 query.
  • La query di Thomas Kejser sembra funzionare leggermente ma costantemente meglio nel 2012 e peggio nel 2008R2, rispetto alla CASEquery di Martin .
  • La versione 2012 sembra avere prestazioni molto migliori. Potrebbe avere a che fare con le impostazioni dei server SQL-Fiddle e non solo con i miglioramenti dell'ottimizzatore.

Domande e tempistiche. Tempi dove fatto:

  • 1 ° senza Null affatto
  • 2 ° con colonna Bavente uno NULLalla volta id.
  • 3 ° con entrambe le colonne che ne hanno una NULLciascuna con ID piccoli.

Eccoci (c'è un problema con i piani, ci riproverò più tardi. Segui i link per ora):


Interrogazione con 2 sottoquery EXISTS

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Query aggregata singola di Martin Smith

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 test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

La domanda di Thomas Kejser

SELECT TOP 3 *
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 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Il mio consiglio (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Ha bisogno di una certa lucidatura sull'output ma l'efficienza è simile alla EXISTSquery. Ho pensato che sarebbe meglio quando non ci sono valori null ma i test mostrano che non lo è.


Suggerimento (2)

Cercare di semplificare la logica:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Sembra funzionare meglio nel 2008R2 rispetto al suggerimento precedente ma peggio nel 2012 (forse il 2 ° INSERTpuò essere riscritto usando IF, come la risposta di @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29

0

Quando si utilizza EXISTS, SQL Server sa che si sta eseguendo un controllo di esistenza. Quando trova il primo valore corrispondente, restituisce VERO e smette di cercare.

quando concatichi 2 colonne e se nessuna è nulla, il risultato sarà null

per esempio

null + 'a' = null

quindi controlla questo codice

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null

-3

Che ne dite di:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Se funziona (non l'ho provato), si otterrebbe una tabella a una riga con 2 colonne, ognuna VERA o FALSA. Non ho testato l'efficienza.


2
Anche se questo è valido in qualsiasi altro DBMS, dubito che abbia la semantica corretta. Supponendo che T.B is nullsia trattato come un risultato booleano allora EXISTS(SELECT true)e EXISTS(SELECT false)entrambi tornerebbero veri. Questo esempio di MySQL indica che entrambe le colonne contengono NULL quando nessuno dei due effettivamente lo fa
Martin Smith
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.