做搜索功能时,精确匹配几乎永远不够用。用户会打错字、会漏字、会把“张三丰”输成“张三风”,甚至直接输入拼音“zhangsanfeng”就指望系统能给出结果。SQLite作为嵌入式数据库中最流行的选择,其实提供了一整套应对模糊查询的工具,从最基础的LIKE,到GLOB,再到FTS5全文索引,配合自定义函数还能实现编辑距离计算。这篇文章把这些能力串起来,做一个能容错的搜索模块。

一、LIKE与GLOB:两种通配符体系的差异
SQLite里最常用的模糊匹配手段是LIKE。它支持两个通配符:百分号%匹配任意长度的任意字符(包括空串),下划线_匹配恰好一个字符。例如WHERE name LIKE '张%'能命中所有姓张的记录。需要注意的一点是,LIKE默认对ASCII字母不区分大小写,但对中文没有大小写概念,所以这一点在中文场景影响不大,可一旦数据里混有英文就要留意。
与LIKE对应的还有GLOB运算符,它的语法类似Unix shell:*匹配任意长度字符,?匹配单个字符,另外还支持[a-z]这样的字符区间。GLOB严格区分大小写,且无法通过PRAGMA修改这个行为。两者的底层实现也不同:LIKE在未建立索引时会走全表扫描,但如果字段有索引且模式是'abc%'这种前缀确定的形式,SQLite可以改用范围扫描;GLOB同样支持前缀优化。下面是一组对比示例:
-- LIKE:不区分英文大小写 SELECT * FROM users WHERE name LIKE 'tom%'; -- 命中 Tom、tommy、TOM -- GLOB:严格区分大小写,支持字符区间 SELECT * FROM users WHERE name GLOB 'T*'; -- 只命中大写T开头 SELECT * FROM users WHERE name GLOB '[a-c]*'; -- 命中a、b、c开头的记录 -- 前缀确定的模式可利用索引 SELECT * FROM users WHERE name LIKE '王_'; -- 姓王且名字共两个字
还有一个容易被忽略的坑:如果用户输入的内容本身包含%或_,直接拼进SQL会产生错误匹配,甚至造成注入风险。正确做法是用ESCAPE子句指定转义符:
-- 用户输入 "50%" 时,转义百分号避免被当作通配符 SELECT * FROM products WHERE title LIKE '%50!%%' ESCAPE '!';
二、FTS5全文索引:让中文搜索又快又灵活
LIKE '%关键词%'这种形式因为前缀不确定,无法利用普通索引,数据量一大查询就会明显变慢。SQLite的FTS5扩展是专门为全文检索设计的虚拟表模块,它会把文本切分成词元并建立倒排索引,查询复杂度与命中文档数相关,而不是与表的总行数相关,几十万行的数据也能毫秒级返回。
FTS5默认按空格和标点分词,这对英文够用,对中文却不友好——整段中文会被当成一个词元,导致搜不到中间的字词。解决办法有两个:一是利用FTS5的unicode61分词器配合tokenize='unicode61',它会把CJK字符逐字切分,适合做单字匹配;二是通过SQLite3可加载扩展机制接入jieba等中文分词器,获得真正的词语级索引。对于人名搜索这类场景,逐字切分反而更合适,因为人名没有固定词汇边界。
-- 创建FTS5虚拟表,逐字切分中文
CREATE VIRTUAL TABLE users_fts USING fts5(name, content='users', content_rowid='id', tokenize='unicode61');
-- 通过触发器保持索引同步
CREATE TRIGGER users_ai AFTER INSERT ON users BEGIN
INSERT INTO users_fts(rowid, name) VALUES (new.id, new.name);
END;
CREATE TRIGGER users_ad AFTER DELETE ON users BEGIN
INSERT INTO users_fts(users_fts, rowid, name) VALUES('delete', old.id, old.name);
END;
CREATE TRIGGER users_au AFTER UPDATE ON users BEGIN
INSERT INTO users_fts(users_fts, rowid, name) VALUES('delete', old.id, old.name);
INSERT INTO users_fts(rowid, name) VALUES (new.id, new.name);
END;
-- 全文检索,支持前缀匹配
SELECT u.* FROM users u
JOIN users_fts f ON u.id = f.rowid
WHERE users_fts MATCH '张*';
FTS5的MATCH语法支持前缀*、NEAR邻近查询、列过滤等高级特性。比如'name:张 OR name:王'限定只在name列搜索,'张 NEAR/3 三'要求两个词距离不超过3个词元。这套能力足以覆盖绝大多数业务搜索需求,而且全部内置在SQLite中,不需要额外部署搜索服务。
三、基于编辑距离实现拼写纠正
前面两种手段解决的是“模式匹配”问题,但用户输入“zhang san feng”想找“张三丰”,或者把“数据库”打成“数剧库”,这时需要的是相似度计算。经典方案是编辑距离(Levenshtein Distance):衡量把一个字符串变成另一个字符串所需的最少编辑操作数,包括插入、删除、替换。距离越小,两个词越相似。
SQLite本身没有内置编辑距离函数,但可以通过Python的sqlite3模块注册自定义函数,把计算逻辑注入SQL引擎。这样就能在SQL中直接按相似度排序,从候选集中挑出最可能的正确词条:
import sqlite3
def levenshtein(a, b):
if len(a) < len(b):
a, b = b, a
prev = list(range(len(b) + 1))
for i, ca in enumerate(a, 1):
cur = [i]
for j, cb in enumerate(b, 1):
cur.append(min(prev[j] + 1, # 删除
cur[j - 1] + 1, # 插入
prev[j - 1] + (ca != cb))) # 替换
prev = cur
return prev[-1]
conn = sqlite3.connect('app.db')
conn.create_function('edit_dist', 2, levenshtein)
# 找出与用户输入最相近的3个名字
keyword = '张三风'
rows = conn.execute('''
SELECT name, edit_dist(name, ?) AS d
FROM users
WHERE edit_dist(name, ?) <= 2
ORDER BY d LIMIT 3
''', (keyword, keyword)).fetchall()
print(rows)
直接对全表计算编辑距离开销不小,工程上一般先用低成本手段缩小候选集:比如取输入的第一个字做LIKE前缀过滤,或者维护一张拼音列(借助pypinyin库生成),让拼音输入也能命中中文字段。候选集缩到几百条以内后,再逐条算编辑距离就非常快了。这种“粗筛加精排”的两段式结构,是所有拼写纠正系统的通用架构。
四、完整方案整合与性能建议
把上面三层能力组合起来,就能得到一个实用的搜索管线:第一层用FTS5索引做全文匹配,命中则直接返回;第二层用拼音列做等值或前缀匹配,覆盖拼音输入场景;第三层对粗筛后的候选集计算编辑距离,给出“您是不是想找”的纠错建议。三层互为补充,绝大多数输入都能得到合理反馈。
性能方面有几点经验值得注意。首先,LIKE '%xx'和LIKE '%xx%'永远走不了索引,高频查询应尽量改写为前缀形式或交给FTS5。其次,FTS5索引会占用额外磁盘空间,插入和更新也有触发器开销,写多读少的表要权衡是否建索引。第三,自定义函数在SQLite中是逐行调用的,Python函数与C引擎之间有调用开销,编辑距离计算务必先做候选集过滤,或者把高频词条的相似度结果缓存到一张纠错表中。最后,记得对用户输入统一做去空格、全角转半角、繁简归一化等预处理,很多“模糊匹配失败”其实是格式不一致造成的,预处理做好后问题就消失了一大半。
SQLite模糊匹配SQLite LIKE查询拼写纠正修改时间:2026-09-14 23:58:44