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).
źródło
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
Jak rozumiem pytanie, chcesz wiedzieć, czy w którejkolwiek z kolumn istnieje wartość null, a nie zwracać wiersze, w których B lub C ma wartość null. Jeśli tak, to dlaczego nie:
Na moim stanowisku testowym z SQL 2008 R2 i milionem wierszy otrzymałem następujące wyniki w ms z zakładki Statystyka klienta:
Jeśli dodasz wskazówkę nolock, wyniki będą jeszcze szybsze:
W celach informacyjnych użyłem generatora SQL Red-gate do wygenerowania danych. Z mojego miliona wierszy 9 886 wierszy miało zerową wartość B, a 10 019 miało zerową wartość C.
W tej serii testów każdy wiersz w kolumnie B ma wartość:
Przed każdym testem (oba zestawy) biegałem
CHECKPOINT
iDBCC DROPCLEANBUFFERS
.Oto wyniki, gdy w tabeli nie ma wartości null. Zauważ, że 2 istniejące rozwiązanie dostarczone przez ypercube jest prawie identyczne z moim pod względem odczytów i czasu wykonania. Uważam (my), że wynika to z zalet wersji Enterprise / Developer korzystającej z zaawansowanego skanowania . Jeśli korzystasz tylko z wersji Standard lub niższej, rozwiązanie Kejsera może być najszybszym rozwiązaniem.
źródło
Czy
IF
oświadczenia są dozwolone?Powinno to umożliwić potwierdzenie istnienia B lub C przy jednym przejściu przez tabelę:
źródło
Testowane w SQL-Fiddle w wersjach: 2008 R2 i 2012 z 30 000 wierszy.
EXISTS
zapytania ilustruje ogromne korzyści w zakresie efektywności, gdy stwierdzi wartości null wcześnie - co jest spodziewane.EXISTS
zapytaniem uzyskuję lepszą wydajność - we wszystkich przypadkach w 2012 r., Czego nie potrafię wyjaśnić.CASE
zapytania Martina .Zapytania i terminy. Czasy, w których wykonano:
B
mającą jednąNULL
po małejid
.NULL
przy małych identyfikatorach.Proszę bardzo (problem dotyczy planów, spróbuję ponownie później. Na razie skorzystaj z linków):
Zapytanie z 2 podzapytaniami EXISTS
Pojedyncze zapytanie agregacyjne Martina Smitha
Pytanie Thomasa Kejsera
Moja sugestia (1)
Wymaga dopracowania danych wyjściowych, ale wydajność jest podobna do
EXISTS
zapytania. Pomyślałem, że byłoby lepiej, gdy nie ma wartości zerowych, ale testy pokazują, że tak nie jest.Sugestia (2)
Próbując uprościć logikę:
Wydaje się, że działa lepiej w 2008R2 niż poprzednia sugestia, ale gorzej w 2012 r. (Być może drugą wersję
INSERT
można przepisać, używającIF
odpowiedzi @ 8kb):źródło
Podczas korzystania z EXISTS SQL Server wie, że przeprowadzasz kontrolę istnienia. Gdy znajdzie pierwszą pasującą wartość, zwraca PRAWDA i przestaje szukać.
po skonkatowaniu 2 kolumn i jeśli jakakolwiek jest pusta, wynik będzie zerowy
na przykład
więc sprawdź ten kod
źródło
Co powiesz na:
Jeśli to zadziała (nie przetestowałem tego), dałoby tabelę z jednym wierszem i 2 kolumnami, każda z nich PRAWDA lub FAŁSZ. Nie przetestowałem wydajności.
źródło
T.B is null
jest to wtedy traktowane jako wynik logicznyEXISTS(SELECT true)
iEXISTS(SELECT false)
oba zwrócą wartość true. Ten przykład MySQL wskazuje, że obie kolumny zawierają NULL, podczas gdy żadna z nich tak nie jest