[Zobacz spis wszystkich wpisów o złych nawykach / najlepszych praktykach]
Jeden ze slajdów w mojej cyklicznej prezentacji Złe nawyki i najlepsze praktyki jest zatytułowany „Nadużycie COUNT(*)
”. Widzę to nadużycie dość często na wolności i przybiera ono różne formy.
Ile wierszy w tabeli?
Zwykle widzę to:
SELECT @count = COUNT(*) FROM dbo.tablename;
SQL Server musi uruchomić skanowanie blokujące całej tabeli w celu uzyskania tej liczby. To jest drogie. Te informacje są przechowywane w widokach katalogu i DMV i można je uzyskać bez tych wszystkich we/wy lub blokowania:
SELECT @count = SUM(p.rows) FROM sys.partitions AS p INNER JOIN sys.tables AS t ON p.[object_id] = t.[object_id] INNER JOIN sys.schemas AS s ON t.[schema_id] = s.[schema_id] WHERE p.index_id IN (0,1) -- heap or clustered index AND t.name = N'tablename' AND s.name = N'dbo';
(Te same informacje można uzyskać z sys.dm_db_partition_stats
, ale w takim przypadku zmień p.rows
do p.row_count
(Tak konsystencja!). W rzeczywistości jest to ten sam widok, co sp_spaceused
używa do obliczania liczby – i chociaż jest znacznie łatwiej wpisać niż powyższe zapytanie, odradzam używanie go tylko do obliczania liczby ze względu na wszystkie dodatkowe obliczenia, które wykonuje – chyba że chcesz również tych informacji. Pamiętaj też, że używa funkcji metadanych, które nie przestrzegają zewnętrznego poziomu izolacji, więc możesz czekać na zablokowanie, gdy wywołasz tę procedurę.
To prawda, że te widoki nie są w 100% dokładne z dokładnością do mikrosekundy. O ile nie używasz sterty, bardziej wiarygodny wynik można uzyskać z sys.dm_db_index_physical_stats()
kolumna record_count
(znowu ta spójność!), jednak ta funkcja może mieć wpływ na wydajność, nadal może blokować i może być nawet droższa niż SELECT COUNT(*)
– musi wykonać te same operacje fizyczne, ale musi obliczyć dodatkowe informacje w zależności od mode
(takich jak fragmentacja, o którą nie dbasz w tym przypadku). Ostrzeżenie w dokumentacji stanowi część historii, istotne, jeśli używasz grup dostępności (i prawdopodobnie wpływa na dublowanie bazy danych w podobny sposób):
Dokumentacja wyjaśnia również, dlaczego ta liczba może nie być wiarygodna dla stosu (a także daje im quasi-pass dla niespójności wierszy i rekordów):
W przypadku sterty liczba rekordów zwracanych przez tę funkcję może nie odpowiadać liczbie wierszy zwracanych przez uruchomienie SELECT COUNT(*) względem sterty. Dzieje się tak, ponieważ wiersz może zawierać wiele rekordów. Na przykład w niektórych sytuacjach aktualizacji pojedynczy wiersz sterty może mieć rekord przekazywania i rekord przekazany w wyniku operacji aktualizacji. Ponadto większość dużych wierszy LOB jest dzielona na wiele rekordów w magazynie LOB_DATA.
Więc skłaniałbym się ku sys.partitions
jako sposób na zoptymalizowanie tego, poświęcając trochę marginalnej dokładności.
- "Ale nie mogę używać DMV; mój licznik musi być bardzo dokładny!"
„Superdokładna” liczba jest właściwie bez znaczenia. Załóżmy, że jedyną opcją „superdokładnego” zliczania jest zablokowanie całej tabeli i uniemożliwienie komukolwiek dodawania lub usuwania jakichkolwiek wierszy (ale bez blokowania wspólnych odczytów), np.:
SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!
Twoje zapytanie leci, skanując wszystkie dane, dążąc do „doskonałego” zliczenia. Tymczasem żądania zapisu są blokowane i czekają. Nagle, gdy zwrócona jest dokładna liczba, blokady na stole zostają zwolnione, a wszystkie żądania zapisu, które zostały ustawione w kolejce i czekają, zaczynają uruchamiać wszelkiego rodzaju wstawki, aktualizacje i usunięcia w tabeli. Jak "super dokładne" jest teraz twoje liczenie? Czy warto było uzyskać „dokładną” liczbę, która jest już strasznie przestarzała? Jeśli system nie jest zajęty, nie stanowi to większego problemu – ale jeśli system nie jest zajęty, twierdzę, że DMV będą dość dokładne.
Mogłeś użyć NOLOCK
zamiast tego, ale oznacza to po prostu, że pisarze mogą zmieniać dane podczas ich czytania i prowadzi to również do innych problemów (mówiłem o tym niedawno). Jest to w porządku na wielu boiskach, ale nie, jeśli twoim celem jest dokładność. DMV-y będą znajdować się na prawo (lub przynajmniej znacznie bliżej) w wielu scenariuszach, a dalej w bardzo niewielu (w rzeczywistości żaden, o którym nie mogę wymyślić).
Na koniec możesz użyć Izolacji zatwierdzonej migawki. Kendra Little ma fantastyczny post o poziomach izolacji migawek, ale powtórzę listę zastrzeżeń, o których wspomniałem w moim NOLOCK
artykuł:
- Zamki Sch-S nadal muszą być brane nawet pod RCSI.
- Poziomy izolacji migawki używają wersjonowania wierszy w tempdb, więc naprawdę musisz przetestować tam wpływ.
- RCSI nie może używać wydajnego skanowania kolejności alokacji; zamiast tego zobaczysz skanowanie zasięgu.
- Paul White (@SQL_Kiwi) ma kilka świetnych postów, które powinieneś przeczytać na temat tych poziomów izolacji:
- Przeczytaj izolację zatwierdzonej migawki
- Modyfikacje danych w izolowaniu migawek odczytu zatwierdzonego
- Poziom izolacji SNAPSHOT
Ponadto, nawet w przypadku RCSI, uzyskanie „dokładnej” liczby wymaga czasu (i dodatkowych zasobów w tempdb). Czy do czasu zakończenia operacji liczenie jest nadal dokładne? Tylko jeśli w międzyczasie nikt nie dotknął stołu. Tak więc jedna z zalet RCSI (czytelnicy nie blokują pisarzy) jest zmarnowana.
Ile wierszy pasuje do klauzuli WHERE?
To jest nieco inny scenariusz – musisz wiedzieć, ile wierszy istnieje dla określonego podzbioru tabeli. Nie możesz do tego użyć DMV, chyba że WHERE
klauzula pasuje do filtrowanego indeksu lub całkowicie obejmuje dokładną partycję (lub wielokrotność).
Jeśli Twój WHERE
klauzula jest dynamiczna, możesz użyć RCSI, jak opisano powyżej.
Jeśli Twój WHERE
klauzula nie jest dynamiczna, możesz również użyć RCSI, ale możesz również rozważyć jedną z następujących opcji:
- Indeks filtrowany – na przykład jeśli masz prosty filtr, taki jak
is_active = 1
lubstatus < 5
, możesz zbudować indeks taki:CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;
Teraz możesz uzyskać całkiem dokładne zliczenia z DMV, ponieważ będą wpisy reprezentujące ten indeks (musisz tylko zidentyfikować index_id zamiast polegać na heap(0)/clustered index(1)). Musisz jednak wziąć pod uwagę niektóre słabości filtrowanych indeksów.
- Widok indeksowany - na przykład, jeśli często liczysz zamówienia według klientów, widok indeksowany może pomóc (choć nie traktuj tego jako ogólnej adnotacji, że „widoki indeksowane poprawiają wszystkie zapytania!”):
CREATE VIEW dbo.view_name WITH SCHEMABINDING AS SELECT customer_id, customer_count = COUNT_BIG(*) FROM dbo.table_name GROUP BY customer_id; GO CREATE UNIQUE CLUSTERED INDEX ix_v ON dbo.view_name(customer_id);
Teraz dane w widoku zostaną zmaterializowane, a licznik gwarantuje synchronizację z danymi w tabeli (jest kilka niejasnych błędów, w których to nieprawda, na przykład ten z
MERGE
, ale generalnie jest to niezawodne). Więc teraz możesz uzyskać liczbę na klienta (lub dla zestawu klientów), wysyłając zapytanie do widoku, przy znacznie niższym koszcie zapytania (1 lub 2 odczyty):SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;
Nie ma jednak darmowego lunchu . Należy wziąć pod uwagę koszty utrzymania indeksowanego widoku i wpływ, jaki będzie to miało na część związaną z zapisem obciążenia. Jeśli nie uruchamiasz tego typu zapytań zbyt często, prawdopodobnie nie będzie to warte zachodu.
Czy przynajmniej jeden wiersz pasuje do klauzuli WHERE?
To też jest nieco inne pytanie. Ale często widzę to:
IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists
Ponieważ oczywiście nie zależy Ci na rzeczywistej liczbie, interesuje Cię tylko to, czy istnieje co najmniej jeden wiersz, naprawdę myślę, że powinieneś go zmienić na następujący:
IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)
To przynajmniej ma szansę na zwarcie przed końcem tabeli i prawie zawsze przewyższa COUNT
zmienność (chociaż w niektórych przypadkach SQL Server jest wystarczająco inteligentny, aby przekonwertować IF (SELECT COUNT...) > 0
do prostszego IF EXISTS()
). W najgorszym przypadku, gdy nie zostanie znaleziony żaden wiersz (lub pierwszy wiersz zostanie znaleziony na ostatniej stronie skanowania), wydajność będzie taka sama.
[Zobacz indeks wszystkich wpisów o złych nawykach / najlepszych praktykach]