Jakie są różne sposoby zamiany ISNULL () w klauzuli WHERE, która używa tylko wartości literalnych?

55

O co tu nie chodzi:

To nie jest pytanie o zapytania typu catch-all, które akceptują dane wejściowe użytkownika lub używają zmiennych.

Dotyczy to wyłącznie zapytań, ISNULL()w których WHEREklauzula została użyta w celu zastąpienia NULLwartości wartością kanaryjską w celu porównania z predykatem, oraz różnych sposobów przepisania tych zapytań, aby były SARGable w SQL Server.

Dlaczego nie masz tam miejsca?

Nasze przykładowe zapytanie dotyczy lokalnej kopii bazy danych przepełnienia stosu na SQL Server 2016 i szuka użytkowników w NULLwieku lub w wieku <18 lat.

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Plan kwerend pokazuje skanowanie całkiem przemyślanego indeksu nieklastrowanego.

Orzechy

Operator skanowania pokazuje (dzięki dodatkom do faktycznego planu wykonania XML w nowszych wersjach SQL Server), że czytamy każdy śmierdzący wiersz.

Orzechy

Łącznie wykonujemy 9157 odczytów i zużywamy około pół sekundy czasu procesora:

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 485 ms,  elapsed time = 483 ms.

Pytanie: Jakie są sposoby przepisania tego zapytania, aby uczynić go bardziej wydajnym, a może nawet SARGable?

Zachęcamy do oferowania innych sugestii. Nie sądzę, że moja odpowiedź jest koniecznie odpowiedź, i istnieje wystarczająco dużo inteligentnych ludzi tam wymyślić alternatywne, które mogą być lepiej.

Jeśli chcesz grać na własnym komputerze, przejdź tutaj, aby pobrać bazę danych SO .

Dzięki!

Erik Darling
źródło

Odpowiedzi:

57

Sekcja odpowiedzi

Istnieją różne sposoby przepisania tego przy użyciu różnych konstrukcji T-SQL. Przyjrzymy się zaletom i wadom i przeprowadzimy ogólne porównanie poniżej.

Po pierwsze : korzystanieOR

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Użycie ORdaje nam bardziej wydajny plan wyszukiwania, który odczytuje dokładną liczbę potrzebnych wierszy, jednak dodaje to, co świat techniczny wzywa a whole mess of malarkeydo planu zapytań.

Orzechy

Zauważ też, że Seek jest tutaj wykonywany dwukrotnie, co naprawdę powinno być bardziej oczywiste dla operatora graficznego:

Orzechy

Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 469 ms,  elapsed time = 473 ms.

Po drugie : Używanie tabel pochodnych z UNION ALL naszym zapytaniem można również przepisać w ten sposób

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

Daje to ten sam typ planu, przy znacznie mniejszym nieprzyzwoitości i bardziej widocznym stopniu uczciwości co do tego, ile razy wskaźnik był poszukiwany (poszukiwany?).

Orzechy

Robi tyle samo odczytów (8233) co ORzapytanie, ale oszczędza około 100 ms czasu procesora.

CPU time = 313 ms,  elapsed time = 315 ms.

Musisz jednak być bardzo ostrożny, ponieważ jeśli ten plan COUNTbędzie działał równolegle, dwie oddzielne operacje zostaną serializowane, ponieważ każda z nich jest traktowana jako globalny agregat skalarny. Jeśli wymusimy równoległy plan przy użyciu flagi śledzenia 8649, problem stanie się oczywisty.

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);

Orzechy

Można tego uniknąć, zmieniając nieco nasze zapytanie.

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

Teraz oba węzły wykonujące wyszukiwanie są w pełni zrównoleglone, dopóki nie natrafimy na operator konkatenacji.

Orzechy

Pod względem wartości w pełni równoległa wersja ma pewne zalety. Kosztem około 100 dodatkowych odczytów i około 90 ms dodatkowego czasu procesora upływ czasu skraca się do 93 ms.

Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 500 ms,  elapsed time = 93 ms.

Co z aplikacją CROSS? Żadna odpowiedź nie jest kompletna bez magii CROSS APPLY!

Niestety mamy więcej problemów COUNT.

SELECT SUM(Records)
FROM dbo.Users AS u 
CROSS APPLY 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u2 
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u2 
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

Ten plan jest okropny. Jest to rodzaj planu, który kończysz, kiedy pojawiasz się ostatni do Dnia Świętego Patryka. Chociaż ładnie równoległy, z jakiegoś powodu skanuje PK / CX. Ew. Koszt planu to 2198 dolców za zapytania.

Orzechy

Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 29532 ms,  elapsed time = 5828 ms.

Co jest dziwnym wyborem, ponieważ jeśli zmuszymy go do użycia indeksu nieklastrowanego, koszt znacznie spadnie do 1798 dolców za zapytanie.

SELECT SUM(Records)
FROM dbo.Users AS u 
CROSS APPLY 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

Hej, szuka! Sprawdź się tam. Zauważ też, że dzięki magii CROSS APPLYnie musimy robić nic głupiego, aby mieć w większości całkowicie równoległy plan.

Orzechy

Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 27625 ms,  elapsed time = 4909 ms.

Zastosuj krzyżowanie kończy się lepiej bez tych COUNTrzeczy.

SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY 
(
    SELECT 1
    FROM dbo.Users AS u2
    WHERE u2.Id = u.Id
    AND u2.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u2
    WHERE u2.Id = u.Id 
    AND u2.Age IS NULL
) x (Records);

Plan wygląda dobrze, ale odczyty i procesor nie stanowią poprawy.

Orzechy

Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 4844 ms,  elapsed time = 863 ms.

Przepisanie krzyża stosuje się, aby uzyskać pochodne połączenie w dokładnie tym samym wszystkim. Nie zamierzam ponownie publikować informacji o planie zapytań i statystykach - naprawdę się nie zmieniły.

SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN 
(
    SELECT u.Id
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT u.Id
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x ON x.Id = u.Id;

Relacyjna algebra : Aby być dokładnym i powstrzymać Joe Celko przed prześladowaniem moich snów, musimy przynajmniej spróbować dziwnych relacji. Tutaj nic nie idzie!

Próba z INTERSECT

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
                   INTERSECT
                   SELECT u.Age WHERE u.Age IS NOT NULL );

Orzechy

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 1094 ms,  elapsed time = 1090 ms.

A oto próba z EXCEPT

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
                   EXCEPT
                   SELECT u.Age WHERE u.Age IS NULL);

Orzechy

Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 2126 ms,  elapsed time = 376 ms.

Mogą być też inne sposoby, aby napisać te, ale zostawię to do ludzi, którzy być może używają EXCEPTi INTERSECTczęściej niż ja.

Jeśli naprawdę potrzebujesz tylko liczby , używam COUNTw moich zapytaniach jako skrótu (czytaj: jestem zbyt leniwy, by czasem wymyślić bardziej zaangażowane scenariusze). Jeśli potrzebujesz tylko liczby, możesz użyć CASEwyrażenia, aby zrobić dokładnie to samo.

SELECT SUM(CASE WHEN u.Age < 18 THEN 1
                WHEN u.Age IS NULL THEN 1
                ELSE 0 END) 
FROM dbo.Users AS u

SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
                ELSE 0 END) 
FROM dbo.Users AS u

Oba mają ten sam plan i mają tę samą charakterystykę procesora i odczytu.

Orzechy

Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 719 ms,  elapsed time = 719 ms.

Zwycięzca? W moich testach wymuszony plan równoległy z SUM nad tabelą pochodną działał najlepiej. I tak, wielu z tych zapytań można było pomóc, dodając kilka filtrowanych indeksów w celu uwzględnienia obu predykatów, ale chciałem pozostawić pewne eksperymenty innym.

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

Dzięki!

Erik Darling
źródło
1
Do NOT EXISTS ( INTERSECT / EXCEPT )wyszukiwania może działać bez INTERSECT / EXCEPTczęści: WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );Inny sposób, który wykorzystuje - EXCEPT: SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ; (gdzie identyfikator użytkownika PK i niepowtarzalny niezerowy kolumnę (-y)).
ypercubeᵀᴹ
Czy to zostało przetestowane? SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;Przepraszam, jeśli przeoczyłem milion przetestowanych wersji!
ypercubeᵀᴹ
@ ypercubeᵀᴹ Oto plan tego. Jest trochę inny, ale ma podobne cechy do UNION ALLplanów (procesor 360ms, odczyty 11k).
Erik Darling,
Hej, Erik, właśnie wędrowałem po świecie sql i wpadłem, żeby powiedzieć „kolumna obliczeniowa”, żeby cię wkurzyć. <3
tygiel
17

Nie grałem w przywracanie bazy danych 110 GB dla tylko jednej tabeli, więc stworzyłem własne dane . Rozkład wieku powinien pasować do tego, co jest na Przepełnieniu stosu, ale oczywiście sama tabela nie będzie pasować. Nie sądzę, że to zbyt duży problem, ponieważ zapytania i tak trafią do indeksów. Testuję na komputerze z 4 procesorami i SQL Server 2016 SP1. Należy zauważyć, że w przypadku zapytań, które kończą się tak szybko, ważne jest, aby nie uwzględniać rzeczywistego planu wykonania. To może trochę spowolnić.

Zacząłem od rozwiązania niektórych z doskonałej odpowiedzi Erika. Dla tego:

SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

Otrzymałem następujące wyniki z sys.dm_exec_sessions w ciągu 10 prób (zapytanie naturalnie poszło dla mnie równolegle):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

Zapytanie, które działało lepiej dla Erika, faktycznie działało gorzej na moim komputerze:

SELECT SUM(Records)
FROM 
(
    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT 1
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records)   
OPTION(QUERYTRACEON 8649);

Wyniki z 10 prób:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

Nie jestem w stanie natychmiast wyjaśnić, dlaczego jest tak źle, ale nie jest jasne, dlaczego chcemy zmusić prawie każdego operatora w planie zapytań do równoległego działania. W oryginalnym planie mamy strefę szeregową, w której znajdują się wszystkie wiersze AGE < 18. Jest tylko kilka tysięcy wierszy. Na moim komputerze otrzymuję 9 logicznych odczytów dla tej części zapytania oraz 9 ms zgłoszonego czasu procesora i czasu, który upłynął. Istnieje również strefa szeregowa dla globalnego agregatu dla wierszy, AGE IS NULLale która przetwarza tylko jeden wiersz na DOP. Na mojej maszynie są to tylko cztery rzędy.

Moja na wynos jest to, że najważniejsza jest optymalizacja części zapytania, która wyszukuje wiersze NULLza, Ageponieważ istnieją miliony takich wierszy. Nie byłem w stanie utworzyć indeksu z mniejszą liczbą stron, które obejmowały dane, niż zwykły skompresowany na stronie w kolumnie. Zakładam, że istnieje minimalny rozmiar indeksu na wiersz lub że nie można uniknąć dużej przestrzeni indeksu za pomocą lew, które próbowałem. Więc jeśli utknęliśmy z mniej więcej taką samą liczbą logicznych odczytów, aby uzyskać dane, to jedynym sposobem na przyspieszenie jest uczynienie zapytania bardziej równoległym, ale należy to zrobić inaczej niż zapytanie Erika, które używało TF 8649. W powyższym zapytaniu mamy stosunek 3,62 czasu procesora do upływu czasu, co jest całkiem dobre. Ideałem byłby stosunek 4,0 na moim komputerze.

Jednym z możliwych obszarów poprawy jest bardziej równomierny podział pracy między wątki. Na poniższym zrzucie ekranu widać, że jeden z moich procesorów postanowił zrobić sobie małą przerwę:

leniwy wątek

Skanowanie indeksu jest jednym z niewielu operatorów, które można wdrożyć równolegle i nie możemy nic zrobić z tym, jak wiersze są rozmieszczone w wątkach. Jest w tym także element szansy, ale dość konsekwentnie widziałem jeden niedoceniony wątek. Jednym ze sposobów obejścia tego problemu jest wykonanie paralelizmu na trudny sposób: w wewnętrznej części zagnieżdżonej pętli. Wszystko w wewnętrznej części zagnieżdżonej pętli będzie realizowane szeregowo, ale wiele wątków szeregowych może działać jednocześnie. Tak długo, jak otrzymamy korzystną metodę dystrybucji równoległej (taką jak okrągły robin), możemy dokładnie kontrolować liczbę wierszy wysyłanych do każdego wątku.

Prowadzę zapytania z DOP 4, więc muszę równomiernie podzielić NULLwiersze w tabeli na cztery segmenty. Jednym ze sposobów jest utworzenie szeregu indeksów w kolumnach obliczeniowych:

ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);

CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);

Nie jestem do końca pewien, dlaczego cztery oddzielne indeksy są trochę szybsze niż jeden indeks, ale to właśnie znalazłem w moich testach.

Aby uzyskać równoległy plan zagnieżdżonej pętli, użyję nieudokumentowanej flagi śledzenia 8649 . Zamierzam również napisać kod nieco dziwnie, aby zachęcić optymalizatora do nie przetwarzania większej liczby wierszy niż to konieczne. Poniżej znajduje się jedna implementacja, która wydaje się działać dobrze:

SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM 
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY 
(
    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);

Wyniki dziesięciu prób:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

Dzięki temu zapytaniu mamy stosunek czasu procesora do upływu 3,85! Ogoliliśmy 17 ms z środowiska wykonawczego i zajęło to tylko 4 obliczone kolumny i indeksy! Każdy wątek przetwarza bardzo blisko tej samej liczby wierszy ogółem, ponieważ każdy indeks ma bardzo zbliżoną tę samą liczbę wierszy, a każdy wątek skanuje tylko jeden indeks:

dobrze podzielona praca

W końcowej nucie możemy także nacisnąć przycisk łatwego dodania i dodać do Agekolumny nieklastrowany WIK :

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

Następujące zapytanie kończy się w ciągu 3 ms na moim komputerze:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

Trudno będzie to pokonać.

Joe Obbish
źródło
7

Chociaż nie mam lokalnej kopii bazy danych przepełnienia stosu, udało mi się wypróbować kilka zapytań. Myślałem o uzyskaniu liczby użytkowników z widoku katalogu systemowego (w przeciwieństwie do bezpośredniego uzyskania liczby wierszy z podstawowej tabeli). Następnie uzyskaj liczbę wierszy, które pasują do kryteriów Erika (a może nie), i wykonaj prostą matematykę.

Użyłem Eksploratora danych wymiany stosu (wraz z SET STATISTICS TIME ON;i SET STATISTICS IO ON;) do przetestowania zapytań. Jako punkt odniesienia, oto kilka zapytań i statystyki CPU / IO:

ZAPYTANIE 1

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms. (Zwrócono 1 wiersze)

Tabela „Użytkownicy”. Liczba skanów 17, logiczne odczyty 201567, fizyczne odczyty 0, odczyty z wyprzedzeniem 2740, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty 0.

Czasy wykonania programu SQL Server: czas procesora = 1829 ms, czas, który upłynął = 296 ms.

ZAPYTANIE 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms. (Zwrócono 1 wiersze)

Tabela „Użytkownicy”. Liczba skanów 17, logiczne odczyty 201567, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0.

Czasy wykonania programu SQL Server: czas procesora = 2500 ms, czas, który upłynął = 147 ms.

ZAPYTANIE 3

--Erik's derived tables/UNION ALL query.
SELECT SUM(Records)
FROM 
(
    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age < 18

    UNION ALL

    SELECT COUNT(Id)
    FROM dbo.Users AS u
    WHERE u.Age IS NULL
) x (Records);

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms. (Zwrócono 1 wiersze)

Tabela „Użytkownicy”. Liczba skanów 34, logiczne odczyty 403134, fizyczne odczyty 0, odczytywanie z wyprzedzeniem 0, logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty 0.

Czasy wykonania programu SQL Server: czas procesora = 3156 ms, czas, który upłynął = 215 ms.

1. próba

Było to wolniejsze niż wszystkie zapytania Erika, które tu wymieniłem ... przynajmniej pod względem upływu czasu.

SELECT SUM(p.Rows)  -
  (
    SELECT COUNT(*)
    FROM dbo.Users AS u
    WHERE u.Age >= 18
  ) 
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms. (Zwrócono 1 wiersze)

Tabela „Stół roboczy”. Liczba skanów 0, logiczne odczyty 0, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0. Tabela „sysrowsets”. Liczba skanów 2, logiczne odczyty 10, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0. Tabela „sysschobjs”. Liczba skanów 1, logiczne odczyty 4, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0. Tabela „Użytkownicy”. Liczba skanów 1, logiczne odczyty 201567, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0.

Czasy wykonania programu SQL Server: czas procesora = 593 ms, czas, który upłynął = 598 ms.

2. próba

Tutaj wybrałem zmienną do przechowywania całkowitej liczby użytkowników (zamiast zapytania podrzędnego). Liczba skanów wzrosła z 1 do 17 w porównaniu z pierwszą próbą. Logiczne odczyty pozostały takie same. Jednak upływ czasu znacznie się zmniejszył.

DECLARE @Total INT;

SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 0 ms. Tabela „Stół roboczy”. Liczba skanów 0, logiczne odczyty 0, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0. Tabela „sysrowsets”. Liczba skanów 2, logiczne odczyty 10, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0. Tabela „sysschobjs”. Liczba skanów 1, logiczne odczyty 4, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0.

Czasy wykonania programu SQL Server: czas procesora = 0 ms, czas, który upłynął = 1 ms. (Zwrócono 1 wiersze)

Tabela „Użytkownicy”. Liczba skanów 17, logiczne odczyty 201567, fizyczne odczyty 0, odczyt z wyprzedzeniem 0, lob logiczne odczyty 0, lob fizyczne odczyty 0, lob odczyty z wyprzedzeniem 0.

Czasy wykonania programu SQL Server: czas procesora = 1471 ms, czas, który upłynął = 98 ms.

Inne uwagi: DBCC TRACEON nie jest dozwolony w Eksploratorze danych wymiany stosów, jak zauważono poniżej:

Użytkownik „STACKEXCHANGE \ svc_sede” nie ma uprawnień do uruchamiania DBCC TRACEON.

Dave Mason
źródło
1
Prawdopodobnie nie mają tych samych indeksów co ja, stąd różnice. I kto wie? Może mój serwer domowy jest na lepszym sprzęcie;) Świetna odpowiedź!
Erik Darling
powinieneś był użyć następującego zapytania do pierwszego ataku (będzie znacznie szybszy, ponieważ unieważnia znaczną część sys.objects-overhead): SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
Thomas Franz
PS: należy pamiętać, że indeksy w pamięci (NONCLUSTERED HASH) nie mają identyfikatora indeksu = 0/1, jak miałby to zwykły indeks stert / klastrów)
Thomas Franz
1

Używać zmiennych?

declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;

Na komentarz można pominąć zmienne

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);
paparazzo
źródło
3
Również:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ
3
Mogę spróbować tego podczas sprawdzania CPU i IO. Wskazówka: jest taka sama jak jedna z odpowiedzi Erika.
Brent Ozar
0

Dobrze za pomocą SET ANSI_NULLS OFF;

SET ANSI_NULLS OFF; 
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18

Table 'Users'. Scan count 17, logical reads 201567

 SQL Server Execution Times:
 CPU time = 2344 ms,  elapsed time = 166 ms.

To coś, co właśnie pojawiło się w mojej głowie. Po prostu wykonałem to w https://data.stackexchange.com

Ale nie tak wydajny jak @blitz_erik

Biju Jose
źródło
0

Trywialnym rozwiązaniem jest obliczenie liczby (*) - liczby (wiek> = 18):

SELECT
    (SELECT COUNT(*) FROM Users) -
    (SELECT COUNT(*) FROM Users WHERE Age >= 18);

Lub:

SELECT COUNT(*)
     - COUNT(CASE WHEN Age >= 18)
FROM Users;

Wyniki tutaj

Salman A.
źródło