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.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 mensagemClientHello. 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 umauth_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: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.Tabela de funcionalidades
Envelope do pacote
Toda mensagem transmitida no wire compartilha a mesma estrutura externa, em ambas as direções: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 funcionalidadeCHUNKED_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:
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/notchunkedsão estritos: o lado em questão exige exatamente esse modo.- As variantes
_optionalsão flexíveis: aceitam o modo que o outro lado escolher.
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:-
O cliente envia
ClientHellocom a versão máxima de protocolo que suporta. -
O cliente lê a resposta e faz o encaminhamento de acordo com o tipo de pacote:
-
Se
negotiated_version ≥ 54458(o recursoADDENDUM), o cliente envia umAddendum. Essa decisão se baseia na versão negociada, e não na versão declarada pelo cliente.
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 deREADY, o fluxo é o seguinte:
-
O cliente envia
Ping. -
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 umEndOfStream 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.
-
O cliente envia
Querycom umquery_idúnico (normalmente um UUID). -
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 pacotesDatacom o mesmotable_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 comINCORRECT_DATA(consulte Data). -
O cliente passa para
READING_RESPONSEe descarrega seu buffer de escrita. -
O cliente lê os pacotes de resposta em loop, fazendo o encaminhamento de acordo com o tipo:
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çãoINSERT; 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:
- O cliente envia
Querycombodydefinido como o SQL de INSERT. - 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
QuerydoINSERTé 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. - 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.
- O cliente envia o(s) bloco(s) de dados. Para cada bloco, ele grava
VarUInt(ClientPacket::Data = 2), depoisString("")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. - O cliente envia o terminador de fim de entrada: um pacote Data com um Block vazio (0 colunas, 0 linhas).
- O cliente consome o stream de resposta até
EndOfStream(sucesso) ouException(falha).
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 colunaType utiliza:
VarUInt— inteiro sem sinal de comprimento variável (consulte VarUInt).String— bytes prefixados por um VarUInt (consulte String).UInt8,Int32etc. — inteiros little-endian de largura fixa.Bool— um único byte,0x00ou0x01.
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.
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 aADDENDUM (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 byte0x04 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 byte0x04 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 porCLIENT_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), depoisforwarded_for(String, condicionado aX_FORWARDED_FOR_IN_CLIENT_INFOv54443),http_referer(String, condicionado aREFERER_IN_CLIENT_INFOv54447) e, por fim,http_handler_name(String) ehttp_request_url(String) (ambos condicionados aHTTP_HANDLER_IN_CLIENT_INFOv54490, nessa ordem). Os camposos_user/client_hostname/client_name/version_*/protocol_versionnã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.
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.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:
- Entrar no modo entre servidores. O servidor que se conecta sinaliza isso dentro do
ClientHello: o campousercontém o marcador de comunicação entre servidores epasswordfica vazio. Em seguida, ele anexa mais duas strings — o nome do cluster e umsaltde 32 bytes recém-gerado (encodeSHA256de um valor aleatório) — imediatamente após os camposuser/password, como parte do mesmo pacoteClientHello. O servidor lê essas duas strings antes de enviar oServerHello, portanto o cliente precisa gravá-las logo de início; esperar primeiro peloServerHellocausa um deadlock, pois o servidor fica bloqueado aguardando a leitura delas. - Obter o nonce. O
ServerHellocontém um nonceUInt64de 8 bytes quandoINTERSERVER_SECRET_V2(v54462) é negociado. - Calcular o hash. Para cada pacote Query que não seja
InitialQuery, o cliente gravaencodeSHA256(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_rolessó é anexado quandoINTERSERVER_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 quandoINTERSERVER_CURRENT_ROLES(v54488) é negociado e a lista está presente.) Para umaInitialQuery, ou quando não há segredo de cluster configurado, o cliente grava uma string vazia. - 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.
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 únicoVarUInt 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ênuoflags & 0x04classificaria 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 comoSELECT {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 prefixotable_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 pacoteProgress 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 comWITH 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çãoextremes 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çãosend_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.
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 temnum_columns = 6 fixo e um schema predefinido. Cada evento corresponde a uma linha.
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 porCOLUMN_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 porTIMEZONE_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 porSSH_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:
- O cliente envia o ClientHello com o prefixo marcador SSH e uma senha vazia.
-
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. -
O servidor responde com
SSHChallengecontendo bytes aleatórios (pacote 18). -
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: -
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 oServerHello— a mesma resposta do fluxo por senha — e o handshake prossegue normalmente (Addendum etc.); em caso de falha, retorna umaExceptione 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.
MergeTreeAllRangesAnnouncementResponse (tipo de pacote 14)
Client → Server, somente entre servidores. Condicionado aparallel_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:
- Um seguidor abre seu pipeline de leitura e envia
MergeTreeAllRangesAnnouncementao iniciador. - Somente quando o
modedo anúncio é diferente deDefault(WithOrder = 1ouReverseOrder = 2, ambos usados em leituras paralelas ordenadas), o iniciador responde comMergeTreeAllRangesAnnouncementResponse. Commode = Default = 0, o iniciador não responde e o seguidor não aguarda — o modoDefaultdistribui intervalos a cadaMergeTreeReadTaskRequeste nunca precisa da lista de partes de antemão. - O seguidor fica bloqueado aguardando a resposta (quando esperada) antes de emitir seu primeiro
MergeTreeReadTaskRequest(pacote de servidor16— enviado do seguidor para o iniciador; o iniciador responde comMergeTreeReadTaskResponse, pacote de cliente10) e usa a lista de partes retornada para restringir a construção das fontes exatamente às partes que pertencem ao seu stream#split_i.
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:- Configurações da camada de transporte — opções de socket TCP e tempos limite, que afetam o comportamento da própria conexão TCP.
- Configurações da camada de aplicação — parâmetros ajustáveis por consulta, transmitidos na lista de configurações do pacote Query, que afetam o que o servidor envia no wire ou como esses dados são enquadrados.
- Configurações fora do escopo — configurações muitas vezes confundidas com configurações de protocolo, mas que, na verdade, controlam a execução de SQL ou o armazenamento.
Configurações da camada de transporte
Opções de socket
Tempos limite
Os tempos limite se aninham da seguinte forma:
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_*eoutput_format_*, exceto a famíliainput_format_native_*/output_format_native_*— as que não sãonativeselecionam ou ajustam outros formatos (por exemplo, via HTTP) e não alteram os blocosDatado protocolo nativo.
*_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 (
SELECTetc.): 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.
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.