-
ClickHouseClient(권장): 하나의 인스턴스를 공유하도록 설계된 스레드 안전 고수준 클라이언트입니다. 쿼리와 대량 삽입을 위한 간편한 비동기 API를 제공하며, 대부분의 애플리케이션에 적합합니다. -
ADO.NET (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): 표준 .NET 데이터베이스 추상화입니다. ORM 통합(Dapper, Linq2db) 또는 ADO.NET 호환성이 필요할 때 사용합니다.ClickHouseBulkCopy는 ADO.NET 연결을 통해 데이터를 효율적으로 삽입하는 도우미 클래스입니다.ClickHouseBulkCopy는 사용 중단 예정이며 향후 릴리스에서 제거될 예정입니다. 대신ClickHouseClient.InsertBinaryAsync를 사용하세요.
마이그레이션 가이드
.csproj파일에서 패키지 이름을 새 이름인ClickHouse.Driver로 바꾸고, 버전을 NuGet에 게시된 최신 버전으로 업데이트하세요.- 코드베이스에서
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에서 패키지를 설치하세요.빠른 시작
구성
ClickHouse 연결은 두 가지 방법으로 구성할 수 있습니다.- 연결 문자열: 호스트, 인증 자격 증명 및 기타 연결 옵션을 지정하는 키/값 쌍을 세미콜론으로 구분한 문자열입니다.
ClickHouseClientSettings객체: 설정 파일에서 불러오거나 코드에서 설정할 수 있는 강력한 타입의 구성 객체입니다.
연결 설정
데이터 포맷 및 직렬화
세션 관리
UseSession 플래그를 활성화하면 서버 세션이 유지되어 SET SQL 문과 임시 테이블을 사용할 수 있습니다. 세션은 60초 동안 활동이 없으면 재설정됩니다(기본 타임아웃). ClickHouse SQL 문이나 서버 구성에서 세션 설정을 지정하면 세션 유지 시간을 연장할 수 있습니다.ClickHouseConnection 클래스는 일반적으로 병렬 실행을 지원하므로 여러 스레드에서 쿼리를 동시에 실행할 수 있습니다. 하지만 UseSession 플래그를 활성화하면 한 연결에서 동시에 실행할 수 있는 쿼리가 1개로 제한됩니다(서버 측 제한 사항입니다).보안
HTTP 클라이언트 구성
로깅 및 디버깅
사용자 지정 설정 및 역할
연결 문자열로 사용자 지정 설정을 지정할 때는
set_ 접두사를 사용하세요(예: “set_max_threads=4”). ClickHouseClientSettings 객체를 사용할 때는 set_ 접두사를 사용하지 마세요.사용 가능한 설정의 전체 목록은 여기를 참조하세요.연결 문자열 예시
기본 연결
사용자 정의 ClickHouse 설정을 적용한 경우
QueryOptions
QueryOptions를 사용하면 클라이언트 수준 설정을 쿼리별로 재정의할 수 있습니다. 모든 속성은 선택 사항이며, 지정한 경우에만 클라이언트 기본값을 재정의합니다.
예시:
InsertOptions
InsertOptions는 QueryOptions를 확장하여 InsertBinaryAsync를 통한 대량 삽입 작업에 필요한 설정을 제공합니다.
QueryOptions의 모든 속성을 InsertOptions에서도 사용할 수 있습니다.
예시:
스키마 확인 쿼리 생략하기
기본적으로InsertBinaryAsync는 삽입할 때마다 컬럼 타입을 확인하기 위해 먼저 SELECT ... WHERE 1=0 쿼리를 보냅니다. 고처리량 환경에서는 다음 2가지 방법으로 이 추가 작업을 없앨 수 있습니다.
방법 1: 컬럼 타입을 명시적으로 제공하기
컴파일 시점에 테이블 스키마를 알고 있다면 ColumnTypes를 통해 직접 전달하세요. 이렇게 하면 스키마 확인 쿼리가 전혀 전송되지 않습니다.
UseSchemaCache = true로 설정하면 스키마를 한 번만 조회한 뒤 같은 ClickHouseClient 인스턴스에서 후속 삽입에 재사용할 수 있습니다.
ColumnTypes가UseSchemaCache보다 우선합니다. 둘 다 설정하면 명시적으로 지정한 타입을 사용합니다.- 스키마 캐시는
ALTER TABLE로 인한 변경 사항을 감지하지 못합니다. 테이블 스키마를 수정했다면 새ClickHouseClient를 생성하거나 해당 테이블에UseSchemaCache를 사용하지 마세요. - 캐시는
ClickHouseClient인스턴스별로 관리되며 (데이터베이스, 테이블)을 키로 사용합니다. 같은 테이블에서 서로 다른 컬럼 부분 집합을 사용해도 캐시된 스키마 하나를 공유합니다.
ClickHouseClient
ClickHouseClient는 ClickHouse와 상호 작용하는 데 권장되는 API입니다. 여러 스레드에서 안전하게 사용할 수 있고, 단일 인스턴스로 사용하도록 설계되었으며, 내부적으로 HTTP 연결 풀링을 관리합니다.
클라이언트 생성
연결 문자열이나ClickHouseClientSettings 객체를 사용해 ClickHouseClient를 생성합니다. 사용 가능한 옵션은 구성 섹션을 참조하세요.
ClickHouse Cloud 서비스의 연결 정보는 ClickHouse Cloud 콘솔에서 확인할 수 있습니다.
서비스를 선택한 후 Connect를 클릭하세요.
**C#**을 선택하세요. 연결 정보가 아래에 표시됩니다.
자가 관리형 ClickHouse를 사용한다면 ClickHouse 관리자가 연결 정보를 설정합니다.
연결 문자열을 사용하는 방법:
ClickHouseClientSettings를 사용합니다:
IHttpClientFactory를 사용하세요.
ClickHouseClient는 애플리케이션 전반에서 오래 유지하며 공유하도록 설계되었습니다. 한 번만 생성해(일반적으로 싱글턴으로) 모든 데이터베이스 작업에 재사용하십시오. HTTP 연결 풀링은 클라이언트가 내부적으로 관리합니다.쿼리 실행
결과를 반환하지 않는 SQL 문을 실행할 때는ExecuteNonQueryAsync를 사용하세요.
ExecuteScalarAsync를 사용하세요:
데이터 삽입
매개변수화된 삽입
ExecuteNonQueryAsync와 매개변수화된 쿼리를 사용하여 데이터를 삽입합니다. SQL에서 매개변수 타입을 {name:Type} 구문으로 지정해야 합니다.
대량 삽입
많은 행을 효율적으로 삽입하려면InsertBinaryAsync를 사용하세요. ClickHouse의 네이티브 행 바이너리 형식으로 데이터를 스트리밍하며, 병렬 배치 업로드를 지원하고, 매개변수화된 쿼리에서 발생할 수 있는 “URL too long” 오류를 방지합니다.
InsertOptions로 배칭과 병렬 처리 수준을 설정하세요.
- 클라이언트는 삽입하기 전에
SELECT * FROM <table> WHERE 1=0쿼리로 테이블 구조를 자동으로 조회합니다. 입력하는 값은 대상 컬럼 타입과 일치해야 합니다. 이 쿼리를 생략하려면InsertOptions.ColumnTypes또는InsertOptions.UseSchemaCache를 사용하세요. MaxDegreeOfParallelism > 1이면 배치가 병렬로 업로드됩니다. 세션과 병렬 삽입은 함께 사용할 수 없으므로 세션을 비활성화하거나MaxDegreeOfParallelism = 1로 설정하세요.- 지정하지 않은 컬럼에 서버가 DEFAULT 값을 적용하게 하려면
InsertOptions.Format에RowBinaryFormat.RowBinaryWithDefaults를 사용하세요.
POCO 삽입
object[] 배열을 구성하는 대신 강한 타입의 POCO 객체를 직접 삽입할 수 있습니다. 타입을 한 번 등록한 후 IEnumerable<T>를 전달하세요.
매핑된 모든 속성에
Type을 명시적으로 지정하면 스키마 확인 쿼리를 실행하지 않습니다. 일부 속성에만 타입을 명시적으로 지정하면 드라이버가 전체 컬럼을 대상으로 스키마 확인 쿼리를 실행하는 방식으로 폴백합니다.
InsertBinaryAsync<T>는 object[] 오버로드와 동일한 InsertOptions(배칭, 병렬 처리, 스키마 캐싱)를 지원합니다.
object[] 오버로드와 달리 InsertBinaryAsync<T>에는 컬럼 목록을 명시적으로 전달할 수 없습니다. 컬럼은 등록된 타입에서 매핑된 속성에 따라 결정됩니다. 삽입할 컬럼을 제어하려면 [ClickHouseNotMapped]로 속성을 제외하거나 [ClickHouseColumn(Name = "...")]로 컬럼명을 변경하세요.InsertOptions에 ColumnTypes를 설정하면 POCO 특성에 지정된 값보다 우선 적용됩니다.스키마 진화
POCO 타입을 등록한 뒤 target table에 컬럼이 추가되어도 POCO 삽입은 문제없이 작동합니다. 드라이버가 POCO에 매핑된 컬럼만 삽입하므로,DEFAULT(또는 다른 기본값 표현식)가 지정된 새 컬럼은 서버가 자동으로 값을 채웁니다. 코드를 변경하거나 타입을 다시 등록할 필요가 없습니다.
삽입 쿼리 배치
바이너리 삽입 시INSERT INTO ... FORMAT ... SQL 문은 행 데이터보다 앞선 요청 body의 첫 줄에 기록됩니다. body는 기본적으로 압축되므로 URL만 확인하는 라우팅 및 로깅에서는 SQL 문을 볼 수 없습니다. SQL 문을 query URL 매개변수로 전송하고 body에는 행 데이터만 담으려면 InsertOptions.QueryPlacement를 InsertQueryPlacement.Url로 설정하세요.
query 매개변수를 기준으로 요청을 라우팅하거나 검사하는 경우, 또는 액세스 로그와 관측성 도구에 SQL 문을 기록하려는 경우 사용합니다. SQL 문이 URL 길이에 포함되므로 이 방식은 명시적으로 활성화해야 합니다. 실제 적용되는 길이 제한은 .NET 런타임, 중간 구성 요소, 서버에서 정한 제한 중 가장 낮은 값입니다. .NET 6부터 .NET 9까지 System.Uri는 인코딩된 전체 요청 URI를 65,519자로 제한합니다. 이 제한을 초과하면 드라이버는 InsertQueryPlacement.Body를 사용하도록 안내하는 InvalidOperationException을 발생시킵니다. ClickHouse의 http_max_uri_size 기본값은 1 MiB이지만, 중간 구성 요소에서 더 낮은 제한을 적용할 수 있습니다. body 모드에서는 SQL 문과 행에 이러한 URL 길이 제한이 적용되지 않습니다. 다만 다른 요청 옵션은 여전히 URL에 포함될 수 있습니다.
이 설정은 Compressor와 무관합니다. 두 모드 모두 body는 동일한 방식으로 인코딩됩니다.
데이터 읽기
SELECT 쿼리는ExecuteReaderAsync로 실행하세요. 반환된 ClickHouseDataReader에서 GetInt64(), GetString(), GetFieldValue<T>() 등의 메서드를 사용하면 결과 컬럼의 데이터에 해당 타입으로 액세스할 수 있습니다.
다음 행으로 이동하려면 Read()를 호출하세요. 더 이상 읽을 행이 없으면 false를 반환합니다. 컬럼은 0부터 시작하는 인덱스나 컬럼명으로 참조할 수 있습니다.
POCO 읽기
컬럼을 인덱스나 이름으로 읽는 대신 쿼리 결과를 직접 정의한 클래스로 스트리밍할 수 있습니다. 클라이언트에 타입을 한 번 등록한 후QueryAsync<T>를 사용하세요.
RegisterPocoType<T>()은 삽입 및 읽기 매핑을 모두 설정하고 두 매핑을 미리 검사합니다. RegisterBinaryInsertType<T>()은 변경 없이 이전 버전과의 호환성을 위해 삽입 전용으로 유지됩니다.
등록할 유형은 다음 조건을 충족해야 합니다.
- 매개변수가 없는 public 생성자가 있어야 합니다.
- public이며
init이 아닌 setter가 있는 public 속성이 하나 이상 있어야 합니다.required속성도 지원됩니다.
InvalidOperationException이 발생합니다. 따라서 object 속성에는 어떤 컬럼이든 매칭할 수 있습니다.
QueryAsync<T>는 다음 컬럼의 값을 각각 대응하는 속성으로 직접 읽어 들입니다.
컬럼이
Nullable(...)인지와 관계없이 모든 행에서 해당 속성 타입의 널 허용 형태(long?, DateOnly? 등)도 사용할 수 있습니다.
Nullable(T) 컬럼에 널을 허용하지 않는 값 타입 속성을 지정해도
등록은 가능하지만, NULL 값이 들어오면 예외가 발생합니다.
LowCardinality(T), SimpleAggregateFunction(f, T), Object(T)와 같은 래퍼는 T와 동일하게 매핑됩니다.
복합 컬럼도 지원하며, 읽기 타입 참고 자료에 명시된
프레임워크 타입을 사용합니다. Array(T)는 T[]로, Tuple(...)은
System.Tuple<...>로, Nested(...)은 Tuple<...>[]로, JSON은 JsonObject로(JsonReadMode=String에서는 string으로),
Variant/Dynamic은 object로 매핑됩니다.
Map(K, V) 컬럼은 예외적으로 처리됩니다. List<KeyValuePair<K, V>> 또는 KeyValuePair<K, V>[]
속성은 어느 MapReadMode에서든 박싱 없이 읽을 수 있으며, wire에서의 순서와 중복된 키를 유지합니다.
Dictionary<K, V> 속성은 기본 모드에서만 사용할 수 있습니다.
키와 값의 타입은 정확히 일치해야 하므로
Map(String, Nullable(Int32))에는 KeyValuePair<string, int?>가 필요합니다.
컬럼에 사용할 수 있는 속성 타입이 둘 이상이면(DateTime 컬럼의 DateTime,
DateTimeOffset 또는 DateOnly, String 컬럼의 string 또는 byte[]) 선언된 속성 타입에 따라 표현 형식이 결정됩니다. 이러한 대체 표현은
POCO 경로에서만 지원하므로 QueryAsync<T>에서는 사용할 수 있지만 MapTo<T>에서는 사용할 수 없습니다.
리더를 수동으로 순회할 때 ClickHouseDataReader.MapTo<T>()를 사용하면 리더를 다음 행으로 이동하지 않고 현재 행을 등록된 POCO로 구체화할 수 있습니다.
MapTo<T>를 사용하세요. 이 메서드는 리더에서 박싱된 값을 가져와 행을 읽으므로 앞서 설명한 다른 속성 타입을 지원하지 않으며,
QueryAsync<T>보다 메모리를 더 많이 할당합니다. 행만 필요하다면
QueryAsync<T>를 사용하세요. 성능 수치는
머티리얼라이즈 경로 선택을 참조하세요.
클라이언트 수준 또는 쿼리별 읽기 값 컨버터는 두 경로에 모두 적용되며
박싱 없는 읽기를 비활성화하지 않습니다. 드라이버는 각 컬럼을 읽은 방식에 맞는 오버로드를 사용해 변환합니다.
박싱 없는 컬럼에는 타입이 지정된 ConvertValue<T>를, 복합 컬럼에는 박싱된 ConvertValue를 사용합니다. 두 오버로드를
일관되게 구현하십시오. 그렇지 않으면 동일한 컬럼도 경로에 따라 다른 결과가 나옵니다.
LoggerFactory가 구성되어 있으면 RegisterPocoType<T>()과 RegisterBinaryInsertType<T>()은 각 속성이 매핑된 컬럼과 매핑에서 제외된 속성 및 그 이유를 보여 주는 Debug 수준의 로그(범주 ClickHouse.Driver.Client)를 출력합니다. 로깅 및 진단을 참조하세요.
SQL 매개변수
ClickHouse에서는 SQL 쿼리의 쿼리 매개변수를{parameter_name:DataType} 형식으로 지정합니다.
예시:
SQL ‘bind’ 매개변수는 HTTP URI 쿼리 매개변수로 전달되므로 지나치게 많이 사용하면 “URL이 너무 깁니다” 예외가 발생할 수 있습니다. 이 제한을 피하려면 대량의 데이터를 삽입할 때
InsertBinaryAsync를 사용하세요.ADO 스타일 @name 플레이스홀더
드라이버는 Dapper 같은 ORM이 생성하는 @name 플레이스홀더도 지원합니다. 이는
클라이언트 측 편의 기능입니다. 요청을 보내기 전에 각 플레이스홀더를
{name:ResolvedType}으로 재작성하므로 서버에는 @가 전달되지 않습니다. 유형을 결정하는 방법은
유형 결정을 참조하세요. 가능하면 명시적인
{name:Type} 형식을 사용하세요.
일치하는 매개변수가 없는 @name은 수정하지 않고 서버로 전달해 서버에서 거부하도록 합니다. 매개변수 이름은
대소문자를 구분하므로 @ID는 이름이 id인 매개변수에 바인딩되지 않습니다.
재작성을 비활성화하려면 드라이버를 처음 사용하기 전에
ClickHouse.Driver.DisableReplacingParameters AppContext 스위치를
설정하세요. 텍스트 재작성만 중단되며 매개변수는 계속 전송되므로
네이티브 {name:Type} 구문으로 작성한 쿼리는 계속 작동합니다.식별자 매개변수
Identifier 매개변수 유형을 사용하면 따옴표로 묶은 문자열 리터럴 대신 데이터베이스명, 테이블명 또는 컬럼명을 안전하게 바인딩할 수 있습니다. SQL에서 {name:Identifier} 구문을 사용하거나 ClickHouseDbParameter.ClickHouseType = "Identifier"로 설정하세요.
쿼리 ID
모든 쿼리에는 고유한query_id가 할당됩니다. 이 ID로 system.query_log 테이블에서 데이터를 조회하거나 장기 실행 쿼리를 취소할 수 있습니다. QueryOptions를 사용하면 쿼리 ID를 직접 지정할 수 있습니다:
사용자 지정 매개변수 유형 매핑
@ 스타일 매개변수(예: WHERE id = @id)를 사용하면 드라이버가 .NET 값의 형식을 바탕으로 ClickHouse 타입을 자동으로 추론합니다. 예를 들어 int는 Int32에 매핑됩니다.
이 기본 동작을 변경하려면 ClickHouseClientSettings의 ParameterTypeResolver를 설정하세요. 개별 매개변수마다 ClickHouseType을 설정하지 않고 모든 DateTime 매개변수에 밀리초 정밀도의 DateTime64(3)을 사용하거나 모든 소수 값에 특정 소수점 이하 자릿수를 적용할 때 유용합니다.
단순 유형 매핑에 DictionaryParameterTypeResolver 사용:
IParameterTypeResolver:
매개변수 값을 고려하거나 이름을 기준으로 유형을 결정하려면 IParameterTypeResolver 인터페이스를 직접 구현하세요. 기본 유형 추론으로 넘어가려면 null을 반환하세요.
QueryOptions.ParameterTypeResolver를 사용하면 개별 쿼리에도 리졸버를 설정할 수 있습니다. 이 값을 설정하면 클라이언트 수준의 리졸버보다 우선 적용됩니다.
유형 결정 우선순위:
리졸버는 유형 결정 우선순위 체계의 한 단계입니다. 우선순위가 높은 순서대로 나열하면 다음과 같습니다.
- 매개변수에 명시적으로 설정한
ClickHouseType - 쿼리의
{name:Type}구문에 지정된 SQL 유형 힌트 IParameterTypeResolver(QueryOptions.ParameterTypeResolver를 우선 사용하고, 설정되지 않았다면ClickHouseClientSettings.ParameterTypeResolver로 폴백)- 기본 제공 유형 추론 (
TypeConverter.ToClickHouseType)
ClickHouseConnection 경로에서도 작동합니다. 클라이언트에서 생성된 연결은 클라이언트의 설정을 상속합니다.
사용자 지정 매개변수 값 포맷팅
IParameterFormatter는 매개변수 값을 직렬화하는 방식을 결정하는 후크입니다. 기본 제공 포맷팅(예: DateTime 정밀도, 소수점 구분자에 적용되는 문화권 설정, 문자열 이스케이프, 숫자 표현)이 스키마나 후속 도구에서 요구하는 방식과 다를 때 사용하세요.
모든 매개변수화된 쿼리에 포맷터를 적용하려면 ClickHouseClientSettings에서 ParameterFormatter를 설정하세요. 포맷터는 값, 해석된 ClickHouse 타입 이름, 매개변수 이름을 받아 서버로 전송할 문자열 표현을 반환합니다. 기본 포맷터에 처리를 맡기려면 null을 반환하세요.
CLR 타입별로 간단한 포맷팅을 적용할 때 DictionaryParameterFormatter 사용:
IParameterFormatter:
QueryOptions.ParameterFormatter를 사용하면 쿼리마다 포맷터를 설정할 수도 있습니다. 설정한 경우 클라이언트 수준의 포맷터보다 우선 적용됩니다.
복합 값:
포맷터는 최상위 컬렉션 매개변수뿐 아니라 복합 값(Array, Tuple, Map, Nullable, LowCardinality, Variant)의 모든 원소에도 적용됩니다. 예를 들어 typeof(int) 매핑은 Array(Int32)에 포함된 각 Int32 원소를 개별적으로 포맷합니다.
복합 값에서 작은따옴표로 감싸기:
복합 리터럴에 포함된 문자열 계열 ClickHouse 타입(String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID)에 대해 드라이버는 포맷터의 출력값을 작은따옴표로 감싸지만, 그 내용은 이스케이프하지 않습니다. 반환된 문자열에 이스케이프되지 않은 작은따옴표나 백슬래시가 있으면 복합 리터럴의 형식이 잘못되어 서버가 쿼리를 거부합니다.
복합 값에 포함되지 않은 최상위 문자열 매개변수는 따옴표로 감싸지 않고 그대로 사용하므로 이스케이프할 필요가 없습니다.
포맷터 우선순위:
IParameterFormatter(QueryOptions.ParameterFormatter에서 가져오며, 설정되지 않은 경우ClickHouseClientSettings.ParameterFormatter로 폴백합니다).null이 아닌 값을 반환하면 해당 값을 사용합니다.HttpParameterFormatter에 내장된 타입별 포맷팅.
null 또는 DBNull 값에는 포맷터를 적용하지 않으며, 이러한 값은 항상 ClickHouse의 null 표시값(\N)으로 직렬화됩니다.
사용자 지정 읽기 값 변환
IReadValueConverter를 사용하면 역직렬화 후 데이터 리더가 반환하는 값의 CLR 유형은 그대로 유지하면서 값을 변환할 수 있습니다. 시간대가 없는 DateTime 컬럼의 DateTime.Kind를 Utc로 설정하거나, 문자열의 공백을 제거하거나 정규화하거나, JSON 컬럼의 값이 애플리케이션 코드에 전달되기 전에 후처리할 때 유용합니다.
모든 읽기에 컨버터를 적용하려면 ClickHouseClientSettings의 ReadValueConverter를 설정하세요. 컨버터는 박싱된 값(GetValue)을 가져오는 경로와 제네릭 값(GetFieldValue<T>)을 가져오는 경로 모두에서 각 행의 컬럼마다 한 번씩 호출됩니다. 컨버터를 설정하지 않으면 추가 처리 비용 없이 리더가 값을 직접 반환합니다.
DictionaryReadValueConverter를 사용한 간단한 CLR 유형별 변환:
For<T>에 등록되지 않은 값은 변환되지 않고 그대로 전달됩니다. CLR 타입이 정확히 일치하는지에 따라 변환기가 선택되므로, 리더가 실제로 생성하는 타입을 등록하세요(예: JsonReadMode.Binary 모드의 JSON 컬럼에는 For<JsonObject>를 등록).
고급 시나리오를 위한 사용자 정의 IReadValueConverter:
ClickHouse 측 타입 문자열에 따라 변환기를 선택해야 한다면(예: CLR 타입은 같지만 ClickHouse 타입은 다른 DateTime과 DateTime('UTC')를 구분하려면) IReadValueConverter를 직접 구현하세요.
GetFieldType, GetSchemaTable)는 컨버터를 거치지 않으므로 실제 반환 값과 일치해야 합니다.
QueryOptions.ReadValueConverter로 쿼리별 컨버터를 설정할 수도 있습니다. 설정하면 클라이언트 수준의 컨버터보다 우선 적용됩니다.
호출 범위:
컨버터는 역직렬화된 셀 값 전체를 대상으로 컬럼마다 한 번 호출되며, 복합 컨테이너 내부에는 재귀적으로 적용되지 않습니다. Array(Int32) 컬럼에서는 int[]가, Tuple(Int32, String) 컬럼에서는 ITuple이 전달됩니다.
실행되는 오버로드:
드라이버가 호출하는 오버로드는 호출자가 컬럼을 읽는 방식에 따라 달라지므로 두 오버로드의 동작이 일치해야
합니다.
ConvertValue<T>— 타입별 접근 메서드인GetByte,GetSByte,GetInt16/32/64,GetUInt16/32/64,GetFloat,GetDouble,GetGuid,GetDateTime,GetIPAddress,GetBigInteger,GetFieldValue<T>, 그리고 POCO 읽기 경로에서 박싱 없이 읽는 모든 컬럼에 사용됩니다.ConvertValue(박싱된 값) —GetValue,GetValues, 인덱서,GetChar,GetTuple, 그리고GetBoolean,GetDecimal,GetString의 값 변환 경로에 사용됩니다.
IsDBNull은 컨버터를 호출하지 않습니다. null 플래그를 직접 읽으므로 컨버터로는
값의 null 여부를 바꿀 수 없습니다. TryGetEnumOrdinal도 컨버터를 거치지 않습니다. 자세한 내용은
enum의 서수 값 읽기를 참조하세요.
컨버터는 ADO.NET ClickHouseConnection 경로에서도 작동합니다. 클라이언트에서 생성한 연결에는 해당 설정이 상속됩니다.
원시 스트리밍
ExecuteRawResultAsync를 사용하면 데이터 리더를 거치지 않고 쿼리 결과를 지정한 포맷으로 직접 스트리밍할 수 있습니다. 데이터를 파일로 내보내거나 다른 시스템에 그대로 전달할 때 유용합니다:
JSONEachRow, CSV, TSV, Parquet, Native. 전체 옵션은 포맷 문서를 참고하세요.
쿼리별 전송 압축
기본적으로 클라이언트는Compression=true(연결 문자열의 기본값)일 때 zstd, lz4, gzip, deflate를 협상하고 스트림을 자동으로 디코딩합니다.
원시 데이터 내보내기(예: Parquet, Arrow, Native)에서는 연결 전체의 설정을 바꾸지 않고 CPU 사용량과 대역폭 사이의 절충점을 조정하도록 다른 코덱(예: zstd 또는 lz4)을 협상할 수 있습니다. QueryOptions.AcceptEncoding과 ClickHouseCommand.AcceptEncoding은 단일 요청의 HTTP Accept-Encoding 헤더를 설정해 기존 기본값을 대체하고, URL에 enable_http_compression=1을 강제로 지정합니다. ClickHouse가 Accept-Encoding을 적용하려면 이 설정이 필요합니다.
HttpClient 구성
별도로 구성할 필요가 없습니다. 드라이버가 생성하는HttpClient는 AutomaticDecompression을 DecompressionMethods.None으로 유지하고, 응답은 드라이버가 직접 디코딩합니다. 따라서 Content-Encoding이 모르는 사이에 제거되지 않으며, 원본 body를 서버가 보낸 그대로 받을 수 있습니다.
오류 body
서버가 4xx/5xx 오류로 응답할 때enable_http_compression=1이 설정되어 있으면, 정상 응답에 사용할 코덱과 동일한 코덱으로 오류 body를 압축합니다. 드라이버는 지원하는 모든 코덱(lz4, zstd, gzip, deflate, br/brotli)으로 압축된 오류 body를 디코딩하므로 ClickHouseServerException의 메시지를 읽을 수 있습니다. 지원하지 않는 코덱(snappy 등)으로 압축된 경우에는 코덱 이름과 원본 오류 메시지를 확인할 수 있는 system.query_log를 안내하는 플레이스홀더를 반환합니다.
응답 압축 해제
Accept-Encoding은 서버에 응답 압축을 요청할 뿐, 압축을 해제하지는 않습니다. 드라이버가 응답의 Content-Encoding을 확인해 직접 압축을 해제하므로, 일반적인 읽기 API(ExecuteReaderAsync, ExecuteScalarAsync, ExecuteNonQueryAsync, QueryAsync<T>, Dapper, EF Core, linq2db)는 별도 설정 없이 압축된 응답을 처리할 수 있습니다. lz4, zstd, gzip, deflate, br의 압축 해제를 지원하지만 snappy는 지원하지 않습니다.
기본적으로 드라이버는 **zstd, lz4, gzip, deflate**를 사용할 수 있다고 서버에 알리며, ClickHouse는 zstd로 압축해 응답합니다. 다른 방식을 사용하려면 클라이언트 전체에 적용할 Accept-Encoding을 직접 설정하세요.
ClickHouseClientSettings를 직접 사용하지 않는 ORM 사용자는 연결 문자열에 지정할 수 있습니다:
enable_http_compression=1도 강제로 추가됩니다. ClickHouse는 이 설정이 있어야 헤더를 처리합니다. 코덱을 명시하면 압축을 요청한 것으로 간주하므로 UseCompression이 false여도 마찬가지입니다. 코덱을 지정하지 않고 UseCompression=false로 설정하면 Accept-Encoding을 전송하지 않습니다.
Accept-Encoding은 4곳에서 설정할 수 있습니다. 다음 순서에서 코덱을 지정한 첫 번째 값이 적용됩니다.
QueryOptions.AcceptEncoding(또는ClickHouseCommand.AcceptEncoding)- 쿼리의
CustomHeaders["Accept-Encoding"] - 클라이언트의
CustomHeaders["Accept-Encoding"] ClickHouseClientSettings.AcceptEncoding또는AcceptEncoding연결 문자열 키워드
identity를 사용하세요.
코덱은 클라이언트가 아닌 서버가 선택합니다. ClickHouse는 자체적으로 정해진 우선순위인 zstd > br > lz4 > snappy > gzip > deflate에 따라 Accept-Encoding의 토큰을 확인하며, 토큰을 나열한 순서와 q값은 모두 무시합니다. 따라서 이 헤더는 특정 코덱을 요구하는 것이 아니라 사용할 수 있는 코덱을 알리는 수단입니다. 선택에 영향을 주려면 원하지 않는 토큰을 제외해야 합니다. 기본 목록에는 zstd가 포함되므로 기본 설정의 쿼리에는 zstd로 압축된 응답이 반환되며, 나머지 토큰은 폴백으로 작동합니다. br은 디코딩할 수 있지만 기본 목록에는 포함되지 않습니다.
코덱별 payload 크기, 서버 CPU 사용량, 클라이언트 CPU 사용량은 데이터, 연결 속도, 서버의 http_zlib_compression_level(제공 시 기본값: 3)에 따라 달라집니다. 압축 조정을 참고하세요.
http_zlib_compression_level. 이 설정은 모든 HTTP 코덱에 적용되며 기본값은 3입니다. 데이터, 연결 속도, CPU 사용량에 맞춰 조정해야 합니다.- 빠른 연결을 사용하는 CPU 바운드 클라이언트. 드라이버는 호출 스레드에서 응답 body를 디코딩하므로 네트워크가 병목이 아니라면 클라이언트 측 디코딩 속도가 성능을 제한할 수 있습니다.
Content-Encoding이 없거나 identity이면 그대로 두고, 지원되는 코덱을 지정하면 디코딩하며, 그 외의 값이면 해당 값을 명시한 오류가 발생합니다. 이중 디코딩의 위험은 없습니다. 호출자가 제공한 핸들러의 AutomaticDecompression이 이미 body를 디코딩했다면 Content-Encoding도 제거하므로, 드라이버는 평문을 그대로 둡니다.
원시 결과는 코덱을 요청하지 않습니다. ExecuteRawResultAsync(및 공개 메서드인 PostStreamAsync / InsertRawStreamAsync)는 body를 서버에서 받은 그대로 전달합니다. 따라서 코덱을 직접 지정하지 않으면 어떤 코덱도 요청하지 않습니다. 드라이버가 이러한 body를 디코딩하지 않으므로, 코덱을 요청하면 내보낸 결과가 사용자도 모르는 사이 압축 파일이 될 수 있습니다. 따라서 HttpClient의 구성 방식과 관계없이 규칙은 간단합니다. body는 서버가 보낸 그대로 전달되며, 코덱을 요청하지 않으면 서버는 평문을 보냅니다. 압축된 바이트를 내보내려면 클라이언트 전체 또는 쿼리별로 코덱을 요청하면 됩니다.
명시적으로 설정한 AcceptEncoding은 클라이언트 전체에 설정했든 쿼리별로 설정했든 원시 요청에도 적용됩니다. 결과를 디코딩하려면 ClickHouseRawResult.ReadDecompressedStreamAsync()를 사용하면 됩니다. ReadAsStreamAsync, ReadAsByteArrayAsync, ReadAsStringAsync, CopyToAsync는 항상 수신한 바이트를 그대로 반환합니다.
leaveOpen으로 생성된 디코더가 반환되므로 디코더를 해제해도 응답은 그대로 유지됩니다. 압축되지 않은 경우에는 HTTP 콘텐츠 스트림 자체가 반환되므로 이를 해제하면 body가 닫힙니다. 어느 경우든 응답은 ClickHouseRawResult가 소유합니다. 스트림을 해제한 후에는 ClickHouseRawResult의 다른 읽기 멤버를 호출하지 마세요. ClickHouseRawResult는 항상 해제해야 하며, 이것만으로 응답과 여기서 추가된 디코더가 모두 해제됩니다(디코더는 풀에서 가져온 버퍼를 사용합니다). 따라서 위의 await using은 선택 사항이며, 그대로 사용해도 안전합니다. 순차적으로 반복 호출하면 동일한 스트림이 반환됩니다. 이 유형은 동시에 사용해도 안전하지 않습니다.
실행 가능한 예시는 Select_007_ResponseCompression.cs를 참조하세요.
삽입(요청) 압축
삽입 시 기본 코덱은 Zstd입니다.InsertOptions.Compressor는 기본적으로 ZstdCompressor.Default로 설정되며,
이는 압축 레벨 3의 zstd를 사용합니다. 코덱을 변경하려면 다른 압축기로 설정하고, body를 압축하지 않고 전송하려면
null로 설정하십시오.
Default 인스턴스와 압축 수준 및 쓰기 버퍼 크기를 인수로 받는
생성자가 있습니다:
압축기 인스턴스를 공유하세요. 각
Default는 공유 인스턴스 하나를 가리키며, 네 가지 압축기 모두
여러 스레드에서 동시에 안전하게 사용할 수 있습니다. 삽입 작업 하나가 배치마다 압축기 하나를 사용하므로
InsertOptions.MaxDegreeOfParallelism이 1보다 크면 여러 스레드에서 동시에 사용하게 됩니다.
이 압축기 중 IDisposable을 구현하는 것은 없습니다. Default와 마찬가지로
인스턴스를 직접 한 번 생성해 재사용하세요.IClickHouseCompressor는 공개되어 있으며, 이를 구현할 때는 멤버 2개만 제공하면 됩니다.
Content-Encoding을 허용해야 합니다. 나머지 멤버인
Decompress, MethodByte, MaxEncodedLength, Encode, Decode에는
NotSupportedException을 발생시키는 기본 구현이 있으므로 코덱에 필요한 멤버만 재정의하십시오.
요청 압축뿐 아니라 응답 body의 디코딩도 지원하려면 Decompress를 구현하십시오. body가 손상되었거나 포맷이 잘못된 경우에는 이 메서드가 반환한 스트림에서
InvalidDataException이 발생하도록 하십시오.
InsertOptions.Compressor는 바이너리 삽입에만 적용됩니다. 드라이버의 다른 요청 body는 별도의 규칙에 따라 압축되며, InsertOptions.Compressor를 거치지 않습니다.
- 모든 SQL 텍스트 요청(
ExecuteReaderAsync,ExecuteScalarAsync,ExecuteNonQueryAsync,QueryAsync<T>,ExecuteRawResultAsync, ADO.NET 계층)은UseCompression이true이면, 즉 기본 설정에서는 SQL 문을Content-Encoding: gzip으로 전송합니다. 이때 사용하는 코덱은 변경할 수 없습니다.AcceptEncoding은 응답에만 적용되므로 요청에는 gzip을 사용하거나 압축하지 않는 두 가지 방법만 있습니다.Compression=false이면 SQL 문을 압축하지 않고 전송합니다. SQL 문은 크기가 작아 보통 신경 쓸 필요가 없지만, 프록시에서 요청을 살펴보거나 패킷을 캡처할 때는 알아두면 유용합니다. - 멀티파트 body—매개변수를 폼 데이터로 전송하는 쿼리(
UseFormDataParameters=true)—는UseCompression설정과 관계없이 항상 압축하지 않고 전송됩니다. - 원시 업로드(
InsertRawStreamAsync,PostStreamAsync)에는 호출마다 지정하는 별도의 플래그가 적용되며UseCompression이나InsertOptions.Compressor는 참조하지 않습니다. 플래그를 설정하면 gzip으로 압축하고, 그렇지 않으면 압축하지 않습니다.InsertRawStreamAsync의useCompression매개변수는 기본값이true이므로, 클라이언트에Compression=false를 설정했더라도false를 전달하지 않으면 원시 업로드가 gzip으로 압축됩니다.
압축 조정
압축은 CPU 자원을 더 사용하는 대신 전송할 데이터의 크기를 줄입니다. 압축이 유리한지는 코덱의 처리 속도와 네트워크 연결 속도의 상대적인 차이에 거의 전적으로 달려 있습니다. 모든 환경에 맞는 설정은 없습니다.압축 여부를 결정하는 단 하나의 수치
코덱의 압축 속도가 네트워크 전송 속도보다 빠르면 압축하는 편이 유리합니다. 읽기 경로에서는 이 기준 속도가 예상보다 낮습니다. ClickHouse가 출력 버퍼에서 HTTP 응답을 단일 스레드로 압축하기 때문입니다. 16-vCPU ClickHouse Cloud 서비스(hits, RowBinary, level 3)에서 측정한 서버의 압축 데이터 출력 속도는 약 100~200 MB/s입니다.
따라서 결과가 크고 한 번에 쿼리 1개만 처리한다고 가정하면, 약 100 MB/s부터는 압축의 이점이 사라집니다. 같은 클라우드 리전 내 단일 HTTPS 스트림은
흔히 이 속도를 넘지만, 공용 인터넷이나 VPN을 거치거나 리전 경계를 넘는 연결은 대개 이보다 느립니다.
삽입 경로에서는 더 빠른 연결에서도 압축이 유리합니다. 클라이언트가 별도의 CPU 코어에서 압축을 수행하며, 일반적으로 서버가 응답을 압축하는 속도보다 빠르기 때문입니다.
배포 환경별 대략적인 가이드
이 표에는 다음 3가지가 반영되지 않았습니다.
- 외부 전송 비용: 데이터 전송량에 따라 요금이 부과된다면 지연 시간 외에도 전송량 자체가 비용에 영향을 미치므로 연결 속도와 관계없이 압축률을 높이는 편이 유리합니다.
- 작은 결과: 위 내용은 모두 큰 payload를 전제로 합니다. 응답이 작을 때는 코덱에 따른 차이가 거의 없고 요청당 오버헤드가 더 큰 영향을 미칩니다.
- 병렬 삽입에서는 삽입에 적용할 기준 속도가 높아집니다. 위의 모든 처리량 수치는 단일 스레드 기준입니다.
InsertOptions.MaxDegreeOfParallelism의 기본값은1이지만, 이 값을 높이면 배치를 동시에 압축하므로 클라이언트의 전체 인코딩 속도가 사용 가능한 코어 수에 대략 비례해 증가합니다. 따라서 빠른 연결에서도 병렬 삽입은 단일 스레드 삽입에서 압축의 이점이 사라지는 속도를 훨씬 넘어서까지 압축의 이점을 유지할 수 있습니다. 표의 삽입 행은 최소 기준으로 보고, 이미 배치를 병렬로 처리하고 있다면 연결 속도가 너무 빨라 압축이 불필요하다고 판단하기 전에 다시 테스트하세요.
코덱 선택
압축 수준
응답 압축은http_zlib_compression_level이라는 단일 서버 설정으로 제어됩니다. 이 설정은 zlib뿐 아니라 모든 HTTP 코덱에 적용되며, 기본값은 3입니다.
측정 결과 변경할 근거가 없다면 기본값을 유지하세요. 값을 높여도 CPU 사용량이 크게 늘어나는 데 비해 데이터 크기는 거의 줄지 않습니다(zstd의 설정값을 3 → 6으로 높이면 서버 CPU 사용량은 약 2배로 늘지만 데이터 크기는 약 14%만 줄어듭니다). br은 성능이 극도로 나빠집니다. 반면 수준을 1로 낮추면 양상이 확연히 달라집니다. lz4의 CPU 부담이 크게 줄어들어 zstd의 CPU 효율상 이점이 사라집니다. 필요한 경우 쿼리별로 설정하세요.
환경에 맞는 성능 역전 지점 측정하기
어떤 코덱과 압축 수준이 적합한지 가장 빨리 알아보려면 동일한 쿼리를 여러 코덱으로 실행해 소요 시간을 비교하면 됩니다.QueryOptions.QueryId를 설정해 해당 행을 찾은 다음,
system.query_log에서 ProfileEvents를 조회하세요.
ORDER BY 없이 LIMIT n만 사용하면 실행할 때마다 서로 다른 행이
반환됩니다. 따라서 매번 다른 데이터를 압축하게 되어 압축 비율을 비교해도 의미가 없습니다. 고정된
결과 집합을 기준으로 비교하세요.
원시 스트림 삽입
InsertRawStreamAsync를 사용하면 파일이나 메모리 스트림에서 CSV, JSON, Parquet 등의 ClickHouse 지원 포맷으로 데이터를 직접 삽입할 수 있습니다.
CSV 파일에서 데이터 삽입:
데이터 수집 동작을 제어하는 옵션은 포맷 설정 문서를 참조하세요.
추가 예시
더 많은 실제 사용 예시는 GitHub 리포지토리의 예시 디렉터리에서 확인하세요.ADO.NET
이 라이브러리는ClickHouseConnection, ClickHouseCommand, ClickHouseDataReader를 통해 ADO.NET을 완벽하게 지원합니다. ORM 통합(Dapper, Linq2db)이나 표준 .NET 데이터베이스 추상화를 사용하려면 이 API가 필요합니다.
ClickHouseDataSource를 사용한 수명 주기 관리
올바른 수명 주기 관리와 연결 풀링을 위해 항상ClickHouseDataSource를 통해 연결을 생성하세요. DataSource는 내부적으로 하나의 ClickHouseClient를 관리하며, 모든 연결은 이 클라이언트의 HTTP 연결 풀을 공유합니다.
ClickHouseCommand 사용
SQL을 실행하려면 연결을 통해 명령어를 생성합니다.ExecuteNonQueryAsync()- INSERT, UPDATE, DELETE 및 DDL 문을 실행합니다ExecuteScalarAsync()- 첫 번째 행의 첫 번째 컬럼 값을 반환합니다ExecuteReaderAsync()- 결과를 순회하는 데 사용하는ClickHouseDataReader를 반환합니다
ClickHouseDataReader 사용
ClickHouseDataReader를 사용하면 쿼리 결과에 타입별로 접근할 수 있습니다.
enum의 서수 값 읽기
Enum8 또는 Enum16 컬럼의 값은 레이블로 구체화됩니다. GetFieldType은 string을 반환하고,
GetString, GetValue, GetFieldValue<string>은 모두 레이블을 반환합니다. 저장된 값이 문자열이므로
숫자형 접근자를 사용하면 enum 컬럼에서 InvalidCastException이 발생합니다.
레이블에 해당하는 서수 값을 가져오려면 TryGetEnumOrdinal을 사용하세요:
Enum8/Enum16 컬럼과 셀 값이 NULL이 아닌 Nullable(Enum...)
컬럼에서는 true를 반환하고 value를 설정합니다. 셀 값이 NULL이거나 enum이 아닌
컬럼에서는 value를 0으로 설정하고 false를 반환합니다. 서수 값은
wire에서 읽은 부호 있는 값이므로 음수일 수 있으며, Enum16의 서수 값은
1바이트로 표현할 수 있는 범위를 넘을 수 있습니다.
모범 사례
연결 수명과 풀링
ClickHouse.Driver는 내부적으로 System.Net.Http.HttpClient를 사용합니다. HttpClient는 엔드포인트마다 연결 풀을 유지합니다. 따라서 다음과 같은 특성이 있습니다.
- 데이터베이스 세션은 연결 풀이 관리하는 HTTP 연결을 통해 다중화됩니다.
- HTTP 연결은 풀에서 자동으로 재사용됩니다.
ClickHouseClient또는ClickHouseConnection객체가 해제된 후에도 연결은 유지될 수 있습니다.
DateTime 처리
-
가능하면 UTC를 사용하세요. 타임스탬프를
DateTime('UTC')컬럼에 저장하고 코드에서는DateTimeKind.Utc를 사용하세요. 이렇게 하면 시간대 해석이 모호해지는 일을 방지할 수 있습니다. -
시간대를 명시적으로 처리하려면
DateTimeOffset을 사용하세요.DateTimeOffset은 항상 특정 시점을 나타내며 UTC 오프셋 정보를 포함합니다. -
SQL 타입 힌트에 시간대를 지정하세요. UTC가 아닌 컬럼에
UnspecifiedDateTime 값을 매개변수로 전달할 때는 SQL에 시간대를 포함하세요.
비동기 삽입
비동기 삽입을 사용하면 배칭을 클라이언트가 아닌 서버에서 처리합니다. 클라이언트 측에서 데이터를 배칭할 필요 없이 서버가 수신 데이터를 버퍼에 모았다가 설정 가능한 임계값에 도달하면 저장소에 플러시합니다. 여러 에이전트가 작은 payload를 전송하는 관측성 워크로드처럼 동시성이 높은 환경에 유용합니다.CustomSettings 또는 연결 문자열에서 비동기 삽입을 활성화하세요.
wait_for_async_insert 설정으로 제어):
주요 설정:
세션
다음과 같은 서버 측 상태 유지 기능이 필요한 경우에만 세션을 활성화하세요.- 임시 테이블 (
CREATE TEMPORARY TABLE) - 여러 SQL 문에 걸쳐 쿼리 컨텍스트 유지
- 세션 수준 설정 (
SET max_threads = 4)
지원되는 데이터 타입
ClickHouse.Driver는 모든 ClickHouse 데이터 타입을 지원합니다. 아래 표는 데이터베이스에서 데이터를 읽을 때 ClickHouse 타입과 네이티브 .NET 타입의 대응 관계를 보여줍니다.
데이터 타입 매핑: ClickHouse에서 읽기
정수 타입
부동 소수점 타입
Decimal 타입
Decimal 타입의 변환은 UseCustomDecimals 설정에 따라 결정됩니다.
불리언 타입
문자열 타입
기본적으로
String과 FixedString(N) 컬럼은 모두 string으로 반환됩니다. byte[]로 읽으려면 연결 문자열(connection string)에 ReadStringsAsByteArrays=true를 설정하세요. 이 옵션은 유효한 UTF-8이 아닐 수 있는 바이너리 데이터를 저장할 때 유용합니다.이 설정은 다른 타입 안에 중첩된 문자열에도 적용됩니다. 따라서 Array(String)은 byte[][]로,
Map(String, String)은 키까지 포함해 Dictionary<byte[], byte[]>로 읽힙니다. 예외는
JSON 컬럼입니다. 이 컬럼의 문자열 말단 값은 항상 텍스트로 읽힙니다. JSON 타입을 참조하세요.날짜 및 시간 타입
ClickHouse는
DateTime 및 DateTime64 값을 내부적으로 Unix 타임스탬프(epoch 이후 경과한 초 또는 초 미만 단위)로 저장합니다. 값은 항상 UTC로 저장되지만, 컬럼에 지정된 시간대에 따라 표시하고 해석하는 방식이 달라질 수 있습니다.
DateTime 값을 읽을 때 DateTime.Kind 속성은 컬럼의 시간대에 따라 설정됩니다.
UTC가 아닌 시간대의 컬럼에서 반환된
DateTime은 해당 시간대의 현지 시각을 나타냅니다. 해당 시간대에 맞는 오프셋이 포함된 DateTimeOffset을 가져오려면 ClickHouseDataReader.GetDateTimeOffset()을 사용하세요:
DateTime('Europe/Amsterdam')가 아닌 DateTime)을 읽을 때 드라이버는 Kind=Unspecified인 DateTime을 반환합니다. 이 값은 시간대를 임의로 가정하지 않고 저장된 현지 시각을 그대로 유지합니다.
시간대를 명시하지 않은 컬럼의 값을 시간대 정보가 포함된 값으로 처리하려면 다음 중 하나를 선택하세요.
- 컬럼 정의에 시간대를 명시하세요:
DateTime('UTC')또는DateTime('Europe/Amsterdam') - 값을 읽은 후 시간대를 직접 적용하세요.
JSON 타입
JSON 컬럼의 반환 타입은
JsonReadMode 설정에 따라 결정됩니다.
-
Binary(기본값):System.Text.Json.Nodes.JsonObject를 반환합니다. JSON 데이터의 구조를 유지한 채 접근할 수 있지만, IP 주소, UUID, 자릿수가 큰 십진수와 같은 특수 ClickHouse 타입은 JSON 구조 안에서 문자열로 변환됩니다. -
String: 원본 JSON을string으로 반환합니다. ClickHouse에서 가져온 JSON 표현을 그대로 유지하므로, 파싱하지 않고 JSON을 전달하거나 역직렬화를 직접 처리할 때 유용합니다.
None은 세 번째 모드입니다. Binary와 동일한 방식으로 읽지만, 쿼리와 함께 서버 설정을 전송하지 않습니다.
서버 설정을 지정할 수 없는 연결에서 사용하십시오.
컬럼 타입에 선언된 경로는 타입이 지정된 경로이고, 문서 내 나머지 경로는
동적 경로입니다. 값이 null일 때 두 경로는 다르게 동작합니다.
타입이 지정된 경로는 항상 JsonObject에 나타납니다. Nullable(T) 또는 Dynamic으로 선언된 경로는 저장된 값이 null인 경우와 문서에 해당 경로가 없는 경우 모두
JSON null로 반환되므로 두 경우를
구분할 수 없습니다.
JSON(x String)은
{"x":""}가 되고, JSON(x Int64)는 {"x":0}이 됩니다.
값이 null인 동적 경로는 객체에서 완전히 제외되므로 ContainsKey는 해당 경로에 대해
false를 반환합니다. 일반 JSON 컬럼에서 {"x":null}을 읽으면 {}가 반환됩니다.
중첩된 타입이 지정된 경로는 상위 경로도 생성하므로 JSON(a.b Nullable(Int64))는 빈 문서에서도
{"a":{"b":null}}을 반환합니다.
이는 서버가 직접 렌더링한 결과이므로 이제
Binary와 String 모드의 결과가 일치합니다. 1.4.0 이전에는
null을 담은 타입이 지정된 경로가 JsonObject에서 제외되어 {"x":null}을 읽으면
{}가 반환되었고, JSON(a.b Nullable(Int64))와 같은 중첩 경로에서는 a 하위 트리 전체가 사라졌습니다.JSON 컬럼 안의 문자열 리프 값은 ReadStringsAsByteArrays 설정값과 관계없이 항상 텍스트로 반환됩니다.
JsonValue에는 바이트 배열 형식이 없어 byte[]를 사용하면 base64로 표현되기 때문입니다. 이는
String, FixedString뿐 아니라 LowCardinality, Nullable 또는 SimpleAggregateFunction으로
감싼 문자열에도 적용됩니다. Array와 Map 안의 문자열도 맵 키를 포함해 마찬가지입니다.
JSON 리더가 타입을 확인할 수 없는 바이트 배열은 여전히 base64로 표현됩니다.
Variant 또는 Dynamic 타입이 지정된 경로에 저장된 값은 타입을 행마다 확인할 수 있으므로,
Variant(Array(UInt8), String)에 포함된 문자열은 base64로 인코딩되어 반환됩니다. 설정값이 어느 쪽이든 결과는 같습니다.JSON 맵의 키 타입이 정확히 String이 아니면(예: Map(LowCardinality(String), String))
NotSupportedException이 발생합니다.JSON(a Int64, a.b Int64)가 있습니다. 두 경로 모두 모든 행에 존재하므로 서버는
중복 키가 포함된 행을 출력합니다: {"a":0,"a":{"b":7}}. JsonObject는 하나의 키에 두 값을
담을 수 없으므로 JsonReadMode.Binary는 두 경로를 명시한 SerializationException을 발생시킵니다.
값이 Map인 경우에도 마찬가지입니다. 예를 들어 JSON(a Map(String, Int64))로 선언한 컬럼에서
동적 경로 a.b도 있는 행을 읽을 때 그렇습니다.
이는 해당 행에서 양쪽 모두 값이 있을 때만 적용됩니다. 한쪽에 값이 없으면(널,
빈 객체 또는 모든 값이 널인 하위 트리) 서버가 두 경로 중 어느 것을 먼저 보내든 값이 있는 쪽을
사용합니다. 따라서 겹치는 경로를 Nullable 타입으로 선언하면 행마다 한쪽만 값으로 채워져
오류 없이 읽을 수 있습니다: JSON(a Nullable(Int64), a.b Nullable(Int64))에서는 예상대로
{"a":5}와 {"a":{"b":7}}을 읽을 수 있습니다.
이러한 컬럼을 JsonReadMode.String으로 읽으면 중복 키까지 포함한 서버의 JSON 텍스트를
그대로 가져올 수 있습니다.
예외 없이 컬럼을 JsonObject로 읽으려면 AllowDuplicateJsonKeys를 설정하세요.
그러면 드라이버는 행에 담긴 두 값 중 마지막 값만 유지하고 나머지는 버리므로
데이터가 손실됩니다: {"a.b":7}을 담은 JSON(a Int64, a.b Int64)는 {"a":0}으로 읽힙니다. 값이 있는 경로의
상위 경로에 스칼라나 배열이 있으면 그 아래에 하위 트리를 배치할 수 없으므로 여전히 예외가
발생합니다.
맵(Map) 타입
ClickHouse의
Map(K, V)은 내부적으로 Array(Tuple(K, V))로 저장되므로 같은 키를 가진 항목을 여러 개 담을 수 있습니다. 반면 Dictionary는 중복 키를 허용하지 않습니다. 따라서 기본 모드에서는 같은 키가 여러 번 나오면 마지막 값만 남고 앞선 키-값 쌍은 제외됩니다. 반환 형식은 MapReadMode 설정으로 선택합니다.
-
Dictionary(기본값):Dictionary<K, V>를 반환합니다. -
KeyValuePairs: 서버가 전송한 순서대로List<KeyValuePair<K, V>>를 반환합니다. 같은 키를 가진 항목을 포함해 모든 키-값 쌍이 유지됩니다.
Map 컬럼의 프레임워크 타입을 지정하며, GetFieldValue<T>, 드라이버가 보고하는 스키마 타입, POCO 속성 매핑에도 적용됩니다. 컬럼의 타입 트리 어디에 맵이 있든 적용되므로 Array(Map(...)), Map(K, Map(...)), Tuple(..., Map(...)), Dynamic도 포함됩니다.
모드에 관계없이 쓰기 경로에서는 두 표현 방식이 모두 허용됩니다. 자세한 내용은 맵 쓰기를 참조하세요.
기타 타입
Dynamic과 Variant 타입은 각 행에 저장된 값의 실제 기본 타입에 해당하는 타입으로 변환됩니다.
Geometry 타입
Geometry는 모든 Geometry 타입의 값을 저장할 수 있는 Variant 타입입니다. 저장된 값은 해당 타입으로 변환됩니다.
타입 매핑: ClickHouse에 쓰기
데이터를 삽입할 때 드라이버는 .NET 타입을 대응하는 ClickHouse 타입으로 변환합니다. 아래 테이블에서 각 ClickHouse 컬럼 타입에 허용되는 .NET 타입을 확인할 수 있습니다.정수 타입
부동 소수점 타입
불리언 타입
문자열 타입
날짜 및 시간 타입
지원 범위를 벗어나는 값바이너리 쓰기 경로에서
Date, Date32, DateTime, DateTime32 값이 지원 범위를 벗어나면 Write 시점에 컬럼 타입과 지원 범위를 명시한 ArgumentOutOfRangeException이 발생합니다. 이전에는 범위를 벗어난 값이 32비트 정수를 거치며 아무런 경고 없이 잘린 뒤 서버에서 재해석되어, 유효한 형식이지만 실제 시점과 다른 타임스탬프가 생성될 수 있었습니다.DateTime.Kind를 따릅니다.
DateTimeOffset 값은 항상 정확한 시점을 유지합니다.
예시: UTC DateTime (정확한 시점 유지)
DateTimeKind.Utc 또는 DateTimeOffset을 사용하세요. 그러면 서버, 클라이언트 또는 컬럼의 시간대와 관계없이 코드가 일관되게 작동합니다.
HTTP 매개변수와 대량 복사 비교
Unspecified DateTime 값을 기록할 때 HTTP 매개변수 바인딩과 대량 복사는 중요한 차이가 있습니다.
대량 복사는 대상 컬럼의 시간대를 파악하고 Unspecified 값을 해당 시간대에 맞게 해석합니다.
HTTP 매개변수는 컬럼의 시간대를 자동으로 파악하지 못합니다. 따라서 SQL 타입 힌트에 시간대를 지정해야 합니다:
Decimal 타입
JSON 타입
JSON 데이터를 쓸 때의 동작은
JsonWriteMode 설정에 따라 달라집니다.
-
String(기본값):string,JsonObject,JsonNode또는 모든 객체를 허용합니다. 모든 입력을System.Text.Json.JsonSerializer로 직렬화한 뒤, 서버 측에서 파싱할 수 있도록 JSON 문자열로 전송합니다. 가장 유연한 모드이며 타입을 등록하지 않아도 사용할 수 있습니다. -
Binary: 등록된 POCO 타입만 허용합니다. 데이터를 클라이언트 측에서 타입 힌트를 완벽하게 지원하는 ClickHouse의 바이너리 JSON 포맷으로 변환합니다. 사용하기 전에connection.RegisterJsonSerializationType<T>()를 호출해야 합니다. 이 모드에서string또는JsonNode값을 쓰면ArgumentException이 발생합니다.
JSON(id UInt64, price Decimal128(2))) 드라이버는 이를 사용해 값의 타입을 정확히 유지하며 직렬화합니다. 이를 통해 일반 JSON으로 직렬화할 때 정밀도가 손실될 수 있는 UInt64, Decimal, UUID, DateTime64 등의 타입도 정밀도를 유지합니다.
POCO는 JsonWriteMode 설정에 따라 두 가지 방식으로 JSON 컬럼에 쓸 수 있습니다.
문자열 모드(기본값): System.Text.Json.JsonSerializer로 POCO를 직렬화합니다. 타입을 등록할 필요가 없으며, 익명 객체에도 사용할 수 있는 가장 간단한 방식입니다.
바이너리 모드: 타입 힌트를 완전히 지원하는 드라이버의 바이너리 JSON 포맷으로 POCO를 직렬화합니다. 사용 전에 connection.RegisterJsonSerializationType<T>()으로 타입을 등록해야 합니다. 이 모드에서는 특성을 사용해 사용자 정의 경로 매핑을 지정할 수 있습니다.
-
[ClickHouseJsonPath("path")]: 프로퍼티를 사용자 정의 JSON 경로에 매핑합니다. 중첩 구조를 다루거나 프로퍼티 이름이 원하는 JSON 키와 다를 때 유용합니다. 바이너리 모드에서만 작동합니다. -
[ClickHouseJsonIgnore]: 프로퍼티를 직렬화 대상에서 제외합니다. 바이너리 모드에서만 작동합니다.
UserId 속성은 userid가 아니라 UserId로 정의된 힌트와만 일치합니다. 이는 userName과 UserName 같은 경로를 서로 다른 필드로 취급하는 ClickHouse의 동작과 일치합니다.
제한 사항(Binary mode에만 해당):
- 직렬화하기 전에
connection.RegisterJsonSerializationType<T>()을 사용하여 연결에 POCO 타입을 등록해야 합니다. 등록되지 않은 타입을 직렬화하면ClickHouseJsonSerializationException예외가 발생합니다. - 딕셔너리 및 배열/리스트 속성을 올바르게 직렬화하려면 컬럼 정의에 타입 힌트를 지정해야 합니다. 타입 힌트가 없다면 String mode를 사용하세요.
- POCO 속성의 null 값은 해당 경로에 대한
Nullable(T)타입 힌트가 컬럼 정의에 있을 때만 기록됩니다. ClickHouse는 동적 JSON 경로에Nullable타입을 허용하지 않으므로 타입 힌트가 없는 null 속성은 건너뜁니다. - String mode에서는
ClickHouseJsonPath와ClickHouseJsonIgnore속성이 무시됩니다(Binary mode에서만 적용됩니다).
기타 타입
Geometry 타입
쓰기를 지원하지 않는 타입
Nested 타입 처리
ClickHouse의 Nested 타입(Nested(...))은 배열과 같은 방식으로 읽고 쓸 수 있습니다.
로깅 및 진단
ClickHouse .NET 클라이언트는Microsoft.Extensions.Logging 추상화와 연동하여 필요할 때만 활성화할 수 있는 경량 로깅을 제공합니다. 로깅을 활성화하면 드라이버는 연결 수명 주기 이벤트, 명령 실행, 전송 작업, 대량 삽입 작업에 대한 구조화된 메시지를 출력합니다. 로깅은 선택 사항으로, 로거를 구성하지 않은 애플리케이션은 추가 처리 비용 없이 실행됩니다.
빠른 시작
appsettings.json 사용
표준 .NET 구성으로 로깅 수준을 설정할 수 있습니다.인메모리 구성 사용
코드에서 범주별 로깅 상세 수준을 설정할 수도 있습니다.범주와 로그 출력 주체
드라이버는 구성 요소별로 로그 레벨을 세밀하게 조정할 수 있도록 별도의 범주를 사용합니다.예시: 연결 문제 진단
- HTTP 클라이언트 팩터리 선택(기본 연결 풀 또는 단일 연결)
- HTTP 핸들러 구성(SocketsHttpHandler 또는 HttpClientHandler)
- 연결 풀 설정(MaxConnectionsPerServer, PooledConnectionLifetime 등)
- 타임아웃 설정(ConnectTimeout, Expect100ContinueTimeout 등)
- SSL/TLS 구성
- 연결 시작 및 종료 이벤트
- 세션 ID 추적
디버그 모드: 네트워크 추적 및 진단
드라이버 라이브러리에는 네트워크 문제 진단을 위해 .NET 네트워킹 내부 동작을 저수준에서 추적하는 보조 기능이 포함되어 있습니다. 이 기능을 활성화하려면 수준을 Trace로 설정한 LoggerFactory를 전달하고 EnableDebugMode를 true로 설정해야 합니다.ClickHouse.Driver.Diagnostic.TraceHelper 클래스를 통해 수동으로 활성화할 수도 있습니다. 이벤트는 ClickHouse.Driver.NetTrace 범주에 기록됩니다. 경고: 이 기능을 활성화하면 로그가 매우 많이 생성되어 성능에 영향을 줍니다. 운영 환경에서는 디버그 모드를 활성화하지 않는 것이 좋습니다.
OpenTelemetry
드라이버는 .NETSystem.Diagnostics.Activity API를 통한 OpenTelemetry 분산 추적을 기본적으로 지원합니다. 이 기능을 활성화하면 드라이버가 데이터베이스 작업에 대한 스팬을 생성합니다. 생성된 스팬은 Jaeger나 OpenTelemetry Collector를 통한 ClickHouse와 같은 관측성 백엔드로 내보낼 수 있습니다.
추적 활성화
ASP.NET Core 애플리케이션에서 ClickHouse 드라이버의ActivitySource를 OpenTelemetry 구성에 추가하세요.
스팬 속성
각 스팬에는 표준 OpenTelemetry 데이터베이스 속성과 디버깅에 활용할 수 있는 ClickHouse 전용 쿼리 통계가 포함됩니다.구성 옵션
ClickHouseDiagnosticsOptions를 사용하여 추적 동작을 제어합니다.
TLS 구성
HTTPS로 ClickHouse에 연결할 때 TLS/SSL 동작을 다양한 방식으로 설정할 수 있습니다.사용자 정의 인증서 유효성 검사
사용자 정의 인증서 유효성 검사 로직이 필요한 운영 환경에서는ServerCertificateCustomValidationCallback 핸들러를 설정한 자체 HttpClient를 제공하세요.
사용자 정의 HttpClient를 제공할 때 유의할 사항
- 자동 압축 해제:
AutomaticDecompression은 비활성화해 두세요. 드라이버가 압축된 응답을 직접 디코딩하므로 필요하지 않습니다. 활성화하면 요청을 보낼 때 핸들러가 마스크에 포함된 모든 알고리즘을 발신Accept-Encoding에 추가합니다. 이로 인해 드라이버가 지정한 범위가 넓어져 ClickHouse가 요청하지 않은 코덱으로 응답할 수 있습니다. 응답 압축 해제를 참조하세요. - 유휴 타임아웃: 연결이 완전히 종료되지 않아 발생하는 오류를 방지하려면
PooledConnectionIdleTimeout을 서버의keep_alive_timeout보다 짧게 설정하세요(ClickHouse Cloud에서는 10초).
성능 최적화
이 섹션에서는 클라이언트의 성능을 최적화하는 방법과 사용 사례에 맞게 조정할 수 있는 다양한 옵션을 설명합니다.한눈에 보기
| 다음과 같은 경우 | 이렇게 하십시오 | |---|---|---| | POCO로 행을 읽는 경우 |MapTo<T> 대신 QueryAsync<T>를 사용하십시오 |
| 대규모 삽입을 수행하는 경우 | InsertOptions.BatchSize를 늘리십시오 |
| 삽입이 많은 console 또는 worker 앱을 실행하는 경우 | Server GC를 활성화하십시오 |
| 네트워크를 통해 대규모 결과를 읽는 경우 | 응답 압축을 활성화된 상태로 유지하십시오(기본값) |
| 고속 링크를 통해 삽입하는 경우 | InsertOptions.Compressor = null을 적용해 보십시오 |
| 동일한 테이블에 여러 번 삽입하는 경우 | UseSchemaCache 또는 ColumnTypes를 사용하십시오 |
| 매우 큰 결과를 읽는 경우 | ReadBufferSize를 늘리십시오 |
읽기: 구체화 경로 선택
결과에서 행을 가져오는 방법은 3가지이며, 방법마다 비용이 다릅니다. 일부 경로에서는 반환된 값을 박싱하므로 메모리 할당이 늘고 성능이 저하됩니다.
hits 데이터셋의 105개 컬럼에서 1,000,000개 행을 읽었을 때:
ORM은 타입이 지정된 접근자를 사용하면 최적화된 경로로 데이터를 읽습니다. linq2db는 각 컬럼에
GetInt64,
GetDouble, GetDateTime을 등록하므로 박싱 없이 데이터를 읽습니다. 반면
GetValue로 데이터를 읽는 코드(Dapper의 dynamic 결과 포함)는 값을 읽을 때마다 박싱합니다. ORM 쿼리의 실행 빈도가 높고
GetValue로 데이터를 읽는다면 해당 쿼리에 QueryAsync<T>를 사용하세요.삽입: 배치 크기와 병렬 처리 수준
배치 크기는 삽입 처리량을 좌우하는 가장 중요한 요소입니다.InsertOptions.BatchSize의 기본값은
100,000행입니다.
큰 배치를 사용하세요. 1,000,000행을 삽입할 때 배치 크기를 10,000행에서 100,000행으로 늘리면
다음과 같은 결과를 얻습니다.
배치 크기를 제어할 수 없다면(예: 여러 소규모 프로듀서가 각각 행을 전송하는 경우) 비동기 삽입을 사용해 서버에서 배칭을 처리하도록 하세요.
병렬 업로드.
InsertOptions.MaxDegreeOfParallelism의 기본값은 1입니다. 배치를 동시에
전송하려면 이 값을 높이세요. 압축이 활성화된 경우 각 배치가 별도의 스레드에서 압축되므로 특히
효과적입니다. 세션과 병렬 삽입은 함께 사용할 수 없습니다. 세션을 비활성화하거나
MaxDegreeOfParallelism = 1을 유지하세요.
스키마 확인 쿼리를 제거하세요. InsertBinaryAsync를 호출할 때마다 컬럼 타입을 확인하기 위해 먼저
SELECT ... WHERE 1=0 쿼리를 전송합니다. ColumnTypes 또는 UseSchemaCache를 사용해 이
추가 왕복 요청을 없애는 방법은 스키마 확인 쿼리 건너뛰기를 참조하세요.
박싱 없이 삽입하는 경로는 기본
RowBinary 포맷에 적용됩니다. RowBinaryWithDefaults는
DBDefault 마커를 찾기 위해 각 값을 검사해야 하므로 더 느린 경로를 사용합니다.압축: 읽기와 삽입에서 효과가 다릅니다
압축은 CPU 자원을 사용해 전송하는 데이터의 크기를 줄입니다. 압축이 유리한지는 데이터 전송 방향, ClickHouse 서버와의 연결 대역폭, 선택한 압축 알고리즘이 데이터에 얼마나 효과적인지, 전송량에 따라 비용이 청구되는지에 따라 달라집니다. 읽기: 서버가 클라이언트와 같은 컴퓨터에서 실행 중인 경우가 아니라면 압축을 켜 두세요. 압축은 기본적으로 켜져 있습니다. 압축하지 않았을 때와 비교한zstd 압축 수준 1의
결과는 다음과 같습니다.
삽입: 압축을 켜기 전에 성능을 측정하세요. 압축으로 얻는 이점이 압축을 켤 만큼 크지 않을 수 있습니다. 압축 해제로 서버에 부하가 추가된다는 점도 고려하세요. Zstd와 LZ4는 부하가 크지 않지만, Brotli 같은 다른 알고리즘은 부하가 클 수 있습니다.
삽입 시 압축을 끄려면:
버퍼
ReadBufferSize는 HTTP 응답을 읽는 데 사용하는 버퍼의 크기를 설정합니다. 기본값은 64 KiB입니다.
드라이버는 공유 풀에서 이 버퍼를 빌려 사용한 뒤 리더를 해제할 때 반환하므로, 쿼리마다
버퍼를 새로 할당하지 않습니다. 결과가 클 때 버퍼를 다시 채우는 횟수를 줄이려면 이 값을 늘리세요.
드라이버는 동시에 열려 있는 리더마다 버퍼를 하나씩 유지하므로, 버퍼 크기와 동시에 열려 있는
리더 수가 늘어날수록 메모리 사용량도 증가합니다.
런타임과 GC
삽입 작업이 많은 애플리케이션에서는 Server GC를 활성화하세요. 동일한 코드에서 할당된 바이트 수가 같더라도 삽입 작업 시 Workstation GC는 Server GC보다 최대 97% 느렸습니다.Server GC는 지연 시간보다 처리량을 위한 설정입니다. 동일한 측정에서 Server GC의
총 일시 중지 시간은 절반에도 못 미쳤지만, 개별 일시 중지 시간은 더 길었습니다
(95백분위수 기준 Server GC는 114.6ms, Workstation GC는 61.9ms). 서비스에서 테일 지연 시간이 중요하다면
두 모드를 모두 측정한 후 선택하세요.
지연 시간: 연결 재사용
새 TCP 연결을 설정하고 TLS 핸드셰이크를 완료하는 데는 상당한 시간이 걸립니다. 연결을 재사용하면 쿼리 지연 시간을 크게 줄일 수 있습니다.- 요청마다 클라이언트를 생성하지 마세요. 별도의
HttpClient를 사용하는 새 클라이언트마다 연결 풀이 새로 생성되고 핸드셰이크도 다시 수행해야 합니다. 애플리케이션의 전체 실행 기간 동안 하나의ClickHouseClient를 사용하세요. 여러 스레드에서 안전하게 사용할 수 있으며 싱글턴으로 사용하도록 설계되었습니다. - ADO.NET과 ORM에서는 모든 연결이 하나의 풀을 공유하도록
ClickHouseDataSource를 사용하세요.
직접 측정하기
성능은 데이터의 구조, 서버 연결 속도, 클라이언트와 서버 중 어느 쪽의 CPU를 더 사용할지, 하드웨어 제약 등에 따라 달라지는 경우가 많습니다. 따라서 실제 데이터와 운영 환경에서 성능을 직접 측정하는 것이 좋습니다. 서버가 수행한 작업을 확인하려면QueryOptions.QueryId를 설정한 뒤 카운터 값을 읽어 오세요.
ORM 지원
ORM을 사용하려면 ADO.NET API(ClickHouseConnection)가 필요합니다. 연결의 수명 주기를 올바르게 관리하려면 ClickHouseDataSource에서 연결을 생성하세요:
Dapper
ClickHouse.Driver는 Dapper와 호환됩니다. 드라이버는 .NET 값에서 타입을 추론해 Dapper의 @parameter 구문을 ClickHouse의 네이티브 {parameter:Type} 구문으로 자동 변환합니다.
연결의 수명 주기를 올바르게 관리하려면 ClickHouseDataSource를 사용하세요.
매개변수 전달 방식
Dapper의 모든 표준 매개변수 전달 방식을 지원합니다. 익명 객체:DynamicParameters(딕셔너리 또는 익명 객체에서):
쿼리 결과를 POCO로 매핑하기
Dapper는 컬럼 이름을 기준으로 해당 속성에 매핑하며, 대소문자는 구분하지 않습니다:ClickHouse 네이티브 매개변수 구문
매개변수의 자료형을 명시적으로 지정해야 한다면, 매개변수 값을Dictionary<string, object>로 전달하고 SQL에서 ClickHouse의 {param:Type} 구문을 직접 사용하세요. 동일한 매개변수에 @param 구문과 {param:Type} 구문을 혼용하지 마세요.
WHERE IN
Dapper의 네이티브 IN 확장 기능이 작동합니다:WHERE id IN (@Ids1, @Ids2, @Ids3)로 재작성하고, 드라이버는 확장된 매개변수를 각각 변환합니다.
배열 매개변수를 사용한 ClickHouse의 has()도 작동합니다:
사용자 정의 타입 핸들러
ITuple, BigInteger, ClickHouseDecimal 등의 일부 ClickHouse 타입은 시작할 때 타입 핸들러를 등록해야 합니다.
Dapper.Contrib
GetAll<T>()와 Get<T>(id)는 정상적으로 작동합니다. 반면 Insert<T>()는 SQL Server 구문(SCOPE_IDENTITY, [])을 생성하므로 작동하지 않습니다. 대신 ClickHouseClient의 네이티브 InsertBinaryAsync 메서드를 사용하는 것이 좋습니다.
제한 사항
Linq2db
이 드라이버는 .NET용 경량 ORM이자 LINQ 프로바이더인 linq2db와 호환됩니다. 자세한 내용은 프로젝트 웹사이트의 문서를 참조하세요. 사용 예시: ClickHouse 프로바이더를 사용해DataConnection을 생성하세요:
BulkCopyAsync를 사용하세요.
Entity Framework Core
ClickHouse용 공식 Entity Framework Core 프로바이더입니다. 익숙한 EF Core 패턴으로 C# 클래스를 ClickHouse 테이블에 매핑하고, LINQ로 쿼리하고,SaveChanges로 데이터를 삽입할 수 있습니다.
- NuGet:
ClickHouse.EntityFrameworkCore - 소스 코드: GitHub
이 프로바이더는 현재 활발히 개발 중입니다. 최신 릴리스는 JOIN, 서브쿼리, 집합 연산을 포함한 LINQ 쿼리와
SaveChanges / BulkInsertAsync를 통한 INSERT, 모든 DDL 작업(CREATE / ALTER / DROP)을 지원하는 마이그레이션, ClickHouse 전용 테이블 엔진 구성을 지원합니다. UPDATE / DELETE는 지원하지 않습니다.설치
빠른 시작
엔터티와DbContext를 정의한 다음 LINQ를 사용해 쿼리하세요.
지원되는 타입
Decimal128/Decimal256 컬럼의 최대 정밀도가 필요하다면 decimal 대신 ClickHouseDecimal(ClickHouse.Driver.Numerics에 정의됨)을 사용하세요. .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를 사용하는 GroupBy — HAVING (.GroupBy() 뒤에 사용하는 .Where()), 하나의 프로젝션에서 여러 집계 사용, 집계 결과에 대한 OrderBy를 포함합니다.
JOIN: 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 (및 + 연산자).
수학 함수: 표준 Math 및 MathF 메서드는 산술, 로그, 삼각 함수, 기타 유틸리티 기능을 수행하는 ClickHouse의 해당 함수로 변환됩니다.
provider는 JOIN 동작이 Entity Framework의 기대에 부합하도록 모든 연결 경로에 set_join_use_nulls=1을 자동으로 추가합니다.
ClickHouse 서버 또는 프로필에서 이 설정의 변경을 허용하지 않는다면(예: readonly=1 프로필), 다음과 같이 자동 추가를 비활성화하세요.
== null 대신 0 또는 ""와 명시적으로 비교하세요.
데이터 삽입
SaveChanges는 드라이버의 네이티브 InsertBinaryAsync API를 사용합니다. 이 API는 데이터를 RowBinary 형식으로 인코딩하고 요청 body를 압축하므로 매개변수화된 SQL보다 훨씬 효율적입니다.
Added에서 Unchanged 상태로 바뀝니다.
배치 크기는 설정할 수 있습니다(기본값: 1000):
대량 삽입
고처리량 데이터 적재에는SaveChanges 대신 BulkInsertAsync를 사용하세요. DbContext의 확장 메서드인 BulkInsertAsync는 EF Core의 변경 추적, 아이덴티티 식별, 상태 관리를 거치지 않고 RowBinary 인코딩과 압축된 요청 body를 사용해 드라이버의 InsertBinaryAsync를 직접 호출합니다.
따라서 삽입 후 엔터티를 추적할 필요가 없는 대규모 데이터셋을 적재할 때 적합합니다:
IEnumerable<T>를 사용할 수 있으며, 엔터티 전체를 메모리에 로드하지 않고 스트리밍 방식으로 처리합니다. 반환값은 삽입된 행의 수입니다. 삽입 후에도 엔터티가 DbContext에 연결되지 않으므로 Added → Unchanged 상태 전환은 발생하지 않습니다.
열거형
ClickHouseEnum8/Enum16 컬럼은 string 속성 또는 C# enum 타입으로 매핑할 수 있습니다. C# 열거형을 사용하면 provider가 열거형과 그 문자열 표현을 자동으로 상호 변환합니다.
사용자 정의 타입 변환
EF Core의ValueConverter를 사용하면 사용자 정의 타입을 provider가 지원하는 타입으로 변환할 수 있습니다. provider는 사용자 정의 타입을 직접 다루지 않으며, EF Core가 데이터를 전달하기 전에 변환합니다.
속성별 변환:
컬럼 타입 어노테이션
string, int, DateTime 같은 스칼라 타입의 ClickHouse 타입은 provider가 자동으로 추론합니다. 매개변수가 있는 타입과 래퍼는 ClickHouse 타입을 명시적으로 지정해야 합니다.
데이터 어노테이션(속성) 사용:
OnModelCreating에서 Fluent API 사용:
Array(Nullable(Int32)) 및 LowCardinality(Nullable(String))처럼 래퍼가 중첩된 형태도 지원됩니다. provider는 각 중첩 단계에서 Nullable과 LowCardinality 래퍼를 자동으로 제거합니다.
Variant 및 Dynamic 컬럼
ClickHouse의Variant(T1, T2, ...) 및 Dynamic 컬럼은 .NET에서 object로 매핑됩니다. object는 자동 타입 추론에 사용하기에는 지나치게 범용적이므로 .HasColumnType()을 사용해 저장소 타입을 명시적으로 지정해야 합니다.
string, ulong, ulong[]).
JSON 컬럼
provider는 ClickHouse의Json 컬럼 타입을 지원하며, 기본적으로 System.Text.Json.Nodes.JsonNode에 매핑하거나 자동 ValueConverter를 통해 string에 매핑합니다.
SaveChanges와 BulkInsertAsync에서 모두 지원됩니다.
Json 컬럼 타입을 사용하는 string으로 매핑하세요. 그러면 provider가 ValueConverter를 자동으로 적용합니다.
- JSON 경로 변환 미지원 — LINQ의
entity.Data["name"]은 ClickHouse의data.nameSQL 구문으로 변환되지 않습니다. JSON이 아닌 컬럼을 기준으로 필터링하고, JSON은 메모리에서 확인하세요. - NULL 의미 체계 — ClickHouse의 JSON 타입은 NULL 값에 대해 SQL NULL 대신
{}(빈 객체)를 반환합니다. - 정수 정밀도 — ClickHouse JSON은 모든 정수를
Int64로 저장합니다.JsonNode로 읽을 때는GetValue<int>()대신GetValue<long>()를 사용하세요.
테이블 엔진
ToTable(name, t => ...) Fluent API로 ClickHouse 테이블 엔진과 엔진별 절을 설정하세요. 엔진을 설정하지 않으면 provider는 MergeTree를 기본 엔진으로 사용하고, 엔터티의 기본 키를 바탕으로 ORDER BY를 지정합니다.
엔진 절:
WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. 모두 HasXxxEngine()이 반환하는 엔진 빌더에 적용됩니다.
컬럼 수준 기능: HasCodec, HasTtl, HasComment, HasDefault — 모두 마이그레이션에 반영됩니다.
데이터 스키핑 인덱스 — HasIndex(...).HasSkippingIndexType(...)를 통해 설정합니다:
마이그레이션
표준 EF Core 마이그레이션 워크플로:마이그레이션 제한 사항
이 provider는 마이그레이션 외에도 다음 기능을 아직 지원하지 않습니다.
UPDATE/DELETE- 트랜잭션:
BeginTransaction은 아무 작업도 수행하지 않습니다. ClickHouse에서는 ACID 트랜잭션이 지원되지 않습니다. - JSON 경로 쿼리 변환: LINQ의
entity.Data["key"]는 ClickHouse의data.keySQL 구문으로 변환되지 않습니다. 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))이 이에 해당합니다. 드라이버는 다음 두 경우를 구분할 수 없습니다.
- 원소가 9개인 1차원 튜플(컴파일러가 생성한 TRest 중첩 구조)
- 마지막 원소가 중첩된
Tuple(String, String)인 원소가 8개인 튜플
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>로 동일합니다.
드라이버는 8번째 인수를 TRest로 간주해 중첩 구조를 펼칩니다. 따라서 마지막 원소가 중첩 튜플인 원소 8개의 튜플은 잘못 직렬화됩니다.
System.Tuple과 ValueTuple 모두 원소가 7개를 초과하면 TRest 중첩 구조를 사용하므로 두 타입 모두 영향을 받습니다. 원소가 7개 이하이거나 마지막 원소가 튜플이 아닌 경우에는 영향을 받지 않습니다.
해결 방법: 드라이버가 내부 튜플을 TRest 중첩 구조와 구분할 수 있도록 내부 튜플을 한 단계 더 감싸세요.
AggregateFunction 컬럼
AggregateFunction(...) 타입 컬럼은 직접 조회하거나 데이터를 삽입할 수 없습니다.
데이터를 삽입하려면: