🏺 第10章 其他数据对象
📄 一句话概括

序列是一个独立于任何表的发号器: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 秒实验

  1. 把 cache 改成 100,重跑,看跳号从 17 变成多少。算一下:发出去 3 个,剩下的 97 个全丢。
  2. 给 Seq 加一个 cycle=True 参数,maxv=5,连取八次, 看它是 1、2、3、4、5、1、2、3 还是别的——和 Oracle 的 CYCLE 行为对一遍。
  3. 把 insert 换成「先开事务、取了号、再 rollback」, 看号有没有还回去。 答案是「不还」——这就是「序列不参与事务」。

常见误区

  1. 「序列保证连续」。 不保证。缓存、重启、回滚都会留空洞。 拿它当单号、当流水号,早晚要在审计面前解释。
  2. 「ROLLBACK 会把号还回去」。 不会。序列不参与事务,取了就是用掉了。
  3. 「CURRVAL 是序列的当前值」。 是「本会话最后一次取到的号」。 别的会话取了多少,和你无关。 新会话没取过就报 ORA-08002。
  4. 「CACHE 越大越好」。 越大越省字典写入,但崩一次跳的号也越多。 而且缓存段一发出去就收不回来,ORDER 也救不了。
自测三问(点开对答案) 1. 序列的号为什么会跳? —— **缓存**:一次预支一段号写进字典,实例崩了没用完的那段就永久丢了。 **重启、`ROLLBACK`、RAC 预支都会造成跳号。** 2. `CURRVAL` 在什么情况下会报错? —— **本会话还没执行过 `NEXTVAL`**,报 `ORA-08002`。 3. `CACHE 20` 和 `NOCACHE` 的差别在哪? —— **`CACHE 20` 一次向字典预支 20 个号,写一次字典;`NOCACHE` 每取一个号写一次。** **前者快但可能跳号,后者不跳但争用高。** 4. 序列号和「流水号」有什么不一样? —— **序列只保证唯一和递增,不保证连续和有序。** **要连续有序得用带行锁的编号表,代价是没有并发。**

第 10 章到此为止。索引、视图、同义词、序列,围着表转的四个对象齐了。 下一篇进第 11 章:表分区与索引分区——把一张大表切成若干块,让它「删得快、查得少」。

相关笔记