Skip to main content
El protocolo nativo es el protocolo binario y orientado a conexión que emplean los client y los servidores de ClickHouse para comunicarse a través de TCP. Transporta consultas SQL, datos de resultados, cargas útiles de INSERT, telemetría de ejecución y señales de error. Es el protocolo en el que se basan el cliente de línea de comandos, el controlador de C++ y la mayoría de los controladores nativos de terceros. Esta página describe el protocolo en sí: la estructura de paquetes, la máquina de estados de la conexión, la negociación de versión y el cuerpo de todos los mensajes distintos de Block. Los bytes contenidos en los paquetes de la familia Data (el Block, sus columnas y las codificaciones de cada tipo) son una cuestión aparte, que se documenta en la especificación del formato Native.
Especificación complementariaEsta página forma parte de un par de documentos y se publica junto con la especificación complementaria del formato Native. Ambas especificaciones se reparten el trabajo de forma nítida: esta página se ocupa de la capa de paquetes y de transporte, mientras que la especificación del formato Native se ocupa de los bytes contenidos en los paquetes de la familia Data.
Hay algunas propiedades que se cumplen siempre. El protocolo es binario y posicional: no existen etiquetas de campo salvo dentro de BlockInfo, de modo que un solo byte fuera de lugar desincroniza todo lo que viene después. Es un protocolo con estado, y cada conexión TCP procesa una sola consulta a la vez: no hay multiplexación. Los enteros de ancho fijo se codifican en little-endian.

Descripción general

Cada mensaje en el wire comienza con un código de tipo de paquete VarUInt, seguido de un cuerpo cuya estructura depende de ese código y de la versión del protocolo negociada. Una conexión pasa por tres fases: un único handshake inicial, después cualquier número de intercambios Ping o Query y, por último, el cierre: El protocolo TCP nativo siempre transporta datos tabulares en formato Native, independientemente de la cláusula FORMAT que incluya la consulta SQL. La conversión a RowBinary, CSV, JSON, etc., es tarea del client, que la realiza después de decodificar los bloques Native. (La interfaz HTTP sigue una ruta de código distinta que sí respeta la cláusula FORMAT; HTTP queda fuera del alcance de este documento.)

Seguridad

Seguridad de transporte (TLS)

TLS opera en la capa de transporte, por debajo del protocolo. Cuando está habilitado, se cifra todo el flujo TCP y los mensajes del protocolo son idénticos byte a byte, se use TLS o no.

Autenticación

La autenticación tiene lugar durante el handshake, en el mensaje ClientHello. Los campos user y password se transmiten como cadenas en texto plano, por lo que es el cifrado a nivel de transporte (TLS) el que protege las credenciales en tránsito. El servidor interpreta un campo user vacío como su usuario de sesión predeterminado configurado, es decir, el valor del ajuste de servidor default_session_user (cuyo valor predeterminado es default), que puede sobrescribirse para cada listener en la sección protocols. Si el servidor tiene configurado un usuario de sesión predeterminado vacío, o si su versión es anterior a la 26.8, se rechaza el campo user vacío con una excepción. Tenga en cuenta que clickhouse-client nunca envía un nombre de usuario vacío, sino que lo sustituye por default en el lado del cliente. La autenticación SSH de tipo desafío-respuesta está disponible a partir de la versión de protocolo 54466; consulte Autenticación SSH de tipo desafío-respuesta.

Secreto entre servidores

Para la ejecución distribuida de consultas, los servidores se autentican entre sí demostrando que conocen un secreto compartido, sin llegar a transmitirlo por el wire. Cada Query incluye un auth_hash SHA-256 de 32 bytes en el campo 4 de Query, calculado a partir de una sal, un nonce, el secreto configurado y la consulta; el servidor receptor lo vuelve a calcular y lo compara. Esta funcionalidad depende de la feature INTERSERVER_SECRET (v54441). Los client externos siempre envían una cadena vacía en este campo. Consulte Autenticación entre servidores.

Control de versiones y control de funcionalidad

Negociación de versión

Durante el handshake, tanto el client como el servidor declaran la versión máxima del protocolo que admiten. La versión negociada es la menor de las dos:
A partir de ese momento, todos los mensajes utilizan la versión negociada para determinar qué campos se incluyen en el wire.

Control de funcionalidad

Cada funcionalidad se identifica por la versión del protocolo en la que se introdujo y está activa cuando la versión negociada es mayor o igual que ese número.
Cuando una funcionalidad está activa, sus campos deben estar presentes en el wire. El protocolo es estrictamente posicional, por lo que omitir un campo sujeto a control de funcionalidad corrompe el flujo de bytes de todos los campos posteriores.

Tabla de funcionalidades

Envoltura del paquete

Todos los mensajes que se transmiten por el wire comparten la misma estructura externa, en ambas direcciones:
Las tablas completas de tipos de paquete se encuentran en la referencia de tipos de paquete. El tipo de paquete es un VarUInt, no un byte de ancho fijo. Para valores inferiores a 128, un VarUInt genera ese mismo byte único, pero las implementaciones deben usar la codificación VarUInt para mantener la compatibilidad en caso de que futuros tipos de paquete alcancen el valor 128 o superior. La referencia de mensajes documenta únicamente el cuerpo de cada paquete, es decir, los bytes que siguen al código de tipo de paquete. La numeración de los campos comienza en 1 con el primer campo del cuerpo.

Enmarcado por fragmentos (v54470+)

Cuando se negocia la funcionalidad CHUNKED_PROTOCOL (consulte el handshake), cada paquete que se transmite en formato wire se encapsula mediante enmarcado por fragmentos. El encapsulado se aplica por dirección: los sentidos client→servidor y servidor→client se negocian por separado y pueden acabar en modos distintos (por fragmentos o sin enmarcar). Disposición en formato wire de cada paquete:
Disposición en formato wire de cada fragmento:
El VarUInt del tipo de paquete está dentro del stream fragmentado: es el primer byte de la carga útil del paquete (el primer byte del primer fragmento), no un byte independiente enviado antes del encapsulado. La carga útil de fragmento de cada paquete es el [VarUInt packet_type_code][message body] completo del sobre del paquete. Si un cliente deja el tipo de paquete fuera del stream fragmentado, el otro extremo leerá ese byte de tipo como el primer byte del tamaño de fragmento u32, lo que desincroniza la conexión. Un mismo paquete puede dividirse en varios fragmentos si el búfer del escritor se llena a mitad del paquete; la división puede producirse en cualquier punto, incluso dentro del VarUInt del tipo de paquete. El lector concatena las cargas útiles de los fragmentos y trata el cero final de 4 bytes como un límite de paquete transparente: lo consume, pero no lo expone al componente que lea los cuerpos de los paquetes. Los paquetes sin cuerpo también se encapsulan: un paquete de un solo byte como Ping o Pong se convierte en [u32 size = 1][0x04][u32 0] una vez negociada la fragmentación. Cualquier descripción de “un solo byte en el wire” que aparezca en otras partes de esta página corresponde a la forma previa a la fragmentación. Negociación. ServerHello y Addendum transportan cada uno dos campos String, uno por dirección, con valores obtenidos de {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
  • chunked / notchunked son estrictos: ese lado exige exactamente ese modo.
  • Las variantes _optional son flexibles: aceptan el modo que elija el otro lado.
El valor acordado para cada dirección se calcula por pares: En el lado del cliente, la preferencia de ENVÍO del cliente se negocia con la preferencia de RECEPCIÓN del servidor, y viceversa. Momento. Las cadenas de negociación viajan por el wire sin encapsular: ClientHello → ServerHello (preferencias del servidor) → Addendum (valores negociados por el cliente). El cambio de encapsulado se aplica a todos los bytes enviados después del vaciado del Addendum. El propio Addendum, el ClientHello y el ServerHello nunca se encapsulan.

Ciclo de vida de la conexión

En todo momento, una conexión se encuentra exactamente en uno de estos cuatro estados: HANDSHAKE, READY, READING_RESPONSE o terminada. Como el protocolo no admite multiplexación, un client que envía una nueva solicitud antes de vaciar la respuesta anterior intercala bytes en la transmisión y corrompe el flujo de datos.

Estados

El flujo nominal avanza en línea recta hacia abajo —HANDSHAKE → READY → READING_RESPONSE → READY—, con el bucle Ping/Pong sobre sí mismo y todas las aristas de fallo confluyendo en el único sumidero Terminated.

Fase de handshake

Autentica al cliente y negocia la versión del protocolo. Se produce exactamente una vez por conexión, antes que cualquier otra operación. La conexión TCP acaba de abrirse y todavía no se ha intercambiado ningún mensaje. El flujo es el siguiente:
  1. El cliente envía ClientHello con la versión máxima del protocolo que admite.
  2. El cliente lee la respuesta y actúa según el tipo de paquete:
  3. Si negotiated_version ≥ 54458 (la funcionalidad ADDENDUM), el cliente envía un Addendum. Esta decisión se basa en la versión negociada, no en la versión declarada por el cliente.
Si todo se completa correctamente, la conexión pasa a READY; ante cualquier error, se cierra.

Fase de Ping

Una comprobación de actividad a nivel de aplicación, independiente del keepalive de TCP. Un intercambio Ping/Pong completado con éxito confirma que la conexión TCP sigue activa en ambas direcciones y que el servidor responde. Ping no tiene estado ni está asociado a ninguna consulta, por lo que varios Pings consecutivos son independientes entre sí. A partir de READY, el flujo es el siguiente:
  1. El client envía Ping.
  2. El client lee la respuesta:

Fase de consulta

El cliente envía una sentencia SQL y el servidor transmite progresivamente bloques de resultados y telemetría de ejecución. La respuesta es una secuencia de paquetes que finaliza con un único EndOfStream o Exception. A partir de READY, el flujo es el siguiente: Si se produce un error en cualquier momento, el servidor envía un Exception en lugar de EndOfStream, lo que pone fin a la consulta.
  1. El client envía Query con un query_id único (normalmente un UUID).
  2. El client envía las tablas externas que haya y, a continuación, el marcador Data vacío. El paquete Data vacío tiene table_name = "", num_columns = 0, num_rows = 0. El servidor no empieza a ejecutar la consulta hasta recibir este marcador. Una tabla externa puede dividirse en varios paquetes Data con el mismo table_name; el primero de ellos fija el esquema de la tabla, y todos los paquetes posteriores con ese nombre deben contener los mismos nombres y tipos de columna, en el mismo orden; de lo contrario, el servidor rechaza la consulta con INCORRECT_DATA (consulte Data).
  3. El client pasa a READING_RESPONSE y vacía su búfer de escritura.
  4. El client lee los paquetes de respuesta en un bucle y los procesa según su tipo:
Tras EndOfStream o un Exception gestionado, la conexión vuelve a READY. Una violación del protocolo o un error de E/S la cierra.
El caso num_rows == 0 suele confundir a las nuevas implementaciones. Un bloque sin filas es un marcador de límite o un encabezado de esquema, no una señal de fin de flujo. Solo EndOfStream o Exception ponen fin a la respuesta.

Fase INSERT

La fase INSERT es la fase de consulta con dos intercambios adicionales. El client envía una sentencia INSERT; el servidor responde con un bloque de esquema que describe la tabla de destino; el client transmite en flujo paquetes Data con las filas y, a continuación, el marcador Data vacío; el servidor termina con EndOfStream o Exception. A partir de READY, el SQL es un INSERT con la forma INSERT INTO <table> [(<cols>)] VALUES, sin ningún literal VALUES (...) en línea, ya que los datos de las filas viajan en paquetes Data. El flujo es el siguiente:
  1. El cliente envía Query con body establecido en la sentencia SQL INSERT.
  2. El cliente envía las tablas externas que haya (algo poco habitual en un INSERT). A diferencia de la fase Query, aquí no envía un marcador Data vacío. El paquete Query del INSERT se envía con datos pendientes, por lo que el bloque vacío de fin de datos se aplaza hasta el paso 5; si se enviara antes del bloque de esquema, el servidor lo interpretaría como el final del flujo de filas, finalizaría el INSERT sin filas y luego analizaría el primer paquete de filas real como un paquete de nivel superior inesperado.
  3. El cliente consume los paquetes de metadata (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) hasta que lee el paquete Data de esquema: un Block con 0 filas pero con la estructura completa de columnas (nombres y tipos). El bloque de esquema actúa como contrato: las filas que el cliente envíe a continuación deben ajustarse a la forma de estas columnas.
  4. El cliente envía uno o varios bloques de datos. Para cada bloque escribe VarUInt(ClientPacket::Data = 2), luego String("") como nombre vacío de tabla externa y, por último, el Block. Los tipos de columna deben corresponderse por posición con las columnas del bloque de esquema.
  5. El cliente envía el terminador de fin de entrada: un paquete Data con un Block vacío (0 columnas, 0 filas).
  6. El cliente consume el flujo de respuesta hasta recibir EndOfStream (éxito) o Exception (error).
INSERT asíncrono (v54484+). Cuando la consulta incluye async_insert = 1, el servidor encola las filas y las vacía como parte de un lote. Con una versión negociada ≥ 54484 (PROGRESS_IN_ASYNC_INSERT), una vez completado el vaciado, el servidor emite un paquete Progress adicional, seguido inmediatamente de los ProfileEvents del insert y, a continuación, de EndOfStream. Con versiones inferiores a 54484, el servidor omite ese Progress final. El paquete es un Progress normal; como el servidor reinicia la canalización de la consulta antes de incorporar los recuentos de escritura, en la práctica el incremento solo contiene el tiempo transcurrido, y las estadísticas de filas y bytes escritos llegan al cliente a través de los ProfileEvents que lo acompañan. A un cliente que ya consume paquetes Progress intercalados en el paso 6 le basta con aceptar un paquete más. La conexión vuelve a READY al recibir EndOfStream o una Exception gestionada. Las violaciones del protocolo y los errores de E/S cierran la conexión.

Referencia de mensajes

Los campos se enumeran en orden de wire. La columna Type utiliza:
  • VarUInt: integer sin signo de longitud variable (consulte VarUInt).
  • String: bytes precedidos por un VarUInt (consulte String).
  • UInt8, Int32, etc.: integers de ancho fijo en formato little-endian.
  • Bool: un único byte, 0x00 o 0x01.
La columna Role indica quién utiliza cada campo:
  • client: lo establecen los client externos.
  • inter-server: solo es relevante en la comunicación entre servidores; los client externos escriben un valor predeterminado.
  • universal: lo utilizan ambos.
Estas tablas documentan únicamente el cuerpo de cada paquete, que sigue al código de tipo de paquete.

ClientHello (tipo de paquete 0)

Client → Server. Es el primer mensaje que se envía tras abrirse la conexión TCP.

ServerHello (tipo de paquete 0)

Server → Client. La respuesta a ClientHello cuando la autenticación se completa correctamente. Rule: un elemento de password_complexity_rules: La lista refleja la política de contraseñas configurada por el operador del servidor y es meramente orientativa: el servidor no aplica estas reglas durante el handshake. Un client que permita cambiar o establecer contraseñas puede usar las reglas para señalar errores antes de enviar al servidor una contraseña que no las cumpla.
Para acotar el consumo de recursos frente a un servidor hostil o mal configurado, limite el count decodificado a 256 entradas y cada String pattern y message a 4096 bytes. Un count de 0 (sin pares a continuación) es el caso habitual en servidores que no tienen configurada ninguna política de contraseñas.

Addendum (sin tipo de paquete)

Client → Server, condicionado por ADDENDUM (v54458). Se envía inmediatamente después de que finaliza el intercambio de handshake. No es un tipo de paquete propio: los campos se transmiten on the wire sin procesar, sin el byte de tipo de paquete como prefijo. El cambio al enmarcado fragmentado se aplica después del vaciado de este Addendum; el propio Addendum se envía sin enmarcar.

Ping (tipo de paquete 4)

Client → Server. Sin cuerpo: el paquete consiste en un único byte 0x04 antes del enmarcado por fragmentos; cuando se negocia la fragmentación, ese byte pasa a ser la carga útil de un byte de un fragmento (consulte enmarcado por fragmentos).

Pong (tipo de paquete 4)

Server → Client. Sin cuerpo: el paquete consiste en un único byte 0x04 antes del enmarcado por fragmentos; cuando se negocia la fragmentación, ese byte pasa a ser la carga útil de un byte de un fragmento (consulte enmarcado por fragmentos).

Exception (tipo de paquete 2)

Server → Client. Se envía cuando el servidor encuentra un error en cualquier fase.

Query (tipo de paquete 1)

Client → Server.

ClientInfo (incrustado en Query)

Client → Server, incrustado en el cuerpo de Query (field 2). Condicionado por CLIENT_INFO (v54032). (Algunos campos de ClientInfo dependen de versiones posteriores, como se indica en cada campo más abajo).
Disposición dependiente de la interfaz (campos 7–12)Los campos 7–12 anteriores corresponden a la rama TCP. Cuando query_interface (campo 6) no es TCP, estos campos se sustituyen por una disposición en formato wire diferente; no se trata simplemente de omisiones opcionales, por lo que un decodificador debe bifurcarse en función del campo 6.
  • query_interface = 2 (HTTP): en su lugar se escribe la información de la solicitud HTTP redirigida por el servidor: http_method (UInt8), http_user_agent (String), luego forwarded_for (String, condicionado por X_FORWARDED_FOR_IN_CLIENT_INFO v54443), http_referer (String, condicionado por REFERER_IN_CLIENT_INFO v54447) y, por último, http_handler_name (String) y http_request_url (String) (ambos condicionados por HTTP_HANDLER_IN_CLIENT_INFO v54490, en ese orden). No se incluyen los campos os_user/client_hostname/client_name/version_*/protocol_version. Los dos últimos contienen el nombre del handler HTTP definido en SQL que se ha asociado a la solicitud y la URL de esta, para que lleguen hasta los segmentos remotos (quedan vacíos en una consulta HTTP que no pase por un handler).
  • Cualquier otra interfaz: no se escribe ninguno de los campos TCP (7–12) ni de los campos HTTP; el stream continúa directamente con quota_key.
Tras esta rama, la disposición vuelve a ser común: quota_key (campo 13) y distributed_depth (campo 14) aparecen a continuación en todas las interfaces, y después version_patch (campo 15) se escribe solo para TCP.Esta rama es relevante sobre todo para el tráfico entre servidores, cuando el servidor iniciador redirige una consulta que llegó originalmente por HTTP. Un decodificador que lea siempre los campos TCP interpretará mal estos paquetes y tratará http_method o http_user_agent como quota_key.
Codificación de OpenTelemetry (campo 16):

Autenticación entre servidores

El campo 4 de Query (auth_hash) no es el secreto compartido del cluster tal como viaja on the wire. Enviar el secreto sin procesar haría fallar la autenticación y, además, lo expondría. En su lugar, un servidor que actúa como client entre servidores demuestra que conoce el secreto mediante un hash SHA-256 con sal:
  1. Entrar en modo entre servidores. El servidor que inicia la conexión lo indica dentro de ClientHello: el campo user contiene el marcador de modo entre servidores y password está vacío. A continuación, añade dos cadenas más —el nombre del cluster y un salt de 32 bytes recién generado (encodeSHA256 de un valor aleatorio)— justo después de los campos user/password, dentro del mismo paquete ClientHello. El servidor lee estas dos cadenas antes de enviar ServerHello, por lo que el client debe escribirlas de antemano; si espera primero a ServerHello, se produce un interbloqueo, porque el servidor se queda bloqueado esperando leerlas.
  2. Obtener el nonce. ServerHello incluye un nonce UInt64 de 8 bytes cuando se negocia INTERSERVER_SECRET_V2 (v54462).
  3. Calcular el hash. En cada paquete Query que no sea InitialQuery, el client escribe encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles) en el campo 4: un digest de 32 bytes. (nonce se representa como cadena decimal y solo está presente cuando se negocia ≥ v54462; external_roles se añade solo cuando se negocia INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472); current_roles es la lista serializada de nombres de roles —[VarUInt count][String]*count, sin el byte de presencia— y se añade solo cuando se negocia INTERSERVER_CURRENT_ROLES (v54488) y la lista está presente). En el caso de una InitialQuery, o si no hay ningún secreto de cluster configurado, el client escribe una cadena vacía.
  4. Verificar. El servidor lee el campo 4 con un límite de 32 bytes y recalcula la misma concatenación con su propia copia del secreto del cluster; si los digests no coinciden, rechaza la conexión.
Los client externos (no entre servidores) nunca entran en este modo y siempre envían un auth_hash vacío.

Setting

Se codifica en línea dentro de la Lista de Settings del cuerpo de Query (el paquete Query, campo 3). La lista está siempre presente, independientemente de la versión negociada, y termina con un Setting con clave vacía: un único VarUInt 0, sin indicadores ni valor a continuación. Lo único que depende de la versión negociada es la codificación de cada setting, condicionada por SETTINGS_SERIALIZED_AS_STRINGS (v54429). v54429+ (STRINGS_WITH_FLAGS): cada setting es la siguiente tripleta: Los campos 2 y 3 se omiten cuando key está vacío. Anterior a 54429 (BINARY): cada setting es [String key][type-specific binary value]. El campo flags no se escribe, y el valor se codifica en la forma binaria nativa del setting (por ejemplo, un integer de ancho fijo o una cadena precedida de su longitud) en lugar de como una cadena decimal o de texto. La lista sigue terminando con una key vacía. Un client que trabaje con una versión negociada inferior a 54429 debe leer y escribir esta forma binaria, no la tripleta anterior. (La excepción son los settings personalizados definidos por el usuario: siempre incluyen flags y un valor de cadena, en ambas codificaciones). El campo flags agrupa:
  • 0x01 — Important: el setting afecta a los resultados de la consulta y los peers más antiguos no deben ignorarlo silenciosamente.
  • 0x02 — Custom: un setting personalizado definido por el usuario.
  • 0x0c — un campo de nivel de 2 bits, no un indicador independiente: 0x00 = Production, 0x04 = Obsolete, 0x08 = Experimental, 0x0c = Beta. Lea ambos bits (flags & 0x0c): una comprobación simplista como flags & 0x04 clasificaría erróneamente Beta (0x0c) como Obsolete.
  • 0x80 — HotReload (recarga de la configuración sin reiniciar; definido en el enum de indicadores, aparece principalmente en los settings de coordinación).

Parámetro

Parámetros de consulta, para consultas parametrizadas como SELECT {x:UInt64}. Se codifican exactamente igual que un Setting con el indicador Custom (0x02) activado y, del mismo modo, terminan con una clave vacía.
El valor del parámetro es la representación SQL del valor, no un literal sin procesar. Los parámetros de tipo String deben pasarse ya entre comillas simples (por ejemplo, el valor de {name:String} es 'Alice', no Alice); de lo contrario, el analizador de valores del servidor los rechazará.

Data (tipo de paquete 1 servidor→cliente, tipo de paquete 2 cliente→servidor)

Se usa en ambas direcciones. Transporta bloques de resultados, datos de INSERT, tablas externas y marcadores de fin de datos. El formato wire es simétrico: en ambas direcciones se incluye un prefijo table_name antes del Block. Lo único que cambia es el byte del tipo de paquete.
El marcador de fin de datos es un paquete cuyo Block está vacío (0 columnas y 0 filas), sea cual sea el valor de table_name. El servidor solo trata un paquete Data del cliente como terminador cuando el bloque decodificado está vacío (block.empty()); un paquete con table_name = "" y un bloque no vacío es un paquete de filas ordinario, no un terminador. Así pues, un stream de filas de INSERT consiste en una secuencia de bloques Data no vacíos seguida de un único bloque Data vacío que lo cierra. Las variantes de bloque y su significado se describen en Variantes de Block.

Progress (packet type 3)

Server → Client. Se envía periódicamente durante la ejecución de la consulta. Todos los campos son VarUInt, y cada paquete contiene incrementos desde el paquete Progress anterior, no totales acumulados. Antes de enviarlo, el servidor lee sus contadores, los restablece a cero de forma atómica y calcula elapsed_ns como la diferencia de tiempo transcurrida desde el último envío. Por lo tanto, un cliente debe acumular localmente los paquetes sucesivos para obtener los totales parciales: si trata un paquete como un valor absoluto, el indicador de progreso retrocederá o mostrará valores inferiores a los reales en cuanto llegue más de un paquete.

ProfileInfo (packet type 6)

Server → Client. Se envía una vez por consulta, cerca del final de la ejecución.

Totals (tipo de paquete 7)

Servidor → Cliente. Se envía en las consultas con WITH TOTALS. El formato wire es idéntico al de Data: una cadena table_name (siempre vacía) seguida de un Block. Lo único que cambia es el byte del tipo de paquete.

Extremes (tipo de paquete 8)

Server → Client. Se envía cuando la configuración extremes está habilitada. El formato wire es idéntico al de Data. El bloque tiene exactamente 2 filas: la fila 0 contiene el mínimo de cada columna y la fila 1, el máximo.

Log (packet type 10)

Server → Client. Se envía cuando la consulta tiene una cola de log activa (configuración send_logs_level; consulte streaming de logs). Usa el mismo envelope y el mismo formato de cuerpo que Data. El bloque tiene un valor fijo de num_columns = 8 y un esquema predefinido. Cada línea de log corresponde a una fila que abarca las 8 columnas, y un único paquete Log puede contener muchas filas.
Las 8 columnas, exactamente en este orden:

ProfileEvents (tipo de paquete 14)

Server → Client. Transporta contadores de rendimiento por consulta. Usa el mismo envoltorio y formato de cuerpo que Data. El bloque tiene un valor fijo num_columns = 6 y un esquema predefinido. Cada evento corresponde a una fila.
Las 6 columnas:
El tipo de elemento de la columna value no es el mismo en todos los paquetes: los servidores más antiguos emiten UInt64 y los más recientes, Int64. Lea la cadena de tipo de la columna en el encabezado del bloque en lugar de dar por supuesto un ancho determinado.

TableColumns (packet type 11)

Server → Client, condicionado por COLUMN_DEFAULTS_METADATA (v54410). El servidor lo envía antes del bloque de esquema de INSERT para transmitir los metadata de valores predeterminados de las columnas, pero solo cuando la versión negociada es ≥ 54410 y la configuración input_format_defaults_for_omitted_fields está habilitada. Por debajo de 54410, el paquete nunca se envía, por lo que un client más antiguo no debe esperarlo: el bloque de esquema Data llega directamente. Un client v54410+ debe estar preparado para cualquiera de los dos órdenes: un TableColumns opcional seguido del bloque de esquema, o directamente el bloque de esquema.
Cuerpo comprimido a partir de v54481Con una versión negociada ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), el servidor escribe ambos campos a través de la misma ruta de salida con compresión opcional, de modo que, cuando la consulta tiene compression = true, todo el cuerpo de TableColumns (external_table + columns_description) va dentro del marco de compresión; el client lo lee a través del flujo descomprimido correspondiente. Cuando la consulta no usa compresión, el cuerpo se transmite sin comprimir, exactamente como se muestra en la tabla anterior. Esto es importante para las respuestas de esquema de INSERT: un client que adapta el tratamiento de la compresión para Log y ProfileEvents, pero no para TableColumns, interpretará mal la respuesta cuando la compresión de la consulta esté habilitada.

TimezoneUpdate (packet type 17)

Server → Client, condicionado por TIMEZONE_UPDATES (v54464). Se envía en un único punto: el inicializador de la función de tabla input (una consulta de la forma INSERT INTO <table> SELECT ... FROM input('<structure>'), que transmite filas desde el client). Justo después de que el servidor envíe el bloque Data del esquema de entrada (consulte la fase INSERT), emite TimezoneUpdate con el valor actual de session_timezone del contexto de la consulta, para que el client analice las filas que está a punto de enviar con la misma zona horaria. El servidor no emite este paquete ante cambios arbitrarios mediante SET session_timezone a mitad de la consulta, ni para indicar al client cómo formatear los bloques de resultados posteriores. El paquete llega una sola vez, inmediatamente después del bloque de esquema de entrada y antes de que el client empiece a enviar bloques de filas. Un decodificador que ignore TimezoneUpdate DEBE consumir igualmente el String final para mantener alineado el wire.

Autenticación SSH de desafío-respuesta (tipos de paquete 11, 12, 18)

Está condicionada por SSH_AUTHENTICATION (v54466) y solo se activa de forma explícita. Una conexión entra en el flujo SSH cuando ClientHello envía user = " SSH KEY AUTHENTICATION " + <real_user> (con los espacios inicial y final) y password = "". El servidor lee el prefijo, lo elimina para recuperar el usuario real y pasa al modo de desafío-respuesta. Este flujo sustituye a la autenticación por contraseña, y el intercambio de desafío-respuesta tiene lugar antes de ServerHello: el servidor pospone su respuesta Hello hasta que la autenticación se completa correctamente.
  1. El cliente envía ClientHello con el prefijo marcador SSH y una contraseña vacía.
  2. El cliente envía SSHChallengeRequest (paquete 11). El servidor aún no ha enviado ServerHello: primero procesa la autenticación y queda bloqueado en este punto a la espera de este paquete.
  3. El servidor responde con SSHChallenge, que contiene bytes aleatorios (paquete 18).
  4. El cliente construye la cadena que se debe firmar y firma esa cadena, no el desafío sin procesar; a continuación, envía SSHChallengeResponse (paquete 12) con la firma. El mensaje firmado es la concatenación byte a byte, sin separadores, de cuatro partes en este orden exacto:
  5. El servidor verifica la firma con la clave pública registrada del usuario y reconstruye la misma cadena decimal(protocol_version) + default_database + user + challenge. Si la verificación es correcta, envía ServerHello (la misma respuesta que en el flujo con contraseña) y el handshake continúa con normalidad (Addendum, etc.); si falla, devuelve una Exception y cierra la conexión. Un cliente que firme únicamente los bytes sin procesar del desafío no superará la autenticación.
Esto es lo contrario de lo que ocurre en el handshake con contraseña, donde ServerHello sigue inmediatamente a ClientHello. Con la autenticación SSH, ServerHello no se envía hasta que se verifica la firma, por lo que el desafío-respuesta SSH se intercala en el handshake antes de que aparezca ningún ServerHello.
Los client externos que no usan autenticación SSH nunca ven los paquetes 11, 12 ni 18: no circulan por el wire a menos que el usuario lo active explícitamente mediante el prefijo del nombre de usuario.

MergeTreeAllRangesAnnouncementResponse (tipo de paquete 14)

Client → Server, solo entre servidores. Condicionado por parallel_replicas_protocol_version ≥ 8 (consulte VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Los clientes externos nunca envían este paquete. Cuando la versión negociada de réplicas paralelas es ≥ 8, cambia el ciclo de solicitud/respuesta del iniciador ante un MergeTreeAllRangesAnnouncement de un seguidor (tipo de paquete 15, dirección servidor→cliente):
  1. Un seguidor abre su canalización de lectura y envía MergeTreeAllRangesAnnouncement al iniciador.
  2. Solo cuando el mode del anuncio es distinto de Default (WithOrder = 1 o ReverseOrder = 2, ambos utilizados en lecturas paralelas ordenadas), el iniciador responde con MergeTreeAllRangesAnnouncementResponse. Con mode = Default = 0, el iniciador no responde y el seguidor no espera: el modo Default reparte los rangos con cada MergeTreeReadTaskRequest y nunca necesita la lista de partes por adelantado.
  3. El seguidor queda bloqueado esperando la respuesta (cuando corresponde) antes de emitir su primer MergeTreeReadTaskRequest (paquete de servidor 16, enviado de seguidor a iniciador; el iniciador responde con MergeTreeReadTaskResponse, paquete de cliente 10), y utiliza la lista de partes devuelta para limitar la construcción de fuentes exactamente a las partes que pertenecen a su stream #split_i.
En versiones inferiores a la 8, el anuncio funciona en modo fire-and-forget sea cual sea el modo, y el seguidor construye fuentes sobre todas las partes conocidas localmente (el comportamiento heredado).

Cuerpo

Cuerpo de RangesInDataPartsDescription

Cuerpo de RangesInDataPartDescription

Cuerpo de MergeTreePartInfo

Cuerpo de MarkRanges

Referencia de tipos de paquete

Client → Server

Server → Client

QueryPlan

QueryPlan es un paquete de cliente exclusivo para la comunicación entre servidores. Se envía a continuación de un Query cuyo stage es QueryPlan (7) y contiene un único stream de plan serializado. Antes de la versión 10, los campos 2 y 3 no existen. Un iniciador que tenga alguno de estos límites de ejecución con un valor no predeterminado no debe enviar esa disposición antigua: en su lugar, envía la consulta SQL original para que el receptor antiguo reconstruya los límites a partir de los ajustes de consulta.

Configuración

Esta sección describe los parámetros ajustables que determinan el comportamiento de las conexiones del protocolo nativo: Los valores predeterminados que se indican a continuación corresponden a una versión reciente del servidor y pueden variar según la versión y la implementación.

Ajustes de la capa de transporte

Opciones de socket

Tiempos de espera

Los tiempos de espera se anidan de la siguiente manera:
El keepalive del sistema operativo se activa primero y puede detectar de forma silenciosa, a nivel del kernel, los pares que ya no responden. El tiempo de espera de recepción de la aplicación es la siguiente línea de defensa. El tiempo de espera por inactividad es el último recurso y cierra las conexiones que llevan mucho tiempo sin usarse.

Límites de conexión

Una conexión que envía consultas con regularidad puede mantenerse abierta indefinidamente. Solo se cierran las conexiones que permanecen inactivas durante una hora, y no existe una duración máxima predeterminada.

Ajustes de la capa de aplicación

Estos ajustes se envían con cada consulta en la lista de Settings del paquete Query. Modifican lo que el servidor transmite on the wire o la forma en que se estructura en tramas.

Compresión

El indicador compression del paquete Query (campo 6) activa o desactiva la compresión; esta configuración determina qué códec se utiliza cuando está activada.

Streaming de logs

Si se establece send_logs_level en cualquier valor distinto de "none", el servidor emite paquetes Log durante la ejecución de la consulta.

Informe de progreso

Se trata de un mínimo objetivo, no de un máximo estricto: el servidor puede enviar paquetes Progress con menor frecuencia si la consulta no genera trabajo con la suficiente rapidez.

Envoltorio de resultados

INSERT asíncrono

Trazas distribuidas

Ajustes fuera del alcance

Estas configuraciones a veces se confunden con configuraciones a nivel de protocolo, pero controlan la ejecución de SQL, el almacenamiento o el uso de CPU, no el comportamiento en formato wire. Una implementación del protocolo no necesita darles ningún tratamiento especial.
  • max_threads — paralelismo en la ejecución de consultas.
  • max_memory_usage — límite de memoria por consulta.
  • max_block_size, preferred_block_size_bytes — dimensionamiento interno de bloques durante el procesamiento de consultas; los bloques wire no dependen de ellas.
  • compile_expressions — compilación JIT; solo afecta a la CPU.
  • async_insert_max_data_size — búfer de cola del lado del servidor.
  • Todas las configuraciones input_format_* y output_format_* excepto la familia input_format_native_* / output_format_native_*: las que no son native seleccionan o ajustan otros formatos (por ejemplo, sobre HTTP) y no modifican los bloques Data del protocolo nativo.
Las configuraciones *_native_* son la excepción: modifican los bytes dentro de los bloques Data de TCP nativo, por lo que una implementación del protocolo debe tenerlas en cuenta. output_format_native_encode_types_in_binary_format cambia el campo type de la columna de una cadena de texto a una codificación binaria de tipos (excepto en una consulta secundaria; consulte disposición en formato wire de columnas), output_format_native_write_json_as_string emite las columnas JSON como String, y output_format_native_use_flattened_dynamic_and_json_serialization selecciona la disposición FLATTENED de Dynamic/JSON. Como afectan al cuerpo del bloque y no al envoltorio del paquete, se describen en la especificación del formato Native; consulte disposición en formato wire de columnas y tipos versionados.

Glosario

Cancel — un paquete iniciado por el cliente (tipo 3) que cancela una consulta en ejecución. No se especifica en detalle en esta página. Marcador de fin de datos del cliente — un paquete Data vacío (0 columnas, 0 filas) que el cliente envía para cerrar un flujo de entrada. Su posición varía según el tipo de consulta:
  • Consulta normal (SELECT, etc.): se envía después del paquete Query y de los posibles paquetes Data de tablas externas para indicar «no hay más datos externos». A continuación, el servidor comienza la ejecución.
  • INSERT: el cliente no envía un marcador previo al esquema. El servidor envía primero el bloque de esquema, el cliente transmite sus bloques Data de filas y solo entonces envía el paquete Data vacío para finalizar el flujo de filas. Si se enviara un marcador vacío antes del bloque de esquema, se interpretaría como un fin inmediato de las filas y se perderían los datos.
Feature — un cambio en el formato wire introducido en una versión específica del protocolo. Está activa cuando la versión negociada es igual o superior a la versión de la feature. Consulte control de versiones y control de funcionalidad. Inter-server — una etiqueta de rol para un campo que solo tiene sentido en consultas distribuidas entre servidores. Los clientes externos escriben un valor predeterminado (normalmente una cadena vacía, 0 o false). Versión negociada — min(client_version, server_version), calculada durante el handshake. Determina qué features están activas durante toda la vida de la conexión. Paquete — un mensaje wire: un código de tipo de paquete VarUInt seguido de un cuerpo cuyo formato depende del tipo. Consulte envoltura del paquete. Código de tipo de paquete — el VarUInt inicial de un paquete que identifica su formato. Actualmente están asignados los valores 0–18. Consulte la referencia de tipos de paquete. Flujo de respuesta — la secuencia de paquetes que el servidor emite durante una consulta. Su longitud no está acotada y termina exactamente con un EndOfStream (éxito) o un Exception (fallo). Consulte la fase de consulta. Bloque de esquema — el bloque de encabezado (un Block con columnas pero 0 filas) que el servidor envía durante la fase INSERT para anunciar la estructura esperada de las columnas antes de que el cliente envíe los datos. Lista de Settings — una secuencia de tuplas (key, flags, value) en el cuerpo de Query, terminada por una clave vacía. Transporta la configuración de la capa de aplicación para cada consulta. Consulte Setting. Stage — un campo VarUInt del paquete Query (campo 5) que controla hasta qué punto el servidor ejecuta la consulta. Los clientes externos suelen enviar 2 (Complete); las consultas distribuidas y los planes de consulta serializados usan los valores superiores. Consulte el campo 5 de Query para ver el conjunto completo de valores wire. Terminador — un paquete que finaliza un flujo. La respuesta de Query termina con EndOfStream (éxito) o Exception (fallo). El flujo de entrada del cliente termina con el marcador Data vacío.