PostgreSQL扛到200万条记录时,成本聚合查询要跑3秒。切到ClickHouse后,同样的查询压进50毫秒。这不是Benchmark对比,是一个团队的真实踩坑记录。
导读:为什么你的分析查询越来越慢
![]()
日志表的增长曲线是欺骗性的。50万条时一切正常,200万条时开始报警,1000万条时数据库变成整个链路的最短板。这个团队的解法不是加索引、不是分库分表,而是直接换了存储引擎。他们的迁移决策逻辑,值得所有做LLM基础设施的团队参考。
一、PostgreSQL是怎么垮掉的
初始架构很标准:UUID主键,租户ID加创建时间联合索引,模型和功字段单索引。低流量阶段完全够用。
压垮它的是这个查询模式:
按租户筛选最近30天记录,按模型分组,求成本和次数。PostgreSQL是行式存储,算SUM(cost_usd)时要把整行读进内存再提取目标列。10万行数据时没感觉,1000万行时每次查询要扫几十万行。
更隐蔽的问题是MVCC开销。LLM日志只追加不修改,但PostgreSQL照样为每次写入维护多版本并发控制。写入量上来后,这部分成本纯纯是浪费。
二、ClickHouse的三张底牌
这个团队锁定ClickHouse,基于三个硬匹配:
「追加写入」对「零更新场景」。LLM请求日志不会被修改或删除(超出保留策略的清理除外)。ClickHouse专为高吞吐插入优化,没有PostgreSQL那种写密集型负载下的MVCC负担。
「列式存储」对「聚合查询」。查SUM(cost_usd) GROUP BY model时,ClickHouse只读cost_usd和model两列。PostgreSQL读整行。20列的表聚合2列,I/O量差10倍。
「物化视图」对「预聚合需求」。数据插入时触发视图更新,把汇总结果写进另一张表。看板直接读汇总表,不用碰原始数据。
参考案例:Langfuse用PostgreSQL做日志后端。月请求量低于100万、需要ACID事务的团队,这套方案能跑。超过100万且带分析查询模式,差距会显现出来。
三、新Schema的设计细节
主表结构:
request_id用String,tenant_id用String,model/provider/feature三个字段用LowCardinality(String)——ClickHouse的枚举优化类型,重复值多的字符串能大幅压缩。input_tokens和output_tokens用UInt32,cost_usd用Float64,latency_ms用UInt32,cached用UInt8,created_at用DateTime。
引擎选MergeTree,按月分区,排序键是(tenant_id, created_at)。index_granularity设8192,这是ClickHouse的默认粒度,每8192行建一个索引标记。
物化视图的设计更关键。汇总表cost_by_model_daily用AggregatingMergeTree引擎,字段包括tenant_id、model、day,以及三个聚合函数类型:total_cost用sum的AggregateFunction,total_reqs用count,avg_latency用avg。物化视图cost_by_model_mv把原始数据实时汇总进这张表。
四、迁移的隐性成本
文章没提但从业者都懂:换数据库不是改配置那么简单。
查询语法要重写。ClickHouse的SQL方言和PostgreSQL有差异,窗口函数、JOIN行为、类型转换都需要逐一核对。团队原有的ORM或查询构建工具可能直接作废。
运维监控要重建。慢查询日志、连接池管理、备份策略,整套SOP得重新写。ClickHouse的集群部署比单机PostgreSQL复杂一个数量级。
团队技能栈要补课。能调优PostgreSQL的DBA,不一定懂MergeTree的合并机制、分区策略、物化视图的刷新逻辑。
这个团队选择迁移,说明痛点已经大到愿意支付这些成本。50毫秒vs3秒,不是数字游戏,是产品体验的根本差异——看板加载从"转圈等待"变成"瞬时响应",分析迭代从"不敢点查询"变成"随便探索"。
五、什么情况下你该考虑换
三个信号同时出现时,PostgreSQL的索引优化已经到天花板:
查询模式是聚合分析型,不是点查型。你需要GROUP BY、SUM、COUNT、AVG,不是SELECT * WHERE id = ?。
数据只增不改,且增长速度快。LLM请求日志、埋点事件、时序指标,都是典型场景。
延迟敏感,且查询维度多变。今天按模型分组,明天按用户分组,后天按功能分组,列式存储的灵活性在这里体现。
反过来,如果你的月请求量还在10万以下,或者事务一致性是硬要求,PostgreSQL仍是更省心的选择。Langfuse的路径证明了这一点。
六、LowCardinality的巧思
Schema里反复出现的LowCardinality(String)值得单拎出来。
ClickHouse的模型字段通常只有几十种取值,provider可能是OpenAI/Anthropic/Google等固定几家,feature是产品内部的枚举值。LowCardinality把这些字符串字典化存储,实际存的是整数ID,查询时自动映射。
压缩率提升之外,更隐蔽的收益在聚合性能。GROUP BY model时,比较的是整数而非字符串,CPU缓存友好度大幅提升。这个优化对LLM日志场景几乎是量身定制——模型名称的枚举空间天然有限。
七、物化视图的权衡
预聚合不是免费午餐。这个团队的实现是插入时实时触发,好处是查询延迟极低,代价是写入路径变长。
如果汇总维度很多,物化视图的数量会爆炸。按模型、按用户、按功能、按模型×功能组合……每个维度组合都要一张汇总表。存储成本和写入延迟需要监控。
更激进的方案是延迟物化,用后台任务批量刷新。牺牲一点实时性,换取写入吞吐。这个团队没选这条路,说明他们的看板场景对实时性有硬要求。
八、分区策略的取舍
toYYYYMM(created_at)按月分区,是时序数据的经典做法。
好处是清理旧数据时直接删分区,比DELETE语句快几个数量级。LLM日志通常有保留期限,30天或90天后自动归档,这个设计让数据生命周期管理变得廉价。
风险是热点问题。如果写入集中在当前月份,所有新数据都往一个分区灌,可能触发MergeTree的合并压力。这个团队的租户ID在排序键里,实际写入会按租户分散,缓解了单分区热点。
九、排序键的设计逻辑
(tenant_id, created_at)作为ORDER BY,是多租户场景的标配。
第一个字段是查询最频繁的过滤条件。他们的典型查询都带tenant_id = $1,把租户ID放排序键首位,ClickHouse可以用稀疏索引快速定位数据范围。
第二个字段是时间范围。created_at > NOW() - INTERVAL '30 days'这种条件,在已按租户筛选的基础上,进一步缩小扫描区间。
注意model不在排序键里。如果查询经常是按模型分组但跨所有租户,这个设计会触发全表扫描。但从他们描述的查询模式看,tenant_id是必带的,所以当前设计是合理的。
十、AggregateFunction类型的陷阱
汇总表里的total_cost AggregateFunction(sum, Float64)不是普通Float64。这是ClickHouse的聚合状态类型,存的是中间计算结果而非最终值。
查询时要用sumMerge(total_cost)来终态化,直接SELECT total_cost会拿到二进制blob。这个细节坑过不少新手。
好处是增量更新极快。新数据进来时,聚合状态直接合并,不用重新计算全量。对于每天50万条写入、需要实时看板的场景,这是性能的关键支点。
十一、从50毫秒还能往哪优化
文章停在50毫秒,但从业者会追问下一步。
如果查询模式进一步固定,可以把物化视图的结果直接缓存到Redis,压到亚毫秒级。代价是实时性下降,需要设计失效策略。
如果数据量再涨两个数量级,可以考虑按租户分片,把不同租户路由到不同节点。ClickHouse的分布式表支持这个,但运维复杂度陡增。
如果成本敏感,可以把冷数据迁到对象存储,ClickHouse通过S3表引擎查询。延迟从50毫秒变500毫秒,但存储成本降一个数量级。
这些都不是这个团队当前的选择,但展示了同一条技术路径上的后续分支。
十二、给其他团队的决策框架
不是所有人都该换ClickHouse。这个案例的价值在于提供了一套判断标准:
先确认你的查询模式。聚合分析型负载是列式存储的主场,事务型负载不是。
再确认你的数据特征。只追加、不修改、时间序列,三条全中才值得考虑。
最后确认你的团队能力。能驾驭PostgreSQL调优,和能运维ClickHouse集群,是两套技能树。迁移成本不只是机器时间,是人的学习曲线。
这个团队的三秒到五十毫秒,是特定约束下的最优解。复制他们的Schema容易,复制他们的决策逻辑更难。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.