You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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格式返回,需要从三个维度获取数据:查询结果元信息、查询数据、查询统计信息,然后手动组装成目标结构。

  1. 定义对应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"`
}
  1. 从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))
}
  1. 注意事项
  • 若查询结果包含复杂类型(如Array、Map),需要在扫描时做对应的类型转换,确保map[string]interface{}能正确存储对应值
  • 单次查询统计需在查询前后分别获取Stats并计算差值,避免连接累计统计的干扰

内容的提问来源于stack exchange,提问作者calebeaires

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 20:41:33