2025-05-21

Ankündigung: DuckDB 1.3.0

The DuckDB team

Zur Installation der neuen Version besuchen Sie bitte die Installationsanleitung. Hinweis: Es kann ein paar Stunden bis Tage dauern, bis einige Client-Bibliotheken (z. B. Go, R, Java) und Extensions (z. B. die UI) erscheinen, wegen der zusätzlichen Änderungen und Review-Runden.

Wir sind stolz, DuckDB 1.3.0 zu veröffentlichen. Dieses Release von DuckDB heißt „Ossivalis“ nach Bucephala Ossivalis, einem Vorfahren der Schellente, der vor Millionen von Jahren lebte.

In diesem Blogbeitrag behandeln wir die wichtigsten Features des neuen Releases. DuckDB bewegt sich ziemlich schnell, und wir können nur einen kleinen Teil der Änderungen in diesem Release abdecken. Die vollständigen Release Notes stehen auf der Release-Seite auf GitHub.

Breaking Changes und Deprecations

Deprecation von altem Linux-glibc

Da alle gängigen Linux-Distributionen inzwischen glibc 2.28 oder neuer nutzen, verlangen DuckDBs offizielle Linux-Binaries mindestens glibc 2.28 oder neuer. Das Release wird mit dem manylinux_2_28-Image von Python gebaut, das ein älteres glibc mit einem neueren Compiler kombiniert. Diese Änderung bedeutet auch, dass Extensions nicht mehr für die Plattform linux_amd64_gcc4 verteilt werden.

Wir schätzen Portabilität sehr, daher ist es natürlich weiterhin möglich, DuckDB aus dem Quellcode für ältere glibc-Versionen zu bauen.

Lambda-Funktionssyntax

Bisher konnten Lambda-Funktionen in DuckDB mit der Single-Arrow-Syntax angegeben werden: x -> x + 1. Der Single-Arrow-Operator wird auch von der JSON-Extension genutzt, um JSON-Extraktion mit der Syntax ->'field' auszudrücken. Die beiden Bedeutungen des Single-Arrow-Operators werden vom Binder gleich behandelt, sie teilen also dieselbe (niedrige) Precedence, was in JSON-Ausdrücken mit Gleichheitsprüfungen zusätzliche Klammern nötig macht:

SELECT (JSON '{"field": 42}')->'field' = 42;
-- throws a Binder Error:
-- No function matches the given name and argument types 'json_extract(JSON, BOOLEAN)
SELECT ((JSON '{"field": 42}')->'field') = 42;
-- return true

Das hat Nutzer oft verwirrt, deshalb deprecatet das neue Release die alte Arrow-Lambda-Syntax und ersetzt sie durch Python-artige Lambda-Syntax:

SELECT list_transform([1, 2, 3], lambda x: x + 1);

Um den Übergang zu erleichtern, erfolgt die Deprecation in mehreren Schritten über das nächste Jahr. Zuerst führt DuckDB 1.3.0 eine neue Einstellung zur Konfiguration der Lambda-Syntax ein:

SET lambda_syntax = 'DEFAULT';
SET lambda_syntax = 'ENABLE_SINGLE_ARROW';
SET lambda_syntax = 'DISABLE_SINGLE_ARROW';

Derzeit aktiviert DEFAULT beide Syntaxstile, also die alte Single-Arrow-Syntax und die Python-artige Syntax. Wir werden die alte Syntax schrittweise auslaufen lassen. DuckDB 1.5.0 wird das letzte Release sein, das die Single-Arrow-Syntax ohne explizites Aktivieren unterstützt. Folgende Versionen werden die Single-Arrow-Syntax zuerst standardmäßig deaktivieren und schließlich das Flag lambda_syntax entfernen und die Single-Arrow-Syntax vollständig deprecaten, sodass das alte Verhalten nicht mehr zugänglich ist.

Serialisieren von Strings in Listen mit Escapes

Ab der neuen Version escaped DuckDB Zeichen wie ' in Strings, die in verschachtelten Datenstrukturen serialisiert werden, um Round-Tripping zwischen dem serialisierten String und der verschachtelten Darstellung zu ermöglichen. Zum Beispiel:

SELECT ['hello ''my'' world'] AS s;

DuckDB Version 1.2.2 gibt [hello 'my' world] zurück, DuckDB 1.3.0 gibt ['hello \'my\' world'] zurück.

Um eine Liste von Strings mit dem alten Verhalten zu serialisieren, nutzen Sie die Funktion array_to_string:

SELECT printf('[%s]', array_to_string(
['hello ''my'' world', 'hello ''cruel'' world'], ', '
)) AS s;
┌─────────────────────────────────────────┐
│ s │
│ varchar │
├─────────────────────────────────────────┤
│ [hello 'my' world, hello 'cruel' world] │
└─────────────────────────────────────────┘

Kleine Änderungen am SQL-Parser

Neue Features

Das neue DuckDB-Release enthält wieder viele spannende neue Features:

External File Cache

DuckDB wird oft genutzt, um von Remote-Dateien zu lesen, z. B. Parquet-Dateien auf HTTP-Servern oder Blob Storage. Vorherige Versionen haben Dateidaten immer vollständig neu gelesen. Mit diesem Release haben wir einen Cache für Daten aus externen Dateien hinzugefügt. Dieser Cache unterliegt dem allgemeinen DuckDB-Speicherlimit. Ist Platz vorhanden, wird er dynamisch genutzt, um Daten aus externen Dateien zu cachen. Das sollte das erneute Ausführen von Queries auf Remote-Daten stark verbessern. Zum Beispiel:

.timer on
.mode trash -- do not show query result
FROM 'https://blobs.duckdb.org/data/shakespeare.parquet';
Run Time (s): real1.456⟩ user 0.037920 sys 0.028510
FROM 'https://blobs.duckdb.org/data/shakespeare.parquet';
Run Time (s): real0.360⟩ user 0.029188 sys 0.007620

Man sieht, dass die Query beim zweiten Mal dank des Caches deutlich schneller ist. In vorherigen Versionen wäre die Laufzeit gleich gewesen.

Der Cache-Inhalt lässt sich mit der Tabellenfunktion duckdb_external_file_cache() so abfragen:

.mode duckbox -- re-enable output
FROM duckdb_external_file_cache();
┌───────────────────────────────────────────────────┬──────────┬──────────┬─────────┐
│ path │ nr_bytes │ location │ loaded │
│ varchar │ int64 │ int64 │ boolean │
├───────────────────────────────────────────────────┼──────────┼──────────┼─────────┤
│ https://blobs.duckdb.org/data/shakespeare.parquet │ 1697483 │ 4 │ true │
│ https://blobs.duckdb.org/data/shakespeare.parquet │ 16384 │ 1681808 │ true │
└───────────────────────────────────────────────────┴──────────┴──────────┴─────────┘

Der Cache ist standardmäßig aktiviert, lässt sich aber so deaktivieren:

SET enable_external_file_cache = false;

Datendateien direkt mit der CLI abfragen

DuckDBs Command Line Interface (CLI) kann jetzt Parquet-, CSV- oder JSON-Dateien direkt abfragen. Dazu nutzt man z. B. eine Parquet-Datei anstelle der Datenbankdatei. Das legt eine View frei, die abgefragt werden kann. Haben wir etwa eine Parquet-Datei namens region.parquet, funktioniert das:

Terminal window
duckdb region.parquet -c 'FROM region;'
┌─────────────┐
│ r_name │
│ varchar │
├─────────────┤
│ AFRICA │
│ AMERICA │
│ ASIA │
│ EUROPE │
│ MIDDLE EAST │
└─────────────┘

Wenn man die CLI so nutzt, startet intern eine temporäre In-Memory-DuckDB-Datenbank und erzeugt zwei Views über die gegebene Datei:

Beide Views können abgefragt werden und liefern dasselbe Ergebnis.

Der Hauptvorteil dieses Features ist die Bedienbarkeit: Wir können die normale Shell nutzen, um zu einer Datei zu navigieren, und dann DuckDB öffnen, ohne den Dateipfad auf SQL-Ebene angeben zu müssen.

TRY-Ausdruck

DuckDB unterstützte bereits TRY_CAST, das versuchte, einen Wert zu casten, die Query aber nicht scheitern ließ, wenn das nicht möglich war. Zum Beispiel:

SELECT TRY_CAST('asdf' AS INTEGER);

gibt NULL zurück. Dieses Release verallgemeinert diese Funktionalität über Casting hinaus auf beliebige Ausdrücke, die Fehler werfen können, mit TRY. Der Logarithmus von 0 ist etwa undefiniert, und log(0) wirft eine Exception und sagt, dass es „cannot take logarithm of zero“. Mit dem neuen TRY gibt das stattdessen NULL zurück, z. B.:

SELECT TRY(log(0));
NULL

Das funktioniert wieder für beliebige Ausdrücke. Wir empfehlen aber, TRY sparsam zu nutzen, wenn ein Fehler oft erwartet wird, weil es einen Performance-Einfluss gibt. Verursacht ein Batch von Zeilen einen Fehler, wechseln wir zur zeilenweisen Ausführung des Ausdrucks, um genau festzustellen, welche Zeile einen Fehler hatte und welche nicht. Das ist langsamer.

Structs aktualisieren

Ab dem neuen Release lässt sich das Sub-Schema von Structs mit der Klausel ALTER TABLE aktualisieren. Sie können Felder hinzufügen, droppen und umbenennen:

CREATE TABLE test (s STRUCT(i INTEGER, j INTEGER));
INSERT INTO test VALUES (ROW(1, 1)), (ROW(2, 2));
ALTER TABLE test DROP COLUMN s.i;
ALTER TABLE test ADD COLUMN s.k INTEGER;
ALTER TABLE test RENAME COLUMN s.j TO l;
┌──────────────────────────────┐
│ s │
│ struct(l integer, k integer) │
├──────────────────────────────┤
│ {'l': 1, 'k': NULL} │
│ {'l': 2, 'k': NULL} │
└──────────────────────────────┘

Das Ändern von Structs wird auch innerhalb von LIST- und MAP-Spalten unterstützt.

Neue Datenbanken einwechseln

Die Klausel ATTACH OR REPLACE erlaubt es, eine Datenbank zu ersetzen, sodass Sie eine Datenbank on the fly tauschen können. Zum Beispiel:

ATTACH 'taxi_v1.duckdb' AS taxi;
USE taxi;
ATTACH OR REPLACE 'taxi_v2.duckdb' AS taxi;

Dieses Feature wurde vom externen Contributor xevix umgesetzt.

UUID-v7-Unterstützung

Warnung Update (2025-05-23). Die UUID-v7-Implementierung in DuckDB v1.3.0 ist nicht konsistent mit dem UUID-Standard, die Timestamp-Werte sind dadurch falsch. Das heißt, die Timestamps in den erzeugten UUID-v7-Werten sind nur korrekt, wenn sie ausschließlich innerhalb von DuckDB genutzt werden. Import oder Export von UUID-v7-Werten von/zu anderen Systemen liefert falsche Timestamps. Wir haben diesen Bug gepatcht, der Fix ist bald in den Preview Builds und im kommenden 1.3.1-Patch-Release verfügbar.

DuckDB unterstützt jetzt UUID v7, eine neuere Version von UUIDs. UUIDv7 kombiniert einen Unix-Timestamp in Millisekunden und Zufallsbits und bietet so sowohl Eindeutigkeit als auch Sortierbarkeit. Das ist nützlich, um z. B. UUIDs nach Alter zu ordnen oder die allgegenwärtigen Spalten ID und TIMESTAMP in vielen Tabellen zu einer einzigen UUIDv7-Spalte zusammenzulegen.

Neue UUIDs lassen sich mit der Skalarfunktion uuidv7() erzeugen. Zum Beispiel:

SELECT uuidv7();
┌──────────────────────────────────────┐
│ uuidv7() │
│ uuid │
├──────────────────────────────────────┤
│ 8196f1f6-e3cf-7a74-bc0e-c89ac1ea1e19 │
└──────────────────────────────────────┘

Es gibt auch zusätzliche Funktionen, um die UUID-Version zu bestimmen (uuid_extract_version()) und den internen Timestamp zu extrahieren (uuid_extract_timestamp()), zum Beispiel:

SELECT uuid_extract_version(uuidv7());
┌────────────────────────────────┐
│ uuid_extract_version(uuidv7()) │
│ uint32 │
├────────────────────────────────┤
│ 7 │
└────────────────────────────────┘
SELECT uuid_extract_timestamp(uuidv7());
┌──────────────────────────────────┐
│ uuid_extract_timestamp(uuidv7()) │
│ timestamp with time zone │
├──────────────────────────────────┤
│ 2025-05-21 08:32:14.61+00 │
└──────────────────────────────────┘

Dieses Feature wurde vom externen Contributor dentiny umgesetzt.

Ausdrucksunterstützung in CREATE SECRET

DuckDB hat eine interne „Secret“-Verwaltung für Dinge wie S3-Credentials. Mit diesem Release ist es möglich, Skalarausdrücke bei der Erzeugung des Secrets zu nutzen. So müssen Secret-Inhalte nicht im Query-Text stehen, was sie leichter aus Logdateien usw. heraushält. Zum Beispiel:

SET VARIABLE my_bearer_token = 'hocus pocus this token is bogus';
CREATE SECRET http (
TYPE http,
BEARER_TOKEN getvariable('my_bearer_token')
);

Man sieht, dass das Feld BEARER_TOKEN im Secret aus der Funktion getvariable in CREATE SECRET gesetzt wird. In der CLI geht das auch über Umgebungsvariablen mit getenv(). Zum Beispiel ist jetzt möglich:

Terminal window
MY_SECRET=asdf duckdb -c \
"CREATE SECRET http (TYPE http, BEARER_TOKEN getenv('MY_SECRET'))"

Spalten entpacken

DuckDB v1.3.0 gibt dem beliebten COLUMNS(*)-Ausdruck einen weiteren Schub. Bisher war das Entpacken der Entitäten in eine Liste möglich, indem man ein führendes *-Zeichen hinzufügte:

CREATE TABLE tbl AS SELECT 21 AS a, 1.234 AS b;
SELECT [*COLUMNS(*)] AS col_exp FROM tbl;
┌─────────────────┐
│ col_exp │
│ decimal(13,3)[] │
├─────────────────┤
│ [21.000, 1.234] │
└─────────────────┘

Diese Syntax ließ sich aber nicht mit anderen Ausdrücken wie Casting kombinieren:

SELECT [*COLUMNS(*)::VARCHAR] AS col_exp FROM tbl;
Terminal window
Binder Error:
*COLUMNS() can not be used in this place

Das neue Schlüsselwort UNPACK hebt diese Einschränkung auf. Der folgende Ausdruck

SELECT [UNPACK(COLUMNS(*)::VARCHAR)] AS col_exp FROM tbl;

ist äquivalent zu:

SELECT [a::VARCHAR, b::VARCHAR] AS col_exp FROM tbl;
┌─────────────┐
│ col_exp │
│ varchar[] │
├─────────────┤
│ [21, 1.234] │
└─────────────┘

Spatial-JOIN-Operator

Wir haben einen neuen spezialisierten Join-Operator als Teil der spatial-Extension hinzugefügt, der die Effizienz von Spatial Joins stark verbessert, also Queries, die zwei Geometry-Spalten mit spezifischen Spatial-Prädikatfunktionen wie ST_Intersects und ST_Contains JOINen.

Ähnlich wie ein HASH_JOIN baut der SPATIAL_JOIN eine temporäre Lookup-Datenstruktur für die kleinere Seite des Joins auf, nur dass es ein R-Tree ist statt einer Hash-Tabelle. Das heißt für Sie: Sie müssen keinen Index zuerst anlegen oder sonstiges Preprocessing tun, um Spatial Joins zu optimieren. Das übernimmt der Join-Operator intern.

Der Query-Optimizer versucht, diesen neuen Operator für LEFT-, OUTER-, INNER- und RIGHT-Spatial-Joins zu instanziieren; eine aktuelle Einschränkung ist, dass der Join nur eine einzelne Join-Bedingung enthalten darf, sonst fällt der Optimizer auf eine weniger effiziente Join-Strategie zurück.

Das folgende Beispiel zeigt, wie der Operator SPATIAL_JOIN Teil des Query-Plans wird. Es ist eine relativ kleine Query, auf meiner Maschine läuft sie aber fast 100× schneller als früher in DuckDB v1.2.2!

LOAD spatial;
-- generate random points
CREATE TABLE points AS
SELECT
ST_Point(x, y) AS geom,
(y * 50) + x // 10 AS id
FROM
generate_series(0, 1000, 5) r1(x),
generate_series(0, 1000, 5) r2(y);
-- generate random polygons
CREATE TABLE polygons AS
SELECT
ST_Buffer(ST_Point(x, y), 5) AS geom,
(y * 50) + x // 10 AS id
FROM
generate_series(0, 500, 10) r1(x),
generate_series(0, 500, 10) r2(y);
-- inspect the join plan
EXPLAIN
SELECT *
FROM polygons
JOIN points ON ST_Intersects(points.geom, polygons.geom);
...
┌─────────────┴─────────────┐
│ SPATIAL_JOIN │
│ ──────────────────── │
│ Join Type: INNER │
│ Conditions: ├──────────────┐
│ ST_Intersects(geom, geom) │ │
│ ~40401 Rows │ │
└─────────────┬─────────────┘ │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│ SEQ_SCAN ││ SEQ_SCAN │
│ ──────────────────── ││ ──────────────────── │
│ Table: points ││ Table: polygons │
│ Type: Sequential Scan ││ Type: Sequential Scan │
│ ~40401 Rows ││ ~2601 Rows │
└───────────────────────────┘└───────────────────────────┘

Für Neugierige gibt es mehr Details im PR.

Interne Änderungen

Für dieses Release gab es auch eine große Zahl interner Änderungen.

Wir haben eine fast vollständige Neuimplementierung von DuckDBs Parquet-Reader und -Writer abgeschlossen. Das sollte Parquet-Performance und -Zuverlässigkeit stark verbessern und hat außerdem die Parquet-Feature-Unterstützung für obskure logische Typen wie UNKNOWN und FLOAT16 erweitert.

Wir haben auch viele interne Änderungen rund um das Lesen mehrerer Dateien (z. B. ein Ordner von Parquet-Dateien) in einer API namens MultiFileReader vorgenommen. Wir haben die Behandlung mehrerer Dateien über viele unserer File-Reader vereinheitlicht, z. B. Parquet, CSV, JSON, Avro usw. So kann DuckDB z. B. Schemaunterschiede zwischen mehreren Dateien einheitlich behandeln.

Wir haben außerdem eine neue String-Kompressionsmethode hinzugefügt, DICT_FSST. Vorher unterstützte DuckDB entweder Dictionary Encoding oder FSST-Kompression („Fast Static Symbol Table“) für Strings. Diese Kompressionsmethoden konnten innerhalb eines Storage-Blocks (standardmäßig 265 kB) nicht gemischt werden. Wir haben aber viele reale Daten gesehen, bei denen ein Teil des Blocks von Dictionary Encoding profitieren würde und ein anderer Teil von FSST. FSST eliminiert Strings standardmäßig nicht per Duplikaterkennung. Dieses Release kombiniert beide Methoden zu einer neuen Kompressionsmethode, DICT_FSST. Diese führt zuerst Dictionary Encoding aus und komprimiert dann das Dictionary mit FSST. Dictionary Encoding und nur-FSST-Encoding bleiben ebenfalls verfügbar. Wir haben auch das Speichern von Validity Masks („welche Zeilen sind NULL?“) in diesem Release optimiert; einige Kompressionsmethoden (wie das neue DICT_FSST) können NULLs intern behandeln, das macht eine separate Validity Mask überflüssig. Zusammen sollten diese neuen Features den benötigten Speicherplatz vor allem für Strings stark reduzieren. Die Kompressionsmethode wird automatisch von DuckDB anhand der tatsächlich beobachteten Kompressionsverhältnisse gewählt, Nutzer müssen das also nicht explizit setzen.

Schlussgedanken

Das waren ein paar Highlights – aber es gibt viele weitere Features und Verbesserungen in diesem Release. Seit v1.2.2 gab es über 3.000 Commits von über 75 Contributors. Die vollständigen Release Notes finden Sie auf GitHub. Wir danken unserer Community für detaillierte Issue-Reports und Feedback. Und unser besonderer Dank gilt den externen Contributors, die Features direkt in diesem Release gelandet haben!