埋点请求返回 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 官方参考可作为起点。