2024-12-09
Die DuckDB-Avro-Erweiterung
Hannes Mühleisen
Update: Avro-Unterstützung gibt es jetzt über die Core Extension
avro.
Das Format Apache™ Avro™
Avro ist ein Binärformat für Record-Daten. Wie viele Innovationen im Datenbereich wurde Avro von Doug Cutting im Rahmen des Apache-Hadoop-Projekts um 2009 entwickelt. Der Name stammt – etwas obskur – von einem untergegangenen britischen Flugzeughersteller. Das Unternehmen baute unter den schwierigen Bedingungen des Zweiten Weltkriegs über 7.000 Avro-Lancaster-Schwerbomber. Aber das führt vom Thema weg.
Das Avro-Format ist ein weiterer Versuch, das Dimensionsreduktionsproblem zu lösen, das entsteht, wenn eine komplexe mehrdimensionale Datenstruktur wie Tabellen (möglicherweise mit verschachtelten Typen) in ein eindimensionales Speicherlayout wie eine flache Datei überführt wird – also eine Folge von Bytes. Die grundlegendste Frage dabei: spalten- oder zeilenorientiertes Layout? Avro verwendet ein zeilenorientiertes Layout und unterscheidet sich damit von seinem berühmten Cousin, dem Format Apache™ Parquet™. Es gibt gute Gründe für ein zeilenorientiertes Format: Ein paar Zeilen an eine Parquet-Datei anzuhängen ist schwierig und ineffizient, wegen des spaltenorientierten Layouts und weil die Parquet-Metadaten am Ende der Datei stehen. In einem zeilenorientierten Format wie Avro mit den Metadaten vorn können wir die Zeilen „einfach“ ans Dateiende anhängen – fertig. So kann Avro das Anhängen weniger Zeilen einigermaßen effizient erledigen.
Avro-kodierte Daten können auf verschiedene Weise auftreten, z. B. in RPC-Nachrichten, aber auch in Dateien. Im Folgenden konzentrieren wir uns auf Dateien, weil sie langfristig überdauern.
Header-Block
Avro-„Object-Container“-Dateien sind in einem vergleichsweise einfachen binären Format kodiert: Jede Datei beginnt mit einem Header-Block, der zuerst die Magic Bytes Obj1 enthält. Danach folgt eine Metadaten-„Map“ (eine Liste von String-Bytearray-Schlüssel-Wert-Paaren). Die Map muss streng genommen nur einen Eintrag für den Schlüssel avro.schema enthalten. Dieser Schlüssel trägt das Avro-Dateischema als JSON. Ein Beispiel für ein solches Schema:
{ "namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "name", "type": "string"}, {"name": "favorite_number", "type": ["int", "null"]}, {"name": "favorite_color", "type": ["string", "null"]} ]}Das Avro-Schema definiert eine Record-Struktur. Records können skalare Datenfelder enthalten (wie int, double, string usw.), aber auch komplexere Typen wie Records (ähnlich DuckDB-STRUCTs), Unions und Listen. Nebenbei: Es ist ziemlich seltsam, dass ein Datenformat zur Definition von Record-Strukturen auf ein anderes Format wie JSON zurückfällt, um sich selbst zu beschreiben – aber das sind die Eigenheiten von Avro.
Datenblöcke
Der Header schließt mit 16 zufällig gewählten Bytes als „Sync Marker“. Danach folgen beliebig viele Datenblöcke: Jeder Datenblock beginnt mit einer Record-Anzahl, gefolgt von einer Größe und einem Byte-Array mit den eigentlichen Records. Optional können die Bytes mit Deflate (gzip) komprimiert sein; das steht in den Header-Metadaten.
Die Datenbytes lassen sich nur mit dem Schema dekodieren. Die Object-File-Spezifikation beschreibt, wie jeder Typ kodiert wird. Im Beispielschema wissen wir, dass jeder Wert ein Record mit drei Feldern ist. Der Root-Level-Record kodiert seine Einträge in der deklarierten Reihenfolge. Dafür sind keine eigenen Bytes nötig. Zuerst lesen wir das Feld name. Strings bestehen aus einer Länge, gefolgt von den String-Bytes. Wie andere Formate (z. B. Thrift) verwendet Avro variable-length integers mit Zigzag-Encoding für Längen, Zähler und Ähnliches. Nach dem String geht es weiter mit favorite_number. Dieses Feld ist ein Union-Typ (kodiert mit der Syntax []). Die Union kann Werte zweier Typen haben, int und null. Der Typ null ist etwas eigen: Er kann nur ausdrücken, dass ein Wert fehlt. Um die Felder favorite_number zu dekodieren, lesen wir zuerst ein int, das angibt, welche Alternative der Union verwendet wurde. Danach lesen wir die Werte mit den „normalen“ Decodern (z. B. int oder null). Dasselbe gilt für favorite_color. Jeder Datenblock endet wieder mit dem Sync Marker. Der Sync Marker prüft, dass der Block vollständig geschrieben wurde und keine Müllbytes in der Datei stehen.
Die DuckDB-Community-Extension avro
Wir haben eine DuckDB-Community-Extension entwickelt, mit der DuckDB Apache-Avro™-Dateien lesen kann.
Die Erweiterung enthält bewusst keine Avro-Schreib-Funktion. Ohne Writer hoffen wir, die Menge der Avro-Dateien in der Welt mit der Zeit zu verringern.
Installation und Laden
Die Installation über das DuckDB-Community-Extension-Repository ist einfach:
INSTALL avro FROM community;LOAD avro;in einer DuckDB-Instanz in Ihrer Nähe.
Seit DuckDB v1.2.1 wird auch DuckDBs WebAssembly-Client unterstützt.
Die Funktion read_avro
Die Erweiterung fügt eine einzige DuckDB-Funktion hinzu, read_avro. Verwendung:
FROM read_avro('some_example_file.avro');Die Funktion stellt den Inhalt der Avro-Datei als DuckDB-Tabelle bereit. Anschließend können Sie beliebige SQL-Konstrukte nutzen, um die Tabelle weiter zu transformieren.
Datei-I/O
Die Funktion read_avro ist in DuckDBs Dateisystem-Abstraktion integriert: Avro-Dateien lassen sich direkt von z. B. HTTP- oder S3-Quellen lesen. Zum Beispiel:
FROM read_avro('https://blobs.duckdb.org/data/userdata1.avro');FROM read_avro('s3://⟨my-example-bucket⟩/some_example_file.avro');sollte „einfach“ funktionieren.
Sie können auch mehrere Dateien globben oder eine Dateiliste übergeben:
FROM read_avro('some-example-file-*.avro');FROM read_avro(['some-example-file-1.avro', 'some-example-file-2.avro']);Stecken in den Dateinamen irgendwie wertvolle Informationen (leider allzu häufig), übergeben Sie das Argument filename an read_avro:
FROM read_avro('some-example-file-*.avro', filename = true);Dann enthält die Ergebnismenge eine zusätzliche Spalte mit dem tatsächlichen Dateinamen der Avro-Datei.
Schema-Konvertierung
Die Erweiterung übersetzt das Avro-Schema automatisch ins DuckDB-Schema. Alle Avro-Typen lassen sich übersetzen, außer rekursiven Typdefinitionen, die DuckDB nicht unterstützt.
Das Typ-Mapping ist sehr geradlinig, außer bei Avros „einzigartiger“ Behandlung von NULL. Anders als andere Systeme behandelt Avro NULL nicht als möglichen Wert in einem Bereich wie INTEGER, sondern als Union des eigentlichen Typs mit einem speziellen NULL-Typ. In DuckDB kann jeder Wert NULL sein. DuckDB unterstützt natürlich auch UNION-Typen, aber das wäre umständlich.
Diese Erweiterung vereinfacht das Avro-Schema wo möglich: Eine Avro-Union aus einem beliebigen Typ und dem speziellen Null-Typ wird auf den Nicht-Null-Typ reduziert. Ein Avro-Record vom Union-Typ ["int", "null"] (wie favorite_number im Beispiel) wird zu einem DuckDB-INTEGER, der eben manchmal NULL ist. Ebenso wird eine Avro-Union mit nur einem Typ in genau diesen Typ umgewandelt. Ein Avro-Record vom Union-Typ ["int"] wird ebenfalls zu einem DuckDB-INTEGER.
Die Erweiterung „flacht“ das Avro-Schema außerdem ab. Avro definiert Tabellen als Root-Level-„Record“-Felder, analog zu DuckDB-STRUCT-Feldern. Für bequemere Nutzung macht die Erweiterung die Einträge eines einzelnen Top-Level-Records zu Top-Level-Spalten.
Implementierung
Intern nutzt die Erweiterung die „offizielle“ Apache Avro C API, mit ein paar kleinen Patches, damit Avro-Dateien aus dem Speicher gelesen werden können.
Einschränkungen und nächste Schritte
Im Folgenden die Einschränkungen der DuckDB-Erweiterung avro und unsere Pläne, sie zu beheben:
-
Die Erweiterung nutzt derzeit keine Parallelität, weder beim Lesen einer einzelnen (großen) Avro-Datei noch beim Lesen einer Dateiliste. Parallelität im zweiten Fall steht auf der Roadmap.
-
Es gibt derzeit kein Pushdown von Projektion oder Filter; das ist ebenfalls für später geplant.
-
Wie oben erwähnt kann DuckDB rekursive Typdefinitionen aus Avro nicht ausdrücken. Das wird sich wohl nicht ändern.
-
Nutzer können keine separate Avro-Schema-Datei angeben. Das wird sich wahrscheinlich nicht ändern: Alle Avro-Dateien, die wir bisher gesehen haben, hatten das Schema eingebettet.
-
Das Flag
union_by_name, das andere Reader in DuckDB unterstützen, fehlt noch. Das ist für die Zukunft geplant.
Fazit
Die neue Community Extension avro für DuckDB ermöglicht es, Avro-Dateien direkt wie Tabellen zu lesen. Wenn Sie einen Haufen Avro-Dateien haben, probieren Sie es aus! Wir hören gerne von Ihnen, wenn etwas schiefgeht.