前言
一条 SELECT 从客户端发出到结果返回,在屏幕上只是 0.01 秒的等待。但这 0.01 秒在数据库内部,是一条完整的执行链路。
这篇文章用一台 CentOS 7 上的达梦 V9 单机实例,把这条链路拆开来看一遍。不只看执行计划,还看线程、10053 trace、硬解析/软解析、算子树、缓冲池,最后落到逻辑读和物理读的计数上。
环境信息
系统版本:
[root@db01:/root]#
[root@db01:/root]# cat /etc/*release*
...
CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"
CentOS Linux release 7.6.1810 (Core)
CentOS Linux release 7.6.1810 (Core)
cpe:/o:centos:centos:7
数据库版本:
[dmdba@db01:/home/dmdba]$ disql sysdba/XXXX
服务器[LOCALHOST:5236]:处于普通打开状态
登录使用时间 : 13.711(ms)
密钥过期时间:2027-04-17
disql V9
11:22:11 sysdba@LOCALHOST:5236 SQL> select * from v$instance\G
*************************** 1. row ***************************
NAME: ZSDMDB
INSTANCE_NAME: ZSDMDB
INSTANCE_NUMBER: 1
HOST_NAME: db01
SVR_VERSION: DM Database Server x64 V9
DB_VERSION: DB Version: 0x7000d
START_TIME: 2026-09-08 11:18:48
STATUS$: OPEN
MODE$: NORMAL
OGUID: 0
DSC_SEQNO: 0
DSC_ROLE: NULL
BUILD_VERSION: 03151060506-20260417-322930-20218
BUILD_TIME: May 13 2026 15:49:55
已用时间: 6.556(毫秒). 执行号:603.
11:26:59 sysdba@LOCALHOST:5236 SQL> SELECT TO_NUMBER(SUBSTR(VER, 1, 2), 'XX'),TO_NUMBER(SUBSTR(VER, 3, 2), 'XX'),
2 TO_NUMBER(SUBSTR(VER, 5, 2), 'XX'),
3 TO_NUMBER(SUBSTR(VER, 7, 2), 'XX')
4 FROM (SELECT RAWTOHEX(CAST(SUBSTR(VER, 3) AS INT)) AS VER
5 FROM (SELECT REGEXP_SUBSTR(ID_CODE, '[^-]+', 1, 1) AS VER));
TO_NUMBER(SUBSTR(VER,1,2),'XX') TO_NUMBER(SUBSTR(VER,3,2),'XX') TO_NUMBER(SUBSTR(VER,5,2),'XX') TO_NUMBER(SUBSTR(VER,7,2),'XX')
------------------------------- ------------------------------- ------------------------------- -------------------------------
9 1 0 26
已用时间: 21.899(毫秒). 执行号:604.
版本号解析出来是 9.1.0.26。
架构:单机。
实践过程
基础测试数据
create tablespace zs_data datafile '/dmdata/ZSDMDB/zs_data01.dbf' size 128 autoextend on next 128;
create user zs identified by Zs123456 default tablespace zs_data;
授权,将public,resource,soi,vti授权给Zs123456:
grant public,resource,soi,vti to zs;
-- 建测试表 T_ZS(ID 主键 + VAL,1 万行)
DROP TABLE IF EXISTS T_ZS;
CREATE TABLE T_ZS(ID INT PRIMARY KEY, VAL INT);
CREATE OR REPLACE PROCEDURE INSERT_T_ZS(N INT) AS
BEGIN
FOR I IN 1..N LOOP
INSERT INTO T_ZS VALUES(I, MOD(I,1000)+1);
END LOOP;
END;
/
CALL INSERT_T_ZS(10000);
COMMIT;
SELECT COUNT(*) AS T_ZS_ROWS FROM T_ZS;
会话执行全过程
一条 SQL 从客户端到结果返回,大致走这么一条链路:
下面是文字说明,
客户端发出 SQL 文本
↓
DM 驱动(JDBC/ODBC)转发
↓
监听线程 `dm_lsnr_thd` 接收连接
↓
会话线程 `dm_sql_thd` 绑定会话 ID
↓
共享池查找计划缓存
├─ 命中 → 软解析(`HARD_PARSE_FLAG=0`)→ 直接复用缓存计划
└─ 未命中 → 硬解析(`HARD_PARSE_FLAG=2`)
↓
语法解析 Parser
↓
语义分析 / 权限校验
↓
优化器(`OPTIMIZER_MODE` 控制新/老模式)
↓
单表访问路径探测 → 算代价 → 选最优计划
↓
计划存入共享池
↓
执行引擎按算子树逐层执行
↓
算子访问数据(`#CSCN2` / `#SSEK2` / `#HASH2` 等)
↓
缓冲池读取
├─ 命中 → 逻辑读(`N_LOGIC_READ`)
└─ 未命中 → 物理读(`N_PHY_READ`)从磁盘读入
↓
一致性读(提交数组判断可见性)
↓
结果集组装
↓
返回客户端
这条链路里,每一步都有对应的证据可以查:
| 环节 | 验证方式 |
|---|---|
| 监听 / 会话 | V$THREADS 看 dm_lsnr_thd、dm_sql_thd |
| 硬解析 / 软解析 | V$SQL_HISTORY.HARD_PARSE_FLAG(2 硬 / 0 软) |
| 优化器决策 | 10053 trace 看 Plan before optimized 和 BEST PLAN |
| 算子树 | EXPLAIN 看 #NSET2→#PRJT2→#CSCN2 等算子链 |
| 缓存命中 | V$SYSSTAT 看 sql cache hit count |
| 逻辑读 / 物理读 | V$SYSSTAT 看 logic read count、physical read count |
| 缓冲池状态 | V$BUFFERPOOL 看各池大小、命中率、脏页数 |
达梦是单进程多线程架构,在后台视图 v$threads 查到具体线程以及作用:
13:53:34 sysdba@LOCALHOST:5236 SQL> select name,THREAD_DESC ,count(1) from v$threads group by name,THREAD_DESC order by 3 desc;
NAME THREAD_DESC COUNT(1)
------------------- ------------------------------------------------------------------------------------- --------------------
temp_worker_thd Temp worker thread 20
dm_osio_thd os io thread 16
dm_pthd_thd Parallel working thread 16
dm_tskwrk_thd Task Worker Thread for SQL parsing and execution for server itself 14
dm_lpq_thd Local parallel working thread 10
dm_hio_thd IO thread for HFS to read data pages 4
dm_audit_thd Thread for flush audit logs 2
dm_wrkgrp_thd User working thread 2
dm_rsyswrk_thd Asynchronous archiving thread 2
dm_sqllog_thd Thread for writing dmsql dmserver 2
dm_purge_thd Purge thread 1
dm_sql_aux_thd User session auxiliary thread 1
dm_quit_thd Thread for executing shutdown-normal operation 1
dm_pwr_thd flush PWR log thread 1
dm_sql_thd User session thread 1
dm_redolog_thd Redo log thread, used to flush log 1
nlgn_task_thread Thread for writing login information 1
dm_chkpnt_thd Flush checkpoint thread 1
dm_trctsk_thd Thread for writing trace information 1
dm_sched_thd Server scheduling thread,used to trigger background checkpoint, time-related triggers 1
dm_deadlock_chk_thd Thread for transaction deadlock cheak 1
dm_lsnr_thd Service listener thread 1
dm_trxbro_thd trx active view broadcast thread 1
23 rows got
各个线程统计说明:
| 线程名 | 数量 | 用途 |
|---|---|---|
temp_worker_thd |
20 | 临时工作线程(临时表/排序中间结果) |
dm_osio_thd |
16 | OS IO 线程(数据页异步读写) |
dm_pthd_thd |
16 | 并行工作线程(全局并行执行) |
dm_tskwrk_thd |
14 | 服务器自身 SQL 解析与执行任务线程 |
dm_lpq_thd |
10 | 本地并行工作线程(单会话内并行) |
dm_hio_thd |
4 | HFS 读数据页 IO 线程 |
dm_audit_thd |
2 | 审计日志刷盘线程 |
dm_wrkgrp_thd |
2 | 用户工作线程(执行用户 SQL) |
dm_rsyswrk_thd |
2 | 异步归档线程 |
dm_sqllog_thd |
2 | SQL 日志(sqllog)写入线程 |
dm_purge_thd |
1 | 清理线程(回收已删版本/UNDO) |
dm_sql_aux_thd |
1 | 用户会话辅助线程 |
dm_quit_thd |
1 | 正常关闭(SHUTDOWN NORMAL)线程 |
dm_pwr_thd |
1 | PWR 性能日志刷盘线程 |
dm_sql_thd |
1 | 用户会话线程(每会话 1 个,当前 1 个连接) |
dm_redolog_thd |
1 | REDO 日志刷盘线程 |
nlgn_task_thread |
1 | 登录信息写入线程 |
dm_chkpnt_thd |
1 | 检查点刷盘线程 |
dm_trctsk_thd |
1 | trace 信息写入线程 |
dm_sched_thd |
1 | 调度线程(周期触发检查点/PURGE/死锁检测) |
dm_deadlock_chk_thd |
1 | 事务死锁检测线程 |
dm_lsnr_thd |
1 | 服务监听线程(端口 5236) |
dm_trxbro_thd |
1 | 活动事务视图广播线程(MVCC 可见性) |
SQL解析验证
10053 跟踪验证硬解析
10053 事件是达梦数据库专为分析优化器行为设计的诊断工具。它能详细记录优化器在硬解析过程中,为 SQL 选择执行路径的完整决策过程。
开启会话级 Trace(不影响其他会话):
ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT FOREVER, LEVEL 1';
执行 SQL 语句:
SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
关闭 Trace:
ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT OFF';
查看结果:
SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME = 'TRACE_PATH';
文件命名格式通常为 DMSERVER_月日_时分_进程ID.trc:
已用时间: 1.997(毫秒). 执行号:612.
13:59:19 sysdba@LOCALHOST:5236 SQL> ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT FOREVER, LEVEL 1';
操作已执行
已用时间: 93.601(毫秒). 执行号:613.
08:43:41 sysdba@LOCALHOST:5236 SQL> SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
T_ZS_ROWS
--------------------
10000
已用时间: 37.365(毫秒). 执行号:614.
08:45:02 sysdba@LOCALHOST:5236 SQL>
08:45:02 sysdba@LOCALHOST:5236 SQL> ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT OFF';
操作已执行
已用时间: 3.892(毫秒). 执行号:615.
08:45:07 sysdba@LOCALHOST:5236 SQL>
08:45:08 sysdba@LOCALHOST:5236 SQL> SELECT PARA_NAME, PARA_VALUE FROM V$DM_INI WHERE PARA_NAME = 'TRACE_PATH';
PARA_NAME PARA_VALUE
---------- --------------------
TRACE_PATH /dmdata/ZSDMDB/trace
已用时间: 72.638(毫秒). 执行号:616.
08:45:13 sysdba@LOCALHOST:5236 SQL>
08:45:13 sysdba@LOCALHOST:5236 SQL> exit
[dmdba@db01:/home/dmdba]$ cd /dmdata/ZSDMDB/trace/
[dmdba@db01:/dmdata/ZSDMDB/trace]$ ll
总用量 36
-rw-r--r-- 1 dmdba dinstall 9547 9月 12 08:45 ZSDMDB_0912_0845_140405615046128.trc
[dmdba@db01:/dmdata/ZSDMDB/trace]$ cat ZSDMDB_0912_0845_140405615046128.trc
DM Database Server x64 V9[03151060506-20260417-322930-20218], May 13 2026 15:52:58 built.
*** 2026-09-12 08:45:02.122000000
*** Start trace 10053 event [level 1]
Current SQL Statement:
SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
*****************************
Parameters for this statement
*****************************
......
use_pln_pool = 1
optimizer_mode = 1
optimizer_version = 70101
n_runs = 1
......
*** Plan before optimized:
project[0x7fb2bc822728] n_exp(1)
group[0x7fb2bc822098]
base table[0x7fb2bc821a28] (T_ZS, FULL SEARCH)
---------------- single table access path probe for T_ZS ----------------
*** path 1: INDEX33555476 (FULL search), cost: 1.09355
*** path 2: INDEX33555477 (FULL search), cost: 1.09355
>>> best access path: INDEX33555477 (FULL search), cost: 1.09355
*** BEST PLAN FOR THIS STATEMENT ***
project[0x7fb2bccd5a18] n_exp(1) (cost: 1.09355, rows: 1)
group[0x7fb2bccd60a8] (cost: 1.09355, rows: 1)
base table[0x7fb2bccd6738] (T_ZS, INDEX33555477, FULL SEARCH) (cost: 1.09355, rows: 10000)
从 trace 文件中,可以提取出以下关键信息:
基本信息
| 项目 | 内容 |
|---|---|
| Trace 事件 | 10053,级别 1 |
| SQL 语句 | SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS; |
| 优化器版本 | 70101 |
| 优化器模式 | optimizer_mode = 1 |
| 计划缓存 | use_pln_pool = 1(开启计划缓存) |
| 并行度 | parallel_degree = 1(串行执行) |
| 页大小 | 32768 字节(32KB) |
| 执行次数 | n_runs = 1 |
解析阶段证据
Plan before optimized(优化前计划):
project[0x7fb2bc822728] n_exp(1)
group[0x7fb2bc822098]
base table[0x7fb2bc821a28] (T_ZS, FULL SEARCH)
这是语法解析 + 语义解析后的初始解析树,说明:
- SQL 已被成功解析为树形结构
- 表
T_ZS已被正确识别 - 聚合操作
COUNT(*)表现为group+project节点 - 初始访问方式标记为
FULL SEARCH
单表访问路径探测
---------------- single table access path probe for T_ZS ----------------
*** path 1: INDEX33555476 (FULL search), cost: 1.09355
*** path 2: INDEX33555477 (FULL search), cost: 1.09355
>>> best access path: INDEX33555477 (FULL search), cost: 1.09355
| 路径 | 索引 | 访问方式 | 代价 |
|---|---|---|---|
| path 1 | INDEX33555476 | FULL search | 1.09355 |
| path 2 | INDEX33555477 | FULL search | 1.09355 |
- 优化器探测了 2 个索引(很可能是表上的两个二级索引)
- 两条路径代价完全相同(1.09355)
- 最终选择 INDEX33555477 作为最优路径
- 注意:虽然走了索引,但访问方式是 FULL search(全索引扫描),而非范围扫描,因为
COUNT(*)需要统计所有行
最终执行计划
*** BEST PLAN FOR THIS STATEMENT ***
project[0x7fb2bccd5a18] n_exp(1) (cost: 1.09355, rows: 1)
group[0x7fb2bccd60a8] (cost: 1.09355, rows: 1)
base table[0x7fb2bccd6738] (T_ZS, INDEX33555477, FULL SEARCH) (cost: 1.09355, rows: 10000)
| 节点 | 操作 | 代价 | 估算行数 |
|---|---|---|---|
| project | 投影输出 | 1.09355 | 1 |
| group | 聚合(COUNT) | 1.09355 | 1 |
| base table | 全索引扫描 T_ZS | 1.09355 | 10000 |
执行流程:
- 对
T_ZS表走INDEX33555477做全索引扫描(FULL SEARCH) - 扫描估算返回 10000 行
- 通过
group节点做 COUNT(*) 聚合 - 最终输出 1 行结果(
T_ZS_ROWS)
10053 小结
- 确认进行了语法/语义解析:
Plan before optimized中的project → group → base table树形结构就是语法解析的直接产物;表名T_ZS被正确解析并绑定,说明语义解析也已完成。 - 硬解析过程完整:优化器对两个索引做了代价探测,最终选择了
INDEX33555477。 - 这是一个"全索引扫描"而非"全表扫描":虽然
COUNT(*)通常走全表扫描,但此处优化器选择了走索引,因为索引通常比表更小,扫描代价更低。 - 代价极低(1.09355):说明表数据量不大,或者统计信息显示数据量很小。
2个视图侧面验证硬解析
数据库重启后进行测试,可以发现 sql cache hit count 计数为 0:
08:52:50 sysdba@LOCALHOST:5236 SQL> SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('sql cache hit count', 'select statements')\G
*************************** 1. row ***************************
NAME: sql cache hit count
STAT_VAL: 0
*************************** 2. row ***************************
NAME: select statements
STAT_VAL: 19
已用时间: 7.618(毫秒). 执行号:503.
HARD_PARSE_FLAG 为 2 代表首次执行:
08:52:55 sysdba@LOCALHOST:5236 SQL> SELECT SQL_ID, TOP_SQL_TEXT, HARD_PARSE_FLAG, N_LOGIC_READ, START_TIME
2 FROM V$SQL_HISTORY
3 WHERE TOP_SQL_TEXT LIKE '%T_ZS_ROWS FROM ZS.T_ZS%'
4 ORDER BY START_TIME DESC\G
*************************** 1. row ***************************
SQL_ID: 21
TOP_SQL_TEXT: SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
HARD_PARSE_FLAG: 2 <======第一次执行 标识2
N_LOGIC_READ: 1
START_TIME: 2026-09-12 08:45:02.148862
软解析验证
达梦数据库优化机制的正常表现。第二次执行相同 SQL 时,系统绕过了你希望通过 10053 事件观察的"硬解析"阶段。
原因在于达梦数据库的计划重用功能(由参数 USE_PLN_POOL 控制,默认为开启)。当 SQL 第一次执行时,数据库会进行完整的"硬解析"(语法、语义、优化器生成计划),并将最终的执行计划存入共享内存池中的 SQL 缓冲区(CACHE_POOL_SIZE 控制其大小)。
第二次执行时,系统会对比 SQL 文本的 HASH 值。如果发现缓存中已有相同计划,就直接"命中"并复用。这就完全跳过了成本高昂的"硬解析"过程,因此 10053 事件也就没有新的优化决策过程可以记录和打印了。
08:53:05 sysdba@LOCALHOST:5236 SQL>
08:54:50 sysdba@LOCALHOST:5236 SQL> ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT FOREVER, LEVEL 1';
操作已执行
已用时间: 5.079(毫秒). 执行号:904.
08:54:52 sysdba@LOCALHOST:5236 SQL> SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
T_ZS_ROWS
--------------------
10000
已用时间: 1.552(毫秒). 执行号:905.
08:54:59 sysdba@LOCALHOST:5236 SQL> ALTER SESSION SET EVENTS '10053 TRACE NAME CONTEXT OFF';
操作已执行
已用时间: 4.314(毫秒). 执行号:906.
08:55:07 sysdba@LOCALHOST:5236 SQL>
08:55:20 sysdba@LOCALHOST:5236 SQL> exit
[dmdba@db01:/dmdata/ZSDMDB/trace]$ ll
总用量 36
-rw-r--r-- 1 dmdba dinstall 9547 9月 12 08:45 ZSDMDB_0912_0845_140405615046128.trc
没有新文件产生
2个视图侧面验证软解析
按 SQL 文本哈希查计划缓存,命中直接取计划 HARD_PARSE_FLAG=0(重复执行):
08:57:06 sysdba@LOCALHOST:5236 SQL> SELECT SQL_ID, TOP_SQL_TEXT, HARD_PARSE_FLAG, N_LOGIC_READ, START_TIME
2 FROM V$SQL_HISTORY
3 WHERE TOP_SQL_TEXT LIKE '%T_ZS_ROWS FROM ZS.T_ZS%'
4 ORDER BY START_TIME DESC\G
*************************** 1. row ***************************
SQL_ID: 21
TOP_SQL_TEXT: SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
HARD_PARSE_FLAG: 0 <==== 第二次执行 标识0
N_LOGIC_READ: 1
START_TIME: 2026-09-12 08:54:59.872599
*************************** 2. row ***************************
SQL_ID: 21
TOP_SQL_TEXT: SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
HARD_PARSE_FLAG: 2 <==== 第一次执行 标识2
N_LOGIC_READ: 1
START_TIME: 2026-09-12 08:45:02.148862
已用时间: 5.052(毫秒). 执行号:1103.
sql cache hit count 计数+1 代表 SQL 命中缓存一次,同样 SQL 测试中只会 1 次计数,而且计数稍有延迟:
08:59:49 sysdba@LOCALHOST:5236 SQL> SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('sql cache hit count', 'select statements')\G
*************************** 1. row ***************************
NAME: sql cache hit count
STAT_VAL: 1
*************************** 2. row ***************************
NAME: select statements
STAT_VAL: 25
结论:硬解析很贵(要优化、要生成计划),软解析很便宜(查缓存即得)。OLTP 吞吐上限由软解析命中率决定,这也是为什么生产规范要求"用绑定变量代替拼接 SQL"——让相同结构的 SQL 共享同一个计划。
优化器与算子树说明
全表扫描 CSCN2
09:12:03 sysdba@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM ZS.T_ZS;
1 #NSET2: [1, 10000, 20]
2 #PRJT2: [1, 10000, 20]; exp_num(3), is_atom(FALSE); INFO_BITS(0)
3 #CSCN2: [1, 10000, 20]; INDEX33555476(T_ZS); btr_scan(1); need_slct(0); prejudge_iescn(0)
已用时间: 5.232(毫秒). 执行号:0.
没过滤条件,全表捞数据。底层 #CSCN2 沿主键索引叶子扫,上面 #PRJT2 投影输出。
主键等值 SSEK2(索引回表)
09:22:05 sysdba@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM ZS.T_ZS WHERE ID=100;
1 #NSET2: [1, 1, 20]
2 #PRJT2: [1, 1, 20]; exp_num(3), is_atom(FALSE); INFO_BITS(0)
3 #BLKUP2: [1, 1, 20]; INDEX33555477(T_ZS); use_clu_addr(0)
4 #SSEK2: [1, 1, 20]; scan_type(ASC), INDEX33555477(T_ZS), scan_range[100,100], is_global(0)
已用时间: 4.016(毫秒). 执行号:0.
ID=100 主键点查,#SSEK2 扫 [100,100] 一行,#BLKUP2 回表拿整行。OLTP 里最理想的计划。
二级索引范围
09:22:25 sysdba@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM ZS.T_ZS WHERE VAL BETWEEN 80 AND 100;
1 #NSET2: [1, 375, 20]
2 #PRJT2: [1, 375, 20]; exp_num(3), is_atom(FALSE); INFO_BITS(0)
3 #SLCT2: [1, 375, 20]; (T_ZS.VAL >= 80 AND T_ZS.VAL <= 100); slct_pushdown(1)
4 #CSCN2: [1, 10000, 20]; INDEX33555476(T_ZS); btr_scan(1); need_slct(1); prejudge_iescn(0)
Predicate Information (identified by operation id):
---------------------------------------------------
3 - filter((T_ZS.VAL >= 80 AND T_ZS.VAL <= 100))
已用时间: 3.224(毫秒). 执行号:0.
VAL 没索引,只能全表扫再过滤。need_slct(1) 表示边扫边过滤。要是 VAL 建了索引,这里就会走 #SSEK2 + #BLKUP2。
排序 SORT3
09:22:46 sysdba@LOCALHOST:5236 SQL> EXPLAIN SELECT * FROM ZS.T_ZS ORDER BY VAL;
1 #NSET2: [2, 10000, 20]
2 #PRJT2: [2, 10000, 20]; exp_num(3), is_atom(FALSE); INFO_BITS(0)
3 #SORT3: [2, 10000, 20]; key_num(1), partition_key_num(0), is_distinct(FALSE), top_flag(0), is_adaptive(0)
4 #CSCN2: [1, 10000, 20]; INDEX33555476(T_ZS); btr_scan(1); need_slct(0); prejudge_iescn(0)
已用时间: 3.372(毫秒). 执行号:0.
VAL 没索引,扫完再 #SORT3 排。代价从 1 涨到 2,排序是有成本的。数据量再大排不下就溢临时表空间。
哈希聚合 HAGR2
09:23:08 sysdba@LOCALHOST:5236 SQL> EXPLAIN SELECT VAL, COUNT(*) FROM ZS.T_ZS GROUP BY VAL;
1 #NSET2: [2, 100, 4]
2 #PRJT2: [2, 100, 4]; exp_num(2), is_atom(FALSE); INFO_BITS(0)
3 #HAGR2: [2, 100, 4]; grp_num(1), sfun_num(1), distinct_flag[0]; slave_empty(0) keys(T_ZS.VAL)
4 #CSCN2: [1, 10000, 4]; INDEX33555476(T_ZS); btr_scan(1); need_slct(0); prejudge_iescn(0)
已用时间: 4.067(毫秒). 执行号:0.
GROUP BY VAL 走哈希聚合。keys(T_ZS.VAL) 按 VAL 分组,VAL 只有 1000 个不同值,哈希桶内存放得下。
哈希连接 HASH2
创建两个连接列无索引的表:
DROP TABLE IF EXISTS T_HASH_A;
DROP TABLE IF EXISTS T_HASH_B;
-- 小表:连接列 CATE 无索引
CREATE TABLE T_HASH_A(ID INT, CATE VARCHAR(20), VAL INT);
-- 大表:连接列 CATE 无索引
CREATE TABLE T_HASH_B(ID INT, CATE VARCHAR(20), DESCR VARCHAR(100));
-- 插入数据
BEGIN
FOR I IN 1..500 LOOP
INSERT INTO T_HASH_A VALUES(I, 'C' || MOD(I,50), I*10);
END LOOP;
FOR I IN 1..5000 LOOP
INSERT INTO T_HASH_B VALUES(I, 'C' || MOD(I,50), 'desc_' || I);
END LOOP;
END;
/
COMMIT;
09:31:16 zs@LOCALHOST:5236 SQL> EXPLAIN
2 SELECT A.ID, A.VAL, B.DESCR
3 FROM T_HASH_A A, T_HASH_B B
4 WHERE A.CATE = B.CATE
5 AND A.VAL > 100;
1 #NSET2: [2, 1225, 152]
2 #PRJT2: [2, 1225, 152]; exp_num(3), is_atom(FALSE); INFO_BITS(0)
3 #HASH2 INNER JOIN: [2, 1225, 152]; KEY_NUM(1); KEY(A.CATE=B.CATE) KEY_NULL_EQU(0)
4 #SLCT2: [1, 25, 56]; A.VAL > 100; slct_pushdown(1)
5 #CSCN2: [1, 500, 56]; INDEX33555478(T_HASH_A as A); btr_scan(1); need_slct(1); prejudge_iescn(0)
6 #CSCN2: [1, 5000, 96]; INDEX33555479(T_HASH_B as B); btr_scan(1); need_slct(0); prejudge_iescn(0)
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access(A.CATE = B.CATE)
4 - filter(A.VAL > 100)
#HASH2 INNER JOIN 出来了。小表 T_HASH_A 过滤后建哈希表,大表 T_HASH_B 扫描探测。KEY(A.CATE=B.CATE) 是连接键,access 挂连接条件,filter 挂过滤条件。
查询形态对照表
这张表就是把前面 EXPLAIN 实测出来的算子链汇总到一起,方便对照。
| 查询形态 | 实测执行计划(算子链) | 关键算子 |
|---|---|---|
| 全表扫描 | #NSET2→#PRJT2→#CSCN2 |
CSCN2 簇集扫描 |
| 主键等值 | #NSET2→#PRJT2→#BLKUP2→#SSEK2(ID=100) |
主键索引扫描+回表 |
| 范围过滤(无索引) | #NSET2→#PRJT2→#SLCT2→#CSCN2 |
全表扫描+过滤 |
| 排序 | #NSET2→#PRJT2→#SORT3(key_num=1)→#CSCN2 |
SORT3 排序 |
| 分组聚合 | #NSET2→#PRJT2→#HAGR2(keys VAL, sfun 2)→#CSCN2 |
HAGR2 哈希聚合 |
| 等值连接(无索引) | #NSET2→#PRJT2→#HASH2 INNER JOIN→#CSCN2×2 |
哈希连接 |
| 去重 | #NSET2→#PRJT2→#DSSEK(range[min,max)) |
索引去重扫描 |
| Top-N | #NSET2→#PRJT2→#SORT3(top_flag=1) |
排序+提前截断 |
| 标量子查询 | #NSET2→#PIPE2→#PRJT2→#SLCT2(VAL>500)→#CSCN2 + #SPL2→#AAGR2(AVG)→#CSCN2 |
SPL2 子查询物化 |
| 内联视图 | #NSET2→#PRJT2→#SLCT2→#PRJT2→#SAGR2→#SSCN |
视图合并 |
看这张表能发现几个规律。最外层永远是 #NSET2,所有计划都从它开始。数据访问就两种:#CSCN2 全表扫,#SSEK2 走索引。中间那层是根据 SQL 要干什么加的,排序加 #SORT3,聚合加 #HAGR2,连接加 #HASH2 或 #MERGE。去重比较特殊,走 #DSSEK,借索引的有序性省掉排序。
查询优化对照
这张表是上一张的"结论版",直接告诉你什么场景优化器会选什么。
| 场景 | 优化器选择 | 成本特征 |
|---|---|---|
| 无索引 / 低选择性 | #CSCN2 全表扫描 |
读全表页,T_ZS 仅 1 万行,代价极低 |
| 主键高选择性等值 | #SSEK2 + #BLKUP2 |
只定位 1 行,回表 1 次 |
| 范围过滤无索引 | #SLCT2 + #CSCN2 |
全表扫后过滤 |
| ORDER BY VAL | #SORT3 |
1 万行内存排序;若走 IDX_TZS_VAL 可免排序 |
| GROUP BY VAL | #HAGR2 |
哈希分组,VAL 仅 1000 个不同值 |
| 无索引连接 | #HASH2 |
小表建哈希桶 |
| 子查询 | #SPL2 物化 |
子结果临时落盘 |
核心就一句话:选择性高就走索引,选择性低或者没索引就全表扫。连接也一样,有索引走归并,没索引走哈希。优化器干的事就是拿统计信息算代价,挑一条它觉得最便宜的路走。
缓冲池——读磁盘还是读内存
数据缓冲池——读写的中枢。
五个层次分工:NORMAL(常规页)→ FAST(热页)→ KEEP(常驻)→ RECYCLE(临时数据读后即弃)→ ROLL(回滚页)。
每层池内用三链管理页:自由链(空页可分配)、LRU 链(按最近使用排序淘汰)、脏链(待写回页)。
读路径:会话逻辑读命中 → 返回;未命中 → 从自由链取页/淘汰 LRU 尾页 → 物理读入(缺页)→ 挂 LRU 头 → 命中率趋近 100%。
写路径:DML 改页置脏入脏链 → 检查点(CKPT_INTERVAL=180s、CKPT_FLUSH_PAGES=1000)按批写回数据文件。
09:42:14 zs@LOCALHOST:5236 SQL>
2 SELECT
3 name AS 缓冲池,
4 SUM(n_pages * page_size) / 1024 / 1024 AS 大小_MB,
5 SUM(N_LOGIC_READS) AS 逻辑读,
6 SUM(N_PHY_READS) AS 物理读,
7 SUM(N_DIRTY) AS 脏页数,
8 ROUND(SUM(rat_hit) / COUNT(*), 4) AS 命中率
9 FROM V$BUFFERPOOL
10 GROUP BY name;
缓冲池 大小_MB 逻辑读 物理读 脏页数 命中率
--------- -------------------- -------------------- -------------------- -------------------- -------------------------
KEEP 8 0 0 0 NULL
RECYCLE 300 0 177 80 NULL
FAST 93 82363 6 31 9.999000000000000E-01
NORMAL 3905 15960 188 0 9.883999999999999E-01
ROLL 0 0 0 0 NULL
对照 SQL:
-- 查看各缓冲池大小和命中率
SELECT
name AS 缓冲池,
SUM(n_pages * page_size) / 1024 / 1024 AS 大小_MB,
SUM(N_LOGIC_READS) AS 逻辑读,
SUM(N_PHY_READS) AS 物理读,
SUM(N_DIRTY) AS 脏页数,
ROUND(SUM(rat_hit) / COUNT(*), 4) AS 命中率
FROM V$BUFFERPOOL
GROUP BY name;
-- 逻辑读 / 物理读前后对比
SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('logic read count','physical read count');
SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS; -- 跑一次,统计随之增长
SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('logic read count','physical read count');
09:52:23 sysdba@LOCALHOST:5236 SQL> SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('logic read count','physical read count');
NAME STAT_VAL
------------------- --------------------
logic read count 100100 <===== 观察逻辑变化
physical read count 103 <===== 观察物理读始终没变
已用时间: 6.057(毫秒). 执行号:1404.
09:52:43 sysdba@LOCALHOST:5236 SQL> SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
T_ZS_ROWS
--------------------
10000
已用时间: 0.764(毫秒). 执行号:1405.
09:52:59 sysdba@LOCALHOST:5236 SQL> SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('logic read count','physical read count');
NAME STAT_VAL
------------------- --------------------
logic read count 100140 <===== 观察逻辑变化
physical read count 103 <===== 观察物理读始终没变
已用时间: 6.640(毫秒). 执行号:1406.
09:53:17 sysdba@LOCALHOST:5236 SQL> SELECT COUNT(*) AS T_ZS_ROWS FROM ZS.T_ZS;
T_ZS_ROWS
--------------------
10000
已用时间: 0.370(毫秒). 执行号:1407.
09:53:23 sysdba@LOCALHOST:5236 SQL>
09:53:28 sysdba@LOCALHOST:5236 SQL> SELECT NAME, STAT_VAL FROM V$SYSSTAT WHERE NAME IN ('logic read count','physical read count');
NAME STAT_VAL
------------------- --------------------
logic read count 100175 <===== 观察逻辑变化
physical read count 103 <===== 观察物理读始终没变
已用时间: 3.473(毫秒). 执行号:1408.
逻辑读从 100100 → 100140 → 100175,每次增加 35~40 左右。物理读始终是 103,没有变化。
原因:表的数据页已经在缓冲池里了,后续每次执行都是逻辑读命中,不需要再读磁盘。逻辑读的增量来自扫描 T_ZS 表数据页、访问数据字典页、读系统表页。物理读只在第一次执行时产生,之后数据页常驻内存,物理读就不再增加。
总结
一条 SELECT COUNT(*) 在达梦里的完整链路:
- 监听:
dm_lsnr_thd接连接 - 会话:
dm_sql_thd绑定会话 - 解析:查共享池,未命中则语法解析、语义解析、权限校验
- 优化:优化器探测访问路径,选最优计划,10053 记录决策过程
- 缓存:计划存入共享池,第二次执行命中缓存走软解析
- 执行:按算子树执行,
#CSCN2/#SSEK2/#HASH2等算子 - 缓冲池:命中走逻辑读,未命中走物理读
- 返回:结果集组装发送客户端
HARD_PARSE_FLAG 从 2 变 0,sql cache hit count 从 0 变 1,logic read count 持续增长而 physical read count 不变,这些都是这条链路的直接证据。




