暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

DM9 一条 SELECT 的 0.01 秒:达梦数据库内部走了一条什么路【#达梦数据库 #达梦同行者征文】

原创 bicewow 13小时前
6

前言

一条 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 从客户端到结果返回,大致走这么一条链路:

clip.png

下面是文字说明,

客户端发出 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$THREADSdm_lsnr_thddm_sql_thd
硬解析 / 软解析 V$SQL_HISTORY.HARD_PARSE_FLAG(2 硬 / 0 软)
优化器决策 10053 trace 看 Plan before optimizedBEST PLAN
算子树 EXPLAIN#NSET2→#PRJT2→#CSCN2 等算子链
缓存命中 V$SYSSTATsql cache hit count
逻辑读 / 物理读 V$SYSSTATlogic read countphysical 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

执行流程:

  1. T_ZS 表走 INDEX33555477全索引扫描(FULL SEARCH)
  2. 扫描估算返回 10000 行
  3. 通过 group 节点做 COUNT(*) 聚合
  4. 最终输出 1 行结果(T_ZS_ROWS

10053 小结

  1. 确认进行了语法/语义解析Plan before optimized 中的 project → group → base table 树形结构就是语法解析的直接产物;表名 T_ZS 被正确解析并绑定,说明语义解析也已完成。
  2. 硬解析过程完整:优化器对两个索引做了代价探测,最终选择了 INDEX33555477
  3. 这是一个"全索引扫描"而非"全表扫描":虽然 COUNT(*) 通常走全表扫描,但此处优化器选择了走索引,因为索引通常比表更小,扫描代价更低。
  4. 代价极低(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(*) 在达梦里的完整链路:

  1. 监听dm_lsnr_thd 接连接
  2. 会话dm_sql_thd 绑定会话
  3. 解析:查共享池,未命中则语法解析、语义解析、权限校验
  4. 优化:优化器探测访问路径,选最优计划,10053 记录决策过程
  5. 缓存:计划存入共享池,第二次执行命中缓存走软解析
  6. 执行:按算子树执行,#CSCN2 / #SSEK2 / #HASH2 等算子
  7. 缓冲池:命中走逻辑读,未命中走物理读
  8. 返回:结果集组装发送客户端

HARD_PARSE_FLAG 从 2 变 0,sql cache hit count 从 0 变 1,logic read count 持续增长而 physical read count 不变,这些都是这条链路的直接证据。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论