🏺 第14章 Oracle 系统调优
📄 一句话概括

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

  1. 把 hit() 里的热点比例从 0.8 改成 0.5(更均匀),重跑。 看看同样的容量下命中率掉多少——这就是「访问模式」对命中率的影响。
  2. 把 hit() 的访问范围从 5000 扩到 20000,重跑。 同样的容量,命中率会明显下降——库里数据变多了,内存没变,就是这个后果。
  3. 把第一段的查询次数从 5000 加到 20000,看两个耗时的比值有没有变化。 如果比值稳定,说明它就是每条 SQL 的固定开销。

常见误区

  1. 「命中率越高越好」。 一条全表扫描能把命中率刷到 99%。 命中率低说明内存不够,命中率高不说明快。
  2. 「SGA_TARGET 设得越大越好」。 物理内存是有限的。 SGA 加上 PGA 超过物理内存,就开始换页,性能断崖式下跌。
  3. 「共享池报 ORA-04031 就是共享池太小」。 有时是【碎片】—— 共享池里有很多小块的空洞,加起来够但连不成一大块。 这种情况先 ALTER SYSTEM FLUSH SHARED_POOL 试试,不行再加。
  4. 「日志缓冲区调大能提高并发」。 反了。它大了提交更慢。提交慢先看磁盘,不是看这个参数。
自测三问(点开对答案) 1. 硬解析和软解析差在哪一步? —— **差在优化器**:硬解析要生成执行计划,软解析直接复用共享池里的旧计划。 2. 为什么「用绑定变量」能减少硬解析? —— **因为共享池按 SQL 文本匹配。** 文本一样才复用,**字面量不同就是不同的文本。** 3. 缓冲池的「计数」是干什么的? —— **防止全表扫描把缓存冲掉。** 只读一次的块计数低,留在冷端,很快被淘汰。 4. SGA 调优什么时候该停手? —— **建议视图里曲线变平的时候。** 再加内存收益接近 0,但风险不小。

下一篇讲排序区。它在 PGA 里,不在 SGA 里,所以调法完全不同。 这一篇讲的「命中率」在排序区里对应的是「内存排序 vs 磁盘排序」的比例—— 掉到磁盘上去,慢的就不是一两成,是一个数量级。

相关笔记