2. Join查询计划解读:看懂Doris在背后做了什么

很多同学拿到EXPLAIN输出就头疼——一堆缩进、大写单词、数字,完全不知道Doris在表达什么。我刚开始接触Doris时也一样,看着那几屏文本,心里只有一个想法:这玩意儿能看懂?

但说实话,看懂查询计划是性能调优的第一步。你连Doris打算怎么执行你的SQL都不知道,怎么优化?今天我就带你把这层窗户纸捅破。

核心观点:EXPLAIN输出不是给DBA看的装饰品,它是你调优的导航地图。读懂了它,你就知道瓶颈在哪、数据怎么流动、Join怎么执行的。

2.1 EXPLAIN输出结构:从整体到局部

我们先跑一个最简单的例子。假设有两张表:orders(订单表)和customers(客户表),我们要做关联查询。

EXPLAIN SELECT o.order_id, c.customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date > '2024-01-01';

输出大概长这样(我简化了部分细节):

+----------------------------------------------------------------------+
| Explain String                                                       |
+----------------------------------------------------------------------+
| PLAN FRAGMENT 0                                                      |
|   OUTPUT EXPRS: `o`.`order_id` | `c`.`customer_name`                |
|   PARTITION: UNPARTITIONED                                           |
|   HAS_COLO_PLAN_NODE: true                                           |
|   STREAM DATA SINK                                                   |
|     EXCHANGE ID: 01                                                  |
|     UNPARTITIONED                                                    |
|                                                                      |
|   1:VEXCHANGE                                                        |
|      offset: 0                                                       |
|                                                                      |
| PLAN FRAGMENT 1                                                      |
|   OUTPUT EXPRS:                                                      |
|   PARTITION: HASH_PARTITIONED: `o`.`customer_id`                    |
|   STREAM DATA SINK                                                   |
|     EXCHANGE ID: 04                                                  |
|     HASH_PARTITIONED: `c`.`customer_id`                             |
|                                                                      |
|   4:VHASH JOIN                                                       |
|   |  join op: INNER JOIN (BROADCAST)                                |
|   |  equal join conjunct: `o`.`customer_id` = `c`.`customer_id`     |
|   |  runtime filters: RF000[in] -> `o`.`customer_id`                |
|   |                                                                  |
|   |--2:VEXCHANGE                                                     |
|   |     offset: 0                                                    |
|   |                                                                  |
|   |--3:VOLAP_SCAN_NODE(`customers`)                                 |
|   |     TABLE: customers                                             |
|   |     PREAGGREGATION: ON                                           |
|   |     partitions=1/1                                               |
|   |     cardinality=100000                                           |
|   |                                                                  |
|   2:VOLAP_SCAN_NODE(`orders`)                                       |
|      TABLE: orders                                                   |
|      PREAGGREGATION: ON                                              |
|      partitions=3/3                                                  |
|      cardinality=5000000                                             |
+----------------------------------------------------------------------+

嗯,信息量不小。我们拆开看。

2.2 Plan Node与Plan Fragment:Doris的执行单元

Plan Fragment是Doris分布式执行的基本单位。每个Fragment可以在不同BE上并行执行。你可以把它理解成「一个可以在多台机器上跑的子任务」。

Plan Node则是Fragment内部的执行步骤。比如扫描数据、做Join、聚合等。

上面那个例子有两个Fragment:

  • Fragment 0:负责最终结果输出,只有一个EXCHANGE节点(接收数据)。
  • Fragment 1:真正的干活单元,包含Scan和Hash Join。

我个人习惯先看Fragment数量。如果发现Fragment特别多(比如十几个),那就要警惕了——可能产生了过多的数据交换。

小技巧:Fragment之间的数据交换是通过EXCHANGE节点完成的。EXCHANGE越多,网络开销越大。理想情况下,大表Scan和小表Join应该尽量在同一个Fragment内完成。

2.3 Join Operator:三种Join方式

Doris支持三种Join算子。我按使用频率排个序:

Join类型 适用场景 内存消耗 我见过的坑
HASH JOIN 大表Join小表,等值连接 较高(需要构建Hash表) 小表不够小,Hash表撑爆内存
MERGE JOIN 两表都已排序,或非等值连接 较低 排序开销被忽略,反而更慢
NESTED LOOP JOIN 小表Join小表,或连接条件复杂 极低 数据量稍大就慢到怀疑人生

HASH JOIN是最常用的。它的逻辑很简单:把右表(通常是较小的表)加载到内存,建一个Hash表,然后遍历左表去匹配。我在项目中遇到过一个问题——右表明明只有几百万行,但每行数据特别宽(几百个字段),结果Hash表建出来好几个G,直接把BE搞OOM了。后来我强制做了列裁剪,只保留Join需要的字段,问题就解决了。

MERGE JOIN呢?它要求两表都按Join key排好序。如果数据本身有序(比如模型里用了Sorted Key),Merge Join就非常快。但如果你没排序硬要用,Doris会先排序再Merge,那开销就大了。

NESTED LOOP JOIN——说实话,我很少主动用它。它就是一个双重循环:对左表的每一行,去右表里找匹配。数据量一上去,复杂度就是O(n*m)。我曾经接手过一个慢查询,Doris自动选择了Nested Loop Join,两个表各100万行,跑了半小时没出结果。改成Hash Join后,3秒搞定。

避坑指南:如果你在EXPLAIN输出里看到NESTED LOOP JOIN,而且两个表的数据量都超过10万行,那大概率是执行计划选错了。检查一下Join条件,是不是缺少等值连接?或者统计信息不准?

2.4 数据分布与Shuffle策略

这是多表关联性能的命门。数据分布不对,再好的Join算子也白搭。

Doris有三种Shuffle策略:

  • BROADCAST:把小表复制到所有节点。适合小表Join大表。
  • SHUFFLE:两表都按Join key重新分区。适合两表都大。
  • COLOCATE:两表数据已经按相同方式分布,无需移动。这是最理想的情况。

回到刚才的EXPLAIN输出,注意这一行:

|  join op: INNER JOIN (BROADCAST)                                |

Doris选择了BROADCAST,把customers表(10万行)广播到所有节点。为什么?因为Doris判断customers足够小,广播比Shuffle更划算。

那什么时候用SHUFFLE?我举个例子:两个大表各1亿行,Join key是user_id。如果广播,1亿行数据复制,网络直接打满。这时候Doris会选SHUFFLE,两表都按user_id哈希分区,每个节点只处理自己那一份。

至于COLOCATE Join,这是Doris的杀手锏。如果两张表在建表时用了相同的DISTRIBUTED BY HASH和BUCKETS,并且数据分布一致,那Join时数据就在本地,零网络开销。我在做实时大宽表场景时,就靠Colocate Join把查询延迟从秒级降到了毫秒级。

关键判断:看EXPLAIN输出里的HASH_PARTITIONED或BROADCAST。如果发现大表被广播了,或者Shuffle的key跟Join key不一致,那就要手动干预了。

2.5 一张图看懂Join查询计划

下面这张SVG图,把整个流程串起来了。你可以对照着看,加深理解。

Doris Join查询计划执行流程 SQL查询 解析 & 生成Plan Fragment Fragment 0(协调节点) EXCHANGE → 结果聚合 负责接收各BE返回的数据 Fragment 1(执行节点) Scan + Hash Join 在多个BE上并行执行 数据分布策略选择 BROADCAST 小表复制到所有节点 SHUFFLE 两表按Join key重分区 COLOCATE 数据本地化,零网络开销 💡 选择依据 表大小、Join key、数据分布

2.6 实战:如何快速定位问题

拿到一个慢查询的EXPLAIN输出,我一般按这个顺序排查:

  1. 看Join类型:是不是HASH JOIN?如果不是,为什么?
  2. 看Shuffle策略:大表有没有被广播?小表有没有被Shuffle?
  3. 看数据量:cardinality字段显示的行数对不对?如果统计信息不准,执行计划可能选错。
  4. 看Runtime Filter:有没有生成?能不能下推到Scan阶段?

我曾经遇到一个案例:EXPLAIN显示Doris选择了BROADCAST,但被广播的表有500万行。结果每个BE都收到了完整的500万行数据,内存直接爆了。后来我查了一下,发现是统计信息没更新,Doris以为这张表只有5万行。跑一次ANALYZE TABLE后,执行计划就正常了。

我的建议:养成习惯,每次上线新查询前,先跑EXPLAIN看一眼。花30秒读一下执行计划,可能帮你省下几小时的排障时间。

好了,关于Join查询计划的解读就到这里。记住,EXPLAIN不是天书,它是Doris在跟你对话。你读懂了它,就知道该怎么调优了。