
「表小就不用建索引」这句话传得很广。到底多小算小,没人说得清。下面把同一份 2000 行的数据在两种库里各查一遍,一个有索引一个没有,数据表放在前面,解释放在后面。环境是 Python 3.11 自带的 SQLite 3.53.1,macOS(APFS),验证脚本在 tools/verify-sqlite-index-count.py。
查询是哪一条
两张库结构完全一样,一张的 cat 列建了索引,一张没建。查询是:
SELECT count(*) FROM t WHERE cat = 'b3';
cat 一共 7 种取值,其中 b3 占 286 行。每个库都连跑 5 次取最小值,避开首次缓存的干扰。
数据表
无索引: 命中 286 行,5 次取最小 0.071 ms
有索引: 命中 286 行,5 次取最小 0.011 ms
有索引库的查询计划:
SEARCH t USING COVERING INDEX idx_t_cat (cat=?)
把行数抬到 20 万行,同一个查询再各跑一次:
无索引 20 万行: 命中 28571 行,5.0 ms
有索引 20 万行: 命中 28571 行,0.354 ms
数字在说什么
2000 行的表,有索引比没索引快 6 倍多一点。20 万行的时候差距拉开到 14 倍。两个比值都不是线性的,原因是无索引那条路的成本里有一块固定开销(打开表、准备扫描),行数少的时候这块占大头,行数多了扫描本身才显出来。
有索引那条路的查询计划值得看一眼:COVERING INDEX。查询只用到 cat 这一列,而索引里正好就存着这一列的值,整条查询根本没碰过表本身,只扫了索引的一小段。这就是 count 类查询从索引里占到便宜的原因,它要的恰好就是索引有的。
查询计划怎么看
无索引的库上跑 EXPLAIN QUERY PLAN,输出是 SCAN t,全表扫。有索引的库上是 SEARCH ... USING COVERING INDEX,按索引定位。建索引之前先跑一遍 explain,一眼就能看出这条查询走的是哪条路,不用猜。
别急着给每列都建
索引不是白给的。每次 INSERT 和 UPDATE 都要顺手维护索引,写入越多的表,这层开销越明显。上面这个实验里的查询是固定的一条,实际业务里查询五花八门,给每种条件都建索引,最后就是一排没人用还得养着的索引。SQLite 可以用 EXPLAIN QUERY PLAN 逐条确认哪些查询真的用上了索引,用不上字的就别建。
这一点没实测写入侧的损耗数字,只测了读侧,写在前面的是读侧结论。