Database
 sql >> Baza danych >  >> RDS >> Database

Niektóre DOWOLNE przekształcenia zbiorcze są uszkodzone

ANY agregacja nie jest czymś, co możemy napisać bezpośrednio w Transact SQL. Jest to jedyna wewnętrzna funkcja używana przez optymalizator zapytań i silnik wykonywania.

Osobiście bardzo lubię ANY agregat, więc trochę rozczarowujące było, gdy dowiedziałem się, że jest zepsuty w dość fundamentalny sposób. Szczególny smak „zepsutego”, o którym tutaj mówię, to odmiana z błędnymi wynikami.

W tym poście przyjrzę się dwóm konkretnym miejscom, w których ANY często pojawia się agregacja, pokazuje problem z niewłaściwymi wynikami i sugeruje obejścia w razie potrzeby.

W tle na ANY zbiorczo, zobacz mój poprzedni post Plany nieudokumentowanych zapytań:DOWOLNE zagregowane.

1. Jeden wiersz na zapytania grupowe

To musi być jedno z najczęstszych codziennych wymagań dotyczących zapytań, z bardzo dobrze znanym rozwiązaniem. Prawdopodobnie piszesz tego rodzaju zapytania codziennie, automatycznie podążając za wzorcem, bez zastanowienia się nad tym.

Pomysł polega na numerowaniu wejściowego zestawu wierszy za pomocą ROW_NUMBER funkcja okna, podzielona według kolumny lub kolumn grupujących. To jest opakowane w wspólne wyrażenie tabelowe lub tabela pochodna i przefiltrowane do wierszy, w których obliczony numer wiersza jest równy jeden. Od ROW_NUMBER uruchamia się ponownie po jednym dla każdej grupy, co daje nam wymagany jeden wiersz na grupę.

Nie ma problemu z tym ogólnym wzorcem. Typ jednego wiersza na zapytanie grupowe, który podlega ANY zbiorczy problem to ten, w którym nie obchodzi nas, który konkretny wiersz jest zaznaczony z każdej grupy.

W takim przypadku nie jest jasne, która kolumna powinna być użyta w obowiązkowym ORDER BY klauzula ROW_NUMBER funkcja okna. W końcu wyraźnie nie obchodzi nas to który wiersz jest wybrany. Jednym z powszechnych rozwiązań jest ponowne użycie PARTITION BY kolumny w ORDER BY klauzula. Tutaj może wystąpić problem.

Przykład

Spójrzmy na przykład z wykorzystaniem zestawu danych zabawek:

CREATE TABLE #Data
(
    c1 integer NULL,
    c2 integer NULL,
    c3 integer NULL
);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, NULL, 1),
    (1, 1, NULL),
    (1, 111, 111),
    -- Group 2
    (2, NULL, 2),
    (2, 2, NULL),
    (2, 222, 222);

Wymagane jest zwrócenie dowolnego pełnego wiersza danych z każdej grupy, gdzie przynależność do grupy jest określona przez wartość w kolumnie c1 .

Po ROW_NUMBER możemy napisać zapytanie podobne do poniższego (zwróć uwagę na ORDER BY klauzula ROW_NUMBER funkcja okna pasuje do PARTITION BY klauzula):

WITH 
    Numbered AS 
    (
        SELECT 
            D.*, 
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM #Data AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

Jak przedstawiono, to zapytanie zostanie wykonane pomyślnie, z poprawnymi wynikami. Wyniki są technicznie niedeterministyczne ponieważ SQL Server może poprawnie zwrócić dowolny z wierszy w każdej grupie. Niemniej jednak, jeśli sam uruchomisz to zapytanie, prawdopodobnie zobaczysz ten sam wynik, co ja:

Plan wykonania zależy od używanej wersji SQL Server i nie zależy od poziomu zgodności bazy danych.

W przypadku SQL Server 2014 i wcześniejszych plan jest następujący:

W przypadku SQL Server 2016 lub nowszego zobaczysz:

Oba plany są bezpieczne, ale z różnych powodów. Odrębne sortowanie plan zawiera ANY zbiorcze, ale sortowanie odrębne implementacja operatora nie ujawnia błędu.

Bardziej złożony plan SQL Server 2016+ nie używa ANY w ogóle. Sortuj umieszcza wiersze w kolejności potrzebnej do operacji numerowania wierszy. Segment operator ustawia flagę na początku każdej nowej grupy. Projekt sekwencyjny oblicza numer wiersza. Wreszcie Filtr operator przekazuje tylko te wiersze, które mają obliczony numer wiersza jeden.

Błąd

Aby uzyskać nieprawidłowe wyniki dla tego zestawu danych, musimy używać programu SQL Server 2014 lub wcześniejszego oraz ANY agregaty muszą być zaimplementowane w Stream Aggregate lub gorliwy zagregowanie haszowania operator (Flow Distinct Hash Match Aggregate nie powoduje błędu).

Jednym ze sposobów zachęcenia optymalizatora do wybrania agregacji strumienia zamiast sortowania odrębnego jest dodanie klastrowego indeksu, aby zapewnić porządkowanie według kolumny c1 :

CREATE CLUSTERED INDEX c ON #Data (c1);

Po tej zmianie plan wykonania staje się:

ANY agregaty są widoczne w Właściwościach okno, gdy Stream Aggregate wybrany jest operator:

Wynik zapytania to:

To jest niewłaściwe . SQL Server zwrócił wiersze, które nie istnieją w danych źródłowych. Nie ma wierszy źródłowych, w których c2 = 1 i c3 = 1 na przykład. Przypominamy, że dane źródłowe to:

Plan wykonania błędnie oblicza oddzielnie ANY agregaty dla c2 i c3 kolumny, ignorując wartości null. Każdy agregat niezależnie zwraca pierwszy niepusty napotkaną wartość, dając wynik, w którym wartości dla c2 i c3 pochodzą z różnych wierszy źródłowych . Nie tego wymagała oryginalna specyfikacja zapytania SQL.

Ten sam błędny wynik można uzyskać z lub bez indeks klastrowy przez dodanie OPTION (HASH GROUP) wskazówka, aby stworzyć plan za pomocą Eager Hash Aggregate zamiast Stream Aggregate .

Warunki

Ten problem może wystąpić tylko wtedy, gdy wiele ANY agregaty są obecne, a zagregowane dane zawierają wartości null. Jak już wspomniano, problem dotyczy tylko Stream Aggregate i chętni Agregacja haszująca operatorzy; Odrębne sortowanie i Flow Distinct nie mają wpływu.

SQL Server 2016 i nowsze starają się uniknąć wprowadzania wielu ANY agreguje dla dowolnego jednego wiersza na grupę wzorca zapytania numeracji wierszy, gdy kolumny źródłowe dopuszczają wartość null. W takim przypadku plan wykonania będzie zawierał Segment , Projekt sekwencyjny i Filtr operatorów zamiast agregatu. Ten kształt planu jest zawsze bezpieczny, ponieważ nie ma ANY używane są agregaty.

Powielanie błędu w SQL Server 2016+

Optymalizator SQL Server nie jest doskonały w wykrywaniu, kiedy kolumna pierwotnie ograniczona jako NOT NULL może nadal generować zerową wartość pośrednią poprzez manipulacje danymi.

Aby to odtworzyć, zaczniemy od tabeli, w której wszystkie kolumny są zadeklarowane jako NOT NULL :

IF OBJECT_ID(N'tempdb..#Data', N'U') IS NOT NULL
BEGIN
    DROP TABLE #Data;
END;
 
CREATE TABLE #Data
(
    c1 integer NOT NULL,
    c2 integer NOT NULL,
    c3 integer NOT NULL
);
 
CREATE CLUSTERED INDEX c ON #Data (c1);
 
INSERT #Data
    (c1, c2, c3)
VALUES
    -- Group 1
    (1, 1, 1),
    (1, 2, 2),
    (1, 3, 3),
    -- Group 2
    (2, 1, 1),
    (2, 2, 2),
    (2, 3, 3);

Możemy tworzyć wartości null z tego zestawu danych na wiele sposobów, z których większość optymalizator może z powodzeniem wykryć, więc unikaj wprowadzania ANY agreguje podczas optymalizacji.

Poniżej pokazano jeden ze sposobów dodawania wartości zerowych, które prześlizgują się pod radarem:

SELECT
    D.c1,
    OA1.c2,
    OA2.c3
FROM #Data AS D
OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2;

To zapytanie daje następujące dane wyjściowe:

Następnym krokiem jest użycie tej specyfikacji zapytania jako danych źródłowych dla standardowego zapytania „dowolny jeden wiersz na grupę”:

WITH
    SneakyNulls AS 
    (
        -- Introduce nulls the optimizer can't see
        SELECT
            D.c1,
            OA1.c2,
            OA2.c3
        FROM #Data AS D
        OUTER APPLY (SELECT D.c2 WHERE D.c2 <> 1) AS OA1
        OUTER APPLY (SELECT D.c3 WHERE D.c3 <> 2) AS OA2
    ),
    Numbered AS 
    (
        SELECT
            D.c1,
            D.c2,
            D.c3,
            rn = ROW_NUMBER() OVER (
                PARTITION BY D.c1
                ORDER BY D.c1) 
        FROM SneakyNulls AS D
    )
SELECT
    N.c1, 
    N.c2, 
    N.c3
FROM Numbered AS N
WHERE
    N.rn = 1;

W dowolnej wersji SQL Server, który generuje następujący plan:

Agregacja strumienia zawiera wiele ANY agreguje, a wynik jest błędny . Żaden ze zwróconych wierszy nie pojawia się w zestawie danych źródłowych:

db<>fiddle online demo

Obejście

Jedynym w pełni niezawodnym obejściem, dopóki ten błąd nie zostanie naprawiony, jest uniknięcie wzorca, w którym ROW_NUMBER ma tę samą kolumnę w ORDER BY klauzula jak w PARTITION BY klauzula.

Kiedy nie obchodzi nas co z każdej grupy wybierany jest jeden wiersz, szkoda, że ​​ORDER BY klauzula jest w ogóle potrzebna. Jednym ze sposobów na obejście problemu jest użycie stałej czasu wykonywania, takiej jak ORDER BY @@SPID w funkcji okna.

2. Aktualizacja niedeterministyczna

Problem z wieloma ANY agregacje na danych wejściowych dopuszczających wartość null nie są ograniczone do jednego wiersza na wzorzec zapytania grupy. Optymalizator zapytań może wprowadzić wewnętrzny ANY agregować w wielu okolicznościach. Jednym z takich przypadków jest aktualizacja niedeterministyczna.

niedeterministyczny aktualizacja to sytuacja, w której instrukcja nie gwarantuje, że każdy wiersz docelowy zostanie zaktualizowany co najwyżej raz. Innymi słowy, istnieje wiele wierszy źródłowych dla co najmniej jednego wiersza docelowego. Dokumentacja wyraźnie ostrzega przed tym:

Zachowaj ostrożność podczas określania klauzuli FROM, aby podać kryteria operacji aktualizacji.
Wyniki instrukcji UPDATE są niezdefiniowane, jeśli instrukcja zawiera klauzulę FROM, która nie jest określona w taki sposób, że tylko jedna wartość jest dostępna dla każdego zaktualizowanego wystąpienia kolumny, czyli jest, jeśli instrukcja UPDATE nie jest deterministyczna.

Aby obsłużyć niedeterministyczną aktualizację, optymalizator grupuje wiersze według klucza (indeks lub RID) i stosuje ANY agregaty do pozostałych kolumn. Podstawowa idea polega na wybraniu jednego wiersza z wielu kandydatów i użyciu wartości z tego wiersza do przeprowadzenia aktualizacji. Istnieją oczywiste podobieństwa do poprzedniego ROW_NUMBER problem, więc nie jest niespodzianką, że dość łatwo jest zademonstrować nieprawidłową aktualizację.

W przeciwieństwie do poprzedniego problemu, SQL Server obecnie nie podejmuje żadnych specjalnych kroków aby uniknąć wielu ANY agreguje na kolumnach dopuszczających wartość null podczas wykonywania niedeterministycznej aktualizacji. Poniższe informacje odnoszą się zatem do wszystkich wersji SQL Server , w tym SQL Server 2019 CTP 3.0.

Przykład

DECLARE @Target table
(
    c1 integer PRIMARY KEY, 
    c2 integer NOT NULL, 
    c3 integer NOT NULL
);
 
DECLARE @Source table 
(
    c1 integer NULL, 
    c2 integer NULL, 
    c3 integer NULL, 
 
    INDEX c CLUSTERED (c1)
);
 
INSERT @Target 
    (c1, c2, c3) 
VALUES 
    (1, 0, 0);
 
INSERT @Source 
    (c1, c2, c3) 
VALUES 
    (1, 2, NULL),
    (1, NULL, 3);
 
UPDATE T
SET T.c2 = S.c2,
    T.c3 = S.c3
FROM @Target AS T
JOIN @Source AS S
    ON S.c1 = T.c1;
 
SELECT * FROM @Target AS T;

db<>fiddle online demo

Logicznie rzecz biorąc, ta aktualizacja powinna zawsze powodować błąd:tabela docelowa nie zezwala na wartości null w żadnej kolumnie. Niezależnie od tego, który pasujący wiersz zostanie wybrany z tabeli źródłowej, próba aktualizacji kolumny c2 lub c3 null musi wystąpią.

Niestety aktualizacja się powiodła, a ostateczny stan tabeli docelowej jest niezgodny z dostarczonymi danymi:

Zgłosiłem to jako błąd. Rozwiązaniem jest unikanie pisania niedeterministycznej UPDATE oświadczenia, więc ANY agregaty nie są potrzebne do rozwiązania niejednoznaczności.

Jak wspomniano, SQL Server może wprowadzić ANY agreguje w większej liczbie okoliczności niż w dwóch podanych tutaj przykładach. Jeśli tak się stanie, gdy zagregowana kolumna zawiera wartości null, istnieje możliwość uzyskania błędnych wyników.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak nie wywoływać natywnie skompilowanych procedur składowanych Hekaton

  2. Chętna szpula indeksująca i optymalizator

  3. FieldShield SDK

  4. Wewnętrzne elementy Z SZYFROWANIEM

  5. Stosowanie reguł terenowych za pomocą klasyfikacji