排序区在 PGA 里,每个会话一份。
内存里排不下,就拆成几段写进临时表空间,再归并读回来——慢一个量级。
所以调排序区只有一件事要做:让「落盘排序」的比例降下来。
V$SYSSTAT 里的 sorts (disk) 就是那个指标。
14.2 讲的是 SGA(共享内存)。这一篇讲 PGA(每会话私有内存),两边调法完全不同。
这一篇还会把「什么操作会排序」列全。
很多人以为只有 ORDER BY 要排序,其实 GROUP BY、DISTINCT、UNION、建索引、
排序合并连接,全都要。
一、排序区在哪:PGA,不在 SGA
| SGA | PGA | |
|---|---|---|
| 谁用 | 所有进程共享 | 每个会话私有 |
| 装什么 | 数据块、SQL、日志缓冲 | 排序区、哈希区、会话变量 |
| 参数 | SGA_TARGET |
PGA_AGGREGATE_TARGET |
这个区分很关键,因为它决定了「加内存加到哪」。
排序慢,加 SGA 没用,得加 PGA。
反过来,缓存命中率低,加 PGA 也没用。
PGA 的总量是 PGA_AGGREGATE_TARGET,但它不是平均分。
Oracle 是按「当前有多少个会话在做排序」动态分的——
十个会话在排序,每人一份;只有一个在排序,它能用得多一些。
这种模式叫「自动 PGA 管理」,对应参数 WORKAREA_SIZE_POLICY=AUTO。
二、什么操作会吃排序区
下面这些都会排序,而且很多是「偷偷排的」:
ORDER BY:最明显的一种。
GROUP BY 和 DISTINCT:要分组或去重,就得先排好(或者用哈希,看优化器选哪个)。
UNION/INTERSECT/MINUS:集合运算要去重,去重就要排。
建索引:索引本身就是按顺序排好的结构。
排序合并连接:两张表各自排好再对撞。
「偷偷排」最典型的是 DISTINCT。
很多人写 SELECT DISTINCT 只是为了「去重」,没意识到它在排序。
如果只是想去重、不在乎顺序,GROUP BY 可能更划算——优化器有机会改用哈希聚合,不落盘。
还有一个容易忽略的:ORDER BY 后面跟了索引列,也可能不排序。
如果索引的顺序正好和 ORDER BY 一致,优化器会直接顺着索引读,执行计划里连 SORT 都没有。
所以「排序慢」的一个解法是「建个索引让它不用排」。
三、内存排序 vs 磁盘排序
排序区够大,全在内存里排完,这叫【最优执行】(optimal)。 不够大,就先读一部分、排好、写进临时表空间,这叫【一次通过】(onepass)。 再不够,同样的数据要读写好几遍,叫【多次通过】(multipass)。
| 模式 | 发生了什么 | 代价 |
|---|---|---|
| optimal | 全在内存里排完 | 最快,无 IO |
| onepass | 排一段、写一段、再归并 | 多一轮临时表空间读写 |
| multipass | 同一批数据反复读写 | 慢一个数量级 |
V$SQL_WORKAREA 里能直接看到每条 SQL 的 EXECUTIONS、OPTIMAL_EXECUTIONS、
ONEPASS_EXECUTIONS、MULTIPASS_EXECUTIONS。
如果 MULTIPASS 不是 0,就该加 PGA 了。
这个视图比「看总体命中率」精确得多,因为它能看到是【哪条 SQL】在落盘。
四、专用模式下的调整
专用服务器模式下,每个会话一个进程、一份 PGA,排序区是私有的。
调法很简单:
PGA_AGGREGATE_TARGET 定总量,WORKAREA_SIZE_POLICY 设成 AUTO,完事。
千万别手动设 SORT_AREA_SIZE。
这个参数是「每个会话最多用多少」,它是按会话上限算的,不是按总量。
设成 100M,一百个会话同时排序就是 10G——机器直接 OOM,而且是在最忙的时候。
手动模式只有一个场景值得用:
某个批处理任务的单个排序特别大,自动管理给不够,又不想把整个 PGA_AGGREGATE_TARGET 抬高。
做法是在那个会话里 ALTER SESSION SET WORKAREA_SIZE_POLICY=MANUAL 再设 SORT_AREA_SIZE,
影响范围只在这一个会话。
五、共享模式下的调整
共享服务器模式下,会话和进程是分离的,排序区不能放在 PGA 里——
因为进程是共用的,下一个人接着用的时候,上一个会话的数据还在里面。
所以这时候排序区从【大池】里分配,参数是 SORT_AREA_SIZE 和 SORT_AREA_RETAINED_SIZE。
SORT_AREA_RETAINED_SIZE 管的是「排完之后保留多少」。
排序做完,结果集还要一行行返回给客户端,这部分内存就是它管的。
设成 0 就是「排完立刻释放」,每次取下一行都要重新读临时段——
这个坑很隐蔽,现象是「排序本身不慢,但取结果特别慢」。
不过说实话:现在几乎没有新系统用共享服务器。
它的好处是「连接数很多时省内存」,坏处是「一个会话的慢 SQL 会拖累同进程的其他人」。
连接数多的问题,现在一般用连接池解决,不用共享服务器。
六、怎么判断排序区够不够
第一个指标:V$SYSSTAT 里的 sorts (memory) 和 sorts (disk) 的比例。
落盘比例超过 5% 就该查了。
注意这是个累计值,要从两次采样相减才看得出「现在」的情况。
第二个指标:V$PGASTAT 里的 cache hit percentage。
这个数是「有多少工作区是在内存里完成的」,90% 以上算健康。
同一张视图里的 over allocation count 更直接:它数的是「内存不够、被迫缩小工作区」的次数,
这个数一直在涨就是明显不够。
第三个:V$PGA_TARGET_ADVICE。
和 14.2 讲的 V$DB_CACHE_ADVICE 一个思路:给一张「如果 PGA 是 X,命中率会是多少」的预测表。
这张表的价值在于告诉你曲线在哪变平。
七、动手 Demo:内存排序和落盘排序,实测差多少
这一篇的 Demo 是自己实现一次外排:把数据分块排好、写进临时文件,再归并读回来。 这正是 Oracle 在排序区不够时做的事,只是它写的是临时表空间。 两次结果会用断言比一遍,确认「快的那条路」和「慢的那条路」答案一样—— 否则比的就不是快慢,是对错。
# 内存排序 vs 落盘排序(外排 + 归并)
import heapq, os, random, tempfile, time
d = os.path.join(tempfile.gettempdir(), "sort63")
os.makedirs(d, exist_ok=True)
rnd = random.Random(9)
rows = [rnd.randint(1, 10 ** 9) for _ in range(400000)]
def ext_sort(rows, cap):
runs = []
for i in range(0, len(rows), cap):
chunk = sorted(rows[i:i + cap])
fp = os.path.join(d, "run%03d.txt" % len(runs))
with open(fp, "w") as f:
f.write("\n".join(str(x) for x in chunk))
runs.append(fp)
def gen(fp):
with open(fp) as f:
for ln in f:
yield int(ln)
out = list(heapq.merge(*[gen(fp) for fp in runs]))
for fp in runs:
os.remove(fp)
return out
t0 = time.perf_counter()
ok = sorted(rows)
t1 = time.perf_counter()
got = ext_sort(rows, 40000) # 每段 4 万,共 10 段
t2 = time.perf_counter()
assert ok == got
print("内存排序 %.3f 秒,落盘归并 %.3f 秒" % (t1 - t0, t2 - t1))
实跑输出:
--------------------------------------------------------------------
第六十三课:排序区——放不下就落盘,慢一个量级
--------------------------------------------------------------------
**排序要么在内存里做,要么拆成几段、各自排好写盘,再归并读回来。** **后者叫外排,慢很多。**
**这一课实测这个差距,并且测出「内存要多大的排序区才够」。**
一、内存排序 vs 落盘排序
--------------------------------------------------------------------
**400000 条整数:**
方式 耗时
-----------------------------
全在内存里排 0.105 秒
分成 10 段落盘再归并 0.764 秒
已选择 2 行
**落盘这一路慢 7.3 倍。** **两次结果完全一样(脚本里断言过),差的全是 IO 和归并。**
**Oracle 里这个差距只会更大。**
**因为它的临时表空间读写在磁盘上,而这里的文件大概率还在系统缓存里**——
**换句话说,上面这个倍数是个【乐观】的下限。**
二、排序区给小了,落盘段数会翻倍
--------------------------------------------------------------------
排序区(条) 落盘段数 耗时
------------------------------
200000 2 0.721 秒
50000 8 0.747 秒
10000 40 0.870 秒
已选择 3 行
**排序区从 20 万降到 1 万,段数从 2 段涨到 40 段,耗时跟着涨。**
**注意涨得不是线性的**——归并的代价和段数有关,也和每段大小有关。
**Oracle 里这个「段数」有个名字:`sorts (disk)` 计数器涨一格就是落盘一次。**
**查 `V$SYSSTAT` 里 `sorts (memory)` 和 `sorts (disk)` 的比例**,**就知道排序区够不够。**
**落盘比例高,就该加大 `PGA_AGGREGATE_TARGET`。**
三、真正吃排序区的操作,比你想的多
--------------------------------------------------------------------
ORDER BY、GROUP BY、DISTINCT 最常见的三种
UNION、INTERSECT、MINUS 要去重,就得排
排序合并连接(SORT MERGE JOIN) 两张表先各自排好再对撞
建索引、ANALYZE 索引本身要按顺序建
**所以「一条查询慢」不一定是它没走索引,也可能是它在偷偷排序。**
**`EXPLAIN PLAN` 里看到 `SORT ORDER BY` 就要留意,看到 `SORT ORDER BY STOPKEY` 反而还好**——
**`STOPKEY` 意思是「只要前 N 行」,排到 N 就停。**
四、专用模式 vs 共享模式:排序区从哪来
--------------------------------------------------------------------
**专用服务器(dedicated):一个会话一个进程,排序区在 PGA 里,私有的。**
**共享服务器(shared):会话和进程是分离的,排序区只能用 large pool 里那块公共的。**
**所以共享服务器模式下,`SORT_AREA_SIZE` 这类参数的意义变了**——
**它不再按会话分配,而是抢公共池。**
**现在几乎没人用共享服务器了,知道有这回事就行。**
五、调排序区的正确姿势
--------------------------------------------------------------------
**第一步:设 `PGA_AGGREGATE_TARGET`(比如 2G),把 `WORKAREA_SIZE_POLICY` 设成 `AUTO`。**
**第二步:什么都不用做。**
**Oracle 会按「当前有多少个排序在跑」自动分配,单个排序最多能用总目标的 5%。**
**第三步:看 `V$PGASTAT` 里的 `cache hit percentage`。**
**这个数在 90% 以上就不用管了;低于 80% 才考虑加内存。**
**别拿 `SORT_AREA_SIZE` 硬调**——
**那是手动管理模式,一个会话就能吃掉一大块内存,几十个会话一起来就 OOM。**
第一段:40 万条整数,全在内存里排 0.1 秒级,分成 10 段落盘再归并要 0.7 秒级。
差七倍上下。
这个倍数要打个折扣看:这里的「磁盘」大概率还在操作系统的文件缓存里,没真的落到盘上。
Oracle 的临时表空间是实打实的读写,所以真实差距只会更大。
换句话说,七倍是【乐观】的下限。
第二段:排序区从 20 万条降到 1 万条,段数从 2 段涨到 40 段,耗时跟着涨。
注意涨得不夸张——因为归并是 k 路归并,段数多了每一路反而更短。
真正贵的是「每段都要写一次盘、读一次盘」这个固定成本。
这也解释了 Oracle 的一个现象:sorts (disk) 涨了不一定就慢很多,但如果 multipass 出现了,
那就不是几倍的事了。
因为 multipass 是「同一批数据读写好几遍」,成本按遍数翻。
第三段列的那些操作,回到执行计划里都能对应上:
SORT ORDER BY、HASH GROUP BY、SORT UNIQUE、MERGE JOIN SORT。
看到 SORT 开头的,就知道它在吃排序区。
30 秒实验
- 把
cap从 40000 改成 400000(一段搞定),再改成 4000(100 段),各跑一次。 看耗时怎么变——这就是「排序区大小」对落盘排序的影响曲线。 - 把数据量从 40 万降到 4 万,重跑。 倍数的差距会缩小——因为固定开销(建文件、开文件)摊到了更少的数据上。
- 把
ext_sort里的归并改成「两两归并、多轮」(外部归并排序的经典做法),再测。 段数多的时候它比一次 k 路归并更省内存。
常见误区
- 「只有
ORDER BY才排序」。GROUP BY、DISTINCT、UNION、建索引、 排序合并连接全都要。 - 「
SORT_AREA_SIZE调大就能加快排序」。 自动管理下这个参数根本不生效。 手动模式下它按会话算,几十个会话一起来就 OOM。 - 「排序慢就加 SGA」。 排序区在 PGA 里。加错地方等于没加。
- 「
sorts (disk)有值就是有问题」。 大查询落盘很正常。 要看比例,还要看有没有multipass。
自测三问(点开对答案)
1. 排序区在 SGA 还是 PGA? —— **PGA。** 每个会话私有一份,由 `PGA_AGGREGATE_TARGET` 管总量。 2. `optimal`、`onepass`、`multipass` 差在哪? —— **全内存、落一次盘、反复落盘。** 到 `multipass` 就慢一个数量级了。 3. 为什么不能手动把 `SORT_AREA_SIZE` 设得很大? —— **它是按会话算的上限。** 一百个会话同时排序,内存需求是它的 100 倍。 4. 判断排序区够不够,看哪两个地方? —— **`V$SYSSTAT` 的 `sorts (disk)` 比例,和 `V$PGASTAT` 的 `cache hit percentage`。**第 14 章到此结束。参数、SGA、排序区,三块内存调完,接下来该看 SQL 本身了。 下一篇进第 15 章:SQL 调优——从「加内存」转向「改写语句、加索引、看执行计划」。
- ☐ (本篇完,下一篇 15.1 常规 SQL 语句优化)
相关笔记
- 系统全局区(SGA)优化
- 索引对象
- PL/SQL 游标
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引