🪟 第14章 索引与数据完整性
📄 一句话概括

三件事:看索引在不在、看它有没有被用上、看库本身有没有坏。 实测 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 秒实验

  1. 把第三条查询从 WHERE v = 9 改成 WHERE v * 2 = 18,看计划。 如果 v 上本来有索引,改成 v * 2 之后就变成 SCAN 了——列上做运算,索引立刻失效。
  2. 在 PRAGMA integrity_check 之前手动插一条孤儿行(先关外键),再跑 foreign_key_check。 能查出 1 条违规——它不报错,只把违规的行列出来,由你决定怎么处理。
  3. 想一想:一张表上有 14 个索引,你怎么判断哪些是多余的? 看使用统计(sys.dm_db_index_usage_stats),长期 seeks = 0 的可以删; 但先观察够一个完整业务周期(月结、季报这种查询可能一个月才跑一次)。

常见误区

  1. 「建了索引就一定用得上」。列上套函数、隐式类型转换、前导通配符、 列参与运算——这四种写法都会让索引失效。 判断办法只有一个:看执行计划里是 SEARCH 还是 SCAN。
  2. 「重建索引和更新统计信息是一回事」。一个重排页(治碎片),一个重算分布(治优化器瞎猜)。 实测耗时差四倍,触发条件也不同。
  3. 「查询慢了就重建所有索引」。 先看执行计划,再看统计信息。 索引设计不对、参数嗅探、锁等待, 重建一百遍也没用,还白白占一个维护窗口。
自测三问(点开对答案) 1. 怎么确认一条查询用上了索引? —— **看执行计划。** 出现 `SEARCH ... USING INDEX` 就是用上了, **出现 `SCAN` 就是全表扫(或全索引扫)。** **别靠「我建了索引」来推断——写法不对,建了也白建。** 2. `REINDEX` 和 `ANALYZE` 分别治什么? —— **`REINDEX` 重排索引页,治碎片;`ANALYZE` 重算列的取值分布,治优化器估算不准。** **前者贵(要搬数据)、后者便宜(抽样算数),实测 128.4 对 30.0 毫秒。** 3. 哪些写法会让索引失效? —— **在列上套函数、隐式类型转换、前导通配符 `LIKE '%x'`、列参与运算。** **共同点是「索引按原值排,你在列上动了手脚」。解法是把运算挪到等号右边。**

第 14 章到此结束。 下一篇进第 15 章 15.1,讲事务: 四个特性、四种模式、以及事务和批处理的关系——这是前面所有章节里, 唯一一个「管一批语句」的机制。

相关笔记