三件事:看索引在不在、看它有没有被用上、看库本身有没有坏。
实测 10 万行:REINDEX 128.4 毫秒、ANALYZE 30.0 毫秒——重建比重新统计贵四倍。
排查顺序:先看执行计划有没有 SCAN,再看统计信息,最后才怀疑索引设计。
第 14 章最后一节。
前面四节讲「索引是什么、怎么建、怎么删、约束怎么加」,这一节讲「日常怎么管」。
教材 14.5 两小节:查看与重建索引、完整性检查清单,
「小结」和「实践与练习」按惯例并进本篇末尾。
这一节没有新语法,全是查询和习惯。 演示里那几条命令都可以直接抄去用——
换个库,把 sqlite_master 换成 sys.indexes、
PRAGMA integrity_check 换成 DBCC CHECKDB 就行。
一、查看:这张表上有哪些索引
SQLite 一条语句就列全了,连建索引的原话都在:
| name | sql |
|---|---|
| ix_t_k | CREATE INDEX ix_t_k ON t(k) |
| ix_t_name | CREATE INDEX ix_t_name ON t(name) |
SQL Server 用 sp_helpindex '表名' 看列表,
或者查 sys.indexes 配 sys.index_columns——后者能看到「组合索引里哪列在前」。
为什么要专门查一遍? 因为接手一个老库时,你根本不知道哪些索引是历史遗留。
见过一个表上 14 个索引的,其中 9 个从没被用过,全是历任开发「顺手加的」。
二、索引有没有被用上
三条查询,三条计划:
| 查询 | 执行计划 | 说明 |
|---|---|---|
WHERE k = 777 |
SEARCH t USING INDEX ix_t_k |
用上了 |
WHERE name = 'name777' |
SEARCH t USING INDEX ix_t_name |
用上了 |
WHERE v = 9 |
SCAN t |
没索引可用 |
第三条 SCAN 就是「建了索引也不等于用上」。
除了「压根没建」,还有一整套写法会让索引失效——这些都跟 SQL Server 一样:
| 写法 | 为什么用不上 |
|---|---|
在列上套函数:WHERE substr(name, 1, 3) = 'nam' |
索引里存的是原值,不是函数结果 |
| 隐式类型转换:字符串列传数字 | 比较前先转换,索引的有序性用不上 |
前导通配符:LIKE '%abc' |
索引按前缀有序,前缀不定就没法定位 |
列参与了运算:WHERE price * 2 > 100 |
同上,索引存的是 price 本身 |
这几条的共同点:索引是「按原值排好的目录」,你在列上动手脚,目录就失效了。
解法也很统一:把运算挪到等号右边。 WHERE price * 2 > 100 改成 WHERE price > 50,
结果一样,索引就能用了。
三、重建与统计信息:两件不同的事
10 万行的表上,两个操作各跑一次:
| 操作 | 10 万行耗时 |
|---|---|
REINDEX(重建全部索引) |
128.4 ms |
ANALYZE(重算统计信息) |
30.0 ms |
别把这两个混成一件事。 REINDEX 重排索引的页(治碎片),
ANALYZE 重新统计「每列有多少不同值、分布如何」(治优化器瞎猜)。
为什么重建更贵? 因为它要真把数据搬一遍;统计信息只是抽样算个数,便宜得多。
所以日常维护的优先级是:统计信息经常更、索引碎片按需重建。
SQL Server 有自动更新统计的机制,
但大表上往往跟不上——百万行以上的表,一次插入十万行,自动更新可能还没触发,
这时候手工 UPDATE STATISTICS 就是必要的。
四、体检:库本身有没有坏
| 检查 | 结果 |
|---|---|
PRAGMA integrity_check |
ok |
PRAGMA foreign_key_check |
0 条违规 |
integrity_check 查的是「页结构有没有坏」——断电、磁盘故障、程序 bug 都可能导致,
返回 ok 就说明 B 树的结构是自洽的。 SQL Server 上对应的是 DBCC CHECKDB。
foreign_key_check 查的是「有没有孤儿行」——子表指向了不存在的父。
这个检查特别有用,因为外键可能压根没开过(14.4 讲的那个坑),
历史数据里早就攒了一批孤儿行,一查就露。
两个都建议定期跑。 索引损坏不常见,
但真发生了会让人怀疑人生——
查询结果莫名其妙不对、报「database disk image is malformed」,排查半天以为是代码。
五、一份可执行的维护清单
| 频率 | 做什么 |
|---|---|
| 每周 | 看慢查询日志,确认索引真的在用 |
| 每月 | 重建碎片率高的索引、更新统计信息 |
| 每季 | 过一遍未使用的索引,删掉只拖慢写入的那些 |
| 变更后 | 新加索引前后各测一次写入耗时,别只看读变快了 |
最后一条最容易忘:加索引的人只关心自己那条查询变快了,没人管写入慢了。
所以「加索引」也该像改代码一样走评审——谁加的、为什么加、测过写入代价没有。
「未使用的索引」怎么找? SQL Server 查 sys.dm_db_index_usage_stats,
看 user_seeks 和 user_scans 是不是长期为 0。
不过要注意:统计是从上次重启开始算的,重启后要观察够一个完整业务周期再下结论。
六、动手 Demo:查看、重建、体检:一次完整的维护
这一段走一遍完整的日常维护:
先列出表上的索引,再逐条看查询有没有用上,然后测 REINDEX 和 ANALYZE 的耗时,
最后给库做个体检。
中间那三条计划是重点——第三条 SCAN 就是「索引没被用上」的样子。
import sqlite3
c = sqlite3.connect(u":memory:", isolation_level=None)
# ... 建 10 万行的 t,并建 ix_t_k、ix_t_name ...
for r in c.execute(u"SELECT name, sql FROM sqlite_master"
u" WHERE type = 'index' AND tbl_name = 't'"
u" AND name NOT LIKE 'sqlite_%'"):
print(r[0], r[1])
for tag, q in ((u"WHERE k", u"SELECT * FROM t WHERE k = 777"),
(u"WHERE name", u"SELECT * FROM t WHERE name = 'name777'"),
(u"WHERE v", u"SELECT * FROM t WHERE v = 9")):
print(tag, c.execute(u"EXPLAIN QUERY PLAN " + q).fetchall()[0][3])
print(u"REINDEX %.1f ms" % clock(lambda: c.execute(u"REINDEX")))
print(u"ANALYZE %.1f ms" % clock(lambda: c.execute(u"ANALYZE")))
print(c.execute(u"PRAGMA integrity_check").fetchone()[0])
print(len(c.execute(u"PRAGMA foreign_key_check").fetchall()))
实跑输出:
--------------------------------------------------------------------
维护与管理:查看索引、重建、体检
--------------------------------------------------------------------
① 查看:这张表上有哪些索引
--------------------------------------------------------------------
1> CREATE INDEX ix_t_k ON t(k)
1> CREATE INDEX ix_t_name ON t(name)
1> SELECT name, sql
FROM sqlite_master
WHERE type = 'index' AND tbl_name = 't' AND name NOT LIKE 'sqlite_%'
name sql
--------------------------------------------
ix_t_k CREATE INDEX ix_t_k ON t(k)
ix_t_name CREATE INDEX ix_t_name ON t(name)
(2 行受影响)
SQLite 直接给出建索引的原句;SQL Server 用 sp_helpindex '表名' 看列表,
或者查 sys.indexes 配 sys.index_columns 看每一列的顺序。
② 索引有没有被用上
--------------------------------------------------------------------
WHERE k = 777 -> SEARCH t USING INDEX ix_t_k (k=?)
WHERE name = 'name777' -> SEARCH t USING INDEX ix_t_name (name=?)
WHERE v = 9 -> SCAN t
第三条 SCAN 就是没索引可用。建了索引不等于用上了——写法不对、类型不匹配都可能绕过它。
常见绕过索引的写法:在列上套函数(WHERE substr(name,1,3) = 'nam')、隐式类型转换、
前导通配符 LIKE '%abc'。
③ 重建与统计信息
--------------------------------------------------------------------
REINDEX 重建索引,ANALYZE 重新统计「每列有多少不同值、分布如何」。
| 操作 | 10 万行耗时 |
|---|---:|
| REINDEX(重建全部索引) | 130.8 ms |
| ANALYZE(重算统计信息) | 29.9 ms |
统计信息决定「优化器选哪条路」。统计过期了,优化器就在瞎猜——
SQL Server 有自动更新统计的机制,但大表上往往跟不上,需要手工 UPDATE STATISTICS。
④ 体检:库本身有没有坏
--------------------------------------------------------------------
| 检查 | 结果 |
|---|---|
| PRAGMA integrity_check | ok |
| PRAGMA foreign_key_check | 0 条违规 |
integrity_check 返回 ok 就说明页结构没问题;SQL Server 对应的是 DBCC CHECKDB。
这两个检查都建议定期跑——索引损坏这种事不常见,但发生了会让人怀疑人生。
⑤ 一份可执行的维护清单
--------------------------------------------------------------------
| 频率 | 做什么 |
|---|---|
| 每周 | 看慢查询日志,确认索引真的在用 |
| 每月 | 重建碎片率高的索引、更新统计信息 |
| 每季 | 过一遍未使用的索引,删掉只拖慢写入的那些 |
| 变更后 | 新加索引前后各测一次写入耗时,别只看读变快了 |
最后一条最容易忘:加索引的人只关心自己那条查询变快了,没人管写入慢了。
所以「加索引」也该像改代码一样走评审——谁加的、为什么加、测过写入代价没有。
查看那一步不只是「看看有没有」:sql 字段给的是建索引的原话,
能一眼看出它是单列还是组合、有没有 UNIQUE。 老库交接时这一步能省很多猜。
三条计划里前两条是 SEARCH、第三条是 SCAN。
这就是判断「索引有没有被用上」的唯一可靠办法——看计划,别靠感觉。
REINDEX 128.4 毫秒、ANALYZE 30.0 毫秒,差四倍。
这也解释了为什么「统计信息可以勤快点更新,索引重建要挑时间」——
一个是抽样算数,一个是真搬数据。
体检那两行输出很短,信息量却不小: ok 说明页结构自洽,
0 说明没有孤儿行。 如果第二行不是 0,
那就说明历史数据里已经有指向不存在父表的记录了——这在外键一直没开过的库上很常见。
30 秒实验
- 把第三条查询从
WHERE v = 9改成WHERE v * 2 = 18,看计划。 如果v上本来有索引,改成v * 2之后就变成SCAN了——列上做运算,索引立刻失效。 - 在
PRAGMA integrity_check之前手动插一条孤儿行(先关外键),再跑foreign_key_check。 能查出 1 条违规——它不报错,只把违规的行列出来,由你决定怎么处理。 - 想一想:一张表上有 14 个索引,你怎么判断哪些是多余的?
看使用统计(
sys.dm_db_index_usage_stats),长期seeks = 0的可以删; 但先观察够一个完整业务周期(月结、季报这种查询可能一个月才跑一次)。
常见误区
- 「建了索引就一定用得上」。列上套函数、隐式类型转换、前导通配符、
列参与运算——这四种写法都会让索引失效。
判断办法只有一个:看执行计划里是
SEARCH还是SCAN。 - 「重建索引和更新统计信息是一回事」。一个重排页(治碎片),一个重算分布(治优化器瞎猜)。 实测耗时差四倍,触发条件也不同。
- 「查询慢了就重建所有索引」。 先看执行计划,再看统计信息。 索引设计不对、参数嗅探、锁等待, 重建一百遍也没用,还白白占一个维护窗口。
自测三问(点开对答案)
1. 怎么确认一条查询用上了索引? —— **看执行计划。** 出现 `SEARCH ... USING INDEX` 就是用上了, **出现 `SCAN` 就是全表扫(或全索引扫)。** **别靠「我建了索引」来推断——写法不对,建了也白建。** 2. `REINDEX` 和 `ANALYZE` 分别治什么? —— **`REINDEX` 重排索引页,治碎片;`ANALYZE` 重算列的取值分布,治优化器估算不准。** **前者贵(要搬数据)、后者便宜(抽样算数),实测 128.4 对 30.0 毫秒。** 3. 哪些写法会让索引失效? —— **在列上套函数、隐式类型转换、前导通配符 `LIKE '%x'`、列参与运算。** **共同点是「索引按原值排,你在列上动了手脚」。解法是把运算挪到等号右边。**第 14 章到此结束。 下一篇进第 15 章 15.1,讲事务: 四个特性、四种模式、以及事务和批处理的关系——这是前面所有章节里, 唯一一个「管一批语句」的机制。
- ☐ (本篇完,下一篇 15.1 事务概述)