使用Leading-Hint控制Join顺序

更新时间:
复制 MD 格式

在多表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 AB,再与C Join。

  • (A JOIN C) JOIN B:先Join AC,再与B Join。

  • A JOIN (B JOIN C):先Join BC,再与A Join。

如果选择错误的Join顺序,中间结果集可能急剧膨胀,导致内存溢出或执行时间激增。

语法说明

Leading Hint通过不同的写法控制Join树的形态。核心规则只有一条:大括号{}内的表先Join成一组,再与外层Join。没有大括号时,表按书写顺序逐个串联。三种树形结构速查:

树形结构

Hint写法

Join方式

典型场景

左深树

leading(t1 t2 t3)

逐表串联,每一步的结果与下一张表Join

按过滤率从高到低排列表,逐步缩小结果集

右深树

leading(t1 {t2 t3})

大括号内的表先Join,结果再与外层表Join

多张小表先合并成一个结果集,再与大表Join

灌木树

leading({t1 t2} {t3 t4})

两组独立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顺序和分发方式(如broadcastcolocated);Hint日志显示Hint的处理结果。

Hint日志的三种状态

状态

输出示例

含义

Used

Used: leading(users cities orders)

Hint已被采纳,实际执行计划遵循了指定的Join顺序。

UnUsed

UnUsed: leading(t1 t2 t3)

语法正确但未被采纳。通常是指定的Join顺序与Join条件冲突或违反了内部约束。

SyntaxError

SyntaxError: leading(...) Msg:can not find table: xxx

Hint存在语法问题。常见原因:表名与FROM子句中的表名或别名不匹配。

无论Hint处于哪种状态,查询都不会报错——优化器在Hint不可用时自动回退到代价模型选择Join顺序。建议调优时先用EXPLAIN SHAPE PLAN确认Hint状态为Used,再执行实际查询。

使用建议

  • Leading Hint中的表名必须与FROM子句中的表名或别名完全一致。

  • Leading Hint支持在表名之间加入shufflebroadcast同时指定数据分发方式,例如leading(t1 shuffle t2 broadcast t3)。优化器优先采用指定的分发方式,但当存在更优策略(如Colocate Join)时会自动调整。

  • 一般原则:将过滤后结果集小的表放在Join顺序前面,减少中间结果膨胀。

  • 建议先通过EXPLAIN对比默认计划与Hint计划,再通过实际执行时间确认优化效果。

  • 当表数量超过10张时,优先考虑将查询拆分为多层子查询,而非依赖单条Leading Hint。