Langfuse 的 ClickHouse 最佳实践:28 条 Schema、查询与写入规则的实战指南
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 TABLE、ALTER TABLE、慢查询排查、JOIN 优化、写入管线设计、更新/删除策略等场景时,必须先按规则目录逐条核对,再给出结论。
其使用优先级明确写在第 16 行起的"IMPORTANT: How to Apply This Skill"中:
- 先在
rules/目录中检查是否有适用的规则文件; - 若有规则,应用规则并在回复中引用(格式为 "Per
rule-name..."); - 若无规则,再使用 LLM 的 ClickHouse 知识或检索官方文档;
- 仍不确定时,使用联网搜索获取当前最佳实践;
- 始终标注来源:规则名、或 "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 取值为 trpc、publicapi、worker、mcp、unknown;其中 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_core、events_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_sync 与 mutations_sync:跨迁移文件的竞态
这是 Langfuse 踩坑后沉淀出的最精细规则之一。每一个新的 canonical 迁移中的元数据 ALTER(ADD/DROP/MODIFY COLUMN、ADD/DROP INDEX)都必须附带:
{CLICKHOUSE_CLUSTERED_ONLY: SETTINGS alter_sync = 2}
而每个产生 mutation 的 ALTER(MATERIALIZE …、UPDATE、DELETE)必须附带:
{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 QUERY。MODIFY 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、状态/类别用 Enum8 或 LowCardinality(String)、时间戳用 DateTime、纯日期用 Date、计数用最小可容纳的 UInt8/16/32、金额用 Decimal(P,S) 或分单位的 Int64、布尔用 Bool。
schema-types-minimize-bitwidth:选用能容纳数据的最小数值类型。
schema-types-lowcardinality:唯一值 < 10,000 的字符串列用 LowCardinality(String)(字典编码),> 10,000 用普通 String;FixedString 仅限严格定长数据(如 2 位国家码)。先用 SELECT uniq(column_name) FROM table_name 确认基数再决定。Langfuse 的 events_full 建表 正是这一规则的落地:type、environment、level 均声明为 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-handling:join_use_nulls=0 以使用默认值处理无匹配。
索引:1 条 HIGH 规则
query-index-skipping-indices:对不在 ORDER BY 前缀里但需要过滤的列,使用跳数索引(data skipping index)。
物化视图:2 条 HIGH 规则
query-mv-incremental:实时聚合用增量物化视图——写入时自动对新区块应用视图查询,结果写入目标表并随时间合并。要点:MV 中用 -State 聚合函数(countState、uniqState),查询时用 -Merge 函数(countMerge、uniqMerge);目标表用 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-update:ALTER TABLE UPDATE 是 mutation——异步后台进程会重写受影响的整个 part,写入放大、磁盘 I/O 尖峰、不可回滚、读取可能混合新旧 part。更新模式应改用 ReplacingMergeTree(version_col):插入新版本行、查询用 FINAL 或 argMax 取最新。Langfuse 大量采用该模式(如 ReplacingMergeTree 表配合版本列处理可变字段)。
insert-mutation-avoid-delete:删除优先用轻量删除(lightweight DELETE)或 DROP PARTITION,而非 ALTER TABLE DELETE。
OPTIMIZE:1 条 HIGH 规则
insert-optimize-avoid-final:OPTIMIZE 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-creation → schema-pk-cardinality-order → schema-pk-prioritize-filters → schema-types-native-types → schema-types-minimize-bitwidth → schema-types-lowcardinality → schema-types-avoid-nullable → schema-partition-low-cardinality → schema-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-algorithm → query-join-filter-before → query-join-use-any → query-index-skipping-indices → schema-pk-filter-on-orderby,检查:过滤是否用 ORDER BY 前缀列、JOIN 前是否先过滤、JOIN 算法是否匹配表规模、非 ORDER BY 过滤列是否用跳数索引。
写入策略审查(摄入 / 更新 / 删除)按顺序读 insert-batch-size → insert-mutation-avoid-update → insert-mutation-avoid-delete → insert-async-small-batches → insert-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 开发与审查规范。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00