Daten gruppieren

  • Beitrags-Autor:
  • Beitrags-Kategorie:Abfragen

Bis hierher haben wir die Daten als originäre Datensätze betrachtet, wie sie in den abgefragten Tabellen stehen. In der Praxis haben wir jedoch oft das Bedürfnis, Datensätze nach verschiedenen Kriterien zu gruppieren, z.B. um die Umsätze eines Jahres zu aggregieren. Das Jahr bildet dann eine Gruppe mit einer Umsatzsumme.

GROUP BY

Zum Gruppieren bietet SQL die Klausel GROUP BY an. Alle dahinter notierten Spalten werden hinsichtlich gleicher Attributwerte zusammengefasst.

Wir hatten uns bereits die DISTINCT-Klausel angesehen, mit der wir Dubletten zu einem Datensatz haben zusammenfassen können. Als Beispiel haben wir dort die verschiedenen Alterseinstufungen der Filme ermittelt, die im Verleih sind. Selbiges können wir auch mithilfe von GROUP BY erreichen.

An dem einfachen Beispiel können Sie gleich die Syntax erkennen. Das Ergebnis ist wie folgt:

+------+
|rating|
+------+
|NC-17 |
|G     |
|PG-13 |
|R     |
|PG    |
+------+

Das Resultat ist identisch zu unsere ehedem mit DISTINCT formulierten Abfrage. Datensätze mit gleichen Attributausprägungen werden zusammengefasst. Insofern unterscheiden sich DISTINCT und GROUP BY, zumindest vordergründig, nicht. Dennoch ist GROUP BY ein vollkommen anderer Schnack. – In Bezug auf das obige Beispiel ist GROUP BY ein „armeseliger Ersatz“ für DISTINCT.

Die Krux besteht darin, GROUP BY in Verbindung mit Aggregationen zu verwenden. Bilden beispielsweise die Summe der von einem Kunden geleisteten Zahlen. Alle Kunden, die Zahlungen geleistet haben, haben dies in der Regel mehrfach getan. Wenn wir also nicht an den einzelnen Datensätzen interessiert sind, sondern an den jeweiligen Summen der Zahlungen, so bildet die jeweilige Summe peo Kunde eine Aggregation. Der Name eines Kunden bildet eine Gruppe, hinter der sich die einzelnen Zahlungen „verbergen“, die wir in einem zusammengefassten Wert, der jeweiligen Summe, ausgeben wollen.

sum() ist eine der fünf Standard-Aggregationsfunktionen von SQL. Wenn wir eine solche Funktion zur Zusammenfassung von Datensätzen verwenden, müssen wir zwingend, die anderen Spalten, so sie nicht ebenfalls aggregiert werden, mittels GROUP BY gruppieren. Tun wir dies nicht, so meldet uns SQL einen Fehler.

+---------+----------+------+
|last_name|first_name|total |
+---------+----------+------+
|Abney    |Rafael    |97.79 |
|Adam     |Nathaniel |133.72|
|Adams    |Kathleen  |92.73 |
|Alexander|Diana     |105.73|
|Allard   |Gordon    |160.68|
|Allen    |Shirley   |126.69|
...
(599 Datensätze insgesamt)

Variieren wir das Statement ein wenig. Konkatenieren wir den Nach- und Vornamen eines jeden Kunden zu jeweils einer Zeichenkette.

Beachten Sie die Gruppierung. Hier habe ich den Zeichenkettenausdruck aus der SELECT-Klausel als Gruppierungskriterium aufgenommen. Sie können hier, jedenfalls bei den meisten Datenbankmanagementsystemen, nicht den Aliasbezeichner der Spalte verwenden. Das entspricht dem SQL-Standard. In PostgreSQL ist dies jedoch zulässig, sodass wir auch das Folgende schreiben dürfen.

So der so, in der ORDER BY-Klausel können wir uns definitiv nicht mehr auf die Nach- und Vornamen als einzelne Datenwerte beziehen. Es gibt sie in der zugrundeliegenden Datenmenge nicht mehr.

Unwahrscheinlich, jedoch vielleicht dennoch gewollt, können wir dennoch separat und abweichend von der Konkatenierung erst nach Vor – und dann nach Nachnamen sortieren. In einem solchen Fall, müssen wir (wiederum zwingend) in der GROUP BY-Klausel, nach den einzelnen Spalten gruppieren.

Das mag Sie jetzt im ersten Moment etwas verwirren. Sehen Sie es aber positiv. Sie haben hierdurch flexible Möglichkeiten, Gruppen und Sortierungen zu bestimmen. Wichtig ist lediglich, alle nicht aggregierten Spalten müssen gruppiert werden.

Aggregationsfunktionen

SQL bietet Ihnen die folgenden Aggregationsfunktionen:

  • avg(): Durchschnitt (Mittelwert)
  • count(): Anzahl
  • min(): Minimum
  • max(): Maximum
  • sum(): Summe

Probieren wir die Funktionen einfach dadurch aus, für jeden Kunden eine kleine Statistik zu erstellen.

+-----------------+-----+------+-------+-------+------------------+
|customer_name    |count|total |minimum|maximum|avg               |
+-----------------+-----+------+-------+-------+------------------+
|Abney, Rafael    |21   |97.79 |0.99   |9.99   |4.6566666666666667|
|Adam, Nathaniel  |28   |133.72|0.99   |9.99   |4.7757142857142857|
|Adams, Kathleen  |27   |92.73 |0.99   |9.99   |3.4344444444444444|
|Alexander, Diana |27   |105.73|0.99   |7.99   |3.9159259259259259|
|Allard, Gordon   |32   |160.68|0.99   |10.99  |5.02125           |
|Allen, Shirley   |31   |126.69|0.99   |8.99   |4.0867741935483871|
|Alvarez, Charlene|27   |114.73|0.99   |10.99  |4.2492592592592593|
...
(599 Datensätze insgesamt)

Zur Übung erstellen wir noch eine weitere Statistik. Wir wollen uns ansehen, wie sich unsere monatlichen Umsätze über die Zeit entwickelt haben.

payment_date ist ein sogenannter TIMESTAMP, ein Datentyp der Datum und Uhrzeit (bis hin zu Sekundenbruchteilen) speichert. Wir benötigen die Informationen zu Jahr und Monat. Das Jahr dürfen wir nicht vergessen. Würden wir nur den Monat beachten, würden wir die Monate verschiedener Jahre zusammenfassen. Wir bemühen die PostgreSQL-Funktion to_char(). Bei anderen Datenbankmanagementsystemen heißt die Funktion in der Regel anders. Wir verwenden die Funktion und übergeben ihr das Zahldatum und einen Formatstring als Argumente. Der Formatstring drückt aus, was wir aus dem Zeitstempel extrahieren wollen.

+-------+-----+--------+-------+-------+------------------+
|month  |count|total   |minimum|maximum|avg               |
+-------+-----+--------+-------+-------+------------------+
|2005-05|1157 |4824.43 |0.99   |11.99  |4.1697752808988764|
|2005-06|2312 |9631.88 |0.99   |11.99  |4.166038062283737 |
|2005-07|6711 |28373.89|0.99   |11.99  |4.2279675160184771|
|2005-08|5687 |24072.13|0.99   |11.99  |4.2328345349041674|
|2006-02|182  |514.18  |0      |9.98   |2.8251648351648352|
+-------+-----+--------+-------+-------+------------------+

Aggregationen und NULL-Werte

Es gibt, zwischen den Aggregationsfunktionen kleine, jedoch feine, Unterschiede. So verlangt sum() sicherlich ein numerisches Argument, um rechnen zu können. Bei count() ist der Datentyp des Arguments unerheblich. Zählen kann man alles, sogar ganze Zeilen (*).

Allen Aggregationsfunktionen ist gemein, NULL-Werte zu ignorieren. Sie erinnern sich hoffentlich, dass wir bei den Adressen, in der Postleitzahlenspalte NULL-Werte gefunden haben. Wir wollen sie uns nochmals vornehmen, um das diesbezügliche Verhalten von Aggregationsfunktionen besser zu verstehen.

Zuerst zählen wir die Zeilen. Dazu verwenden wir den Asterisk. Danach zählen wir die IDs und zuletzt die Postleitzahlen. Irgendwo wird sich ein Eintrag in einer Spalte eines Datensatzes befinden. Daher ist der Asterisk der beste Kandidat, um die Anzahl von Datensätzen zu ermitteln. Weil address_id der Primärschlüssel der Tabelle ist, muss dort ebenso zwingend ein Eintrag vorhanden sein – auch ein guter Kandidat. Wenn jedoch irgendwo, in einer explizit angegebenen Spalte, hier postal_code ein NULL auftritt, wird er nicht gezählt.

+----------+---------+------------------+
|count_rows|count_ids|count_postal_codes|
+----------+---------+------------------+
|603       |603      |599               |
+----------+---------+------------------+

Dieses Verhalten, NULL zu ignorieren, ist allen Aggregationsfunktionen gemeinsam!

Gruppenauswahl mit HAVING

Datensätze haben wir mittels einer WHERE-Bedingung ausgewählt. In Verbindung mit Gruppierung bestimmt diese auch weiterhin, welche Datensätze einer Tabelle in einer Abfrage Berücksichtigung finden. Um nun aber, nachdem wir Gruppen gebildet haben, nur die gruppierten Datensätze auszuwählen, die uns interessieren, verwenden wir HAVING.

Betrachten wir hierzu nochmals unsere oben erstellte Kundenstatistik. Wir interessieren uns für die Kunden, die weniger als 100 Dollar umgesetzt haben. Für sie wollen wir eine Werbeaktion starten. Wir müssen also in Erfahrung bringen, wer diese Kunden sind.

+------------------+-----+-----+-------+-------+------------------+
|customer_name     |count|total|minimum|maximum|avg               |
+------------------+-----+-----+-------+-------+------------------+
|Abney, Rafael     |21   |97.79|0.99   |9.99   |4.6566666666666667|
|Adams, Kathleen   |27   |92.73|0.99   |9.99   |3.4344444444444444|
|Andrew, Jose      |25   |96.75|0.99   |7.99   |3.87              |
|Andrews, Ida      |23   |76.77|0.99   |5.99   |3.3378260869565217|
|Aquino, Oscar     |20   |99.8 |0.99   |8.99   |4.99              |
|Armstrong, Melanie|25   |92.75|0.99   |5.99   |3.71              |
...
(insgesamt 204 Datensätze)

Spätestens in der HAVING-Klausel dürfen wir nicht mehr mit Aliasbezeichnern arbeiten. – Es gibt jedoch Datenbankmanagementsysteme, die dies, entgegen dem SQL-Standard, erlauben. PostgreSQL verhält sich jedoch ganz getreu dem Standard.

Beachten Sie: Die Einschränkung in der HAVING-Klausel schränkt nicht die Anzahl der originär zu verarbeitenden Datensätze ein. Das würde eine WHERE-Klausel besorgen. HAVING bestimmt eine Gruppenauswahlbedingung. Das bedeutet auch, dass die folgende Formulierung ganz, ganz schlecht ist.

Augenscheinlich interessieren wir uns ausschließlich für die Kundin „Annette Olson“. Das Ergebnis passt auch. Jedoch verarbeitet unser Datenbankserver hier auch alle anderen Kunden, erstellt die Statistiken und erst am Ende wird unsere Kundin herausgefiltert. Besser wäre es gewesen, erst die Kundin zu filtern und (in diesem Fall) auf einen Gruppenfilter zu verzichten.

Lange Rede, kurzer Sinn: Verwenden Sie WHERE für die Datensatzauswahl und HAVING für die Gruppenauswahl. Achten Sie darauf an der korrekten Stelle zu filtern. Eine Gruppenauswahl ist bereits syntaktisch auf HAVING eingeschränkt. Wie oben gezeigt, besteht aber die Möglichkeit, eine eigentliche Datensatzauswahl in HAVING zu formulieren, was zu ineffizientem Code führt! – Einige gute Datenbank-Entwicklungssysteme, wie z.B. DataGrip, erkennen das und warnen davor.