首页
/ Langfuse 的 ClickHouse 最佳实践:28 条 Schema、查询与写入规则的实战指南

Langfuse 的 ClickHouse 最佳实践:28 条 Schema、查询与写入规则的实战指南

2026-09-09 09:46:27作者:彭桢灵Jeremy

Langfuse 将 Trace、Observation、Score 等核心可观测数据存储在 ClickHouse 中,并沉淀了一套基于 ClickHouse 官方最佳实践的 Agent Skill(位于 .agents/skills/clickhouse-best-practices)。它把 ClickHouse 列式存储、稀疏索引、MergeTree 引擎特性下最容易被"通用数据库直觉"误导的经验,编码为 28 条原子规则(Schema 设计 / 查询优化 / 数据写入 三大类),并针对 Langfuse 的 events 表、canonical 迁移模板与查询归因体系给出专属约束。读完本文,你将掌握这套规则的全部内容、Langfuse 代码库中的落地位置,以及在任何 ClickHouse 项目中可复用的审查清单与排障方法。

这份 Skill 是什么:面向 Agent 的 ClickHouse 权威指南

clickhouse-best-practices 是一个以 Review(审查)为工作流核心的 Agent 技能包:当开发者或 AI 助手面对 CREATE TABLEALTER TABLE、慢查询排查、JOIN 优化、写入管线设计、更新/删除策略等场景时,必须先按规则目录逐条核对,再给出结论。

其使用优先级明确写在第 16 行起的"IMPORTANT: How to Apply This Skill"中:

  1. 先在 rules/ 目录中检查是否有适用的规则文件;
  2. 若有规则,应用规则并在回复中引用(格式为 "Per rule-name...");
  3. 若无规则,再使用 LLM 的 ClickHouse 知识或检索官方文档;
  4. 仍不确定时,使用联网搜索获取当前最佳实践;
  5. 始终标注来源:规则名、或 "general ClickHouse guidance"。

之所以让规则优先于通用直觉,文档给出了根本原因:ClickHouse 有特殊的列式存储、稀疏索引与 MergeTree 合并机制,通用数据库经验在这里极易产生误导(例如"ORDER BY 可以随时改""频繁 UPDATE 没关系""索引越多越好"在 ClickHouse 中都不成立),而规则编码的是经过验证的 ClickHouse 专属经验。

规则文件本身遵循统一模板(见 rules/_template.md):每个规则文件包含 YAML frontmatter(title / impact / tags)、为什么这条规则重要、反例 + 解释、正例 + 解释、以及权衡与适用场景。28 条规则按影响度优先级组织为下表:

优先级 类别 影响 前缀 规则数
1 主键选择 CRITICAL schema-pk- 4
2 数据类型选择 CRITICAL schema-types- 5
3 JOIN 优化 CRITICAL query-join- 5
4 插入批量 CRITICAL insert-batch- 1
5 避免 Mutation CRITICAL insert-mutation- 2
6 分区策略 HIGH schema-partition- 4
7 跳数索引 HIGH query-index- 1
8 物化视图 HIGH query-mv- 2
9 异步插入 HIGH insert-async- 2
10 避免 OPTIMIZE HIGH insert-optimize- 1
11 JSON 使用 MEDIUM schema-json- 1

Langfuse 专属规则:代码库内的硬性约束

SKILL.md 用一整节篇幅定义了在 Langfuse 仓库内必须遵守的专属规则,每条都能在源码中找到对应实现,是理解 Langfuse ClickHouse 架构的关键入口。

1. 查 events 表必须走查询构建器

规则原文:查询 events 表必须使用 event-query-builder.ts除非先确认构建器无法表达该查询,否则不得手写 events SQL。这保证了 events 这类核心热路径表的所有查询都经过统一的过滤、投影与分页处理,避免手写 SQL 引入不一致。

2. 绝不在 events 表上使用 FINAL

events 表在设计上保证不需要 FINAL,而 FINAL 关键字会显著拖慢性能。这一点与通用规则 insert-optimize-avoid-final 呼应:OPTIMIZE TABLE ... FINAL 会强制合并所有 parts、重写整个分区,而 SELECT 侧的 FINAL 修饰符也会带来额外的合并开销,在 events 这类高频表上是被禁止的。

3. 查询归因:log_comment 中的 JSON

Langfuse 将查询归因信息写入 ClickHouse 的 system.query_log.log_comment 字段,JSON 结构由 queryTags.ts 定义。读取方式为:

JSONExtractString(log_comment, 'surface')
JSONExtractString(log_comment, 'route')
JSONExtractString(log_comment, 'projectId')

已知的 surface 取值为 trpcpublicapiworkermcpunknown;其中 ClickhouseWriter 的插入使用 projectId = "MULTI_PROJECT"(因为写入是跨项目批量聚合的,不属于任何单个项目)。

归因的传播链路是:入口点调用 headerPropagation.ts 中的 contextWithLangfuseProps(...),通过 OpenTelemetry baggage 携带 surface、可选的 route 与可选的 projectId;ClickHouse repository 层再用 normalizeClickHouseQueryTags(...) 读取 baggage 并写入 log_comment。实践要求是:在入口点设置归因,而不是在每个 repository 调用中传递 tags,这样归因信息能沿调用链自动传播。

4. canonical 迁移模板与集群占位符

packages/shared/clickhouse/migrations/canonical/唯一的 canonical 模板树,同时渲染为集群版与非集群版安装。两条硬性要求:

  • 每个集群感知的 DDL 位置都必须放置 {CLICKHOUSE_CLUSTER_CLAUSE}
  • {CLICKHOUSE_REPLICATION_PREFIX} 只用于两种模式下刻意不同的引擎——有些表在两种模式下都故意保持非复制(例如 events_coreevents_full 等中间表),不要机械地给所有表加复制前缀。

0041_create_events_core_mv.up.sql 为例,可以看到模板占位符与 TO 目标表物化视图的实际形态:

CREATE MATERIALIZED VIEW IF NOT EXISTS events_core_mv {CLICKHOUSE_CLUSTER_CLAUSE} TO events_core AS
SELECT ... FROM events_full;

5. alter_syncmutations_sync:跨迁移文件的竞态

这是 Langfuse 踩坑后沉淀出的最精细规则之一。每一个新的 canonical 迁移中的元数据 ALTERADD/DROP/MODIFY COLUMNADD/DROP INDEX)都必须附带:

{CLICKHOUSE_CLUSTERED_ONLY: SETTINGS alter_sync = 2}

而每个产生 mutation 的 ALTERMATERIALIZE …UPDATEDELETE)必须附带:

{CLICKHOUSE_CLUSTERED_ONLY: SETTINGS mutations_sync = 2}

注意:即使单个迁移文件只含一条 ALTER 也必须加,因为竞态发生在"迁移文件之间"而非"文件内部"。原因链条如下:alter_sync 默认值为 1,语句只要发起副本在 Keeper 中提升了表的元数据版本号就立即返回;随后 golang-migrate 立即打开下一个迁移文件,其对该表的第一条 ALTER 可能落在仍停留在旧元数据版本的副本上,ClickHouse 会因副本元数据版本落后于公共版本而拒绝入队并整体中止运行(错误码 517)

两个关键澄清:

  • mutations_sync 不能替代 alter_sync:前者管的是 mutation 何时完成,后者管的是元数据传播;
  • 渲染器对非集群版 MergeTree 迁移会省略这些片段{CLICKHOUSE_UNCLUSTERED_ONLY:...} 只用于刻意的模式差异;
  • 不要为已发布的迁移"规范化"补加同步设置——历史兼容性测试刻意保护其既有输出,两条渲染模式都必须通过 prepareMigrations.test.ts

6. 迁移中禁用 CREATE OR REPLACE VIEW

规则明确禁止在 ClickHouse 迁移中使用 CREATE OR REPLACE VIEW(以及 CREATE OR REPLACE TABLE / EXCHANGE TABLES):原子替换依赖 renameat2 文件系统能力,而 NFS 托管的自托管部署(如 AWS EFS 上的 ClickHouse 数据盘)不支持,迁移会失败并导致部署启动中止(GitHub issue #14906)。

正确的替代做法是在同一个迁移文件中用两条语句重定义普通视图:

DROP VIEW IF EXISTS <name> {CLICKHOUSE_CLUSTER_CLAUSE};
CREATE VIEW <name> {CLICKHOUSE_CLUSTER_CLAUSE} AS …;

配套注意点:迁移执行器传 x-multi-statement=true,golang-migrate 按 ; 拆分文件且不做 SQL 解析,所以注释和字符串字面量里不能出现分号;每条语句都要幂等(IF EXISTS / IF NOT EXISTS),这样 migrate force 后脏的半应用迁移可以重跑。drop→create 窗口内读取视图会瞬时失败——对 analytics_* 导出视图可以接受,因此普通视图应避免放在产品热路径上

7. 物化视图禁止 drop-and-recreate

源表仍在接收实时写入时,绝不可通过 DROP 后 CREATE 来更新物化视图:DROP 与 CREATE 之间写入的每一行都会静默且永久丢失。正确做法是用 MODIFY QUERY 原地替换 SELECT:

ALTER TABLE <mv> {CLICKHOUSE_CLUSTER_CLAUSE} MODIFY QUERY <select>

这会无中断地切换转换逻辑。当新查询新增列时,对目标表执行 ALTER ... ADD COLUMN IF NOT EXISTS …(这些目标表 ALTER 必须携带集群版 alter_sync 模板片段,确保没有任何主机在新列就绪前应用新 MV 查询), MODIFY QUERYMODIFY QUERY 仅对 TO 表型 MV 可行——Langfuse 的所有 MV 都使用 TO(如 0041_create_events_core_mv.up.sql 中的 TO events_core)。

Schema 设计规则(主键、类型、分区、JSON)

主键选择:4 条 CRITICAL 规则

schema-pk-plan-before-creation:ClickHouse 的 ORDER BY 决定物理排序与稀疏索引,且建表后不可修改,选错只能重建表并全量迁移数据。因此必须在建表前分析查询模式(列出 Top 5-10 查询、统计 WHERE 列频率、优先能排除大量行的列、限制在 4-5 个键列),再定 ORDER BY。反例与正例见 schema-pk-plan-before-creation.md

schema-pk-cardinality-order:稀疏索引按数据块(granule)工作,低基数列放在前面才能实现整块跳过。UUID 打头意味着每个 granule 的 event_id 都不同、索引无法跳过任何块。推荐列序:低基数(event_type、status、country)→ 日期粗粒度(toDate(timestamp))→ 中高基数(user_id、session_id)→ 高基数(event_id、uuid)。技巧:能用日级过滤时优先 toDate(timestamp) 而非原生 DateTime,索引体积从 32 位降到 16 位。

schema-pk-prioritize-filters:把频繁出现在过滤条件中的列纳入键。

schema-pk-filter-on-orderby:查询过滤必须使用 ORDER BY 前缀列,否则无法利用主键裁剪(该规则同时出现在查询审查清单中)。

数据类型:5 条 CRITICAL 规则

schema-types-native-types:全 String 建表浪费存储、阻碍压缩、拖慢比较。正例映射(详见 schema-types-native-types.md):UUID 用 UUID(16 字节而非 36)、ID 用 UInt32/UInt64、状态/类别用 Enum8LowCardinality(String)、时间戳用 DateTime、纯日期用 Date、计数用最小可容纳的 UInt8/16/32、金额用 Decimal(P,S) 或分单位的 Int64、布尔用 Bool

schema-types-minimize-bitwidth:选用能容纳数据的最小数值类型。

schema-types-lowcardinality:唯一值 < 10,000 的字符串列用 LowCardinality(String)(字典编码),> 10,000 用普通 StringFixedString 仅限严格定长数据(如 2 位国家码)。先用 SELECT uniq(column_name) FROM table_name 确认基数再决定。Langfuse 的 events_full 建表 正是这一规则的落地:typeenvironmentlevel 均声明为 LowCardinality(String),成本字段使用 Decimal(18,12),usage/cost 明细用 Map(LowCardinality(String), UInt64)Map(LowCardinality(String), Decimal(18,12))

schema-types-enum:有限取值集合且需要校验时用 Enum

schema-types-avoid-nullable:尽量避开 Nullable,用 DEFAULT 替代(Nullable 列无法利用某些压缩与优化路径)。

分区:4 条 HIGH 规则

  • schema-partition-low-cardinality:分区数量控制在 100-1,000 个,避免分区爆炸;
  • schema-partition-lifecycle:分区服务于数据生命周期管理(TTL、删除、冷热),而不是查询加速;
  • schema-partition-query-tradeoffs:理解分区裁剪的权衡;
  • schema-partition-start-without:小表可考虑先不分区,后续按需添加。

JSON:1 条 MEDIUM 规则

schema-json-when-to-use:动态 schema 才用 JSON 类型;字段已知时用类型化列(Langfuse 中 Map(...) 与显式列即属此类)。

查询优化规则(JOIN、索引、物化视图)

JOIN:5 条 CRITICAL 规则

query-join-choose-algorithm:ClickHouse 默认 hash join 会把右表整体载入内存,算法选择表如下:

算法 适用场景 权衡
parallel_hash 中小型可入内存的表 24.11 起默认;快、并发
hash 通用、所有 JOIN 类型 单线程构建哈希表
direct 字典查找(INNER/LEFT) 最快,不构建哈希表
full_sorting_merge 已按连接键排序的表 省去排序,内存占用低
partial_merge 大表、内存受限 内存最小化,执行较慢
grace_hash 大数据集、内存可调 灵活,可落盘
auto 自适应选择 先试 hash,内存压力时回退

设置示例:SET join_algorithm = 'auto';、内存受限的大表联查用 SET join_algorithm = 'partial_merge';、按主键列连接用 full_sorting_merge注意 ClickHouse 24.12+ 会自动把小表放右侧;更早版本需手动保证小表在 RIGHT 侧。

query-join-use-any:只需一条匹配时用 ANY JOIN(LEFT ANY JOIN / INNER ANY JOIN / RIGHT ANY JOIN),返回首个匹配、内存更少、执行更快。

query-join-filter-before:JOIN 之前先过滤各表,而不是 JOIN 之后再 WHERE。

query-join-consider-alternatives:考虑用字典(Dictionary)或反规范化替代 JOIN。

query-join-null-handlingjoin_use_nulls=0 以使用默认值处理无匹配。

索引:1 条 HIGH 规则

query-index-skipping-indices:对不在 ORDER BY 前缀里但需要过滤的列,使用跳数索引(data skipping index)

物化视图:2 条 HIGH 规则

query-mv-incremental:实时聚合用增量物化视图——写入时自动对新区块应用视图查询,结果写入目标表并随时间合并。要点:MV 中用 -State 聚合函数(countStateuniqState),查询时用 -Merge 函数(countMergeuniqMerge);目标表用 AggregatingMergeTree已有历史数据不会自动包含,需单独回填。Langfuse 的 events_core_mv 就是从 events_full 增量投影到 events_core 的 TO 表 MV(见 0041)。

query-mv-refreshable:复杂 JOIN 场景用可刷新物化视图。

数据写入规则(批量、异步、Mutation、OPTIMIZE)

批量插入:1 条 CRITICAL 规则

insert-batch-size:每次 INSERT 生成一个新 part,单行/小批量会产生成千上万的小 part,压垮合并进程。推荐:最小 1,000 行、理想 10,000-100,000 行、同步插入约每秒 1 次。监控 part 数(超过每分区约 3,000 会阻塞插入):

SELECT table, count() as parts, sum(rows) as total_rows
FROM system.parts
WHERE active AND database = 'default'
GROUP BY table
ORDER BY parts DESC;

异步插入:2 条 HIGH 规则

insert-async-small-batches:客户端无法批量时启用服务端缓冲的 async inserts:

SET async_insert = 1;
SET wait_for_async_insert = 1;  -- 确认持久化

刷新触发条件(任一先到):缓冲达到 async_insert_max_data_size、超时 async_insert_busy_timeout_ms、或累积的插入查询数达到上限。返回模式上 wait_for_async_insert=1 推荐(等待落盘、确认持久化),=0 为 fire-and-forget、出错无感知,仅在可接受数据丢失时使用

insert-format-native:优先用 Native 格式以获得最佳性能。

Mutation:2 条 CRITICAL 规则

insert-mutation-avoid-updateALTER TABLE UPDATE 是 mutation——异步后台进程会重写受影响的整个 part,写入放大、磁盘 I/O 尖峰、不可回滚、读取可能混合新旧 part。更新模式应改用 ReplacingMergeTree(version_col):插入新版本行、查询用 FINALargMax 取最新。Langfuse 大量采用该模式(如 ReplacingMergeTree 表配合版本列处理可变字段)。

insert-mutation-avoid-delete:删除优先用轻量删除(lightweight DELETE)或 DROP PARTITION,而非 ALTER TABLE DELETE

OPTIMIZE:1 条 HIGH 规则

insert-optimize-avoid-finalOPTIMIZE TABLE ... FINAL 会强制立即合并全部分区、重写整个分区、无视约 150GB 的 part 大小保护、可能引发内存压力/OOM,应依赖后台合并。注意 OPTIMIZE FINAL ≠ SELECT 的 FINAL:ReplacingMergeTree 去重场景下 SELECT 用 FINAL 是必要且可接受的。OPTIMIZE FINAL 仅限一次性场景(冻结前收尾、导出准备)。

三套审查流程:Schema / 查询 / 写入

SKILL.md 把 28 条规则组织成三条可执行的 Review 路径:

Schema 审查CREATE TABLE / ALTER TABLE)按顺序读 schema-pk-plan-before-creationschema-pk-cardinality-orderschema-pk-prioritize-filtersschema-types-native-typesschema-types-minimize-bitwidthschema-types-lowcardinalityschema-types-avoid-nullableschema-partition-low-cardinalityschema-partition-lifecycle,检查项包括:主键/ORDER BY 列序(低到高基数)、类型匹配实际数据范围、LowCardinality 应用、分区键基数有界(100-1,000)、ReplacingMergeTree 有版本列、新 canonical 迁移中的 ALTER 同步片段、无 CREATE OR REPLACE VIEW/TABLE、MV 不 drop-and-recreate。

查询审查SELECT / JOIN / 聚合)按顺序读 query-join-choose-algorithmquery-join-filter-beforequery-join-use-anyquery-index-skipping-indicesschema-pk-filter-on-orderby,检查:过滤是否用 ORDER BY 前缀列、JOIN 前是否先过滤、JOIN 算法是否匹配表规模、非 ORDER BY 过滤列是否用跳数索引。

写入策略审查(摄入 / 更新 / 删除)按顺序读 insert-batch-sizeinsert-mutation-avoid-updateinsert-mutation-avoid-deleteinsert-async-small-batchesinsert-optimize-avoid-final,检查:批量 10K-100K 行、高频变更不用 ALTER TABLE UPDATE、更新模式用 Replacing/CollapsingMergeTree、高频小批量启用异步插入。

标准输出格式:让审查结论可引用

规则要求审查回复遵循固定结构,便于机器与人类同时消费:

## Rules Checked
- rule-name-1 - Compliant / Violation found
- rule-name-2 - Compliant / Violation found
...

## Findings
### Violations
- rule-name: 问题描述
  - Current: 当前代码做了什么
  - Required: 应该怎么做
  - Fix: 具体修正

### Compliant
- rule-name: 为何正确

## Recommendations
按优先级排列的修改建议(引用规则)

每个违例条目都要求给出"现状 / 要求 / 修正"三段式描述,这使审查结论可以直接转化为 PR 修改项。

何时启用这套规则

触发场景覆盖全部 ClickHouse 日常操作:CREATE TABLE 语句、ALTER TABLE 修改、ORDER BY/PRIMARY KEY 讨论、数据类型选择、慢查询排查、JOIN 优化、数据摄入管线设计、更新/删除策略、ReplacingMergeTree 等专用引擎使用、分区策略决策。换句话说,只要动手写 ClickHouse DDL、DML 或排查 ClickHouse 性能问题,就应该先翻这套规则

规则文件结构:可编程的领域知识

每个规则文件(rules/ 下的 28 个 *.md)统一包含:YAML frontmatter(标题、影响级别、标签)、规则重要性说明、带解释的反例、带解释的正例、以及额外的权衡/适用场景/参考链接。这种"一规则一文件 + 统一模板"的结构让规则既能被 Agent 按名检索引用,也能被开发者作为速查手册使用;README.md 则汇总了安装命令(npx skills add ClickHouse/clickhouse-agent-skills)、前缀-规则数对照表和触发短语。

对 Langfuse 开发者而言,把本文的通用规则与 SKILL.md 中的 Langfuse 专属约束(events 查询构建器、禁用 FINAL、log_comment 归因、canonical 模板占位符、alter_sync/mutations_sync、禁 CREATE OR REPLACE、MV 用 MODIFY QUERY)结合起来,就是一套完整且可立即执行的 ClickHouse 开发与审查规范。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395