it-swarm.com.de

Wie kann man EXISTS in mehreren Spalten effizient überprüfen?

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 besteht darin, zu bestimmen, ob eine der nullbaren Spalten B oder C tatsächlich NULL Werte enthält (und wenn ja, welche).

Nehmen Sie außerdem an, dass die Tabelle Millionen von Zeilen enthält (und dass keine Spaltenstatistiken verfügbar sind, die angezeigt 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, dies zu erreichen, aber alle haben Schwächen.

Zwei separate EXISTS -Anweisungen. Dies hätte den Vorteil, dass die Abfragen das Scannen vorzeitig beenden können, sobald eine NULL gefunden. Wenn jedoch beide Spalten tatsächlich keine NULLs enthalten, werden zwei vollständige Scans ausgeführt.

Einzelaggregatabfrage

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

Dies könnte beide Spalten gleichzeitig verarbeiten, so dass der schlimmste Fall eines vollständigen Scans vorliegt. Der Nachteil ist, dass selbst wenn in beiden Spalten sehr früh ein NULL auftritt, die Abfrage den gesamten Rest der Tabelle scannt.

Benutzervariablen

Ich kann denke an einen dritten Weg, 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 Produktionscode geeignet, da das korrekte Verhalten für eine aggregierte Verkettungsabfrage ist nicht definiert. und das Beenden des Scans durch Auslösen eines Fehlers ist ohnehin 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 Bezug auf die Lesevorgänge 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)       |
+----------+------------+------+---------+----------+----------------------+----------+------------------+

Für die Antwort von @ Thomas habe ich TOP 3 In TOP 2 Geändert, damit es möglicherweise früher beendet werden kann. Ich habe standardmäßig einen parallelen Plan für diese Antwort erhalten, habe es also auch mit einem MAXDOP 1 - Hinweis versucht, um die Anzahl der Lesevorgänge mit den anderen Plänen vergleichbarer zu machen. Ich war etwas überrascht von den Ergebnissen, 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

Shortcircuits

Der Plan für die Daten von ypercube lautet

Not Shortcircuit

Daher wird dem Plan ein blockierender Sortieroperator hinzugefügt. Ich habe es auch mit dem Hinweis HASH GROUP Versucht, aber am Ende werden immer noch alle Zeilen gelesen

Not Shortcircuit

Der Schlüssel scheint also darin zu bestehen, einen Operator hash match (flow distinct) zu erhalten, damit dieser Plan kurzgeschlossen werden kann, da die anderen Alternativen ohnehin alle Zeilen blockieren und verbrauchen. Ich glaube nicht, dass es einen Hinweis gibt, um dies spezifisch zu erzwingen, aber anscheinend "Im Allgemeinen wählt der Optimierer einen Flow Distinct, bei dem festgestellt wird, dass weniger Ausgabezeilen erforderlich sind, als unterschiedliche Werte im Eingabesatz vorhanden sind." =.

Die Daten von @ ypercube enthalten nur 1 Zeile in jeder Spalte mit NULL -Werten (Tabellenkardinalität = 30300), und die geschätzten Zeilen, die in den Operator ein- und ausgehen, sind beide 1. Indem das Prädikat für den Optimierer etwas undurchsichtiger gemacht wurde, wurde ein Plan mit dem Operator Flow Distinct erstellt.

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 möglicherweise immer noch mehr Zeilen als erforderlich verarbeitet, falls die erste Zeile, auf die sie mit einem NULL stößt, NULL-Werte in beiden Spalten B und enthält C. Der Scanvorgang wird fortgesetzt, anstatt sofort beendet zu werden. Eine Möglichkeit, dies zu vermeiden, besteht darin, die Zeilen beim Scannen zu entfernen. Meine letzte Änderung zu Antwort von Thomas Kejser ist 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, wenn das Prädikat WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL wäre, aber gegen die vorherigen Testdaten gibt man mir keinen Plan mit einem Flow Distinct, wohingegen der NullExists IS NOT NULL Dies tut (Plan unten) ).

Unpivoted

26
Martin Smith

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
20
Thomas Kejser

Wenn 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 Prüfstand mit SQL 2008 R2 und einer Million Zeilen wurden auf der Registerkarte Client-Statistiken 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) habe ich CHECKPOINT und DBCC DROPCLEANBUFFERS Ausgeführt.

Hier sind die Ergebnisse, wenn die Tabelle keine Nullen enthält. Beachten Sie, dass die von ypercube bereitgestellte 2-Lösung in Bezug auf Lesevorgänge und Ausführungszeit nahezu identisch mit meiner ist. Ich (wir) glauben, dass dies auf die Vorteile der Enterprise/Developer Edition mit Advanced Scanning zurückzuführen ist. Wenn Sie nur die Standard Edition oder niedriger verwenden, ist die Lösung von Kejser möglicherweise die schnellste Lösung.

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
6
Thomas

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

  • Die Abfrage EXISTS zeigt einen enormen Effizienzvorteil, wenn Nullen frühzeitig gefunden werden - was erwartet wird.
  • Mit der Abfrage EXISTS erhalte ich eine bessere Leistung - in allen Fällen im Jahr 2012, was ich nicht erklären kann.
  • In 2008R2 ist es langsamer als die beiden anderen Abfragen, wenn keine Nullen vorhanden sind. Je früher die Nullen gefunden werden, desto schneller wird es und wenn beide Spalten früh Nullen haben, ist es viel schneller als die anderen beiden Abfragen.
  • Die Abfrage von Thomas Kejser scheint 2012 leicht, aber konstant besser und 2008R2 schlechter zu sein als die Abfrage von Martin CASE.
  • Die Version 2012 scheint eine weitaus bessere Leistung zu haben. Dies hängt jedoch möglicherweise mit den Einstellungen der SQL-Fiddle-Server zusammen und nicht nur mit Verbesserungen des Optimierers.

Abfragen und Timings. Timings wo erledigt:

  • 1. ohne Nullen
  • 2. mit Spalte B mit einem NULL an einem kleinen id.
  • 3. Beide Spalten haben jeweils ein NULL bei kleinen IDs.

Los geht's (es gibt ein Problem mit den Plänen, ich werde es später noch einmal versuchen. Folgen Sie vorerst den Links):


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

Thomas Kejsers Abfrage

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 Abfrage EXISTS. Ich dachte, es wäre besser, wenn es keine Nullen gibt, 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 besser zu funktionieren als der vorherige Vorschlag, aber 2012 schlechter (vielleicht kann das 2. INSERT mit IF umgeschrieben werden, wie die Antwort von @ 8kb):

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

Sind IF Anweisungen 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      
4
8kb

Wenn Sie EXISTS verwenden, weiß SQL Server, dass Sie eine Existenzprüfung durchführen. Wenn der erste übereinstimmende Wert gefunden wird, wird TRUE zurückgegeben und die Suche beendet.

wenn Sie 2 Spalten zusammenfassen und 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
0
AmmarR