> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-vortex-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> 네이티브 TCP 프로토콜로 ClickHouse에 연결하는 공식 C# 클라이언트입니다.

# ClickHouse C# 네이티브 TCP 클라이언트

<h2 id="experimental-status">
  실험적 기능
</h2>

<Warning>
  네이티브 TCP 클라이언트 API는 실험적 기능입니다. 공개 API는 향후 릴리스에서 변경될 수 있습니다.
</Warning>

기본 클라이언트, 데이터 소스, 세션, 작업 인터페이스와 의존성 주입 확장 기능을 사용하면
컴파일러 경고 `CHTCP0001`이 발생합니다. API를 평가하는 동안 이 경고를 인지하고 사용하려면
프로젝트 파일의 `NoWarn`에 추가하세요:

```xml theme={null}
<PropertyGroup>
  <NoWarn>$(NoWarn);CHTCP0001</NoWarn>
</PropertyGroup>
```

API를 사용하는 코드는 향후 릴리스에서 수정해야 할 수 있다는 점을 받아들인 후에만 경고를 표시하지 않도록 설정하세요.

<h2 id="supported-net-versions">
  지원되는 .NET 버전
</h2>

네이티브 TCP 클라이언트는 다음 .NET 버전을 지원합니다.

* .NET 8.0
* .NET 9.0
* .NET 10.0

<h2 id="supported-clickhouse-versions">
  지원되는 ClickHouse 버전
</h2>

TCP 클라이언트는 ClickHouse 서버의 최근 LTS 버전 2개와 최근 릴리스 3개를 공식 지원합니다.

<h2 id="installation">
  설치
</h2>

네이티브 TCP 클라이언트는 `ClickHouse.Driver` 패키지에 포함되어 있습니다. NuGet에서 해당 패키지를 설치하세요:

```bash theme={null}
dotnet add package ClickHouse.Driver
```

또는 NuGet Package Manager를 사용할 수 있습니다:

```bash theme={null}
Install-Package ClickHouse.Driver
```

<h2 id="quick-start">
  빠른 시작
</h2>

```csharp theme={null}
using ClickHouse.Driver.Tcp;

// Create and reuse a client. It is thread-safe and owns a connection pool.
await using var client = new ClickHouseTcpClient(
    "Host=my.clickhouse;Port=9000;Username=user");

// Execute a query
object version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="migrating-from-http">
  HTTP 클라이언트에서 마이그레이션
</h2>

TCP API는 `IClickHouseClient`와 별개이며, `ClickHouseClient`나
`ClickHouseConnection`에서 선택할 수 있는 전송 옵션이 아닙니다. 대부분의 SQL은 재사용할 수 있지만,
`ClickHouseTcpClient`를 생성하고 각 작업을 그에 대응하는 TCP 작업으로 마이그레이션해야 합니다.

<h3 id="migrating-methods">
  메서드 대응 관계
</h3>

| HTTP 클라이언트 | TCP 클라이언트 | 주요 차이점 |
| - | - | - |
| `new ClickHouseClient(...)` | `new ClickHouseTcpClient(...)` | `ClickHouseTcpClientOptions` 또는 TCP 연결 문자열을 사용하고, TLS를 사용하지 않는 경우에는 일반적으로 9000번인 네이티브 포트를 지정하세요. 어느 클라이언트든 여러 작업에서 재사용하세요. |
| `ExecuteNonQueryAsync(...)` | `ExecuteAsync(...)` | TCP 메서드는 ClickHouse가 SQL 문을 수신했음을 확인하면 완료되며, 영향을 받은 행 수는 반환하지 않습니다. |
| `ExecuteScalarAsync(...)` | `ExecuteScalarAsync(...)` | 두 메서드 모두 첫 번째 값을 반환하며, 결과가 비어 있으면 `null`을 반환합니다. TCP 메서드는 연결을 재사용할 수 있도록 응답을 끝까지 읽은 후 반환합니다. |
| `ExecuteReaderAsync(...)` | `QueryAsync(...)`, `QueryAsync<T>(...)`, 또는 `StreamAsync(...)` | TCP에는 `DbDataReader`가 없습니다. 메모리를 할당해 반환하는 `object[]` 행, 매핑된 POCO, 또는 빌려 쓰는 열 지향 블록 중에서 선택하세요. |
| `QueryAsync<T>(...)` | `QueryAsync<T>(...)` | TCP는 POCO 매핑을 자동으로 생성하고 캐시하므로 별도로 등록할 필요가 없습니다. HTTP 매핑 속성 대신 `ClickHouseTcpColumn`과 `ClickHouseTcpNotMapped`를 사용하세요. |
| `InsertBinaryAsync(table, columns, rows)` | `InsertRowsAsync(sql, rows)` | 구체화된 `IReadOnlyList<object[]>`를 전달하세요. SQL 문은 `VALUES`로 끝나야 합니다. TCP 메서드는 삽입된 행 수를 반환하지 않습니다. |
| `InsertBinaryAsync<T>(table, rows)` | `InsertRowsAsync<T>(sql, rows)` | TCP에서는 POCO를 등록할 필요가 없지만, SQL 문에 지정된 모든 컬럼은 호환되는 public 속성에 매핑되어야 합니다. |
| — | `InsertAsync(sql, columns)` | 행 삽입 시 발생하는 행에서 컬럼으로의 변환과 값 형식 박싱을 피하려면 형식이 지정된 `IColumn` 값을 사용하세요. |
| `PingAsync(...)` | `PingAsync(...)` | HTTP는 Ping에 실패하면 `false`를 반환합니다. TCP는 `Pong`을 받으면 정상적으로 완료되며, 그렇지 않으면 예외를 발생시킵니다. |
| `ExecuteRawResultAsync(...)`, `InsertRawStreamAsync(...)`, `PostStreamAsync(...)` | 대응 메서드 없음 | 원시 스트림이나 CSV, JSONEachRow, Parquet 같은 포맷에는 HTTP 클라이언트를 계속 사용하세요. |
| `CreateConnection()` 또는 ADO.NET 형식 | 대응 메서드 없음 | ADO.NET과 ORM에는 HTTP 클라이언트를 계속 사용하세요. |

예를 들어 HTTP 리더를 사용하는 반복문은 다음과 같습니다.

```csharp theme={null}
using var reader = await http.ExecuteReaderAsync(
    "SELECT id, name FROM events ORDER BY id");

while (reader.Read())
{
    Console.WriteLine($"{reader.GetFieldValue<ulong>(0)}: {reader.GetString(1)}");
}
```

비동기 TCP 행 스트림으로 전환됩니다:

```csharp theme={null}
await foreach (object[] row in tcp.QueryAsync(
    "SELECT id, name FROM events ORDER BY id"))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

<h3 id="migrating-options-parameters">
  옵션과 매개변수
</h3>

HTTP `QueryOptions`와 `ClickHouseParameterCollection`은 이에 대응하는 TCP
유형과 서로 바꿔 사용할 수 없습니다. 값을 `ClickHouseTcpQueryOptions`로 옮기세요. 이 유형에서는
설정이 문자열이며 매개변수가 옵션 객체에 포함됩니다:

```csharp theme={null}
var options = new ClickHouseTcpQueryOptions
{
    QueryId = "events-by-score",
    Settings = new Dictionary<string, string>
    {
        ["max_execution_time"] = "30",
    },
    Parameters = new ClickHouseTcpParameterCollection
    {
        { "minimum", 18.0 },
    },
};

await foreach (object[] row in client.QueryAsync(
    "SELECT id, score FROM events WHERE score >= {minimum:Float64}",
    options))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

TCP 클라이언트는 ClickHouse 네이티브 `{name:Type}` 플레이스홀더를 지원하지만
ADO.NET 방식의 `@name` 플레이스홀더는 재작성하지 않습니다. 쿼리별 데이터베이스와 역할, 베어러 토큰, 사용자 정의
헤더, 매개변수 리졸버 또는 포매터, 읽기 컨버터도 지원하지 않습니다. HTTP에서만 사용할 수 있는
기능은 [제한 사항](#limitations)을 참조하십시오.

<h3 id="migrating-lifetime">
  연결과 결과의 수명
</h3>

두 상위 수준 클라이언트 모두 여러 스레드에서 안전하게 사용할 수 있으므로 재사용해야 합니다. TCP 클라이언트는 HTTP 연결이 아닌 지속적인 네이티브
연결을 관리하고 `IAsyncDisposable`을 구현하므로, 일반적으로 애플리케이션 종료 시
비동기적으로 해제해야 합니다.

`QueryAsync`가 반환한 행은 호출자가 소유합니다. `StreamAsync`가 반환한 블록과 그 컬럼은 임시로 빌려 쓰는 데이터로,
현재 반복에서만 유효합니다. 반복이 끝난 뒤에도 필요한 값은 복사하십시오. 연결이 풀로 반환되도록
열거를 항상 완료하거나 해제하십시오. 여러 작업을 동일한 연결에서 실행하면서 세션 상태를
유지해야 한다면 `OpenSessionAsync`를 사용하십시오.

동일한 테이블에 두 클라이언트를 사용하는 방법은 전체
[HTTP에서 TCP로 마이그레이션하는 예시](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Tcp/Core/Tcp_004_MigratingFromHttp.cs)를 참조하십시오.

<h2 id="configuration">
  구성
</h2>

네이티브 TCP 클라이언트는 다음 2가지 방법으로 구성할 수 있습니다.

* **연결 문자열:** 서버 엔드포인트, 인증 자격 증명 및 기타 연결 옵션을 지정하는 키/값 쌍을 세미콜론으로 구분한 문자열입니다.
* **`ClickHouseTcpClientOptions` 객체:** 코드에서 설정하는, 타입이 명확하게 지정된 변경 불가능한 구성 객체입니다.

두 방식 중 하나를 `ClickHouseTcpClient` 생성자에 전달하세요. `ClickHouseTcpConnectionStringBuilder`를 사용하면
연결 문자열을 파싱하거나 생성한 후 `ClickHouseTcpClientOptions`로 변환할 수 있습니다.

```csharp theme={null}
var builder = new ClickHouseTcpConnectionStringBuilder
{
    Host = "my.clickhouse",
    Username = "user",
    UseTls = true,
    Compression = "zstd",
};

await using var client = new ClickHouseTcpClient(builder.ToOptions());
```

다음 섹션에서는 사용 가능한 설정과 각 설정의 기본값 및 적용 시 동작을 설명합니다.

<h3 id="connection-settings">
  연결 설정
</h3>

연결 문자열에서 `TimeSpan` 속성 값은 초 단위로 지정합니다.

| 속성 | 유형 | 기본값 | 연결 문자열 키 | 설명 |
| - | - | - | - | - |
| 호스트 | `string` | `"localhost"` | `Host` | ClickHouse 서버의 호스트명 또는 IP 주소 |
| 포트 | `int?` | `null` | `Port` | 네이티브 프로토콜 포트. 생략하면 TLS 미사용 시 9000, TLS 사용 시 9440을 사용합니다 |
| 사용자 이름 | `string` | `"default"` | `Username` | 인증에 사용하는 사용자 이름 |
| 비밀번호 | `string` | `""` | `Password` | 인증에 사용하는 비밀번호 |
| 데이터베이스 | `string` | `"default"` | `Database` | 작업에 사용할 기본 데이터베이스 |
| 할당량 키 | `string` | `""` | `QuotaKey` | ClickHouse의 키 기반 할당량에 사용하는 키 |
| 연결 타임아웃 | `TimeSpan` | 30초 | `DialTimeout` | 소켓 연결, TLS 협상, 네이티브 프로토콜 핸드셰이크에 적용되는 타임아웃 |
| 읽기 타임아웃 | `TimeSpan` | 5분 | `ReadTimeout` | 작업 중 개별 데이터 읽기에 허용되는 최대 시간. 0으로 설정하면 타임아웃이 비활성화됩니다 |

<h3 id="data-format-serialization">
  데이터 포맷과 직렬화
</h3>

| 속성 | 유형 | 기본값 | 연결 문자열 키 | 설명 |
| - | - | - | - | - |
| SendJsonAndDynamicSerializationSettings | `bool` | `true` | `SendJsonAndDynamicSerializationSettings` | 클라이언트가 지원하는 `JSON` 및 `Dynamic` wire 형식을 요청합니다. 설정을 변경할 수 없는 읽기 전용 사용자는 `false`로 설정하세요. 이 경우 해당 타입의 데이터를 읽지 못할 수 있습니다. |
| MaxSendBufferBytes | `int` | 1 MiB | `MaxSendBufferBytes` | 버퍼링된 삽입 데이터의 조정 가능한 상한입니다. 클라이언트는 각 컬럼을 처리한 후 상한을 확인하므로, 컬럼 하나만으로도 버퍼가 상한을 초과할 수 있습니다. |
| Compressor | `IClickHouseCompressor` | `Lz4Compressor.Default` | `Compression` | 클라이언트가 기록하는 블록을 압축하고 결과를 압축해서 받도록 요청합니다. 연결 문자열에는 `lz4`, `zstd`, `none`을 사용할 수 있으며, 사용자 정의 압축기는 옵션을 통해서만 설정할 수 있습니다. |

<h3 id="connection-pooling">
  연결 풀링(Connection pooling)
</h3>

하나의 연결은 한 번에 하나의 작업만 실행합니다. 풀을 사용하면 여러 작업을 동시에 실행할 수 있으며,
작업 간에 연결을 재사용합니다.

| 속성 | 유형 | 기본값 | 연결 문자열 키 | 설명 |
| - | - | - | - | - |
| MinPoolSize | `int` | `0` | `MinPoolSize` | 풀이 가능한 한 열어 두는 연결 수이며, 현재 사용 중인 연결도 포함됩니다 |
| MaxPoolSize | `int` | `20` | `MaxPoolSize` | 열린 연결 수와 동시 작업 수의 절대 상한입니다. 이를 초과하는 작업은 연결을 사용할 수 있을 때까지 대기합니다 |
| PoolTimeout | `TimeSpan` | 30초 | `PoolTimeout` | 풀이 가득 찼을 때 연결을 기다리는 최대 시간입니다. 새 연결을 수립하는 시간은 `DialTimeout`으로 별도 제한됩니다 |
| MaxConnectionLifetime | `TimeSpan` | 30분 | `MaxConnectionLifetime` | 연결을 재사용할 수 있는 최대 수명입니다. 0으로 설정하면 제한이 비활성화되며, 실행 중인 작업은 중단되지 않습니다 |
| IdleTimeout | `TimeSpan` | 5분 | `IdleTimeout` | 사용되지 않는 연결이 재사용 대상으로 유지되는 최대 시간입니다. 0으로 설정하면 제한이 비활성화됩니다 |
| SweepInterval | `TimeSpan?` | `null` (자동 산출) | `SweepInterval` | 만료된 연결을 폐기하고 `MinPoolSize`를 복원하는 주기입니다. 기본값은 수명 제한과 유휴 제한을 바탕으로 산출됩니다 |
| PoolReusePolicy | `ClickHouseTcpPoolReusePolicy` | `Lifo` | `PoolReusePolicy` | 유휴 연결 중 가장 최근에 반환된 연결(`Lifo`)과 가장 오래전에 반환된 연결(`Fifo`) 중 어느 것을 선택할지 지정합니다 |

<h3 id="security">
  보안
</h3>

| 속성 | 유형 | 기본값 | 연결 문자열 키 | 설명 |
| - | - | - | - | - |
| UseTls | `bool` | `false` | `UseTls` | native protocol 핸드셰이크 전에 TLS를 활성화합니다. `Port`를 생략하고 TLS를 활성화하면 기본 포트가 9000에서 9440으로 변경됩니다. |
| TlsServerName | `string` | `null` | `TlsServerName` | SNI로 전송되어 서버 인증서와 대조되는 호스트명입니다. `null`이면 `Host`를 사용합니다. |
| TlsAllowInvalidCertificates | `bool` | `false` | `TlsAllowInvalidCertificates` | 유효성 검사에 실패한 인증서를 허용합니다. 로컬 개발 환경에서만 사용하십시오. |
| TlsCaCertificatePath | `string` | `null` | `TlsCaCertificatePath` | 신뢰할 수 있는 루트 인증서가 하나 이상 포함된 PEM 파일의 경로입니다. 호스트의 신뢰 저장소를 대체하며 `TlsAllowInvalidCertificates`와 함께 사용할 수 없습니다. |
| ConfigureTls | `Action<SslClientAuthenticationOptions>` | `null` | — | 다른 TLS 속성이 적용된 후 각 핸드셰이크 전에 TLS 옵션을 조정합니다. `ClickHouseTcpClientOptions`를 통해서만 사용할 수 있습니다. |

<Warning>
  TLS를 사용하지 않으면 비밀번호와 모든 쿼리 데이터가 암호화되지 않은 채 네트워크를 통해 전송됩니다. 로컬 개발 환경이 아니라면
  `TlsAllowInvalidCertificates`를 `false`로 설정해 두십시오.
</Warning>

보안 native protocol은 기존 연결에서 보안 연결로 전환하는 방식이 아니라 별도의 서버 엔드포인트를 사용합니다. TLS
클라이언트는 보안 native protocol 포트에 연결해야 하며, 평문 연결로 폴백되지 않습니다. `ConfigureTls`는
마지막에 적용되며 인증서 및 호스트명 유효성 검사를 재정의할 수 있으므로 사용자 정의 콜백을
주의 깊게 검토하십시오.

<h3 id="logging-debugging">
  로깅 및 디버깅
</h3>

| 속성 | 유형 | 기본값 | 연결 문자열 키 | 설명 |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | `ClickHouse.Driver.Tcp.*` 카테고리에서 연결, 풀, 작업의 수명 주기 이벤트를 기록하는 로거를 제공합니다 |
| IncludeSqlInActivityTags | `bool` | `false` | — | SQL 문 텍스트를 OpenTelemetry `db.query.text` 속성에 추가합니다. 텍스트 길이는 `StatementMaxLength`로 제한됩니다 |
| StatementMaxLength | `int` | `5` | — | 디버그 로그와 활동 태그에 포함되는 SQL 문의 최대 문자 수입니다. 0 이하이면 텍스트를 포함하지 않습니다 |

클라이언트 로거는 클라이언트 측의 수명 주기 및 작업 이벤트를 기록합니다. 서버 로그 패킷은
대신 `ClickHouseTcpQueryCallbacks.OnLog`로 전송됩니다. SQL 문 텍스트에는 민감한 데이터가 포함될 수 있으므로 기본적으로
활동 태그에서는 제외하고 디버그 로그에서는 길이를 대폭 줄입니다.

<h3 id="custom-settings">
  사용자 지정 설정
</h3>

`ClickHouseTcpClientOptions.CustomSettings`는 모든 쿼리와 삽입 작업에 ClickHouse 서버 설정을 적용합니다. 작업별 `ClickHouseTcpQueryOptions.Settings` 값은 이름이 같은 클라이언트 수준의 값을 재정의합니다.

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    CustomSettings = new Dictionary<string, string>
    {
        ["max_threads"] = "4",
    },
};
```

연결 문자열에서 각 서버 설정 이름 앞에 `set_` 접두사를 붙이세요.

```text theme={null}
Host=my.clickhouse;set_max_threads=4;set_max_memory_usage=10000000000
```

`CustomSettings` 또는 작업별 `Settings`에 설정을 추가할 때는 `set_` 접두사를 붙이지 마세요.

<h3 id="query-options">
  ClickHouseTcpQueryOptions
</h3>

`ClickHouseTcpQueryOptions`는 작업별 설정을 제공합니다. 모든 속성은 선택 사항입니다.

| 속성 | 유형 | 기본값 | 설명 |
| - | - | - | - |
| QueryId | `string` | `null` | `system.query_log`, 로그, 트레이스 스팬에 사용되는 식별자입니다. `null`이거나 비어 있으면 작업마다 새 GUID가 생성됩니다 |
| Settings | `IReadOnlyDictionary<string, string>` | `null` | 이 작업에 적용할 서버 설정입니다. 동일한 설정이 클라이언트 수준의 `CustomSettings`에도 있으면 이 값이 우선합니다 |
| Parameters | `ClickHouseTcpParameterCollection` | `null` | SQL 문의 `{name:Type}` 플레이스홀더에 바인딩되는 값입니다 |
| Callbacks | `ClickHouseTcpQueryCallbacks` | `null` | 진행 상황, 프로필 정보, 서버 로그, 프로필 이벤트, 합계, 극값을 처리하는 동기식 콜백입니다 |

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    QueryId = $"report-{Guid.NewGuid():N}",
    Settings = new Dictionary<string, string>
    {
        ["max_threads"] = "4",
    },
};

object count = await client.ExecuteScalarAsync(
    "SELECT count() FROM numbers(1000)",
    queryOptions);
```

<h3 id="insert-options">
  ClickHouseTcpInsertOptions
</h3>

`ClickHouseTcpInsertOptions`는 `ClickHouseTcpQueryOptions`를 확장하여 네이티브 블록 삽입에 필요한 설정을 추가합니다.
모든 쿼리 옵션 속성도 사용할 수 있습니다.

| 속성 | 유형 | 기본값 | 설명 |
| - | - | - | - |
| MaxRowsPerBlock | `int?` | `50,000` | wire 블록당 최대 행 수입니다. `null`로 설정하면 삽입 데이터를 하나의 블록으로 기록합니다. 값을 낮추면 행 및 POCO 삽입에 필요한 임시 메모리 사용량도 줄어듭니다. |
| DeduplicationToken | `string` | `null` | 서버 측 삽입 중복 제거에 사용할 배치를 식별합니다. 같은 배치를 재시도할 때는 동일한 토큰을 재사용하고, 새 배치에는 각각 다른 토큰을 사용합니다. |

```csharp theme={null}
var insertOptions = new ClickHouseTcpInsertOptions
{
    MaxRowsPerBlock = 10_000,
    DeduplicationToken = batchContentHash,
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, name) VALUES",
    rows,
    insertOptions);
```

<h2 id="clickhouse-tcp-client">
  ClickHouseTcpClient
</h2>

`ClickHouseTcpClient`는 네이티브 TCP 프로토콜용 고수준 API입니다. SQL 문과
스칼라 쿼리를 실행하고, 결과를 열 지향 블록, 행 또는 POCO 형태로 스트리밍하며, 열 지향 데이터나 행 데이터를 삽입하고,
특정 연결에 고정된 세션을 엽니다.

이 클라이언트는 여러 스레드에서 안전하게 사용할 수 있으며 연결 풀을 자체적으로 관리합니다. 엔드포인트마다 클라이언트를 하나씩 생성해
애플리케이션 전체에서 공유하고 종료할 때 리소스를 해제하세요. 작업은
`MaxPoolSize`에 지정된 한도까지 동시에 실행되며, 한도를 초과한 작업은 연결을 얻을 때까지 최대 `PoolTimeout` 동안 대기합니다.

<h3 id="creating-a-client">
  클라이언트 생성
</h3>

연결 문자열(connection string) 또는 `ClickHouseTcpClientOptions`를 사용해 클라이언트를 생성합니다. 사용 가능한 설정은
[구성](#configuration)을 참조하세요.

연결 문자열을 사용하는 경우:

```csharp theme={null}
using ClickHouse.Driver.Tcp;

await using var client = new ClickHouseTcpClient(
    "Host=localhost;Port=9000;Username=default");
```

타입이 엄격하게 지정된 옵션 사용:

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    Username = "user",
    UseTls = true,
    MaxPoolSize = 20,
};

await using var client = new ClickHouseTcpClient(options);
```

비동기 해제를 권장합니다. 클라이언트를 해제하면 유휴 연결을 닫고 진행 중인 작업이 완료되기를
최대 `PoolTimeout`까지 기다린 후, 완료되지 않은 작업을 중단합니다.

<h3 id="dependency-injection">
  의존성 주입
</h3>

`AddClickHouseTcpDataSource`는 싱글턴 데이터 소스 하나를 등록하고, 공유 클라이언트를
`IClickHouseTcpClient`와 `IClickHouseTcpOperations`로 사용할 수 있게 합니다.

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using Microsoft.Extensions.DependencyInjection;

services.AddClickHouseTcpDataSource(
    "Host=my.clickhouse;UseTls=true;Username=user");

public sealed class ReportService(IClickHouseTcpClient client)
{
    public async Task<object> CountAsync(CancellationToken cancellationToken) =>
        await client.ExecuteScalarAsync(
            "SELECT count() FROM events",
            cancellationToken: cancellationToken);
}
```

서비스 제공자가 데이터 소스와 연결 풀을 관리합니다. 주입된 클라이언트는 해제하지 말고,
애플리케이션 종료 시 서비스 제공자를 해제하세요. 옵션에 `LoggerFactory`가 지정되지 않은 경우
등록 시 컨테이너의 `ILoggerFactory`를 사용합니다. `serviceKey`를 사용하면 서로 독립적인
여러 데이터 소스를 등록할 수 있습니다.

<h3 id="executing-queries">
  쿼리 실행
</h3>

DDL이나 뮤테이션과 같이 행을 반환하지 않는 SQL 문에는 `ExecuteAsync`를 사용하세요.

```csharp theme={null}
await client.ExecuteAsync("TRUNCATE TABLE events");
```

이 메서드는 서버가 SQL 문을 수신했음을 확인하면 완료됩니다. SQL 문이 행을 반환하는 경우
`ExecuteAsync`는 반환된 행을 읽고 버립니다.
`INSERT INTO ... VALUES`를 사용할 때는 `InsertAsync` 또는 `InsertRowsAsync`를 사용하고, 값을 직접 추가하지 말고 SQL 문을
`VALUES`에서 끝내십시오.

<h3 id="reading-data">
  데이터 읽기
</h3>

애플리케이션에 필요한 결과 형태에 맞춰 읽기 API를 선택하세요.

| API | 결과 | 절충 관계 |
| - | - | - |
| `ExecuteScalarAsync` | 첫 번째 행의 첫 번째 컬럼 | 단일 값 쿼리에 가장 간편하지만, 전체 결과를 끝까지 읽어 처리합니다 |
| `QueryAsync` | 각 행의 독립적인 `object[]` | 모델이 필요 없지만, 행마다 배열을 할당하고 값 타입을 박싱합니다 |
| `QueryAsync<T>` | 각 행의 독립적인 POCO | 명시적인 타입에 따라 매핑하지만, 행마다 객체를 하나씩 할당하고 값을 채웁니다 |
| `StreamAsync` | 빌려 쓰는 열 지향 `Block` 값 | 가장 빠르며 메모리 할당, 복사, 행 단위 머티리얼라이즈를 피합니다 |

<h4 id="scalar-reads">
  스칼라 읽기
</h4>

`ExecuteScalarAsync`는 첫 번째 행의 첫 번째 컬럼 값을 `object`로 반환합니다.

```csharp theme={null}
object value = await client.ExecuteScalarAsync("SELECT count() FROM events");
ulong count = (ulong)value;
```

이 메서드는 응답 전체를 읽고 첫 번째 값을 제외한 나머지 값은 버립니다. 결과가 많은 쿼리를 조기에 중단하는 용도로 사용하지 말고
한 행만 반환하는 쿼리를 작성하세요. 첫 번째 값이 `NULL`이거나 쿼리가
행을 반환하지 않으면 모두 `null`이 반환됩니다.

<h4 id="row-reads">
  행 읽기
</h4>

`QueryAsync`는 각 행을 해당 행 전용 `object[]`로 스트리밍하며, 배열의 값은 결과 컬럼 순서대로 들어 있습니다.

```csharp theme={null}
await foreach (object[] row in client.QueryAsync(
    "SELECT id, name, score FROM events ORDER BY id"))
{
    Console.WriteLine($"{row[0]}: {row[1]} ({row[2]})");
}
```

열거가 진행된 후에도 행 배열은 계속 유효합니다. 이 방식은 결과
모델이 없을 때 편리하지만, 행마다 배열을 하나씩 할당하고 값 타입을 박싱합니다.

<h4 id="poco-reads">
  POCO 읽기
</h4>

`QueryAsync<T>`는 컬럼을 값을 설정할 수 있는 공개 속성에 매핑합니다. 이름을 비교할 때는 대소문자와
밑줄을 구분하지 않습니다. 속성 이름을 명시적으로 지정하려면 `ClickHouseTcpColumn`을, 매핑에서
속성을 제외하려면 `ClickHouseTcpNotMapped`를 사용하세요.

```csharp theme={null}
public sealed class Event
{
    public ulong Id { get; set; }

    [ClickHouseTcpColumn(Name = "event_name")]
    public string Name { get; set; } = string.Empty;

    public DateTime RecordedAt { get; set; }
}

await foreach (Event row in client.QueryAsync<Event>(
    "SELECT id, event_name, recorded_at FROM events ORDER BY id"))
{
    Console.WriteLine($"{row.Id}: {row.Name} at {row.RecordedAt:O}");
}
```

POCO 매핑은 데이터 변환을 수행하고 각 행에 객체 하나를 할당합니다.

<h4 id="columnar-block-reads">
  열 지향 블록 읽기
</h4>

`StreamAsync`는 네이티브 열 지향 쿼리 결과를 `Block` 값의 시퀀스로 반환합니다. 데이터를 읽는 가장 빠른 방법입니다.

```csharp theme={null}
await foreach (Block block in client.StreamAsync(
    "SELECT id, score FROM events ORDER BY id"))
{
    ReadOnlySpan<ulong> ids = block.Column<ulong>("id").Values;
    ReadOnlySpan<double> scores = block.Column<double>("score").Values;

    for (int row = 0; row < block.RowCount; row++)
    {
        Console.WriteLine($"{ids[row]}: {scores[row]}");
    }
}
```

블록은 빌려 쓰는 객체입니다. 블록과 그 컬럼 및 값 스팬은 현재
반복에서만 유효합니다. 이를 직접 해제하거나 반복 이후까지 보관하지 마세요. 반복문 본문이 끝난 뒤에도 필요한 데이터는 소유한 메모리에 명시적으로 복사해야 합니다.

<Note>
  연결이 풀로 반환되도록 열거를 끝까지 완료하거나 열거자를 해제하세요.
</Note>

`Block.Column<T>`은 디코더가 생성한 CLR 타입에 접근합니다. 변환이 필요하면 `Block.ReadAs<T>` 또는
`IDateTimeColumn`과 같은 전용 인터페이스를 사용하세요.

<h3 id="inserting-data">
  데이터 삽입
</h3>

네이티브 삽입은 데이터를 열 지향 wire 블록 형태로 전송합니다. SQL 문은 값 목록을 직접 포함하지 않는 `INSERT INTO ... VALUES`
형식이어야 합니다. 클라이언트 측 작업을 최소화하려면 타입이 지정된 컬럼을 사용하세요. 애플리케이션에 이미 행 지향 데이터가 있다면 행 또는
POCO 오버로드를 사용하세요.

<h4 id="columnar-inserts">
  열 지향 삽입
</h4>

`InsertAsync`는 타입이 지정된 컬럼별로 미리 묶어 놓은 데이터를 받습니다. 이 방식이 데이터를 삽입하는 가장 빠른 방법입니다.

```csharp theme={null}
var columns = new IColumn[]
{
    ClickHouseTcpColumn.Create("id", new ulong[] { 1, 2, 3 }),
    ClickHouseTcpColumn.Create("name", new[] { "Ada", "Grace", "Alan" }),
    ClickHouseTcpColumn.Create("score", new[] { 99.5, 97.25, 91.0 }),
};

await client.InsertAsync(
    "INSERT INTO events (id, name, score) VALUES",
    columns);
```

컬럼은 인수 순서가 아니라 이름을 기준으로 SQL 컬럼 목록과 일치해야 하며, 각 컬럼의 행
수도 같아야 합니다. SQL 문에서 생략된 테이블 컬럼의 기본값은 서버가 채웁니다. 삽입이 완료될 때까지
호출자가 소유한 배열을 수정하지 마세요.

이 API를 사용하면 행 삽입에 필요한 행에서 컬럼으로의 변환과 값 유형의 박싱을 피할 수 있습니다. 빌려온 결과 블록에서
읽은 컬럼도 해당 블록이 해제되기 전에 삽입이 완료된다면 `InsertAsync`에 직접 전달할 수 있습니다.

<h4 id="row-inserts">
  행 삽입
</h4>

`InsertRowsAsync`는 `object[]` 형식의 행을 받습니다. 각 값은 순서대로 SQL 컬럼 목록의 컬럼에 대응합니다.

```csharp theme={null}
var rows = new[]
{
    new object[] { 1UL, "Ada", 99.5 },
    new object[] { 2UL, "Grace", 97.25 },
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, name, score) VALUES",
    rows);
```

행은 블록 단위로 컬럼(column)으로 변환됩니다. 각 행에는 SQL 문에 명시된 모든 컬럼의 값이 하나씩 있어야 하며,
대상 컬럼이 `Variant` 또는 `Dynamic`인 경우를 제외하면 같은 컬럼의 값은 모두 동일한 CLR 타입을 사용해야 합니다.
작업이 완료될 때까지 행을 수정하지 마십시오.

<h4 id="poco-inserts">
  POCO 삽입
</h4>

제네릭 `InsertRowsAsync<T>` 오버로드는 POCO 읽기에 적용되는 것과 동일한 명명 및 속성 규칙에 따라 대상 컬럼을 공개 읽기 가능 프로퍼티에 매핑합니다.

```csharp theme={null}
public sealed class EventToInsert
{
    public ulong Id { get; set; }

    [ClickHouseTcpColumn(Name = "event_name")]
    public string Name { get; set; } = string.Empty;

    public DateTime RecordedAt { get; set; }

    [ClickHouseTcpNotMapped]
    public string? ImportBatch { get; set; }
}

var rows = new[]
{
    new EventToInsert
    {
        Id = 1,
        Name = "Ada",
        RecordedAt = DateTime.UtcNow,
        ImportBatch = "batch-42",
    },
    new EventToInsert
    {
        Id = 2,
        Name = "Grace",
        RecordedAt = DateTime.UtcNow,
        ImportBatch = "batch-42",
    },
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, event_name, recorded_at) VALUES",
    rows);
```

SQL 문에 지정된 모든 컬럼은 호환되는 public getter에 매핑되어야 합니다. POCO 행은
wire 블록 단위로 변환되므로, 앞선 블록이 이미 전송된 뒤에야 이후 블록의
유효하지 않은 값이 발견될 수 있습니다. `ClickHouseTcpNotMapped`를 사용하면
`ImportBatch`처럼 애플리케이션에서만 사용하는 속성을 매핑에서 제외할 수 있습니다.

<h3 id="sql-parameters">
  SQL 매개변수
</h3>

네이티브 클라이언트는 값을 ClickHouse 고유의 `{name:Type}` 플레이스홀더에 바인딩합니다. ADO.NET 스타일의 `@name` 플레이스홀더를
재작성하지는 않습니다.

```csharp theme={null}
var parameters = new ClickHouseTcpParameterCollection
{
    { "minimum", 18.0 },
    { "ids", new ulong[] { 1, 2 } },
};

var options = new ClickHouseTcpQueryOptions { Parameters = parameters };

await foreach (object[] row in client.QueryAsync(
    "SELECT id, score FROM events " +
    "WHERE score >= {minimum:Float64} AND id IN {ids:Array(UInt64)}",
    options))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

각 매개변수는 SQL 플레이스홀더 또는
`ClickHouseTcpParameter.ClickHouseType` 속성으로 유형을 지정해야 합니다. 테이블과 컬럼
이름에는 `Identifier` 유형을 사용하세요. `DateTime` 또는 `DateTimeOffset`이 특정 시점을 나타내는 경우
`{timestamp:DateTime('UTC')}`처럼 시간대를 명시하세요. 그렇지 않으면 서버 세션의 시간대에 따라 값이 가리키는 시점이 달라질 수 있습니다.

<h3 id="query-id">
  쿼리 ID
</h3>

모든 작업에는 ClickHouse 시스템 테이블, 클라이언트 로그, 추적 스팬에서 사용하는 쿼리 ID가 있습니다. `QueryId`가 null이거나 비어 있으면 클라이언트가 새 GUID를 생성합니다. 작업을 애플리케이션 식별자와 연관시켜야 할 때는 쿼리 ID를 설정하세요:

```csharp theme={null}
var options = new ClickHouseTcpQueryOptions
{
    QueryId = $"report-{Guid.NewGuid():N}",
};

await client.ExecuteAsync("OPTIMIZE TABLE events FINAL", options);
```

<h3 id="query-progress-metadata">
  쿼리 진행 상황 및 메타데이터
</h3>

응답 중간에 전달되는 메타데이터를 수신하려면 `ClickHouseTcpQueryOptions.Callbacks`를 설정하세요.

```csharp theme={null}
ClickHouseTcpProgress total = default;

var options = new ClickHouseTcpQueryOptions
{
    Callbacks = new ClickHouseTcpQueryCallbacks
    {
        OnProgress = progress => total += progress,
        OnProfileInfo = profile =>
            Console.WriteLine($"{profile.Rows} rows in {profile.Blocks} blocks"),
    },
};

await foreach (object[] _ in client.QueryAsync(
    "SELECT number FROM numbers(1000000)",
    options))
{
}
```

진행 상황 값은 누적 합계가 아닌 증가분입니다. 콜백은 응답을 읽는
스레드에서 패킷 순서대로 동기적으로 실행됩니다. 콜백은 신속하게 처리하고 예외를 던지지 않도록 하십시오. `OnLog`, `OnProfileEvents`, `OnTotals`, `OnExtremes`에 전달되는 블록은 일시적으로 참조하는 것이므로
콜백 밖으로 전달해서는 안 됩니다.

ClickHouse는 삽입 작업으로 업로드된 행에 대한 쿼리 진행 상황 패킷을 보내지 않습니다. 클라이언트가 전송한
블록의 진행 상황을 확인하려면 `OnBlockWritten`을 사용하십시오. 이 콜백은 전송 진행 상황을 알려 줄 뿐,
서버가 삽입을 커밋했음을 뜻하지는 않습니다.

<h3 id="cancellation">
  취소
</h3>

모든 작업에 `CancellationToken`을 전달할 수 있습니다.

```csharp theme={null}
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(2));

try
{
    await foreach (object[] row in client.QueryAsync(
        "SELECT number, sleepEachRow(0.1) FROM numbers(100)",
        cancellationToken: cancellation.Token))
    {
        Console.WriteLine(row[0]);
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Query cancelled.");
}
```

스트리밍된 결과를 취소하거나 끝까지 처리하지 않으면 해당 연결을 안전하게 재사용할 수 없으므로 클라이언트는
연결을 풀에 반환하지 않고 닫습니다. 클라이언트는 계속 사용할 수 있으며, 이후 작업에는 다른 연결을
열거나 기존 연결을 재사용합니다. 따라서 고정된 세션에서 작업을 취소하면 세션이 종료되고 임시 테이블과 설정이
손실될 수 있습니다.

<h3 id="server-information-health-checks">
  서버 정보 및 상태 검사
</h3>

`GetServerInfoAsync`는 네이티브 핸드셰이크에서 확인한 서버 아이덴티티, 버전, 시간대 및 협상된 프로토콜 정보를
반환합니다.

```csharp theme={null}
ClickHouseTcpServerInfo server = await client.GetServerInfoAsync();

Console.WriteLine($"ClickHouse {server.Version}");
Console.WriteLine($"Protocol revision: {server.ProtocolRevision}");
Console.WriteLine($"Server timezone: {server.Timezone}");
```

wire 수준의 기능은 클라이언트와 서버가 협상한 revision인 `ProtocolRevision`에 따라 사용 가능 여부를 판단합니다.
데이터 타입과 함수 같은 SQL 기능은 `Version`에 따라 사용 가능 여부를 판단합니다. 이 메서드는 기존 연결을
사용할 수 있으면 재사용하고, 풀이 비어 있으면 새 연결을 엽니다.

native protocol을 통한 준비 상태 확인에는 `PingAsync`를 사용하세요.

```csharp theme={null}
await client.PingAsync(cancellationToken);
```

Ping에 성공하면 네이티브 연결과 인증이 정상적으로 작동하는지 확인할 수 있습니다. 다만 애플리케이션
테이블에 대한 액세스 가능 여부는 확인할 수 없습니다.

<h3 id="more-examples">
  추가 예시
</h3>

읽기, 삽입, 타입, 세션, TLS, 관측성 및
오류 처리를 다루는 완전한 실행 가능 예시는
GitHub 리포지토리의
[네이티브 TCP 예시](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples/Tcp)를 참조하세요.

<h2 id="sessions">
  세션
</h2>

일반적인 클라이언트 작업은 풀에서 사용 가능한 연결을 빌려 사용하므로, 연결에 종속된 상태는
격리되지 않고 호출 간에 유지된다는 보장도 없습니다. 풀의 연결마다 서로 다른 서버에 접속할 수도 있습니다.
임시 테이블이나 `SET` SQL 문을 사용하거나 일련의 작업을 동일한 연결에서 실행해야 한다면 세션을 여세요.

```csharp theme={null}
await using IClickHouseTcpSession session = await client.OpenSessionAsync();

await session.ExecuteAsync("SET max_threads = 2");
await session.ExecuteAsync("""
    CREATE TEMPORARY TABLE session_events
    (
        id UInt64,
        name String
    )
    ENGINE = Memory
    """);

await session.InsertRowsAsync(
    "INSERT INTO session_events (id, name) VALUES",
    new[]
    {
        new object[] { 1UL, "Ada" },
        new object[] { 2UL, "Grace" },
    });

object count = await session.ExecuteScalarAsync(
    "SELECT count() FROM session_events");
```

`IClickHouseTcpSession`은 클라이언트와 동일한 쿼리, 실행, 삽입, 핑, 서버 정보 API를
제공합니다. 세션은 스레드로부터 안전하지 않습니다. 세션에서는 작업이 순차적으로 처리되므로 현재 작업이
끝나기 전에 다른 작업을 시작하면 요청이 거부됩니다. 스트리밍 결과를 끝까지 순회하거나
열거자를 해제할 때까지 세션은 사용 중인 상태로 유지됩니다.

각 세션은 유지되는 동안 `MaxPoolSize` 슬롯 1개를 차지합니다. 클라이언트는 풀의 다른
연결을 계속 사용할 수 있지만, 풀에 연결이 1개뿐이면 다른 클라이언트 작업은
세션이 해제되거나 `PoolTimeout`이 만료될 때까지 대기합니다.

상위 클라이언트를 해제하기 전에 세션을 해제하세요. 세션을 해제하면 전용으로 할당된
연결을 풀에 반환하지 않고 닫으므로, 이후 호출자가 해당 세션의
임시 테이블이나 설정을 이어받지 않습니다. 연결 또는 프로토콜 오류, 취소, 결과 스트림의
처리 중단으로 세션을 더 이상 사용할 수 없게 될 수 있습니다. `IsOpen`이 `false`가 되면 새 세션을 여세요.

<h2 id="best-practices">
  모범 사례
</h2>

<h3 id="best-practices-client-lifetime">
  클라이언트 수명과 풀링
</h3>

엔드포인트와 구성의 조합마다 `ClickHouseTcpClient` 또는 `ClickHouseTcpDataSource`를 하나씩 생성하고
애플리케이션이 실행되는 동안 재사용하세요. 클라이언트는 스레드 안전성을 보장하며 자체 연결 풀을 관리합니다.
작업마다 클라이언트를 생성하면 매번 새 풀이 만들어져 연결을 재사용할 수 없습니다.

의존성 주입을 사용하는 애플리케이션에서는 `AddClickHouseTcpDataSource`를 한 번 등록하고
`IClickHouseTcpClient`를 주입하세요. 서비스 공급자가 클라이언트를 관리하므로 주입받아 사용하는 쪽에서
인스턴스를 해제해서는 안 됩니다. 의존성 주입을 사용하지 않는다면 애플리케이션 종료 시 `await using`으로 클라이언트를 해제하세요.

각 네이티브 연결은 한 번에 하나의 작업만 처리합니다. 따라서 `MaxPoolSize`는 열린 연결 수와
동시 작업 수를 모두 제한하며, 추가 호출자는 슬롯이 확보될 때까지 최대 `PoolTimeout`만큼 기다립니다.
서버 측 연결 및 쿼리 제한을 초과하지 않으면서 애플리케이션의 실제 동시 작업량을 감당할 수 있는 값을 선택하세요.
이 값을 높여도 개별 쿼리가 빨라지지는 않습니다.

다음과 같이 풀의 연결을 사용하는 작업에 유의하세요.

* 스트리밍 결과는 열거자가 해제될 때까지 연결을 점유합니다.
* 세션은 유지되는 동안 연결 하나를 점유하며, 한 번에 하나의 작업만 처리합니다.
* 결과 처리를 중단하거나, 진행 중인 작업을 취소하거나, 프로토콜 또는 연결에 장애가 발생하면
  해당 연결은 재사용되지 않고 닫힙니다.

세션은 사용 후 즉시 해제하고, 비동기 결과는 항상 끝까지 열거하거나 열거자를 해제하세요.
풀 고갈, 예기치 않은 연결 종료, 빈번한 재연결을 진단하려면 `ClickHouse.Driver.Tcp.Pool` 로깅 범주를 사용하세요.

<h3 id="best-practices-timeouts">
  타임아웃과 취소
</h3>

TCP 클라이언트는 대기 상황에 따라 서로 다른 시간 제한을 적용합니다.

| 메커니즘 | 제한 대상 |
| - | - |
| `DialTimeout` | 새 연결을 설정할 때 소켓 연결, TLS 협상, 네이티브 핸드셰이크에 걸리는 시간 |
| `PoolTimeout` | 모든 연결이 사용 중일 때 풀 슬롯을 기다리는 시간 |
| `ReadTimeout` | 응답을 읽는 동안 서버가 한 차례 응답하지 않는 시간. 전체 쿼리 실행 시간은 아님 |
| `CancellationToken` | 풀 대기와 결과 열거를 포함해 호출자가 제어하는 작업 전체 |
| `max_execution_time` | 서버 측 쿼리 실행 시간 |

풀 슬롯을 기다렸다가 새 연결을 여는 데는 최대
`PoolTimeout + DialTimeout`이 걸릴 수 있습니다. `ReadTimeout`은 전송 계층에서 읽을 때마다 다시 시작되므로,
진행 상황이나 데이터를 계속 보내는 장기 실행 쿼리는 이 시간보다 오래 실행될 수 있습니다. 정상적인
작업에서도 서버가 응답하지 않을 수 있는 시간을 고려해 충분히 길게 설정하세요. 다른 타임아웃으로 작업 시간이
확실히 제한되는 경우에만 `TimeSpan.Zero`로 설정하세요.

작업 시작부터 끝까지 타임아웃을 적용하려면 취소 토큰을 사용하세요.

```csharp theme={null}
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));

var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["max_execution_time"] = "25",
    },
};

await foreach (Block block in client.StreamAsync(
    "SELECT * FROM large_report",
    queryOptions,
    timeout.Token))
{
    // Process each block before advancing.
}
```

클라이언트는 안전하게 전송할 수 있는 경우 네이티브 Cancel 패킷을 전송한 다음, 응답을 끝까지 읽지 못한
연결을 닫습니다. 이후에도 풀에 있는 클라이언트는 사용할 수 있지만,
취소로 인해 고정된 세션은 사용할 수 없게 될 수 있습니다. 취소되거나 시간 초과된 삽입은
처리 결과를 알 수 없으므로, 무작정 다시 실행하지 말고 [삽입 재시도 지침](#error-handling-retries)을 따르세요.

<h3 id="best-practices-datetime">
  날짜 및 시간 처리
</h3>

타임스탬프에는 `DateTime('UTC')` 또는 `DateTime64(S, 'UTC')`를 사용하고, 애플리케이션 코드에서는 `DateTimeOffset` 또는
`Kind`가 `Utc`인 `DateTime`을 사용하세요. 이렇게 하면 나타내는 시점이 명확해지고
서버나 세션 시간대에 의존하지 않습니다.

TCP 클라이언트는 `DateTime` 값을 기록할 때 다음 규칙을 적용합니다.

| `DateTime.Kind` | 해석 |
| - | - |
| `Utc` | UTC 시점이 유지됩니다. |
| `Local` | 값이 해당하는 UTC 시점으로 변환됩니다. |
| `Unspecified` | 시계에 표시된 시각을 컬럼 시간대 기준으로 해석하며, 컬럼에 시간대가 지정되지 않았다면 세션 시간대를 기준으로 해석합니다. |

일광 절약 시간 전환으로 건너뛰어 실제로 존재하지 않는 시각은 해당하는
시점이 없으므로 거부됩니다. 시계를 되돌릴 때 중복되는 시각은 먼저 발생한 시점을 선택합니다. 어느 시점인지
명시해야 한다면 `DateTimeOffset`을 사용하세요.

특정 시점을 나타내는 SQL 매개변수는 플레이스홀더에 시간대를 포함하세요.

```csharp theme={null}
var parameters = new ClickHouseTcpParameterCollection
{
    new ClickHouseTcpParameter(
        "captured_at",
        DateTimeOffset.UtcNow,
        "DateTime64(3, 'UTC')"),
};

var queryOptions = new ClickHouseTcpQueryOptions
{
    Parameters = parameters,
};

await client.ExecuteAsync(
    "INSERT INTO events (captured_at) VALUES ({captured_at:DateTime64(3, 'UTC')})",
    queryOptions);
```

열 지향 읽기에서는 `DateTime`과 `DateTime64`가 기본적으로 원시 `uint` 또는 `long` 값을 반환합니다.
날짜와 시간으로 표현된 값이 더 편리하다면 `Block.ReadAs<DateTimeOffset>`, `IDateTimeColumn.GetDateTimeOffset` 또는
해당하는 POCO 속성을 사용하세요. 모든 자릿수를 보존해야 한다면 원시 값을 그대로 사용하세요.
`DateTimeOffset`의 해상도는 100나노초이므로 소수점 자릿수가 8 또는 9인 `DateTime64`를 이를 통해 읽으면
틱보다 작은 단위의 정밀도가 손실됩니다.

`Date`와 `Date32`에는 `DateOnly`를 사용하세요. 음수이거나 24시간을 초과할 수 있는 `Time`과 `Time64` 값에는
`TimeSpan`을 사용하세요. `TimeOnly`는 컬럼 값이 하루 중 시각으로 제한된 경우에만 적합합니다.

<h3 id="best-practices-streaming">
  스트리밍과 결과의 수명
</h3>

고처리량 작업에는 `StreamAsync`와 타입이 지정된 컬럼을 사용하세요. 이는 ClickHouse의
열 지향 저장 구조에 맞으며 행마다 배열을 생성하거나 박싱 및 POCO 할당을 수행하지 않아도 됩니다.
메모리 할당을 최소화하는 것보다 애플리케이션이 소유하는 모델이 중요하다면 `QueryAsync<T>`를 사용하고,
컴파일 시점에 스키마를 알 수 없을 때만 타입이 지정되지 않은 `QueryAsync`를 사용하세요.

`StreamAsync`가 반환하는 블록은 임시로 빌려 쓰는 데이터입니다. 블록과 그 컬럼, 복합 하위 컬럼,
모든 스팬은 현재 루프 반복에서만 유효합니다. 해당 반복 중에 바로 처리하거나 반복이 끝난 뒤에도
필요한 값은 복사하세요:

```csharp theme={null}
ulong[] retained;

await foreach (Block block in client.StreamAsync("SELECT id FROM events"))
{
    ReadOnlySpan<ulong> ids = block.Column<ulong>("id").Values;
    retained = ids.ToArray();
}
```

반환된 블록을 직접 해제하지 마십시오. 열거자 해제는 `await foreach`에 맡기십시오. 결과의 일부만
필요하다면 SQL에서 `LIMIT`로 결과를 제한하고 필요한 컬럼만 선택하십시오. 반복을
도중에 중단하면 남은 응답이 취소되어 해당 연결을 재사용할 수 없게 됩니다.

<h3 id="best-practices-inserts">
  삽입 형태와 배칭
</h3>

데이터가 이미 열 지향 형태라면 타입이 지정된 컬럼과 함께 `InsertAsync`를 사용하는 것이 좋습니다. 이 방식은
클라이언트 측 변환이 가장 적습니다. POCO 데이터에는 `InsertRowsAsync<T>`가 가장 적합합니다. 타입이 지정되지 않은
`object[]` 행은 가장 유연하지만 배열을 할당하고 값 타입을 박싱합니다.

작은 삽입을 여러 번 실행하기보다 삽입 호출 한 번에 적절한 수의 행을 전송하세요. 호출 한 번에서
`MaxRowsPerBlock`은 데이터를 네이티브 wire 블록으로 나눕니다. 기본값인 50,000행은
행과 POCO 입력을 변환할 때 사용하는 임시 컬럼 버퍼의 크기도 제한합니다. 이 값을 늘리면
폭이 좁은 행의 처리량이 개선될 수 있지만 최대 메모리 사용량도 증가합니다. `null`로 설정하면 전체
삽입을 하나의 블록으로 기록하며, 해당 블록의 행 지향 입력 전체를 한꺼번에 변환합니다.

`MaxSendBufferBytes`는 인코딩된 데이터가 소켓으로 플러시되기 전까지 버퍼에 저장되는 양을 별도로
제한합니다. 컬럼 간에 확인하는 유연한 상한이므로 폭이 넓은 컬럼 하나만으로도 이 값을 초과할 수 있습니다.

행과 POCO 입력은 블록 단위로 변환됩니다. 따라서 앞선 블록이 이미 전송된 뒤에야
뒤쪽 블록에서 잘못된 값이 발견될 수 있습니다. 데이터 품질이 불확실하다면 삽입 전에
NULL 허용 여부와 CLR 타입을 검사하고, 동일한 논리적 배치를 재시도할 가능성이 있다면 데이터에서 파생한 `DeduplicationToken`을 설정하세요.

<h2 id="supported-data-types">
  지원되는 데이터 타입
</h2>

아래 표는 디코딩된 `IColumn<T>`가 반환하는 타입과
`Block.ReadAs<T>`를 통한 변환, `QueryAsync<T>`의 POCO 매핑 또는 삽입을 통해 사용할 수 있는 추가 타입을 구분합니다. 특정 매핑의 지원 여부는 이러한 작업에 사용되는
리졸버를 통해 확인할 수 있습니다.

```csharp theme={null}
bool canRead = ClickHouseTcpTypes.CanRead("Array(Nullable(DateTime))", typeof(DateTime?[]));
bool canWrite = ClickHouseTcpTypes.CanWrite("FixedString(8)", typeof(byte[]));
```

`Nullable`, `Array`, `Tuple`, `Map`, `LowCardinality`의 매핑은
각 타입의 내부 타입에도 재귀적으로 적용됩니다.

<h3 id="type-map-reading">
  타입 매핑: ClickHouse에서 읽기
</h3>

`Default .NET type`은 `Block.Column<T>`와 `IColumn<T>`에서 제공하는 `T`입니다.
`Also readable as` 컬럼에 나열된 타입은 `Block.ReadAs<T>`와 POCO 속성을 통해서도 읽을 수 있습니다.

<h4 id="type-map-reading-integer">
  정수 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `Int8` | `sbyte` |
| `UInt8` | `byte` |
| `Int16` | `short` |
| `UInt16` | `ushort` |
| `Int32` | `int` |
| `UInt32` | `uint` |
| `Int64` | `long` |
| `UInt64` | `ulong` |
| `Int128` | `Int128` |
| `UInt128` | `UInt128` |
| `Int256` | `Int256` |
| `UInt256` | `UInt256` |

<h4 id="type-map-reading-floating-point">
  부동 소수점 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `Float32` | `float` |
| `Float64` | `double` |
| `BFloat16` | `float` |

<h4 id="type-map-reading-decimal">
  Decimal 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `Decimal(P, S)`, `P <= 18`인 경우 | `decimal` |
| `Decimal(P, S)`, `P > 18`인 경우 | `ClickHouseTcpDecimal` |
| `Decimal32(S)` | `decimal` |
| `Decimal64(S)` | `decimal` |
| `Decimal128(S)` | `ClickHouseTcpDecimal` |
| `Decimal256(S)` | `ClickHouseTcpDecimal` |

`ClickHouseTcpDecimal`은 크기 제한이 없는 정수 가수와 소수 자릿수를 저장하므로 `decimal`이 표현할 수 있는
정밀도를 초과하는 값도 보존합니다.

<h4 id="type-map-reading-boolean">
  불리언 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `Bool` | `bool` |

<h4 id="type-map-reading-strings">
  문자열 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 | 추가로 읽을 수 있는 타입 |
| - | - | - |
| `String` | `string` | `byte[]` |
| `FixedString(N)` | `byte[]` | `string` |

데이터가 유효한 UTF-8이 아닐 수 있다면 `IStringColumn.GetBytes(row)`를 사용하거나 `byte[]`로 읽으세요.
`FixedString(N)`은 끝에 붙은 0바이트를 포함해 `N`바이트를 모두 보존합니다.

<h4 id="type-map-reading-datetime">
  날짜 및 시간 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 | 추가로 읽을 수 있는 타입 |
| - | - | - |
| `Date` | `DateOnly` | — |
| `Date32` | `DateOnly` | — |
| `DateTime`, `DateTime('timezone')` | Unix 시간을 초 단위로 나타내는 `uint` | `DateTimeOffset`, `DateTime` |
| `DateTime64(S)`, `DateTime64(S, 'timezone')` | 정밀도 `S`에 따른 단위 수를 나타내는 `long` | `DateTimeOffset`, `DateTime` |
| `Time` | 시간을 초 단위로 나타내는 `int` | `TimeSpan`, `TimeOnly` |
| `Time64(S)` | 정밀도 `S`에 따른 단위 수를 나타내는 `long` | `TimeSpan`, `TimeOnly` |
| `IntervalNanosecond`부터 `IntervalYear`까지 | `long` | — |

열 지향으로 읽을 때 정확한 wire 값을 유지하기 위해 원시 정수 표현을 기본값으로 사용합니다.
날짜와 시간으로 변환할 때는 타입에 선언된 시간대를 사용하며, 선언된 시간대가 없으면 세션 시간대를
사용합니다. `Time` 또는 `Time64`를 `TimeOnly`로 읽을 때 값이 음수이거나
24시간 이상이면 예외가 발생합니다. ClickHouse의 전체 값 범위를 처리하려면 `TimeSpan`을 사용하세요.

<h4 id="type-map-reading-enum">
  Enum 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 | 추가로 읽을 수 있는 타입 |
| - | - | - |
| `Enum8(...)` | `sbyte` | `string` 레이블 |
| `Enum16(...)` | `short` | `string` 레이블 |

`IEnumColumn`에서는 선언된 레이블과 서수 값의 대응 관계도 확인할 수 있습니다.

<h4 id="type-map-reading-other-scalar">
  기타 스칼라 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `UUID` | `Guid` |
| `IPv4` | `IPAddress` |
| `IPv6` | `IPAddress` |
| `Nothing` | `object` (모든 행의 값은 `null`) |

<h4 id="type-map-reading-composite">
  복합 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 | 열 지향 뷰 |
| - | - | - |
| `Nullable(T)` | `T?` | `INullableColumn<T>` |
| `Array(T)` | `T[]` | `IArrayColumn<T>` |
| `Tuple(T1, ..., Tn)` | `(T1, ..., Tn)` | `ITupleColumn` |
| `Map(K, V)` | `KeyValuePair<K, V>[]` | `IMapColumn<K, V>` |
| `LowCardinality(T)` | `T`와 동일 | `ILowCardinalityColumn<T>` |
| `Nested(...)` | `object[][]` | `INestedColumn` |

튜플에는 최대 7개의 원소를 담을 수 있습니다. `Nested` 컬럼은 필드 수에 관계없이
`INestedColumn`을 통해 읽는 것이 가장 좋습니다. `INestedColumn`은 이름이 지정된 개별 필드 컬럼과
공유 행 오프셋을 제공하므로 객체로 래핑된 레코드를 할당할 필요가 없습니다. ClickHouse의 기본 설정인
`flatten_nested = 1`에서는 중첩 필드가 점으로 구분된 별도의 `Array(T)` 컬럼으로 전달됩니다.

<h4 id="type-map-reading-variant-dynamic-json">
  Variant, Dynamic 및 JSON 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 | 열 지향 뷰 |
| - | - | - |
| `Variant(T1, ..., Tn)` | `object` | `IVariantColumn` |
| `Dynamic` | `object` | `IDynamicColumn` |
| `JSON` | `string` | `IColumn<string>` |

`Variant`와 `Dynamic`은 각 행에서 선택된 타입에 해당하는 CLR 값이나 `null`을 반환합니다. 두 타입의
열 지향 뷰에서는 판별자 스트림과 각 런타임 타입에 대응하는 타입이 지정된 하위 컬럼에 접근할 수 있어, 컬럼 단위로
데이터를 처리할 때 박싱을 피할 수 있습니다.

TCP 클라이언트는 ClickHouse의 String 직렬화 방식으로 `JSON`을 읽습니다. 클라이언트는 기본적으로
`output_format_native_write_json_as_string = 1`을 활성화하며, 이 설정을 비활성화하면 JSON 읽기가
지원되지 않습니다. ClickHouse가 JSON을 파싱하고 정규화하기 때문에 반환된 텍스트는 삽입된 텍스트와 키 순서,
공백 및 숫자 표기 방식이 다를 수 있습니다.

<h4 id="type-map-reading-geometry">
  Geometry 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `Point` | `(double, double)` |
| `Ring` | `(double, double)[]` |
| `LineString` | `(double, double)[]` |
| `Polygon` | `(double, double)[][]` |
| `MultiLineString` | `(double, double)[][]` |
| `MultiPolygon` | `(double, double)[][][]` |
| `Geometry` | `object` |

`Geometry`는 6가지 Geometry 타입을 담는 `IVariantColumn`로 제공됩니다.

<h4 id="type-map-reading-qbit">
  QBit 타입
</h4>

| ClickHouse 타입 | 기본 .NET 타입 |
| - | - |
| `QBit(Int8, N)` | `sbyte[]` |
| `QBit(BFloat16, N)` | `float[]` |
| `QBit(Float32, N)` | `float[]` |
| `QBit(Float64, N)` | `double[]` |

TCP 클라이언트는 스트라이드가 없는 인수 2개짜리 `QBit(T, N)` 형식을 지원합니다. 스트라이드가 있는 인수 3개짜리
형식은 지원하지 않습니다.

<h4 id="type-map-reading-aggregate-function">
  AggregateFunction 타입
</h4>

| ClickHouse 타입 | .NET 타입 |
| - | - |
| `SimpleAggregateFunction(function, T)` | `T`와 동일 |
| `AggregateFunction(function, ...)` | 지원되지 않음 |

`AggregateFunction` 컬럼에는 함수별 중간 상태가 저장됩니다. `sumMerge(column)` 등을 사용해
서버에서 최종 값을 계산한 후, 중간 상태 대신 해당 값을 읽으세요.

<h3 id="type-map-writing">
  타입 매핑: ClickHouse에 쓰기
</h3>

각 엔트리는 `InsertAsync`에 전달된 `IColumn`의 한 행에 해당하는 CLR 유형입니다. 매핑은
엄격하게 적용되며, TCP 클라이언트는 `Convert`와 같은 방식으로 형식을 변환하지 않습니다.

<h4 id="type-map-writing-integer">
  정수 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Int8` | `sbyte` |
| `UInt8` | `byte` |
| `Int16` | `short` |
| `UInt16` | `ushort` |
| `Int32` | `int` |
| `UInt32` | `uint` |
| `Int64` | `long` |
| `UInt64` | `ulong` |
| `Int128` | `Int128` |
| `UInt128` | `UInt128` |
| `Int256` | `Int256` |
| `UInt256` | `UInt256` |

<h4 id="type-map-writing-floating-point">
  부동 소수점 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Float32` | `float` |
| `Float64` | `double` |
| `BFloat16` | `float` |

`BFloat16`에 값을 쓸 때는 각 `float` 값이 16비트 브레인 부동 소수점 형식으로 변환되면서 하위 비트가 잘립니다.

<h4 id="type-map-writing-decimal">
  Decimal 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Decimal(P, S)`, `P <= 18`인 경우 | `decimal` |
| `Decimal(P, S)`, `P > 18`인 경우 | `ClickHouseTcpDecimal` |
| `Decimal32(S)` | `decimal` |
| `Decimal64(S)` | `decimal` |
| `Decimal128(S)` | `ClickHouseTcpDecimal` |
| `Decimal256(S)` | `ClickHouseTcpDecimal` |

스케일이 적용된 가수가 선언된 정밀도를 초과하면 쓰기 작업에서 `OverflowException`이 발생합니다.

<h4 id="type-map-writing-boolean">
  불리언 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Bool` | `bool` |

<h4 id="type-map-writing-strings">
  문자열 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 | 참고 |
| - | - | - |
| `String` | `string`, `byte[]` | 문자열은 UTF-8로 인코딩되며, 바이트 배열은 변경 없이 기록됩니다. |
| `FixedString(N)` | `byte[]` | 모든 값은 정확히 `N`바이트여야 합니다. |

`FixedString(N)`은 의도적으로 `string`을 허용하지 않습니다. 정확한 이진 표현이 명확해지도록
값을 명시적으로 인코딩하고 필요한 길이만큼 채우세요.

<h4 id="type-map-writing-datetime">
  날짜 및 시간 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Date`, `Date32` | `DateOnly` |
| `DateTime`, `DateTime('timezone')` | `uint`, `DateTimeOffset`, `DateTime` |
| `DateTime64(S)`, `DateTime64(S, 'timezone')` | `long`, `DateTimeOffset`, `DateTime` |
| `Time` | `int`, `TimeSpan`, `TimeOnly` |
| `Time64(S)` | `long`, `TimeSpan`, `TimeOnly` |
| `IntervalNanosecond`부터 `IntervalYear`까지 | `long` |

정수 값을 그대로 사용할 때는 읽기 테이블에 표시된 단위가 적용됩니다. `DateTimeOffset`은 절대 시점을 유지합니다.
`DateTime`은 `Kind` 값에 따라 처리됩니다. `Unspecified` 값은 컬럼에 지정된 시간대의 현지 시각으로 해석되며,
컬럼에 시간대가 지정되지 않았다면 세션 시간대의 현지 시각으로 해석됩니다.

<h4 id="type-map-writing-enum">
  Enum 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Enum8(...)` | `sbyte`, `string` 레이블 |
| `Enum16(...)` | `short`, `string` 레이블 |

정의되지 않은 레이블은 데이터 전송 전에 거부됩니다.

<h4 id="type-map-writing-other-scalar">
  기타 스칼라 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `UUID` | `Guid` |
| `IPv4` | `IPAddress` |
| `IPv6` | `IPAddress` |
| `Nothing` | 지원되지 않음 |

<h4 id="type-map-writing-composite">
  복합 타입
</h4>

허용되는 데이터 구조는 재귀적이므로 `Array(Nullable(DateTime))`은 각 행에서 `DateTime?[]`을
허용하고, `Map(String, UInt32)`는 `KeyValuePair<string, uint>[]`을 허용합니다.

| ClickHouse 타입 | 한 행에 허용되는 .NET 타입 |
| - | - |
| `Nullable(T)` | `T`에 허용되는 타입의 널 허용 형태 |
| `Array(T)` | `T[]` |
| `Tuple(T1, ..., Tn)` | `(T1, ..., Tn)` |
| `Map(K, V)` | `KeyValuePair<K, V>[]` |
| `LowCardinality(T)` | `T`와 동일 |
| `Nested(...)` | 행 지향 CLR 데이터 구조 없음 |

`Nested`에 직접 데이터를 쓰려면 이전 읽기 작업에서 반환된 밀집 컬럼 구조를 사용합니다. 애플리케이션에서 생성하는
삽입 데이터에는 기본 `flatten_nested = 1` 표현을 유지하고, 점으로 구분된 각 `Array(T)` 필드를
별도의 컬럼으로 기록하십시오.

<h4 id="type-map-writing-variant-dynamic-json">
  Variant, Dynamic 및 JSON 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 | 참고 |
| - | - | - |
| `Variant(T1, ..., Tn)` | `object` | NULL이 아닌 각 값의 런타임 타입에 따라 대체 타입이 선택됩니다. |
| `Dynamic` | `object` | NULL이 아닌 각 값의 런타임 타입을 추론합니다. |
| `JSON` | `string` | 값은 유효한 JSON 텍스트여야 합니다. |

`Variant`에서는 런타임 타입이 대체 타입 중 정확히 하나의 기본 CLR 타입과 일치해야 합니다. `DateTime`을
`DateTime` 대체 타입에 쓰는 것과 같은 편의 매핑은 `Variant` 내부에 적용되지 않습니다.
대신 해당 대체 타입의 기본 원시 타입을 사용하세요. `Dynamic`은 스칼라 값과 재귀적으로 지원되는
배열, 맵, 튜플 값을 지원합니다.

<h4 id="type-map-writing-geometry">
  Geometry 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `Point` | `(double, double)` |
| `Ring` | `(double, double)[]` |
| `LineString` | `(double, double)[]` |
| `Polygon` | `(double, double)[][]` |
| `MultiLineString` | `(double, double)[][]` |
| `MultiPolygon` | `(double, double)[][][]` |
| `Geometry` | `object` |

애플리케이션에서 생성한 `Geometry` 컬럼에서는 `Point`와 `MultiPolygon`만 서로 구별되는 CLR 형태를 가집니다.
`Ring`과 `LineString`은 CLR 형태가 같고 `Polygon`과 `MultiLineString`도 마찬가지이므로, 클라이언트는
`object` 값이 어느 타입을 나타내는지 추론할 수 없습니다. 디코딩된 `Geometry` 컬럼에는
판별자가 그대로 남아 있어 이러한 모호성 없이 다시 삽입할 수 있습니다.

<h4 id="type-map-writing-qbit">
  QBit 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `QBit(Int8, N)` | 길이가 `N`인 `sbyte[]` |
| `QBit(BFloat16, N)` | 길이가 `N`인 `float[]` |
| `QBit(Float32, N)` | 길이가 `N`인 `float[]` |
| `QBit(Float64, N)` | 길이가 `N`인 `double[]` |

<h4 id="type-map-writing-aggregate-function">
  AggregateFunction 타입
</h4>

| ClickHouse 타입 | 허용되는 .NET 타입 |
| - | - |
| `SimpleAggregateFunction(function, T)` | `T`와 동일 |
| `AggregateFunction(function, ...)` | 지원되지 않음 |

<h2 id="compression">
  압축
</h2>

클라이언트는 기본적으로 압축을 사용하며, 기록하는 블록에는 LZ4를 적용합니다. ClickHouse도 응답
블록에 기본적으로 LZ4를 사용합니다. 압축은 결과 블록과 삽입 시 업로드되는 블록에 적용되지만,
쿼리 텍스트, 진행 상태, 예외 및 기타 프로토콜 패킷은 압축되지 않고 일반적인 wire 인코딩을 유지합니다.

클라이언트가 사용할 코덱은 연결 문자열의 `Compression` 키로 선택하세요:

```csharp theme={null}
await using var lz4 = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=lz4"); // Default

await using var zstd = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=zstd");

await using var uncompressed = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=none");
```

동등한 기능을 하는 강타입 구성에서는 `IClickHouseCompressor`를 사용합니다:

```csharp theme={null}
using ClickHouse.Driver.Compression;
using ClickHouse.Driver.Tcp;

var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    Compressor = ZstdCompressor.Default,
};

await using var client = new ClickHouseTcpClient(options);
```

압축을 비활성화하려면 `Compressor = null`로 설정하세요. 사용자 정의 압축기는 네이티브 블록
프레이밍을 지원해야 합니다. HTTP 본문만 지원하는 압축기를 사용하면 클라이언트를 생성할 때 오류가 발생합니다.

<h3 id="compression-response-codec">
  응답 코덱 선택
</h3>

`Compressor`는 클라이언트가 기록하는 블록의 압축 방식을 결정합니다. 네이티브 쿼리 패킷은
서버에 압축을 요청하지만 코덱은 지정하지 않습니다. 따라서 ClickHouse는
`network_compression_method` 설정에 따라 응답 코덱을 선택합니다. 서버의 기본 코덱은 LZ4입니다.

필요에 따라 작업별로 응답 코덱을 설정하세요:

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["network_compression_method"] = "ZSTD",
    },
};

await foreach (Block block in client.StreamAsync(
    "SELECT * FROM events",
    queryOptions))
{
    // Process the block.
}
```

LZ4는 일반적으로 CPU 사용량이 적어 고속 네트워크에서 기본 옵션으로 가장 적합합니다. ZSTD는 일반적으로 CPU를 약간 더 사용하지만
압축 결과의 크기가 더 작습니다. 서버가 같은 호스트에 있거나 연결 속도가 매우 빠르다면 압축을 비활성화하는 것이 유리할 수 있습니다.
기본값을 변경하기 전에 실제 사용 환경을 반영한 데이터로 성능을 측정하세요.

<h2 id="error-handling">
  오류 처리
</h2>

ClickHouse 또는 네이티브 연결에서 발생한 오류는 `ClickHouseTcpException`을 상속하며,
`ClickHouseTcpException`은 다시 `DbException`을 상속합니다.

| 예외 | 의미 |
| - | - |
| `ClickHouseTcpServerException` | ClickHouse가 핸드셰이크나 작업을 거부했거나 작업을 실행하는 도중 오류가 발생했습니다. |
| `ClickHouseTcpConnectionException` | 소켓, TLS 협상 또는 이미 설정된 연결에 문제가 발생했거나 `DialTimeout` 또는 `ReadTimeout`의 제한 시간이 지났습니다. 원인은 `InnerException`에서 확인할 수 있으며, 타임아웃이라면 `TimeoutException`입니다. |
| `ClickHouseTcpProtocolException` | 응답이 native protocol에 맞지 않거나 클라이언트가 처리할 수 없는 유형 또는 wire 표현을 포함하고 있습니다. |

`ClickHouseTcpServerException`에서는 서버가 보낸 정확한 숫자 코드인 `RawCode`와
드라이버에 해당 코드의 이름이 정의되어 있다면 그에 대응하는 `ClickHouseErrorCode`인 `Code`를 확인할 수 있습니다. 또한
서버 예외의 `Name`과 `ServerStackTrace`, 중첩된 서버 예외도
`InnerException`을 통해 확인할 수 있습니다.

```csharp theme={null}
try
{
    await client.ExecuteAsync("SELECT * FROM missing_table");
}
catch (ClickHouseTcpServerException ex)
    when (ex.Code == ClickHouseErrorCode.UnknownTable)
{
    Console.WriteLine($"ClickHouse error {ex.RawCode}: {ex.Message}");
}
catch (ClickHouseTcpConnectionException ex)
{
    Console.WriteLine($"Connection failed: {ex.InnerException?.Message}");
}
catch (ClickHouseTcpException ex)
{
    Console.WriteLine($"Native client failure: {ex.Message}");
}
```

`ClickHouseErrorCode`에 정의되지 않은 코드는 `Code == ClickHouseErrorCode.Unknown`으로 표시됩니다. 열거형에 모든 ClickHouse 오류가 정의되어 있다고 가정하지 말고
`RawCode`를 확인하세요.

인수 유효성 검사 및 객체 수명 주기 관련 오류에는 일반적인 .NET 예외 타입이 그대로 사용됩니다.
취소하면 `OperationCanceledException`이 발생합니다. `DialTimeout`과 `ReadTimeout`이 발생하면 내부 예외로 `TimeoutException`을 포함하는
`ClickHouseTcpConnectionException`이 발생하고, 해당 연결은 폐기됩니다.
`PoolTimeout`이 발생하면 `ClickHouseTcpException`이 아닌 일반 `TimeoutException`이 발생합니다. 연결 자체에
문제가 생긴 것은 아니므로 동시 작업 수를 줄이거나 풀 크기를 늘리거나 결과 열거를 완료해야 합니다.

<h3 id="error-handling-retries">
  작업 재시도
</h3>

삽입 중 연결이 끊기거나 타임아웃이 발생하면 삽입 결과를 알 수 없습니다. 클라이언트가 응답을 받지 못하기 전에
서버가 이미 행을 커밋했을 수 있습니다. 재시도하기 전에 해당 논리적 배치에 사용할
`DeduplicationToken`을 재시도 시에도 동일하게 유지되도록 설정하세요:

```csharp theme={null}
var insertOptions = new ClickHouseTcpInsertOptions
{
    DeduplicationToken = batchContentHash,
};

await client.InsertAsync(
    "INSERT INTO events (id, payload) VALUES",
    columns,
    insertOptions);
```

해당 배치를 재시도할 때는 같은 토큰을 사용하고, 새 데이터에는 다른 토큰을 사용하십시오. 다른
작업은 SQL 문을 다시 실행해도 안전하고 해당 오류가 일시적일 때만 재시도하십시오.
대부분의 서버 오류는 재시도하기보다 쿼리나 구성을 변경해야 해결할 수 있습니다.

<h2 id="logging-and-diagnostics">
  로깅 및 진단
</h2>

TCP 클라이언트는 `Microsoft.Extensions.Logging`과 연동됩니다. 로깅은 선택 사항이며,
`LoggerFactory`가 없으면 클라이언트는 로거를 생성하지 않고 로그 메시지의 포맷도 지정하지 않습니다.

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Debug);
});

var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    LoggerFactory = loggerFactory,
    StatementMaxLength = 100,
};

await using var client = new ClickHouseTcpClient(options);
```

클라이언트를 의존성 주입 확장 기능을 통해 등록하면, 전달된 옵션에 로거 팩토리가 이미 지정되어 있지 않은 한 컨테이너의
`ILoggerFactory`를 사용합니다.

<h3 id="logging-categories">
  로깅 범주
</h3>

| 범주 | 이벤트 및 로그 수준 |
| - | - |
| `ClickHouse.Driver.Tcp.Client` | 작업 시작·완료·취소·중단과 행/바이트 수는 `Debug` 수준으로, 실패는 `Error` 수준으로 기록됩니다. |
| `ClickHouse.Driver.Tcp.Connection` | 연결 시도, TLS, 핸드셰이크, 서버 버전, 프로토콜 revision 및 시간대는 `Debug` 수준으로, 연결 실패는 `Warning` 수준으로 기록됩니다. |
| `ClickHouse.Driver.Tcp.Pool` | 연결 재사용은 `Trace` 수준으로, 연결 생성·종료·폐기 및 대기 중인 데이터 처리는 `Debug` 수준으로, 풀 고갈 및 백그라운드 작업 실패는 `Warning` 수준으로 기록됩니다. |

표준 .NET 로깅 구성에서 범주별 로그 수준을 설정하세요.

```json theme={null}
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "ClickHouse.Driver.Tcp.Client": "Debug",
      "ClickHouse.Driver.Tcp.Connection": "Debug",
      "ClickHouse.Driver.Tcp.Pool": "Debug"
    }
  }
}
```

작업 시작 메시지는 `Debug` 수준으로 기록되며, SQL 문을 최대 `StatementMaxLength`자까지 포함합니다.
기본값은 5자입니다. 로그에서 SQL 문을 제외하려면 이 값을 0 이하로 설정하십시오.
값을 늘릴 때는 먼저 매개변수와 리터럴에 민감한 데이터가 포함될 가능성을 고려하십시오.

<h3 id="logging-server-messages">
  서버 로그 메시지
</h3>

로깅 통합은 클라이언트 측 동작을 보고합니다. ClickHouse 자체의 쿼리 로그 패킷은
이와 별개로 `ClickHouseTcpQueryCallbacks.OnLog`에 전달됩니다. 이 패킷을 활성화하려면
`send_logs_level` 서버 설정을 사용하세요:

```csharp theme={null}
var serverLogger = loggerFactory.CreateLogger("ClickHouse.Server");

var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["send_logs_level"] = "warning",
    },
    Callbacks = new ClickHouseTcpQueryCallbacks
    {
        OnLog = block =>
        {
            IColumn<string> messages = block.Column<string>("text");
            for (int row = 0; row < block.RowCount; row++)
            {
                serverLogger.LogWarning("{ClickHouseMessage}", messages[row]);
            }
        },
    },
};
```

로그 블록은 일시적으로 빌려 쓰는 것으로, 콜백이 반환되면 해제됩니다. 동기적으로 처리하거나
보관해야 할 값은 별도로 복사해 두세요. 콜백은 신속하게 실행하고 예외가 발생하지 않도록 하세요. 콜백에서 예외가 발생하면
해당 작업과 연결이 종료됩니다.

<h2 id="opentelemetry">
  OpenTelemetry
</h2>

TCP 클라이언트는 .NET의 `System.Diagnostics.Activity` API를 통해 분산 추적 스팬을 내보냅니다.
OpenTelemetry 트레이서 프로바이더에서 TCP 활동 소스를 구독하세요:

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseTcpDiagnostics.ActivitySourceName)
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());
```

콘솔 애플리케이션을 사용하거나 수동으로 설정하는 경우:

```csharp theme={null}
using OpenTelemetry;
using OpenTelemetry.Trace;

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseTcpDiagnostics.ActivitySourceName)
    .AddConsoleExporter()
    .Build();
```

활동 소스 이름은 `ClickHouse.Driver.Tcp`로, HTTP 클라이언트의 소스와 별개이므로
각 전송 방식의 활동을 독립적으로 수집할 수 있습니다. 구독하는 리스너가 없으면 작업 시
활동이 생성되지 않습니다.

<h3 id="opentelemetry-spans-attributes">
  스팬과 속성
</h3>

클라이언트는 SQL 작업, 핑, 새 연결에 대한 스팬을 생성합니다. SQL 스팬의 이름에는
`SELECT`나 `INSERT`처럼 SQL 문의 첫 키워드가 사용됩니다. 첫 키워드를
인식할 수 없는 SQL 문에는 `query`가 사용됩니다. `connect` 스팬은 소켓 연결, TLS 협상,
네이티브 핸드셰이크를 포함합니다. SQL 문 스팬에는 연결 풀에서 연결을 기다린 시간도 포함됩니다.

| 속성 | 설명 |
| - | - |
| `db.system.name` | `clickhouse` |
| `db.namespace` | 설정된 데이터베이스 |
| `db.operation.name` | 식별 가능한 경우 SQL 문의 첫 키워드 |
| `db.user` | 설정된 ClickHouse 사용자명 |
| `server.address` | 서버 호스트명 또는 주소 |
| `server.port` | 확인된 네이티브 프로토콜 포트 |
| `db.clickhouse.query_id` | 생성되었거나 호출자가 지정한 쿼리 ID |
| `db.query.text` | 선택적으로 포함되는, 길이가 제한된 SQL 문 |
| `db.clickhouse.read_rows` | 서버의 진행 상황 패킷이 보고한 행 수 |
| `db.clickhouse.read_bytes` | 서버의 진행 상황 패킷이 보고한 바이트 수 |
| `db.clickhouse.written_rows` | 서버가 보고한 행 수 또는 클라이언트 측 삽입 작업에서 전송한 행 수 |
| `db.clickhouse.written_bytes` | 서버가 보고한 경우 기록된 바이트 수 |
| `db.clickhouse.elapsed_ns` | 진행 상황 패킷에 포함된 서버 실행 시간 |
| `db.clickhouse.result_rows` | 서버의 프로필 요약에 포함된 결과 행 수 |
| `db.clickhouse.result_bytes` | 서버의 프로필 요약에 포함된 결과 바이트 수 |
| `error.type` | 실패한 작업의 예외 유형 |
| `db.response.status_code` | 서버 예외에 해당하는 ClickHouse 오류 코드(숫자) |

서버나 클라이언트에 보고할 만한 값이 있을 때만 카운터가 포함됩니다. 예를 들어,
데이터 블록을 업로드하는 삽입 작업에서는 클라이언트가 전송한 행 수는 보고되지만,
서버의 읽기 카운터는 보고되지 않습니다. 실패한 스팬에는 예외 유형, 메시지, 스택
추적이 담긴 `exception` 이벤트도 포함됩니다.

<h3 id="opentelemetry-sql-text">
  SQL 텍스트와 민감한 데이터
</h3>

기본적으로 스팬에는 SQL 텍스트가 포함되지 않습니다. SQL 텍스트를 명시적으로 포함하도록 설정하고 적절한 길이 제한을 지정하세요.

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    IncludeSqlInActivityTags = true,
    StatementMaxLength = 1_000,
};
```

`StatementMaxLength`는 `db.query.text` 속성과 디버그 로그에 기록되는 SQL의 길이를 모두 제한합니다. 값이 0
이하면 두 곳 모두에서 해당 내용을 생략합니다. 서버 오류에 쿼리 세부 정보가 포함될 수 있으므로
내보낸 예외 메시지와 스택 트레이스도 검토하세요.

<h3 id="opentelemetry-trace-context">
  추적 컨텍스트 전파
</h3>

`Activity.Current`가 W3C ID 포맷을 사용하면 클라이언트는 트레이스 ID, 스팬 ID, 트레이스 플래그,
트레이스 상태를 네이티브 쿼리 패킷에 기록합니다. 그러면 ClickHouse는 동일한
분산 트레이스에 서버 스팬을 생성할 수 있습니다. 서버 측 스팬 수집과 샘플링은 ClickHouse 구성에 따라 결정됩니다.
예를 들어 샘플링 확률을 쿼리별로 설정할 수 있습니다:

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["opentelemetry_start_trace_probability"] = "1",
    },
};

await client.ExecuteAsync("SELECT count() FROM events", queryOptions);
```

전파된 쿼리 ID를 트레이스 ID와 함께 사용하면 클라이언트 스팬을 `system.query_log`와 연관시킬 수 있으며, ClickHouse의 `system.opentelemetry_span_log`가 구성되어 있다면 이 로그와도 연관시킬 수 있습니다.

<h2 id="limitations">
  제한 사항
</h2>

TCP 클라이언트는 아직 실험 단계이므로 HTTP 클라이언트의 모든 기능을 지원하지는 않습니다.

<h3 id="limitations-http-only">
  HTTP에서만 지원되는 기능
</h3>

TCP API는 ADO.NET 공급자가 아니므로 ORM에서 사용할 수 없습니다. 또한
CSV, JSONEachRow, Parquet 또는 원시 스트림의 입력과 출력, bearer 인증 또는 사용자 정의 HTTP
헤더, 사용자 정의 매개변수 유형 결정·서식 지정·읽기 변환, 쿼리별 데이터베이스
또는 역할을 지원하지 않습니다. 애플리케이션에 이러한 기능이 필요하다면
[HTTP 클라이언트](./http)를 사용하세요.

<h3 id="limitations-sql-parameters">
  SQL 매개변수
</h3>

ClickHouse 네이티브 `{name:Type}` 플레이스홀더만 지원합니다. TCP 클라이언트는 ADO.NET 방식의 `@name` 플레이스홀더를
재작성하지 않습니다. ClickHouse 25.8\~26.6 버전에서는 `limit`나 `offset`처럼 서버 설정과 이름이 같은 매개변수가
설정으로 해석되어 거부될 수 있습니다. 해당 버전을 지원해야 한다면 매개변수 이름을 `row_limit` 등으로 변경하세요.

<h3 id="limitations-data-types">
  데이터 타입 지원 범위
</h3>

[지원되는 데이터 타입](#supported-data-types) 섹션에서 전체 매핑을 설명합니다.
현재 네이티브 코덱에는 다음과 같은 주요 제약이 있습니다.

* 튜플에는 최대 7개의 원소를 담을 수 있습니다.
* `AggregateFunction(function, ...)`의 중간 상태는 지원되지 않습니다. 서버에서
  `sumMerge(column)` 등을 사용해 최종 값을 계산하고 그 결과를 읽으세요.
* 인수가 2개이고 스트라이드를 사용하지 않는 `QBit(T, N)` 레이아웃만 지원됩니다.
* `JSON`은 ClickHouse의 String 직렬화를 사용하므로
  `output_format_native_write_json_as_string = 1`이 필요합니다. 클라이언트는 기본적으로 이 설정을 요청합니다.
* 애플리케이션에서 구성한 삽입 데이터는 `flatten_nested = 0`일 때
  `Nested(...)`에 대응하는 행 지향 형태를 지원하지 않습니다. 대신 기본값인 평탄화된 점 표기법의 `Array(T)` 컬럼을 사용하세요.
* 애플리케이션에서 구성한 `Geometry` 컬럼은 `Ring`과 `LineString`처럼 CLR
  형태가 같은 기하 도형 타입을 구별해 추론할 수 없습니다. 구체적인 기하 도형 타입으로 삽입하거나 판별자가 유지된
  디코딩된 `Geometry` 컬럼을 다시 삽입하세요.

<h3 id="limitations-retries">
  자동 재시도
</h3>

클라이언트는 실패한 작업을 자동으로 재시도하지 않습니다. 다시 실행해도 안전한 작업만 재시도하고,
삽입 성공 여부를 알 수 없다면 재시도하기 전에 재시도 간 동일한 `DeduplicationToken`을 설정하세요.
자세한 내용은 [작업 재시도](#error-handling-retries)를 참조하세요.
