ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL LIKE模糊查询的陷阱:从慢查询到索引优化与安全转义

SQL LIKE模糊查询的陷阱:从慢查询到索引优化与安全转义 我第一次意识到 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 不是所有模糊匹配都适合用 LIKELIKE 能表达的模式其实很有限。它可以做到前缀、后缀、中间包含可以用_表达“任意一个字符”但很难表达“包含数字但不包含字母”“要么是 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 用CHARINDEXMySQL 用LOCATEPostgreSQL 可以用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 为例可以在查询前加EXPLAINPostgreSQL 用EXPLAIN ANALYZESQL 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 支持REGEXPMySQL 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 有多少种写法而是一个模糊查询在进入生产环境之前应该被认真对待过。它匹配什么、是否走索引、能否被用户输入利用、长期维护成本是多少这些比语法本身更重要。
返回列表