Wie kann EXISTS für mehrere Spalten effizient überprüft werden?

26

Dies ist ein Problem, auf das ich regelmäßig stoße und für das ich noch keine gute Lösung gefunden habe.

Angenommen, die folgende Tabellenstruktur

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

und die Anforderung ist , ob eine der beiden Spalten auf NULL festlegbare , um zu bestimmen Boder Ctatsächlich jegliche enthalten NULLWerte (und wenn ja , welche (s)).

Nehmen Sie außerdem an, die Tabelle enthält Millionen von Zeilen (und es sind keine Spaltenstatistiken verfügbar, die überprüft werden könnten, da ich an einer allgemeineren Lösung für diese Klasse von Abfragen interessiert bin).

Ich kann mir ein paar Möglichkeiten vorstellen, wie ich das angehen könnte, aber alle haben Schwächen.

Zwei getrennte EXISTSAussagen. Dies hätte den Vorteil, dass die Abfragen den Scanvorgang vorzeitig beenden können, sobald ein NULLgefunden wird. Aber wenn beide Spalten tatsächlich keine NULLs enthalten, ergeben sich zwei vollständige Scans.

Einzelne aggregierte Abfrage

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

Dadurch können beide Spalten gleichzeitig verarbeitet werden, sodass der schlimmste Fall ein vollständiger Scan ist. Der Nachteil besteht darin, dass NULLdie Abfrage auch dann den gesamten Rest der Tabelle durchsucht , wenn sie sehr früh auf ein in beiden Spalten stößt .

Benutzervariablen

Ich kann mir einen dritten Weg vorstellen, dies zu tun

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)

Dies ist jedoch nicht für Seriencode geeignet, da das richtige Verhalten für eine Abfrage der aggregierten Verkettung undefiniert ist. und das Beenden des Scans durch einen Fehler ist sowieso eine schreckliche Lösung.

Gibt es eine andere Option, die die Stärken der oben genannten Ansätze kombiniert?

Bearbeiten

Nur um dies mit den Ergebnissen zu aktualisieren, die ich in Form von Lesevorgängen für die bisher eingereichten Antworten erhalte (unter Verwendung der Testdaten von @ 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Bei der Antwort von @ Thomas habe ich mich zu geändert TOP 3, TOP 2um möglicherweise zuzulassen, dass es früher beendet wird. Ich habe standardmäßig einen parallelen Plan für diese Antwort erhalten, also habe ich es auch mit einem MAXDOP 1Hinweis versucht , um die Anzahl der Lesevorgänge mit den anderen Plänen vergleichbarer zu machen. Die Ergebnisse überraschten mich ein wenig, da ich in meinem früheren Test diesen Abfragekurzschluss gesehen hatte, ohne die gesamte Tabelle zu lesen.

Der Plan für meine Testdaten, die kurzschließen, ist unten

Kurzschlüsse

Der Plan für die Daten von ypercube lautet

Kein Kurzschluss

Daher wird dem Plan ein blockierender Sortieroperator hinzugefügt. Ich habe auch versucht, mit dem HASH GROUPHinweis, aber das endet immer noch alle Zeilen zu lesen

Kein Kurzschluss

Es scheint also der Schlüssel zu sein, einen hash match (flow distinct)Operator dazu zu bringen, diesen Plan kurzschließen zu lassen, da die anderen Alternativen sowieso alle Zeilen blockieren und verbrauchen. Ich glaube nicht, dass es einen Hinweis gibt, dies speziell zu erzwingen, aber anscheinend "wählt das Optimierungsprogramm im Allgemeinen einen Flow Distinct, bei dem bestimmt wird, dass weniger Ausgabezeilen erforderlich sind, als unterschiedliche Werte in der Eingabemenge vorhanden sind.".

Die Daten von @ypercube enthalten in jeder Spalte nur eine Zeile mit NULLWerten (Tabellenkardinalität = 30300), und die geschätzten Zeilen, die in den Operator hinein- und aus ihm herausgehen, sind beide 1. Indem das Prädikat für das Optimierungsprogramm etwas undurchsichtiger gemacht wurde, wurde ein Plan mit dem Flow Distinct-Operator generiert.

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 

Bearbeiten 2

Eine letzte Änderung, die mir einfiel, war, dass die obige Abfrage NULLmöglicherweise immer noch mehr Zeilen als erforderlich verarbeitet, falls die erste Zeile, auf die sie trifft, in beiden Spalten Bund NULL enthält C. Der Scanvorgang wird fortgesetzt und nicht sofort beendet. Eine Möglichkeit, dies zu vermeiden, besteht darin, die Zeilen beim Scannen zu deaktivieren. Mein letzter Änderungsantrag zu Thomas Kejsers Antwort ist also unten

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

Es wäre wahrscheinlich besser für das Prädikat, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLaber gegen die vorherigen Testdaten, wenn man mir keinen Plan mit Flow Distinct gibt, wohingegen NullExists IS NOT NULLderjenige (Plan unten) es tut.

Nicht gedreht

Martin Smith
quelle

Antworten:

20

Wie wäre es mit:

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
Thomas Kejser
quelle
Ich mag diesen Ansatz. Es gibt jedoch einige mögliche Probleme, die ich bei der Bearbeitung meiner Frage anspreche. Wie geschrieben TOP 3könnte nur sein , TOP 2wie zur Zeit wird es scannen , bis es einen von jedem der folgenden findet (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). (NULL,NULL)Zwei von diesen drei wären ausreichend - und wenn der erste gefunden wird, wird auch der zweite nicht benötigt. Auch um den Plan kurzzuschließen, müsste der Unterschied hash match (flow distinct)eher über einen Operator als über hash match (aggregate)oder implementiert werdendistinct sort
Martin Smith
6

Wie ich die Frage verstehe, möchten Sie wissen, ob in einem der Spaltenwerte eine Null vorhanden ist, anstatt tatsächlich die Zeilen zurückzugeben, in denen entweder B oder C Null ist. Wenn dies der Fall ist, warum nicht:

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

Auf meinem Teststand mit SQL 2008 R2 und einer Million Zeilen wurden auf der Registerkarte Client Statistics die folgenden Ergebnisse in ms angezeigt:

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

Wenn Sie den Nolock-Hinweis hinzufügen, sind die Ergebnisse noch schneller:

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

Als Referenz habe ich den SQL-Generator von Red-gate verwendet, um die Daten zu generieren. Von meinen einer Million Zeilen hatten 9.886 Zeilen einen Null-B-Wert und 10.019 einen Null-C-Wert.

In dieser Testreihe hat jede Zeile in Spalte B einen Wert:

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

Vor jedem Test (beide Sätze) lief ich CHECKPOINTund DBCC DROPCLEANBUFFERS.

Hier sind die Ergebnisse, wenn die Tabelle keine Nullen enthält. Beachten Sie, dass die von ypercube bereitgestellte 2-Exists-Lösung in Bezug auf Lesevorgänge und Ausführungszeit nahezu identisch mit der von mir ist. Ich (wir) glaube, dies liegt an den Vorteilen der Enterprise / Developer Edition, die Advanced Scanning verwendet . Wenn Sie nur die Standard Edition oder eine niedrigere Version verwenden, ist die Lösung von Kejser möglicherweise die schnellste.

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
Thomas
quelle
4

Sind IFAussagen erlaubt?

Dies sollte es Ihnen ermöglichen, das Vorhandensein von B oder C bei einem Durchgang durch die Tabelle zu bestätigen:

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      
8kb
quelle
4

Getestet in SQL-Fiddle in den Versionen: 2008 r2 und 2012 mit 30K Zeilen.

  • Die EXISTSAbfrage zeigt einen enormen Effizienzvorteil, wenn Nullen frühzeitig gefunden werden - was zu erwarten ist.
  • Ich bekomme eine bessere Leistung mit der EXISTSAbfrage - in allen Fällen im Jahr 2012, was ich nicht erklären kann.
  • In 2008R2 ist es langsamer als die anderen 2 Abfragen, wenn es keine Nullen gibt. Je früher es die Nullen findet, desto schneller wird es und wenn beide Spalten früh Nullen haben, ist es viel schneller als die anderen 2 Abfragen.
  • Die Abfrage von Thomas Kejser scheint im Jahr 2012 leicht, aber konstant besser und im Jahr 2008R2 schlechter zu sein als die von Martin CASE .
  • Die Version 2012 scheint eine weitaus bessere Leistung zu haben. Dies hat möglicherweise mit den Einstellungen der SQL-Fiddle-Server zu tun und nicht nur mit Verbesserungen am Optimierer.

Abfragen und Timings. Timings wo getan:

  • Erster ohne Nullen
  • 2. Spalte Bmit einer NULLkleinenid .
  • 3. Beide Spalten haben NULLjeweils eine kleine ID.

Los geht's (es gibt ein Problem mit den Plänen, ich versuche es später noch einmal. Folgen Sie den Links fürs Erste):


Abfrage mit 2 EXISTS-Unterabfragen

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

Martin Smiths Single Aggregate Query

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

Abfrage von 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

Mein Vorschlag (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

Die Ausgabe muss etwas poliert werden, aber die Effizienz ähnelt der EXISTSAbfrage. Ich dachte, es wäre besser, wenn es keine Nullen gäbe, aber Tests zeigen, dass dies nicht der Fall ist.


Vorschlag (2)

Der Versuch, die Logik zu vereinfachen:

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 ;

Es scheint 2008R2 eine bessere Leistung zu bringen als der vorherige Vorschlag, 2012 jedoch eine schlechtere (möglicherweise kann die zweite INSERTmit IF@ 8kb umgeschrieben werden ):

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

Wenn Sie EXISTS verwenden, weiß SQL Server, dass Sie eine Existenzprüfung durchführen. Wenn es den ersten passenden Wert findet, gibt es TRUE zurück und hört auf zu suchen.

Wenn Sie 2 Spalten verketten und wenn eine null ist, ist das Ergebnis null

z.B

null + 'a' = null

Überprüfen Sie diesen Code

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
AmmarR
quelle
-3

Wie wäre es mit:

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

Wenn dies funktioniert (ich habe es nicht getestet), würde es eine einzeilige Tabelle mit 2 Spalten ergeben, von denen jede entweder WAHR oder FALSCH ist. Ich habe die Effizienz nicht getestet.

David Horowitz
quelle
2
Auch wenn dies in einem anderen DBMS gültig ist, bezweifle ich, dass es die richtige Semantik hat. Angenommen, dies T.B is nullwird dann als boolesches Ergebnis behandelt EXISTS(SELECT true)und EXISTS(SELECT false)würde beide true zurückgeben. Dieses MySQL-Beispiel zeigt an, dass beide Spalten NULL enthalten,
Martin Smith,