Skip to main content
El protocolo nativo es el protocolo binario orientado a conexión que los clientes y servidores de ClickHouse usan sobre TCP. Transporta consultas SQL, datos de resultados, payloads de INSERT, telemetría de ejecución y señales de error. Es el protocolo en el que se basan el Client de línea de comandos, el driver nativo de C++ y la mayoría de los drivers nativos de terceros. Esta página describe el protocolo en sí: el encapsulado de paquetes, la máquina de estados de la conexión, la negociación de versiones y el cuerpo de todos los mensajes que no son Block. Los bytes dentro de los paquetes de la familia Data (el Block, sus columnas y las codificaciones de cada tipo) son un aspecto aparte, documentado en la especificación de Formato nativo.
Especificación complementariaEsta página es una de las dos partes de un conjunto y se publica junto con la especificación complementaria de Formato nativo. Ambas especificaciones reparten claramente el trabajo: esta página cubre la capa de paquetes y transporte; la especificación de Formato nativo cubre los bytes dentro de los paquetes de la familia Data.
Hay varias propiedades que se mantienen en todo el protocolo. Es binario y posicional: no hay etiquetas de campo salvo dentro de BlockInfo, así que un solo byte fuera de lugar desincroniza todo lo que viene después. Tiene estado, y cada conexión TCP procesa una consulta cada vez; no hay multiplexación. Los enteros de ancho fijo están en formato 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 handshake único, luego cualquier cantidad de intercambios Ping o Query, y por último el cierre: El protocolo TCP nativo siempre transporta datos tabulares en el formato Native, independientemente de cualquier cláusula FORMAT en SQL. Volver a formatear en RowBinary, CSV, JSON, etc. es tarea del Client, y se hace después de decodificar los bloques Native. (La interfaz HTTP sigue una ruta de código distinta que respeta la cláusula FORMAT; HTTP queda fuera del alcance de esta explicación.)

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 por byte, tanto si se usa TLS como si no.

Autenticación

La autenticación se realiza durante el handshake, en el mensaje ClientHello. Los campos user y password se transmiten como cadenas de texto en claro, por lo que el cifrado a nivel de transporte (TLS) es lo que protege las credenciales durante el tránsito. La autenticación SSH de desafío-respuesta está disponible a partir de la versión 54466 del protocolo; consulta Autenticación SSH de 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 transmitir el secreto por la red. 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. Esto está controlado por la funcionalidad INTERSERVER_SECRET (v54441). Los Clients externos siempre envían aquí una cadena vacía. Consulte Autenticación entre servidores.

Versionado y control de funcionalidad

Negociación de versiones

Tanto el Client como el servidor indican la versión máxima del protocolo que admiten durante el handshake. La versión negociada es la menor de las dos:
Cada mensaje posterior usa la versión negociada para determinar qué campos están presentes en el wire.

Controles de funcionalidad

Una 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 a ese número.
Cuando una funcionalidad está activa, sus campos deben estar presentes en la representación binaria transmitida. El protocolo es estrictamente posicional, por lo que omitir un campo protegido por un control de funcionalidad corrompe el flujo de bytes de todos los campos posteriores.

Tabla de funcionalidades

Encapsulado del paquete

Todos los mensajes transmitidos comparten la misma estructura externa en ambos sentidos:
Las tablas completas de los tipos de paquetes están en la referencia de tipos de paquetes. El tipo de paquete es un VarUInt, no un byte de ancho fijo. Para valores inferiores a 128, un VarUInt produce el mismo byte, pero las implementaciones deben usar la codificación VarUInt para seguir siendo compatibles si en el futuro los tipos de paquetes llegan a 128 o más. La referencia de mensajes documenta solo el cuerpo de cada paquete: los bytes que vienen después del 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 (ver el handshake), cada paquete en el wire se encapsula con enmarcado por fragmentos. Este encapsulado es por dirección: Client→servidor y servidor→Client se negocian por separado y pueden quedar en modos distintos (por fragmentos o sin enmarcar). Formato en el wire de cada paquete:
Formato en el wire por fragmento:
El tipo de paquete VarUInt está dentro del flujo fragmentado: es el primer byte de la carga útil del paquete (el primer byte del primer fragmento), no un byte separado enviado por delante del enmarcado. La carga útil fragmentada de cada paquete es el [VarUInt packet_type_code][message body] completo de la envoltura del paquete. Un Client que deja el tipo de paquete fuera del flujo fragmentado hace que el par lea ese byte de tipo como el primer byte del tamaño del fragmento u32, desincronizando la conexión. Un solo paquete puede dividirse en varios fragmentos si el búfer del escritor se llena a mitad del paquete; la división puede caer 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 se lo expone a quien esté leyendo los cuerpos de los paquetes. Los paquetes sin cuerpo siguen yendo encapsulados: un paquete de un solo byte como Ping o Pong se convierte en [u32 size = 1][0x04][u32 0] una vez que se negocia la fragmentación. Cualquier descripción de “un solo byte en el wire” en otra parte de esta página corresponde a la forma previa a la fragmentación. Negociación. ServerHello y Addendum llevan cada uno dos campos String, uno por dirección, con valores tomados de {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
  • chunked / notchunked son estrictos: ese lado requiere 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 Client, la preferencia de ENVÍO del Client se negocia frente a la preferencia de RECEPCIÓN del servidor, y viceversa. Temporización. Las cadenas de negociación viajan por el wire sin enmarcado: ClientHello → ServerHello (preferencias del servidor) → Addendum (valores negociados del Client). El cambio al enmarcado se aplica a cada byte enviado después de que se vacíe el Addendum. El propio Addendum, ClientHello y ServerHello siempre van sin enmarcado.

Ciclo de vida de la conexión

En cualquier momento, una conexión está exactamente en uno de cuatro estados: HANDSHAKE, READY, READING_RESPONSE o terminada. Como el protocolo no multiplexa, un Client que envía una nueva solicitud antes de terminar de leer la respuesta anterior intercala bytes en tránsito y corrompe el flujo.

Estados

El flujo nominal sigue una línea recta — HANDSHAKE → READY → READING_RESPONSE → READY — con el bucle de Ping/Pong y todas las transiciones de error convergiendo en el único estado terminal Terminated.

Fase de handshake

En ella se autentica y se negocia la versión del protocolo. Ocurre exactamente una vez por conexión, antes de cualquier otra cosa. La conexión TCP acaba de establecerse y aún no se ha intercambiado ningún mensaje. El flujo:
  1. El Client envía ClientHello con la versión máxima del protocolo que admite.
  2. El Client lee la respuesta y actúa según el tipo de paquete:
  3. Si negotiated_version ≥ 54458 (la funcionalidad ADDENDUM), el Client envía un Addendum. Esta decisión se basa en la versión negociada, no en la versión declarada del Client.
Si la operación tiene éxito, la conexión pasa a READY; ante cualquier error, se termina.

Fase de Ping

Una comprobación de liveness a nivel de aplicación, independiente del keepalive de TCP. Un intercambio de Ping/Pong completado con éxito confirma que la conexión TCP está activa en ambas direcciones y que el servidor responde. Ping es sin estado y no está correlacionado con ninguna consulta, por lo que varios Pings secuenciales son independientes. Partiendo de READY, el flujo es:
  1. El Client envía Ping.
  2. El Client lee la respuesta:

Fase de consulta

El Client envía una sentencia SQL; el servidor devuelve en streaming los bloques de resultados y la telemetría de ejecución. La respuesta es una secuencia de paquetes terminada por un único EndOfStream o una Exception. Partiendo de READY, el flujo es: Ante cualquier error, el servidor envía una Exception en lugar de EndOfStream, lo que termina la consulta.
  1. El Client envía Query con un query_id único (normalmente, un UUID).
  2. El Client envía cualquier tabla externa y luego 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 que recibe este marcador.
  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:
Con EndOfStream o una Exception controlada, la conexión vuelve a READY. Una infracción del protocolo o un error de I/O la termina.
El caso num_rows == 0 suele confundir a las implementaciones nuevas. Un bloque de cero filas es un marcador de límite o una cabecera de esquema, no una señal de fin de stream. Solo EndOfStream o Exception pone fin a la respuesta.

Fase INSERT

La fase INSERT es la fase de consulta con dos intercambios adicionales. El Client envía una instrucción INSERT; el servidor responde con un bloque de esquema que describe la tabla de destino; el Client transmite paquetes Data con las filas y, después, el marcador Data vacío; el servidor finaliza con EndOfStream o Exception. Partiendo 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 fluyen a través de paquetes Data. El flujo:
  1. El Client envía Query con body establecido en el SQL de INSERT.
  2. El Client envía cualquier tabla externa (algo poco habitual en INSERT). A diferencia de la fase consulta, no envía aquí un marcador Data vacío. El paquete consulta de INSERT se envía con datos pendientes, por lo que el bloque vacío de fin de datos se difiere hasta el paso 5; enviarlo antes del bloque de esquema haría que el servidor lo interpretara como el final del flujo de filas, completara el INSERT sin filas y luego analizara el primer paquete de fila real como un paquete suelto de nivel superior.
  3. El Client consume los paquetes de metadatos (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) hasta leer el paquete Data del esquema: un Block con 0 filas, pero con la estructura completa de columnas (nombres y tipos). El bloque de esquema es el contrato: las filas que el Client envíe a continuación deben coincidir con estas estructuras de columna.
  4. El Client envía uno o varios bloques de datos. Para cada bloque, escribe VarUInt(ClientPacket::Data = 2), luego String("") para el nombre vacío de la tabla externa y, después, el Block. Los tipos de columna deben alinearse por posición con las columnas del bloque de esquema.
  5. El Client envía el terminador de fin de entrada: un paquete Data con un Block vacío (0 columnas, 0 filas).
  6. El Client consume el flujo de respuesta hasta EndOfStream (éxito) o Exception (fallo).
INSERT asíncrono (v54484+). Cuando la consulta lleva async_insert = 1, el servidor pone las filas en cola 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 adicional de Progress, seguido inmediatamente por los ProfileEvents del insert y luego EndOfStream. Por debajo de 54484, el servidor omite ese Progress final. El paquete es un Progress normal; como el servidor restablece el query pipeline 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 Client mediante los ProfileEvents correspondientes. Un Client que ya consume paquetes Progress entrelazados en el paso 6 solo necesita aceptar uno más. La conexión vuelve a READY en EndOfStream o tras una Exception controlada. Las infracciones del protocolo y los errores de I/O la terminan.

Referencia de mensajes

Los campos se enumeran en el orden del wire. La columna Type usa:
  • VarUInt — entero sin signo de longitud variable (consulta VarUInt).
  • String — bytes con prefijo VarUInt (consulta String).
  • UInt8, Int32, etcétera — enteros little-endian de ancho fijo.
  • Bool — un único byte, 0x00 o 0x01.
La columna Role indica quién usa cada campo:
  • Client — lo establecen los clientes externos.
  • inter-server — solo tiene sentido para la comunicación entre servidores; los clientes externos escriben un valor predeterminado.
  • universal — lo usan ambos.
Estas tablas documentan solo el body de cada paquete, después del código de tipo de paquete.

ClientHello (tipo de paquete 0)

Client → servidor. El primer mensaje tras abrirse la conexión TCP.

ServerHello (tipo de paquete 0)

Servidor → Client. La respuesta a ClientHello tras una autenticación correcta. Rule — un elemento de password_complexity_rules: La lista refleja la configuración de la política de contraseñas del operador del servidor y es puramente informativa: el servidor no aplica estas reglas durante el handshake. Un Client que exponga funcionalidad para cambiar o establecer contraseñas puede usar las reglas para señalar errores antes de enviar al servidor una contraseña no válida.
Para limitar el uso 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 posteriores) es el caso habitual en servidores sin ninguna política de contraseñas configurada.

Apéndice (sin tipo de paquete)

Client → Server, controlado por ADDENDUM (v54458). Se envía inmediatamente después de que se completa el intercambio de handshake. No es un tipo de paquete distinto: los campos van por el wire en bruto, sin prefijo de byte de tipo de paquete. El cambio al enmarcado por fragmentos se aplica después de que este Apéndice se haya enviado por completo; el propio Apéndice no lleva enmarcado.

Ping (tipo de paquete 4)

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

Pong (tipo de paquete 4)

Servidor → client. Sin body: el paquete es un solo byte 0x04 antes del enmarcado por fragmentos; cuando se negocia la fragmentación, el byte pasa a ser el payload de un byte de un fragmento (consulta el enmarcado por fragmentos).

Exception (tipo de paquete 2)

Servidor → client. Se envía cuando el servidor detecta un error en cualquier fase.

Consulta (tipo de paquete 1)

Cliente → servidor.

ClientInfo (incluido en consulta)

Client → Server, incluido en el cuerpo de consulta (campo 2). Condicionado por CLIENT_INFO (v54032). (Algunos campos dentro de ClientInfo están condicionados por versiones posteriores, como se indica más abajo en cada campo.)
Layout 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 un layout wire diferente; no son simples 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 reenviada por el servidor: http_method (UInt8), http_user_agent (String), y después forwarded_for (String, condicionado por X_FORWARDED_FOR_IN_CLIENT_INFO v54443) y http_referer (String, condicionado por REFERER_IN_CLIENT_INFO v54447). No están presentes los campos os_user/client_hostname/client_name/version_*/protocol_version.
  • Cualquier otra interfaz: no se escribe ninguno de los campos TCP (7–12) ni ninguno de los campos HTTP; el flujo continúa directamente con quota_key.
Después de esta rama, el layout vuelve a unirse: quota_key (campo 13) y distributed_depth (campo 14) aparecen en todas las interfaces, y luego version_patch (campo 15) se escribe solo para TCP.Esta rama importa principalmente para el tráfico entre servidores, donde el servidor que inicia reenvía una consulta que originalmente llegó por HTTP. Un decodificador que siempre lea los campos TCP interpretará mal esos paquetes, tratando http_method o http_user_agent como quota_key.
Codificación de OpenTelemetry (campo 16):

Autenticación entre servidores

El campo 4 de consulta (auth_hash) no es el secreto compartido del clúster en la representación wire. Enviar el secreto en bruto haría que la autenticación fallara y además lo expondría. En su lugar, un servidor que actúa como Client entre servidores demuestra que conoce el secreto con un hash SHA-256 con salt:
  1. Entrar en modo entre servidores. El servidor que se conecta lo indica dentro de ClientHello: el campo user es el marcador entre servidores y password está vacío. A continuación, añade dos strings más —el nombre del clúster y una salt de 32 bytes recién generada (encodeSHA256 de un valor aleatorio)— inmediatamente después de los campos user/password, como parte del mismo paquete ClientHello. El servidor lee estas dos strings antes de enviar ServerHello, por lo que un Client debe escribirlas de antemano; esperar primero a ServerHello provoca interbloqueos, porque el servidor queda bloqueado leyéndolas.
  2. Obtener el nonce. ServerHello incluye un nonce UInt64 de 8 bytes cuando se negocia INTERSERVER_SECRET_V2 (v54462).
  3. Calcular el hash. Para cada paquete consulta que no sea InitialQuery, el Client escribe encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles) en el campo 4: un digest de 32 bytes. (nonce va en su forma de string decimal, presente solo cuando se negocia ≥ v54462; external_roles se añade solo cuando se negocia INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472).) Para un InitialQuery, o cuando no hay ningún secreto de clúster configurado, el Client escribe en su lugar un string vacío.
  4. Verificar. El servidor lee el campo 4 con un límite de 32 bytes y vuelve a calcular la misma concatenación usando su propia copia del secreto del clúster; la conexión se rechaza si los digests difieren.
Los clientes externos (no entre servidores) nunca entran en este modo y siempre envían un auth_hash vacío.

Configuración

Codificada en línea en la lista de configuraciones del cuerpo de Query (el paquete Query, campo 3). La lista siempre está presente, independientemente de la versión negociada, y termina con una SETTING con key vacía: un único VarUInt 0, sin indicadores ni valor a continuación. Solo la codificación de cada configuración depende de la versión negociada, controlada por SETTINGS_SERIALIZED_AS_STRINGS (v54429). v54429+ (STRINGS_WITH_FLAGS) — cada configuración es el triplete que se muestra aquí: Los campos 2 y 3 no están presentes cuando key está vacía. Pre-54429 (BINARY) — cada configuración es [String key][type-specific binary value]: el campo flags no se escribe, y el valor se codifica en la forma binaria nativa de la configuración (por ejemplo, un entero de ancho fijo o una cadena con prefijo de longitud) en lugar de como una cadena decimal o de texto. La lista sigue terminando con una key vacía. Un client que apunte a una versión negociada inferior a 54429 debe leer y escribir esta forma binaria, no el triplete anterior. (Las configuraciones personalizadas definidas por el usuario son la excepción: siempre llevan flags y un valor de cadena, en ambas codificaciones). El campo flags contiene:
  • 0x01Importante: la configuración afecta a los resultados de la consulta y no debe ser ignorada silenciosamente por pares más antiguos.
  • 0x02Personalizada: una configuración personalizada definida por el usuario.
  • 0x0c — un campo tier de 2 bits, no un indicador independiente: 0x00 = Production, 0x04 = Obsolete, 0x08 = Experimental, 0x0c = Beta. Lea los 2 bits completos (flags & 0x0c) — una comprobación ingenua de flags & 0x04 clasificaría erróneamente Beta (0x0c) como Obsolete.
  • 0x80HotReload (recarga de configuración sin reinicio; definido en el enum de indicadores, presente principalmente en configuraciones de coordinación).

Parámetro

Parámetros de consulta para consultas parametrizadas como SELECT {x:UInt64}. Se codifican de forma idéntica a un SETTING con el indicador Custom (0x02) activado, y terminan con una clave vacía de la misma forma.
El valor del parámetro es la representación SQL del valor, no un literal sin procesar. Los parámetros de tipo cadena 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→client, tipo de paquete 2 client→servidor)

En ambas direcciones. Transporta bloques de resultados, datos de INSERT, tablas externas y marcadores de fin de datos. El wire format es simétrico: ambas direcciones incluyen un prefijo table_name antes del Block. Solo cambia 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, independientemente de table_name. El servidor trata un paquete Data del client como terminador solo 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 normal, no un terminador. Por tanto, un flujo de filas de INSERT es una secuencia de bloques Data no vacíos seguida de un bloque Data vacío que lo finaliza. Las variantes de bloque y su significado se documentan en Block variants.

Progress (tipo de paquete 3)

Servidor → Cliente. 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 server lee sus contadores, los restablece atómicamente a cero y calcula elapsed_ns como la diferencia de tiempo desde el último envío. Por lo tanto, un client debe acumular localmente los paquetes sucesivos para obtener totales acumulados; tratar un paquete como un valor absoluto hace que la visualización del progreso retroceda o cuente de menos cuando llega más de un paquete.

ProfileInfo (tipo de paquete 6)

Servidor → client. Se envía una vez por consulta, hacia el final de la ejecución.

Totales (tipo de paquete 7)

Servidor → client. Se envía para consultas con WITH TOTALS. El formato de transmisión es idéntico a Data: una cadena table_name (siempre vacía) seguida de un Block. Solo cambia el byte del tipo de paquete.

Extremes (tipo de paquete 8)

Servidor → client. Se envía cuando la configuración extremes está habilitada. El formato de transmisión es idéntico a 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)

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

ProfileEvents (tipo de paquete 14)

Servidor → cliente. Contiene contadores de rendimiento por consulta. La envoltura y el formato del cuerpo son los mismos que en Data. El bloque tiene un num_columns = 6 fijo y un esquema predefinido. Cada evento es una fila.
Las 6 columnas:
El tipo de elemento de la columna value no es fijo entre paquetes: los servidores antiguos emiten UInt64 y los más nuevos, Int64. Lee la cadena de tipo de la columna desde la cabecera del bloque en lugar de asumir un tamaño fijo.

TableColumns (tipo de paquete 11)

Server → client, condicionado por COLUMN_DEFAULTS_METADATA (v54410). El server lo envía antes del bloque de esquema de INSERT para transportar metadatos 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 no se envía nunca, 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 estas dos secuencias: un TableColumns opcional y luego el bloque de esquema.
Cuerpo comprimido a partir de v54481Con una versión negociada ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), el server 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 frame de compresión; el client lo lee a través del flujo descomprimido correspondiente. Cuando la consulta no tiene compresión, el cuerpo se transmite sin comprimir, exactamente como muestra la tabla anterior. Esto es importante para las respuestas de esquema de INSERT: un client que cambie el manejo 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 (tipo de paquete 17)

Server → Client, controlado por TIMEZONE_UPDATES (v54464). Se envía exactamente en un lugar: el inicializador de la table function 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ía el bloque Data del esquema de entrada (consulta la fase INSERT), emite TimezoneUpdate con la session_timezone actual del contexto de la consulta para que el client procese las filas que está a punto de enviar con la misma zona horaria. El servidor no emite este paquete para cambios arbitrarios de SET session_timezone a mitad de consulta, ni para indicarle al client cómo debe dar formato a bloques de resultados posteriores. El paquete llega una sola vez, inmediatamente después del bloque del esquema de entrada y antes de que el client empiece a enviar bloques de filas. Un decodificador que ignore TimezoneUpdate DEBE igualmente consumir el String final para mantener alineado el wire.

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

Controlada por SSH_AUTHENTICATION (v54466) y disponible solo mediante activación 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 cambia al modo de desafío-respuesta. Este flujo se ejecuta en lugar de la autenticación por contraseña, y el intercambio de desafío-respuesta ocurre antes de ServerHello: el servidor aplaza su respuesta Hello hasta que la autenticación se complete 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 todavía no ha enviado ServerHello: primero procesa la autenticación y queda bloqueado aquí, esperando este paquete.
  3. El servidor responde con SSHChallenge, que contiene bytes aleatorios (paquete 18).
  4. El cliente construye la cadena que debe firmar y firma esa, no el desafío en bruto; luego 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 public key registrada del usuario, reconstruyendo la misma cadena decimal(protocol_version) + default_database + user + challenge. Si la validación tiene éxito, 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 termina la connection. Un client que firme solo los bytes del desafío en bruto no superará la autenticación.
Esto es lo contrario del handshake de autenticación por contraseña, en el que ServerHello sigue inmediatamente a ClientHello. Con la autenticación SSH, ServerHello se retiene hasta que se verifica la firma, por lo que el challenge-response de SSH se intercala en el handshake antes de que aparezca cualquier ServerHello.
Los clientes externos que no usan autenticación SSH nunca ven los paquetes 11, 12 o 18; no aparecen en el wire a menos que el usuario lo habilite explícitamente mediante el prefijo del nombre de usuario.

Referencia de tipos de paquete

Cliente → Servidor

Servidor → Cliente

Configuración

Esta sección abarca los parámetros ajustables que definen las conexiones del protocolo nativo: Los valores predeterminados que se muestran a continuación reflejan una versión reciente del servidor; pueden variar según la versión y el despliegue.

Ajustes de la capa de transporte

Opciones de socket

Tiempos de espera

Los tiempos de espera se anidan así:
El keepalive del sistema operativo se activa primero y puede detectar silenciosamente peers caídos a nivel del kernel. El tiempo de espera de recepción de la aplicación es la siguiente línea de defensa. El tiempo de espera de inactividad es el último recurso, y elimina las conexiones que llevan mucho tiempo sin usarse.

Límites de conexión

Una conexión que ejecuta consultas con regularidad puede permanecer activa indefinidamente. Solo las conexiones inactivas se cierran al cabo de una hora, y no existe un tiempo de vida máximo predeterminado.

Ajustes de la capa de aplicación

Estos ajustes se envían con cada consulta en la lista de ajustes del paquete Query. Cambian lo que el servidor envía por la red o cómo se estructura.

Compresión

El campo compression del paquete Query (campo 6) activa o desactiva la compresión; estos ajustes seleccionan qué códec se usa cuando está activada.

Streaming de logs

Establecer send_logs_level en cualquier valor distinto de "none" hace que el servidor emita paquetes Log durante la ejecución de la consulta.

Reporte de Progress

Este es un mínimo objetivo, no un máximo estricto: el servidor puede enviar paquetes Progress con menor frecuencia cuando la consulta no genera trabajo con suficiente rapidez.

Envolvente del resultado

INSERT asíncrono

Trazabilidad distribuida

Ajustes fuera del alcance

A veces estos ajustes se confunden con ajustes a nivel de protocolo, pero controlan la ejecución de SQL, el almacenamiento o el uso de CPU, en lugar del comportamiento en el wire. Una implementación del protocolo no necesita tratarlos de forma especial.
  • max_threads — paralelismo dentro de la ejecución de la consulta.
  • 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 en el wire son independientes de estos.
  • compile_expressions — compilación JIT; solo CPU.
  • async_insert_max_data_size — búfer de cola del lado del servidor.
  • Todos los ajustes input_format_* y output_format_* excepto la familia input_format_native_* / output_format_native_* — los que no son native seleccionan o ajustan otros formatos (por ejemplo, sobre HTTP) y no cambian los bloques Data del protocolo nativo.
Los ajustes *_native_* son la excepción: cambian los bytes dentro de los bloques Data del TCP nativo, por lo que una implementación del protocolo debe tenerlos 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, output_format_native_write_json_as_string emite las columnas JSON como String, y output_format_native_use_flattened_dynamic_and_json_serialization selecciona el layout FLATTENED de Dynamic/JSON. Como afectan al cuerpo del bloque y no a la envoltura del paquete, se especifican en la especificación Native Format; consulta column wire layout y versioned types.

Glosario

Cancel — un paquete iniciado por el client (tipo 3) que aborta una consulta en ejecución. No se detalla en esta página. Marcador de fin de datos del client — un paquete Data vacío (0 columnas, 0 filas) que el client 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 cualquier paquete Data de tablas externas para indicar que “no hay más datos externos”. El server comienza entonces la ejecución.
  • INSERT: el client no envía un marcador previo al esquema. El server envía primero el bloque de esquema, el client transmite sus blocks Data de filas y solo después envía el paquete Data vacío para terminar el flujo de filas. Si se enviara un marcador vacío antes del bloque de esquema, se interpretaría como un final inmediato de las filas y los datos se perderían.
Feature — un cambio en el formato wire introducido en una versión específica del protocol. Está activa cuando la versión negociada es igual o superior a la versión de la feature. Consulta control de versiones y puertas de feature. Inter-server — una etiqueta de rol para un field que solo tiene sentido en distributed queries de server a server. Los clients externos escriben un default value (normalmente una cadena vacía, 0 o false). Versión negociadamin(client_version, server_version), calculada durante el handshake. Determina qué features están activas durante el lifetime de la connection. Packet — un mensaje wire: un código de tipo de paquete VarUInt seguido de un body cuyo formato depende del tipo. Consulta envoltura de 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. Consulta la referencia de tipos de paquetes. Flujo de respuesta — la secuencia de paquetes que el server emite durante una consulta. Su longitud es abierta y termina con exactamente un EndOfStream (éxito) o Exception (error). Consulta la fase de consulta. Schema block — el header block (un Block con columnas pero 0 filas) que el server envía durante la fase de INSERT para indicar las shapes de columna esperadas antes de que el client envíe datos. Lista de Settings — una secuencia de tuples (key, flags, value) en el body de Query, terminada por una key vacía. Lleva la configuración por consulta de la capa de aplicación. Consulta Setting. Stage — un field VarUInt del paquete Query (field 5) que controla hasta dónde ejecuta la consulta el server. Los clients externos suelen enviar 2 (Complete); las distributed queries y los planes de consulta serializados usan los valores más altos. Consulta el field 5 de Query para ver el conjunto completo de valores wire. Terminator — un paquete que pone fin a un flujo. La respuesta de Query termina con EndOfStream (éxito) o Exception (error). El flujo de entrada del client termina con el marcador Data vacío.
Last modified on June 25, 2026