AWS Databricks中高效为数百列独立计算10分位数的优化方案咨询
AWS Databricks中高效为数百列独立计算10分位数的优化方案咨询
嗨,针对你这个2000万行+400多列的大表分位数计算场景,我有几个针对性的优化思路,绝对比你目前的尝试高效太多——毕竟直接用ntile做全表排序,400多列就相当于400次全表排序,不卡28小时才怪呢😂
1. 优先用approxQuantile预计算阈值,再批量分配分位数
这是我最推荐的方案,核心是避开全表排序的巨大开销:
approxQuantile是Spark原生的分布式分位数计算工具,不需要把数据拉到单节点,也不用全表排序,速度快到飞起- 先算出每列的9个分位数阈值(10分位数需要9个分割点),再用
Bucketizer或者条件判断给每个值打1-10的分位数标签
代码示例:
from pyspark.sql import functions as F from pyspark.ml.feature import Bucketizer # 导入原表 df = spark.sql("select * from my_space.table1") target_cols = df.columns[1:] # 排除person_id num_deciles = 10 # 批量计算所有列的分位数阈值(relativeError控制精度,0为精确但慢,0.01足够满足业务需求) quantile_thresholds = {} for col in target_cols: # 生成1/10到9/10的分位数点 quantile_points = [i/num_deciles for i in range(1, num_deciles)] # 计算阈值,返回的是已排序的列表 quantile_thresholds[col] = df.approxQuantile(col, quantile_points, relativeError=0.01) # 基于阈值给每个列打10分位数标签 result_df = df.select("person_id") for col in target_cols: # 初始化Bucketizer,设置分割点(包含正负无穷处理边界值) bucketizer = Bucketizer( splits=[float('-inf')] + quantile_thresholds[col] + [float('inf')], inputCol=col, outputCol=f"{col}_decile_temp" ) # 应用Bucketizer并把0起始的桶编号转为1起始 col_decile_df = bucketizer.transform(df.select("person_id", col)) \ .withColumn(f"{col}_decile", F.col(f"{col}_decile_temp") + 1) \ .drop(f"{col}_decile_temp", col) # 关联到结果表 result_df = result_df.join(col_decile_df, on="person_id", how="inner") # 写入目标表 result_df.write.mode("overwrite").saveAsTable("my_space.my_new_table")
这个方法的优势:
- 只需要读取原表两次:一次算阈值,一次打标签,IO开销极小
- 分位数计算是分布式并行的,完全利用集群资源
- 精度可调节,0.01的误差在绝大多数业务场景下完全可以接受,要精确的话把
relativeError设为0就行(速度会慢一点,但还是比ntile快)
2. 调整Spark Shuffle参数,榨干集群性能
配合上面的方法,再调几个参数能进一步提速:
- 把
spark.sql.shuffle.partitions从默认200改成1000-2000(根据你的worker数量,比如10个worker的话设为1000,每个worker处理100个分区) - 确保
spark.sql.adaptive.enabled是开启的(Spark 3.x默认开启),它会自动根据数据量调整Shuffle分区数 - 增大
spark.executor.memoryOverhead到4G以上,处理大量列时内存压力会大一些
3. 如果你必须要精确分位数,试试percent_rank替代ntile
如果业务要求绝对精确的分位数,不能用近似计算,可以用percent_rank代替ntile,但要注意还是会触发全表排序,只是内存开销比ntile略小:
from pyspark.sql import functions as F from pyspark.sql.window import Window result_df = df.select("person_id") for col in target_cols: window_spec = Window.orderBy(col) col_decile_df = df.select("person_id", col) \ .withColumn("p_rank", F.percent_rank().over(window_spec)) \ .withColumn(f"{col}_decile", F.floor(F.col("p_rank") * 10) + 1) \ .drop("p_rank", col) result_df = result_df.join(col_decile_df, on="person_id", how="inner")
但这个方法还是逃不过全表排序,所以优先还是用approxQuantile的方案。
为什么你的尝试2会跑这么久?
因为ntile(num_deciles).over(Window.orderBy(col))会触发全表排序,而且400多列就要做400次全表排序,每一次排序都要把2000万行数据在集群间Shuffle,这个开销是指数级的,跑28小时都不意外。
总结一下,最值得尝试的就是approxQuantile+Bucketizer的组合,这个方案能把你的作业时间从几十小时压缩到几十分钟,完全不需要拆分列或者大幅扩容集群。
备注:内容来源于stack exchange,提问作者Isolated
相关产品推荐
相关产品推荐

