메인 콘텐츠로 건너뛰기
ClickHouse API의 모든 코드 예시는 여기에서 확인할 수 있습니다. 연결 구성은 Configuration을 참조하십시오. 지원되는 데이터 타입과 Go 타입 매핑은 Data Types를 참조하십시오.

연결

다음 예시는 server 버전을 반환하며, ClickHouse에 보안이 설정되어 있지 않고 default 사용자로 접근할 수 있다고 가정할 때 ClickHouse에 연결하는 방법을 보여줍니다. 연결에는 기본 네이티브 포트를 사용합니다.
conn, err := clickhouse.Open(&clickhouse.Options{
    Addr: []string{fmt.Sprintf("%s:%d", env.Host, env.Port)},
    Auth: clickhouse.Auth{
        Database: env.Database,
        Username: env.Username,
        Password: env.Password,
    },
})
if err != nil {
    return err
}
v, err := conn.ServerVersion()
fmt.Println(v)
전체 예시 이후의 모든 예시에서는, 별도 표기가 없는 한 ClickHouse conn 변수가 이미 생성되어 사용 가능한 상태라고 가정합니다.

실행

임의의 SQL 문은 Exec 메서드로 실행할 수 있습니다. 이는 DDL과 간단한 SQL 문에 유용합니다. 대량 삽입이나 쿼리를 반복 처리하는 용도로는 사용하지 않는 것이 좋습니다.
conn.Exec(context.Background(), `DROP TABLE IF EXISTS example`)
err = conn.Exec(context.Background(), `
    CREATE TABLE IF NOT EXISTS example (
        Col1 UInt8,
        Col2 String
    ) engine=Memory
`)
if err != nil {
    return err
}
conn.Exec(context.Background(), "INSERT INTO example VALUES (1, 'test-1')")
전체 예시 쿼리에 Context를 전달할 수 있습니다. 이를 사용해 쿼리 수준의 특정 설정을 전달할 수 있습니다. 자세한 내용은 Using Context를 참조하십시오.

배치 삽입

대량의 행을 삽입할 수 있도록 클라이언트는 배치 처리 semantics를 제공합니다. 이를 위해 먼저 행을 추가할 수 있는 배치를 준비해야 합니다. 준비된 배치는 마지막에 Send() 메서드로 전송됩니다. 배치는 Send가 실행될 때까지 메모리에 유지됩니다. 연결 누수를 방지하려면 배치에 대해 Close를 호출하는 것이 좋습니다. 이는 배치를 준비한 직후 defer 키워드를 사용해 처리할 수 있습니다. 이렇게 하면 Send가 호출되지 않더라도 연결이 정리됩니다. 단, 행이 하나도 추가되지 않았다면 쿼리 로그에는 0개의 행이 삽입된 것으로 표시됩니다.
conn, err := GetNativeConnection(nil, nil, nil)
if err != nil {
    return err
}
ctx := context.Background()
defer func() {
    conn.Exec(ctx, "DROP TABLE example")
}()
conn.Exec(context.Background(), "DROP TABLE IF EXISTS example")
err = conn.Exec(ctx, `
    CREATE TABLE IF NOT EXISTS example (
            Col1 UInt8
        , Col2 String
        , Col3 FixedString(3)
        , Col4 UUID
        , Col5 Map(String, UInt8)
        , Col6 Array(String)
        , Col7 Tuple(String, UInt8, Array(Map(String, String)))
        , Col8 DateTime
    ) Engine = Memory
`)
if err != nil {
    return err
}

batch, err := conn.PrepareBatch(ctx, "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1000; i++ {
    err := batch.Append(
        uint8(42),
        "ClickHouse",
        "Inc",
        uuid.New(),
        map[string]uint8{"key": 1},             // Map(String, UInt8)
        []string{"Q", "W", "E", "R", "T", "Y"}, // Array(String)
        []interface{}{ // Tuple(String, UInt8, Array(Map(String, String)))
            "String Value", uint8(5), []map[string]string{
                {"key": "value"},
                {"key": "value"},
                {"key": "value"},
            },
        },
        time.Now(),
    )
    if err != nil {
        return err
    }
}

return batch.Send()
전체 예시 ClickHouse 관련 권장 사항은 여기를 참조하십시오. 배치는 go-routine 간에 공유해서는 안 되며, 각 루틴마다 별도의 batch를 생성해야 합니다. 위 예시에서 행을 추가할 때 변수 타입이 컬럼 타입과 일치해야 한다는 점에 유의하십시오. 매핑은 대체로 명확하지만, 이 인터페이스는 유연하게 동작하도록 설계되어 있어 정밀도 손실이 없는 경우 타입이 변환됩니다. 예를 들어, 다음은 datetime64에 문자열을 삽입하는 방법을 보여줍니다.
batch, err := conn.PrepareBatch(ctx, "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1000; i++ {
    err := batch.Append(
        "2006-01-02 15:04:05.999",
    )
    if err != nil {
        return err
    }
}

return batch.Send()
전체 예시 각 컬럼 타입별로 지원되는 Go 타입의 전체 요약은 타입 변환을 참조하십시오.

임시 컬럼

임시 컬럼은 삽입 시에만 존재하는 쓰기 전용 컬럼으로, 저장되지 않으며 조회할 수도 없습니다. 삽입 시점에 파생 컬럼 값을 계산하는 데 유용합니다.
ctx := context.Background()
ddl := `
CREATE OR REPLACE TABLE test
(
    id UInt64,
    unhexed String EPHEMERAL,
    hexed FixedString(4) DEFAULT unhex(unhexed)
)
ENGINE = MergeTree
ORDER BY id`

if err := conn.Exec(ctx, ddl); err != nil {
    return err
}

// ephemeral 컬럼 값을 지정하여 삽입
if err := conn.Exec(ctx, "INSERT INTO test (id, unhexed) VALUES (1, '5a90b714')"); err != nil {
    return err
}

// ephemeral이 아닌 컬럼만 쿼리 가능합니다
rows, err := conn.Query(ctx, "SELECT id, hexed, hex(hexed) FROM test")
전체 예시

행 조회

QueryRow 메서드를 사용해 단일 행을 쿼리하거나, Query를 통해 결과 집합을 순회할 수 있는 커서를 얻을 수 있습니다. 전자는 데이터를 직렬화해 저장할 대상을 받지만, 후자는 각 행마다 Scan을 호출해야 합니다.
row := conn.QueryRow(context.Background(), "SELECT * FROM example")
var (
    col1             uint8
    col2, col3, col4 string
    col5             map[string]uint8
    col6             []string
    col7             []interface{}
    col8             time.Time
)
if err := row.Scan(&col1, &col2, &col3, &col4, &col5, &col6, &col7, &col8); err != nil {
    return err
}
fmt.Printf("row: col1=%d, col2=%s, col3=%s, col4=%s, col5=%v, col6=%v, col7=%v, col8=%v\n", col1, col2, col3, col4, col5, col6, col7, col8)
전체 예시
rows, err := conn.Query(ctx, "SELECT Col1, Col2, Col3 FROM example WHERE Col1 >= 2")
if err != nil {
    return err
}
for rows.Next() {
    var (
        col1 uint8
        col2 string
        col3 time.Time
    )
    if err := rows.Scan(&col1, &col2, &col3); err != nil {
        return err
    }
    fmt.Printf("row: col1=%d, col2=%s, col3=%s\n", col1, col2, col3)
}
rows.Close()
return rows.Err()
전체 예시 두 경우 모두 각 컬럼 값을 역직렬화할 변수의 포인터를 전달해야 합니다. 이 변수들은 SELECT 문에 지정된 순서대로 전달해야 하며, 위와 같은 SELECT *의 경우 기본적으로 컬럼 선언 순서가 사용됩니다. 삽입과 마찬가지로 Scan 메서드도 대상 변수가 적절한 타입이어야 합니다. 이 역시 가능한 경우 타입 변환이 이루어지도록 유연하게 설계되어 있으며, 정밀도 손실이 없을 때만 변환됩니다. 예를 들어 위 예시에서는 UUID 컬럼을 string 변수로 읽어옵니다. 각 컬럼 타입에 대해 지원되는 Go 타입의 전체 목록은 타입 변환을 참조하십시오. 마지막으로, QueryQueryRow 메서드에 Context를 전달할 수 있다는 점에도 유의하십시오. 이는 쿼리 수준 설정에 사용할 수 있습니다. 자세한 내용은 Using Context를 참조하십시오.

비동기 삽입

비동기 삽입은 Async 메서드를 통해 사용할 수 있습니다. 이를 사용하면 클라이언트가 서버에서 삽입을 완료할 때까지 기다릴지, 아니면 데이터가 수신되는 즉시 응답할지를 지정할 수 있습니다. 즉, wait_for_async_insert 매개변수를 제어합니다.
conn, err := GetNativeConnection(nil, nil, nil)
if err != nil {
    return err
}
ctx := context.Background()
if err := clickhouse_tests.CheckMinServerServerVersion(conn, 21, 12, 0); err != nil {
    return nil
}
defer func() {
    conn.Exec(ctx, "DROP TABLE example")
}()
conn.Exec(ctx, `DROP TABLE IF EXISTS example`)
const ddl = `
    CREATE TABLE example (
            Col1 UInt64
        , Col2 String
        , Col3 Array(UInt8)
        , Col4 DateTime
    ) ENGINE = Memory
`
if err := conn.Exec(ctx, ddl); err != nil {
    return err
}
for i := 0; i < 100; i++ {
    if err := conn.AsyncInsert(ctx, fmt.Sprintf(`INSERT INTO example VALUES (
        %d, '%s', [1, 2, 3, 4, 5, 6, 7, 8, 9], now()
    )`, i, "Golang SQL database driver"), false); err != nil {
        return err
    }
}
전체 예시

열 지향 삽입

삽입은 열 포맷으로 수행할 수 있습니다. 데이터가 이미 이런 구조로 되어 있다면 행으로 피벗할 필요가 없어져 성능상 이점을 얻을 수 있습니다.
batch, err := conn.PrepareBatch(context.Background(), "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

var (
    col1 []uint64
    col2 []string
    col3 [][]uint8
    col4 []time.Time
)
for i := 0; i < 1_000; i++ {
    col1 = append(col1, uint64(i))
    col2 = append(col2, "Golang SQL database driver")
    col3 = append(col3, []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9})
    col4 = append(col4, time.Now())
}
if err := batch.Column(0).Append(col1); err != nil {
    return err
}
if err := batch.Column(1).Append(col2); err != nil {
    return err
}
if err := batch.Column(2).Append(col3); err != nil {
    return err
}
if err := batch.Column(3).Append(col4); err != nil {
    return err
}

return batch.Send()
전체 예시

구조체 사용하기

Golang 구조체는 사용자에게 ClickHouse의 데이터 행(row)을 논리적으로 표현하는 방법을 제공합니다. 이를 위해 네이티브 인터페이스는 몇 가지 편리한 함수를 제공합니다.

serialize와 함께 사용하는 Select

Select 메서드를 사용하면 한 번의 호출로 응답 행 집합을 struct 슬라이스에 마샬링할 수 있습니다.
var result []struct {
    Col1           uint8
    Col2           string
    ColumnWithName time.Time `ch:"Col3"`
}

if err = conn.Select(ctx, &result, "SELECT Col1, Col2, Col3 FROM example"); err != nil {
    return err
}

for _, v := range result {
    fmt.Printf("row: col1=%d, col2=%s, col3=%s\n", v.Col1, v.Col2, v.ColumnWithName)
}
전체 예시

구조체 스캔

ScanStruct를 사용하면 쿼리의 단일 행을 구조체로 마샬링할 수 있습니다.
var result struct {
    Col1  int64
    Count uint64 `ch:"count"`
}
if err := conn.QueryRow(context.Background(), "SELECT Col1, COUNT() AS count FROM example WHERE Col1 = 5 GROUP BY Col1").ScanStruct(&result); err != nil {
    return err
}
전체 예시

구조체 추가

AppendStruct를 사용하면 기존 배치에 구조체를 추가할 수 있으며, 이 구조체는 완전한 행으로 해석됩니다. 이때 구조체의 필드는 이름과 유형이 모두 테이블과 일치해야 합니다. 모든 컬럼에는 이에 대응하는 구조체 필드가 있어야 하지만, 일부 구조체 필드에는 대응되는 컬럼이 없을 수 있습니다. 이러한 필드는 무시됩니다.
batch, err := conn.PrepareBatch(context.Background(), "INSERT INTO example")
if err != nil {
    return err
}
defer batch.Close()

for i := 0; i < 1_000; i++ {
    err := batch.AppendStruct(&row{
        Col1:       uint64(i),
        Col2:       "Golang SQL database driver",
        Col3:       []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9},
        Col4:       time.Now(),
        ColIgnored: "this will be ignored",
    })
    if err != nil {
        return err
    }
}
전체 예시

매개변수 바인딩

클라이언트는 Exec, Query, QueryRow 메서드에서 매개변수 바인딩을 지원합니다. 아래 예시와 같이 이름 있는 매개변수, 번호 매겨진 매개변수, 위치 매개변수를 사용할 수 있습니다. 각 방식의 예시는 아래에 나와 있습니다.
var count uint64
// 위치 기반 바인드
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 >= ? AND Col3 < ?", 500, now.Add(time.Duration(750)*time.Second)).Scan(&count); err != nil {
    return err
}
// 250
fmt.Printf("Positional bind count: %d\n", count)
// 숫자 기반 바인드
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 <= $2 AND Col3 > $1", now.Add(time.Duration(150)*time.Second), 250).Scan(&count); err != nil {
    return err
}
// 100
fmt.Printf("Numeric bind count: %d\n", count)
// 이름 기반 바인드
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 <= @col1 AND Col3 > @col3", clickhouse.Named("col1", 100), clickhouse.Named("col3", now.Add(time.Duration(50)*time.Second))).Scan(&count); err != nil {
    return err
}
// 50
fmt.Printf("Named bind count: %d\n", count)
전체 예시

특수한 경우

기본적으로 슬라이스는 쿼리에 매개변수로 전달되면 쉼표로 구분된 값 목록으로 펼쳐집니다. [ ]로 감싼 값 집합을 주입해야 하는 경우 ArraySet을 사용해야 합니다. 그룹/튜플이 필요하고, 예를 들어 IN 연산자와 함께 사용하기 위해 ( )로 감싸야 하는 경우 GroupSet을 사용할 수 있습니다. 이는 아래 예시와 같이 여러 그룹이 필요한 경우에 특히 유용합니다. 마지막으로, DateTime64 필드는 매개변수가 올바르게 렌더링되도록 하려면 정밀도가 필요합니다. 하지만 필드의 정밀도 수준은 클라이언트가 알 수 없으므로 사용자가 직접 제공해야 합니다. 이를 쉽게 처리할 수 있도록 DateNamed 매개변수를 제공합니다.
var count uint64
// 배열은 펼쳐집니다
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 IN (?)", []int{100, 200, 300, 400, 500}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Array unfolded count: %d\n", count)
// 배열은 []로 유지됩니다
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col4 = ?", clickhouse.ArraySet{300, 301}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Array count: %d\n", count)
// 그룹 Set을 사용하면 ( ) 목록을 구성할 수 있습니다
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col1 IN ?", clickhouse.GroupSet{[]interface{}{100, 200, 300, 400, 500}}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Group count: %d\n", count)
// 중첩이 필요한 경우 더 유용합니다
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE (Col1, Col5) IN (?)", []clickhouse.GroupSet{{[]interface{}{100, 101}}, {[]interface{}{200, 201}}}).Scan(&count); err != nil {
    return err
}
fmt.Printf("Group count: %d\n", count)
// 시간 값에 정밀도가 필요한 경우 DateNamed를 사용하세요#
if err = conn.QueryRow(ctx, "SELECT count() FROM example WHERE Col3 >= @col3", clickhouse.DateNamed("col3", now.Add(time.Duration(500)*time.Millisecond), clickhouse.NanoSeconds)).Scan(&count); err != nil {
    return err
}
fmt.Printf("NamedDate count: %d\n", count)
전체 예시

Context 사용

Go Context는 API 경계를 넘어 마감 시간, 취소 신호 및 기타 요청 범위 값을 전달하는 수단을 제공합니다. 연결의 모든 메서드는 첫 번째 인수로 Context를 받습니다. 앞선 예시에서는 context.Background()를 사용했지만, 이 기능을 사용하면 설정과 마감 시간을 전달하고 쿼리를 취소할 수 있습니다. withDeadline으로 생성한 Context를 전달하면 쿼리에 실행 시간 제한을 설정할 수 있습니다. 이는 절대 시간을 기준으로 하며, 만료되면 연결만 해제되고 ClickHouse에 취소 신호만 전송된다는 점에 유의하십시오. 또는 WithCancel을 사용해 쿼리를 명시적으로 취소할 수도 있습니다. 도우미 함수 clickhouse.WithQueryIDclickhouse.WithQuotaKey를 사용하면 쿼리 ID와 쿼터 키를 지정할 수 있습니다. 쿼리 ID는 로그에서 쿼리를 추적하거나 취소할 때 유용합니다. 쿼터 키는 고유한 키 값을 기준으로 ClickHouse 사용량에 제한을 적용하는 데 사용할 수 있습니다. 자세한 내용은 Quotas Management를 참조하십시오. 또한 Context를 사용하면 Connection Settings에 나와 있는 것처럼 연결 전체가 아니라 특정 쿼리에만 설정이 적용되도록 할 수 있습니다. 마지막으로 clickhouse.WithBlockSize를 통해 블록 버퍼 크기를 제어할 수 있습니다. 이는 연결 수준 설정인 BlockBufferSize를 재정의하며, 한 번에 디코딩되어 메모리에 유지되는 블록의 최대 개수를 제어합니다. 값을 크게 하면 메모리 사용량이 늘어나는 대신 더 높은 수준의 병렬 처리가 가능해질 수 있습니다. 위에서 설명한 내용의 예시는 아래와 같습니다.
dialCount := 0
conn, err := clickhouse.Open(&clickhouse.Options{
    Addr: []string{fmt.Sprintf("%s:%d", env.Host, env.Port)},
    Auth: clickhouse.Auth{
        Database: env.Database,
        Username: env.Username,
        Password: env.Password,
    },
    DialContext: func(ctx context.Context, addr string) (net.Conn, error) {
        dialCount++
        var d net.Dialer
        return d.DialContext(ctx, "tcp", addr)
    },
})
if err != nil {
    return err
}
if err := clickhouse_tests.CheckMinServerServerVersion(conn, 22, 6, 1); err != nil {
    return nil
}
// context를 사용해 특정 API 호출에 설정을 전달할 수 있습니다
ctx := clickhouse.Context(context.Background(), clickhouse.WithSettings(clickhouse.Settings{
    "async_insert": "1",
}))

// context를 사용해 쿼리를 취소할 수 있습니다
ctx, cancel := context.WithCancel(context.Background())
go func() {
    cancel()
}()
if err = conn.QueryRow(ctx, "SELECT sleep(3)").Scan(); err == nil {
    return fmt.Errorf("expected cancel")
}

// 쿼리에 마감 시간을 설정합니다. 지정한 절대 시각에 도달하면 쿼리가 취소됩니다.
// 쿼리는 ClickHouse에서 계속 끝까지 실행됩니다
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
defer cancel()
if err := conn.Ping(ctx); err == nil {
    return fmt.Errorf("expected deadline exceeeded")
}

// 로그에서 쿼리 추적을 돕기 위해 Query id를 설정합니다. 예: system.query_log 참조
var one uint8
queryId, _ := uuid.NewUUID()
ctx = clickhouse.Context(context.Background(), clickhouse.WithQueryID(queryId.String()))
if err = conn.QueryRow(ctx, "SELECT 1").Scan(&one); err != nil {
    return err
}

conn.Exec(context.Background(), "DROP QUOTA IF EXISTS foobar")
defer func() {
    conn.Exec(context.Background(), "DROP QUOTA IF EXISTS foobar")
}()
ctx = clickhouse.Context(context.Background(), clickhouse.WithQuotaKey("abcde"))
// quota key를 설정합니다. 먼저 quota를 생성합니다
if err = conn.Exec(ctx, "CREATE QUOTA IF NOT EXISTS foobar KEYED BY client_key FOR INTERVAL 1 minute MAX queries = 5 TO default"); err != nil {
    return err
}

type Number struct {
    Number uint64 `ch:"number"`
}
for i := 1; i <= 6; i++ {
    var result []Number
    if err = conn.Select(ctx, &result, "SELECT number FROM numbers(10)"); err != nil {
        return err
    }
}
전체 예시 보기

Progress, profile 및 log 정보

쿼리에 대해 Progress, Profile 및 Log 정보를 요청할 수 있습니다. Progress 정보는 ClickHouse에서 읽고 처리한 행 수와 바이트 수에 대한 통계를 제공합니다. 한편, Profile 정보는 비압축 바이트 수, 행 수, 블록 수를 포함하여 클라이언트에 반환된 데이터의 요약을 제공합니다. 마지막으로, Log 정보는 스레드 관련 통계(예: 메모리 사용량 및 데이터 처리 속도)를 제공합니다. 이 정보를 얻으려면 Context를 사용해야 하며, 여기에 콜백 함수를 전달할 수 있습니다.
totalRows := uint64(0)
// context를 사용하여 progress 및 profile 정보에 대한 콜백 전달
ctx := clickhouse.Context(context.Background(), clickhouse.WithProgress(func(p *clickhouse.Progress) {
    fmt.Println("progress: ", p)
    totalRows += p.Rows
}), clickhouse.WithProfileInfo(func(p *clickhouse.ProfileInfo) {
    fmt.Println("profile info: ", p)
}), clickhouse.WithLogs(func(log *clickhouse.Log) {
    fmt.Println("log info: ", log)
}))

rows, err := conn.Query(ctx, "SELECT number from numbers(1000000) LIMIT 1000000")
if err != nil {
    return err
}
for rows.Next() {
}

// 참고: rows.Err() 확인을 생략하지 마십시오
if err := rows.Err(); err != nil {
    return err
}

fmt.Printf("Total Rows: %d\n", totalRows)
rows.Close()
전체 예시

동적 스캔

반환되는 필드의 스키마나 유형을 알 수 없는 table을 읽어야 할 수 있습니다. 이는 애드혹 데이터 분석을 수행하거나 범용 도구를 작성할 때 흔히 발생합니다. 이를 위해 쿼리 응답에서 컬럼 유형 정보를 제공합니다. 이 정보는 Go 리플렉션과 함께 사용하여 올바른 유형의 변수를 런타임에 생성하고, 이를 Scan에 전달하는 데 사용할 수 있습니다.
const query = `
SELECT
        1     AS Col1
    , 'Text' AS Col2
`
rows, err := conn.Query(context.Background(), query)
if err != nil {
    return err
}
defer rows.Close()
var (
    columnTypes = rows.ColumnTypes()
    vars        = make([]interface{}, len(columnTypes))
)
for i := range columnTypes {
    vars[i] = reflect.New(columnTypes[i].ScanType()).Interface()
}
for rows.Next() {
    if err := rows.Scan(vars...); err != nil {
        return err
    }
    for _, v := range vars {
        switch v := v.(type) {
        case *string:
            fmt.Println(*v)
        case *uint8:
            fmt.Println(*v)
        }
    }
}
// 참고: rows.Err() 확인을 생략하지 마십시오
if err := rows.Err(); err != nil {
    return err
}
전체 예시

외부 테이블

외부 테이블을 사용하면 클라이언트가 SELECT 쿼리와 함께 데이터를 ClickHouse로 전송할 수 있습니다. 이 데이터는 임시 테이블에 저장되며, 쿼리 실행 시 쿼리 내에서 사용할 수 있습니다. 쿼리와 함께 외부 데이터를 클라이언트로 보내려면, Context를 통해 전달하기 전에 ext.NewTable로 외부 테이블을 생성해야 합니다.
table1, err := ext.NewTable("external_table_1",
    ext.Column("col1", "UInt8"),
    ext.Column("col2", "String"),
    ext.Column("col3", "DateTime"),
)
if err != nil {
    return err
}

for i := 0; i < 10; i++ {
    if err = table1.Append(uint8(i), fmt.Sprintf("value_%d", i), time.Now()); err != nil {
        return err
    }
}

table2, err := ext.NewTable("external_table_2",
    ext.Column("col1", "UInt8"),
    ext.Column("col2", "String"),
    ext.Column("col3", "DateTime"),
)

for i := 0; i < 10; i++ {
    table2.Append(uint8(i), fmt.Sprintf("value_%d", i), time.Now())
}
ctx := clickhouse.Context(context.Background(),
    clickhouse.WithExternalTable(table1, table2),
)
rows, err := conn.Query(ctx, "SELECT * FROM external_table_1")
if err != nil {
    return err
}
for rows.Next() {
    var (
        col1 uint8
        col2 string
        col3 time.Time
    )
    rows.Scan(&col1, &col2, &col3)
    fmt.Printf("col1=%d, col2=%s, col3=%v\n", col1, col2, col3)
}
// 참고: rows.Err() 확인을 생략하지 마십시오
if err := rows.Err(); err != nil {
    return err
}
rows.Close()

var count uint64
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM external_table_1").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_1: %d\n", count)
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM external_table_2").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_2: %d\n", count)
if err := conn.QueryRow(ctx, "SELECT COUNT(*) FROM (SELECT * FROM external_table_1 UNION ALL SELECT * FROM external_table_2)").Scan(&count); err != nil {
    return err
}
fmt.Printf("external_table_1 UNION external_table_2: %d\n", count)
전체 예시

OpenTelemetry

ClickHouse는 TCP와 HTTP 전송 모두에서 추적 컨텍스트 전파를 지원합니다. TCP를 사용할 경우 클라이언트는 스팬을 네이티브 바이너리 프로토콜로 직렬화합니다. clickhouse.WithSpan을 사용해 Context를 통해 쿼리에 스팬을 첨부하세요.
HTTP 전송 제한ClickHouse 서버는 표준 traceparent / tracestate HTTP 헤더를 수신하지만, clickhouse-go의 HTTP 전송은 현재 이를 보내지 않으므로 HTTP에서는 WithSpan이 적용되지 않습니다. 우회 방법으로 연결 옵션의 HttpHeaders를 통해 헤더를 수동으로 설정할 수 있습니다.
var count uint64
rows := conn.QueryRow(clickhouse.Context(context.Background(), clickhouse.WithSpan(
    trace.NewSpanContext(trace.SpanContextConfig{
        SpanID:  trace.SpanID{1, 2, 3, 4, 5},
        TraceID: trace.TraceID{5, 4, 3, 2, 1},
    }),
)), "SELECT COUNT() FROM (SELECT number FROM system.numbers LIMIT 5)")
if err := rows.Scan(&count); err != nil {
    return err
}
// 참고: rows.Err() 확인을 생략하지 마십시오
if err := rows.Err(); err != nil {
    return err
}
fmt.Printf("count: %d\n", count)
전체 예시 트레이싱 활용 방법에 대한 자세한 내용은 OpenTelemetry 지원에서 확인할 수 있습니다.
마지막 수정일 2026년 6월 10일