在嵌入式应用或单机工具里,SQLite常以轻量、零配置著称,但面对千万级数据仍会出现查询变慢。传统的完整B树索引对所有行一视同仁,当业务只关心其中一小部分数据时,这种无差别索引既占空间又拖慢写入。SQLite从3.8.0版本开始支持部分索引,从3.9.0版本支持表达式索引,两者都能让索引更聪明地服务于真实查询模式。

什么是部分索引
部分索引(partial index)是指在创建索引时附加一个WHERE子句,只有满足该条件的行才会被纳入索引结构。数据库在插入或更新数据时,会先判断新行是否符合条件,符合才维护索引。这意味着索引体积可能只有原表的一小部分,查询规划器在命中条件时也能直接利用该索引。
例如一个用户消息表,绝大多数消息已读,运营后台只频繁查询未读消息。如果对整个表的已读状态建索引,写入每条已读消息都要更新索引,而这部分恰恰不是查询热点。使用部分索引可以把索引限制在未读行,既加速查询又减轻写入负担。
CREATE TABLE messages ( id INTEGER PRIMARY KEY, user_id INTEGER, content TEXT, is_read INTEGER DEFAULT 0 ); -- 仅为未读消息建立索引 CREATE INDEX idx_unread_msgs ON messages(user_id) WHERE is_read = 0; -- 以下查询可命中部分索引 SELECT id, content FROM messages WHERE user_id = 42 AND is_read = 0;
需要注意,查询语句中的WHERE条件必须能匹配部分索引的WHERE子句,规划器才会选择它。如果只写user_id = 42而漏掉is_read = 0,SQLite通常无法使用该部分索引。另外,部分索引的表达式必须是确定性的,不能包含随机函数或时间函数。
什么是表达式索引
表达式索引(expression index)是把一个表达式的计算结果作为索引键,而不是单纯的列名。最常见场景是大小写不敏感查询:应用常使用upper(name)或lower(email)进行比对,如果直接对列建索引,函数调用会让索引失效。表达式索引提前把函数结果算好存起来,查询时便能走索引。
表达式必须是确定性的,也就是说同样的输入永远得到同样输出。SQLite允许的表达式包括列引用、内置确定性函数、算术运算等。创建后,查询中的表达式必须与索引定义逐字一致,规划器才会识别并采用。
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
username TEXT
);
-- 以大写后的用户名为索引键
CREATE INDEX idx_username_upper ON accounts(upper(username));
-- 查询时使用相同表达式即可命中
SELECT id FROM accounts
WHERE upper(username) = upper('Alice');
这种索引在登录校验、搜索框联想中非常实用。不过表达式索引会占用额外空间,并且每次写入都要重算表达式,因此只对高频查询且写冲突不激烈的字段使用更为划算。
两者结合的实际案例
设想一个设备日志表,记录海量传感器数据,其中只有level为'ERROR'的条目需要被运维面板频繁检索,且面板总是按天格式化时间展示。我们可以同时利用部分索引与表达式索引,仅对错误日志以日期字符串建立索引。
下面的例子用strftime把时间戳转成日期,并限制只索引错误级别。这样索引行数可能不足全表千分之一,却完美覆盖核心告警查询。
CREATE TABLE device_log (
id INTEGER PRIMARY KEY,
ts INTEGER,
level TEXT,
msg TEXT
);
CREATE INDEX idx_err_day ON device_log(strftime('%Y-%m-%d', ts, 'unixepoch'))
WHERE level = 'ERROR';
-- 查询某天错误日志
SELECT msg FROM device_log
WHERE level = 'ERROR'
AND strftime('%Y-%m-%d', ts, 'unixepoch') = '2023-05-01';
从EXPLAIN QUERY PLAN可以看到,SQLite对该语句使用了idx_err_day,扫描行数骤降。若去掉部分条件,索引退化为完整表达式索引,体积与维护成本都会上升。可见两者结合能进一步贴合业务访问特征。
使用时的注意事项
部分索引与表达式索引虽好,但也有边界。首先是SQLite版本要求,旧版Android或某些遗留系统自带的SQLite可能不支持,需要在连接时查询sqlite_version()确认。其次是写入放大问题:表达式越复杂,插入与更新越慢,应在测试环境用真实数据量跑一遍基准。
另外,部分索引的WHERE条件与查询条件不匹配是常见的坑。开发者容易以为建了索引就一定能用,实际上只要查询少了那个常量条件,优化器便会放弃。建议用EXPLAIN QUERY PLAN定期复查核心SQL,确保索引真正被命中而不是沦为摆设。
| 索引类型 | 适用场景 | 主要收益 | 潜在风险 |
|---|---|---|---|
| 部分索引 | 只查表中少数满足条件行 | 缩小索引体积、降低写负担 | 查询条件须严格匹配 |
| 表达式索引 | 查询带确定性函数或运算 | 函数查询也能走索引 | 写入需重算表达式 |
合理地设计这两类索引,往往比盲目加硬件更有效。建议在数据建模阶段就梳理出高频查询模式,把部分索引与表达式索引作为常规手段写入迁移脚本,而不是等慢查询出现后再补救。
SQLitepartial_indexexpression_index修改时间:2026-08-11 23:00:47