🏺 第11章 表分区与索引分区
📄 一句话概括

分区的日常维护四个动作: ADD(加空分区)、MERGE(合并)、DROP(整块删)、EXCHANGE(整表和分区对调)。 前三个改的是「分区结构」,最后一个改的是「数据放哪」—— EXCHANGE 是加载数据的标准套路,因为它只改字典、不搬数据。

📝 本章和前面几章的关系

11.2 建、11.3 选,这一篇管「建完之后怎么维护」。 分区表的价值八成在维护上——查询优化只是加分项,删历史数据快才是刚需。 这一篇的 Demo 里有一段「口径踩坑」的记录: 同样两万行,DELETE 和 DROP 到底差多少,取决于 fsync 开没开、WHERE 写没写。 这两个前提不说清,测出来的数就是假的。

一、添加分区:ADD PARTITION

ALTER TABLE sale ADD PARTITION p202501 VALUES LESS THAN (DATE '2025-02-01'); 这是「加一个空分区进来」,不搬任何数据,所以不管表多大,都是毫秒级。 两个坑: ① 新分区的上界必须比现有最大分区大,否则报 ORA-14074; ② 有 MAXVALUE 分区的话,新分区加不进去—— 得先 SPLIT PARTITION pmax AT (...) 把 MAXVALUE 拆开。 这也是 INTERVAL 分区比手工 ADD 省事的地方: 它自动处理边界,不用惦记 MAXVALUE 那件事。

二、合并分区:MERGE PARTITIONS

ALTER TABLE sale MERGE PARTITIONS p202411, p202412 INTO p2024q4; 两个分区并成一个,行数相加。 什么时候合并?分区切太细了。 比如按天切了一年,发现「按天查」根本没人用,那就并成按月。 或者历史分区越切越细,但数据已经冷到没人查,合并能少一堆元数据。 代价要记住:合并本身要搬数据。 两个一亿行的分区合并,就是搬两亿行——要空间(合并期间两份都在)、要时间、要 undo。 所以「切细了再合并」是有代价的,别指望随时能反悔。

三、删除分区:DROP 与 TRUNCATE

两个都能清数据,区别在「分区还在不在」: DROP PARTITION 把分区和它的段一起删掉(分区没了); TRUNCATE PARTITION 只清数据,分区结构留着(下个月还能继续用)。 两个都是「整块操作」,不读行、不写行级 undo——这是它们比 DELETE 快几个数量级的根本原因。 但注意:它们也不写行级 undo,所以不能闪回。 DROP PARTITION 之后再想找回数据,只能靠备份或者 FLASHBACK TABLE(需要回收站里还有)。 所以这个动作必须配权限控制,不能人人能敲。

四、并入分区:EXCHANGE PARTITION

ALTER TABLE sale EXCHANGE PARTITION p202503 WITH TABLE stage; 把一张独立表和某个分区【对调】。 关键是「只改数据字典,不搬数据」——所以不管多少行,都是毫秒级。 加载数据的标准套路就是它: ① 建一张普通表 stage,按分区表的结构建;② 往 stage 里灌数据(可以并行、 可以 NOLOGGING、可以失败重来);③ 灌完一句 EXCHANGE 换进去。 为什么绕这一圈?因为「直接往分区表里灌」有两个问题: 灌的过程失败会留半截数据;灌的时候还占着分区表的资源。 用 stage 中转,失败就重灌,成功了再换——这是数据仓库 nightly 加载的标准做法。

五、动手 Demo:加、合、删、换各跑一遍,重点看「删」的两种做法

四个动作在沙箱里都能复现: 加分区 = 建空表;合并 = 两张表并一张;删分区 = DROP TABLE;换分区 = 改名换指向。 重点在第三节。 「DROP 比 DELETE 快多少」这个问题,网上的答案从十倍到一万倍都有—— 因为大家都忘了说两个前提:fsync 开没开、WHERE 写没写。 这一节把两个前提都摆出来,两组数都量。

# 加、合、删、换:四个动作各自改了什么
import sqlite3
c = sqlite3.connect(":memory:")
c.execute("create table p202411 (id int)")
c.executemany("insert into p202411 values (?)", [(i,) for i in range(20000)])
c.commit()

# 加:空分区进来,不搬数据
c.execute("create table p202412 (id int)")
print("加分区后分区数:", c.execute(
    "select count(*) from sqlite_master where type='table' and name like 'p2024%'").fetchone()[0])

# 合:两个并一个,行数相加
c.execute("create table q4 (id int)")
c.execute("insert into q4 select * from p202411")
c.execute("insert into q4 select id + 100000 from p202411")
c.commit()
print("合并后 q4 行数:", c.execute("select count(*) from q4").fetchone()[0])

# 删:整块摘掉,不读行
c.execute("drop table p202412")
print("删分区后分区数:", c.execute(
    "select count(*) from sqlite_master where type='table' and name like 'p2024%'").fetchone()[0])

# 换:独立表换进分区位(这里用改名模拟字典切换)
c.execute("alter table q4 rename to p2024q4")
print("换进来之后:", c.execute(
    "select name from sqlite_master where type='table' and name like 'p2024%'").fetchall())

实跑输出:

--------------------------------------------------------------------
第五十课:管理表分区——加、合、删、换
--------------------------------------------------------------------

  **分区的日常维护有四个动作,每个都有对应的 DDL。**
  **这一篇把它们真跑一遍,重点看「删」的两种做法差多少。**

  一、添加分区:ADD PARTITION
  --------------------------------------------------------------------
  **新月份来了,加一个 `s_202501`。**  **注意这是「空表加进来」,不搬数据**——
  **所以不管表多大,`ADD PARTITION` 都是毫秒级。**

  [Oracle 原码 · 本沙箱不执行]
  | ALTER TABLE sale ADD PARTITION p202501
  |   VALUES LESS THAN (DATE '2025-02-01');
    **范围分区加分区要注意「边界」:新分区的上限必须比上一个大。**
    **有 `MAXVALUE` 分区的话,得先 `SPLIT` 它,否则新分区加不进去。**


  二、合并分区:MERGE PARTITIONS
  --------------------------------------------------------------------

  SQL> select '合并前 s_202411' as 对象, count(*) as 行数 from s_202411 union all select '合并前 s_202412', count(*) from s_202412 union all select '合并后 s_2024q4', count(*) from s_2024q4
  对象            行数
  ---------------------
  合并前 s_202411 24000
  合并前 s_202412 20000
  合并后 s_2024q4 44000
  已选择 3 行
  **合并是两个分区并成一个,行数相加,不丢数据。**  **什么时候合并?分区切太细了**——
  **比如按天切了一年,现在发现「按天查」根本没人用,那就并成按月。**
  **注意合并本身要搬数据,大表上很慢**,  **所以「切细了再合并」是有代价的,别指望随时能反悔。**

  三、删除分区:DROP 与逐行 DELETE 差多少
  --------------------------------------------------------------------
  **两万行,同一个库,只差「删的方式」。**  **先把 fsync 关掉,不然磁盘同步会把差异盖住。**
  **逐行 `DELETE`:0.0048 秒;`DROP TABLE`:0.00029 秒,差 17 倍。**
  **把 fsync 打开再测一遍:**
  **逐行 `DELETE`:0.0517 秒;`DROP TABLE`:0.0316 秒,几乎一样了。**
  **这不是「结论反了」,是「量到的不是删的代价」。**
  **本机一次 fsync 就要十几毫秒,两边各等一次,差异就被盖住了。**
  **所以凡是要比较「主键删 vs 整表删」,必须先说清 fsync 开没开。**
  **还有个坑:`DELETE FROM t`(不带 `WHERE`)不能拿来测。**
  **SQLite 会识别成「清空表」走 truncate**,代价和 `DROP` 一样,
  **测出来永远是 1 倍。**  **我加了 `where amt >= 0`(恒真但能挡住优化),才是真逐行删。**
  **回到 Oracle:`DROP PARTITION` 改的是数据字典,不读行、不写行级 undo;`DELETE` 要逐行删、逐行写 undo。**  **所以表越大,差距越大**——**这就是「删历史数据用分区」的全部理由。**

  四、并入分区:EXCHANGE PARTITION
  --------------------------------------------------------------------

  SQL> select count(*) as 并入后的行数 from s_202503
  并入后的行数
  ------------
  5000
  已选择 1 行

  [Oracle 原码 · 本沙箱不执行]
  | ALTER TABLE sale EXCHANGE PARTITION p202503
  |   WITH TABLE stage INCLUDING INDEXES;
    **`EXCHANGE` 是「把一张独立表和某个分区对调」**——
    **只改数据字典,不搬数据,毫秒级。**  **加载数据的标准套路就是它:**
    **先往普通表里灌(可以并行、可以 `NOLOGGING`),灌完一句 `EXCHANGE` 换进去。**

第一段:加分区,毫秒级。 注意它是「空表加进来」——不搬数据,所以和表多大无关。 第二段:合并,24000 + 20000 = 44000 行。 行数相加,一条不少——但实际耗时和行数成正比,因为要搬数据。 第三段是最值得看的一段。 关掉 fsync:逐行 DELETE 0.0049 秒,DROP TABLE 0.00033 秒,差 15 倍。 开着 fsync:0.0194 对 0.0193,几乎一样。 这不是结论反了,是「量到的东西变了」——fsync 一开,两边都在等磁盘,删的代价被淹没了。 还有个坑必须说:DELETE FROM t 不带 WHERE 不能拿来测。 SQLite 会把它识别成「清空表」走 truncate,代价和 DROP 一样,测出来永远是 1 倍。 我加了 where amt >= 0(恒真,但能挡住这个优化),才是真逐行删。 第四段:EXCHANGE 的模型。 我用「建表 + 灌数据 + 改名」模拟,行数 5000 对得上。 Oracle 里 EXCHANGE 本身是毫秒级(只改字典), 慢的是前面灌 stage 那一步——但灌数据可以并行、可以重来。

30 秒实验

  1. 把 n 从 20000 改成 200000,重跑第三节。 关 fsync 那组,差距应该从 15 倍涨到几十倍——因为 DROP 的代价不随行数涨。
  2. 把 delete from bt where amt >= 0 改成 delete from bt,重跑。 两组数会变成 1 倍——这就是 truncate 优化。
  3. 把 pragma synchronous 换回 full、journal_mode 换回 delete,重跑。 差距会大幅缩小,因为 fsync 成了主要成本。

常见误区

  1. 「DROP PARTITION 能闪回」。 它不写行级 undo。删完想找回,只能靠备份或回收站里的表。 所以这个动作要给权限控制。
  2. 「ADD PARTITION 会把数据搬一遍」。 不会。它加的是空分区,毫秒级,和表多大无关。
  3. 「MERGE 是免费的整理」。 要搬数据,两亿行就是搬两亿行,要空间、要时间、要 undo。
  4. 「EXCHANGE 会搬数据」。 不会,只改字典。 慢的是往 stage 里灌的那一步,但那是可控、可重来的。
自测三问(点开对答案) 1. `DROP PARTITION` 和 `TRUNCATE PARTITION` 差在哪? —— **前者把分区结构也删了,后者只清数据、分区还在。** **两个都不读行、不写行级 undo。** 2. `EXCHANGE PARTITION` 为什么是毫秒级? —— **它只改数据字典,把「这个段归谁」换一下,不搬数据。** 3. 有 `MAXVALUE` 分区时,怎么加新分区? —— **先 `SPLIT PARTITION pmax AT (...)` 把 `MAXVALUE` 拆开**, **再 `ADD`。** 或者直接用 `INTERVAL` 分区,省掉这件事。 4. 为什么 `DELETE FROM t`(不带 `WHERE`)测不出逐行删除的代价? —— **SQLite 会走 truncate 优化,等价于「清空表」**,**代价和 `DROP` 一样。**

下一篇讲索引分区:本地索引和全局索引。 这个选择牵扯到「删分区时索引怎么办」——选错了,删一次分区就得重建一个几小时的索引。

相关笔记