SGA 是一块共享内存,里面最大的那块叫数据库缓冲池,活的只有一件:把读过的块留在内存里。 命中率不是越高越好,它取决于「访问有没有局部性」——这一篇用一个 128 块的模拟实测。
前三篇讲的是「存」,这一篇讲「算」。 SGA/PGA/后台进程是第 14 章(系统调优)的地基—— 那一章调的每一个参数,都落在这里的某一块内存上。 这一篇不背参数名。 参数会随版本变,但「缓冲池是怎么工作的」不会变。 先把机制搞懂,参数只是旋钮。
一、SGA 里那几个池
| 池 | 装什么 | 调小了会怎样 |
|---|---|---|
| 共享池 | SQL 文本、执行计划、数据字典缓存 | 硬解析变多,CPU 升高 |
| 数据库缓冲池 | 数据块(读过的、改脏的) | 物理读变多,I/O 升高 |
| 日志缓冲区 | 还没写进重做日志的改动 | 提交变慢(要等写盘) |
| 大池 | RMAN 备份、并行查询的缓冲区 | 备份变慢 |
| Java 池 | JVM 里的 Java 对象 | Java 存储过程跑不动 |
共享池和缓冲池是最常被提起的两个,但它们管的事完全不同: 共享池管「这条 SQL 怎么执行」,缓冲池管「这块数据在不在内存」。 一个查得慢,先分是哪一种: 同一条 SQL 反复硬解析 → 共享池问题;同一条 SQL 执行计划没变但物理读很高 → 缓冲池问题。
二、PGA:每个进程自己的一块
PGA 不共享。 一个会话连上来,服务进程就有自己的一块 PGA, 排序区、游标状态、绑定变量都在这儿。 所以「人多就吃内存」主要吃的是 PGA:一千个会话,每个排序区几 MB,加起来就是几个 GB。 排序在 PGA 里做不完,就会落到临时表空间——这是「磁盘排序」,比内存排序慢一两个数量级。 14.3 节讲排序区优化,讲的就是怎么少落盘。
三、缓冲池怎么工作:LRU
缓冲池是一堆「块槽位」。 要读一个块,先看它在不在槽里: 在就是命中(不读盘),不在就读盘并放进来。 池满了要踢一个出去。踢谁? 按「最久没被用过的先踢」(LRU)—— 因为最近用过的,接下来还可能用;很久没用的,多半不会用了。 Oracle 比这复杂一点:它分「热端」和「冷端」,还有全表扫描用的 LRU 链, 防止一次大查询把整个池子冲干净。 但基本思想就是这个 LRU。
四、commit 到底写了什么
提交时,LGWR 把日志缓冲区的内容写到重做日志文件,然后返回成功。 它不等数据块落盘。 所以 commit 很快,但「快」是有前提的: 日志写是顺序写(append 到文件尾),数据写是随机写。 顺序写快得多,这就是「先写日志」这个设计的收益。 这条因果贯穿第 7 章和第 13 章: 日志是用来「重做」的,不是用来「回滚」的;回滚靠的是 undo(8.5 节)。
五、动手 Demo:128 块的模拟盘 + LRU 缓存:命中率是量出来的
不去猜「缓冲池多大合适」,直接写一个模型跑。 128 个块的盘、N 个槽位的池、LRU 淘汰,跑 500 次访问,数命中。 关键是访问序列做了两版: 一版八成落在 16 个热块上(有局部性),一版均匀撒在 128 块上(没局部性)。 两条曲线一对比,「命中率取决于什么」就清楚了。
import random
# 128 个块的模拟盘;缓冲池放 N 块;LRU 淘汰
def sim(cache_size, accesses):
cache, reads, hits = [], 0, 0
for blk in accesses:
if blk in cache:
cache.remove(blk); cache.append(blk); hits += 1
else:
reads += 1
if len(cache) >= cache_size:
cache.pop(0)
cache.append(blk)
return hits, reads
# 访问序列:八成落在 16 个热块上(有局部性)
random.seed(7)
seq = [random.randrange(16) if random.random() < 0.8 else random.randrange(128)
for _ in range(500)]
for cs in (4, 8, 16, 32, 64, 128):
h, r = sim(cs, seq)
print(cs, h, r, 100.0 * h / (h + r))
实跑输出:
--------------------------------------------------------------------
第九课:Oracle 服务器结构——SGA 是那块「别每次都读盘」的缓存
--------------------------------------------------------------------
**SGA(系统全局区)是一块共享内存,所有会话共用。**
**它里面最大的那块叫缓冲池,干的活只有一件:**
**把读过的块留在内存里。**
**这一节不背参数,做一件事:**
**自己写一个带缓存的小模拟,量出「命中率」到底受什么影响。**
先把模型说清楚
--------------------------------------------------------------------
**假设盘上有 128 个块。**
**缓冲池只能放 N 个块(N 远小于 128)。**
**每次要读一个块:池里有就命中(不读盘),没有就读盘并把它放进来。**
**池满了要踢一个出去——按「最久没用过的先踢」,也就是 LRU。**
缓存越大,命中率越高——但不是线性
--------------------------------------------------------------------
访问 **500 次**,其中**八成落在 16 个热块**上(模拟「常用数据」):
缓存块数 命中 读盘 命中率
----------------------------------------------------------
4 84 416 16.8%
8 172 328 34.4%
16 312 188 62.4%
32 408 92 81.6%
64 425 75 85.0%
128 426 74 85.2%
**从 4 块加到 16 块,命中率涨得最快;再往上加,收益越来越小。**
**这就是「缓冲池不是越大越好」的量化版本:**
**小的时候缺的是「热块放不下」,大的时候缺的是「冷块本来就该读盘」。**
换成均匀访问,曲线立刻变
--------------------------------------------------------------------
同样 500 次,但**均匀撒在 128 个块上**(没有热块):
缓存块数 命中 读盘 命中率
----------------------------------------------------------
4 15 485 3.0%
8 30 470 6.0%
16 65 435 13.0%
32 121 379 24.2%
64 235 265 47.0%
128 374 126 74.8%
**缓存 64 块(占一半)也只有五成左右命中。**
**因为每次访问都有一半概率落在池外,跟池子多大关系不大。**
**结论:缓冲池的命中率取决于「访问有没有局部性」,不取决于池子大小。**
**Oracle 调优里那句「看 buffer cache 命中率」,看的就是这件事。**
真库上怎么查
--------------------------------------------------------------------
[Oracle 原码 · 本沙箱不执行]
| SELECT name, value FROM v$sysstat
| WHERE name IN ('session logical reads', 'physical reads');
|
| -- 命中率 = 1 - physical reads / session logical reads
**`session logical reads` 是「一共要了多少块」,`physical reads` 是「其中多少次真去读了盘」。**
**两者一减,就是缓存替你挡掉的活。**
**这个比值一般要在 90% 以上,低了说明内存不够或者 SQL 在扫全表。**
后台进程:谁在替你干活
--------------------------------------------------------------------
SQL> select p, job from bg
P JOB
---------------------------------------------------------------
DBWn 数据库写进程:把缓冲池里改脏的块刷到数据文件
LGWR 日志写进程:把日志缓冲区的内容写到重做日志(提交时必须写)
CKPT 检查点:记下「到这儿为止都刷干净了」,让恢复有起点
SMON 系统监控:实例恢复、清理临时段
PMON 进程监控:清理异常死掉的会话,释放它的锁
ARCn 归档进程:日志组切换时把旧日志拷出去(归档模式才有)
已选择 6 行
**记住一条因果:**
**提交(commit)只保证「日志写到了盘」,不保证「数据写到了盘」。**
**所以提交很快。数据什么时候落盘,由 DBWn 和 CKPT 决定。**
**第 13 章讲事务、第 7 章讲日志,根子都在这句话上。**
有局部性那一组:缓存从 4 块加到 16 块,命中率从不到两成跳到六成; 再加到 64 块,只涨到八成五,然后基本不动了。 这就是「边际收益递减」的量化版。 均匀访问那一组:缓存 64 块(占一半)也只有四成七, 缓存 128 块(全放得下)才到七成五—— 因为 128 块全放得下时还不到 100%,说明前面已经读过的块被踢出去过。 LRU 在随机访问下几乎没用。 结论不是「缓存没用」,而是「缓存的收益来自重复访问」。 所以调优的顺序应该是:先看访问有没有局部性(SQL 是不是在扫全表),再看池子够不够大。 反过来做,只会白加内存。 这段模拟没有考虑「改脏的块要写回」, 真库上还有 DBWn 刷盘、检查点、全表扫描的冷端保护—— 但「命中率由局部性决定」这个结论不受影响。
30 秒实验
- 把热块范围从 16 改成 4(局部性更强),再看曲线。命中率会在很小的缓存下就冲到很高。
- 把访问次数从 500 加到 5000,看命中率变不变。基本不变——命中率是「比例」,和跑多久无关。
- 把 LRU 换成 FIFO(先进先出),再跑一次。 有局部性时差别不大,均匀访问时 FIFO 略差。这就是「为什么用 LRU 不用 FIFO」的量化解释。
常见误区
- 把「命中率 99%」当成健康指标。 一条全表扫描的 SQL 能把命中率拉得很高, 但它其实是最慢的那种查询。 命中率要结合「逻辑读总量」一起看。
- 以为 commit 之后数据就在数据文件里了。 commit 只保证日志落盘, 数据块什么时候写由 DBWn 决定。
- 以为加大缓冲池就能解决慢查询。 如果慢在「扫全表」或者「没索引」, 加内存只是让它多缓存一些本来就用不上的块。
自测三问(点开对答案)
1. SGA 和 PGA 最本质的区别是什么? —— **共享还是私有。** SGA 所有会话共用一块,PGA 每个服务进程一块。 **所以人一多,SGA 不变、PGA 线性涨。** 2. 为什么 commit 很快? —— **因为它只写日志(顺序写),不写数据文件(随机写)。** **日志顺序 append,几毫秒就完;数据块攒着让 DBWn 批量刷。** 3. 命中率低,一定是内存不够吗? —— **不一定。** 如果是「每次访问都落在不同的块上」(没有局部性),**池子再大也救不了**。 先看 SQL 是不是在扫全表。下一篇是本章最后一篇:数据字典——库自己维护的那本账,
user_/all_/dba_ 三层前缀各看什么。DBA 日常干的活有一大半就是查它。
- ☐ (本篇完,下一篇 2.5 数据字典)