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

Nieprawidłowe wyniki przy łączeniu scalanym

Każdy produkt ma błędy, a SQL Server nie jest wyjątkiem. Korzystanie z funkcji produktu w nieco nietypowy sposób (lub łączenie ze sobą stosunkowo nowych funkcji) to świetny sposób na ich znalezienie. Błędy mogą być interesujące, a nawet pouczające, ale być może część radości zostanie utracona, gdy odkrycie skutkuje uruchomieniem pagera o 4 nad ranem, być może po szczególnie towarzyskim wieczorze z przyjaciółmi…

Błąd, który jest tematem tego posta, jest prawdopodobnie dość rzadki w środowisku naturalnym, ale nie jest to klasyczny przypadek. Znam przynajmniej jednego konsultanta, który spotkał się z nim w systemie produkcyjnym. W zupełnie niezwiązanym temacie powinienem skorzystać z okazji, aby przywitać się ze zrzędliwym starym DBA (blog).

Zacznę od pewnego istotnego tła na temat łączenia przez scalanie. Jeśli jesteś pewien, że wiesz już wszystko o łączeniu przez scalanie lub po prostu chcesz przejść do sedna, przewiń w dół do sekcji zatytułowanej „Błąd”.

Scal Dołącz

Połączenie scalające nie jest strasznie skomplikowaną rzeczą i może być bardzo skuteczne w odpowiednich okolicznościach. Wymaga, aby jego dane wejściowe były posortowane według kluczy łączenia i działa najlepiej w trybie jeden-do-wielu (gdzie przynajmniej jego dane wejściowe są unikatowe w kluczach łączenia). W przypadku łączeń jeden-do-wielu o średniej wielkości, łączenie szeregowe nie jest wcale złym wyborem, pod warunkiem, że można spełnić wymagania sortowania danych wejściowych bez przeprowadzania sortowania jawnego.

Unikanie sortowania jest najczęściej osiągane przez wykorzystanie kolejności zapewnianej przez indeks. Sprzężenie scalające może również korzystać z zachowanej kolejności sortowania z wcześniejszego, nieuniknionego sortowania. Fajną rzeczą w łączeniu przez scalanie jest to, że może przestać przetwarzać wiersze wejściowe, gdy tylko jeden z nich zabraknie wierszy. Ostatnia rzecz:merge join nie ma znaczenia, czy kolejność sortowania danych wejściowych jest rosnąca czy malejąca (chociaż oba dane wejściowe muszą być takie same). Poniższy przykład wykorzystuje standardową tabelę liczb do zilustrowania większości powyższych punktów:

CREATE TABLE #T1 (col1 integer CONSTRAINT PK1 PRIMARY KEY (col1 DESC));
CREATE TABLE #T2 (col1 integer CONSTRAINT PK2 PRIMARY KEY (col1 DESC));
 
INSERT #T1 SELECT n FROM dbo.Numbers WHERE n BETWEEN 10000 AND 19999;
INSERT #T2 SELECT n FROM dbo.Numbers WHERE n BETWEEN 18000 AND 21999;

Zauważ, że indeksy wymuszające klucze podstawowe w tych dwóch tabelach są zdefiniowane jako malejące. Plan zapytań dla INSERT ma wiele interesujących funkcji:

Czytając od lewej do prawej (co jest tylko rozsądne!) Wstawka indeksu klastrowego ma ustawioną właściwość „Sortowanie żądań DML”. Oznacza to, że operator wymaga wierszy w kolejności klucza Clustered Index. Indeks klastrowy (wymuszanie w tym przypadku klucza podstawowego) jest zdefiniowany jako DESC , więc wiersze z wyższymi wartościami muszą dotrzeć jako pierwsze. Indeks klastrowy w mojej tabeli Numbers to ASC , dzięki czemu optymalizator zapytań unika jawnego sortowania, szukając najpierw najwyższego dopasowania w tabeli Numbers (21 999), a następnie skanując w kierunku najniższego dopasowania (18 000) w odwrotnej kolejności indeksów. Widok „Drzewo planów” w SQL Sentry Plan Explorer wyraźnie pokazuje odwrotne (do tyłu) skanowanie:

Skanowanie wstecz odwraca naturalną kolejność indeksu. Skanowanie wstecz ASC klucz indeksu zwraca wiersze w malejącej kolejności kluczy; skanowanie wstecz DESC klucz indeksu zwraca wiersze w rosnącej kolejności kluczy. „Kierunek skanowania” nie wskazuje sam w sobie kolejności zwracanych kluczy – musisz wiedzieć, czy indeks to ASC lub DESC aby podjąć tę decyzję.

Korzystanie z tych tabel testowych i danych (T1 ma 10 000 wierszy ponumerowanych od 10 000 do 19 999 włącznie; T2 ma 4000 wierszy ponumerowanych od 18 000 do 21 999) następujące zapytanie łączy ze sobą dwie tabele i zwraca wyniki w kolejności malejącej obu kluczy:

SELECT
    T1.col1,
    T2.col1
FROM #T1 AS T1 
JOIN #T2 AS T2 
    ON T2.col1 = T1.col1 
ORDER BY 
    T1.col1 DESC, 
    T2.col1 DESC;

Zapytanie zwraca prawidłowe pasujące 2000 wierszy, zgodnie z oczekiwaniami. Plan powykonawczy jest następujący:

Łączenie scalające nie działa w trybie wiele do wielu (górne dane wejściowe są unikatowe w kluczach łączenia), a oszacowanie kardynalności 2000 wierszy jest dokładnie poprawne. Skanowanie indeksu klastrowego tabeli T2 jest uporządkowana (chociaż musimy chwilę poczekać, aby dowiedzieć się, czy jest to kolejność do przodu, czy do tyłu), a oszacowanie kardynalności na 4000 wierszy jest również dokładne. Skanowanie indeksu klastrowego tabeli T1 jest również uporządkowany, ale odczytano tylko 2001 wierszy, podczas gdy oszacowano 10 tys. Widok drzewa planu pokazuje, że oba skanowania indeksów klastrowych są uporządkowane do przodu:

Przypomnij sobie czytanie DESC index FORWARD utworzy wiersze w odwrotnej kolejności kluczy. Dokładnie tego wymaga ORDER BY T1.col DESC, T2.col1 DESC klauzula, więc nie jest konieczne jawne sortowanie. Pseudo-kod dla łączenia scalania jeden-do-wielu (odtworzony z bloga Craig Freedman's Merge Join) to:

Skanowanie w kolejności malejącej T1 zwraca wiersze zaczynające się od 19 999 i pracujące w dół do 10 000. Skanowanie malejącej kolejności T2 zwraca wiersze zaczynające się od 21 999 i pracujące w dół do 18 000. Wszystkie 4000 wierszy w T2 zostaną ostatecznie odczytane, ale iteracyjny proces scalania zatrzymuje się, gdy wartość klucza 17,999 zostanie odczytana z T1 , ponieważ T2 zabraknie rzędów. Przetwarzanie scalania kończy się zatem bez pełnego odczytu T1 . Czyta wiersze od 19999 do 17999 włącznie; łącznie 2001 wierszy, jak pokazano w powyższym planie wykonania.

Zachęcamy do ponownego uruchomienia testu za pomocą ASC zamiast tego indeksy, zmieniając także ORDER BY klauzula z DESC do ASC . Opracowany plan wykonania będzie bardzo podobny i nie będą potrzebne żadne rodzaje.

Podsumowując punkty, które będą ważne za chwilę, Merge Join wymaga danych wejściowych posortowanych według klucza łączenia, ale nie ma znaczenia, czy klucze są posortowane rosnąco czy malejąco.

Błąd

Aby odtworzyć błąd, przynajmniej jedna z naszych tabel musi zostać podzielona na partycje. Aby utrzymać wyniki w zarządzaniu, ten przykład użyje tylko niewielkiej liczby wierszy, więc funkcja partycjonowania również potrzebuje małych granic:

CREATE PARTITION FUNCTION PF (integer)
AS RANGE RIGHT
FOR VALUES (5, 10, 15);
 
CREATE PARTITION SCHEME PS
AS PARTITION PF
ALL TO ([PRIMARY]);


Pierwsza tabela zawiera dwie kolumny i jest podzielona na partycje według klucza podstawowego:

CREATE TABLE dbo.T1
(
    T1ID    integer IDENTITY (1,1) NOT NULL,
    SomeID  integer NOT NULL,
 
    CONSTRAINT [PK dbo.T1 T1ID]
        PRIMARY KEY CLUSTERED (T1ID)
        ON PS (T1ID)
);


Druga tabela nie jest podzielona na partycje. Zawiera klucz podstawowy i kolumnę, która zostanie dołączona do pierwszej tabeli:

CREATE TABLE dbo.T2
(
    T2ID    integer IDENTITY (1,1) NOT NULL,
    T1ID    integer NOT NULL,
 
    CONSTRAINT [PK dbo.T2 T2ID]
        PRIMARY KEY CLUSTERED (T2ID)
        ON [PRIMARY]
);

Przykładowe dane

Pierwsza tabela ma 14 wierszy, wszystkie z tą samą wartością w SomeID kolumna. SQL Server przypisuje IDENTITY wartości kolumn, ponumerowane od 1 do 14.

INSERT dbo.T1
    (SomeID)
VALUES
    (123), (123), (123),
    (123), (123), (123),
    (123), (123), (123),
    (123), (123), (123),
    (123), (123);


Druga tabela jest po prostu wypełniona IDENTITY wartości z pierwszej tabeli:

INSERT dbo.T2 (T1ID)
SELECT T1ID
FROM dbo.T1;

Dane w dwóch tabelach wyglądają tak:

Zapytanie testowe

Pierwsze zapytanie po prostu łączy obie tabele, stosując pojedynczy predykat klauzuli WHERE (co w tym bardzo uproszczonym przykładzie pasuje do wszystkich wierszy):

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123;

Wynik zawiera wszystkie 14 wierszy, zgodnie z oczekiwaniami:

Ze względu na małą liczbę wierszy optymalizator wybiera plan łączenia zagnieżdżonych pętli dla tego zapytania:

Wyniki są takie same (i nadal poprawne), jeśli wymusimy połączenie haszujące lub scalające:

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (HASH JOIN);
 
SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Połączenie scalające istnieje od jednego do wielu, z jawnym sortowaniem według T1ID wymagane dla tabeli T2 .

Problem malejącego indeksu

Wszystko jest w porządku, aż pewnego dnia (z ważnych powodów, które nie muszą nas tutaj dotyczyć) inny administrator doda malejący indeks do SomeID kolumna tabeli 1:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC);


Nasze zapytanie nadal daje poprawne wyniki, gdy optymalizator wybiera zagnieżdżone pętle lub sprzężenie haszujące, ale to zupełnie inna historia, gdy używane jest sprzężenie scalające. W poniższym przykładzie nadal zastosowano wskazówkę dotyczącą zapytania, aby wymusić sprzężenie scalające, ale jest to tylko konsekwencja małej liczby wierszy w przykładzie. Optymalizator naturalnie wybrałby ten sam plan łączenia łączenia z różnymi danymi tabeli.

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Plan wykonania to:

Optymalizator zdecydował się na użycie nowego indeksu, ale zapytanie generuje teraz tylko pięć wierszy danych wyjściowych:

Co się stało z pozostałymi 9 rzędami? Żeby było jasne, ten wynik jest błędny. Dane się nie zmieniły, więc wszystkie 14 wierszy powinno zostać zwróconych (ponieważ nadal są z planem zagnieżdżonych pętli lub Hash Join).

Przyczyna i wyjaśnienie

Nowy indeks nieklastrowy w SomeID nie jest zadeklarowana jako unikatowa, więc klucz indeksu klastrowego jest dyskretnie dodawany do wszystkich poziomów indeksu nieklastrowanego. SQL Server dodaje T1ID kolumna (klucz klastrowy) do indeksu nieklastrowanego, tak jakbyśmy utworzyli indeks w następujący sposób:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC, T1ID);


Zwróć uwagę na brak DESC kwalifikator na dodanym dyskretnie T1ID klucz. Klucze indeksu to ASC domyślnie. To nie jest problem sam w sobie (choć przyczynia się). Drugą rzeczą, która dzieje się automatycznie z naszym indeksem, jest to, że jest on podzielony na partycje w taki sam sposób, jak tabela podstawowa. Tak więc pełna specyfikacja indeksu, gdybyśmy mieli napisać to wprost, wyglądałaby następująco:

CREATE NONCLUSTERED INDEX [dbo.T1 SomeID]
ON dbo.T1 (SomeID DESC, T1ID ASC)
ON PS (T1ID);


Teraz jest to dość złożona struktura z kluczami w różnych porządkach. Jest to wystarczająco skomplikowane, aby optymalizator zapytań popełnił błąd podczas wnioskowania o kolejności sortowania zapewnianej przez indeks. Aby to zilustrować, rozważ następujące proste zapytanie:

SELECT 
    T1ID,
    PartitionID = $PARTITION.PF(T1ID)
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID ASC;

Dodatkowa kolumna pokaże nam, do której partycji należy bieżący wiersz. W przeciwnym razie jest to po prostu proste zapytanie, które zwraca T1ID wartości w porządku rosnącym, WHERE SomeID = 123 . Niestety wyniki nie są zgodne z zapytaniem:

Zapytanie wymaga, aby T1ID wartości powinny być zwracane w porządku rosnącym, ale to nie jest to, co otrzymujemy. Otrzymujemy wartości w kolejności rosnącej na partycję , ale same partycje są zwracane w odwrotnej kolejności! Jeśli partycje zostały zwrócone w kolejności rosnącej (oraz T1ID wartości pozostały posortowane w ramach każdej partycji, jak pokazano), wynik byłby poprawny.

Plan zapytania pokazuje, że optymalizator został zdezorientowany przez wiodący DESC klucza indeksu i pomyślał, że aby uzyskać poprawne wyniki, należy odczytać partycje w odwrotnej kolejności:

Wyszukiwanie partycji rozpoczyna się od skrajnej prawej partycji (4) i przechodzi wstecz do partycji 1. Można by pomyśleć, że możemy rozwiązać problem przez jawne sortowanie według numeru partycji ASC w ORDER BY klauzula:

SELECT 
    T1ID,
    PartitionID = $PARTITION.PF(T1ID)
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    PartitionID ASC, -- New!
    T1ID ASC;

To zapytanie zwraca te same wyniki (to nie jest błąd drukarski ani błąd kopiowania/wklejania):

Identyfikator partycji jest nadal malejący kolejność (nie rosnąca, jak określono) i T1ID jest sortowane tylko rosnąco w obrębie każdej partycji. Takie jest zamieszanie optymalizatora, naprawdę myśli (weź głęboki oddech), że skanowanie indeksu z kluczem wiodącym i malejącym podzielonym na partycje w kierunku do przodu, ale z odwróconymi partycjami, da w wyniku kolejność określoną przez zapytanie.

Nie obwiniam tego, żeby być szczerym, różne względy porządku sortowania również sprawiają, że boli mnie głowa.

Jako ostatni przykład rozważ:

SELECT 
    T1ID
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID DESC;

Wyniki to:

Ponownie T1ID porządek sortowania w każdej partycji prawidłowo maleje, ale same partycje są wymienione od tyłu (przechodzą od 1 do 3 w dół rzędów). Gdyby partycje zostały zwrócone w odwrotnej kolejności, wyniki byłyby prawidłowe:14, 13, 12, 11, 10, 9, … 5, 4, 3, 2, 1 .

Powrót do połączenia scalającego

Przyczyna nieprawidłowych wyników z zapytaniem Merge Join jest teraz oczywista:

SELECT
    T2.T2ID
FROM dbo.T1 AS T1
JOIN dbo.T2 AS T2
    ON T2.T1ID = T1.T1ID
WHERE
    T1.SomeID = 123
OPTION (MERGE JOIN);

Połączenie scalające wymaga posortowanych danych wejściowych. Dane wejściowe z T2 jest jawnie posortowany według T1TD więc to jest w porządku. Optymalizator błędnie twierdzi, że indeks na T1 może dostarczyć wiersze w T1ID zamówienie. Jak widzieliśmy, tak nie jest. Wyszukiwanie indeksu daje takie same wyniki, jak zapytanie, które już widzieliśmy:

SELECT 
    T1ID
FROM dbo.T1
WHERE
    SomeID = 123
ORDER BY
    T1ID ASC;

Tylko pierwszych 5 wierszy znajduje się w T1ID zamówienie. Następna wartość (5) z pewnością nie jest w porządku rosnącym, a łączenie scalające interpretuje to jako koniec strumienia, a nie powoduje błąd (osobiście spodziewałem się tutaj potwierdzenia sprzedaży detalicznej). W każdym razie efekt jest taki, że scalanie łączenia nieprawidłowo kończy przetwarzanie wcześnie. Przypominamy, że (niepełne) wyniki to:

Wniosek

Moim zdaniem jest to bardzo poważny błąd. Proste wyszukiwanie indeksu może zwrócić wyniki, które nie przestrzegają ORDER BY klauzula. Co więcej, wewnętrzne rozumowanie optymalizatora jest całkowicie zepsute dla partycjonowanych nieunikalnych indeksów nieklastrowanych z malejącym kluczem wiodącym.

Tak, to jest nieco nietypowa aranżacja. Ale, jak widzieliśmy, prawidłowe wyniki mogą zostać nagle zastąpione nieprawidłowymi wynikami tylko dlatego, że ktoś dodał malejący indeks. Pamiętaj, że dodany indeks wyglądał wystarczająco niewinnie:brak wyraźnego ASC/DESC niezgodność kluczy i brak wyraźnego partycjonowania.

Błąd nie ogranicza się do łączenia połączeń. Ofiarą może paść potencjalnie każde zapytanie, które obejmuje tabelę podzieloną na partycje i które opiera się na kolejności sortowania indeksu (jawne lub niejawne). Ten błąd występuje we wszystkich wersjach SQL Server od 2008 do 2014 CTP 1 włącznie. Baza danych Windows SQL Azure nie obsługuje partycjonowania, więc problem nie występuje. SQL Server 2005 używał innego modelu implementacji partycjonowania (na podstawie APPLY ) i nie cierpi z tego powodu.

Jeśli masz chwilę, rozważ zagłosowanie na mój element Connect w związku z tym błędem.

Rozdzielczość

Rozwiązanie tego problemu jest teraz dostępne i udokumentowane w artykule z bazy wiedzy. Pamiętaj, że poprawka wymaga aktualizacji kodu i flagi śledzenia 4199 , który umożliwia szereg innych zmian procesora zapytań. Rzadko zdarza się, aby błąd dotyczący nieprawidłowych wyników był naprawiany pod 4199. Poprosiłem o wyjaśnienie tego i odpowiedź brzmiała:

Mimo że ten problem dotyczy nieprawidłowych wyników, takich jak inne poprawki dotyczące procesora zapytań, włączono tę poprawkę tylko pod flagą śledzenia 4199 dla programu SQL Server 2008, 2008 R2 i 2012. Jednak ta poprawka jest „włączona” przez domyślnie bez flagi śledzenia w SQL Server 2014 RTM.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Minimalizowanie wpływu DBCC CHECKDB:DOS i DONT

  2. Błędy połączenia z bazą danych lub uwierzytelniania z ruchomym typem

  3. Zestaw problemów 1 – Identyfikacja jednostek

  4. Model bazy danych dla ankiety online. Część 3

  5. SQL Pivot – Dowiedz się, jak konwertować wiersze na kolumny