☰
SQL判断字段是否包含某个数据:LIKE、FIND_IN_SET与索引优化全攻略
2026/10/17 6:37:20 网站建设 项目流程

做定时报表和数据处理的人,几乎每天都要面对一个问题:怎么判断一个字段里有没有某个数据。它在代码里只是一个布尔判断,到 SQL 里却藏着不少门道——字段是文本还是多值串、数据库是 MySQL 还是 PostgreSQL、数据量是百万行还是几万行,都会影响你该用哪种写法。最近我在处理订单备注匹配时,一个LIKE '%关键词%'直接把几百万行的表拖进了全表扫描,查一次要几十秒;换掉写法、补上索引之后,耗时降到毫秒级。这个经历让我决定把这几种判断字段包含数据的方法完整梳理一遍,把使用场景、坑点和优化思路一起写清楚。

所谓"字段包含一个数据",不是一句"用 LIKE 就行"能概括的。至少有三层语义:一是字段值是文本,判断其中有没有某个子串;二是字段值本身是多值结构,比如逗号分隔的标签或 JSON 数组,判断其中有没有某个成员;三是字段值是精确值,判断它是否归属某个候选集合。三种语义对应着完全不同的函数和写法,用错了一口钟。

这篇文章适合写业务 SQL 的后端开发、做数据清洗的数据分析师,以及准备数据库面试的候选人。我会按语义分类把方法讲透,每个方案都给出可复制的写法,同时对比 MySQL、PostgreSQL、SQL Server、Oracle 四个主流数据库的差异。

1. 先分清"包含"的三种语义:这一步错了后面全白写

1.1 文本子串匹配:判断字段值里有没有某段文字

最经典的场景是查订单备注、日志内容、商品描述。假如 order_table 的 remark 字段存着一行文本,业务要找出所有包含"退款"两个字的订单,那本质上是要判断一个字符串里是否存在某个子串。对应到 SQL,日常主力就是 LIKE,其次是定位类函数、正则表达式。

这个语义是大多数学过 SQL 的人第一时间想到的"包含"。但它有个隐含前提:字段值是文本,匹配目标是文本的一部分,而且访问者不关心它出现在开头、中间还是结尾。稍微复杂一点,需求会变成"包含数字""包含金额单位""包含退或者换"这类带模式的内容,这时候就要请正则出场。

1.2 集合成员匹配:字段值是多值结构,判断其中有没有某个成员

现实中还有一类"包含"——字段里存的是多个值拼起来的一个串,典型的是逗号分隔的标签,比如 user_tags = 'vip,premium,newuser'。业务说"查包含 premium 标签的用户",这里的"包含"其实是集合的成员判断,不是子串判断。

如果直接user_tags LIKE '%premium%',会把 premium_test、premium_plus 甚至 superpremium 这种不相干的数据一起捞出来,产生虚假命中。这类字段要用 FIND_IN_SET(MySQL)、数组拆分(PostgreSQL 的 string_to_array + ANY)或 JSON 函数来处理。

还有一种更隐蔽的情况,字段里存的是一大串 JSON 数组,数据内容同样是集合成员判断,只是结构从纯文本升级成了 JSON。这类字段如果继续用 LIKE 去碰,不仅要忍受全表扫描,还可能出现把"premium_plus"误判成"premium"的脏结果。

1.3 精确值归属匹配:字段的值是否落在某个候选集合里

第三种语义更像"是否等于列表里的某一种",比如 status 字段判断是否是 1、2、3 这三个状态之一。SQL 里直接用 IN:WHERE status IN (1, 2, 3)。

它虽然不叫"包含",但业务和面试中常常被放在一起讨论。"包含一个数"和"属于一组数"在自然语言里很容易混,代码里一个是判断目标在字段内,一个是判断字段在目标集内,方向是反的。把三种语义理清楚,后面几节的工具才不会用错。

语义典型字段推荐方向代表函数
文本子串remarkLIKE 子串匹配LIKE、CHARINDEX、INSTR、STRPOS、REGEXP
集合成员user_tags成员精确匹配FIND_IN_SET、string_to_array、JSON_CONTAINS、@>
精确归属status枚举列表匹配IN、= ANY

2. LIKE 系写法:最直觉,也最容易掉进通配符的坑

2.1 LIKE 的三种匹配位置与索引机会

LIKE 做"包含"判断,写法上通常是这样:

-- 任意位置包含"退款" SELECT * FROM order_table WHERE remark LIKE '%退款%'; -- 以"退款"开头(这个写法有可能走索引) SELECT * FROM order_table WHERE remark LIKE '退款%'; -- 以"退款"结尾 SELECT * FROM order_table WHERE remark LIKE '%退款';

这里要特别提醒一句:LIKE '%退款%' 和 LIKE '退款%' 不只是匹配范围不同,性能差异也极大。'退款%' 这种前缀匹配在 B+ 树索引上可以做成范围扫描,只有 '%退款%' 和 '%退款' 必须扫描整列数据,这个问题我放到第 5 节详细说。

实际项目里我最常见的误用场景是:写报表的人为了图省事,把所有"包含"需求全部套%关键词%,结果一个简单的条件过滤拖出几十秒的查询。其实很多"包含"需求仔细问下去,业务方真正想要的是"以 XX 开头"或者"等于某个枚举值",压根不需要中段模糊。

2.2 下划线不是普通下划线:通配符转义

我刚接触 SQL 时在这里被坑过一次。用户表里有一个 account 字段,我需要查所有包含下划线的账号,下意识写:

SELECT * FROM user_table WHERE account LIKE '%_%';

返回结果几乎包含了全表数据,当时还以为是权限问题,仔细一查才意识到:LIKE 里有两个通配符,% 代表任意数量字符,_ 代表任意单个字符。LIKE '%_%'的语义是"任意字符出现一次即可",基本上所有非空字符串都能命中,结果自然全错。正确写法必须转义:

SELECT * FROM user_table WHERE account LIKE '%\_%' ESCAPE '\';

如果业务要匹配的字符本身就含有 % 或 _,就得用 ESCAPE 指定转义符。上面例子用反斜杠,也可以换成任意不会出现在数据里的字符,比如LIKE '%#_%' ESCAPE '#',转义符只要统一即可。

还有一点很多人不知道:当你要匹配的文本以通配符开头时,比如搜索包含 "10%" 的折扣说明,SQL 是:

WHERE remark LIKE '%10\%%' ESCAPE '\';

第一个 % 是任意前缀,中间的 % 是字面意义百分号,最后两个 %% 属于通配符的一部分。这个写法容易乱,建议先在 SELECT 里单独跑一下不带 WHERE 的表达式,确认结果再套进业务查询。

2.3 正则版本的包含:REGEXP / REGEXP_LIKE / ~

LIKE 解决不了"包含数字""包含至少一位大写字母"这类模式需求,这时用正则。MySQL 里常见的写法:

-- MySQL 8.0 SELECT * FROM order_table WHERE REGEXP_LIKE(remark, '退款|退货'); -- 等价旧写法 SELECT * FROM order_table WHERE remark REGEXP '退款|退货';

PostgreSQL 用 ~ 或者~*(忽略大小写):

SELECT * FROM order_table WHERE remark ~ '退款|退货';

Oracle 用 REGEXP_LIKE:

SELECT * FROM order_table WHERE REGEXP_LIKE(remark, '退款|退货');

SQL Server 原生不支持正则判断,所以大部分场景靠 LIKE 硬扛。需要真正做复杂模式匹配时,可以自己实现 CLR 函数或引入外部服务,但从我的实操经验看,能不用就别用,维护成本高,而且优化器通常拿不到很好的执行计划。先用 LIKE 做一层粗筛,再对候选集做正则过滤,往往更实际。

用正则判断"包含"时还有个惯性错误:很多人习惯写成'^退款',结果查不到中间包含"退款"的数据。如果你要做的是包含判断,正则以'退款'开头即可,^和$是特意限制位置才用的锚点。

大小写方面,MySQL 多数 collation 默认不区分大小写,PostgreSQL 的 ~ 区分,SQL Server 看排序规则。判断包含关键字时,如果业务不关心大小写,PostgreSQL 建议直接用~*,比LOWER(remark) ~ ...更合理,后者会让列上的索引彻底失效。

3. 定位函数系:CHARINDEX、INSTR、LOCATE、STRPOS 怎么选

3.1 四个数据库定位函数速查

除了 LIKE,判断文本子串"包含"还有一类定位函数:返回子串在字段中的位置,位置大于 0 就说明包含。不同数据库的函数名完全不同:

数据库函数正确用法参数顺序
MySQLLOCATELOCATE('退款', remark) > 0子串在前,字符串在后
MySQLINSTRINSTR(remark, '退款') > 0字符串在前,子串在后
SQL ServerCHARINDEXCHARINDEX('退款', remark) > 0子串在前,字符串在后
OracleINSTRINSTR(remark, '退款') > 0字符串在前,子串在后
PostgreSQLSTRPOSSTRPOS(remark, '退款') > 0字符串在前,子串在后
PostgreSQLPOSITIONPOSITION('退款' IN remark) > 0用 IN 连接

注意看参数顺序,这是最离谱的坑。MySQL 的 LOCATE 是LOCATE(substr, str),而 MySQL 的 INSTR 是INSTR(str, substr),同一个数据库里两个函数的顺序居然是反的。Oracle 的 INSTR 和 MySQL 的 INSTR 表面同名,实际用法一致(str 在前),但很多人从 MySQL 转 Oracle 时会下意识迁移成INSTR('退款', remark),查出来恒等于 0,还不报错,这种代码审查极难发现。

PostgreSQL 的 POSITION 写法是POSITION('退款' IN remark),不是POSITION('退款', remark),函数签名和别的定位函数完全不是一个路子,迁移脚本里特别容易漏。

3.2 返回 0 还是 NULL:排查结果缺失的另一条线

定位函数没找到子串时统一返回 0,找到就返回从 1 开始的位置。但字段本身是 NULL 时,所有定位函数都返回 NULL,NULL 和任何数字比较结果都是 unknown,WHERE 条件会把它过滤掉。

所以查"不包含某个字"的反向查询要格外小心:

-- 这是错的:NOT LIKE 会排除 NULL 行,也可能排除业务需要保留的空值 WHERE remark NOT LIKE '%退款%'; -- 推荐:把 NULL 的语义显式表达出来 WHERE remark IS NULL OR remark NOT LIKE '%退款%';

同理,正向查询里如果你写WHERE LOCATE('退款', remark) = 0来查不包含的数据,NULL 行一样不会出现。数据里有大把 NULL 的时候,报表结果会比预期少一截,这是实际项目里经常被忽略的差异。

3.3 定位函数和 LIKE 的性能与适用边界

定位函数和 LIKE '%xx%' 从执行计划上看同属无法利用普通索引的扫描型写法,性能量级差不多。定位函数的优势在于它返回一个数字位置,可以做进一步处理,比如判断子串出现在开头(返回 1)、取子串后面的内容:

-- 查找退款原因中"金额"出现的位置,并截取它之后 30 个字符 SELECT remark, SUBSTRING(remark, LOCATE('金额', remark), 30) AS around_amount FROM order_table WHERE LOCATE('金额', remark) > 0;

这种能力 LIKE 给不了,LIKE 只能告诉你"有没有",不能告诉你"在哪"。所以如果你只是过滤,用哪个都行;如果要基于位置继续加工,定位函数是唯一合理选择。

我个人的习惯是:跨数据库的通用查询优先写 POSITION,虽然不同库的 POSITION 略有差异,但它比 CHARINDEX / INSTR 更接近 SQL 标准;如果是 MySQL 内部项目,直接 LOCATE;SQL Server 项目就用 CHARINDEX。选一个函数全项目统一,别跟数据库默认偏好较劲,后续维护的人会感谢你。

4. CSV 和 JSON 字段的"包含":集合成员查找的正确姿势

4.1 FIND_IN_SET:MySQL 的逗号分隔字符串专属

MySQL 提供 FIND_IN_SET,专门处理逗号分隔的字符串里有没有某个成员:

-- 返回 2,表示 'premium' 在第 2 个位置 SELECT FIND_IN_SET('premium', 'vip,premium,newuser'); SELECT * FROM user_table WHERE FIND_IN_SET('premium', user_tags) > 0;

它的核心价值是精确的成员匹配。LIKE '%premium%'会把 'premium_plus' 一起匹配出来,FIND_IN_SET 不会,因为它按逗号边界逐项比较。

但别高兴太早,它有几个边界条件:

  • 对空格敏感。user_tags = 'vip, premium'这种数据里带着空格,FIND_IN_SET 匹配 'premium' 返回 0。写代码前先清理数据,或者查询时用 REPLACE 预处理。
  • 分隔符只能是英文逗号。中文逗号、分号、竖线分隔的都不支持,需要其他手段。
  • 字段为 NULL 时返回 NULL,空字符串时返回 0。

如果分隔符不是逗号,比如竖线分割的权限串,可以用 REPLACE 把分隔符转成逗号再 FIND_IN_SET,但要注意数据本身不能包含逗号,否则结果会失真。

PostgreSQL 没有 FIND_IN_SET,但可以用数组函数达到同样效果:

SELECT * FROM user_table WHERE 'premium' = ANY(string_to_array(user_tags, ','));

SQL Server 没有 FIND_IN_SET,常见的替代是给字符串两边加逗号再用 LIKE:

WHERE ',' + user_tags + ',' LIKE '%,premium,%';

这个写法的思路是人为制造边界,避免把 premium_test 误匹配进来。它仍然是一个 LIKE,性能不算好,但在数据量小、没法拆表的情况下是个务实方案。

4.2 JSON 数组:JSON_CONTAINS 与 Postgres 的 @> 运算符

如果字段是 JSON 数组,MySQL 5.7 以后可以这样:

-- tags_json 形如 ["premium","vip","newuser"] SELECT * FROM user_table WHERE JSON_CONTAINS(tags_json, '"premium"', '$');

注意,第二个参数必须是合法的 JSON 字符串,所以字符串值要带双引号,是'"premium"'而不是'premium'。我第一次写完漏了引号,报错之后才意识到问题,这个细节面试里也很容易考到。

如果是数字成员,写JSON_CONTAINS(tags_json, '2', '$'),数字本身不需要额外引号。

PostgreSQL 用原生 jsonb 包含运算符,语义上更自然:

SELECT * FROM user_table WHERE tags_jsonb @> '["premium"]';

@>表示右侧 JSON 是左侧的子集,所以左侧写完整字段,右侧写包含目标元素的数组。这是 jsonb 类型最常用的操作之一。要注意它匹配的是 JSON 值语义而不是字符串字面量,字符串 "2" 和数字 2 会被区分开,建索引和写查询时都得想清楚类型。

很多人都知道 JSON 结构方便灵活,但性能问题容易被忽略。MySQL 8.0 支持在 JSON 列上建多值索引,专门加速这类包含查询;PostgreSQL 则在 jsonb 上直接建 GIN 索引即可。没有索引时,千万行级 JSON 字段的包含判断就是逐行解析,慢到怀疑人生。

4.3 反范式字段的本质问题:能拆表就拆表

上面的函数再多,也只是在给"把多个值塞进一个字段"这种设计擦屁股。逗号分隔字符串有几个天生缺陷:

  • 查询永远无法优雅地使用普通 B+ 树索引,只能靠函数或全文索引打补丁。
  • 更新一个标签要读出整个字符串改完写回,并发场景容易互相覆盖。
  • 一致性没法保证,可能出现 'vip,vip' 这种重复项或前后多加一个逗号的脏数据。
  • 字符串长度有限,标签一多就存不下。

所以,如果标签是需要频繁按成员查询的维度,正确做法是拆成关联表:

-- 标签子表 CREATE TABLE user_tag ( user_id INT NOT NULL, tag_name VARCHAR(50) NOT NULL, PRIMARY KEY (user_id, tag_name) ); -- 查包含 premium 标签的用户 SELECT DISTINCT u.* FROM user_table u JOIN user_tag t ON t.user_id = u.user_id WHERE t.tag_name = 'premium';

拆表之后,tag_name 直接走主键索引,千万行级 JOIN 通常都能在几十毫秒内完成,比起全表扫描简直是质变。我见过一个真实场景,某项目的用户标签匹配查询原来用 LIKE 全扫要十几秒,拆表后降到 20 毫秒左右,代价是写入时多一条 insert,但换来的是查询端的稳定和可预测,这笔账非常划算。

如果实在不想拆表,可以退而求其次用 JSON 类型加 JSON/GIN 索引,或者用 MySQL 的原生 SET 类型。但我的建议很直接:多值字段需要高频按值查询时,表结构设计阶段就应该往子表或 JSONB 方向走,而不是拿逗号字符串硬撑。

5. 性能工程:包含查询为什么慢,以及怎么救

5.1 前缀通配符与索引的缘分

一个常见的误解是"只要是等值或 LIKE 就一定能用索引"。实际上,B+ 树索引按字符串顺序存储键值,所以:

  • LIKE '退款%'可以转化为范围条件,索引可用。
  • LIKE '%退款%'和LIKE '%退款'无法确定起点,只能全表扫描或扫全索引。

正则表达式和定位函数也一样,只要模式不是前缀确定型,普通索引都帮不上忙。

如果需要按"包含"查询,又不想全表扫,有几个变通方案:

  • 前缀索引:ALTER TABLE order_table ADD KEY idx_remark_prefix (remark(10));只能加速LIKE '退款%'这类前缀匹配,对中间包含没帮助。
  • 冗余列:写入时额外维护一列is_refund TINYINT,表示备注里是否含退款关键词,查询直接WHERE is_refund = 1。把计算从查询时挪到写入时,这是处理高频固定判断的常用手段。
  • 反向设计:如果业务总在查"包含某个后缀"的模式,可以考虑存一个反转字符串列,把后缀查询变成前缀查询,从而用上索引。这个技巧听上去有点绕,但遇到特定场景确实很好用。

5.2 全文索引:处理真正的中段匹配

如果业务就是需要LIKE '%关键词%'这种中段包含,且数据量到了百万以上,普通索引已经没戏,就得上全文索引。

MySQL 的 InnoDB 全文索引配合 ngram 解析器可以处理中文:

ALTER TABLE order_table ADD FULLTEXT INDEX ft_remark (remark) WITH PARSER ngram; SELECT * FROM order_table WHERE MATCH(remark) AGAINST('退款' IN BOOLEAN MODE);

要注意,全文索引的匹配语义和 LIKE 子串匹配不完全等价。它先分词再匹配,所以搜"退款"可能匹配到包含该词的文档,但分词粒度、停用词、最小 token 长度都会影响召回结果。目标关键词太短(小于 token 大小)时可能直接搜不到,需要先调整ngram_token_size这类参数。

PostgreSQL 有一个很讨喜的方案,pg_trgm 扩展能在普通 LIKE 模糊查询场景用 GIN 索引加速:

CREATE EXTENSION pg_trgm; CREATE INDEX idx_remark_trgm ON order_table USING GIN (remark gin_trgm_ops); -- 此时 LIKE '%退款%' 会走索引 SELECT * FROM order_table WHERE remark LIKE '%退款%';

pg_trgm 的原理是提取连续三个字符的 trigram 做索引键,查询时把模式串也拆成 trigram 求交集。它对 3 个字符以上的关键词效果明显,对太短的词会退化成扫描。

全文索引不是免费的:写入变慢、索引占空间、查询计划可能不稳定。它适合"高频模糊搜索"场景,不适合一天只跑一次的临时分析,后者直接全扫反而省心。

5.3 数据模型级优化:拆表和冗余标记

当"包含"查询成为核心业务路径时,所有 SQL 层面的技巧都不如表结构设计来得彻底。

按前文说的,把多值字段拆成子表后,查询条件变成 tag_name 的等值匹配,这是数据库最擅长的事,索引和优化器都能发挥到极致。JSON 字段加 GIN 索引是妥协方案,保留灵活性的同时也能接受,但写入和校验成本比子表高。

还有一类查询,判断的维度是"某个关键词是否出现"这种布尔型语义。与其每次 LIKE,不如在写入时算好并落到一个布尔列,比如订单表的has_refund_flag。这个列上有索引,查询直接等值命中,耗时稳定在 1 毫秒左右。代价就是写入口要维护计算逻辑或者触发器,属于典型的空间换时间。

哪种方案适合你,可以从三个问题来判断:

  1. 判断结果是要做高频过滤,还是一次性分析?
  2. 匹配目标是否固定?关键词会不会经常变?
  3. 字段形态还能不能改?能不能拆表?

如果判断目标固定且高频,冗余布尔列最稳;如果关键词多变但查询高频,全文索引或 pg_trgm 合理;如果只是临时统计,LIKE 全扫也没关系,做好限流和 LIMIT 就行。最忌讳的是在千万级大表上,把LIKE '%关键词%'直接变成线上 API 的查询条件而不做任何加固。

6. 面试和选型实战中的应答逻辑

6.1 面试官问这个问题,其实想听分类讨论

"SQL 中判断一个字段是否包含一个数据的方法有哪些"这个问题,如果只回答"LIKE",在面试里只能算及格线。面试官真正想看的是:你能否意识到"包含"有不同语义,能否在不同数据库里挑出对应函数,能否说出性能差异。

一个好的回答结构大概是:先反问业务语义,是文本子串、多值成员还是精确归属;然后按语义给方案;最后补充性能边界——LIKE 通配符前缀会影响索引、中段包含要想到全文索引、多值字段优先拆表或用 JSON 索引。

这种结构化的回答,比背十个函数名更能体现项目经验。回答时主动提一句"不同数据库的函数名差异很大,迁移时要注意",基本就把自己的实战经验亮出来了。

6.2 一个可以抄作业的决策清单

以我实际项目里的习惯,面对"某字段是否包含某数据"时,会按下面这个顺序做决策:

场景首选方案备选方案不建议
小表、低频过滤LIKE '%xx%'定位函数上全文索引
大表、前缀匹配LIKE 'xx%' + 普通索引前缀索引裸 LIKE
大表、中段匹配全文索引 / pg_trgm冗余布尔列裸 LIKE
逗号分隔多值FIND_IN_SET拆子表 JOINLIKE '%x%'
JSON 多值jsonb / JSON_CONTAINS + 索引拆子表LIKE 字符串
固定关键词高频判断冗余布尔列 + 等值条件触发器维护每次 LIKE

这个表是我多次踩坑总结出来的经验值,用在大多数业务系统里都成立。你也可以把它当成面试回答的提纲,按行展开讲,比零散列举函数强得多。

6.3 几个常见变体问题的思路

面试还经常出现一些变体,比如"查字段同时包含 A 和 B 的记录"。如果按子串理解,可以:

WHERE remark LIKE '%A%' AND remark LIKE '%B%';

如果按正则,需要处理 AB 和 BA 两种顺序:

WHERE remark REGEXP 'A.*B|B.*A';

再比如"查不包含某值的数据",核心坑是 NULL 处理。第 3 节已经说过定位函数返回 NULL 的场景,回答时主动提 NULL 会加分。

还有一个高频题:"如何优化 WHERE name LIKE '%abc%'"。除了全文索引、pg_trgm,还可以说数据量评估、前缀拆列、反向列、冗余标记等思路。把第 5 节的内容用自己的话讲一遍,这个问题基本就稳了。我在实际面试候选人的时候,也会特别关注对方能不能主动区分"文本包含"和"集合包含",能区分的人,项目经验通常都不会太差。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询