Woher kommen dieser konstante Scan und der linke äußere Join in einem einfachen SELECT-Abfrageplan?

21

Ich habe diesen Tisch:

CREATE TABLE [dbo].[Accounts] (
    [AccountId] UNIQUEIDENTIFIER UNIQUE NOT NULL DEFAULT NEWID(),
    -- WHATEVER other columns
);
GO
CREATE UNIQUE CLUSTERED INDEX [AccountsIndex]
    ON [dbo].[Accounts]([AccountId] ASC);
GO

Diese Abfrage:

DECLARE @result UNIQUEIDENTIFIER
SELECT @result = AccountId FROM Accounts WHERE AccountId='guid-here'

Wird mit einem Abfrageplan ausgeführt, der aus einem einzelnen Index-Suchvorgang besteht - wie erwartet:

SELECT <---- Clustered Index Seek

Diese Abfrage bewirkt dasselbe:

DECLARE @result UNIQUEIDENTIFIER
SET @result = (SELECT AccountId FROM Accounts WHERE AccountId='guid-here')

Es wird jedoch mit einem Plan ausgeführt, bei dem das Ergebnis der Indexsuche nach außen verschoben wird. Als Ergebnis wird ein konstanter Scan durchgeführt und dann in Compute Scalar eingespeist:

SELECT <--- Compute Scalar <--- Left Outer Join <--- Constant Scan
                                      ^
                                      |------Clustered Index Seek

Was ist diese zusätzliche Magie? Was bewirkt dieser konstante Scan, gefolgt von Left Outer Join?

scharfer Zahn
quelle

Antworten:

29

Die Semantik der beiden Aussagen ist unterschiedlich:

  • Das erste setzt den Wert der Variablen nicht, wenn keine Zeile gefunden wird.
  • Die zweite Option setzt die Variable immer auf null, auch wenn keine Zeile gefunden wird.

Der konstante Scan erzeugt eine leere Zeile (ohne Spalten!), Die dazu führt, dass die Variable aktualisiert wird, falls nichts mit der Basistabelle übereinstimmt. Der linke Join stellt sicher, dass die leere Zeile den Join überlebt. Man kann sich vorstellen, dass die Variablenzuweisung am Stammknoten des Ausführungsplans stattfindet.

Verwenden SELECT @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result does not change
SELECT @result = AccountId 
FROM Accounts 
WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'};

SELECT @result;

Ergebnis 1

Verwenden SET @result

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
(
    SELECT AccountId 
    FROM Accounts 
    WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'}
);

SELECT @result;

Ergebnis 2

Ausführungspläne

SELECT ZuordnungAm Wurzelknoten kommt keine Zeile an, daher erfolgt keine Zuweisung.

SET ZuordnungEine Zeile kommt immer am Wurzelknoten an, sodass eine variable Zuordnung erfolgt.


Die zusätzlichen Funktionen Constant Scan und Nested Loops Left Outer Join sind kein Grund zur Sorge. Insbesondere der Join ist kostengünstig, da er am äußeren Eingang garantiert auf eine Zeile und am inneren Eingang (in Ihrem Beispiel) auf höchstens eine Zeile trifft.

Es gibt andere Möglichkeiten, um sicherzustellen, dass eine Zeile aus der Unterabfrage generiert wird, um sicherzustellen, dass eine Variablenzuweisung erfolgt. Eine Möglichkeit besteht darin, ein redundantes Skalaraggregat zu verwenden (keine Gruppenklausel):

-- Set initial value
DECLARE @result uniqueidentifier = {guid 'FE2CA909-1162-4C6C-A7AC-33B257E28539'};

-- @result set to null
SET @result = 
    (
        SELECT MAX(AccountId)
        FROM Accounts 
        WHERE AccountId={guid '7AD4D33C-1ED7-4183-B7F3-48C33D666525'} 
    );
SELECT @result;

Ergebnis 3

Ausführungsplan für skalare Aggregate

Beachten Sie, dass das Skalaraggregat eine Zeile erzeugt, obwohl es keine Eingabe empfängt.

Dokumentation:

Wenn die SELECT-Anweisung keine Zeilen zurückgibt, behält die Variable ihren aktuellen Wert bei. Wenn expression eine skalare Unterabfrage ist, die keinen Wert zurückgibt, wird die Variable auf NULL gesetzt.

Für die Zuweisung von Variablen empfehlen wir, SET @local_variable anstelle von SELECT @local_variable zu verwenden.

Weitere Lektüre:

Paul White sagt GoFundMonica
quelle