达梦数据库内存管理
达梦内存管理:内存池、缓冲区与排序区
核心论点:达梦(DM8)并不把每一次小内存申请都交给操作系统的
malloc/free,而是用自管内存把「数据页缓存、共享内存池、会话运行时内存」拆开管理。BUFFER、SORT_BUF_SIZE、HJ_BUF_SIZE 设小了会把复杂查询赶出内存、打到临时表空间;设大了会在多会话下把物理内存乘爆,甚至拖垮操作系统。本文按「为什么自管 → 两类内存池差在哪 → 参数过大过小会怎样 → 排序区/哈希区在复杂 SQL 里怎么工作 → 完整上机」展开,SQL 均可在单实例上复现。
前言
本文所要解决的问题有:
1)共享内存池和运行时内存池,谁在实例启动时向操作系统要内存,谁在会话起来之后才出现?
2)BUFFER、SORT_BUF_SIZE 设得太小和太大,分别会在哪一层出问题?
3)一条带 ORDER BY 和等值 JOIN 的 SQL,扫描、排序、哈希分别消耗哪一块内存?
1. 为什么达梦要自己管内存,而不是每次调用操作系统
数据库是典型的「高频申请、高频释放」软件:解析 SQL、建执行计划、排序、哈希连接、缓存数据页,每秒钟可能发生成千上万次小块内存进出。如果每一次都走操作系统:
- 要陷入内核,发生系统调用和可能的线程切换;
- 堆上容易产生碎片,长时间运行后「总量够、却申请不到连续块」;
- 数据库自己看不到「这块内存是哪个会话、哪条 SQL 拿走的」,泄露和越界很难定位。
达梦官方文档《DM 内存结构》中:
「数据库管理系统是一种对内存申请和释放操作频率很高的软件,如果每次对内存的使用都使用操作系统函数来申请和释放,效率会比较低,加入自己的内存管理是 DBMS 系统所必须的。」
——出处:DM 内存结构(达梦产品手册)
因此 DMServer 作为单进程、多线程的共享服务器,启动时先向操作系统要几块「大池子」,之后绝大多数小分配都在池内完成:用完归还池,而不是归还操作系统。操作系统看到的,主要是 dmserver 进程的 RSS;池内部谁占用、有没有溢出,要靠 V$MEM_POOL、V$BUFFERPOOL 来看。
关系结构:
操作系统物理内存
└── dmserver 进程
├── 数据缓冲区 BUFFER / KEEP / RECYCLE / FAST
│ (数据页;自由链 / LRU 链 / 脏链)
├── 共享内存池 MEMORY_POOL (实例启动时向 OS 申请)
│ └── 排序块、哈希块等短生命周期小片内存
└── 运行时内存池
├── SESSION 池 (会话建立时创建,断开时释放)
└── VM 池 (语句执行期)
| 区域 | 典型参数 | 存什么 | 生命周期 |
|---|---|---|---|
| 数据缓冲区 | BUFFER / KEEP / RECYCLE / FAST_POOL_PAGES |
数据页、索引页 | 实例级,启动时格式化成分页 |
| 共享内存池 | MEMORY_POOL / MEMORY_TARGET / MEMORY_EXTENT_SIZE |
字典、SQL 计划缓存、日志缓冲、排序/哈希等短生命周期块 | 实例启动时向 OS 申请,可扩展/收缩 |
| 运行时内存池 | 会话池、虚拟机(VM)池 | 本会话的解析、执行器私有结构 | 会话建立时创建,会话结束时销毁 |
BUFFER 不是内存池。它是按数据页切好的高速缓存,用自由链 / LRU 链 / 脏链管理。内存池解决的是「小块分配不要每次找 OS」。把这两件事当成同一个旋钮,是调参里最常见的误区。
2. 共享内存池 与 运行时内存池
2.1 共享内存池
共享内存池由 MEMORY_POOL 指定初始大小。实例启动时向操作系统申请这一大片。之后:
- 需要小片内存:从池里切;
- 用完:还回池,供其他模块复用;
- 池不够:按
MEMORY_EXTENT_SIZE扩展; MEMORY_TARGET是扩展后的收缩水位:超过该值后,空闲时尽量缩回目标大小。
高并发时把 MEMORY_POOL 适当调大,是为了减少运行期再向操作系统伸手。新人培训课件《DM7 性能诊断与优化》里的建议逻辑是:公共内存池过小,会频繁向 OS 申请,拉低效率。
相关参数还可以看到 MEMORY_N_POOLS / N_MEM_POOLS:把公共池切成多片,降低并发分配时的锁冲突。并发会话多时,片数过少会出现「内存总量还够,但分配排队」的假象。
2.2 运行时内存池
除共享池外,功能模块还会持有自己的运行时池。对应用最有体感的是:
- 会话内存池(SESSION):客户端连上来、服务器为该会话准备私有环境时创建。事务控制块、非特殊操作符的临时结构,多从这里出。
- 虚拟机内存池(VIRTUAL MACHINE / VM):语句真正执行时创建。执行器、表达式计算等大量工作内存来自这里。
它们的共同特点是:随使用产生,随会话(或语句)结束释放。这些运行时池会从操作系统申请一片,作为本模块自己的池来用。
还可以这样理解分工:
- 生命周期与整个会话绑定的(会话池、VM 池)→ 独立池,会话之间互不抢同一把分配锁,这正是连接池「会话要保持」的底层原因之一;
- 生命周期只覆盖某个操作符的(排序块、哈希块)→ 往往不再单独向 OS 再开一个池,而是从共享池里切,用完立刻还。
所以「运行时内存池 = 会话动态申请」这句话是对的,但要补半句:动态申请的对象,优先是达梦自己的池,而不是每次直接 malloc。 只有池的初始值用尽、尚未顶到 target 时,才会再向操作系统要;超过 target 之后,更倾向于回到共享池,避免和 OS 频繁交互。
2.3 一条 SQL 实际会碰几块池
简化时间线:
客户端连接
└─ 创建 SESSION 运行时池 ← 会话级,连接在就在
收到 SQL
├─ 解析 / 绑定 / 计划
│ └─ 字典缓冲、SQL 缓冲(共享池侧)
└─ 执行
├─ 创建 / 使用 VM 池 ← 本语句执行期
├─ 扫表:数据页走 BUFFER
├─ ORDER BY / DISTINCT:排序区(短生命周期,多从共享池切)
└─ HASH JOIN / HASH GROUP:哈希区(同上,超限则外存哈希)
会话断开
└─ SESSION / VM 池归还
用 V$MEM_POOL 时,会同时看到名字像共享池的条目,以及大量带会话线程号的运行时池。把 CREATOR 和 V$SESSIONS.THRD_ID 关联,就能回答「哪条会话把内存抬起来了」。
3. BUFFER、SORT_BUF_SIZE、哈希参数:太小和太大分别怎样
过小、过大的对照见本节各小节和第 3.4 表。是否真的外溢、是否真的在淘汰,只能看第 5 节里 V$BUFFERPOOL、RECYCLE、SET TIMING ON 的上机截图。
3.1 BUFFER:数据页高速缓存
启动时按 BUFFER 向 OS 申请连续内存,按页大小切开,挂到自由链。读数据:先找缓冲,没有再读盘并放入 LRU;改数据:页变脏,进脏链,由检查点/淘汰刷盘。
设太小:
- 自由链很快耗尽,LRU 不停淘汰;
V$BUFFERPOOL里FREE接近 0,N_DISCARD64(或淘汰计数)持续上涨;- 缓冲命中率低,
iostat的%util/await升高,SQL 看起来「优化器没问题,但就是慢」。
设太大:
- 实例启动直接失败,或启动后把 OS 可用内存吃光;
- Linux 开始 Swap(
si/so不为 0),此时再快的缓冲也比不过被换到磁盘上的「假内存」; - 挤压共享池、排序区、哈希区,复杂查询反而更爱打临时表;
- 脏页总量变大,检查点刷盘更猛,出现周期性 IO 尖峰。
经验范围(不是公式):单机 OLTP 常把 BUFFER 放到物理内存的 60%~80%,且 MAX_BUFFER 建议与 BUFFER 对齐,避免无节制动态扩张。数据量比内存小,可以按数据量来,没必要为了「看起来大方」把缓冲开到远超数据文件。
BUFFER_POOLS 是缓冲区分片数,并发高时用质数(如 47、101)降低缓冲内部闩锁冲突。它不增加总容量,只改变并发结构。
RECYCLE 是临时表空间用的缓冲。排序外溢、哈希外溢、WITH 物化、临时表,都会打到这里。只调 SORT_BUF_SIZE 却把 RECYCLE 留在默认几十 MB,外排序依然会很痛。
3.2 SORT_BUF_SIZE:单线程排序上限
排序区不是启动时划死的一块「排序专用大数组」,而是每次排序先申请、排完就释放。SORT_BUF_SIZE 约束的是**单个排序操作(常见口径:单个线程)**能用的内存上限。
还有一组配套参数(名称以你版本 V$PARAMETER 为准):
| 参数 | 作用 |
|---|---|
SORT_BUF_SIZE |
单次/单线程排序内存上限 |
SORT_BUF_GLOBAL_SIZE |
实例内排序内存总和上限 |
SORT_BLK_SIZE |
排序分片大小 |
SORT_FLAG |
0 整片排序;1 大内存分片排序(一般不建议长期打开) |
设太小:
- 内排序变外排序:排不下的批次写入临时段,再多路归并;
RECYCLE和临时表空间上涨,磁盘排序比内存排序慢一个数量级很常见;- 建索引、
ORDER BY大结果集、GROUP BY/DISTINCT都会受影响。
设太大:
- 它往往是会话级可改参数。100 个会话同时排序,理论峰值接近
100 × SORT_BUF_SIZE,再和SORT_BUF_GLOBAL_SIZE去争; - 共享池被排序块占满,其他会话解析、哈希一起挨饿;
- 连接池场景下,「测环境单会话很快、生产一高峰就 OOM」多半是这个乘法没算。
手册对默认值的建议随版本有 2MB / 20MB 等差异,以当前实例为准,不要背死数字。建索引可以会话内临时调大,作业跑完改回去。
3.3 哈希区:HJ_BUF_* 与 HAGR_BUF_*
哈希连接消耗内存,因为要选较小的一侧做 Build,在内存里建哈希表,再探测另一侧。达梦把哈希缓冲称作虚拟缓冲:并不是预先划死一块「哈希专用物理内存」,而是按数据量估算;能放下就在内存池里做哈希,放不下就走外存哈希(分区写临时段,再逐分区哈希)。
官方手册原文:
「DM8 提供了为哈希连接而设定的缓冲区,不过该缓冲区是个虚拟缓冲区。……如果计算出的数据量大小超过了哈希缓冲区的大小,则使用 DM8 创新的外存哈希方式;如果没有超过哈希缓冲区的大小,实际上还是使用内存池来进行哈希操作。」
——出处:同上,《DM 内存结构》3.4 节
| 参数 | 含义 |
|---|---|
HJ_BUF_SIZE |
单次哈希连接可用内存 |
HJ_BUF_GLOBAL_SIZE |
实例内哈希连接内存总和 |
HJ_BLK_SIZE |
哈希操作符每次分配块大小 |
HAGR_BUF_SIZE / HAGR_BUF_GLOBAL_SIZE |
哈希分组、DISTINCT、集合运算、分析函数等 |
HAGR_HASH_SIZE |
聚集时哈希桶个数 |
HJ_BUF_SIZE 太小: Build 表估不准或真实更大,反复外存哈希,等值连接没有索引时尤其明显。
HJ_BUF_SIZE 太大: 单条 SQL 吃得很爽,多条并发 HASH JOIN 会顶满 HJ_BUF_GLOBAL_SIZE,再和 BUFFER 抢物理内存。
HJ_BUF_GLOBAL_SIZE 太大: 看起来「哈希很宽裕」,实际是从整机内存里挖走一块,BUFFER 命中率下降,OLTP 与分析混跑时最容易中招。
嵌套循环靠索引反复探测,几乎不吃哈希区;哈希连接吃内存、换的是「没索引也能做大结果等值连接」。调哈希参数之前,先看执行计划里到底是 NEST LOOP 还是 HASH JOIN,否则会调错对象。
3.4 一张表收口
| 参数 | 过小 | 过大 |
|---|---|---|
MEMORY_POOL |
频繁向 OS 扩展;N_EXTEND_EXCLUSIVE 长期 > 0 |
启动占用高,空闲也浪费;可能挤 BUFFER |
BUFFER |
命中率低、淘汰多、物理 IO 高 | Swap、启动失败、检查点 IO 风暴、挤占排序/哈希 |
SORT_BUF_SIZE |
外排序、临时表空间涨、ORDER BY/建索引慢 | 会话数一乘就爆;挤共享池 |
HJ_BUF_SIZE |
外存哈希、等值大连接变慢 | 单 SQL 吃内存;并发哈希互相排队 |
RECYCLE |
外排序/外哈希即使「算法对了」也被磁盘拖死 | 临时场景不占那么多时浪费 |
4. 复杂查询里,排序区和哈希区到底在干什么
ORDER BY / GROUP BY / DISTINCT
│
├─ 工作集 ≤ SORT_BUF_SIZE → 内存内排序 → 得到有序结果
└─ 工作集 > SORT_BUF_SIZE → 写入临时段(RECYCLE)→ 多路归并
等值 HASH JOIN
│
├─ Build 侧 ≤ HJ_BUF_SIZE → 内存中建哈希表并探测
└─ Build 侧 > HJ_BUF_SIZE → 分区写入临时段 → 逐分区再哈希
上面只是机制;你的库走的是哪一条,以实验 5、实验 6 的 EXPLAIN 和耗时截图为准。
4.1 排序区:ORDER BY 只是入口
会用到排序的,远不止 ORDER BY:
ORDER BY/GROUP BY(无合适索引时)DISTINCT、UNION(去重)MERGE JOIN前的有序化- 创建索引时的键排序
- 部分窗口函数
内排序: 数据量 ≤ SORT_BUF_SIZE(再受全局上限约束),全程内存完成,结束即释放。
外排序: 装不下就分批写临时段(走临时表空间,缓冲落在 RECYCLE),再归并。结果仍然正确,代价是 IO 和 CPU 双涨。
所以看到「这条 SQL 的计划并不差,但一加 ORDER BY 就慢」,优先看:
- 能不能用索引避免排序;
- 避免不了时,单次排序量是否远大于
SORT_BUF_SIZE; - 临时表空间和
RECYCLE是否已经成为第二瓶颈。
4.2 哈希区:等值连接的「用内存换随机 IO」
哈希连接典型条件:
- 等值连接(
t1.c1 = t2.c1);非等值(>、BETWEEN)通常不会选它; - 连接列缺索引,或优化器认为建哈希表比嵌套循环反复探索引更划算;
- 选较小的 row source 做 Build 表。
内存足够:Build 侧在内存建哈希表,Probe 侧逐行探测,CPU 密集、IO 少。
内存不够:两表按哈希函数分区写入临时存储,再对每个分区做内存哈希——这就是外存哈希。分区次数越多,越接近「自己实现了一遍磁盘 hash join」。
HAGR_* 覆盖的是另一类哈希:GROUP BY 哈希聚合、DISTINCT、集合运算等。一条 SQL 完全可能同时占用排序区和哈希区,例如:
SELECT dept_id, COUNT(*), MAX(amount)
FROM big_fact f
JOIN dim_dept d ON f.dept_id = d.dept_id -- 可能 HASH JOIN → HJ_BUF_*
GROUP BY dept_id -- 可能 HASH GROUP → HAGR_BUF_*
ORDER BY 2 DESC; -- 排序区 → SORT_BUF_*
扫描这些表的数据页仍然走 BUFFER。因此「复杂查询慢」要拆成三问:页缓命中够不够、哈希是否外溢、排序是否外溢。只加 BUFFER,解决不了后两问。
4.3 和达梦其他产品的关系(不只单实例)
- 数据守护(DataWatch):备库同样有自己的
BUFFER与内存池。备库内存往往小于主库,若按主库原样拷贝dm.ini,备库可能起不来,或在重演 REDO 时把 OS 打满。 - 读写分离 / DSC:每个节点一份缓冲,不存在「集群共享一块 BUFFER」。SQL 打到哪一节点,排序/哈希就在那一节点的运行时内存里发生。
- DMETL / 数据迁移:大批量装数、排序清洗时,作业进程和数据库会话会叠加内存。ETL 主机和数据库主机的 BUFFER 规划要分开算,不要默认「都在一台机器上,BUFFER 开到 80%」。
5. 上机实验
第 1 步:看清四个参数(证明 BUFFER 和内存池不是一回事)
SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN (
'BUFFER',
'MEMORY_POOL',
'SORT_BUF_SIZE',
'HJ_BUF_SIZE'
);

第 2 步:再连一次库,证明运行时池跟着会话走
在窗口执行下列sql语句
SELECT SESS_ID, USER_NAME, STATE, THRD_ID
FROM V$SESSIONS
ORDER BY SESS_ID;
在新连接的窗口里再执行上面那句。用户多出一行 SESS_ID。
比内存池:
SELECT COUNT(*) AS POOL_CNT FROM V$MEM_POOL;
SELECT NAME, COUNT(*) AS CNT
FROM V$MEM_POOL
GROUP BY NAME
ORDER BY CNT DESC;


第 3 步:造表 + 对比排序内存
CREATE TABLE MEM_FACT (
ID INT NOT NULL,
DEPT INT NOT NULL,
AMOUNT DECIMAL(18,2),
PAD VARCHAR(180)
);
INSERT INTO MEM_FACT
SELECT LEVEL,
MOD(LEVEL, 50) + 1,
MOD(LEVEL, 1000) * 0.37,
RPAD('X', 180, 'Y')
FROM DUAL
CONNECT BY LEVEL <= 50000;
COMMIT;
SELECT COUNT(*) FROM MEM_FACT;
让服务器把行按 ID 排完再写入另一张表,避免 Manager 把行画在屏幕上。
先热身一次(耗时丢掉,只为把数据打进 BUFFER):
SELECT COUNT(*), SUM(ID) FROM MEM_FACT;
再准备结果表,同一窗口继续:
CREATE TABLE MEM_OUT (ID INT, PAD VARCHAR(180));
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 1);
TRUNCATE TABLE MEM_OUT;
INSERT INTO MEM_OUT SELECT ID, PAD FROM MEM_FACT ORDER BY ID DESC;
COMMIT;
看这条 INSERT 的耗时。同一窗口再:
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 32);
TRUNCATE TABLE MEM_OUT;
INSERT INTO MEM_OUT SELECT ID, PAD FROM MEM_FACT ORDER BY ID DESC;
COMMIT;
每档连跑两遍,只记第二遍(第一遍可能还在填缓冲)。比较的是 INSERT ... ORDER BY 的耗时。
最后把排序缓冲改回第 1 步的原值:
SP_SET_PARA_VALUE(1, 'SORT_BUF_SIZE', 2);


第 4 步:对比哈希内存(大表自己连自己)
维表很小的 JOIN 往往看不出差异,所以用自连接把 Build 侧做大。
SP_SET_PARA_VALUE(1, 'HJ_BUF_SIZE', 2);
SELECT /*+ USE_HASH(A, B) */ COUNT(*)
FROM MEM_FACT A, MEM_FACT B
WHERE A.ID = B.ID
AND A.DEPT = 1;
看耗时。同一窗口再:
SP_SET_PARA_VALUE(1, 'HJ_BUF_SIZE', 64);
SELECT /*+ USE_HASH(A, B) */ COUNT(*)
FROM MEM_FACT A, MEM_FACT B
WHERE A.ID = B.ID
AND A.DEPT = 1;
/*+ USE_HASH(A, B) */ 是提示优化器走哈希连接。两次差不多,说明本机数据量未打满 HJ_BUF,差异不明显。
做完清理:
DROP TABLE MEM_FACT;


6. 调参时的一组可执行原则
- 先分清三种慢: 页缓命中不够(BUFFER)、排序外溢(SORT_* + RECYCLE)、哈希外溢(HJ_* / HAGR_*)。用
EXPLAIN+V$BUFFERPOOL+V$MEM_POOL三件套,不要凭感觉加内存。 - BUFFER 按数据热度和物理内存一起算,不是按「参数表里谁默认大」谁就该最大。改完必须重启,用淘汰计数而不是「感觉」验收。
- SORT_BUF_SIZE 按单条 SQL 的排序工作集估,再乘并发。 连接池里的每个活连接都可能同时排序。
- 哈希参数看 Build 侧大小。 小维表哈希吃不满
HJ_BUF_SIZE;大表无索引等值连接才需要认真调,并给HJ_BUF_GLOBAL_SIZE留并发余量。 - 共享池负责「少找 OS」。
N_EXTEND_EXCLUSIVE长期大于 0,再考虑加MEMORY_POOL/MEMORY_TARGET,而不是先把 BUFFER 再加 20GB。 - 守护、集群、ETL 节点各自算一份内存账。 达梦产品线里每个进程都有自己的缓冲与池,拷贝一份主库
dm.ini到小规格备库,是现场常见事故。
7.总结
达梦把内存管起来,不是为了多几个参数名字,而是为了在单进程多线程模型下,把「数据页缓存」和「小块工作内存」从操作系统的通用堆里拆出来。共享内存池在启动时要一大块,换的是后续少做系统调用;运行时内存池跟着会话走,换的是会话之间少抢同一把分配锁,也换来连接池必须被正确关闭、否则池不释放。排序区和哈希区则是复杂查询里最容易被忽略的两条旁路:它们在够用时几乎无感,不够用时不会立刻报错,而是改走临时表空间——于是故障表现常常是「磁盘很忙、SQL 计划却看不出明显错误」。
参考(引用已在正文用双引号标出)
- 达梦产品手册:《DM 内存结构》,https://eco.dameng.com/document/dm/zh-cn/pm/memory-structure.html
- 达梦培训材料:《DM7 性能诊断与优化》(实例内存参数表:
MEMORY_POOL、BUFFER、SORT_BUF_SIZE、HJ_BUF_*) - 动态视图:
V$PARAMETER/V$DM_INI、V$MEM_POOL、V$BUFFERPOOL、V$SESSIONS、V$SQL_STAT(以所装版本系统视图说明为准)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)