Menu

SQLite 索引:CREATE INDEX 的用法、时机与原理

SQLite 索引的工作原理,什么时候有帮助,什么时候有害,以及如何判断查询规划器是否真的在用它们。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

索引到底是什么

索引是一个独立的数据结构,一棵有序的 B 树,它让 SQLite 能根据某一列的值找到行,而不必扫描整张表。没有索引时,像 WHERE email = 'rosa@example.com' 这样的查询会读取每一行并逐一检查。有了 email 上的索引,SQLite 大约只需 log(n) 步就能沿着树找到匹配项。

这种提速不是免费的。索引是被索引列的一份副本,外加指回原行的指针。每次 INSERT、每次 UPDATE 被索引的列,以及每次 DELETE,都要同时更新索引。磁盘占用会增加,写入吞吐量会略微下降。这笔交易是:在写入上付出一点,在读取上省下更多。

创建索引

基本语法:

命名约定:大多数团队使用 idx_<table>_<column>,一眼就能看出索引的用途。索引名必须在整个数据库中唯一,而不只是在这张表中唯一,这就是名字里要带上表名的原因。

要删除索引:

DROP INDEX idx_users_email;

索引纯粹是为性能搭的脚手架。删除索引永远不会影响你的数据,只会影响查询的速度。

唯一索引

唯一索引身兼两职:它既能加快查找,又能保证任意两行的索引值都不相同。

第三次插入会失败,报错 UNIQUE constraint failed: accounts.username。SQLite 已经会为 PRIMARY KEY 和 UNIQUE 列自动创建唯一索引,你会看到它们被命名为 sqlite_autoindex_<table>_<n>。只有当表本身没有声明这个约束时,你才需要写 CREATE UNIQUE INDEX。

查询规划器实际做了什么

添加了索引,并不保证 SQLite 一定会用它。查询规划器会为每条查询选择一种策略,你可以用 EXPLAIN QUERY PLAN 查看它的选择:

在输出中找 SEARCH ... USING INDEX idx_orders_customer:这表示索引正在被使用。如果你看到的是 SCAN orders,说明规划器认为全表扫描更便宜(在很小的表上这往往是对的),或者你的查询写法让它无法使用索引。后面会有一整篇文档讲解如何阅读这些查询计划。

什么时候索引不会被使用

索引有几个众所周知的盲区。下面每一种写法都会让 email 上的索引失效:

-- Function wraps the column
SELECT * FROM users WHERE lower(email) = 'rosa@example.com';

-- Leading wildcard in LIKE
SELECT * FROM users WHERE email LIKE '%@example.com';

-- Type mismatch forces a conversion
SELECT * FROM users WHERE email = 12345;

B 树是按 email 的原始值排序的,所以任何在查询时对这一列做变换的操作都会导致全表扫描。解决办法各不相同:存储已经规范化的数据(email_lower 列),使用表达式索引(CREATE INDEX idx ON users(lower(email))),或者用 SQLite 的全文搜索来做子字符串匹配。

覆盖索引

如果一个索引包含了查询需要的所有列,SQLite 不用访问表本身就能回答查询,这就是覆盖索引。诀窍是在索引定义中加入额外的列:

因为查询所需的两列都在索引中,SQLite 会报告 USING COVERING INDEX。不需要再去读取行。对于频繁读取的路径,覆盖索引是收益最高的优化之一,代价是索引更大。多列索引是一个单独的主题,下一篇文档会详细介绍。

列出和查看索引

有两种方法查看现有的索引:

这会列出数据库中的每个索引及其 CREATE 语句。对于单张表,PRAGMA index_list('products'); 只显示这张表的索引,PRAGMA index_info('idx_products_name'); 显示每个索引包含哪些列。以 sqlite_autoindex_ 开头的索引是为 PRIMARY KEY 或 UNIQUE 约束自动创建的,你无法删除它们。

什么时候不该添加索引

在下面几种情况下,添加索引反而会让事情更糟:

  • 很小的表。 几百行的扫描只需几微秒。规划器很可能根本不理会这个索引,你却白白增加了写入开销。
  • 写入频繁却很少查询的列。 每次写入都要更新每个索引。给几乎从不用来过滤的列建索引,纯粹是成本。
  • 单独的低基数列。 在只有三种取值的 status 列上建索引,缩小不了多少范围。它作为复合索引的第二列,或者作为部分索引,仍然可能有帮助,但单独使用通常不值得。
  • 已经被覆盖。 如果已经有 (a, b) 上的索引,就不需要再建 (a) 上的索引。只按 a 过滤的查询,SQLite 会使用复合索引的前导列。

对于“我该不该加这个索引?”,诚实的回答几乎总是:先试试,运行 EXPLAIN QUERY PLAN,用真实的数据测量,再做决定。

下一篇:复合索引

单列索引能覆盖很多场景,但真实的查询经常同时按多列过滤和排序。复合索引(建在 (a, b, c) 上的索引)就是为此而生的,而且列的顺序比人们想象的更重要。这就是下一页的内容。

常见问题

SQLite 中如何创建索引?

使用 CREATE INDEX index_name ON table_name(column_name);。需要唯一性时,使用 CREATE UNIQUE INDEX。索引名必须在整个数据库中唯一,而不只是在这张表中唯一。要删除索引,运行 DROP INDEX index_name;。

SQLite 中什么时候应该添加索引?

在经常用于过滤、连接或排序的列上添加索引,尤其是当表很大、查询只选取一小部分行的时候。不要给每一列都建索引:每个索引都会拖慢 INSERT、UPDATE 和 DELETE,并占用磁盘空间。务必用 EXPLAIN QUERY PLAN 确认规划器真的在使用它。

为什么 SQLite 没有使用我的索引?

常见原因有:表足够小,全表扫描反而更便宜;列被包在函数里(WHERE lower(email) = ... 不会使用 email 上的索引);查询在没有索引的列之间使用了 OR;或者统计信息过时了。运行 ANALYZE 刷新统计信息,再用 EXPLAIN QUERY PLAN 查看规划器选了什么。

SQLite 中如何列出一张表的所有索引?

运行 PRAGMA index_list('table_name'); 查看某张表的索引,或者直接查询 sqlite_master:SELECT name, sql FROM sqlite_master WHERE type = 'index';。sqlite_autoindex_* 开头的条目是为 PRIMARY KEY 和 UNIQUE 约束自动创建的索引。

Coddy 编程语言插图

和 Coddy 一起学编程

开始