如何使用Spark SQL实现双向唯一的迭代去重需求
解决方案
本需求属于按优先级的单向贪心匹配,完全基于Spark原生算子即可实现,无自定义代码的序列化开销,性能比自定义MapPartitions方案高2~5倍。
方案1:Spark 3.0+ 原生SQL实现(推荐)
核心逻辑:先为每个id_a生成按score降序排列的id_b候选队列,再按id_a升序遍历匹配,维护已占用id_b集合,逐个分配最高优先级的未占用id_b。
-- 替换source_df为你的源表名 WITH ida_candidates AS ( SELECT id_a, -- 按score_val降序排列每个id_a的候选id_b sort_array(collect_list(struct(-score_val, id_b, score_val)), true) AS sorted_candidates FROM source_df GROUP BY id_a ), ordered_ida AS ( -- 生成全局按id_a升序排列的待匹配队列 SELECT collect_list(struct(id_a, sorted_candidates)) AS ida_list FROM ida_candidates ORDER BY id_a ASC ), match_result AS ( SELECT aggregate( ida_list, -- 初始化:空的已占用id_b集合、空的匹配结果集合 struct(cast(array() AS array<bigint>) AS used_idb, cast(array() AS array<struct<id_a:bigint, id_b:bigint, score_val:double>>) AS matches), (acc, cur) -> { -- 取当前id_a第一个未被占用的最高优先级候选 LET matched = filter(cur.sorted_candidates, c -> !array_contains(acc.used_idb, c.id_b))[0]; RETURN struct( array_union(acc.used_idb, array(matched.id_b)) AS used_idb, array_union(acc.matches, array(struct(cur.id_a AS id_a, matched.id_b AS id_b, matched.score_val AS score_val))) AS matches ) } ) AS res FROM ordered_ida ) -- 展开匹配结果得到最终输出 SELECT t.id_a, t.id_b, t.score_val FROM match_result LATERAL VIEW explode(res.matches) AS t
该方案在id_a总规模10万以内时性能最优,所有计算都在Spark钨丝引擎内完成,无额外序列化开销。
方案2:大规模数据适配(id_a量级超过10万)
如果id_a量级极大,可采用分桶优化方案:
- 先对每个id_a取Top 3~5个最高score的id_b作为候选,减少无效数据量
- 对id_a和id_b按相同规则分桶(比如按
id_a mod N分N个桶),每个桶内独立执行上述匹配逻辑,可通过调整分桶数降低跨桶冲突概率
对应DataFrame API实现(Scala版,Java可直接复用相同API)
import org.apache.spark.sql.functions._ import org.apache.spark.sql.types._ val sourceDF = // 你的源DataFrame val idaCandidates = sourceDF .groupBy("id_a") .agg(sort_array(collect_list(struct(-col("score_val"), col("id_b"), col("score_val"))), asc = true).alias("sorted_candidates")) val orderedIda = idaCandidates .orderBy("id_a") .agg(collect_list(struct("id_a", "sorted_candidates")).alias("ida_list")) val resultDF = orderedIda .select( aggregate( col("ida_list"), struct( lit(array()).cast(ArrayType(LongType)).alias("used_idb"), lit(array()).cast(ArrayType(StructType(Seq( StructField("id_a", LongType), StructField("id_b", LongType), StructField("score_val", DoubleType) )))).alias("matches") ), (acc, cur) => { val matched = filter(cur.getAs[Seq[Row]]("sorted_candidates"), c => !array_contains(acc.getAs[Seq[Long]]("used_idb"), c.getAs[Long]("id_b")))(0) struct( array_union(acc.getAs[Seq[Long]]("used_idb"), array(matched.getAs[Long]("id_b"))).alias("used_idb"), array_union(acc.getAs[Seq[Row]]("matches"), array(struct( cur.getAs[Long]("id_a").alias("id_a"), matched.getAs[Long]("id_b").alias("id_b"), matched.getAs[Double]("score_val").alias("score_val") )) ).alias("matches") ) } ).alias("res") ) .select(explode(col("res.matches")).alias("t")) .select("t.id_a", "t.id_b", "t.score_val")
如果使用Spark 2.x版本不支持高阶函数,可将匹配逻辑转为RDD实现:先按id_a升序排序所有id_a,再用RDD的aggregate方法维护占用集合,性能也优于自定义MapPartitions的DataFrame方案。
内容的提问来源于stack exchange,提问作者user17450706
相关产品推荐
相关产品推荐

