2024-06-20

Datenverarbeitung in der Kommandozeile: DuckDB als Unix-Tool nutzen

Gábor Szárnyas

In diesem Blogpost tauchen wir ins Terminal ein, um DuckDB mit traditionellen Tools zu vergleichen, die in Unix-Shells (Bash, Zsh usw.) genutzt werden. Wir lösen mehrere Probleme, die Operationen wie Projektion und Filterung brauchen, um die Unterschiede zwischen SQL-Abfragen in DuckDB und spezialisierten Kommandozeilentools zu zeigen. Dabei zeigen wir ein paar coole Features wie DuckDBs mächtigen CSV-Reader und den Positionalen-Join-Operator. Los geht’s!

Die Unix-Philosophie

Um die Bühne zu setzen, erinnern wir uns an die Unix-Philosophie. Sie besagt, dass Programme:

Unix-ähnliche Systeme wie macOS, Linux und WSL unter Windows haben diese Philosophie angenommen. Tools wie grep, sed und sort sind allgegenwärtig und werden in Shell-Skripten weit genutzt.

Als zweckgebautes Datenverarbeitungstool passt DuckDB ziemlich gut zur Unix-Philosophie. Erstens wurde es als schnelles In-Process-analytisches SQL-Datenbanksystem entworfen (eine Sache tun und sie gut tun). Zweitens hat es einen eigenständigen Kommandozeilenclient, der CSV-Dateien konsumieren und produzieren kann (zusammenarbeiten) und auch das Lesen und Schreiben von Textströmen unterstützt (Textströme handhaben). Dank dessen funktioniert DuckDB gut im Ökosystem der Unix-CLI-Tools, wie in mehreren Beiträgen gezeigt.

Portabilität und Nutzbarkeit

Unix-CLI-Tools sind schnell, robust und auf allen großen Plattformen verfügbar, haben aber oft umständliche Syntax, die schwer zu merken ist. Schlimmer noch: Diese Tools kommen oft mit leichten Unterschieden zwischen Systemen – denken Sie an die Unterschiede zwischen GNU sed und macOS’ sed oder die Unterschiede zwischen Regex-Syntax zwischen Programmen, treffend festgehalten in Donald Knuths Bemerkung „I define Unix as 30 definitions of regular expressions living under one roof.“

Es gibt Shells, die speziell für Dataframe-Verarbeitung spezialisiert sind, wie das Nushell-Projekt, ältere Unix-Shells (z. B. die Bourne-Shell sh und Bash) sind aber immer noch die am weitesten verbreiteten, besonders auf Servern.

Gleichzeitig haben wir DuckDB, ein extrem portables Datenbanksystem, das auf allen Plattformen dieselbe SQL-Syntax nutzt. Mit Version 1.0.0, die kürzlich erschienen ist, ist DuckDBs Syntax – basierend auf dem bewährten und weit genutzten PostgreSQL-Dialekt – jetzt in einem stabilen Zustand. Ein weiteres attraktives Feature von DuckDB ist, dass es eine interaktive Shell bietet, die schnelles Debuggen unterstützt. Außerdem ist DuckDB in mehreren Host-Sprachen sowie im Browser über WebAssembly verfügbar, sodass DuckDB-SQL-Skripte, wenn Sie sich je entscheiden, Ihre SQL-Skripte außerhalb der Shell zu nutzen, ohne Änderungen in eine große Vielfalt von Umgebungen portiert werden können.

Datenverarbeitung mit Unix-Tools und DuckDB

Im Folgenden geben wir Beispiele für die Umsetzung einfacher Datenverarbeitungsaufgaben mit den CLI-Tools, die in den meisten Unix-Shells bereitstehen, und mit DuckDB-SQL-Abfragen. Wir nutzen DuckDB v1.0.0 und führen es im In-Memory-Modus aus. Dieser Modus macht für die Probleme, die wir angehen, Sinn, weil wir keine Tabellen anlegen und die Operationen nicht speicherintensiv sind, es also keine Daten zu persistieren oder auf die Platte auszulagern gibt.

Datensätze

Wir nutzen die vier Eingabedateien mit Informationen zu Städten und Flughäfen in den Niederlanden.

[`pop.csv`](/data/cli/pop.csv), die Bevölkerung jeder der 10 bevölkerungsreichsten Städte. ```csv city,province,population Amsterdam,North Holland,905234 Rotterdam,South Holland,656050 The Hague,South Holland,552995 Utrecht,Utrecht,361924 Eindhoven,North Brabant,238478 Groningen,Groningen,234649 Tilburg,North Brabant,224702 Almere,Flevoland,218096 Breda,North Brabant,184716 Nijmegen,Gelderland,179073 ```
[`area.csv`](/data/cli/area.csv), die Fläche jeder der 10 bevölkerungsreichsten Städte. ```csv city,area Amsterdam,219.32 Rotterdam,324.14 The Hague,98.13 Utrecht,99.21 Eindhoven,88.92 Groningen,197.96 Tilburg,118.13 Almere,248.77 Breda,128.68 Nijmegen,57.63 ```
[`cities-airports.csv`](/data/cli/cities-airports.csv), die [IATA-Codes](https://en.wikipedia.org/wiki/IATA_airport_code) ziviler Flughäfen, die gegebene Städte bedienen. ```csv city,IATA Amsterdam,AMS Haarlemmermeer,AMS Eindhoven,EIN Groningen,GRQ Eelde,GRQ Maastricht,MST Beek,MST Rotterdam,RTM The Hague,RTM ```
[`airport-names.csv`](/data/cli/airport-names.csv), die Flughafennamen zu gegebenen IATA-Codes. ```csv IATA,airport name AMS,Amsterdam Airport Schiphol EIN,Eindhoven Airport GRQ,Groningen Airport Eelde MST,Maastricht Aachen Airport RTM,Rotterdam The Hague Airport ```

Sie können alle Eingabedateien als einzelne Zip-Datei herunterladen.

Spalten projizieren

Spalten zu projizieren ist ein sehr häufiger Datenverarbeitungsschritt. Nehmen wir die Datei pop.csv und projizieren die erste und letzte Spalte, city und population.

Unix-Shell: cut

In der Unix-Shell nutzen wir den Befehl cut und geben das Trennzeichen der Datei (-d) und die zu projizierenden Spalten (-f) an.

Terminal window
cut -d , -f 1,3 pop.csv

Das erzeugt die folgende Ausgabe:

city,population
Amsterdam,905234
Rotterdam,656050
The Hague,552995
Utrecht,361924
Eindhoven,238478
Groningen,234649
Tilburg,224702
Almere,218096
Breda,184716
Nijmegen,179073

DuckDB: SELECT

In DuckDB können wir den CSV-Reader nutzen, um die Daten zu laden, und dann die SELECT-Klausel mit Spaltenindizes (#i) nutzen, um die zu projizierenden Spalten zu bezeichnen:

SELECT #1, #3 FROM 'pop.csv';

Beachten Sie, dass wir kein Schema definieren oder die Daten in eine Tabelle laden mussten. Stattdessen haben wir einfach 'pop.csv' in der FROM-Klausel genutzt, wie wir es mit einer regulären Tabelle tun würden. DuckDB erkennt, dass das eine CSV-Datei ist, und ruft die Funktion read_csv auf, die automatisch den Dialekt der CSV-Datei (Trennzeichen, Vorhandensein von Quotes usw.) sowie das Schema der Tabelle ableitet. Das erlaubt uns, Spalten einfach mit SELECT #1, #3 zu projizieren. Wir könnten auch die besser lesbare Syntax SELECT city, population nutzen.

Um die Ausgabe der Lösungen mit Unix-Tools und DuckDB gleichwertig zu machen, packen wir die Abfrage in eine COPY ... TO-Anweisung:

COPY (
SELECT #1, #3 FROM 'pop.csv'
) TO '/dev/stdout/';

Diese Abfrage erzeugt dasselbe Ergebnis wie die Ausgabe des Unix-Befehls oben.

Um das in einen eigenständigen CLI-Befehl zu verwandeln, können wir den DuckDB-Kommandozeilenclient mit dem Argument -c ⟨query⟩{:.language-sql .highlight} aufrufen, das die SQL-Abfrage ausführt und beendet, sobald sie fertig ist. Mit dieser Technik kann die Abfrage oben in den folgenden One-Liner verwandelt werden:

Terminal window
duckdb -c "COPY (SELECT #1, #3 FROM 'pop.csv') TO '/dev/stdout/'"

Im Folgenden lassen wir die Codeblöcke mit dem eigenständigen Befehl duckdb{:.language-sql .highlight} weg: Alle Lösungen können im Template duckdb -c ⟨query⟩{:.language-sql .highlight} ausgeführt werden und liefern dasselbe Ergebnis wie die Lösungen mit Unix-Tools.

Dateien sortieren

Eine weitere häufige Aufgabe ist, Dateien nach gegebenen Spalten zu sortieren. Ranken wir die Städte innerhalb der Provinzen nach ihrer Bevölkerung. Dafür müssen wir die Datei pop.csv zuerst nach dem Namen der province aufsteigend sortieren, dann nach der population absteigend. Dann geben wir zuerst die Spalte province zurück, gefolgt von den Spalten city und population.

Unix-Shell: sort

In der Unix-Shell verlassen wir uns auf das Tool sort. Wir geben das Trennzeichen der CSV-Datei mit dem Argument -t an und setzen die Schlüssel, nach denen sortiert werden soll, mit -k-Argumenten. Zuerst sortieren wir nach der zweiten Spalte (province) mit -k 2,2. Dann sortieren wir nach der dritten Spalte (population) und setzen die Reihenfolge auf umgekehrt (r) und numerisch (n) mit -k 3rn. Beachten Sie, dass wir den Header der Datei separat behandeln müssen: Wir nehmen die erste Zeile mit head -n 1 und den Rest der Zeilen mit tail -n +2, sortieren letzteren und kleben sie wieder mit dem Header zusammen. Schließlich führen wir eine Projektion aus, um die Spalten umzuordnen. Leider kann der Befehl cut die Spalten nicht umordnen, deshalb nutzen wir stattdessen awk:

Terminal window
(head -n 1 pop.csv; tail -n +2 pop.csv \
| sort -t , -k 2,2 -k 3rn) \
| awk -F , '{ print $2 "," $1 "," $3 }'

Das Ergebnis ist das folgende:

province,city,population
Flevoland,Almere,218096
Gelderland,Nijmegen,179073
Groningen,Groningen,234649
North Brabant,Eindhoven,238478
North Brabant,Tilburg,224702
North Brabant,Breda,184716
North Holland,Amsterdam,905234
South Holland,Rotterdam,656050
South Holland,The Hague,552995
Utrecht,Utrecht,361924

DuckDB: ORDER BY

In DuckDB laden wir einfach die CSV und geben die Spaltenreihenfolge über SELECT province, city, population an, dann setzen wir die Sortierkriterien auf die ausgewählten Spalten (province ASC und population DESC). Der CSV-Reader erkennt Typen automatisch, die Sortierung ist also standardmäßig numerisch. Schließlich umgeben wir die Abfrage mit einer COPY-Anweisung, um die Ergebnisse auf die Standardausgabe zu drucken.

COPY (
SELECT province, city, population
FROM 'pop.csv'
ORDER BY province ASC, population DESC
) TO '/dev/stdout/';

Spalten schneiden

Eine häufige Aufgabe ist, die Schnittmenge zweier Spalten zu berechnen, d. h. Entitäten zu finden, die in beiden vorhanden sind. Finden wir die Städte, die sowohl zu den 10 bevölkerungsreichsten Städten gehören als auch eigene Flughäfen haben.

Unix-Shell: comm

Die Unix-Lösung für die Schnittmenge nutzt das Tool comm, das zwei sortierte Dateien zeilenweise vergleichen soll. Wir cutten zuerst die relevante Spalte aus beiden Dateien. Wegen der Sortieranforderung wenden wir sort auf beide Eingaben an, bevor wir die Schnittmenge bilden. Die Schnittmenge wird mit comm -12 gebildet, wobei das Argument -12 bedeutet, dass wir nur Zeilen behalten wollen, die in beiden Dateien sind. Wir verlassen uns wieder auf head und tail, um die Header und den Rest der Dateien während der Verarbeitung separat zu behandeln und sie am Ende zusammenzukleben.

Terminal window
head -n 1 pop.csv | cut -d , -f 1; \
comm -12 \
<(tail -n +2 pop.csv | cut -d , -f 1 | sort) \
<(tail -n +2 cities-airports.csv | cut -d , -f 1 | sort)

Das Skript erzeugt die folgende Ausgabe:

city
Amsterdam
Eindhoven
Groningen
Rotterdam
The Hague

DuckDB: INTERSECT ALL

Die DuckDB-Lösung liest die CSV-Dateien, projiziert die Felder city und wendet die Klausel INTERSECT ALL an, um die Schnittmenge zu berechnen:

COPY (
SELECT city FROM 'pop.csv'
INTERSECT ALL
SELECT city FROM 'cities-airports.csv'
) TO '/dev/stdout/';

Zeilen zusammenkleben

Zeilen zeilenweise zusammenzukleben ist eine wiederkehrende Aufgabe. In unserem Beispiel wissen wir, dass die Dateien pop.csv und area.csv gleich viele Zeilen haben, sodass wir eine einzelne Datei erzeugen können, die sowohl die Bevölkerung als auch die Fläche jeder Stadt im Datensatz enthält.

Unix-Shell: paste

In der Unix-Shell nutzen wir den Befehl paste und entfernen das doppelte Feld city mit cut:

Terminal window
paste -d , pop.csv area.csv | cut -d , -f 1,2,3,5

Die Ausgabe ist die folgende:

city,province,population,area
Amsterdam,North Holland,905234,219.32
Rotterdam,South Holland,656050,324.14
The Hague,South Holland,552995,98.13
Utrecht,Utrecht,361924,99.21
Eindhoven,North Brabant,238478,88.92
Groningen,Groningen,234649,197.96
Tilburg,North Brabant,224702,118.13
Almere,Flevoland,218096,248.77
Breda,North Brabant,184716,128.68
Nijmegen,Gelderland,179073,57.63

DuckDB: POSITIONAL JOIN

In DuckDB können wir einen POSITIONAL JOIN nutzen. Dieser Join-Typ ist eine von DuckDBs SQL-Erweiterungen und bietet eine knappe Syntax, um Tabellen zeilenweise anhand der Position jeder Zeile in der Tabelle zu kombinieren. Das Joinen der zwei Tabellen mit POSITIONAL JOIN ergibt zwei city-Spalten – wir nutzen die Klausel EXCLUDE, um die doppelte Spalte zu entfernen:

COPY (
SELECT pop.*, area.* EXCLUDE city
FROM 'pop.csv'
POSITIONAL JOIN 'area.csv'
) TO '/dev/stdout/';

Filtern

Filtern ist eine weitere sehr häufige Operation. Dafür nutzen wir die Datei cities-airports.csv. Für jeden Flughafen enthält diese Datei seinen IATA-Code und die Hauptstädte, die er bedient:

city,IATA
Amsterdam,AMS
Haarlemmermeer,AMS
Eindhoven,EIN
...

Versuchen wir, zwei Abfragen zu formulieren:

  1. Alle Städte finden, deren Name auf dam endet.

  2. Alle Flughäfen finden, deren IATA-Code den ersten drei Buchstaben eines bedienten Stadtnamens entspricht, der Stadtname aber nicht auf dam endet.

Unix-Shell: grep

Um die erste Frage in der Unix-Shell zu beantworten, nutzen wir grep und den regulären Ausdruck ^[^,]*dam,:

Terminal window
grep "^[^,]*dam," cities-airports.csv

In diesem Ausdruck bezeichnet ^ den Zeilenanfang, [^,]* sucht nach einem String, der das Kommazeichen (das Trennzeichen) nicht enthält. Der Ausdruck dam, stellt sicher, dass das Ende des Strings im ersten Feld dam ist. Die Ausgabe ist:

Amsterdam,AMS
Rotterdam,RTM

Versuchen wir, die zweite Frage zu beantworten. Dafür müssen wir die ersten drei Zeichen im Feld city mit dem Feld IATA matchen, aber das müssen wir case-insensitive tun. Wir brauchen auch eine negative Bedingung, um die Zeilen auszuschließen, in denen der Stadtname auf dam endet. Beide Anforderungen sind mit einem einzelnen grep- oder egrep-Befehl schwer zu erreichen, weil ihnen die Unterstützung für zwei Features fehlt. Erstens unterstützen sie kein case-insensitives Matching mit einer Backreference (grep -i allein reicht dafür nicht). Zweitens unterstützen sie keine negativen Lookbehinds. Deshalb nutzen wir pcregrep und formulieren unsere Frage so:

Terminal window
pcregrep -i '^([a-z]{3}).*?(?<!dam),\1$' cities-airports.csv

Hier rufen wir pcregrep mit dem case-insensitiven Flag (-i) auf, das in pcregrep auch Backreferences wie \1 betrifft. Wir erfassen die ersten drei Buchstaben mit ([a-z]{3}) (z. B. Ams) und matchen sie mit dem zweiten Feld über die Backreference: ,\1$. Wir nutzen ein non-greedy .*?, um zum Ende des ersten Felds zu gehen, und wenden dann einen negativen Lookbehind mit dem Ausdruck (?<!dam) an, um sicherzustellen, dass das Feld nicht auf dam endet. Das Ergebnis ist eine einzelne Zeile:

Eindhoven,EIN

DuckDB: WHERE ... LIKE

Beantworten wir die Fragen jetzt in DuckDB. Um die erste Frage zu beantworten, können wir LIKE für Pattern Matching nutzen. Der Header sollte nicht Teil der Ausgabe sein, deshalb deaktivieren wir ihn mit HEADER false. Die vollständige Abfrage sieht so aus:

COPY (
FROM 'cities-airports.csv'
WHERE city LIKE '%dam'
) TO '/dev/stdout/' (HEADER false);

Für die zweite Frage nutzen wir String-Slicing, um die ersten drei Zeichen zu extrahieren, upper für Case-Insensitivity und NOT LIKE für die negative Bedingung:

COPY (
FROM 'cities-airports.csv'
WHERE upper(city[1:3]) = IATA
AND city NOT LIKE '%dam'
) TO '/dev/stdout/' (HEADER false);

Diese Abfragen geben genau dieselben Ergebnisse zurück wie die Lösungen mit grep und pcregrep.

In beiden Abfragen haben wir die FROM-first-Syntax genutzt. Wird die SELECT-Klausel weggelassen, wird die Abfrage ausgeführt, als wäre SELECT * genutzt worden, d. h. sie gibt alle Spalten zurück.

Dateien joinen

Tabellen zu joinen ist eine wesentliche Aufgabe in der Datenverarbeitung. Unser nächstes Beispiel nutzt einen Join, um Kombinationen aus Stadtname und Flughafenname zurückzugeben. Das wird erreicht, indem die Dateien cities-airports.csv und airport-names.csv über ihre IATA-Code-Felder gejoint werden.

Unix-Shell: join

Unix-Tools unterstützen das Joinen von Dateien über den Befehl join, der Zeilen zweier sortierter Eingaben über ein gemeinsames Feld joint. Damit das funktioniert, sortieren wir die Dateien nach ihren IATA-Feldern und führen dann den Join über die 2. Spalte der ersten Datei (-1 2) und die 1. Spalte der zweiten Datei (-2 1) aus. Wir müssen den Header weglassen, damit der Befehl join funktioniert, also tun wir genau das und konstruieren einen neuen Header mit einem echo-Befehl:

Terminal window
echo "IATA,city,airport name"; \
join -t , -1 2 -2 1 \
<(tail -n +2 cities-airports.csv | sort -t , -k 2,2) \
<(tail -n +2 airport-names.csv | sort -t , -k 1,1)

Das Ergebnis ist das folgende:

IATA,city,airport name
AMS,Amsterdam,Amsterdam Airport Schiphol
AMS,Haarlemmermeer,Amsterdam Airport Schiphol
EIN,Eindhoven,Eindhoven Airport
GRQ,Eelde,Groningen Airport Eelde
GRQ,Groningen,Groningen Airport Eelde
MST,Beek,Maastricht Aachen Airport
MST,Maastricht,Maastricht Aachen Airport
RTM,Rotterdam,Rotterdam The Hague Airport
RTM,The Hague,Rotterdam The Hague Airport

DuckDB

In DuckDB laden wir die CSV-Dateien und verbinden sie mit der Klausel NATURAL JOIN, die über Spalte(n) mit demselben Namen joint. Damit das Ergebnis mit dem der Unix-Lösung übereinstimmt, nutzen wir die Klausel ORDER BY ALL, die das Ergebnis nach allen Spalten sortiert, beginnend bei der ersten und für Tie-Breaking bis zur letzten Spalte durchgehend.

COPY (
SELECT "IATA", "city", "airport name"
FROM 'cities-airports.csv'
NATURAL JOIN 'airport-names.csv'
ORDER BY ALL
) TO '/dev/stdout/';

Strings ersetzen

Ihnen ist vielleicht aufgefallen, dass wir sehr saubere Datensätze nutzen. Das ist natürlich sehr unrealistisch, also reduzieren wir in einem bösen Twist die Datenqualität ein wenig:

Und während wir dabei sind, holen wir den Datensatz diesmal auch über HTTPS, mit der URL https://duckdb.org/data/cli/pop.csv.

Unix-Shell: curl und sed

Unter Unix werden Remote-Datensätze typischerweise über curl geholt. Die Ausgabe von curl wird in die nachfolgenden Verarbeitungsschritte gepiped, in diesem Fall eine Reihe von sed-Befehlen.

Terminal window
curl -s https://duckdb.org/data/cli/pop.csv \
| sed 's/\([^,]*,.*\) \(.*,[^,]*\)/\1_\2/g' \
| sed 's/,/;/g' \
| sed 's/\([0-9][0-9][0-9]\)$/,\1/'

Das ergibt die folgende Ausgabe:

city;province;population
Amsterdam;North_Holland;905,234
Rotterdam;South_Holland;656,050
The Hague;South_Holland;552,995
Utrecht;Utrecht;361,924
Eindhoven;North_Brabant;238,478
Groningen;Groningen;234,649
Tilburg;North_Brabant;224,702
Almere;Flevoland;218,096
Breda;North_Brabant;184,716
Nijmegen;Gelderland;179,073

DuckDB: httpfs und regexp_replace

In DuckDB nutzen wir die folgende Abfrage:

COPY (
SELECT
city,
replace(province, ' ', '_') AS province,
regexp_replace(population::VARCHAR, '([0-9][0-9][0-9])$', ',\1')
AS population
FROM 'https://duckdb.org/data/cli/pop.csv'
) TO '/dev/stdout/' (DELIMITER ';');

Beachten Sie, dass die FROM-Klausel jetzt eine HTTPS-URL statt einer einfachen CSV-Datei hat. Das Vorhandensein des Präfixes https:// löst aus, dass DuckDB die httpfs-Extension lädt und sie nutzt, um das JSON-Dokument zu holen. Wir nutzen die Funktion replace, um die Leerzeichen durch Unterstriche zu ersetzen, und die Funktion regexp_replace für die Ersetzung mit einem regulären Ausdruck. (Wir hätten auch String-Formatierungsfunktionen wie format und printf nutzen können). Um das Trennzeichen auf ein Semikolon zu ändern, serialisieren wir die Datei mit der COPY-Anweisung und der Option DELIMITER ';'.

JSON lesen

Als letzte Übung fragen wir die Zahl der Sterne ab, die dem Repository duckdb/duckdb auf GitHub gegeben wurden.

Unix-Shell: curl und jq

Mit Unix-Tools können wir curl nutzen, um die JSON-Datei von https://api.github.com zu holen, und ihre Ausgabe an jq pipen, um das JSON-Objekt abzufragen.

Terminal window
curl -s https://api.github.com/repos/duckdb/duckdb \
| jq ".stargazers_count"

DuckDB: read_json

In DuckDB nutzen wir die Funktion read_json und rufen sie mit der URL des Remote-HTTPS-Endpunkts auf. Das Schema der JSON-Datei wird automatisch erkannt, sodass wir einfach SELECT nutzen können, um das benötigte Feld zurückzugeben.

SELECT stargazers_count
FROM read_json('https://api.github.com/repos/duckdb/duckdb');

Ausgabe

Beide Befehle geben die aktuelle Zahl der Sterne des Repositories zurück.

Performance

An diesem Punkt fragen Sie sich vielleicht nach der Performance der DuckDB-Lösungen. Immerhin bestanden all unsere bisherigen Beispiele nur aus ein paar Zeilen, sodass ein Benchmark gegeneinander keine messbaren Performance-Unterschiede ergeben wird. Wechseln wir also zum niederländischen Bahnverkehrsdatensatz, den wir in einem früheren Blogpost genutzt haben, und formulieren ein anderes Problem.

Wir nutzen die Bahnverkehrsdatei 2023 (services-2023.csv.gz) und zählen die Zahl der Intercity-Verbindungen, die in diesem Jahr betrieben wurden.

Unter Unix können wir den Befehl gzcat nutzen, um die Datei csv.gz in eine Pipeline zu dekomprimieren. Dann können wir grep oder pcregrep (das performanter ist) nutzen und das Ganze mit dem Befehl wc abschließen, um die Zahl der Zeilen zu zählen (-l). In DuckDB unterstützt der eingebaute CSV-Reader auch komprimierte CSV-Dateien, sodass wir das ohne Extra-Konfiguration nutzen können.

Terminal window
gzcat services-2023.csv.gz | grep '^[^,]*,[^,]*,Intercity,' | wc -l
gzcat services-2023.csv.gz | pcregrep '^[^,]*,[^,]*,Intercity,' | wc -l
duckdb -c "SELECT count(*) FROM 'services-2023.csv.gz' WHERE \"Service:Type\" = 'Intercity';"

Wir testen die Tools auch auf unkomprimierter Eingabe:

Terminal window
gunzip -k services-2023.csv.gz
grep '^[^,]*,[^,]*,Intercity,' services-2023.csv | wc -l
pcregrep '^[^,]*,[^,]*,Intercity,' services-2023.csv | wc -l
duckdb -c "SELECT count(*) FROM 'services-2023.csv' WHERE \"Service:Type\" = 'Intercity';"

Um das Rauschen in den Messungen zu reduzieren, haben wir das Benchmarking-Tool hyperfine genutzt und die mittlere Ausführungszeit von 10 Läufen genommen. Die Experimente wurden auf einem MacBook Pro mit einer 12-Kern-M2-Pro-CPU und 32 GB RAM unter macOS Sonoma 14.5 durchgeführt. Die folgende Tabelle zeigt die Laufzeiten der Lösungen auf komprimierter und unkomprimierter Eingabe:

Tool Laufzeit (komprimiert) Laufzeit (unkomprimiert)
grep 2.6.0-FreeBSD 20.9 s 20.5 s
pcregrep 8.45 3.1 s 2.9 s
DuckDB 1.0.0 4.2 s 1.2 s

Die Ergebnisse zeigen, dass grep auf komprimierter Eingabe am langsamsten war, während DuckDB leicht von gzcat+pcregrep geschlagen wird, das in 3,1 Sekunden lief, verglichen mit DuckDBs 4,2 Sekunden. Auf unkomprimierter Eingabe kann DuckDB alle CPU-Kerne von Anfang an nutzen (statt mit einem Single-Thread-Dekompressionsschritt zu starten) und so sowohl grep als auch pcregrep deutlich schlagen: 2,5× schneller als pcregrep und mehr als 15× schneller als grep.

Obwohl dieses Beispiel ziemlich einfach ist, gibt es bei komplexeren Abfragen mehr Optimierungsmöglichkeiten, und es können größere Zwischendatensätze entstehen. Beides kann innerhalb eines Shell-Skripts angegangen werden (indem man Optimierungen manuell umsetzt und die Zwischendatensätze auf die Platte schreibt), das wird aber wahrscheinlich weniger effizient sein als das, was ein DBMS sich ausdenken kann. Shell-Skripte, die komplexe Pipelines umsetzen, können auch sehr brüchig sein und müssen selbst für kleine Änderungen neu durchdacht werden, wodurch der Performance-Vorteil einer Datenbank bei komplexeren Problemen noch signifikanter wird.

Zusammenfassung

In diesem Beitrag haben wir DuckDB als eigenständige CLI-Anwendung genutzt und seine Fähigkeiten erkundet, bestehende Kommandozeilentools (sort, grep, comm, join usw.) zu ergänzen oder zu ersetzen. Obwohl wir DuckDB offensichtlich sehr mögen und es in vielen Fällen bevorzugen, glauben wir auch, dass Unix-Tools ihren Platz haben: Auf den meisten Systemen sind sie schon vorinstalliert, und eine gut gewählte Toolchain von Unix-Befehlen kann schnell, effizient und portabel sein (dank POSIX-Compliance). Zusätzlich können sie für bestimmte Probleme sehr knapp sein. Um ihre Vorteile zu ernten, müssen Sie aber die Syntax und Eigenheiten jedes Tools lernen, etwa grep-Varianten, awk sowie fortgeschrittene wie xargs und parallel. In der Zwischenzeit ist DuckDBs SQL leicht zu lernen (Sie kennen wahrscheinlich schon eine ganze Menge davon), und DuckDB übernimmt die meiste Optimierung für Sie.

Wenn Sie einen Lieblings-CLI-Einsatzfall für DuckDB haben, lassen Sie es uns in den sozialen Medien wissen oder reichen Sie ihn bei DuckDB Snippets ein. Happy hacking!