优化器是达梦数据库的核心组件之一,负责为一个 SQL 查询找到“最优”的执行方式,并生成执行计划。执行计划最终决定了查询的性能表现——同样一条 SQL,走全表扫描还是索引、用哈希连接还是嵌套循环,耗时可能相差几个数量级。
本文从优化器工作原理讲起,重点深入 HINT(优化器提示词):它何时该用、怎么写、如何验证生效,以及生产环境如何在不改应用代码的情况下动态注入。
优化器的首要优化目标通常是最快响应时间。达梦并不总是等全部结果算完才开始返回数据,而是可以通过 FIRST_ROWS 等参数设定优先返回的记录数,从而提升用户感知速度。
优化器生成最终执行计划,大致包含三个步骤:
在不改变语义的前提下,对 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
);
这是核心步骤。优化器会为每种可能的执行路径估算“代价”,代价综合了 I/O、CPU、内存等资源消耗。估算依据主要包括:
-- 查看表/列统计信息是否过期,是排查“优化器选错计划”的第一步
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');
基于估算代价,从候选方案中选择代价最小的作为最终执行计划。多表连接时,连接顺序与连接算法的组合空间会迅速膨胀,这也是 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 不会改变 SQL 语义,只影响计划生成阶段的路径选择。典型使用场景:
| 场景 | 说明 |
|---|---|
| 统计信息失真 | 采样不足、数据倾斜、统计未及时更新,导致基数估算偏差 |
| 索引选择错误 | 本该走高选择性索引,却选了全表扫描或错误索引 |
| 连接方式不当 | 大表对大表误用嵌套循环,或小表驱动大表时未用索引连接 |
| 紧急止血 | 生产峰值期间,先用 HINT 稳住性能,再慢慢修根因 |
| 规避全局参数副作用 | 不想改全局 INI,只想对单条 SQL 做语句级控制 |
重要提醒:HINT 语法错误不会报错,会被直接忽略。操作后务必用
EXPLAIN检查执行计划,确认 HINT 是否真的生效。
将 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');
注入使用注意点:
ENABLE_INJECT_HINT = 1EXPLAIN 验证SELECT /*+ HINT1 HINT2 */ 列名 FROM 表名 WHERE ...;
UPDATE 表名 /*+ HINT1 HINT2 */ SET 列名 = :v WHERE ...;
DELETE FROM 表名 /*+ HINT1 HINT2 */ WHERE ...;
实操约束:
/*+ 中星号与加号之间不能有空格-- 有别名时必须用别名
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;
当优化器选错访问路径时,最直接的干预手段。
-- 强制使用指定索引
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:
-- 强制哈希连接(大表等值连接常见首选)
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 |
两侧已按连接键有序,或可低成本利用索引有序性 |
多表连接时,连接顺序决定中间结果大小。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 冲突,优化器通常以连接顺序提示为准。
达梦支持用 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:运行阶段参数(对视图场景需特别留意有效性)当基表行数估算不准,或你想做“如果表有 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;
约束:
-- 语句级并行(任务个数以第一次设置为准)
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';
-- 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');
-- 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
);
-- 驱动侧过滤后可能只有几十行,右表有连接键索引:嵌套循环通常更合适
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;
-- 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';
-- 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 定位为可控的临时约束,而不是长期架构依赖。
建议分层处理:
先修统计信息与 SQL 写法
收集统计、消除困难正则(如 '%abc%')、避免无意义 SELECT *、能用 UNION ALL 就不要 UNION。
再考虑语句级 HINT
在可改代码的环境中,用最小必要 HINT 锁定关键路径,并附注释说明原因与失效条件。
生产不可改 SQL 时用注入
SF_INJECT_HINT 做紧急止血;同时建跟踪项:规则名、负责人、预期下线条件。
定期回归
数据分布变化后,昨天的“最优 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';
EXPLAIN 验证。/*+ ... */(开发友好),以及 SF_INJECT_HINT(生产友好)。INDEX/NO_INDEX)、连接方法(USE_HASH/USE_NL/USE_MERGE)、连接顺序(ORDER)、语句级 INI、STAT、并行与缓存控制。-- 强制索引
/*+ 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);
文章
阅读量
获赞
