sparksql中的broadcast join和prestodb中的dynamic filter比较

本文对比了SparkSQL和Presto在处理Broadcast Join和Dynamic Filter上的策略。在给定的例子中,SparkSQL通过Broadcast Join优化了查询,尤其是在面对小表时。而Presto在处理动态生成的过滤条件时,未实现Broadcast,导致全表扫描和shuffle操作,影响性能。文章还引用了Presto的一个PR讨论动态过滤优化的可能性。

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

    今天在prestodb的qq群里看到有人提到说一个子查询在presto中非常慢:

    SELECT *
    FROM his_data_opt
    WHERE act_no IN (
            SELECT act_no
            FROM id_act_map
            WHERE id_number = '726067685144725'
            );

    可以看出,这是一个普通的非相关子查询,如果内部子查询经过过滤条件只剩几条,那么整个查询应该非常完美的在几秒中出结果,结果却卡着不动。原来是presto join处理的问题,对于普通的where条件,外部查询会把这个where条件下推,进行表过滤,但现在外表这个过滤条件是一个动态生成的条件,presto在进行上层的逻辑计划优化时,不知道这个动态生成的条件到底会产生多少条结果,于是presto把外部表进行了全表扫描,这在presto中成为dynamic filter,目前有人提了PR,还没有合并到主版本中。来看presto中的执行计划,查询语句如下:

    explain
    
    SELECT *
    FROM nation
    WHERE regionkey IN (
            SELECT regionkey
            FROM region
            WHERE regionkey = 1
            );

    执行计划:
    这里写图片描述

    从图中可以看出,presto对region表进行了谓词下推,而对nation表则没有生成Filter这样的物理操作符。同时可以看出,即使对于region和nation这样只有几十行的表,presto依然会对这两个表进行repartition,即shuffle操作,物理操作符是RemoteExchange,这代价也忒大了点吧,难怪会慢的不行。另外可以看出,对于这样的子查询,presto把它解析成了一个semi join来处理,即子查询中如果有重复的regionkey被查出来,只返回一个即可,这满足semi join的语义。
    那么来看看sparksql中怎么处理这个问题的吧,同样的表,同样的sql语句,来看sparksql中的执行计划:

    这里写图片描述
    从图中可以看出,通过Broadcast实现了连接查询,是不是表本身太小了导致的呢?换大表试试:

    explain
    
    SELECT *
    FROM orders
    WHERE o_orderkey IN (
            SELECT l_orderkey
            FROM lineitem
            WHERE l_orderkey = 100
            );

    orders表150万行,lineitem表600万行,执行计划如下:
    这里写图片描述
    发现还是broadcast实现的join,于是直接执行了一下,然后去yarn上看看DAG图,如下:
    这里写图片描述
    可以看出,对lineitem首先进行过滤,过滤完后只剩5条,然后把这5条数据进行broadcast,和presto进行比较的话,相当于在这里把一个dynamic filter广播到了集群的所有节点上。然后用这个条件去查询外表,就会快很多了。可见,presto和sparksql在join的实现上,还有有一定差距的,presto实现了类似hadoop里的map-side join,才有可能使用dynamic filter。没有对比就没有伤害啊!

    还没完,prestodb的社区里,来看看上边提到的这个dynamic filter相关的PR:
    https://github.com/prestodb/presto/issues/7428

    SELECT 
        count(*)
    FROM 
        fact_table a 
    JOIN d_date b 
    ON a.date_key = b.date_key 
    WHERE 
        b.year = 2017;

    pr中有这个查询语句,虽然不像上边那样的子查询,但是却可以像上边子查询那样来分析,比如,b.year = 2017这个查询条件下推到b表后,b表过滤后的数据条数非常少,比如只有一条,那么如果用这一条再和a表join,那么查询将会非常快,可惜,presto中还是因为没有实现broadcast,导致其不能完成这样的优化。

    ### Spark多层Left Join导致的'no available'问题解决方案及SQL语句 在Spark中,当执行多层左连接操作时出现“no available”错误,通常是由于数据倾斜、资源不足或Shuffle操作不当引起的。以下是针对此问题的详细解决方案及相关的SQL优化方法。 #### 1. 数据倾斜处理 数据倾斜是导致“no available”错误的主要原因之一。以下是一些有效的解决策略: - **广播小表**:如果参与连接的小表可以被广播到所有节点上,则可以通过广播连接减少Shuffle操作。使用`BROADCAST`提示来强制广播小表[^2]。 ```sql SELECT /*+ BROADCAST(table1) */ * FROM table1 LEFT JOIN table2 ON table1.id = table2.id; ``` - **分离倾斜数据**:如果已知某些键存在数据倾斜,可以将这些倾斜键的数据分离出来单独处理,然后再与其他数据合并[^2]。 ```sql -- 分离倾斜数据 SELECT * FROM table1_1 LEFT JOIN table2 ON table1_1.id = table2.id UNION ALL SELECT /*+ BROADCAST(table1_2) */ * FROM table1_2 LEFT JOIN table2 ON table1_2.id = table2.id; ``` - **打散数据(未知倾斜键)**:对于未知的倾斜键,可以通过添加随机前缀(盐值)来分散数据,从而避免单个分区的数据量过大[^2]。 ```sql SELECT id, value, CONCAT(id, CAST((RAND() * 10000) % 3 AS STRING)) AS new_id FROM A; ``` - **打散数据(已知倾斜键)**:如果已知某些键会导致倾斜,可以在这些键上动态生成多个副本,并通过`CASE WHEN`逻辑进行处理[^2]。 ```sql SELECT t1.id, t1.id_rand, t2.name FROM ( SELECT id, CASE WHEN id IS NULL THEN CONCAT('SkewData_', CAST(RAND() AS STRING)) ELSE id END AS id_rand FROM test1 WHERE statis_date = '20200703' ) t1 LEFT JOIN test2 t2 ON t1.id_rand = t2.id; ``` #### 2. 调整Shuffle行为 Shuffle操作可能导致资源分配不均,进而引发“no available”错误。可以通过以下方式优化Shuffle行为: - **启用Shuffled Hash Join**:如果两个表的字段哈希值计算后生成的分区数不同,可能会导致错误。可以通过设置参数`spark.sql.join.preferSortMergeJoin=false`来启用Shuffled Hash Join[^1]。 ```python spark.conf.set("spark.sql.join.preferSortMergeJoin", "false") ``` - **增加Shuffle分区数**:通过调整`spark.sql.shuffle.partitions`参数,可以增加Shuffle分区数,从而减少单个任务的压力[^1]。 ```python spark.conf.set("spark.sql.shuffle.partitions", "500") ``` #### 3. SQL语句优化 在编写SQL语句时,可以采用以下策略来提高性能并避免“no available”错误: - **过滤无用数据**:在连接之前,先对数据进行过滤,以减少不必要的Shuffle操作。 ```sql SELECT * FROM ( SELECT * FROM table1 WHERE key IS NOT NULL ) t1 LEFT JOIN ( SELECT * FROM table2 WHERE key IS NOT NULL ) t2 ON t1.key = t2.key; ``` - **分步执行复杂查询**:对于复杂的多层左连接,可以将其拆分为多个简单的查询步骤,逐步完成数据处理。 ```sql -- 第一步:连接table1table2 CREATE TEMPORARY VIEW temp1 AS SELECT * FROM table1 LEFT JOIN table2 ON table1.key = table2.key; -- 第二步:连接temp1table3 SELECT * FROM temp1 LEFT JOIN table3 ON temp1.key = table3.key; ``` #### 4. 资源调优 确保Spark集群有足够的资源来处理大规模的Shuffle操作。可以通过以下方式优化资源分配: - **增加Executor内存**:通过调整`spark.executor.memory`参数来为每个Executor分配更多内存[^1]。 ```python spark.conf.set("spark.executor.memory", "8g") ``` - **启用动态分配**:通过设置`spark.dynamicAllocation.enabled=true`来根据工作负载动态调整资源分配[^1]。 ```python spark.conf.set("spark.dynamicAllocation.enabled", "true") ``` --- ### 示例代码 以下是一个完整的示例,展示如何处理多层左连接中的“no available”问题: ```python from pyspark.sql import SparkSession from pyspark.sql.functions import col, broadcast # 创建SparkSession spark = SparkSession.builder.appName("Multiple Left Join").getOrCreate() # 加载数据 df1 = spark.read.format("csv").option("header", "true").load("path/to/df1.csv") df2 = spark.read.format("csv").option("header", "true").load("path/to/df2.csv") df3 = spark.read.format("csv").option("header", "true").load("path/to/df3.csv") # 清理数据 df1 = df1.filter(col("key").isNotNull()) df2 = df2.filter(col("key").isNotNull()) df3 = df3.filter(col("key").isNotNull()) # 使用广播连接 result = df1.join(broadcast(df2), df1.key == df2.key, "left") \ .join(broadcast(df3), df1.key == df3.key, "left") # 保存结果 result.write.format("csv").option("header", "true").save("path/to/output") ``` ---
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值