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:
- eine Sache tun und sie gut tun sollten,
- zusammenarbeiten sollten und
- Textströme handhaben sollten.
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.
cut -d , -f 1,3 pop.csvDas erzeugt die folgende Ausgabe:
city,populationAmsterdam,905234Rotterdam,656050The Hague,552995Utrecht,361924Eindhoven,238478Groningen,234649Tilburg,224702Almere,218096Breda,184716Nijmegen,179073DuckDB: 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:
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:
(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,populationFlevoland,Almere,218096Gelderland,Nijmegen,179073Groningen,Groningen,234649North Brabant,Eindhoven,238478North Brabant,Tilburg,224702North Brabant,Breda,184716North Holland,Amsterdam,905234South Holland,Rotterdam,656050South Holland,The Hague,552995Utrecht,Utrecht,361924DuckDB: 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.
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:
cityAmsterdamEindhovenGroningenRotterdamThe HagueDuckDB: 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:
paste -d , pop.csv area.csv | cut -d , -f 1,2,3,5Die Ausgabe ist die folgende:
city,province,population,areaAmsterdam,North Holland,905234,219.32Rotterdam,South Holland,656050,324.14The Hague,South Holland,552995,98.13Utrecht,Utrecht,361924,99.21Eindhoven,North Brabant,238478,88.92Groningen,Groningen,234649,197.96Tilburg,North Brabant,224702,118.13Almere,Flevoland,218096,248.77Breda,North Brabant,184716,128.68Nijmegen,Gelderland,179073,57.63DuckDB: 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,IATAAmsterdam,AMSHaarlemmermeer,AMSEindhoven,EIN...Versuchen wir, zwei Abfragen zu formulieren:
-
Alle Städte finden, deren Name auf
damendet. -
Alle Flughäfen finden, deren IATA-Code den ersten drei Buchstaben eines bedienten Stadtnamens entspricht, der Stadtname aber nicht auf
damendet.
Unix-Shell: grep
Um die erste Frage in der Unix-Shell zu beantworten, nutzen wir grep und den regulären Ausdruck ^[^,]*dam,:
grep "^[^,]*dam," cities-airports.csvIn 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,AMSRotterdam,RTMVersuchen 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:
pcregrep -i '^([a-z]{3}).*?(?<!dam),\1$' cities-airports.csvHier 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,EINDuckDB: 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:
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 nameAMS,Amsterdam,Amsterdam Airport SchipholAMS,Haarlemmermeer,Amsterdam Airport SchipholEIN,Eindhoven,Eindhoven AirportGRQ,Eelde,Groningen Airport EeldeGRQ,Groningen,Groningen Airport EeldeMST,Beek,Maastricht Aachen AirportMST,Maastricht,Maastricht Aachen AirportRTM,Rotterdam,Rotterdam The Hague AirportRTM,The Hague,Rotterdam The Hague AirportDuckDB
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:
- Das Leerzeichen im Provinznamen durch einen Unterstrich ersetzen, z. B.
North HollandzuNorth_Hollandmachen. - Tausendertrennzeichen-Kommas hinzufügen, z. B.
905234zu905,234machen. - Das Trennzeichen der CSV auf das Semikolon (
;) ändern.
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.
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;populationAmsterdam;North_Holland;905,234Rotterdam;South_Holland;656,050The Hague;South_Holland;552,995Utrecht;Utrecht;361,924Eindhoven;North_Brabant;238,478Groningen;Groningen;234,649Tilburg;North_Brabant;224,702Almere;Flevoland;218,096Breda;North_Brabant;184,716Nijmegen;Gelderland;179,073DuckDB: 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.
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.
gzcat services-2023.csv.gz | grep '^[^,]*,[^,]*,Intercity,' | wc -lgzcat services-2023.csv.gz | pcregrep '^[^,]*,[^,]*,Intercity,' | wc -lduckdb -c "SELECT count(*) FROM 'services-2023.csv.gz' WHERE \"Service:Type\" = 'Intercity';"Wir testen die Tools auch auf unkomprimierter Eingabe:
gunzip -k services-2023.csv.gzgrep '^[^,]*,[^,]*,Intercity,' services-2023.csv | wc -lpcregrep '^[^,]*,[^,]*,Intercity,' services-2023.csv | wc -lduckdb -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!