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

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 :
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

Enveloppe de paquet

Tous les messages transmis sur le réseau ont la même structure externe, dans les deux sens :
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 :
Format binaire par fragment :
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 : 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.

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 :
  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 :

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 :
À 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.

ServerHello (type de paquet 0)

Serveur → Client. Réponse à ClientHello lorsque l’authentification réussit. Rule — élément de password_complexity_rules : 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. 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.

Query (type de paquet 1)

Client → serveur.

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.)
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) :

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 : 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.
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.
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.

ProfileInfo (type de paquet 6)

Serveur → Client. Envoyé une fois par requête, vers la fin de l’exécution.

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.

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.

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.
Les 8 colonnes, dans cet ordre exact :

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.
Les 6 colonnes :
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.
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. 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. 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 :
  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

Serveur → Client

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

Délais d’attente

Les délais d’attente s’imbriquent ainsi :
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

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

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

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

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

INSERT asynchrone

Traçage distribué

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 July 2, 2026