You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spark Scala中分区排序范围窗口的去重计数优化问询

优化Spark中分组12小时窗口内设备去重计数的性能问题

问题背景

给定以下小时级时间粒度的DataFrame,需计算每个Group与TimeString(或UnixTimestamp)组合对应的过去12小时窗口内的DeviceId去重数量,窗口范围为当前时间点的前12小时(不含当前时间点)。

原始DataFrame:

+----------------------------------------------------+
|DeviceId | TimeString       | UnixTimestamp | Group |
+----------------------------------------------------+
|8313726  | 2023-10-17 01:00 | 1697529600    | g1    |
|0065322  | 2023-10-17 01:00 | 1697529600    | g2    |
|2902499  | 2023-10-17 00:00 | 1697526000    | g2    |
|1506928  | 2023-10-17 00:00 | 1697526000    | g1    |
|7466245  | 2023-10-17 14:00 | 1697576400    | g3    |
+------------------+----------------+----------------+

示例输出要求:

+----------------------------------------------------+
| Group | TimeString       | UniqueDevicesPast12Hours|
+----------------------------------------------------+
| g1    | 2023-10-17 00:00 |  1                   |
| g2    | 2023-10-17 01:00 |  2                   |
| g2    | 2023-10-17 00:00 |  1                   |
| g1    | 2023-10-17 01:00 |  2                   |
| g3    | 2023-10-17 14:00 |  1                   |
+------------------+----------------+----------------+

注:第一行表示g1组在2023-10-17 00:00之前12小时(2023-10-16 12:00至2023-10-16 23:00)内的唯一设备数。

已尝试方案及问题

方案1:UDF+窗口聚合

通过窗口函数收集12小时内的DeviceId,再用UDF计算去重数量,代码如下:

val windowSpecLast12 = Window.partitionBy("Group").
orderBy(col("UnixTimestamp").cast(LongType)). // 按数值时间戳排序
rangeBetween(-12 * 3600, -1) // 12小时的秒数范围

// 定义UDF计算数组内DeviceId的去重数量
val distinctCountUDF = udf((ids: Seq[String]) => ids.distinct.size)

df = df.withColumn("DistinctCountLast12", distinctCountUDF(collect_list("DeviceId").over(windowSpecLast12)))

问题:该方案逻辑可行,但大数据量下UDF性能极差,无法满足性能要求。

方案2:dense_rank替代去重计数

尝试用dense_rank()替代UDF计算去重数量,代码如下:

df = df.withColumn("RankLast12", dense_rank().over(windowSpecLast12))

问题:触发Spark分析异常:

org.apache.spark.sql.AnalysisException: Window Frame specifiedwindowframe(RangeFrame, -43200, -1) must match the required frame specifiedwindowframe(RowFrame, unboundedpreceding$(), currentrow$());

原因是dense_rank()这类排序窗口函数默认要求窗口帧为ROW BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,与我们使用的RANGE类型窗口帧不兼容。

优化方案:Spark SQL高效实现

方案A:精确去重计算(自连接+分组聚合)

通过自连接关联同一分组内12小时窗口内的数据,再分组统计去重设备数,完全利用Spark内置优化,避免UDF性能瓶颈。

Spark SQL代码:

WITH base_data AS (
    SELECT 
        `Group`,
        TimeString,
        UnixTimestamp,
        DeviceId
    FROM your_table_name -- 替换为你的表名或视图名
),
window_data AS (
    SELECT 
        b1.`Group`,
        b1.TimeString,
        b2.DeviceId
    FROM base_data b1
    INNER JOIN base_data b2
        ON b1.`Group` = b2.`Group`
        -- 关联条件:b2的时间在b1当前时间的前12小时到前1秒之间
        AND b2.UnixTimestamp BETWEEN (b1.UnixTimestamp - 12 * 3600) AND (b1.UnixTimestamp - 1)
)
SELECT 
    `Group`,
    TimeString,
    COUNT(DISTINCT DeviceId) AS UniqueDevicesPast12Hours
FROM window_data
GROUP BY `Group`, TimeString
ORDER BY `Group`, TimeString;

方案B:近似去重计算(窗口内置函数)

如果业务允许一定误差(误差率约5%),可以使用Spark内置的approx_count_distinct窗口函数,性能远高于精确计算,且无需UDF。

Spark SQL代码:

SELECT 
    `Group`,
    TimeString,
    approx_count_distinct(DeviceId) OVER (
        PARTITION BY `Group`
        ORDER BY UnixTimestamp
        -- 窗口范围:当前时间前12小时到前1秒
        RANGE BETWEEN 12 * 3600 PRECEDING AND 1 PRECEDING
    ) AS UniqueDevicesPast12Hours
FROM your_table_name;

注:该方案依赖Spark版本对approx_count_distinct窗口函数的支持,建议使用Spark 3.0+版本。

内容的提问来源于stack exchange,提问作者aamirr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 03:33:16