Skip to main content
Le protocole natif est le protocole binaire orienté connexion utilisé par les clients et serveurs ClickHouse sur TCP. Il transporte les requêtes SQL, les données de résultat, les charges utiles INSERT, la télémétrie d’exécution et les signaux d’erreur. C’est le protocole sous-jacent au client en ligne de commande, au client C++ et à la plupart des drivers natifs tiers. Cette page couvre le protocole lui-même : tramage des paquets, machine à états de la connexion, négociation de version et corps de chaque message autre que Block. Les octets à l’intérieur des paquets de la famille Data (le Block, ses colonnes et les encodages propres à chaque type) relèvent d’un sujet distinct, documenté dans la spécification Native Format.
Spécification complémentaireCette page est l’un des deux volets de cet ensemble et est publiée avec la spécification complémentaire Native Format. Les deux spécifications se répartissent clairement les rôles : cette page couvre la couche des paquets et du transport ; la spécification Native Format couvre les octets à l’intérieur des paquets de la famille Data.
Quelques propriétés restent vraies dans l’ensemble du protocole. Le protocole est binaire et positionnel : il n’y a pas de balises de champ, sauf dans BlockInfo, donc un seul octet mal placé désynchronise tout ce qui suit. Il est avec état, et chaque connexion TCP traite une requête à la fois — il n’y a pas de multiplexage. Les entiers à largeur fixe sont en little-endian.

Vue d’ensemble

PropriétéValeur
TransportTCP, éventuellement encapsulé dans TLS
Ordre des octetsLittle-endian pour les entiers à largeur fixe
EncodageBinaire et positionnel (sans balises de champ, sauf dans BlockInfo)
Modèle de connexionAvec état, une requête à la fois, sans multiplexage
VersionnementNégocié lors du handshake ; certaines fonctionnalités dépendent de la version
Format de donnéesLe Native Format pour toutes les données tabulaires
Chaque message transmis commence par un code de type de paquet VarUInt, suivi d’un corps dont la structure dépend de ce code et de la version du protocole négociée. Une connexion se déroule en trois phases — un handshake initial unique, puis un nombre quelconque d’échanges Ping ou Query, puis la fermeture : Le protocole TCP natif transporte toujours des données tabulaires au format Native, quelle que soit la clause FORMAT dans la requête SQL. Le reformatage en RowBinary, CSV, JSON, etc. relève du client et s’effectue après le décodage des blocs Native. (L’interface HTTP suit un chemin de code différent qui, lui, respecte bien la clause FORMAT ; HTTP n’est pas traité ici.)

Sécurité

Sécurité de transport (TLS)

TLS se situe à la couche de transport, en dessous du protocole. Lorsqu’il est activé, l’ensemble du flux TCP est chiffré, et les messages du protocole sont rigoureusement identiques octet pour octet, que TLS soit utilisé ou non.

Authentification

L’authentification a lieu lors du handshake, dans le message ClientHello. Les champs user et password sont transmis sous forme de chaînes en clair ; la protection des credentials en transit repose donc sur le chiffrement au niveau du transport (TLS). L’authentification SSH par challenge-response est disponible à partir de la version 54466 du protocole — voir Authentification SSH par challenge-response.

Secret inter-serveurs

Pour l’exécution de requêtes distribuées, les serveurs s’authentifient mutuellement en prouvant qu’ils connaissent un secret partagé, sans transmettre ce secret sur le réseau. Chaque Query contient un auth_hash SHA-256 de 32 octets dans le champ 4 de Query, calculé à partir d’un salt, d’un nonce, du secret configuré et de la requête, puis recalculé et comparé par le serveur destinataire. Ce mécanisme est conditionné par la fonctionnalité INTERSERVER_SECRET (v54441). Les clients externes envoient toujours ici une chaîne vide. Voir Authentification inter-serveurs.

Gestion des versions et feature gates

Négociation de la version

Le client et le serveur indiquent tous deux la version maximale du protocole qu’ils prennent en charge lors de l’établissement de la connexion. La version négociée est la plus petite des deux :
negotiated_version = min(client_version, server_version)
Chaque message ultérieur utilise la version négociée pour déterminer quels champs sont présents dans les données binaires sérialisées transmises.

Feature gates

Une fonctionnalité est définie par la version du protocole qui l’a introduite, et elle est active lorsque la version négociée est supérieure ou égale à ce numéro.
Lorsqu’une fonctionnalité est active, ses champs doivent être présents dans le flux binaire. Le protocole étant strictement positionnel, l’omission d’un champ soumis à un feature gate corrompt le flux d’octets de tous les champs suivants.

Tableau des fonctionnalités

FonctionnalitéVersionAffecteImpact sur le format binaire
BLOCK_INFOallBlockAjoute le préfixe BlockInfo (is_overflows, bucket_number) à chaque Block.
CLIENT_INFO54032QueryAjoute le bloc ClientInfo au corps de Query.
TIMEZONE54058ServerHelloAjoute le champ timezone à ServerHello.
QUOTA_KEY_IN_CLIENT_INFO54060ClientInfoAjoute le champ quota_key à ClientInfo.
DISPLAY_NAME54372ServerHelloAjoute le champ display_name à ServerHello.
VERSION_PATCH54401ServerHello, ClientInfoAjoute le champ version_patch aux deux.
SERVER_LOGS54406LogLe serveur émet des paquets Log lorsque send_logs_level est défini.
COLUMN_DEFAULTS_METADATA54410TableColumnsLe serveur peut envoyer le paquet TableColumns (type 11) avec les métadonnées des valeurs par défaut des colonnes avant le bloc de schéma d’entrée/INSERT. Envoyé uniquement lorsque la version négociée ≥ 54410 et que input_format_defaults_for_omitted_fields est activé. En dessous de cette version, le paquet n’est jamais envoyé ; les clients ne doivent pas l’attendre.
WRITE_CLIENT_INFO54420ProgressAjoute wrote_rows et wrote_bytes à Progress. (Malgré son nom, cela ne contrôle pas le bloc ClientInfo — c’est le rôle de CLIENT_INFO (v54032).)
SETTINGS_SERIALIZED_AS_STRINGS54429Query (encodage des settings)Modifie la manière dont la liste des settings, toujours présente, est encodée ; ne contrôle pas si les settings sont envoyés. v54429+ écrit chaque setting sous la forme (name, flags, value-as-string) ; les pairs plus anciens écrivent (name, type-specific-binary-value) sans indicateurs. Voir Setting.
INTERSERVER_SECRET54441QueryAjoute à Query le champ inter-serveur auth_hash — un SHA-256 salé du secret du cluster, et non le secret brut. Les clients externes envoient une chaîne vide. Voir Inter-server authentication.
OPEN_TELEMETRY54442ClientInfoAjoute le trace context OpenTelemetry à ClientInfo.
DISTRIBUTED_DEPTH54448ClientInfoAjoute le champ distributed_depth à ClientInfo.
INITIAL_QUERY_START_TIME54449ClientInfoAjoute le champ initial_time (Int64, largeur fixe).
PROFILE_EVENTS54451ProfileEventsLe serveur émet des paquets ProfileEvents pendant l’exécution de la requête.
PARALLEL_REPLICAS54453ClientInfoAjoute à ClientInfo des champs de coordination des replicas parallèles.
CUSTOM_SERIALIZATION54454Block (Column)Ajoute l’octet has_custom_serialization après la type string de chaque colonne.
ADDENDUM54458HandshakeLe client envoie un addendum (quota_key) après l’échange de handshake.
PARAMETERS54459QueryAjoute la liste des paramètres au corps de Query.
SERVER_QUERY_TIME_IN_PROGRESS54460ProgressAjoute le champ elapsed_ns à Progress.
PASSWORD_COMPLEXITY_RULES54461ServerHelloAjoute à ServerHello une liste de motifs regex de politique de mot de passe et de messages lisibles par l’utilisateur.
INTERSERVER_SECRET_V254462ServerHelloAjoute à ServerHello un nonce UInt64 de 8 octets. Utilisé pour la signature des requêtes inter-serveurs ; les clients externes le décodent et l’ignorent.
TOTAL_BYTES_IN_PROGRESS54463ProgressAjoute à Progress le champ total_bytes_to_read (VarUInt), entre total_rows et wrote_rows.
TIMEZONE_UPDATES54464TimezoneUpdateAjoute le paquet serveur TimezoneUpdate (type 17). Corps : un seul String contenant le timezone de la session. Envoyé uniquement par l’initialiseur de la table function input, juste après le bloc de schéma d’entrée, afin que le client analyse les lignes qu’il envoie avec la session_timezone du serveur. Voir TimezoneUpdate.
SPARSE_SERIALIZATION54465Block (Column)Le serveur peut définir has_custom_serialization = 1 et émettre une colonne encodée en sparse. Wire format : type sur 1 octet (0x01 = SPARSE), puis flux de décalages VarUInt terminé par EOG, puis les valeurs non par défaut encodées de manière dense dans le type interne. Voir kind_stack and sparse encoding.
SSH_AUTHENTICATION54466Flux d’authentificationAjoute l’authentification SSH par challenge-response. Opt-in : le client envoie un user de la forme " SSH KEY AUTHENTICATION " + <real_user> avec un mot de passe vide pour la déclencher. Voir SSH challenge-response authentication.
TABLE_READ_ONLY_CHECK54467TablesStatusResponseAjoute un indicateur is_readonly à la ligne de chaque table dans TablesStatusResponse. Les clients externes qui n’émettent pas TablesStatusRequest ne voient aucun changement du wire format.
SYSTEM_KEYWORDS_TABLE54468tables systèmeLe serveur renseigne system.keywords afin que le clickhouse-client canonique puisse autocompléter les keywords. Aucun changement du wire format du protocole natif.
ROWS_BEFORE_AGGREGATION54469ProfileInfoAjoute applied_aggregation (Bool) et rows_before_aggregation (VarUInt) à ProfileInfo, dans cet ordre à la fin.
CHUNKED_PROTOCOL54470Tramage de connexionLe tramage par fragments de chaque paquet encapsule chaque corps de paquet. Négocié dans Addendum. ServerHello contient la préférence du serveur pour chaque direction ; Addendum contient le choix final du client. Voir chunked framing.
VERSIONED_PARALLEL_REPLICAS_PROTOCOL54471ServerHello, AddendumLes deux côtés échangent une version VarUInt du protocole de coordination des répliques parallèles. Le champ de ServerHello est placé immédiatement après protocol_version (avant timezone). Le champ d’Addendum est ajouté après les chaînes du protocole fragmenté. Valeur actuelle : 7 (DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION).
INTERSERVER_EXTERNALLY_GRANTED_ROLES54472QueryAjoute un champ String external_roles au corps de Query, entre le terminateur des settings et le hachage du secret inter-server. Les clients externes envoient une liste de rôles vide (un seul octet 0x00, c.-à-d. VarUInt 0 dans une enveloppe String).
V2_DYNAMIC_AND_JSON_SERIALIZATION54473Column bodyLe serveur peut émettre la sérialisation V2 pour les types de colonnes Dynamic et JSON — cela détermine la version de state_prefix utilisée. Voir versioned types.
SERVER_SETTINGS54474ServerHelloLe serveur diffuse ses server settings non par défaut sous forme de liste à la fin de ServerHello, après nonce. Format : triplets (key, flags, value) terminés par une clé vide — identique à la settings list du Query packet.
QUERY_AND_LINE_NUMBERS54475ClientInfoAjoute script_query_number (VarUInt) et script_line_number (VarUInt) à la fin de ClientInfo. Utilisé par clickhouse-client pour attribuer les erreurs dans les scripts multi-instructions ; les clients externes envoient 0, 0.
JWT_IN_INTERSERVER54476ClientInfoAjoute un indicateur de présence JWT de type UInt8 ainsi qu’un String jwt optionnel à la fin de ClientInfo. Les clients externes (sans JWT) envoient l’octet 0x00. (Écrit DBMS_MIN_REVISON_WITH_JWT_IN_INTERSERVER en C++ — notez la faute de frappe dans le nom de la constante.)
QUERY_PLAN_SERIALIZATION54477ServerHello, QueryPlan packetServerHello ajoute VarUInt query_plan_serialization_version après les server settings. Introduit également ClientPacket::QueryPlan (code 13) pour le transport inter-server de plans de requête préconstruits — les clients externes ne l’envoient jamais.
PARALLEL_BLOCK_MARSHALLING54478Block (Column)Le serveur peut encapsuler les colonnes dans ColumnBLOB (compressé inline) pour le traitement parallèle. Dépend du fait que la requête ait la compression activée ET rows > 1 ; sinon, le format binaire habituel des colonnes s’applique. Les clients qui n’activent jamais la compression sur les Query packets sortants ne voient aucun changement sur le wire.
VERSIONED_CLUSTER_FUNCTION_PROTOCOL54479ServerHelloAjoute VarUInt cluster_function_protocol_version à la fin de ServerHello. Utilisé pour les table functions *Cluster (s3Cluster, etc.). Les clients externes le décodent et l’ignorent.
OUT_OF_ORDER_BUCKETS_IN_AGGREGATION54480BlockInfoAjoute le champ 3 (out_of_order_buckets: Vec<Int32>) au flux de BlockInfo balisé par champs. Décodé comme [VarUInt count][Int32]*count. Les clients externes ne l’émettent pas eux-mêmes ; le décodeur lit toute liste non vide envoyée par le serveur.
COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS54481Log, ProfileEvents, TableColumnsLe serveur peut encapsuler les corps des paquets Log, ProfileEvents et TableColumns dans la trame de compression. À cette version, les trois corps passent par le même chemin de sortie éventuellement compressé, qui ne devient une véritable trame de compression que lorsque la requête a compression = true. Les clients qui n’activent jamais la compression sur les Query packets sortants ne voient aucun changement sur le wire.
REPLICATED_SERIALIZATION54482Block (Column)Le serveur peut émettre des colonnes avec kind&#95;stack 0x04 = REPLICATED — une forme compacte de type dictionnaire pour les valeurs répétées — voir kind_stack and sparse encoding. En dessous de cette version, l’émetteur développait ces colonnes avant l’envoi. Décodé via recherche d’index (elements[indexes[i]] par row) ; types feuille et types internes Nullable/Array/Tuple/Map/Nested/LowCardinality pris en charge.
NULLABLE_SPARSE_SERIALIZATION54483Block (Column)Compose la sérialisation sparse avec Nullable(T). En dessous de cette version, l’émetteur développait sparse pour les colonnes Nullable avant l’envoi ; à partir de v54483, les données sur le wire sont sparse-over-Nullable. Voir kind_stack and sparse encoding.
PROGRESS_IN_ASYNC_INSERT54484Progress (INSERT)Lors d’un INSERT asynchrone (async_insert = 1), une fois l’insert flushé, le serveur envoie un paquet Progress supplémentaire, puis les ProfileEvents de l’insert, avant EndOfStream. Dépend de la version négociée ≥ 54484 ; en dessous, le serveur omet ce Progress final. Le format binaire de Progress est inchangé — seule son émission est nouvelle. En pratique, l’incrément transporte le temps écoulé ; les compteurs de rows écrites sont signalés via les ProfileEvents associés. Un client qui draine déjà les paquets Progress entrelacés n’a besoin d’aucun changement de format, seulement de tolérer un paquet supplémentaire.
CLIENT_AGENT_IN_CLIENT_INFO54485ClientInfoAjoute un String client_agent final à ClientInfo. Le client canonique détecte automatiquement un identifiant d’agent dans son environnement (par exemple claude-code, cursor, gemini-cli ou la valeur de la variable AGENT) ; un client externe pour lequel rien n’est détecté envoie une chaîne vide. Obligatoire dès lors que la version négociée est ≥ 54485 — l’omettre désynchronise le reste du Query packet.

Enveloppe de paquet

Tous les messages transmis sur le réseau ont la même structure externe, dans les deux sens :
[VarUInt: packet_type_code]    always encoded as VarUInt
[message body]                 format depends on packet_type_code
Les tableaux complets des types de paquets se trouvent dans la référence des types de paquets. Le type de paquet est un VarUInt, et non un octet de largeur fixe. Pour les valeurs inférieures à 128, un VarUInt produit le même octet, mais les implémentations doivent utiliser l’encodage VarUInt afin de rester compatibles si de futurs types de paquets devaient atteindre 128 ou plus. La référence des messages documente uniquement la charge utile de chaque paquet — les octets situés après le code du type de paquet. La numérotation des champs commence à 1 avec le premier champ de la charge utile.

Tramage par fragments (v54470+)

Lorsque la fonctionnalité CHUNKED_PROTOCOL est négociée (voir le handshake), chaque paquet sur le wire est tramé par fragments. Ce tramage est propre à chaque sens : client→serveur et serveur→client sont négociés séparément et peuvent au final utiliser des modes différents (avec fragments ou sans encapsulation). Format binaire par paquet :
<chunk>...   one or more chunks; their payloads concatenated form the whole packet
[u32 LE = 0] zero-size terminator marking end of packet
Format binaire par fragment :
[u32 LE: chunk_size]   chunk_size in [1, UINT32_MAX]
[chunk_size bytes]     packet bytes (see note below)
Le type de paquet VarUInt se trouve dans le flux découpé en fragments : c’est le premier octet de la charge utile du paquet (le premier octet du premier fragment), et non un octet distinct envoyé avant le tramage. La charge utile fragmentée de chaque paquet correspond à l’intégralité de [VarUInt packet_type_code][message body] de l’enveloppe de paquet. Un client qui laisse le type de paquet en dehors du flux découpé en fragments amène le pair à lire cet octet de type comme le premier octet de la taille de fragment u32, ce qui désynchronise la connexion. Un même paquet peut être réparti sur plusieurs fragments si le tampon d’écriture se remplit en plein milieu du paquet ; la coupure peut se produire n’importe où, y compris au milieu du VarUInt du type de paquet. Le lecteur concatène les charges utiles des fragments et traite le zéro final sur 4 octets comme une limite de paquet transparente — il le consomme, mais ne le transmet pas à ce qui lit les corps de paquet. Les paquets sans corps restent enveloppés : un paquet d’un seul octet comme Ping ou Pong devient [u32 size = 1][0x04][u32 0] une fois le découpage en fragments négocié. Toute mention ailleurs sur cette page d’un « octet unique sur le réseau » correspond à la forme antérieure au découpage en fragments. Négociation. ServerHello et Addendum transportent chacun deux champs String, un par direction, avec des valeurs tirées de {"chunked", "notchunked", "chunked_optional", "notchunked_optional"} :
  • chunked / notchunked sont stricts : ce côté exige exactement ce mode.
  • Les variantes _optional sont souples : elles acceptent le mode choisi par l’autre côté.
La valeur retenue pour chaque direction est calculée par paire :
Préférence serveurPréférence clientValeur retenue
*_optionaln’importe laquellesuivre le CLIENT (son starts_with("chunked"))
n’importe laquelle*_optionalsuivre le SERVEUR
chunked strictchunked strictchunked
notchunked strictnotchunked strictnotchunked
incompatibilité stricteincompatibilité stricteerreur de protocole — la connexion DOIT être interrompue
Côté client, la préférence d’ENVOI du client est négociée avec la préférence de RÉCEPTION du serveur, et inversement. Temporalité. Les chaînes de négociation transitent sur le réseau sans tramage : ClientHelloServerHello (préférences du serveur) → Addendum (valeurs négociées du client). Le basculement vers le tramage s’applique à chaque octet envoyé après l’écriture de l’Addendum. L’Addendum lui-même, le ClientHello et le ServerHello sont toujours envoyés sans tramage.

Cycle de vie de la connexion

À tout moment, une connexion se trouve dans un seul des quatre états suivants : HANDSHAKE, READY, READING_RESPONSE, ou terminée. Comme le protocole ne prend pas en charge le multiplexage, un client qui envoie une nouvelle requête avant d’avoir entièrement lu la réponse précédente entrelace les octets dans le flux de données transmis et corrompt le flux.

États

Le scénario nominal suit un chemin direct — HANDSHAKE → READY → READING_RESPONSE → READY — avec la boucle Ping/Pong sur elle-même, et toutes les transitions d’échec convergent vers l’unique sink Terminated.
StateDescription
HANDSHAKEÉtat initial après l’ouverture de la connexion TCP. Seuls les messages de handshake sont valides. Passe à READY en cas de succès ou se termine en cas d’échec.
READYAu repos. Le client peut envoyer Ping, Query, ou fermer la connexion. La connexion peut rester en READY indéfiniment (sous réserve de idle_connection_timeout, voir les limites de connexion).
READING_RESPONSEÉtat atteint lorsque le client envoie une Query. Le client doit vider intégralement le flux de réponse du serveur avant de revenir à READY. Le seul paquet client→serveur autorisé ici est Cancel (non décrit sur cette page).
TerminatedN’est plus utilisable. Le client doit ouvrir une nouvelle connexion TCP et relancer le handshake.

Phase de handshake

Authentification et négociation de la version du protocole. Cette phase ne se produit qu’une seule fois par connexion, avant toute autre opération. La connexion TCP vient de s’ouvrir et aucun message n’a encore été échangé. Le déroulement :
  1. Le client envoie ClientHello avec la version maximale du protocole qu’il prend en charge.
  2. Le client lit la réponse et la traite selon le type de paquet :
    Type de paquetAction
    Hello (0)Décode ServerHello. Calcule negotiated_version = min(client_ver, server_ver). Passe à l’étape 3.
    Exception (2)Décode Exception. Le retourne comme erreur et termine la connexion.
    anything elseViolation du protocole. Termine la connexion.
  3. Si negotiated_version ≥ 54458 (la fonctionnalité ADDENDUM), le client envoie un Addendum. Cette décision repose sur la version négociée, et non sur la version déclarée du client.
En cas de succès, la connexion passe à READY ; en cas d’erreur, elle est interrompue.

Phase Ping

Une vérification de vivacité au niveau de l’application, indépendante du keepalive TCP. Un aller-retour Ping/Pong réussi confirme que la connexion TCP est active dans les deux sens et que le serveur répond. Ping est sans état et n’est corrélé à aucune requête, de sorte que plusieurs Pings successifs sont indépendants. À partir de READY, le flux est :
  1. Le client envoie Ping.
  2. Le client lit la réponse :
    Type de paquetAction
    Pong (4)Disponibilité confirmée. Revenir à READY.
    Exception (2)Décoder Exception et la renvoyer en erreur.
    tout autre casViolation du protocole.

Phase de requête

Le client soumet une instruction SQL ; le serveur renvoie en continu des blocs de résultats et la télémétrie d’exécution. La réponse est une séquence de paquets qui se termine par exactement un EndOfStream ou une Exception. À partir de READY, le flux est : En cas d’erreur à n’importe quelle étape, le serveur envoie une Exception au lieu de EndOfStream, ce qui met fin à la requête.
  1. Le client envoie Query avec un query_id unique (généralement un UUID).
  2. Le client envoie les tables externes éventuelles, puis le marqueur Data vide. Le paquet Data vide a table_name = "", num_columns = 0, num_rows = 0. Le serveur ne commence pas à exécuter la requête tant qu’il n’a pas reçu ce marqueur.
  3. Le client passe à READING_RESPONSE et vide son tampon d’écriture.
  4. Le client lit les paquets de réponse en boucle et les traite selon leur type :
    Type de paquetAction
    Data (1)Décode le bloc. Le premier paquet Data est l’en-tête de schéma ; les suivants sont des blocs de résultat (à accumuler) ; un bloc vide est un marqueur de délimitation. num_rows == 0 n’est pas la fin de la requête.
    Progress (3)Métriques d’exécution. Chaque paquet est un incrément par rapport au précédent — à accumuler localement.
    EndOfStream (5)Requête terminée. Quitter la boucle et revenir à READY.
    ProfileInfo (6)Données de profiling post-exécution.
    Totals (7)Bloc des totaux d’agrégation (même format binaire que Data).
    Extremes (8)Bloc des valeurs min/max (même format binaire que Data).
    Log (10)Ligne du journal du serveur.
    TableColumns (11)Métadonnées des valeurs par défaut des colonnes.
    ProfileEvents (14)Compteurs de performance.
    Exception (2)Décode et renvoie comme erreur. Quitter la boucle et revenir à READY.
    tout autre typeInattendu pendant la phase de requête. Mettre fin à la connexion.
À la réception de EndOfStream ou d’une Exception gérée, la connexion revient à READY. Une violation du protocole ou une erreur d’E/S y met fin.
Le cas num_rows == 0 piège souvent les nouvelles implémentations. Un bloc de zéro ligne est un marqueur de délimitation ou un en-tête de schéma, pas un signal de fin de flux. Seuls EndOfStream ou Exception mettent fin à la réponse.

Phase INSERT

La phase INSERT correspond à la phase de requête, avec deux échanges supplémentaires. Le client soumet une instruction INSERT ; le serveur répond avec un bloc de schéma décrivant la table cible ; le client envoie des paquets Data contenant les lignes, puis le marqueur Data vide ; le serveur termine avec EndOfStream ou Exception. À partir de READY, la requête SQL est un INSERT de la forme INSERT INTO <table> [(<cols>)] VALUES — sans littéral VALUES (...) intégré à la requête, puisque les données des lignes transitent via des paquets Data. Le flux :
  1. Le client envoie Query avec body contenant la requête SQL INSERT.
  2. Le client envoie les éventuelles tables externes (cas rare pour INSERT). Contrairement à la phase de requête, il n’envoie pas ici de marqueur Data vide. Le paquet Query de INSERT est envoyé avec des données en attente ; le bloc vide de fin de données est donc reporté à l’étape 5. L’envoyer avant le bloc de schéma amènerait le serveur à l’interpréter comme la fin du flux de lignes, à terminer l’INSERT sans aucune ligne, puis à analyser le premier vrai paquet de lignes comme un paquet de niveau supérieur parasite.
  3. Le client consomme les paquets de métadonnées (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) jusqu’à lire le paquet Data de schéma — un Block avec 0 ligne, mais la structure complète des colonnes (noms et types). Le bloc de schéma fait foi : les lignes envoyées ensuite par le client doivent correspondre à cette structure de colonnes.
  4. Le client envoie un ou plusieurs blocs de données. Pour chaque bloc, il écrit VarUInt(ClientPacket::Data = 2), puis String("") pour le nom vide de la table externe, puis le Block. Les types de colonnes doivent correspondre, par position, à ceux des colonnes du bloc de schéma.
  5. Le client envoie le terminateur de fin d’entrée : un paquet Data avec un Block vide (0 colonne, 0 ligne).
  6. Le client consomme le flux de réponse jusqu’à EndOfStream (succès) ou Exception (échec).
INSERT asynchrone (v54484+). Lorsque la requête contient async_insert = 1, le serveur place les lignes en file d’attente et les écrit dans le cadre d’un lot. À la version négociée ≥ 54484 (PROGRESS_IN_ASYNC_INSERT), une fois l’écriture terminée, le serveur émet un paquet Progress supplémentaire, immédiatement suivi des ProfileEvents de l’INSERT, puis de EndOfStream. En dessous de 54484, le serveur omet ce Progress final. Ce paquet est un Progress ordinaire ; comme le serveur réinitialise le pipeline de requête avant d’y intégrer les compteurs d’écriture, l’incrément ne contient en pratique que le temps écoulé, et les statistiques sur les lignes et octets écrits parviennent au client via les ProfileEvents associés. Un client qui consomme déjà les paquets Progress entrelacés à l’étape 6 n’a qu’à accepter un paquet de plus. La connexion revient à l’état READY à la réception de EndOfStream ou d’une Exception gérée. Les violations du protocole et les erreurs d’E/S y mettent fin.

Référence des messages

Les champs sont listés dans l’ordre du format wire. La colonne Type utilise :
  • VarUInt — entier non signé à longueur variable (voir VarUInt).
  • String — octets préfixés par un VarUInt (voir String).
  • UInt8, Int32, etc. — entiers little-endian à largeur fixe.
  • Bool — un seul octet, 0x00 ou 0x01.
La colonne Role indique qui utilise chaque champ :
  • client — défini par les clients externes.
  • inter-serveur — n’a de sens que pour la communication de serveur à serveur ; les clients externes écrivent une valeur par défaut.
  • universal — utilisé par les deux.
Ces tableaux documentent uniquement le corps de chaque paquet, après le code de type de paquet.

ClientHello (type de paquet 0)

Client → Server. Le premier message après l’établissement de la connexion TCP.
#ChampTypeRôleDescription
1client_nameStringuniverselIdentifiant du client (p. ex., "clickhouse-client")
2version_majorVarUIntuniverselVersion majeure du client
3version_minorVarUIntuniverselVersion mineure du client
4protocol_versionVarUIntuniverselVersion maximale du protocole prise en charge par le client
5databaseStringuniverselNom de la base de données par défaut
6userStringuniverselNom d’utilisateur pour l’authentification
7passwordStringuniverselMot de passe (en texte brut)

ServerHello (type de paquet 0)

Serveur → Client. Réponse à ClientHello lorsque l’authentification réussit.
#ChampTypeRôleConditionDescription
1server_nameStringuniverseltoujoursIdentifiant du serveur
2version_majorVarUIntuniverseltoujoursVersion majeure du serveur
3version_minorVarUIntuniverseltoujoursVersion mineure du serveur
4protocol_versionVarUIntuniverseltoujoursVersion du protocole du serveur
4aparallel_replicas_protocol_versionVarUIntuniverselVERSIONED_PARALLEL_REPLICAS_PROTOCOL (v54471)Version du protocole de coordination des répliques parallèles du serveur. Position sur le wire : immédiatement après protocol_version, avant timezone. Valeur actuelle : 7.
5timezoneStringuniverselTIMEZONE (v54058)Fuseau horaire du serveur (par ex. : "UTC")
6display_nameStringuniverselDISPLAY_NAME (v54372)Nom du serveur lisible par l’humain
7version_patchVarUIntuniverselVERSION_PATCH (v54401)Version corrective du serveur
8proto_send_chunked_srvStringuniverselCHUNKED_PROTOCOL (v54470)Mode de fragmentation sortante préféré du serveur. L’une des valeurs suivantes : "chunked", "notchunked", "chunked_optional", "notchunked_optional". Voir tramage par fragments. Se trouve AVANT password_complexity_rules sur le wire même si sa version d’activation est plus élevée.
9proto_recv_chunked_srvStringuniverselCHUNKED_PROTOCOL (v54470)Mode de fragmentation entrante préféré du serveur. Même ensemble de valeurs que le champ 8.
10password_complexity_rulesRule[]universelPASSWORD_COMPLEXITY_RULES (v54461)Politique de mot de passe du serveur. VarUInt count suivi de count × Rule. Voir ci-dessous.
11nonceUInt64inter-serveurINTERSERVER_SECRET_V2 (v54462)Nonce aléatoire LE de 8 octets. Le schéma de signature des query inter-serveurs du serveur l’utilise. Les clients externes DOIVENT le décoder (pour conserver l’alignement du flux) et DEVRAIENT ignorer sa valeur.
12server_settingsSetting[]universelSERVER_SETTINGS (v54474)Diffusion des settings non par défaut du serveur. Format : zéro, un ou plusieurs triplets (String key, VarUInt flags, String value), terminés par une clé vide. Identique à la liste des settings du paquet Query.
13query_plan_serialization_versionVarUIntuniverselQUERY_PLAN_SERIALIZATION (v54477)Version de sérialisation du plan de query prise en charge par le serveur. Les clients externes la décodent puis l’ignorent.
14cluster_function_protocol_versionVarUIntuniverselVERSIONED_CLUSTER_FUNCTION_PROTOCOL (v54479)Version du protocole de fonction de table *Cluster du serveur. Les clients externes la décodent puis l’ignorent.
Rule — élément de password_complexity_rules :
#ChampTypeDescription
1patternStringExpression régulière à laquelle un mot de passe conforme doit correspondre.
2messageStringExplication lisible par l’humain affichée lorsqu’un mot de passe ne respecte pas cette règle.
Cette liste reflète la configuration de politique de mot de passe définie par l’opérateur du serveur et reste purement indicative : le serveur n’applique pas ces règles pendant le handshake. Un client qui propose une fonctionnalité de changement ou de définition de mot de passe peut s’en servir pour signaler les erreurs avant d’envoyer au serveur un mot de passe non conforme.
Pour borner l’utilisation des ressources face à un serveur hostile ou mal configuré, plafonnez la valeur décodée de count à 256 entrées et limitez chaque String pattern et message à 4096 octets. Une valeur count de 0 (aucune paire ensuite) est le cas le plus courant sur les serveurs sans politique de mot de passe configurée.

Addendum (sans type de paquet)

Client → Server, activé par ADDENDUM (v54458). Envoyé immédiatement après la fin du handshake. Il ne s’agit pas d’un type de paquet distinct — les champs sont transmis bruts sur le wire, sans octet de préfixe de type de paquet.
#ChampTypeRôleConditionDescription
1quota_keyStringuniverseltoujoursClé de quota de ressource pour les quotas à clé côté serveur. Les clients qui n’utilisent pas de quota à clé envoient une chaîne vide.
2proto_send_chunkedStringuniverselCHUNKED_PROTOCOL (v54470)Fragmentation sortante négociée du client : "chunked" ou "notchunked". Calculée à partir de proto_recv_chunked_srv de ServerHello.
3proto_recv_chunkedStringuniverselCHUNKED_PROTOCOL (v54470)Fragmentation entrante négociée du client. Calculée à partir de proto_send_chunked_srv.
4parallel_replicas_protocol_versionVarUIntuniverselVERSIONED_PARALLEL_REPLICAS_PROTOCOL (v54471)Version du protocole de coordination des répliques parallèles prise en charge par le client. Les clients externes ne participant pas aux requêtes distribuées DEVRAIENT tout de même envoyer une version valide (actuellement 7) afin que la vérification de compatibilité du serveur réussisse.
Le basculement vers le tramage par fragments s’applique après l’envoi de cet Addendum — l’Addendum lui-même n’est pas tramé.

Ping (type de paquet 4)

Client → Serveur. Pas de corps — le paquet se compose d’un seul octet 0x04 avant le tramage par fragments ; lorsque le découpage en fragments est négocié, l’octet devient la charge utile d’un fragment d’un octet (voir tramage par fragments).

Pong (type de paquet 4)

Serveur → Client. Sans corps — le paquet se compose d’un unique octet 0x04 avant le tramage par fragments ; lorsque le découpage en blocs est négocié, cet octet devient la charge utile d’un octet d’un bloc (voir le tramage par fragments).

Exception (type de paquet 2)

Serveur → client. Envoyé lorsque le serveur rencontre une erreur à n’importe quelle étape.
#ChampTypeRôleDescription
1codeInt32universelCode d’erreur
2nameStringuniverselClasse d’exception (par ex. "DB::Exception")
3messageStringuniverselMessage d’erreur lisible par un humain
4stack_traceStringuniverselStack trace côté serveur
5has_nested (obsolète)BooluniverselOctet de compatibilité obsolète. Toujours écrit sous la forme false par le serveur

Query (type de paquet 1)

Client → serveur.
#ChampTypeRôleConditionDescription
1query_idStringuniverseltoujoursIdentifiant unique de la requête (UUID)
2client_infoClientInfouniverselCLIENT_INFO (v54032)Voir ClientInfo
3settingsSetting[]universeltoujoursVoir Setting. Toujours présent (terminé par une clé vide) ; seul l’encodage propre à chaque setting dépend de la version — voir la note sur l’encodage dans Setting. Un client ne doit pas omettre ce champ pour les versions négociées inférieures à 54429.
3aexternal_rolesStringuniverselINTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472)Liste sérialisée des noms de rôles accordés par une source externe. Liste vide = octet 0x00 (VarUInt 0) encapsulé dans une String ([VarUInt 1][0x00] sur le wire). Les clients externes envoient toujours une liste vide.
4auth_hashStringinter-serveurINTERSERVER_SECRET (v54441)Hash d’authentification inter-serveur — pas la valeur brute du secret de cluster. Voir Inter-server authentication ci-dessous. Les clients externes (et toute InitialQuery) envoient une chaîne vide.
5stageVarUIntuniverseltoujoursÉtape du traitement de la requête. 0 = FetchColumns, 1 = WithMergeableState, 2 = Complete, 3 = WithMergeableStateAfterAggregation, 4 = WithMergeableStateAfterAggregationAndLimit, 7 = QueryPlan. Les valeurs 3/4 apparaissent dans les requêtes distribuées ; 7 accompagne un plan de requête sérialisé. Les clients externes envoient normalement 2.
6compressionVarUIntuniverseltoujours0 = désactivé, 1 = activé
7query_bodyStringuniverseltoujoursTexte SQL
8parametersParameter[]clientPARAMETERS (v54459)Voir Parameter. Terminé par une clé vide.

ClientInfo (intégré à Query)

Client → Server, intégré au corps de Query (champ 2). Conditionné par CLIENT_INFO (v54032). (Certains champs de ClientInfo sont conditionnés par des versions ultérieures, comme indiqué champ par champ ci-dessous.)
#ChampTypeRôleConditionDescription
1query_kindUInt8universeltoujours0 = NoQuery, 1 = InitialQuery, 2 = SecondaryQuery. Les clients externes envoient 1.
2initial_userStringuniverseltoujoursUtilisateur ayant initié la requête
3initial_query_idStringuniverseltoujoursID de la requête d’origine
4initial_addressStringuniverseltoujoursAdresse du socket du client à l’origine, au format host:port
5initial_timeInt64clientINITIAL_QUERY_START_TIME (v54449)Heure de début de la requête (en microsecondes). Encodage sur 8 octets à largeur fixe, et non en VarUInt
6query_interfaceUInt8universeltoujours1 = TCP, 2 = HTTP
7os_userStringclientsi interface = TCPNom d’utilisateur de l’OS
8client_hostnameStringclientsi interface = TCPNom d’hôte de la machine cliente
9client_nameStringclientsi interface = TCPNom de l’application cliente
10version_majorVarUIntuniverselsi interface = TCPVersion majeure du client
11version_minorVarUIntuniverselsi interface = TCPVersion mineure du client
12protocol_versionVarUIntuniverselsi interface = TCPVersion propre du protocole TCP du client d’origine (DBMS_TCP_PROTOCOL_VERSION), et non la version négociée. La révision du pair détermine uniquement quels champs sont présents ; cette valeur correspond à la version intégrée à la compilation de l’initiateur. Ainsi, lorsqu’un client plus récent communique avec un serveur plus ancien, elle peut être supérieure à la révision négociée ou à celle du serveur.
13quota_keyStringuniverselQUOTA_KEY_IN_CLIENT_INFO (v54060)Clé de quota de ressources pour les quotas à clé côté serveur. Les clients qui n’utilisent pas de quota à clé envoient une chaîne vide.
14distributed_depthVarUIntinter-serverDISTRIBUTED_DEPTH (v54448)Profondeur d’imbrication de la requête distribuée. Les clients externes envoient 0.
15version_patchVarUIntuniverselVERSION_PATCH (v54401), TCP uniquementVersion de correctif du client
16open_telemetry(ci-dessous)clientOPEN_TELEMETRY (v54442)Contexte de trace. Les clients sans tracing envoient 0.
17collaborate_with_initiatorVarUIntinter-serverPARALLEL_REPLICAS (v54453)Bool encodé en VarUInt. Les clients externes envoient 0.
18count_participating_replicasVarUIntinter-serverPARALLEL_REPLICAS (v54453)Les clients externes envoient 0.
19number_of_current_replicaVarUIntinter-serverPARALLEL_REPLICAS (v54453)Les clients externes envoient 0.
20script_query_numberVarUIntclientQUERY_AND_LINE_NUMBERS (v54475)Position de l’instruction dans un script multi-instruction, indexée à partir de 1. Les clients externes envoient 0.
21script_line_numberVarUIntclientQUERY_AND_LINE_NUMBERS (v54475)Numéro de ligne dans le script source, indexé à partir de 1. Les clients externes envoient 0.
22jwt_presentUInt8inter-serverJWT_IN_INTERSERVER (v54476)0 = aucun JWT ; 1 = un JWT suit. Les clients externes sans authentification JWT envoient 0.
23jwtStringinter-serverJWT_IN_INTERSERVER (v54476), si jwt_present=1Bearer token JWT, présent uniquement lorsque le champ 22 = 1.
24client_agentStringclientCLIENT_AGENT_IN_CLIENT_INFO (v54485)Champ final. Identifiant de l’outil/agent client, détecté automatiquement à partir de l’environnement (par ex. claude-code, cursor, gemini-cli ou la variable d’environnement AGENT). Les clients externes sans agent détecté envoient une chaîne vide. Présent sur le chemin Query normal dès que la version négociée est ≥ 54485 (envoyé sur toutes les interfaces, pas uniquement TCP).
Disposition dépendante de l’interface (champs 7–12)Les champs 7 à 12 ci-dessus correspondent à la branche TCP. Lorsque query_interface (champ 6) n’est pas TCP, ces champs sont remplacés par une autre disposition sur le wire — il ne s’agit pas simplement de champs omis de façon facultative ; un décodeur doit donc bifurquer selon la valeur du champ 6.
  • query_interface = 2 (HTTP) : les informations de requête HTTP relayée par le server sont écrites à la place — http_method (UInt8), http_user_agent (String), puis forwarded_for (String, conditionné par X_FORWARDED_FOR_IN_CLIENT_INFO v54443) et http_referer (String, conditionné par REFERER_IN_CLIENT_INFO v54447). Les champs os_user/client_hostname/client_name/version_*/protocol_version ne sont pas présents.
  • Toute autre interface : aucun des champs TCP (7–12) ni aucun des champs HTTP n’est écrit ; le stream continue directement avec quota_key.
Après cette bifurcation, la disposition redevient commune : quota_key (champ 13) et distributed_depth (champ 14) suivent pour toutes les interfaces, puis version_patch (champ 15) n’est écrit que pour TCP.Cette bifurcation concerne surtout le trafic inter-serveurs, où le server initiateur relaie une query arrivée initialement via HTTP. Un décodeur qui lit systématiquement les champs TCP interprétera mal ces paquets — en traitant http_method ou http_user_agent comme quota_key.
Encodage OpenTelemetry (champ 16) :
[UInt8: has_trace]              0 = no trace data follows, 1 = trace data follows
If has_trace == 1:
  [16 bytes: trace_id]          byte-swapped per-8-bytes
  [8 bytes:  span_id]           byte-swapped
  [String:   trace_state]       W3C trace state
  [UInt8:    trace_flags]       W3C trace flags

Authentification inter-serveur

Le champ 4 de Query (auth_hash) n’est pas le secret partagé du cluster transmis sur le réseau. Envoyer le secret brut ferait à la fois échouer l’authentification et le divulguerait. À la place, un serveur agissant comme client inter-serveur prouve qu’il connaît le secret à l’aide d’un hachage SHA-256 salé :
  1. Entrer en mode inter-serveur. Le serveur qui se connecte l’indique dans ClientHello : le champ user contient le marqueur inter-serveur et password est vide. Il ajoute ensuite deux chaînes supplémentaires — le nom du cluster et un salt de 32 octets nouvellement généré (encodeSHA256 d’une valeur aléatoire) — immédiatement après les champs user/password, dans le même paquet ClientHello. Le serveur lit ces deux chaînes avant d’envoyer ServerHello, donc un client doit les écrire d’emblée ; attendre ServerHello d’abord provoque un interblocage, car le serveur reste bloqué à les lire.
  2. Obtenir le nonce. ServerHello contient un nonce UInt64 de 8 octets lorsque INTERSERVER_SECRET_V2 (v54462) est négocié.
  3. Calculer le hachage. Pour chaque paquet Query autre que InitialQuery, le client écrit encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles) dans le champ 4 — un condensat de 32 octets. (nonce est sous forme de chaîne décimale et n’est présent que si une version ≥ v54462 a été négociée ; external_roles n’est ajouté que lorsque INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472) est négocié.) Pour un InitialQuery, ou lorsqu’aucun secret de cluster n’est configuré, le client écrit à la place une chaîne vide.
  4. Vérifier. Le serveur lit le champ 4 avec une limite de 32 octets et recalcule la même concaténation à l’aide de sa propre copie du secret du cluster ; la connexion est rejetée si les condensats diffèrent.
Les clients externes (non inter-serveur) n’entrent jamais dans ce mode et envoient toujours un auth_hash vide.

Paramètre

Encodé directement dans la liste des paramètres du corps de Query (le paquet Query, champ 3). La liste est toujours présente, quelle que soit la version négociée, et se termine par un Setting avec une clé vide — un simple VarUInt 0, sans indicateurs ni valeur après. Seul l’encodage de chaque paramètre dépend de la version négociée, selon SETTINGS_SERIALIZED_AS_STRINGS (v54429). v54429+ (STRINGS_WITH_FLAGS) — chaque paramètre est le triplet présenté ici :
#ChampTypeRôleDescription
1keyStringuniverselNom du paramètre. Vide = fin de la liste.
2flagsVarUIntuniverselIndicateurs de bits de métadonnées ; voir ci-dessous.
3valueStringuniverselValeur du paramètre sous forme de chaîne
Les champs 2 et 3 sont absents lorsque key est vide. Avant 54429 (BINARY) — chaque paramètre est [String key][type-specific binary value] : le champ flags n’est pas écrit, et la valeur est encodée dans la forme binaire native du paramètre (par exemple, un entier de largeur fixe ou une chaîne préfixée par sa longueur) plutôt que comme chaîne décimale/texte. La liste se termine toujours par un key vide. Un client ciblant une version négociée inférieure à 54429 doit lire et écrire cette forme binaire, et non le triplet ci-dessus. (Les paramètres personnalisés définis par l’utilisateur font exception : ils comportent toujours flags et une valeur de chaîne, dans les deux encodages.) Le champ flags regroupe :
  • 0x01Important : le paramètre affecte les résultats de la requête et ne doit pas être ignoré silencieusement par des pairs plus anciens.
  • 0x02Custom : un paramètre personnalisé défini par l’utilisateur.
  • 0x0c — un champ de tier sur 2 bits, et non un indicateur indépendant : 0x00 = Production, 0x04 = Obsolete, 0x08 = Experimental, 0x0c = Beta. Lisez bien les 2 bits (flags & 0x0c) — un test naïf flags & 0x04 classerait à tort Beta (0x0c) comme Obsolete.
  • 0x80HotReload (rechargement de la config sans redémarrage ; défini dans l’enum des indicateurs, rencontré principalement pour les paramètres de coordination).

Paramètre

Paramètres de requête, pour les requêtes paramétrées telles que SELECT {x:UInt64}. Ils sont encodés de façon identique à un Setting avec l’indicateur Custom (0x02) activé, et se terminent de la même manière par une clé vide.
#ChampTypeRôleDescription
1keyStringclientNom du paramètre. Vide = fin de la liste.
2flagsVarUIntclientToujours 0x02 (Custom)
3valueStringclientValeur du paramètre sous forme de chaîne. Voir la note ci-dessous concernant les guillemets.
La valeur du paramètre est la représentation SQL de la valeur, et non un littéral brut. Les paramètres de type chaîne doivent être transmis déjà entre guillemets simples (par exemple, la valeur de {name:String} est 'Alice', et non Alice) ; sinon, l’analyseur de valeurs du serveur les rejette.

Data (type de paquet 1 serveur→client, type de paquet 2 client→serveur)

Dans les deux sens. Transporte des blocs de résultats, des données INSERT, des tables externes et des marqueurs de fin des données. Le format binaire transmis est symétrique — dans les deux sens, un préfixe table_name est inclus avant le Block. Seul l’octet du type de paquet diffère.
[VarUInt: packet_type]     1 (server→client) or 2 (client→server)
[String:  table_name]      External table name; empty in most cases
[Block]                    See the Native Format spec for the Block layout
ChampTypeRôleDescription
table_nameStringuniverselNom de la table externe. La valeur vide ("") est le cas le plus courant — pour la table principale, le résultat de la requête et le flux de lignes INSERT. Un table_name vide à lui seul n’est pas le marqueur de fin des données (les paquets de lignes INSERT normaux contiennent eux aussi "").
Corps du blocVoir Structure des blocs et des colonnes.
Le marqueur de fin des données est un paquet dont le Block est vide — 0 colonnes et 0 lignes — quelle que soit la valeur de table_name. Le serveur traite un paquet client Data comme terminateur uniquement lorsque le bloc décodé est vide (block.empty()) ; un paquet avec table_name = "" et un bloc non vide est un paquet de lignes ordinaire, pas un terminateur. Ainsi, un flux de lignes INSERT est une séquence de blocs Data non vides suivie d’un bloc Data vide qui y met fin. Les variantes de bloc et leur signification sont décrites dans Variantes de bloc.

Progress (type de paquet 3)

Serveur → Client. Envoyé périodiquement pendant l’exécution d’une requête. Tous les champs sont des VarUInt, et chaque paquet contient les incréments depuis le paquet Progress précédent, et non des totaux cumulés. Avant l’envoi, le serveur lit ses compteurs et les réinitialise atomiquement à zéro, puis calcule elapsed_ns comme le temps écoulé depuis le dernier envoi. Un client doit donc accumuler localement les paquets successifs pour obtenir des totaux cumulés — traiter un paquet comme une valeur absolue fait reculer l’affichage de progression ou provoque un sous-comptage dès que plusieurs paquets arrivent.
#ChampTypeRôleConditionDescription
1rowsVarUIntuniverseltoujoursLignes lues depuis le paquet précédent (à ajouter au total cumulé)
2bytesVarUIntuniverseltoujoursOctets lus depuis le paquet précédent (à ajouter au total cumulé)
3total_rowsVarUIntuniverseltoujoursIncrément du nombre total estimé de lignes à lire ; à cumuler (peut valoir 0 dans un paquet donné)
4total_bytesVarUIntuniverselTOTAL_BYTES_IN_PROGRESS (v54463)Incrément du nombre total estimé d’octets à lire ; à cumuler. Se situe ENTRE total_rows et wrote_rows dans l’encodage binaire.
5wrote_rowsVarUIntuniverselWRITE_CLIENT_INFO (v54420)Lignes écrites depuis le paquet précédent (pour INSERT) ; à cumuler
6wrote_bytesVarUIntuniverselWRITE_CLIENT_INFO (v54420)Octets écrits depuis le paquet précédent (pour INSERT) ; à cumuler
7elapsed_nsVarUIntuniverselSERVER_QUERY_TIME_IN_PROGRESS (v54460)Nanosecondes écoulées depuis le paquet précédent (un delta, pas le temps total de la requête) ; à cumuler

ProfileInfo (type de paquet 6)

Serveur → Client. Envoyé une fois par requête, vers la fin de l’exécution.
#ChampTypeRôleConditionDescription
1rowsVarUIntuniverseltoujoursNombre total de lignes traitées
2blocksVarUIntuniverseltoujoursNombre total de blocs traités
3bytesVarUIntuniverseltoujoursNombre total d’octets traités
4applied_limitBooluniverseltoujoursIndique si une clause LIMIT a été appliquée
5rows_before_limitVarUIntuniverseltoujoursNombre de lignes avant LIMIT
6obsoleteBooluniverseltoujoursOctet de compatibilité obsolète. Le serveur écrit toujours true ici et le client l’ignore à la lecture ; ce n’est pas un indicateur signifiant que « rows_before_limit a été calculé ». L’état significatif de la limite correspond au champ 4 (applied_limit) conjointement au champ 5. À lire puis à ignorer.
7applied_aggregationBooluniverselROWS_BEFORE_AGGREGATION (v54469)Indique si GROUP BY a été appliqué
8rows_before_aggregationVarUIntuniverselROWS_BEFORE_AGGREGATION (v54469)Nombre de lignes avant agrégation

Totaux (type de paquet 7)

Serveur → client. Envoyé pour les requêtes avec WITH TOTALS. Le format binaire est identique à Data : une chaîne table_name (toujours vide), suivie d’un bloc. Seul l’octet indiquant le type de paquet diffère.
[VarUInt: 7]                packet type
[String:  table_name]       always empty
[Block]                     see the Native Format spec

Extremes (type de paquet 8)

Serveur → Client. Envoyé lorsque le paramètre extremes est activé. Le format binaire de transmission est identique à Data. Le bloc contient exactement 2 lignes : la ligne 0 contient le minimum de chaque colonne, la ligne 1 contient le maximum.
[VarUInt: 8]                packet type
[String:  table_name]       always empty
[Block]                     num_rows = 2

Log (type de paquet 10)

Serveur → Client. Envoyé lorsque la requête dispose d’une file d’attente de logs active (paramètre send_logs_level ; voir la transmission des logs en continu). Même format d’enveloppe et de corps que Data. Le bloc a un num_columns = 8 fixe et un schéma prédéfini. Chaque ligne de log constitue une ligne sur les 8 colonnes, et un paquet Log peut contenir de nombreuses lignes.
[VarUInt: 10]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 8, num_rows = number of log lines
Les 8 colonnes, dans cet ordre exact :
#NomTypeDescription
1event_timeDateTimeHorodatage de l’événement (secondes depuis l’époque Unix)
2event_time_microsecondsUInt32Composante en microsecondes
3host_nameStringNom d’hôte du serveur qui émet le log
4query_idStringID de la requête à laquelle le log appartient
5thread_idUInt64ID du thread du système d’exploitation
6priorityInt8Niveau de log (priorité Poco : 1 = Fatal, … 8 = Trace)
7sourceStringNom du logger
8textStringTexte du message de log

ProfileEvents (type de paquet 14)

Serveur → Client. Transporte les compteurs de performances de chaque requête. Même format d’enveloppe et de corps que Data. Le bloc a un num_columns = 6 fixe et un schéma prédéfini. Chaque événement constitue une ligne.
[VarUInt: 14]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 6, num_rows = number of events
Les 6 colonnes :
#NomTypeDescription
1host_nameStringNom d’hôte du serveur
2current_timeDateTimeHorodatage de l’événement
3thread_idUInt64ID du thread
4typeEnum8Type d’événement : 1 = incrément (compteur), 2 = jauge. Le stockage sous-jacent utilise un octet signé.
5nameStringNom de l’événement (p. ex., "Query", "NetworkReceiveBytes")
6valueInt64Valeur du compteur ou mesure de la jauge
Le type d’élément de la colonne value n’est pas fixe d’un paquet à l’autre — les anciens serveurs émettent UInt64, les plus récents Int64. Lisez la chaîne de type de la colonne dans l’en-tête du bloc plutôt que de supposer une taille donnée.

TableColumns (type de paquet 11)

Serveur → Client, conditionné par COLUMN_DEFAULTS_METADATA (v54410). Le serveur l’envoie avant le bloc de schéma INSERT pour transmettre les métadonnées des valeurs par défaut des colonnes, mais uniquement lorsque la version négociée est ≥ 54410 et que le paramètre input_format_defaults_for_omitted_fields est activé. En dessous de 54410, le paquet n’est jamais envoyé ; un ancien client ne doit donc pas l’attendre — le bloc de schéma Data arrive directement. Un client v54410+ doit être prêt à l’un ou l’autre ordre : un TableColumns facultatif, puis le bloc de schéma.
#ChampTypeRôleDescription
1external_tableStringuniversalNom de la table externe. Vide = table principale.
2columns_descriptionStringuniversalDéfinitions textuelles des colonnes, par ex. "id Int32, name String DEFAULT ''". Texte libre — à analyser comme une chaîne.
Corps compressé à partir de v54481+À partir d’une version négociée ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), le serveur écrit les deux champs via le même flux de sortie avec compression éventuelle ; lorsque la requête a compression = true, l’intégralité du corps TableColumns (external_table + columns_description) se trouve dans la trame de compression, et le client la lit via le flux décompressé correspondant. Lorsque la requête n’utilise pas de compression, le corps est transmis sans compression, exactement comme indiqué dans le tableau ci-dessus. C’est important pour les réponses de schéma INSERT : un client qui gère la compression pour Log et ProfileEvents, mais pas pour TableColumns, lira mal la réponse lorsque la compression de la requête est activée.

TimezoneUpdate (type de paquet 17)

Serveur → Client, conditionné par TIMEZONE_UPDATES (v54464). Envoyé à un seul endroit : l’initialiseur de la table function input (une query de la forme INSERT INTO <table> SELECT ... FROM input('<structure>'), qui transmet des lignes depuis le client). Juste après que le serveur a envoyé le bloc Data du schéma d’entrée (voir la phase INSERT), il émet TimezoneUpdate avec la valeur courante de session_timezone du contexte de la query, afin que le client interprète les lignes qu’il s’apprête à envoyer avec le même fuseau horaire. Le serveur n’émet pas ce paquet pour des changements arbitraires de SET session_timezone au milieu d’une query, ni pour indiquer au client comment formater les blocs de résultats envoyés ensuite.
#ChampTypeRôleDescription
1timezoneStringuniverselLe nouveau fuseau horaire par défaut de la session (par ex. "UTC", "Europe/Berlin").
Le paquet arrive une seule fois, immédiatement après le bloc du schéma d’entrée et avant que le client ne commence à envoyer des blocs de lignes. Un décodeur qui ignore TimezoneUpdate DOIT quand même consommer le String final pour conserver l’alignement sur le wire.

Authentification SSH par défi-réponse (types de paquets 11, 12 et 18)

Soumise à SSH_AUTHENTICATION (v54466) et disponible uniquement sur activation explicite. Une connexion entre dans le flux SSH lorsque ClientHello envoie user = " SSH KEY AUTHENTICATION " + <real_user> (avec les espaces au début et à la fin) et password = "". Le serveur lit le préfixe, le supprime pour retrouver l’utilisateur réel, puis bascule vers le mécanisme de défi-réponse.
PacketCodeDirectionBody
SSHChallengeRequest11Client → Server(aucun corps)
SSHChallenge18Server → ClientString challenge — octets aléatoires ; l’un des composants de la chaîne signée (voir ci-dessous)
SSHChallengeResponse12Client → ServerString signature — signature SSH sur la concaténation définie ci-dessous, et non sur le défi brut
Ce flux remplace l’authentification par mot de passe, et l’échange de défi-réponse a lieu avant ServerHello — le serveur diffère sa réponse Hello jusqu’à ce que l’authentification réussisse :
  1. Le client envoie ClientHello avec le préfixe marqueur SSH et un mot de passe vide.
  2. Le client envoie SSHChallengeRequest (paquet 11). Le serveur n’a pas encore envoyé ServerHello — il traite d’abord l’authentification et reste bloqué ici en attendant ce paquet.
  3. Le serveur répond avec SSHChallenge, contenant des octets aléatoires (paquet 18).
  4. Le client construit la chaîne à signer et signe celle-ci, et non le défi brut, puis envoie SSHChallengeResponse (paquet 12) avec la signature. Le message signé est la concaténation, octet par octet et sans séparateurs, de quatre parties dans cet ordre exact :
    to_sign = decimal(protocol_version) + default_database + user + challenge
    
    PartSource
    decimal(protocol_version)La version du protocole du client sous forme de chaîne ASCII décimale (par ex. "54466") — le numéro de version sous forme de chaîne, et non d’un VarUInt ou d’un entier à largeur fixe. Le serveur valide à l’aide de la même version du protocole que celle reçue dans ClientHello.
    default_databaseLe champ database de ClientHello (chaîne vide s’il n’y en a pas).
    userLe nom de l’utilisateur réel une fois le préfixe marqueur " SSH KEY AUTHENTICATION " supprimé — le même nom que le serveur retrouve après suppression du préfixe.
    challengeLes octets bruts de challenge provenant du paquet SSHChallenge.
  5. Le serveur vérifie la signature à l’aide de la clé publique enregistrée de l’utilisateur, en reconstruisant la même chaîne decimal(protocol_version) + default_database + user + challenge. En cas de succès, il envoie ServerHello — la même réponse que dans le flux par mot de passe — et la négociation initiale se poursuit normalement (Addendum, etc.) ; en cas d’échec, il renvoie une Exception et met fin à la connexion. Un client qui signe uniquement les octets bruts du défi échouera à l’authentification.
C’est l’inverse de la négociation initiale par mot de passe, où ServerHello suit immédiatement ClientHello. Avec l’authentification SSH, ServerHello n’est envoyé qu’après vérification de la signature, de sorte que le challenge-réponse SSH s’intercale dans la négociation initiale avant tout ServerHello.
Les clients externes qui n’utilisent pas l’authentification SSH ne voient jamais les paquets 11, 12 ou 18 — ils ne transitent pas sur le réseau, sauf si l’utilisateur choisit explicitement de les activer via le préfixe du nom d’utilisateur.

Référence des types de paquets

Client → serveur

CodeNomFormat du corpsDescription
0HelloClientHelloInitialisation de la négociation
1QueryQueryDemande d’exécution de requête
2DataDataBloc de données (données INSERT, tables externes, marqueur de fin des données)
3Cancel(aucun corps)Annuler la requête en cours
4PingPingVérification de disponibilité
5TablesStatusRequestnon spécifiéVérification de l’état des tables
6KeepAlivenon spécifiéMaintien de la connexion active
7Scalarnon spécifiéBloc de données scalaire
8IgnoredPartUUIDsnon spécifiéParts à exclure de la requête
9ReadTaskResponsenon spécifiéRéponse de lecture du cluster S3
10MergeTreeReadTaskResponsenon spécifiéRéponse de tâche de lecture parallèle
11SSHChallengeRequestAuthentification SSHDemande de challenge d’authentification SSH
12SSHChallengeResponseAuthentification SSHRéponse au challenge d’authentification SSH
13QueryPlannon spécifiéPlan de requête

Serveur → Client

CodeNomFormat du corpsDescription
0HelloServerHelloRéponse de négociation initiale
1DataDataBloc de données de résultat
2ExceptionExceptionErreur
3ProgressProgressProgression de l’exécution de la requête
4PongPongRéponse de vérification de disponibilité
5EndOfStream(aucun corps)Exécution de la requête terminée
6ProfileInfoProfileInfoDonnées de profilage post-exécution
7TotalsTotalsLigne GROUP BY WITH TOTALS
8ExtremesExtremesValeurs min/max (bloc de 2 lignes)
9TablesStatusResponsenon spécifiéRéponse d’état des tables
10LogLogLignes de journal d’exécution de la requête
11TableColumnsTableColumnsDescriptions de colonnes pour les valeurs par défaut
12PartUUIDsnon spécifiéID uniques de parts
13ReadTaskRequestnon spécifiéDemande de tâche de lecture du cluster
14ProfileEventsProfileEventsCompteurs de performances
15MergeTreeAllRangesAnnouncementnon spécifiéInitialisation de la lecture parallèle
16MergeTreeReadTaskRequestnon spécifiéAttribution de tâche de lecture parallèle
17TimezoneUpdateTimezoneUpdateMise à jour du fuseau horaire du serveur
18SSHChallengeSSH authChallenge d’authentification SSH

Configuration

Cette section présente les paramètres réglables qui influent sur les connexions du protocole natif : Les valeurs par défaut ci-dessous reflètent une version récente du serveur ; elles peuvent varier selon les versions et les déploiements.

Paramètres de la couche transport

Options de socket

OptionPar défautCôtéDescription
TCP_NODELAYactivéles deuxAlgorithme de Nagle désactivé. Les petits paquets sont envoyés immédiatement.
SO_KEEPALIVEactivé (client), valeur par défaut de l’OS (serveur)asymétriqueSondes TCP keepalive au niveau du noyau. Le client l’active explicitement lorsque tcp_keep_alive_timeout > 0. Le serveur hérite de la valeur par défaut de l’OS.
SO_RCVBUF / SO_SNDBUFvaleurs par défaut de l’OSTaille des tampons de socket. Non ajustée par le protocole.

Délais d’attente

SettingDefaultUnitSideDescription
connect_timeout10secondesclientDélai d’attente pour établir la connexion TCP initiale.
handshake_timeout_ms10000millisecondesclientDélai d’attente pour recevoir ServerHello pendant la négociation initiale.
send_timeout300secondesles deuxSi aucun octet ne peut être écrit pendant cet intervalle, la connexion lève une exception.
receive_timeout300secondesles deuxSi aucun octet ne peut être lu pendant cet intervalle, la connexion lève une exception.
tcp_keep_alive_timeout290secondesclientDurée d’inactivité avant que l’OS n’envoie la première sonde TCP keepalive.
receive_data_timeout_ms2000millisecondesclientDélai d’attente pour recevoir le premier paquet Data d’une réplique.
connect_timeout_with_failover_ms1000millisecondesclientDélai d’attente de connexion par tentative lors du parcours des répliques.
connect_timeout_with_failover_secure_ms1000millisecondesclientDélai d’attente de connexion par tentative lors du parcours des répliques via TLS.
hedged_connection_timeout_ms50millisecondesclientDélai d’attente de connexion par tentative pour les requêtes hedgées.
poll_interval10secondesserveurGranularité de la boucle de vérification des connexions inactives et de l’arrêt du serveur.
Les délais d’attente s’imbriquent ainsi :
tcp_keep_alive_timeout (290s)
      < receive_timeout (300s)
      < idle_connection_timeout (3600s)
      < tcp_close_connection_after_queries_seconds (0 = unlimited by default)
Le keepalive du système d’exploitation se déclenche en premier et peut détecter de manière transparente les pairs défaillants au niveau du noyau. Le délai d’attente de réception de l’application constitue la ligne de défense suivante. Le délai d’inactivité est le dernier recours, qui nettoie les connexions restées inutilisées pendant longtemps.

Limites de connexion

ParamètreDéfautUnitéCôtéDescription
max_connections4096nombreserveurNombre maximal de connexions TCP concurrentes.
idle_connection_timeout3600secondesserveurDurée maximale pendant laquelle une connexion inactive peut rester ouverte.
tcp_close_connection_after_queries_num0 (illimité)nombreserveurNombre maximal de requêtes par connexion avant fermeture forcée.
tcp_close_connection_after_queries_seconds0 (illimité)secondesserveurDurée de vie totale maximale d’une connexion, quelle que soit l’activité.
Une connexion qui exécute régulièrement des requêtes peut rester active indéfiniment. Seules les connexions inactives sont fermées au bout d’une heure, et il n’existe pas de durée de vie maximale par défaut.

Paramètres de la couche applicative

Ces paramètres sont transmis avec chaque requête dans la liste des paramètres du paquet Query. Ils modifient ce que le serveur envoie sur la connexion, ou la façon dont ces données sont mises en trame.

Compression

ParamètrePar défautUnitéDescription
network_compression_method"LZ4"chaîneCodec de compression utilisé lorsque l’indicateur compression du paquet Query est activé. Valeurs : "LZ4", "LZ4HC", "ZSTD", "NONE".
network_zstd_compression_level11–15Niveau ZSTD lorsque network_compression_method == "ZSTD".
L’indicateur compression du paquet Query (champ 6) active ou désactive la compression ; ces paramètres sélectionnent le codec utilisé lorsqu’elle est activée.

Transmission des logs

ParamètrePar défautUnitéDescription
send_logs_level"fatal"stringNiveau de log minimum. Valeurs : "none", "fatal", "error", "warning", "information", "debug", "trace".
send_logs_source_regexp""stringFiltre Regex appliqué à la source du logger. Vide = toutes les sources sont acceptées.
Si send_logs_level est défini sur une valeur autre que "none", le serveur émet des paquets Log pendant l’exécution de la requête.

Rapport de progression

ParamètreValeur par défautUnitéDescription
interactive_delay100000microsecondesIntervalle minimal visé entre deux paquets Progress consécutifs.
Il s’agit d’un minimum visé, et non d’un maximum strict : le server peut envoyer des paquets Progress moins fréquemment si la query ne produit pas suffisamment de travail assez vite.

Enveloppe de résultat

ParamètrePar défautUnitéDescription
extremesfalseboolLorsque la valeur est true, le serveur envoie un paquet Extremes avec les valeurs min/max pour chaque colonne.
max_result_rows0 (illimité)nombreLimite du nombre de lignes transmises. Le comportement est contrôlé par result_overflow_mode.
max_result_bytes0 (illimité)octets non compressésLimite du volume d’octets non compressés. Le comportement est contrôlé par result_overflow_mode.
result_overflow_mode"throw"string"throw" met fin au flux avec Exception ; "break" envoie des résultats partiels suivis de EndOfStream.

INSERT asynchrone

SettingDefaultUnitDescription
async_inserttrueboolSi true, les données INSERT sont mises en file d’attente côté serveur, puis regroupées par lots.
wait_for_async_inserttrueboolSi true (avec async_insert activé), le serveur ne renvoie la réponse qu’une fois les données en file d’attente écrites sur le stockage.
wait_for_async_insert_timeout120secondesDurée maximale pendant laquelle le serveur attend un flush avant de renvoyer la réponse.

Traçage distribué

ParamètrePar défautUnitéDescription
opentelemetry_start_trace_probability0.0probabilité de 0 à 1Probabilité, côté serveur, d’associer le contexte OpenTelemetry à la télémétrie de réponse.

Paramètres hors du périmètre

Ces paramètres sont parfois pris à tort pour des paramètres de protocole, mais ils contrôlent l’exécution SQL, le stockage ou l’utilisation du CPU, et non le comportement sur le réseau. Une implémentation du protocole n’a pas besoin de les traiter de manière particulière.
  • max_threads — parallélisme lors de l’exécution de la requête.
  • max_memory_usage — limite de mémoire par requête.
  • max_block_size, preferred_block_size_bytes — dimensionnement interne des blocs pendant le query processing ; les blocs transmis sur le réseau en sont indépendants.
  • compile_expressions — compilation JIT ; CPU uniquement.
  • async_insert_max_data_size — buffer de file d’attente côté serveur.
  • Tous les paramètres input_format_* et output_format_* à l’exception de la famille input_format_native_* / output_format_native_* — les paramètres non-native sélectionnent ou ajustent d’autres formats (par exemple via HTTP) et ne modifient pas les blocs Data du protocole natif.
Les paramètres *_native_* font exception : ils modifient les octets à l’intérieur des blocs Data en native TCP ; une implémentation du protocole doit donc en tenir compte. output_format_native_encode_types_in_binary_format fait passer le champ type de la colonne d’une chaîne textuelle à un encodage binaire du type, output_format_native_write_json_as_string émet les colonnes JSON comme une String, et output_format_native_use_flattened_dynamic_and_json_serialization sélectionne le layout FLATTENED Dynamic/JSON. Comme ces paramètres affectent le body du bloc plutôt que le packet envelope, ils sont décrits dans la spécification Native Format — voir column wire layout et types versionnés.

Glossaire

Cancel — un paquet initié par le client (type 3) qui interrompt une requête en cours. Il n’est pas décrit en détail sur cette page. Marqueur de fin des données client — un paquet Data vide (0 colonnes, 0 lignes) que le client envoie pour fermer un flux d’entrée. Son emplacement diffère selon le type de requête :
  • Requête normale (SELECT, etc.) : envoyé après le paquet Query et les éventuels paquets Data de table externe pour signaler « plus de données externes ». Le serveur commence alors l’exécution.
  • INSERT : le client n’envoie pas de marqueur avant le schéma. Le serveur envoie d’abord le bloc de schéma, le client transmet ensuite ses blocs Data de lignes, puis envoie seulement après le paquet Data vide pour terminer le flux de lignes. Envoyer un marqueur vide avant le bloc de schéma serait interprété comme une fin immédiate des lignes et entraînerait la perte des données.
Feature — une modification du format sur le réseau introduite dans une version spécifique du protocole. Elle est active lorsque la version négociée est égale ou supérieure à la version de la fonctionnalité. Voir gestion des versions et feature gates. Inter-server — une étiquette de rôle pour un champ qui n’a de sens que dans les requêtes distribuées de serveur à serveur. Les clients externes écrivent une valeur par défaut (généralement une chaîne vide, 0 ou false). Version négociéemin(client_version, server_version), calculé pendant la négociation initiale. Détermine quelles fonctionnalités sont actives pendant toute la durée de vie de la connexion. Packet — un message sur le réseau : un code de type de paquet VarUInt suivi d’un body dont le format dépend du type. Voir packet envelope. Code de type de paquet — le VarUInt initial d’un paquet qui identifie son format. Les valeurs 0–18 sont actuellement attribuées. Voir la référence des types de paquet. Flux de réponse — la séquence de paquets émise par le serveur pendant une requête. Sa longueur est ouverte et il se termine par exactement un EndOfStream (succès) ou une Exception (échec). Voir la phase de requête. bloc de schéma — le header block (un Block avec des colonnes mais 0 ligne) que le serveur envoie pendant la phase INSERT pour annoncer les formes de colonnes attendues avant que le client n’envoie les données. Settings list — une séquence de tuples (key, flags, value) dans le body de Query, terminée par une key vide. Transporte une configuration de couche applicative propre à chaque requête. Voir Setting. Stage — un champ VarUInt du paquet Query (champ 5) qui contrôle jusqu’où le serveur exécute la requête. Les clients externes envoient généralement 2 (Complete) ; les requêtes distribuées et les query plans sérialisés utilisent des valeurs plus élevées. Voir le champ 5 de Query pour l’ensemble complet des valeurs sur le réseau. Terminator — un paquet qui termine un flux. La réponse à Query se termine par EndOfStream (succès) ou Exception (échec). Le flux d’entrée du client se termine par le marqueur Data vide.
Last modified on June 29, 2026