> ## Documentation Index
> Fetch the complete documentation index at: https://clickhouse.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ClickHouse 네이티브 TCP 프로토콜 사양: 패킷 프레이밍, 연결 수명 주기, 버전 협상, 각 메시지의 body

# Native Protocol

native protocol은 ClickHouse 클라이언트와 서버가 TCP 상에서 통신할 때 사용하는 바이너리 기반의 연결 지향 프로토콜입니다. 이 프로토콜은 SQL 쿼리, 결과 데이터, `INSERT` payload, 실행 텔레메트리, 오류 신호를 전달합니다. command-line client와 C++ 드라이버를 비롯해 대부분의 타사 네이티브 드라이버가 이 프로토콜을 기반으로 동작합니다.

이 페이지에서는 패킷 프레이밍, 연결 상태 머신(State Machine), 버전 협상, 그리고 `Block`을 제외한 모든 메시지의 body 등 프로토콜 자체를 다룹니다. `Data` 계열 패킷 내부의 바이트(`Block`, 그 컬럼, 유형별 인코딩)는 별도로 다루며, [Native 형식](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md) 사양에 설명되어 있습니다.

> [!NOTE]
>
> **관련 사양**
>
> 이 페이지는 짝을 이루는 두 문서 중 하나로, 관련 사양인 [Native 형식](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md)과 함께 게시됩니다. 두 사양은 역할이 명확히 나뉩니다. 이 페이지는 패킷 및 전송 계층을 다루고, Native 형식 사양은 `Data` 계열 패킷 내부의 바이트를 다룹니다.

다음 특성은 프로토콜 전반에 걸쳐 적용됩니다. 이 프로토콜은 바이너리 기반이며 위치 기반(positional)입니다. `BlockInfo` 내부를 제외하면 필드 태그가 없으므로, 바이트 하나만 어긋나도 그 뒤의 모든 데이터가 동기화를 잃습니다. 또한 상태 유지형 프로토콜이며, 각 TCP 연결은 한 번에 하나의 쿼리만 처리합니다. 즉, 다중화는 지원하지 않습니다. 고정 폭 정수는 리틀 엔디언으로 인코딩됩니다.

## 개요

| 속성 | 값 |
| - | - |
| 전송 방식 | TCP(선택적으로 TLS로 래핑 가능) |
| 바이트 순서 | 고정 폭 정수는 리틀 엔디언 |
| 인코딩 | 바이너리 및 위치 기반(`BlockInfo`를 제외하면 필드 태그 없음) |
| 연결 모델 | 상태 유지형, 한 번에 하나의 쿼리만 처리, 다중화(multiplexing) 없음 |
| 버전 관리 | 핸드셰이크 시 협상되며, 개별 기능의 사용 여부는 버전에 따라 결정됨 |
| 데이터 포맷 | 모든 테이블 형식 데이터에 [Native 형식](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md) 사용 |

wire 상의 모든 메시지는 `VarUInt` 패킷 유형 코드로 시작하며, 그 뒤에는 해당 코드와 협상된 프로토콜 버전에 따라 구조가 달라지는 body가 이어집니다.

연결은 세 단계로 진행됩니다. 먼저 핸드셰이크를 한 번 수행하고, 이어서 `Ping` 또는 `Query` 교환을 원하는 횟수만큼 진행한 뒤, 마지막으로 연결을 종료합니다:

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: TCP connect

    rect rgb(220, 235, 255)
        Note over C,S: Handshake
        C->>S: ClientHello (name, version, db, user, password)
        S->>C: ServerHello (server_name, version, [timezone, display_name, ...])
        Note over C,S: negotiated_version = min(client, server)
        opt negotiated_version ≥ 54458
            C->>S: Addendum (quota_key)
        end
    end

    rect rgb(220, 245, 225)
        Note over C,S: Query phase
        C->>S: Query packet (ClientInfo, settings, params, SQL)
        C->>S: External-table Data packets (0 or more)
        C->>S: Empty Data marker — the "go" signal
        loop until EndOfStream or Exception
            S->>C: Data / Progress / Log / ProfileInfo / Totals / ...
        end
        S->>C: EndOfStream
    end
```

네이티브 TCP 프로토콜은 SQL에 어떤 `FORMAT` 절이 지정되어 있든 관계없이 항상 표 형식의 데이터를 Native 형식으로 전달합니다. `RowBinary`, `CSV`, `JSON` 등으로 다시 포맷팅하는 작업은 클라이언트의 몫이며, Native 블록을 디코딩한 후에 수행됩니다. (HTTP 인터페이스는 `FORMAT` 절을 *실제로* 반영하는 별도의 코드 경로입니다. HTTP는 이 문서에서 다루지 않습니다.)

## 보안

### 전송 보안(TLS)

TLS는 프로토콜 아래의 전송 계층(transport layer)에서 동작합니다. TLS를 활성화하면 TCP 스트림 전체가 암호화되며, 프로토콜 메시지는 TLS 사용 여부와 관계없이 바이트 단위까지 완전히 동일합니다.

### 인증

인증은 핸드셰이크 과정에서 [`ClientHello`](#clienthello) 메시지를 통해 이루어집니다. `user` 및 `password` 필드는 평문 문자열로 전송되므로, 전송 중인 자격 증명은 전송 계층 암호화(TLS)로 보호됩니다.

서버는 비어 있는 `user` 필드를 구성된 기본 세션 사용자로 해석합니다. 기본 세션 사용자는 `default_session_user` 서버 설정(기본값은 `default`)으로 지정되며, `protocols` 섹션에서 리스너별로 재정의할 수 있습니다. 서버에 기본 세션 사용자가 빈 값으로 구성되어 있거나 서버 버전이 26.8 미만이면, 비어 있는 `user` 필드는 예외와 함께 거부됩니다. 참고로 `clickhouse-client`는 빈 사용자 이름을 전송하지 않으며, 클라이언트 측에서 `default`로 대체합니다.

SSH 챌린지-응답(challenge-response) 인증은 프로토콜 버전 54466부터 사용할 수 있습니다. 자세한 내용은 [SSH 챌린지-응답 인증](#ssh-authentication)을 참조하십시오.

### 서버 간 시크릿

분산 쿼리 실행(distributed query execution) 시 서버들은 공유 시크릿을 wire 상으로 전송하지 않고, 해당 시크릿을 알고 있음을 증명하는 방식으로 서로를 인증합니다. 각 Query는 [`Query`](#query) 필드 4에 32바이트 SHA-256 `auth_hash`를 담고 있습니다. 이 값은 salt, nonce, 구성된 시크릿, 쿼리를 바탕으로 계산되며, 수신 서버는 이 값을 직접 다시 계산하여 비교합니다. 이 동작은 `INTERSERVER_SECRET` 기능(v54441)이 지원되는 경우에만 사용됩니다. 외부 클라이언트는 이 필드에 항상 빈 문자열을 전송합니다. 자세한 내용은 [서버 간 인증](#inter-server-authentication)을 참고하십시오.

## 버전 관리 및 기능 게이트

### 버전 협상(Version negotiation)

클라이언트와 서버는 핸드셰이크 과정에서 각자 지원하는 최대 프로토콜 버전을 알립니다. \*\*협상된 버전(negotiated version)\*\*은 두 버전 중 더 낮은 버전으로 결정됩니다.

```text theme={null}
negotiated_version = min(client_version, server_version)
```

이후의 모든 메시지는 협상된 버전(negotiated version)에 따라 wire 상에 어떤 필드가 포함되는지 결정합니다.

### 기능 게이트(Feature gates)

각 기능은 해당 기능이 도입된 프로토콜 버전으로 식별되며, 협상된 버전(negotiated version)이 그 버전 번호 이상이면 **활성화**됩니다.

> [!WARNING]
>
> 기능이 활성화된 경우 해당 기능의 필드는 wire 상에 **반드시** 포함되어야 합니다. 프로토콜은 엄격하게 위치 기반(positional)으로 해석되므로, 기능 게이트가 적용된 필드를 생략하면 그 뒤에 오는 모든 필드의 바이트 스트림이 손상됩니다.

### 기능 표

| 기능 | 버전 | 영향 범위 | wire 영향 |
| - | - | - | - |
| BLOCK\_INFO | all | Block | 모든 Block에 BlockInfo 접두부(`is_overflows`, `bucket_number`)를 추가합니다. |
| CLIENT\_INFO | 54032 | Query | Query body에 ClientInfo 블록을 추가합니다. |
| TIMEZONE | 54058 | ServerHello | ServerHello에 `timezone` 필드를 추가합니다. |
| QUOTA\_KEY\_IN\_CLIENT\_INFO | 54060 | ClientInfo | ClientInfo에 `quota_key` 필드를 추가합니다. |
| DISPLAY\_NAME | 54372 | ServerHello | ServerHello에 `display_name` 필드를 추가합니다. |
| VERSION\_PATCH | 54401 | ServerHello, ClientInfo | 두 곳 모두에 `version_patch` 필드를 추가합니다. |
| SERVER\_LOGS | 54406 | Log | `send_logs_level`이 설정되어 있으면 서버가 Log 패킷을 전송합니다. |
| COLUMN\_DEFAULTS\_METADATA | 54410 | TableColumns | 서버는 INSERT/input 스키마 블록보다 먼저 컬럼 기본값 메타데이터가 담긴 [`TableColumns`](#tablecolumns) 패킷(type 11)을 전송할 수 있습니다. 협상된 버전이 54410 이상이고 **동시에** `input_format_defaults_for_omitted_fields`가 활성화된 경우에만 전송됩니다. 이 버전 미만에서는 해당 패킷이 전송되지 않으므로 클라이언트는 이 패킷을 기다리면 안 됩니다. |
| WRITE\_CLIENT\_INFO | 54420 | Progress | Progress에 `wrote_rows`와 `wrote_bytes`를 추가합니다. (이름과 달리 ClientInfo 블록을 제어하지 **않습니다**. ClientInfo 블록은 `CLIENT_INFO`(v54032)가 제어합니다.) |
| SETTINGS\_SERIALIZED\_AS\_STRINGS | 54429 | Query (설정 인코딩) | 항상 전송되는 설정 목록의 인코딩 **방식**을 변경하며, 설정 전송 여부를 제어하지는 **않습니다**. v54429 이상은 각 설정을 `(name, flags, value-as-string)` 형태로 기록하고, 이전 버전 피어는 플래그 없이 `(name, type-specific-binary-value)` 형태로 기록합니다. [Setting](#setting)을 참조하십시오. |
| INTERSERVER\_SECRET | 54441 | Query | Query에 서버 간 `auth_hash` 필드를 추가합니다. 이 값은 원본 시크릿이 아니라 클러스터 시크릿에 솔트를 적용한 SHA-256 해시입니다. 외부 클라이언트는 빈 문자열을 전송합니다. [서버 간 인증](#inter-server-authentication)을 참조하십시오. |
| OPEN\_TELEMETRY | 54442 | ClientInfo | ClientInfo에 OpenTelemetry 트레이스 컨텍스트를 추가합니다. |
| DISTRIBUTED\_DEPTH | 54448 | ClientInfo | ClientInfo에 `distributed_depth` 필드를 추가합니다. |
| INITIAL\_QUERY\_START\_TIME | 54449 | ClientInfo | `initial_time` 필드(Int64, 고정 길이)를 추가합니다. |
| PROFILE\_EVENTS | 54451 | ProfileEvents | 쿼리 실행 중 서버가 ProfileEvents 패킷을 전송합니다. |
| PARALLEL\_REPLICAS | 54453 | ClientInfo | ClientInfo에 병렬 레플리카 조정용 필드를 추가합니다. |
| CUSTOM\_SERIALIZATION | 54454 | Block (Column) | 각 컬럼의 type 문자열 뒤에 `has_custom_serialization` 바이트를 추가합니다. |
| ADDENDUM | 54458 | Handshake | 핸드셰이크 교환이 끝난 후 클라이언트가 addendum(`quota_key`)을 전송합니다. |
| PARAMETERS | 54459 | Query | Query body에 파라미터 목록을 추가합니다. |
| SERVER\_QUERY\_TIME\_IN\_PROGRESS | 54460 | Progress | Progress에 `elapsed_ns` 필드를 추가합니다. |
| PASSWORD\_COMPLEXITY\_RULES | 54461 | ServerHello | ServerHello에 비밀번호 정책 정규식 패턴과 사람이 읽을 수 있는 설명 메시지 목록을 추가합니다. |
| INTERSERVER\_SECRET\_V2 | 54462 | ServerHello | ServerHello에 8바이트 `UInt64` nonce를 추가합니다. 서버 간 쿼리 서명에 사용되며, 외부 클라이언트는 디코딩만 하고 무시합니다. |
| TOTAL\_BYTES\_IN\_PROGRESS | 54463 | Progress | Progress의 `total_rows`와 `wrote_rows` 사이에 `total_bytes_to_read`(VarUInt) 필드를 추가합니다. |
| TIMEZONE\_UPDATES | 54464 | TimezoneUpdate | `TimezoneUpdate` 서버 패킷(type 17)을 추가합니다. body는 세션 시간대를 담은 단일 `String`입니다. `input` 테이블 함수 이니셜라이저만 input 스키마 블록 직후에 이 패킷을 전송하며, 이를 통해 클라이언트는 자신이 전송하는 행을 서버의 `session_timezone` 기준으로 파싱합니다. [TimezoneUpdate](#timezoneupdate)를 참조하십시오. |
| SPARSE\_SERIALIZATION | 54465 | Block (Column) | 서버는 `has_custom_serialization = 1`로 설정하고 희소 인코딩된 컬럼을 전송할 수 있습니다. wire 포맷은 1바이트 kind(0x01 = SPARSE), EOG로 끝나는 VarUInt 오프셋 스트림, 내부 타입으로 밀집 인코딩된 기본값이 아닌 값들의 순서로 구성됩니다. [kind\_stack 및 희소 인코딩](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding)을 참조하십시오. |
| SSH\_AUTHENTICATION | 54466 | 인증 흐름 | SSH 챌린지-응답 인증을 추가합니다. 옵트인 방식으로, 클라이언트가 `" SSH KEY AUTHENTICATION " + <real_user>` 형태의 `user`와 빈 비밀번호를 전송하면 활성화됩니다. [SSH 챌린지-응답 인증](#ssh-authentication)을 참조하십시오. |
| TABLE\_READ\_ONLY\_CHECK | 54467 | TablesStatusResponse | TablesStatusResponse의 각 테이블 행에 `is_readonly` 플래그를 추가합니다. `TablesStatusRequest`를 보내지 않는 외부 클라이언트에는 wire 변경이 없습니다. |
| SYSTEM\_KEYWORDS\_TABLE | 54468 | 시스템 테이블 | 표준 `clickhouse-client`가 키워드를 자동 완성할 수 있도록 서버가 `system.keywords`를 채웁니다. 네이티브 프로토콜의 wire 변경은 없습니다. |
| ROWS\_BEFORE\_AGGREGATION | 54469 | ProfileInfo | ProfileInfo 끝에 `applied_aggregation`(Bool)과 `rows_before_aggregation`(VarUInt)을 이 순서대로 추가합니다. |
| CHUNKED\_PROTOCOL | 54470 | 연결 프레이밍 | 패킷 단위 청크 프레이밍으로 모든 패킷 body를 감쌉니다. Addendum에서 협상됩니다. ServerHello는 방향별 서버 선호 설정을 전달하고, Addendum은 클라이언트의 최종 선택을 전달합니다. [청크 프레이밍](#chunked-framing)을 참조하십시오. |
| VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL | 54471 | ServerHello, Addendum | 양쪽 모두 `VarUInt` 형식의 병렬 레플리카 조정 프로토콜 버전을 교환합니다. ServerHello의 필드는 **`protocol_version` 바로 뒤**(`timezone` 앞)에 위치합니다. Addendum의 필드는 chunked 프로토콜 문자열 뒤에 추가됩니다. 현재 값: `8` (`DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION`). 버전 `8`에서는 [`MergeTreeAllRangesAnnouncementResponse`](#mergetreeallrangesannouncementresponse)(클라이언트 패킷 `14`)가 추가됩니다. 협상된 병렬 레플리카 버전이 `≥ 8`이면 initiator는 `Default` 모드가 아닌 모든 follower 공지에 대해 해당 스트림의 권위 있는 파트 목록으로 응답하며, follower는 이 응답을 받은 후에 읽기 요청을 보냅니다. `8` 미만에서는 공지를 보낸 뒤 응답을 기다리지 않습니다(fire-and-forget). |
| INTERSERVER\_EXTERNALLY\_GRANTED\_ROLES | 54472 | Query | Query 본문의 settings 종료자와 interserver-secret 해시 사이에 `String external_roles` 필드를 추가합니다. 외부 클라이언트는 빈 역할 목록(단일 바이트 `0x00`, 즉 String 봉투 안의 VarUInt 0)을 전송합니다. |
| V2\_DYNAMIC\_AND\_JSON\_SERIALIZATION | 54473 | Column body | 서버는 `Dynamic` 및 `JSON` 컬럼 타입에 V2 직렬화를 사용할 수 있으며, 이 게이트에 따라 해당 타입이 사용하는 `state_prefix` 버전이 결정됩니다. [버전이 지정된 타입](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#versioned-types)을 참조하십시오. |
| SERVER\_SETTINGS | 54474 | ServerHello | 서버는 기본값이 아닌 자신의 설정을 목록 형태로 ServerHello 끝부분의 `nonce` 뒤에 실어 보냅니다. 포맷: 빈 키로 끝나는 `(key, flags, value)` 3중 항목의 나열로, Query 패킷의 settings 목록과 동일합니다. |
| QUERY\_AND\_LINE\_NUMBERS | 54475 | ClientInfo | ClientInfo 끝부분에 `script_query_number`(VarUInt)와 `script_line_number`(VarUInt)를 추가합니다. clickhouse-client가 여러 구문으로 이루어진 스크립트에서 오류 위치를 식별하는 데 사용하며, 외부 클라이언트는 `0, 0`을 전송합니다. |
| JWT\_IN\_INTERSERVER | 54476 | ClientInfo | ClientInfo 끝부분에 JWT 존재 여부를 나타내는 UInt8과 선택적 `String jwt`를 추가합니다. JWT가 없는 외부 클라이언트는 바이트 `0x00`을 전송합니다. (C++에서는 `DBMS_MIN_REVISON_WITH_JWT_IN_INTERSERVER`로 표기되어 있으므로 상수 이름의 오타에 유의하십시오.) |
| QUERY\_PLAN\_SERIALIZATION | 54477 | ServerHello, QueryPlan packet | ServerHello의 서버 설정 뒤에 `VarUInt query_plan_serialization_version`이 추가됩니다. 또한 미리 빌드된 쿼리 계획을 서버 간에 전달하기 위한 `ClientPacket::QueryPlan`(코드 `13`)을 도입하며, 외부 클라이언트는 이 패킷을 전송하지 않습니다. 쿼리 계획 직렬화 버전 `10`에서는 직렬화된 계획 앞에 `VarUInt max_threads`와 `Bool concurrency_control`이 붙습니다. 둘 중 하나라도 기본값이 아니면 initiator는 버전 `10` 미만의 피어에 SQL 전송 방식을 사용하며, 피어는 전송된 쿼리 설정을 바탕으로 직접 계획을 빌드합니다. |
| PARALLEL\_BLOCK\_MARSHALLING | 54478 | Block (Column) | 서버는 병렬 처리를 위해 컬럼을 `ColumnBLOB`(인라인 압축)으로 감쌀 수 있습니다. 쿼리에 압축이 활성화되어 있고(AND) `rows > 1`인 경우에만 적용되며, 그렇지 않으면 일반 컬럼 wire 포맷이 사용됩니다. 송신 Query 패킷에서 압축을 활성화하지 않는 클라이언트에는 wire 변경이 없습니다. |
| VERSIONED\_CLUSTER\_FUNCTION\_PROTOCOL | 54479 | ServerHello | ServerHello 끝부분에 `VarUInt cluster_function_protocol_version`을 추가합니다. `*Cluster` 테이블 함수(`s3Cluster` 등)에 사용됩니다. 현재 값: `8` (`DBMS_CLUSTER_PROCESSING_PROTOCOL_VERSION`). 버전 `7`은 비공개 저장소 기능(Iceberg compaction)용으로 예약되어 있으며, `8`은 서버 간 클러스터 읽기 작업 페이로드(`ReadTaskResponse` 본문으로, 여기서는 명세하지 않음 — 아래 참조)에 선택적 `read_source_index`를 추가합니다. 외부 클라이언트는 이 값을 디코딩한 후 무시합니다. |
| OUT\_OF\_ORDER\_BUCKETS\_IN\_AGGREGATION | 54480 | BlockInfo | BlockInfo의 필드 태그 스트림에 필드 3(`out_of_order_buckets: Vec<Int32>`)을 추가합니다. `[VarUInt count][Int32]*count` 형태로 디코딩됩니다. 외부 클라이언트가 이 필드를 직접 전송하지는 않으며, 디코더는 서버가 보내는 비어 있지 않은 목록을 그대로 읽습니다. |
| COMPRESSED\_LOGS\_PROFILE\_EVENTS\_COLUMNS | 54481 | Log, ProfileEvents, TableColumns | 서버는 [`Log`](#log), [`ProfileEvents`](#profileevents), [`TableColumns`](#tablecolumns) 패킷 본문을 [압축 프레임](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#compression-frame)으로 감쌀 수 있습니다. 이 버전에서는 세 본문 모두 압축이 선택적으로 적용되는 동일한 출력 경로를 거치며, 쿼리에 `compression = true`가 설정된 경우에만 실제 압축 프레임이 됩니다. 송신 Query 패킷에서 압축을 활성화하지 않는 클라이언트에는 wire 변경이 없습니다. |
| REPLICATED\_SERIALIZATION | 54482 | Block (Column) | 서버는 kind\_stack이 `0x04 = REPLICATED`인 컬럼을 전송할 수 있습니다. 이는 반복되는 값을 딕셔너리 방식으로 압축한 형태입니다. [kind\_stack 및 희소 인코딩](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding)을 참조하십시오. 이 버전 미만에서는 writer가 이러한 컬럼을 펼친 뒤 전송했습니다. 인덱스 조회(행마다 `elements[indexes[i]]`)로 디코딩되며, 리프 타입과 `Nullable`/`Array`/`Tuple`/`Map`/`Nested`/`LowCardinality` 내부 타입을 지원합니다. |
| NULLABLE\_SPARSE\_SERIALIZATION | 54483 | Block (Column) | 희소 직렬화를 `Nullable(T)`와 결합합니다. 이 버전 미만에서는 writer가 Nullable 컬럼의 희소 표현을 펼친 뒤 전송했으며, v54483 이상에서는 wire 데이터가 Nullable 위에 희소 직렬화가 적용된 형태가 됩니다. [kind\_stack 및 희소 인코딩](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#kind-stack-and-sparse-encoding)을 참조하십시오. |
| PROGRESS\_IN\_ASYNC\_INSERT | 54484 | Progress (INSERT) | **비동기** INSERT(`async_insert = 1`)에서 삽입이 flush되면 서버는 `EndOfStream` 전에 [`Progress`](#progress) 패킷을 하나 더 보내고, 이어서 해당 삽입의 `ProfileEvents`를 전송합니다. *협상된* 버전이 54484 이상일 때만 적용되며, 그 미만에서는 서버가 이 마지막 Progress를 생략합니다. Progress wire 포맷은 그대로이며, 전송 시점만 새로 추가되었습니다. 실제로 이 증분에는 경과 시간이 담기고, 기록된 행 카운터는 함께 전송되는 ProfileEvents로 보고됩니다. 중간에 섞여 오는 Progress를 이미 처리하는 클라이언트는 포맷을 바꿀 필요 없이 패킷 하나를 더 받을 수 있기만 하면 됩니다. |
| CLIENT\_AGENT\_IN\_CLIENT\_INFO | 54485 | ClientInfo | ClientInfo 끝에 `client_agent` `String`을 추가합니다. 표준 클라이언트는 실행 환경에서 에이전트 식별자(예: `claude-code`, `cursor`, `gemini-cli` 또는 `AGENT` 변수 값)를 자동으로 감지하며, 감지된 값이 없는 외부 클라이언트는 빈 문자열을 전송합니다. 협상된 버전이 54485 이상이면 필수이며, 생략하면 Query 패킷의 나머지 부분이 어긋납니다. |
| INTERNAL\_QUERY\_FLAG | 54486 | ClientInfo | ClientInfo 끝에 `is_internal` `UInt8`을 추가합니다. 사용자가 실행하지 않은 서버 내부 쿼리이면 `1`이며, 이 값은 원격 쿼리로 전파되어 해당 `system.query_log` 행이 내부 쿼리로 표시됩니다. 외부 클라이언트는 `0`을 전송합니다. 협상된 버전이 54486 이상이면 필수이며, 생략하면 Query 패킷의 나머지 부분이 어긋납니다. |
| INTERSERVER\_CURRENT\_ROLES | 54488 | ClientInfo | ClientInfo 끝에 선택적 String 목록인 `current_roles`(`[UInt8 present]` 다음, 값이 `1`이면 `[VarUInt count][String]*count`)를 추가하고, 서버 간 쿼리에서는 `auth_hash`가 직렬화된 목록까지 포함하도록 확장합니다. initiator의 활성화된 역할 이름을 전달하므로, 보조 노드는 사용자의 기본 역할로 대체하지 않고 initiator와 동일하게 행 정책 범위를 적용합니다. 서버 간 쿼리에서 `push_external_roles_in_interserver_queries = 1`인 경우에만 채워지며, 외부 클라이언트는 `0`(없음)을 전송합니다. 협상된 버전이 54488 이상이면 필수이며, 존재 여부 바이트를 생략하면 Query 패킷의 나머지 부분이 어긋납니다. |
| CURRENT\_AGGREGATION\_VARIANT\_SELECTION\_METHOD | 54489 | Aggregation (two-level buckets) | 단일 `String` 키에 대한 집계 방식은 `enable_packed_string_keys_in_aggregation`에 따라 결정됩니다. 이 revision 미만의 피어는 항상 packed 방식을 사용하고 해당 설정을 알지 못하므로, 이러한 피어와 2단계 버킷을 주고받으면 동일한 키가 서로 다른 버킷에 들어갈 수 있습니다. 따라서 initiator는 이러한 피어에 대해 안전 측(fail closed)으로 동작합니다. 즉, 해당 피어의 2단계 임계값을 0으로 설정하고 단일 단계 블록을 직접 다시 버킷으로 나눕니다. 같은 릴리스 안에서도 방식이 바뀔 수 있으므로 피어의 버전이 아닌 revision을 기준으로 비교합니다. 네이티브 프로토콜 wire 변경은 없습니다. |
| HTTP\_HANDLER\_IN\_CLIENT\_INFO | 54490 | ClientInfo | ClientInfo의 **HTTP** 분기에서 `http_referer` 바로 뒤에 `http_handler_name`과 `http_request_url`(둘 다 `String`)을 추가합니다. 일치한 SQL 정의 HTTP 핸들러의 이름과 요청 URL을 전달하므로, 분산 핸들러 쿼리에서도 원격 샤드의 `currentHandler`/`currentRequestURL` 및 `system.query_log`의 `http_handler_name`/`http_request_url` 컬럼 값이 채워집니다. `query_interface = HTTP`일 때만 기록되며(핸들러가 아닌 HTTP 쿼리에서는 빈 문자열), 협상된 버전이 54490 이상이면 필수입니다. 생략하면 Query 패킷의 나머지 부분이 어긋납니다. |
| QUANTILE\_DETERMINISTIC\_SKIP\_DEGREE | 54491 | Block (Column) | `AggregateFunction(quantileDeterministic, ...)`(및 `quantiles`/`median` 형태)의 상태 버전을 `0`에서 `1`로 올려, 직렬화된 각 상태 끝에 `UInt8 skip_degree`를 추가합니다. 이 집계 함수 하나의 상태 페이로드만 바뀌며, 컬럼 프레이밍은 그대로이고 다른 집계 함수는 모두 각자의 버전을 유지합니다. 이 revision 미만에서는 writer가 버전 `0` 상태를 기록하므로 이전 피어는 영향을 받지 않습니다. |
| STRING\_WITH\_SIZE\_STREAM\_SERIALIZATION | 54492 | Block (Column) | `String` 컬럼이 값마다 길이 접두사를 붙이는 레이아웃에서 누적 바이트 오프셋을 담은 별도 스트림(`Array`와 동일한 오프셋 레이아웃)으로 바뀌며, `[UInt64 × num_rows offsets][data blob]` 형태로 그대로 전송됩니다. 협상된 revision에 따라 적용되며, 54492 미만에서는 writer가 값별 길이 접두사를 사용합니다. wire에는 이를 나타내는 컬럼별 표시가 없으며, 양쪽 피어 모두 revision만으로 이를 판단합니다. [String 컬럼 레이아웃](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#string-type)을 참조하십시오. |
| UNIQ\_STATE\_VERSION\_1 | 54493 | Block (Column) | `AggregateFunction(uniq, ...)`의 상태 버전을 `0`에서 `1`로 올립니다. 버전 `1` 상태는 플래그 바이트로 시작하며, 그 뒤에 버전 `0` 레이아웃의 메인 샘플이 이어집니다. 플래그의 비트 `0`이 설정되어 있으면 전체 64비트 해시(각각 상수 `0x9E3779B97F4A0000`과 XOR한 값)로 이루어진 작은 추가 샘플이 뒤따릅니다(`UInt8 skip_degree`, `VarUInt count`, 그다음 8바이트 값 `count`개). 이 샘플은 최대 16384개 값으로 솎아 내며, 32비트 메인 샘플의 정확도가 떨어지고 과거에는 오버플로로 무의미한 값이 나오던 20억 이상의 카디널리티에서도 추정치를 정확하게 유지합니다. 비트 `1`은 64비트 샘플이 불완전함을 나타냅니다(상태가 버전 `0` 상태를 흡수하여 해당 요소를 64비트 샘플이 반영하지 못한 경우). 이때는 해당 샘플을 기록하지 않으며, 추정치는 32비트 메인 샘플만으로 계산됩니다. 두 비트는 상호 배타적이고 다른 비트는 정의되어 있지 않으므로, 리더는 두 비트가 모두 설정되었거나 다른 비트가 설정된 플래그 바이트를 거부해야 합니다. 이 집계 함수 하나의 상태 페이로드만 바뀌며, 컬럼 프레이밍은 그대로이고 다른 집계 함수는 모두 각자의 버전을 유지합니다. `uniq`에 대한 패스스루 콤비네이터(`uniqIf`, `uniqArray`, `uniqForEach`, `uniqMap`, `uniqArgMin` / `uniqArgMax`, `uniqOrNull` / `uniqOrDefault`, `uniqResample` 및 암시적 `Nullable` 인수 어댑터)도 동일한 게이트를 따르며, 페이로드 내부에 중첩된 `uniq` 상태에 버전을 전달합니다. 이 revision 미만에서는 writer가 버전 `0` 상태(메인 샘플만)를 기록하므로 이전 피어는 영향을 받지 않습니다. |

## 패킷 엔벌로프

wire 상에서 전송되는 모든 메시지는 송수신 방향과 관계없이 동일한 외부 구조를 가집니다.

```text theme={null}
[VarUInt: packet_type_code]    always encoded as VarUInt
[message body]                 format depends on packet_type_code
```

전체 패킷 유형 테이블은 [패킷 유형 참고](#packet-type-reference)에서 확인할 수 있습니다.

패킷 유형은 고정 폭 바이트가 아니라 `VarUInt`입니다. 값이 128 미만이면 `VarUInt`로 인코딩해도 동일한 단일 바이트가 생성되지만, 향후 패킷 유형 값이 128 이상이 되더라도 호환성이 유지되도록 구현체는 반드시 `VarUInt` 인코딩을 사용해야 합니다.

[메시지 참고](#message-reference)에서는 각 패킷의 **body**, 즉 패킷 유형 코드 뒤에 오는 바이트만 설명합니다. 필드 번호는 body의 첫 번째 필드를 1로 하여 시작합니다.

### 청크 프레이밍 (v54470+)

`CHUNKED_PROTOCOL` 기능이 **협상되면**([핸드셰이크](#handshake-phase) 참조), wire 상의 모든 패킷은 청크 프레이밍으로 감싸여 전송됩니다. 이 래핑은 **방향별로** 적용됩니다. 클라이언트→서버 방향과 서버→클라이언트 방향은 각각 따로 협상되므로, 두 방향이 서로 다른 모드(청크 방식 또는 프레이밍 없음)로 동작할 수도 있습니다.

패킷별 wire 레이아웃:

```text theme={null}
<chunk>...   one or more chunks; their payloads concatenated form the whole packet
[u32 LE = 0] zero-size terminator marking end of packet
```

청크별 wire 레이아웃:

```text theme={null}
[u32 LE: chunk_size]   chunk_size in [1, UINT32_MAX]
[chunk_size bytes]     packet bytes (see note below)
```

패킷 유형 `VarUInt`은 청크 스트림 **내부**에 위치합니다. 즉, 프레이밍보다 먼저 별도로 전송되는 바이트가 아니라 패킷 payload의 첫 번째 바이트(첫 번째 청크의 첫 번째 바이트)입니다. 각 패킷의 청크 payload는 [패킷 엔벌로프](#packet-envelope)의 `[VarUInt packet_type_code][message body]` 전체입니다. 클라이언트가 패킷 유형을 청크 스트림 바깥에 두면 피어는 해당 유형 바이트를 `u32` 청크 크기의 첫 번째 바이트로 읽게 되고, 그 결과 연결의 동기화가 깨집니다.

writer의 버퍼가 패킷 중간에 가득 차면 하나의 패킷이 여러 청크로 분할될 수 있습니다. 분할 지점은 패킷 유형의 `VarUInt` 내부를 포함해 어디든 될 수 있습니다. 리더는 청크 payload를 이어 붙이고, 끝에 붙는 4바이트 0을 투명한 패킷 경계로 처리합니다. 즉, 이 값을 소비하되 패킷 body를 읽는 쪽에는 노출하지 않습니다.

body가 없는 패킷도 마찬가지로 래핑됩니다. 청킹이 협상되면 `Ping`이나 `Pong` 같은 단일 바이트 패킷은 `[u32 size = 1][0x04][u32 0]`이 됩니다. 이 페이지의 다른 부분에서 "wire 상의 단일 바이트"로 설명하는 내용은 청킹 이전의 형태를 가리킵니다.

**협상.** ServerHello와 Addendum은 각각 방향별로 하나씩 총 두 개의 `String` 필드를 전달하며, 각 값은 `{"chunked", "notchunked", "chunked_optional", "notchunked_optional"}` 중 하나입니다.

* `chunked` / `notchunked`는 엄격한 값으로, 해당 측이 반드시 그 모드를 사용해야 함을 의미합니다.
* `_optional` 변형은 유연한 값으로, 상대 측이 선택한 모드를 그대로 따릅니다.

각 방향의 합의 값은 양측 값을 짝지어 다음과 같이 결정됩니다.

| 서버 선호 | 클라이언트 선호 | 합의 결과 |
| - | - | - |
| `*_optional` | 모든 값 | CLIENT를 따름 (클라이언트 값의 `starts_with("chunked")`) |
| 모든 값 | `*_optional` | SERVER를 따름 |
| `chunked` 엄격 | `chunked` 엄격 | `chunked` |
| `notchunked` 엄격 | `notchunked` 엄격 | `notchunked` |
| 엄격 불일치 | 엄격 불일치 | **프로토콜 오류** — 연결을 반드시 종료해야 합니다 |

클라이언트 측에서는 클라이언트의 SEND 선호를 서버의 RECV 선호와 맞춰 협상하며, 반대 방향도 같은 방식으로 협상합니다.

**타이밍.** 협상 문자열은 프레이밍되지 않은 wire로 전송되며, 순서는 ClientHello → ServerHello(서버 선호) → Addendum(클라이언트가 협상한 값)입니다. 프레이밍 전환은 Addendum이 플러시된 *이후*에 전송되는 모든 바이트부터 적용됩니다. Addendum 자체와 ClientHello, ServerHello는 항상 프레이밍 없이 전송됩니다.

## 연결 수명 주기

연결은 어느 시점에서든 `HANDSHAKE`, `READY`, `READING_RESPONSE`, 종료의 네 가지 상태 중 정확히 하나에 속합니다. 이 프로토콜은 다중화(multiplexing)를 지원하지 않으므로, 클라이언트가 이전 응답을 모두 읽어 들이기 전에 새 요청을 보내면 wire 상에서 바이트가 뒤섞여 스트림이 손상됩니다.

### 상태

```mermaid theme={null}
stateDiagram-v2
    [*] --> HANDSHAKE: TCP connect

    HANDSHAKE --> READY: handshake ok
    READY --> READING_RESPONSE: send Query
    READING_RESPONSE --> READY: EndOfStream / Exception
    READY --> READY: Ping / Pong

    HANDSHAKE --> Terminated: handshake error
    READING_RESPONSE --> Terminated: protocol violation / I/O error
    READY --> Terminated: close
    Terminated --> [*]
```

정상 경로는 `HANDSHAKE → READY → READING_RESPONSE → READY` 순으로 곧장 이어집니다. `Ping`/`Pong`은 자기 루프를 이루며, 모든 실패 경로는 단일 `Terminated` 싱크로 모입니다.

| State | 설명 |
| - | - |
| `HANDSHAKE` | TCP 연결이 열린 직후의 초기 상태입니다. [핸드셰이크](#handshake-phase) 메시지만 유효합니다. 성공하면 `READY`로 전환되고, 실패하면 종료됩니다. |
| `READY` | 유휴 상태입니다. 클라이언트는 [Ping](#ping-phase)이나 [Query](#query-phase)를 보내거나 연결을 닫을 수 있습니다. 연결은 `READY` 상태로 무기한 유지될 수 있습니다(단, `idle_connection_timeout`의 적용을 받습니다. [연결 제한](#connection-limits) 참조). 이 상태에서 `Data` (2) 또는 `Scalar` (7) 패킷을 보내면 프로토콜 위반이 됩니다. 이 경우 서버는 패킷 body를 디코딩하지 않으며, `Exception` 패킷도 먼저 보내지 않고 바로 연결을 종료합니다. body를 읽지 않은 채 남겨 두기 때문에 클라이언트에서는 정상적인 종료 대신 TCP 리셋이 발생할 수 있습니다. |
| `READING_RESPONSE` | 클라이언트가 Query를 보내면 이 상태로 진입합니다. 클라이언트는 `READY`로 돌아가기 전에 서버의 응답 스트림을 끝까지 모두 읽어야 합니다. 이 상태에서 클라이언트가 서버로 보낼 수 있는 패킷은 Cancel(이 페이지에서는 다루지 않음)뿐입니다. |
| Terminated | 더 이상 사용할 수 없는 상태입니다. 클라이언트는 새 TCP 연결을 열고 핸드셰이크를 처음부터 다시 수행해야 합니다. |

### 핸드셰이크 단계

인증을 수행하고 프로토콜 버전을 협상합니다. 연결당 정확히 한 번, 다른 모든 작업보다 먼저 이루어집니다.

TCP 연결이 막 수립되었고 아직 메시지가 하나도 교환되지 않은 상태입니다. 흐름은 다음과 같습니다.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: ClientHello
    alt success
        S->>C: ServerHello
    else error
        S->>C: Exception — connection terminates
    end
    Note over C,S: negotiated_version = min(client, server)
    opt negotiated_version ≥ 54458
        C->>S: Addendum
    end
```

1. 클라이언트는 자신이 지원하는 최대 프로토콜 버전을 담아 [`ClientHello`](#clienthello)를 전송합니다.

2. 클라이언트는 응답을 읽은 후 패킷 유형에 따라 다음과 같이 처리합니다.

   | 패킷 유형 | 동작 |
   | - | - |
   | `Hello` (0) | [`ServerHello`](#serverhello)를 디코딩하고 `negotiated_version = min(client_ver, server_ver)`를 계산한 후 3단계로 진행합니다. |
   | `Exception` (2) | [`Exception`](#exception)을 디코딩하여 오류로 반환하고 연결을 종료합니다. |
   | 그 외의 패킷 | 프로토콜 위반이므로 연결을 종료합니다. |

3. `negotiated_version ≥ 54458`(`ADDENDUM` 기능)이면 클라이언트는 [`Addendum`](#addendum)을 전송합니다. 이 판단은 클라이언트가 선언한 버전이 아니라 **협상된** 버전을 기준으로 합니다.

핸드셰이크에 성공하면 연결은 `READY` 상태로 전환되며, 오류가 발생하면 연결이 종료됩니다.

### Ping 단계

TCP keepalive와는 별개로 동작하는 애플리케이션 수준의 활성 상태(liveness) 확인입니다. Ping/Pong 왕복이 성공하면 TCP 연결이 양방향으로 살아 있으며 서버가 정상적으로 응답하고 있음을 확인할 수 있습니다. Ping은 상태를 유지하지 않으며(stateless) 어떤 쿼리와도 연관되지 않으므로, 연속으로 여러 번 보낸 Ping은 서로 독립적입니다.

`READY` 상태에서 시작하는 흐름은 다음과 같습니다.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Ping (0x04)
    alt responsive
        S->>C: Pong (0x04)
    else error
        S->>C: Exception
    end
```

1. 클라이언트가 [`Ping`](#ping)을 전송합니다.
2. 클라이언트가 응답을 읽습니다.

   | 패킷 유형 | 동작 |
   | - | - |
   | `Pong` (4) | 연결이 활성 상태임을 확인합니다. `READY`로 돌아갑니다. |
   | `Exception` (2) | [`Exception`](#exception)을 디코딩하여 오류로 반환합니다. |
   | 그 외 모든 패킷 | 프로토콜 위반(Protocol violation)입니다. |

### 쿼리 단계

클라이언트가 SQL 문을 전송하면 서버는 결과 블록과 실행 텔레메트리(execution telemetry)를 스트리밍으로 반환합니다. 응답은 일련의 패킷으로 구성되며, 반드시 하나의 `EndOfStream` 또는 `Exception`으로 종료됩니다.

`READY` 상태부터의 흐름은 다음과 같습니다.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Query message
    C->>S: External-table Data packets (0 or more)
    C->>S: Empty Data marker — end-of-client-data (required)
    S->>C: Data — header block (N cols, 0 rows)
    loop until EndOfStream or Exception
        S->>C: Progress / Log (interleaved)
        S->>C: Data — result block (N cols, M rows)
        S->>C: Totals / Extremes (aggregation queries)
        S->>C: ProfileInfo / ProfileEvents (profiling)
    end
    S->>C: Data — empty block (boundary marker)
    S->>C: Progress — final updates
    S->>C: EndOfStream — authoritative end of query
```

어느 시점에서든 오류가 발생하면 서버는 `EndOfStream` 대신 `Exception`을 전송하며, 이에 따라 쿼리가 종료됩니다.

1. 클라이언트는 고유한 `query_id`(일반적으로 UUID)를 포함하여 [`Query`](#query)를 전송합니다.
2. 클라이언트는 외부 테이블(external table)을 모두 전송한 다음, 빈 Data 마커를 전송합니다. 빈 Data 패킷의 값은 `table_name = ""`, `num_columns = 0`, `num_rows = 0`입니다. 서버는 이 마커를 수신해야 쿼리 실행을 시작합니다. 외부 테이블은 동일한 `table_name`을 가진 여러 `Data` 패킷으로 나뉘어 전송될 수 있습니다. 이때 첫 번째 패킷이 테이블의 스키마를 확정하며, 이후 같은 이름으로 전송되는 모든 패킷은 동일한 컬럼명과 유형을 동일한 순서로 포함해야 합니다. 그렇지 않으면 서버는 `INCORRECT_DATA` 오류와 함께 쿼리를 거부합니다([Data](#data) 참조).
3. 클라이언트는 `READING_RESPONSE` 상태로 전환하고 쓰기 버퍼를 플러시합니다.
4. 클라이언트는 루프를 돌며 응답 패킷을 읽고, 패킷 유형에 따라 처리합니다:

   | 패킷 유형 | 동작 |
   | - | - |
   | `Data` (1) | 블록을 디코딩합니다. 첫 번째 Data는 스키마 헤더이고, 이후의 Data는 결과 블록(누적)이며, 빈 블록은 경계 마커입니다. `num_rows == 0`은 쿼리 종료를 의미하지 **않습니다**. |
   | `Progress` (3) | 실행 메트릭입니다. 각 패킷은 직전 패킷 대비 **증분**이므로 로컬에서 누적하십시오. |
   | `EndOfStream` (5) | 쿼리가 완료되었음을 나타냅니다. 루프를 종료하고 `READY` 상태로 돌아갑니다. |
   | `ProfileInfo` (6) | 실행 후 프로파일링 데이터입니다. |
   | `Totals` (7) | 집계 합계 블록입니다(Data와 동일한 wire 형식). |
   | `Extremes` (8) | 최솟값/최댓값 블록입니다(Data와 동일한 wire 형식). |
   | `Log` (10) | 서버 로그 라인입니다. |
   | `TableColumns` (11) | 컬럼 기본값 메타데이터입니다. |
   | `ProfileEvents` (14) | 성능 카운터입니다. |
   | `Exception` (2) | 디코딩하여 오류로 반환합니다. 루프를 종료하고 `READY` 상태로 돌아갑니다. |
   | 그 외 모든 유형 | 쿼리 단계에서 예상치 못한 패킷입니다. 연결을 종료합니다. |

`EndOfStream`을 수신하거나 `Exception`을 처리하면 연결은 `READY` 상태로 돌아갑니다. 프로토콜 위반이나 I/O 오류가 발생하면 연결이 종료됩니다.

> [!NOTE]
>
> `num_rows == 0`은 처음 구현할 때 흔히 실수하는 부분입니다. 행이 0개인 블록은 경계 마커 또는 스키마 헤더일 뿐, 스트림 종료 신호가 아닙니다. 응답은 `EndOfStream` 또는 `Exception`을 받았을 때만 종료됩니다.

### INSERT 단계

INSERT 단계는 [쿼리 단계](#query-phase)에 두 번의 교환이 추가된 형태입니다. 클라이언트가 `INSERT` SQL 문을 전송하면 서버는 대상 테이블의 구조를 설명하는 \*\*스키마 블록(schema block)\*\*으로 응답합니다. 이어서 클라이언트는 행이 담긴 Data 패킷을 스트리밍한 뒤 빈 Data 마커를 전송하고, 서버는 `EndOfStream` 또는 `Exception`으로 처리를 마칩니다.

`READY` 상태에서 시작하며, SQL은 `INSERT INTO <table> [(<cols>)] VALUES` 형태의 `INSERT`입니다. 행 데이터는 Data 패킷을 통해 전달되므로 인라인 `VALUES (...)` 리터럴은 포함하지 않습니다. 전체 흐름은 다음과 같습니다.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: Query packet (INSERT body)
    C->>S: External-table Data packets (0 or more)
    Note over C,S: No empty Data marker here —<br/>it would end the row stream before it starts
    opt metadata before schema
        S->>C: TableColumns / Progress / ...
    end
    S->>C: Data packet — schema block (columns, 0 rows)
    Note over C,S: Schema block is the contract:<br/>rows sent next must match these column shapes
    loop one or more blocks
        C->>S: Data packet (rows N)
    end
    C->>S: Data packet — empty block (0 rows), end-of-input terminator
    loop until EndOfStream or Exception
        S->>C: Progress / ProfileInfo / Log / ProfileEvents
    end
    opt async_insert = 1 and negotiated_version ≥ 54484
        S->>C: trailing Progress, then insert ProfileEvents
    end
    S->>C: EndOfStream
```

1. 클라이언트는 `body`를 INSERT SQL로 설정한 [`Query`](#query)를 전송합니다.
2. 클라이언트는 외부 테이블(external table)이 있으면 이를 전송합니다(INSERT에서는 드문 경우입니다). [Query 단계](#query-phase)와 달리 여기서는 빈 Data 마커를 전송하지 **않습니다**. `INSERT` `Query` 패킷은 보류 중인 데이터가 있는 상태로 전송되므로, 빈 데이터 종료 블록은 5단계로 미뤄집니다. 스키마 블록보다 먼저 이를 전송하면 서버는 이를 행 스트림의 끝으로 해석하여 행 없이 INSERT를 완료한 뒤, 첫 번째 실제 행 패킷을 예상치 못한 최상위 패킷으로 파싱하게 됩니다.
3. 클라이언트는 스키마 Data 패킷을 읽을 때까지 메타데이터 패킷(TableColumns, Progress, ProfileInfo, Log, ProfileEvents)을 소비합니다. 스키마 Data 패킷은 행은 0개이지만 전체 컬럼 구조(이름과 타입)를 갖춘 Block입니다. 스키마 블록은 일종의 계약으로, 클라이언트가 이후 전송하는 행은 이 컬럼 형태와 일치해야 합니다.
4. 클라이언트는 데이터 블록을 전송합니다. 각 블록마다 `VarUInt(ClientPacket::Data = 2)`를 쓰고, 이어서 빈 외부 테이블 이름에 해당하는 `String("")`을 쓴 다음 Block을 씁니다. 컬럼 타입은 위치 순서대로 스키마 블록의 컬럼과 일치해야 합니다.
5. 클라이언트는 입력 종료 표시를 전송합니다. 이는 빈 Block(컬럼 0개, 행 0개)을 담은 Data 패킷입니다.
6. 클라이언트는 `EndOfStream`(성공) 또는 `Exception`(실패)을 받을 때까지 응답 스트림을 소비합니다.

**비동기 INSERT(v54484+).** 쿼리에 `async_insert = 1`이 포함되면 서버는 행을 큐에 넣었다가 배치 단위로 플러시합니다. 협상된 버전이 54484(`PROGRESS_IN_ASYNC_INSERT`) 이상이면, 플러시가 완료된 후 서버는 추가 [`Progress`](#progress) 패킷을 보내고, 곧바로 해당 삽입의 `ProfileEvents`와 `EndOfStream`을 차례로 보냅니다. 54484 미만에서는 서버가 이 마지막 Progress를 보내지 않습니다. 이 패킷은 일반적인 `Progress`이지만, 서버가 쓰기 카운트를 반영하기 전에 쿼리 파이프라인을 재설정하므로 실제로는 증분에 경과 시간만 담깁니다. 기록된 행 수와 바이트 통계는 함께 전송되는 `ProfileEvents`를 통해 클라이언트에 전달됩니다. 6단계에서 이미 중간에 섞여 들어오는 Progress를 소비하는 클라이언트라면 패킷 하나를 더 받기만 하면 됩니다.

연결은 `EndOfStream` 또는 처리된 `Exception`을 받으면 `READY` 상태로 돌아갑니다. 프로토콜 위반이나 I/O 오류가 발생하면 연결이 종료됩니다.

## 메시지 참고

필드는 wire 상의 전송 순서대로 나열됩니다. `Type` 컬럼에는 다음 값이 사용됩니다.

* `VarUInt` — 가변 길이 부호 없는 정수입니다([VarUInt](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#varuint) 참조).
* `String` — VarUInt 길이 접두사가 붙은 바이트열입니다([String](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#string) 참조).
* `UInt8`, `Int32` 등 — 고정 폭 리틀 엔디언 정수입니다.
* `Bool` — `0x00` 또는 `0x01` 값을 갖는 1바이트입니다.

`Role` 컬럼은 각 필드를 사용하는 주체를 나타냅니다.

* **클라이언트** — 외부 클라이언트가 설정합니다.
* **서버 간** — 서버 간 통신에서만 의미가 있으며, 외부 클라이언트는 기본값을 기록합니다.
* **공통** — 양쪽 모두 사용합니다.

이 표에서는 패킷 유형 코드 다음에 오는 각 패킷의 body만 다룹니다.

### ClientHello (패킷 유형 0)

Client → Server. TCP 연결이 수립된 후 전송되는 첫 번째 메시지입니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | client\_name | String | 공통 | 클라이언트 식별자(예: `"clickhouse-client"`) |
| 2 | version\_major | VarUInt | 공통 | 클라이언트 메이저 버전 |
| 3 | version\_minor | VarUInt | 공통 | 클라이언트 마이너 버전 |
| 4 | protocol\_version | VarUInt | 공통 | 클라이언트가 지원하는 최대 프로토콜 버전 |
| 5 | database | String | 공통 | 기본 데이터베이스 이름 |
| 6 | user | String | 공통 | 인증에 사용할 사용자 이름입니다. 비어 있으면 서버의 기본 세션 사용자(`default_session_user` 서버 설정)가 사용되며, 26.8 이전 버전의 서버에서는 거부됩니다. [인증](#authentication)을 참조하십시오. |
| 7 | password | String | 공통 | 비밀번호(평문) |

### ServerHello (패킷 유형 0)

Server → Client. 인증에 성공하면 ClientHello에 대한 응답으로 전송됩니다.

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | server\_name | String | 공통 | 항상 | 서버 식별자 |
| 2 | version\_major | VarUInt | 공통 | 항상 | 서버 메이저 버전 |
| 3 | version\_minor | VarUInt | 공통 | 항상 | 서버 마이너 버전 |
| 4 | protocol\_version | VarUInt | 공통 | 항상 | 서버의 프로토콜 버전 |
| 4a | parallel\_replicas\_protocol\_version | VarUInt | 공통 | VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL (v54471) | 서버의 병렬 레플리카 coordination 프로토콜 버전입니다. **wire 상 위치: `protocol_version` 바로 다음**, `timezone` 앞입니다. 현재 값: `8`. |
| 5 | timezone | String | 공통 | TIMEZONE (v54058) | 서버 시간대(예: `"UTC"`) |
| 6 | display\_name | String | 공통 | DISPLAY\_NAME (v54372) | 사람이 읽을 수 있는 서버 이름 |
| 7 | version\_patch | VarUInt | 공통 | VERSION\_PATCH (v54401) | 서버 패치 버전 |
| 8 | proto\_send\_chunked\_srv | String | 공통 | CHUNKED\_PROTOCOL (v54470) | 서버가 선호하는 아웃바운드 청킹 방식입니다. `"chunked"`, `"notchunked"`, `"chunked_optional"`, `"notchunked_optional"` 중 하나입니다. [청크 프레이밍](#chunked-framing)을 참조하십시오. **버전 게이트는 더 높지만 wire 상에서는 `password_complexity_rules`보다 앞에 위치합니다.** |
| 9 | proto\_recv\_chunked\_srv | String | 공통 | CHUNKED\_PROTOCOL (v54470) | 서버가 선호하는 인바운드 청킹 방식입니다. 허용되는 값은 필드 8과 동일합니다. |
| 10 | password\_complexity\_rules | Rule\[] | 공통 | PASSWORD\_COMPLEXITY\_RULES (v54461) | 서버의 비밀번호 정책입니다. `VarUInt count` 뒤에 `count × Rule`이 이어집니다. 아래를 참조하십시오. |
| 11 | 논스 | UInt64 | 서버 간 | INTERSERVER\_SECRET\_V2 (v54462) | 8바이트 LE 무작위 논스입니다. 서버 간 쿼리 서명 방식에 사용됩니다. 외부 클라이언트는 스트림 정렬을 유지하기 위해 이 값을 반드시(MUST) 디코딩해야 하며, 값 자체는 무시해야 합니다(SHOULD). |
| 12 | server\_settings | Setting\[] | 공통 | SERVER\_SETTINGS (v54474) | 서버가 브로드캐스트하는, 기본값이 아닌 설정입니다. 형식: 0개 이상의 `(String key, VarUInt flags, String value)` 3요소 묶음이 이어지고 빈 키로 끝납니다. [Query 패킷의 설정 목록](#setting)과 동일합니다. |
| 13 | query\_plan\_serialization\_version | VarUInt | 공통 | QUERY\_PLAN\_SERIALIZATION (v54477) | 서버가 지원하는 쿼리 계획 직렬화 버전입니다. 외부 클라이언트는 디코딩한 후 무시합니다. |
| 14 | cluster\_function\_protocol\_version | VarUInt | 공통 | VERSIONED\_CLUSTER\_FUNCTION\_PROTOCOL (v54479) | 서버의 `*Cluster` 테이블 함수 프로토콜 버전입니다. 현재 값: `8`. 이 값에 따라 서버 간 클러스터 읽기 작업 payload(별도로 명세되지 않은 `ReadTaskResponse` body)에 추가되는 필드가 결정됩니다. 버전 `7`은 비공개 저장소 기능(Iceberg 컴팩션)용으로 예약되어 있으며, `8`에서는 선택적 `read_source_index`가 추가됩니다. 외부 클라이언트는 클러스터 읽기에 참여하지 않으므로 이 필드를 디코딩한 후 무시합니다. |

**Rule** — `password_complexity_rules`의 요소:

| # | 필드 | 유형 | 설명 |
| - | - | - | - |
| 1 | pattern | String | 정책을 준수하는 비밀번호가 일치해야 하는 정규식 패턴입니다. |
| 2 | message | String | 비밀번호가 이 규칙을 충족하지 못할 때 표시되는, 사람이 읽을 수 있는 설명입니다. |

이 목록은 서버 운영자가 구성한 비밀번호 정책을 반영하며 권고용일 뿐입니다. 서버는 핸드셰이크 중에 이 규칙을 강제하지 않습니다. 비밀번호 변경/설정 기능을 제공하는 클라이언트는 이 규칙을 활용해, 규칙에 맞지 않는 비밀번호를 서버로 보내기 전에 오류를 미리 표시할 수 있습니다.

> [!NOTE]
>
> 악의적이거나 잘못 구성된 서버로 인한 과도한 리소스 사용을 막으려면 디코딩된 `count`는 최대 256개 항목으로, 각 `pattern` 및 `message` String은 최대 4096바이트로 제한하십시오. 비밀번호 정책이 구성되지 않은 서버에서는 일반적으로 `count`가 `0`입니다(뒤따르는 쌍 없음).

### Addendum (패킷 유형 없음)

Client → Server 방향이며, `ADDENDUM`(v54458)으로 게이트됩니다. 핸드셰이크 교환이 완료된 직후에 전송됩니다. 별도의 패킷 유형이 아니므로, 필드는 패킷 유형 바이트 접두사 없이 원시 형태 그대로 wire로 전송됩니다.

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | quota\_key | String | 공통 | 항상 | 서버 측 키 기반 쿼터에 사용하는 리소스 쿼터 키입니다. 키 기반 쿼터를 사용하지 않는 클라이언트는 빈 문자열을 전송합니다. |
| 2 | proto\_send\_chunked | String | 공통 | CHUNKED\_PROTOCOL (v54470) | 클라이언트가 협상한 아웃바운드 청킹 방식으로, 값은 `"chunked"` 또는 `"notchunked"`입니다. ServerHello의 `proto_recv_chunked_srv`를 기준으로 결정됩니다. |
| 3 | proto\_recv\_chunked | String | 공통 | CHUNKED\_PROTOCOL (v54470) | 클라이언트가 협상한 인바운드 청킹 방식입니다. `proto_send_chunked_srv`를 기준으로 결정됩니다. |
| 4 | parallel\_replicas\_protocol\_version | VarUInt | 공통 | VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL (v54471) | 클라이언트가 지원하는 병렬 레플리카 coordination 프로토콜 버전입니다. 분산 쿼리에 참여하지 않는 외부 클라이언트라도 서버의 compatibility 검사를 통과할 수 있도록 유효한 버전(현재 `8`)을 전송해야 합니다(SHOULD). |

청크 프레이밍으로의 전환은 이 Addendum이 플러시된 *이후*에 적용되며, Addendum 자체는 프레이밍되지 않습니다.

### Ping (패킷 유형 4)

Client → Server. body는 없습니다. 청크 프레이밍을 적용하기 전에는 이 패킷이 단일 바이트 `0x04`로만 구성되며, 청킹(chunking)이 협상되면 이 바이트가 청크의 1바이트 payload가 됩니다([청크 프레이밍](#chunked-framing) 참조).

### Pong (패킷 유형 4)

Server → Client. body가 없습니다. 이 패킷은 청크 프레이밍 적용 전에는 단일 바이트 `0x04`이며, 청킹이 협상되면 이 바이트가 청크의 1바이트 payload가 됩니다([청크 프레이밍](#chunked-framing) 참조).

### Exception (패킷 유형 2)

Server → Client. 어느 단계에서든 서버에서 오류가 발생하면 전송됩니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | code | Int32 | 공통 | 오류 코드 |
| 2 | name | String | 공통 | 예외 클래스(예: `"DB::Exception"`) |
| 3 | message | String | 공통 | 사람이 읽을 수 있는 형태의 오류 메시지 |
| 4 | stack\_trace | String | 공통 | 서버 측 스택 트레이스 |
| 5 | has\_nested (obsolete) | Bool | 공통 | 더 이상 사용되지 않는 호환성용 바이트입니다. 서버는 항상 `false`로 기록합니다 |

### Query (패킷 유형 1)

Client → Server.

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | query\_id | String | 공통 | 항상 | 고유한 쿼리 식별자(UUID) |
| 2 | client\_info | ClientInfo | 공통 | CLIENT\_INFO (v54032) | [ClientInfo](#clientinfo)를 참조하십시오 |
| 3 | settings | Setting\[] | 공통 | 항상 | [Setting](#setting)을 참조하십시오. **항상 포함됩니다**(빈 키로 종료됨). 버전에 따라 달라지는 것은 설정별 *인코딩*뿐이며, 자세한 내용은 [Setting](#setting)의 인코딩 관련 참고 사항을 확인하십시오. 협상된 버전이 `54429` 미만인 경우에도 클라이언트는 이 필드를 생략하면 안 됩니다. |
| 3a | external\_roles | String | 공통 | INTERSERVER\_EXTERNALLY\_GRANTED\_ROLES (v54472) | 외부에서 부여된 역할 이름을 직렬화한 목록입니다. 빈 목록은 바이트 `0x00`(VarUInt 0)을 String 엔벌로프로 감싼 형태입니다(전송 시 `[VarUInt 1][0x00]`). 외부 클라이언트는 항상 빈 값을 전송합니다. |
| 4 | auth\_hash | String | 서버 간 | INTERSERVER\_SECRET (v54441) | 서버 간 인증 해시이며, 원시 클러스터 시크릿이 **아닙니다**. 아래의 [서버 간 인증](#inter-server-authentication)을 참조하십시오. 외부 클라이언트(및 모든 `InitialQuery`)는 빈 문자열을 전송합니다. |
| 5 | stage | VarUInt | 공통 | 항상 | 쿼리 처리 스테이지입니다. `0` = FetchColumns, `1` = WithMergeableState, `2` = Complete, `3` = WithMergeableStateAfterAggregation, `4` = WithMergeableStateAfterAggregationAndLimit, `7` = QueryPlan. 값 `3`/`4`는 분산 쿼리에서 사용되며, `7`은 직렬화된 쿼리 계획과 함께 전송됩니다. 외부 클라이언트는 일반적으로 `2`를 전송합니다. |
| 6 | compression | VarUInt | 공통 | 항상 | 0 = 비활성화, 1 = 활성화 |
| 7 | query\_body | String | 공통 | 항상 | SQL 텍스트 |
| 8 | parameters | Parameter\[] | 클라이언트 | PARAMETERS (v54459) | [Parameter](#parameter)를 참조하십시오. 빈 키로 종료됩니다. |

### ClientInfo (Query에 포함)

Client → Server 방향이며, Query body(필드 2)에 포함됩니다. `CLIENT_INFO`(v54032)부터 사용할 수 있습니다. (ClientInfo 내부의 일부 필드는 아래 필드별로 명시된 것처럼 이후 버전부터 사용할 수 있습니다.)

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | query\_kind | UInt8 | 공통 | 항상 | 0 = NoQuery, 1 = InitialQuery, 2 = SecondaryQuery. 외부 클라이언트는 `1`을 보냅니다. |
| 2 | initial\_user | String | 공통 | 항상 | 쿼리를 시작한 사용자 |
| 3 | initial\_query\_id | String | 공통 | 항상 | 최초 쿼리 ID |
| 4 | initial\_address | String | 공통 | 항상 | 요청을 처음 보낸 클라이언트의 소켓 주소입니다. 서버는 이 값을 절대 해석하지 않습니다(호스트명이나 서비스 이름을 조회하지 않음). `SECONDARY_QUERY`에서는 이 값이 보관되어 `system.query_log`, 서버 간 인증 등에 사용되며, 허용되는 형식은 IPv4 `a.b.c.d:port` 또는 대괄호로 감싼 IPv6 `[addr]:port`입니다. 이때 호스트는 IP 리터럴이어야 하고 포트는 `0..65535` 범위의 10진수여야 합니다. 그 밖의 형식(예: `localhost:9000`, `host:http`, `:9000` 또는 `/tmp/ch.sock` 같은 UNIX 소켓 경로)은 `INCORRECT_DATA` 오류와 함께 거부됩니다. `INITIAL_QUERY`에서는 서버가 이 필드를 실제 피어 주소로 덮어쓰므로 어떤 값이든 허용됩니다(단순한 `ip:port` 형식이 아닌 값은 기본값 `0.0.0.0:0`으로 대체됩니다). 외부 클라이언트는 자신의 `ip:port`를 전송해야 합니다. |
| 5 | initial\_time | Int64 | 클라이언트 | INITIAL\_QUERY\_START\_TIME (v54449) | 쿼리 시작 시간(마이크로초). VarUInt가 아닌 고정 길이 8바이트 |
| 6 | query\_interface | UInt8 | 공통 | 항상 | 1 = TCP, 2 = HTTP |
| 7 | os\_user | String | 클라이언트 | interface = TCP일 때 | OS 사용자 이름 |
| 8 | client\_hostname | String | 클라이언트 | interface = TCP일 때 | 클라이언트 컴퓨터의 호스트명 |
| 9 | client\_name | String | 클라이언트 | interface = TCP일 때 | 클라이언트 애플리케이션 이름 |
| 10 | version\_major | VarUInt | 공통 | interface = TCP일 때 | 클라이언트 메이저 버전 |
| 11 | version\_minor | VarUInt | 공통 | interface = TCP일 때 | 클라이언트 마이너 버전 |
| 12 | protocol\_version | VarUInt | 공통 | interface = TCP인 경우 | 요청을 시작한 클라이언트 자체의 TCP 프로토콜 버전(`DBMS_TCP_PROTOCOL_VERSION`)이며, 협상된 버전이 **아닙니다**. 상대측 revision은 어떤 필드가 포함될지만 결정합니다. 이 값은 initiator에 컴파일 시점에 내장된 버전이므로, 최신 클라이언트가 이전 버전의 서버와 통신할 때는 협상된 revision 또는 서버 revision보다 높을 수 있습니다. |
| 13 | quota\_key | String | 공통 | QUOTA\_KEY\_IN\_CLIENT\_INFO (v54060) | 서버 측 키 기반 쿼터에 사용하는 리소스 쿼터 키입니다. 키 기반 쿼터를 사용하지 않는 클라이언트는 빈 문자열을 전송합니다. |
| 14 | distributed\_depth | VarUInt | 서버 간 | DISTRIBUTED\_DEPTH (v54448) | 분산 쿼리의 중첩 깊이입니다. 외부 클라이언트는 `0`을 전송합니다. |
| 15 | version\_patch | VarUInt | 공통 | VERSION\_PATCH (v54401), TCP 전용 | 클라이언트 패치 버전 |
| 16 | open\_telemetry | (아래 참조) | 클라이언트 | OPEN\_TELEMETRY (v54442) | 추적 컨텍스트입니다. 추적(tracing)을 사용하지 않는 클라이언트는 `0`을 전송합니다. |
| 17 | collaborate\_with\_initiator | VarUInt | 서버 간 | PARALLEL\_REPLICAS (v54453) | VarUInt로 인코딩된 Bool 값입니다. 외부 클라이언트는 `0`을 전송합니다. |
| 18 | count\_participating\_replicas | VarUInt | 서버 간 | PARALLEL\_REPLICAS (v54453) | 외부 클라이언트는 `0`을 전송합니다. |
| 19 | number\_of\_current\_replica | VarUInt | 서버 간 | PARALLEL\_REPLICAS (v54453) | 외부 클라이언트는 `0`을 전송합니다. |
| 20 | script\_query\_number | VarUInt | 클라이언트 | QUERY\_AND\_LINE\_NUMBERS (v54475) | 여러 SQL 문으로 이루어진 스크립트에서 해당 SQL 문의 위치(1부터 시작)입니다. 외부 클라이언트는 `0`을 전송합니다. |
| 21 | script\_line\_number | VarUInt | 클라이언트 | QUERY\_AND\_LINE\_NUMBERS (v54475) | 원본 스크립트 내의 줄 번호로, 1부터 시작합니다. 외부 클라이언트는 `0`을 전송합니다. |
| 22 | jwt\_present | UInt8 | 서버 간 | JWT\_IN\_INTERSERVER (v54476) | `0` = JWT 없음, `1` = 뒤에 JWT가 이어짐. JWT 인증을 사용하지 않는 외부 클라이언트는 `0`을 전송합니다. |
| 23 | jwt | String | 서버 간 | JWT\_IN\_INTERSERVER (v54476), jwt\_present=1일 때 | JWT bearer 토큰입니다. 필드 22가 `1`인 경우에만 포함됩니다. |
| 24 | client\_agent | String | 클라이언트 | CLIENT\_AGENT\_IN\_CLIENT\_INFO (v54485) | 후행 필드입니다. 클라이언트 도구/에이전트의 식별자로, 실행 환경에서 자동으로 감지됩니다(예: `claude-code`, `cursor`, `gemini-cli` 또는 `AGENT` 환경 변수). 에이전트가 감지되지 않은 외부 클라이언트는 빈 문자열을 전송합니다. 협상된 버전(negotiated version)이 54485 이상이면 일반 Query 경로에 포함됩니다(TCP뿐 아니라 모든 인터페이스에서 전송됨). |
| 25 | is\_internal | UInt8 | 클라이언트 | INTERNAL\_QUERY\_FLAG (v54486) | 후행 필드입니다. 서버 내부 쿼리(사용자가 실행하지 않은 쿼리)이면 `1`이며, 이 값은 원격 쿼리로 전파되어 `system.query_log`에서 해당 쿼리를 내부 쿼리로 표시하는 데 사용됩니다. `query_kind`(필드 1)와는 별개입니다. 외부 클라이언트는 `0`을 전송합니다. 협상된 버전이 54486 이상이면 포함됩니다(TCP뿐 아니라 모든 인터페이스에서 전송됨). |
| 26 | current\_roles | UInt8 \[+ String 목록] | 서버 간 | INTERSERVER\_CURRENT\_ROLES (v54488) | 후행 필드입니다. `[UInt8 present]`가 먼저 오고, `present = 1`이면 initiator의 활성화된(enabled) 역할 이름 목록인 `[VarUInt count][String]*count`가 이어집니다. 이 필드를 통해 secondary 노드는 사용자의 기본 역할 대신 initiator의 역할을 기준으로 row policy의 범위를 지정할 수 있습니다. 서버 간 쿼리이면서 `push_external_roles_in_interserver_queries = 1`인 경우에만 값이 채워지며, 그 외의 경우와 외부 클라이언트에서는 `present = 0`입니다. 협상된 버전이 54488 이상이면 존재 여부 바이트가 필수입니다(TCP뿐 아니라 모든 인터페이스에서 전송됨). 서버 간 쿼리에서는 직렬화된 목록이 `auth_hash`에도 반영됩니다([서버 간 인증](#inter-server-authentication) 참조). |

> [!NOTE]
>
> **인터페이스에 따라 달라지는 레이아웃(필드 7–12)**
>
> 위의 필드 7–12는 **TCP** 분기입니다. `query_interface`(필드 6)가 TCP가 **아닌** 경우 이 필드들은 다른 wire 레이아웃으로 *대체됩니다*. 단순히 선택적 필드가 생략되는 것이 아니므로, 디코더는 필드 6의 값에 따라 분기해야 합니다.
>
> * `query_interface = 2`(**HTTP**): 대신 서버가 전달한 HTTP 요청 정보가 기록됩니다. 순서는 `http_method`(`UInt8`), `http_user_agent`(`String`), `forwarded_for`(`String`, `X_FORWARDED_FOR_IN_CLIENT_INFO` v54443 게이트 적용), `http_referer`(`String`, `REFERER_IN_CLIENT_INFO` v54447 게이트 적용)이며, 마지막으로 `http_handler_name`(`String`)과 `http_request_url`(`String`)이 이 순서대로 기록됩니다(둘 다 `HTTP_HANDLER_IN_CLIENT_INFO` v54490 게이트 적용). `os_user`/`client_hostname`/`client_name`/`version_*`/`protocol_version` 필드는 포함되지 않습니다. 마지막 두 필드는 매칭된 SQL 정의 HTTP 핸들러 이름과 요청 URL을 담고 있어 원격 세그먼트까지 그대로 전달됩니다(핸들러를 사용하지 않는 HTTP 쿼리에서는 비어 있음).
> * 그 밖의 인터페이스: TCP 필드(7–12)와 HTTP 필드 모두 기록되지 않으며, 스트림은 곧바로 `quota_key`로 이어집니다.
>
> 이 분기가 끝나면 레이아웃은 다시 하나로 합쳐집니다. 모든 인터페이스에서 `quota_key`(필드 13)와 `distributed_depth`(필드 14)가 이어지고, `version_patch`(필드 15)는 TCP에서만 기록됩니다.
>
> 이 분기는 주로 서버 간 트래픽, 즉 시작 서버(initiating server)가 원래 HTTP로 수신한 쿼리를 전달하는 상황에서 중요합니다. 항상 TCP 필드를 읽는 디코더는 이러한 패킷을 잘못 해석하여 `http_method`나 `http_user_agent`를 `quota_key`로 처리하게 됩니다.

OpenTelemetry 인코딩(필드 16):

```text theme={null}
[UInt8: has_trace]              0 = no trace data follows, 1 = trace data follows
If has_trace == 1:
  [16 bytes: trace_id]          byte-swapped per-8-bytes
  [8 bytes:  span_id]           byte-swapped
  [String:   trace_state]       W3C trace state
  [UInt8:    trace_flags]       W3C trace flags
```

### 서버 간 인증

Query 필드 4(`auth_hash`)는 wire 상에서 공유 클러스터 시크릿(cluster secret) 자체가 **아닙니다**. 원시 시크릿을 그대로 전송하면 인증에 실패할 뿐만 아니라 시크릿도 유출됩니다. 대신 서버 간 클라이언트로 동작하는 서버는 솔트를 적용한 SHA-256 해시로 자신이 시크릿을 알고 있음을 증명합니다.

1. **서버 간 모드 진입.** 연결하는 서버는 `ClientHello` 안에서 이를 알립니다. 이때 `user` 필드에는 서버 간 마커가 들어가고 `password`는 비어 있습니다. 이어서 같은 `ClientHello` 패킷 안에서 `user`/`password` 필드 바로 뒤에 두 개의 문자열, 즉 클러스터 이름과 새로 생성한 32바이트 `salt`(임의 값의 `encodeSHA256`)를 추가합니다. 서버는 `ServerHello`를 보내기 **전에** 이 두 문자열을 읽으므로 클라이언트는 이를 미리 기록해야 합니다. `ServerHello`를 먼저 기다리면 서버가 이 문자열을 읽느라 대기 상태에 있으므로 교착 상태(deadlock)가 발생합니다.
2. **논스 획득.** `INTERSERVER_SECRET_V2`(v54462)가 협상되면 `ServerHello`에 8바이트 `UInt64` 논스가 포함됩니다.
3. **해시 계산.** `InitialQuery`가 아닌 모든 Query 패킷에 대해 클라이언트는 필드 4에 `encodeSHA256(salt + nonce + cluster_secret + query + query_id + initial_user + external_roles + current_roles)`, 즉 32바이트 다이제스트를 기록합니다. (`nonce`는 10진수 문자열 형식이며 v54462 이상으로 협상된 경우에만 포함됩니다. `external_roles`는 `INTERSERVER_EXTERNALLY_GRANTED_ROLES`(v54472)가 협상된 경우에만 추가됩니다. `current_roles`는 직렬화된 역할 이름 목록(존재 여부 바이트를 제외한 `[VarUInt count][String]*count`)이며, `INTERSERVER_CURRENT_ROLES`(v54488)가 협상되고 목록이 있는 경우에만 추가됩니다.) `InitialQuery`이거나 클러스터 시크릿이 구성되지 않은 경우에는 클라이언트가 빈 문자열을 대신 기록합니다.
4. **검증.** 서버는 최대 32바이트까지 필드 4를 읽고, 자신이 보유한 클러스터 시크릿으로 동일한 값을 이어 붙여 해시를 다시 계산합니다. 다이제스트가 일치하지 않으면 연결을 거부합니다.

외부(서버 간이 아닌) 클라이언트는 이 모드에 진입하지 않으며 항상 빈 `auth_hash`를 전송합니다.

### Setting

Query body의 설정 목록([Query](#query) 패킷의 필드 3)에 인라인으로 인코딩됩니다. 이 목록은 협상된 버전과 관계없이 **항상 존재하며**, 키가 비어 있는 Setting으로 끝납니다. 이 종료 표시는 단일 `VarUInt 0`이며, 그 뒤에는 flags나 value가 오지 않습니다. 협상된 버전에 따라 달라지는 것은 설정별 인코딩 방식뿐이며, 이는 `SETTINGS_SERIALIZED_AS_STRINGS`(v54429)로 결정됩니다.

**v54429+ (`STRINGS_WITH_FLAGS`)** — 각 설정은 다음 3개 항목으로 구성됩니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | key | String | 공통 | 설정 이름입니다. 비어 있으면 목록의 끝을 의미합니다. |
| 2 | flags | VarUInt | 공통 | 메타데이터 비트 플래그입니다. 아래를 참조하십시오. |
| 3 | value | String | 공통 | 문자열로 표현한 설정 값입니다 |

`key`가 비어 있으면 필드 2와 3은 존재하지 않습니다.

**Pre-54429 (`BINARY`)** — 각 설정은 `[String key][type-specific binary value]` 형식입니다. `flags` 필드는 기록되지 **않으며**, 값은 10진수/텍스트 문자열이 아니라 해당 설정의 네이티브 바이너리 형식(예: 고정 폭 정수 또는 길이 접두사가 붙은 문자열)으로 인코딩됩니다. 이 경우에도 목록은 빈 `key`로 끝납니다. 협상된 버전이 `54429` 미만인 환경을 대상으로 하는 클라이언트는 위의 3개 항목 형식이 아니라 이 바이너리 형식으로 읽고 써야 합니다. (단, 사용자 정의 설정은 예외로, 두 인코딩 모두에서 항상 `flags`와 문자열 값을 포함합니다.)

`flags` 필드에는 다음 값이 포함됩니다.

* `0x01` — **Important**: 쿼리 결과에 영향을 주는 설정이므로 이전 버전의 피어가 이를 임의로 무시해서는 안 됩니다.
* `0x02` — **Custom**: 사용자 지정 설정입니다.
* `0x0c` — 독립적인 플래그가 아니라 **2비트 등급(tier)** 필드입니다: `0x00` = Production, `0x04` = Obsolete, `0x08` = Experimental, `0x0c` = Beta. 반드시 2비트 전체(`flags & 0x0c`)를 읽어야 합니다. 단순히 `flags & 0x04`로 검사하면 Beta(`0x0c`)를 Obsolete로 잘못 분류하게 됩니다.
* `0x80` — **HotReload**(재시작 없이 구성을 다시 로드함. flags enum에 정의되어 있으며, 주로 조정(coordination) 설정에서 사용됩니다).

### Parameter

`SELECT {x:UInt64}`와 같은 매개변수화된 쿼리에 사용되는 쿼리 매개변수입니다. `Custom` 플래그(`0x02`)가 설정된 [Setting](#setting)과 동일한 방식으로 인코딩되며, 마찬가지로 빈 키로 종료됩니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | key | String | 클라이언트 | 매개변수 이름입니다. 빈 값은 목록의 끝을 의미합니다. |
| 2 | flags | VarUInt | 클라이언트 | 항상 `0x02` (Custom) |
| 3 | value | String | 클라이언트 | 문자열로 표현된 매개변수 값입니다. 따옴표 처리에 관해서는 아래 참고 사항을 확인하십시오. |

> [!NOTE]
>
> 매개변수 값은 원시 리터럴이 아니라 값의 SQL 표현입니다. String 타입 매개변수는 작은따옴표로 감싼 형태로 전달해야 합니다(예: `{name:String}`의 값은 `Alice`가 아니라 `'Alice'`). 그렇지 않으면 서버의 값 파서가 해당 값을 거부합니다.

### 데이터(Data) (패킷 유형 1 서버→클라이언트, 패킷 유형 2 클라이언트→서버)

양방향으로 사용됩니다. 결과 블록, INSERT 데이터, 외부 테이블(external table), 데이터 종료 마커를 전달합니다.

wire 형식은 양방향이 대칭입니다. 두 방향 모두 Block 앞에 `table_name` 접두사가 붙으며, 패킷 유형 바이트만 다릅니다.

```text theme={null}
[VarUInt: packet_type]     1 (server→client) or 2 (client→server)
[String:  table_name]      External table name; empty in most cases
[Block]                    See the Native Format spec for the Block layout
```

| 필드 | 유형 | 역할 | 설명 |
| - | - | - | - |
| table\_name | String | 공통 | 외부 테이블(External table) 이름입니다. 메인 테이블, 쿼리 결과, INSERT 행 스트림에서는 일반적으로 빈 값(`""`)을 사용합니다. 빈 `table_name`만으로는 데이터 종료 마커가 **되지 않습니다**(일반 INSERT 행 패킷도 `""`를 전달합니다). 비어 있지 않은 이름은 한 쿼리 내의 여러 클라이언트 `Data` 패킷에서 반복해서 나타날 수 있습니다. 해당 이름을 가진 첫 번째 패킷이 외부 테이블을 생성하고 스키마를 바인딩하며(컬럼은 있지만 `num_rows = 0`인 패킷도 포함됩니다), 이후 같은 이름을 가진 각 패킷은 동일한 컬럼명과 타입을 동일한 순서로 선언해야 합니다. 서버는 블록 헤더를 바인딩된 스키마와 비교하여 일치하지 않으면 `Exception`(`INCORRECT_DATA`)으로 응답합니다. |
| 블록 body | — | — | [블록 및 컬럼 구조](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#block-and-column-structure)를 참조하십시오. |

**데이터 종료 마커**는 `table_name`과 관계없이 블록이 비어 있는(컬럼 `0`개, 행 `0`개) 패킷입니다. 서버는 디코딩된 블록이 비어 있을 때(`block.empty()`)에만 클라이언트 `Data` 패킷을 종료 신호로 처리합니다. `table_name = ""`이더라도 블록이 비어 있지 않은 패킷은 종료 신호가 아닌 일반 행 패킷입니다. 따라서 INSERT 행 스트림은 비어 있지 않은 `Data` 블록의 시퀀스와 그 끝을 알리는 하나의 빈 `Data` 블록으로 구성됩니다.

블록 변형과 각각의 의미는 [블록 변형](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#block-variants)을 참조하십시오.

### Progress (패킷 유형 3)

Server → Client. 쿼리 실행 중에 주기적으로 전송됩니다. 모든 필드는 VarUInt이며, 각 패킷에는 누적 합계가 아니라 **이전 `Progress` 패킷 이후의 증가분**이 담깁니다. 서버는 전송하기 전에 카운터를 읽은 뒤 원자적으로 0으로 초기화하고, 마지막 전송 이후 경과한 시간을 `elapsed_ns`로 계산합니다. 따라서 클라이언트는 현재까지의 합계를 구하려면 연속된 패킷을 로컬에서 **반드시 누적해야** 합니다. 패킷 값을 절댓값으로 취급하면 패킷이 두 개 이상 도착하는 순간부터 진행 상황 표시가 뒤로 되돌아가거나 실제보다 적게 집계됩니다.

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | rows | VarUInt | 공통 | 항상 | 이전 패킷 이후 읽은 행 수(현재 합계에 더함) |
| 2 | bytes | VarUInt | 공통 | 항상 | 이전 패킷 이후 읽은 바이트 수(현재 합계에 더함) |
| 3 | total\_rows | VarUInt | 공통 | 항상 | 읽어야 할 예상 총 행 수의 증가분으로, 누적해야 합니다(패킷에 따라 0일 수 있음) |
| 4 | total\_bytes | VarUInt | 공통 | TOTAL\_BYTES\_IN\_PROGRESS (v54463) | 읽어야 할 예상 총 바이트 수의 증가분으로, 누적해야 합니다. wire 상에서는 `total_rows`와 `wrote_rows` 사이에 위치합니다. |
| 5 | wrote\_rows | VarUInt | 공통 | WRITE\_CLIENT\_INFO (v54420) | 이전 패킷 이후 기록된 행 수(INSERT 시)로, 누적해야 합니다 |
| 6 | wrote\_bytes | VarUInt | 공통 | WRITE\_CLIENT\_INFO (v54420) | 이전 패킷 이후 기록된 바이트 수(INSERT 시)로, 누적해야 합니다 |
| 7 | elapsed\_ns | VarUInt | 공통 | SERVER\_QUERY\_TIME\_IN\_PROGRESS (v54460) | 이전 패킷 이후 경과한 나노초(총 쿼리 시간이 아닌 차이값)로, 누적해야 합니다 |

### ProfileInfo (패킷 유형 6)

Server → Client. 쿼리당 한 번, 실행이 끝나갈 무렵 전송됩니다.

| # | 필드 | 유형 | 역할 | 조건 | 설명 |
| - | - | - | - | - | - |
| 1 | rows | VarUInt | 공통 | 항상 | 처리된 전체 행 수 |
| 2 | blocks | VarUInt | 공통 | 항상 | 처리된 전체 블록 수 |
| 3 | bytes | VarUInt | 공통 | 항상 | 처리된 전체 바이트 수 |
| 4 | applied\_limit | Bool | 공통 | 항상 | LIMIT 절 적용 여부 |
| 5 | rows\_before\_limit | VarUInt | 공통 | 항상 | LIMIT 적용 전 행 수 |
| 6 | *obsolete* | Bool | 공통 | 항상 | 더 이상 사용되지 않는 호환성용 바이트입니다. 서버는 이 위치에 항상 `true`를 기록하고, 클라이언트는 읽은 뒤 이 값을 버립니다. 이 값은 "`rows_before_limit`이 계산되었음"을 나타내는 플래그가 **아닙니다**. 실제 limit 상태는 필드 4(`applied_limit`)와 필드 5를 함께 보고 판단합니다. 읽은 후 무시하십시오. |
| 7 | applied\_aggregation | Bool | 공통 | ROWS\_BEFORE\_AGGREGATION (v54469) | GROUP BY 적용 여부 |
| 8 | rows\_before\_aggregation | VarUInt | 공통 | ROWS\_BEFORE\_AGGREGATION (v54469) | 집계 전 행 수 |

### Totals (패킷 유형 7)

Server → Client. `WITH TOTALS`를 사용하는 쿼리에 대해 전송됩니다. wire 형식은 [Data](#data)와 동일하며, `table_name` 문자열(항상 비어 있음) 뒤에 Block이 이어지는 구조입니다. 패킷 유형 바이트만 다릅니다.

```text theme={null}
[VarUInt: 7]                packet type
[String:  table_name]       always empty
[Block]                     see the Native Format spec
```

### Extremes (패킷 유형 8)

Server → Client. `extremes` 설정이 활성화되면 전송됩니다. wire 형식은 [Data](#data)와 동일합니다. 블록은 정확히 2개의 행으로 구성되며, 0번 행에는 각 컬럼의 최솟값이, 1번 행에는 최댓값이 담깁니다.

```text theme={null}
[VarUInt: 8]                packet type
[String:  table_name]       always empty
[Block]                     num_rows = 2
```

### Log (패킷 유형 10)

Server → Client. 쿼리에 활성 로그 큐가 있을 때 전송됩니다(`send_logs_level` 설정으로 지정되며, 자세한 내용은 [로그 스트리밍](#log-streaming)을 참고하십시오).

[Data](#data)와 동일한 엔벌로프 및 body 포맷을 사용합니다. 블록은 `num_columns = 8`로 고정되어 있으며 미리 정의된 스키마를 따릅니다. 각 로그 라인은 8개 컬럼 전체에 걸친 하나의 행에 해당하며, 하나의 Log 패킷에 여러 행이 담길 수 있습니다.

```text theme={null}
[VarUInt: 10]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 8, num_rows = number of log lines
```

8개의 컬럼은 정확히 다음 순서로 구성됩니다.

| # | 이름 | 유형 | 설명 |
| - | - | - | - |
| 1 | event\_time | DateTime | 이벤트 타임스탬프(epoch 이후 경과한 초) |
| 2 | event\_time\_microseconds | UInt32 | 마이크로초 부분 |
| 3 | host\_name | String | 로그를 생성한 서버 호스트명 |
| 4 | query\_id | String | 로그가 속한 쿼리 ID |
| 5 | thread\_id | UInt64 | OS 스레드 ID |
| 6 | priority | Int8 | 로그 레벨(Poco priority: 1 = Fatal, … 8 = Trace, 9 = Test) |
| 7 | source | String | 로거 이름 |
| 8 | text | String | 로그 메시지 텍스트 |

### ProfileEvents (패킷 유형 14)

Server → Client. 쿼리별 성능 카운터를 전달합니다.

엔벌로프와 body 포맷은 [Data](#data)와 동일합니다. 블록은 `num_columns = 6`으로 고정되어 있으며 미리 정의된 스키마를 사용합니다. 각 이벤트는 하나의 행에 해당합니다.

```text theme={null}
[VarUInt: 14]               packet type
[String:  table_name]       always empty
[Block]                     num_columns = 6, num_rows = number of events
```

다음 6개의 컬럼으로 구성됩니다.

| # | 이름 | 유형 | 설명 |
| - | - | - | - |
| 1 | host\_name | String | 서버 호스트명 |
| 2 | current\_time | DateTime | 이벤트 타임스탬프 |
| 3 | thread\_id | UInt64 | 스레드 ID |
| 4 | type | Enum8 | 이벤트 유형: 1 = Increment(카운터), 2 = Gauge. 내부적으로는 부호 있는 1바이트로 저장됩니다. |
| 5 | name | String | 이벤트 이름(예: `"Query"`, `"NetworkReceiveBytes"`) |
| 6 | value | Int64 | 카운터 값 또는 게이지 측정값 |

> [!NOTE]
>
> `value` 컬럼의 원소 타입은 패킷마다 동일하지 않습니다. 이전 버전 서버는 `UInt64`를, 최신 버전 서버는 `Int64`를 전송합니다. 특정 크기를 가정하지 말고 블록 헤더에서 해당 컬럼의 타입 문자열을 읽으십시오.

### TableColumns (패킷 유형 11)

Server → Client 방향이며, `COLUMN_DEFAULTS_METADATA`(v54410)로 활성화됩니다. 서버는 컬럼 기본값 메타데이터를 전달하기 위해 INSERT 스키마 블록보다 먼저 이 패킷을 전송합니다. 단, 협상된 버전(negotiated version)이 54410 이상이고 **동시에** `input_format_defaults_for_omitted_fields` 설정이 활성화된 경우에만 전송합니다. 54410 미만에서는 이 패킷이 전송되지 않으므로, 이전 버전의 클라이언트는 이 패킷을 기다려서는 **안 됩니다**. 이 경우 스키마 `Data` 블록이 바로 전송됩니다. v54410 이상의 클라이언트는 두 가지 순서에 모두 대비해야 합니다. 즉, 선택적으로 `TableColumns`가 먼저 오고 그다음에 스키마 블록이 올 수 있습니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | external\_table | String | 공통 | 외부 테이블 이름입니다. 비어 있으면 메인 테이블을 의미합니다. |
| 2 | columns\_description | String | 공통 | 텍스트 형식의 컬럼 정의입니다(예: `"id Int32, name String DEFAULT ''"`). 자유 형식 텍스트이므로 문자열로 파싱하십시오. |

> [!NOTE]
>
> **v54481 이상에서 압축되는 body**
>
> 협상된 버전이 54481 이상(`COMPRESSED_LOGS_PROFILE_EVENTS_COLUMNS`)이면 서버는 **두** 필드를 모두 동일한 선택적 압축 출력 경로로 기록합니다. 따라서 쿼리에 `compression = true`가 설정된 경우 `TableColumns` body 전체(`external_table` + `columns_description`)가 [압축 프레임](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#compression-frame) 안에 포함되며, 클라이언트는 이에 대응하는 압축 해제 스트림을 통해 body를 읽습니다. 쿼리에 압축이 적용되지 않으면 body는 위 표에 나온 그대로 압축되지 않은 상태로 전송됩니다. 이는 `INSERT` 스키마 응답에서 특히 중요합니다. `Log`와 `ProfileEvents`에만 압축 처리를 전환하고 `TableColumns`에는 전환하지 않는 클라이언트는 쿼리 압축이 활성화된 경우 응답을 잘못 읽게 됩니다.

### TimezoneUpdate (패킷 유형 17)

Server → Client 방향이며, `TIMEZONE_UPDATES`(v54464) 게이트가 적용됩니다. 이 패킷이 전송되는 곳은 단 한 곳, 바로 `input` 테이블 함수의 초기화 과정입니다(클라이언트로부터 행을 스트리밍하는 `INSERT INTO <table> SELECT ... FROM input('<structure>')` 형태의 쿼리). 서버는 입력 스키마 `Data` 블록을 전송한 직후([INSERT 단계](#insert-phase) 참조), 쿼리 Context의 현재 `session_timezone`을 담은 `TimezoneUpdate`를 전송합니다. 이를 통해 클라이언트는 곧 전송할 행을 동일한 시간대 기준으로 파싱할 수 있습니다. 쿼리 도중 임의로 실행된 `SET session_timezone` 변경에 대해서는 서버가 이 패킷을 전송하지 **않으며**, 이후 결과 블록의 포맷 방식을 클라이언트에 알리는 용도로도 사용되지 않습니다.

| # | 필드 | 유형 | 역할 | 설명 |
| - | - | - | - | - |
| 1 | timezone | String | 공통 | 새 세션 기본 시간대(예: `"UTC"`, `"Europe/Berlin"`). |

이 패킷은 입력 스키마 블록 직후, 클라이언트가 행 블록 전송을 시작하기 전에 한 번만 도착합니다. `TimezoneUpdate`를 무시하는 디코더라도 wire 정렬을 유지하려면 뒤따르는 `String`을 반드시 읽어서 소비해야 합니다.

### SSH 챌린지-응답 인증 (패킷 유형 11, 12, 18)

`SSH_AUTHENTICATION`(v54466)으로 제어되며, 명시적으로 활성화한 경우에만 사용됩니다. ClientHello가 `user = " SSH KEY AUTHENTICATION " + <real_user>`(앞뒤 공백 포함)와 `password = ""`를 전송하면 연결이 SSH 흐름으로 진입합니다. 서버는 접두사를 읽은 뒤 이를 제거하여 실제 사용자를 복원하고, 챌린지-응답 방식으로 전환합니다.

| 패킷 | 코드 | 방향 | body |
| - | - | - | - |
| SSHChallengeRequest | 11 | 클라이언트 → 서버 | (body 없음) |
| SSHChallenge | 18 | 서버 → 클라이언트 | `String challenge` — 무작위 바이트이며, 서명 대상 문자열을 구성하는 요소 중 하나입니다(아래 참조) |
| SSHChallengeResponse | 12 | 클라이언트 → 서버 | `String signature` — 원시 챌린지가 **아니라** 아래에 정의된 연결(concatenation) 문자열에 대한 SSH 서명 |

이 흐름은 비밀번호 인증 대신 실행되며, 챌린지-응답 교환은 ServerHello **이전에** 이루어집니다. 서버는 인증에 성공할 때까지 Hello 응답을 보내지 않습니다.

1. 클라이언트는 SSH 마커 접두사와 빈 비밀번호를 포함한 ClientHello를 전송합니다.

2. 클라이언트는 `SSHChallengeRequest`(패킷 11)를 전송합니다. 이 시점에 서버는 아직 ServerHello를 전송하지 **않은** 상태이며, 인증을 먼저 처리하기 위해 이 패킷이 도착할 때까지 대기합니다.

3. 서버는 무작위 바이트를 담은 `SSHChallenge`(패킷 18)로 응답합니다.

4. 클라이언트는 서명할 문자열을 구성한 뒤 원시 챌린지가 아닌 **이 문자열**에 서명하고, 서명을 담은 `SSHChallengeResponse`(패킷 12)를 전송합니다. 서명 대상 메시지는 다음 4개 파트를 정확히 이 순서대로, 구분자 없이 바이트 단위로 연결한 것입니다.

   ```text theme={null}
   to_sign = decimal(protocol_version) + default_database + user + challenge
   ```

   | 파트 | 출처 |
   | - | - |
   | `decimal(protocol_version)` | 클라이언트의 프로토콜 버전을 **10진수 ASCII 문자열**로 표현한 값입니다(예: `"54466"`). VarUInt나 고정 폭 정수가 아니라 버전 번호를 문자열로 나타낸 것입니다. 서버는 `ClientHello`에서 수신한 것과 동일한 프로토콜 버전을 사용하여 검사합니다. |
   | `default_database` | `ClientHello`의 `database` 필드입니다(없으면 빈 문자열). |
   | `user` | **`" SSH KEY AUTHENTICATION "` 마커 접두사를 제거한** 실제 사용자 이름으로, 서버가 접두사를 제거하여 복원하는 이름과 동일합니다. |
   | `challenge` | `SSHChallenge` 패킷의 원시 `challenge` 바이트입니다. |

5. 서버는 동일한 `decimal(protocol_version) + default_database + user + challenge` 문자열을 재구성한 뒤, 사용자에게 등록된 공개 키로 서명을 검증합니다. 검증에 성공하면 비밀번호 흐름과 동일한 응답인 `ServerHello`를 전송하고, 이후 핸드셰이크가 정상적으로 진행됩니다(Addendum 등). 실패하면 `Exception`을 반환하고 연결을 종료합니다. 원시 챌린지 바이트에만 서명하는 클라이언트는 인증에 실패합니다.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    C->>S: ClientHello (SSH marker user, empty password)
    C->>S: SSHChallengeRequest (11)
    Note over C,S: Server has NOT sent ServerHello yet —<br/>it authenticates first and blocks here
    S->>C: SSHChallenge (18) — random bytes to sign
    C->>S: SSHChallengeResponse (12) — signature over version+db+user+challenge
    alt signature verifies
        S->>C: ServerHello — handshake continues normally
    else verification fails
        S->>C: Exception — connection terminates
    end
```

> [!NOTE]
>
> 이는 ServerHello가 ClientHello 직후에 바로 전송되는 비밀번호 핸드셰이크와는 반대 순서입니다. SSH auth에서는 서명 검증이 완료될 때까지 ServerHello 전송이 보류되므로, ServerHello가 전송되기 전에 SSH challenge-response 과정이 핸드셰이크 중간에 삽입됩니다.

SSH auth를 사용하지 않는 외부 클라이언트는 패킷 11, 12, 18을 전혀 받지 않습니다. 사용자가 username 접두사를 통해 명시적으로 사용하도록 선택하지 않는 한, 이 패킷들은 wire상에 전송되지 않습니다.

### MergeTreeAllRangesAnnouncementResponse (패킷 유형 14)

Client → Server, 서버 간 통신 전용입니다. `parallel_replicas_protocol_version ≥ 8` 조건으로 게이트됩니다([VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL](#feature-table) 참고). 외부 클라이언트는 이 패킷을 전송하지 않습니다.

협상된 병렬 레플리카 버전이 `≥ 8`이면, 팔로워가 보낸 [`MergeTreeAllRangesAnnouncement`](#packet-type-reference)(패킷 유형 `15`, server→client 방향)에 대한 initiator의 요청/응답 사이클이 다음과 같이 달라집니다.

1. 팔로워가 읽기 파이프라인을 열고 initiator에 `MergeTreeAllRangesAnnouncement`를 전송합니다.
2. **announcement의 `mode`가 `Default`가 아닐 때만**(`WithOrder = 1` 또는 `ReverseOrder = 2`이며, 둘 다 순서 보장 병렬 읽기에 사용됨) initiator가 `MergeTreeAllRangesAnnouncementResponse`로 응답합니다. `mode = Default = 0`이면 initiator는 응답하지 않으며 팔로워도 대기하지 않습니다. `Default` 모드에서는 `MergeTreeReadTaskRequest`마다 범위를 할당하므로 파트 목록을 미리 받을 필요가 없습니다.
3. 팔로워는 첫 번째 [`MergeTreeReadTaskRequest`](#packet-type-reference)(서버 패킷 `16` — 팔로워→initiator로 전송되며, initiator는 클라이언트 패킷 `10`인 `MergeTreeReadTaskResponse`로 응답함)를 보내기 전에 응답을 기다리며(응답이 예정된 경우), 반환된 파트 목록을 바탕으로 자신의 `#split_i` 스트림이 소유한 파트만 대상으로 소스를 생성합니다.

버전 `8` 미만에서는 모드와 관계없이 announcement를 fire-and-forget 방식으로 전송하며, 팔로워는 로컬에서 인식하는 모든 파트를 대상으로 소스를 생성합니다(기존 방식).

#### Body

| # | 필드 | 유형 | 설명 |
| - | - | - | - |
| 1 | version | Int64 (리틀 엔디언) | 송신자의 병렬 레플리카 프로토콜 버전입니다. 수신자의 TCP revision이 `≥ DBMS_MIN_REVISION_WITH_VERSIONED_PARALLEL_REPLICAS_PROTOCOL`(`54471`)이면 `DBMS_PARALLEL_REPLICAS_PROTOCOL_VERSION`(현재 `8`)과 같고, 그렇지 않으면 `DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION`(`3`)으로 폴백합니다. 수신 측은 `DBMS_MIN_SUPPORTED_PARALLEL_REPLICAS_PROTOCOL_VERSION`보다 작은 값을 모두 거부합니다. |
| 2 | parts | [RangesInDataPartsDescription](#rangesindatapartsdescription) | 코디네이터가 해당 announcement의 스트림에 등록한 파트의 기준 집합입니다. 목록이 비어 있으면 코디네이터에 해당 스트림이 존재하지 않는다는 의미입니다(예: 팔로워가 initiator가 생성한 것보다 더 많은 split을 알린 경우). 이때 해당 스트림에 대한 팔로워의 풀은 즉시 완료 상태로 전환됩니다. |
| 3 | stream\_id | String | 이 응답이 응답하는 announcement의 `stream_id`를 그대로 반환합니다(split 토폴로지를 사용하는 경우 테이블 이름 뒤에 `#split_i` 접미사가 붙은 형태). |

#### RangesInDataPartsDescription body

| # | 필드 | 유형 | 설명 |
| - | - | - | - |
| 1 | count | VarUInt | 뒤이어 오는 파트 디스크립터의 개수입니다. 디코더는 `100'000'000'000`을 초과하는 값을 잘못된 형식으로 판단하여 거부합니다. |
| 2 | parts | `count`번 반복되는 [RangesInDataPartDescription](#rangesindatapartdescription) | 코디네이터(coordinator)에 등록된 순서대로 나열된 디스크립터입니다. |

#### RangesInDataPartDescription body

| # | 필드 | 유형 | 게이트 | 설명 |
| - | - | - | - | - |
| 1 | info | [MergeTreePartInfo](#mergetreepartinfo) | 공통 | 파트 아이덴티티(파티션, 블록 범위, 수준, 뮤테이션)입니다. |
| 2 | ranges | [MarkRanges](#markranges) | 공통 | 이 스트림이 처리할 수 있는 `info` 내의 마크 범위입니다. 목록이 비어 있으면 파트는 등록되어 있지만 현재 할당된 작업이 없다는 뜻입니다. |
| 3 | rows | VarUInt | 공통 | `ranges`에 포함된 전체 행 수입니다. |
| 4 | projection\_name | String | `DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_PROJECTION` (PR v5) | 프라이머리 파트의 행이면 빈 값이고, 그 외에는 프로젝션 이름입니다. |
| 5 | min\_marks\_per\_task | VarUInt | `DBMS_PARALLEL_REPLICAS_MIN_VERSION_WITH_MIN_MARKS_PER_TASK` (PR v6) | 팔로워의 풀이 이 파트에 대해 하나의 읽기 작업으로 묶어야 하는 마크 수의 하한입니다. |

#### MergeTreePartInfo body

| # | 필드 | 유형 | 설명 |
| - | - | - | - |
| 1 | version | Int64 (리틀 엔디언) | 항상 `DBMS_MERGE_TREE_PART_INFO_VERSION` (`1`)입니다. 디코더는 그 외의 값을 모두 거부합니다. |
| 2 | partition\_id | String | 파티션 식별자입니다(예: 파티션되지 않은 테이블은 `"all"`, 그 외에는 파티션 키 튜플 표현식을 문자열로 변환한 값). |
| 3 | min\_block | Int64 (리틀 엔디언) | 파트의 블록 범위에서 첫 번째 블록 번호입니다. |
| 4 | max\_block | Int64 (리틀 엔디언) | 파트의 블록 범위에서 마지막 블록 번호입니다(이 값 포함). |
| 5 | level | UInt32 (리틀 엔디언) | 머지 수준입니다. |
| 6 | mutation | Int64 (리틀 엔디언) | 이 파트를 생성한 뮤테이션 버전입니다(뮤테이션이 적용되지 않은 파트는 `0`). |
| 7 | use\_legacy\_max\_level | Bool (text) | 단일 ASCII 바이트(`'1'` 또는 `'0'`)로 인코딩되며, 파트 이름 포맷의 하위 호환성을 위한 플래그입니다. |

#### MarkRanges body

| # | 필드 | 유형 | 설명 |
| - | - | - | - |
| 1 | size | UInt64 (리틀 엔디언) | 뒤에 이어지는 마크 범위 쌍의 개수입니다. 참고: VarUInt가 **아니라** 리틀 엔디언 고정 폭 값입니다. |
| 2 | ranges | `(UInt64 begin, UInt64 end)`가 `size`번 반복되며, 각 값은 리틀 엔디언 | 반개구간 `[begin, end)` 형태의 마크 인터벌입니다. |

## 패킷 유형 참고

### Client → Server

| 코드 | 이름 | Body 포맷 | 설명 |
| - | - | - | - |
| 0 | Hello | [ClientHello](#clienthello) | 핸드셰이크 시작 |
| 1 | Query | [Query](#query) | 쿼리 실행 요청 |
| 2 | Data | [Data](#data) | 데이터 블록(INSERT 데이터, 외부 테이블, 데이터 종료 마커) |
| 3 | Cancel | (body 없음) | 실행 중인 쿼리 취소 |
| 4 | Ping | [Ping](#ping) | 활성 상태(liveness) 확인 |
| 5 | TablesStatusRequest | 명시되지 않음 | 테이블 상태 확인 |
| 6 | KeepAlive | 명시되지 않음 | 연결 유지(keepalive) |
| 7 | Scalar | 명시되지 않음 | 스칼라 데이터 블록 |
| 8 | IgnoredPartUUIDs | 명시되지 않음 | 쿼리에서 제외할 파트 |
| 9 | ReadTaskResponse | 명시되지 않음 | S3 클러스터 읽기 응답 |
| 10 | MergeTreeReadTaskResponse | 명시되지 않음 | 병렬 읽기 작업 응답 |
| 11 | SSHChallengeRequest | [SSH 인증](#ssh-authentication) | SSH 인증 챌린지 요청 |
| 12 | SSHChallengeResponse | [SSH 인증](#ssh-authentication) | SSH 인증 챌린지 응답 |
| 13 | QueryPlan | [QueryPlan](#queryplan) | 쿼리 계획 |
| 14 | MergeTreeAllRangesAnnouncementResponse | [MergeTreeAllRangesAnnouncementResponse](#mergetreeallrangesannouncementresponse) | 팔로워가 보낸 [`MergeTreeAllRangesAnnouncement`](#packet-type-reference)에 대한 initiator의 응답입니다(`parallel_replicas_protocol_version ≥ 8`인 경우에만 사용 가능 — [VERSIONED\_PARALLEL\_REPLICAS\_PROTOCOL](#feature-table) 참고). 서버 간 통신에서만 사용되며, 외부 클라이언트는 이 패킷을 보내지 않습니다. |

### Server → Client

| 코드 | 이름 | Body 포맷 | 설명 |
| - | - | - | - |
| 0 | Hello | [ServerHello](#serverhello) | 핸드셰이크 응답 |
| 1 | Data | [Data](#data) | 결과 데이터 블록 |
| 2 | Exception | [Exception](#exception) | 오류 |
| 3 | Progress | [Progress](#progress) | 쿼리 실행 진행 상황 |
| 4 | Pong | [Pong](#pong) | 활성 상태(liveness) 확인 응답 |
| 5 | EndOfStream | (body 없음) | 쿼리 완료 |
| 6 | ProfileInfo | [ProfileInfo](#profileinfo) | 실행 후 프로파일링 데이터 |
| 7 | Totals | [Totals](#totals) | GROUP BY WITH TOTALS 행 |
| 8 | Extremes | [Extremes](#extremes) | 최솟값/최댓값(2개 행으로 구성된 블록) |
| 9 | TablesStatusResponse | 명시되지 않음 | 테이블 상태 응답 |
| 10 | Log | [Log](#log) | 쿼리 실행 로그 라인 |
| 11 | TableColumns | [TableColumns](#tablecolumns) | 기본값 처리를 위한 컬럼 설명 |
| 12 | PartUUIDs | 명시되지 않음 | 고유 파트 ID |
| 13 | ReadTaskRequest | 명시되지 않음 | 클러스터 읽기 작업 요청 |
| 14 | ProfileEvents | [ProfileEvents](#profileevents) | 성능 카운터 |
| 15 | MergeTreeAllRangesAnnouncement | 명시되지 않음 | 병렬 읽기 초기화 |
| 16 | MergeTreeReadTaskRequest | 명시되지 않음 | 병렬 읽기 작업 할당 |
| 17 | TimezoneUpdate | [TimezoneUpdate](#timezoneupdate) | 서버 시간대 업데이트 |
| 18 | SSHChallenge | [SSH 인증](#ssh-authentication) | SSH 인증 챌린지 |

### QueryPlan

`QueryPlan`은 서버 간 통신에서만 사용되는 클라이언트 패킷입니다. `stage`가 `QueryPlan`(`7`)인 [`Query`](#query) 다음에 전송되며, 직렬화된 계획 스트림 1개를 포함합니다.

| # | 필드 | 유형 | 게이트 | 설명 |
| - | - | - | - | - |
| 1 | serialization\_version | VarUInt | 공통 | 쿼리 계획 직렬화 버전입니다. 수신 측이 `ServerHello`에서 알린 `query_plan_serialization_version`을 초과할 수 없습니다. |
| 2 | max\_threads | VarUInt | 직렬화 버전 ≥ `10` | 계획 수준의 스레드 제한입니다. `0`이면 계획 단위의 별도 제한이 없습니다. |
| 3 | concurrency\_control | Bool | 직렬화 버전 ≥ `10` | 계획이 동시성 제어(concurrency control)에 참여하는지 여부입니다. |
| 4 | plan | 버전이 지정된 쿼리 계획 스트림 | 공통 | 직렬화된 계획 트리, 스텝 설정, 헤더, 준비된 Set입니다. 내부 레이아웃은 쿼리 계획 직렬화 버전에 따라 결정됩니다. |

버전 `10` 이전에는 필드 2와 3이 없습니다. 실행 제한 중 하나라도 기본값이 아닌 initiator는 이전 레이아웃을 전송해서는 안 됩니다. 대신 원본 SQL 쿼리를 전송하여, 이전 버전의 수신 측이 쿼리 설정에서 제한을 재구성할 수 있도록 해야 합니다.

## 구성

이 섹션에서는 native protocol 연결의 동작을 결정하는 조정 가능한 매개변수를 다룹니다.

* [전송 계층 설정](#transport-layer-settings) — TCP 연결 자체의 동작에 영향을 주는 TCP 소켓 옵션과 타임아웃입니다.
* [애플리케이션 계층 설정](#application-layer-settings) — [Query 패킷의 설정 목록](#setting)에 담겨 전달되는 쿼리별 매개변수로, 서버가 wire 상에서 전송하는 내용이나 프레이밍 방식에 영향을 줍니다.
* [범위 외 설정](#settings-out-of-scope) — 프로토콜 설정과 혼동하기 쉽지만 실제로는 SQL 실행이나 저장소를 제어하는 설정입니다.

아래의 기본값은 최근 서버 릴리스를 기준으로 하며, 버전 및 배포 환경에 따라 다를 수 있습니다.

### 전송 계층 설정

#### 소켓 옵션

| 옵션 | 기본값 | 적용 측 | 설명 |
| - | - | - | - |
| `TCP_NODELAY` | on | 양쪽 | Nagle 알고리즘을 비활성화하여 작은 패킷을 즉시 전송합니다. |
| `SO_KEEPALIVE` | on(클라이언트), OS 기본값(서버) | 비대칭 | 커널 수준의 TCP keepalive 프로브입니다. 클라이언트는 `tcp_keep_alive_timeout > 0`이면 이 옵션을 명시적으로 활성화합니다. 서버는 OS 기본값을 그대로 따릅니다. |
| `SO_RCVBUF` / `SO_SNDBUF` | OS 기본값 | — | 소켓 버퍼 크기입니다. 프로토콜에서 별도로 조정하지 않습니다. |

#### 타임아웃

| 설정 | 기본값 | 단위 | 적용 측 | 설명 |
| - | - | - | - | - |
| `connect_timeout` | 10 | 초 | 클라이언트 | 초기 TCP 연결 수립에 대한 타임아웃입니다. |
| `handshake_timeout_ms` | 10000 | 밀리초 | 클라이언트 | 핸드셰이크 중 ServerHello 수신에 대한 타임아웃입니다. |
| `send_timeout` | 300 | 초 | 양쪽 | 이 시간 동안 1바이트도 쓰지 못하면 연결에서 예외가 발생합니다. |
| `receive_timeout` | 300 | 초 | 양쪽 | 이 시간 동안 1바이트도 읽지 못하면 연결에서 예외가 발생합니다. |
| `tcp_keep_alive_timeout` | 290 | 초 | 클라이언트 | OS가 첫 번째 TCP keepalive 프로브를 보내기 전까지의 유휴 시간입니다. |
| `receive_data_timeout_ms` | 2000 | 밀리초 | 클라이언트 | 레플리카로부터 첫 번째 Data 패킷 수신에 대한 타임아웃입니다. |
| `connect_timeout_with_failover_ms` | 1000 | 밀리초 | 클라이언트 | 레플리카를 순회할 때 시도별로 적용되는 연결 타임아웃입니다. |
| `connect_timeout_with_failover_secure_ms` | 1000 | 밀리초 | 클라이언트 | TLS로 레플리카를 순회할 때 시도별로 적용되는 연결 타임아웃입니다. |
| `hedged_connection_timeout_ms` | 50 | 밀리초 | 클라이언트 | 헤지 요청(hedged request)에서 시도별로 적용되는 연결 타임아웃입니다. |
| `poll_interval` | 10 | 초 | 서버 | 서버의 유휴 연결 및 종료 확인 루프의 세분화 수준입니다. |

타임아웃은 다음과 같이 중첩됩니다.

```text theme={null}
tcp_keep_alive_timeout (290s)
      < receive_timeout (300s)
      < idle_connection_timeout (3600s)
      < tcp_close_connection_after_queries_seconds (0 = unlimited by default)
```

OS keepalive가 가장 먼저 동작하며, 커널 수준에서 응답하지 않는 피어를 별도의 알림 없이 감지할 수 있습니다. 그다음 방어선은 애플리케이션의 수신 타임아웃입니다. 유휴 타임아웃은 오랫동안 사용되지 않은 연결을 회수하는 최후의 수단입니다.

#### 연결 제한

| 설정 | 기본값 | 단위 | 적용 위치 | 설명 |
| - | - | - | - | - |
| `max_connections` | 4096 | 개수 | 서버 | 최대 동시 TCP 연결 수입니다. |
| `idle_connection_timeout` | 3600 | 초 | 서버 | 유휴 연결이 열린 상태로 유지될 수 있는 최대 시간입니다. |
| `tcp_close_connection_after_queries_num` | 0 (무제한) | 개수 | 서버 | 연결이 강제로 종료되기 전까지 연결당 처리할 수 있는 최대 쿼리 수입니다. |
| `tcp_close_connection_after_queries_seconds` | 0 (무제한) | 초 | 서버 | 활동 여부와 관계없이 연결이 유지될 수 있는 최대 총 시간입니다. |

쿼리를 주기적으로 실행하는 연결은 무기한 유지될 수 있습니다. 1시간 동안 유휴 상태인 연결만 정리되며, 기본적으로 최대 연결 수명 제한은 없습니다.

### 애플리케이션 계층 설정

이 설정들은 [Query 패킷의 설정 목록](#setting)에 포함되어 쿼리별로 전달됩니다. 이를 통해 서버가 wire로 전송하는 내용이나 메시지의 프레이밍 방식이 달라집니다.

#### 압축

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `network_compression_method` | `"ZSTD"` | string | Query 패킷의 `compression` 플래그가 설정된 경우 사용할 압축 코덱입니다. 값: `"LZ4"`, `"LZ4HC"`, `"ZSTD"`, `"NONE"`. |
| `network_zstd_compression_level` | 3 | 1–15 | `network_compression_method == "ZSTD"`일 때 적용되는 ZSTD 압축 수준입니다. |

[Query 패킷](#query)의 `compression` 플래그(필드 6)는 압축 사용 여부를 결정하며, 위 설정은 압축이 켜져 있을 때 사용할 코덱을 지정합니다.

#### 로그 스트리밍

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `send_logs_level` | `"fatal"` | string | 최소 로그 레벨입니다. 가능한 값: `"none"`, `"fatal"`, `"error"`, `"warning"`, `"information"`, `"debug"`, `"trace"`, `"test"`. |
| `send_logs_source_regexp` | `""` | string | 로거 소스에 적용할 정규식 필터입니다. 비워 두면 모든 소스가 통과합니다. |

`send_logs_level`을 `"none"` 이외의 값으로 설정하면 서버는 쿼리 실행 중에 [Log](#log) 패킷을 전송합니다.

#### 진행 상황 보고

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `interactive_delay` | 100000 | 마이크로초 | 연속된 Progress 패킷 간의 목표 최소 간격입니다. |

이 값은 엄격한 최댓값이 아니라 목표 최솟값입니다. 쿼리의 작업 처리 속도가 충분히 빠르지 않으면 서버는 Progress 패킷을 이보다 드물게 전송할 수 있습니다.

#### 결과 엔벌로프

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `extremes` | false | bool | true로 설정하면 서버가 컬럼별 최솟값/최댓값을 담은 [Extremes](#extremes) 패킷을 전송합니다. |
| `max_result_rows` | 0 (무제한) | 개수 | 전송할 수 있는 최대 행 수입니다. 한도 초과 시 동작은 `result_overflow_mode`에 따라 결정됩니다. |
| `max_result_bytes` | 0 (무제한) | 비압축 바이트 | 전송할 수 있는 최대 비압축 바이트 크기입니다. 한도 초과 시 동작은 `result_overflow_mode`에 따라 결정됩니다. |
| `result_overflow_mode` | `"throw"` | string | `"throw"`는 Exception과 함께 스트림을 종료하고, `"break"`는 부분 결과를 전송한 뒤 EndOfStream을 전송합니다. |

#### 비동기 INSERT

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `async_insert` | true | bool | true로 설정하면 INSERT 데이터가 서버 측 큐에 쌓인 후 배치로 처리됩니다. |
| `wait_for_async_insert` | true | bool | true로 설정하면(`async_insert`가 활성화된 경우) 큐에 쌓인 데이터가 플러시될 때까지 서버가 응답을 보류합니다. |
| `wait_for_async_insert_timeout` | 120 | 초 | 서버가 응답을 반환하기 전까지 플러시를 기다리는 최대 시간입니다. |

#### 분산 추적(Distributed tracing)

| 설정 | 기본값 | 단위 | 설명 |
| - | - | - | - |
| `opentelemetry_start_trace_probability` | 0.0 | 0–1 사이의 확률 | 서버 측에서 응답 텔레메트리에 OpenTelemetry 컨텍스트를 첨부할 확률입니다. |

### 범위 외 설정

다음 설정은 프로토콜 수준 설정으로 오해받기도 하지만, 실제로는 wire 동작이 아니라 SQL 실행, 저장소 또는 CPU 사용을 제어합니다. 프로토콜 구현에서 이 설정들을 별도로 처리할 필요는 없습니다.

* `max_threads` — 쿼리 실행 시의 병렬 처리 수준입니다.
* `max_memory_usage` — 쿼리별 메모리 사용 상한입니다.
* `max_block_size`, `preferred_block_size_bytes` — 쿼리 처리 중 내부 블록 크기를 조정하며, wire 블록은 이 설정과 무관합니다.
* `compile_expressions` — JIT 컴파일 설정으로, CPU에만 영향을 줍니다.
* `async_insert_max_data_size` — 서버 측 큐 버퍼 설정입니다.
* `input_format_native_*` / `output_format_native_*` 계열을 **제외한** 모든 `input_format_*` 및 `output_format_*` 설정 — `native`가 아닌 설정은 다른 포맷(예: HTTP에서 사용하는 포맷)을 선택하거나 조정할 뿐, native protocol의 `Data` 블록에는 영향을 주지 않습니다.

단, `*_native_*` 설정은 예외입니다. 이 설정들은 native TCP `Data` 블록 내부의 바이트를 변경하므로 프로토콜 구현에서 반드시 고려해야 합니다. `output_format_native_encode_types_in_binary_format`은 컬럼의 `type` 필드를 텍스트 문자열 대신 바이너리 타입 인코딩으로 바꾸고(단, 보조 쿼리에는 적용되지 않습니다. [컬럼 wire 레이아웃](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#column-wire-layout) 참조), `output_format_native_write_json_as_string`은 `JSON` 컬럼을 `String`으로 출력하며, `output_format_native_use_flattened_dynamic_and_json_serialization`은 FLATTENED `Dynamic`/`JSON` 레이아웃을 선택합니다. 이 설정들은 패킷 엔벌로프가 아닌 블록 body에 영향을 주므로 [Native Format](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md) 명세에서 다룹니다. 자세한 내용은 [컬럼 wire 레이아웃](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#column-wire-layout) 및 [버전이 지정된 타입](https://clickhouse.com/docs/ko/resources/develop-contribute/native-protocol/format.md#versioned-types)을 참조하십시오.

## 용어집

**Cancel** — 클라이언트가 보내는 패킷(유형 3)으로, 실행 중인 쿼리를 중단합니다. 이 페이지에서는 자세히 다루지 않습니다.

**End-of-client-data marker** — 클라이언트가 입력 스트림을 닫기 위해 보내는 빈 Data 패킷(컬럼 0개, 행 0개)입니다. 전송 위치는 쿼리 종류에 따라 다릅니다.

* **일반 쿼리(`SELECT` 등):** Query 패킷과 외부 테이블 Data 패킷 뒤에 전송되어 "더 이상 외부 데이터가 없음"을 알립니다. 이후 서버가 쿼리 실행을 시작합니다.
* **`INSERT`:** 클라이언트는 스키마 블록 이전에 마커를 보내지 **않습니다**. 서버가 먼저 스키마 블록을 보내면 클라이언트가 행 Data 블록을 스트리밍하고, 그런 다음에야 빈 Data 패킷을 보내 행 스트림을 종료합니다. 스키마 블록보다 먼저 빈 마커를 보내면 곧바로 행이 끝난 것으로 해석되어 데이터가 손실됩니다.

**Feature** — 특정 프로토콜 버전에서 도입된 wire 포맷 변경 사항입니다. 협상된 버전이 해당 기능의 버전 이상이면 활성화됩니다. [버전 관리 및 기능 게이트](#versioning-and-feature-gates)를 참조하십시오.

**Inter-server** — 서버 간 분산 쿼리에서만 의미가 있는 필드에 붙는 역할 레이블입니다. 외부 클라이언트는 기본값(보통 빈 문자열, 0 또는 false)을 기록합니다.

**Negotiated version** — 핸드셰이크 과정에서 계산되는 `min(client_version, server_version)` 값입니다. 연결이 유지되는 동안 활성화되는 기능을 결정합니다.

**Packet** — wire 메시지로, VarUInt 패킷 유형 코드 뒤에 유형별로 포맷이 다른 body가 이어집니다. [패킷 엔벌로프](#packet-envelope)를 참조하십시오.

**Packet type code** — 패킷 맨 앞에 위치하여 패킷의 포맷을 식별하는 VarUInt입니다. 현재 0–18 값이 할당되어 있습니다. [패킷 유형 참고](#packet-type-reference)를 참조하십시오.

**Response stream** — 쿼리 실행 중 서버가 내보내는 패킷 시퀀스입니다. 길이는 정해져 있지 않으며, 정확히 하나의 `EndOfStream`(성공) 또는 `Exception`(실패)으로 종료됩니다. [쿼리 단계](#query-phase)를 참조하십시오.

**Schema block** — INSERT 단계에서 클라이언트가 데이터를 보내기 전에 서버가 예상 컬럼 구조를 알리기 위해 보내는 헤더 블록(컬럼은 있지만 행이 0개인 Block)입니다.

**Settings list** — Query body에 포함된 `(key, flags, value)` 튜플의 시퀀스로, 빈 키로 종료됩니다. 쿼리별 애플리케이션 계층 구성을 전달합니다. [Setting](#setting)을 참조하십시오.

**Stage** — [Query](#query) 패킷의 VarUInt 필드(필드 5)로, 서버가 쿼리를 어느 단계까지 실행할지 제어합니다. 외부 클라이언트는 일반적으로 `2`(Complete)를 보내며, 분산 쿼리와 직렬화된 쿼리 계획에서는 더 높은 값을 사용합니다. 전체 wire 값 목록은 [Query](#query) 필드 5를 참조하십시오.

**Terminator** — 스트림을 종료하는 패킷입니다. Query 응답은 `EndOfStream`(성공) 또는 `Exception`(실패)으로 끝나고, 클라이언트의 입력 스트림은 빈 Data 마커로 끝납니다.
