如何用Golang ClickHouse库复刻ClickHouse Client CLI的JSON查询结果格式?
如何用clickhouse-go/v2实现ClickHouse CLI JSON格式的完整返回结果
当使用clickhouse-client命令行工具并指定JSON格式执行查询时,会同时返回查询结果及统计信息,示例如下:
执行命令:
clickhouse-client --password=test --user=default --format=JSON --query="select 1 + 1"
返回结果:
{ "meta": [ { "name": "plus(1, 1)", "type": "UInt16" } ], "data": [ { "plus(1, 1)": 2 } ], "rows": 1, "statistics": { "elapsed": 0.001043427, "rows_read": 1, "bytes_read": 1 } }
尝试使用Golang官方ClickHouse库github.com/ClickHouse/clickhouse-go/v2实现该格式的返回结果但未成功,求实现思路或解决方案。
实现思路与代码示例
要实现和CLI一致的JSON格式返回,需要从三个维度获取数据:查询结果元信息、查询数据、查询统计信息,然后手动组装成目标结构。
- 定义对应JSON结构的Go结构体
先定义和CLI返回格式完全匹配的结构体:
type ClickHouseJSONResponse struct { Meta []ColumnMeta `json:"meta"` Data []map[string]interface{} `json:"data"` Rows int `json:"rows"` Statistics QueryStatistics `json:"statistics"` } type ColumnMeta struct { Name string `json:"name"` Type string `json:"type"` } type QueryStatistics struct { Elapsed float64 `json:"elapsed"` RowsRead int `json:"rows_read"` BytesRead int `json:"bytes_read"` }
- 从clickhouse-go/v2中获取各部分数据
- 元信息(Meta):通过查询结果的
Columns()和ColumnTypes()方法获取列名与类型 - 查询数据(Data):逐行读取结果,将每行数据转为
map[string]interface{} - 统计信息(Statistics):通过连接的
Stats()方法获取查询耗时、读取行数和字节数
完整示例代码:
package main import ( "context" "encoding/json" "fmt" "log" "github.com/ClickHouse/clickhouse-go/v2" ) type ClickHouseJSONResponse struct { Meta []ColumnMeta `json:"meta"` Data []map[string]interface{} `json:"data"` Rows int `json:"rows"` Statistics QueryStatistics `json:"statistics"` } type ColumnMeta struct { Name string `json:"name"` Type string `json:"type"` } type QueryStatistics struct { Elapsed float64 `json:"elapsed"` RowsRead int `json:"rows_read"` BytesRead int `json:"bytes_read"` } func main() { ctx := context.Background() // 初始化ClickHouse连接 conn, err := clickhouse.Open(&clickhouse.Options{ Addr: []string{"localhost:9000"}, Auth: clickhouse.Auth{ Database: "default", Username: "default", Password: "test", }, }) if err != nil { log.Fatal(err) } defer conn.Close() // 执行查询 query := "select 1 + 1" rows, err := conn.Query(ctx, query) if err != nil { log.Fatal(err) } defer rows.Close() // 构建Meta信息 var meta []ColumnMeta cols := rows.Columns() colTypes, _ := rows.ColumnTypes() for i, col := range cols { meta = append(meta, ColumnMeta{ Name: col, Type: colTypes[i].DatabaseTypeName(), }) } // 读取Data数据 var data []map[string]interface{} for rows.Next() { rowMap := make(map[string]interface{}) // 动态扫描每行数据到map values := make([]interface{}, len(cols)) valuePtrs := make([]interface{}, len(cols)) for i := range values { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { log.Fatal(err) } for i, col := range cols { rowMap[col] = values[i] } data = append(data, rowMap) } if err := rows.Err(); err != nil { log.Fatal(err) } // 获取单次查询的统计信息(避免累计统计干扰) preStats := conn.Stats() // 这里重新执行查询是为了精准统计,实际业务中可在首次查询前后记录Stats rows, err = conn.Query(ctx, query) if err != nil { log.Fatal(err) } rows.Close() postStats := conn.Stats() queryStats := QueryStatistics{ Elapsed: postStats.Elapsed.Sub(preStats.Elapsed).Seconds(), RowsRead: int(postStats.RowsRead - preStats.RowsRead), BytesRead: int(postStats.BytesRead - preStats.BytesRead), } // 组装完整响应 response := ClickHouseJSONResponse{ Meta: meta, Data: data, Rows: len(data), Statistics: queryStats, } // 转为JSON输出 jsonData, err := json.MarshalIndent(response, "", " ") if err != nil { log.Fatal(err) } fmt.Println(string(jsonData)) }
- 注意事项
- 若查询结果包含复杂类型(如Array、Map),需要在扫描时做对应的类型转换,确保
map[string]interface{}能正确存储对应值 - 单次查询统计需在查询前后分别获取Stats并计算差值,避免连接累计统计的干扰
内容的提问来源于stack exchange,提问作者calebeaires
相关产品推荐
相关产品推荐

