🦏 第12章 索引
📄 一句话概括

看表上有哪些索引用 pragma index_list(表), 看索引用哪几列用 pragma index_info(索引名)。 origin 那一列最有用:c 是手工建的、u 是唯一约束带来的、 pk 是主键——后两种是约束的一部分,删不得。 索引也记在 sqlite_master 里,tbl_name 告诉你它属于哪张表; 自动索引的 sql 列是 NULL。

📝 本章和前面几章的关系

建完索引,下一步是「确认它真的在、真的被用上」。 这一篇给三处看索引的地方,加一个验证的地方。 排查慢查询的顺序永远是三步:先看表上有没有相关索引(index_list), 再看索引的列对不对(index_info),最后看执行计划走没走(explain)。 三步走完,问题基本就定位了,不用瞎猜。

一、表上有哪些索引:index_list

pragma index_list(big),五列: 序号、索引名、是否唯一(unique)、来源(origin)、是否部分索引(partial)。 origin 那一列最有用: c 是 create index 建的,u 是唯一约束自动建的,pk 是主键。 三种来源,处理方式完全不同。 所以 origin 是 u 或 pk 的索引,别想着删—— 那是约束带来的,删了约束本身就没了。 要动它,得动约束,不是动索引。 partial 是「部分索引」:只给满足条件的行建索引, 比如「只给未删除的行建索引」。 这里显示 0 就是普通索引。

二、索引用了哪些列:index_info

pragma index_info(idx_big_name),三列: 序号、列序号(cid,对应表里的第几列)、列名。 复合索引会给出多行,行序就是索引里的列序。 所以 (city, age) 的索引,第 0 行是 city、第 1 行是 age—— 这个顺序就是「最左前缀」里的那个顺序。 Demo 里还查了一个自动索引 sqlite_autoindex_goods_1: 它对应 goods 表上那句 name text unique,所以 origin 是 u,sql 列是 NULL。 这类索引删不掉,也不该删。 它是约束的实现方式—— 约束负责「不许重复」,索引负责「快速判断重不重复」,两件事是一体的。

三、索引也记在 sqlite_master 里

select name, tbl_name, sql from sqlite_master where type = 'index',一行一个索引。 sql 列对自动索引是 NULL(因为它不是一句 create index 建的), 对手工建的就存着原句。 所以「忘了当初建的是什么索引」,查这一列就够了。 tbl_name 告诉你它属于哪张表。 这是「索引名全局唯一」却还能知道归属的原因—— 名字里看不出,系统表里记着。 type = 'index' 这个条件不能省:sqlite_master 里还堆着表、视图、触发器, 不加条件全查出来会乱。

四、MySQL 那边用 SHOW

show index from 表名;—— 一行一个索引列,Key_name 是索引名、Column_name 是列名、Non_unique 是「是否不唯一」。 show create table 表名; 也能看到索引: KEY idx_name (name) 这种行就是索引定义。 两条一起看,能对上「索引名」和「建它的语句」。 本沙箱没有 show,就用 pragma index_list 加 index_info 对着看。 信息量够,只是要查两次。 MySQL 8 还有几张现成的视图:sys.schema_unused_indexes 列「从没用过的索引」, information_schema.statistics 是 show index 的原始表。 本沙箱没有这些,只能靠 explain 一条条试。

五、验证「到底走没走索引」:explain

explain query plan 是这一篇里唯一「验证」的动作,前面三处都只是「看有什么」, 它才是「看用没用」。 SEARCH … USING INDEX 是走了,SCAN 是没走。 这一条是索引排查的硬证据,比任何猜测都可靠。 MySQL 那边是 explain select …;,输出十几列,关键看两列: type 是 all 就是全表扫,ref/range 是走了索引;key 直接写用了哪个索引。 排查顺序建议固定下来: 先 index_list 看有没有、再 index_info 看列对不对、最后 explain 看用没用。 三步都不长,但能省掉大量「我觉得应该是索引问题」的瞎猜。

六、索引太多怎么看出问题

一个信号是「同前缀的索引有好几个」。 (a)、(a, b)、(a, b, c) 三个同时存在时,前两个基本多余—— 因为最长的那个能覆盖它们的用途(最左前缀)。 删掉短的能省空间、少拖慢写入。 另一个信号是「从来没被用过的索引」。 建的时候想着「以后可能要用」,结果两年没人用—— 它只贡献成本,不贡献收益。 所以「建索引时记一笔为什么建」很值得: 在注释里、在文档里、或者干脆在索引名里带上业务含义。 以后才有依据判断「这个能不能删」。 没记录的索引,谁也不敢删。

七、动手 Demo:三处看索引,一处验索引

Demo 四段:index_list 看清单、index_info 看列(包括一个自动索引)、sqlite_master 看定义、 最后用执行计划验证。 第三段那张表里有一行 sql 是 NULL——那一行就是「约束带来的索引」的样子。

import _q46 as Q
c = Q.blank()
c.execute("CREATE TABLE big (id INTEGER PRIMARY KEY, name TEXT, "
          "city TEXT, age INTEGER)")
c.execute("CREATE TABLE goods (id INTEGER PRIMARY KEY, "
          "name TEXT UNIQUE)")
c.execute("CREATE INDEX idx_big_name ON big (name)")
c.execute("CREATE INDEX idx_big_city ON big (city)")

# 1) 表上有哪些索引
Q.show(c, "PRAGMA index_list(big)")

# 2) 每个索引用的是哪一列
Q.show(c, "PRAGMA index_info(idx_big_name)")
Q.show(c, "PRAGMA index_info(sqlite_autoindex_goods_1)")

# 3) 索引也记在 sqlite_master 里
Q.show(c, "SELECT name, tbl_name, sql FROM sqlite_master "
          "WHERE type = 'index'")

# 4) 到底走没走索引:看执行计划
sql = "SELECT id FROM big WHERE name = '用户30000'"
Q.sqlprint("EXPLAIN QUERY PLAN " + sql)
for row in c.execute("EXPLAIN QUERY PLAN " + sql):
    print("  " + row[3])

实跑输出:

--------------------------------------------------------------------
第六十五课:查看索引——有三处能看到
--------------------------------------------------------------------

  看表上有哪些索引:index_list
  SQL> PRAGMA index_list(big)
  seq     name             unique     origin     partial
  -------------------------------------------------------
  0       idx_big_city     0          c          0
  1       idx_big_name     0          c          0
  (2 行)
  **五列:序号、索引名、是否唯一(`unique`)、来源(`origin`)、是否部分索引(`partial`)。**
  **`origin` 那一列最有用**:
  **`c` 表示 `CREATE INDEX` 建的,`u` 表示唯一约束自动建的,`pk` 表示主键**——
  **三种来源,用途不同。**
  **所以 `origin` 是 `u` 或 `pk` 的索引,别想着删——那是约束带来的,删了约束就没了。**

  看索引用了哪些列:index_info
  SQL> PRAGMA index_info(idx_big_name)
  seqno     cid     name
  ------------------------
  0         1       name
  (1 行)
  **三列:序号、列序号(`cid`,对应表里的第几列)、列名。**
  **复合索引会给出多行,顺序就是索引里的顺序。**
  SQL> PRAGMA index_info(sqlite_autoindex_goods_1)
  seqno     cid     name
  ------------------------
  0         1       name
  (1 行)
  **`goods` 表上这个索引不是 `create index` 建的**——
  **它是建表时那句 `name TEXT UNIQUE` 自动带来的。**
  **所以 `sqlite_master` 里它的 `sql` 列是 NULL。**
  **这类索引删不掉,也不该删。** **它的 `origin` 是 `u`,意思是「跟着约束来的」。**

  索引也记在 sqlite_master 里
  SQL> SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index'
  name                         tbl_name     sql
  ----------------------------------------------------------------------------------
  idx_big_name                 big          CREATE INDEX idx_big_name ON big (name)
  idx_big_city                 big          CREATE INDEX idx_big_city ON big (city)
  sqlite_autoindex_goods_1     goods        NULL
  (3 行)
  **`sql` 列对自动索引是 NULL**(因为它不是一句 `create index` 建的),
  **对 `CREATE INDEX` 建的则存着原句。** **`tbl_name` 告诉你它属于哪张表**——
  **这就是「索引名全局唯一」却还能知道归属的原因。**

  MySQL 那边的两条语句
  **`SHOW INDEX FROM 表名;`**——
  **一行一个索引列,`Key_name` 是索引名、`Column_name` 是列名、`Non_unique` 是「是否不唯一」。**
  **`SHOW CREATE TABLE 表名;`** 也能看到索引:
  **`KEY idx_name (name)` 这种行就是索引定义。**
  **两条一起看,能对上「索引名」和「建它的语句」。**
  **本沙箱没有 `SHOW`,就用 `PRAGMA index_list` 加 `index_info` 对着看。**
  **信息量够,只是要查两次。**

  验证「到底走没走索引」:EXPLAIN QUERY PLAN
  SQL> EXPLAIN QUERY PLAN SELECT id FROM big WHERE name = '用户30000'
  SEARCH big USING COVERING INDEX idx_big_name (name=?)

  **`SEARCH … USING INDEX` 是走了;`SCAN` 是没走。** **这一条是索引排查的唯一硬证据**——
  **别靠「感觉快了」判断,看执行计划。**
  **MySQL 那边是 `EXPLAIN SELECT …;`**,输出更宽(十几列),**关键看 `type` 和 `key` 两列**:
  **`type` 是 `ALL` 就是全表扫,`ref`/`range` 是走了索引;`key` 那一列直接写用了哪个索引。**
  **第 12 章这一篇给的方法,在 MySQL 上一样能用,只是语句不同。**

  索引太多怎么看出问题
  **一个信号是「同前缀的索引有好几个」**:
  **`(a)`、`(a, b)`、`(a, b, c)` 三个索引同时存在时,前两个基本是多余的**——
  **因为最长的那个能覆盖它们的用途。** **删掉短的能省空间、少拖慢写入。**
  **另一个信号是「从来没被用过的索引」**:**MySQL 8 的 `sys.schema_unused_indexes` 能列出来**,
  **本沙箱没有这个视图,只能靠 `EXPLAIN` 一条条试。**
  **所以「建索引时记一笔为什么建」,以后才有依据删。**

第一段两行:idx_big_city、idx_big_name,origin 都是 c。 第二段两个索引各一列:idx_big_name 是 name,自动索引也是 name(来自 unique)。 第三段三行:两个 create index 建的带原句,自动索引那行 sql 是 NULL。 第四段一条 SEARCH:USING COVERING INDEX idx_big_name。

30 秒实验

  1. pragma index_list(big) 里 partial 全是 0,这个值是干什么用的?
  2. 为什么自动索引的 sql 列是 NULL?它到底有没有「建它的语句」?
  3. (a)、(a, b)、(a, b, c) 三个索引并存,删哪几个不影响性能?

常见误区

  1. 以为 index_list 能看到「索引用了哪些列」。 它只给名字,列要用 index_info 另查。
  2. 看到 sql 是 NULL 以为是坏数据。 那是约束自动建的索引,本来就没有 create index 语句。
  3. 靠「感觉快」判断索引生效。 只有执行计划能证明,SCAN 就是没用上。
自测三问(点开对答案) 1. `origin` 的三个值分别是什么意思? —— `c` 是 `create index` 手工建的,`u` 是唯一约束自动建的,`pk` 是主键带来的。 后两种是约束的一部分,删不得。 2. 想知道复合索引的列顺序,看哪个? —— `pragma index_info(索引名)`,返回的行序就是索引的列序,也就是最左前缀里的顺序。 3. 自动索引为什么删不掉? —— 它是 `UNIQUE` 或主键约束的实现方式,删了约束就失效。要动只能动约束本身。

会看也会验了,下一篇讲最后一件:12.4 删除索引——删对了省空间,删错了掉性能。

相关笔记