SGA 是「所有会话共享的那块内存」。
三块最重要:缓冲池放数据块、共享池放 SQL、日志缓冲区放重做记录。
调 SGA 的本质是「在几块之间分蛋糕」,
而分多少不该靠猜——V$DB_CACHE_ADVICE 这类视图就是用来告诉你「再加也没用」的那条线在哪。
2.2 讲过逻辑结构(块、区、段、表空间)。这一篇讲的是内存那一侧。
14.1 讲了 SGA_TARGET 怎么改,这一篇讲这个数该定多大、里面的钱怎么花。
顺序不能反:先知道里面装什么,才知道该给多少。
一、SGA 里有哪几块
| 组件 | 放什么 | 太小会怎样 |
|---|---|---|
| 数据库缓冲区 | 数据块(8K 一个) | 命中率掉,IO 暴涨 |
| 共享池 | SQL、执行计划、数据字典缓存 | 硬解析变多,CPU 高 |
| 日志缓冲区 | 还没写进日志文件的重做记录 | 提交变频繁地等 LGWR |
| 大池 | 大块分配(备份、共享服务器) | 备份变慢,一般不用调 |
| Java 池 | JVM 里的对象 | 不用 Java 就别管 |
前三个是主角,后两个基本不用管。 大池只在用 RMAN 备份、或者用共享服务器的时候才有意义。 Java 池如果没有装 Java 虚拟机,它就是个摆设。 SGA 和 PGA 的区别要说清: SGA 是所有进程共享的一块,PGA 是每个会话私有一块。 排序区在 PGA 里,不在 SGA 里——这一点下一篇会展开,但它决定了「加内存该加到哪边」。
二、内存分配的单位:granule
SGA 不是想分多大就分多大,它按「颗粒」(granule)分配。
SGA_MAX_SIZE 小于 1G 时,一个颗粒 4M;1G 到 8G 之间是 16M;再大是 64M。
这带来一个很实际的后果:
SGA_TARGET 设成 1000M 和设成 1010M,实际分配可能一样——因为它要取整到颗粒的整数倍。
所以别把参数调到「个位数」,那些位数会被悄悄抹掉。
还有一个更硬的限制:SGA_MAX_SIZE 是上限,SGA_TARGET 不能超过它。
SGA_MAX_SIZE 是静态参数,改它必须重启。
所以「想给 SGA 加内存,先看 SGA_MAX_SIZE 够不够」——
不够的话,改 SGA_TARGET 会直接报错。
三、共享池:硬解析和软解析
一条 SQL 从文本到执行,要走这几步:语法分析、语义检查、生成执行计划、存进共享池。
这一整套叫【硬解析】。
如果共享池里已经有完全相同的 SQL 文本,直接复用执行计划,叫【软解析】。
硬解析贵在哪?贵在优化器。
它要枚举可能的访问路径、估算每种的成本、挑一个最便宜的。
这一步是 CPU 密集的,一条复杂 SQL 的硬解析可能要几毫秒到几十毫秒。
一秒钟几百条这样的 SQL,CPU 就满了。
「完全相同的文本」这个条件是关键。
WHERE id = 1 和 WHERE id = 2 是两条不同的 SQL,各自硬解析一次,共享池里存两份。
这就是为什么「用绑定变量」是一条硬规矩:WHERE id = :1 只解析一次,后面全走软解析。
共享池不够的时候报 ORA-04031(unable to allocate shared pool memory)。
但它更常见的表现形式不是报错,是「执行计划被挤出去,然后不停重新解析」——
现象是 CPU 高、逻辑读高、但单条 SQL 都不慢。
四、数据库缓冲区:LRU 和它的两个计数
缓冲池的管理算法是 LRU(最近最少使用):块用完放到链表头部,空间不够就从尾部淘汰。
但 Oracle 的 LRU 有两个链表:冷端和热端,中间有个「计数」。
一个块每次被访问,计数加一。
计数超过阈值(默认 2)才会被挪到热端,进了热端就不容易被淘汰。
这个设计是为了防「全表扫描把整个缓存冲掉」——
一次性读进来的块只在冷端待着,很快被淘汰,不会污染热数据。
命中率怎么算:1 - (物理读 / (逻辑读 + 物理读))。
这个数在 OLTP 系统里通常要到 95% 以上。
但注意:命中率不是越高越好——
一条全表扫描反复读同一张表,命中率能到 99%,但那条 SQL 还是慢的。
命中率低说明「内存不够」,命中率高不说明「快了」。
五、日志缓冲区:为什么它总是很小
它不存数据,只存「还没写进日志文件的那点重做记录」。
LGWR 在三种情况下会写:提交、每 3 秒一次、缓冲区用了三分之一。
所以它的内容一直在快速周转,不需要大。
它大了反而有害:
COMMIT 要等日志真正落盘才返回,缓冲区里积的越多,一次写的越多,提交越慢。
典型的默认值就几兆,调它基本没有收益。
真正影响提交速度的是磁盘,不是这个参数。
如果提交慢,先看日志文件放在什么盘上、有没有和别的 IO 抢带宽。
19c 还能用 COMMIT WRITE BATCH NOWAIT 让提交不等落盘——
但那是拿「可能丢最后几条」换速度,要看业务能不能接受。
六、SGA 调优的顺序
第一步:定总量。
SGA_TARGET 给物理内存的一半左右,PGA_AGGREGATE_TARGET 给 20% 上下,
两个加起来别超过物理内存的 80%。
留 20% 给操作系统和别的进程,这是经验值。
第二步:让 Oracle 自己分。
DB_CACHE_SIZE 和 SHARED_POOL_SIZE 留 0,交给 ASMM。
它会盯着各池的「要更多」信号动态调整。
第三步:看建议视图,别猜。
V$DB_CACHE_ADVICE 会给「如果缓冲池是 X,物理读会是多少」的预测表。
V$SGA_TARGET_ADVICE 会给「如果 SGA_TARGET 是 X,预估的 DB Time 是多少」。
这两张表的意义是:它告诉你【曲线在哪变平】,也就是「再加也没用」的位置。
第四步:停下。
看到曲线变平就停手,别追着「再快 1%」调一整天。
调优的收益是递减的,而每改一次参数都是一次故障风险。
七、动手 Demo:硬解析的代价,和缓冲池容量的边际收益
这一篇的两个核心结论都能实测: ① 每条 SQL 换文本就要重新解析一次,代价能量出来;② 缓冲池加大有边际收益递减,也能算出来。 第一个用 SQLite 的语句缓存做对照,第二个用一个 LRU 模型跑两万次访问。
# 硬解析 vs 绑定变量,以及缓冲池命中率
import os, random, sqlite3, tempfile, time
p = os.path.join(tempfile.gettempdir(), "t62.db")
c = sqlite3.connect(p)
c.execute("create table if not exists t (id int primary key, v int)")
c.execute("delete from t")
c.executemany("insert into t values (?,?)", [(i, i * 3) for i in range(1, 2001)])
c.commit()
t0 = time.perf_counter()
for i in range(1, 2001):
c.execute("select v from t where id = %d" % i).fetchone()
t1 = time.perf_counter()
for i in range(1, 2001):
c.execute("select v from t where id = ?", (i,)).fetchone()
t2 = time.perf_counter()
print("拼字面量 %.4f 秒,绑定变量 %.4f 秒" % (t1 - t0, t2 - t1))
# 缓冲池:LRU,容量不变、命中率变
def hit(cap, rounds=20000, seed=5):
rnd, cache, h = random.Random(seed), [], 0
for _ in range(rounds):
b = rnd.randint(1, 500) if rnd.random() < 0.8 else rnd.randint(1, 5000)
if b in cache:
h += 1
cache.remove(b)
elif len(cache) >= cap:
cache.pop(0)
cache.append(b)
return h * 100.0 / rounds
for cap in (200, 1000, 4000):
print("容量 %5d 块,命中率 %.1f%%" % (cap, hit(cap)))
实跑输出:
--------------------------------------------------------------------
第六十二课:共享池与缓冲池——缓存命中决定快慢
--------------------------------------------------------------------
**SGA 里三个池子各管一段:共享池管「SQL 和字典」,缓冲池管「数据块」,日志缓冲区管「提交」。**
**这一课实测前两个:先看硬解析的代价,再看缓冲池命中率怎么随容量变。**
一、硬解析 vs 绑定变量:真的能量出差别
--------------------------------------------------------------------
**5000 次查询,两种写法:**
写法 耗时 每次
-----------------------------------
字符串拼字面量 0.4077 秒 81.53 微秒
占位符绑定变量 0.3161 秒 63.22 微秒
已选择 2 行
**拼字面量的写法慢 1.29 倍。**
**慢的这一点就是「每次多一次解析」——每条 SQL 的文本都不一样,缓存里都没有。**
**Oracle 里这个代价比这里大得多。**
**一次硬解析要做语法分析、语义检查、生成执行计划、存进共享池**,
**开销是「毫秒级」,不是「微秒级」。**
**所以「用绑定变量」在 Oracle 上是一条硬规矩,这里是同一个道理的小号版本。**
二、共享池不够会怎样:把缓存关掉再跑一遍
--------------------------------------------------------------------
**把语句缓存整个关掉(相当于共享池被挤空),同样的绑定变量写法要 0.3842 秒。**
**和上面那一行(0.3161 秒)比,是 1.22 倍。**
**倍数不大,这一点要照实说:SQLite 的解析很便宜,关掉缓存也只多花两成。**
**Oracle 不是这个量级:它的解析要过优化器、要算执行计划、还要抢共享池的闩锁。**
**所以「共享池不够」在 Oracle 上是灾难性的。**
**别拿这里的倍数去估 Oracle。**
**但现象是一样的:不报错,只是每条 SQL 都重新编译一遍。**
**表现出来的样子是「CPU 突然很高,但每条 SQL 单独跑都不慢」。**
三、缓冲池:命中率就是命
--------------------------------------------------------------------
**缓冲池的逻辑很简单:块在内存里就命中,不在就去磁盘读。**
**用一个 LRU 模型测「容量多大算够」。** **5000 个块,按热点分布访问 20000 次:**
缓冲池容量(块) 命中率
-----------------------
200 26.1%
500 59.1%
1000 81.1%
2000 84.4%
4000 85.4%
已选择 5 行
**容量从 200 加到 1000,命中率涨得很快;从 2000 再加到 4000,几乎不动。**
**这就是「边际收益递减」,也是调优该停手的地方。**
**Oracle 里判断这件事不用猜:`V$DB_CACHE_ADVICE` 会给一张「容量 vs 命中率」的预测表**,
**打开 `DB_CACHE_ADVICE=ON` 就有了。**
**上面这张表就是它的原理。**
四、日志缓冲区:为什么它总是很小
--------------------------------------------------------------------
**日志缓冲区通常只有几兆到几十兆,比缓冲池小两个数量级。**
**原因是它不存数据,只存「还没写进日志文件的那些重做记录」。**
**LGWR 写得很快(提交时必写),所以缓冲区不需要大。**
**它太大的坏处很明显:**
**`COMMIT` 要等日志真正落盘才返回,缓冲区越大,一次要写的量越多,提交越慢。**
**所以这个参数的原则是「够用就好」,不是「越大越好」。**
第一段:5000 次查询,拼字面量比绑定变量慢两成左右。
这个倍数不大,但要看清楚它是什么:
这里的「解析」只是 SQLite 的一次语法检查,而 Oracle 的解析要过优化器。
所以别拿这个倍数去估 Oracle——那边是毫秒对微秒的差距。
第二段:把语句缓存关掉,再跑同样的绑定变量写法,还是慢两成。
这个对照说明「缓存有没有命中」才是关键,跟 SQL 怎么写关系不大。
换到 Oracle 上,这就是「共享池够不够」的问题。
第三段:缓冲池命中率的表。
容量 200 块时命中率只有两成多,1000 块时到八成,再往上加几乎不动。
这条曲线就是 V$DB_CACHE_ADVICE 想告诉你的东西。
注意这张表里的访问是【按热点分布】的:八成访问集中在 500 个块里。
如果访问完全均匀,要想高命中率,容量就得接近总块数。
所以「命中率多少算够」没有统一答案,取决于你的数据访问有没有热点。
第四段:日志缓冲区小是有道理的,不是 Oracle 抠门。
它就是个周转站,内容一直在被 LGWR 搬走。
30 秒实验
- 把
hit()里的热点比例从 0.8 改成 0.5(更均匀),重跑。 看看同样的容量下命中率掉多少——这就是「访问模式」对命中率的影响。 - 把
hit()的访问范围从 5000 扩到 20000,重跑。 同样的容量,命中率会明显下降——库里数据变多了,内存没变,就是这个后果。 - 把第一段的查询次数从 5000 加到 20000,看两个耗时的比值有没有变化。 如果比值稳定,说明它就是每条 SQL 的固定开销。
常见误区
- 「命中率越高越好」。 一条全表扫描能把命中率刷到 99%。 命中率低说明内存不够,命中率高不说明快。
- 「
SGA_TARGET设得越大越好」。 物理内存是有限的。 SGA 加上 PGA 超过物理内存,就开始换页,性能断崖式下跌。 - 「共享池报
ORA-04031就是共享池太小」。 有时是【碎片】—— 共享池里有很多小块的空洞,加起来够但连不成一大块。 这种情况先ALTER SYSTEM FLUSH SHARED_POOL试试,不行再加。 - 「日志缓冲区调大能提高并发」。 反了。它大了提交更慢。提交慢先看磁盘,不是看这个参数。
自测三问(点开对答案)
1. 硬解析和软解析差在哪一步? —— **差在优化器**:硬解析要生成执行计划,软解析直接复用共享池里的旧计划。 2. 为什么「用绑定变量」能减少硬解析? —— **因为共享池按 SQL 文本匹配。** 文本一样才复用,**字面量不同就是不同的文本。** 3. 缓冲池的「计数」是干什么的? —— **防止全表扫描把缓存冲掉。** 只读一次的块计数低,留在冷端,很快被淘汰。 4. SGA 调优什么时候该停手? —— **建议视图里曲线变平的时候。** 再加内存收益接近 0,但风险不小。下一篇讲排序区。它在 PGA 里,不在 SGA 里,所以调法完全不同。 这一篇讲的「命中率」在排序区里对应的是「内存排序 vs 磁盘排序」的比例—— 掉到磁盘上去,慢的就不是一两成,是一个数量级。
- ☐ (本篇完,下一篇 14.3 排序区优化)
相关笔记
- 调整初始化参数
- 逻辑存储结构
- Oracle 服务器结构
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引