Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

ClickHouse C# client

ClickHouse에 연결하기 위한 공식 C# 클라이언트입니다. 클라이언트 소스 코드는 GitHub 리포지토리에서 확인할 수 있습니다. 원저자는 Oleg V. Kozlyuk입니다.

이 라이브러리는 두 가지 주요 API를 제공합니다.

  • ClickHouseClient (권장): 싱글턴으로 사용하도록 설계된 고수준의 스레드 안전한 클라이언트입니다. 쿼리와 대량 삽입을 위한 간단한 비동기 API를 제공합니다. 대부분의 애플리케이션에 가장 적합합니다.

  • ADO.NET (ClickHouseDataSource, ClickHouseConnection, ClickHouseCommand): 표준 .NET 데이터베이스 추상화입니다. ORM 통합(Dapper, Linq2db)과 ADO.NET 호환성이 필요할 때 필수입니다. ClickHouseBulkCopy는 ADO.NET 연결을 사용해 데이터를 효율적으로 삽입할 수 있도록 돕는 헬퍼 클래스입니다. ClickHouseBulkCopy는 더 이상 권장되지 않으며 향후 릴리스에서 제거될 예정이므로, 대신 ClickHouseClient.InsertBinaryAsync를 사용하십시오.

두 API는 동일한 기본 HTTP 연결 풀을 공유하며, 같은 애플리케이션에서 함께 사용할 수 있습니다.

마이그레이션 가이드

  1. .csproj 파일에서 패키지 이름을 ClickHouse.Driver로 변경하고, NuGet의 최신 버전으로 업데이트합니다.
  2. 코드베이스의 모든 ClickHouse.Client 참조를 ClickHouse.Driver로 변경합니다.

지원되는 .NET 버전

ClickHouse.Driver는 다음과 같은 .NET 버전을 지원합니다.

  • .NET 6.0
  • .NET 8.0
  • .NET 9.0
  • .NET 10.0

지원되는 ClickHouse 버전

이 클라이언트는 공식적으로 최신 릴리스 3개와 최신 LTS 릴리스 2개를 지원합니다.

설치

NuGet에서 패키지를 설치합니다:

dotnet add package ClickHouse.Driver

또는 NuGet 패키지 관리자를 사용하세요:

Install-Package ClickHouse.Driver

빠른 시작

using ClickHouse.Driver;

// 클라이언트 생성 (일반적으로 싱글턴으로 사용)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// 쿼리 실행
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);

구성

ClickHouse 연결을 구성하는 방법은 두 가지입니다.

  • 연결 문자열: 호스트, 인증 자격 증명, 기타 연결 옵션을 지정하는 세미콜론으로 구분된 키/값 쌍입니다.
  • ClickHouseClientSettings object: 설정 파일에서 로드하거나 코드에서 설정할 수 있는 강타입 구성 객체입니다.

아래에는 모든 설정의 전체 목록과 각 설정의 기본값 및 영향이 나와 있습니다.

연결 설정

속성 유형 기본값 연결 문자열 키 설명
Host string "localhost" Host ClickHouse 서버의 호스트명 또는 IP 주소
Port ushort 8123 (HTTP) / 8443 (HTTPS) Port 포트 번호입니다. 기본값은 프로토콜에 따라 달라집니다
Username string "default" Username 인증에 사용할 사용자 이름
Password string "" Password 인증에 사용할 비밀번호
Database string "" Database 기본 데이터베이스입니다. 비어 있으면 서버 또는 사용자의 기본값을 사용합니다
Protocol string "http" Protocol 연결 프로토콜: "http" 또는 "https"
Path string null Path 리버스 프록시 환경에서 사용할 URL 경로(예: /clickhouse)
Timeout TimeSpan 2분 Timeout 작업 제한 시간입니다. 연결 문자열에는 초 단위로 저장됩니다

데이터 포맷 및 직렬화

속성 유형 기본값 연결 문자열 키 설명
UseCompression bool true Compression 데이터 전송에 gzip 압축을 사용합니다
UseCustomDecimals bool true UseCustomDecimals 임의 정밀도에는 ClickHouseDecimal을 사용하고, false이면 .NET decimal을 사용합니다(128비트 제한)
ReadStringsAsByteArrays bool false ReadStringsAsByteArrays StringFixedString 컬럼을 string 대신 byte[]로 읽습니다. 바이너리 데이터에 유용합니다
UseFormDataParameters bool false UseFormDataParameters 매개변수를 URL 쿼리 문자열 대신 폼 데이터로 전송합니다
ReadBufferSize int 8192 (8 KiB) ReadBufferSize HTTP 쿼리 응답을 읽는 데 사용하는 버퍼 크기(바이트)입니다. 큰 결과 집합에서 버퍼를 다시 채우는 횟수를 줄이려면 값을 늘리십시오. 85,000바이트 이상 값은 Large Object Heap에 할당되며, 이로 인해 LOH 단편화와 더 긴 GC 일시 중지가 발생할 수 있습니다.
ParameterTypeResolver IParameterTypeResolver null @ 스타일 매개변수 유형 매핑을 위한 사용자 지정 리졸버입니다. 사용자 지정 매개변수 유형 매핑을 참조하십시오
ParameterFormatter IParameterFormatter null 매개변수 값 직렬화를 위한 사용자 지정 포매터입니다. 사용자 지정 매개변수 값 포맷팅을 참조하십시오
ReadValueConverter IReadValueConverter null 데이터 리더가 반환하는 값에 적용되는 사용자 지정 변환입니다. 사용자 지정 읽기 값 변환을 참조하십시오
JsonReadMode JsonReadMode Binary JsonReadMode JSON 데이터를 반환하는 방식입니다: Binary(JsonObject 반환) 또는 String(원시 JSON 문자열 반환)
JsonWriteMode JsonWriteMode String JsonWriteMode JSON 데이터를 전송하는 방식입니다: String(JsonSerializer를 통해 직렬화하며 모든 입력 허용) 또는 Binary(타입 힌트가 있는 등록된 POCO만)

세션 관리

속성 유형 기본값 연결 문자열 키 설명
UseSession bool false UseSession 상태 유지 세션을 활성화합니다. 요청은 직렬로 처리됩니다
SessionId string null SessionId 세션 ID입니다. null이고 UseSession이 true이면 GUID가 자동 생성됩니다

보안

속성 유형 기본값 연결 문자열 키 설명
SkipServerCertificateValidation bool false HTTPS 인증서 검증을 생략합니다. 운영 환경에서는 사용하지 마십시오

HTTP 클라이언트 구성

속성 유형 기본값 연결 문자열 키 설명
HttpClient HttpClient null 미리 구성된 사용자 지정 HttpClient 인스턴스
HttpClientFactory IHttpClientFactory null HttpClient 인스턴스를 생성하기 위한 사용자 지정 팩터리
HttpClientName string null HttpClientFactory가 특정 클라이언트를 생성하는 데 사용할 이름

로깅 및 디버깅

속성 유형 기본값 연결 문자열 키 설명
LoggerFactory ILoggerFactory null 진단용 로깅을 위한 로거 팩토리
EnableDebugMode bool false .NET 네트워크 추적을 활성화합니다(수준이 Trace로 설정된 LoggerFactory 필요); 성능에 상당한 영향이 있습니다

사용자 지정 설정 & 역할

속성 유형 기본값 연결 문자열 키 설명
CustomSettings IDictionary<string, object> 비어 있음 set_* 접두사 ClickHouse 서버 설정이며, 아래 참고 사항을 참조하십시오
Roles IReadOnlyList<string> 비어 있음 Roles 쉼표로 구분된 ClickHouse 역할(예: Roles=admin,reader)
ApplicationInfo IReadOnlyDictionary<string, string> 비어 있음 애플리케이션별 쿼리 식별을 위해 HTTP User-Agent 헤더에 추가되는 자유 형식 태그입니다.

연결 문자열 예시

기본 연결

Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb

사용자 지정 ClickHouse 설정 사용 시

Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000

QueryOptions

QueryOptions를 사용하면 쿼리마다 클라이언트 수준 설정을 재정의할 수 있습니다. 모든 속성은 선택 사항이며, 지정한 경우에만 클라이언트 기본값을 덮어씁니다.

Property Type Description
QueryId string system.query_log에서 추적하거나 취소할 때 사용할 사용자 지정 쿼리 식별자
Database string 이 쿼리에 사용할 기본 데이터베이스를 재정의합니다
Roles IReadOnlyList<string> 이 쿼리에 적용할 클라이언트 역할을 재정의합니다
CustomSettings IDictionary<string, object> 이 쿼리에 대한 ClickHouse 서버 설정입니다(예: max_threads)
CustomHeaders IDictionary<string, string> 이 쿼리에 대한 추가 HTTP 헤더
UseSession bool? 이 쿼리의 세션 동작을 재정의합니다
SessionId string 이 쿼리의 세션 ID입니다(UseSession = true 필요)
BearerToken string 이 쿼리에 사용할 인증 토큰을 재정의합니다
ParameterTypeResolver IParameterTypeResolver @ 스타일 매개변수 유형 매핑에 사용할 클라이언트 수준 리졸버를 재정의합니다. 사용자 지정 매개변수 유형 매핑을 참조하십시오
ParameterFormatter IParameterFormatter @ 스타일 매개변수 값 직렬화에 사용할 클라이언트 수준 포매터를 재정의합니다. 사용자 지정 매개변수 값 포맷팅을 참조하십시오
ReadValueConverter IReadValueConverter 데이터 리더가 반환하는 값에 적용되는 클라이언트 수준 변환을 재정의합니다. 사용자 지정 읽기 값 변환을 참조하십시오
MaxExecutionTime TimeSpan? 서버 측 쿼리 시간 초과입니다(max_execution_time 설정으로 전달됨). 이를 초과하면 서버가 쿼리를 취소합니다
AcceptEncoding string 쿼리별 Accept-Encoding 헤더를 재정의합니다(예: "zstd", "zstd, gzip;q=0.5"). 또한 URL에 enable_http_compression=1을 강제로 설정합니다. Per-query transport compression을 참조하십시오.

예시:

var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

InsertOptions

InsertOptionsInsertBinaryAsync를 통한 대량 삽입 작업에 필요한 설정을 QueryOptions에 추가한 옵션입니다.

속성 유형 기본값 설명
BatchSize int 100,000 배치당 행 수
MaxDegreeOfParallelism int 1 동시에 업로드할 배치 수
Format RowBinaryFormat RowBinary 바이너리 형식: RowBinary 또는 RowBinaryWithDefaults
ColumnTypes IReadOnlyDictionary<string, string> null 컬럼 이름 → ClickHouse 타입 문자열. 설정하면 스키마 확인 쿼리를 건너뜁니다.
UseSchemaCache bool false 클라이언트 수명 동안 (database, table)별 전체 table 스키마를 캐시합니다.

모든 QueryOptions 속성은 InsertOptions에서도 사용할 수 있습니다.

예시:

var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);

스키마 확인 쿼리 건너뛰기

기본적으로 InsertBinaryAsync는 각 삽입 전에 컬럼 타입을 확인하기 위해 SELECT ... WHERE 1=0 쿼리를 전송합니다. 높은 처리량이 필요한 환경에서는 두 가지 방법으로 이 오버헤드를 제거할 수 있습니다:

옵션 1: 컬럼 타입을 명시적으로 제공

컴파일 시점에 테이블 스키마를 알고 있다면 ColumnTypes를 통해 직접 전달하십시오. 그러면 스키마 확인 쿼리는 전혀 전송되지 않습니다:

var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);

옵션 2: 스키마 캐시 사용

같은 테이블에 반복해서 삽입하는 경우, UseSchemaCache = true로 설정하면 스키마를 한 번만 쿼리한 뒤 동일한 ClickHouseClient 인스턴스의 후속 삽입에서 이를 재사용합니다:

var options = new InsertOptions { UseSchemaCache = true };

// 첫 번째 호출 시 서버에서 스키마를 가져옵니다
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// 두 번째 호출은 캐시된 스키마를 재사용합니다 — 추가 왕복 없음
await client.InsertBinaryAsync("my_table", columns, batch2, options);

ClickHouseClient

ClickHouseClient는 ClickHouse와 상호 작용할 때 권장되는 API입니다. 스레드 안전을 보장하며, singleton으로 사용하도록 설계되었고, 내부적으로 HTTP 연결 풀링을 관리합니다.

클라이언트 생성

연결 문자열 또는 ClickHouseClientSettings 객체를 사용해 ClickHouseClient를 생성합니다. 사용 가능한 옵션은 구성 섹션을 참조하십시오.

ClickHouse Cloud 서비스의 세부 정보는 ClickHouse Cloud 콘솔에서 확인할 수 있습니다.

서비스를 선택하고 Connect를 클릭합니다:

ClickHouse Cloud 서비스 연결 버튼

**C#**을 선택합니다. 아래에 연결 세부 정보가 표시됩니다.

ClickHouse Cloud C# 연결 세부 정보

자가 관리형 ClickHouse를 사용하는 경우 연결 세부 정보는 ClickHouse 관리자가 설정합니다.

연결 문자열 사용:

using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");

또는 ClickHouseClientSettings를 사용할 수 있습니다:

using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);

의존성 주입 시나리오에서는 IHttpClientFactory를 사용하세요:

// In your DI configuration
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
}).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");

쿼리 실행

결과를 반환하지 않는 SQL 문에는 ExecuteNonQueryAsync를 사용하십시오:

// 테이블 생성
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// 테이블 삭제
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");

단일 값을 조회하려면 ExecuteScalarAsync를 사용합니다:

var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"행 수: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"서버 버전: {version}");

데이터 삽입

매개변수화된 삽입

ExecuteNonQueryAsync를 사용해 매개변수화된 쿼리로 데이터를 삽입합니다. 매개변수 타입은 SQL에서 {name:Type} 구문으로 지정해야 합니다:

using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);

대량 삽입

대량의 행을 효율적으로 삽입하려면 InsertBinaryAsync를 사용합니다. 이 메서드는 ClickHouse의 네이티브 행 바이너리 형식으로 데이터를 스트리밍하고, 병렬 Batch 업로드를 지원하며, 매개변수화 쿼리에서 발생할 수 있는 "URL too long" 오류를 방지합니다.

// IEnumerable<object[]>로 데이터 준비
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// 기본 삽입
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");

대규모 데이터셋에서는 InsertOptions를 사용해 배칭과 병렬성을 설정합니다:

var options = new InsertOptions
{
    BatchSize = 100_000,           // 배치당 행 수 (기본값: 100,000)
    MaxDegreeOfParallelism = 4     // 병렬 배치 업로드 수 (기본값: 1)
};

POCO 삽입

object[] 배열을 구성하는 대신, 강력한 형식이 지정된 POCO 객체를 직접 삽입할 수 있습니다. 타입을 한 번 등록한 다음 IEnumerable<T>를 전달하면 됩니다:

// 테이블 컬럼과 일치하는 POCO 정의
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// 타입 등록 (클라이언트 수명 주기당 한 번만)
client.RegisterBinaryInsertType<SensorReading>();

// 직접 삽입 — 컬럼 이름은 속성 이름에서 자동으로 파생됨
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);

기본적으로 공개적으로 읽을 수 있는 모든 속성은 이름을 엄격하게 대소문자 구분하여 일치시키는 방식으로 컬럼에 매핑됩니다. 속성을 사용해 이 매핑을 사용자 지정할 수 있습니다:

public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // 다른 이름의 컬럼에 매핑
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // 명시적 ClickHouse 유형 지정
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // 삽입 대상에서 제외
    public string InternalTag { get; set; }
}
Attribute 용도
[ClickHouseColumn(Name = "...")] 대상 컬럼 이름을 재정의
[ClickHouseColumn(Type = "...")] ClickHouse 타입을 명시적으로 선언
[ClickHouseNotMapped] 삽입 대상에서 해당 속성을 제외

매핑된 모든 속성에 명시적인 Type이 지정되면 스키마 확인 쿼리는 완전히 생략됩니다. 일부 속성에만 명시적 타입이 있으면 드라이버는 전체 컬럼 집합에 대해 스키마 확인 쿼리를 사용합니다.

InsertBinaryAsync<T>object[] 오버로드와 동일한 InsertOptions(배칭, 병렬 처리, 스키마 캐싱)를 지원합니다.

스키마 진화

타입이 등록된 후 대상 테이블에 컬럼이 추가되더라도 POCO 삽입은 원활하게 동작합니다. 드라이버는 POCO에 매핑된 컬럼만 삽입하므로, DEFAULT(또는 다른 기본 표현식)가 있는 새 컬럼은 서버가 자동으로 채웁니다. 코드를 변경하거나 다시 등록할 필요가 없습니다.


데이터 읽기

SELECT 쿌리를 실행하려면 ExecuteReaderAsync를 사용합니다. 반환된 ClickHouseDataReaderGetInt64(), GetString(), GetFieldValue<T>() 같은 메서드를 통해 결과 컬럼에 형식에 맞게 접근할 수 있도록 해줍니다.

다음 행으로 이동하려면 Read()를 호출합니다. 더 이상 행이 없으면 false를 반환합니다. 컬럼은 인덱스(0부터 시작) 또는 컬럼 이름으로 접근할 수 있습니다.

using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}

POCO 읽기

컬럼을 인덱스나 이름으로 읽는 대신, 쿼리 결과를 사용자 정의 클래스에 직접 스트리밍할 수 있습니다. 클라이언트에 해당 타입을 한 번만 등록한 다음 QueryAsync<T>를 사용하십시오:

// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
등록

RegisterPocoType<T>()는 삽입과 읽기 매핑을 모두 설정하고, 두 매핑을 모두 사전에 검증합니다. RegisterBinaryInsertType<T>()는 변경 없이 유지되며, 하위 호환성(backwards compatibility)을 위해 계속 삽입 전용으로 남아 있습니다.

등록된 형식은 다음 조건을 충족해야 합니다.

  • 매개변수가 없는 public 생성자
  • public이며 init이 아닌 setter가 있는 public 속성이 1개 이상 있어야 합니다. required 속성도 지원됩니다.
컬럼 매칭

컬럼 매칭은 대소문자를 구분합니다. 결과 컬럼이 누락되면 해당 속성은 기본값으로 유지되며, 추가 결과 컬럼은 무시됩니다.

자동 변환은 수행되지 않으며, 유형이 일치하지 않으면 InvalidOperationException 예외가 발생합니다.

단일 행 구체화하기

리더를 수동으로 순회할 때는 ClickHouseDataReader.MapTo<T>()를 사용하여 리더를 다음으로 진행시키지 않고 현재 행을 등록된 POCO로 구체화합니다:

var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
등록 진단

LoggerFactory가 설정되면 RegisterPocoType<T>()RegisterBinaryInsertType<T>()는 어떤 속성이 어떤 컬럼에 매핑되었는지와 어떤 항목이 왜 건너뛰어졌는지를 보여주는 Debug 수준의 로그(카테고리: ClickHouse.Driver.Client)를 출력합니다. 로깅 및 진단을 참조하십시오.


SQL 매개변수

ClickHouse에서 SQL 쿼리의 쿼리 매개변수는 일반적으로 {parameter_name:DataType} 포맷을 사용합니다.

예시:

SELECT {value:Array(UInt16)} as a
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})

Identifier 매개변수

Identifier 매개변수 유형을 사용하면 따옴표로 묶은 문자열 리터럴 대신 데이터베이스, 테이블 또는 컬럼 이름을 안전하게 바인딩할 수 있습니다. SQL에서는 {name:Identifier} 구문을 사용하거나, ClickHouseDbParameter.ClickHouseType = "Identifier"로 설정하여 사용할 수 있습니다:

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);

값은 있는 그대로 전송되며, server는 이를 일반 SQL 식별자로 치환하고 자체적으로 backtick 인용과 escaping을 적용합니다. 특수 문자(backtick 포함)가 있는 식별자도 안전하게 round-trip됩니다.


쿼리 ID

모든 쿼리에는 고유한 query_id가 할당되며, 이 값은 system.query_log 테이블에서 데이터를 조회하거나 장시간 실행 중인 쿼리를 취소하는 데 사용할 수 있습니다. QueryOptions를 통해 사용자 지정 쿼리 ID를 지정할 수 있습니다:

var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);

사용자 지정 매개변수 타입 매핑

@ 스타일 매개변수(예: WHERE id = @id)를 사용하면 드라이버가 .NET 값 형식을 기준으로 ClickHouse 타입을 자동으로 추론합니다. 예를 들어 intInt32로 매핑됩니다.

이 기본 동작을 재정의하려면 ClickHouseClientSettings에서 ParameterTypeResolver를 설정하십시오. 이렇게 하면 각 개별 매개변수에 ClickHouseType을 일일이 설정하지 않고도, 모든 DateTime 매개변수에 밀리초 정밀도를 위해 DateTime64(3)를 사용하거나 모든 decimal에 특정 scale을 적용할 수 있어 유용합니다.

간단한 타입 매핑에 DictionaryParameterTypeResolver 사용:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);

고급 시나리오를 위한 사용자 지정 IParameterTypeResolver:

값을 고려하거나 이름을 기준으로 확인해야 하는 경우 IParameterTypeResolver 인터페이스를 직접 구현하십시오. 기본 추론을 사용하도록 하려면 null을 반환하십시오:

public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}

단일 쿼리에도 QueryOptions.ParameterTypeResolver를 통해 리졸버를 설정할 수 있습니다. 설정된 경우 클라이언트 수준의 리졸버보다 우선합니다.

타입 결정 우선순위:

리졸버는 우선순위 체인의 한 단계입니다. 우선순위가 높은 순서부터 낮은 순서는 다음과 같습니다.

  1. 매개변수에 명시적으로 설정된 ClickHouseType
  2. 쿼리의 {name:Type} 구문에 지정된 SQL type hint
  3. IParameterTypeResolver (QueryOptions.ParameterTypeResolver에서 가져오고, 없으면 ClickHouseClientSettings.ParameterTypeResolver를 사용)
  4. 기본 제공 타입 추론(TypeConverter.ToClickHouseType)

리졸버는 ADO.NET ClickHouseConnection 경로에서도 작동합니다. 설정은 클라이언트에서 생성된 연결에 상속됩니다.


사용자 지정 매개변수 값 포맷팅

IParameterFormatter는 매개변수 값이 어떻게 직렬화되는지 결정하는 후크입니다. 기본 제공 포맷팅(예: DateTime 정밀도, Decimal 문화권 형식, 문자열 이스케이프 처리, 숫자 표현)이 스키마 또는 다운스트림 도구에서 기대하는 방식과 맞지 않을 때 사용합니다.

매개변수화된 모든 쿼리에 포맷터를 적용하려면 ClickHouseClientSettings에서 ParameterFormatter를 설정합니다. 이 포맷터는 값, 확인된 ClickHouse type name, 그리고 매개변수 이름을 받아 서버로 전송할 문자열 표현을 반환합니다. 기본 포맷터를 사용하려면 null을 반환합니다.

간단한 CLR 형식별 포맷팅에는 DictionaryParameterFormatter 사용:

using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);

고급 사용 사례를 위한 사용자 지정 IParameterFormatter:

public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}

QueryOptions.ParameterFormatter를 통해 쿼리별로 포맷터를 설정할 수도 있습니다. 설정된 경우 클라이언트 수준 포맷터보다 우선 적용됩니다.

복합 값:

포맷터는 최상위 컬렉션 매개변수와 복합 값(Array, Tuple, Map, Nullable, LowCardinality, Variant) 내부의 각 요소 모두에 대해 실행됩니다. 예를 들어 typeof(int) 매핑은 Array(Int32)의 각 Int32 요소를 개별적으로 포맷합니다.

복합 컨텍스트에서의 작은따옴표 래핑:

복합 리터럴 안에 포함된 문자열 계열 ClickHouse 타입(String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID)의 경우, 드라이버는 포맷터의 출력값을 작은따옴표로 감싸지만 그 내용은 이스케이프하지 않습니다. 반환된 문자열에 이스케이프되지 않은 작은따옴표나 백슬래시가 포함되어 있으면 복합 리터럴의 형식이 잘못되어 서버가 쿼리를 거부합니다.

최상위 문자열 매개변수(복합 값에 포함되지 않은 경우)는 감싸지지 않은 채 그대로 사용되므로, 이 경우에는 이스케이프가 필요하지 않습니다.

포맷터 우선순위:

  1. IParameterFormatter (QueryOptions.ParameterFormatter에서 가져오고, 없으면 ClickHouseClientSettings.ParameterFormatter로 대체). null이 아닌 값을 반환하면 해당 값을 사용합니다.
  2. HttpParameterFormatter의 내장 타입별 포맷팅.

포맷터는 null 또는 DBNull 값에는 적용되지 않으며, 이러한 값은 항상 ClickHouse null 센티널(\N)로 직렬화됩니다.


사용자 지정 읽기 값 변환

IReadValueConverter를 사용하면 역직렬화 후 데이터 리더가 반환하는 값을 CLR 타입은 변경하지 않은 채 변환할 수 있습니다. 일반적인 용도로는 시간대가 없는 DateTime 컬럼에서 DateTime.Kind = Utc를 설정하거나, 문자열을 트리밍하거나 정규화하거나, JSON column이 application code에 전달되기 전에 후처리하는 작업이 있습니다.

모든 읽기에 컨버터를 적용하려면 ClickHouseClientSettings에서 ReadValueConverter를 설정하십시오. 컨버터는 boxed(GetValue) 경로와 generic(GetFieldValue<T>) 경로 모두에서 각 행의 각 컬럼마다 한 번 호출됩니다. 컨버터를 설정하지 않으면 오버헤드가 전혀 없으며 리더는 값을 직접 반환합니다.

간단한 CLR 타입별 변환에 DictionaryReadValueConverter 사용:

using ClickHouse.Driver.ADO.Readers;

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);

런타임 CLR 유형이 For<T>에 등록되지 않은 값은 변경되지 않은 채 그대로 전달됩니다. 디스패치는 정확히 일치하는 CLR 유형을 기준으로 이루어지므로, 리더가 실제로 생성하는 유형을 등록하십시오(예: JsonReadMode.Binary의 JSON 컬럼에는 For<JsonObject>를 사용).

고급 시나리오를 위한 사용자 지정 IReadValueConverter:

ClickHouse 측 타입 문자열을 기준으로 디스패치해야 한다면(예를 들어 DateTimeDateTime('UTC')를 구분해야 하는 경우 — 둘 다 동일한 CLR 유형으로 나타남), IReadValueConverter를 직접 구현하십시오:

public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickhouseType)
    {
        if (value is DateTime dt && clickhouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickhouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickhouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}

컨버터는 런타임 CLR 형식을 보존해야 합니다. 컬럼 메타데이터(GetFieldType, GetSchemaTable)는 이를 거쳐 다시 처리되지 않으므로, 반환되는 값과 일관성을 유지해야 합니다.

QueryOptions.ReadValueConverter를 통해 쿼리별 컨버터를 설정할 수도 있습니다. 이 값을 설정하면 클라이언트 수준 컨버터보다 우선합니다.

디스패치 경계:

컨버터는 역직렬화된 전체 셀 값을 대상으로 컬럼마다 한 번씩 호출되며, 복합 컨테이너 내부로 재귀적으로 들어가지는 않습니다. Array(Int32) 컬럼에서는 전달되는 값이 int[]이고, Tuple(Int32, String)에서는 ITuple입니다.

컨버터는 ADO.NET ClickHouseConnection 경로에서 동작하며, 이 설정은 클라이언트에서 생성된 연결에 상속됩니다.


Raw 스트리밍

데이터 리더를 거치지 않고 특정 포맷으로 쿼리 결과를 직접 스트리밍하려면 ExecuteRawResultAsync를 사용합니다. 이는 데이터를 파일로 내보내거나 다른 시스템으로 그대로 전달할 때 유용합니다:

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();

일반적으로 사용되는 포맷: JSONEachRow, CSV, TSV, Parquet, Native. 모든 옵션은 포맷 문서를 참조하십시오.


쿼리별 전송 압축

기본적으로 Compression=true(connection string의 기본값)인 경우 클라이언트는 gzip, deflate를 협상하며, HTTP 클라이언트는 스트림 압축을 자동으로 해제합니다.

원시 내보내기(예: Parquet, Arrow, Native)에서는 connection 전체 설정을 변경하지 않고도 CPU 사용량과 대역폭을 절충하기 위해 다른 코덱(예: zstd 또는 lz4)을 협상할 수 있습니다. QueryOptions.AcceptEncodingClickHouseCommand.AcceptEncoding은 단일 요청에 대해 HTTP Accept-Encoding 헤더를 설정하고, 기존에 적용된 기본값을 대체하며, URL에 enable_http_compression=1을 강제로 추가합니다(ClickHouse는 Accept-Encoding을 적용하기 전에 이를 요구합니다).

using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();

HttpClient 구성

드라이버가 생성하는 기본 HttpClient에는 AutomaticDecompression = GZip | Deflate가 설정되어 있으며, 해당 알고리즘으로 압축된 데이터를 자동으로 압축 해제하고 응답에서 Content-Encoding을 제거합니다. 일반적인 쿼리에는 이 설정이 적합하지만, 원시 압축 데이터를 직접 처리하려는 경우에는 적합하지 않습니다.

이 경우 AutomaticDecompression = DecompressionMethods.None으로 설정된 HttpClient 또는 HttpClientFactory를 ClickHouseClient에 전달하십시오.

오류 본문

서버가 4xx/5xx로 응답하고 enable_http_compression=1이 설정된 경우, 정상 응답에 사용했을 것과 동일한 코덱으로 오류 본문을 압축합니다. 드라이버는 BCL에 포함된 코덱(gzip, deflate, br/brotli)에 대해서는 이를 압축 해제하므로 ClickHouseServerException에 표시되는 메시지를 읽을 수 있습니다. 디코딩할 수 없는 코덱(zstd, lz4, …)의 경우, 드라이버는 코덱 이름을 포함하고 원래 오류 텍스트는 system.query_log에서 확인하라고 안내하는 자리표시자 메시지를 반환합니다.


Raw 스트림 삽입

InsertRawStreamAsync를 사용하면 CSV, JSON, Parquet 또는 기타 지원되는 ClickHouse 포맷의 파일 또는 메모리 스트림에서 데이터를 직접 삽입할 수 있습니다.

CSV 파일에서 삽입:

await using var fileStream = File.OpenRead("data.csv");

using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: fileStream,
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);

추가 예시

실제 사용에 도움이 되는 추가 예시는 GitHub 리포지토리의 examples 디렉터리에서 확인하십시오.

ADO.NET

이 라이브러리는 ClickHouseConnection, ClickHouseCommand, ClickHouseDataReader를 통해 ADO.NET을 완전하게 지원합니다. 이 API는 ORM 통합(Dapper, Linq2db)과 표준 .NET 데이터베이스 추상화가 필요할 때 사용해야 합니다.

ClickHouseDataSource를 사용한 수명 주기 관리

적절한 수명 주기 관리와 연결 풀링을 위해 항상 ClickHouseDataSource에서 연결을 생성하세요. DataSource는 내부적으로 단일 ClickHouseClient를 관리하며, 모든 연결은 해당 HTTP 연결 풀을 공유합니다.

using ClickHouse.Driver.ADO;

// DataSource를 한 번만 생성합니다 (DI에 싱글톤으로 등록)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// 필요할 때마다 경량 연결을 생성합니다
await using var connection = await dataSource.OpenConnectionAsync();

// 연결을 사용합니다
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();

종속성 주입 시:

// Startup.cs 또는 Program.cs에서
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// 서비스에서
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // 연결 사용...
    }
}

ClickHouseCommand 사용하기

연결을 통해 SQL을 실행할 명령을 생성합니다:

await using var connection = await dataSource.OpenConnectionAsync();

// SQL로 명령 생성
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// 실행 및 결과 읽기
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}

명령 메서드:

  • ExecuteNonQueryAsync() - INSERT, UPDATE, DELETE, DDL 문에 사용됩니다
  • ExecuteScalarAsync() - 첫 번째 행의 첫 번째 컬럼을 반환합니다
  • ExecuteReaderAsync() - 결과를 순회할 수 있는 ClickHouseDataReader를 반환합니다

ClickHouseDataReader 사용

ClickHouseDataReader는 쿼리 결과에 타입이 지정된 형태로 접근할 수 있도록 제공합니다:

await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // 컬럼 인덱스로 접근
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // 컬럼 이름으로 접근
    var email = reader.GetString("email");

    // 제네릭 방식으로 접근
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // null 여부 확인
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}

모범 사례

연결 수명 및 풀링

ClickHouse.Driver는 내부적으로 System.Net.Http.HttpClient를 사용합니다. HttpClient에는 엔드포인트별 연결 풀이 있습니다. 그에 따라 다음과 같은 특성이 있습니다.

  • 데이터베이스 세션은 연결 풀에서 관리하는 HTTP 연결을 통해 다중화됩니다.
  • HTTP 연결은 풀에서 자동으로 재사용됩니다.
  • ClickHouseClient 또는 ClickHouseConnection 객체를 dispose한 뒤에도 연결이 유지될 수 있습니다.

권장 패턴:

시나리오 권장 방식
일반적인 사용 싱글턴 ClickHouseClient 사용
ADO.NET / ORMs ClickHouseDataSource 사용 (같은 풀을 공유하는 연결 생성)
DI 환경 IHttpClientFactory와 함께 ClickHouseClient 또는 ClickHouseDataSource를 싱글턴으로 등록

DateTime 처리

  1. 가능하면 항상 UTC를 사용하십시오. 타임스탬프는 DateTime('UTC') 컬럼에 저장하고, 코드에서는 DateTimeKind.Utc를 사용하십시오. 이렇게 하면 시간대와 관련된 모호성을 없앨 수 있습니다.

  2. 시간대를 명시적으로 처리해야 할 때는 DateTimeOffset을 사용하십시오. DateTimeOffset은 항상 특정 시점을 나타내며, 오프셋 정보도 함께 포함합니다.

  3. SQL type hint에 시간대를 지정하십시오. UTC가 아닌 컬럼을 대상으로 하는 Unspecified DateTime 값을 매개변수로 사용할 때는 SQL에 시간대를 포함하십시오.

    var parameters = new ClickHouseParameterCollection();
    parameters.AddParameter("dt", myDateTime);
    
    await client.ExecuteNonQueryAsync(
        "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
        parameters
    );

비동기 삽입

비동기 삽입은 배칭의 책임을 클라이언트에서 서버로 옮깁니다. 클라이언트 측에서 배칭해야 하는 대신, 서버가 들어오는 데이터를 버퍼에 저장했다가 구성 가능한 임계값에 따라 스토리지로 플러시합니다. 이는 많은 에이전트가 작은 페이로드를 전송하는 관측성 워크로드와 같은 고동시성 시나리오에서 유용합니다.

CustomSettings 또는 연결 문자열(connection string)을 통해 비동기 삽입을 활성화하세요:

// CustomSettings 사용
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // 권장: 플러시 확인 응답 대기

// 또는 connection string을 통해 설정
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"

두 가지 모드 (wait_for_async_insert로 제어):

모드 동작 사용 사례
wait_for_async_insert=1 데이터가 디스크에 플러시된 후 삽입이 완료되어 반환됩니다. 오류는 클라이언트에 반환됩니다. 대부분의 workload에 권장됨
wait_for_async_insert=0 데이터가 버퍼링되면 즉시 삽입이 반환됩니다. 데이터가 영구 저장된다는 보장은 없습니다. 데이터 손실을 허용할 수 있을 때만

주요 설정:

설정 설명
async_insert_max_data_size 버퍼가 이 크기(바이트)에 도달하면 플러시
async_insert_busy_timeout_ms 이 timeout(밀리초) 이후 플러시
async_insert_max_query_number 이 수만큼 쿼리가 누적되면 플러시

세션

상태를 유지하는 서버 측 기능이 필요할 때만 세션을 활성화하세요. 예:

  • 임시 테이블 (CREATE TEMPORARY TABLE)
  • 여러 SQL 문에 걸쳐 쿼리 컨텍스트 유지
  • 세션 수준 설정 (SET max_threads = 4)

세션을 활성화하면 동일한 세션이 동시에 사용되지 않도록 요청이 직렬화됩니다. 따라서 세션 상태가 필요하지 않은 워크로드에는 오버헤드가 발생합니다.

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // 선택 사항 -- 제공하지 않으면 자동 생성됩니다
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);

ADO.NET 사용(ORM 호환성을 위해):

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();

지원 데이터 타입

ClickHouse.Driver는 모든 ClickHouse 데이터 타입을 지원합니다. 아래 표는 데이터베이스에서 데이터를 읽을 때 ClickHouse 타입과 네이티브 .NET 타입 간의 매핑을 보여줍니다.

타입 매핑: ClickHouse에서 읽어올 때

정수 타입

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

부동 소수점 타입

ClickHouse 타입 .NET 타입
Float32 float
Float64 double
BFloat16 float

Decimal 타입

ClickHouse 타입 .NET 타입
Decimal(P, S) decimal / ClickHouseDecimal
Decimal32(S) decimal / ClickHouseDecimal
Decimal64(S) decimal / ClickHouseDecimal
Decimal128(S) decimal / ClickHouseDecimal
Decimal256(S) decimal / ClickHouseDecimal

불리언 타입

ClickHouse 타입 .NET 타입
Bool bool

String 타입

ClickHouse 타입 .NET 타입
String string
FixedString(N) string

날짜 및 시간 타입

ClickHouse 타입 .NET Type
Date DateTime
Date32 DateTime
DateTime DateTime
DateTime32 DateTime
DateTime64 DateTime
Time TimeSpan
Time64 TimeSpan

ClickHouse는 DateTimeDateTime64 값을 내부적으로 Unix timestamp(epoch 이후의 초 또는 그보다 작은 단위)로 저장합니다. 저장은 항상 UTC로 이루어지지만, 컬럼에는 연결된 시간대가 있을 수 있으며 이 시간대는 값이 표시되고 해석되는 방식에 영향을 줍니다.

DateTime 값을 읽을 때 DateTime.Kind 속성은 컬럼의 시간대에 따라 설정됩니다:

컬럼 정의 반환되는 DateTime.Kind 참고
DateTime('UTC') Utc UTC 시간대를 명시적으로 지정
DateTime('Europe/Amsterdam') Unspecified 오프셋 적용
DateTime Unspecified 현지 시각이 그대로 유지됨

UTC가 아닌 컬럼의 경우, 반환되는 DateTime은 해당 시간대의 현지 시각을 나타냅니다. 해당 시간대에 맞는 올바른 오프셋이 포함된 DateTimeOffset을 가져오려면 ClickHouseDataReader.GetDateTimeOffset()을 사용하십시오:

var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)

명시적인 시간대가 없는 컬럼(즉, DateTime('Europe/Amsterdam')이 아니라 DateTime)의 경우, 드라이버는 Kind=UnspecifiedDateTime을 반환합니다. 이렇게 하면 시간대에 대해 별도로 가정하지 않고, 저장된 wall-clock time을 정확히 그대로 유지할 수 있습니다.

명시적인 시간대가 없는 컬럼에서 시간대 인식 동작이 필요하다면, 다음 중 하나를 사용하십시오.

  1. 컬럼 정의에 명시적인 시간대를 사용합니다: DateTime('UTC') 또는 DateTime('Europe/Amsterdam')
  2. 읽은 후 시간대를 직접 적용합니다.

JSON 타입

ClickHouse Type .NET Type 비고
Json JsonObject 기본값 (JsonReadMode=Binary)
Json string JsonReadMode=String일 때

JSON 컬럼의 반환 타입은 JsonReadMode 설정으로 제어됩니다:

  • Binary (기본값): System.Text.Json.Nodes.JsonObject를 반환합니다. JSON 데이터에 구조적으로 접근할 수 있지만, IP 주소, UUID, 큰 Decimal 값과 같은 ClickHouse의 특수 타입은 JSON 구조 안에서 문자열 표현으로 변환됩니다.

  • String: 원본 JSON을 string으로 반환합니다. ClickHouse의 JSON 표현을 그대로 유지하므로, parsing 없이 JSON을 그대로 전달해야 하거나 역직렬화를 직접 처리하려는 경우에 유용합니다.

// 설정을 통해 string 모드 구성
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// 또는 connection string을 통해 구성
// "Host=localhost;JsonReadMode=String"

기타 타입

ClickHouse 타입 .NET 타입
UUID Guid
IPv4 IPAddress
IPv6 IPAddress
Nothing DBNull
Dynamic 참고 사항을 참조하세요
Array(T) T[] (중첩된 Array(Array(T))는 가변 T[][]로 읽히며, 직사각형 데이터를 다차원 CLR 배열로 구체화하려면 reader.GetFieldValue<T[,]>(ordinal)를 사용하십시오)
Tuple(T1, T2, …) Tuple<T1, T2, ...> / LargeTuple
Map(K, V) Dictionary<K, V>
Nullable(T) T?
Enum8 string
Enum16 string
LowCardinality(T) T와 동일
SimpleAggregateFunction 기반 타입과 동일
Nested(…) Tuple[]
Variant(T1, T2, …) 참고 사항을 참조하세요
QBit(T, dimension) T[]

Geometry 타입

ClickHouse 타입 .NET 타입
Point Tuple<double, double>
Ring Tuple<double, double>[]
LineString Tuple<double, double>[]
Polygon Ring[]
MultiLineString LineString[]
MultiPolygon Polygon[]
Geometry 참고 사항을 참조하세요

타입 매핑: ClickHouse에 쓰기

데이터를 삽입할 때 드라이버는 .NET 타입을 해당 ClickHouse 타입으로 변환합니다. 아래 표에는 각 ClickHouse 컬럼 타입에 허용되는 .NET 타입이 나와 있습니다.

정수 타입

ClickHouse 타입 허용되는 .NET 타입 참고 사항
Int8 sbyte, Convert.ToSByte()와 호환되는 모든 타입
UInt8 byte, Convert.ToByte()와 호환되는 모든 타입
Int16 short, Convert.ToInt16()와 호환되는 모든 타입
UInt16 ushort, Convert.ToUInt16()와 호환되는 모든 타입
Int32 int, Convert.ToInt32()와 호환되는 모든 타입
UInt32 uint, Convert.ToUInt32()와 호환되는 모든 타입
Int64 long, Convert.ToInt64()와 호환되는 모든 타입
UInt64 ulong, Convert.ToUInt64()와 호환되는 모든 타입
Int128 BigInteger, decimal, double, float, int, uint, long, ulong, Convert.ToInt64()와 호환되는 모든 타입
UInt128 BigInteger, decimal, double, float, int, uint, long, ulong, Convert.ToInt64()와 호환되는 모든 타입
Int256 BigInteger, decimal, double, float, int, uint, long, ulong, Convert.ToInt64()와 호환되는 모든 타입
UInt256 BigInteger, decimal, double, float, int, uint, long, ulong, Convert.ToInt64()와 호환되는 모든 타입

부동 소수점 타입

ClickHouse 타입 Accepted .NET Types 비고
Float32 float, any Convert.ToSingle() compatible
Float64 double, any Convert.ToDouble() compatible
BFloat16 float, any Convert.ToSingle() compatible 16비트 brain float 포맷으로 잘라냄

불리언 타입

ClickHouse 타입 허용되는 .NET 타입 참고 사항
Bool bool

String 타입

ClickHouse 타입 허용되는 .NET 타입 참고
String string, byte[], ReadOnlyMemory<byte>, Stream 바이너리 타입은 직접 기록되며, 스트림은 seek 가능하거나 불가능할 수 있습니다
FixedString(N) string, byte[], ReadOnlyMemory<byte>, Stream String은 UTF-8로 인코딩된 후 패딩되며, 바이너리 타입은 정확히 N바이트여야 합니다

날짜 및 시간 타입

ClickHouse 타입 Accepted .NET Types 참고 사항
Date DateTime, DateTimeOffset, DateOnly, NodaTime 타입 Unix 일수로 변환되어 UInt16으로 저장되며, 지원 범위는 [1970-01-01, 2149-06-06]입니다
Date32 DateTime, DateTimeOffset, DateOnly, NodaTime 타입 Unix 일수로 변환되어 Int32로 저장되며, 지원 범위는 [1900-01-01, 2299-12-31]입니다
DateTime DateTime, DateTimeOffset, DateOnly, NodaTime 타입 자세한 내용은 아래를 참조하십시오. 지원 범위는 UTC 기준 [1970-01-01, 2106-02-07 06:28:15]입니다
DateTime32 DateTime, DateTimeOffset, DateOnly, NodaTime 타입 DateTime과 동일합니다
DateTime64 DateTime, DateTimeOffset, DateOnly, NodaTime 타입 정밀도는 Scale parameter를 기준으로 결정됩니다
Time TimeSpan, int ±999:59:59 범위로 제한되며, int는 초 단위로 처리됩니다
Time64 TimeSpan, decimal, double, float, int, long, string 문자열은 [-]HHH:MM:SS[.fraction] 형식으로 parse되며, ±999:59:59.999999999 범위로 제한됩니다

드라이버는 값을 쓸 때 DateTime.Kind를 따릅니다:

DateTime.Kind HTTP 매개변수 벌크 복사
Utc 시점이 그대로 유지됩니다 시점이 그대로 유지됩니다
Local 시점이 그대로 유지됩니다 시점이 그대로 유지됩니다
Unspecified 매개변수 타입의 시간대 기준 시각으로 처리됩니다(기본값: UTC) 컬럼의 시간대 기준 시각으로 처리됩니다

DateTimeOffset 값은 항상 정확한 시점을 유지합니다.

예시: UTC DateTime (시점 유지)

var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC

예시: 지정되지 않은 DateTime(현지 시계 시간)

var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30

권장 사항: 가장 단순하고 예측 가능한 동작을 위해 모든 DateTime 작업에는 DateTimeKind.Utc 또는 DateTimeOffset을 사용하십시오. 이렇게 하면 서버 시간대, 클라이언트 시간대, 컬럼 시간대와 관계없이 코드가 항상 일관되게 동작합니다.

HTTP 매개변수와 대량 복사

Unspecified DateTime 값을 쓸 때는 HTTP 매개변수 바인딩과 대량 복사 방식 사이에 중요한 차이가 있습니다:

대량 복사는 대상 컬럼의 시간대를 알고 있으므로 Unspecified 값을 해당 시간대로 올바르게 해석합니다.

HTTP 매개변수는 컬럼의 시간대를 자동으로 알지 못합니다. 따라서 SQL 타입 힌트에 시간대를 지정해야 합니다:

// 올바름: SQL 타입 힌트에 시간대 지정 - 타입이 자동으로 추출됨
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// 잘못됨: 시간대 힌트 없이 UTC로 해석됨
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// 문자열 값 "2024-01-15 14:30:00"이 암스테르담 시간이 아닌 UTC로 해석됨!
DateTime.Kind 대상 컬럼 HTTP 매개변수(tz 힌트 포함) HTTP 매개변수(tz 힌트 없음) 대량 복사
Utc UTC 시점 유지 시점 유지 시점 유지
Utc Europe/Amsterdam 시점 유지 시점 유지 시점 유지
Local 임의 시점 유지 시점 유지 시점 유지
Unspecified UTC UTC로 간주 UTC로 간주 UTC로 간주
Unspecified Europe/Amsterdam 암스테르담 시간으로 간주 UTC로 간주 암스테르담 시간으로 간주

Decimal 타입

ClickHouse 타입 허용되는 .NET 타입 비고
Decimal(P,S) decimal, ClickHouseDecimal, Convert.ToDecimal()과 호환되는 모든 타입 정밀도를 초과하면 OverflowException이 발생합니다
Decimal32 decimal, ClickHouseDecimal, Convert.ToDecimal()과 호환되는 모든 타입 최대 정밀도 9
Decimal64 decimal, ClickHouseDecimal, Convert.ToDecimal()과 호환되는 모든 타입 최대 정밀도 18
Decimal128 decimal, ClickHouseDecimal, Convert.ToDecimal()과 호환되는 모든 타입 최대 정밀도 38
Decimal256 decimal, ClickHouseDecimal, Convert.ToDecimal()과 호환되는 모든 타입 최대 정밀도 76

JSON 타입

ClickHouse 타입 허용되는 .NET 타입 참고
Json string, JsonObject, JsonNode, 임의의 객체 동작은 JsonWriteMode 설정에 따라 달라집니다

JSON을 쓸 때의 동작은 JsonWriteMode 설정으로 제어됩니다:

입력 타입 JsonWriteMode.String (기본값) JsonWriteMode.Binary
string 그대로 전달됩니다 ArgumentException이 발생합니다
JsonObject ToJsonString()를 통해 직렬화됩니다 ArgumentException이 발생합니다
JsonNode ToJsonString()를 통해 직렬화됩니다 ArgumentException이 발생합니다
등록된 POCO JsonSerializer.Serialize()로 직렬화됩니다 타입 힌트와 함께 바이너리 인코딩되며, 사용자 지정 경로 속성도 지원합니다
등록되지 않은 POCO / 익명 객체 JsonSerializer.Serialize()로 직렬화됩니다 ClickHouseJsonSerializationException이 발생합니다
  • String (기본값): string, JsonObject, JsonNode 또는 임의의 객체를 허용합니다. 모든 입력은 System.Text.Json.JsonSerializer를 통해 직렬화되며, 서버 측에서 파싱할 수 있도록 JSON 문자열로 전송됩니다. 가장 유연한 모드이며 타입 등록 없이도 사용할 수 있습니다.

  • Binary: 등록된 POCO 타입만 허용합니다. 데이터는 클라이언트 측에서 전체 타입 힌트 지원과 함께 ClickHouse의 바이너리 JSON 포맷으로 변환됩니다. 사용 전에 connection.RegisterJsonSerializationType<T>()를 호출해야 합니다. 이 모드에서 string 또는 JsonNode 값을 쓰면 ArgumentException이 발생합니다.

// 기본 String 모드는 모든 입력을 처리할 수 있습니다
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary 모드는 명시적 활성화 및 타입 등록이 필요합니다
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
타입이 지정된 JSON 컬럼

JSON 컬럼에 타입 힌트가 있는 경우(예: JSON(id UInt64, price Decimal128(2))), 드라이버는 이 힌트를 사용해 값을 원래 타입 정보를 온전히 유지하면서 직렬화합니다. 이를 통해 일반 JSON으로 직렬화할 때 정밀도가 손실될 수 있는 UInt64, Decimal, UUID, DateTime64 같은 타입의 정밀도를 보존할 수 있습니다.

POCO 직렬화

POCO는 JsonWriteMode에 따라 두 가지 방식으로 JSON 컬럼에 쓸 수 있습니다:

String 모드(기본값): POCO는 System.Text.Json.JsonSerializer를 통해 직렬화됩니다. 타입 등록은 필요하지 않습니다. 가장 간단한 방식이며 익명 객체에도 사용할 수 있습니다.

Binary 모드: POCO는 드라이버의 바이너리 JSON 포맷을 사용해 직렬화되며, 타입 힌트를 완전히 지원합니다. 사용 전에 connection.RegisterJsonSerializationType<T>()로 타입을 등록해야 합니다. 이 모드에서는 특성을 사용해 사용자 지정 경로 매핑을 적용할 수 있습니다:

  • [ClickHouseJsonPath("path")]: 속성을 사용자 지정 JSON 경로에 매핑합니다. 중첩 구조를 다루거나 속성 이름이 원하는 JSON 키와 다를 때 유용합니다. Binary 모드에서만 작동합니다.

  • [ClickHouseJsonIgnore]: 직렬화 대상에서 속성을 제외합니다. Binary 모드에서만 작동합니다.

CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // 직렬화되지 않음
}

// Binary 모드: 유형을 등록하고 Binary 모드를 활성화합니다
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// POCO 삽입 - 사용자 정의 경로 속성을 통해 중첩 구조의 JSON으로 직렬화됩니다
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// 결과 JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}

컬럼 타입 힌트와 속성 이름의 매칭은 대소문자를 구분합니다. 속성 UserIduserid가 아니라 UserId로 정의된 힌트에만 일치합니다. 이는 userNameUserName 같은 경로가 각각 별도의 필드로 공존할 수 있도록 허용하는 ClickHouse 동작과 일치합니다.

제한 사항(Binary 모드 전용):

  • POCO 타입은 직렬화 전에 connection.RegisterJsonSerializationType<T>()를 사용해 connection에 등록해야 합니다. 등록되지 않은 타입을 직렬화하려고 하면 ClickHouseJsonSerializationException이 발생합니다.
  • 딕셔너리 및 배열/리스트 속성이 올바르게 직렬화되려면 컬럼 정의에 타입 힌트가 필요합니다. 힌트가 없으면 대신 String 모드를 사용하십시오.
  • POCO 속성의 NULL 값은 컬럼 정의의 해당 경로에 Nullable(T) 타입 힌트가 있을 때만 기록됩니다. ClickHouse는 동적 JSON 경로 내부에 Nullable 타입을 허용하지 않으므로, 힌트가 없는 null 속성은 건너뜁니다.
  • ClickHouseJsonPathClickHouseJsonIgnore 특성은 String 모드에서는 무시됩니다(Binary 모드에서만 작동합니다).

기타 타입

ClickHouse 타입 허용되는 .NET 타입 비고
UUID Guid, string 문자열은 Guid로 파싱됨
IPv4 IPAddress, string IPv4여야 하며, 문자열은 IPAddress.Parse()로 파싱됨
IPv6 IPAddress, string IPv6여야 하며, 문자열은 IPAddress.Parse()로 파싱됨
Nothing 임의 아무것도 기록하지 않음(no-op)
Dynamic 지원되지 않음 (NotImplementedException 발생)
Array(T) IList, null null은 빈 배열로 기록됨. 중첩 타입(Array(Array(T)) 및 그보다 더 깊은 중첩)의 경우 계단형 구조(T[][], List<List<T>>)와 직사각형 다차원 CLR 배열(T[,], T[,,], …)이 모두 허용되며, CLR rank는 ClickHouse 중첩 깊이와 일치해야 함.
Tuple(T1, T2, …) ITuple, IList 요소 수는 튜플의 원소 수와 일치해야 함. 요소가 7개를 초과하는 경우 ValueTuple 주의사항을 참조하십시오.
Map(K, V) IDictionary
Nullable(T) null, DBNull, 또는 T에서 허용하는 타입 값 앞에 null 플래그 바이트를 기록함
Enum8 string, sbyte, 숫자 타입 문자열은 enum 딕셔너리에서 조회됨
Enum16 string, short, 숫자 타입 문자열은 enum 딕셔너리에서 조회됨
LowCardinality(T) T에서 허용하는 타입 기반 타입에 위임
SimpleAggregateFunction 기반 타입에서 허용하는 타입 기반 타입에 위임
Nested(…) 튜플의 IList 요소 수는 필드 수와 일치해야 함
Variant(T1, T2, …) T1, T2, … 중 하나에 일치하는 값 일치하는 타입이 없으면 ArgumentException 발생
QBit(T, dim) IList Array에 위임되며, 차원은 메타데이터로만 사용됨

Geometry 타입

ClickHouse 타입 허용되는 .NET 타입 비고
Point System.Drawing.Point, ITuple, IList (요소 2개)
Ring Point의 IList
LineString Point의 IList
Polygon Ring의 IList
MultiLineString LineString의 IList
MultiPolygon Polygon의 IList
Geometry 위의 모든 Geometry 타입 모든 Geometry 타입을 포함하는 Variant

쓰기에서 지원되지 않음

ClickHouse 타입 참고 사항
Dynamic NotImplementedException이 발생합니다
AggregateFunction AggregateFunctionException이 발생합니다

중첩 타입 처리

ClickHouse 중첩 타입(Nested(...))은 배열 문법으로 읽고 쓸 수 있습니다.

CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);

로깅 및 진단

ClickHouse .NET 클라이언트는 Microsoft.Extensions.Logging 추상화와 통합되어 가볍고 필요할 때 선택적으로 사용할 수 있는 로깅을 제공합니다. 활성화하면 드라이버는 connection 수명 주기 이벤트, 명령 실행, 전송 작업, 대량 삽입 작업에 대해 구조화된 메시지를 출력합니다. 로깅은 완전히 선택 사항이므로 로거를 구성하지 않은 애플리케이션도 추가 오버헤드 없이 계속 실행됩니다.

빠른 시작

using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

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

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

appsettings.json 사용

표준 .NET 구성을 사용해 로깅 수준을 설정할 수 있습니다:

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

인메모리 구성 사용하기

코드에서 범주별로 로깅 상세도를 설정할 수도 있습니다:

using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);

범주 및 이미터

드라이버는 전용 범주를 사용하므로 구성 요소별로 로그 레벨을 세밀하게 조정할 수 있습니다:

범주 소스 주요 내용
ClickHouse.Driver.Connection ClickHouseConnection 연결 수명 주기, HTTP 클라이언트 팩터리 선택, 연결 열기/닫기, 세션 관리
ClickHouse.Driver.Command ClickHouseCommand 쿼리 실행 시작/완료, 시간 측정, 쿼리 ID, 서버 통계, 오류 세부 정보
ClickHouse.Driver.Transport ClickHouseConnection 저수준 HTTP 스트리밍 요청, 압축 플래그, 응답 상태 코드, 전송 실패
ClickHouse.Driver.Client ClickHouseClient 바이너리 삽입, 쿼리 및 기타 작업
ClickHouse.Driver.NetTrace TraceHelper 네트워크 추적, 디버그 모드가 활성화된 경우에만 해당

예시: 연결 문제 진단하기

{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}

다음 내용이 로그에 기록됩니다:

  • HTTP 클라이언트 팩터리 선택(기본 풀 또는 단일 연결)
  • HTTP handler 구성(SocketsHttpHandler 또는 HttpClientHandler)
  • 연결 풀 설정(MaxConnectionsPerServer, PooledConnectionLifetime 등)
  • timeout 설정(ConnectTimeout, Expect100ContinueTimeout 등)
  • SSL/TLS 구성
  • 연결 열림/닫힘 이벤트
  • 세션 ID 추적

디버그 모드: 네트워크 추적 및 진단

네트워킹 문제를 진단하는 데 도움이 되도록, 드라이버 라이브러리에는 .NET 네트워킹 내부 구성의 저수준 트레이싱을 활성화하는 도우미가 포함되어 있습니다. 이를 활성화하려면 수준이 Trace로 설정된 LoggerFactory를 전달하고 EnableDebugMode를 true로 설정해야 합니다(또는 ClickHouse.Driver.Diagnostic.TraceHelper 클래스를 통해 수동으로 활성화할 수 있습니다). 이벤트는 ClickHouse.Driver.NetTrace 범주에 기록됩니다. 경고: 이렇게 하면 매우 상세한 로그가 대량으로 생성되며 성능에도 영향을 줍니다. 프로덕션 환경에서는 디버그 모드를 활성화하지 않는 것이 좋습니다.

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // 네트워크 이벤트를 확인하려면 Trace 수준으로 설정해야 합니다
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // 저수준 네트워크 추적 활성화
};

OpenTelemetry

이 드라이버는 .NET System.Diagnostics.Activity API를 통해 OpenTelemetry 분산 트레이싱을 기본으로 지원합니다. 이 기능을 활성화하면 드라이버가 데이터베이스 작업에 대한 스팬을 생성하며, 생성된 스팬은 Jaeger나 ClickHouse 자체(OpenTelemetry Collector 사용)와 같은 관측성 백엔드로 내보낼 수 있습니다.

트레이싱 활성화

ASP.NET Core 애플리케이션에서는 ClickHouse 드라이버의 ActivitySource를 OpenTelemetry 구성에 추가하세요:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // ClickHouse 드라이버 스팬 구독
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // 또는 AddJaegerExporter() 등 사용 가능

콘솔 애플리케이션, 테스트 또는 수동 설정 시:

using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();

스팬 속성

각 스팬에는 표준 OpenTelemetry 데이터베이스 속성과, 디버깅에 활용할 수 있는 ClickHouse 전용 쿼리 통계가 포함됩니다.

속성 설명
db.system 항상 "clickhouse"
db.name 데이터베이스 이름
db.user 사용자 이름
db.statement SQL 쿼리(활성화된 경우)
db.clickhouse.read_rows 쿼리가 읽은 행 수
db.clickhouse.read_bytes 쿼리가 읽은 바이트 수
db.clickhouse.written_rows 쿼리가 기록한 행 수
db.clickhouse.written_bytes 쿼리가 기록한 바이트 수
db.clickhouse.elapsed_ns 서버 측 실행 시간(나노초)

구성 옵션

ClickHouseDiagnosticsOptions를 사용해 추적 동작을 제어합니다:

using ClickHouse.Driver.Diagnostic;

// 스팬에 SQL 문 포함 (기본값: 보안상 false)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// 긴 SQL 문 잘라내기 (기본값: 1000자)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;

TLS 구성

HTTPS를 통해 ClickHouse에 연결할 때 TLS/SSL 동작은 여러 방식으로 구성할 수 있습니다.

사용자 지정 인증서 유효성 검사

사용자 지정 인증서 유효성 검사 로직이 필요한 프로덕션 환경에서는 ServerCertificateCustomValidationCallback 핸들러가 구성된 자체 HttpClient를 제공하십시오:

using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // 압축이 활성화된 경우 필요 (기본값)
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,

    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // 예시: 특정 인증서 지문 허용
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // 예시: 특정 발급자의 인증서 허용
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // 기본값: 표준 유효성 검사 사용
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);

ORM 지원

ORM은 ADO.NET API(ClickHouseConnection)를 사용해야 합니다. 연결 수명 주기를 올바르게 관리하려면 ClickHouseDataSource에서 연결을 생성하십시오:

// DataSource를 싱글톤으로 등록
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// ORM에서 사용할 연결 생성
await using var connection = await dataSource.OpenConnectionAsync();
// ORM에 연결 전달...

Dapper

ClickHouse.Driver는 Dapper와 함께 사용할 수 있습니다. 드라이버는 Dapper의 @parameter 구문을 ClickHouse의 네이티브 {parameter:Type} 구문으로 자동 변환하며, 타입은 .NET 값에서 자동으로 추론됩니다.

적절한 연결 수명 주기 관리를 위해 ClickHouseDataSource를 사용하세요:

var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // DI에 싱글톤으로 등록

using var connection = dataSource.CreateConnection();

매개변수 전달 방식

Dapper의 모든 표준 매개변수 전달 방식을 지원합니다.

익명 객체:

await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });

POCO 클래스:

class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);

딕셔너리:

var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);

DynamicParameters (딕셔너리나 익명 객체에서):

var dynParams = new DynamicParameters(new { Id = 1 });
// 또는: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);

POCO로 쿼리 결과 매핑하기

Dapper는 이름을 기준으로 컬럼을 속성에 매핑합니다(대소문자를 구분하지 않음).

class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// 테이블에서 조회
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// 리터럴에서 조회
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();

ClickHouse 네이티브 매개변수 구문

타입을 명시적으로 제어해야 할 때는 SQL에서 ClickHouse의 {param:Type} 구문을 직접 사용하고, 매개변수 값에는 Dictionary<string, object>를 사용하십시오. 동일한 매개변수에 @param 구문과 {param:Type} 구문을 함께 사용하지 마십시오.

var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);

WHERE IN

Dapper의 네이티브 IN 확장은 정상적으로 동작합니다:

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });

Dapper는 이를 WHERE id IN (@Ids1, @Ids2, @Ids3)로 재작성하고, 드라이버는 확장된 각 매개변수를 개별적으로 변환합니다.

배열 매개변수를 사용하는 ClickHouse의 has() 함수도 동작합니다:

var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);

사용자 지정 타입 핸들러

일부 ClickHouse 타입(예: ITuple, BigInteger, ClickHouseDecimal)은 애플리케이션 시작 시 핸들러를 등록해야 합니다:

// ClickHouseDecimal (Decimal64/128/256 컬럼용)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (Int128/Int256/UInt128/UInt256 컬럼용)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (IPv4/IPv6 컬럼용)
SqlMapper.AddTypeHandler(new IpAddressHandler());

type handler 구현 예시는 Dapper 예시를 참조하십시오.

Dapper.Contrib

GetAll<T>()Get<T>(id)는 작동합니다. Insert<T>()는 작동하지 않습니다. SQL Server 구문(SCOPE_IDENTITY, [])을 생성하기 때문입니다. 대신 ClickHouseClient 네이티브 InsertBinaryAsync 메서드를 사용하는 것이 좋습니다.

[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);

속성 이름은 ClickHouse 컬럼 이름과 정확히 일치해야 합니다(대소문자 구분).

제한 사항

항목 상태 세부 정보
result로 사용하는 Tuple 동작함 SqlMapper.TypeHandler<ITuple> 등록이 필요합니다
parameter로 사용하는 Tuple 지원되지 않음 Dapper는 ITuple/Tuple<>DbParameter 값으로 직렬화할 수 없습니다
매개변수로 사용하는 중첩 타입 지원되지 않음 같은 이유로 Dapper는 복합 타입을 매개변수 값으로 허용하지 않습니다
매개변수로 사용하는 Geo 타입 지원되지 않음 Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon
Dapper.Contrib.Insert<T>() 지원되지 않음 SQL Server 전용 구문을 생성합니다
Nothing 타입 지원되지 않음 .NET에서 의미 있게 표현할 방법이 없습니다

Linq2db

이 드라이버는 .NET용 경량 ORM 및 LINQ 프로바이더인 linq2db와 호환됩니다. 자세한 내용은 프로젝트 웹사이트 문서를 참조하십시오.

예시 사용법:

ClickHouse 프로바이더를 사용하여 DataConnection을 생성합니다:

using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);

테이블 매핑은 특성 또는 Fluent API 구성으로 정의할 수 있습니다. 클래스 이름과 속성 이름이 테이블 및 컬럼 이름과 정확히 일치하면 별도의 구성이 필요하지 않습니다:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

쿼리 실행:

await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();

대량 복사:

효율적인 대량 삽입을 위해 BulkCopyAsync를 사용하세요.

await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);

Entity Framework Core

ClickHouse용 공식 Entity Framework Core 프로바이더입니다. C# 클래스를 ClickHouse 테이블에 매핑하고, LINQ로 쿼리하고, SaveChanges를 통해 데이터를 삽입하는 작업을 모두 익숙한 EF Core 패턴으로 수행할 수 있습니다.

설치

dotnet add package ClickHouse.EntityFrameworkCore

.NET 10.0 및 EF Core 10이 필요합니다.

빠른 시작

엔터티와 DbContext를 정의한 다음 LINQ로 쿼리합니다:

using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// 쿼리
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();

지원되는 타입

범주 ClickHouse 타입 CLR 타입
정수 Int8Int64, UInt8UInt64 sbyte, short, int, long, byte, ushort, uint, ulong
큰 정수 Int128, Int256, UInt128, UInt256 BigInteger
부동소수점 Float32, Float64, BFloat16 float, double
Decimal Decimal(P,S), Decimal32(S), Decimal64(S), Decimal128(S) decimal 또는 ClickHouseDecimal
Bool Bool bool
문자열 String, FixedString(N) string
열거형 Enum8(...), Enum16(...) string 또는 C# enum
날짜/시간 Date, Date32, DateTime, DateTime64(P, 'TZ') DateOnly, DateTime
시간 Time, Time64(N) TimeSpan
UUID UUID Guid
네트워크 IPv4, IPv6 IPAddress
배열 Array(T) T[], List<T>, IList<T>, ICollection<T>, IReadOnlyList<T>, IReadOnlyCollection<T>, IEnumerable<T>
Map(K, V) Dictionary<K,V>
튜플 Tuple(T1, ...) Tuple<...> 또는 ValueTuple<...>
Variant Variant(T1, T2, ...) object
Dynamic Dynamic object
JSON Json JsonNode 또는 string
지리 공간 Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon, Geometry Tuple<double,double> 및 해당 배열, Geometry의 경우 object
래퍼 Nullable(T), LowCardinality(T) 자동으로 언래핑됩니다

Decimal128/Decimal256 컬럼의 전체 정밀도가 필요한 경우 decimal 대신 ClickHouse.Driver.NumericsClickHouseDecimal을 사용하세요 — .NET decimal은 유효 자릿수가 28~29자리로 제한됩니다.

지원되는 LINQ 작업

쿼리: Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking

GROUP BY 및 집계: Count, LongCount, Sum, Average, Min, Max와 함께 사용하는 GroupByHAVING(.GroupBy() 뒤의 .Where()), 단일 프로젝션의 여러 집계, 집계 결과에 대한 OrderBy를 포함합니다.

조인: Join(INNER), GroupJoin/SelectMany 패턴(LEFT 및 CROSS). LEFT JOIN은 일치하는 항목이 없는 행에 실제 null을 반환합니다(아래의 LEFT JOIN NULL 의미 체계 참조).

서브쿼리: 상관 Contains / IN, Any / EXISTS, All, 그리고 프로젝션 내 스칼라 서브쿼리.

집합 연산: Concat(→ UNION ALL), Union(→ UNION DISTINCT), Intersect, Except.

인라인 로컬 컬렉션: 메모리 내 컬렉션(int[], List<T> 등)에 대한 조인과 Contains는 일련의 UNION으로 변환됩니다.

문자열 메서드: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat(및 + 연산자).

수학 함수: 표준 MathMathF 메서드는 산술, 로그, 삼각, 유틸리티 함수를 비롯해 해당 ClickHouse 함수로 변환됩니다.

LEFT JOIN NULL 의미 체계

프로바이더는 JOIN 동작에 대한 Entity Framework의 기대에 맞추기 위해 모든 연결 경로에 set_join_use_nulls=1을 자동으로 주입합니다.

ClickHouse 서버 또는 프로필에서 이 설정 변경을 금지하는 경우(예: readonly=1 프로필) 다음과 같이 비활성화하십시오:

optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());

옵트아웃을 활성화하면 LEFT JOIN은 ClickHouse 컬럼의 기본값을 반환하며, EF의 null 기반 탐색 감지 기능이 더 이상 예상대로 작동하지 않습니다. == null 대신 0 / ""에 대한 명시적 비교를 사용하세요.

데이터 삽입

SaveChanges는 드라이버의 네이티브 InsertBinaryAsync API를 사용합니다 — GZip으로 압축된 RowBinary 인코딩을 사용하므로, 매개변수화된 SQL보다 훨씬 효율적입니다:

await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();

엔터티는 저장 후 다른 EF Core 프로바이더와 마찬가지로 Added에서 Unchanged로 전환됩니다.

배치 크기는 설정할 수 있으며(기본값은 1000):

optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));

대량 삽입

높은 처리량의 로드에는 SaveChanges 대신 BulkInsertAsync를 사용하세요. 이는 DbContext의 확장 메서드로, EF Core의 변경 추적기, identity resolution, 상태 관리를 완전히 우회하고 RowBinary 인코딩과 GZip 압축을 사용해 드라이버의 InsertBinaryAsync를 직접 호출합니다.

따라서 삽입 후 엔터티 추적이 필요 없는 대규모 데이터셋을 로드하는 데 적합합니다:

var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);

입력은 어떤 IEnumerable<T>이든 사용할 수 있으며, 모든 엔터티를 메모리에 로드하지 않고 스트리밍 방식으로 처리합니다. 반환값은 삽입된 행 수입니다. 삽입 후 엔터티는 DbContext에 attach되지 않으므로 AddedUnchanged 상태 전환이 발생하지 않습니다.

열거형

ClickHouse Enum8/Enum16 컬럼은 string 속성 또는 C# enum 타입에 매핑할 수 있습니다. C# 열거형을 사용하면 프로바이더가 열거형과 해당 문자열 표현 간에 자동으로 변환합니다:

public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// enum 값으로 쿼리
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();

사용자 지정 타입 변환

EF Core의 ValueConverter 시스템을 사용하면 사용자 지정 타입을 프로바이더가 이미 지원하는 타입에 매핑할 수 있습니다. 프로바이더는 사용자 지정 타입을 직접 처리하지 않으며, EF Core가 그 경계에서 타입을 변환합니다.

속성별 변환:

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// OnModelCreating에서:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");

재사용 가능한 컨버터 클래스:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// 단일 속성에 적용:
.HasConversion<MoneyConverter>()

// 또는 컨벤션을 통해 특정 유형의 모든 속성에 적용:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}

컬럼 타입 어노테이션

string, int, DateTime 등과 같은 스칼라 타입의 경우 프로바이더가 ClickHouse 타입을 자동으로 추론합니다. 매개변수화된 타입과 래퍼의 경우에는 ClickHouse 타입을 명시적으로 지정해야 합니다.

데이터 어노테이션(속성) 사용:

using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}

OnModelCreating에서 Fluent API 사용:

modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});

Array(Nullable(Int32))LowCardinality(Nullable(String))와 같은 중첩된 래퍼 형식이 지원되며, 프로바이더는 모든 중첩 수준에서 NullableLowCardinality를 자동으로 해제합니다.

Variant 및 Dynamic 컬럼

ClickHouse Variant(T1, T2, ...)Dynamic 컬럼은 .NET의 object에 매핑됩니다. object는 자동 형식 유추를 하기에는 너무 범용적이므로, .HasColumnType()을 사용해 저장 형식을 명시적으로 선언해야 합니다:

public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// OnModelCreating에서:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// 또는:
entity.Property(e => e.Payload).HasColumnType("Dynamic");

읽는 시점에 값은 저장된 discriminator에 대응하는 .NET 형식으로 자동으로 역직렬화됩니다(예: string, ulong, ulong[]).

JSON 컬럼

이 프로바이더는 ClickHouse의 Json 컬럼 타입을 지원하며, System.Text.Json.Nodes.JsonNode(프라이머리) 또는 string(자동 ValueConverter를 통해)으로 매핑됩니다:

using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// OnModelCreating에서:
entity.Property(e => e.Data).HasColumnType("Json");

JSON 읽기와 쓰기는 SaveChangesBulkInsertAsync를 통해 모두 수행할 수 있습니다:

ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"

raw JSON 문자열을 선호하는 경우, 속성을 Json 컬럼 유형의 string으로 매핑하세요. 프로바이더에서 ValueConverter를 자동으로 적용합니다:

public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // 원시 JSON 문자열
}

entity.Property(e => e.Data).HasColumnType("Json");

테이블 엔진

ToTable(name, t => ...) Fluent API를 사용해 ClickHouse 테이블 엔진과 엔진별 절을 구성합니다. 엔진을 지정하지 않으면 프로바이더는 엔터티의 기본 키(primary key)에서 도출한 ORDER BY를 사용하며, 기본 테이블 엔진으로 MergeTree를 적용합니다.

modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});

지원되는 엔진 계열:

Engine Fluent method 참고
MergeTree HasMergeTreeEngine() 구성하지 않으면 기본값
ReplacingMergeTree HasReplacingMergeTreeEngine("Version", "IsDeleted") or HasReplacingMergeTreeEngine<T>(e => e.Version) Version / IsDeleted 컬럼은 선택 사항
SummingMergeTree HasSummingMergeTreeEngine(…) or HasSummingMergeTreeEngine<T>(e => new { … }) 합산할 컬럼은 선택 사항
AggregatingMergeTree HasAggregatingMergeTreeEngine()
CollapsingMergeTree HasCollapsingMergeTreeEngine("Sign") or HasCollapsingMergeTreeEngine<T>(e => e.Sign) Sign 컬럼은 Int8이어야 합니다
VersionedCollapsingMergeTree HasVersionedCollapsingMergeTreeEngine("Sign", "Version") or <T>(e => e.Sign, e => e.Version)
GraphiteMergeTree HasGraphiteMergeTreeEngine("config_section")
Log, TinyLog, StripeLog, Memory HasLogEngine(), HasTinyLogEngine(), HasStripeLogEngine(), HasMemoryEngine() ORDER BY / PARTITION BY 미사용

엔진 절: WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. 모두 HasXxxEngine()이 반환하는 엔진 빌더에 연결됩니다.

컬럼 수준 기능: HasCodec, HasTtl, HasComment, HasDefault — 모두 마이그레이션에 포함됩니다.

데이터 스키핑 인덱스HasIndex(...).HasSkippingIndexType(...)를 통해 사용합니다:

modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// 매개변수가 있는 인덱스 (예: bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);

표준(스키핑이 아닌) 인덱스는 ClickHouse에 해당 기능이 없으므로 아무 경고 없이 무시됩니다. 고유 인덱스는 ClickHouse가 고유성을 강제하지 않으므로 예외를 발생시킵니다.

마이그레이션

EF Core의 표준 마이그레이션 워크플로:

dotnet ef migrations add InitialCreate
dotnet ef database update

지원되는 작업:

Operation Emits
CREATE TABLE 엔진 절, ORDER BY, PARTITION BY, SETTINGS, 컬럼 코덱/TTL/주석/기본값을 포함합니다
ALTER TABLE ADD COLUMN
ALTER TABLE DROP COLUMN
ALTER TABLE MODIFY COLUMN 유형 변경과 어노테이션 추가/제거(CODEC, TTL, COMMENT, DEFAULT)를 처리합니다
ALTER TABLE RENAME COLUMN
RENAME TABLE
ALTER TABLE ADD INDEX / DROP INDEX 데이터 스키핑 인덱스만 지원합니다
CREATE DATABASE / DROP DATABASE EnsureCreated / EnsureDeleted 및 마이그레이션을 통해 수행됩니다

마이그레이션 제한 사항

기능 이유
외래 키 ClickHouse는 외래 키를 강제하지 않습니다. 마이그레이션은 AddForeignKey를 거부하며, 모델 유효성 검사기는 모델 빌드 시점에 경고를 출력합니다.
고유 제약 조건 / 고유 인덱스 ClickHouse는 고유성을 강제하지 않습니다. 고유 인덱스는 마이그레이션 시 예외를 발생시킵니다.
서버 생성 값(auto-increment / IDENTITY) ClickHouse에는 이에 해당하는 기능이 없습니다.
Nested(…) 컬럼 매핑된 CLR 유형으로는 아직 지원되지 않습니다.
JSON으로 매핑된 소유 엔터티(.ToJson()) 소유 엔터티에 대한 구조적 JSON 매핑은 아직 구현되지 않았습니다. 대신 Json 컬럼에서 JsonNode / string을 사용하십시오(JSON 컬럼 참조).

마이그레이션 외에도 이 프로바이더는 아직 다음을 지원하지 않습니다:

  • UPDATE / DELETE
  • 트랜잭션: BeginTransaction은 no-op입니다. ClickHouse는 ACID 트랜잭션을 지원하지 않습니다.
  • JSON 경로 쿼리 변환: LINQ의 entity.Data["key"]는 ClickHouse의 data.key SQL 구문으로 변환되지 않습니다. JSON이 아닌 컬럼을 기준으로 필터링하고, 메모리에서 JSON을 확인하십시오.

제한 사항

요소가 8개 이상이고 마지막 위치에 중첩 튜플이 있는 튜플

요소가 7개를 초과하는 C# ValueTuple 타입은 컴파일러가 생성한 중첩 방식을 사용합니다. 즉, 8번째 제네릭 인수(TRest)는 나머지 요소를 담는 또 다른 ValueTuple입니다. 예를 들어 (int, int, int, int, int, int, int, string, string)ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>로 컴파일됩니다.

이로 인해 ClickHouse 컬럼이 8개 요소로 이루어진 튜플이고 마지막 요소 자체도 튜플인 경우 모호성이 생깁니다. 예를 들어 Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))가 이에 해당합니다. 드라이버는 다음 두 경우를 구분할 수 없습니다.

  • 컴파일러가 생성한 TRest 중첩을 사용하는 9개 요소의 평면 튜플
  • 마지막 요소가 중첩된 Tuple(String, String)8개 요소 튜플

두 경우 모두 동일한 .NET Type인 ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>가 생성됩니다.

드라이버는 8번째 인수를 TRest로 처리합니다(즉, 평탄화합니다). 따라서 마지막에 중첩 튜플이 있는 8개 요소 케이스는 잘못 serialize됩니다.

이는 System.TupleValueTuple 모두에 영향을 줍니다. 둘 다 요소가 7개를 초과하면 TRest 중첩을 사용하기 때문입니다. 요소가 7개 이하인 튜플이나 마지막 요소 자체가 튜플이 아닌 튜플은 영향을 받지 않습니다.

우회 방법: 드라이버가 이를 TRest 중첩과 구분할 수 있도록 내부 튜플을 한 겹 더 감싸십시오:

// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))

AggregateFunction 컬럼

AggregateFunction(...) 유형의 컬럼은 직접 쿼리하거나 삽입할 수 없습니다.

삽입하려면:

INSERT INTO t VALUES (uniqState(1));

조회하려면:

SELECT uniqMerge(c) FROM t;

Navigation