跳转到主要内容

使用 Theta sketches 进行近似计算

Apache Druid 可以支持点击流的实时收集、流式传输和交互式可视化。点击流分析中一个常见的问题是统计唯一项(unique things),例如访客或会话。通常这需要扫描所有明细数据,因为当你聚合数据时,唯一计数(unique counts)是无法直接相加的。

大数据集上的计数和集合操作问题

想象一下,您想了解观看过某部电视剧剧集的访客数量。假设您发现某一天有 1000 名独立访客观看了第一集,800 名访客观看了第二集。您可能想进一步探索趋势,例如:

  • 有多少访客观看了两集
  • 有多少访客观看了至少一集
  • 有多少访客观看了第一集但没有观看第二集?

仅凭聚合数字是无法回答这些问题的。您必须返回到明细数据并扫描每一行。如果数据量足够大,这可能需要很长时间,这意味着无法进行交互式的数据探索。

另一个麻烦是,唯一计数不能很好地与汇总(rollup)一起工作。对于这个例子,如果您能为每 15 分钟的时间间隔1、节目和剧集只保留一行数据,那就太好了。毕竟,您感兴趣的不是单个用户 ID,而是唯一计数。

有没有办法避免每次都计算明细数据,甚至实现汇总功能?Theta sketches 就是为此而生的。

使用 Theta sketches 进行快速近似集合操作

使用 Theta sketches 可以获得用于构建 sketch 的值集及其唯一计数的快速近似估计。Theta sketches 是一种概率数据结构,能够对具有已知误差分布的大数据进行近似分析。Druid 的实现依赖于 Apache DataSketches 库。

以下属性描述了 Theta sketches:

  • 与其他 sketches 类似,Theta sketches 是可合并的(mergeable)。这意味着您可以处理汇总后的数据,并跨各种时间间隔合并这些 sketches。因此,您可以利用 Druid 的汇总功能。
  • Theta sketches 特有的功能是支持集合操作。给定两个针对数据子集的 Theta sketches,您可以计算这两个子集的并集、交集或差集。这使您能够回答诸如示例中观看过特定剧集组合的访客数量等问题。

在本教程中,您将学习如何执行以下操作:

  • 在数据摄取时从输入数据创建 Theta sketches。
  • 在 Theta sketches 上执行唯一计数和集合操作查询,以探索前面提出的问题。

先决条件

在开始之前,请按照单机快速入门中的说明下载 Druid 并在本地运行。您无需向 Druid 集群加载任何数据。

完成教程:加载文件教程:查询数据会很有帮助。

示例数据

本教程使用以下数据:

  • date:时间戳。在本例中只是日期,但如前所述,在现实生活中使用更细的粒度是有意义的。
  • uid:用户 ID。
  • show:电视节目名称。
  • episode:剧集标识符。
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 sketches 摄取数据

使用 INSERT INTO 语句和 EXTERN 函数内嵌摄取示例数据。在 Druid Web 控制台中,转到查询(Query)视图并运行以下查询:

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 语句中的 theta_uid 列。它在摄取过程中定义了 uid 列上的 thetaSketch 聚合器。在这种情况下,您不关心单个用户 ID,只关心唯一计数。相反,您使用 DS_THETA 函数在 uid 的值上创建 Theta sketches。

DS_THETA 有一个可选的第二个参数,用于控制 sketches 的精度和大小。

GROUP BY 语句将同一天观看同一节目的每集条目进行分组。

查询 Theta sketch 列

从 Theta sketch 列计算唯一计数估计涉及以下步骤:

  1. 通过 Druid SQL 中的 DS_THETA 聚合函数合并列中的 Theta sketches。
  2. 使用 THETA_SKETCH_ESTIMATE 函数从合并后的 sketch 中检索估计值。

在第 1 步和第 2 步之间,您可以应用集合函数,如稍后在 集合操作 中演示的那样。

基本计数

让我们先看看 Druid 中的数据是什么样的。在查询编辑器中运行以下 SQL 语句:

SELECT * FROM ts_tutorial

View data with SELECT all query

Theta sketch 列 theta_uid 显示为 Base64 编码的字符串;其背后是一个位图(bitmap)。

以下查询使用 THETA_SKETCH_ESTIMATE 来计算用户 ID 的唯一计数,并按其他维度进行分组:

SELECT
__time,
"show",
"episode",
THETA_SKETCH_ESTIMATE(theta_uid) AS users
FROM ts_tutorial

Count distinct with Theta sketches

过滤后的指标

Druid 具有使用 过滤指标(filtered metrics)的功能。这意味着您可以在查询的 SELECT 部分包含 WHERE 子句。

信息

对于 Theta sketches,过滤器子句必须插入到聚合器和估计器之间。

例如,查询观看过 Bridgerton 的总独立用户数:

SELECT APPROX_COUNT_DISTINCT_DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton') AS users
FROM ts_tutorial

Count distinct with Theta sketches and filters

在前面的查询中,APPROX_COUNT_DISTINCT_DS_THETA 等同于调用 DS_THETATHETA_SKETCH_ESIMATE,如下所示:

SELECT THETA_SKETCH_ESTIMATE(
DS_THETA(theta_uid) FILTER(WHERE "show" = 'Bridgerton')
) AS users
FROM ts_tutorial

APPROX_COUNT_DISTINCT_DS_THETA 函数应用以下逻辑:

  • DS_THETA:从 Theta sketches 列创建一个新的 Theta sketch。
  • THETA_SKETCH_ESTIMATE:从 DS_THETA 的输出中计算唯一计数估计值。

请注意,过滤器子句将聚合查询限制为仅匹配过滤器的行。

集合操作

您可以将聚合器中的过滤功能与集合操作结合使用,以最终回答引言中的问题。

有多少用户观看了 Bridgerton 的两集?使用 THETA_SKETCH_INTERSECT 计算两个(或更多)段的交集的唯一计数:

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

Count distinct with Theta sketches, filters, and set operations

同样,集合函数被拼接到聚合器和估计器之间。

类似地,使用 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

Count distinct with Theta sketches, filters, and set operations

最后,还有 THETA_SKETCH_NOT,它计算两个或多个段的集合差。结果描述了有多少访客观看了 Bridgerton 的第一集但没有观看第二集。

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

Count distinct with Theta sketches, filters, and set operations

结论

  • 在 Apache Druid 中,可以使用 Theta sketches 对大数据集进行唯一计数。
  • 这使我们能够使用汇总(rollup)并丢弃单个值,仅在 sketches 中保留统计近似值。
  • 借助 Theta sketch 集合操作,关联分析变得更容易,例如回答哪些细分市场相关或重叠多少等问题。

了解更多

请参阅以下主题以获取更多信息:

致谢

本教程改编自社区成员 Hellmar Becker 的一篇博文

脚注

  1. 为什么要 15 分钟而不是 1 小时?15 分钟的间隔更适合国际时区,因为它们并不总是按小时对齐。例如,印度有时差 30 分钟,尼泊尔甚至有 45 分钟。通过 15 分钟的汇总,您也可以获得任何这些时区的小时总和!