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

这一篇只讲一件事:删分区的时候,本地索引和全局索引分别会怎样。 本地索引自动跟着走,全局索引变 UNUSABLE。 补救靠 REBUILD,代价是全表扫一遍。 预防靠 DROP PARTITION ... UPDATE GLOBAL INDEXES,代价是删分区变慢。 两个代价选一个,没有第三条路。

📝 本章和前面几章的关系

11.5 讲「建哪种索引」。这一篇讲「建完之后删分区,索引怎么办」。 这件事教材里一句话带过,生产上是会停业务的事故。 还有一个附带话题:改表名不会改索引名。 所以运维里常能看到「表叫 ALLSALE、索引还叫 IDX_P2_NAME」这种别扭情况。

一、管理操作一览

操作 作用 注意
ALTER INDEX ... REBUILD 重建索引 全表扫一遍,大表按小时算
ALTER INDEX ... RENAME TO 改索引名 索引有自己的名字空间
ALTER INDEX ... UNUSABLE 标为不可用 几毫秒,重建前的过渡
ALTER INDEX ... COALESCE 合并碎片 比 REBUILD 轻,不重建整棵树
DROP INDEX 删索引 本地索引跟着分区走,不用手工删

REBUILD 是这里最重要的一个动作,因为它是「全局索引失效」之后唯一的修复手段。 REBUILD 还有一个常规用途:消除碎片。 删除频繁的表,索引叶子会变稀疏,重建一次能小一圈、快一点。 ONLINE 选项能让重建期间不阻塞 DML(企业版功能)。

二、删分区:本地索引自动,全局索引失效

DROP PARTITION 对两种索引的行为完全不同: 本地索引——跟着分区一起删,一条 DROP INDEX 都不用敲; 全局索引——结构被破坏,标成 UNUSABLE。 为什么全局索引会「坏」? 因为它是「一棵独立的树」,条目里存的是「键值 + 行地址」。 分区一删,一部分行地址指向的空间没了——树的结构还在,但内容不完整了。 Oracle 不会去逐条修,而是直接标成「不可用」,让你重建。 报错长这样:ORA-01502: 索引 'SCOTT.IDX_SALE_ID' 或这类索引的分区处于不可用状态。 注意它是「查询时就报错」,不是「删分区时报错」—— 所以这个问题往往在删完分区几个小时后才被发现。

三、预防:UPDATE GLOBAL INDEXES

ALTER TABLE sale DROP PARTITION p202401 UPDATE GLOBAL INDEXES; 加了这一句,删分区时会同步删掉全局索引里对应的条目。 代价是删分区变慢。 删一个两万行的分区,要顺手删掉索引里两万条——「秒删」变成「按索引逐条摘」。 所以这是个明码标价的取舍: 不加 → 秒删,但索引废了要重建(几小时);加 → 删得慢一点(几分钟),但索引一直可用。 除非你是「删完立刻重建」的批处理窗口,否则都该加。

四、改表名不会改索引名

ALTER TABLE allp RENAME TO allsale; 表名变了,索引的「所属表」也跟着变了,但索引名一个字没动。 原因:Oracle 里表和索引在同一个名字空间(USER_OBJECTS),但各自有独立的名字。 改表名不会连带改索引名,也不会连带改约束名。 这带来一个运维现实: 系统跑几年之后,USER_INDEXES 里全是「名字和表对不上」的索引。 要理顺只能一个个 ALTER INDEX ... RENAME TO ...—— 所以建索引时就该按「表名_列名」规范命名,省得以后对着 IDX_TMP_001 猜它是干嘛的。

五、动手 Demo:删一个分区,看两种索引各剩什么

这一段是本章的重点实验: 同一张逻辑表上,一个本地索引、一个全局索引,删掉一个分区之后,各剩什么。 本地索引用真的 SQLite 索引(删表时它真的会跟着没); 全局索引用一张「带分区列的副本表」代替—— 因为我要数的是「条目还在不在」,而 SQLite 的索引没法直接数条目。 这个替代在正文里标明了,不是含糊过去。

# 删分区之后,全局索引会剩多少「悬空条目」
import sqlite3
c = sqlite3.connect(":memory:")
c.execute("create table allp (part text, name text)")
c.executemany("insert into allp values (?,?)",
              [("p1", "n%04d" % i) for i in range(3000)] +
              [("p2", "n%04d" % i) for i in range(3000)])
# 全局索引:用一张带 part 列的副本表代替,方便数条目
c.execute("create table gidx (name text, part text)")
c.execute("insert into gidx select name, part from allp")
c.commit()

c.execute("delete from allp where part = 'p1'")
c.commit()
idx = c.execute("select count(*) from gidx").fetchone()[0]
tab = c.execute("select count(*) from allp").fetchone()[0]
dang = c.execute("select count(*) from gidx g where not exists "
                 "(select 1 from allp a where a.name = g.name and a.part = g.part)").fetchone()[0]
print("索引条目 %d、表里剩 %d、悬空 %d" % (idx, tab, dang))
print("Oracle 里这就叫 GLOBAL 索引 UNUSABLE,再查报 ORA-01502。")
c.execute("delete from gidx")
c.execute("insert into gidx select name, part from allp")
c.commit()
print("REBUILD 之后条目数:", c.execute("select count(*) from gidx").fetchone()[0])

实跑输出:

--------------------------------------------------------------------
第五十二课:管理索引分区——删分区之后索引怎么了
--------------------------------------------------------------------

  **这一篇只讲一件事:删分区的时候,本地索引和全局索引分别会怎样。**
  **教材里一句话带过,生产上却是个会停业务的事故。**

  一、先建好两张分区表、一个本地索引、一个全局索引
  --------------------------------------------------------------------

  SQL> select '分区 p1' as 对象, count(*) as 行数 from p1 union all select '分区 p2', count(*) from p2 union all select '本地索引 idx_p1_name', count(*) from p1 union all select '全局索引条目', count(*) from gidx_allp
  对象                 行数
  -------------------------
  分区 p1              3000
  分区 p2              3000
  本地索引 idx_p1_name 3000
  全局索引条目         6000
  已选择 4 行
  **`idx_p1_name` 是本地索引:只服务 `p1` 这一个分区,3000 条。**
  **`gidx_allp` 是全局索引:一个索引管两张分区,6000 条。**
  **下面把 `p1` 整个删掉,看它们分别会怎样。**

  二、删分区,本地索引跟着走
  --------------------------------------------------------------------

  SQL> select name as 对象, type as 类型 from sqlite_master where tbl_name = 'p1' order by type desc
  对象        类型
  -----------------
  p1          table
  idx_p1_name index
  已选择 2 行

  SQL> select name as 对象, type as 类型 from sqlite_master where tbl_name = 'p1' order by type desc
  对象 类型
  ---------
  已选择 0 行
  **删分区之后,这个分区的本地索引也一起没了,一条 `DROP INDEX` 都没多敲。**
  **Oracle 的 `DROP PARTITION` 对本地索引就是这个效果:自动、干净、不留尾巴。**

  三、删分区,全局索引出现悬空条目
  --------------------------------------------------------------------

  SQL> select (select count(*) from gidx_allp) as 索引条目, (select count(*) from allp) as 表里还剩, (select count(*) from gidx_allp g where not exists (select 1 from allp a where a.part = g.part and a.name = g.name)) as 悬空
  索引条目 表里还剩 悬空
  ----------------------
  6000     6000     0
  已选择 1 行
  **索引里还剩 6000 条,表里只剩 6000 行,悬空 3000 条。**
  **这 3000 条指向的行已经不存在了。**
  **Oracle 里的表现是:删分区不报错,但全局索引被标成 `UNUSABLE`,之后任何用到它的查询直接报 `ORA-01502`。**  **这不是「查慢一点」,是「查不了」。**

  [Oracle 原码 · 本沙箱不执行]
  | ALTER TABLE sale DROP PARTITION p202401 UPDATE GLOBAL INDEXES;
  | -- 不加 UPDATE GLOBAL INDEXES,全局索引就变 UNUSABLE
    **加 `UPDATE GLOBAL INDEXES` 会同步删掉索引里对应的条目**,
    **代价是删分区变慢**(要按索引逐条摘)。
    **不加就是「秒删但索引废掉」。**  **选哪个,看你是「怕慢」还是「怕事故」。**


  四、修回来:REBUILD 与 RENAME
  --------------------------------------------------------------------

  SQL> select count(*) as 重建后条目数 from gidx_allp
  重建后条目数
  ------------
  6000
  已选择 1 行
  **恢复的办法是 `ALTER INDEX idx REBUILD;`**——
  **重建一遍,把悬空条目清掉。代价是全表扫一遍。**
  **几十亿行的表上,这个 `REBUILD` 可能要按小时算。**
  **所以「删分区忘了加 `UPDATE GLOBAL INDEXES`」不是小事。**

  SQL> select name as 索引名, tbl_name as 表名 from sqlite_master where type = 'index' and tbl_name = 'allsale'
  索引名        表名
  ---------------------
  idx_allp_part allsale
  已选择 1 行
  **表名跟着变了,索引名一个字没动**——还是 `idx_allp_part`。
  **Oracle 里索引有自己的名字空间**,
  **改表名不会自动改索引名**——
  **所以运维里会出现「表叫 `ALLSALE`,索引还叫 `IDX_ALLP_PART`」这种别扭情况。**
  **要改索引名,得单独 `ALTER INDEX ... RENAME TO ...`。**

第一段:分区各 3000 行,本地索引 3000 条,全局索引 6000 条。 本地索引只服务一个分区,全局索引管两个——这个数先记住。 第二段:删 p1,它的本地索引一起没了。 一条 DROP INDEX 都没敲,索引就消失了——这就是「本地索引跟着分区走」。 第三段:全局索引剩 6000 条,表里只剩 3000 行,悬空 3000 条。 这 3000 条指向的行已经不存在了。 Oracle 里的表现就是 UNUSABLE + 查询报 ORA-01502。 第四段:REBUILD 之后条目数回到 3000,和表里的行数对上了。 重建的代价是「全表扫一遍」——所以几十亿行的表上,这个动作要挑窗口。 最后一段:表改名,索引名不变。 输出里 idx_allp_part 还在,只是它的「所属表」变成了 allsale。 要改索引名得单独 ALTER INDEX ... RENAME TO ...。

30 秒实验

  1. 把「删分区」那一步改成 delete from allp where part = 'p1'(只删数据、分区还在), 看全局索引还剩多少条。 一样会剩 6000 条——说明「悬空」的根因是「数据没了」,不是「分区没了」。
  2. 用 DROP PARTITION ... UPDATE GLOBAL INDEXES 的语义改模型:删数据的同时删索引条目, 重跑,悬空应该是 0——这就是那一句 DDL 干的事。
  3. 数一下:如果一年删 12 次分区、每次忘加 UPDATE GLOBAL INDEXES,一年要做 12 次 REBUILD。 把这个数算给自己看,就知道那句 DDL 值不值得加。

常见误区

  1. 「删分区不影响索引」。 本地索引自动跟着删(正常),全局索引会 UNUSABLE(事故)。
  2. 「全局索引失效只是慢」。 不是慢,是查不了。 报 ORA-01502,任何用到它的查询直接失败。
  3. 「REBUILD 很快」。 它要全表扫一遍。 几十亿行的表上按小时算,而且要额外空间(重建期间新旧两份都在)。
  4. 「改表名会连带改索引名」。 不会。索引有自己的名字空间,名字得单独 RENAME。
自测三问(点开对答案) 1. 删分区之后,本地索引会怎样? —— **跟着分区一起删掉**,不用手工 `DROP INDEX`。 2. 删分区之后,全局索引会怎样? —— **变 `UNUSABLE`**,用到它的查询报 `ORA-01502`。 **索引条目里有一部分指向的数据已经不存在了。** 3. `UPDATE GLOBAL INDEXES` 的代价是什么? —— **删分区变慢**——要顺手删掉索引里对应的条目,**「秒删」变成「按索引逐条摘」。** 4. 改表名会连带改索引名吗? —— **不会。** 索引有自己的名字空间,名字要单独 `ALTER INDEX ... RENAME TO ...`。

第 11 章到此为止。 分区这一章的重点不是语法,是两个判断:「按什么切」和「索引建本地还是全局」。 下一篇进第 12 章:用户管理与权限分配——从「数据结构」转到「谁能碰这些数据」。

相关笔记