news 2026/9/10 21:06:50

Langfuse 数据删除最佳实践:用轻量删除、ReplacingMergeTree 与 DROP PARTITION 取代 ALTER TABLE DELETE 突变

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Langfuse 数据删除最佳实践:用轻量删除、ReplacingMergeTree 与 DROP PARTITION 取代 ALTER TABLE DELETE 突变

Langfuse 数据删除最佳实践:用轻量删除、ReplacingMergeTree 与 DROP PARTITION 取代 ALTER TABLE DELETE 突变

【免费下载链接】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

本文基于 Langfuse 仓库中的 ClickHouse 最佳实践规则文档(insert-mutation-avoid-delete.md),深入讲解为何必须规避ALTER TABLE DELETE这类数据突变(mutation)操作,并结合 Langfuse 实际生产表结构(traces / observations / scores / events 等)展示 ReplacingMergeTree 软删除标记、轻量 DELETE 与按月分区 DROP PARTITION 的落地用法。读完本文,你将掌握一套"可插入、可删除、不重写"的 ClickHouse 数据生命周期管理方案,并能在 Langfuse 的仓库代码中找到每一处实践对应的实现依据。

规则核心:为什么ALTER TABLE DELETE是 CRITICAL 级反模式

在 ClickHouse 中,ALTER TABLE DELETE属于突变(mutation)操作,它不是一个即时生效的普通删除,而是一个异步后台进程:凡是命中的 data part(数据分片),ClickHouse 都要把整个 part 读出来、剔除命中行、再重写回磁盘。这意味着:

  • 写入放大严重:哪怕只删几行,只要这些行分散在多个 part 中,所有相关 part 都会被完整重写;
  • 磁盘 I/O 尖峰:重写过程会抢占集群 I/O,拖慢同一时刻的查询与写入;
  • 不可回滚:突变一旦提交无法撤销;
  • 读不一致:SELECT 可能读到已突变与未突变 part 混合的结果。

该规则在规则文件 YAML 元数据中被标记为impact: CRITICAL,即影响等级最高的约束项;在 SKILL.md 的规则分类优先级表中,"Mutation Avoidance(突变规避)"同样位列 CRITICAL 等级。规则文档给出的反模式示例非常典型:

-- Mutation delete for cleanup ALTER TABLE orders DELETE WHERE status = 'cancelled'; -- Time-based cleanup via mutation (very expensive) ALTER TABLE sessions DELETE WHERE created_at < now() - INTERVAL 7 DAY;

第一句按业务条件清理,第二句按时间批量清理旧数据,二者都命中大量 part,代价高昂。接下来分别介绍三种正确替代方案。

方案一:CollapsingMergeTree——高频软删除的正确姿势

针对"频繁删除少量行"的场景,规则文档推荐使用折叠树引擎 CollapsingMergeTree。其核心思想是用插入替代删除:为表增加一个sign列(1 表示活跃,-1 表示删除),删除动作变成一次sign = -1的普通插入,折叠合并引擎会在后台把同一排序键下+1/-1成对的数据自动抵消。

规则文档中的完整示例:

CREATE TABLE orders ( order_id UInt64, customer_id UInt64, total Decimal(10,2), sign Int8 -- 1 = active, -1 = deleted ) ENGINE = CollapsingMergeTree(sign) ORDER BY order_id; -- Insert order INSERT INTO orders VALUES (123, 456, 99.99, 1); -- "Delete" by inserting with sign = -1 INSERT INTO orders VALUES (123, 456, 99.99, -1); -- Query collapses +1 and -1 pairs SELECT order_id, sum(total * sign) as total FROM orders GROUP BY order_id HAVING sum(sign) > 0;

关键点在于:删除动作本身是普通 INSERT,成本与一次插入完全相同,不产生任何 part 重写;查询时用sum(total * sign)聚合抵消,配合HAVING sum(sign) > 0过滤掉已删除行。代价是删除的行会继续占用存储,直到后台合并真正将+1/-1对物理清除。

Langfuse 的实践:is_deleted标记 + ReplacingMergeTree

Langfuse 没有照搬sign列,而是采用同构思路的变体:is_deleted标记列 + ReplacingMergeTree 版本列。查看 Langfuse 的 ClickHouse 迁移文件 0001_traces.up.sql:

CREATE TABLE traces {CLICKHOUSE_CLUSTER_CLAUSE} ( ... `event_ts` DateTime64(3), `is_deleted` UInt8, ... ) ENGINE = {CLICKHOUSE_REPLICATION_PREFIX}ReplacingMergeTree(event_ts, is_deleted) Partition by toYYYYMM(timestamp)

同样的模式贯穿整个仓库的核心表:

  • 0002_observations.up.sql:ReplacingMergeTree(event_ts, is_deleted),按月toYYYYMM(start_time)分区;
  • 0003_scores.up.sql:ReplacingMergeTree(event_ts, is_deleted)
  • 0011_add_blob_storage_file_log.up.sql 与 0024_dataset_run_items.up.sql:同样的引擎与标记列。

ReplacingMergeTree 的两个参数(event_ts, is_deleted)含义深刻:event_ts是版本列,合并时保留版本最新的一行;is_deleted是删除标记列,同版本号时按该列决定保留哪一行。这样"软删除"就退化成一次携带is_deleted = 1的普通插入——与规则文档"用插入替代突变"的精神完全一致。

写入侧同样遵循该约定:在 clickhouse.ts 中,Langfuse 写入blob_storage_file_log表时显式携带is_deleted: 0;读取侧在 query-fragments.ts 中通过e.is_deleted = 0过滤已删除行,保证业务查询只见活跃数据。

方案二:轻量删除(Lightweight DELETE,23.3+)——偶发删除的首选

如果删除频率不高、规模不大,规则文档推荐使用 ClickHouse 23.3+ 引入的轻量删除语法,也就是标准的DELETE FROM

-- Marks rows, doesn't rewrite immediately DELETE FROM orders WHERE status = 'cancelled'; -- Physical deletion happens during normal merges

轻量删除的精髓是"打标记、不重写":执行时只在索引中标记命中行并立即对后续查询隐藏,物理空间回收交给后台正常的 merge 过程,避免了一整轮 part 重写。它适合偶发性的中等规模删除——比突变便宜得多,又比软删除方案简单直接,无需改动表结构。

Langfuse 的实践:trace 删除路径中的轻量 DELETE

Langfuse 的 trace 删除正是这种模式的工程化样板。在 traces.ts 中,deleteTraces先做一次 preflight 计数,无命中行则直接跳过;随后执行的是带时间边界收敛的轻量 DELETE。更典型的实现在 events.ts:

const deleteQuery = (table: string) => ` DELETE FROM ${table} WHERE project_id = {projectId: String} AND trace_id IN ({traceIds: Array(String)}) AND start_time >= {minTs: String}::DateTime64(3) AND start_time <= {maxTs: String}::DateTime64(3) `;

这段代码体现了三条值得复用的实战经验:

  1. 先查后删:先用SELECT ... LIMIT 1/ count 预检(见hasAnyEvent与 preflight 逻辑),无数据直接跳过,避免空 DELETE;
  2. 范围收敛:删除条件同时带上start_time的 min/max 边界,把命中范围压缩到最小,减少需要标记的 part 数量;
  3. 超时保护:通过clickhouseConfigs.request_timeout使用env.LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS配置超时(见 events.ts),防止大删除长时间占住连接。

方案三:DROP PARTITION——按分区批量删除的瞬时方案

当删除目标是"整块旧数据"(例如按时间维度清理一个月前的数据)时,最优雅的方式是直接丢弃整个分区。规则文档的对比示例:

-- Instant deletion of old data ALTER TABLE events DROP PARTITION '202301'; -- Much faster than: ALTER TABLE events DELETE WHERE toYYYYMM(timestamp) = 202301;

DROP PARTITION是元数据级操作,只需删除分区的目录引用,瞬间完成;而等价条件的ALTER TABLE DELETE却要逐 part 重写,二者性能差距是数量级的。前提是表必须按删除维度(通常是时间)设计分区键——这正是"分区服务于数据生命周期"这一设计原则的体现。

Langfuse 的实践:按月分区 + TTL 生命周期管理

Langfuse 的所有核心表都按toYYYYMM(...)按月分区(如 traces 按toYYYYMM(timestamp)、observations 按toYYYYMM(start_time)),天然为"按月 DROP PARTITION"或"按月 TTL"做好了结构准备。更进一步的实践是 TTL:

  • 在 handleEventPropagationJob.ts 的注释中明确写道:"Relies on table TTL for partition cleanup instead of explicit DROP PARTITION."——Langfuse 的事件传播表依赖 TTL 自动回收分区数据,而非显式执行 DROP PARTITION;
  • 在 0023_traces_aggregating_merge_trees.up.sql 中,聚合合并树表使用TTL toDate(start_time) + INTERVAL 7 DAYINTERVAL 30 DAY分级保留数据。

TTL 本质上就是"到期自动 DROP 分区/part"的托管化表达,与手动DROP PARTITION同属生命周期管理,但无需应用层定时任务。二者组合的完整策略是:按月分区 + TTL 自动清理 + 需要即时清除时手动 DROP PARTITION

删除策略选型速查表

规则文档给出的四方案对比表是选型的核心依据,完整摘录如下:

MethodSpeedWhen to Use
ALTER DELETESlowRare corrections only
CollapsingMergeTreeFastFrequent soft deletes
Lightweight DELETEMediumOccasional deletes
DROP PARTITIONInstantBulk deletion by partition

结合 Langfuse 仓库的工程实践,可以进一步细化选型准则:

  1. 纠错类、一次性、命中行少:用轻量DELETE FROM(如 events.ts 中按 trace_id + 时间范围删除);
  2. 高频软删除 / 业务删除:用 ReplacingMergeTree +is_deleted标记(如 0001_traces.up.sql),删除即插入;
  3. 整块过期数据:按月分区 +DROP PARTITION或 TTL(如 0023_traces_aggregating_merge_trees.up.sql);
  4. 罕见修正:仅在无法用上述方案覆盖时,才考虑ALTER TABLE DELETE突变。

迁移与部署中的配套约束

规避突变的规则在 Langfuse 的迁移流程中还有配套约束,写 SQL 时需一并遵守(详见 SKILL.md 的 Langfuse-Specific Rules 一节):

  • 任何会创建突变的 ALTER(MATERIALIZE ...UPDATEDELETE,在集群模式下必须携带{CLICKHOUSE_CLUSTERED_ONLY: SETTINGS mutations_sync = 2},确保突变完成后才进入下一个迁移文件;
  • mutations_sync不能替代alter_sync:前者控制突变何时结束,后者控制元数据传播,二者用途不同;
  • 不要在 events 表上使用FINAL:events 表设计上不依赖 FINAL 就能读到正确数据,使用该关键字反而伤害性能(见 SKILL.md 中 "Never use FINAL on the events table" 一条)。这与本文方案一"用聚合/标记替代 FINAL"的思路一脉相承——对 events 这类按时间流式写入的表,Langfuse 刻意避免依赖合并语义,将"正确性"前置到写入侧。

总结

ALTER TABLE DELETE是 ClickHouse 中代价最高的删除方式,应被视作最后手段而非默认选项。Langfuse 仓库用三种互补机制覆盖了全部删除场景:ReplacingMergeTree +is_deleted标记承接高频软删除(traces / observations / scores / dataset_run_items 全部采用该模式),轻量 DELETE处理偶发的中等规模删除(trace / events 删除路径配合 preflight 预检与时间范围收敛),按月分区 + TTL / DROP PARTITION负责批量过期数据回收。阅读本仓库的迁移目录(canonical)与删除相关仓库层代码(traces.ts、events.ts),即可获得一套可复制的生产级 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 21:06:22

CANN/GE模型配置属性API

aclmdlConfigAttr 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFl…

作者头像 李华
网站建设 2026/9/10 21:01:06

机器学习学习曲线:原理、应用与sklearn实践

1. 学习曲线在机器学习中的核心作用 学习曲线是机器学习模型诊断的重要工具&#xff0c;它通过绘制训练集和验证集上的性能指标&#xff08;如准确率、损失值&#xff09;随训练样本数量或训练迭代次数的变化趋势&#xff0c;直观展示模型的学习过程。在sklearn中&#xff0c;l…

作者头像 李华
网站建设 2026/9/10 20:55:45

Java ThreadLocal原理、应用与性能优化指南

1. ThreadLocal基础概念与核心价值 ThreadLocal是Java并发编程中一个容易被忽视但极其重要的工具类。我第一次接触ThreadLocal是在处理一个用户会话跟踪的需求时&#xff0c;当时需要在同一个线程的不同方法间传递用户ID&#xff0c;但又不希望使用显式的参数传递。ThreadLocal…

作者头像 李华