避开 case_sensitive_like 坑:SQLite 范围查询替代 LIKE 的性能优化之路

前言

先说结论!!!

  • sqlite 的参数case_sensitive_like为OFF时,Like的前缀模糊会走全表查询
  • sqlite的索引是区分大小写,具备有序性

一、问题发现

1.1 用户报告

搜索 URI 时,发现搜索时间极长:

复制代码
搜索: /api/local/boce
结果: 仅 1 条记录
耗时: 324 秒(5 分多钟)

1.2 性能监控日志部署

为了定位具体的性能瓶颈,添加了性能监控日志:

修改位置 : get_field_ids 方法

代码变更:

python 复制代码
# 在查询开始前记录时间
_perf_start = time.time()

# ... 执行查询 ...

# 查询完成后记录性能日志
_perf_elapsed = time.time() - _perf_start
_status = "✓" if _perf_elapsed < 0.5 else "⚠️" if _perf_elapsed < 2.0 else "❌"
public(
    "[SQL性能]{} get_field_ids[{}] 耗时: {:.3f}秒 | type={} | value={} | results={}".format(
        _status, Table, _perf_elapsed, query_type, normalized_value[:50],
        len(field_value) if field_value else 0
    ),
    color="success" if _perf_elapsed < 0.5 else "warning" if _perf_elapsed < 2.0 else "error"
)

1.3 性能日志分析

通过日志输出,精确定位了性能瓶颈:

log 复制代码
[SQL性能]❌ get_field_ids[uri_list] 耗时: 324.112秒 | type=uri_prefix | value=/api/local/boce | results=1
[SQL性能]❌ get_url_id 耗时: 324.113秒 | url=/api/local/boce | results=1
[SQL性能]✓ count_query 耗时: 0.056秒 | conditions=3 | total=19172
[SQL性能]✓ join_query 耗时: 0.123秒 | limit=20 | offset=0 | results=19

关键发现 : 100% 的延迟(324.112秒)都在 get_field_ids[uri_list] 查询中,其他所有操作加起来仅 0.179 秒。


二、问题定位

2.1 数据库结构分析

表结构:

sql 复制代码
CREATE TABLE `test` (
    `uri_id` INTEGER PRIMARY KEY AUTOINCREMENT,
    `uri` TEXT DEFAULT "" NOT NULL,
    `time` INTEGER NOT NULL DEFAULT 0
);

CREATE UNIQUE INDEX `idx_uri` ON `test` (`uri`);

数据规模:

  • 记录数: 2,807,293(约 280 万条)
  • 数据库文件大小: 237 MB
  • 索引大小: 约 156 MB(推算)

2.2 查询语句分析

原始代码:

python 复制代码
elif normalized_value.startswith('/'):
    like_value = "{}%".format(normalized_value)
    field_value = db_obj.table(Table).where('{} like ?'.format(Key), (like_value,)).field(Field).select()

生成的 SQL:

sql 复制代码
SELECT uri_id FROM uri_list WHERE uri LIKE '/api/local/boce%'

2.3 查询计划检查

使用 EXPLAIN QUERY PLAN 分析查询执行计划:

bash 复制代码
$ sqlite3 uri_list.db "EXPLAIN QUERY PLAN SELECT uri_id FROM uri_list WHERE uri LIKE '/api/local/boce%'"
QUERY PLAN
`--SCAN uri_list USING COVERING INDEX idx_uri

关键发现 : 使用 SCAN(全索引扫描),而不是 SEARCH(索引查找)!

2.4 对比测试

同时测试其他查询方式:

查询类型 SQL 查询计划 耗时
精确匹配 WHERE uri = '/api/local/boce' SEARCH <0.01秒
LIKE 查询 WHERE uri LIKE '/api/local/boce%' SCAN 324秒
范围查询 WHERE uri >= '/api/local/boce' AND uri < '/api/local/boce\uffff' SEARCH <0.01秒

三、根本原因分析

3.1 初步假设:前缀索引问题

疑问 : 为什么前缀匹配(LIKE '/api/local/boce%')不使用索引?

假设: 可能需要创建专门的前缀索引

sql 复制代码
CREATE INDEX idx_uri_prefix ON uri_list(uri COLLATE NOCASE);

验证: 通过测试发现,即使添加前缀索引,LIKE 查询仍然使用 SCAN。

结论: 前缀索引方案无效。

3.2 深入分析:SQLite LIKE 优化限制

SQLite LIKE 优化的条件

根据 SQLite 官方文档,LIKE 查询使用索引需要满足:

  1. ✅ 左侧不能有通配符:LIKE 'abc%'(满足)
  2. ✅ 只能使用 ASCII 字符(满足)
  3. ✅ 不能使用 ESCAPE 子句(满足)
  4. 右侧必须是字面量,不能是参数(不满足!)
  5. 大小写敏感设置必须匹配索引排序

3.3 核心发现:case_sensitive_like = OFF

测试验证
python 复制代码
# 测试 case_sensitive_like 设置的影响
for setting in ['OFF', 'ON']:
    cursor.execute(f'PRAGMA case_sensitive_like={setting}')

    cursor.execute("EXPLAIN QUERY PLAN SELECT * FROM t WHERE uri LIKE '/api/%'")
    plan = cursor.fetchone()

    print(f'case_sensitive_like={setting}: {plan}')

测试结果:

复制代码
case_sensitive_like = OFF:
┌─────────────────────────────────────────────────────────────┐
│ LIKE '/api%'  →  SCAN (全表扫描)                             │
│ 匹配结果: ['/api/local/boce', '/API/LOCAL/BOCE', '/api/other']│
│              ↑ 不区分大小写,匹配了所有大小写变体              │
└─────────────────────────────────────────────────────────────┘

case_sensitive_like = ON:
┌─────────────────────────────────────────────────────────────┐
│ LIKE '/api%'  →  SEARCH (索引查找) ✓                         │
│ 匹配结果: ['/api/local/boce', '/api/other']                  │
│              ↑ 区分大小写,只匹配小写                          │
└─────────────────────────────────────────────────────────────┘
根本原因总结
复制代码
┌────────────────────────────────────────────────────────────────────┐
│                    问题根本原因                                    │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  1. SQLite 默认: case_sensitive_like = OFF                        │
│     → LIKE 查询不区分大小写                                        │
│                                                                    │
│  2. 索引排序: BINARY(区分大小写)                                 │
│     → idx_uri 按 A < a < B < b ... 排序                           │
│                                                                    │
│  3. 冲突: 不区分大小写的查询无法使用区分大小写的索引               │
│     → LIKE '/api/%' 需要匹配 '/API/...', '/Api/...', '/api/...'   │
│     → 但 BINARY 索引中这些值分散在不同位置                         │
│     → SQLite 无法直接跳到所有匹配区域                              │
│                                                                    │
│  4. 结果: SCAN(全表扫描)→ 324 秒                                │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

3.4 B-tree 索引结构分析

索引中的实际数据顺序
复制代码
B-tree 索引按 BINARY 排序(区分大小写):
┌─────────────────────────────────────────────────────────┐
│  索引条目 (按字典序)                                      │
├─────────────────────────────────────────────────────────┤
│  /                                                     │
│  /                                                     │
│  /API/LOCAL/BOCE      ← 大写,与小写分开存储             │
│  /API/other                                            │
│  /Api/Local/Boce      ← 混合大小写,又是另一个位置       │
│  /api/index.php                                       │
│  /api/local/boce      ← 小写目标位置                    │
│  /api/local/boce/test                                   │
│  /api/other                                            │
│  ... (280万条记录继续)                                 │
└─────────────────────────────────────────────────────────┘

SCAN (全表扫描):

复制代码
LIKE '/api/local/boce%' (case_sensitive_like=OFF)

需要: 匹配所有大小写变体
     - /api/local/boce
     - /API/LOCAL/BOCE
     - /Api/Local/Boce
     ...

问题: 这些值分散在索引的不同位置
     → 无法直接跳转
     → 必须扫描整个索引
     → 280万条 × 0.73 MB/s ≈ 324 秒

SEARCH (索引查找):

复制代码
uri >= '/api/local/boce' AND uri < '/api/local/boce\uffff'

利用: B-tree 的有序性
     1. 二分查找定位起点: O(log n) ≈ 22 次
     2. 顺序读取匹配记录: O(k),k=结果数
     → 22 + 1 ≈ 0.01 秒

四、解决方案

原理:

  • 将前缀匹配 LIKE '/prefix%' 转换为范围查询
  • uri >= '/prefix' AND uri < '/prefix\uffff'
  • \uffff 是 Unicode 最大字符 (U+FFFF)

优势:

  1. ✅ 不依赖 PRAGMA 设置(更可靠)
  2. ✅ 支持参数化查询(防止 SQL 注入)
  3. ✅ 性能始终优秀(无论 case_sensitive_like)
  4. ✅ 代码简洁,无需额外配置

注意事项:

  • URI 路径通常是区分大小写的(符合规范)
  • 极少数包含 \uffff 字符的 URI 不会被匹配(可接受的边缘情况)

修改后:

python 复制代码
elif normalized_value.startswith('/'):
    field_value = db_obj.table(Table).where('{} >= ? AND {} < ?'.format(Key, Key), (normalized_value, normalized_value + '\uffff')).field(Field).select()

五、优化结果

5.1 性能对比

搜索内容 优化前 优化后 提升倍数
/api/local/boce 324.112秒 0.01X秒 ~32,000倍
/ 50秒 0.1秒 ~500倍
/api/index.php 5秒 0.01秒 ~500倍

5.3 查询计划对比

优化前:

sql 复制代码
EXPLAIN QUERY PLAN SELECT uri_id FROM uri_list WHERE uri LIKE '/api/local/boce%';
-- SCAN uri_list USING COVERING INDEX idx_uri

优化后:

sql 复制代码
EXPLAIN QUERY PLAN SELECT uri_id FROM uri_list
WHERE uri >= '/api/local/boce' AND uri < '/api/local/boce\uffff';
-- SEARCH uri_list USING COVERING INDEX idx_uri (uri>? AND uri<?)

六、技术总结

6.1 关键知识点

1. SQLite 查询计划
类型 说明 性能
SCAN 全表/全索引扫描 O(n),慢
SEARCH 索引二分查找 O(log n + k),快
2. B-tree 索引
  • B-tree 是有序的,支持范围查询
  • 二分查找定位:O(log n)
  • 顺序读取匹配记录:O(k)
3. LIKE 查询优化限制
python 复制代码
# SQLite LIKE 优化生效条件
WHERE col LIKE 'abc%'     # ✅ 前缀固定,字面量
WHERE col LIKE ?          # ❌ 使用参数
WHERE col LIKE 'abc%' COLLATE BINARY  # ✅ 指定 BINARY 排序
4. case_sensitive_like
设置 LIKE 行为 索引使用
OFF(默认) 不区分大小写 SCAN(除非索引匹配)
ON 区分大小写 SEARCH
相关推荐
爱可生开源社区1 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1771 天前
《从零搭建NestJS项目》
数据库·typescript
加号32 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏2 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐2 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再2 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest2 天前
数据库SQL学习
数据库·sql
jnrjian2 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle
十月南城2 天前
数据湖技术对比——Iceberg、Hudi、Delta的表格格式与维护策略
大数据·数据库·数据仓库·hive·hadoop·spark
Henry Zhu1232 天前
数据库:并发控制基本概念
服务器·数据库