[Siehe einen Index aller Beiträge zu schlechten Angewohnheiten/Best Practices]
Eine der Folien in meiner wiederkehrenden Präsentation über schlechte Angewohnheiten und Best Practices trägt den Titel „Missbrauch von COUNT(*)
." Ich sehe diesen Missbrauch ziemlich häufig in freier Wildbahn und er nimmt verschiedene Formen an.
Wie viele Zeilen in der Tabelle?
Normalerweise sehe ich das:
SELECT @count = COUNT(*) FROM dbo.tablename;
SQL Server muss einen blockierenden Scan für die gesamte Tabelle ausführen, um diese Anzahl abzuleiten. Das ist teuer. Diese Informationen werden in den Katalogansichten und DMVs gespeichert, und Sie können sie ohne all diese E/A-Vorgänge oder Blockierungen abrufen:
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';
(Sie können dieselben Informationen von sys.dm_db_partition_stats
abrufen , aber ändern Sie in diesem Fall p.rows
zu p.row_count
(yay Konsistenz!). Tatsächlich ist dies dieselbe Ansicht, die sp_spaceused
verwendet verwendet, um die Zählung abzuleiten – und obwohl es viel einfacher zu tippen ist als die obige Abfrage, rate ich davon ab, sie nur zum Ableiten einer Zählung zu verwenden, da sie all die zusätzlichen Berechnungen durchführt – es sei denn, Sie möchten diese Informationen auch. Beachten Sie auch, dass Metadatenfunktionen verwendet werden, die Ihrer äußeren Isolationsstufe nicht gehorchen, sodass Sie beim Aufrufen dieser Prozedur möglicherweise auf eine Blockierung warten müssen.)
Nun, es stimmt, dass diese Ansichten nicht zu 100 % auf die Mikrosekunde genau sind. Sofern Sie keinen Heap verwenden, erhalten Sie ein zuverlässigeres Ergebnis aus sys.dm_db_index_physical_stats()
Spalte record_count
(Nochmals yay Konsistenz!), diese Funktion kann sich jedoch auf die Leistung auswirken, kann immer noch blockieren und kann sogar teurer sein als ein SELECT COUNT(*)
– es muss die gleichen physikalischen Operationen ausführen, aber je nach mode
zusätzliche Informationen berechnen (z. B. Fragmentierung, die Sie in diesem Fall nicht interessiert). Die Warnung in der Dokumentation erzählt einen Teil der Geschichte, die relevant ist, wenn Sie Verfügbarkeitsgruppen verwenden (und sich wahrscheinlich auf ähnliche Weise auf die Datenbankspiegelung auswirkt):
Die Dokumentation erklärt auch, warum diese Zahl für einen Heap möglicherweise nicht zuverlässig ist (und gibt ihnen auch einen Quasi-Pass für die Inkonsistenz von Zeilen und Datensätzen):
Bei einem Heap stimmt die Anzahl der von dieser Funktion zurückgegebenen Datensätze möglicherweise nicht mit der Anzahl der Zeilen überein, die zurückgegeben werden, wenn ein SELECT COUNT(*) für den Heap ausgeführt wird. Dies liegt daran, dass eine Zeile mehrere Datensätze enthalten kann. Beispielsweise kann in einigen Aktualisierungssituationen eine einzelne Heap-Zeile als Ergebnis der Aktualisierungsoperation einen Weiterleitungsdatensatz und einen weitergeleiteten Datensatz haben. Außerdem werden die meisten großen LOB-Zeilen im LOB_DATA-Speicher in mehrere Datensätze aufgeteilt.
Also würde ich zu sys.partitions
tendieren als Weg, dies zu optimieren, indem ein geringfügiges Maß an Genauigkeit geopfert wird.
- "Aber ich kann die DMVs nicht verwenden; meine Zählung muss sehr genau sein!"
Eine "supergenaue" Zählung ist eigentlich ziemlich bedeutungslos. Nehmen wir an, dass Ihre einzige Option für eine "supergenaue" Zählung darin besteht, die gesamte Tabelle zu sperren und zu verbieten, dass irgendjemand Zeilen hinzufügt oder löscht (aber ohne gemeinsame Lesevorgänge zu verhindern), z. B.:
SELECT @count = COUNT(*) FROM dbo.table_name WITH (TABLOCK); -- not TABLOCKX!
Ihre Abfrage summt also, scannt alle Daten und arbeitet auf diese „perfekte“ Zählung hin. Inzwischen werden Schreibanfragen blockiert und warten. Wenn Ihre genaue Zählung zurückgegeben wird, werden Ihre Sperren auf der Tabelle plötzlich freigegeben, und all diese Schreibanforderungen, die in der Warteschlange standen und warteten, beginnen, alle Arten von Einfügungen, Aktualisierungen und Löschungen gegen Ihre Tabelle abzufeuern. Wie "supergenau" ist Ihre Zählung jetzt? Hat es sich gelohnt, eine "genaue" Zählung zu erhalten, die bereits schrecklich veraltet ist? Wenn das System nicht ausgelastet ist, dann ist das kein so großes Problem – aber wenn das System nicht ausgelastet ist, würde ich ziemlich stark argumentieren, dass die DMVs verdammt genau sein werden.
Sie hätten NOLOCK
verwenden können stattdessen, aber das bedeutet nur, dass Autoren die Daten ändern können, während Sie sie lesen, und führt auch zu anderen Problemen (darüber habe ich kürzlich gesprochen). Es ist für viele Stadien in Ordnung, aber nicht, wenn Ihr Ziel Genauigkeit ist. Die DMVs werden in vielen Szenarien direkt (oder zumindest viel näher) sein und in sehr wenigen weiter entfernt (tatsächlich kann ich mir keine vorstellen).
Schließlich könnten Sie Read Committed Snapshot Isolation verwenden. Kendra Little hat einen fantastischen Beitrag über die Snapshot-Isolationsstufen, aber ich wiederhole die Liste der Vorbehalte, die ich in meinem NOLOCK
erwähnt habe Artikel:
- Sch-S-Sperren müssen auch unter RCSI noch verwendet werden.
- Snapshot-Isolationsstufen verwenden die Zeilenversionierung in tempdb, daher müssen Sie die Auswirkungen dort wirklich testen.
- RCSI kann keine effizienten Scans der Zuordnungsreihenfolge verwenden; Sie sehen stattdessen Reichweitenscans.
- Paul White (@SQL_Kiwi) hat einige großartige Beiträge zu diesen Isolationsstufen, die Sie lesen sollten:
- Festgeschriebene Snapshot-Isolation lesen
- Datenänderungen unter Read Committed Snapshot Isolation
- Die SNAPSHOT-Isolationsstufe
Darüber hinaus erfordert das Abrufen der "genauen" Anzahl selbst mit RCSI Zeit (und zusätzliche Ressourcen in tempdb). Ist die Zählung nach Abschluss der Operation noch genau? Nur wenn in der Zwischenzeit niemand den Tisch berührt hat. Somit wird einer der Vorteile von RCSI (Leser blockieren keine Schreiber) verschwendet.
Wie viele Zeilen stimmen mit einer WHERE-Klausel überein?
Dies ist ein etwas anderes Szenario – Sie müssen wissen, wie viele Zeilen für eine bestimmte Teilmenge der Tabelle vorhanden sind. Sie können die DMVs dafür nicht verwenden, es sei denn, WHERE
-Klausel mit einem gefilterten Index übereinstimmt oder eine exakte Partition (oder mehrere) vollständig abdeckt.
Wenn Ihr WHERE
-Klausel dynamisch ist, könnten Sie wie oben beschrieben RCSI verwenden.
Wenn Ihr WHERE
-Klausel nicht dynamisch ist, könnten Sie auch RCSI verwenden, aber Sie könnten auch eine dieser Optionen in Betracht ziehen:
- Gefilterter Index – zum Beispiel wenn Sie einen einfachen Filter wie
is_active = 1
haben oderstatus < 5
, dann könnten Sie einen Index wie diesen erstellen:CREATE INDEX ix_f ON dbo.table_name(leading_pk_column) WHERE is_active = 1;
Jetzt können Sie ziemlich genaue Zahlen von den DMVs erhalten, da es Einträge geben wird, die diesen Index darstellen (Sie müssen nur die index_id identifizieren, anstatt sich auf heap(0)/clustered index(1) zu verlassen). Sie müssen jedoch einige der Schwächen gefilterter Indizes berücksichtigen.
- Indizierte Ansicht - Wenn Sie beispielsweise häufig Bestellungen nach Kunden zählen, könnte eine indizierte Ansicht hilfreich sein (obwohl Sie dies bitte nicht als allgemeine Bestätigung verstehen, dass "indizierte Ansichten alle Abfragen verbessern!"):
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);
Jetzt werden die Daten in der Ansicht materialisiert, und die Zählung wird garantiert mit den Tabellendaten synchronisiert (es gibt ein paar obskure Fehler, bei denen dies nicht zutrifft, wie z. B. dieser mit
MERGE
, aber im Allgemeinen ist dies zuverlässig). Jetzt können Sie Ihre Zählungen pro Kunde (oder für eine Gruppe von Kunden) erhalten, indem Sie die Ansicht zu viel geringeren Abfragekosten (1 oder 2 Lesevorgänge) abfragen:SELECT customer_count FROM dbo.view_name WHERE customer_id = <x>;
Es gibt jedoch kein kostenloses Mittagessen . Sie müssen den Aufwand für die Verwaltung einer indizierten Ansicht und die Auswirkungen auf den Schreibanteil Ihrer Arbeitslast berücksichtigen. Wenn Sie diese Art von Abfrage nicht sehr oft ausführen, lohnt sich die Mühe wahrscheinlich nicht.
Entspricht mindestens eine Zeile einer WHERE-Klausel?
Auch dies ist eine etwas andere Frage. Aber das sehe ich oft:
IF (SELECT COUNT(*) FROM dbo.table_name WHERE <some clause>) > 0 -- or = 0 for not exists
Da Sie sich offensichtlich nicht um die tatsächliche Anzahl kümmern, interessiert es Sie nur, ob mindestens eine Zeile vorhanden ist. Ich denke wirklich, Sie sollten sie wie folgt ändern:
IF EXISTS (SELECT 1 FROM dbo.table_name WHERE <some clause>)
Dies hat zumindest die Möglichkeit, kurzzuschließen, bevor das Ende der Tabelle erreicht ist, und wird COUNT
fast immer übertreffen Variation (obwohl es einige Fälle gibt, in denen SQL Server intelligent genug ist, um IF (SELECT COUNT...) > 0
zu konvertieren zu einem einfacheren IF EXISTS()
). Im absoluten Worst-Case-Szenario, bei dem keine Zeile gefunden wird (oder die erste Zeile auf der allerletzten Seite des Scans gefunden wird), bleibt die Leistung gleich.
[Siehe einen Index aller Beiträge zu schlechten Angewohnheiten/Best Practices]