在多表Join查询中,Join的执行顺序对查询性能有显著影响。SelectDB优化器基于代价模型自动选择Join顺序,但某些场景下可能不是最优解。Leading Hint允许用户显式指定表的Join顺序,包括左深树和右深树结构。本文介绍Leading Hint的语法、使用方式和验证方法。
什么是Leading Hint
Leading Hint是一种查询优化提示,用于告诉优化器按照指定的顺序执行多表Join,而非依赖代价模型自动选择。
多表Join的执行顺序决定了中间结果集的大小。以三表Join为例,A JOIN B JOIN C可能的Join顺序有:
-
(A JOIN B) JOIN C:先Join A和B,再与C Join。
-
(A JOIN C) JOIN B:先Join A和C,再与B Join。
-
A JOIN (B JOIN C):先Join B和C,再与A Join。
如果选择错误的Join顺序,中间结果集可能急剧膨胀,导致内存溢出或执行时间激增。
语法说明
Leading Hint通过不同的写法控制Join树的形态。核心规则只有一条:大括号{}内的表先Join成一组,再与外层Join。没有大括号时,表按书写顺序逐个串联。三种树形结构速查:
树形结构 |
Hint写法 |
Join方式 |
典型场景 |
左深树 |
|
逐表串联,每一步的结果与下一张表Join |
按过滤率从高到低排列表,逐步缩小结果集 |
右深树 |
|
大括号内的表先Join,结果再与外层表Join |
多张小表先合并成一个结果集,再与大表Join |
灌木树 |
|
两组独立Join后,两个结果集再Join |
表天然分为两组,各组内Join条件选择性高 |
基本语法(左深树)
使用/*+ LEADING(t1 t2 t3 ...) */指定Join顺序,默认生成左深树结构:
-- 左深树:((t1 JOIN t2) JOIN t3) JOIN t4
SELECT /*+ LEADING(t1 t2 t3 t4) */
...
FROM t1
JOIN t2 ON t1.id = t2.t1_id
JOIN t3 ON t2.id = t3.t2_id
JOIN t4 ON t3.id = t4.t3_id;
左深树是最常用的形态,执行过程类似流水线:t1先与t2 Join,结果与t3 Join,最后与t4 Join,每一步都基于前一步的结果。当你清楚哪张表应该最先被扫描(通常是过滤后最小的表),用左深树将它排在首位即可。
大括号语法(右深树与灌木树)
大括号{}用于将表分组,组内的表先Join成一个整体,再与组外的表Join。嵌套方式不同,产生的树形结构也不同:
-- 右深树:t2和t3先Join,结果再与t1 Join
-- 适合:多张小维度表先合并,再与大事实表Join
SELECT /*+ LEADING(t1 {t2 t3}) */
...
FROM t1
JOIN t2 ON t1.id = t2.t1_id
JOIN t3 ON t2.id = t3.t2_id;
-- 灌木树:两组独立Join后,两个结果集再合并
-- 适合:表天然分为两组,各组有独立的关联条件
SELECT /*+ LEADING({t1 t2} {t3 t4}) */
...
FROM t1
JOIN t2 ON t1.id = t2.t1_id
JOIN t3 ON t3.id = t4.t3_id
JOIN t4 ON t2.id = t3.t2_id;
右深树的典型场景是将多张小表(如维度表)放入大括号先合并,得到一个较小的中间结果集,再与大表Join。灌木树则适合两组表分别有高效的Join条件,各自Join后产生的中间结果都比较小,最后合并时数据量可控。
实践示例
小表先行减少中间结果集
电商场景:订单表(大表)、用户表(中表)、城市表(小表)。优化器可能选择先Join订单和用户(两张大表),导致中间结果集过大。手动指定先Join用户和城市:
-- 不使用Hint:优化器可能选择 orders JOIN users JOIN cities
-- 使用Leading Hint:先Join小表,减少中间结果集
SELECT /*+ LEADING(users cities orders) */
u.username, c.city_name, SUM(o.amount) AS total
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN cities c ON u.city_id = c.city_id
WHERE o.order_date >= '2024-01-01'
GROUP BY u.username, c.city_name;
分组并行Join减少数据传输
当两组表分别有高效的Join条件时,使用灌木树结构让两组并行Join后再合并:
-- 灌木树:(orders JOIN order_items) JOIN (products JOIN categories)
-- 两组各自的Join条件选择性高,中间结果集小
SELECT /*+ LEADING({orders order_items} {products categories}) */
o.order_id, oi.quantity, p.product_name, c.category_name
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
WHERE o.order_date = '2024-06-01';
带子查询的场景
Leading Hint中使用表的别名来引用子查询或派生表:
-- 子查询使用别名在Leading中引用
SELECT /*+ LEADING(top_users o p) */
top_users.username, p.product_name, o.amount
FROM (
SELECT user_id, username FROM users WHERE vip_level >= 3
) top_users
JOIN orders o ON top_users.user_id = o.user_id
JOIN products p ON o.product_id = p.product_id
WHERE o.order_date >= '2024-01-01';
何时使用Leading Hint
-
统计信息不准确:优化器基于错误的行数估计选择了次优Join顺序。
-
数据倾斜:某些Join Key的数据严重倾斜,导致中间结果集大小与估计差距很大。
-
复杂查询调优:5张以上表的Join,优化器搜索空间受限可能无法找到全局最优解。
-
稳定执行计划:业务SQL需要固定执行计划,避免因统计信息波动导致性能抖动。
验证Leading Hint生效
使用EXPLAIN SHAPE PLAN查看Join树结构和Hint日志,确认Hint是否被采纳:
EXPLAIN SHAPE PLAN
SELECT /*+ LEADING(users cities orders) */
u.username, c.city_name, SUM(o.amount)
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN cities c ON u.city_id = c.city_id
GROUP BY u.username, c.city_name;
-- 输出示例(Join树 + Hint日志):
-- PhysicalResultSink
-- --hashAgg[GLOBAL]
-- ----hashJoin[INNER_JOIN colocated] hashCondition=((o.user_id = u.user_id))
-- ------PhysicalOlapScan[orders]
-- ------hashJoin[INNER_JOIN broadcast] hashCondition=((u.city_id = c.city_id))
-- --------PhysicalOlapScan[users]
-- --------PhysicalOlapScan[cities]
--
-- Hint log:
-- Used: leading(users cities orders)
-- UnUsed:
-- SyntaxError:
输出分为两部分:Join树以缩进显示各算子的层级关系,可以直观看到Join顺序和分发方式(如broadcast、colocated);Hint日志显示Hint的处理结果。
Hint日志的三种状态
状态 |
输出示例 |
含义 |
Used |
|
Hint已被采纳,实际执行计划遵循了指定的Join顺序。 |
UnUsed |
|
语法正确但未被采纳。通常是指定的Join顺序与Join条件冲突或违反了内部约束。 |
SyntaxError |
|
Hint存在语法问题。常见原因:表名与FROM子句中的表名或别名不匹配。 |
无论Hint处于哪种状态,查询都不会报错——优化器在Hint不可用时自动回退到代价模型选择Join顺序。建议调优时先用EXPLAIN SHAPE PLAN确认Hint状态为Used,再执行实际查询。
使用建议
-
Leading Hint中的表名必须与FROM子句中的表名或别名完全一致。
-
Leading Hint支持在表名之间加入
shuffle或broadcast同时指定数据分发方式,例如leading(t1 shuffle t2 broadcast t3)。优化器优先采用指定的分发方式,但当存在更优策略(如Colocate Join)时会自动调整。 -
一般原则:将过滤后结果集小的表放在Join顺序前面,减少中间结果膨胀。
-
建议先通过EXPLAIN对比默认计划与Hint计划,再通过实际执行时间确认优化效果。
-
当表数量超过10张时,优先考虑将查询拆分为多层子查询,而非依赖单条Leading Hint。