Jest to problem, z którym spotykam się okresowo i nie znalazłem jeszcze dobrego rozwiązania.
Zakładając następującą strukturę tabeli
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
a warunkiem jest ustalenie, czy któraś z zerowalnych kolumn B
lub C
faktycznie zawiera jakieś NULL
wartości (a jeśli tak, to które).
Załóżmy również, że tabela zawiera miliony wierszy (i że nie są dostępne żadne statystyki kolumn, które można by zajrzeć, ponieważ jestem zainteresowany bardziej ogólnym rozwiązaniem dla tej klasy zapytań).
Mogę wymyślić kilka sposobów podejścia do tego, ale wszystkie mają słabości.
Dwie oddzielne EXISTS
wypowiedzi. Miałoby to tę zaletę, że pozwalało zapytaniom zatrzymać skanowanie wcześnie, jak tylko NULL
zostanie znalezione. Ale jeśli obie kolumny faktycznie nie zawierają NULL
s, to zostaną wykonane dwa pełne skany.
Pojedyncze zapytanie zagregowane
SELECT
MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
Może to przetwarzać obie kolumny jednocześnie, więc najgorszy przypadek jednego pełnego skanowania. Wadą jest to, że nawet jeśli NULL
bardzo wcześnie napotka w obu kolumnach zapytanie, nadal skanuje całą resztę tabeli.
Zmienne użytkownika
I można myśleć o trzeciej drodze robi to
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT
@B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
@C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
/*Divide by zero error if both @B and @C are 1.
Might happen next row as no guarantee of order of
assignments*/
@D = 1 / (2 - (@B + @C))
FROM T
OPTION (MAXDOP 1)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
BEGIN
SELECT 'B,C both contain NULLs'
RETURN;
END
ELSE
RETURN;
END CATCH
SELECT ISNULL(@B,0),
ISNULL(@C,0)
ale nie jest to odpowiednie dla kodu produkcyjnego, ponieważ poprawne zachowanie dla zagregowanego zapytania konkatenacji jest niezdefiniowane. a zakończenie skanowania przez zgłoszenie błędu jest i tak dość okropnym rozwiązaniem.
Czy istnieje inna opcja, która łączy w sobie zalety powyższych podejść?
Edytować
Aby to zaktualizować o wyniki, które otrzymałem w zakresie odczytów dla odpowiedzi przesłanych do tej pory (używając danych testowych @ ypercube)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | 2 * EXISTS | CASE | Kejser | Kejser | Kejser | ypercube | 8kb |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | | | | MAXDOP 1 | HASH GROUP, MAXDOP 1 | | |
| No Nulls | 15208 | 7604 | 8343 | 7604 | 7604 | 15208 | 8346 (8343+3) |
| One Null | 7613 | 7604 | 8343 | 7604 | 7604 | 7620 | 7630 (25+7602+3) |
| Two Null | 23 | 7604 | 8343 | 7604 | 7604 | 30 | 30 (18+12) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Na odpowiedź @ Thomasa zmieniłem TOP 3
na, TOP 2
aby potencjalnie umożliwić wcześniejsze wyjście. Domyślnie dostałem plan równoległy dla tej odpowiedzi, więc wypróbowałem go również ze MAXDOP 1
wskazówką, aby liczba odczytów była bardziej porównywalna z innymi planami. Byłem nieco zaskoczony wynikami, ponieważ w moim wcześniejszym teście widziałem to zwarcie zapytania bez czytania całej tabeli.
Plan moich danych testowych, że zwarcia są poniżej
Plan dla danych ypercube jest
Dodaje więc do planu operatora sortowania blokującego. Próbowałem też z HASH GROUP
podpowiedzi, ale nadal kończy się czytanie wszystkich wierszy
Wydaje się więc, że kluczem jest skłonienie hash match (flow distinct)
operatora do umożliwienia temu planowi zwarcia, ponieważ inne alternatywy i tak blokują i zużywają wszystkie rzędy. Nie sądzę, aby istniała wskazówka, aby wymusić to w sposób konkretny, ale najwyraźniej „ogólnie rzecz biorąc, optymalizator wybiera Flow Distinct, w którym określa, że wymaganych jest mniej wierszy wyjściowych, niż w zestawie danych wejściowych są wyraźne wartości”. .
Dane @ ypercube mają tylko 1 wiersz w każdej kolumnie z NULL
wartościami (liczność tabeli = 30300), a szacowane wiersze wchodzące i wychodzące z operatora są oba 1
. Uczyniwszy predykat nieco nieprzejrzystym dla optymalizatora, wygenerował plan z operatorem Flow Distinct.
SELECT TOP 2 *
FROM (SELECT DISTINCT
CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
, CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
FROM test T
WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT
Edytuj 2
Ostatnia poprawka, która przyszła mi do głowy, polega na tym, że powyższe zapytanie może nadal przetwarzać więcej wierszy niż to konieczne, w przypadku gdy pierwszy napotkany wiersz NULL
ma wartość NULL w obu kolumnach B
i C
. Będzie kontynuować skanowanie zamiast natychmiastowego wyjścia. Jednym ze sposobów uniknięcia tego byłoby rozłożenie wierszy podczas ich skanowania. Więc moja ostatnia poprawka do odpowiedzi Thomasa Kejsera znajduje się poniżej
SELECT DISTINCT TOP 2 NullExists
FROM test T
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
(CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
Prawdopodobnie byłoby lepiej, gdyby predykat był, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL
ale w porównaniu z poprzednimi danymi testowymi, że nie daje mi się planu z Flow Distinct, podczas gdy NullExists IS NOT NULL
ten robi (plan poniżej).
TOP 3
może być tylkoTOP 2
jak obecnie będzie to skanowanie aż znajdzie jednym z każdego z poniższych(NOT_NULL,NULL)
,(NULL,NOT_NULL)
,(NULL,NULL)
. Każde 2 z tych 3 byłoby wystarczające - a jeśli znajdzie(NULL,NULL)
pierwszy, to drugi też nie będzie potrzebny. Również w celu zwarcia plan musiałby zaimplementować odróżnienie za pośrednictwemhash match (flow distinct)
operatora zamiasthash match (aggregate)
lubdistinct sort