2 Punkte von GN⁺ 2024-10-23 | Noch keine Kommentare. | Auf WhatsApp teilen
  • 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
    • Beispiel: C.Int64

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
    • Beispiel: C.Int64

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
    • Beispiel: C.a.b
  • 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
    • Beispiel: C.a.d.:Int64
  • Verschachtelte JSON-Objekte können mit der Syntax JSON_column.^some.path wie Subspalten vom JSON-Typ gelesen werden
    • Beispiel: C.^a
  • 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.

Noch keine Kommentare.