用 Druid Theta sketch 分析用户交集、并集与差集

本文译编自 Apache Druid 官方教程 Approximations with Theta sketches。原页未署个人作者,维护与发布方为 Apache Software Foundation;文末明确说明教程改编自社区成员 Hellmar Becker 的文章,本文保留该致谢、全部示例及原图。

为什么去重计数不能直接相加

Apache Druid 可以支持点击流数据的实时采集、流式处理和交互式可视化。点击流分析常见的一类问题,是统计不重复的访客或会话。通常需要扫描明细数据,因为随着聚合层级变化,去重数量不能简单相加。

假设你想知道一部电视剧的观看人数:某一天有 1000 名不同观众看了第一集,有 800 名不同观众看了第二集。接着可能还想问:

  • 多少人两集都看了?
  • 多少人至少看过其中一集?
  • 多少人只看了第一集,没有看第二集?

仅凭 1000 和 800 两个聚合结果,无法回答这些问题。你得回到明细逐行扫描。当数据量足够大时,这可能耗时很长,使交互式探索变得困难。去重计数与预聚合 rollup 配合也不直接:理想情况下,每个 15 分钟时间段、节目、集数只保留一行,而你关心的是去重数量,并不需要在这张分析表里保留每个用户 ID。有没有办法既不反复处理全部明细,又能使用 rollup?Theta sketch 就是一个选择。

用 Theta sketch 进行快速近似集合分析

Theta sketch 是一种概率数据结构,可以快速估计构建摘要时所使用值的去重数量,并具有已知的误差分布。Druid 的实现依赖 Apache DataSketches。

  • 可合并:可以先处理 rollup 后的数据,再把不同时间段的摘要合并,从而利用 Druid 的预聚合能力。
  • 支持集合运算:对于两个数据子集的 Theta sketch,可以计算并集、交集和差集。这能回答哪些观众看过某种节目组合、哪些人群彼此重叠等问题。

本教程将完成两件事:在入库时从输入数据构建 Theta sketch;再对这些摘要执行去重计数和集合运算查询,回答前面的问题。

Theta sketch:先合并摘要,再做集合运算,最后估计
图 1:原创查询过程示意。过滤作用于聚合,集合运算作用于摘要,估计放在最后。

准备环境

先按照 Druid 单机快速入门 下载并在本机启动 Druid。本教程不要求预先向集群装入数据;先完成“加载文件”和“查询数据”教程会更容易跟上。

编者补充:需要可执行 SQL 入库任务的本地环境。按 Theta 扩展说明 确认扩展加载列表包含 druid-datasketches;若已有其他扩展,不要用只含一个扩展的示例覆盖整个列表。本文以读取时的 latest 文档为准,没有在本机启动 Druid 或核验特定安装的配置。

示例数据

数据有四列:date 是时间戳(示例只用日期,实际业务往往需要更细粒度);uid 是用户 ID;show 是节目名称;episode 是集数标识。以下保留原文完整的 19 条数据和表头,其中重复观看记录也是示例的一部分。

date,uid,show,episode
2022-05-19,alice,Game of Thrones,S1E1
2022-05-19,alice,Game of Thrones,S1E2
2022-05-19,alice,Game of Thrones,S1E1
2022-05-19,bob,Bridgerton,S1E1
2022-05-20,alice,Game of Thrones,S1E1
2022-05-20,carol,Bridgerton,S1E2
2022-05-20,dan,Bridgerton,S1E1
2022-05-21,alice,Game of Thrones,S1E1
2022-05-21,carol,Bridgerton,S1E1
2022-05-21,erin,Game of Thrones,S1E1
2022-05-21,alice,Bridgerton,S1E1
2022-05-22,bob,Game of Thrones,S1E1
2022-05-22,bob,Bridgerton,S1E1
2022-05-22,carol,Bridgerton,S1E2
2022-05-22,bob,Bridgerton,S1E1
2022-05-22,erin,Game of Thrones,S1E1
2022-05-22,erin,Bridgerton,S1E2
2022-05-23,erin,Game of Thrones,S1E1
2022-05-23,alice,Game of Thrones,S1E1

在入库时构建 Theta sketch

在 Druid Web 控制台进入 Query 视图,使用 INSERT INTO 与 EXTERN 将上述 CSV 作为内联数据导入:

INSERT INTO "ts_tutorial"
WITH "source" AS (SELECT * FROM TABLE(
  EXTERN(
    '{"type":"inline","data":"date,uid,show,episode\n2022-05-19,alice,Game of Thrones,S1E1\n2022-05-19,alice,Game of Thrones,S1E2\n2022-05-19,alice,Game of Thrones,S1E1\n2022-05-19,bob,Bridgerton,S1E1\n2022-05-20,alice,Game of Thrones,S1E1\n2022-05-20,carol,Bridgerton,S1E2\n2022-05-20,dan,Bridgerton,S1E1\n2022-05-21,alice,Game of Thrones,S1E1\n2022-05-21,carol,Bridgerton,S1E1\n2022-05-21,erin,Game of Thrones,S1E1\n2022-05-21,alice,Bridgerton,S1E1\n2022-05-22,bob,Game of Thrones,S1E1\n2022-05-22,bob,Bridgerton,S1E1\n2022-05-22,carol,Bridgerton,S1E2\n2022-05-22,bob,Bridgerton,S1E1\n2022-05-22,erin,Game of Thrones,S1E1\n2022-05-22,erin,Bridgerton,S1E2\n2022-05-23,erin,Game of Thrones,S1E1\n2022-05-23,alice,Game of Thrones,S1E1"}',
    '{"type":"csv","findColumnsFromHeader":true}'
  )
) EXTEND ("date" VARCHAR, "show" VARCHAR, "episode" VARCHAR, "uid" VARCHAR))
SELECT
  TIME_FLOOR(TIME_PARSE("date"), 'P1D') AS "__time",
  "show",
  "episode",
  COUNT(*) AS "count",
  DS_THETA("uid") AS "theta_uid"
FROM "source"
GROUP BY 1, 2, 3
PARTITIONED BY DAY

SELECT 中的 DS_THETA("uid") AS "theta_uid" 在入库时对 uid 建立 thetaSketch 聚合。这个分析场景关心的是不重复的人数,因此输出里不再保留单个 uid,而是保留它们的摘要。DS_THETA 还有一个可选的第二参数,用于控制摘要的大小和精度。

GROUP BY 1, 2, 3 把同一天、同一节目、同一集的观看记录聚合起来;TIME_FLOOR(..., 'P1D') 形成按日的时间桶,PARTITIONED BY DAY 则声明按日分区。count 保存记录数,theta_uid 保存用户集合的摘要,这两者不是同一种计数。

编者说明:这条 SQL 会向 ts_tutorial 写入数据,是入库操作而非只读查询。请使用教程专用数据源;重复执行 INSERT INTO 会追加数据,不能把它当成自动覆盖或幂等初始化。重复值可能在 sketch 的去重计算中合并,但 count 等指标仍可能受重复导入影响。

查询 Theta sketch 列

从一列 Theta sketch 求去重数量估计,通常分两步:先用 Druid SQL 聚合函数 DS_THETA 合并相关行的摘要,再用 THETA_SKETCH_ESTIMATE 取得合并后摘要的估计值。后面要介绍的集合函数,放在这两步之间。

基本计数

先在查询编辑器查看表的内容:

SELECT * FROM ts_tutorial
官方原图:SELECT * 显示按日期、节目和集数聚合后的数据及 Base64 摘要列
图 2:官方原图,显示导入后的行及 theta_uid 摘要列。图片来自原教程,不是本次运行截图。

theta_uid 在界面上显示为 Base64 编码字符串。原文把其背后的表示称作“bitmap”;使用时更准确的理解是序列化的 Theta sketch 对象,而不是原始用户列表,也不应假定它是能无损恢复所有用户 ID 的普通位图。

下列查询把每一行已存的 sketch 转成该行对应日期、节目和集数的用户数量估计:

SELECT
  __time,
  "show",
  "episode",
  THETA_SKETCH_ESTIMATE(theta_uid) AS users
FROM ts_tutorial
官方原图:将每行 Theta sketch 转为用户去重数量估计
图 3:官方原图,将摘要显示为对应行的用户数量估计。原图中的 SQL 比网页代码多出 GROUP BY 1, 2, 3, 4,本文保留原图并明确标注该差异。

编者澄清:上方网页代码本身没有 GROUP BY,也没有跨行的 DS_THETA 聚合;它直接估计每一行的摘要。官方截图额外按所选表达式分组,也不能被理解为使用 DS_THETA 合并了跨时间的摘要。若要跨日期或跨分组汇总,应像下面的查询那样先合并摘要,不能把这些行的去重数相加。

给聚合添加过滤条件

Druid 支持过滤指标,即在 SELECT 中为聚合附加 FILTER(WHERE ...)。对于 Theta sketch,过滤应放在聚合器与最终估计器之间。以下查询统计看过 Bridgerton 的全部不同观众:

SELECT APPROX_COUNT_DISTINCT_DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton') AS users
FROM ts_tutorial
官方原图:Bridgerton 全部观众去重估计为 5
图 4:官方原图,示例数据中 Bridgerton 的用户去重估计为 5。

APPROX_COUNT_DISTINCT_DS_THETA 相当于先调用 DS_THETA,再调用 THETA_SKETCH_ESTIMATE:

SELECT THETA_SKETCH_ESTIMATE(
         DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton')
       ) AS users
FROM ts_tutorial
  • DS_THETA:合并 theta_uid 这一列中选定行的摘要,得到一个新的 Theta sketch。
  • THETA_SKETCH_ESTIMATE:计算合并结果的去重数量估计。

过滤子句只让满足条件的行参与相应聚合。原文解释中的 THETA_SKETCH_ESIMATE 少了字母 T;本文统一采用示例 SQL 中正确的 THETA_SKETCH_ESTIMATE 拼写。

求交集:两集都看过的人

把聚合过滤与集合运算组合起来,就能回答开头的问题。用 THETA_SKETCH_INTERSECT 计算两个或多个子集的交集,再估计看过 Bridgerton 两集的不同用户数:

SELECT THETA_SKETCH_ESTIMATE(
         THETA_SKETCH_INTERSECT(
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E1'),
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E2')
         )
       ) AS users
FROM ts_tutorial
官方原图:Bridgerton 第一集和第二集观众交集估计为 1
图 5:官方原图,两集观众的交集估计为 1。

集合函数位于聚合器和估计器之间:先分别得到两集的观众摘要,再求交集,最后取数量估计。

求并集:至少看过其中一集的人

使用 THETA_SKETCH_UNION 合并两个集合,得到至少看过其中一集的不同用户数量估计:

SELECT THETA_SKETCH_ESTIMATE(
         THETA_SKETCH_UNION(
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E1'),
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E2')
         )
       ) AS users
FROM ts_tutorial
官方原图:Bridgerton 第一集和第二集观众并集估计为 5
图 6:官方原图,两集观众的并集估计为 5。

求差集:看过第一集但没看第二集的人

THETA_SKETCH_NOT 计算有顺序的集合差。这里从第一集观众集合中排除第二集观众,结果回答“看过第一集,但没看第二集”的人数:

SELECT THETA_SKETCH_ESTIMATE(
         THETA_SKETCH_NOT(
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E1'),
           DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton' AND "episode" = 'S1E2')
         )
       ) AS users
FROM ts_tutorial
官方原图:看过 Bridgerton 第一集但没看第二集的观众估计为 3
图 7:官方原图,第一集观众减去第二集观众后的数量估计为 3。

编者提示:差集不能随意交换两个输入。图中的 1、5、3 来自原文官方截图,结合这份小数据可以理解其含义,但不能因此把 Theta sketch 对任意规模数据都描述为精确计数。

本教程得到的结论

  • 在 Apache Druid 中,可以用 Theta sketch 对大型数据集做去重数量分析。
  • 摘要允许配合 rollup:可以舍弃分析表中的单个原始值,仅保留用于统计近似的摘要。
  • 交集、并集和差集有助于分析人群关联,例如哪些细分人群相关、彼此重叠多少。

查询精度由 Theta sketch 的大小 k 以及执行的运算共同决定。减小或增大摘要不是免费的精度开关:要结合空间成本、误差需求和集合运算评估。统计上可合并的是摘要,不能把不同时段或人群的去重计数直接相加。

继续阅读

为什么开头举的是 15 分钟,而不是一小时

原文脚注指出,不同时区的偏移不一定是整数小时。例如印度使用半小时偏移,尼泊尔使用 45 分钟偏移。15 分钟粒度更容易组合成这些时区各自的小时桶,因此比只保留整点小时桶更灵活。涉及用户去重时,这里的“组合”仍应使用摘要合并,而不是直接相加每个桶的去重人数。

致谢、版权与核验范围

官方教程注明改编自社区成员 Hellmar Becker 的 Druid Data Cookbook: Counting Unique Visitors for Overlapping Segments(2022 年 6 月 5 日)。本文以当前 Druid 官方教程为翻译对象,没有将原博文中的第三方封面素材另行搬入。

Copyright © 2026 Apache Software Foundation。除另有说明,原教程采用 CC BY-SA 4.0。本中文译编保留署名、来源及相同许可;翻译、术语澄清、静态审查补充和原创示意图为本次修改。Druid 软件另有其软件许可证;不要把文档许可和软件许可混同。Apache Druid、Druid 及 Druid 标志是 Apache Software Foundation 在美国及其他国家的商标或注册商标。

本稿只静态检查数据、SQL 和技术说明,没有执行入库或查询,没有启动服务;截图全部为官方原图。未把概率估计宣传为精确结果,也未据此声称任何吞吐量、延迟或安全保证。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容