Czy robimy coś źle, czy jest to błąd SQL Server?
Jest to błąd nieprawidłowych wyników, który należy zgłosić za pośrednictwem zwykłego kanału wsparcia. Jeśli nie masz umowy o pomoc techniczną, możesz wiedzieć, że płatne incydenty są zwykle zwracane, jeśli Microsoft potwierdzi zachowanie jako błąd.
Błąd wymaga trzech składników:
- Zagnieżdżone pętle z zewnętrznym odniesieniem (zastosowanie)
- Leniwa szpula indeksu po wewnętrznej stronie, która szuka zewnętrznego odniesienia
- Wewnętrzny operator konkatenacji
Na przykład zapytanie w pytaniu tworzy plan podobny do następującego:
Istnieje wiele sposobów na usunięcie jednego z tych elementów, więc błąd nie będzie już odtwarzany.
Na przykład można utworzyć indeksy lub statystyki, co oznacza, że optymalizator zdecyduje się nie używać bufora Lazy Index. Lub można użyć podpowiedzi, aby wymusić mieszanie lub scalenie unii zamiast konkatenacji. Można również przepisać zapytanie, aby wyrazić tę samą semantykę, ale skutkuje to innym kształtem planu, w którym brakuje jednego lub więcej wymaganych elementów.
Więcej szczegółów
Leniwa szpula indeksu leniwie buforuje wewnętrzne rzędy wyników bocznych w tabeli roboczej indeksowanej wartościami odniesienia zewnętrznego (parametr skorelowany). Jeśli bufor Lazy Index zostanie zapytany o zewnętrzne odniesienie, które widział wcześniej, pobiera buforowany wiersz wyników ze swojej tabeli roboczej („przewijanie do tyłu”). Jeśli bufor zostanie zapytany o zewnętrzną wartość odniesienia, której nie widział wcześniej, uruchamia swoje poddrzewo z bieżącą zewnętrzną wartością odniesienia i buforuje wynik („ponowne powiązanie”). Predykat wyszukiwania w buforze indeksu Lazy wskazuje klucze do tabeli roboczej.
Problem występuje w tym konkretnym kształcie planu, gdy szpula sprawdza, czy nowe odniesienie zewnętrzne jest takie samo jak wcześniej. Łączenie zagnieżdżonych pętli poprawnie aktualizuje swoje zewnętrzne odniesienia i powiadamia operatorów o swoich wewnętrznych danych wejściowych za PrepRecompute
pomocą metod interfejsu. Na początku tej kontroli operatorzy strony wewnętrznej czytają CParamBounds:FNeedToReload
właściwość, aby sprawdzić, czy odniesienie zewnętrzne uległo zmianie od ostatniego razu. Przykładowy ślad stosu pokazano poniżej:
Gdy przedstawione powyżej poddrzewo istnieje, szczególnie tam, gdzie jest używana konkatenacja, coś idzie nie tak (być może problem ByVal / ByRef / Copy) z powiązaniami, że CParamBounds:FNeedToReload
zawsze zwraca false, niezależnie od tego, czy zewnętrzne odniesienie rzeczywiście się zmieniło, czy nie.
Jeśli istnieje to samo poddrzewo, ale używana jest łączenie scalające lub łączenie mieszające, ta podstawowa właściwość jest ustawiana poprawnie przy każdej iteracji, a bufor Lazy Index jest odpowiednio przewijany lub wiązany ponownie za każdym razem. Nawiasem mówiąc, Distinct Sort i Stream Aggregate są nienaganne. Podejrzewam, że Merge i Hash Union tworzą kopię poprzedniej wartości, podczas gdy Concatenation używa odwołania. Niestety sprawdzenie tego bez dostępu do kodu źródłowego SQL Server jest prawie niemożliwe.
Wynik netto jest taki, że szpula indeksu Lazy w problematycznym kształcie planu zawsze myśli, że już widziała bieżące zewnętrzne odniesienie, przewija się, szukając do swojej tabeli roboczej, na ogół nie znajduje niczego, więc żaden wiersz nie jest zwracany dla tego zewnętrznego odniesienia. Przechodząc przez wykonanie w debuggerze, bufor wykonuje tylko swoją RewindHelper
metodę, a nigdy swoją ReloadHelper
metodę (reload = rebind w tym kontekście). Jest to widoczne w planie wykonania, ponieważ wszyscy operatorzy w buforze mają „Liczba wykonań = 1”.
Wyjątek stanowią oczywiście pierwsze zewnętrzne odwołania, które podano jako bufor Lazy Index. To zawsze wykonuje poddrzewo i buforuje wiersz wyników w tabeli roboczej. Wszystkie kolejne iteracje powodują przewijanie do tyłu, co spowoduje wygenerowanie wiersza (pojedynczego wiersza w pamięci podręcznej), gdy bieżąca iteracja będzie miała taką samą wartość dla odniesienia zewnętrznego, jak za pierwszym razem.
Tak więc dla każdego zestawu danych wejściowych po zewnętrznej stronie łączenia zagnieżdżonych pętli zapytanie zwróci tyle wierszy, ile przetworzonych duplikatów pierwszego wiersza (plus jeden dla samego pierwszego wiersza oczywiście).
Próbny
Tabela i przykładowe dane:
CREATE TABLE #T1
(
pk integer IDENTITY NOT NULL,
c1 integer NOT NULL,
CONSTRAINT PK_T1
PRIMARY KEY CLUSTERED (pk)
);
GO
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6);
Poniższe (trywialne) zapytanie generuje poprawną liczbę dwóch dla każdego wiersza (łącznie 18) za pomocą scalania:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C;
Jeśli teraz dodamy wskazówkę dotyczącą zapytania w celu wymuszenia konkatenacji:
SELECT T1.c1, C.c1
FROM #T1 AS T1
CROSS APPLY
(
SELECT COUNT_BIG(*) AS c1
FROM
(
SELECT T1.c1
UNION
SELECT NULL
) AS U
) AS C
OPTION (CONCAT UNION);
Plan wykonania ma problematyczny kształt:
Wynik jest teraz niepoprawny, tylko trzy rzędy:
Chociaż takie zachowanie nie jest gwarantowane, pierwszy wiersz ze skanowania indeksów klastrowych ma c1
wartość 1. Istnieją dwa inne wiersze o tej wartości, więc w sumie powstają trzy wiersze.
Teraz obetnij tabelę danych i załaduj ją z większą liczbą duplikatów „pierwszego” wiersza:
TRUNCATE TABLE #T1;
INSERT #T1 (c1)
VALUES
(1), (2), (3), (4), (5), (6),
(1), (2), (3), (4), (5), (6),
(1), (1), (1), (1), (1), (1);
Teraz plan konkatenacji to:
I, jak wskazano, powstaje 8 rzędów, wszystkie c1 = 1
oczywiście:
Zauważyłem, że otworzyłeś element Connect dla tego błędu, ale tak naprawdę nie jest to miejsce do zgłaszania problemów, które mają wpływ na produkcję. W takim przypadku naprawdę powinieneś skontaktować się z pomocą techniczną Microsoft.
Ten błąd błędnych wyników został naprawiony na pewnym etapie. Nie jest już dla mnie odtwarzany w żadnej wersji SQL Server od 2012 roku. Wykonuje repozytorium na SQL Server 2008 R2 SP3-GDR kompilacja 10.50.6560.0 (X64).