绝大多数开发者都背过“不要在LIKE查询前加百分号”的军规,但鲜有人深究这条规则背后复杂的代价模型。当你写下 WHERE name LIKE '%张%' 时,数据库优化器会直接放弃B+树索引的范围扫描能力,强制触发全表扫描。这并非因为索引失效,而是B+树的有序性决定了它只能利用前缀匹配——索引键值从左到右排列,百分号打头意味着匹配点可能出现在任意位置,优化器计算后发现随机回表代价远超直接扫描全表,于是选择最粗暴的执行计划。更隐蔽的陷阱是 LIKE '张_' 这种单字符匹配,虽然走了索引,但扫描范围比等值查询大了数个数量级,在高并发场景下同样会拖垮响应时间。

通配符的性能损耗不是线性的,而是随着数据量和匹配模式呈指数级恶化。以一个百万级用户表为例,WHERE name LIKE 'A%' 可能只扫描几千行就返回结果,但 WHERE name LIKE '%A%' 需要扫描全部百万行,CPU时间从毫秒级飙升到秒级。如果业务代码里还拼接了 OR 条件或者多个LIKE子句,优化器可能连索引覆盖都放弃,直接走聚簇索引全扫描。很多团队监控平台只关注慢查询阈值,却忽略了这些“中等耗时查询”在高峰期堆积造成的连接池耗尽,这才是真正的事故根源。

通配符位置决定索引命运

数据库索引的本质是排序后的数据结构。当你执行 SELECT * FROM orders WHERE order_no LIKE '2024%' 时,优化器会执行索引范围扫描(Index Range Scan),因为所有以“2024”开头的键值在B+树上物理相邻,只需定位到第一个匹配键然后顺序读取即可。但如果是 LIKE '%2024%',优化器只能选择全索引扫描或全表扫描——它必须检查每一个键值是否包含“2024”,这个过程无法利用B+树的有序性做跳跃查找。更致命的是 LIKE '%2024' 这种后缀匹配,虽然看起来只少了一个百分号,但执行计划和双百分号完全一致,都是索引失效。很多开发者误以为后缀百分号可以走索引,实际上数据库只能利用前缀做二分查找,后缀匹配必须逐行比对。

复合索引场景下这个问题被进一步放大。假设你在 (status, create_time) 上建立了联合索引,查询 WHERE status = 'active' AND create_time LIKE '2024-01%' 可以完美利用索引前导列做过滤,再对时间做范围扫描。但如果写成 WHERE status LIKE '%active%' AND create_time LIKE '%2024-01%',整个联合索引直接作废,数据库退化为全表扫描。我见过最典型的案例是某个电商后台的订单搜索功能,产品经理要求支持“任意关键词模糊匹配订单备注”,开发直接用 LIKE '%关键词%' 实现,结果备注字段是大文本类型,单次查询扫描数十GB数据,直接把只读库CPU打满。

SQL注入:通配符是攻击者的高速公路

通配符在LIKE子句中不仅是性能杀手,更是SQL注入攻击的放大器。大多数开发者知道要对单引号做转义,但很少有人意识到百分号和下划线本身就是危险字符。攻击者可以通过注入通配符改变查询语义,实现信息窃取甚至权限绕过。考虑这个典型的登录校验逻辑:SELECT * FROM users WHERE username LIKE '$input' AND password = '$hash'。如果攻击者在用户名输入框填入 admin%' -- ,最终拼接出的SQL变成 WHERE username LIKE 'admin%' --' AND password = 'xxx',注释符干掉了密码校验部分,只需匹配到admin开头的任意用户即可登录。这不是理论攻击,2019年某知名CMS就因此漏洞导致大量站点被批量入侵。

更隐蔽的攻击手法是利用下划线做精准猜测。假设有个密码重置接口根据邮箱和密保问题答案查询用户:SELECT * FROM users WHERE email = '$email' AND answer LIKE '$input'。攻击者输入 _ 可以匹配任意单字符,通过不断调整下划线数量和位置,结合应用程序返回的“答案错误”或“用户不存在”差异,可以逐字符推断出密保答案。这种侧信道攻击在CTF竞赛中屡试不爽,但在企业安全审计中却经常被忽视,因为开发者的思维定式是“我已经参数化了,通配符只是业务需求”。

参数化查询不等于绝对安全

很多技术文章把参数化查询吹捧为SQL注入的银弹,但在LIKE场景下,参数化只能防止SQL语法注入,无法阻止通配符逻辑注入。当你使用 PreparedStatement 绑定参数时,百分号和下划线作为数据值的一部分被合法传入,数据库不会将其视为语法元素,但它们仍然会改变LIKE的匹配行为。真正安全的做法是在应用层对用户输入的通配符做转义处理:

// Java示例:转义LIKE特殊字符
public static String escapeLikeWildcards(String input) {
    if (input == null) return null;
    return input
        .replace("\\", "\\\\")  // 先转义转义符本身
        .replace("%", "\\%")    // 转义百分号
        .replace("_", "\\_");   // 转义下划线
}

// 使用时配合ESCAPE子句
String sql = "SELECT * FROM products WHERE name LIKE ? ESCAPE '\\'";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "%" + escapeLikeWildcards(userInput) + "%");

注意代码中的转义顺序——必须先处理反斜杠自身,否则后续替换会引入新的转义符。ESCAPE子句告诉数据库将反斜杠视为转义字符,这样 \% 就被解释为字面百分号而非通配符。很多ORM框架并没有内置这个转义逻辑,开发者需要手动封装工具方法。我审计过的项目中,大约七成存在LIKE注入风险,其中不乏金融和医疗行业的系统,问题根源都是“参数化查询保平安”的盲目自信。

全文索引:告别LIKE的终极方案

对于真正的模糊搜索需求,全文索引是远比LIKE优雅的解决方案。MySQL的InnoDB引擎从5.6版本开始原生支持全文索引,PostgreSQL的tsvector更是在分词和排序能力上碾压LIKE。全文索引通过倒排索引技术,将文本拆分为词元后建立词到文档的映射,查询时直接定位包含关键词的文档集合,时间复杂度与索引大小相关而非数据总量。一个千万级文章的模糊搜索,LIKE '%关键词%' 需要30秒,全文索引只需要50毫秒,这是四个数量级的差距。

-- MySQL全文索引示例
ALTER TABLE articles ADD FULLTEXT INDEX ft_content (title, body);
SELECT * FROM articles WHERE MATCH(title, body) AGAINST('数据库 性能' IN BOOLEAN MODE);

但全文索引不是万能药。它有自己的局限性:最小词长度限制(MySQL默认3个字符)、停用词过滤、不支持真正的后缀匹配。如果你的搜索场景是“以某个字符串结尾”或者“包含特殊符号”,全文索引同样无能为力。这时候需要考虑Elasticsearch这类专用搜索引擎,它们基于倒排索引和分词器实现了更灵活的文本匹配能力,但引入了数据同步和一致性的架构复杂度。技术选型的关键在于诚实地评估业务需求——如果只是简单的后台管理搜索,全文索引足够;如果是面向C端的站内搜索,ES的投入是值得的。

正则表达式的性能陷阱

有些开发者觉得LIKE功能太弱,转而使用正则表达式实现复杂匹配。MySQL的REGEXP和PostgreSQL的~运算符确实强大,但代价是彻底放弃索引优化。正则表达式引擎需要对每一行数据执行状态机匹配,这个过程无法利用任何索引结构。我在性能调优中见过最夸张的案例是 WHERE content REGEXP '.*(手机|电脑|平板).*',这个查询在五百万行数据上跑了47秒,而同样的逻辑用全文索引只需要0.2秒。更糟糕的是,正则表达式容易写出灾难性回溯模式,单个匹配就可能消耗数秒CPU时间,直接阻塞其他查询。

如果业务确实需要正则匹配,务必加上前置过滤条件缩小扫描范围。比如先通过时间范围或状态字段将扫描行数降到万级以下,再应用正则表达式。PostgreSQL的trigram索引是个例外——通过pg_trgm扩展可以对LIKE和正则表达式建立GIN索引,实现模糊匹配的索引加速。它的原理是将字符串拆分为连续的三个字符组,查询时先通过三元组快速过滤候选集,再做精确匹配。这是目前关系型数据库中对模糊查询最友好的索引方案,但写入性能和存储开销需要额外评估。

工程化防御体系搭建

单点的技术优化不足以根治通配符滥用问题,需要从代码规范、监控告警、架构设计三个层面建立纵深防御。代码规范层面,在CR检查清单中强制要求LIKE查询必须注释说明索引使用情况和预期扫描行数,禁止在生产代码中使用前置百分号,除非有DBA团队的书面豁免。监控层面,对数据库慢查询日志做模式分析,自动识别包含 LIKE '% 的查询并关联到具体业务线和开发团队,设置渐进式告警——新出现的模糊查询先通知,持续超过阈值自动升级为故障工单。

架构层面,建立查询路由机制。对于模糊搜索需求,统一走搜索引擎集群而非直接查询关系型数据库。可以采用CQRS模式,写入时通过CDC将数据同步到Elasticsearch,读取时由网关层根据查询特征自动路由——精确匹配走数据库缓存,模糊搜索走搜索引擎。这套架构初期投入较大,但长期来看避免了数据库被慢查询拖垮的风险,也解耦了业务开发和DBA的相互等待。我参与过的一个电商重构项目,将订单搜索从MySQL LIKE迁移到ES后,P99延迟从12秒降到80毫秒,数据库CPU使用率下降了40个百分点。

通配符问题的本质是开发者对数据库执行模型的理解不足。B+树索引是为精确匹配和前缀匹配设计的,不是通用搜索工具。当你写下LIKE子句时,脑子里应该浮现出B+树的物理结构和优化器的成本估算逻辑,而不是把它当作一个黑盒字符串匹配函数。这种底层认知的差异,正是资深工程师和CRUD Boy的分水岭。