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(*) und COUNT(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

  1. Cursor-Definition: Der Cursor table_cursor wählt die Tabellen aus, die das Kriterium erfüllen (Schema lw und Tabellennamen, die nicht mit _NEW enden).
  2. Schleife: In der Schleife durchläuft der Cursor jede Tabelle.
  3. Dynamische SQL für COUNT(*): Die Zeilenanzahl wird mit dynamischem SQL berechnet (sp_executesql), da der Tabellenname dynamisch zusammengesetzt wird.
  4. Einfügen der Ergebnisse: Die berechnete Zeilenanzahl und der Zeitstempel werden für jede Tabelle in die RowCountResults-Tabelle eingefügt.
  5. 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ßlich row_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 den Clustered 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

  1. Cursor-Definition und Schleife: Der Cursor table_cursor durchläuft alle Tabellen, die das Kriterium erfüllen.
  2. sp_spaceused verwenden: Für jede Tabelle wird sp_spaceused aufgerufen, und die Ergebnisse werden in eine temporäre Tabelle @Result eingefügt.
  3. Zeilenanzahl abrufen: Die Zeilenanzahl wird aus der temporären Tabelle @Result entnommen.
  4. Einfügen der Ergebnisse: Die Tabelle RowCountResults speichert den DateStamp, den vollständigen TableName und die Count-Spalte für jede Tabelle.
  5. 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(*) und COUNT(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 als COUNT(*).
  • 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.

Exkurs – Zeilen von