Een vooruitblik op DuckDB v2.0
TL;DR: DuckDB v2.0 verschijnt dit najaar. In dit artikel geven we een vooruitblik op de belangrijkste functies: DuckDB als server, triggers, het VARIANT-type, asynchrone I/O, een nieuwe SQL-parser, een nieuw opslagformaat en meer.
DuckDB v2.0 krijgt de naam "Cyanoptera", vernoemd naar de kaneelteal (Anas cyanoptera), een opvallende roodbruine eend die voorkomt in West-Amerika.
Een grote versie-update is niet iets wat we lichtzinnig doen; het is meer dan alleen ceremonie. Versie 2.0 introduceert een nieuwe SQL-parser, een nieuw standaard opslagformaat, een herziene C-API en een klein aantal zorgvuldig gekozen breaking changes. Bovenal is het een feature-release, gebouwd op basis van meer dan 10.000 commits sinds de release van v1.5 in maart. Waar vorig jaar het jaar van het lakehouse was, luidt deze release het jaar in van DuckDB als server.
DuckDB ontwikkelt zich snel, en we kunnen hier slechts een fractie van de wijzigingen behandelen. We hebben de nieuwe functies samengevat in een lijst, beginnend bij de SQL-functies en eindigend bij de engine.
1. DuckDB als server: Quack en CONNECT
Vanaf het begin was DuckDB een in-process database. Op aanhoudende vraag hebben we nu een client/server-modus toegevoegd. De quack-extensie implementeert het native protocol van DuckDB om met andere DuckDB-instanties te communiceren. Deze extensie was kort voor DuckCon #7 als preview uitgebracht en wordt in v2.0 stabiel.
Elk DuckDB-proces kan nu databases over het netwerk serveren, en elke andere DuckDB kan zich hiermee verbinden en queries routeren met het nieuwe CONNECT-statement.
Voorbeeld: DuckDB server
CALL quack_serve(
token = 'my_token'
);
Voorbeeld: DuckDB client
ATTACH 'quack:server.example.com' AS qk (TOKEN 'my_token');
CONNECT qk;
SELECT count(*) FROM events;
-- Voert uit op de server, resultaten worden teruggestreamd
DISCONNECT;
CONNECT is de opvolger van de remote.query($$...$$) workaround. Bovendien is CONNECT niet beperkt tot Quack; het koppelt je sessie aan elke remote database die dit ondersteunt. De nieuwe remote pushdown optimizer (#22914) stuurt SQL direct naar PostgreSQL en MySQL in plaats van tabellen over het netwerk te trekken:
CONNECT 'postgres://localhost/mydb';
SELECT count(*) FROM orders; -- Draait op de PostgreSQL-server
DISCONNECT;
Hoewel men bij analytische systemen vaak denkt dat transactionele workloads niet mogelijk zijn, is DuckDB vanaf dag één gebouwd als een transactionele multi-connection database met volledige MVCC en transactie-isolatie. In een single-user scenario was dit zelden nodig, maar het client/server-patroon laat deze architectuur nu schitteren in multi-tenant, langdurige implementaties.
Het langdurig draaien van DuckDB brengt nieuwe uitdagingen met zich mee, vandaar dat v2.0 inzet op betere metrics, logs en observeerbaarheid (zie bijv. de herziening van de metrics-laag in #22799).
2. VARIANT wordt een eersteklas burger
Het VARIANT-type werd geïntroduceerd in v1.5 en kan worden gezien als "JSON op steroïden". Net als JSON kan een VARIANT-kolom in elke rij data met een andere vorm opslaan. In tegenstelling tot JSON is het geen tekstformaat: DuckDB detecteert automatisch de gemeenschappelijke structuur in semi-gestructureerde data en "shredt" deze. Hierdoor wordt de data efficiënt gecomprimeerd en kunnen queries snel worden uitgevoerd, zonder dat je een schema hoeft te definiëren. Dit maakt VARIANT ideaal voor real-time log-ingestie.
In v2.0 werkt deze pipeline end-to-end:
- Shredded execution direct vanuit storage (#20912).
- Extraction pushdown in scans (#22478).
- Shredded VARIANT lezen en schrijven voor Parquet.
- Een familie van
variant_*functies.
CREATE TABLE events (payload VARIANT);
INSERT INTO events
VALUES ('{"user": {"id": 42, "tags": ["a", "b"]}}'::JSON::VARIANT);
SELECT variant_type(payload), variant_keys(payload)
FROM events;
SELECT *
FROM events
WHERE variant_contains(payload, {'user': {'id': 42}}::VARIANT);
Op de langere termijn is het plan om het reguliere JSON-type te baseren op VARIANT, zodat bestaande JSON-workloads profiteren van deze voordelen zonder dat queries aangepast hoeven te worden.
3. Triggers
Triggers zijn een veelgevraagde functie en worden in v2.0 volledig ondersteund: BEFORE en AFTER triggers, FOR EACH ROW en FOR EACH STATEMENT, transitietabellen via REFERENCING OLD/NEW TABLE, meerdere triggers per event, RETURNING op getriggerde tabellen en DROP TRIGGER.
Een klassiek gebruiksscenario is het bijhouden van audit-tabellen:
CREATE TABLE target (id INTEGER, val INTEGER);
CREATE TABLE audit (id INTEGER, old_val INTEGER, new_val INTEGER);
CREATE TRIGGER trg_audit AFTER UPDATE ON target
REFERENCING OLD TABLE AS o NEW TABLE AS n
FOR EACH STATEMENT
INSERT INTO audit
SELECT n.id, o.val, n.val
FROM o
JOIN n ON o.id = n.id;
INSERT INTO target VALUES (1, 10), (2, 20);
UPDATE target SET val = val * 10 WHERE id <= 2;
SELECT * FROM audit;
Resultaat:
| id | old_val | new_val |
|---|---|---|
| 1 | 10 | 100 |
| 2 | 20 | 200 |
4. Toevoegingen aan het SQL-dialect
Het SQL-dialect van DuckDB blijft groeien. Enkele hoogtepunten uit deze release:
- NEAREST joins (#24137): Maakt top-k similarity search mogelijk als join-clausule, handig voor vector- en embedding-workloads.
``sql SELECT q.userid, t.productid FROM users q INNER JOIN products t APPROX NEAREST 2 BY SIMILARITY arraycosinesimilarity(q.embedding, t.embedding); ``
- DML in CTE's (#21634, #21997, #24217): Maakt het mogelijk om
INSERT,UPDATE,DELETEenCOPYte gebruiken als stappen in een pipeline.
``sql WITH moved AS MATERIALIZED ( DELETE FROM staging RETURNING ) INSERT INTO archive SELECT FROM moved; ``
- Geneste schema's (#23492, #24222): Maakt schema's binnen schema's mogelijk.
``sql CREATE SCHEMA finance; CREATE SCHEMA finance.reports; CREATE TABLE finance.reports.q3 (revenue DECIMAL); ``
- Nieuwe variabele-syntaxis (#21194): Gebruik
$xoveral waar een expressie is toegestaan.
``sql SET VARIABLE threshold = 100; SELECT * FROM orders WHERE amount > $threshold; ``
- JSON-mutatiefuncties (#23786):
jsonset,jsoninsert,jsonreplaceenjsonremovemaken het mogelijk om JSON-documenten ter plekke aan te passen.
``sql SELECT json_set('{"a":1}', '$.b', '2'); -- Resultaat: {"a":1,"b":2} ``
- Recursieve CTE's met USING KEY aggregatie (#19481): Maakt iteratieve algoritmen mogelijk in pure SQL.
``sql WITH RECURSIVE tbl(a, b) USING KEY (a, avg(b)) AS ( SELECT 1, 5 UNION SELECT a, b - 1 FROM tbl WHERE b > 0 ) TABLE tbl; ``
Overige toevoegingen zijn: SQL-standaard FETCH FIRST 2 ROWS ONLY (#23533), OVERLAY() (#22456), UNNEST in GROUP BY (#23644), en beter gedefinieerde MERGE / UPDATE ... FROM semantiek voor multi-matched rows (#24058).
5. Asynchrone I/O
Interactie met object stores zoals S3 is centraal in de DuckDB-ervaring. Hoewel DuckDB al parallel kon lezen, beperkte synchrone toegang de snelheid. Versie 2.0 introduceert asynchrone I/O in de hele engine.
Hierdoor schaalt de I/O-laag nu onafhankelijk van de query-processing laag, wat resulteert in meer parallellisme voor remote reads en aanzienlijk snellere queries op netwerkopslag. Dit is eerst geïmplementeerd voor Parquet (#23662), gevolgd door CSV (#23961) en het eigen bestandsformaat van DuckDB (#24654). Ook zijn er asynchrone Parquet-writes (#23283) en nieuwe MMAP- en DIRECT_IO-modi (#22988) toegevoegd.
6. Algemene snelheidswinst
Veel werk is besteed aan het sneller maken van bestaande queries. Belangrijke verbeteringen:
- Partial aggregates worden nu gepusht onder joins (#22572).
- Redundante aggregaties worden hergebruikt (#24543).
- De recursive CTE engine is volledig herschreven (#22211).
- Aggregaties spillen nu naar disk wanneer het geheugen onvoldoende is (#24499).
- De Windows CLI is circa 2,2× sneller geworden bij multi-threaded result materialization (#24036).
Microbenchmark: Reachability over a graph (1 miljoen edges)
CREATE TABLE edges AS
SELECT (range % 100_000)::INTEGER AS src,
((range * 13 + 7) % 100_000)::INTEGER AS dst
FROM range(1_000_000);
WITH RECURSIVE reachable(node) AS (
SELECT 0
UNION
SELECT dst FROM edges, reachable WHERE src = node
)
SELECT count(*) FROM reachable;
| Versie | Uitvoertijd |
|---|---|
| DuckDB v1.5.4 | 4.90 s |
| DuckDB v2.0 (preview) | 0.12 s |
Conclusie: v2.0 is ongeveer 40× sneller voor deze specifieke recursive query.
Daarnaast is row-group pruning uitgebreid: min-max indexen (zone maps) en Parquet Bloom filters slaan nu data over voor structs, lists, decimals, UUID's, IN-filters en zelfs functiepredicaten.
Query planning is nu ook partition-aware (#22336). Voor lakehouse-formaten (DuckLake, Iceberg, Hive-partitioned Parquet op S3) betekent dit dat de planner en optimizer optimaal gebruikmaken van bestaande partitionering, wat het verschil kan maken tussen een volledige scan of het overslaan van het grootste deel van de dataset.
7. Opslagformaat v2.0
De standaardversie van het opslagformaat is verhoogd naar v2.0.0 (#22875). De belangrijkste wijziging is de introductie van buffer-managed ART indexen (#21458, #23605). Indexen worden niet langer volledig in het geheugen gepind, waardoor grote geïndexeerde tabellen direct openen en indexen on-demand worden ingeladen.
Andere verbeteringen:
- Kolom-metadata wordt nu lazy geladen (#22333).
- De
DICT_FSSTstring-compressiemethode is standaard ingeschakeld (#23733). - Verwijderingen (deletes) worden compacter opgeslagen (#24336).
- De storage-laag voert strengere corruptie-validatie uit bij het lezen.
8. Een volledig nieuwe SQL-parser
DuckDB heeft altijd een parser gebruikt die is afgeleid van PostgreSQL. In v2.0 stappen we over op een eigen, moderne, uitbreidbare PEG-based parser (#22194).
Deze wijziging is nauw verbonden met het extensie-ecosysteem: extensies kunnen nu direct inbreken in de grammatica, waardoor extensies met geheel nieuwe SQL-syntaxis mogelijk worden. Daarnaast biedt het betere foutmeldingen met precieze locaties en een eerste dialect compatibility mode:
SET dialect_compatibility_mode = 'spark';
9. Tijdzones, kalenders en collations zonder ICU
Tijdzone-bewuste timestamps, kalenders en collations werden voorheen aangedreven door de ICU-bibliotheek. Hoewel ICU een goede bibliotheek is, gebruikte DuckDB slechts een klein deel ervan. In v2.0 is de ICU-bibliotheek volledig verwijderd. De icu-extensie implementeert deze functies nu zelf (#24463, #24403), waarbij de tijdzonedata direct uit de IANA-database is opgebouwd en gecomprimeerd is tot circa 45 kB.
Prestatieverbetering (microbenchmark op MacBook):
| Query | v1.5.4 (ICU) | v2.0 (native) | Versnelling |
|---|---|---|---|
| ts AT TIME ZONE 'Europe/Paris', 25M rows | 0.24 s | 0.11 s | 2.2× |
| Filter met COLLATE de, 5M rows | 0.15 s | 0.06 s | 2.6× |
10. Extensies één keer schrijven, zelf hosten
Voorheen moesten extensies vaak worden herbouwd voor elke nieuwe DuckDB-release omdat ze gebruikmaakten van een onstabiele C++ API. In v2.0 is de stabiele C-API uitgebreid, zodat extensies één keer geschreven en gebouwd kunnen worden en blijven werken over verschillende versies heen.
De C-API wordt nu gegenereerd vanuit een declaratieve, versiebeheerde specificatie in YAML (api_spec/), waardoor API- en ABI-drift wordt voorkomen.
Voorbeeld van een eenvoudige extensie in C:
#include "duckdb_extension.h"
DUCKDB_EXTENSION_EXTERN
static void AddNumbers(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) {
idx_t count = duckdb_data_chunk_get_size(input);
int64_t *a = (int64_t *) duckdb_vector_get_data(duckdb_data_chunk_get_vector(input, 0));
int64_t *b = (int64_t *) duckdb_vector_get_data(duckdb_data_chunk_get_vector(input, 1));
int64_t *result = (int64_t *) duckdb_vector_get_data(output);
for (idx_t row = 0; row < count; row++) {
result[row] = a[row] + b[row];
}
}
DUCKDB_EXTENSION_ENTRYPOINT(duckdb_connection con, duckdb_extension_info info, duckdb_extension_access *access) {
duckdb_scalar_function f = duckdb_create_scalar_function();
duckdb_scalar_function_set_name(f, "add_numbers");
duckdb_logical_type bigint = duckdb_create_logical_type(DUCKDB_TYPE_BIGINT);
duckdb_scalar_function_add_parameter(f, bigint);
duckdb_scalar_function_add_parameter(f, bigint);
duckdb_scalar_function_set_return_type(f, bigint);
duckdb_destroy_logical_type(&bigint);
duckdb_scalar_function_set_function(f, AddNumbers);
duckdb_register_scalar_function(con, f);
duckdb_destroy_scalar_function(&f);
return true;
}
Gebruik: LOAD addnumbers; SELECT addnumbers(40, 2);
Daarnaast kunnen gebruikers in v2.0 hun eigen vertrouwde repositories registreren (#24777), zodat organisaties hun eigen extensies kunnen hosten en signeren:
SET allow_extension_repositories = 'allowed';
CREATE EXTENSION REPOSITORY my_repo FROM 'https://extensions.example.org';
INSTALL my_ext FROM my_repo;
LOAD my_repo/my_ext;
Repositories kunnen worden gedefinieerd via een URL (https, s3, lokaal pad) en worden beveiligd met RSA-publieke sleutels.
Bonus: DuckDB Foundation – Advisory Board
Vanaf dit najaar wordt een stakeholder advisory board toegevoegd aan de DuckDB Foundation. Deze raad zal input leveren over de roadmap voor de ontwikkeling van DuckDB, DuckLake en Quack, zodat belangrijke belanghebbenden invloed kunnen uitoefenen op de richting van de projecten.
Slotbeschouwingen
Dit zijn slechts enkele hoogtepunten. Sommige details kunnen nog wijzigen voor de release in het najaar. DuckDB v2.0 zal een klein aantal breaking changes bevatten, waaronder het nieuwe standaard opslagformaat en de voltooiing van de overgang naar de nieuwe lambda-syntaxis.
Sinds de release van v1.5 zijn er meer dan 10.000 commits gedaan door vele bijdragers. We danken de community voor de feedback en bijdragen. Voor wie alvast wil experimenteren: de preview builds bevatten momenteel de meeste van deze functies.
Groetjes,