SQLite LIKE 字面搜索:参数绑定之后,百分号和下划线仍然需要转义

10-01 3阅读

用户输入文件名片段“报告_终稿”,搜索结果却出现“报告甲终稿”。即使查询使用了问号占位符,这个结果仍可能完全符合数据库规则:参数绑定确保输入作为数据传递,而 LIKE 会继续把数据里的下划线解释成单字符通配符。

搜索框如果承诺按用户输入的文字查找,就需要在进入 LIKE 之前处理模式字符。百分号表示任意长度的片段,下划线表示一个字符;若只是把关键词放在两个百分号中间,并没有实现真正的字面包含搜索。

SQLite LIKE 字面搜索:参数绑定之后,百分号和下划线仍然需要转义

AI概念配图,非真实界面:以抽象物件说明本文主题,不代表运行结果。

为模式选择一个明确的转义字符

下面程序用 Python 3 和内置 sqlite3 创建内存表。它选感叹号作为转义符,避免示例同时被 Python 反斜杠和 SQL 字符串规则干扰。函数依次转义感叹号、百分号和下划线,再在最外层添加表示包含关系的通配符。

顺序不能交换。如果最后才把感叹号翻倍,前面为了保护百分号刚加进去的感叹号也会被再次处理,含义就变了。数据库查询中的 ESCAPE 必须与构造函数使用同一个字符;只改其中一边,会让生成的模式变成另一种搜索语言。

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE files(id INTEGER PRIMARY KEY, name TEXT)")
con.executemany("INSERT INTO files(name) VALUES (?)", [
    ("报告_终稿",), ("报告甲终稿",), ("折扣50%",),
    ("折扣50元",), ("wow!",), ("x!_%y",), (None,),
])

def contains_literal(term):
    if not isinstance(term, str) or not term:
        raise ValueError("nonempty text required")
    escaped = term.replace("!", "!!").replace("%", "!%").replace("_", "!_")
    return "%" + escaped + "%"

def search(term):
    return [row[0] for row in con.execute(
        "SELECT name FROM files WHERE name LIKE ? ESCAPE '!' ORDER BY id",
        (contains_literal(term),),
    )]

loose = [r[0] for r in con.execute(
    "SELECT name FROM files WHERE name LIKE ? ORDER BY id", ("%报告_终稿%",)
)]
assert loose == ["报告_终稿", "报告甲终稿"]
assert search("报告_终稿") == ["报告_终稿"]
assert search("50%") == ["折扣50%"]
assert search("wow!") == ["wow!"]
assert search("!_%") == ["x!_%y"]
assert search("不存在") == []
try:
    search("")
except ValueError:
    pass
else:
    raise AssertionError("empty term accepted")
print("wildcard:", loose)
print("literal:", search("报告_终稿"), search("50%"))
print("escape and empty-input checks passed")
con.close()

参数绑定和模式转义是两道不同处理

第一条宽松查询会返回两条记录,说明下划线仍具有通配作用;字面搜索只返回原文确实包含下划线的记录。另一组用百分号复现相同差异,并额外测试转义符本身。打印结果可以直接和断言对照,不需要凭页面展示猜测原因。

最终模式仍通过问号占位符传入,不能因为做过 LIKE 转义就改成字符串拼接 SQL。搜索语义转义没有处理 SQL 字面量引号,也不负责授权和字段选择;把这两层混为一谈,会让一次功能修复同时引入另一类错误。

代码拒绝空关键词,是为了避免外层两个百分号匹配大量记录。也可以把空输入定义为展示首页或直接返回空结果,但必须写进接口规则。NULL 字段不会因为模式是全通配就自动成为匹配项,缺失名称的记录需要单独处理。

把查询承诺限定在确实实现的范围

LIKE 的字面模式只解决百分号、下划线与转义符的含义,并没有把比较方式改成逐字节相等。SQLite 默认对英文大小写进行有限折叠,其他文字的行为另有边界。若界面要求区分大小写或完整 Unicode 规则,应另外选择并测试比较策略。

包含搜索前面带有通配符,数据量大时可能难以利用普通索引快速定位。正确转义不代表性能自动解决;先用代表性数据看执行计划与耗时,再决定是否需要专门的全文搜索或其他索引设计,不要把小样本速度当成容量结论。

验收样本应覆盖单独的百分号、下划线、感叹号、它们相邻出现以及完全没有特殊字符的输入。把构造模式集中在一个函数中,所有入口共用同一套测试。以后更换数据库或搜索后端时,也能明确识别需要重新验证的语义。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。