Skąd bierze się to skanowanie ciągłe i lewy zewnętrzny łącznik w trywialnym planie zapytań SELECT?

21

Mam ten stół:

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

To zapytanie:

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

wykonuje się z planem zapytań składającym się z pojedynczego wyszukiwania indeksowego - zgodnie z oczekiwaniami:

SELECT <---- Clustered Index Seek

To zapytanie robi to samo:

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

ale jest wykonywany zgodnie z planem, w którym wynik szukania indeksu jest łączony z lewej strony z wynikiem jakiegoś stałego skanowania, a następnie podawany do skalera obliczeniowego:

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

Co to za dodatkowa magia? Co robi to skanowanie ciągłe, a następnie lewy zewnętrzny łącznik?

sharptooth
źródło

Odpowiedzi:

29

Semantyka obu instrukcji jest inna:

  • Pierwszy nie ustawia wartości zmiennej, jeśli nie znaleziono wiersza.
  • Drugi zawsze ustawia zmienną, w tym na null, jeśli nie znaleziono wiersza.

Ciągłe skanowanie tworzy pusty wiersz (bez kolumn!), Co spowoduje aktualizację zmiennej, na wypadek gdyby nic nie pasowało z tabeli podstawowej. Lewe złączenie zapewnia, że ​​pusty wiersz przetrwa złączenie. Przypisanie zmiennej można traktować jako działanie w węźle głównym planu wykonania.

Za pomocą 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;

Wynik 1

Za pomocą 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;

Wynik 2

Plany wykonania

WYBIERZ przydziałŻaden wiersz nie dociera do węzła głównego, więc nie następuje przypisanie.

Przypisanie SETWiersz zawsze dociera do węzła głównego, więc następuje przypisanie zmiennej.


Dodatkowe stałe skanowanie i zagnieżdżone pętle pozostawione przy zewnętrznym złączeniu nie stanowią powodu do niepokoju. Zwłaszcza połączenie jest tanie, ponieważ gwarantuje napotkanie jednego rzędu na wejściu zewnętrznym i co najwyżej jednego rzędu (w twoim przykładzie) na wejściu wewnętrznym.

Istnieją inne sposoby zapewnienia generowania wiersza z podzapytania w celu zapewnienia przypisania zmiennej. Jednym z nich jest użycie redundantnego agregatu skalarnego (bez grupowania według klauzul):

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

Wynik 3

Skalarny plan wykonania zagregowanego

Zauważ, że agregat skalarny tworzy wiersz, nawet jeśli nie otrzymuje żadnych danych wejściowych.

Dokumentacja:

Jeśli instrukcja SELECT nie zwraca wierszy, zmienna zachowuje swoją bieżącą wartość. Jeśli wyrażenie jest skalarnym podzapytaniem, które nie zwraca żadnej wartości, zmienna jest ustawiona na NULL.

Do przypisywania zmiennych zalecamy użycie SET @ zmienna_lokalna zamiast SELECT @ zmienna_lokalna.

Dalsza lektura:

Paul White mówi GoFundMonica
źródło