【spark】行列互转的思考

本文通过一个具体的案例,详细介绍了如何在Spark SQL中使用CASEWHEN、STACK和LATERALVIEW EXPLODE结合PIVOT进行行转列操作。对不同方法进行了性能比较,结果显示CASEWHEN方法在处理2.5亿条数据时耗时最短,为1.3分钟,而LATERALVIEW EXPLODE耗时最长,达到10分钟。文章还对PIVOT和STACK函数进行了简要说明,并强调在实际工作中应考虑语法通用性和性能因素。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文基于一个具体的案例,介绍spark中case when,stack,lateral view explode,pivot的用法,并做一些性能上的比较

一、背景

群里有个小伙伴出了一道题目是行转列(多行数据转成多列数据)

如图,把上表的数据通过sql变成下表的数据

当然实现方法还是比较多的,我也想了几种方式,并随便搞了2.5亿条数据测试下性能(表格式是text,每个文件块大小都很均匀)

二、实现方法

不同引擎里,会有不同的函数或者语法做行列的转换,我这里就针对spark引擎来说,我的测试环境是spark-3.1.2,我想到了几种方法来解这道题,包括最简单的CASE WHEN,STACK,LATERAL VIEW EXPLODE,包括在2.4左右的版本才添加的PIVOT(类似excel的透视方法),下面我来说说我的三种想法和实现

数据初始化:

CREATE TABLE `t`(
  `class` string,
  `people` string,
  `code` int,
  `weight` int);

insert into t select 'A','y',30,60 
union all select 'A','n',20,70;

1、CASE WHEN

这个语法我应该不用过多阐述,非常基础的行转列操作

SELECT class ,
    sum(CASE people WHEN 'y' THEN code ELSE 0 END ) as y_code,
    sum(CASE people WHEN 'y' THEN weight ELSE 0 END ) as y_weight,
    sum(CASE people WHEN 'n' THEN code ELSE 0 END ) as n_code,
    sum(CASE people WHEN 'n' THEN weight ELSE 0 END ) as n_weight
FROM t
GROUP BY class;

处理时长:1.3min

2、LATERAL VIEW EXPLODE + PIVOT

pivot语法,是将如下格式转换为最终的结果,所以要思考怎么将数据处理成如下格式,在方案二里用的是字段拼接(concat)把code和weight的key和值都拼到一起,再加上LATERAL VIEW EXPLODE把数据拆成多行

具体处理过程,大家可以把sql一段一段执行下看看结果是什么

classatrribute_keyvalue
Ay_code30
Ay_weight60
An_code20
An_weight70

 

select * from 
(select class,
concat(people,'_',split(value1,'_')[0]) as atrribute_key,
cast(split(value1,'_')[1] as bigint) as value from
(select class,people,value1
from 
(select class,people,concat('code_',code,':','weight_',weight) agg from t)tmp 
lateral view explode(split(agg,':')) tmp as value1)tmp) models
pivot(
sum(value)
for atrribute_key in ('y_code','y_weight','n_code','n_weight'));

处理时长:10min 

3、STACK + PIVOT

最后一种是和第二种其实差不多,核心都是用pivot做最后一步处理,只是将数据转成pivot能处理的数据的方式不同,使用的是stack

select * from 
(select class,
stack(2, concat(people,'_','code'),`code`,concat(people,'_','weight'),`weight`) as (`atrribute_key`,`value`)
from t) t
pivot(
sum(value)
for atrribute_key in ('y_code','y_weight','n_code','n_weight'));

处理时长:5min(我多跑了几次,有的是4.9min,截图是5.2min) 

三、介绍下函数

LATERAL VIEW EXPLODE和CASE WHEN我想大家应该都很常见了

主要说说PIVOT和STACK这两个比较少见的

1、PIVOT

我想官方文档其实介绍的比较清楚了,大家看下面官方文档的链接就可以了

sum(value)是每个单元格填的值,for后面是把atrribute_key字段中每一种可能都作为单独的一列

当然sql写的时候,需要把所有的列的值都写出来,如果用spark的DSL的语法写的话,不需要全部列出来,对于值不确定,不清楚会有几列的场景,就比较友好了~

df.groupBy("class").pivot("atrribute_key").agg(sum("value")).show()

PIVOT Clause - Spark 3.2.1 Documentation

2、STACK

这个函数其实官方手册里没有写的很明白到底怎么用

我来解释下,stack方法,会使得一行数据变多行,并且该函数会返回多列

我总共传了5个参数,第一个参数是个数字,说明要把一行数据拆成几行

后面传入的参数依次是,第一行的第一列填入什么内容,第一行的第二列填入什么内容,在我的case中,第一行的第一列填入的是people字段的值和“_”和“code”拼接,会变成“y_code”,然后第一行的第二列填入code的值

再接着后面两个参数分别是:第二行的第一列填入什么内容,第二行的第二列填入什么内容

所以原来的表,就会转换成如下的样子

Spark SQL, Built-in Functions

四、总结

其实用了几个比较少见的花里胡哨的函数或者语法,这些函数还不一定每个计算引擎/数据库里都有,反而会带来切换引擎时的有缝切换,其实这并不是我们想看到的,因为建议大家在写sql的时候,尽量还是写些通用的语法,减少切换引擎的工作量,并且我们看到在这个例子中,CASE WHEN的性能是最好的,1.3min就跑完了,其次是STACK,最差的是LATERAL VIEW EXPLODE

所以如果列数确定,直接case when就得了,不确定的情况下,又有spark较新的版本,可以使用DSL语法实现pivot与stack结合使用

太久太久没写文章了(前两篇文章分别是22年1月和21年7月)

但是其实我的写作并没有停止,只是没写在这里罢了

对未来着实是迷茫,不知道自己的职业发展会走向何方,虽然现在每天压力也很大,也学了很多东西,但是缺乏技术的深度,我蛮担心能力会越来越钝,变得离不开现在的环境,慢慢失去竞争力,哎不知如何是好~

### 使用 Spark SQL 实现实行列转换 #### 行转列(Pivoting) 行转列通常用于将多行数据聚合到单个结果集中,形成更宽泛的结果表。这可以通过 `PIVOT` 操作来完成。 对于简单的场景,可以使用内置函数如 `pivot()` 来实现实现: ```sql SELECT department, SUM(CASE WHEN gender = 'Male' THEN salary ELSE 0 END) AS male_salary, SUM(CASE WHEN gender = 'Female' THEN salary ELSE 0 END) AS female_salary FROM employees GROUP BY department; ``` 当涉及到更多动态值时,则应采用标准的 `PIVOT` 语句[^3]: ```sql SELECT * FROM ( SELECT department, gender, salary FROM employees ) PIVOT( SUM(salary) FOR gender IN ('Male', 'Female') ); ``` 此查询会创建一个新的表格视图,在其中每个性别对应的薪资总和被展示在同一行内不同列下。 #### 列转行(Unpivoting) 相反的过程——即把多个属性展开成若干条记录的形式称为“unpivot”。虽然 Spark SQL 并未直接提供 unpivot 的功能,但是可以通过自定义的方式轻松达成目的。 一种方法是利用 union all 结合 case when 或者 explode 函数配合 map 类型来进行转换: ```scala import org.apache.spark.sql.functions._ val df = spark.createDataFrame(Seq( (1, "A", 100), (2, "B", 200) )).toDF("id", "category", "value") // Method using Union All with Case When df.select($"id", expr("CASE WHEN category='A' THEN value END as A"), expr("CASE WHEN category='B' THEN value END as B")) .na.fill(0) // Or use Explode function combined with Map type df.withColumn("pairs", arrays_zip(array(lit("A"), lit("B")), array(col("value")))) .withColumn("exploded_pairs", explode_outer($"pairs")) .select($"id", $"exploded_pairs.*") ``` 第二种方式更加简洁高效,并且能够很好地处理缺失值的情况。 为了更好地理解这两种变换的实际应用场景以及它们之间相互作用的影响,建议读者尝试构建一些实际案例并观察其效果变化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值