Skip to main content
O protocolo nativo é o protocolo binário e orientado a conexão usado por clientes e servidores do ClickHouse para se comunicarem via TCP. Ele transporta consultas SQL, dados de resultado, cargas úteis de INSERT, telemetria de execução e sinais de erro. É o protocolo usado pelo cliente de linha de comando, pelo driver nativo em C++ e pela maioria dos drivers nativos de terceiros. Esta página trata do protocolo em si: o enquadramento de pacotes, a máquina de estados da conexão, a negociação de versão e o corpo de cada mensagem que não seja Block. Os bytes dentro dos pacotes da família Data (o Block, suas colunas e as codificações de cada tipo) são um assunto à parte, documentado na especificação do Native Format.
Especificação complementarEsta página é metade de um par e é publicada junto com a especificação complementar do Native Format. As duas especificações dividem o trabalho de forma clara: esta página cobre a camada de pacotes e de transporte; a especificação do Native Format cobre os bytes dentro dos pacotes da família Data.
Algumas propriedades valem para todo o protocolo. Ele é binário e posicional: não há marcadores de campo, exceto dentro de BlockInfo, então um único byte fora do lugar dessincroniza tudo o que vem depois. Ele é stateful, e cada conexão TCP processa uma consulta por vez — não há multiplexação. Inteiros de largura fixa são little-endian.

Visão geral

Toda mensagem no wire começa com um código de tipo de pacote VarUInt, seguido por um corpo cuja estrutura depende desse código e da versão do protocolo negociada. Uma conexão passa por três fases: um handshake único, seguido de qualquer número de trocas Ping ou Query e, por fim, o encerramento: O protocolo TCP nativo sempre transporta dados tabulares no Native Format, independentemente de qualquer cláusula FORMAT no SQL. A reformatação para RowBinary, CSV, JSON etc. cabe ao cliente, que a realiza depois de decodificar os blocos Native. (A interface HTTP usa um caminho de código diferente, que de fato respeita a cláusula FORMAT; o HTTP não é abordado aqui.)

Segurança

Segurança de transporte (TLS)

O TLS opera na camada de transporte, abaixo do protocolo. Quando ele está habilitado, todo o stream TCP é criptografado, e as mensagens do protocolo são idênticas byte a byte, com ou sem o uso de TLS.

Autenticação

A autenticação ocorre durante o handshake, na mensagem ClientHello. Os campos user e password trafegam como strings em texto simples; por isso, é a criptografia na camada de transporte (TLS) que protege as credenciais em trânsito. O servidor interpreta um campo user vazio como o usuário de sessão padrão configurado: a configuração de servidor default_session_user (cujo valor default é default), que pode ser sobrescrita por listener na seção protocols. Se o servidor estiver configurado com um usuário de sessão padrão vazio, ou se for anterior à versão 26.8, um campo user vazio será rejeitado com uma exceção. Observe que o clickhouse-client nunca envia um nome de usuário vazio: ele o substitui por default no lado do cliente. A autenticação SSH por desafio-resposta está disponível a partir da versão de protocolo 54466. Consulte Autenticação SSH por desafio-resposta.

Segredo entre servidores

Na execução de consultas distribuídas, os servidores se autenticam entre si comprovando que conhecem um segredo compartilhado — sem transmitir o segredo pelo wire. Cada Query carrega um auth_hash SHA-256 de 32 bytes no campo 4 de Query, calculado a partir de um salt, um nonce, o segredo configurado e a consulta; o servidor receptor recalcula esse valor e o compara. Esse comportamento depende da feature INTERSERVER_SECRET (v54441). Clientes externos sempre enviam uma string vazia nesse campo. Consulte Autenticação entre servidores.

Versionamento e feature gates

Negociação de versão

Durante o handshake, tanto o cliente quanto o servidor informam a versão máxima do protocolo com a qual são compatíveis. A versão negociada é a menor das duas:
Todas as mensagens seguintes usam a versão negociada para determinar quais campos estão presentes no wire.

Feature gates

Uma funcionalidade é identificada pela versão do protocolo em que foi introduzida e fica ativa quando a versão negociada é maior ou igual a esse número.
Quando uma funcionalidade está ativa, seus campos devem estar presentes no wire. Como o protocolo é estritamente posicional, omitir um campo controlado por feature gate corrompe o fluxo de bytes de todos os campos subsequentes.

Tabela de funcionalidades

Envelope do pacote

Toda mensagem transmitida no wire compartilha a mesma estrutura externa, em ambas as direções:
As tabelas completas de tipos de pacote estão na referência de tipos de pacote. O tipo de pacote é um VarUInt, e não um byte de largura fixa. Para valores abaixo de 128, um VarUInt gera o mesmo byte único, mas as implementações devem usar a codificação VarUInt para continuarem compatíveis caso futuros tipos de pacote cheguem a 128 ou mais. A referência de mensagens documenta apenas o corpo de cada pacote, ou seja, os bytes que vêm depois do código do tipo de pacote. A numeração dos campos começa em 1, a partir do primeiro campo do corpo.

Enquadramento por fragmentos (v54470+)

Quando a funcionalidade CHUNKED_PROTOCOL é negociada (consulte o handshake), cada pacote transmitido no wire é encapsulado em enquadramento por fragmentos. O encapsulamento é por direção: cliente→servidor e servidor→cliente são negociados separadamente e podem acabar operando em modos diferentes (por fragmentos ou sem enquadramento). Layout wire de cada pacote:
Layout wire por fragmento:
O VarUInt do tipo de pacote fica dentro do stream fragmentado: ele é o primeiro byte da carga útil do pacote (o primeiro byte do primeiro fragmento), e não um byte separado enviado antes do enquadramento. A carga útil do fragmento de cada pacote é o [VarUInt packet_type_code][message body] completo do envelope do pacote. Se um cliente deixar o tipo de pacote fora do stream fragmentado, o peer lerá esse byte de tipo como o primeiro byte do tamanho u32 do fragmento, o que dessincroniza a conexão. Um único pacote pode ser dividido em vários fragmentos se o buffer de gravação encher no meio do pacote; a divisão pode ocorrer em qualquer ponto, inclusive dentro do VarUInt do tipo de pacote. O leitor concatena as cargas úteis dos fragmentos e trata o zero final de 4 bytes como uma fronteira de pacote transparente: ele o consome, mas não o repassa a quem estiver lendo os corpos dos pacotes. Pacotes sem corpo também são encapsulados: um pacote de um único byte, como Ping ou Pong, passa a ser [u32 size = 1][0x04][u32 0] depois que a fragmentação é negociada. Qualquer descrição de “um único byte no wire” em outras partes desta página refere-se à forma anterior à fragmentação. Negociação. O ServerHello e o Addendum carregam, cada um, dois campos String, um para cada direção, com valores do conjunto {"chunked", "notchunked", "chunked_optional", "notchunked_optional"}:
  • chunked / notchunked são estritos: o lado em questão exige exatamente esse modo.
  • As variantes _optional são flexíveis: aceitam o modo que o outro lado escolher.
O valor acordado para cada direção é calculado par a par: No lado do cliente, a preferência de ENVIO do cliente é negociada com a preferência de RECEBIMENTO do servidor, e vice-versa. Momento da troca. As strings de negociação trafegam no wire sem enquadramento: ClientHello → ServerHello (preferências do servidor) → Addendum (valores negociados pelo cliente). A mudança para o enquadramento vale para todos os bytes enviados depois do flush do Addendum. O próprio Addendum, o ClientHello e o ServerHello são sempre enviados sem enquadramento.

Ciclo de vida da conexão

Em qualquer momento, uma conexão está em exatamente um destes quatro estados: HANDSHAKE, READY, READING_RESPONSE ou encerrada. Como o protocolo não oferece multiplexação, um cliente que envia uma nova requisição antes de terminar de ler a resposta anterior intercala bytes no wire e corrompe o fluxo de dados.

Estados

O caminho feliz segue em linha reta — HANDSHAKE → READY → READING_RESPONSE → READY — com o laço de Ping/Pong no próprio estado e todas as transições de falha convergindo para um único estado final, Terminated.

Fase de handshake

Realiza a autenticação e negocia a versão do protocolo. Ocorre exatamente uma vez por conexão, antes de qualquer outra etapa. A conexão TCP acabou de ser aberta e nenhuma mensagem foi trocada ainda. O fluxo é o seguinte:
  1. O cliente envia ClientHello com a versão máxima de protocolo que suporta.
  2. O cliente lê a resposta e faz o encaminhamento de acordo com o tipo de pacote:
  3. Se negotiated_version ≥ 54458 (o recurso ADDENDUM), o cliente envia um Addendum. Essa decisão se baseia na versão negociada, e não na versão declarada pelo cliente.
Em caso de sucesso, a conexão passa para READY; em caso de erro, ela é encerrada.

Fase de Ping

Uma verificação de atividade (liveness) no nível da aplicação, independente do keepalive do TCP. Uma ida e volta de Ping/Pong bem-sucedida confirma que a conexão TCP está ativa nos dois sentidos e que o servidor está respondendo. O Ping é sem estado e não está associado a nenhuma consulta, portanto vários Pings sequenciais são independentes entre si. A partir de READY, o fluxo é o seguinte:
  1. O cliente envia Ping.
  2. O cliente lê a resposta:

Fase de consulta

O cliente envia uma instrução SQL; o servidor retorna, via streaming, blocos de resultado e telemetria de execução. A resposta é uma sequência de pacotes encerrada por exatamente um EndOfStream ou Exception. A partir de READY, o fluxo é o seguinte: Em caso de erro em qualquer etapa, o servidor envia um Exception em vez de EndOfStream, o que encerra a consulta.
  1. O cliente envia Query com um query_id único (normalmente um UUID).
  2. O cliente envia as tabelas externas, se houver, e, em seguida, o marcador Data vazio. O pacote Data vazio tem table_name = "", num_columns = 0, num_rows = 0. O servidor só começa a executar a consulta depois de receber esse marcador. Uma tabela externa pode ser dividida em vários pacotes Data com o mesmo table_name; o primeiro deles define o schema da tabela, e todos os pacotes seguintes com esse nome devem conter os mesmos nomes e tipos de coluna, na mesma ordem. Caso contrário, o servidor rejeita a consulta com INCORRECT_DATA (consulte Data).
  3. O cliente passa para READING_RESPONSE e descarrega seu buffer de escrita.
  4. O cliente lê os pacotes de resposta em loop, fazendo o encaminhamento de acordo com o tipo:
Após um EndOfStream ou um Exception tratado, a conexão retorna a READY. Uma violação de protocolo ou um erro de E/S encerra a conexão.
O caso num_rows == 0 costuma confundir quem está criando uma nova implementação. Um bloco com zero linhas é um marcador de fronteira ou um cabeçalho de schema, e não um sinal de fim de stream. Somente EndOfStream ou Exception encerram a resposta.

Fase de INSERT

A fase de INSERT é a fase de consulta com duas trocas adicionais. O cliente envia uma instrução INSERT; o servidor responde com um bloco de esquema que descreve a tabela de destino; o cliente transmite pacotes Data com as linhas e, em seguida, o marcador Data vazio; o servidor encerra com EndOfStream ou Exception. A partir de READY, o SQL é um INSERT no formato INSERT INTO <table> [(<cols>)] VALUES, sem um literal VALUES (...) embutido, já que os dados das linhas trafegam pelos pacotes Data. O fluxo:
  1. O cliente envia Query com body definido como o SQL de INSERT.
  2. O cliente envia eventuais tabelas externas (raro em INSERT). Diferentemente da fase de consulta, aqui ele não envia um marcador Data vazio. O pacote Query do INSERT é enviado com dados pendentes, então o bloco vazio de fim de dados fica adiado para o passo 5; enviá-lo antes do bloco de esquema faria o servidor interpretá-lo como o fim do stream de linhas, concluir o INSERT sem nenhuma linha e, em seguida, analisar o primeiro pacote real de linhas como um pacote de nível superior avulso.
  3. O cliente consome os pacotes de metadados (TableColumns, Progress, ProfileInfo, Log, ProfileEvents) até ler o pacote Data de esquema — um Block com 0 linhas, mas com a estrutura completa das colunas (nomes e tipos). O bloco de esquema funciona como contrato: as linhas que o cliente enviar em seguida devem corresponder a esses formatos de coluna.
  4. O cliente envia o(s) bloco(s) de dados. Para cada bloco, ele grava VarUInt(ClientPacket::Data = 2), depois String("") para o nome vazio da tabela externa e, por fim, o Block. Os tipos de coluna devem corresponder, por posição, às colunas do bloco de esquema.
  5. O cliente envia o terminador de fim de entrada: um pacote Data com um Block vazio (0 colunas, 0 linhas).
  6. O cliente consome o stream de resposta até EndOfStream (sucesso) ou Exception (falha).
Inserção assíncrona (v54484+). Quando a consulta inclui async_insert = 1, o servidor enfileira as linhas e faz o flush delas como parte de um lote. A partir da versão negociada 54484 (PROGRESS_IN_ASYNC_INSERT), após a conclusão do flush, o servidor emite um pacote Progress adicional, seguido imediatamente pelos ProfileEvents do insert e, depois, por EndOfStream. Em versões anteriores à 54484, o servidor omite esse Progress final. O pacote é um Progress comum; como o servidor redefine o pipeline da consulta antes de incorporar as contagens de gravação, na prática o incremento traz apenas o tempo decorrido, e as estatísticas de linhas e bytes gravados chegam ao cliente pelos ProfileEvents que o acompanham. Um cliente que já consome pacotes Progress intercalados no passo 6 só precisa aceitar mais um pacote. A conexão volta ao estado READY ao receber EndOfStream ou uma Exception tratada. Violações de protocolo e erros de E/S encerram a conexão.

Referência de mensagens

Os campos são listados na ordem de wire. A coluna Type utiliza:
  • VarUInt — inteiro sem sinal de comprimento variável (consulte VarUInt).
  • String — bytes prefixados por um VarUInt (consulte String).
  • UInt8, Int32 etc. — inteiros little-endian de largura fixa.
  • Bool — um único byte, 0x00 ou 0x01.
A coluna Role indica quem usa cada campo:
  • client — definido por clientes externos.
  • entre servidores — relevante apenas para a comunicação entre servidores; clientes externos escrevem um valor default.
  • universal — usado por ambos.
Estas tabelas documentam apenas o corpo de cada pacote, após o código do tipo de pacote.

ClientHello (tipo de pacote 0)

cliente → servidor. A primeira mensagem enviada após a abertura da conexão TCP.

ServerHello (tipo de pacote 0)

Servidor → Cliente. A resposta ao ClientHello quando a autenticação é bem-sucedida. Rule — um elemento de password_complexity_rules: A lista reflete a configuração da política de senhas definida pelo operador do servidor e tem caráter apenas informativo — o servidor não aplica essas regras durante o handshake. Um cliente que ofereça a funcionalidade de alterar/definir senha pode usar as regras para apontar erros antes de enviar ao servidor uma senha fora da política.
Para limitar o uso de recursos diante de um servidor hostil ou mal configurado, restrinja o count decodificado a 256 entradas e cada String pattern e message a 4096 bytes. Um count igual a 0 (sem pares em seguida) é o caso mais comum em servidores sem política de senhas configurada.

Adendo (sem tipo de pacote)

cliente → servidor, condicionado a ADDENDUM (v54458). Enviado logo após a conclusão do handshake. Não é um tipo de pacote distinto — os campos vão on the wire em formato bruto, sem o byte de prefixo de tipo de pacote. A mudança para o enquadramento fragmentado só passa a valer depois que este Adendo é descarregado (flush) — o Adendo em si não é enquadrado.

Ping (tipo de pacote 4)

cliente → servidor. Sem corpo — o pacote é um único byte 0x04 antes do enquadramento por fragmentos; quando a fragmentação é negociada, esse byte passa a ser a carga útil, de um único byte, de um fragmento (consulte enquadramento por fragmentos).

Pong (tipo de pacote 4)

Servidor → Cliente. Sem corpo — o pacote consiste em um único byte 0x04 antes do enquadramento por fragmentos; quando a fragmentação é negociada, esse byte se torna a carga útil de um byte de um fragmento (consulte enquadramento por fragmentos).

Exception (tipo de pacote 2)

servidor → cliente. Enviado quando o servidor encontra um erro em qualquer fase.

Query (tipo de pacote 1)

cliente → servidor.

ClientInfo (embutido em Query)

Cliente → Servidor, embutido no corpo de Query (campo 2). Condicionado por CLIENT_INFO (v54032). (Alguns campos de ClientInfo são condicionados por versões posteriores, conforme indicado em cada campo abaixo.)
Layout dependente da interface (campos 7–12)Os campos 7–12 acima correspondem à ramificação TCP. Quando query_interface (campo 6) não é TCP, esses campos são substituídos por um layout wire diferente — não se trata apenas de omissões opcionais; por isso, um decodificador deve ramificar com base no campo 6.
  • query_interface = 2 (HTTP): em vez disso, são gravadas as informações da requisição HTTP encaminhada pelo servidor — http_method (UInt8), http_user_agent (String), depois forwarded_for (String, condicionado a X_FORWARDED_FOR_IN_CLIENT_INFO v54443), http_referer (String, condicionado a REFERER_IN_CLIENT_INFO v54447) e, por fim, http_handler_name (String) e http_request_url (String) (ambos condicionados a HTTP_HANDLER_IN_CLIENT_INFO v54490, nessa ordem). Os campos os_user/client_hostname/client_name/version_*/protocol_version não estão presentes. Os dois últimos contêm o nome do HTTP handler definido em SQL correspondente e a URL da requisição, para que cheguem preservados aos shards remotos (ficam vazios em uma consulta HTTP sem handler).
  • Qualquer outra interface: nenhum dos campos TCP (7–12) nem dos campos HTTP é gravado; o stream continua diretamente com quota_key.
Após essa ramificação, o layout volta a ser comum: quota_key (campo 13) e distributed_depth (campo 14) vêm em seguida para todas as interfaces e, depois, version_patch (campo 15) é gravado apenas para TCP.Essa ramificação é relevante principalmente no tráfego entre servidores, quando o servidor iniciador encaminha uma consulta que chegou originalmente via HTTP. Um decodificador que sempre lê os campos TCP interpretará incorretamente esses pacotes, tratando http_method ou http_user_agent como quota_key.
Codificação OpenTelemetry (campo 16):

Autenticação entre servidores

O campo 4 do Query (auth_hash) não é o segredo compartilhado do cluster transmitido no wire. Enviar o segredo bruto faria a autenticação falhar e, além disso, o exporia. Em vez disso, um servidor que atua como cliente entre servidores prova que conhece o segredo por meio de um hash SHA-256 com salt:
  1. Entrar no modo entre servidores. O servidor que se conecta sinaliza isso dentro do ClientHello: o campo user contém o marcador de comunicação entre servidores e password fica vazio. Em seguida, ele anexa mais duas strings — o nome do cluster e um salt de 32 bytes recém-gerado (encodeSHA256 de um valor aleatório) — imediatamente após os campos user/password, como parte do mesmo pacote ClientHello. O servidor lê essas duas strings antes de enviar o ServerHello, portanto o cliente precisa gravá-las logo de início; esperar primeiro pelo ServerHello causa um deadlock, pois o servidor fica bloqueado aguardando a leitura delas.
  2. Obter o nonce. O ServerHello contém um nonce UInt64 de 8 bytes quando INTERSERVER_SECRET_V2 (v54462) é negociado.
  3. Calcular o hash. Para cada pacote Query que não seja InitialQuery, o cliente grava encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles) no campo 4 — um digest de 32 bytes. (nonce é usado em sua forma de string decimal e só está presente quando a versão negociada é ≥ v54462; external_roles só é anexado quando INTERSERVER_EXTERNALLY_GRANTED_ROLES (v54472) é negociado; current_roles é a lista serializada de nomes de funções — [VarUInt count][String]*count, sem o byte de presença — e só é anexada quando INTERSERVER_CURRENT_ROLES (v54488) é negociado e a lista está presente.) Para uma InitialQuery, ou quando não há segredo de cluster configurado, o cliente grava uma string vazia.
  4. Verificar. O servidor lê o campo 4 com um limite de 32 bytes e recalcula a mesma concatenação usando sua própria cópia do segredo do cluster; a conexão é rejeitada se os digests forem diferentes.
Clientes externos (que não fazem comunicação entre servidores) nunca entram nesse modo e sempre enviam um auth_hash vazio.

Setting

Codificado inline na lista de configurações do corpo da Query (o pacote Query, campo 3). A lista está sempre presente, independentemente da versão negociada, e é encerrada por um Setting com chave vazia — um único VarUInt 0, sem flags nem valor em seguida. Apenas a codificação de cada configuração depende da versão negociada, controlada por SETTINGS_SERIALIZED_AS_STRINGS (v54429). v54429+ (STRINGS_WITH_FLAGS) — cada configuração é a tripla mostrada a seguir: Os campos 2 e 3 são omitidos quando key está vazio. Pré-54429 (BINARY) — cada configuração é [String key][type-specific binary value]: o campo flags não é gravado, e o valor é codificado na forma binária nativa da configuração (por exemplo, um inteiro de largura fixa ou uma string prefixada pelo comprimento), e não como uma string decimal/de texto. A lista continua sendo encerrada por uma key vazia. Um cliente voltado a uma versão negociada inferior a 54429 deve ler e gravar essa forma binária, e não a tripla acima. (As configurações personalizadas definidas pelo usuário são a exceção: elas sempre incluem flags e um valor string, em ambas as codificações.) O campo flags contém:
  • 0x01 — Important: a configuração afeta os resultados da consulta e não deve ser ignorada silenciosamente por peers mais antigos.
  • 0x02 — Custom: uma configuração personalizada definida pelo usuário.
  • 0x0c — um campo de tier de 2 bits, e não uma flag independente: 0x00 = Production, 0x04 = Obsolete, 0x08 = Experimental, 0x0c = Beta. Leia os 2 bits juntos (flags & 0x0c) — um teste ingênuo flags & 0x04 classificaria Beta (0x0c) erroneamente como Obsolete.
  • 0x80 — HotReload (recarregamento da configuração sem reinicialização; definida no enum de flags, encontrada principalmente em configurações de coordenação).

Parâmetro

Parâmetros de consulta, usados em consultas parametrizadas como SELECT {x:UInt64}. São codificados de forma idêntica a uma Setting com o flag Custom (0x02) ativado e, da mesma forma, terminam com uma chave vazia.
O valor do parâmetro é a representação SQL do valor, e não um literal bruto. Parâmetros do tipo String devem ser passados já entre aspas simples (por exemplo, o valor para {name:String} é 'Alice', e não Alice); caso contrário, o parser de valores do servidor os rejeitará.

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

Usado em ambas as direções. Transporta blocos de resultado, dados de INSERT, tabelas externas e marcadores de fim de dados. O formato wire é simétrico — em ambas as direções há um prefixo table_name antes do bloco. Somente o byte do tipo de pacote muda.
O marcador de fim de dados é um pacote cujo Block está vazio — 0 colunas e 0 linhas —, independentemente do valor de table_name. O servidor só trata um pacote Data do cliente como terminador quando o bloco decodificado está vazio (block.empty()); um pacote com table_name = "" e um bloco não vazio é um pacote de linhas comum, e não um terminador. Portanto, um stream de linhas do INSERT é uma sequência de blocos Data não vazios, seguida de um único bloco Data vazio que o encerra. As variantes de bloco e seus significados estão documentados em Variantes de bloco.

Progress (tipo de pacote 3)

servidor → cliente. Enviado periodicamente durante a execução da consulta. Todos os campos são VarUInt, e cada pacote transporta incrementos desde o pacote Progress anterior, e não totais acumulados. Antes do envio, o servidor lê seus contadores e os redefine para zero de forma atômica, além de calcular elapsed_ns como a diferença de tempo desde o último envio. Portanto, um cliente deve acumular localmente os pacotes sucessivos para obter os totais parciais — tratar um pacote como valor absoluto faz com que a exibição do progresso retroceda ou apresente valores abaixo do real assim que chega mais de um pacote.

ProfileInfo (tipo de pacote 6)

Servidor → Cliente. Enviado uma vez por consulta, próximo ao fim da execução.

Totals (tipo de pacote 7)

Servidor → Cliente. Enviado para consultas com WITH TOTALS. O formato wire é idêntico ao de Data: uma string table_name (sempre vazia) seguida de um bloco. A única diferença é o byte do tipo de pacote.

Extremes (tipo de pacote 8)

Servidor → Cliente. Enviado quando a configuração extremes está habilitada. O formato wire é idêntico ao de Data. O bloco tem exatamente 2 linhas: a linha 0 contém o valor mínimo de cada coluna e a linha 1, o valor máximo.

Log (tipo de pacote 10)

Servidor → Cliente. Enviado quando a consulta possui uma fila de logs ativa (configuração send_logs_level; consulte streaming de logs). Mesmo envelope e formato de corpo de Data. O bloco tem um valor fixo de num_columns = 8 e um schema predefinido. Cada linha de log corresponde a uma linha que abrange as 8 colunas, e um único pacote Log pode transportar várias linhas.
As 8 colunas, exatamente nesta ordem:

ProfileEvents (tipo de pacote 14)

Servidor → Cliente. Transporta contadores de desempenho por consulta. Usa o mesmo envelope e formato de corpo de Data. O bloco tem num_columns = 6 fixo e um schema predefinido. Cada evento corresponde a uma linha.
As 6 colunas:
O tipo de elemento da coluna value não é o mesmo em todos os pacotes — servidores mais antigos emitem UInt64, e os mais recentes, Int64. Leia a string do tipo da coluna no cabeçalho do bloco, em vez de presumir uma largura fixa.

TableColumns (tipo de pacote 11)

Servidor → Cliente, condicionado por COLUMN_DEFAULTS_METADATA (v54410). O servidor o envia antes do bloco de schema do INSERT para transportar metadados de valores padrão das colunas, mas somente quando a versão negociada é ≥ 54410 e a configuração input_format_defaults_for_omitted_fields está habilitada. Abaixo de 54410, o pacote nunca é enviado; portanto, um cliente mais antigo não deve aguardá-lo — o bloco Data do schema chega diretamente. Um cliente v54410+ deve estar preparado para qualquer uma das ordens: um TableColumns opcional seguido do bloco de schema.
Corpo comprimido a partir da v54481Com a versão negociada ≥ 54481 (COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS), o servidor grava ambos os campos pelo mesmo caminho de saída opcionalmente comprimido. Assim, quando a consulta tem compression = true, todo o corpo do TableColumns (external_table + columns_description) fica dentro do frame de compressão, e o cliente o lê pelo stream descomprimido correspondente. Quando a consulta não usa compressão, o corpo trafega na rede não comprimido, exatamente como mostra a tabela acima. Isso é importante para as respostas de schema do INSERT: um cliente que ajusta o tratamento de compressão para Log e ProfileEvents, mas não para TableColumns, interpretará a resposta incorretamente quando a compressão da consulta estiver habilitada.

TimezoneUpdate (tipo de pacote 17)

Servidor → Cliente, condicionado por TIMEZONE_UPDATES (v54464). É enviado em um único ponto: o inicializador da função de tabela input (uma consulta no formato INSERT INTO <table> SELECT ... FROM input('<structure>'), que transmite linhas a partir do cliente). Logo após o servidor enviar o bloco Data com o schema de entrada (consulte a fase de INSERT), ele emite TimezoneUpdate com o session_timezone atual do contexto da consulta, para que o cliente interprete as linhas que está prestes a enviar usando o mesmo fuso horário. O servidor não emite este pacote para alterações arbitrárias de SET session_timezone no meio da consulta, nem para informar ao cliente como formatar blocos de resultado posteriores. O pacote chega uma única vez, imediatamente após o bloco com o schema de entrada e antes de o cliente começar a enviar os blocos de linhas. Um decodificador que ignore TimezoneUpdate DEVE, ainda assim, consumir a String final para manter o wire alinhado.

Autenticação SSH por desafio-resposta (tipos de pacote 11, 12, 18)

Controlada por SSH_AUTHENTICATION (v54466) e disponível apenas mediante adesão explícita. Uma conexão entra no fluxo SSH quando o ClientHello envia user = " SSH KEY AUTHENTICATION " + <real_user> (com os espaços inicial e final) e password = "". O servidor lê o prefixo, remove-o para recuperar o usuário real e passa para o modo de desafio-resposta. O fluxo substitui a autenticação por senha, e a troca de desafio-resposta ocorre antes do ServerHello — o servidor só envia sua resposta Hello depois que a autenticação é concluída com sucesso:
  1. O cliente envia o ClientHello com o prefixo marcador SSH e uma senha vazia.
  2. O cliente envia SSHChallengeRequest (pacote 11). O servidor ainda não enviou o ServerHello — ele processa a autenticação primeiro e fica bloqueado neste ponto, aguardando esse pacote.
  3. O servidor responde com SSHChallenge contendo bytes aleatórios (pacote 18).
  4. O cliente monta a string a ser assinada e assina essa string, não o desafio bruto; em seguida, envia SSHChallengeResponse (pacote 12) com a assinatura. A mensagem assinada é a concatenação byte a byte, sem separadores, de quatro partes exatamente nesta ordem:
  5. O servidor verifica a assinatura com a chave pública registrada do usuário, reconstruindo a mesma string decimal(protocol_version) + default_database + user + challenge. Em caso de sucesso, envia o ServerHello — a mesma resposta do fluxo por senha — e o handshake prossegue normalmente (Addendum etc.); em caso de falha, retorna uma Exception e encerra a conexão. Um cliente que assine apenas os bytes brutos do desafio terá a autenticação recusada.
Este é o inverso do handshake com senha, em que o ServerHello vem imediatamente após o ClientHello. Com autenticação SSH, o ServerHello é retido até que a assinatura seja verificada, de modo que o challenge-response SSH é intercalado no handshake antes que qualquer ServerHello seja recebido.
Clientes externos que não usam autenticação SSH nunca veem os pacotes 11, 12 ou 18 — eles não trafegam no wire, a menos que o usuário opte explicitamente por esse modo por meio do prefixo no nome de usuário.

MergeTreeAllRangesAnnouncementResponse (tipo de pacote 14)

Client → Server, somente entre servidores. Condicionado a parallel_replicas_protocol_version ≥ 8 (consulte VERSIONED_PARALLEL_REPLICAS_PROTOCOL). Clientes externos nunca enviam este pacote. Quando a versão de parallel-replicas negociada é ≥ 8, o ciclo de requisição/resposta do iniciador para o MergeTreeAllRangesAnnouncement de um seguidor (tipo de pacote 15, direção servidor→cliente) passa a funcionar assim:
  1. Um seguidor abre seu pipeline de leitura e envia MergeTreeAllRangesAnnouncement ao iniciador.
  2. Somente quando o mode do anúncio é diferente de Default (WithOrder = 1 ou ReverseOrder = 2, ambos usados em leituras paralelas ordenadas), o iniciador responde com MergeTreeAllRangesAnnouncementResponse. Com mode = Default = 0, o iniciador não responde e o seguidor não aguarda — o modo Default distribui intervalos a cada MergeTreeReadTaskRequest e nunca precisa da lista de partes de antemão.
  3. O seguidor fica bloqueado aguardando a resposta (quando esperada) antes de emitir seu primeiro MergeTreeReadTaskRequest (pacote de servidor 16 — enviado do seguidor para o iniciador; o iniciador responde com MergeTreeReadTaskResponse, pacote de cliente 10) e usa a lista de partes retornada para restringir a construção das fontes exatamente às partes que pertencem ao seu stream #split_i.
Em versões anteriores à 8, o anúncio é do tipo fire-and-forget independentemente do modo, e o seguidor constrói fontes sobre todas as partes conhecidas localmente (o comportamento legado).

Corpo

Corpo de RangesInDataPartsDescription

Corpo de RangesInDataPartDescription

Corpo de MergeTreePartInfo

Corpo de MarkRanges

Referência de tipos de pacote

Client → Server

Servidor → Cliente

QueryPlan

QueryPlan é um pacote de cliente usado exclusivamente na comunicação entre servidores. Ele é enviado após uma Query cujo stage é QueryPlan (7) e contém um único stream de plano serializado. Antes da versão 10, os campos 2 e 3 não existem. Um iniciador com qualquer um dos limites de execução diferente do padrão não deve enviar esse layout antigo: em vez disso, ele envia a consulta SQL original, para que o receptor mais antigo reconstrua os limites a partir das configurações da consulta.

Configuração

Esta seção aborda os parâmetros ajustáveis que determinam o comportamento das conexões do protocolo nativo: Os valores padrão abaixo refletem um lançamento recente do servidor e podem variar entre versões e implantações.

Configurações da camada de transporte

Opções de socket

Tempos limite

Os tempos limite se aninham da seguinte forma:
O keepalive do SO é acionado primeiro e pode detectar peers inativos silenciosamente no nível do kernel. O tempo limite de recebimento da aplicação é a próxima linha de defesa. O tempo limite de idle é o último recurso, que encerra conexões sem uso há muito tempo.

Limites de conexão

Uma conexão que envia consultas regularmente pode permanecer aberta indefinidamente. Apenas conexões idle são encerradas após uma hora, e, por padrão, não há limite máximo para o ciclo de vida.

Configurações da camada de aplicação

Essas configurações são enviadas por consulta na lista de configurações do pacote Query. Elas alteram o que o servidor envia no wire ou a forma como os dados são enquadrados.

Compressão

A flag compression no pacote Query (campo 6) ativa e desativa a compressão; estas configurações definem qual codec é usado quando ela está ativada.

Streaming de logs

Definir send_logs_level com qualquer valor diferente de "none" faz com que o servidor envie pacotes Log durante a execução da consulta.

Relatório de progresso

Trata-se de um valor mínimo desejado, não de um máximo rígido: o servidor pode enviar pacotes Progress com menos frequência quando a consulta não estiver gerando trabalho com rapidez suficiente.

Envelope de resultado

INSERT assíncrono

Rastreamento distribuído

Configurações fora do escopo

Estas configurações às vezes são confundidas com configurações de nível de protocolo, mas controlam a execução de SQL, o armazenamento ou o uso de CPU, e não o comportamento no wire. Uma implementação do protocolo não precisa tratá-las de forma especial.
  • max_threads — paralelismo na execução de consultas.
  • max_memory_usage — limite de memória por consulta.
  • max_block_size, preferred_block_size_bytes — dimensionamento interno de blocos durante o processamento de consultas; os blocos wire independem dessas configurações.
  • compile_expressions — compilação JIT; afeta apenas a CPU.
  • async_insert_max_data_size — buffer de fila do lado do servidor.
  • Todas as configurações input_format_* e output_format_*, exceto a família input_format_native_* / output_format_native_* — as que não são native selecionam ou ajustam outros formatos (por exemplo, via HTTP) e não alteram os blocos Data do protocolo nativo.
As configurações *_native_* são a exceção: elas alteram os bytes dentro dos blocos Data do TCP nativo, portanto uma implementação do protocolo precisa levá-las em conta. output_format_native_encode_types_in_binary_format muda o campo type da coluna de uma string textual para uma codificação binária de tipos (exceto em uma consulta secundária; consulte layout wire de colunas), output_format_native_write_json_as_string emite colunas JSON como String e output_format_native_use_flattened_dynamic_and_json_serialization seleciona o layout FLATTENED de Dynamic/JSON. Como afetam o corpo do bloco, e não o envelope do pacote, essas configurações estão definidas na especificação do Native Format — consulte layout wire de colunas e tipos versionados.

Glossário

Cancel — um pacote iniciado pelo cliente (tipo 3) que interrompe uma consulta em execução. Não é detalhado nesta página. Marcador de fim dos dados do cliente — um pacote Data vazio (0 colunas, 0 linhas) que o cliente envia para encerrar um stream de entrada. Sua posição varia conforme o tipo de consulta:
  • Consulta normal (SELECT etc.): enviado após o pacote Query e eventuais pacotes Data de tabelas externas, para sinalizar que “não há mais dados externos”. Em seguida, o servidor inicia a execução.
  • INSERT: o cliente não envia um marcador antes do schema. O servidor envia primeiro o bloco de schema, o cliente transmite seus blocos Data de linhas e só então envia o pacote Data vazio para encerrar o stream de linhas. Um marcador vazio enviado antes do bloco de schema seria interpretado como fim imediato das linhas, e os dados seriam perdidos.
Feature — uma alteração no formato wire introduzida em uma versão específica do protocolo. Fica ativa quando a versão negociada é igual ou superior à versão da feature. Consulte versionamento e feature gates. Inter-server — um rótulo de função para um campo que só tem significado em consultas distribuídas entre servidores. Clientes externos gravam um valor default (geralmente string vazia, 0 ou false). Versão negociada — min(client_version, server_version), calculada durante o handshake. Determina quais features ficam ativas enquanto a conexão existir. Pacote — uma mensagem wire: um código de tipo de pacote VarUInt seguido de um corpo cujo formato depende do tipo. Consulte envelope do pacote. Código de tipo de pacote — o VarUInt inicial de um pacote, que identifica seu formato. Atualmente, os valores de 0 a 18 estão atribuídos. Consulte a referência de tipos de pacote. Stream de resposta — a sequência de pacotes que o servidor emite durante uma consulta. Não tem comprimento predefinido e é encerrado por exatamente um EndOfStream (sucesso) ou Exception (falha). Consulte a fase de consulta. Bloco de schema — o header block (um bloco com colunas, mas 0 linhas) que o servidor envia durante a fase de INSERT para anunciar o formato esperado das colunas antes que o cliente envie os dados. Lista de settings — uma sequência de tuplas (key, flags, value) no corpo do Query, encerrada por uma chave vazia. Transporta configurações da camada de aplicação específicas de cada consulta. Consulte Setting. Stage — um campo VarUInt no pacote Query (campo 5) que controla até que etapa o servidor executa a consulta. Clientes externos normalmente enviam 2 (Complete); consultas distribuídas e planos de consulta serializados usam valores mais altos. Consulte o campo 5 de Query para ver o conjunto completo de valores wire. Terminador — um pacote que encerra um stream. A resposta ao Query termina com EndOfStream (sucesso) ou Exception (falha). O stream de entrada do cliente termina com o marcador Data vazio.