Langfuse 的 ClickHouse 最佳实践:28 条 Schema、查询与写入规则的实战指南
Langfuse 的 ClickHouse 最佳实践28 条 Schema、查询与写入规则的实战指南【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuseLangfuse 将 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/目录中检查是否有适用的规则文件若有规则应用规则并在回复中引用格式为 Perrule-name...若无规则再使用 LLM 的 ClickHouse 知识或检索官方文档仍不确定时使用联网搜索获取当前最佳实践始终标注来源规则名、或 general ClickHouse guidance。之所以让规则优先于通用直觉文档给出了根本原因ClickHouse 有特殊的列式存储、稀疏索引与 MergeTree 合并机制通用数据库经验在这里极易产生误导例如ORDER BY 可以随时改频繁 UPDATE 没关系索引越多越好在 ClickHouse 中都不成立而规则编码的是经过验证的 ClickHouse 专属经验。规则文件本身遵循统一模板见 rules/_template.md每个规则文件包含 YAML frontmattertitle / impact / tags、为什么这条规则重要、反例 解释、正例 解释、以及权衡与适用场景。28 条规则按影响度优先级组织为下表优先级类别影响前缀规则数1主键选择CRITICALschema-pk-42数据类型选择CRITICALschema-types-53JOIN 优化CRITICALquery-join-54插入批量CRITICALinsert-batch-15避免 MutationCRITICALinsert-mutation-26分区策略HIGHschema-partition-47跳数索引HIGHquery-index-18物化视图HIGHquery-mv-29异步插入HIGHinsert-async-210避免 OPTIMIZEHIGHinsert-optimize-111JSON 使用MEDIUMschema-json-1Langfuse 专属规则代码库内的硬性约束SKILL.md 用一整节篇幅定义了在 Langfuse 仓库内必须遵守的专属规则每条都能在源码中找到对应实现是理解 Langfuse ClickHouse 架构的关键入口。1. 查events表必须走查询构建器规则原文查询events表必须使用 event-query-builder.ts除非先确认构建器无法表达该查询否则不得手写eventsSQL。这保证了events这类核心热路径表的所有查询都经过统一的过滤、投影与分页处理避免手写 SQL 引入不一致。2. 绝不在events表上使用FINALevents表在设计上保证不需要FINAL而FINAL关键字会显著拖慢性能。这一点与通用规则insert-optimize-avoid-final呼应OPTIMIZE TABLE ... FINAL会强制合并所有 parts、重写整个分区而 SELECT 侧的FINAL修饰符也会带来额外的合并开销在events这类高频表上是被禁止的。3. 查询归因log_comment中的 JSONLangfuse 将查询归因信息写入 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与可选的projectIdClickHouse 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 迁移中的元数据ALTERADD/DROP/MODIFY COLUMN、ADD/DROP INDEX都必须附带{CLICKHOUSE_CLUSTERED_ONLY: SETTINGS alter_sync 2}而每个产生 mutation 的ALTERMATERIALIZE …、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-statementtruegolang-migrate 按;拆分文件且不做 SQL 解析所以注释和字符串字面量里不能出现分号每条语句都要幂等IF EXISTS/IF NOT EXISTS这样migrate force后脏的半应用迁移可以重跑。drop→create 窗口内读取视图会瞬时失败——对analytics_*导出视图可以接受因此普通视图应避免放在产品热路径上。7. 物化视图禁止 drop-and-recreate源表仍在接收实时写入时绝不可通过 DROP 后 CREATE 来更新物化视图DROP 与 CREATE 之间写入的每一行都会静默且永久丢失。正确做法是用MODIFY QUERY原地替换 SELECTALTER 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-creationClickHouse 的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.mdUUID 用UUID16 字节而非 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 用普通StringFixedString仅限严格定长数据如 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小表可考虑先不分区后续按需添加。JSON1 条 MEDIUM 规则schema-json-when-to-use动态 schema 才用JSON类型字段已知时用类型化列Langfuse 中Map(...)与显式列即属此类。查询优化规则JOIN、索引、物化视图JOIN5 条 CRITICAL 规则query-join-choose-algorithmClickHouse 默认 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只需一条匹配时用ANYJOINLEFT ANY JOIN/INNER ANY JOIN/RIGHT ANY JOIN返回首个匹配、内存更少、执行更快。query-join-filter-beforeJOIN之前先过滤各表而不是 JOIN 之后再 WHERE。query-join-consider-alternatives考虑用字典Dictionary或反规范化替代 JOIN。query-join-null-handlingjoin_use_nulls0以使用默认值处理无匹配。索引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 insertsSET async_insert 1; SET wait_for_async_insert 1; -- 确认持久化刷新触发条件任一先到缓冲达到async_insert_max_data_size、超时async_insert_busy_timeout_ms、或累积的插入查询数达到上限。返回模式上wait_for_async_insert1推荐等待落盘、确认持久化0为 fire-and-forget、出错无感知仅在可接受数据丢失时使用。insert-format-native优先用 Native 格式以获得最佳性能。Mutation2 条 CRITICAL 规则insert-mutation-avoid-updateALTER 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。OPTIMIZE1 条 HIGH 规则insert-optimize-avoid-finalOPTIMIZE TABLE ... FINAL会强制立即合并全部分区、重写整个分区、无视约 150GB 的 part 大小保护、可能引发内存压力/OOM应依赖后台合并。注意OPTIMIZE FINAL≠ SELECT 的FINALReplacingMergeTree 去重场景下 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 开发与审查规范。【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考