SQL字段包含判断:各数据库写法与性能优化全攻略
2026/9/24 20:04:02 网站建设 项目流程

做开发这几年,“判断一个字段里有没有某个数据”大概是我见过最容易被问烂、也最容易写错的一类SQL。别看它原理简单,真落到不同数据库上,写法差异能把你坑到怀疑人生——同样的逻辑在MySQL里跑得好好的,搬到SQL Server上直接报错;本地数据量小感觉不出来,等上了生产几百万行,一条不走索引的查询就能把数据库拖垮。

这篇文章我打算抛开教科书式的罗列,从实际开发角度把这个问题拆透:先讲清楚“包含”这个词在不同业务场景下到底意味着什么,再分数据库给出正确写法,最后用真实经验告诉你哪些写法不能碰、为什么不能碰。如果你平时写SQL多,或者正在排查一条慢查询,这篇文章应该能帮你省不少事。

1. 先搞清楚“包含”到底有几种意思

在动手写SQL之前,我建议你先想明白一个问题:你要的“包含”,是哪种“包含”?这个没搞清楚,后面全白搭。我见过太多人拿着LIKE一顿匹配,结果查出来的数据驴唇不对马嘴,排查半天才发现是业务语义理解错了。

1.1 三种常见的“包含”语义

第一种是模糊子串匹配。这是最常用的,典型场景就是搜索:传一个关键词,去用户表里找“备注”字段里包含这个关键词的所有记录。比如搜索“项目经理”,就希望把所有备注里出现过这四个字的人都捞出来。

第二种是精确子串定位。你不光想知道包不包含,还想知道它出现在什么位置,或者想以它为标准截取后面的内容。这种需求就不能用LIKE了,因为LIKE只能告诉你是或否,给不出位置信息。

第三种是集合成员判断。这种最容易被忽略,字段里存的是一串用逗号分隔的ID,比如“1,3,5,8”,你想判断这串数据里是否包含“3”。你要是直接用LIKE '%3%',那恭喜你,13、23、135这些全部命中,结果完全错误。

看到没有,同一个“包含”,三种语义,三种写法,错了就是线上事故。我后来养成了个习惯:接到需求先问清楚“匹配到什么程度算命中”,再决定用哪个函数。

1.2 不同数据库对这个需求的支持差异

还有一个让人头大的点:不同数据库的内置函数完全不是一个套路。

MySQL有LOCATE、INSTR、FIND_IN_SET,还有REGEXP;SQL Server最常用的是CHARINDEX,但FIND_IN_SET这种函数压根不存在;PostgreSQL又有自己的POSITION和STRPOS;Oracle的INSTR虽然叫法和MySQL不一样,但功能接近。

更要命的是,哪怕函数名一样,用法也可能不同。比如LOCATE和INSTR,虽然都是找子串位置,参数顺序完全相反。我见过有同事把MySQL的代码直接丢到Oracle里跑,结果运行出来全是乱套的数据——因为INSTR的参数顺序不一样,他以为找到的是A在B里的位置,实际是B在A里的位置。

所以这篇文章后面每一节,我都会明确标注数据库类型。你按需取用,别串台。

2. 各数据库判断字段包含的详细写法

下面我按数据库分类,把每种写法都过一遍。这里直接给结论和代码,配合实际场景讲原理。

2.1 MySQL:LOCATE、INSTR、FIND_IN_SET、REGEXP

MySQL判断包含,最常用的就是这四个。先看一个用户表,假设表名叫user_info,字段remark存备注信息。

用LOCATE判断子串位置

SELECT * FROM user_info WHERE LOCATE('项目经理', remark) > 0;

LOCATE返回子串第一次出现的位置,从1开始计数,找不到返回0。所以判断“大于0”就是包含。

要注意的是,LOCATE有两个重载形式:

  • LOCATE(substr, str):返回substr在str中第一次出现的位置
  • LOCATE(substr, str, pos):从str的第pos个位置开始查找

这个pos参数在“判断包含”的场景里用得少,但在“判断是否从第N位之后出现过”这种需求里很实用。

用INSTR快速判断

SELECT * FROM user_info WHERE INSTR(remark, '项目经理') > 0;

INSTR和LOCATE功能几乎一样,区别就是参数顺序反过来了:INSTR(str, substr)。我第一次用的时候也经常搞混,后来记了个口诀:INSTR先给“被找的”,再给“要找的”,LOCATE反过来。

这两个函数性能上差别微乎其微,看团队规范选一个统一用就行。我个人习惯用INSTR多一点,因为Oracle也用它,迁移成本低。

用LIKE做模糊匹配

SELECT * FROM user_info WHERE remark LIKE '%项目经理%';

LIKE是大家最先学会的写法。%是通配符,代表任意长度的任意字符。要注意的是,LIKE判断包含是靠两边的%实现的,少了开头的%,就变成了“以某个字符串开头”的判断。

用REGEXP做正则匹配

SELECT * FROM user_info WHERE remark REGEXP '项目经理|产品经理';

如果你想匹配多个关键词,REGEXP最方便。还有一个小技巧是词边界匹配,比如你只想匹配独立的“经理”,不想匹配“项目经理”里的“经理”,可以用[[:<:]][[:>:]]这两个POSIX词边界符号。不过这个语法可读性很差,我看到代码里这么用都得停下来理解半天,团队里用的话一定要写注释。

用FIND_IN_SET做集合判断

这个专门针对逗号分隔的字段。假设user_info表有个role_ids字段,存的是“1,3,5,8”这种格式:

SELECT * FROM user_info WHERE FIND_IN_SET('3', role_ids) > 0;

FIND_IN_SET的第二个参数是逗号分隔的字符串列表,它会精确匹配列表中的每一项,不会出现LIKE那种“3命中13”的误伤。

2.2 SQL Server:CHARINDEX、LIKE、CONTAINS

SQL Server是我见过踩坑最多的数据库,因为很多从MySQL转过来的人会惯性写出INSTR,结果直接报错——SQL Server根本没有INSTR这个函数。

用CHARINDEX查找子串位置

SELECT * FROM user_info WHERE CHARINDEX('项目经理', remark) > 0;

CHARINDEX的语法是CHARINDEX(expressionToFind, expressionToSearch),返回子串起始位置,找不到返回0。参数顺序和INSTR一样:先给要找的,再给被找的。

用LIKE做模糊匹配

SELECT * FROM user_info WHERE remark LIKE '%项目经理%';

这个和MySQL一样,没什么好说的。但SQL Server的LIKE还有一个细节:它默认对中文的匹配受排序规则(Collation)影响。如果你的库是中文排序规则(比如Chinese_PRC_CI_AS),那LIKE匹配是大小写不敏感的,同时全角和半角在某些情况下也可能被等同于一个字符。这个坑平时不显眼,等到你拿固定字符串精确匹配的时候就会踩到。

用CONTAINS做全文检索

SELECT * FROM user_info WHERE CONTAINS(remark, '项目经理');

CONTAINS是SQL Server全文索引(Full-Text Index)的查询方式。它的优势是快——对于大文本字段,LIKE走不了索引,全文索引却能高效匹配。但前提是你必须提前在字段上创建全文索引,而且全文索引的维护有额外开销。

我有一条很深的教训:以前在做一个合同管理系统的搜索功能时,用户要在大段的合同文本里搜关键词。一开始用的LIKE '%关键词%',合同表才十几万行就慢到不行,DBA找到我说这条查询把数据库CPU拉满了。后来改成全文索引,搜索响应时间从秒级降到毫秒级。

补充:SQL Server 2016+ 的 STRING_SPLIT

如果你遇到的是逗号分隔的字段,SQL Server没有FIND_IN_SET这个函数,但可以用STRING_SPLIT配合EXISTS或者JOIN来实现:

SELECT * FROM user_info u WHERE EXISTS ( SELECT 1 FROM STRING_SPLIT(u.role_ids, ',') s WHERE s.value = '3' );

这个写法是把字段拆成多行再做精确匹配,逻辑上和FIND_IN_SET等价,但可读性差一些。注意STRING_SPLIT是从SQL Server 2016才开始有的,老版本库用不了。

2.3 PostgreSQL:POSITION、STRPOS、ILIKE、正则

PostgreSQL在字符串处理上算是几个数据库里最灵活的,但风格也和前两个完全不同。

用POSITION查找子串位置

SELECT * FROM user_info WHERE POSITION('项目经理' IN remark) > 0;

PostgreSQL的POSITION用的是SQL标准语法,写起来有点反直觉——不是函数调用式,而是POSITION('要找的' IN '被找的')。这只是写法风格问题,功能上和MySQL的LOCATE没区别。

用STRPOS查找子串位置

SELECT * FROM user_info WHERE STRPOS(remark, '项目经理') > 0;

STRPOS是PostgreSQL里更常用的写法,语法是STRPOS(string, substring),和MySQL的INSTR参数顺序一致。有意思的是,STRPOS源码实现上就是调用的POSITION,两者底层逻辑完全一样,选哪个纯粹看口味。

用LIKE / ILIKE做模糊匹配

-- 大小写敏感 SELECT * FROM user_info WHERE remark LIKE '%项目经理%'; -- 大小写不敏感 SELECT * FROM user_info WHERE remark ILIKE '%项目经理%';

PostgreSQL的LIKE是严格区分大小写的,这是和MySQL、SQL Server最大的区别。所以如果你要忽略大小写匹配英文文本,一定要用ILIKE,或者用LOWER函数转换(但那样索引就失效了,下面会讲)。

用正则做复杂匹配

SELECT * FROM user_info WHERE remark ~ '项目经理|产品经理';

PostgreSQL的~是正则匹配操作符,~*是忽略大小写的正则匹配,!~!~*是对应的不匹配。正则能力非常强,适合做复杂的模糊搜索。

2.4 Oracle:INSTR、LIKE、CONTAINS

Oracle在语法上更接近MySQL,函数生态也是以INSTR为主。

用INSTR查找子串位置

SELECT * FROM user_info WHERE INSTR(remark, '项目经理') > 0;

注意看,Oracle的INSTR参数顺序是INSTR(string, substring),和MySQL完全一致。所以你在MySQL写INSTR,迁到Oracle也能跑,这是我之前说更习惯用INSTR的原因之一。

用LIKE做模糊匹配

SELECT * FROM user_info WHERE remark LIKE '%项目经理%';

Oracle的LIKE同样支持%通配符。但Oracle的LIKE对NULL的处理比较隐蔽:如果字段值是NULL,LIKE判断结果是NULL而不是FALSE,这一点在很多数据库里都一样,后面我专门讲。

用CONTAINS做全文检索

SELECT * FROM user_info WHERE CONTAINS(remark, '项目经理') > 0;

Oracle的CONTAINS需要显式指定大于0,否则语法上会觉得差点什么。使用前提同样是建立全文索引(Oracle叫Oracle Text索引)。

2.5 一个快速对比表

数据库找位置函数模糊匹配集合判断全文搜索
MySQLLOCATE、INSTRLIKEFIND_IN_SETMATCH...AGAINST
SQL ServerCHARINDEXLIKESTRING_SPLIT + EXISTSCONTAINS
PostgreSQLPOSITION、STRPOSLIKE / ILIKE无原生,可用正则tsvector
OracleINSTRLIKE无原生,可用正则CONTAINS

这张表基本覆盖了90%的开发需求。真要用的时候先查一下自己库的类型,再对着选函数。

3. 性能问题:为什么你的“包含”查询这么慢

写得对只是及格线,跑得快才是分水岭。这一节我讲几个特别重要的性能经验,全部来自实际生产环境里的惨痛教训。

3.1 LIKE的索引陷阱:%开头的通配符走不了索引

这是数据库初学者最容易忽略的问题。假设user_info表有十万行数据,remark字段上建了索引:

-- 可以走索引:以固定前缀开头 SELECT * FROM user_info WHERE remark LIKE '项目经理%'; -- 无法走索引:开头是通配符,只能用全表扫 SELECT * FROM user_info WHERE remark LIKE '%项目经理%';

原因也好理解:B+树索引是按字段值的有序结构组织的。'项目经理%'能通过索引二分查找快速定位到前缀匹配的范围;但'%项目经理%'是在字符串任意位置找,索引的有序性完全用不上,只能遍历全表,对每一行做一次子串匹配。

十万行的表感觉不明显,一旦上了百万级,区别就是毫秒和分钟的差别。

那如果业务就是需要“任意位置包含”怎么办?三个方案:

第一个方案,加全文索引,用CONTAINS或MATCH...AGAINST,这对长文本最有效。代价是索引占用更多存储,写性能也受影响,适合读远多于写的场景。

第二个方案,如果提前知道要匹配的片段,可以拆出独立的关联表,比如把合同的每个标签拆一行存,建索引后就快了。这个本质上是反范式设计,把“字段包含”变成“关联表存在”,用空间换时间。

第三个方案,用Like命中了就认了,但配合限制查询范围(比如添加时间条件)把扫描行数降下来。实在扛不住再上Elasticsearch这类全文检索引擎,但那是另一个话题了。

3.2 对字段套函数,索引瞬间失效

这条我要重点说,因为我见过太多人踩雷。假设remark字段有索引,你想要判断字段是否以某个前缀开头,有人会这样写:

-- 错误示范:对字段套了函数,索引失效 SELECT * FROM user_info WHERE LEFT(remark, 4) = '项目经理';

这个写法在功能上没毛病,但数据库优化器会对全表的每一行调用LEFT函数,再拿结果和常量比较,索引完全用不上。正确的写法是:

-- 正确写法:字段保持原样,让前缀匹配用上索引 SELECT * FROM user_info WHERE remark LIKE '项目经理%';

类似的还有SUBSTR(remark, 1, 4) = '项目经理'DATE_FORMAT(create_time, '%Y-%m') = '2025-06'等等。核心原则就一条:别把字段包在函数里。如果你确实需要不区分大小写匹配,写WHERE LOWER(remark) = 'abc'也会让字段索引失效,这时候优先考虑改用ILIKE(PostgreSQL),或者在原列上建立函数索引(MySQL 8.0+和PostgreSQL都支持),但函数索引的维护成本要自己权衡。

3.3 千万避开“使用LIKE判断集合成员”

逗号分隔字段是一个很经典的错误用法。假设role_ids存的是“1,3,5,8”,你想判断是否包含角色3:

-- 错误示范:遇到13、23、35全部误伤 SELECT * FROM user_info WHERE role_ids LIKE '%3%'; -- 错误示范:精确匹配但会漏数据,因为3可能在中间 SELECT * FROM user_info WHERE role_ids = '3' OR role_ids LIKE '3,%' OR role_ids LIKE '%,3' OR role_ids LIKE '%,3,%';

第一个写法会把13、23、135、321全捞出来;第二个写法虽然能精确匹配,但SQL写出来又臭又长,效率也差。

更好的做法还是上面说的FIND_IN_SET(MySQL)或者拆表。我处理过一条慢SQL,就是这种“用LIKE匹配ID列表”的写法,几百万行的配置表每次查询都全表扫,后来把字段拆成关联表,加了索引,查询时间从3秒降到50毫秒。

3.4 全文索引到底什么时候用

判断一个字段是否包含某个数据,如果只是短字符串(如状态码、备注短词),用CHARINDEX、INSTR这类函数配合过滤条件就够了。但如果字段里存的是大段文本,比如文章正文、合同约定、工单描述,那LIKE的性能就是灾难性的。我在一个真实项目里对比过:文章表50万行,正文平均3000字,用LIKE '%关键词%'搜索平均耗时1.8秒,换用全文索引后降到100毫秒以内,差距接近20倍。

全文索引的核心原理是倒排索引——提前把文本分词,建一个“词 -> 文档列表”的映射,搜索时直接查词表,不用扫原表。代价是建索引耗时、占用额外磁盘、写入时更新索引变慢。所以它适合读多写少、文本长、搜索频繁的场景。

4. 实战踩坑记录与解决实录

这一节我专门整理几个我真正踩过的坑,每一个都是在线上环境里花了大几个小时才定位出来的。写下来算是给你提个醒。

4.1 大小写匹配的数据库差异

不同数据库对大小写的敏感程度完全不一样。MySQL默认的utf8mb4_general_ci排序规则不区分大小写,所以LIKE '项目管理' 能匹配到“项目管理”也能匹配到“项目管理”——对,中文没有大小写问题,但英文和字母数字组合就有影响了。

SQL Server更麻烦,它的排序规则决定行为。在默认的Chinese_PRC_CI_AS下,LIKE不区分大小写,这时候在代码里写LIKE 'AB%',可能把“ab”开头的也查出来,如果需要精确匹配,就得用COLLATE强制指定区分大小写的排序规则:

SELECT * FROM user_info WHERE remark LIKE 'AB%' COLLATE Chinese_PRC_CS_AS;

PostgreSQL正好相反,LIKE默认区分大小写,要用ILIKE或正则大小写不敏感模式才能查全。如果你从MySQL迁到PostgreSQL,这个差异必然踩坑——我见过不止一个团队迁移后出现“搜索结果变少”的线上事故,全是因为大小写策略变了。

4.2 NULL参与判断时容易翻车

假设remark字段值是NULL,下面这条语句的执行结果是NULL,而不是FALSE:

SELECT * FROM user_info WHERE remark LIKE '%项目经理%';

如果LIKE条件不满足,按逻辑应该不返回这条数据,结果也符合预期。但如果你写的是NOT LIKE:

SELECT * FROM user_info WHERE remark NOT LIKE '%项目经理%';

这条语句同样不返回NULL那条数据。对于习惯了面向对象语言的人来说,这非常反直觉——NULL既不属于“包含”也不属于“不包含”,它是“不知道”。所以在写否定判断时,最好显式加上条件:

SELECT * FROM user_info WHERE remark IS NOT NULL AND remark NOT LIKE '%项目经理%';

另外,CHARINDEX、LOCATE、INSTR这些函数遇到NULL参数时,返回值也是NULL,不是0。这意味不能用“函数返回值 = 0”这种方式判断不包含,必须同时处理NULL。我在写代码生成工具时专门在模板里加了这个逻辑,避免漏数据。

4.3 特殊字符转义:匹配“100%”时出了诡异数据

还有一个隐藏很深的坑是%和_这两个通配符本身。假设你想查备注里包含“100%”的记录:

-- 错误示范:%是通配符,会匹配任意内容,结果异常 SELECT * FROM user_info WHERE remark LIKE '%100%%';

这条SQL的本意是“包含100%”,但因为%被当成通配符,实际匹配的是“100”开头的任意字符串加上结尾的任意内容,结果范围完全跑偏。正确写法是用ESCAPE指定转义字符:

SELECT * FROM user_info WHERE remark LIKE '%100\%%' ESCAPE '\';

同理,下划线_匹配的是任意单个字符。比如搜索“A_B”,如果不转义,会同时匹配“ACB”、“AXB”等等。所以在做包含判断时,遇到这些特殊字符必须先转义。

4.4 中文全角半角导致的“包含”判断失灵

这个坑特别隐蔽。有一次用户反馈搜索“备注包含『张三』”的数据查不到,我开始以为是编码问题,排查了很久才发现,备注里存的是全角空格或全角标点,搜索条件用的是半角,导致匹配不上。

比如备注是“张三,项目负责人”用的是全角逗号,你搜“张三,项目”用的是半角逗号,LIKE直接匹配失败。这种问题没法靠SQL一概解决,只能靠业务侧统一输入规范,或者在写入时做规范化清洗。如果历史数据已经脏了,就得写一次性脚本把全角转半角:

-- MySQL示例:用REPLACE批量清洗常见全角标点 UPDATE user_info SET remark = REPLACE(REPLACE(remark, ',', ','), ':', ':');

这种清洗脚本上线前一定要先备份数据,并在测试库上验证替换结果,脏数据的坑往往比预想的多。

4.5 正则表达式的灾难性回溯

正则虽然强大,但写不好会触发灾难性回溯。我遇到过一个真实事故:一个搜索接口用了PostgreSQL的正则匹配字段内容,正则表达式里写了嵌套的量词,比如(a+)+$这种“有毒”模式,结果某个用户提交了一个特殊关键词,数据库直接卡死,CPU飙到100%。

排查了很久才找到原因,后来我把对外搜索统一替换成ILIKE加限定条件,需要复杂匹配的走全文索引,不再允许用户输入直接拼接到正则表达式里。这里的经验是:正则表达式不要直接拼接用户输入,尤其是后续可能写成嵌套量词的场景。如果一定要用正则,必须做好长度和模式校验。

5. 如何根据业务场景选择合适的方法

说了这么多,最后给你一个落地建议。当我们面对“判断字段是否包含某个数据”这个需求时,按下面这个顺序顺下来选型,基本不会错。

5.1 先判断数据形态

如果是短字符串、随意位置匹配,直接选LIKE或CHARINDEX/INSTR这类基础函数。数据量小无所谓,数据量大就把查询条件里加上其他过滤维度,减少扫描范围。

如果是大段文本、关键词搜索,赶紧上全文索引。MySQL用MATCH...AGAINST,SQL Server用CONTAINS,Oracle也内置了全套方案。别在LIKE上硬耗,性能差距是数量级的。

如果是逗号分隔的ID列表,MySQL用FIND_IN_SET最方便;SQL Server用STRING_SPLIT拆行;PostgreSQL可以用正则或者unnest函数。但根本上,这种设计适合低频小表,高频大表建议重建为关联表。

如果是JSON字段内的数据,MySQL 5.7+有JSON_CONTAINS,PostgreSQL有@>运算符,SQL Server有JSON_VALUE配合查询,这又是一套独立的语法体系。这里不展开说,但你要知道每个库都有JSON专项函数,别用LIKE硬匹配JSON文本——那种方式既慢又容易误命中键名。

5.2 再评估查询频率和性能底线

低频率查询(比如管理后台偶尔手动查一下),性能要求不高,怎么写简单怎么写。高频接口(比如用户每次搜索都触发),就必须把性能和稳定性放在第一位,优先考虑走索引的方案,同时给搜索接口加白名单长度限制。

我经常跟团队说一句话:SQL写法的好坏只有在数据量上来之后才会暴露。本地几万条数据,怎么写都不慢;上了生产几百万行,一次全表扫描可能就是几十秒。所以写每条SQL前,务必问自己一句:这条查询的数据量级是多少?索引能不能用上?

5.3 通用建议

最后给几条通用建议:

一是团队规范统一。别让一个项目里同时出现LOCATE、INSTR、CHARINDEX三种用法,别人review代码时容易看错参数顺序。定好标准,写进团队代码规范里。

二是对“包含”类查询做好SQL审查。在代码评审阶段,凡是出现LIKE '%xxx%'或对字段套函数的,reviewer要格外关注,确认是否有更好的方案。

三是测试数据要有“脏数据”样本。造测试数据时故意放一些边界情况:NULL、空字符串、超长文本、含特殊字符的字段、大小写混合的字段。这样能提前暴露匹配规则问题,而不是等上线后用户来报bug。

写在最后的经验

做数据库开发这些年,我最大的体会是:越是看起来简单的问题,越值得多花两分钟去想清楚“为什么”。就拿“判断字段包含一个数据”来说,函数选型、参数顺序、大小写规则、NULL处理、通配符转义、索引利用,每一个细节背后都是生产环境里真实踩出来的坑。你把这些细节都摸透了,写SQL的功底就不一样了。

最后分享一个我自己的小习惯:每接触一个新的数据库,我都会先建一张临时表,把LIKE、CHARINDEX、LOCATE、CONTAINS这些常用函数从头到尾过一遍,看看它们在当前版本下的行为差异,然后在笔记里记录归档。下次遇到跨库迁移或者新项目选型,翻一眼笔记就能避开大部分坑,省下的时间绝对对得起当初这几分钟。

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

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

立即咨询