注册
达梦数据库优化器与 HINT 深度实践
技术分享/ 文章详情 /

达梦数据库优化器与 HINT 深度实践

🌌 2026/08/07 143 0 0

优化器是达梦数据库的核心组件之一,负责为一个 SQL 查询找到“最优”的执行方式,并生成执行计划。执行计划最终决定了查询的性能表现——同样一条 SQL,走全表扫描还是索引、用哈希连接还是嵌套循环,耗时可能相差几个数量级。

本文从优化器工作原理讲起,重点深入 HINT(优化器提示词):它何时该用、怎么写、如何验证生效,以及生产环境如何在不改应用代码的情况下动态注入。


一、优化器在做什么

优化器的首要优化目标通常是最快响应时间。达梦并不总是等全部结果算完才开始返回数据,而是可以通过 FIRST_ROWS 等参数设定优先返回的记录数,从而提升用户感知速度。

优化器生成最终执行计划,大致包含三个步骤:

1. 查询转换(Query Transformation)

在不改变语义的前提下,对 SQL 进行“重写”,转换为更高效的形式。例如:

  • 将低效的 NOT IN 子查询转换为 NOT EXISTS
  • 将过滤条件提前执行(谓词下推),减少中间结果集
  • 视图合并、子查询展开等
-- 语义等价,但优化器可能对 NOT EXISTS 生成更好的半连接计划 -- 改写前 SELECT * FROM orders o WHERE o.cust_id NOT IN (SELECT c.cust_id FROM blacklist c); -- 改写后(语义等价,且通常更利于半连接优化) SELECT * FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM blacklist c WHERE c.cust_id = o.cust_id );

2. 估算代价(Cost Estimation)

这是核心步骤。优化器会为每种可能的执行路径估算“代价”,代价综合了 I/O、CPU、内存等资源消耗。估算依据主要包括:

  • 选择率(Selectivity):满足条件的记录占比
  • 基数(Cardinality):预计处理的行数
  • 统计信息:表行数、索引分布、直方图等
-- 查看表/列统计信息是否过期,是排查“优化器选错计划”的第一步 SELECT TABLE_NAME, NUM_ROWS, LAST_ANALYZED FROM USER_TABLES WHERE TABLE_NAME IN ('ORDERS', 'CUSTOMERS'); -- 收集统计信息(按实际权限与规范调整) DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'ORDERS'); DBMS_STATS.GATHER_TABLE_STATS('SYSDBA', 'CUSTOMERS');

3. 生成计划(Plan Generation)

基于估算代价,从候选方案中选择代价最小的作为最终执行计划。多表连接时,连接顺序与连接算法的组合空间会迅速膨胀,这也是 HINT 经常介入的场景。

-- 不实际执行,只看优化器选择的计划 EXPLAIN SELECT o.order_id, c.cust_name FROM orders o, customers c WHERE o.cust_id = c.cust_id AND o.status = 'OPEN';

二、HINT 是什么:给优化器的“人工指令”

**HINT(优化器提示词)**本质上是写给优化器的人工指令,用来引导(甚至强制)它选择特定执行计划,而不是完全依赖代价估算。

HINT 不会改变 SQL 语义,只影响计划生成阶段的路径选择。典型使用场景:

场景 说明
统计信息失真 采样不足、数据倾斜、统计未及时更新,导致基数估算偏差
索引选择错误 本该走高选择性索引,却选了全表扫描或错误索引
连接方式不当 大表对大表误用嵌套循环,或小表驱动大表时未用索引连接
紧急止血 生产峰值期间,先用 HINT 稳住性能,再慢慢修根因
规避全局参数副作用 不想改全局 INI,只想对单条 SQL 做语句级控制

重要提醒:HINT 语法错误不会报错,会被直接忽略。操作后务必用 EXPLAIN 检查执行计划,确认 HINT 是否真的生效。


三、HINT 的两种使用方式

方式一:直接嵌入 SQL

将 HINT 写成特殊注释 /*+ HINT内容 */,紧跟在 SELECT / INSERT / UPDATE / DELETE / MERGE 关键字之后。

-- 正确:HINT 紧跟 SELECT SELECT /*+ INDEX(o, IDX_ORDERS_STATUS) */ o.order_id, o.amount FROM orders o WHERE o.status = 'OPEN'; -- 错误示范:HINT 位置不对 / * 与 + 之间有空格 → 会被忽略 SELECT /* + INDEX(o, IDX_ORDERS_STATUS) */ ...

适用场景:开发、测试阶段,可以改 SQL 源码时。

方式二:动态注入(SF_INJECT_HINT

通过系统函数把 HINT 与 SQL 文本绑定,无需修改应用代码。规则可存于 SYSINJECTHINT,由库侧自动匹配应用。

适用场景:生产环境、第三方应用、不方便发版改 SQL 时。

-- 1. 开启注入开关(需具备相应权限) SP_SET_PARA_VALUE(1, 'ENABLE_INJECT_HINT', 1); -- 2. 注入规则:模糊匹配 + 强制走指定索引 SF_INJECT_HINT( 'SELECT * FROM employee_info WHERE emp_id > 1;', -- sql_text 'INDEX(EMPLOYEE_INFO, IDX_EMP_ID)', -- hint_text 'INJECT_EMP_INDEX', -- name '强制 employee_info 使用 emp_id 索引', -- description TRUE, -- validate:是否生效 TRUE -- fuzzy:TRUE=模糊匹配 ); -- 3. 查看已注入规则 SELECT NAME, HINT_TEXT, VALIDATE, SQL_TEXT FROM SYSINJECTHINT WHERE NAME = 'INJECT_EMP_INDEX'; -- 4. 验证计划是否按预期变化 EXPLAIN SELECT * FROM employee_info WHERE emp_id > 1; -- 5. 临时禁用 / 删除规则 -- SF_ALTER_HINT('INJECT_EMP_INDEX', 'STATUS', 'DISABLED'); -- SF_DEINJECT_HINT('INJECT_EMP_INDEX');

注入使用注意点:

  1. 需设置 ENABLE_INJECT_HINT = 1
  2. 匹配对象是整条语句,不是子查询片段
  3. SQL 会经系统格式化,格式化后的 SQL 与规则名需全局唯一
  4. 模糊匹配时,空格/文本细节仍可能影响匹配成功率,注入后务必用 EXPLAIN 验证
  5. 规则变更后若仍走旧计划,可能需要清理计划缓存后再测

四、HINT 语法规范(写错就会被静默忽略)

SELECT /*+ HINT1 HINT2 */ 列名 FROM 表名 WHERE ...; UPDATE 表名 /*+ HINT1 HINT2 */ SET 列名 = :v WHERE ...; DELETE FROM 表名 /*+ HINT1 HINT2 */ WHERE ...;

实操约束:

  1. /*+ 中星号与加号之间不能有空格
  2. HINT 必须紧跟 DML/查询关键字之后
  3. SQL 中表有别名时,HINT 里必须写别名,写表名无效
  4. 一个语句中最多指定 8 个索引类 HINT(官方约束)
  5. 若 HINT 无法实现(如索引不存在),优化器会忽略该 HINT,退回自动选择
-- 有别名时必须用别名 SELECT /*+ INDEX(a, IDX_T1_NAME) */ * FROM t1 a WHERE a.id > 2011 AND a.name < 'XXX'; -- 下面这种写法(写了表名却没用别名)常见“看起来写了 HINT,实际没生效” SELECT /*+ INDEX(t1, IDX_T1_NAME) */ * FROM t1 a WHERE a.id > 2011;

五、常用 HINT 分类与代码示例

5.1 索引类:INDEX / NO_INDEX

当优化器选错访问路径时,最直接的干预手段。

-- 强制使用指定索引 EXPLAIN SELECT /*+ INDEX(t1, IDX_T1_ID) */ * FROM t1 WHERE id > 1000 AND name LIKE 'DB%'; -- 禁止使用某个“看起来合适但实际代价更高”的索引 EXPLAIN SELECT /*+ NO_INDEX(emp, EMP_IDX) */ * FROM emp WHERE employee_id = 100; -- 多索引提示(最多 8 个) EXPLAIN SELECT /*+ INDEX(a, IDX_A_ID) INDEX(b, IDX_B_AID) */ * FROM t1 a, t2 b WHERE a.id = b.a_id AND a.status = 1;

何时考虑强制索引:

  • 条件列选择性高,但统计信息低估了过滤效果
  • 存在多个候选索引,优化器总选“宽而慢”的那个
  • 回表代价被低估,导致错误地坚持走二级索引

何时考虑 NO_INDEX

  • 过滤后结果集很大,索引扫描 + 回表反而比全表扫描更慢
  • 某些历史索引被误选,短期又不能删索引

5.2 连接方法类:USE_HASH / USE_NL / USE_MERGE

-- 强制哈希连接(大表等值连接常见首选) EXPLAIN SELECT /*+ USE_HASH(a, b) */ * FROM t1 a, t2 b WHERE a.id = b.id; -- 禁止某种方向的哈希方向(注意方向性) -- NO_USE_HASH(a, b):不允许 a 左 b 右的哈希;b 左 a 右仍可能被选 EXPLAIN SELECT /*+ NO_USE_HASH(a, b) */ * FROM t1 a, t2 b WHERE a.id = b.id; -- 强制嵌套循环(小驱动表 + 大表有可用索引时很有效) EXPLAIN SELECT /*+ USE_NL(a, b) */ * FROM t1 a, t2 b WHERE a.id = b.id AND a.id < 100; -- 左表驱动 + 右表走索引的索引连接 EXPLAIN SELECT /*+ USE_NL_WITH_INDEX(t1, IDX_T2_ID) */ * FROM t1, t2 WHERE t1.id = t2.id; -- 归并连接(连接列通常需要有序/可利用索引) EXPLAIN SELECT /*+ USE_MERGE(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id;

选型经验(经验法则,不是绝对规则):

连接方式 更适合的场景
USE_HASH 中大型表等值连接,内存足够建哈希表
USE_NL / USE_NL_WITH_INDEX 驱动侧很小,被驱动侧有高效索引
USE_MERGE 两侧已按连接键有序,或可低成本利用索引有序性

5.3 连接顺序类:ORDER

多表连接时,连接顺序决定中间结果大小。ORDER 可以压缩优化器的搜索空间。

-- 指定连接顺序提示 EXPLAIN SELECT /*+ ORDER(t1, t2, t3) */ * FROM t1, t2, t3, t4 WHERE t1.id = t2.id AND t2.id = t3.id AND t3.id = t4.id; -- 顺序 + 方法组合:得到更确定的计划 EXPLAIN SELECT /*+ OPTIMIZER_MODE(1) ORDER(t1, t2, t3, t4) USE_HASH(t1, t2) USE_HASH(t2, t3) USE_HASH(t3, t4) */ * FROM t1, t2, t3, t4 WHERE t1.id = t2.id AND t2.id = t3.id AND t3.id = t4.id;

注意:若连接顺序 HINT 与连接方法 HINT 冲突,优化器通常以连接顺序提示为准

5.4 INI 参数类 HINT(语句级覆盖)

达梦支持用 HINT 对部分 INI 参数做语句级设置:优先级高于 INI 文件,但只影响当前语句/会话语义范围内的取值,不会改写 INI 文件本身。

-- 查看哪些参数支持 HINT 方式指定 SELECT PARA_NAME, HINT_TYPE, DEFAULT_VALUE FROM V$HINT_INI_INFO WHERE PARA_NAME LIKE '%HASH%'; -- 语句级打开哈希连接能力 EXPLAIN SELECT /*+ ENABLE_HASH_JOIN(1) */ * FROM t1, t2 WHERE t1.c1 = t2.d1; -- 语句级关闭哈希连接,观察是否退回嵌套循环等路径 EXPLAIN SELECT /*+ ENABLE_HASH_JOIN(0) */ * FROM t1, t2 WHERE t1.c1 = t2.d1;

HINT_TYPE 含义:

  • OPT:分析/优化阶段参数
  • EXEC:运行阶段参数(对视图场景需特别留意有效性)

5.5 统计信息类:STAT

当基表行数估算不准,或你想做“如果表有 100 万行会怎样”的计划推演时,可用 STAT 手动指定行数。

-- 行数可用整数,或 K / M / G 后缀 EXPLAIN SELECT /*+ STAT(t1, 1M) STAT(t2, 50K) USE_HASH(t1, t2) */ * FROM t1, t2 WHERE t1.id = t2.id;

约束:

  • 只能针对基表,视图/派生表无效
  • 有别名时必须用别名
  • 设置行数后,相关统计也会被相应调整参与代价计算

5.6 并行与结果缓存

-- 语句级并行(任务个数以第一次设置为准) SELECT /*+ PARALLEL(4) */ COUNT(*) FROM big_fact; SELECT /*+ PARALLEL(big_fact 4) */ COUNT(*) FROM big_fact; -- 手动控制结果集缓存 SELECT /*+ RESULT_CACHE */ id, name FROM sysobjects WHERE type$ = 'SCHOBJ'; SELECT /*+ NO_RESULT_CACHE */ id, name FROM sysobjects WHERE type$ = 'SCHOBJ'; -- 禁用计划缓存(排查“旧计划粘住”问题时很有用) SELECT /*+ PLAN_NO_CACHE */ * FROM orders WHERE status = 'OPEN';

5.7 其他实用 HINT

-- MPP 环境:将对象按本地对象处理 SELECT /*+ LOCAL_OBJECT(t1) */ * FROM t1 WHERE id < 100; -- INSERT 时忽略唯一键冲突行(冲突行不插入也不报错) INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(t1(c1, c2)) */ INTO t1(c1, c2, c3) VALUES (1, 2, 'x');

六、实战:从“慢 SQL”到“可控计划”

场景 A:本该走索引,却全表扫

-- 1) 先看原始计划 EXPLAIN SELECT * FROM employee_info WHERE emp_id = 10086; -- 2) 强制索引后再对比 EXPLAIN SELECT /*+ INDEX(employee_info, IDX_EMP_ID) */ * FROM employee_info WHERE emp_id = 10086; -- 3) 若应用改不动 SQL,则注入 SP_SET_PARA_VALUE(1, 'ENABLE_INJECT_HINT', 1); SF_INJECT_HINT( 'SELECT * FROM employee_info WHERE emp_id = 10086;', 'INDEX(EMPLOYEE_INFO, IDX_EMP_ID)', 'FIX_EMP_BY_ID', '紧急:强制 emp_id 等值查询走索引', TRUE, TRUE );

场景 B:小表驱动大表,却选了哈希连接

-- 驱动侧过滤后可能只有几十行,右表有连接键索引:嵌套循环通常更合适 EXPLAIN SELECT /*+ USE_NL(d, e) ORDER(d, e) */ * FROM departments d, employees e WHERE d.dept_id = e.dept_id AND d.dept_id = 10;

场景 C:OR 条件导致计划发散

-- OR 容易被拆成类似 UNION 的路径;同列 OR 可优先改写成 IN -- 改写前 SELECT * FROM city_dim WHERE city = 'ShangHai' OR city = 'WuHan' OR city = 'BeiJing'; -- 改写后 SELECT * FROM city_dim WHERE city IN ('ShangHai', 'WuHan', 'BeiJing'); -- 若暂时不能改写,可结合语句级参数 HINT 做对照实验 -- (具体参数以 V$HINT_INI_INFO 中支持项为准,例如 OPTIMIZER_OR_NBEXP) EXPLAIN SELECT /*+ OPTIMIZER_OR_NBEXP(2) */ * FROM city_dim WHERE city = 'ShangHai' OR city = 'WuHan' OR city = 'BeiJing';

场景 D:HINT 写了却不生效——排查清单

-- 1. 语法与位置是否正确?别名是否一致? EXPLAIN SELECT /*+ INDEX(a, IDX_A_ID) */ * FROM t1 a WHERE a.id = 1; -- 2. 索引是否真实存在、是否可用? SELECT INDEX_NAME, TABLE_NAME, UNIQUENESS FROM USER_INDEXES WHERE TABLE_NAME = 'T1'; -- 3. 注入规则是否命中? SELECT * FROM SYSINJECTHINT WHERE VALIDATE = 'TRUE'; -- 4. 是否被旧计划缓存干扰? SELECT /*+ PLAN_NO_CACHE */ /* 或清理相关缓存后再测 */ 1 FROM sysdual; -- 5. 对照执行计划操作符含义 SELECT * FROM V$SQL_NODE_NAME;

七、HINT 与根因治理:别把“止血”当成“治愈”

HINT 很强,但也容易被滥用。推荐把 HINT 定位为可控的临时约束,而不是长期架构依赖。

建议分层处理:

  1. 先修统计信息与 SQL 写法
    收集统计、消除困难正则(如 '%abc%')、避免无意义 SELECT *、能用 UNION ALL 就不要 UNION

  2. 再考虑语句级 HINT
    在可改代码的环境中,用最小必要 HINT 锁定关键路径,并附注释说明原因与失效条件。

  3. 生产不可改 SQL 时用注入
    SF_INJECT_HINT 做紧急止血;同时建跟踪项:规则名、负责人、预期下线条件。

  4. 定期回归
    数据分布变化后,昨天的“最优 HINT”可能变成今天的性能陷阱。版本升级、索引变更、分区策略调整后,都要重新 EXPLAIN

-- 一个可维护的 HINT 注释模板(建议写在 SQL 旁或变更单里) -- HINT: INDEX(o, IDX_ORDERS_STATUS) -- WHY : status='OPEN' 选择性约 0.3%,统计信息经常低估 -- RISK: 若 OPEN 订单占比升高到 >20%,需重新评估是否改为全表/分区扫描 -- OWNER: DBA-Zhang / 复审日期: 2026-09-01 SELECT /*+ INDEX(o, IDX_ORDERS_STATUS) */ * FROM orders o WHERE o.status = 'OPEN';

八、小结

  1. 达梦优化器走“查询转换 → 代价估算 → 计划生成”三步;代价估算高度依赖统计信息与基数判断。
  2. HINT 是人工干预优化器的手段,不改语义,只改路径;写错不会报错,必须用 EXPLAIN 验证。
  3. 两类落地方式:嵌入 /*+ ... */(开发友好),以及 SF_INJECT_HINT(生产友好)。
  4. 高频 HINT 族:索引(INDEX/NO_INDEX)、连接方法(USE_HASH/USE_NL/USE_MERGE)、连接顺序(ORDER)、语句级 INI、STAT、并行与缓存控制。
  5. HINT 适合止血与精准控制;长期仍应回到统计信息、索引设计与 SQL 写法本身。

附录:快速速查

-- 强制索引 /*+ INDEX(表或别名, 索引名) */ -- 禁止索引 /*+ NO_INDEX(表或别名, 索引名) */ -- 连接方法 /*+ USE_HASH(a, b) */ /*+ USE_NL(a, b) */ /*+ USE_MERGE(a, b) */ -- 连接顺序 /*+ ORDER(t1, t2, t3) */ -- 语句级参数 /*+ ENABLE_HASH_JOIN(1) */ -- 行数提示 /*+ STAT(t1, 1M) */ -- 并行 / 缓存 / 禁用计划缓存 /*+ PARALLEL(4) */ /*+ RESULT_CACHE */ /*+ PLAN_NO_CACHE */ -- 动态注入(需 ENABLE_INJECT_HINT=1) SF_INJECT_HINT(sql_text, hint_text, name, description, validate, fuzzy);
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服