- ClickHouse führt einen neuen JSON-Typ ein, der Werte je JSON-Pfad in einem echten spaltenorientierten Speicher ablegt, um den Engpass zu vermeiden, JSON-Dokumente als Strings zu speichern und jedes Mal neu zu parsen
- Kern der Implementierung sind die Typen Variant und Dynamic; selbst wenn derselbe JSON-Pfad unterschiedliche Typen wie Integer, String oder Array enthält, werden sie nicht zwangsweise auf einen kleinsten gemeinsamen Typ vereinheitlicht
- Mit den Standardwerten
max_dynamic_paths 1024 und max_dynamic_types 32 wird die Zahl der Subspalten und typspezifischen Dateien begrenzt, um den Anstieg von File Descriptors und Merge-Kosten zu kontrollieren
- Mit Type Hints,
SKIP und SKIP REGEXP lässt sich die Speicherstrategie pro Pfad anpassen; Werte können über eine Subspalten-Syntax wie C.a.b gelesen werden
- Der neue Typ soll das deprecated
Object('json') ersetzen; Verbesserungen, um JSON-Key-Pfade in Primary Keys oder Data-Skipping-Indizes zu verwenden, stehen weiterhin auf der Roadmap
Die Herausforderung, JSON an spaltenorientierten Speicher anzupassen
- JSON wird als gängiges Format für halbstrukturierte und unstrukturierte Daten in Logs, Observability, Echtzeit-Datenstreaming, Mobile-App-Speichern und Machine-Learning-Pipelines verwendet
- ClickHouse ist eine echte spaltenorientierte Datenbank, die Tabellen als Sammlung von Spaltendateien auf der Festplatte speichert und darauf Kompression sowie vektorisierte Filter und Aggregationen ausführt
- Um auch mit JSON dieselbe Performance zu erreichen, müssen die Werte jedes eindeutigen JSON-Pfads wie Spalten gespeichert werden, statt Dokumente in einer String-Spalte abzulegen und später zu parsen
Vier Einschränkungen, die der neue JSON-Typ adressiert
-
Spaltenorientierte Speicherung pro Pfad
- Auch Werte je JSON-Pfad müssen sich wie normale Spalten, etwa numerische Spalten, komprimieren sowie vektorisiert filtern und aggregieren lassen
-
Dynamisch wechselnde Typen
- Im selben JSON-Pfad
a können unterschiedliche Typen wie Integer, Float oder Array auftreten
- ClickHouse kann das nicht im Voraus wissen, und die Typen sind möglicherweise nicht kompatibel; eine Zusammenführung auf einen kleinsten gemeinsamen Typ könnte Informationen verlieren
-
Vermeidung einer Explosion von Spaltendateien
- Wenn für jeden neuen JSON-Pfad eine neue Spaltendatei angelegt wird, steigt die Zahl der Dateien auf der Festplatte bei Daten mit vielen eindeutigen Keys stark an
- File Descriptors verbrauchen Speicher, und viele zu verarbeitende Dateien wirken sich auch auf die Merge-Performance aus
-
Dichte Speicherung seltener Keys
- Bei vielen eindeutigen, aber selten vorkommenden JSON-Keys sollten
NULL oder Default-Werte nicht für jede Zeile ohne Wert wiederholt gespeichert werden
- Nur tatsächliche Werte müssen dicht gespeichert werden, damit Analysen im PB-Maßstab skalierbar bleiben
Variant-Typ: die Grundlage ohne erzwungene Typvereinheitlichung
- Der Datentyp Variant ist eine unabhängige Funktion, die auch außerhalb von JSON verwendet werden kann, und erlaubt das Speichern und Lesen von Werten unterschiedlicher Datentypen in einer einzelnen Tabellenspalte
- Bestehende ClickHouse-Spalten haben einen festen Typ; eingefügte Werte müssen diesen Typ haben oder werden implizit konvertiert
Nullable-Spalten verwenden zusätzlich zur Wertedatei eine NULL-Maskendatei
Array speichert Array-Größen in einer separaten Datei und berechnet darüber Offsets
- Eine Variant-Spalte speichert Werte desselben konkreten Typs in typspezifischen Subspalten
- Beispiel: Alle
Int64-Werte werden in C.Int64.bin, alle String-Werte in C.String.bin gespeichert
- Welchen Typ jede Zeile verwendet, wird über eine
UInt8-Discriminator-Spalte verfolgt
- Der Discriminator-Wert ist der Index in einer sortierten Liste von Typnamen
- Discriminator
255 ist als NULL-Wert reserviert
- Aufgrund dieses Designs kann Variant maximal 255 konkrete Typen haben
- Typspezifische Datendateien sind als dichte Speicherung aufgebaut und enthalten nur Zeilen mit Werten
- Typspezifische Dateien speichern keine
NULL-Werte
- Um aus einer Discriminator-Zeile die Zeilenposition in der tatsächlichen Typdatei zu finden, wird eine
UInt64-Offset-Spalte im Speicher verwendet
- Dieser Offset wird nicht auf der Festplatte gespeichert und kann bei Bedarf aus der Discriminator-Spaltendatei erzeugt werden
- Variant unterstützt beliebige Verschachtelung
- Die Typreihenfolge von
Variant(T1, T2) und Variant(T2, T1) hat dieselbe Bedeutung
- In einem Variant kann wiederum ein Variant enthalten sein
- Werte eines bestimmten verschachtelten Typs werden gelesen, indem der Typname wie eine Subspalte angehängt wird
Dynamic-Typ: Speicherung ohne vorab bekannte Typliste
- Der Dynamic-Typ ist eine unabhängige Funktion, die auf Variant implementiert ist und auch außerhalb des JSON-Kontexts verwendet werden kann
- Dynamic erweitert Variant um zwei Funktionen
- In einer Spalte können Werte beliebiger Typen gespeichert werden, ohne die Typliste vorher festzulegen
- Die Zahl der Typen, die in separaten Spaltendateien gespeichert werden, kann begrenzt werden
- Die interne Speicherweise entspricht Variant, ergänzt um eine Datei
C.dynamic_structure.bin
- Diese Datei enthält die Liste der als Subspalten gespeicherten Typen sowie Größenstatistiken der typspezifischen Spaltendateien
- Diese Metadaten werden für das Lesen von Subspalten und für Merges von Datenparts verwendet
Dynamic(max_types=N) begrenzt die Zahl der Typen, die in separaten Dateien gespeichert werden
0 <= N < 255
- Der Standardwert ist 32
- Wird das Limit erreicht, werden Werte weiterer Typen in einer einzelnen Spaltendatei wie
C.SharedVariant.bin gespeichert
- Der Typ dieser Datei ist
String
- Jede Zeile enthält einen String-Wert der Struktur
<binary_encoded_data_type><binary_value>
- Werte mehrerer Typen können in einer einzelnen Spaltendatei gespeichert und wieder gelesen werden
- Auch Dynamic kann wie Variant bestimmte Typwerte lesen, indem der Typname als Subspalte verwendet wird
Deklaration und Speicherstruktur des JSON-Typs
- Der neue JSON-Typ speichert JSON-Objekte mit beliebiger Struktur und ermöglicht das Lesen jedes JSON-Werts als pfadbasierte Subspalte
- Die Typdeklaration kann optionale Parameter und Hints enthalten
<column_name> JSON(
max_dynamic_paths=N,
max_dynamic_types=M,
some.path TypeName,
SKIP path.to.skip,
SKIP REGEXP 'paths_regexp')
max_dynamic_paths
- Standardwert ist 1024
- Gibt die Zahl der JSON-Key-Pfade an, die als separate Subspalten gespeichert werden
- Pfade oberhalb des Limits werden gemeinsam in einer einzelnen Subspalte mit spezieller Struktur gespeichert
max_dynamic_types
- Standardwert ist 32
- Der Wertebereich reicht von
0 bis 254
- Gibt die Zahl der Datentypen an, die in einer einzelnen JSON-Key-Pfad-Spalte als separate Spaltendateien gespeichert werden
- Neue Typen oberhalb des Limits werden gemeinsam in einer einzelnen speziellen Spaltendatei gespeichert
some.path TypeName
- Ein Type Hint für einen bestimmten JSON-Pfad
- Dieser Pfad wird immer als Subspalte des angegebenen Typs gespeichert und bietet damit Performance-Garantien
SKIP path.to.skip
- Überspringt einen bestimmten JSON-Pfad beim Parsen
- Dieser Pfad wird nicht in der JSON-Spalte gespeichert
- Ist der angegebene Pfad ein verschachteltes JSON-Objekt, wird das gesamte verschachtelte Objekt übersprungen
SKIP REGEXP 'path_regexp'
- Überspringt beim JSON-Parsing Pfade, die auf den regulären Ausdruck passen
- Passende Pfade werden nicht in der JSON-Spalte gespeichert
JSON-Pfade wie Spalten lesen
- Jeder eindeutige Leaf-Pfadwert einer JSON-Spalte wird auf der Festplatte auf eine von zwei Arten gespeichert
- Pfade mit Type Hint werden als normale Spaltendateien gespeichert
- Pfade, deren Typ sich dynamisch ändern kann, werden als Dynamic-Subspalten gespeichert
- Der JSON-Typ verwendet eine spezielle Datei namens
object_structure
- Sie enthält Metadaten zu dynamischen Pfaden
- Sie enthält Statistiken zu Non-Null-Werten jedes dynamischen Pfads
- Sie wird zum Lesen von Subspalten und für Merges von Datenparts verwendet
- Die Explosion von Spaltendateien wird über zwei Begrenzungsstufen kontrolliert
max_dynamic_types begrenzt innerhalb eines JSON-Key-Pfads die Zahl der Typen, die in separaten Dateien gespeichert werden
max_dynamic_paths begrenzt die Zahl der JSON-Key-Pfade, die als separate Subspalten gespeichert werden
- Zusätzliche dynamische JSON-Pfade oberhalb des
max_dynamic_paths-Limits werden als shared data gespeichert
- Beispieldateien sind
C.object_shared_data.size0.bin, C.object_shared_data.paths.bin, C.object_shared_data.values.bin
object_shared_data.values hat den Typ String
- Jeder Eintrag hat die Struktur
<binary_encoded_data_type><binary_value>
- Auch für shared data werden zusätzliche Statistiken in
object_structure.bin gespeichert
- Für die ersten 10000 Pfade unter den aktuell in der shared-data-Spalte gespeicherten Pfaden werden Non-Null-Wertstatistiken gespeichert
JSON-Pfadsyntax und verschachtelte Objekte
- Der JSON-Typ kann den Leaf-Wert jedes Pfads als auf dem Pfadnamen basierende Subspalte lesen
- Werte von Pfaden ohne Type Hint haben immer den Typ Dynamic
- Beispiel: Der Typ von
C.a.d ist Dynamic
- Subspalten der Untertypen von Dynamic werden mit spezieller JSON-Syntax gelesen
- Verschachtelte JSON-Objekte können mit der Syntax
JSON_column.^some.path wie Subspalten vom JSON-Typ gelesen werden
- Die aktuelle Dot-Syntax liest aus Performance-Gründen keine verschachtelten Objekte
- Für das Lesen literaler Werte pro Pfad ist die aktuelle Speicherstruktur effizient
- Um ein vollständiges Unterobjekt pro Pfad zu lesen, müssen mehr Daten gelesen werden, was langsamer sein kann
- Für die Rückgabe von Objekten ist die Syntax
.^ erforderlich
- ClickHouse plant, die beiden
.-Syntaxvarianten zusammenzuführen
Kompakte Discriminator-Serialisierung
- Viele dynamische JSON-Pfade können überwiegend denselben Werttyp haben
- Wenn es viele eindeutige, aber selten vorkommende JSON-Pfade gibt, enthalten die Discriminator-Dateien der einzelnen Pfade hauptsächlich
255, also NULL-Werte
- Solche Dateien lassen sich zwar gut komprimieren, enthalten aber weiterhin viele Duplikate, wenn alle Zeilen denselben Wert haben
- ClickHouse implementiert ein compact format für die Discriminator-Serialisierung
- Statt üblicherweise alle
UInt8-Discriminator-Werte zu schreiben, werden nur drei Werte serialisiert, wenn die Discriminators des Ziel-Granules alle gleich sind
- Kennzeichen für das compact granule format
- Kennzeichen für die Zahl der Werte in diesem Granule
- Discriminator-Wert
- Diese Optimierung wird über die MergeTree-Einstellung
use_compact_variant_discriminators_serialization gesteuert
- Standardmäßig ist sie aktiviert
- In manchen Fällen werden statt 8192 Werten bei normaler Index-Granularität nur drei Werte gespeichert
Release-Status und nächste Schritte
- Der neue JSON-Typ ist als Ersatz für den deprecated Typ Object('json') konzipiert
- Die Implementierung ist im ClickHouse-24.08-Release als experimental Feature zu Testzwecken verfügbar
- Die JSON-Roadmap enthält Verbesserungen, um JSON-Key-Pfade im Primary Key einer Tabelle oder in Data-Skipping-Indizes (data-skipping index) zu verwenden
- Bausteine wie Variant und Dynamic bilden auch die Grundlage für die Unterstützung weiterer halbstrukturierter Typen wie XML und YAML
- ClickHouse-Cloud-Nutzer, die den neuen JSON-Datentyp testen möchten, müssen sich an den ClickHouse-Support wenden und Zugang zur private preview anfordern
Noch keine Kommentare.