04总结-索引
目录
一、索引的取舍:什么时候“多建”,什么时候“克制”
二、SQL 基本结构 & EXPLAIN QUERY PLAN
基本 SQL 结构
EXPLAIN QUERY PLAN 用法
看懂几句关键输出
控制优化器的“指令”
三、联合索引 & 左前缀匹配 & 范围截断
左前缀匹配
ORDER BY 与索引顺序
范围条件切断(Range Terminates Order)
四、典型业务场景的索引设计
1. 点查 / 详情页
2. 列表页(过滤 + 排序 + 分页)
3. 搜索框 / 前缀 LIKE
4. IN / OR 条件
5. DISTINCT / GROUP BY
6. 倒序取最新
五、高级索引:表达式 / 部分 / 覆盖
1. 表达式索引(解决“函数导致索引失效”)
2. 部分索引(热点数据单独建索引)
3. 覆盖索引(只用索引、不回表)
六、优化器与维护:优化器、ANALYZE、VACUUM、PRAGMA
ANALYZE
VACUUM
常用 PRAGMA 开关
七、常见坑 & 使用习惯
八、SQLite 的 WITHOUT ROWID:什么时候用
九、子查询 vs JOIN & EXPLAIN
十、DISTINCT & ORDER BY
十一、多值条件与子查询写法
同一列多值
大子查询 + 关联条件
对索引列少做函数 / 计算
十二、调优流程模板(拿去就能用)
一、索引的取舍:什么时候“多建”,什么时候“克制”
索引 = 给数据库加“目录 / 索引页”:
让查东西更快,但要多占空间、写入变慢。
面试要点:什么时候“多建”,什么时候“克制”
-
读多写少(比如报表系统、日志分析):
→ 倾向于多建索引,查询体验更重要。 -
写多读少(比如高频写入的日志表、流水记录):
→ 要克制建索引,避免写入性能崩掉。
二、SQL 基本结构 & EXPLAIN QUERY PLAN
基本 SQL 结构
SELECT ...
FROM ...
WHERE ...
ORDER BY ...;
EXPLAIN QUERY PLAN 用法
在上面那句前面加一行 EXPLAIN QUERY PLAN:
EXPLAIN QUERY PLAN
SELECT ... FROM ... WHERE ... ORDER BY ...;
看懂几句关键输出
-
SCAN TABLE user
→ 没用索引,整表一扫。 -
SEARCH TABLE user USING INDEX idx_user_email (email=?)
→ 用了索引,按 email 查,效率较高。 -
USING COVERING INDEX
→ 查询需要的列在索引里就够了,不用回表。 -
USE TEMP B-TREE FOR ORDER BY
→ 为了排序,额外创建了临时结构,说明没用索引满足排序。 -
EXECUTE LIST SUBQUERY
→ 在执行你写的子查询。
控制优化器的“指令”
-
INDEXED BY 索引名:强制用指定索引。 -
NOT INDEXED:这次别用索引。 -
PRAGMA optimize:整体稍微调优一下。
三、联合索引 & 左前缀匹配 & 范围截断
左前缀匹配
CREATE INDEX idx (a, b, c);
可以覆盖以下前缀:
-
WHERE a = ?✅ -
WHERE a = ? AND b = ?✅ -
WHERE a = ? AND b = ? AND c = ?✅ -
WHERE b = ?❌(不能跳过 a)
范围条件示例:
-
WHERE a > ? AND b = ?:
a 是范围,后面的 b 通常失效。
ORDER BY 与索引顺序
-
ORDER BY a ASC, b ASC
能被INDEX(a, b)吃下,无需额外排序。
范围切断示例:
-
WHERE user_id = ? AND status = ?
→ (a, b) 全用。 -
WHERE user_id > ? AND status = ?
→ a 用了“范围”,截断了,b 只能当普通过滤条件,索引帮不上多大忙。
范围条件切断(Range Terminates Order)
当 a 用了范围(> < BETWEEN LIKE 'x%')后,a 之后的列不再用于等值匹配或排序。
例如:
INDEX(a, b);
WHERE a > ? AND b = ?;
这里的 b 通常无法继续利用索引。
设计联合索引时:
-
把等值过滤多、选择性高的列放前面;
-
把范围列放后面。
四、典型业务场景的索引设计
1. 点查 / 详情页
WHERE email = ?;
-
用 UNIQUE / 主键索引。
-
必要时做覆盖索引
(email, id, ...)。
2. 列表页(过滤 + 排序 + 分页)
WHERE user_id = ? AND created_at BETWEEN ...
ORDER BY created_at DESC;
→ 索引设计:INDEX(user_id, created_at DESC [ , 其他展示列…])
3. 搜索框 / 前缀 LIKE
WHERE name LIKE 'abc%'; -- 可以用索引
→ INDEX(name)
忽略大小写:
-
建索引:
INDEX(LOWER(name)) -
查询:
WHERE LOWER(name) LIKE 'abc%'
4. IN / OR 条件
-
col IN (...)→ 单列索引基本能用。 -
复杂
OR→ 拆成多条 SQL 用UNION ALL。
5. DISTINCT / GROUP BY
-
DISTINCT col/GROUP BY col→INDEX(col)帮你少排序、少折腾。
6. 倒序取最新
ORDER BY created_at DESC LIMIT 20;
→ INDEX(created_at DESC)
五、高级索引:表达式 / 部分 / 覆盖
1. 表达式索引(解决“函数导致索引失效”)
问题:
WHERE REPLACE(phone, '-', '') = ... -- 导致普通索引用不上
方案:
CREATE INDEX idx_user_phone_clean ON user(REPLACE(phone, '-', ''));
注意: 查询里的表达式必须和索引里的完全一致。
2. 部分索引(热点数据单独建索引)
问题:
只常用查 status = 'PAID' 的订单,别的状态不常查。
方案:
CREATE INDEX idx_order_paid
ON orders(user_id, created_at)
WHERE status = 'PAID';
要点: 查询条件要包含这个 status='PAID'(或更强),索引才会被用。
3. 覆盖索引(只用索引、不回表)
场景: 列表页只查 id, created_at, amount
方案:
CREATE INDEX idx_order_list
ON orders(user_id, created_at DESC, id, amount);
在 EXPLAIN 里看到 USING COVERING INDEX 就是命中覆盖索引。
六、优化器与维护:优化器、ANALYZE、VACUUM、PRAGMA
优化器 = 数据库里的“路线规划师”:
专门负责“怎么执行 SQL 才又快又省”。
-
ANALYZE / VACUUM / PRAGMA
= 给这个路线规划师提供最新地图、打扫道路、调一些开关。
ANALYZE
-
大改数据后跑一下,让优化器“重新认识”表的大小和分布,少瞎选执行计划。
VACUUM
-
删除很多数据后,做一次大扫除,回收磁盘、减少碎片。
常用 PRAGMA 开关
-
case_sensitive_like:LIKE 是否区分大小写,影响索引使用。 -
automatic_index:允许 SQLite 临时帮你建索引救急。 -
foreign_keys:外键约束,和性能无关,但和数据正确性强相关,建议开。
七、常见坑 & 使用习惯
-
WHERE 里别随便
函数(索引列):
不是不能用,是要么改成范围,要么建“表达式索引”。 -
LIKE 想用索引,前面不能是
%。
xxx%行,%xxx不行。 -
联合索引:等值列在前,范围列在后。
-
那种取值很少、分布很平均的列(性别、布尔值)一般不要单独建索引。
-
深分页别用
OFFSET 999999,改用“基于上一页最后一条记录继续往下翻”的方式(基于游标 / 上一条主键)。 -
尽量不要无脑
SELECT *,尤其是列表页和热点接口。 -
事务里少放耗时操作,否则容易“锁住全场”。
八、SQLite 的 WITHOUT ROWID:什么时候用
普通表:
-
都有隐藏的
rowid,所有索引最后都指向 rowid,再通过 rowid 找到真实数据。
WITHOUT ROWID:
-
会直接用主键作为表的 B-Tree key,主键既是逻辑键,也是物理定位键:
-
少一层跳转;
-
少存一个 rowid B-Tree。
-
适用场景:
-
一般在 联合主键、读多写少、对体积敏感 的场景考虑,比如一些多列主键的中间表。
九、子查询 vs JOIN & EXPLAIN
-
子查询:在 SQL 里再套一层查询,表达“先查 A 再用 A 查 B”,直观但有时会慢,尤其是相关子查询。
-
JOIN:多表拼在一起查,结构化、适合复杂多表查询,也更容易被优化器好好优化。
-
EXPLAIN:让你看到数据库打算怎么执行这条 SQL,用来分析性能、检查索引是否生效。
子查询 vs JOIN 的选择:
-
谁更容易用上已有索引,就用谁;
-
子查询慢就改 JOIN,JOIN 复杂就改子查询,试 EXPLAIN 看执行计划。
十、DISTINCT & ORDER BY
-
DISTINCT:去掉重复的行,只保留“唯一值”。
-
ORDER BY:把结果按某个字段(或多个字段)排个顺序。
-
一起用时:先去重,再排序,常见于“查所有不重复的 XX,并按字母/时间/大小排好”。
少用无意义的 DISTINCT / ORDER BY:
-
确定本来就唯一就别 DISTINCT;
-
不需要排序就别 ORDER BY。
十一、多值条件与子查询写法
同一列多值
-
a IN (?,?,?)优于a=? OR a=? OR a=?。
大子查询 + 关联条件
EXISTS (SELECT 1 FROM ... WHERE ... AND o.user_id = u.id
)
往往比:
user_id IN (SELECT user_id FROM ...)
更适合利用索引。
对索引列少做函数 / 计算
-
改成范围或预先计算好的值;
-
必要时用表达式索引,而不是在 WHERE 里乱包函数。
十二、调优流程模板(拿去就能用)
-
明确需求:过滤条件、排序字段、返回列、数据量级与写入比。
-
过滤条件:WHERE 里会按什么筛?
user_id?status?created_at? -
排序字段:要怎么排?
ORDER BY created_at DESC? -
返回列:真的需要哪些列?是
id, amount, created_at,还是你懒得想直接SELECT *? -
数据量级:这表是 1 万行、100 万行还是 1 亿行?
-
读写比例:读多写少?(偏向多建索引)还是写多读少?(谨慎加索引)
-
-
写出“理想索引”:
等值放前,范围放后;尽量覆盖常用列。 -
跑
EXPLAIN QUERY PLAN:
看是否出现 SCAN / 临时排序 / 是否 COVERING INDEX。-
有没有
SCAN TABLE xxx→ 表示在全表扫,索引没用上; -
有没有
USE TEMP B-TREE FOR ORDER BY→ 说明在额外排序,没用索引顺序; -
有没有
USING COVERING INDEX ...→ 说明命中了覆盖索引,性能一般不错。
目的:看现在的实际执行计划,跟你“理想状态”差在哪儿。
-
-
按计划修索引:
加 / 改索引顺序,必要时用表达式索引 / 部分索引。 -
跑 ANALYZE:
让优化器获取新统计。 -
压测与回归:观测实际延迟、IO、写入成本。
-
这条 SQL 的耗时是不是明显下降了?
-
QPS 提升了吗?RT 降了多少?
-
IO 是否减少?(读盘次数、页数)
-
写入速度有没有被新的索引拖慢太多?(INSERT/UPDATE 变慢没?)
同时做回归验证:
-
有没有影响其他 SQL?比如新索引让另一个查询选了个更差的计划;
-
业务结果是否正确。
→ 验证“看起来很美”的改动,在现实里是不是真的美。
-
-
长期维护:
数据分布变化大时重跑 ANALYZE,定期清理冷索引。
调优不是“一次性作业”,而是“长期维护”——数据变,你的索引方案也要跟着进化。
