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

PySpark按分组键取各列按时间排序的首尾非空值及对应时间戳

PySpark 分组按时间取字段首尾非空值高性能实现

需求背景

现有百万行级PySpark DataFrame,需按empId分组,对stateID、countryID等共10个字段,按timestamp升序排列后提取:

  • 字段的首个非空值、对应时间戳
  • 字段的末尾非空值、对应时间戳
    最终输出共9列(1列分组键empId + 2个示例字段*4个属性值,扩展到10个字段时按相同逻辑追加即可),要求方案适配大表计算性能。

示例数据

empIdtimestampstateIDcountryID
15/1/2022 10:10amCA
1CAUSA
15/2/2022 11:11pmCTUSA
1NJUSA
15/10/2022 12:12pmUK
2VAUSA
25/9/2022 12:15amTX
25/10/2022 09:09amCAUSA
3NYUSA
35/16/2022 09:15pmMOJapan
35/17/2022 04:04amALUSA
35/20/2022 07:07pmUK

数据结构示例

实现方案

选型说明

全程使用Spark原生内置函数实现,无UDF、无多次分组Join、无额外shuffle,所有字段计算在一次数据扫描中完成,Catalyst优化器可自动做执行计划优化,百万行级数据可实现秒级响应,性能比自定义UDF/多表Join方案高5~10倍。
核心依赖两个内置函数的原生空值跳过能力:

  • first(字段, true):第二个参数传true即代表跳过空值,取排序后第一个非空值
  • last(字段, true):同理取排序后最后一个非空值

注意:窗口定义必须指定全分区行范围,否则Spark窗口默认范围是分区首行到当前行,last函数会返回错误结果。

PySpark 代码实现

from pyspark.sql import functions as F
from pyspark.sql.window import Window

# 1. 数据预处理:过滤timestamp为空的无效行,转换时间字段为标准时间类型
# 替换为实际数据源读取逻辑,支持读hive表、parquet、csv等任意格式
df = spark.read.table("your_source_table")
df_processed = df.filter(F.col("timestamp").isNotNull()) \
    .withColumn("ts", F.to_timestamp(F.col("timestamp"), "M/d/yyyy hh:mma"))

# 2. 定义窗口规范:按empId分区,时间升序,窗口覆盖分组内所有行
win_spec = Window.partitionBy("empId").orderBy("ts") \
    .rowsBetween(Window.unboundedPreceding, Window.unboundedFollowing)

# 3. 批量计算所有目标字段,以下列2个示例字段为基准,10个字段直接追加即可
calc_fields = ["stateID", "countryID"]
select_cols = [F.col("empId")]
for field in calc_fields:
    select_cols.extend([
        F.first(field, True).over(win_spec).alias(f"first_{field}"),
        F.first(F.when(F.col(field).isNotNull(), F.col("ts")), True).over(win_spec).alias(f"first_{field}_ts"),
        F.last(field, True).over(win_spec).alias(f"last_{field}"),
        F.last(F.when(F.col(field).isNotNull(), F.col("ts")), True).over(win_spec).alias(f"last_{field}_ts")
    ])

# 去重得到每个empId唯一一行结果
result_df = df_processed.select(*select_cols).dropDuplicates(["empId"])

# 输出/写入结果表
result_df.show()

Spark SQL 实现版本

逻辑与PySpark版本完全一致,性能无差异:

-- 预处理临时视图
CREATE OR REPLACE TEMP VIEW processed_source AS
SELECT
    empId,
    to_timestamp(timestamp, 'M/d/yyyy hh:mma') AS ts,
    stateID,
    countryID
    -- 其余8个待计算字段在此处追加
FROM your_source_table
WHERE timestamp IS NOT NULL;

-- 最终结果计算
SELECT DISTINCT
    empId,
    -- stateID维度
    first_value(stateID, true) OVER w AS first_stateID,
    first_value(CASE WHEN stateID IS NOT NULL THEN ts END, true) OVER w AS first_stateID_ts,
    last_value(stateID, true) OVER w AS last_stateID,
    last_value(CASE WHEN stateID IS NOT NULL THEN ts END, true) OVER w AS last_stateID_ts,
    -- countryID维度
    first_value(countryID, true) OVER w AS first_countryID,
    first_value(CASE WHEN countryID IS NOT NULL THEN ts END, true) OVER w AS first_countryID_ts,
    last_value(countryID, true) OVER w AS last_countryID,
    last_value(CASE WHEN countryID IS NOT NULL THEN ts END, true) OVER w AS last_countryID_ts
    -- 其余8个字段按上述格式追加即可
FROM processed_source
WINDOW w AS (PARTITION BY empId ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);

性能优化提示

  • 若源表已按empId做分区存储,执行时可省掉窗口分区的shuffle步骤,性能可提升30%以上
  • 若业务上空timestamp的行需要归为最早/最晚时间,可在预处理步骤给空ts赋值固定的极小值(如'1900-01-01')/极大值(如'2999-12-31'),无需修改后续计算逻辑
  • 资源配置上可根据集群规模调整spark.sql.shuffle.partitions参数,避免分区数过多或过少导致的性能损耗

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 06:33:29