ClickHouse 埋点事件表:先验证时间与重复,再谈漏斗看板

埋点请求返回 200,并不等于事件已经以正确语义进入 ClickHouse。一个可复核的接收链路,至少要把事件时间、接收时间、身份和重复定义分开。下面用一张最小示例表和几条 SQL 演示验收方法;它是独立的测试表,不是任何产品的生产建表模板。

为什么保留两个时间?

event_time 是行为发生时间,received_at 是接收端看到请求的时间。移动端离线缓存、批量上传或重试会使两者相差很远。业务报表通常需要按事件时间归属日期;排查延迟则要看接收时间。只保存一个模糊的 timestamp,会让跨天数据难以解释。

一张可直接执行的测试表

以下 SQL 使用 ClickHouse 的 MergeTree、DateTime64 和 LowCardinality 类型。可在独立测试库执行;若要重复演练,请更换表名或先清理自己的测试表,不要对生产表操作。

sql 复制代码
CREATE TABLE IF NOT EXISTS event_ingest_demo
(
    project_id  String,
    event_name  LowCardinality(String),
    distinct_id String,
    event_time  DateTime64(3, 'UTC'),
    received_at DateTime64(3, 'UTC'),
    event_id    String,
    properties  String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (project_id, event_name, toDate(event_time),
          distinct_id, event_time, event_id);

INSERT INTO event_ingest_demo VALUES
('demo', 'signup', 'user-1',
 toDateTime64('2026-09-29 10:00:00', 3, 'UTC'),
 toDateTime64('2026-09-29 10:00:02', 3, 'UTC'),
 'evt-001', '{"plan":"free"}'),
('demo', 'signup', 'user-1',
 toDateTime64('2026-09-29 10:00:00', 3, 'UTC'),
 toDateTime64('2026-09-29 10:00:04', 3, 'UTC'),
 'evt-001', '{"plan":"free"}'),
('demo', 'purchase', 'user-1',
 toDateTime64('2026-09-29 10:03:00', 3, 'UTC'),
 toDateTime64('2026-09-29 10:03:01', 3, 'UTC'),
 'evt-002', '{"amount":39}');

第一、二行故意使用相同的 event_id,模拟同一业务事件被传输重试。第三行是另一个事件。当前 MergeTree 表不会自动删除前两行。

先看原始行,而不是直接打开看板:

sql 复制代码
SELECT event_name, distinct_id, event_id,
       event_time, received_at, properties
FROM event_ingest_demo
WHERE project_id = 'demo'
ORDER BY event_time, received_at;

再检查是否有共享事件 ID 的记录:

sql 复制代码
SELECT project_id, event_id, count() AS copies
FROM event_ingest_demo
WHERE project_id = 'demo' AND event_id != ''
GROUP BY project_id, event_id
HAVING copies > 1
ORDER BY copies DESC;

在这组样本中,查询应返回 evt-001、copies = 2。如果客户端协议没有稳定事件 ID,不能简单用"同一用户 + 同一事件名 + 同一秒"代替:用户可能真的连续操作两次。需要先定义去重键,才能谈去重率。

为什么不直接换成 ReplacingMergeTree?

ClickHouse 官方文档说明,ReplacingMergeTree 根据 ORDER BY 排序键在后台合并时移除重复行;合并发生时间不确定,因此不保证查询时已经没有重复。FINAL 可用于查询时处理,但不能把它当作无需评估成本的默认开关。

更重要的是:如果排序键包含 received_at,一次重试就可能产生不同键,根本不会被这套规则视为重复。如果排序键过短,又可能误合并真实事件。因此先用样本确认事件 ID、版本字段和查询口径,再决定在接收层、写入层还是查询层处理重复。

从测试表走向真实链路

真实事件链路还要验证空身份、未来时间、迟到事件、属性类型变化、写入失败和重试后的最终行数。Superset 图表适合展示查询结果,但不能替代原始行对账与接收服务监控。

SensorFlow 公开仓库的架构是兼容的 Sensors Data SDK 上报 → Go 接收 → ClickHouse → Apache Superset。其无许可证 Demo 可以先看样本事件;真实 SDK 数据接收需要许可证,并且应按实际 SDK 版本和 payload 验证。本文的 event_ingest_demo 只是通用 ClickHouse 验收练习,不代表 SensorFlow 的现有生产表结构或已完成全模式兼容测试。

建表时最终的分区、排序键、TTL、副本与批量写入策略,应按自己的查询模式、数据量和故障要求测试,而不是直接复制演示 SQL。MergeTree 官方参考可作为起点。

相关推荐
Dreams_l2 小时前
基于SpringBoot实现的抽奖系统(一)
java·spring boot·后端
余槐i2 小时前
rollbackFor 不设置,Checked 异常不回滚:Spring 默认规则与两行修复的取舍
java·spring boot·后端·spring·事务管理
孙启超2 小时前
【AI开发之Rust】第 17 课:项目总览与核心架构 —— AI 助手 Rust 核心从 0 到 1
开发语言·后端·rust
EatFan3 小时前
JunoYi 框架实践:Spring Boot 项目为什么拆成 framework、module、server 三层?
java·spring boot·后端·framework·module·模块化·junoyi
汉堡大王95273 小时前
一张图三句需求,我用 Trae Work 做了一块能看日出日落和月相的天文机械表
前端·后端·github
mudtools3 小时前
在.NET现有系统中快速集成飞书任务分配能力
后端·c#·.net
知守观3 小时前
ThreadLocal + 异步线程导致用户数据串号:一次跨请求数据泄漏的完整复盘
后端
前端冒菜师3 小时前
我为什么做了 Iris,又为什么停下了它
后端·ai编程
子一!!3 小时前
集成Spring家族的Spring论坛实战==一阶段
java·后端·spring