这一篇只讲一件事:删分区的时候,本地索引和全局索引分别会怎样。
本地索引自动跟着走,全局索引变 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 秒实验
- 把「删分区」那一步改成
delete from allp where part = 'p1'(只删数据、分区还在), 看全局索引还剩多少条。 一样会剩 6000 条——说明「悬空」的根因是「数据没了」,不是「分区没了」。 - 用
DROP PARTITION ... UPDATE GLOBAL INDEXES的语义改模型:删数据的同时删索引条目, 重跑,悬空应该是 0——这就是那一句 DDL 干的事。 - 数一下:如果一年删 12 次分区、每次忘加
UPDATE GLOBAL INDEXES,一年要做 12 次REBUILD。 把这个数算给自己看,就知道那句 DDL 值不值得加。
常见误区
- 「删分区不影响索引」。 本地索引自动跟着删(正常),全局索引会
UNUSABLE(事故)。 - 「全局索引失效只是慢」。 不是慢,是查不了。
报
ORA-01502,任何用到它的查询直接失败。 - 「
REBUILD很快」。 它要全表扫一遍。 几十亿行的表上按小时算,而且要额外空间(重建期间新旧两份都在)。 - 「改表名会连带改索引名」。 不会。索引有自己的名字空间,名字得单独
RENAME。
自测三问(点开对答案)
1. 删分区之后,本地索引会怎样? —— **跟着分区一起删掉**,不用手工 `DROP INDEX`。 2. 删分区之后,全局索引会怎样? —— **变 `UNUSABLE`**,用到它的查询报 `ORA-01502`。 **索引条目里有一部分指向的数据已经不存在了。** 3. `UPDATE GLOBAL INDEXES` 的代价是什么? —— **删分区变慢**——要顺手删掉索引里对应的条目,**「秒删」变成「按索引逐条摘」。** 4. 改表名会连带改索引名吗? —— **不会。** 索引有自己的名字空间,名字要单独 `ALTER INDEX ... RENAME TO ...`。第 11 章到此为止。 分区这一章的重点不是语法,是两个判断:「按什么切」和「索引建本地还是全局」。 下一篇进第 12 章:用户管理与权限分配——从「数据结构」转到「谁能碰这些数据」。
- ☐ (本篇完,下一篇 12.1 用户与模式的关系)
相关笔记
- 创建索引分区
- 管理表分区
- 索引对象
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引