SQLite 的 WHERE 子句负责从表中筛选符合条件的行,很多查询性能问题和结果异常都来自对条件过滤的理解不够深入。简单等值判断只是基础,真正容易出错的是多个条件叠加时的优先级、NULL 的参与、模糊匹配的差异以及子查询过滤中的空值传播。如果这些细节处理不好,写出来的 SQL 可能返回错误结果,或者扫描范围远大于预期。本文围绕这些高级用法展开,从逻辑运算、模式匹配、子查询、表达式和索引几个角度逐一说明,并配合可运行的 SQL 示例。

一、用括号明确 AND 与 OR 的优先级
SQLite 的 WHERE 子句支持 AND、OR、NOT 等逻辑运算,其中 AND 的优先级高于 OR,这和其他主流数据库一致。如果写成 WHERE status = 1 OR role = 'admin' AND level > 5,解析时会先计算 role = 'admin' AND level > 5,再把结果与 status = 1 做 OR。这个顺序常常和直觉不一致,为了避免歧义,多个 OR 和 AND 混合时应该使用括号显式分组。
SELECT id, username, status, role, level FROM users WHERE (status = 1 OR role = 'admin') AND level >= 5;
上面的写法先圈定状态为 1 或角色为 admin 的用户,再要求等级不低于 5。如果去掉括号,逻辑会变成另一种结果。
SELECT id, username FROM users WHERE status = 1 OR role = 'admin' AND level >= 5;
此时 SQLite 会先执行 role = 'admin' AND level >= 5,再把结果与 status = 1 合并。也就是说,所有状态为 1 的用户都会被返回,而角色为 admin 且等级不足 5 的用户也会被包含进来。实际开发中建议先列出所有过滤条件,按业务逻辑分成若干括号组,再逐组测试,减少条件遗漏。
除了 AND 与 OR,NOT 运算符也可以通过改写条件来减少全表扫描。例如 NOT (status = 1) 不如直接写成 status != 1 直观,但要注意 NULL 的影响。利用德摩根定律把 NOT (a AND b) 改写成 NOT a OR NOT b,有时能帮助优化器选择不同索引。复杂条件先按优先级拆分,再用括号组合,会让 WHERE 子句的可维护性明显提升。
二、LIKE 与 GLOB 的匹配规则差异
SQLite 的 LIKE 默认不区分 ASCII 字母大小写,而 GLOB 区分大小写并且使用通配符语法更接近文件系统。LIKE 支持百分号 % 匹配任意长度字符,下划线 _ 匹配单个字符;GLOB 使用星号 * 和问号 ?,分别对应任意长度和单字符。很多开发者习惯用 LIKE 做模糊搜索,却忽略了大小写行为可能带来不符合预期的匹配结果。
SELECT title FROM articles WHERE title LIKE 'sqlite%'; -- 可以匹配 SQLite、sqlite、SQLITE 等 SELECT title FROM articles WHERE title GLOB 'SQLite*'; -- 仅匹配以大写 S 开头的 SQLite 字符串
如果希望 LIKE 区分大小写,可以执行 PRAGMA case_sensitive_like = ON,或者在列定义时使用 COLLATE BINARY,但这个设置会影响整个连接。更灵活的方法是使用 GLOB,但它只支持通配符,不支持 LIKE 的 ESCAPE 转义子句。例如要匹配包含百分号的字符串,可以写 LIKE '%\%%' ESCAPE '\',将反斜杠指定为转义字符,此时 \% 匹配字面百分号。
SELECT name FROM files WHERE name LIKE '%\%%' ESCAPE '\';
LIKE 在模式以通配符开头时无法使用普通索引,比如 LIKE '%keyword%' 会导致全表扫描。针对前缀搜索 LIKE 'keyword%' 可以走索引,因此如果有高频模糊查询,建议把匹配模式设计为前缀匹配,或者使用 FTS 全文搜索扩展。GLOB 通配符同样存在这个限制,需要评估数据量和查询频率后再决定匹配方案。
三、IN 与 EXISTS 子查询过滤的差异
IN 适合判断某个字段是否在给定集合中,但当子查询结果包含 NULL 时会出现容易忽略的问题。对于 NOT IN,如果子查询返回任何 NULL,整个 NOT IN 的结果可能为空集,原因是 NULL 表示未知,任何值与 NULL 比较既不等于也不不等于,导致排除条件无法确认。此时改用 NOT EXISTS 通常更安全。
SELECT id, name FROM customers WHERE id IN (SELECT customer_id FROM orders WHERE amount > 100);
SELECT id, name FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.amount > 100
);
IN 和 EXISTS 在语义上可以互相改写,但执行计划可能差异很大。对于小集合 IN 比较简单;对于关联子查询,EXISTS 往往能在找到第一条匹配后停止扫描,配合对应索引效率很高。SQLite 优化器对 EXISTS 子查询会做半连接优化,但仍建议在关联列上建立索引。还要注意 IN 子句中的值类型,如果列表里混合数据类型,SQLite 按类型排序和比较,可能产生意外结果。
EXISTS 的另一优势是只关心是否存在,不返回子查询具体列,因此子查询里写 SELECT 1 或 SELECT * 没有实际性能差别。IN 则要求子查询返回单列,多列比较需要使用元组 IN 的扩展写法,但 SQLite 从较新版本才支持。更通用的做法是使用 EXISTS 配合多个关联条件,可读性和兼容性更好。
四、NULL 值的过滤陷阱与 IS NULL 判断
SQLite 中 NULL 表示未知或缺失,NULL 参与任何比较运算,包括等于、不等于、大于、小于,结果都是 NULL,而不是 TRUE 或 FALSE。因此 WHERE 子句里的 column != NULL 不会返回任何行,因为 NULL 不等于任何值,包括它自己。正确判断必须使用 IS NULL 或 IS NOT NULL。
SELECT id, nickname FROM users WHERE nickname IS NULL; SELECT id, nickname FROM users WHERE nickname IS NOT NULL;
在聚合过滤或分组后,空值也会影响结果。比如 WHERE count(*) > 0 没问题,但 HAVING 中如果某列均为 NULL,SUM 可能为 NULL。对可能为 NULL 的列做算术运算,建议使用 COALESCE 或 IFNULL 提供默认值。例如 WHERE COALESCE(discount, 0) > 0.2 可避免直接比较 NULL。处理 NULL 时,可以用 CASE 表达式返回明确状态,方便组合条件。
布尔表达式中的 NULL 也受三值逻辑影响,比如 WHERE (a = 1 OR b = 2) AND c IS NULL 需要结合括号理解。建议在表设计阶段尽量给业务字段设置 NOT NULL 默认值,减少过滤时的空值分支。如果无法避免,优先使用 IS NULL 判断,不要使用 = NULL 或 != NULL。
五、表达式过滤与 CASE 条件分支
WHERE 子句中不仅可以使用列名比较,还可以使用函数、算术表达式和 CASE 表达式。表达式过滤会阻止索引的直接使用,除非创建了对应的表达式索引。比如 WHERE LOWER(name) = 'tom' 无法利用 name 列上的普通索引,但创建 LOWER(name) 的表达式索引后可以加速。
SELECT id, name FROM users WHERE LOWER(name) = 'tom';
CASE 表达式可以放在 WHERE 中,但更推荐先写清楚条件组合。例如根据不同类型使用不同阈值:
SELECT id, type, score FROM games
WHERE score > CASE type
WHEN 'easy' THEN 10
WHEN 'hard' THEN 50
ELSE 30
END;
对于日期范围过滤,直接使用字符串比较通常可行,因为 SQLite 日期存储为 TEXT 且格式固定时字典序与时间序一致。但更规范的做法是使用 date 函数或 julianday 计算。表达式过滤会让查询变慢,所以高频条件应避免在列上包函数,可以把函数移到比较值一侧,例如 WHERE created_at >= date('now','-7 day') 函数在右侧,对 created_at 索引友好。
六、利用索引提升 WHERE 过滤性能
WHERE 条件高效执行的关键在于减少需要扫描的行数。SQLite 使用 B-Tree 索引加速等值、范围和前缀 LIKE 查询。多列复合索引需要遵循最左前缀原则,例如索引 (status, created_at) 可以加速 WHERE status = 1 AND created_at > '2024-01-01',但单独按 created_at 过滤用不上。
CREATE INDEX idx_orders_status_time ON orders(status, created_at); SELECT order_id FROM orders WHERE status = 'paid' AND created_at >= '2024-01-01';
对于 OR 条件,SQLite 可能分别使用不同索引再合并结果,但如果 OR 两侧覆盖完全不同且数据量大,索引合并性能不如 UNION ALL 显式拆分。可以用 EXPLAIN QUERY PLAN 查看实际执行计划,确认是否使用了索引。部分索引也是优化 WHERE 的有力工具,只对满足特定条件的行建立索引,例如只对未删除数据建索引,减小体积并提升写入性能。
CREATE INDEX idx_active_users ON users(email) WHERE deleted = 0;
如果查询中同时包含 ORDER BY 和 WHERE,应尽量让索引同时满足过滤和排序,避免额外排序。对于结果集较大的查询,考虑使用覆盖索引减少回表。通过这些手段,WHERE 子句不仅能返回正确结果,还能保持较好的响应速度。注意不要盲目给每列单独建索引,过多的索引会拖慢写入和占用空间,应该根据实际查询频率和条件组合来设计。
SQLite WHERE子句条件过滤SQL查询优化修改时间:2026-10-07 05:28:09