关于 SQL 中的 LIKE,很多初学者容易陷入一个误区:以为它只是“模糊查询”而已,写个%关键词%就能走遍天下。但真正上过生产环境、写过慢查询排查报告、或者被 SQL 注入漏洞折腾过的人会明白,LIKE 远不止“模糊匹配”这么简单——它既可以是快速筛选数据的利器,也可能成为拖垮数据库性能的隐患,甚至在你疏忽大意时变成攻击者绕过认证的入口。
这篇文章会从 LIKE 的基础语法讲起,把%、_、转义符这些通配符掰开揉碎,再结合性能优化和安全性问题,给出实际项目中可落地的建议。无论你是刚学 SQL 的学生、准备面试的求职者,还是需要维护业务数据库的工程师,这篇文章都值得收藏备用。
1. 为什么 LIKE 值得你花时间系统学习
在数据库管理系统中,WHERE条件里的精确匹配用等号=就能解决,但现实业务里我们经常面临的是“不确定的查询条件”。
典型的场景包括:
- 搜索框里输入“华为”,期望查出“华为 Mate 60 Pro”“华为 MatePad”“华为智能手表”等所有相关商品。
- 输入手机号后四位“8899”,需要反查用户全号。
- 统计所有以“2024”开头的订单编号。
- 判断某个 URL 是否包含指定域名关键字。
这些场景用=完全无能为力,而LIKE正是为这类“模式匹配”而生的操作符。它通过配合通配符(wildcard),让我们不需要知道完整值,只需要描述出一段“模式”,就能把符合条件的数据捞出来。
但使用 LIKE 的代价也不小。如果对 LIKE 的执行原理不够了解,随手写一个LIKE '%关键字%'放到几百万行的表上,就可能导致一次全表扫描,接口响应从 50 毫秒变成 5 秒。另一面,如果把用户输入直接拼进 SQL 语句,LIKE 关键字还会被用于构造注入 payload,造成严重的数据安全事故。
所以,这篇关于 LIKE 的文章要解决的,不只是“怎么写”,还包括“什么时候用”“怎么写才快”“怎么写才安全”。
2. LIKE 基础语法与核心概念
先明确技术定义:LIKE是 SQL 标准中的模式匹配运算符,用于在WHERE子句中判断某个字段的值是否匹配指定的字符模式。它返回布尔结果:匹配返回TRUE,不匹配返回FALSE,如果字段值为NULL,则结果既不是TRUE也不是FALSE,而是UNKNOWN。
2.1 基本语法结构
SELECT 列名 FROM 表名 WHERE 列名 LIKE '模式';当模式中不包含任何通配符时,LIKE 的行为等同于=。
-- 等价于 WHERE name = '张三' SELECT * FROM users WHERE name LIKE '张三';这一点常被忽略,但理解它对后续排查“为什么 LIKE 没生效”很有帮助。
2.2 LIKE 与等号、IN 的对比
| 操作符 | 匹配方式 | 典型场景 | 索引利用情况 |
|---|---|---|---|
= | 精确匹配 | 查 id=100 的记录 | 支持索引,性能最好 |
IN | 枚举匹配 | 查 id IN (1,2,3) | 支持索引,性能较好 |
LIKE | 模式匹配 | 查 name LIKE '张%' | 前缀匹配时可部分利用索引;前后都带%时基本全表扫描 |
这里要强调一个核心判断:LIKE 不是不能走索引,而是取决于通配符的位置。模式以固定前缀开头(如'张%'),数据库查询优化器有较大机会使用 B+ 树索引进行范围扫描;而模式以%开头(如'%张'),因为无法确定起始位置,大多数数据库会退化为全表扫描。这一点后面章节会专门展开。
3. 通配符全面解析:% 、_ 、转义符
LIKE 模式匹配的核心在于通配符,如果对通配符理解不准确,很容易写出“查不到数据”或“查出脏数据”的语句。
3.1 百分号%
%匹配任意数量的字符,包括零个字符。
-- 找出所有以"张"开头的姓名 SELECT * FROM users WHERE name LIKE '张%'; -- 找出所有包含"工程师"的职位名称 SELECT * FROM jobs WHERE title LIKE '%工程师%'; -- 找出所有以.com结尾的邮箱 SELECT * FROM users WHERE email LIKE '%.com';需要特别注意的是:
%可以匹配零个字符,所以LIKE '张%'也能匹配“张”这个单字。LIKE '%%'表示匹配所有非 NULL 的值,这在实际代码 review 中属于典型的无意义条件。
3.2 下划线_
_精确匹配一个字符。它比%严格得多,一个_对应且仅对应一个字符。
-- 匹配以"张"开头,后面紧跟两个字的姓名 SELECT * FROM users WHERE name LIKE '张__'; -- 匹配手机号第二位是5的记录 SELECT * FROM users WHERE phone LIKE '_5%';新手最常见的错误是把_和%混用,比如想匹配“张”开头的所有名字,却写成了LIKE '张_',结果只返回了两个字的名字,漏掉了三个字以上的记录。
3.3 中括号[]与排除符[^](SQL Server 等数据库支持)
在 SQL Server 中,LIKE 还支持字符列表匹配。
-- 匹配以 A、B 或 C 开头的产品代码 SELECT * FROM products WHERE product_code LIKE '[ABC]%'; -- 匹配 NOT 以 A、B、C 开头的产品代码 SELECT * FROM products WHERE product_code LIKE '[^ABC]%';需要注意,这个语法不是所有数据库都支持。MySQL 中[ABC]会被当作普通字符处理,PostgreSQL 则需要通过正则表达式或SIMILAR TO实现类似效果。跨数据库迁移时,这里是一个容易踩坑的兼容性问题。
3.4 转义符与特殊字符匹配
如果业务数据本身包含%或_,直接写LIKE '%50%'会把“50%”当作任意字符加“50”加任意字符,结果会出乎意料。
此时需要使用ESCAPE子句指定转义符号。
-- 在 MySQL / PostgreSQL / SQL Server 中通用写法 SELECT * FROM products WHERE discount_rate LIKE '50!%' ESCAPE '!';上述语句中,!被定义为转义符,!%表示真正的百分号字符,而不是通配符。同理,!_表示真正的下划线。
判断是否应该转义的规则很简单:当搜索关键字本身含有%、_或自定义转义符时,必须先转义再拼接模式。
4. 完整示例:从单表查询到多条件组合
为了让案例可以实际运行,我们模拟一个电商订单表。假设表结构如下:
CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_name VARCHAR(50), product_name VARCHAR(100), order_no VARCHAR(50), order_status VARCHAR(20) );插入几条测试数据:
INSERT INTO orders VALUES (1, '张三', '华为 Mate 60 Pro', '20240101001', 'PAID'), (2, '李四', '苹果 iPhone 15', '20240201002', 'UNPAID'), (3, '张伟', '华为 MatePad', '20240301003', 'SHIPPED'), (4, '王五', '小米电视', '20240401004', 'PAID'), (5, '张芳', '华为智能手表', '20240501005', 'CANCELLED');4.1 场景一:搜索框前缀匹配
业务需求是搜索“华为”相关商品,搜索框输入“华为”后,后端应查询 product_name 以“华为”开头的记录。此时应使用前缀匹配:
SELECT order_id, customer_name, product_name FROM orders WHERE product_name LIKE '华为%';执行结果会返回订单 1、3、5。这种写法是 LIKE 查询中对索引最友好的形式。
4.2 场景二:任意位置模糊匹配
如果业务要求搜索所有包含“Mate”的商品,因为“Mate”可能出现在品牌名之后,也可能出现在名称末尾,所以需要前后都加%:
SELECT order_id, customer_name, product_name FROM orders WHERE product_name LIKE '%Mate%';这种写法会返回“华为 Mate 60 Pro”和“华为 MatePad”,但数据库大概率需要扫描整张表的 product_name 列。数据量小时无所谓,数据量大了就要考虑性能预案。
4.3 场景三:按订单号后缀匹配
订单号是20240101001,业务需要查所有以001结尾的订单。此时通配符在前,是典型的“无法走索引”查询:
SELECT * FROM orders WHERE order_no LIKE '%001';如果订单号表数据极大,建议额外存储“订单号倒序”字段,或者使用数据库的生成列(Generated Column)来优化,否则每次查询都是全表扫描。
4.4 场景四:多条件组合查询
LIKE 可以和AND、OR、IN、BETWEEN组合使用,用于构建复杂的业务筛选逻辑。
SELECT order_id, customer_name, product_name, order_status FROM orders WHERE customer_name LIKE '张%' AND product_name LIKE '%Mate%' AND order_status IN ('PAID', 'SHIPPED');这里要注意 AND 与 OR 的优先级:AND优先级高于OR。如果条件中混合了 LIKE、AND、OR,务必用括号明确分组,否则结果可能不符合预期。
-- 推荐写法:用括号明确 OR 分组 SELECT * FROM orders WHERE product_name LIKE '华为%' OR (product_name LIKE '%电视%' AND order_status = 'PAID');4.5 场景五:大小写与字符集问题
不同数据库对 LIKE 的大小写敏感性不同:
- MySQL 默认排序规则(collation)不区分大小写,
LIKE 'huawei%'和LIKE 'Huawei%'结果一致。 - PostgreSQL 默认区分大小写,需要写成
LIKE 'Huawei%'或使用ILIKE。 - SQL Server 取决于数据库或列的排序规则。
如果生产环境需要强制忽略大小写,在 MySQL 中可以给字段指定utf8mb4_general_ci排序规则,在 PostgreSQL 中可以直接使用ILIKE。
5. LIKE 的隐藏角色:SQL 注入入口
这是很多教程不会细讲,但实际项目中极其重要的部分。热搜词中出现了“sql注入万能密码绕过”,它与 LIKE 有密切关系。
在网络搜索材料中可以看到,一种经典的万能密码绕过姿势是:登录 SQL 为
SELECT * FROM users WHERE username = 'admin' AND password = 'xxx';攻击者如果知道背后的 LIKE 查询逻辑,就能通过输入' OR 1=1 --这类内容改变原查询语义。但这里有一个更常见的 LIKE 注入场景:搜索接口。
假设后端代码写的是:
String sql = "SELECT * FROM products WHERE product_name LIKE '%" + keyword + "%'";攻击者在搜索框输入:
%' OR 1=1 --拼接后 SQL 变成:
SELECT * FROM products WHERE product_name LIKE '%%' OR 1=1 -- %';OR 1=1会让 WHERE 条件恒真,相当于一次全表数据泄出。如果数据库用户权限过大,甚至可以用UNION SELECT读取其他表的数据。
所以关于 LIKE 的安全性,必须记住两条铁律:
- 永远不要直接拼接用户输入构造 LIKE 模式。
- 即使用户只输入普通文本,也要对
%、_、\等特殊字符做转义处理。
更推荐的做法是使用参数化查询。以 Java 的 PreparedStatement 为例:
// 1. 先对用户输入做转义 String keyword = userInput .replace("!", "!!") .replace("%", "!%") .replace("_", "!_"); // 2. 构造模式 String pattern = "%" + keyword + "%"; // 3. 使用参数化查询 String sql = "SELECT * FROM products WHERE product_name LIKE ? ESCAPE '!'"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, pattern); ResultSet rs = ps.executeQuery();用 Python + MySQL 的写法类似:
cursor = conn.cursor(prepared=True) sql = "SELECT * FROM products WHERE product_name LIKE %s ESCAPE '!'" pattern = f"%{escape_like(keyword)}%" cursor.execute(sql, (pattern,))只有把输入当作参数传递,数据库驱动才会对输入做安全处理。LIKE 模式中的通配符则需要我们自己在业务层管理,这恰恰是模糊查询安全性最容易失控的地方。
6. LIKE 与索引性能分析
前面多次提到 LIKE 和索引的关系,这里展开讲清楚原理和优化手段。
6.1 为什么前缀匹配能走索引
数据库的 B+ 树索引结构是按列的值排序存储的。当模式是'abc%'时,查询优化器知道所有符合条件的记录在索引中会连续分布在abc到abd之间,于是可以在索引上进行范围扫描,定位到起始位置后顺序读取,性能接近等值查询。
6.2 为什么%abc%会全表扫描
模式'%abc%'无法确定起始值位置,优化器只能遍历整张表的每一行,检查每个字段值是否包含abc。这就是经典的“不能使用索引”的场景。数据量上百万之后,这类查询的延迟会显著增大。
6.3 场景实测思路
可以先用EXPLAIN观察执行计划。以 MySQL 为例:
EXPLAIN SELECT * FROM orders WHERE product_name LIKE '华为%'; EXPLAIN SELECT * FROM orders WHERE product_name LIKE '%Mate%';EXPLAIN输出中type列如果从range变成ALL,就说明查询从索引范围扫描退化成了全表扫描。rows列的估算值也能直观看出扫描行数的差异。
6.4 LIKE 性能优化常用方案
针对无法避免的%关键字%查询,业界常见的优化手段有以下几种:
| 方案 | 思路 | 适用场景 | 注意点 |
|---|---|---|---|
| 前缀匹配改写 | 尽量让模式以固定前缀开头 | 业务允许只按前缀搜索 | 改变业务语义,需评估 |
| 覆盖索引 | 查询列都包含在索引中 | 查询字段较少 | 减少回表开销 |
| 全文索引 | 使用 FULLTEXT 索引或 ES | 搜索内容较长、词频明显 | 语法不同,需维护分词 |
| 生成列 | 把反转字段存入生成列,查后缀走前缀索引 | 订单号、手机号后缀查询 | 存储成本增加 |
| 引入搜索引擎 | 数据同步到 Elasticsearch | 复杂搜索、高并发 | 增加运维成本 |
一个典型的生成列优化例子:订单需要频繁按订单号后缀查询,可以在建表时增加一个order_no_rev,存储REVERSE(order_no),查询时把LIKE '%001'改写为LIKE '100%'。
ALTER TABLE orders ADD COLUMN order_no_rev VARCHAR(50) GENERATED ALWAYS AS (REVERSE(order_no)) STORED; CREATE INDEX idx_order_no_rev ON orders(order_no_rev); SELECT * FROM orders WHERE order_no_rev LIKE CONCAT(REVERSE('001'), '%');这类优化适合查询量大的核心业务表,小表没必要这么复杂。
6.5 慢日志与 LIKE 排查
如果线上出现 LIKE 慢查询,第一步不是急着改 SQL,而是确认慢在哪。用 MySQL 慢查询日志或performance_schema找到对应 SQL 后,重点看两个指标:扫描行数和返回行数。如果扫描行数是返回行数的几十上百倍,说明该 SQL 在 LIKE 部分消耗了大量无效 IO,优先考虑改写或加索引。
7. 常见问题与排查方法
结合 LIKE 使用中的高频问题,整理成排查表格,方便读者直接对照。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LIKE 查不出预期数据 | 模式中%或_被当作通配符使用 | 检查搜索词是否含特殊字符 | 使用 ESCAPE 转义 |
| 只匹配到单字,匹配不到多字 | _数量写错,每个_只代表一个字符 | 检查模式中下划线个数 | 改用%表示任意长度 |
| 查询很慢,接口超时 | 模式以%开头,全表扫描 | 用 EXPLAIN 查看 type 列 | 改写为前缀匹配或引入索引/搜索引擎 |
| 大小写不一致导致漏数据 | 数据库排序规则区分大小写 | 查数据库 collation 配置 | 统一排序规则或使用 ILIKE |
| 搜索框输入特殊字符报错 | 通配符未转义或单引号未处理 | 查看详细报错信息 | 参数化查询 + 转义处理 |
| LIKE 结果包含了 NULL 行但显示为空 | WHERE 条件对 NULL 判断为 UNKNOWN | 检查表中数据是否为空 | 用 IS NULL / IS NOT NULL 单独处理 |
| 高并发搜索拖垮数据库 | 大量%关键字%查询同时发生 | 监控慢查询数和 QPS | 加缓存或引入搜索引擎 |
| 跨数据库迁移后 LIKE 行为不同 | 不同数据库对大小写、字符列表语法支持不同 | 查询目标数据库文档 | 统一切换到标准语法 |
补充一个经常被忽略的点:LIKE对NULL字段永远不返回TRUE。所以写法是WHERE remark LIKE '%test%'时,remark为 NULL 的记录不会出现在结果集里。如果业务上需要把 NULL 一起查出来,就要写成:
SELECT * FROM products WHERE remark LIKE '%test%' OR remark IS NULL;8. 最佳实践与工程建议
把 LIKE 使用经验收敛成几条能直接落到项目里的建议。
8.1 查询模式设计
不要什么都写成%关键字%。先分析业务场景,能前缀匹配就前缀匹配。搜索“华为手机”,如果业务上更希望返回“华为手机”开头的商品,就优先使用LIKE '华为手机%'。这种方式既快又准确。
8.2 用户输入统一转义
所有走到 LIKE 的用户输入,统一走一个工具方法处理。不要在每个业务代码里各自实现,避免漏转义。
public static String escapeLike(String input) { if (input == null) { return ""; } return input .replace("!", "!!") .replace("%", "!%") .replace("_", "!_"); }这样在其他开发同事使用 LIKE 时,只需要调用工具方法即可,降低整体安全风险。
8.3 禁止拼接 SQL
这是所有 SQL 相关文章都要强调的红线。任何用户可控的输入,一律使用参数化查询或 ORM 的绑定参数特性。MyBatis 中,应使用#{}而不是${}:
<select id="searchProducts" resultType="Product"> SELECT * FROM products WHERE product_name LIKE CONCAT('%', #{keyword}, '%') </select>注意,不要写成:
<!-- 反例:存在 SQL 注入风险 --> <select id="searchProducts" resultType="Product"> SELECT * FROM products WHERE product_name LIKE '%${keyword}%' </select>8.4 约定字段顺序,建立规范
对核心表,建议根据查询频率建立“搜索字段白名单”。例如商品名称允许 LIKE,商品描述默认不参与 LIKE。业务上需要搜索描述的场景单独走搜索引擎,避免每个查询都在全表重字段上扫描。
8.5 大数据量下优先考虑搜索引擎
当单表数据量达到千万级以上,且 LIKE 无法命中索引时,与其不断优化 SQL,不如直接考虑引入 Elasticsearch。数据库负责事务和精确查询,搜索引擎负责全文检索。这是目前中大型项目的通用做法。
8.6 测试环境验证再上线
任何涉及 LIKE 改写、加索引的操作,都必须在测试环境用真实数据量验证。重点看执行计划是否从ALL变为range或ref,以及接口 P95 延迟是否达到预期。先小流量放量,再全量发布。
9. 总结
SQL 中的 LIKE 看似是一个基础操作符,实际使用时却涉及语法细节、通配符语义、索引机制、安全防护等多个层面。
这篇文章并没有停留在“怎么用 LIKE”的表面,而是把 LIKE 放到真实业务环境中做了完整拆解:前缀匹配走索引、%关键字%全表扫描、特殊字符需要转义、用户输入必须参数化。如果你手头正好在写或者 review 包含 LIKE 的查询逻辑,建议回到自己的表结构上检查两个点:第一,模式是否以通配符开头;第二,用户输入是否走了安全的传参方式。
接下来你可以继续深入的方向包括:复合索引在多个查询条件下的选择逻辑、全文索引与 LIKE 的适用边界、以及不同数据库(MySQL、PostgreSQL、SQL Server)之间模糊查询语法的差异。从 LIKE 这个点切进去,你其实已经开始理解数据库查询优化和系统安全设计这两座大山了。