Введение
Пакетная запись также называется массовой записью.Для сценария вставки нескольких фрагментов данных в одну таблицу это может уменьшить количество запросов на вставку и повысить пропускную способность и эффективность. Официальный драйвер Golang от Clickhouse clickhouse-go [1] поддерживает эту ключевую функцию, но вступление к документу не очень подробное, всего одно предложение:
Bulk write support : begin->prepare->(in loop exec)->commit
Он не представляет подробно использование и принципы.Библиотека, используемая автором при разработке бизнеса, - sqlx [2] , и sql также поддерживает драйвер clickhouse-go. Обратитесь к официальному образцу кода [3] :
...
tx, err := connect.Begin()
checkErr(err)
stmt, err := tx.Prepare("INSERT INTO example (country_code, os_id, browser_id, categories, action_day, action_time) VALUES (?, ?, ?, ?, ?, ?)")
checkErr(err)
for i := 0; i < 100; i++ {
if _, err := stmt.Exec(
"RU",
10+i,
100+i,
[]int16{1, 2, 3},
time.Now(),
time.Now(),
); err != nil {
log.Fatal(err)
}
}
...
Массовая запись, которую я написал, аналогична приведенному выше коду, но когда он был отправлен коллеге для проверки, он поднял вопрос: отправляет ли stmt.Exec запрос на запись в базу данных каждый раз, когда он выполняется ? На самом деле, я не уверен в этом вопросе, и официальные документы не ясны. Учитывая строгость, чтобы сделать мой пиар более убедительным, я сам проверил соответствующий исходный код.
Здесь следует отметить, что если вы используете функцию перехода по коду в редакторе, вы перейдете к реализации функции database/sql
в библиотеке Exec
, На самом деле код, который мы хотим увидеть, является реализацией в clickhouse-go, Что касается причины почему редактор прыгает на базу данных/sql, я не разобрался, когда писал эту статью, так что давайте сначала копать яму .
основная реализация
Основной код stmt.Exec выглядит следующим образом [4] :
func (stmt *stmt) execContext(ctx context.Context, args []driver.Value) (driver.Result, error) {
if stmt.isInsert {
stmt.counter++
if err := stmt.ch.block.AppendRow(args); err != nil {
return nil, err
}
if (stmt.counter % stmt.ch.blockSize) == 0 {
stmt.ch.logf("[exec] flush block")
if err := stmt.ch.writeBlock(stmt.ch.block); err != nil {
return nil, err
}
if err := stmt.ch.encoder.Flush(); err != nil {
return nil, err
}
}
return emptyResult, nil
}
if err := stmt.ch.sendQuery(stmt.bind(convertOldArgs(args))); err != nil {
return nil, err
}
if err := stmt.ch.process(); err != nil {
return nil, err
}
return emptyResult, nil
}
Вышеприведенный код невелик и очень нагляден, при выполнении Exec stmt.ch.block.AppendRow(args)
параметр sql сначала будет привязан к блоку локального кеша, а потом уже (stmt.counter % stmt.ch.blockSize)
будет судить, достигает ли размер локального кеша порога, и достигает ли он порога , он будет выполнен, и Flush()
данные будут записаны на удаленный конец. Подводя итог, основная логика реализации в clickhouse-go такова:
Нижний уровень поддерживает блок кеша и устанавливает block_size для управления размером кеша.
При выполнении stmt.Exec не будет напрямую писать в удаленный ClickHouse, а будет вставлять в блок параметр Append
После каждого Append судите о соотношении между размером блока и block_size, если точно делится, обновляйте блок (то есть пишите в clickhouse)
Поэтому параметр block_size очень важен, он представляет собой верхний предел локального кеша, если он слишком велик, то программа будет занимать часть памяти. Сначала автор поставил так, чтобы 100000
в журнале отладки не было видно stmt.ch.logf("[exec] flush block")
распечатанный лог.После настройки мало, можно увидеть следующий вывод:
...
[clickhouse][connect=1][begin] tx=false, data=false
[clickhouse][connect=1][prepare]
[clickhouse][connect=1][read meta] <- data: packet=1, columns=6, rows=0
[clickhouse][connect=1][exec] flush block
[clickhouse][connect=1][exec] flush block
....
Подведем итог
Многие драйверы БД поддерживают функцию массовой записи, и драйвер clickhouse-go не исключение, но его документация не очень подробная, но в документации указано, что это нужно делать в begin/commit. Кроме того, clickhouse не поддерживает транзакции, и формулировка begin/commit может сбивать с толку.
В этой статье анализируется исходный код clickhouse-go, разбирается процесс выполнения массовой записи и помогает разобраться в его конкретной реализации.
Рекомендации
[1]
кликхаус-го: https://github.com/ClickHouse/clickhouse-го
[2]sqlx: https://github.com/jmoiron/sqlx
[3]Официальный пример кода: https://github.com/ClickHouse/clickhouse-go/blob/master/examples/sqlx.go#L35-L51 .
[4]Основной код выглядит следующим образом: https://github.com/clickhouse/clickhouse-go/blob/master/stmt.go#L44-L68 .
[5]Заявление INSERT INTO: https://clickhouse.tech/docs/en/sql-reference/statements/insert-into/
[6]go-clickhouse-batchinsert: https://github.com/MaruHyl/go-clickhouse-batchinsert/blob/master/batch.go#L349-L354