前言
先说结论!!!
- 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 查询使用索引需要满足:
- ✅ 左侧不能有通配符:
LIKE 'abc%'(满足) - ✅ 只能使用 ASCII 字符(满足)
- ✅ 不能使用 ESCAPE 子句(满足)
- ❌ 右侧必须是字面量,不能是参数(不满足!)
- ❌ 大小写敏感设置必须匹配索引排序
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 vs SEARCH 对比
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)
优势:
- ✅ 不依赖 PRAGMA 设置(更可靠)
- ✅ 支持参数化查询(防止 SQL 注入)
- ✅ 性能始终优秀(无论 case_sensitive_like)
- ✅ 代码简洁,无需额外配置
注意事项:
- 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 |