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

PySpark中通过While Loop迭代关联实现层级描述补全

问题描述

原始PySpark DataFrame定义及展示:

data = [('service 1','s1', 's2'),
       ('service 2','s2', 's4'),
       ('service 3','s3', 's5'),
       ('service 5','s5', 's6'),
       ('service 4','s4', 's3')]
sdf = spark.createDataFrame(data, schema = ['description', 'service', 'parent'])
sdf.show()

输出:

+-----------+-------+--------+
|description|service|parent  |
+-----------+-------+--------+
|  service 1|     s1|      s2|
|  service 2|     s2|      s4|
|  service 3|     s3|      s5|
|  service 5|     s5|      s6|
|  service 4|     s4|      s3|
|  service 6|     s6|    NULL|
+-----------+-------+--------+

需求

判断parent列的值是否存在于service列中,添加对应description的新列;最终删除service和原始parent列,期望结果如下:

+-----------+-----------+-----------+-----------+-----------+-----------+
|description|     parent|    parent1|    parent2|    parent3|    parent4|
+-----------+-----------+-----------+-----------+-----------+-----------+
|  service 1|  service 2|  service 4|  service 3|  service 5|  service 6|
|  service 2|  service 4|  service 3|  service 5|  service 6|       NULL|
|  service 3|  service 5|  service 6|       NULL|       NULL|       NULL|
|  service 5|  service 6|       NULL|       NULL|       NULL|       NULL|
|  service 4|  service 3|  service 5|  service 6|       NULL|       NULL|
+-----------+-----------+-----------+-----------+-----------+-----------+

当前问题

已实现基于service和parent列的循环关联,但生成的parent列仅保存service值,无法替换为对应的description。当前执行代码及最终输出如下:

执行代码:

i = 0
while(sdf.filter(F.col(f"parent{i if i>0 else ''}").isNotNull()).count() > 0):

  sdf = sdf.alias("a1").join(sdf.alias("a2").select("service", "parent"),
                            F.col(f"a1.parent{i if i>0 else ''}")==F.col("a2.service"), 
                            how="left") \
           .withColumn(f"parent{i+1}", F.col("a2.parent")) \
          .drop(F.col("a2.service")) \
          .drop(F.col("a2.parent")) 
  i += 1
  display(sdf)

最终输出:

+-----------+-------+------+-------+-------+-------+-------+-------+
|description|service|parent|parent1|parent2|parent3|parent4|parent5|
+-----------+-------+------+-------+-------+-------+-------+-------+
|  service 1|     s1|    s2|     s4|     s3|     s5|     s6|   null|
|  service 2|     s2|    s4|     s3|     s5|     s6|   null|   null|
|  service 3|     s3|    s5|     s6|   null|   null|   null|   null|
|  service 5|     s5|    s6|   null|   null|   null|   null|   null|
|  service 4|     s4|    s3|     s5|     s6|   null|   null|
+-----------+-------+------+-------+-------+-------+-------+-------+
解决方案

方案1:循环关联+映射替换

核心思路是先建立service到description的映射,初始时将原始parent列替换为对应描述,后续每轮循环都通过当前parent的描述关联到对应记录,获取下一级parent的描述。

from pyspark.sql import functions as F

# 构建service与description的映射表
service_desc_map = sdf.select("service", "description").alias("map")

# 初始处理:替换原始parent为对应description,删除原始service列
sdf_processed = sdf.alias("main").join(
    service_desc_map,
    F.col("main.parent") == F.col("map.service"),
    "left"
).withColumnRenamed("map.description", "parent") \
 .drop("map.service", "service")

i = 0
while sdf_processed.filter(F.col(f"parent{i if i>0 else ''}").isNotNull()).count() > 0:
    current_col = f"parent{i if i>0 else ''}"
    next_col = f"parent{i+1}"
    
    # 关联原表,通过当前parent的description找到对应service记录,获取其parent的description
    sdf_processed = sdf_processed.alias("a1").join(
        sdf.alias("a2").select("description", "parent"),
        F.col(f"a1.{current_col}") == F.col("a2.description"),
        "left"
    ).join(
        service_desc_map,
        F.col("a2.parent") == F.col("map.service"),
        "left"
    ).withColumn(next_col, F.col("map.description")) \
     .drop("a2.description", "a2.parent", "map.service", "map.description")
    
    i += 1

# 展示最终结果
sdf_processed.show()

方案2:递归CTE构建层级链(更高效)

利用Spark SQL的递归CTE直接构建每个节点向上的完整父级描述链,再将链展开为多列,避免循环关联的开销。

from pyspark.sql import functions as F

# 使用递归CTE生成每个节点的父级描述链
hierarchy_df = spark.sql("""
    WITH RECURSIVE node_hierarchy AS (
        -- 起始节点:所有有父节点的记录,初始父链为对应父节点的description
        SELECT 
            s.description AS root_desc,
            s.parent AS current_parent_service,
            ARRAY(COALESCE((SELECT description FROM sdf WHERE service = s.parent), NULL)) AS parent_descs
        FROM sdf s
        WHERE s.parent IS NOT NULL
        UNION ALL
        -- 递归遍历:向上查找父节点的父节点
        SELECT 
            nh.root_desc,
            s.parent AS current_parent_service,
            ARRAY_APPEND(nh.parent_descs, COALESCE((SELECT description FROM sdf WHERE service = s.parent), NULL)) AS parent_descs
        FROM node_hierarchy nh
        JOIN sdf s ON nh.current_parent_service = s.service
        WHERE s.parent IS NOT NULL
    )
    -- 合并无父节点的记录(父链为空数组)
    SELECT root_desc AS description, parent_descs FROM node_hierarchy
    UNION ALL
    SELECT description, ARRAY() AS parent_descs FROM sdf WHERE parent IS NULL
    -- 去重,保留每个节点的最长父链
    GROUP BY description, parent_descs
    ORDER BY description
""")

# 获取最大层级深度,用于展开数组为列
max_depth = hierarchy_df.select(F.size(F.col("parent_descs"))).agg(F.max("size(parent_descs)")).collect()[0][0]

# 将父链数组展开为多个parent列,不足长度补NULL
result_df = hierarchy_df.select(
    "description",
    *[F.coalesce(F.col("parent_descs")[i], F.lit(None)).alias(f"parent{i if i>0 else ''}") for i in range(max_depth)]
)

result_df.show()

方案说明

  • 方案1适合层级较浅的场景,逻辑直观,容易调试;
  • 方案2通过递归CTE一次性构建完整层级链,性能更优,适合层级较深的数据集。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 15:09:54