Einleitung
In T-SQL gibt es verschiedene Möglichkeiten, die Anzahl der Zeilen in einer Tabelle zu ermitteln. Jede dieser Methoden hat ihre eigenen Vor- und Nachteile und kann je nach Szenario optimal oder suboptimal sein. Im Folgenden werden die gängigsten Ansätze vorgestellt und beschrieben, wie sie in der Praxis eingesetzt werden können.
Verwendung von COUNT(*)
und COUNT(1)
Allgemeines
Die gebräuchlichste Methode, die Anzahl der Zeilen einer Tabelle in T-SQL zu ermitteln, ist die Verwendung von COUNT(*)
. Dieses Statement zählt alle vorhandenen Zeilen, ohne zu berücksichtigen, ob diese NULL-Werte enthalten. Ein einfaches Beispiel wäre:
SELECT COUNT(*)
FROM dbo.MyTable;
Ähnlich wie COUNT(*)
ist auch COUNT(1)
eine häufig verwendete Methode. Sie funktioniert identisch, indem jede Zeile gezählt wird, ohne jedoch explizit alle Spalten auszuwählen. Ein Beispiel wäre:
SELECT COUNT(1)
FROM dbo.MyTable;
Interne Verarbeitung
COUNT(*)
und COUNT(1)
greifen auf die tatsächlichen Datenzeilen der Tabelle zu und durchlaufen jede Zeile, um die Anzahl zu berechnen. Wenn es keinen geeigneten Index gibt, führt der SQL Server einen sogenannten “Full-Table Scan” durch, bei dem jede Zeile der Tabelle vollständig gelesen wird. Dies kann zu einem hohen I/O-Aufwand führen und ist insbesondere bei großen Tabellen sehr ressourcenintensiv.
Wenn die Tabelle jedoch über einen geeigneten Index verfügt, kann der SQL Server den Index verwenden, um die Zeilen effizienter zu zählen. In diesem Fall wird ein “Clustered Index Scan” oder “Index Seek” durchgeführt, abhängig davon, ob ein Clustered Index vorhanden ist und ob dieser für die Zählung verwendet werden kann. Der Optimierer wählt die effizienteste Methode basierend auf den vorhandenen Indizes und Statistiken.
In der Praxis behandelt der SQL Server-Optimierer COUNT(*)
und COUNT(1)
identisch, da beide Methoden darauf abzielen, die Anzahl der Zeilen zu berechnen, unabhängig von den Spalten.
Unterschied zwischen COUNT(*)
und COUNT(1)
Obwohl COUNT(*)
und COUNT(1)
in der Praxis oft als identisch angesehen werden, gibt es Unterschiede in der Syntax, jedoch keine signifikanten Unterschiede in der Verarbeitung durch den SQL Server.
COUNT(*)
zählt jede Zeile in der Tabelle, einschließlich aller Spalten, ohne Rücksicht darauf, ob diese NULL-Werte enthalten. Die Verwendung von*
bedeutet, dass alle Zeilen gezählt werden, unabhängig davon, ob Spaltenwerte NULL sind oder nicht. Der SQL Server-Optimierer versteht, dass nur die Zeilen gezählt werden müssen, sodass die Spalteninhalte nicht tatsächlich gelesen werden.COUNT(1)
verwendet stattdessen eine Konstante (in diesem Fall “1”), um jede Zeile zu zählen. Der SQL Server behandelt diese Konstante jedoch genauso wie*
, da die Konstante für die Zählung keine Rolle spielt.
Der SQL Server-Optimierer optimiert beide Abfragen auf die gleiche Weise, und sie führen zur selben Ausführungsstrategie. Daher gibt es keine Performance-Unterschiede zwischen COUNT(*)
und COUNT(1)
. Die Wahl zwischen den beiden Varianten ist somit eher eine Frage der persönlichen Präferenz oder des Coding-Stils.
Verhalten bei NULL-Werten
Weder COUNT(*)
noch COUNT(1)
berücksichtigen NULL-Werte explizit, da beide Methoden alle Zeilen zählen, unabhängig davon, ob in einer bestimmten Spalte NULL-Werte vorhanden sind. Das bedeutet, dass NULL-Werte bei der Zählung nicht ausgeschlossen werden.
Wenn jedoch eine bestimmte Spalte gezählt werden soll, und NULL-Werte nicht berücksichtigt werden sollen, kann COUNT(Spalte)
verwendet werden. Dabei zählt der SQL Server nur die Zeilen, in denen der Wert der angegebenen Spalte nicht NULL ist. Ein Beispiel hierfür ist:
SELECT COUNT(Spalte)
FROM dbo.MyTable;
In diesem Fall werden nur die Zeilen gezählt, in denen Spalte
einen Wert ungleich NULL hat. Das ist nützlich, wenn nur die Zeilen mit gültigen (nicht NULL) Werten in einer bestimmten Spalte berücksichtigt werden sollen.
Zusammenfassend lässt sich sagen:
COUNT(*)
undCOUNT(1)
zählen alle Zeilen, unabhängig von NULL-Werten in den Spalten.COUNT(Spalte)
zählt nur die Zeilen, in denen die angegebene Spalte einen Wert ungleich NULL hat.COUNT(*)
zählt jede Zeile in der Tabelle, einschließlich aller Spalten, ohne Rücksicht darauf, ob diese NULL-Werte enthalten. Die Verwendung von*
bedeutet, dass alle Spalten berücksichtigt werden, was jedoch nicht bedeutet, dass die Spalteninhalte tatsächlich gelesen werden – der Optimierer versteht, dass nur die Zeilen gezählt werden müssen.COUNT(1)
verwendet stattdessen eine Konstante (in diesem Fall “1”), um jede Zeile zu zählen. Der SQL Server behandelt diese Konstante jedoch genauso wie*
, da die Konstante für die Zählung keine Rolle spielt.
Der SQL Server-Optimierer optimiert beide Abfragen auf die gleiche Weise, und sie führen zur selben Ausführungsstrategie. Daher gibt es keine Performance-Unterschiede zwischen COUNT(*)
und COUNT(1)
. Die Wahl zwischen den beiden Varianten ist somit eher eine Frage der persönlichen Präferenz oder des Coding-Stils.
Locks
Bei der Verwendung von COUNT(*)
und COUNT(1)
kommt es zu Locks auf die Datenzeilen, die gelesen werden. Diese Sperren verhindern, dass andere Prozesse die Daten ändern, während die Zählung erfolgt. Das kann insbesondere bei großen Tabellen und bei hoher Parallelität zu Performanceproblemen führen.
Performance
Die Methode COUNT(*)
ist genau, kann jedoch bei sehr großen Tabellen langsam sein, da jede einzelne Zeile gescannt werden muss. Dies führt zu einem hohen Ressourcenverbrauch, insbesondere bei umfangreichen Tabellen ohne geeignete Indexierung.
WITH (NOLOCK)
Um Sperren zu vermeiden, kann der WITH (NOLOCK)
-Hinweis verwendet werden. Ein Beispiel sieht folgendermaßen aus:
SELECT COUNT(*)
FROM dbo.MyTable WITH (NOLOCK);
Dies ist besonders nützlich in hochfrequentierten Umgebungen, um sicherzustellen, dass die Zählung keine unnötigen Performance-Engpässe verursacht. Es besteht jedoch das Risiko, dass die Ergebnisse inkonsistent sind, wenn die Tabelle während der Zählung geändert wird. Trotz WITH (NOLOCK)
können Schema-Locks auftreten, die nicht vermieden werden können. Diese Locks schützen die Tabellenstruktur vor Änderungen, während die Abfrage ausgeführt wird.
Beispiel
Erklärung
- Cursor-Definition: Der Cursor
table_cursor
wählt die Tabellen aus, die das Kriterium erfüllen (Schemalw
und Tabellennamen, die nicht mit_NEW
enden). - Schleife: In der Schleife durchläuft der Cursor jede Tabelle.
- Dynamische SQL für
COUNT(*)
: Die Zeilenanzahl wird mit dynamischem SQL berechnet (sp_executesql
), da der Tabellenname dynamisch zusammengesetzt wird. - Einfügen der Ergebnisse: Die berechnete Zeilenanzahl und der Zeitstempel werden für jede Tabelle in die
RowCountResults
-Tabelle eingefügt. - Cursor schließen: Nach der Verarbeitung wird der Cursor geschlossen und freigegeben.
Verwendung von sys.dm_db_partition_stats
Allgemeines
Eine Alternative zu COUNT(*)
ist die Verwendung der Systemansicht sys.dm_db_partition_stats
. Diese Methode greift auf Metadaten zu und ist daher oft erheblich schneller, da keine vollständigen Durchläufe über die Daten erforderlich sind. Die folgende Abfrage zeigt, wie dies funktioniert:
SELECT SUM(row_count) AS TotalRowCount
FROM sys.dm_db_partition_stats
WHERE object_id = OBJECT_ID('dbo.MyTable')
AND index_id IN (0, 1);
Interne Verarbeitung
sys.dm_db_partition_stats
greift auf die Metadaten des SQL Servers zu, um die Anzahl der Zeilen in einer Tabelle zu ermitteln. Diese Metadaten sind über den Partitionierungsmechanismus verfügbar, und die Abfrage greift auf Informationen über die Anzahl der Datensätze in den jeweiligen Partitionen zu. Da es sich um eine Metadatenabfrage handelt, ist diese Methode in der Regel sehr schnell.
Locks
Bei der Verwendung von sys.dm_db_partition_stats
können verschiedene Arten von Sperren auftreten, hauptsächlich im Zusammenhang mit dem Zugriff auf die Metadaten. Die häufigsten Locks, die hier auftreten können, sind Sch-S (Schema Stability Locks) und Latches.
- Sch-S (Schema Stability Locks): Diese Sperren treten auf, wenn eine Abfrage auf Metadaten zugreift, ohne die Struktur der Tabelle zu ändern. Diese Sperre verhindert Änderungen an der Tabellenstruktur, erlaubt jedoch gleichzeitig das Lesen der Daten. In seltenen Fällen kann es zu Konflikten kommen, wenn gleichzeitig eine Änderung an der Tabellenstruktur vorgenommen werden soll.
- Latches: Latches sind eine Art interne Synchronisationsmechanismen, die der SQL Server verwendet, um sicherzustellen, dass Speicherstrukturen wie Buffer Pools konsistent bleiben. Latches sind in der Regel sehr kurzlebig, können jedoch bei starker Konkurrenz durch andere Abfragen oder Operationen im System zu Verzögerungen führen.
Performance
Da sys.dm_db_partition_stats
lediglich auf Metadaten zugreift, wird kein vollständiger Scan der Tabelle benötigt. Dadurch werden I/O-Operationen minimiert, was insbesondere bei großen Tabellen einen erheblichen Performancegewinn bedeuten kann. Es kann jedoch zu minimalen Verzögerungen bei der Aktualisierung der Metadaten kommen, sodass das Ergebnis nicht immer zu 100 % aktuell ist.
Beispiel
Erklärung
sys.dm_db_partition_stats
liefert Partitionsinformationen, einschließlichrow_count
, für jede Partition in der Datenbank.ps.index_id IN (0, 1)
: Beschränkt die Zeilenzählung auf Heaps (index_id = 0
) und denClustered Index
(index_id = 1
), um doppelte Zählungen zu vermeiden.SUM(ps.row_count)
summiert die Zeilenanzahl aller relevanten Partitionen jeder Tabelle.
Verwendung von sp_spaceused
Allgemeines
Eine weitere Möglichkeit besteht in der Verwendung der gespeicherten Systemsprozedur sp_spaceused
, die allgemeine Informationen über die Größe einer Tabelle zur Verfügung stellt, einschließlich der Anzahl der Zeilen. Ein Beispiel wäre:
EXEC sp_spaceused 'dbo.MyTable';
Der Nachteil dieser Methode ist, dass das Ergebnis nicht direkt in eine weitere Abfrage eingebunden werden kann, da die Prozedur eine Ergebnismenge zurückgibt und keine Werte, die einfach weiterverarbeitet werden könnten. Es eignet sich also eher für Ad-hoc-Abfragen als für prozedurale Zwecke.
Interne Verarbeitung
sp_spaceused
führt intern eine Abfrage auf die Metadaten des SQL Servers aus, ähnlich wie sys.dm_db_partition_stats
. Diese Abfrage kombiniert die Anzahl der Zeilen mit anderen Informationen zur Größe der Tabelle, wie Speicherplatzbedarf und Datengröße.
Locks
Bei der Verwendung von sp_spaceused
können ebenfalls Sch-S (Schema Stability Locks) auftreten. Diese Sperren werden gesetzt, um sicherzustellen, dass während der Abfrage der Tabellenmetadaten keine Änderungen an der Struktur der Tabelle vorgenommen werden. Das bedeutet, dass sp_spaceused
sicherstellt, dass die Tabellenstruktur stabil bleibt, während die Statistiken abgerufen werden. In hochfrequentierten Umgebungen, in denen häufig Änderungen an der Tabellenstruktur vorgenommen werden, können diese Locks zu Blockierungen führen.
Performance
Die Methode ist in der Regel effizient, da sie auf Metadaten zugreift und keinen vollständigen Scan der Tabelle durchführt. Sie ist jedoch nicht ideal, wenn die Zählung innerhalb einer weiteren Abfrage verwendet werden soll, da die Prozedur nicht direkt eingebunden werden kann.
Beispiel
Erklärung
- Cursor-Definition und Schleife: Der Cursor
table_cursor
durchläuft alle Tabellen, die das Kriterium erfüllen. - sp_spaceused verwenden: Für jede Tabelle wird
sp_spaceused
aufgerufen, und die Ergebnisse werden in eine temporäre Tabelle@Result
eingefügt. - Zeilenanzahl abrufen: Die Zeilenanzahl wird aus der temporären Tabelle
@Result
entnommen. - Einfügen der Ergebnisse: Die Tabelle
RowCountResults
speichert denDateStamp
, den vollständigenTableName
und dieCount
-Spalte für jede Tabelle. - Cursor schließen: Der Cursor wird nach Abschluss der Schleife geschlossen und freigegeben.
Mit dieser Abfrage wird die Zeilenanzahl für jede Tabelle in der gewünschten Form in die RowCountResults
-Tabelle eingefügt.
Vergleich der Methoden
Die verschiedenen Methoden, die zur Zählung der Zeilen in einer Tabelle verwendet werden, funktionieren auf unterschiedliche Weise, was sowohl Performance als auch Genauigkeit betrifft.
COUNT(*)
undCOUNT(1)
: Diese beiden Methoden sind sehr genau, da sie jede einzelne Zeile der Tabelle durchlaufen. Sie eignen sich gut für Szenarien, in denen eine exakte Zählung erforderlich ist, sind jedoch bei großen Tabellen mit einem hohen I/O-Aufwand verbunden.sys.dm_db_partition_stats
: Diese Methode ist ideal, wenn eine schnelle und weniger exakte Zählung der Zeilen benötigt wird, zum Beispiel für Monitoring-Zwecke. Sie greift auf Metadaten zu, wodurch sie oft deutlich schneller ist alsCOUNT(*)
.sp_spaceused
: Diese Methode eignet sich gut für Ad-hoc-Abfragen, wenn neben der Anzahl der Zeilen auch Informationen zur Größe der Tabelle benötigt werden. Sie bietet eine schnelle Möglichkeit, auf Metadaten zuzugreifen, ist aber weniger flexibel in prozeduralen Kontexten.
Performance der unterschiedlichen Methoden
Die Performance der verschiedenen Methoden zur Ermittlung der Anzahl der Zeilen hängt stark von der Größe der Tabelle, der vorhandenen Indexierung und den zugrunde liegenden Datenbankbedingungen ab. In der Regel ist die schnellste Methode die Verwendung von sys.dm_db_partition_stats
, da diese Methode lediglich auf Metadaten zugreift und keinen vollständigen Scan der Tabelle erfordert. Dadurch werden I/O-Operationen minimiert, was insbesondere bei großen Tabellen einen erheblichen Performancegewinn bedeuten kann.
COUNT(*)
und COUNT(1)
hingegen führen häufig einen vollständigen Scan der Tabelle oder des Indexes durch, was bei sehr großen Tabellen zu langen Laufzeiten führen kann, insbesondere wenn keine geeigneten Indizes vorhanden sind. Diese Methoden sind genau, können aber bei umfangreichen Daten zu einem hohen Ressourcenverbrauch führen.
Die Verwendung von sp_spaceused
bietet ebenfalls eine schnelle Möglichkeit, die Anzahl der Zeilen zu ermitteln, da auch hier auf Metadaten zurückgegriffen wird. Sie ist jedoch nicht ideal, wenn die Zählung innerhalb einer weiteren Abfrage verwendet werden soll, da die Prozedur nicht direkt eingebunden werden kann.
Hier ein Beispiel für mehr als 700 Millionen Zeilen auf ca 300 Tabellen verteilt. Wie man sieht ist die Verwendung der sys.dm_db_partition_stats die schnellste Methode mit einer Gesamtdurchlaufszeit 313 ms für über 700 Millionen Zeilen, was einer Dauer von 0.00000043710375959409 ms bzw. 437.10375959409 Pikosekunden pro Zeile bedeutet.
Use Cases
COUNT(*)
und COUNT(1)
COUNT(*)
und COUNT(1)
sind ideal, wenn eine genaue Zählung aller Zeilen in einer Tabelle benötigt wird. Diese Methoden sind besonders dann nützlich, wenn keine Metadaten verwendet werden sollen, sondern die tatsächlichen Datenzeilen gezählt werden müssen, etwa für Berichte oder Analysen, bei denen Präzision im Vordergrund steht. Sie sind jedoch bei großen Tabellen ressourcenintensiv und führen häufig zu hohen I/O-Kosten, insbesondere wenn kein geeigneter Index vorhanden ist.
sys.dm_db_partition_stats
sys.dm_db_partition_stats
ist eine hervorragende Wahl, wenn eine schnelle Übersicht über die Anzahl der Zeilen benötigt wird, beispielsweise für Monitoring- oder Wartungszwecke. Da diese Methode auf Metadaten zugreift, ist sie sehr performant und für den Einsatz in Umgebungen geeignet, in denen die Datenmengen groß sind und eine exakte Zählung nicht erforderlich ist. Diese Methode bietet eine gute Balance zwischen Performance und Genauigkeit, solange keine parallelen Datenänderungen stattfinden, die zu Abweichungen führen könnten.
sp_spaceused
sp_spaceused
ist nützlich, wenn nicht nur die Anzahl der Zeilen, sondern auch zusätzliche Informationen zur Größe einer Tabelle benötigt werden, beispielsweise für Speicherplatzanalysen oder Ad-hoc-Abfragen zur allgemeinen Tabellenstatistik. Die Methode ist effizient, wenn es darum geht, schnell eine Übersicht über die Tabelle zu erhalten, aber weniger geeignet, wenn das Ergebnis in weiteren Abfragen verarbeitet werden soll.
Nach Lade-Vorgängen in ein Data Warehouse
Nach abgeschlossenen Lade-Vorgängen in ein Data Warehouse sind die Zeilenanzahlen, die durch alle drei Methoden (COUNT(*)
, sys.dm_db_partition_stats
und sp_spaceused
) ermittelt werden, identisch, da die Metadaten nach einem COMMIT
aktualisiert werden und somit alle Änderungen widerspiegeln. Während des Ladevorgangs selbst kann es jedoch zu Unterschieden kommen, da sys.dm_db_partition_stats
und sp_spaceused
auf Metadaten basieren, die möglicherweise noch nicht aktualisiert wurden, solange die Änderungen nicht durch ein COMMIT
bestätigt wurden. Daher sollte während eines laufenden Ladevorgangs Vorsicht bei der Interpretation der Ergebnisse geboten sein, da noch nicht endgültige (nicht committed) Datenänderungen nicht in den Metadaten reflektiert werden.