序列是一个独立于任何表的发号器:seq.NEXTVAL 取下号,seq.CURRVAL 读当前号。
它保证「唯一」和「递增」,但不保证「连续」。
跳号来自缓存:一次向数据字典预支一段号,实例崩了没用完的那段就永久丢了。
多实例(RAC)还会乱序——每个实例各管一段,谁也不等谁。
第 9 章讲约束时提过主键要「无意义的代理键」。序列就是造代理键的工具。
12c 之后还有 IDENTITY 列,本质是「表 + 序列」打包,这一篇末尾会说。
序列和表的关系是「没有关系」。
它不绑定表、不回滚、不参与事务——
这一点和 AUTO_INCREMENT 的直觉不一样,是很多坑的根源。
一、序列是什么:一个不绑定表的计数器
CREATE SEQUENCE emp_seq START WITH 1 INCREMENT BY 1;
建完就能用:INSERT INTO emp (empno, ...) VALUES (emp_seq.NEXTVAL, ...);
序列最反直觉的三条:
① 不参与事务——ROLLBACK 不会把号还回去;② 不保证连续——缓存、重启、回滚都会留空洞;
③ 不属于任何表——删表不影响序列,删序列也不会让表出错(只是以后插不进去)。
为什么「不参与事务」是设计而不是缺陷?
因为如果序列要跟着事务回滚,就得对序列加锁,那所有并发插入都串行化了,
发号器就变成了整个库的瓶颈。
所以 Oracle 选了「宁可跳号,也要并发」。
二、创建序列:六个参数各管什么
| 参数 | 作用 | 常见取值 |
|---|---|---|
START WITH |
起始值 | 1 |
INCREMENT BY |
步长,可为负 | 1 |
MAXVALUE / NOMAXVALUE |
上限 | 默认 10^28 |
CACHE n / NOCACHE |
一次预支几个号 | 20(默认) |
CYCLE / NOCYCLE |
到顶是否绕回 | NOCYCLE |
ORDER / NOORDER |
是否按请求顺序发号 | NOORDER |
CACHE 是最需要理解的一个。
默认 20:第一次 NEXTVAL 时把字典里的 last_number 推进 20,内存里留着这 20 个号慢慢发。
好处是 20 个号只写一次字典——高并发下这一步能省掉大量 SQ 队列等待。
CACHE 给小了的代价:
每次缓存用完都要写一次字典,SQ 争用上来了。
给大了的代价:
实例崩一次跳的号更多。
所以「缓存多大」是个取舍,不是越大越好。
ORDER 只在 RAC 下有意义:
它让号按「请求顺序」发,代价是实例之间要通信。
单实例不需要它。
三、管理序列:NEXTVAL 与 CURRVAL 的规矩
NEXTVAL 取下一个号,CURRVAL 读「本会话最后一次取到的号」。
CURRVAL 在同一个会话里没取过号就报 ORA-08002——
它不是「序列的当前值」,是「我的当前值」。
NEXTVAL 不能用在 WHERE 子句、CHECK 约束、子查询里。
它只能出现在 INSERT ... VALUES、UPDATE ... SET、SELECT 列表这些地方。
原因是「发号」有副作用,不能在一个可能被执行多次的位置上做。
一个实用细节:会话之间 CURRVAL 互不影响。
A 会话取到 100,B 会话取到 200,A 的 CURRVAL 还是 100。
所以「取号后马上用它做主键」在同一个会话里是安全的,
跨会话就不行——那得靠 RETURNING 或 IDENTITY。
改序列:ALTER SEQUENCE emp_seq INCREMENT BY 5;
START WITH 不能改——
要改起始值只能删了重建,或者先 ALTER ... INCREMENT BY 调到位再调回来。
后者是常见技巧,但要注意它会让号跳一段。
四、跳号与乱序:两个真实场景
场景一:缓存没用完,实例重启。
缓存 20,字典记到 20,实际只发到 3 号,然后实例崩了。
重启后从 21 开始——中间 17 个号从来没被发出去过,但也不会再发。
场景二:RAC 两个实例同时发号。
实例 A 预支 1~10,实例 B 预支 11~20。
A 发了 1,B 发了 11 并先落库,A 再发 2 后落库。
表里按写入顺序看就是 11、2——号变小了。
这两件事合起来说明:序列号是「唯一标识」,不是「流水号」。
要「连续且有序」,那不该用序列,该用一张带行锁的编号表——代价是并发能力归零。
所以「能不能接受跳号」这个问题的本质是「能不能接受串行」。
12c 之后有 IDENTITY 列:
id NUMBER GENERATED BY DEFAULT AS IDENTITY——建表时一句搞定,不用自己建序列。
它底下还是一个序列,所以跳号、乱序的规矩一模一样,只是少写一行 DDL。
五、动手 Demo:把序列的缓存语义做出来,看号怎么跳
沙箱里没有 Oracle 序列,但它的语义很具体:先把字典里的 last_number 推进一段,
再在内存里慢慢发。
我照这个语义写了一个类,字典就是一张表。
要观察的就是两件事:字典什么时候跳、号什么时候断。
这两个数是能算准的,不是估的。
# 序列:缓存段一发出去,跳号和乱序就都来了
import sqlite3
class Seq(object):
def __init__(self, c, name, cache=20):
self.c, self.name, self.cache, self.buf, self.curr = c, name, cache, [], None
c.execute("create table if not exists seq_store (name text primary key, last_number int)")
if not c.execute("select 1 from seq_store where name = ?", (name,)).fetchone():
c.execute("insert into seq_store values (?, 0)", (name,))
c.commit()
def last(self):
return self.c.execute("select last_number from seq_store where name = ?",
(self.name,)).fetchone()[0]
def nextval(self):
if not self.buf:
base = self.last()
self.c.execute("update seq_store set last_number = ? where name = ?",
(base + self.cache, self.name))
self.c.commit()
self.buf = list(range(base + self.cache, base, -1))
self.curr = self.buf.pop()
return self.curr
c = sqlite3.connect(":memory:")
s = Seq(c, "seq_nocache", cache=1)
print("NOCACHE 连取五次:", [s.nextval() for _ in range(5)])
print(" 字典:", s.last())
c = sqlite3.connect(":memory:")
s = Seq(c, "seq_cache", cache=20)
print("CACHE 20 连取三次:", [s.nextval() for _ in range(3)])
print(" 字典:", s.last(), "(已经跳到 20)")
last = s.curr
s2 = Seq(c, "seq_cache", cache=20)
print("模拟重启后第一号:", s2.nextval(), "中间空了", s2.curr - last - 1, "个号")
实跑输出:
--------------------------------------------------------------------
第四十六课:序列对象——发号器,以及它为什么会跳号
--------------------------------------------------------------------
**序列(sequence)是「不绑定表的主键生成器」。**
**`emp_seq.nextval` 取下一个号,`emp_seq.currval` 读当前号,两个函数而已。**
**沙箱里没有 Oracle 序列,但它的语义不复杂,我用一张字典表加一个类照原样做一遍。**
**重点不是语法,是「为什么号会跳」**——这一条在生产上是真会被人追问的。
一、先看 NOCACHE:每个号都落字典,一个都不跳
--------------------------------------------------------------------
**连取五次:1、2、3、4、5**
**字典里的 `last_number` 现在是 5**——每取一个号,字典就更新一次。
**所以 `NOCACHE` 不跳号,代价是「每次 `nextval` 都要写一次数据字典」。**
**高并发下这就是热点争用,`SQ` 队列等待就是它闹的。**
二、再看 CACHE 20:字典一步跳 20,内存里慢慢发
--------------------------------------------------------------------
**连取三次:1、2、3**
**但字典里的 `last_number` 已经是 20 了。**
**这就是缓存的意思:**
**一次向字典预支 20 个号,字典记到 20,剩下的 17 个号放在内存里等着发。**
**省的是「写字典」的次数:20 个号只写一次。**
三、跳号是怎么来的:缓存里的号没发完,实例崩了
--------------------------------------------------------------------
**崩溃前发出的最后一个号是 3,重启后第一次 `nextval` 拿到 21。**
**中间空了 17 个号。**
**这 17 个号不是「丢了」,是「从来没被发出去过」。**
**字典里已经记到 20,内存里那 17 个没人知道了,只能从 21 继续。**
**所以 Oracle 序列只保证「唯一」和「递增」,不保证「连续」。**
**拿序列当单号、当流水号的人,早晚要在审计面前解释这件事。**
四、CYCLE 与 MAXVALUE:到顶了怎么办
--------------------------------------------------------------------
**`MAXVALUE 5` + `CYCLE`,连取六次:1、2、3、4、5、1**
**到 5 之后回头从 1 开始。**
**不加 `CYCLE` 的话,取到上限会报 `ORA-08004`,不会自己绕回去。**
**生产上几乎没人给主键序列加 `CYCLE`**——绕回去就意味着主键冲突。
五、ORDER 与 NOORDER:两个实例同时取号
--------------------------------------------------------------------
SQL> select id, who from o
ID 落库的应用
-------------
11 实例 B
2 实例 A
已选择 2 行
**两个实例各预支一段:A 拿到 1~10,B 拿到 11~20。**
**A 先取,拿到 1;B 后取,拿到 11。**
**但 A 的 2 是第二次 `nextval` 才发出去的,落库时间晚于 B 的 11。**
**所以表里按写入顺序看是 11、2**——**号变小了。**
**单实例不会有这个问题,多实例(RAC)才有:每个实例各管一段缓存,谁也不等谁。**
**要严格按请求顺序发号,得用 `ORDER` 加 `NOCACHE`**——**缓存段一旦发出去,后面就没法再排序了。**
**所以「序列号有跳、有乱」是设计上的取舍,不是 bug。**
**需要「连续且有序」的号,那不该用序列,该用一张行锁表。**
第一段:NOCACHE(每次只预支 1 个)。
连取五次拿到 1 到 5,字典里的 last_number 正好是 5——每取一个号写一次字典,一个都不跳。
代价就是「每次都写字典」,高并发下这就是热点。
第二段:CACHE 20。
只取了三次(1、2、3),但字典已经记到 20 了。
这就是预支:一次向字典要 20 个号,剩下 17 个放在内存里等着发。
省的是「写字典」的次数。
第三段:模拟崩溃。
崩溃前最后发出去的是 3 号,重启后第一次 nextval 拿到 21 号,中间空了 17 个。
这 17 个号不是「丢了」,是「从来没被发出去过」——但也不会再发,所以看起来就是跳号。
第四段:CYCLE 的实测。
MAXVALUE 5 加 CYCLE,连取六次是 1、2、3、4、5、1。
不加 CYCLE 的话,取到上限报 ORA-08004,不会自己绕回去。
生产上几乎没人给主键序列加 CYCLE——绕回去就意味着主键冲突。
第五段:两个实例同时取号。
表里按写入顺序看是 11、2——后发的号先落库,先发的号后落库,主键就不单调了。
这不是 bug,是「缓存段一发出去就没法再排序」的必然结果。
30 秒实验
- 把
cache改成 100,重跑,看跳号从 17 变成多少。算一下:发出去 3 个,剩下的 97 个全丢。 - 给
Seq加一个cycle=True参数,maxv=5,连取八次, 看它是 1、2、3、4、5、1、2、3 还是别的——和 Oracle 的CYCLE行为对一遍。 - 把
insert换成「先开事务、取了号、再rollback」, 看号有没有还回去。 答案是「不还」——这就是「序列不参与事务」。
常见误区
- 「序列保证连续」。 不保证。缓存、重启、回滚都会留空洞。 拿它当单号、当流水号,早晚要在审计面前解释。
- 「
ROLLBACK会把号还回去」。 不会。序列不参与事务,取了就是用掉了。 - 「
CURRVAL是序列的当前值」。 是「本会话最后一次取到的号」。 别的会话取了多少,和你无关。 新会话没取过就报ORA-08002。 - 「
CACHE越大越好」。 越大越省字典写入,但崩一次跳的号也越多。 而且缓存段一发出去就收不回来,ORDER也救不了。
自测三问(点开对答案)
1. 序列的号为什么会跳? —— **缓存**:一次预支一段号写进字典,实例崩了没用完的那段就永久丢了。 **重启、`ROLLBACK`、RAC 预支都会造成跳号。** 2. `CURRVAL` 在什么情况下会报错? —— **本会话还没执行过 `NEXTVAL`**,报 `ORA-08002`。 3. `CACHE 20` 和 `NOCACHE` 的差别在哪? —— **`CACHE 20` 一次向字典预支 20 个号,写一次字典;`NOCACHE` 每取一个号写一次。** **前者快但可能跳号,后者不跳但争用高。** 4. 序列号和「流水号」有什么不一样? —— **序列只保证唯一和递增,不保证连续和有序。** **要连续有序得用带行锁的编号表,代价是没有并发。**第 10 章到此为止。索引、视图、同义词、序列,围着表转的四个对象齐了。 下一篇进第 11 章:表分区与索引分区——把一张大表切成若干块,让它「删得快、查得少」。
- ☐ (本篇完,下一篇 11.1 分区技术简介)
相关笔记
- 索引对象
- 数据完整性和约束性
- 创建数据表
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引