🏺 第2章 Oracle 体系结构
📄 一句话概括

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 秒实验

  1. 把热块范围从 16 改成 4(局部性更强),再看曲线。命中率会在很小的缓存下就冲到很高。
  2. 把访问次数从 500 加到 5000,看命中率变不变。基本不变——命中率是「比例」,和跑多久无关。
  3. 把 LRU 换成 FIFO(先进先出),再跑一次。 有局部性时差别不大,均匀访问时 FIFO 略差。这就是「为什么用 LRU 不用 FIFO」的量化解释。

常见误区

  1. 把「命中率 99%」当成健康指标。 一条全表扫描的 SQL 能把命中率拉得很高, 但它其实是最慢的那种查询。 命中率要结合「逻辑读总量」一起看。
  2. 以为 commit 之后数据就在数据文件里了。 commit 只保证日志落盘, 数据块什么时候写由 DBWn 决定。
  3. 以为加大缓冲池就能解决慢查询。 如果慢在「扫全表」或者「没索引」, 加内存只是让它多缓存一些本来就用不上的块。
自测三问(点开对答案) 1. SGA 和 PGA 最本质的区别是什么? —— **共享还是私有。** SGA 所有会话共用一块,PGA 每个服务进程一块。 **所以人一多,SGA 不变、PGA 线性涨。** 2. 为什么 commit 很快? —— **因为它只写日志(顺序写),不写数据文件(随机写)。** **日志顺序 append,几毫秒就完;数据块攒着让 DBWn 批量刷。** 3. 命中率低,一定是内存不够吗? —— **不一定。** 如果是「每次访问都落在不同的块上」(没有局部性),**池子再大也救不了**。 先看 SQL 是不是在扫全表。

下一篇是本章最后一篇:数据字典——库自己维护的那本账, user_/all_/dba_ 三层前缀各看什么。DBA 日常干的活有一大半就是查它。

相关笔记