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 Blub Cfaktycznie zawiera jakieś NULLwartoś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 EXISTSwypowiedzi. Miałoby to tę zaletę, że pozwalało zapytaniom zatrzymać skanowanie wcześnie, jak tylko NULLzostanie znalezione. Ale jeśli obie kolumny faktycznie nie zawierają NULLs, 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 NULLbardzo 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 3na, TOP 2aby 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 1wskazó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 GROUPpodpowiedzi, 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 NULLwartoś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 NULLma wartość NULL w obu kolumnach Bi 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 NULLale w porównaniu z poprzednimi danymi testowymi, że nie daje mi się planu z Flow Distinct, podczas gdy NullExists IS NOT NULLten robi (plan poniżej).

TOP 3może być tylkoTOP 2jak 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