Dataframe 使用分区批处理生成多个列,无需无序排列或自定义联接逻辑

Dataframe 使用分区批处理生成多个列,无需无序排列或自定义联接逻辑,dataframe,apache-spark,join,partitioning,Dataframe,Apache Spark,Join,Partitioning,设置 在我的管道中有几个用例,我需要使用一个外部工具的输出在一个非常宽的数据帧中添加几个列,该工具必须对大批量进行操作 外部工具将包含多个必需列和任意ID列的CSV文件作为输入,并输出包含输出列和我传入的相同ID的CSV文件。并非每一个输入行都会进入输出文件 概念 从概念上讲,这相当简单: 仅从源数据框中选择所需的列 通过调用投影数据帧上的mapPartitions来获取派生数据帧 使用提供的迭代器生成输入文件 调用外部二进制文件 将输出CSV文件作为输出分区的迭代器进行流式处理 使用my i

设置

在我的管道中有几个用例,我需要使用一个外部工具的输出在一个非常宽的数据帧中添加几个列,该工具必须对大批量进行操作

外部工具将包含多个必需列和任意ID列的CSV文件作为输入,并输出包含输出列和我传入的相同ID的CSV文件。并非每一个输入行都会进入输出文件

概念

从概念上讲,这相当简单:

  • 仅从源数据框中选择所需的列
  • 通过调用投影数据帧上的mapPartitions来获取派生数据帧
  • 使用提供的迭代器生成输入文件
  • 调用外部二进制文件
  • 将输出CSV文件作为输出分区的迭代器进行流式处理
  • 使用my id列将派生数据帧左键连接到源数据帧
  • 问题

    实际上,执行左连接将导致源数据帧的无序移动,从而通过ID列对其进行重新分区。假设外部工具基本上是一个平面图,为每个输入行发出一个或0个项目,那么从语义上来说,洗牌是不必要的。我可以保证这个“连接”只需要来自当前存在的分区的数据。在一般情况下,我不能对输入数据帧的分布做任何假设

    当前解决方案

    这里显而易见的解决方案是手动实现分区连接

    最简单的例子是将整个(宽)数据帧分区(在mapPartition内)具体化为一个整行列表作为值(作为行对象或case类)。使用该集合生成输入CSV、调用二进制文件、将输出CSV具体化为由输入ID键入的HashMap,并手动执行列表的左联接,创建新行或输出案例类对象

    这是可行的,但有几个不好的特点。由于输入数据帧可能有300多个列,因此必须具体化整个分区的成本非常高。此外,任何使用mapPartitions的解决方案都是如此,如果输入数据帧碰巧被某些键列分割,mapPartitions会丢弃这些信息,即使在我们的例子中我们没有更改键。这意味着任何下游连接都必须重新排列数据帧以恢复正确的分布(可能完全相同)

    内存问题可以通过使用sortWithinPartition通过我们的连接键对2个数据帧的分区进行有效排序来解决(这将在需要时退回到外部排序),然后下拉到RDDAPI以使用zipPartitions,最后使用提供的迭代器手动实现所需的任何连接机制。这样做的好处是,我们一次只实现几行。这个解决方案就是我们目前正在使用的,虽然它可以工作,但它需要大量的自定义代码,我宁愿不必维护这些代码。我也不喜欢跳过RDD层,因为我想在catalyst中尽可能多地表达我的管道。如前所述,此解决方案还存在丢失分发问题,因为它基于mapPartitions

    问题

    有没有一个老生常谈的习惯做法,就是这样做而不用洗牌?理想情况下,在维护输入数据帧的分布知识的同时

    我的研究

    据我所知,维护分发知识的唯一方法是使用withColumn。这是有意义的,因为Spark很明显,我们只是添加列,而不是更改分区表达式中可能包含的任何列

    然而,withColumn在这里不能很好地开箱即用,因为我们需要一次处理一整批记录

    在输入数据帧被低基数表达式(想想组/自定义分区ID)分区的情况下,我可以按ID将组聚合到一个列表中,使用UDF调用二进制文件,将结果分解回来,最后的数据帧完全按照我的要求执行(没有无序排列,它仍然由同一个表达式分发)

    不幸的是,我不能保证输入数据帧是由任何这样的列进行分区的。我希望我可以使用spark\u partition\u id列作为上述情况下的完美groupId,groupBy希望使用我的UDF方法,但经过一些实验,spark似乎不理解按分区id进行分组在语义上可以是无混洗的,因为所有的行都已经位于同一位置


    在某一点上,我甚至试图通过强制HashClusteredDistribution使源数据帧看起来像是正确分区的,但这太糟糕了,我放弃了。

    以防这对任何人都有帮助,我使用SparkSessionExtensions API注册了一个自定义分析器规则和一个自定义计划器,得到了一个类似这样的工作POC。其思想是在SortMergeJoinExec节点周围注入定制的物理节点,以提供所需的分发信息,并停止确保注入洗牌的要求。要明确的是,这绝对是危险的,很多事情都可能出错,但这是可能的。