我第一次意识到 SQL 里的 LIKE 不是“省事工具”,是在一次线上慢查询排查里。业务方想按订单号前缀查最近一批异常单据,SQL 写得很自然:SELECT * FROM orders WHERE order_no LIKE '202406%'。查询条件看起来没问题,但执行计划显示它并没有走索引,几百万行的表直接被扫了一遍。从那天起,我对 LIKE 的态度就从“会用”变成了“要理解它到底怎么匹配”。
LIKE 是 SQL 标准里最常用的模糊匹配操作符,几乎每个数据库管理系统都支持。它的语法少到看一遍文档就能写出来:一个字段、一个 LIKE、一个带通配符的字符串。但真正到了生产环境,LIKE 带来的问题往往不是“不会写”,而是“写得太随意”。比如%放左边还是放右边,会影响索引使用;_匹配一个字符容易被忽略,却在数据校验时造成误判;用户输入没有转义,会让一个本该只查几条数据的查询变成全表扫描。
所以我更愿意把 LIKE 看作一个“入门容易、做好难”的匹配原语。这篇文章不打算只罗列一遍语法,而是想聊清楚:LIKE 的模式匹配到底怎么工作,什么样的写法适合什么场景,以及当它变慢、误判、甚至变成安全入口时,我们应该按什么顺序排查和修正。
1. 先搞清楚 LIKE 是“模式匹配”,不是“内容包含”那么简单
1.1 LIKE 在 SQL 表达式中的真实角色
LIKE 在 SQL 中是一个谓词,它做的事情不是“包含某个词”,而是“判断某个字符串是否符合某种模式”。=判断的是“值完全相同”,LIKE 判断的是“字符串轮廓是否落在某个模式里”。
举个最常见的例子:
-- 查名字叫“张”的人 SELECT * FROM user_profile WHERE name = '张'; -- 查所有以“张”开头的人 SELECT * FROM user_profile WHERE name LIKE '张%';看起来只是符号不同,但表达的问题类型完全不同。LIKE '张%'不是“包含张”,而是“张后面可以跟任意内容,也可以什么都不跟”。如果需求真的是“包含张”,写法是LIKE '%张%'。很多新手会把三者混在一起,等出了问题再回头查数据,往往浪费大量时间。
这里有一个容易被忽略的语义问题:LIKE '张'其实等价于name = '张',前提是模式里没有通配符。但我不建议为了省一个字符就这么写。=的语义更清晰,也更符合代码阅读者对等值查询的预期。让代码表达意图,比让优化器替你猜要可靠得多。
另外,LIKE 返回的结果是三值逻辑:真、假、未知。NULL LIKE '%'不会返回真,而是返回未知。在WHERE子句里,只有“真”会被保留。这导致一个很反直觉的现象:你想查所有备注里包含“退货”的记录,但备注为NULL的行不会出来,即使它们占据很大比例。如果你希望“没有备注”也算一种需要关注的情况,就必须显式加上OR remark IS NULL。
1.2 不是所有模糊匹配都适合用 LIKE
LIKE 能表达的模式其实很有限。它可以做到前缀、后缀、中间包含,可以用_表达“任意一个字符”,但很难表达“包含数字但不包含字母”“要么是 3 位数字要么是 4 位大写字母”这类组合规则。
真正的复杂模式匹配,应该交给正则表达式。LIKE 更像一把小刀,适合处理简单明确的划痕;正则才是瑞士军刀,但需要更多技巧和更高成本。很多人一提到“模糊查询”就下意识写LIKE '%xxx%',等需求变成“以数字开头,后面是 6 到 8 位字母”时,LIKE 就变成一个由一堆 OR 拼接出来的怪物。
我的判断是:先用自然语言把需求说清楚,再决定匹配工具。如果需求是“包含某段固定文本”,可以考虑函数定位;如果是“字段开头/结尾满足某种简单规律”,LIKE 很合适;如果涉及字符类型、次数限制、分组交替,直接用正则,不要硬用 LIKE 凑。
2. 通配符、转义和排序规则:LIKE 最容易误判的几个细节
2.1 四个通配符的真实行为
LIKE 常见的通配符有%和_,在一些数据库里还支持字符集合。
%匹配任意长度的字符串,包括空字符串。LIKE 'a%'能匹配a、abc、a123。_匹配且仅匹配任意一个字符。LIKE 'a_c'能匹配abc、a1c,但不能匹配ac,也不能匹配abdc。[abc]匹配方括号内的任意一个字符,常见于 SQL Server 等数据库实现,比如LIKE '[张李王]%'匹配以张、李、王开头的名字。[^abc]或[!abc]匹配不在括号内的任意一个字符,同样是方言扩展,不是所有数据库都支持。
这里最大的坑不是记不住语法,而是想当然地认为所有数据库行为一致。标准 SQL 只定义了%和_,字符集合是部分数据库的扩展。MySQL 的 LIKE 默认不支持[a-z]这种写法,如果写成LIKE '[张李王]%',会被当作以左方括号开头、张李王]%结尾的普通字符串去匹配,结果为空。T-SQL 能识别,MySQL 不认识,PostgreSQL 也走自己的一套正则。跨数据库迁移时,这一条最容易埋雷。
另一个常见误判是连续使用多个_。LIKE '___'可以匹配任意 3 个字符,但如果业务想表达“数字或字母组成的 3 位编码”,_会把标点、空格也算进去。它的含义是“任意字符”,不是“任意字母数字”。要更精确的限制,只能用正则或额外加条件。
正确做法是:在写任何带_、%的查询前,先想清楚数据里到底会不会出现这些字符本身。很多订单号、备注、文件名里本来就带%和_,一旦用户搜索一个包含%的字符串,比如查“折扣5%”,LIKE 就会把它当成通配符解释,查询结果和预期完全不一样。
2.2 转义:查“5%”不是写LIKE '%5%%'就行
假设促销表里有一条记录叫618 限时 5% 返现,你想找出所有包含5%的促销名称,最容易写错的是:
-- 反例:这里的 5% 会被当成 5 后面可以跟任意内容 SELECT * FROM promotion WHERE title LIKE '%5%%';这条 SQL 的意图是“标题包含 5,并且 5 后面任意”,而不是“包含 5% 这个整体”。要想让%变成普通字符,需要声明转义字符:
-- 常见写法:用 ESCAPE 声明一个转义符 SELECT * FROM promotion WHERE title LIKE '%5\%%' ESCAPE '\';注意,不同数据库对反斜杠的处理不一样,MySQL 默认把\当作字符串转义符,SQL Server 用方括号或ESCAPE子句,PostgreSQL 也有自己的规则。所以这只是一个示例结构,真正落地前要先在当前数据库环境里跑一条验证语句。
如果你要找的是一个固定子串,而且这个子串里恰好包含通配符,我的建议是放弃 LIKE,改用函数定位。比如:
-- 找标题里包含“5%”的记录,按字面理解,不需要转义 SELECT * FROM promotion WHERE CHARINDEX('5%', title) > 0;不同数据库的函数名不同,SQL Server 用CHARINDEX,MySQL 用LOCATE,PostgreSQL 可以用POSITION。但共同点是:函数参数不会被当成通配符解释,语义更直观,也少一层转义风险。
2.3 大小写和排序规则:同一个 LIKE,在不同库里结果不一样
LIKE 是否区分大小写,不由 LIKE 本身决定,而是由字段的排序规则或 collation 决定。
- MySQL 的
utf8_general_ci不区分大小写,LIKE 'abc%'能匹配Abc。 - PostgreSQL 默认的
LIKE是区分大小写的,除非使用ILIKE或显式指定不区分大小写的 collation。 - SQL Server 的区分情况取决于列或数据库的 collation。
这个点经常让习惯了某一套数据库的人换到另一个环境后产生误判。最好的做法不是背每个数据库的默认值,而是在建表或写查询前,先确认当前列使用的排序规则,并用一条简单数据验证。案例:我之前接手一个迁移项目,代码从 MySQL 迁到 PostgreSQL,很多业务方反馈“搜索不到了”,最后发现就是大小写敏感差异,SQL 本身不需要改,但排序规则需要统一。
3. LIKE 变慢时,按这四个层次排查比直接调参更重要
3.1 通配符位置决定了索引能不能用
这是一个基础的数据库知识,但很多慢 LIKE 查询都死在这里。
普通 B+ 树索引是按照字符串的字典序组织的。LIKE 'abc%'意味着查询可以从“abc 开头的最小值”一路扫到“abc 开头的最大值”,这是一个有边界的范围扫描,所以优化器有机会使用索引。LIKE '%abc'和LIKE '%abc%'都因为不知道开头是什么,很难直接拿索引做范围定位,通常只能扫描整棵索引或回表。
所以,如果业务允许,尽量把通配符放在模式末尾,不要放在开头。比如:
-- 相对容易利用索引 WHERE order_no LIKE '202406%'; -- 通常很难利用索引 WHERE order_no LIKE '%202406%';但要注意,这并不是“一定走索引”的保证。优化器还会看数据量、统计信息、表的行数、返回行数比例等因素。如果一张小表总共只有几百行,优化器认为全表扫描比走索引更快,它也不会用索引。判断依据要交给EXPLAIN或等价的执行计划工具,不要靠猜。
3.2 排查链路:先看输入,再看执行计划,再看资源,最后看应用
遇到 LIKE 查询变慢,不要急着加索引,也不要一上来就换全文搜索。按层次排查会更有效。
第一层,看输入和结果集。确认实际执行的 SQL 里,LIKE 后的模式到底是什么。最容易被忽略的是,用户输入了一个%,程序又把它直接拼进模式,最后变成LIKE '%%%',匹配整张表。先用最小输入复现,记录实际返回行数和耗时。这一步能排除 30% 的“假慢查询”。
第二层,看执行计划。以 MySQL 为例,可以在查询前加EXPLAIN;PostgreSQL 用EXPLAIN ANALYZE;SQL Server 可以查看图形化执行计划。重点看两件事:扫描类型是不是全表扫描或全索引扫描,估算行数和实际行数差异大不大。如果发现LIKE '%abc%'导致全表扫描,这一步基本就能定位。
第三层,看数据和统计信息。字段本身是不是有函数包裹?比如WHERE DATE(create_time) LIKE '2024%',这种写法几乎无法用create_time上的索引,因为优化器要先对每一行执行函数才能判断。列类型是否隐式转换?比如 varchar 字段和数值类型比较,也容易让索引失效。还有统计信息是否过期,数据分布是否均匀。
第四层,看应用层调用方式。同样的 SQL,如果是在循环里被执行了几百次,问题不在 LIKE,而在代码结构。是否一次查询返回了过多列?业务是否只需要id,却SELECT *?是否可以用 JOIN 或预计算结果替代?这些都要一起排查。
这个排查顺序可以沉淀成一个模板,后面再遇到慢 LIKE,直接按“输入 → 执行计划 → 数据 → 应用”走一遍,比盲目改 SQL 可靠。
3.3 如果走不了索引,有哪些工程化替代
中间匹配在很多业务里避不开,比如搜索订单号“这段编号出现在某个位置”,搜索商品名“包含某个词”。如果你的表已经到百万级,LIKE '%keyword%'会带来真实成本。这时有几种常见路线,但各有边界。
第一,做冗余列。比如需要后缀匹配,就冗余一列反转后的字符串,再用LIKE 'dcba%'配合索引。这种做法能解决一部分问题,但增加了写入逻辑和一致性维护成本。适合“读多写少、查询模式固定”的场景。
第二,用全文索引。MySQL 的 FULLTEXT、PostgreSQL 的tsvector、SQL Server 的 Full-Text Search,都更适合大文本的自然语言搜索。但全文索引不是简单的子串查找,它涉及分词、词根、相关性排序,行为可能和LIKE不一样。切换之前,必须用业务真实数据跑一轮验证,特别要注意中文分词是否符合预期。
第三,引入外部搜索服务。数据量继续增大后,再靠数据库做模式匹配就不太合理了,搜索服务更适合。但引入它意味着架构复杂度上升,有运维成本,也有数据同步延迟。如果只是“几百条配置表里做个模糊筛选”,完全没必要。
一句话判断:数据量小,直接 LIKE,别过度设计;数据量大,先看能不能改成前缀匹配;前缀匹配也解决不了,再考虑全文索引或搜索服务,而不是在原 SQL 上继续打补丁。
4. 别把 LIKE 当万能模糊查询:替代方案与边界
4.1 查找固定子串,函数定位更安全
当需求不是“匹配一种模式”,而是“判断某个固定字符串是否出现”,LIKE 并不是唯一的方案,也不一定是最佳方案。
比如你要查说明列里有没有出现5%,用 LIKE 就得处理通配符转义;用CHARINDEX('5%', remark) > 0就很简单,因为函数参数是字面值,不会被解释成模式。类似需求还有:判断字符串里是否包含某个逗号、某个文件名后缀、某个固定的 SKU 前缀。
函数定位的另一个好处是语义清楚,后续维护的人不需要理解通配符。它的代价通常是很难走索引,因此在数据量大的高频查询里要谨慎。但它适合解决“固定文本存在性”的判断,尤其是特殊字符。
4.2 复杂模式用正则,但要控制成本
如果你需要“以数字开头”“中间必须是 4 位字母”“不能包含某种字符”,LIKE 的表达能力是不够的。这时应使用数据库提供的正则表达式能力。
- MySQL 支持
REGEXP,MySQL 8 开始提供REGEXP_LIKE等函数。 - PostgreSQL 支持
~、~*等操作符。 - Oracle 有
REGEXP_LIKE。 - SQL Server 原生没有内置的正则函数,通常需要借助 CLR 或外部处理,使用时必须确认版本和部署边界,不能默认它有。
正则表达式很强,但成本也高得明显。它通常无法利用普通索引,CPU 消耗高,模式写得不好还可能造成不必要的全表扫描。我的建议是:只在小结果集、内部查询或低频后台任务里用;不要直接把用户输入拼成正则模式,更不要让外部请求随意传一个正则进来。
4.3 大文本搜索,应该交给全文索引
还有一个高频误区:一遇到“搜索文章标题”“搜索商品描述”,很多人第一反应是LIKE '%关键词%'。在小数据量下没问题,但一旦数据量变大,这种查询会拖垮数据库。
全文索引和 LIKE 的区别在于,它不要求“字符串里连续出现这个子串”,而是基于词项、分词、倒排索引去匹配,还能做相关性排序。它适合大量文本的自然语言搜索,但不一定适合“必须精确包含某段字符”的业务。
举个例子,你想搜索描述里包含iPhone 15的记录。全文索引可能把iPhone和15当成两个词,匹配逻辑和 LIKE 完全不同。如果业务要求“必须完整出现iPhone 15这个连续字符串”,全文索引反而不如 LIKE 直观。
所以替代方案的边界很清晰:
| 匹配需求 | 推荐方案 | 索引利用 | 备注 |
|---|---|---|---|
| 等值匹配 | = | 好 | 语义最清晰 |
| 前缀匹配 | LIKE 'abc%' | 可走索引 | 最稳妥的 LIKE 用法 |
| 后缀匹配 | LIKE '%abc' | 通常差 | 数据量大考虑反转列或搜索服务 |
| 中间包含 | LIKE '%abc%' | 通常差 | 小表可用;大表考虑全文索引或搜索服务 |
| 包含固定特殊字符 | CHARINDEX/LOCATE/POSITION | 通常差 | 不需要考虑通配符转义 |
| 复杂字符模式 | 正则 | 通常差 | 控制结果集,避免高并发 |
4.4 别为了“看起来高效”而提前引入复杂方案
在实际项目里,我看到过不少反向踩坑:一张只有几千行的字典表,为了“支持以后扩展”就直接上 Elasticsearch,最后团队要维护一套额外服务,索引同步还有延迟。能用一个简单LIKE解决的问题,被复杂化了。
我的判断是:先量化数据量、查询频率和性能要求,再决定方案。如果查询频率低,即使全表扫描几百毫秒,业务也能接受,那就不要动。如果查询频率高、数据量大,再逐步升级方案。这个顺序,比一开始就选所谓的最强技术要稳妥得多。
5. 用户输入一旦进入 LIKE,校验和转义就不是可选项
5.1 参数化查询是底线,但不是终点
无论使用哪种数据库,都不应该用字符串拼接的方式构造 LIKE 查询。这是一个不需要讨论的底线。拼接一旦包含用户输入,就存在 SQL 注入风险。
正确的做法是使用参数化查询或预编译语句:
-- 推荐:用参数占位,而不是拼接字符串 SELECT * FROM user_profile WHERE nickname LIKE ? ;但这里必须强调:参数化解决了注入问题,不代表 LIKE 就安全了。即使你用了参数占位,用户仍然可以传入%,最终执行出来的模式是LIKE '%%%',结果就是匹配所有非空字符串。这不会导致脱库,但会让一个普通查询变成全表扫描,在高并发场景下形成明显的性能风险。
所以,处理用户输入时,要分开看两件事:第一,防止用户输入的字符串被当作 SQL 代码执行,靠参数化解决;第二,防止用户输入的通配符被当作匹配模式执行,靠转义和校验解决。只做前者,不算完整。
5.2 通配符转义:把用户输入当成普通文本来匹配
大多数业务场景里的搜索框,用户想找的是普通字面文本,而不是 SQL 通配符。用户输入%时,他的本意大概率是“包含百分号”,而不是“匹配任意内容”。
因此,一个合理的做法是先转义用户输入里的%、_、转义符本身,再放到 LIKE 模式里。伪代码可以这样理解:
function escapeLike(input): return input.replace(/[\\%_]/g, char -> '\\' + char)然后在 SQL 里写成:
SELECT * FROM product WHERE product_name LIKE '%' || escapedInput || '%' ESCAPE '\';不同数据库的字符串拼接和转义规则不同,这个示例只说明处理思路,不能直接复制到所有环境。落地之前,先在当前数据库里用几条包含%、_、\的数据验证。
除了转义,还要限制输入长度。一个非常长的搜索词,本身不会造成安全漏洞,但会让查询变得笨重,也容易让执行计划选择更差的路径。给输入加上长度上限,是成本最低的保护手段。
5.3 从查询设计上控制暴露面
如果这个 LIKE 查询来自外部接口,数据库账号不应该使用高权限账号。只读账号、限制返回行数、设置查询超时,都是兜底手段。这样即使某天因为模式写错导致全表扫描,也不至于影响整个数据库实例。
还有一点容易被忽略:监控慢查询日志时,要特别关注那些模式里包含大量通配符的语句。比如一段时间内突然出现大量LIKE '%%...%%',可能不是正常业务行为,而是有人在用特殊输入试探。这不是攻击教学,而是防御性开发的常识:接口暴露得越多,输入校验就必须越严。
6. 把模糊查询沉淀成一套可持续维护的决策框架
6.1 LIKE 使用决策清单
以后遇到任何模糊查询需求,可以按清单过一遍,而不必每次都从零开始想。
第一问:需求到底是“匹配模式”还是“查找固定内容”。如果是固定内容,比如“包含某个 SKU 后缀”,优先考虑函数定位;如果是一组字符串的规律,比如“以 A 开头,后面是数字”,再用 LIKE 或正则。
第二问:通配符出现在哪个位置。%在左还是右,主导了索引利用的可能。写 SQL 之前,先看能不能把模糊条件转成前缀匹配。
第三问:数据量级和查询频率是否支撑当前写法。几百行的小表,LIKE '%xxx%'完全没问题;几百万行的流水表,就要谨慎。先量化,再决定方案。
第四问:用户输入是否可能被通配符放大。外部搜索框里的输入,必须先做长度限制、通配符转义和参数化。
第五问:当前方案是否方便长期维护。代码里是几个简洁的 LIKE 条件,还是一长串正则表达式?新同事接手时能不能看懂?如果模式复杂到难以理解,就应该抽象成独立函数或配置。
6.2 慢 LIKE 排查模板
把前面的排查链路整理成一个可以复用的模板,直接按步骤走:
- 复现:用最小输入复现问题,记录实际返回行数和耗时。
- 拿参数:查看程序里最终执行 SQL 的具体模式,确认是否把用户输入直接拼入。
- 看执行计划:确认扫描方式、索引使用、估算行数。
- 查字段:列类型、排序规则、是否存在隐式转换或函数包裹。
- 查数据:统计信息、数据分布、表大小。
- 查调用:是否在循环中执行、返回列是否过多、并发量多大。
- 验证方案:改成前缀匹配、加索引、换函数或全文索引后,用执行计划对比效果。
这个模板不复杂,但它能避免两种常见错误:一是只看执行计划,忽略用户输入导致的全表匹配;二是只看 SQL 写法,忽略统计信息过期等环境因素。
6.3 长期来看,LIKE 不是“不能用”,而是“要用在刀刃上”
很多团队在经历一两次 LIKE 慢查询后,会对它形成一种条件反射式反感,恨不得把所有模糊查询都换成全文索引。这种倾向也不对。
LIKE 的核心价值是简单、直观、可预测。对于前缀匹配、小表筛选、低频后台搜索,它仍然是最合适的工具。问题出在“无边界使用”:把%放左边、把用户输入直接拼接、在千万级表上做任意位置匹配,还期待它表现稳定。
我给出的长期建议是:
- 能用前缀匹配,就不要做中间匹配。
- 能匹配固定文本,就不用通配符。
- 能参数化,就绝不拼接字符串。
- 能先查小结果集,就不要在大表上跑复杂模式。
- 能明确用正则或全文索引的场景,就不要让 LIKE 硬扛。
回到开头那个订单查询问题,后来的解决方式并不复杂:把需求改成“按订单号前 14 位精确匹配”,配合普通 B+ 树索引,查询时间从秒级降到毫秒级。LIKE 本身没有错,错的是我们一开始把“模糊”理解成了“随机包含任意内容”。
真正值得记住的不是 LIKE 有多少种写法,而是:一个模糊查询在进入生产环境之前,应该被认真对待过。它匹配什么、是否走索引、能否被用户输入利用、长期维护成本是多少,这些比语法本身更重要。