服务端阅读 06月1日 00:42
SQLite 索引如何影响查询性能?什么时候反而会变慢?
SQLite 的索引本质上是在表数据之外维护一棵 B-Tree,让查询可以少扫很多行。没有索引时,WHERE user_id = ? 可能要从头读到尾;有合适索引时,SQLite 可以沿着树快速定位候选记录。问题在于索引不是越多越好,它会占空间,也会拖慢写入,因为每次 INSERT、UPDATE、DELETE 都要同步维护索引页。先用执行计划确认有没有命中优化 SQLite 查询不要靠感觉,先看 EXPLAIN QUERY PLAN。它会告诉你是全表扫描,还是使用了某个索引。比如下面这个查询,如果 orders(user_id, created_at) 上有复合索引,通常能同时服务过滤和排序。EXPLAIN QUERY PLANSELECT id, totalFROM ordersWHERE user_id = 42ORDER BY created_at DESCLIMIT 20;CREATE INDEX idx_orders_user_createdON orders(user_id, created_at DESC);看到 SCAN TABLE 不一定就是坏事,小表全扫可能比走索引更快。真正要警惕的是百万级表上频繁出现全表扫描,或者查询返回很少数据却读了大量页面。复合索引要看最左前缀SQLite 的复合索引遵循类似“从左往右”的使用方式。(user_id, status, created_at) 能很好支持 user_id = ? AND status = ?,也能支持只按 user_id 查;但如果查询只有 status = ?,这个索引通常帮不上太多。索引列顺序要按过滤条件、选择性和排序需求一起定,不是把所有列随手堆进去。CREATE INDEX idx_task_user_status_timeON task(user_id, status, updated_at DESC);如果 status 只有“open/closed”两个值,单独给它建索引可能收益很低。低选择性字段更适合放在高选择性字段后面,或者和其他条件组成复合索引。覆盖索引和部分索引很实用覆盖索引指查询需要的列都在索引里,SQLite 不必再回表读取数据页。对于高频列表页,这能明显减少 I/O。部分索引则只索引满足条件的行,适合软删除、状态过滤这类场景。CREATE INDEX idx_article_published_listON article(category_id, published_at DESC, title)WHERE deleted_at IS NULL AND status = 'published';部分索引的坑在于查询条件必须能让优化器判断它适用。如果你的 SQL 写法和索引条件对不上,比如状态值经过函数转换,可能不会命中。追问哪些字段最值得建索引?优先考虑高频出现在 WHERE、JOIN、ORDER BY、GROUP BY 里的字段。还要看选择性,用户 ID、订单号这类区分度高的列通常收益大,性别、布尔状态这类列单独建索引可能很鸡肋。取舍是读多写少的表可以多一点索引,写入密集的日志表要克制。建完以后一定用执行计划验证,而不是只看 SQL 看起来像会命中。为什么加了索引查询还是慢?可能是索引列顺序不对、条件里用了函数、LIKE 前面有通配符,或者返回数据太多导致回表成本很高。比如 WHERE date(created_at) = '2026-01-01' 会让普通 created_at 索引很难发挥作用。可以改成范围条件:created_at >= '2026-01-01' AND created_at < '2026-01-02'。边界是优化器会按统计信息估算成本,数据分布变化后需要重新分析。SQLite 需要手动更新统计信息吗?需要,尤其是大量导入、删除或数据分布明显变化后。可以执行 ANALYZE; 让 SQLite 收集索引统计信息,帮助优化器选择更合适的执行计划。新版本也可以用 PRAGMA optimize; 做轻量维护。踩坑点是测试库数据很少时计划很好,线上数据一多就完全不同,所以压测数据要尽量接近真实分布。索引太多会带来什么问题?首先是写入变慢,每次写表都要更新多个索引。其次是数据库文件变大,缓存命中率下降,备份和迁移也更慢。还有一个隐蔽问题是优化器可选路径太多,但并不代表每条路径都好。实践里可以定期查出重复或长期不用的索引,保留能覆盖核心查询的那几组。