本文章主要内容包括查看执行计划、索引优化、SQL优化与参数调优。
实验部分基础数据基于达梦数据库DMHR示例库。
返回树形文本计划,不实际执行SQL。
关键字段:
cost:预估代价rows:处理行数bytes:字节数常见操作符:
CSCN2:全表扫描(或聚簇索引扫描),通常是性能优化的重点对象。SSEK2:二级索引扫描,一般效率较高。BLKUP2:回表操作,根据索引找到行数据,如果量大会有随机IO开销。SSCN:索引全扫描,不需要扫描表。NSET2/PRJT2:结果集收集/投影操作,位于计划顶层,一般无需优化。HASH2 INNER JOIN:HASH内连接。INDEX JOIN SEMI JOIN:索引半连接。EXPLAIN SELECT * FROM EMPLOYEE;
以表格形式返回更详细的计划信息。
EXPLAIN FOR SELECT * FROM EMPLOYEE;
会真实执行SQL,返回实际执行计划和统计信息。
SET AUTOTRACE TRACE; -- 开启
SET AUTOTRACE OFF; -- 关闭
-- 查看全部执行计划
SELECT * FROM v$cachepln;
-- 查看指定操作的查询计划
SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%SQL语句片段%';
-- 导出执行计划
ALTER SESSION SET EVENTS 'IMMEDIATE TRACE NAME PLNDUMP, LEVEL 'CACHE_ITEM'';
-- 清空执行计划
--为防止内存执行计划影响实验结果,后续实验初期执行清空语句清理内存(生产环境慎用)
SP_CLEAR_PLAN_CACHE();
SP_CLEAR_PLAN_CACHE('CACHE_ITEM');
数据库为了提高性能,不会每次执行SQL都重新解析和优化,为了避免重复解析,会缓存SQL执行计划。如果数据库环境发生变化,但缓存计划没有及时更新,就会出现EXPLAIN查询的计划与实际执行的计划不一致。如:
绑定变量的首次执行计划会被缓存并强制复用,导致即使后续传入的参数值更适合其他执行路径,也会执行缓存的执行计划。SQL性能也可能严重退化。
-- 创建一张表TEST_PLAN_MISMATCH,插入100000数据,其中99000条数据的STATUS值为1,1000条为2。
CREATE TABLE TEST_PLAN_MISMATCH
(
ID NUMBER,
STATUS NUMBER,
CONTENT VARCHAR2(100)
);
INSERT INTO TEST_PLAN_MISMATCH
SELECT
LEVEL,
CASE
WHEN LEVEL <= 99000 THEN 1
ELSE 2
END,
'TEST'
FROM DUAL
CONNECT BY LEVEL <= 100000;
COMMIT;
-- 在STATUS创建索引,但是由于STATUS列数据分布极度不均,当查询条件为STATUS=1时仍会进行全表扫描,当STATUS=2时会使用索引查询
CREATE INDEX IDX_PLAN_STATUS ON TEST_PLAN_MISMATCH(STATUS);
-- 收集统计信息
DBMS_STATS.GATHER_TABLE_STATS(
USER,
'TEST_PLAN_MISMATCH'
);
SP_CLEAR_PLAN_CACHE();
DEFINE STATUS = 2;
SELECT count(*) FROM TEST_PLAN_MISMATCH WHERE STATUS = &STATUS;
DEFINE STATUS = 1;
SELECT count(*) FROM TEST_PLAN_MISMATCH WHERE STATUS = &STATUS;
SELECT * FROM v$cachepln;
替换变量会在编译前进行文本解析,实际执行的语句是解析替换的语句,每次值不同都重新生成新的执行计划。
STATUS = 1数据量偏大,执行全表扫描。STATUS = 2占比较小,优化器的执行计划应为索引查询,但是由于内存中缓存了STATUS = 1的执行计划,会复用执行计划。-- 执行查询,并且导出实际查询计划
SELECT * FROM TEST_PLAN_MISMATCH WHERE STATUS=2;
SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%SELECT * FROM TEST_PLAN_MISMATCH WHERE STATUS=2;%';
ALTER SESSION SET EVENTS 'IMMEDIATE TRACE NAME PLNDUMP, LEVEL 1896360288';
UPDATE TEST_PLAN_MISMATCH SET STATUS=2 WHERE ID<=90000;
-- 更新统计信息
DBMS_STATS.GATHER_TABLE_STATS(
USER,
'TEST_PLAN_MISMATCH'
);
EXPLAIN SELECT * FROM TEST_PLAN_MISMATCH WHERE STATUS=2;
此时仍使用缓存的执行计划,且对应语句只有一条查询记录,CACHE_ITEM相同。
SELECT * FROM TEST_PLAN_MISMATCH WHERE STATUS=2;
SELECT CACHE_ITEM, SQLSTR FROM V$CACHEPLN WHERE SQLSTR LIKE '%SELECT * FROM TEST_PLAN_MISMATCH WHERE STATUS=2;%';
ALTER SESSION SET EVENTS 'IMMEDIATE TRACE NAME PLNDUMP, LEVEL 1896360288';
在2.1中,列与绑定参数进行计算时,优化器会根据绑定的参数进行对应的调整。其相关参数BEXP_CALC_ST_FLAG配置如下。
优化器计算过滤条件选择率时,要不要额外考虑某些场景的开关,主要配置参数如下:
| 值 | 作用 |
|---|---|
| 0 | 使用默认策略,不进行调整 |
| 1 | 同一表不同列之间比较时进行调整 |
| 2 | 包含函数过滤条件时进行调整 |
| 4 | 同一表中AND/OR过滤条件进行调整 |
| 8 | 对CONTAINS过滤条件进行调整 |
| 16 | 单表过滤条件时进行调整 |
| 32 | 其他特定过滤场景 |
| 64 | 其他特定过滤场景 |
| 128 | 对列与绑定参数/值比较的过滤条件进行选择率调整 |
| 按位组合 | BEXP_CALC_ST_FLAG=31,开启1+2+4+8+16 |
不启用这些额外的选择率计算调整。
SELECT * FROM EMPLOYEE WHERE DEPARTMENT_ID = 101 -- 通过DEPARTMENT_ID=101预计返回多少行决定要不要走索引
同一张表不同列之间进行比较。
SELECT * FROM EMPLOYEE WHERE DEPARTMENT_ID = MANAGER_ID -- 估算:A = B到底有多少行满足
包含函数的过滤条件。
SELECT * FROM EMPLOYEE WHERE SUBSTR(job_id,1,1) = '5'; -- 针对函数过滤条件进行选择率计算调整
同一张表中的AND/OR过滤条件。
EXPLAIN SELECT * FROM EMPLOYEE WHERE EMPLOYEE_NAME = '罗利平' OR EMPLOYEE_NAME = '王岳荪';
针对CONTAINS条件。
CREATE CONTEXT INDEX IDX_EMP_NAME_CONTEXT ON EMPLOYEE(EMPLOYEE_NAME) LEXER DEFAULT_LEXER;
EXPLAIN SELECT * FROM EMPLOYEE WHERE CONTAINS(EMPLOYEE_NAME, '姜');
单表过滤条件。
列与绑定参数/值进行比较时的选择率计算。
-- 创建表test_cache
CREATE TABLE test_cache (
id INT PRIMARY KEY,
flag INT,
name VARCHAR(100),
create_time DATE
);
-- 插入100w条数据,其中flag=1占2%,2~4999是均匀低频值:每个值约200条
INSERT INTO test_cache
SELECT LEVEL,
CASE WHEN MOD(LEVEL, 10) = 1 THEN 1 ELSE MOD(LEVEL, 5000) END,
'USER_' || LEVEL,
SYSDATE - LEVEL
FROM DUAL CONNECT BY LEVEL <= 1000000;
-- 在flag列创建索引
CREATE INDEX idx_test_flag ON test_cache(flag);
-- 统计信息
DBMS_STATS.GATHER_TABLE_STATS(
USER,
'TEST_CACHE'
);
SELECT name,
SUM(TO_NUMBER(org_size))/1024.0/1024.0 AS sum_org_size_mb,
SUM(TO_NUMBER(total_size))/1024.0/1024.0 AS sum_total_size_mb,
SUM(TO_NUMBER(RESERVED_SIZE))/1024.0/1024.0 AS sum_RESERVED_SIZE_MB,
SUM(TO_NUMBER(target_size))/1024.0/1024.0 AS sum_target_size_mb,
SUM(n_extend_exclusive) sum_n_extend_exclusive
FROM (
SELECT
CASE WHEN name LIKE 'SHARE POOL%' THEN 'SHARE POOL' ELSE name END AS name,
org_size,
total_size,
RESERVED_SIZE,
target_size,
n_extend_exclusive
FROM v$mem_pool
) GROUP BY name
ORDER BY 3 DESC, 4 DESC, 5 DESC;
结论:动态参数SQL支持计划复用、缓存高效;固定参数SQL引发大量硬解析、缓存膨胀。
联合索引必须要带上第一个字段才会生效(联合索引只有第一列才是有序的);不能跳过联合索引中间的列,会降低索引效果。
查询过程中尽量不要使用
SELECT *。
CREATE INDEX IDX1 ON EMPLOYEE (DEPARTMENT_ID, JOB_ID, SALARY);
EXPLAIN SELECT DEPARTMENT_ID, JOB_ID, salary
FROM EMPLOYEE
WHERE DEPARTMENT_ID = 101 AND JOB_ID = '11' AND salary = 30000;
EXPLAIN SELECT *
FROM EMPLOYEE
WHERE DEPARTMENT_ID = 101 AND JOB_ID = '11' AND salary = 30000;
EXPLAIN SELECT *
FROM EMPLOYEE
WHERE JOB_ID = '11' AND salary = 30000;
定位到EMPLOYEE_NAME后,再通过SLCT2过滤(因为索引跳过了hire_date列,salary无法用于精确定位)。
EXPLAIN SELECT *
FROM EMPLOYEE
WHERE DEPARTMENT_ID = 101 AND salary = 30000;
在索引列进行如计算、函数、类型转换等操作,会导致索引失效。
CREATE INDEX IDX2 ON EMPLOYEE (EMPLOYEE_NAME);
EXPLAIN SELECT * FROM EMPLOYEE WHERE LEFT(EMPLOYEE_NAME, 2) = '马学';
在索引列使用范围查询,会导致右边的列失效。
EXPLAIN SELECT *
FROM EMPLOYEE
WHERE DEPARTMENT_ID > 101 AND JOB_ID = '11' AND salary = 30000;
在达梦数据库中,如果OR条件是针对同一个索引列(EMPLOYEE_NAME)的多个等值查询时,它会自动将这个OR转化为对索引的多次精确定位。
1. 不等值和空值:全表扫描
EXPLAIN SELECT * FROM EMPLOYEE WHERE EMPLOYEE_NAME != '马学铭';
EXPLAIN SELECT * FROM EMPLOYEE WHERE EMPLOYEE_NAME IS NULL;
2. OR条件针对同一个索引列
EXPLAIN SELECT * FROM EMPLOYEE WHERE EMPLOYEE_NAME = '马学铭' OR EMPLOYEE_NAME = '谢俊人';
3. OPTIMIZER_OR_NBEXP参数配置
OPTIMIZER_OR_NBEXP决定优化器面对A OR B时,是把OR拆开分别处理、整体处理,还是进一步做OR范围合并。
| 值 | 含义 |
|---|---|
| 0 | 不进行OR优化 |
| 1 | UNION_FOR_OR场景优化为无KEY比较方式 |
| 2 | OR表达式优先作为整体处理 |
| 4 | 相关子查询中的OR也优先作为整体处理 |
| 8 | OR布尔表达式的范围合并 |
| 16 | 同一列的范围条件+IS NULL优化 |
| 32 | OR公因子包含子查询时,使用UNION_OR,避免特定情况下过滤条件无法下放 |
| 按位组合 | OPTIMIZER_OR_NBEXP=31,开启1+2+4+8+16 |
-- 查看当前配置,默认配置为29:1+4+8+16
SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME = 'OPTIMIZER_OR_NBEXP';
-- 创建两个索引,使用OR查看数据
CREATE INDEX IDX_EMPLOYEE_NAME ON EMPLOYEE(EMPLOYEE_NAME);
CREATE INDEX IDX_EMPLOYEE_EMAIL ON EMPLOYEE(EMAIL);
EXPLAIN SELECT * FROM EMPLOYEE WHERE EMPLOYEE_NAME = '马学铭' OR EMAIL = 'chengqingwu@dameng.com';
OPTIMIZER_OR_NBEXP=0:分别使用两个索引。OPTIMIZER_OR_NBEXP=1:转换成UNION FOR OR,使用"无KEY比较方式"。OPTIMIZER_OR_NBEXP=2:作为整体处理。OPTIMIZER_OR_NBEXP=4:与2相比,对相关子查询里面的OR,也优先按照整体方式进行优化。
OPTIMIZER_OR_NBEXP=8:OR范围合并。
SELECT * FROM T_ORDER WHERE SCORE BETWEEN 100 AND 300 OR SCORE BETWEEN 200 AND 500;
-- 优化为 → SCORE BETWEEN 100 AND 500
SELECT * FROM T_USER WHERE AGE > 18 OR AGE IS NULL;
1. 字符串不加引号,会调用隐式转换进行全表扫描
CREATE INDEX INDEX01 ON EMPLOYEE(PHONE_NUM);
EXPLAIN SELECT * FROM EMPLOYEE WHERE PHONE_NUM = '15312348552';
-- 将索引列转换为数值类型,导致该字段上的索引无法使用
EXPLAIN SELECT * FROM EMPLOYEE WHERE PHONE_NUM = 15312348552;
2. 隐式转换
隐式转换主要发生在不同数据类型的值进行比较、运算或赋值。
-6128: 无效的数字错误,中断业务流程。SELECT * FROM EMPLOYEE WHERE EMAIL = 235465; -- -6128: 无效的数字
CREATE TABLE T_ORDER (
CREATE_TIME DATE
);
INSERT INTO T_ORDER VALUES ('2026-08-23');
SELECT * FROM T_ORDER WHERE CREATE_TIME = TO_DATE('2026-08-23', 'YYYY-MM-DD');
不同数值类型之间混合运算:数据库可能进行数值类型提升/转换。
两张表关联时,如果关联字段的类型不一致,也会导致索引失效。
CREATE TABLE t1(
a1 INT,
a2 VARCHAR(10)
);
CREATE TABLE t2(
b1 INT,
b2 VARCHAR(10)
);
INSERT INTO t1 VALUES(1,'1'),(2,'2');
INSERT INTO t2 VALUES(1,'1'),(2,'2');
CREATE INDEX indx1 ON t1(a2);
EXPLAIN SELECT * FROM t1 JOIN t2 ON t1.a2 = t2.b1;
SELECT CASE
WHEN STATUS = 1 THEN '正常' -- VARCHAR
ELSE 0 -- NUMBER
END
FROM T1;
1. LIKE百分号写最右可能导致索引失效
EXPLAIN SELECT * FROM DMHR.EMPLOYEE WHERE EMPLOYEE_NAME LIKE '马%'; -- 索引生效
EXPLAIN SELECT * FROM DMHR.EMPLOYEE WHERE EMPLOYEE_NAME LIKE '%铭'; -- 全表扫描
2. LIKE_OPT_FLAG参数配置
达梦的LIKE_OPT_FLAG是动态、会话级参数。核心取值如下:
| 值 | 作用 |
|---|---|
| 0 | 不进行LIKE优化 |
| 1 | 对首尾存在通配符的LIKE,优化成POSITION();特殊情况下优化成REVERSE() |
| 2 | COL1 LIKE COL2 |
| 4 | COL1 LIKE 'A' |
| 8 | 可计算的LIKE表达式直接优化成常量 |
| 16 | 针对函数索引列的LIKE,优化成BETWEEN…AND… |
| 按位组合 | LIKE_OPT_FLAG=31,开启1+2+4+8+16 |
CREATE TABLE T_USER (
ID INT,
NAME VARCHAR(100)
);
INSERT INTO T_USER VALUES (1,'ZhangSan'),(2,'LiSi'),(3,'WangWu'),(4,'ZhangLi'),(5,'XiaoZhang');
SP_SET_PARA_VALUE(1, 'LIKE_OPT_FLAG', 1);
CREATE INDEX IDX_NAME_POS ON T_USER(POSITION('Zhang', NAME));
EXPLAIN SELECT * FROM T_USER WHERE NAME LIKE '%Zhang%';
COL1 LIKE COL2 || '%'SELECT * FROM T WHERE NAME LIKE PREFIX || '%'; -- PREFIX表示另一列
NAME LIKE 'A' || 'B%'优化成NAME LIKE 'AB%'SP_SET_PARA_VALUE(1, 'LIKE_OPT_FLAG', 4);
EXPLAIN SELECT * FROM T_USER WHERE NAME LIKE 'Zh' || 'a%';
LIKE_OPT_FLAG=8:表达式可以算出来,就提前算。与4类似,一个偏向拼接一个偏向计算。
LIKE_OPT_FLAG=16:LIKE → BETWEEN
UPPER(NAME) LIKE 'ABC%' → UPPER(NAME) >= 'ABC' AND UPPER(NAME) < 'ABD'
使用数据量较小、索引比较完备的表,然后使其索引和条件较大表进行数据筛选。
EXPLAIN SELECT E.EMPLOYEE_NAME, E.EMAIL, E.PHONE_NUM
FROM DEPARTMENT D
JOIN EMPLOYEE E ON E.DEPARTMENT_ID = D.DEPARTMENT_ID;
-- 执行耗时: 7毫秒996微秒601纳秒
SELECT COUNT(E.EMPLOYEE_ID)
FROM EMPLOYEE E
JOIN DEPARTMENT D ON E.DEPARTMENT_ID = D.DEPARTMENT_ID;
-- 执行耗时: 13毫秒878微秒100纳秒
-- 子查询
EXPLAIN
SELECT
(SELECT DEPARTMENT.DEPARTMENT_NAME FROM DEPARTMENT WHERE DEPARTMENT.DEPARTMENT_ID = EMPLOYEE.DEPARTMENT_ID),
EMPLOYEE.EMPLOYEE_NAME
FROM EMPLOYEE;
-- 执行耗时: 29毫秒61微秒100纳秒
-- 连接查询
EXPLAIN
SELECT DEPARTMENT.DEPARTMENT_NAME, EMPLOYEE.EMPLOYEE_NAME
FROM EMPLOYEE
JOIN DEPARTMENT ON DEPARTMENT.DEPARTMENT_ID = EMPLOYEE.DEPARTMENT_ID;
-- 执行耗时: 15毫秒718微秒100纳秒
ENABLE_RQ_TO_NONREF_SPL相关子查询参数配置
将相关查询表达式转化为非相关查询表达式,使相关查询从之前的平坦化方式转为一行一行处理。
| 值 | 含义 |
|---|---|
| 0 | 不启用 |
| 1 | 优化查询项(SELECT列表)中的相关子查询 |
| 2 | 优化查询项+WHERE中的相关子查询 |
| 4 | 相关查询采用SPL去相关后,可以作为单表过滤条件 |
| 按位组合 | ENABLE_RQ_TO_NONREF_SPL=7,开启1+2+4 |
SP_SET_PARA_VALUE(1, 'ENABLE_RQ_TO_NONREF_SPL', 0);
EXPLAIN
SELECT
e.employee_id,
e.employee_name,
e.department_id,
(
SELECT AVG(e2.salary)
FROM dmhr.employee e2
WHERE e2.department_id = e.department_id
) AS avg_dept_salary
FROM dmhr.employee e;
EXPLAIN
SELECT /*+ ENABLE_RQ_TO_NONREF_SPL(2) */
e.employee_id,
e.employee_name,
e.department_id
FROM dmhr.employee e
WHERE EXISTS (
SELECT 1
FROM dmhr.employee e2
WHERE e2.department_id = e.department_id
AND e2.salary > 20000
);
SELECT /*+ ENABLE_RQ_TO_NONREF_SPL(4) */
e.employee_id,
(
SELECT MAX(e2.salary)
FROM dmhr.employee e2
WHERE e2.department_id = e.department_id
) AS max_salary
FROM dmhr.employee e
WHERE e.department_id = 101;
-- 添加索引前
EXPLAIN SELECT COUNT(EMPLOYEE_NAME) FROM EMPLOYEE GROUP BY DEPARTMENT_ID;
-- 执行耗时: 13毫秒736微秒200纳秒
-- 在DEPARTMENT_ID添加索引
CREATE INDEX IDX_DEPARTMENT_ID ON EMPLOYEE (DEPARTMENT_ID);
EXPLAIN SELECT COUNT(EMPLOYEE_NAME) FROM EMPLOYEE GROUP BY DEPARTMENT_ID;
-- 执行耗时: 11毫秒265微秒300纳秒
-- 1. 创建测试表
CREATE TABLE test_insert_efficiency (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 单条插入
INSERT INTO test_insert_efficiency (name, age, email) VALUES ('张三', 25, 'zhangsan@test.com');
INSERT INTO test_insert_efficiency (name, age, email) VALUES ('李四', 30, 'lisi@test.com');
INSERT INTO test_insert_efficiency (name, age, email) VALUES ('王五', 28, 'wangwu@test.com');
-- 执行耗时: 1毫秒396微秒200纳秒
-- 批量插入
INSERT INTO test_insert_efficiency (name, age, email) VALUES
('赵六', 35, 'zhaoliu@test.com'),
('孙七', 22, 'sunqi@test.com'),
('周八', 40, 'zhouba@test.com');
-- 执行耗时: 348微秒500纳秒
UNION ALL:获取所有数据但不去重。UNION:获取所有数据且数据去重,不包含重复数据。SELECT EMPLOYEE.EMPLOYEE_NAME, EMPLOYEE.job_id FROM EMPLOYEE
UNION ALL
SELECT DEPARTMENT.DEPARTMENT_NAME, DEPARTMENT.location_id FROM DEPARTMENT;
SELECT EMPLOYEE.EMPLOYEE_NAME, EMPLOYEE.job_id FROM EMPLOYEE
UNION
SELECT DEPARTMENT.DEPARTMENT_NAME, DEPARTMENT.location_id FROM DEPARTMENT;
文章
阅读量
获赞
