SQL SELECT COUNT: Syntax, NULL-Werte, Performance und Beispiele
Eine SELECT-Abfrage mit COUNT(...) ermittelt, wie viele Zeilen oder Werte die Bedingungen einer Abfrage erfüllen. Die drei wichtigsten Varianten sind COUNT(*) zum Zählen sämtlicher Zeilen, COUNT(expression) zum Erfassen aller Nicht-NULL-Werte eines Ausdrucks sowie COUNT(DISTINCT expression) zum Zählen eindeutiger Nicht-NULL-Werte. COUNT lässt sich mit FROM, WHERE, GROUP BY und HAVING kombinieren, um beispielsweise zu bestimmen, wie viele abgeschlossene Bestellungen einem bestimmten Kunden zugeordnet sind.
Dieses Tutorial behandelt die Syntax, den Umgang mit NULL-Werten, Performance-Aspekte, bedingtes Zählen mit CASE WHEN, Joins, Unterabfragen sowie datenbankspezifische Besonderheiten von MySQL 8.x, PostgreSQL 15+, SQL Server 2022 und Oracle 19c. Sämtliche Beispiele basieren auf demselben Schema mit zwei Tabellen, das sich in eine kompatible Datenbank übernehmen lässt.
Die wichtigsten Erkenntnisse
COUNTist eine Aggregatfunktion, mit der sich nach dem Anwenden von Filtern die Anzahl von Zeilen oder Nicht-NULL-Werten zusammenfassen lässt.COUNT(*),COUNT(column)undCOUNT(DISTINCT column)beantworten unterschiedliche Fragestellungen, insbesondere wenn Duplikate oder NULL-Werte vorkommen.COUNT(*)berücksichtigt jede Zeile der Eingabemenge, währendCOUNT(column)Zeilen überspringt, in denen die betreffende Spalte NULL enthält.- In Kombination mit
GROUP BYkannCOUNTWerte pro Gruppe ermitteln. MitHAVINGlassen sich diese gruppierten Ergebnisse anschließend filtern. COUNT(DISTINCT column)entfernt doppelte Nicht-NULL-Werte vor dem Zählen. NULL selbst wird dabei nicht berücksichtigt.COUNT(CASE WHEN ... THEN 1 END)ermöglicht es, mehrere voneinander unabhängige Bedingungen innerhalb eines einzigen Tabellendurchlaufs zu zählen.- Funktionen für näherungsweise Zählungen unterscheiden sich je nach Datenbanksystem und sollten als optionale Optimierung verstanden werden, nicht als standardmäßiger Ersatz für exakte Ergebnisse.
Voraussetzungen
- Zugriff auf einen SQL-Client, der mit einer Datenbank verbunden ist und das Ausführen von Leseabfragen ermöglicht.
- Eine Beispieldatenbank mit MySQL 8.x, PostgreSQL 15+, SQL Server 2022 oder Oracle 19c+, wenn die Beispiele praktisch nachvollzogen werden sollen.
- Grundkenntnisse zu
SELECT,INSERTundCREATE TABLE, damit die Beispieldaten angelegt werden können.
Beispielschema für dieses Tutorial
Führe die folgenden DDL- und DML-Anweisungen einmal aus. NULL-Werte in den Spalten status und amount dienen dazu, das Verhalten von NULL zu demonstrieren. Mehrere Kunden teilen sich dieselben Städte, damit sich GROUP BY sinnvoll zeigen lässt. Der Kunde Hank besitzt keine Bestellungen und veranschaulicht dadurch das Verhalten eines LEFT JOIN. Für Datumswerte werden einfache Zeichenketten verwendet, sodass das Skript unverändert mit MySQL 8.x, PostgreSQL 15+, SQL Server 2022 und Oracle 19c ausgeführt werden kann.
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(100),
status VARCHAR(20),
signup_date DATE
);
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount DECIMAL(10, 2),
status VARCHAR(20),
order_date DATE
);
INSERT INTO customers (customer_id, name, city, status, signup_date) VALUES
(1, 'Alice', 'Austin', 'active', '2024-01-15'),
(2, 'Bob', 'Austin', NULL, '2024-02-10'),
(3, 'Carol', 'Boston', 'active', '2024-03-05'),
(4, 'Dan', 'Boston', 'pending', '2024-04-20'),
(5, 'Eve', 'Chicago', 'active', '2024-05-12'),
(6, 'Frank', 'Chicago', 'inactive', '2024-06-01'),
(7, 'Grace', 'Denver', 'active', '2024-07-08'),
(8, 'Hank', 'Denver', 'active', '2024-09-01');
INSERT INTO orders (order_id, customer_id, amount, status, order_date) VALUES
(101, 1, 100.00, 'completed', '2024-10-01'),
(102, 1, NULL, 'pending', '2024-10-02'),
(103, 2, 50.00, 'completed', '2024-10-03'),
(104, 2, 75.50, 'cancelled', '2024-10-04'),
(105, 3, 200.00, 'completed', '2024-10-05'),
(106, 3, 120.00, 'pending', '2024-10-06'),
(107, 4, 90.00, 'completed', '2024-10-07'),
(108, 5, 45.00, 'pending', '2024-10-08'),
(109, 5, 60.00, 'completed', '2024-10-09'),
(110, 6, 30.00, 'cancelled', '2024-10-10'),
(111, 7, 85.00, 'completed', '2024-10-11'),
(112, 7, 95.00, 'pending', '2024-10-12'),
(113, 3, 110.00, 'completed', '2024-10-13');
Verwandte SQL-Konzepte sind SQL JOINs sowie die Aggregatfunktionen SUM, AVG und COUNT.
Was ist die SQL-Funktion COUNT?
COUNT kommt immer dann zum Einsatz, wenn ermittelt werden soll, wie viele Zeilen vorhanden sind, wie viele Werte einer Spalte nicht NULL sind oder wie viele unterschiedliche Werte vorkommen. Die Funktion gehört zu den am häufigsten verwendeten Aggregatfunktionen in SQL und findet sich unter anderem in Dashboards, Validierungsabfragen, der Seitennavigation und in Berichtssystemen relationaler Datenbanken. COUNT wird nach einer Filterung mit WHERE ausgeführt, kann mit Window Functions kombiniert werden und führt leicht zu falschen Ergebnissen, wenn verschiedene COUNT-Varianten verwendet werden, ohne deren Regeln für NULL-Werte zu berücksichtigen.
COUNT-Syntax und Parameter
COUNT besitzt drei gebräuchliche Formen. Welche Variante sinnvoll ist, hängt davon ab, welche Information ermittelt werden soll.
-- Jede Zeile zählen, auch wenn einzelne Spalten NULL enthalten
SELECT COUNT(*) FROM table_name;
-- Nur Zeilen zählen, in denen die angegebene Spalte NICHT NULL ist
SELECT COUNT(column_name) FROM table_name;
-- Eindeutige Nicht-NULL-Werte einer Spalte zählen
SELECT COUNT(DISTINCT column_name) FROM table_name;
Die folgenden Regeln sind dabei wichtig:
- Ergibt ein
expressionfür eine Zeile NULL, wird diese Zeile vonCOUNT(expression)nicht berücksichtigt. - Standard-SQL erlaubt innerhalb von
COUNT(DISTINCT ...)nur ein Argument. Müssen eindeutige Kombinationen aus mehreren Spalten gezählt werden, kann dafür eine abgeleitete Tabelle verwendet werden. COUNTliefert0, wenn keine passenden Eingabezeilen existieren.SUM,AVG,MINundMAXgeben in diesem Fall dagegenNULLzurück.
Verwende WHERE für Bedingungen, die vor der Aggregation gelten sollen. HAVING wird verwendet, wenn eine Bedingung erst nach GROUP BY auf das aggregierte Ergebnis angewendet werden soll.
Was COUNT zurückgibt und wie NULL-Werte behandelt werden
Zusammengefasst zählt COUNT(*) Zeilen unabhängig von enthaltenen NULL-Werten. COUNT(column) überspringt Zeilen, in denen die angegebene Spalte NULL ist. COUNT(DISTINCT column) entfernt sowohl NULL-Werte als auch Duplikate.
Zeigt beispielsweise ein Bericht acht Kunden und ein anderer nur sieben, sollte zuerst geprüft werden, ob eine Abfrage COUNT(*) und die andere COUNT(status) verwendet.
Führe folgende Abfrage mit dem Beispielschema aus:
SELECT COUNT(*) AS customer_rows, COUNT(status) AS non_null_status
FROM customers;
Ausgabe:
customer_rows | non_null_status
---------------+-----------------
8 | 7
customer_rows hat den Wert 8, weil die Tabelle customers acht Zeilen enthält. non_null_status ergibt 7, da Bob in der Spalte status einen NULL-Wert besitzt und COUNT(status) diese Zeile deshalb nicht mitzählt.
Wie verhält sich COUNT(1)?
In älteren Oracle- und DB2-Anwendungen ist häufig COUNT(1) anzutreffen. Da der konstante Wert 1 niemals NULL ist, werden damit ebenso wie bei COUNT(*) sämtliche Zeilen gezählt.
Hinweis: COUNT(*) und COUNT(1) führen in MySQL 8.x, PostgreSQL 15+, SQL Server 2019+ und Oracle 19c+ zum gleichen Ausführungsplan. Beide Varianten ermitteln die Anzahl der Zeilen, ohne dafür den Wert einer konkreten Spalte auswerten zu müssen. Die frühere Annahme, COUNT(1) sei schneller, geht auf ein Optimizer-Verhalten von Oracle 7 zurück, das bereits vor langer Zeit korrigiert wurde. Für neuen SQL-Code ist COUNT(*) in der Regel die passendere Wahl, da es die standardisierte Schreibweise für das Zählen von Zeilen ist.
NULL-Werte zählen
Häufig stellt sich auch die Frage, wie viele Zeilen in einer bestimmten Spalte tatsächlich NULL enthalten. Eine direkte Variante von COUNT dafür existiert nicht, jedoch lassen sich dafür zwei Vorgehensweisen verwenden:
-- Variante 1: Anzahl der Nicht-NULL-Werte von der Gesamtzahl abziehen
SELECT COUNT(*) - COUNT(status) AS null_status_count
FROM customers;
-- Variante 2: CASE-Ausdruck zählen, der ausschließlich für NULL den Wert 1 liefert
SELECT COUNT(CASE WHEN status IS NULL THEN 1 END) AS null_status_count
FROM customers;
Ausgabe beider Abfragen:
null_status_count
-------------------
1
Die Variante mit CASE lässt sich unkompliziert auf mehrere Spalten erweitern. Die Subtraktion ist dagegen kürzer, insbesondere wenn selektive Indizes genutzt werden können.
COUNT(*) vs. COUNT(column) vs. COUNT(DISTINCT column)
Unterschiede beim Umgang mit NULL-Werten
Die folgende Abfrage beantwortet drei unterschiedliche Fragen zur Tabelle orders: Wie viele Zeilen existieren insgesamt, wie viele Bestellungen besitzen einen Nicht-NULL-Wert in amount und wie viele unterschiedliche Kunden haben Bestellungen aufgegeben?
SELECT COUNT(*) AS all_orders,
COUNT(amount) AS orders_with_amount,
COUNT(DISTINCT customer_id) AS distinct_buyers
FROM orders;
Ausgabe:
all_orders | orders_with_amount | distinct_buyers
------------+--------------------+-----------------
13 | 12 | 7
all_orders beträgt 13, da die Tabelle dreizehn Zeilen enthält. orders_with_amount ergibt 12, weil die Bestellung 102 in amount den Wert NULL enthält und deshalb von COUNT(amount) übersprungen wird. distinct_buyers liefert 7, weil sieben verschiedene Kunden mindestens eine Bestellung aufgegeben haben. Hank besitzt keine Bestellung.
Performance und Verwendung von Indizes
Grundsätzlich sind COUNT(*) und COUNT(1) effizient und funktional gleichwertig. COUNT(column) verhält sich ähnlich, schließt jedoch NULL-Werte aus. COUNT(DISTINCT column) ist üblicherweise die aufwendigste Variante, weil vor dem Zählen zunächst doppelte Werte entfernt werden müssen. Ohne einen passenden Covering Index kann der Aufwand mit zunehmender Datenmenge deutlich steigen.
COUNT(*) und COUNT(1)
Beide Varianten fragen lediglich die Anzahl der Zeilen ab und benötigen dafür keine konkreten Spalteninhalte. Die Query Planner von MySQL 8.x, PostgreSQL 15+, SQL Server 2019+ und Oracle 19c+ wählen für beide Formen im Allgemeinen denselben Ausführungsplan. Häufig wird dabei der kleinste verfügbare Index verwendet, über den sich die Abfrage beantworten lässt. Wird ein COUNT(*) auf einer großen Tabelle unerwartet langsam, kann der Ausführungsplan beispielsweise mit EXPLAIN ANALYZE in PostgreSQL oder EXPLAIN FORMAT=TREE in MySQL 8.x untersucht werden.
COUNT(column)
Ein Index-only-Ausführungspfad kann vorteilhaft sein, wenn die betreffende Spalte indexiert ist. Da NULL-Werte nicht berücksichtigt werden, kann das Ergebnis unterhalb von COUNT(*) liegen. Ein messbarer Vorteil gegenüber COUNT(*) zeigt sich insbesondere bei sehr großen Tabellen mit breiten Datensätzen.
COUNT(DISTINCT column)
Das Entfernen von Duplikaten erfordert meist eine Sortier- oder Hash-Operation. Fehlt ein passender Covering Index, können Abfragen über Tabellen mit mehreren hundert Millionen Zeilen entsprechend lange dauern.
Wenn COUNT(DISTINCT) zu teuer wird, kommen häufig drei Ansätze infrage:
- Covering Index: Ein Index über die für
DISTINCTrelevanten Spalten kann es dem Query Planner ermöglichen, sortierte Schlüssel direkt zu verarbeiten, anstatt umfangreiche Tabellendaten zu hashen. - Näherungsweise Zählungen: Funktionen wie
APPROX_COUNT_DISTINCTin SQL Server 2019+ oderhllin PostgreSQL eignen sich beispielsweise für Dashboards, bei denen kein mathematisch exakter Wert notwendig ist. - Materialisierte Aggregationen: Wird derselbe eindeutige Wert immer wieder benötigt, können vorberechnete Zusammenfassungen wiederholte Berechnungen vermeiden.
Hinweis: Mit EXPLAIN ANALYZE SELECT COUNT(DISTINCT customer_id) FROM orders; lässt sich in PostgreSQL erkennen, ob die Datenbank beispielsweise einen indexgestützten Aggregate -> Sort-Plan oder einen speicherintensiveren Aggregate -> HashAggregate-Ansatz verwendet. Vor einer Optimierung sollte deshalb immer zuerst der Ausführungsplan geprüft werden.
Vergleich: Wann welche COUNT-Variante sinnvoll ist
| Variante | Zählt NULL-Zeilen | Zählt Duplikate | Typischer Anwendungsfall | Index-Verhalten |
|---|---|---|---|---|
COUNT(*) |
Ja | Ja | Gesamtzahl der Zeilen | Kann einen geeigneten Index oder einen Tabellenscan verwenden |
COUNT(column) |
Nein | Ja | Nicht-NULL-Werte einer Spalte zählen | Kann von einem Index auf der entsprechenden Spalte profitieren |
COUNT(DISTINCT column) |
Nein | Nein | Eindeutige Nicht-NULL-Werte zählen | Erfordert häufig Sortierung oder Hashing und nutzt einen Index nicht immer effizient |
SQL SELECT COUNT mit WHERE
WHERE filtert Datensätze vor der Aggregation. Dadurch verarbeitet COUNT ausschließlich Zeilen, die die angegebene Bedingung erfüllen.
Zeilen mit einer einzelnen Bedingung zählen
Eine einzelne WHERE-Bedingung schränkt die Eingabemenge ein, bevor COUNT ausgeführt wird. Die folgende Abfrage zählt alle Bestellungen, deren status den Wert 'completed' besitzt:
SELECT COUNT(*) AS completed_orders
FROM orders
WHERE status = 'completed';
Ausgabe:
completed_orders
------------------
7
Sieben der dreizehn Zeilen in orders haben den Status 'completed'. Die übrigen sechs Bestellungen besitzen entweder den Status 'pending' oder 'cancelled'.
Mehrere Bedingungen mit AND und OR zählen
Mit AND und OR können mehrere Bedingungen miteinander kombiniert werden. Die folgende Abfrage zählt abgeschlossene Bestellungen, deren amount größer als 50 ist:
SELECT COUNT(*) AS completed_large_orders
FROM orders
WHERE status = 'completed' AND amount > 50;
Ausgabe:
completed_large_orders
------------------------
6
Sechs der sieben abgeschlossenen Bestellungen liegen oberhalb des Grenzwerts. Die Bestellung 103 wird nicht berücksichtigt, weil ihr Betrag genau 50 beträgt und die Bedingung ausdrücklich einen Wert größer als 50 verlangt.
Datensätze innerhalb eines Zeitraums zählen
Berichte müssen häufig ermitteln, wie viele Ereignisse innerhalb eines bestimmten aktuellen Zeitraums stattgefunden haben. Dafür kann CURRENT_DATE in PostgreSQL und Oracle, CURDATE() in MySQL oder GETDATE() in SQL Server innerhalb der Filterbedingung verwendet werden.
-- PostgreSQL 15+ / Oracle 19c
SELECT COUNT(*) AS orders_last_7_days
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '7 days';
-- MySQL 8.x
SELECT COUNT(*) AS orders_last_7_days
FROM orders
WHERE order_date >= CURDATE() - INTERVAL 7 DAY;
-- SQL Server 2022
SELECT COUNT(*) AS orders_last_7_days
FROM orders
WHERE order_date >= DATEADD(day, -7, CAST(GETDATE() AS DATE));
Für die Beispieldaten liefern alle drei Abfragen dasselbe Ergebnis, wenn das aktuelle Datum 2024-10-13 ist:
orders_last_7_days
--------------------
8
Wird eine solche Datumsbedingung in einer produktiven Umgebung häufig ausgeführt, kann ein B-Tree-Index auf order_date sinnvoll sein.
SQL SELECT COUNT mit GROUP BY
Ergebnisse gruppieren und Werte pro Gruppe zählen
GROUP BY erzeugt für jeden eindeutigen Wert der Gruppierungsspalte eine Ergebniszeile. COUNT ermittelt dabei, wie viele Eingabezeilen zu jeder Gruppe gehören. Die folgende Abfrage zählt die Kunden pro Stadt:
SELECT city, COUNT(*) AS customers_in_city
FROM customers
GROUP BY city
ORDER BY city;
Ausgabe:
city | customers_in_city
---------+-------------------
Austin | 2
Boston | 2
Chicago | 2
Denver | 2
Jede Stadt liefert den Wert 2, weil die Beispieldaten bewusst jeweils zwei Kunden pro Stadt enthalten. In realen Datenbeständen unterscheiden sich diese Werte üblicherweise. Eine Sortierung nach dem Zähler in absteigender Reihenfolge ist ein gängiges Muster, um Gruppen nach ihrer Größe zu ordnen.
Gruppierte Ergebnisse mit HAVING filtern
HAVING wird erst nach der Gruppierung angewendet. WHERE filtert dagegen einzelne Eingabezeilen vor der Aggregation. Ein Join zwischen customers und orders mit anschließender Filterung anhand des berechneten Zählers verdeutlicht den Unterschied.
SELECT c.city, COUNT(o.order_id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.city
HAVING COUNT(o.order_id) >= 4
ORDER BY c.city;
Ausgabe:
city | order_count
--------+-------------
Austin | 4
Boston | 4
Chicago und Denver erscheinen nicht im Ergebnis, weil ihre Bestellmengen mit 3 beziehungsweise 2 unterhalb des mit HAVING festgelegten Grenzwerts liegen.
SQL SELECT COUNT mit DISTINCT
DISTINCT entfernt doppelte Werte, bevor COUNT die endgültige Anzahl berechnet.
Eindeutige Werte einer Spalte zählen
COUNT(DISTINCT column) gibt an, wie viele unterschiedliche Nicht-NULL-Werte innerhalb einer Spalte vorkommen. Die Variante eignet sich also für die Frage, wie viele verschiedene Werte existieren, und nicht dafür, wie viele Zeilen auf diese Werte verweisen. Die folgende Abfrage zählt die unterschiedlichen Städte in customers:
SELECT COUNT(DISTINCT city) AS distinct_cities
FROM customers;
Ausgabe:
distinct_cities
-----------------
4
Die acht Kunden verteilen sich auf vier Städte: Austin, Boston, Chicago und Denver. DISTINCT entfernt die mehrfach vorkommenden Städtenamen, sodass COUNT den Wert 4 zurückgibt.
COUNT DISTINCT im Vergleich zu COUNT auf einer deduplizierten Unterabfrage
Sollen eindeutige Kombinationen aus zwei oder mehr Spalten gezählt werden, kann eine DISTINCT-Abfrage in eine abgeleitete Tabelle verschoben und anschließend deren Zeilenzahl ermittelt werden. Da orders mehrere Einträge für denselben Kunden enthalten kann, eignet sich die Kombination aus customer_id und status für dieses Beispiel.
SELECT COUNT(*) AS distinct_customer_status_pairs
FROM (SELECT DISTINCT customer_id, status FROM orders) AS pairs;
Ausgabe:
distinct_customer_status_pairs
--------------------------------
12
Aus den dreizehn Bestellzeilen werden zwölf eindeutige Kombinationen aus (customer_id, status), weil Carols doppelte Kombination mit dem Status 'completed' zusammengeführt wird.
Warum nicht COUNT(DISTINCT customer_id, status)?
Standard-SQL erlaubt innerhalb von COUNT(DISTINCT ...) nur einen einzelnen Ausdruck. PostgreSQL und Oracle akzeptieren diese Mehrspaltenform nicht direkt. SQL Server und MySQL unterstützen entsprechende Varianten mit datenbankspezifischen Besonderheiten bei NULL-Werten, die sich zudem zwischen Versionen unterscheiden können. Eine abgeleitete Tabelle ist daher eine portable Lösung mit konsistentem Verhalten.
Hinweis zur Performance
Sowohl COUNT(DISTINCT ...) als auch die Variante mit einer abgeleiteten Tabelle verursachen Aufwand für die Deduplizierung. Ein Index, der sämtliche in DISTINCT verwendeten Spalten abdeckt, kann den Sortieraufwand reduzieren. Fehlt ein solcher Index, kann der Optimizer auf Hashing oder externe Sortierung zurückgreifen. Der Ausführungsplan lässt sich mit EXPLAIN ANALYZE in PostgreSQL, EXPLAIN FORMAT=TREE in MySQL 8.x oder SET STATISTICS PROFILE ON in SQL Server untersuchen.
SQL COUNT mit CASE WHEN
Bedingtes Zählen mit CASE WHEN innerhalb von COUNT
COUNT(CASE WHEN ... THEN 1 END) übergibt COUNT nur dann einen Nicht-NULL-Wert, wenn die angegebene Bedingung erfüllt ist. Dadurch lassen sich mehrere bedingte Zählungen innerhalb eines einzigen Tabellendurchlaufs ausführen. Die folgende Abfrage liefert die Anzahl der Bestellungen für verschiedene Statuswerte:
SELECT
COUNT(CASE WHEN status = 'completed' THEN 1 END) AS completed_orders,
COUNT(CASE WHEN status = 'pending' THEN 1 END) AS pending_orders,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) AS cancelled_orders
FROM orders;
Ausgabe:
completed_orders | pending_orders | cancelled_orders
------------------+----------------+------------------
7 | 4 | 2
Addiert ergeben die drei Werte 13 und entsprechen damit dem Ergebnis von COUNT(*) FROM orders. Drei separate Abfragen mit unterschiedlichen WHERE-Bedingungen würden die Tabelle dreimal durchsuchen. Die Variante mit CASE WHEN kann alle drei Werte in einem einzigen Durchlauf berechnen.
Mehrere Bedingungen in einer einzigen Abfrage zählen
Jeder CASE-Ausdruck kann sich auf andere Spalten beziehen oder mehrere Bedingungen mit AND und OR kombinieren. Dadurch können in einem einzigen Tabellendurchlauf mehrere voneinander unabhängige Werte ermittelt werden. Das folgende Beispiel zählt abgeschlossene Bestellungen mit vorhandenem Betrag sowie ausstehende Bestellungen, bei denen der Betrag fehlt:
SELECT
COUNT(CASE WHEN status = 'completed' AND amount IS NOT NULL THEN 1 END) AS completed_paid,
COUNT(CASE WHEN status = 'pending' AND amount IS NULL THEN 1 END) AS pending_missing_amount
FROM orders;
Ausgabe:
completed_paid | pending_missing_amount
----------------+------------------------
7 | 1
SQL COUNT mit JOINs
JOIN-Operationen können die Anzahl der Zeilen vervielfachen, bevor COUNT ausgeführt wird. Wird dieses Verhalten nicht berücksichtigt, können Zähler in Testdaten korrekt erscheinen, aber bei realen Eins-zu-viele-Beziehungen falsche Ergebnisse liefern.
Das Fan-out-Problem
Ein typischer Fehler besteht darin, Kunden nach einem Join mit orders zu zählen, während gleichzeitig nach dem Bestellstatus gefiltert wird:
SELECT COUNT(*) AS customer_count
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed';
Ausgabe:
customer_count
----------------
7
Das Ergebnis 7 steht hier für sieben abgeschlossene Bestellzeilen und nicht für sieben unterschiedliche Kunden. Carol trägt allein zwei Zeilen zum Ergebnis bei, da sie zwei Bestellungen mit dem Status 'completed' besitzt.
Wenn stattdessen eindeutige Kunden gezählt werden sollen, muss COUNT(DISTINCT) auf den Kundenschlüssel angewendet werden:
SELECT COUNT(DISTINCT c.customer_id) AS customers_with_completed_orders
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'completed';
Ausgabe:
customers_with_completed_orders
---------------------------------
6
Bei jeder Eins-zu-viele-Verknüpfung vor einer Aggregation muss geklärt werden, ob die Zeilen auf der Viele-Seite mit COUNT(*) oder COUNT(many_table.id) gezählt werden sollen oder ob eindeutige Elemente auf der Eins-Seite mit COUNT(DISTINCT one_table.id) benötigt werden. Eine Verwechslung beider Ansätze führt zu schwer erkennbaren Zählfehlern.
COUNT mit INNER JOIN
INNER JOIN behält ausschließlich Kunden, für die mindestens eine passende Bestellung existiert. Deshalb unterscheiden sich die resultierenden Zähler von Outer-Join-Varianten, sobald Kunden ohne Bestellung vorhanden sind. Die folgende Abfrage gruppiert nach Kunden und zählt deren Bestellungen:
SELECT c.name, COUNT(o.order_id) AS order_count
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
ORDER BY c.customer_id;
Ausgabe:
name | order_count
-------+-------------
Alice | 2
Bob | 2
Carol | 3
Dan | 1
Eve | 2
Frank | 1
Grace | 2
Hank erscheint nicht im Ergebnis, weil für ihn keine passende Zeile in orders existiert. Durch den INNER JOIN wird er vollständig ausgeschlossen. Ein LEFT JOIN behält dagegen auch Kunden ohne Bestellung bei. Dadurch muss anschließend bewusst entschieden werden, wie solche Zeilen gezählt werden sollen.
COUNT mit LEFT JOIN und NULL-Werten
LEFT JOIN behält Kunden auch dann im Ergebnis, wenn keine passende Bestellung vorhanden ist. COUNT(*) zählt die vom Join erzeugte Platzhalterzeile mit, obwohl auf der rechten Seite keine Übereinstimmung besteht. COUNT(o.order_id) überspringt dieselbe Zeile, weil die Bestell-ID NULL ist.
Warnung: Nach einem LEFT JOIN zählt COUNT(*) auch eine verbundene Zeile, bei der sämtliche Spalten der rechten Tabelle NULL enthalten. COUNT(o.order_id) berücksichtigt ausschließlich tatsächlich vorhandene Bestellungen. Die Wahl der falschen Variante verändert deshalb die Ergebnisse für Datensätze ohne verknüpfte Untereinträge.
SELECT c.name,
COUNT(*) AS rows_after_join,
COUNT(o.order_id) AS matched_orders
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.name
ORDER BY c.customer_id;
Ausgabe:
name | rows_after_join | matched_orders
-------+-----------------+----------------
Alice | 2 | 2
Bob | 2 | 2
Carol | 3 | 3
Dan | 1 | 1
Eve | 2 | 2
Frank | 1 | 1
Grace | 2 | 2
Hank | 1 | 0
Bei Hank wird der Unterschied deutlich. rows_after_join hat den Wert 1, weil der LEFT JOIN für ihn eine Platzhalterzeile erzeugt, in der sämtliche Spalten aus orders NULL sind. matched_orders ist dagegen 0, da COUNT(o.order_id) den NULL-Wert überspringt. Eine ungeeignete COUNT-Variante kann das sichtbare Ergebnis somit unbemerkt von null auf eins verändern.
SQL COUNT in Unterabfragen und abgeleiteten Tabellen
COUNT in einer WHERE-Unterabfrage verwenden
Eine korrelierte Unterabfrage wird für jede Zeile der äußeren Abfrage ausgeführt. Mit COUNT kann dadurch jeder Kunde mit seinem eigenen aggregierten Wert verglichen werden. Die folgende Abfrage liefert Kunden mit mehr als zwei Bestellungen:
SELECT name, city
FROM customers c
WHERE (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id) > 2;
Ausgabe:
name | city
-------+--------
Carol | Boston
Carol ist der einzige Treffer, weil sie drei Bestellungen mit den IDs 105, 106 und 113 besitzt. Alle anderen Kunden haben höchstens zwei Bestellungen. Korrelierte Unterabfragen lassen sich leicht formulieren, können bei großen Datenmengen jedoch teuer werden, da die innere Abfrage für jede äußere Zeile erneut ausgeführt wird. Wenn lediglich geprüft werden muss, ob überhaupt ein passender Datensatz vorhanden ist, kann EXISTS geeigneter sein.
COUNT in einer abgeleiteten Tabelle verwenden
Abgeleitete Tabellen ermöglichen es, Aggregatwerte übersichtlich für eine äußere WHERE-Bedingung bereitzustellen.
SELECT city_rollups.city, city_rollups.order_count
FROM (
SELECT c.city, COUNT(o.order_id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.city
) AS city_rollups
WHERE city_rollups.order_count >= 3
ORDER BY city_rollups.city;
Ausgabe:
city | order_count
---------+-------------
Austin | 4
Boston | 4
Chicago | 3
SQL COUNT in verschiedenen Datenbanksystemen
Hinweis: Funktionen wie APPROX_COUNT_DISTINCT oder PostgreSQL hll tauschen einen Teil der Genauigkeit gegen höhere Geschwindigkeit ein. Für Berechnungen, die zwingend exakte Werte benötigen, beispielsweise finanzielle Salden, sollten sie nicht verwendet werden.
COUNT in MySQL
Bei InnoDB kann COUNT(*) relativ effizient ausgeführt werden, wenn der Optimizer einen schmalen sekundären Index durchsuchen kann, anstatt den größeren geclusterten Primärindex zu verarbeiten.
-- MySQL 8.x
SELECT COUNT(*) AS orders_total FROM orders;
Ausgabe:
orders_total
--------------
13
Warum InnoDB einen sekundären Index verwenden kann
Die Blattseiten eines geclusterten Index enthalten vollständige Datensätze. Sekundäre Indexblätter speichern dagegen Schlüssel und Verweise. Bei breiten Tabellen kann COUNT(*) deshalb bevorzugt den kleinsten sekundären Index verwenden. Mit EXPLAIN lässt sich überprüfen, welchen Index der Optimizer ausgewählt hat:
-- MySQL 8.x
EXPLAIN SELECT COUNT(*) FROM orders;
Beispielausgabe, wobei sich die einzelnen Spalten je nach MySQL-Version geringfügig unterscheiden können:
id | select_type | table | type | key | rows | Extra
----+-------------+--------+-------+-----------------+------+-------------
1 | SIMPLE | orders | index | idx_customer_id | 13 | Using index
Das Feld key zeigt den Index, den der Optimizer ausgewählt hat. Ein Eintrag, der nicht PRIMARY entspricht, beispielsweise idx_customer_id, weist auf die Verwendung eines sekundären Index hin. Using index im Feld Extra bestätigt, dass die Abfrage direkt anhand des Index beantwortet werden kann, ohne vollständige Zeilendaten zu laden.
Hinweis zu älteren MyISAM-Tabellen
Wenn eine ältere MySQL-Tabelle selbst bei einer sehr großen Zeilenanzahl nahezu sofort ein Ergebnis für COUNT(*) liefert, sollte zunächst die verwendete Storage Engine geprüft werden, bevor eine außergewöhnlich effiziente Optimierung angenommen wird:
SELECT table_name, engine
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'orders';
Ausgabe:
table_name | engine
------------+--------
orders | InnoDB
MyISAM speicherte die exakte Anzahl der Tabellenzeilen in den Metadaten und konnte ein ungefiltertes COUNT(*) deshalb ohne das Durchsuchen der eigentlichen Daten beantworten. InnoDB besitzt keinen vergleichbaren, allgemein gültigen Zähler, da durch MVCC die sichtbare Zeilenanzahl vom jeweiligen Transaktions-Snapshot abhängt. Dieser Unterschied fällt häufig bei der Migration älterer MyISAM-Tabellen auf InnoDB auf. Das Feld engine in information_schema.tables bietet eine schnelle Möglichkeit, die aktuell verwendete Storage Engine zu überprüfen.
COUNT in PostgreSQL einschließlich Window Functions
PostgreSQL unterstützt COUNT auch als Window Function, beispielsweise mit COUNT(*) OVER (PARTITION BY ...). Während GROUP BY jede Gruppe zu einer einzelnen Ergebniszeile zusammenfasst, bleiben bei einer Window Function sämtliche Detailzeilen erhalten und erhalten zusätzlich den entsprechenden Gruppenzähler. Das eignet sich für Berichte, in denen sowohl die einzelnen Datensätze als auch gruppierte Gesamtwerte benötigt werden:
-- PostgreSQL 15+
SELECT customer_id,
name,
city,
COUNT(*) OVER (PARTITION BY city) AS customers_in_city
FROM customers
ORDER BY city, customer_id;
Ausgabe:
customer_id | name | city | customers_in_city
-------------+-------+---------+------------------
1 | Alice | Austin | 2
2 | Bob | Austin | 2
3 | Carol | Boston | 2
4 | Dan | Boston | 2
5 | Eve | Chicago | 2
6 | Frank | Chicago | 2
7 | Grace | Denver | 2
8 | Hank | Denver | 2
Jede Zeile behält ihre ursprünglichen Detailinformationen und erhält zusätzlich die Spalte customers_in_city mit der Anzahl der Datensätze in der jeweiligen Partition. In den Beispieldaten beträgt der Wert in jeder Zeile 2, da jede Stadt genau zwei Kunden enthält. Bei realen Daten hängt der Wert entsprechend von der jeweiligen Partition ab.
Für große Datenbestände mit vielen eindeutigen Werten kann die PostgreSQL-Erweiterung hll exakte Ergebnisse gegen näherungsweise Werte mit konstantem Speicherverbrauch austauschen. Sie wird einmal pro Datenbank installiert:
-- PostgreSQL 15+
CREATE EXTENSION IF NOT EXISTS hll;
Die folgende Abfrage schätzt mithilfe von HyperLogLog die Anzahl unterschiedlicher Kunden. Die verschachtelten Funktionen hashen zunächst jede customer_id, fassen die Hashwerte anschließend in einer hll-Struktur zusammen und lesen daraus schließlich die geschätzte Kardinalität:
-- PostgreSQL 15+
SELECT hll_cardinality(hll_add_agg(hll_hash_integer(customer_id))) AS approx_distinct_buyers
FROM orders;
Ausgabe:
approx_distinct_buyers
------------------------
7
Die Schätzung entspricht hier dem exakten Ergebnis von COUNT(DISTINCT customer_id), da HyperLogLog bei kleinen Kardinalitäten auf lineares Zählen zurückgreift. Bei größeren Datenbeständen ist mit einem Fehler von ungefähr zwei Prozent zu rechnen, während der Speicherverbrauch im niedrigen Kilobyte-Bereich bleibt. Solche Näherungen eignen sich beispielsweise für Dashboards oder Telemetriedaten. Für Abrechnung und Datenabgleich sollte dagegen weiterhin ein exaktes COUNT(DISTINCT ...) eingesetzt werden.
COUNT in Oracle
Oracle unterstützt das grundlegende Muster COUNT(*) OVER (PARTITION BY ...) ähnlich wie PostgreSQL. Zu den Oracle-spezifischen Besonderheiten gehören die Tabelle DUAL, standardmäßig großgeschriebene nicht in Anführungszeichen gesetzte Bezeichner und umfangreiche Möglichkeiten für analytische Window Frames.
DUAL ist eine integrierte Oracle-Tabelle mit einer einzelnen Zeile. Sie wird häufig verwendet, um einen Ausdruck auszuwerten, ohne dafür Anwendungsdaten abzufragen. Dadurch eignet sie sich unter anderem für grundlegende Prüfungen in Stored Procedures und Migrationsskripten:
-- Oracle 19c
SELECT COUNT(*) AS one
FROM dual;
Ausgabe:
ONE
-----
1
COUNT(*) liefert bei DUAL den Wert 1, weil diese Tabelle genau eine Zeile enthält. Vergleichbare Abfragen werden in bestehenden Anwendungen beispielsweise verwendet, um eine Datenbankverbindung oder die Kompilierbarkeit von prozeduralem Code zu prüfen.
Oracle stellt COUNT außerdem als analytische Funktion mit expliziten Window-Frame-Klauseln bereit. Damit können fortlaufende Zähler Zeile für Zeile erzeugt werden. Der Frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW weist die Datenbank an, von der ersten sortierten Zeile bis einschließlich der aktuell verarbeiteten Zeile zu zählen:
-- Oracle 19c
SELECT order_id,
order_date,
COUNT(*) OVER (
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS running_order_count
FROM orders
ORDER BY order_date;
Ausgabe, im ursprünglichen Beispiel gekürzt:
ORDER_ID | ORDER_DATE | RUNNING_ORDER_COUNT
----------+-------------+---------------------
101 | 2024-10-01 | 1
102 | 2024-10-02 | 2
103 | 2024-10-03 | 3
...
113 | 2024-10-13 | 13
Nicht in Anführungszeichen gesetzte Oracle-Bezeichner werden üblicherweise in Großbuchstaben ausgegeben, wie hier bei ORDER_ID. Für näherungsweise Kardinalitätsberechnungen stellt Oracle ab Version 12c 12.1.0.2 APPROX_COUNT_DISTINCT(column) bereit, darunter auch in Oracle 19c und 23c. Die Funktion kann eingesetzt werden, wenn ein exaktes COUNT(DISTINCT ...) zu lange benötigt. Wiederholt verwendete Näherungswerte können zusätzlich in materialisierten Aggregationen vorbereitet werden.
COUNT in SQL Server mit Transact-SQL
SQL Server 2019 führte APPROX_COUNT_DISTINCT als integrierte, auf HyperLogLog basierende Alternative zu einem exakten COUNT(DISTINCT ...) ein. Die Funktion eignet sich für Situationen, in denen beispielsweise Dashboards eindeutige Werte aus Tabellen mit mehreren hundert Millionen Zeilen benötigen und eine Abweichung von ungefähr zwei Prozent zugunsten eines vorhersehbaren Speicherverbrauchs und einer schnelleren Ausführung akzeptabel ist.
-- SQL Server 2022
SELECT APPROX_COUNT_DISTINCT(customer_id) AS approx_buyers
FROM orders;
Ausgabe:
approx_buyers
---------------
7
In diesem Beispiel beträgt das Ergebnis exakt 7, weil SQL Server ebenso wie PostgreSQL hll bei kleinen Kardinalitäten lineares Zählen verwendet. Bei einer Tabelle mit einer Milliarde Zeilen würde der Näherungswert voraussichtlich innerhalb von ungefähr zwei Prozent des exakten Ergebnisses liegen und könnte gleichzeitig erheblich schneller berechnet werden. Die Funktion eignet sich damit beispielsweise für Monitoring- und Kapazitätsplanungsabfragen. Für finanzielle Berichte ist weiterhin ein exaktes COUNT(DISTINCT ...) angemessen.
Dokumentation: COUNT und APPROX_COUNT_DISTINCT.
Häufig gestellte Fragen
Was ist der Unterschied zwischen COUNT(*) und COUNT(column_name) in SQL?
COUNT(*) zählt jede Zeile des Ergebnisses, selbst wenn eine oder mehrere Spalten NULL enthalten. COUNT(column_name) berücksichtigt dagegen nur Zeilen, in denen die angegebene Spalte nicht NULL ist. Für die Gesamtzahl aller Zeilen sollte COUNT(*) verwendet werden.
Berücksichtigt SQL COUNT NULL-Werte?
COUNT(*) zählt Zeilen auch dann, wenn sie irgendwo NULL-Werte enthalten. COUNT(column_name) überspringt NULL innerhalb der konkret angegebenen Spalte. COUNT(DISTINCT column_name) entfernt sowohl NULL-Werte als auch doppelte Werte, bevor gezählt wird.
Wie kann ich in SQL Zeilen zählen, die eine bestimmte Bedingung erfüllen?
Eine WHERE-Bedingung wird vor COUNT angewendet, beispielsweise mit SELECT COUNT(*) FROM orders WHERE status = 'completed';. Sollen innerhalb einer Abfrage mehrere unterschiedliche Bedingungen gezählt werden, können einzelne CASE WHEN-Ausdrücke in COUNT verwendet werden.
Wie funktioniert COUNT mit GROUP BY?
GROUP BY definiert Gruppen von Zeilen und COUNT liefert für jede dieser Gruppen eine eigene Anzahl. Spalten in der SELECT-Liste, die nicht aggregiert werden, müssen ebenfalls in GROUP BY erscheinen oder innerhalb einer Aggregatfunktion verwendet werden.
Wie unterscheidet sich die Performance von COUNT(*) und COUNT(DISTINCT column)?
COUNT(*) muss keine Duplikate entfernen. COUNT(DISTINCT column) benötigt dagegen in der Regel eine Sortier- oder Hash-Operation, sofern ein geeigneter Covering Index diesen Aufwand nicht reduziert.
Kann COUNT zusammen mit einem JOIN verwendet werden?
Ja. Eine Eins-zu-viele-Verknüpfung kann vor der Aggregation mehrere verbundene Zeilen erzeugen. Bei einem LEFT JOIN sollte COUNT(*) nur dann verwendet werden, wenn eine nicht zugeordnete Zeile trotzdem als einzelne Platzhalterzeile zählen soll. Andernfalls sollte ein Nicht-NULL-Schlüssel der verknüpften Tabelle gezählt werden.
Wie verwende ich COUNT zusammen mit DISTINCT?
Mit SELECT COUNT(DISTINCT column_name) FROM table_name; lässt sich die Anzahl eindeutiger Nicht-NULL-Werte ermitteln. Die grundlegende Syntax ist in MySQL 8.x, PostgreSQL 15+, SQL Server 2022 und Oracle 19c gleich.
Funktioniert COUNT in MySQL, PostgreSQL, Oracle und SQL Server gleich?
Die grundlegenden COUNT-Varianten verhalten sich bei standardnahen SQL-Abfragen weitgehend gleich. Unterschiede bestehen unter anderem bei der Syntax von Window Functions, der Unterstützung für APPROX_COUNT_DISTINCT, den Ausführungsstrategien von InnoDB für ein ungefiltertes COUNT(*) und der optionalen PostgreSQL-Erweiterung hll.
Wann sollte EXISTS anstelle von COUNT verwendet werden?
EXISTS ist geeignet, wenn lediglich festgestellt werden muss, ob mindestens eine passende Zeile vorhanden ist. Eine Bedingung wie WHERE (SELECT COUNT(*) ...) > 0 zählt zunächst sämtliche passenden Datensätze. EXISTS kann dagegen abbrechen, sobald der erste Treffer gefunden wurde.
-- Bei großen Tabellen langsamer: zählt zunächst sämtliche passenden Zeilen
SELECT name FROM customers c
WHERE (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.customer_id) > 0;
-- Schneller: beendet die Suche nach dem ersten Treffer
SELECT name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
Beide Abfragen liefern für die Beispieldaten sieben Kunden zurück, also alle außer Hank. Je größer die verbundene oder abgefragte Tabelle wird, desto deutlicher kann sich der Performance-Unterschied bemerkbar machen.
Warum liefert COUNT bei einer leeren Tabelle 0 statt NULL?
COUNT gibt 0 zurück, wenn die Eingabemenge keine Zeilen enthält. SUM, AVG, MIN und MAX liefern in derselben Situation dagegen NULL. Ein zusätzlicher COALESCE-Ausdruck um COUNT, der lediglich NULL durch null ersetzen soll, ist daher nicht erforderlich.
Fazit
Dieses Tutorial hat die drei Varianten der Aggregatfunktion COUNT sowie die jeweiligen Regeln für NULL-Werte und Duplikate erläutert. Behandelt wurden außerdem das Filtern vor einer Aggregation mit WHERE, das Filtern gruppierter Ergebnisse mit HAVING, gruppenbezogene Zähler mit GROUP BY, bedingtes Zählen mit CASE WHEN, das Verhalten von Eins-zu-viele-Verknüpfungen mit INNER JOIN und LEFT JOIN, Unterabfragen und abgeleitete Tabellen sowie datenbankspezifische Besonderheiten von MySQL 8.x, PostgreSQL 15+, SQL Server 2022 und Oracle 19c. Zusätzlich wurden Möglichkeiten für näherungsweise Zählungen wie APPROX_COUNT_DISTINCT und die PostgreSQL-Erweiterung hll betrachtet.
Mit diesen Mustern lässt sich für unterschiedliche Anforderungen die passende COUNT-Variante auswählen. NULL-Werte können gezählt, doppelte Ergebnisse nach Eins-zu-viele-Joins vermieden und reine Zeilenzähler von eindeutigen Entitätszählern unterschieden werden. Ausführungspläne können vor der Optimierung langsamer COUNT(DISTINCT ...)-Abfragen untersucht werden, während sich gleichzeitig zwischen portablem ANSI-SQL und datenbankspezifischen Erweiterungen wechseln lässt.
Für eine weiterführende Vertiefung bieten sich die Konzepte GROUP BY, JOIN und DISTINCT sowie grundlegende SQL-Abfragemuster an. Werden diese Techniken mit realen Datenmengen getestet, kann eine verwaltete Datenbankumgebung dabei helfen, Entwicklungs- und Testdatenverkehr von produktiven Systemen zu trennen.


