2023-09-26

Ankündigung von DuckDB 0.9.0

Mark Raasveldt, Hannes Mühleisen

Bild der Gelbschnabelente

Das DuckDB-Team freut sich, das neueste DuckDB-Release (0.9.0) bekannt zu geben. Diese Version heißt Undulata nach der Gelbschnabelente, die in Afrika beheimatet ist.

Zur Installation der neuen Version siehe die Installationsanleitung. Die vollständigen Release Notes finden Sie auf GitHub.

Was ist neu in 0.9.0

Es gab zu viele Änderungen, um jede im Detail zu besprechen. Einige besonders spannende Features möchten wir aber hervorheben:

Im Folgenden eine Zusammenfassung dieser neuen Features mit Beispielen – beginnend mit einer Änderung in unserem SQL-Dialekt, die standardmäßig intuitivere Ergebnisse liefern soll.

Breaking Changes in SQL

Struct-Auto-Casting. Bisher wurden die Namen von Struct-Einträgen bei den Auto-Casting-Regeln ignoriert. Struct-Feldnamen konnten dadurch still umbenannt werden. Ab diesem Release führt das zu einem Fehler.

CREATE TABLE structs (s STRUCT(i INTEGER));
INSERT INTO structs VALUES ({'k': 42});
Terminal window
Mismatch Type Error: Type STRUCT(k INTEGER) does not match with STRUCT(i INTEGER). Cannot cast STRUCTs with different names

Unbenannte Structs, die mit der Funktion ROW erzeugt werden, lassen sich weiterhin in Struct-Felder einfügen.

INSERT INTO structs VALUES (ROW(42));

Verbesserungen im Kernsystem

Out-of-Core-Hash-Aggregationen und Leistungsverbesserungen der Hash-Aggregation. Bei großen Datensätzen ist Speicherverwaltung immer ein potenzieller Schmerzpunkt. Über eine streaming Execution Engine und einen Buffer Manager unterstützt DuckDB viele Operationen auf Datensätzen, die größer als der Speicher sind. DuckDB will außerdem Abfragen unterstützen, deren Zwischenergebnisse nicht in den Speicher passen, indem auf die Platte ausgelagert wird.

In diesem Release wird das Auslagern auf die Platte durch Out-of-Core-Hash-Aggregationen weiter ausgebaut. Hash-Tabellen, die bei GROUP BY-Abfragen oder DISTINCT-Operationen wegen vieler eindeutiger Gruppen nicht in den Speicher passen, werden jetzt auf die Platte geschrieben statt eine Out-of-Memory-Exception zu werfen. Durch geschickte Radix-Partitionierung fällt der Leistungsabfall allmählich aus, Leistungssprünge werden vermieden. Nur der Teil der Tabelle, der nicht in den Speicher passt, wird ausgelagert.

Die Leistung unserer Hash-Aggregation hat sich generell verbessert, besonders bei vielen Gruppen. Zum Beispiel berechnen wir die Zahl eindeutiger Zeilen in einem Datensatz mit 30 Millionen Zeilen und 15 Spalten so:

SELECT count(*) FROM (SELECT DISTINCT * FROM tbl);

Liegen alle Daten im Speicher, sollte die Abfrage etwa 6 GB brauchen. Die Abfrage lässt sich aber auch mit weniger Speicher zu Ende führen. Die Tabelle zeigt, wie die Laufzeit mit sinkendem Speicherlimit beeinflusst wird:

memory limit v0.8.1 v0.9.0
10 GB 8.52 s 2.91 s
9 GB 8.52 s 3.45 s
8 GB 8.52 s 3.45 s
7 GB 8.52 s 3.47 s
6 GB OOM 3.41 s
5 GB OOM 3.67 s
4 GB OOM 3.87 s
3 GB OOM 4.20 s
2 GB OOM 4.39 s
1 GB OOM 4.91 s

Compressed Materialization. DuckDBs streaming Execution Engine hat einen geringen Speicherbedarf, für Operationen wie gruppierte Aggregation wird aber mehr Speicher gebraucht. Dieser Fußabdruck lässt sich durch Kompression verringern. DuckDB nutzt bereits viele Kompressionstechniken im Speicherformat, viele davon sind zur Laufzeit einer Abfrage aber zu teuer. Bestimmte leichte Kompressionstechniken sind so günstig, dass der Gewinn durch weniger Speicher den Aufwand für (De-)Kompression überwiegt.

In diesem Release komprimieren wir Strings und Integer-Typen unmittelbar bevor die Daten in gruppierte Aggregation und Sortierung gehen. Über Statistiken werden beide Typen auf den kleinstmöglichen Integer-Typ komprimiert. Beispielsweise bei folgender Tabelle:

┌───────┬─────────┐
│ id │ name │
│ int32 │ varchar │
├───────┼─────────┤
│ 300 │ alice │
│ 301 │ bob │
│ 302 │ eve │
│ 303 │ mallory │
│ 304 │ trent │
└───────┴─────────┘

Die Spalte id nutzt ein 32-Bit-Integer. Aus den Statistiken wissen wir: Minimum 300, Maximum 304. Wir können 300 abziehen und auf ein 8-Bit-Integer casten – statt 4 Bytes nur noch 1.

Die Spalte name nutzt unseren internen String-Typ mit 16 Bytes. Die Statistiken sagen aber, dass der längste String hier nur 7 Bytes hat. Das passt in ein 64-Bit-Integer:

alice -> alice005
bob -> bob00003
eve -> eve00003
mallory -> mallory7
trent -> trent005

Das reduziert die Breite von 16 auf 8 Bytes. Damit komprimierte Strings sortierbar bleiben, drehen wir auf Big-Endian-Maschinen die Bytes, damit unsere Vergleichsoperatoren weiter stimmen:

alice005 -> 500ecila
bob00003 -> 30000bob
eve00003 -> 30000eve
mallory7 -> 7yrollam
trent005 -> 500tnert

Kleinere Abfragezwischenstände bedeuten weniger oder kein Auslagern auf die Platte, weniger teure I/O und damit schnellere Abfragen.

Leistungsverbesserungen bei Window-Funktionen (#7831, #7996, #8050, #8491). Dieses Release bringt viele Verbesserungen der Window-Funktionen: bessere Vektorisierung, mehr Wiederverwendung partieller Aggregate und bessere Parallelität durch Work Stealing. Dadurch ist die Leistung von Window-Funktionen deutlich gestiegen, besonders wenn es keine oder wenige Partitionen gibt.

SELECT
sum(driver_pay) OVER (
ORDER BY dropoff_datetime ASC
RANGE BETWEEN
INTERVAL 3 DAYS PRECEDING AND
INTERVAL 0 DAYS FOLLOWING
)
FROM tripdata;
Version Run time
v0.8.0 33.8 s
v0.9.0 3.8 s

Speicherverbesserungen

Vacuuming gelöschter Row Groups. Ab diesem Release werden bei DELETE ganze gelöschte Row Groups automatisch aufgeräumt. Außerdem kann die Datenbankdatei beim Checkpoint gekürzt werden, sodass die Datei nach dem Löschen von Daten kleiner wird. Das passiert nur, wenn die gelöschten Row Groups am Ende der Datei liegen. Das System verschiebt Daten noch nicht, um die Dateigröße auf der Platte zu verkleinern. Freie Blöcke weiter vorne in der Datei werden stattdessen für spätere Daten wiederverwendet.

Verbesserungen am Index-Storage (#7930, #8112, #8437, #8703). Speicherbedarf im RAM und auf der Platte von ART-Indizes wurde stark reduziert. Besonders bei Indizes für PRIMARY KEY, UNIQUE oder FOREIGN KEY ist der Fußabdruck deutlich kleiner.

CREATE TABLE integers (i INTEGER PRIMARY KEY);
INSERT INTO integers FROM range(10000000);
Version Size
v0.8.0 278 MB
v0.9.0 78 MB

Außerdem können Indizes durch die geänderte Speicherung auf der Platte jetzt inkrementell geschrieben werden statt immer komplett neu. Checkpoints bei Tabellen mit Indizes werden dadurch deutlich schneller.

Erweiterungen

Automatisches Laden von Erweiterungen. Ab diesem Release kann DuckDB vertrauenswürdige Erweiterungen automatisch installieren und laden. Viele Workflows hängen von Kern-Erweiterungen ab, die nicht mitgeliefert werden, etwa httpfs; Nutzer mussten sich merken, die nötigen Erweiterungen vorab zu laden. Jetzt werden sie automatisch geladen (und optional installiert), sobald sie in einer Abfrage vorkommen.

In Python funktioniert das folgende Snippet jetzt ohne explizites Laden der Erweiterungen httpfs oder json.

import duckdb
duckdb.sql("FROM 'https://raw.githubusercontent.com/duckdb/duckdb/main/data/json/example_n.ndjson'")

Der Satz automatisch ladbarer Erweiterungen ist auf offizielle Erweiterungen von DuckLabs beschränkt und steht hier. Das Verhalten lässt sich über autoinstall_known_extensions und autoload_known_extensions abschalten, oder über die allgemeinere Einstellung enable_external_access. Siehe die Konfigurationsoptionen.

DuckDB-Wasm-Erweiterungen. Dieses Release fügt ladbare Erweiterungen für DuckDB-Wasm hinzu. Bisher mussten gewünschte Erweiterungen im Wasm-Client fest eingebacken sein. Jetzt können sie dynamisch geladen werden. Beim Laden wird das Wasm-Bundle heruntergeladen und die Funktionalität der Erweiterung aktiviert. Probieren Sie es in unserer Wasm-Shell.

LOAD inet;
SELECT '127.0.0.1'::INET;

AWS-Erweiterung. Dieses Release markiert den Start der DuckDB-AWS-Erweiterung. Sie enthält AWS-bezogene Funktionen, die auf dem AWS SDK aufbauen. Derzeit gibt es eine Funktion, LOAD_AWS_CREDENTIALS, die die AWS Credential Provider Chain nutzt, um Credentials automatisch zu holen und zu setzen:

CALL load_aws_credentials();
SELECT * FROM 's3://some-bucket/that/requires/authentication.parquet';

Mehr dazu in der Dokumentation.

Experimentelle Iceberg-Erweiterung. Dieses Release markiert den Start der DuckDB-Iceberg-Erweiterung. Sie ermöglicht das Lesen von Tabellen im Iceberg-Format.

SELECT count(*)
FROM iceberg_scan('data/iceberg/lineitem_iceberg', allow_moved_paths = true);

Mehr dazu in der Dokumentation.

Experimentelle Azure-Erweiterung. Dieses Release markiert den Start der DuckDB-Azure-Erweiterung. Damit kann DuckDB Daten auf Azure nativ lesen, ähnlich wie Daten auf S3.

SET azure_storage_connection_string = '<your_connection_string>';
SELECT * FROM 'azure://<my_container>/*.csv';

Mehr dazu in der Dokumentation.

Clients

Experimentelle PySpark-API. Dieses Release fügt dem Python-Client eine experimentelle Spark-API hinzu. Sie soll vollständig kompatibel zur PySpark-API sein: Sie nutzen die vertraute Spark-API, darunter arbeitet DuckDB. Alle Statements werden über unsere Relational API in DuckDBs interne Pläne übersetzt und von DuckDBs Query-Engine ausgeführt.

from duckdb.experimental.spark.sql import SparkSession as session
from duckdb.experimental.spark.sql.functions import lit, col
import pandas as pd
spark = session.builder.getOrCreate()
pandas_df = pd.DataFrame({
'age': [34, 45, 23, 56],
'name': ['Joan', 'Peter', 'John', 'Bob']
})
df = spark.createDataFrame(pandas_df)
df = df.withColumn(
'location', lit('Seattle')
)
res = df.select(
col('age'),
col('location')
).collect()
print(res)
#[
# Row(age=34, location='Seattle'),
# Row(age=45, location='Seattle'),
# Row(age=23, location='Seattle'),
# Row(age=56, location='Seattle')
#]

Die API ist derzeit experimentell, Features fehlen noch. Feedback ist uns sehr willkommen. Melden Sie fehlende Funktionalität bitte über Discord oder auf GitHub.

Schlussbemerkung

Die vollständigen Release Notes finden Sie auf GitHub. Wir danken allen Beitragenden für die Arbeit an DuckDB.