EXPLAIN QUERY PLAN 验证;
然后亲手给一个列加上索引,看着计划从 SCAN 变成 SEARCH … USING INDEX,
并量出这个索引的代价(建它花了 2.5 秒、占 16 KB)。做完这一节,你就知道索引"买"的是什么。
0003 你已经看到过 SCAN 与 SEARCH … USING INDEX 的差别,但那时是彩蛋。
使命的验收里有一条硬要求:「说明索引为何生效(EXPLAIN QUERY PLAN 说得清)」——
这一节把它讲透:索引不是"加了就快",而是你写的查询能不能用上它。
(a, b, c) 的索引:
WHERE a=? 能用、a=? AND b=? 能用、b=? 单独用不能用。
因为索引是先按 a 排、a 相同再按 b 排 —— 只看 b 就没法定位。SEARCH … USING INDEX(用索引定位)、USING COVERING INDEX(索引里就有全部所需列,连原表都不用回)、
SCAN(逐行扫全表)。USE TEMP B-TREE FOR ORDER BY 表示排序借不到索引,值得关注。用 0001 建好的副本(/tmp/sqlite-练习/ctx.db);先自己在纸上写下预测,再对照输出:
sqlite3 -readonly /tmp/sqlite-练习/ctx.db
-- ① project_path + status
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE project_path='x' AND status='active';
-- `--SEARCH memories USING INDEX idx_memories_project_status_expires (project_path=? AND status=?)
-- ② 只用 status
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE status='active';
-- `--SCAN memories ← 没有任何索引以 status 开头
-- ③ project_path + category
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE project_path='x' AND category='y';
-- `--SEARCH memories USING INDEX idx_memories_project_category_hash (project_path=? AND category=?)
-- ④ 只取索引里已有的两列
EXPLAIN QUERY PLAN SELECT project_path, status FROM memories WHERE project_path='x';
-- `--SEARCH memories USING COVERING INDEX idx_memories_project_status_expires (project_path=?)
-- ⑤ 加上排序
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE project_path='x' ORDER BY status;
-- `--SEARCH memories USING INDEX idx_memories_project_status_expires (project_path=?)
对照一下你的预测:①②③ 的差别全在"索引的第一列是谁";④ 因为只要两列、而这两列都在索引里,
于是变成 COVERING INDEX(最快的形态);⑤ 排序没有额外报 TEMP B-TREE —— 说明它借到了索引的顺序。
在副本上做(不要动原库):
-- 先确认 ② 是全表扫
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE category='ARCHITECTURE';
-- `--SCAN memories
-- 建索引(实测:耗时约 2.5 秒)
CREATE INDEX idx_cat_test ON memories(category);
-- 再看一次:变成走索引了
EXPLAIN QUERY PLAN SELECT * FROM memories WHERE category='ARCHITECTURE';
-- `--SEARCH memories USING INDEX idx_cat_test (category=?)
-- 量一下这个索引的代价(16 KB)
SELECT name, sum(pgsize)/1024 AS kb FROM dbstat WHERE name LIKE 'idx_cat%' GROUP BY name;
三个结论,请记住:① 索引换来的是"不再全表扫";② 它是有代价的 —— 占空间、拖慢写入(每次插入都要更新索引); ③ 加索引之前先问:我的查询会用到它吗? —— 索引列与查询条件对不上,加了也是白占地方。
1. 复合索引 (a, b, c) 能被哪种条件用上?
对。索引按 a 排、a 相同再按 b 排、再按 c 排 —— 所以必须从最左列开始用。跳过 a 去查 b 或 c,索引帮不上忙(这就是"最左前缀")。
2. USING COVERING INDEX 比 USING INDEX 好在哪?
对。查询需要的列全都在索引里时,SQLite 直接在索引上完成,连原表都不用碰 —— 这是最快的一种形态。它不改变结果,只是少读了一层。
3. 加索引的代价是什么?
对。索引要占磁盘(实测那个小索引 16 KB,大表上可能是几百 MB),而且每次 INSERT/UPDATE 都要同步更新索引 —— 所以索引不是越多越好。