Description
La spécification officielle complète du formatNative est disponible ici, et la spécification complémentaire du protocole Native — le protocole TCP sous-jacent qui le transporte — est disponible ici.
Les deux spécifications ont été générées par des LLM à partir du code source de ClickHouse. Le code reste la principale source de référence : en cas de désaccord entre la spécification et le code, c’est le code qui fait foi.
Native est le format le plus efficace de ClickHouse, car il est véritablement « colonnaire »
dans le sens où il ne convertit pas les colonnes en lignes.
Dans ce format, les données sont écrites et lues par blocs, au format binaire.
Pour chaque bloc, le nombre de lignes, le nombre de colonnes, les noms et types des colonnes, ainsi que les parties des colonnes dans le bloc sont enregistrés les uns à la suite des autres.
C’est le format utilisé dans l’interface native pour les échanges entre serveurs, par le client en ligne de commande et par les clients C++.
Format filaire des types de données
Les données sont transmises dans un format colonnaire, ce qui signifie que chaque colonne est envoyée séparément et que toutes les valeurs d’une colonne sont envoyées ensemble sous la forme d’un seul tableau. Chaque colonne d’un bloc contient un en-tête similaire à RowBinaryWithNamesAndTypes.Lors de l’utilisation du protocole binaire TCP natif (ou lorsque le endpoint HTTP reçoit
?client_protocol_version=<n>),
une structure BlockInfo est écrite avant le nombre de colonnes et de lignes. Les exemples de cette section utilisent
l’interface HTTP standard sans version de protocole, ce qui omet BlockInfo.Structure d’un bloc
La requête suivante renvoie deux colonnes,number et str, et comporte trois lignes :
Blocs multiples
Cependant, dans de nombreux cas, les données ne tiendront pas dans un seul bloc, et ClickHouse les enverra sous forme de plusieurs blocs. Prenons la requête suivante, qui récupère deux lignes avec une taille de bloc réduite afin de forcer le découpage des données à raison d’une ligne par bloc :Types de données simples
Le format filaire d’une valeur individuelle de l’un de ces types de données simples est similaire àRowBinary/RowBinaryWithNamesAndTypes.
La liste complète des types correspondant à cette description est la suivante :
- (U)Int8, (U)Int16, (U)Int32, (U)Int64, (U)Int128, (U)Int256
- Float32, Float64
- Bool
- String
- FixedString(N)
- Date
- Date32
- DateTime
- DateTime64
- IPv4
- IPv6
- UUID
Types de données complexes
L’encodage des types suivants diffère deRowBinary et de RowBinaryWithNamesAndTypes.
- Nullable
- LowCardinality
- Array
- Map
- Variant
- Dynamic
- JSON
Nullable
Dans le formatNative, une colonne Nullable est précédée d’un nombre d’octets égal au nombre de lignes du bloc, avant les données proprement dites. Chacun de ces octets indique si la valeur est NULL ou non. Par exemple, avec cette requête, chaque nombre impair sera NULL à la place :
Nullable(String). L’indicateur de valeur nulle provient toujours de l’octet de masque de nullable —
une valeur de masque de 0x01 signifie que la ligne est NULL, quel que soit le contenu de la chaîne. Pour les lignes NULL,
la chaîne sous-jacente est stockée sous la forme d’une chaîne vide (longueur LEB128 0). Notez qu’une chaîne vide non NULL
a également une longueur LEB128 de 0 ; seul l’octet de masque permet donc de distinguer les deux cas. Par exemple, la requête suivante :
LowCardinality
Contrairement à RowBinary, oùLowCardinality est transparent, le format Native utilise un encodage colonnaire basé sur un dictionnaire. Une colonne est encodée sous la forme d’un préfixe de version, puis d’un dictionnaire de valeurs uniques et d’un tableau d’index entiers pointant vers ce dictionnaire.
Une colonne peut être définie comme
LowCardinality(Nullable(T)), mais il n’est pas possible de la définir comme Nullable(LowCardinality(T)) — cela renverra toujours une erreur du serveur.UInt64(LE) de valeur 1, écrit une fois par colonne. Ensuite, pour chaque bloc, les éléments suivants sont écrits :
UInt64(LE)— champ de bitsIndexesSerializationType. Les bits 0–7 encodent la largeur de l’index (0 = UInt8, 1 = UInt16, 2 = UInt32, 3 = UInt64). Le bit 8 (NeedGlobalDictionaryBit) n’est jamais défini dans le format Native (le serveur lève une exception s’il le rencontre). Le bit 9 indique la présence de clés de dictionnaire supplémentaires. Le bit 10 indique que le dictionnaire doit être réinitialisé.UInt64(LE)— nombre de clés du dictionnaire, suivi des clés sérialisées en bloc à l’aide de l’encodage du type interne.UInt64(LE)— nombre de lignes, suivi des valeurs d’index sérialisées en bloc à l’aide de la largeur UInt appropriée.
String, 0 pour les types numériques). Pour LowCardinality(Nullable(T)), l’index 0 représente NULL, et les clés sont sérialisées sans le wrapper Nullable.
Par exemple, LowCardinality(String) avec 5 lignes ['foo', 'bar', 'baz', 'foo', 'bar'] :
LowCardinality(Nullable(String)), l’indice 0 est NULL :
Array
Contrairement à RowBinary, où chaque array est préfixé par un nombre d’éléments au format LEB128, le format Native encode les arrays sous forme de deux sous-flux colonnaires :- N offsets
UInt64cumulatifs (little-endian, 8 octets chacun). La ligneicontientoffset[i] - offset[i-1]éléments, avecoffset[-1]implicitement égal à 0. - Tous les éléments imbriqués de toutes les lignes, sérialisés en bloc de façon contiguë.
Array(UInt32) avec 3 lignes [[0, 10], [1, 11], [2, 12]] :
Array(String) avec 4 lignes [[], ['0'], ['0','1'], ['0','1','2']] :
Map
UnMap(K, V) est encodé sous la forme Array(Tuple(K, V)) — les offsets du tableau, suivis de toutes les clés, puis de toutes les valeurs. Cela diffère de RowBinary, où les clés et les valeurs sont entrelacées pour chaque entrée.
Par exemple, Map(String, UInt64) avec 3 lignes [{'a':0,'b':10}, {'a':1,'b':11}, {'a':2,'b':12}] :
Variant
Contrairement à RowBinary, où chaque ligne contient son propre octet de discriminant suivi directement de la valeur, le format Native sépare les discriminants des données. Une colonneVariant est encodée comme suit :
- préfixe du mode des discriminants
UInt64(LE)(0= BASIC,1= COMPACT). La sortie du format Native utilise généralement BASIC (0) ; le mode COMPACT peut apparaître lors de la lecture de données stockées avecuse_compact_variant_discriminators_serializationactivé. - N discriminants
UInt8, un par ligne. - Les données de chaque variant type dans une colonne en bloc distincte ne contenant que les lignes correspondantes, dans l’ordre des discriminants.
Variant(String, UInt32) avec 5 lignes [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] (triés : String = 0, UInt32 = 1) :
Dynamic
Contrairement à RowBinary, où chaque valeur est autodescriptive (préfixe de type + valeur), le format Native sérialiseDynamic en un préfixe de structure suivi d’une colonne Variant.
Le préfixe de structure contient une version de sérialisation UInt64(LE), puis le nombre de types dynamiques (sous forme de VarUInt), puis les noms des types sous forme de chaînes. Dans la version V1, le nombre de types est écrit deux fois pour assurer la compatibilité. Les données qui suivent forment une colonne Variant dont la liste de types correspond aux types dynamiques, plus un type interne SharedVariant, triés par ordre alphabétique.
Par exemple, Dynamic avec 5 lignes [0::UInt32, 'hello', NULL, 3::UInt32, 'hello'] :
JSON
Contrairement à RowBinary, où chaque ligne est auto-descriptive avec des noms de chemins et des valeurs, le format Native sérialiseJSON dans une structure colonnaire. L’encodage est complexe et dépend de la version : il se compose d’un préfixe de structure avec la version de sérialisation, des noms de chemins dynamiques et l’organisation des données partagées, suivis de chemins typés (chacun sous la forme d’une colonne en bloc), de chemins dynamiques (chacun sous la forme d’une colonne Dynamic) et de données partagées pour les chemins en dépassement.
Pour une interopérabilité plus simple, envisagez d’utiliser le paramètre output_format_native_write_json_as_string=1, qui sérialise les colonnes JSON sous forme de simples chaînes de texte JSON (une String par ligne).