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 OR
daje nam bardziej wydajny plan wyszukiwania, który odczytuje dokładną liczbę potrzebnych wierszy, jednak dodaje to, co świat techniczny wzywa a whole mess of malarkey
do planu zapytań.
Zauważ też, że Seek jest tutaj wykonywany dwukrotnie, co naprawdę powinno być bardziej oczywiste dla operatora graficznego:
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?).
Robi tyle samo odczytów (8233) co OR
zapytanie, 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 COUNT
bę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);
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.
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.
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 APPLY
nie musimy robić nic głupiego, aby mieć w większości całkowicie równoległy plan.
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 COUNT
rzeczy.
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.
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 );
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);
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ą EXCEPT
i INTERSECT
częściej niż ja.
Jeśli naprawdę potrzebujesz tylko liczby
, używam COUNT
w 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ć CASE
wyraż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.
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!
NOT EXISTS ( INTERSECT / EXCEPT )
wyszukiwania może działać bezINTERSECT / EXCEPT
częś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)).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!UNION ALL
planów (procesor 360ms, odczyty 11k).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:
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):
Zapytanie, które działało lepiej dla Erika, faktycznie działało gorzej na moim komputerze:
Wyniki z 10 prób:
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 NULL
ale 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
NULL
za,Age
ponieważ 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ę:
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ć
NULL
wiersze w tabeli na cztery segmenty. Jednym ze sposobów jest utworzenie szeregu indeksów w kolumnach obliczeniowych: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:
Wyniki dziesięciu prób:
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:
W końcowej nucie możemy także nacisnąć przycisk łatwego dodania i dodać do
Age
kolumny nieklastrowany WIK :Następujące zapytanie kończy się w ciągu 3 ms na moim komputerze:
Trudno będzie to pokonać.
źródło
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;
iSET STATISTICS IO ON;
) do przetestowania zapytań. Jako punkt odniesienia, oto kilka zapytań i statystyki CPU / IO:ZAPYTANIE 1
ZAPYTANIE 2
ZAPYTANIE 3
1. próba
Było to wolniejsze niż wszystkie zapytania Erika, które tu wymieniłem ... przynajmniej pod względem upływu czasu.
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ł.
Inne uwagi: DBCC TRACEON nie jest dozwolony w Eksploratorze danych wymiany stosów, jak zauważono poniżej:
źródło
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')
Używać zmiennych?
Na komentarz można pominąć zmienne
źródło
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Dobrze za pomocą
SET ANSI_NULLS OFF;
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
źródło
Trywialnym rozwiązaniem jest obliczenie liczby (*) - liczby (wiek> = 18):
Lub:
Wyniki tutaj
źródło