做SQL开发这些年,“查一个字段里到底有没有某个值”可能是写的最多的条件了,但也是踩坑最多的地方之一。很多人一上来就是LIKE '%关键词%',跑通了就完事,等到数据量上来、或者字段里存的是逗号分隔的ID串、再或者要区分大小写的时候,才发现问题一堆。这篇就把我在不同数据库里常用的几种判断字段包含的方法、各自的适用场景、性能差异和真实踩过的坑,一次性说清楚。
1. 判断包含的底层逻辑:先搞清字段里到底存的是什么
先说个最容易被忽略的问题:“包含”这个词在不同场景下,含义完全不同。
- 字段存的是字符串:要判断是否包含某个子串,比如
title字段包含“SQL优化”。 - 字段存的是逗号分隔的ID列表:要判断列表里是否包含某个ID,比如
role_ids字段是"1,3,5",要查有没有3。 - 字段存的是JSON:要判断JSON数组里有没有某个元素。
- 字段存的是全文文本:要判断是否包含某个词,但还要考虑分词、词序、同义词。
这些场景用的方法完全不一样。如果把逗号分隔的列表当成普通字符串用LIKE '%3%'去查,那13、35、43都会被匹配出来,结果就是一堆脏数据。所以第一步,先看清楚字段的数据类型和存储格式,再选方法。
另外还要注意大小写敏感的问题。MySQL里LIKE默认不区分大小写,但PostgreSQL里LIKE区分大小写,ILIKE才不区分。SQL Server的LIKE默认不区分大小写(取决于排序规则)。同一个SQL,换个数据库行为就变了,这点在写跨库兼容代码的时候很容易翻车。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最常用的LIKE系列:通配符的边界和转义陷阱
LIKE是最直觉的方法,但在数据库引擎里,写法不同,执行计划可能就是天壤之别。
2.1 左模糊、右模糊、全模糊的性能差异
sql复制-- 右模糊:可以走索引
SELECT * FROM article WHERE title LIKE 'SQL优化%';
-- 左模糊:一般无法走索引
SELECT * FROM article WHERE title LIKE '%SQL优化';
-- 全模糊:肯定无法走索引
SELECT * FROM article WHERE title LIKE '%SQL优化%';
很多人只知道“用%包起来就行”,但实际上一个%放在哪,直接决定了查询能不能用上索引。LIKE 'SQL优化%'相当于告诉引擎“以SQL优化开头”,如果字段上有普通B+树索引,优化器可以用索引范围扫描(如同查>= 'SQL优化' AND < 'SQL优调')。而LIKE '%SQL优化%'必须扫描全表每个字符串才能确定中间有没有这个子串,数据量一大就慢得没法看。
2.2 ILIKE、like lower、collate怎么选
PostgreSQL和SQLite(部分版本)支持ILIKE,MySQL没有,SQL Server没有。跨库写代码的话,最稳妥的方式是统一转为小写再比较:
sql复制-- PostgreSQL
SELECT * FROM users WHERE name ILIKE '%张%';
-- 或者全转小写
SELECT * FROM users WHERE LOWER(name) LIKE LOWER('%张%');
但LOWER(name)有个副作用:函数包裹字段后,字段索引很可能失效。PostgreSQL里可以建表达式索引来解决:
sql复制CREATE INDEX idx_users_name_lower ON users (LOWER(name));
SQL Server里如果排序规则本身区分大小写,可以用COLLATE强制不区分:
sql复制SELECT * FROM users WHERE name LIKE '%张%' COLLATE Chinese_PRC_CI_AS;
2.3 特殊字符的转义:下划线和百分号是合法的数据
%是通配任意长度的字符,_是通配单个字符。但如果字段里的业务数据本身就包含%或_,比如产品编码有A_B这种格式,直接LIKE '%A_B%'会把AXB也查出来。
解决办法是声明转义字符:
sql复制-- 用 \ 作为转义字符,匹配 A_B
SELECT * FROM product WHERE code LIKE '%A\_B%' ESCAPE '\';
SQL Server默认用[]包裹就可以变成字面量:
sql复制-- SQL Server 匹配 A_B
SELECT * FROM product WHERE code LIKE '%A[_]B%';
Oracle也是用ESCAPE,MySQL同样支持ESCAPE。这里踩过一次很深的坑:测试环境数据少,_匹配单个字符的差异不明显,上线后生产数据里恰好有一批A1B、A2B编码,全部被误判成包含A_B,导致一批数据被错误标记。从那以后,所有业务表里含通配符的匹配,都强制加ESCAPE。
2.4 替代方案:CHARINDEX、LOCATE、INSTR、POSITION
LIKE只能告诉你好不好判断,但如果你还想知道被包含的位置,或者想在WHERE里做更复杂的条件判断,可以用位置函数。
| 数据库 | 函数 | 用法 | 返回值 |
|---|---|---|---|
| SQL Server | CHARINDEX | CHARINDEX('abc', col) |
子串起始位置,找不到返回0 |
| MySQL | LOCATE / INSTR | LOCATE('abc', col) / INSTR(col, 'abc') |
起始位置,找不到返回0 |
| PostgreSQL | POSITION / STRPOS | POSITION('abc' IN col) / STRPOS(col, 'abc') |
起始位置,找不到返回0 |
| Oracle | INSTR | INSTR(col, 'abc') |
起始位置,找不到返回0 |
| SQLite | INSTR | INSTR(col, 'abc') |
起始位置,找不到返回0 |
用法逻辑很统一:返回值大于0,就说明包含。
sql复制-- SQL Server 判断包含
SELECT * FROM article WHERE CHARINDEX('SQL优化', title) > 0;
-- MySQL 判断包含
SELECT * FROM article WHERE LOCATE('SQL优化', title) > 0;
-- Oracle 判断包含
SELECT * FROM article WHERE INSTR(title, 'SQL优化') > 0;
这个系列比LIKE '%...%'写起来啰嗦一点,但好处是语义明确,而且SQL Server的CHARINDEX在某些执行计划下比LIKE更可控。另外,如果你要判断“包含A且包含B”,用位置函数可以避免嵌套两层LIKE:
sql复制-- MySQL 同时包含 SQL 和 优化
SELECT * FROM article WHERE LOCATE('SQL', title) > 0 AND LOCATE('优化', title) > 0;
3. 正则匹配:复杂模式判断的正确打开方式
LIKE的通配符能力有限,一个%一个_就没了。当判断条件变成“包含以字母开头后面跟3个数字的模式”、“包含手机号格式”、“字段中间任意位置包含C或D”这些,正则就该上场了。
3.1 MySQL REGEXP / RLIKE
MySQL从8.0开始完整支持REGEXP_LIKE、REGEXP_INSTR、REGEXP_SUBSTR这些函数,老版本也有REGEXP操作符:
sql复制-- 包含手机号片段(1开头,11位数字)
SELECT * FROM customer WHERE phone REGEXP '^1[0-9]{10}$';
-- 包含字母,且字母后面紧跟4位数字
SELECT * FROM product WHERE sku REGEXP '[A-Za-z][0-9]{4}';
注意MySQL的REGEXP默认是不区分大小写的,如果业务要求区分,要用BINARY修饰:
sql复制SELECT * FROM product WHERE sku REGEXP BINARY 'ABC[0-9]+';
3.2 PostgreSQL ~ 和 ~~
PostgreSQL用波浪号~表示正则匹配,~*表示不区分大小写,!~和!~*表示不匹配:
sql复制-- 区分大小写匹配
SELECT * FROM sku WHERE code ~ '^[A-Z]{2}[0-9]{4}';
-- 不区分大小写
SELECT * FROM sku WHERE code ~* '^abc';
配合SIMILAR TO也能做模式匹配,但语法是SQL标准风格,实际用得少,不如直接正则。
3.3 SQL Server:没有内嵌正则,但有近似方案
SQL Server原生不支持开箱即用的正则函数。旧版本里最接近的解决方案是用LIKE配合[]、[^]做字符集匹配,比如:
sql复制-- 匹配存在数字
SELECT * FROM t WHERE col LIKE '%[0-9]%';
-- 匹配存在非数字
SELECT * FROM t WHERE col LIKE '%[^0-9]%';
SQL Server 2022开始有了STRING_SPLIT函数,但那是用来拆分字符串的,不是用来做正则匹配的。真要做复杂正则,一般是在应用层做,或者用SQLCLR封装一个正则函数。但要小心:SQLCLR默认是关闭的,开启有权限和性能风险,涉及生产环境一般不建议为了一个匹配函数去开启它。
3.4 正则的性能提醒:能不用就不用
正则表达式引擎的匹配开销是远高于LIKE和LOCATE的。全模糊LIKE '%xx%'已经够慢了,正则更慢。正则适合数据量小、或者临时排查数据的场景,不适合高频线上查询。 如果业务上高频需要复杂的模式匹配,更好的方案是:建一个额外的计算列,在插入时把“是否匹配”的结果存起来(比如0/1标志),再给这个标志列建索引。查询时直接过滤标志位,速度会快几个数量级。
4. 特定场景的杀手锏:FIND_IN_SET、全文本检索和JSON包含
三个高频特殊场景,各有各的最优解。
4.1 MySQL FIND_IN_SET:逗号分隔ID列表的正确打开方式
业务上很常见的一种建模:一个字段存多个ID,用逗号拼起来,比如角色的权限ID列表permission_ids = "1,3,5,8"。这种字段用LIKE '%3%'查就是灾难。
MySQL专门提供了FIND_IN_SET:
sql复制SELECT * FROM user_role WHERE FIND_IN_SET('3', permission_ids) > 0;
FIND_IN_SET会把第二个参数按逗号拆分,然后精确匹配第一个参数,1到N是位置,0是没找到。它能准确地分辨出3和13、35的区别。
但要注意:FIND_IN_SET内部要拆分字符串,字段索引完全用不上,数据量大时全表扫描。这个函数比较适合小表(比如角色表、配置表),不适合在千万级用户表上直接过滤。真要在千万级表上做这类查询,建议冗余一张关联表(一行一个权限ID),走连接查询。
4.2 全文本检索:别再对大文本用LIKE了
如果字段是长文本,比如文章内容、商品详情描述,要判断是否包含某个词,LIKE '%xx%'不仅在超大文本上性能差,还可能因为%通配导致匹配结果不精确。
MySQL的MATCH ... AGAINST和PostgreSQL的to_tsquery、SQL Server的CONTAINS才是正解。
sql复制-- MySQL 全文检索(需建全文索引)
ALTER TABLE article ADD FULLTEXT INDEX ft_title_content (title, content);
SELECT * FROM article WHERE MATCH(title, content) AGAINST('SQL优化');
-- PostgreSQL 全文检索
SELECT * FROM article WHERE to_tsvector('simple', title || ' ' || content) @@ to_tsquery('simple', 'SQL优化');
-- SQL Server 全文检索(需建全文索引)
SELECT * FROM article WHERE CONTAINS((title, content), 'SQL优化');
全文检索的本质是分词后建倒排索引,它匹配的是“词”而不是“字符子串”,所以MATCH ... AGAINST('优化')可能只匹配到分词后就是“优化”的记录,匹配不到“优化器”这种扩展词形态。这和LIKE '%优化%'的粗细粒度不一样。做业务查询前一定要和产品确认:你要的是子串包含,还是词语包含。这两者的结果集差异,往往在拿到数据前没人能预料到。
4.3 JSON字段的包含判断
MySQL 5.7+、PostgreSQL、SQL Server 2016+都能直接处理JSON字段。JSON数组“是否包含某个值”有专门的操作符或函数。
sql复制-- MySQL 8.0 判断JSON数组内是否包含指定值
SELECT * FROM user_profile WHERE JSON_CONTAINS(tags, '"VIP"');
-- PostgreSQL 判断JSONB数组包含
SELECT * FROM user_profile WHERE tags @> '["VIP"]';
-- SQL Server 判断JSON数组包含
SELECT * FROM user_profile WHERE JSON_VALUE(tags, '$[0]') = 'VIP';
MySQL的JSON_CONTAINS第一个参数是目标JSON,第二个参数是要找的JSON片段(注意要带引号)。JSON_CONTAINS(tags, '"VIP"')里的'"VIP"'是合法JSON字符串,整段JSON数组里有没有这个字符串元素,这个函数的语义是JSON层面的元素包含,比字符串匹配严谨得多。
PostgreSQL的@>操作符对JSONB类型非常高效,但它要求字段类型是JSONB而不是JSON,建表时就要规划好。
4.4 SQL Server 2016+:STRING_SPLIT 与 EXISTS 的黄金组合
SQL Server 2016之前,拆分逗号分隔字符串是一件非常痛苦的事(要用递归CTE或者XML)。2016之后有了STRING_SPLIT:
sql复制SELECT u.*
FROM user_role u
WHERE EXISTS (
SELECT 1
FROM STRING_SPLIT(u.permission_ids, ',')
WHERE value = '3'
);
STRING_SPLIT按逗号拆成行,然后EXISTS判断是否存在指定的值,这种写法和FIND_IN_SET语义一致,比LIKE '%3%'精确得多。强烈建议用EXISTS而不是IN,因为IN会把拆分结果物化一次,性能略差,而且如果子查询返回NULL,NOT IN会出语义错误(一条都不返回),NOT EXISTS则没有这个问题。
5. 慢SQL排查:从LIKE全模糊到索引失效,我踩过的三个典型坑
这一节放点实战踩坑经验。判断字段包含的SQL写起来一行,优化起来能查半天,以下三个坑是我在不同数据库上真实遇过的。
5.1 坑一:全模糊查询把接口拖垮
有一次线上接口超时,排查发现慢SQL是一条用户搜索:
sql复制SELECT * FROM user WHERE name LIKE '%小%';
用户表500万行,name字段有普通索引,但%在开头导致索引完全用不上,每次查询全表扫描,平均耗时4秒多。当时产品给的需求是“用户手动输入完整用户名后搜索”,但代码里直接拼了一个全模糊。
修复方案: 和产品确认后,把搜索逻辑改成右模糊(LIKE '小%'),前端在用户输入结束后自动触发,这样走了索引,查询耗时降到20毫秒。如果确实要全模糊,就上全文索引或Elasticsearch,而不是继续用LIKE '%xx%'硬扛。
这里分享一个排查技巧:MySQL里可以用EXPLAIN看type列。如果type不是range而是ALL,那基本就是没走索引。PostgreSQL用EXPLAIN ANALYZE看Seq Scan和Index Scan的区别,一眼就能看出问题。
5.2 坑二:转小写导致索引失效
当时另一个生产问题是:用户输入查询不区分大小写,开发直接用LOWER(name)包了一层:
sql复制SELECT * FROM user WHERE LOWER(name) = LOWER('ZhangSan');
单看逻辑没问题,但LOWER(name)是函数包裹列,MySQL和PostgreSQL的普通索引在这种情况下默认失效(除非建了表达式索引)。结果又是全表扫描。
修复方案: 两个方向。一是建表达式索引(PostgreSQL:CREATE INDEX ON user (LOWER(name));、MySQL 8.0也支持函数索引)。二是如果业务允许,直接在应用层把输入统一转小写,只存小写数据,查询时也用全小写字符串去匹配,这样普通索引就能直接用。
5.3 坑三:FIND_IN_SET在千万级表上的灾难
有一张订单表1000万行,其中有个tag_ids字段存逗号分隔的标签ID。产品需求是筛出“包含标签3”的订单:
sql复制SELECT * FROM orders WHERE FIND_IN_SET('3', tag_ids) > 0;
开发环境数据少跑得飞快,上了生产直接把数据库CPU打满。原因是FIND_IN_SET每行都要拆分字符串,10万个字符的字段每行拆一次,CPU开销呈指数上涨。
修复方案: 业务侧做规范化改造,把订单和标签关系拆成一张order_tag_rel关联表(order_id, tag_id),每个标签一行,查询走关联表的索引。改造后相同场景的查询耗时从原来的5秒降到100毫秒以内。如果实在无法改表结构,那就只能离线用脚本把“包含标签3的订单ID集合”缓存到Redis,查询时先查缓存再回表,这个方案治标不治本,但能快速止损。
5.4 排查索引是否被使用的通用套路
不管是LIKE、LOCATE还是FIND_IN_SET,遇到慢SQL,第一件事不是调SQL,而是EXPLAIN看执行计划。重点看几列:
| 数据库 | 关键列 | 期望值 |
|---|---|---|
| MySQL | type | 至少是 range,最好是 ref/const |
| PostgreSQL | 是否有 Seq Scan | 尽量要 Index Scan / Bitmap Heap Scan |
| SQL Server | Index Scan vs Table Scan | 尽量找 Index Seek |
如果出现ALL、Seq Scan、Table Scan,说明没走索引。这时候优先考虑改写SQL(比如把全模糊改成右模糊),其次考虑加索引(比如表达式索引、全文索引),最后才考虑上全文搜索引擎。顺序反了,往往会做很多无用功。
6. 给新手和资深开发各自的实用清单
判断字段是否包含这个需求,看起来简单,实际写的时候要回答三个问题:数据是什么存储格式?匹配粒度是子串还是词?查询频率和数据量级是多少? 三个问题都清楚了,再选方法,基本不会出错。
6.1 方法总览对照表
| 场景 | 推荐方法 | 典型数据库 | 索引友好度 |
|---|---|---|---|
| 字符串前缀匹配 | LIKE 'xx%' | 所有主流数据库 | 高(可走索引) |
| 字符串子串匹配 | LIKE '%xx%' | 所有主流数据库 | 低(全表扫描) |
| 子串位置判断 | CHARINDEX / LOCATE / INSTR / POSITION | SQL Server / MySQL / Oracle / PostgreSQL | 低(函数触发) |
| 复杂模式匹配 | REGEXP / ~ / LIKE [0-9] | MySQL / PostgreSQL / SQL Server | 低 |
| 逗号分隔列表 | FIND_IN_SET / STRING_SPLIT + EXISTS | MySQL / SQL Server | 低(建议拆表) |
| 长文本词语匹配 | 全文检索 | MySQL FULLTEXT / PG to_tsquery / SQL Server CONTAINS | 高(倒排索引) |
| JSON数组元素 | JSON_CONTAINS / @> / JSON_VALUE | MySQL / PostgreSQL / SQL Server | 中(需用JSON索引,PG的GIN) |
6.2 给新手的三个原则
- 先确认数据类型再写SQL:字符串和JSON的判断方法完全不同,用错方法数据就错。
- 不要盲目相信
LIKE '%xx%':对小表无所谓,对大表性能是灾难;对逗号分隔字段,它会返回大量错误数据。 - 用
EXPLAIN养成看执行计划的习惯:不要只关心返回结果对不对,还要关心查询成本高不高。
6.3 给资深开发的两个进阶建议
- 高频判断需求,用列存储/冗余标志位:如果“是否包含某个值”是高频筛选条件,在表设计阶段就增加一个
is_vip、has_permission之类的布尔列,插入时算好,查询时直接WHERE is_vip = 1。存储成本换来查询性能,划算。 - 考虑将字符串集合归一化成关联表:字段里存逗号列表本身就是反范式设计,只适合读多写少的小表。任何数据量增长明显的场景,尽早拆关联表,不要等到慢SQL打上门再改。
从最开始写LIKE '%xx%'查不出来、到后来用FIND_IN_SET踩了大坑、再到现在根据不同场景选不同方案,这套判断包含逻辑的演进,本质上就是我对“索引、执行计划、存储模型”理解加深的过程。下次再接到“判断字段包含”的需求,先别急着写SQL,把字段的数据格式和查询量级问清楚,你的方案就已经比大多数人有优势了。
