Перейти к основному содержанию
Все примеры кода для API ClickHouse можно найти здесь. Параметры подключения см. в разделе Конфигурация. Сведения о поддерживаемых типах данных и сопоставлении типов Go см. в разделе Типы данных.

Подключение

Следующий пример возвращает версию сервера и показывает, как подключиться к ClickHouse, предполагая, что ClickHouse не защищён и доступен для пользователя default. Обратите внимание, что для подключения используется стандартный порт native.
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 уже создана и доступна.

Выполнение

Произвольные команды можно выполнять с помощью метода Exec. Это полезно для DDL и простых операторов. Его не следует использовать для крупных вставок или перебора результатов запросов.
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. Это позволяет задавать определённые настройки на уровне запроса — см. Использование контекста.

Вставка батчами

Чтобы выполнить вставку большого числа строк, клиент поддерживает батчи. Для этого нужно подготовить батч, в который можно добавлять строки. Затем он отправляется с помощью метода 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 приведены здесь. Батчи не следует использовать совместно в разных горутинах — создавайте отдельный батч для каждой горутины. В приведённом выше примере обратите внимание: при добавлении строк типы переменных должны соответствовать типу столбца. Хотя это соответствие обычно очевидно, данный интерфейс старается быть гибким, и типы будут преобразованы, если это не приведёт к потере точности. Например, ниже показано, как вставить строку в 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
}

// Вставка с указанием значения эфемерного столбца
if err := conn.Exec(ctx, "INSERT INTO test (id, unhexed) VALUES (1, '5a90b714')"); err != nil {
    return err
}

// Запрашивать можно только неэфемерные столбцы
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 считывается в строковую переменную. Полный список поддерживаемых типов Go для каждого типа столбца см. в разделе Преобразования типов. Наконец, обратите внимание, что в методы Query и QueryRow можно передавать 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. Для работы с ними нативный интерфейс предоставляет несколько удобных функций.

Select с сериализацией

Метод Select позволяет за один вызов преобразовать набор строк ответа в срез структур.
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 необходимо указывать precision, чтобы параметры отображались корректно. Однако уровень precision для поля клиенту неизвестен, поэтому пользователь должен указать его самостоятельно. Для этого предусмотрен параметр 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)
// GroupSet позволяет формировать списки вида ( )
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)
Полный пример

Использование контекста

Контексты Go позволяют передавать дедлайны, сигналы отмены и другие значения уровня запроса через границы API. Все методы соединения принимают контекст в качестве первого аргумента. Хотя в предыдущих примерах использовался context.Background(), эту возможность можно использовать для передачи настроек и дедлайнов, а также для отмены запросов. Передача контекста, созданного с помощью withDeadline, позволяет задавать ограничения на время выполнения запросов. Обратите внимание: это абсолютное время, и по истечении срока будет только освобождено соединение и отправлен сигнал отмены в ClickHouse. В качестве альтернативы для явной отмены запроса можно использовать WithCancel. Вспомогательные функции clickhouse.WithQueryID и clickhouse.WithQuotaKey позволяют указать Query id и ключ квоты. Query id может быть полезен для отслеживания запросов в журналах и для их отмены. Ключ квоты можно использовать, чтобы накладывать ограничения на использование ClickHouse на основе уникального значения ключа — подробнее см. в разделе Управление квотами. Контекст также можно использовать, чтобы настройка применялась только к конкретному запросу, а не ко всему соединению, как показано в разделе Настройки соединения. Наконец, размер буфера block можно задать с помощью clickhouse.WithBlockSize. Это переопределяет настройку уровня соединения BlockBufferSize и задает максимальное число blocks, которые декодируются и одновременно хранятся в памяти. Более высокие значения потенциально дают больше параллелизма, но ценой большего потребления памяти. Примеры всего перечисленного приведены ниже.
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
}
// контекст можно использовать для передачи настроек конкретному вызову API
ctx := clickhouse.Context(context.Background(), clickhouse.WithSettings(clickhouse.Settings{
    "async_insert": "1",
}))

// запросы можно отменить с помощью контекста
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")
}

// установить идентификатор запроса для трассировки запросов в журнале, например в 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"))
// установить ключ квоты — сначала создаём квоту
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
    }
}
Полный пример

Информация о Прогрессе, профиле и Log

Для запросов можно запрашивать информацию о Прогрессе, профиле и Log. Информация о Прогрессе содержит статистику о количестве строк и байтов, которые были прочитаны и обработаны в ClickHouse. В отличие от этого, информация профиля предоставляет сводку по данным, возвращённым клиенту, включая общее количество байтов (в несжатом виде), строк и блоков. Наконец, информация Log предоставляет статистику по потокам, например использование памяти и скорость обработки данных. Для получения этой информации пользователь должен использовать Context, в который можно передавать функции обратного вызова.
totalRows := uint64(0)
// используйте контекст для передачи колбэка с информацией о прогрессе и профиле
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()
Полный пример

Динамическое сканирование

Может потребоваться читать таблицы, для которых заранее неизвестны схема или типы возвращаемых полей. Это часто встречается при эпизодическом анализе данных или при разработке универсальных инструментов. Для этого в ответах на запросы доступна информация о типах столбцов. Её можно использовать вместе с рефлексией 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
}
Полный пример

Внешние таблицы

Внешние таблицы позволяют клиенту отправлять данные в ClickHouse вместе с запросом SELECT. Эти данные помещаются во временную таблицу и могут использоваться в самом запросе для вычислений. Чтобы отправить внешние данные вместе с запросом, пользователь должен создать внешнюю таблицу через 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, чтобы привязать спан к запросу через контекст.
Ограничение HTTP-транспортаХотя сервер ClickHouse принимает стандартные HTTP-заголовки traceparent / tracestate, HTTP-транспорт clickhouse-go сейчас их не отправляет — WithSpan не работает через HTTP. В качестве обходного решения можно задать заголовки вручную через 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.
Последнее изменение 10 июня 2026 г.