SQL Server nieprzewidywalne wyniki wyboru (błąd dbms?)

37

Poniżej znajduje się prosty przykład, który zwraca dziwne wyniki, które są nieprzewidywalne i nie możemy wyjaśnić tego w naszym zespole. Czy robimy coś źle, czy jest to błąd SQL Server?

Po pewnym badaniu zmniejszyliśmy obszar wyszukiwania do klauzuli unii w podzapytaniu , która wybiera jeden rekord z tabeli „men”

Działa zgodnie z oczekiwaniami w SQL Server 2000 (zwraca 12 wierszy), ale w 2008 i 2012 zwraca tylko jeden wiersz.

create table dual (dummy int)

insert into dual values (0)

create table men (
man_id int,
wife_id int )

-- there are 12 men, 6 married 
insert into men values (1, 1)
insert into men values (2, 2)
insert into men values (3, null)
insert into men values (4, null)
insert into men values (5, null)
insert into men values (6, 3)
insert into men values (7, 5)
insert into men values (8, 7)
insert into men values (9, null)
insert into men values (10, null)
insert into men values (11, null)
insert into men values (12, 9)

Zwraca tylko jeden wiersz: 1 1 2

select 
man_id,
wife_id,
(select count( * ) from 
    (select dummy from dual
     union select men.wife_id  ) family_members
) as family_size
from men
--where wife_id = 2 -- uncomment me and try again

Odkomentuj ostatnią linię i daje: 2 2 2

Istnieje wiele dziwnych zachowań:

  • Po serii kropli, tworzy, obcina i wstawia do tabeli „mężczyzn” czasami działa (zwraca 12 wierszy)
  • Po zmianie „union select men.wife_id” na „union all select men.wife_id” lub „union select isnull (men.wife_id, null)” (!!!) zwraca 12 wierszy (zgodnie z oczekiwaniami).
  • Dziwne zachowanie wydaje się nie mieć związku z typem danych kolumny „wife_id”. Zaobserwowaliśmy to w systemie programistycznym ze znacznie większymi zestawami danych.
  • „gdzie żona_id> 0” zwraca 6 wierszy
  • obserwujemy również dziwne zachowanie poglądów z tego rodzaju stwierdzeniami. SELECT * zwraca podzbiór wierszy, SELECT TOP 1000 zwraca wszystkie
Ryszard Bocian
źródło

Odpowiedzi:

35

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:

  1. Zagnieżdżone pętle z zewnętrznym odniesieniem (zastosowanie)
  2. Leniwa szpula indeksu po wewnętrznej stronie, która szuka zewnętrznego odniesienia
  3. Wewnętrzny operator konkatenacji

Na przykład zapytanie w pytaniu tworzy plan podobny do następującego:

Plan z adnotacjami

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 PrepRecomputepomocą metod interfejsu. Na początku tej kontroli operatorzy strony wewnętrznej czytają CParamBounds:FNeedToReloadwłaściwość, aby sprawdzić, czy odniesienie zewnętrzne uległo zmianie od ostatniego razu. Przykładowy ślad stosu pokazano poniżej:

CParamBounds: FNeedToReload

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:FNeedToReloadzawsze 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ą RewindHelpermetodę, a nigdy swoją ReloadHelpermetodę (reload = rebind w tym kontekście). Jest to widoczne w planie wykonania, ponieważ wszyscy operatorzy w buforze mają „Liczba wykonań = 1”.

RewindHelper

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;

Scal plan unijny

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:

Plan konkatenacji

Wynik jest teraz niepoprawny, tylko trzy rzędy:

Wynik z trzech rzędów

Chociaż takie zachowanie nie jest gwarantowane, pierwszy wiersz ze skanowania indeksów klastrowych ma c1wartość 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:

8-rzędowy plan konkatenacji

I, jak wskazano, powstaje 8 rzędów, wszystkie c1 = 1oczywiście:

Wynik z 8 wierszy

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).

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

Dlaczego używasz podzapytania bez instrukcji from? Myślę, że może to spowodować różnicę w serwerach z 2005 i 2008 roku. Może mógłbyś użyć jawnego przyłączenia?

select 
m1.man_id,
m1.wife_id,
(select count( * ) from 
    (select dummy from dual
     union
     select m2.wife_id
     from men m2
     where m2.man_id = m1.man_id) family_members
) as family_size
from men m1

źródło
3
Tak, to działa, ale moja wersja też powinna działać. Powyżej abstrakcyjnego przykładu znajduje się znacznie uproszczona wersja naszego zapytania produkcyjnego, co ma o wiele większy sens.