ClickHouse Go 驱动需显式启用 HTTP 协议(?protocol=http)并禁用压缩(compress=false)才能实现流式查询;应避免高层 API 触发全量加载,改用 http.Client + stream=1 + JSONEachRow + json.Decoder 逐行解析,并统一使用 UTC 时间戳与显式时区构造条件。

ClickHouse 官方 Go 驱动不支持原生 HTTP 协议的流式响应
直接用 github.com/ClickHouse/clickhouse-go(v2)连接 ClickHouse 时,如果日志查询结果超百万行,rows.Scan() 会把全部结果加载进内存,OOM 风险极高。这不是你 SQL 写得不对,而是驱动默认使用 TCP 协议 + 块缓冲机制,且不暴露底层 reader。
- 必须显式启用 HTTP 协议连接:在 DSN 中加上
?protocol=http,否则走的是二进制协议,无法利用 ClickHouse 的stream模式 - HTTP 模式下需手动设置
compress=false,否则 Go 的http.Transport默认解压会破坏 ClickHouse 的压缩流格式(尤其用 LZ4 时) - 别用
db.QueryRow()或rows.Columns()做元信息探测——HTTP 模式下这些调用可能触发完整结果拉取,绕过流式本意
用 clickhouse-go/v2 的 Query 方法实现逐行流式读取
不是所有 Query 调用都流式;只有配合 ch.WithQueryID() 和手动构造 http.Request 才能真正控制连接生命周期。更稳妥的做法是绕过高层封装,直接用 http.Client 发起带 stream=1 参数的 GET 请求。
- 构造 URL 示例:
http://user:pass@localhost:8123/?database=default&query=SELECT%20*%20FROM%20logs%20WHERE%20ts%3E%3D%272024-01-01%27&stream=1&format=JSONEachRow - 务必设置
Timeout和KeepAlive:ClickHouse 在 stream 模式下可能长时间保持连接,http.DefaultClient的默认 30s timeout 会导致中断 - 响应体是 JSON 每行一条记录,用
json.Decoder逐条解码,避免json.Unmarshal([]byte)全量解析
处理日志时间范围查询时,DateTime64 和时区容易导致漏数据
ClickHouse 的 DateTime64(3, 'UTC') 字段,在 Go 里用 time.Time 解析时若未显式指定 Location,会按本地时区解释字符串,造成 where 条件偏移。
- 写入日志时统一用 UTC 时间戳(推荐
toUnixTimestamp64Milli(now64())),查询时也用 UTC 构造条件:ts >= toDateTime64('2024-01-01 00:00:00', 3, 'UTC') - Go 中构造 time 变量必须绑定 UTC:
t := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),不能依赖time.Now().UTC()后再转格式——时区转换可能丢失精度 - 避免用
BETWEEN:ClickHouse 对DateTime64的BETWEEN边界处理有隐式截断,改用ts >= ? AND ts 更可靠
聚合查询返回大量分组时,GROUP BY 内存爆掉的应急方案
查“每分钟错误码分布”这类需求,如果日志量大、错误码离散度高,ClickHouse 默认的 max_bytes_before_external_group_by 可能不够,报错 Memory limit (for query) exceeded。
立即学习“go语言免费学习笔记(深入)”;
- 临时调大参数:在 query string 里加
&settings=max_bytes_before_external_group_by=2000000000(2GB),但治标不治本 - 更稳的方式是拆分时间粒度:先按小时聚合(
toStartOfHour(ts)),再对每小时结果做二次分组,用 Go 控制并发请求 - 禁用
ORDER BY:除非真要排序,否则GROUP BY后加ORDER BY count() DESC会强制全局排序,极大拖慢并显著增加内存占用
流式读取和时区对齐是日志分析中最容易被跳过的两环,一不留神就查到旧数据或 OOM,而不是模型或算法的问题。


















