chconn is a pure Go driver for ClickHouse using the native TCP protocol. It is designed for high-performance, column-oriented data access with full generics support.
If you have any suggestion or comment, please feel free to open an issue.
go get github.com/vahid-sohrabloo/chconn/v3Requires the latest two stable Go releases.
package main
import (
"context"
"fmt"
"github.com/vahid-sohrabloo/chconn/v3/chpool"
"github.com/vahid-sohrabloo/chconn/v3/column"
)
func main() {
conn, err := chpool.New("clickhouse://default:@localhost:9000/default")
if err != nil {
panic(err)
}
defer conn.Close()
// Create table
err = conn.Exec(context.Background(), `CREATE TABLE IF NOT EXISTS example (
id UInt64,
name String
) Engine=Memory`)
if err != nil {
panic(err)
}
// Insert
colID := column.New[uint64]()
colName := column.NewString()
colID.Append(1)
colName.Append("Alice")
colID.Append(2)
colName.Append("Bob")
err = conn.Insert(context.Background(), "INSERT INTO example (id, name) VALUES", colID, colName)
if err != nil {
panic(err)
}
// Select
colIDRead := column.New[uint64]()
colNameRead := column.NewString()
selectStmt, err := conn.Select(context.Background(), "SELECT id, name FROM example", colIDRead, colNameRead)
if err != nil {
panic(err)
}
defer selectStmt.Close()
for selectStmt.Next() {
for i := 0; i < selectStmt.RowsInBlock(); i++ {
fmt.Printf("id=%d name=%s\n", colIDRead.Row(i), colNameRead.Row(i))
}
}
if err := selectStmt.Err(); err != nil {
panic(err)
}
}The fastest way to insert data. Append values to typed columns and insert in bulk:
col1 := column.New[uint64]()
col2 := column.NewString()
col3 := column.New[uint64]().Nullable()
// Pre-allocate write buffers for large inserts
col1.SetWriteBufferSize(1_000_000)
col2.SetWriteBufferSize(1_000_000)
col3.SetWriteBufferSize(1_000_000)
for i := range 1_000_000 {
col1.Append(uint64(i))
col2.Append(fmt.Sprintf("row_%d", i))
if i%2 == 0 {
col3.Append(uint64(i))
} else {
col3.AppendNil()
}
}
err := conn.Insert(ctx, "INSERT INTO table (id, name, value) VALUES", col1, col2, col3)Read data block by block for maximum throughput:
colID := column.New[uint64]()
colName := column.NewString()
selectStmt, err := conn.Select(ctx, "SELECT id, name FROM table", colID, colName)
if err != nil {
return err
}
defer selectStmt.Close()
var ids []uint64
var names []string
for selectStmt.Next() {
ids = colID.Read(ids)
names = colName.Read(names)
}
if err := selectStmt.Err(); err != nil {
return err
}Familiar SQL-style row iteration with Query and Scan:
rows, err := conn.Query(ctx, "SELECT id, name FROM users WHERE age > {age: UInt32}",
chconn.IntParameter("age", 18),
)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id uint64
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
fmt.Printf("%d: %s\n", id, name)
}
return rows.Err()Map results directly to Go structs by field name:
type User struct {
ID uint64
Name string
Email string
}
// Collect all rows into a slice
users, err := chconn.QueryAll[User](ctx, conn, "SELECT id, name, email FROM users")
// Get exactly one row
user, err := chconn.QueryExactlyOne[User](ctx, conn,
"SELECT id, name, email FROM users WHERE id = {id: UInt64}",
chconn.UintParameter("id", 42),
)
// Iterator pattern
for user, err := range chconn.QueryIter[User](ctx, conn, "SELECT id, name, email FROM users") {
if err != nil {
return err
}
fmt.Println(user.Name)
}Scanning options:
RowToStructByName[T]— match columns to struct fields by name (case-insensitive)RowToStructByNameLax[T]— same, but tolerates extra/missing fieldsRowToStructByPos[T]— match by column positionRowTo[T]— auto-detect: struct by name,map[string]any, or scalar
For multi-batch inserts or row-by-row appending:
stmt, err := conn.InsertStream(ctx, "INSERT INTO table (id, name) VALUES")
if err != nil {
return err
}
// Row-by-row append
for i := range 1000 {
if err := stmt.Append(uint64(i), fmt.Sprintf("name_%d", i)); err != nil {
return err
}
}
// Or write column batches
col1 := column.New[uint64]()
col2 := column.NewString()
col1.Append(1001)
col2.Append("batch_row")
if err := stmt.Write(ctx, col1, col2); err != nil {
return err
}
return stmt.Flush(ctx)Type-safe query parameters using ClickHouse native parameter syntax:
rows, err := conn.Query(ctx,
"SELECT * FROM events WHERE ts > {start: DateTime} AND level = {level: String}",
chconn.StringParameter("start", "2024-01-01 00:00:00"),
chconn.StringParameter("level", "error"),
)Available parameter functions: IntParameter, UintParameter, Float32Parameter, Float64Parameter, StringParameter, and their slice variants (IntSliceParameter, etc.).
pool, err := chpool.New("clickhouse://user:password@localhost:9000/mydb")
// or with full config:
config, _ := chpool.ParseConfig("clickhouse://user:password@localhost:9000/mydb")
config.MaxConns = 10
config.MinConns = 2
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.HealthCheckPeriod = time.Minute
pool, err := chpool.NewWithConfig(ctx, config)The pool implements the same Select, Insert, Query, Exec methods as a single connection.
| ClickHouse Type | Go Column |
|---|---|
| Bool | column.New[bool]() |
| UInt8/16/32/64/128/256 | column.New[uint8](), column.New[types.Uint128](), etc. |
| Int8/16/32/64/128/256 | column.New[int8](), column.New[types.Int128](), etc. |
| Float32/64 | column.New[float32](), column.New[float64]() |
| BFloat16 | column.New[types.BFloat16]() |
| Decimal32/64/128/256 | column.New[types.Decimal32](), etc. |
| String | column.NewString() |
| FixedString(N) | column.New[[N]byte]() |
| Date, Date32 | column.NewDate[types.Date](), column.NewDate[types.Date32]() |
| DateTime, DateTime64 | column.NewDate[types.DateTime](), column.NewDate[types.DateTime64]() |
| Time, Time64 | column.New[types.ChTime](), column.New[types.ChTime64]() |
| UUID | column.New[types.UUID]() |
| IPv4, IPv6 | column.New[types.IPv4](), column.New[types.IPv6]() |
| Enum8, Enum16 | column.New[int8](), column.New[int16]() |
| Array(T) | col.Array() |
| Nullable(T) | col.Nullable() |
| LowCardinality(T) | col.LowCardinality() |
| Map(K, V) | column.NewMap[K, V](keyCol, valCol) |
| Tuple(T1, ..., Tn) | column.NewTuple(cols...) or typed column.NewTuple2[T](col1, col2) |
| Nested | column.NewNested(cols...) |
| JSON | column.NewJSON() |
| Variant(T1, ..., Tn) | column.NewVariant(cols...) |
| Dynamic | column.NewDynamic(cols...) |
| Point, Ring, Polygon, MultiPolygon | column.NewPoint() |
| Nothing | column.NewNothing() |
// LZ4 (recommended)
config.Compress = chconn.CompressLZ4
// ZSTD
config.Compress = chconn.CompressZSTDOr via connection string: clickhouse://localhost:9000/mydb?compress=lz4
clickhouse://user:pass@host:9440/db?sslmode=verify-full&sslrootcert=/path/to/ca.pemSSL modes: disable, allow, prefer (default), require, verify-ca, verify-full
Generate Go model structs and column boilerplate from ClickHouse tables:
# Install
go install github.com/vahid-sohrabloo/chconn/v3/cmd/chgen@latest
# Generate model from live table
chgen model -dsn "clickhouse://localhost:9000" -table users -out models/user.go
# Generate model from SQL file
chgen model -sql create_users.sql -out models/user.go
# Generate column declarations for insert/select
chgen columns -dsn "clickhouse://localhost:9000" -table users -out columns/user_columns.gosb := sqlbuilder.NewSelectBuilder()
query, _ := sb.Select("id", "name", "email").
From("users").
Where("age > 18").
PreWhere("active = 1").
OrderBy("name").
Limit(100).
Build()Supports: Select, From, Where, PreWhere, Having, GroupBy, OrderBy, Limit, Offset, Distinct, Final, Join (all types), ArrayJoin.
Export query results as JSON:
exporter := format.NewJSON(1000, func(data []byte, cols []column.ColumnCore) {
os.Stdout.Write(data)
})
selectStmt, _ := conn.Select(ctx, "SELECT * FROM table", cols...)
exporter.Read(selectStmt) // JSON objects
exporter.ReadCompact(selectStmt) // JSON arrays (compact)
exporter.ReadEachRow(selectStmt) // One JSON object per rowMonitor query execution in real time:
opts := &chconn.QueryOptions{
QueryID: "my-query-123",
OnProgress: func(p *chconn.Progress) {
fmt.Printf("Read %d rows, %d bytes\n", p.ReadRows, p.ReadBytes)
},
OnProfile: func(p *chconn.Profile) {
fmt.Printf("Result: %d rows in %d blocks\n", p.Rows, p.Blocks)
},
}
selectStmt, err := conn.SelectWithOption(ctx, "SELECT ...", opts, cols...)var chErr *chconn.ChError
if errors.As(err, &chErr) {
fmt.Printf("ClickHouse error %d: %s\n", chErr.Code, chErr.Message)
fmt.Println(chErr.StackTrace)
}
// Sentinel errors
if errors.Is(err, chconn.ErrNoRows) { ... }
if errors.Is(err, chconn.ErrTooManyRows) { ... }All operations accept context.Context for timeouts and cancellation:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := conn.Insert(ctx, "INSERT INTO ...", cols...)- Go: latest two stable releases
- ClickHouse: 24.3+
Tested in CI against ClickHouse 24.3, 24.8, 25.3, 25.8, and 26.2.
For more information, please see the documentation and Go package reference.
For comparison with other Go ClickHouse drivers, see the benchmark repository and ch-bench results.
name \ time/op chconn chgo go-clickhouse uptrace
TestSelect100MUint64-16 150ms 154ms 8019ms 3045ms
TestSelect10MString-16 271ms 447ms 969ms 822ms
TestInsert10M-16 198ms 514ms 561ms 304ms
name \ alloc/op chconn chgo go-clickhouse uptrace
TestSelect100MUint64-16 111kB 262kB 3202443kB 800941kB
TestSelect10MString-16 1.63MB 1.79MB 1626.51MB 241.03MB
TestInsert10M-16 26.0MB 283.7MB 1680.4MB 240.2MB
name \ allocs/op chconn chgo go-clickhouse uptrace
TestSelect100MUint64-16 35 6683 200030937 100006069
TestSelect10MString-16 49 1748 30011991 20001120
TestInsert10M-16 26 80 224 50