ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SQL字段包含判断:各数据库写法与性能优化全攻略

2026/9/24 20:04:05 拓冰建站 浏览量
SQL字段包含判断:各数据库写法与性能优化全攻略 做开发这几年“判断一个字段里有没有某个数据”大概是我见过最容易被问烂、也最容易写错的一类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还有REGEXPSQL Server最常用的是CHARINDEX但FIND_IN_SET这种函数压根不存在PostgreSQL又有自己的POSITION和STRPOSOracle的INSTR虽然叫法和MySQL不一样但功能接近。更要命的是哪怕函数名一样用法也可能不同。比如LOCATE和INSTR虽然都是找子串位置参数顺序完全相反。我见过有同事把MySQL的代码直接丢到Oracle里跑结果运行出来全是乱套的数据——因为INSTR的参数顺序不一样他以为找到的是A在B里的位置实际是B在A里的位置。所以这篇文章后面每一节我都会明确标注数据库类型。你按需取用别串台。2. 各数据库判断字段包含的详细写法下面我按数据库分类把每种写法都过一遍。这里直接给结论和代码配合实际场景讲原理。2.1 MySQLLOCATE、INSTR、FIND_IN_SET、REGEXPMySQL判断包含最常用的就是这四个。先看一个用户表假设表名叫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 ServerCHARINDEX、LIKE、CONTAINSSQL 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 PostgreSQLPOSITION、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 OracleINSTR、LIKE、CONTAINSOracle在语法上更接近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的处理比较隐蔽如果字段值是NULLLIKE判断结果是NULL而不是FALSE这一点在很多数据库里都一样后面我专门讲。用CONTAINS做全文检索SELECT * FROM user_info WHERE CONTAINS(remark, 项目经理) 0;Oracle的CONTAINS需要显式指定大于0否则语法上会觉得差点什么。使用前提同样是建立全文索引Oracle叫Oracle Text索引。2.5 一个快速对比表数据库找位置函数模糊匹配集合判断全文搜索MySQLLOCATE、INSTRLIKEFIND_IN_SETMATCH...AGAINSTSQL ServerCHARINDEXLIKESTRING_SPLIT EXISTSCONTAINSPostgreSQLPOSITION、STRPOSLIKE / ILIKE无原生可用正则tsvectorOracleINSTRLIKE无原生可用正则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也会让字段索引失效这时候优先考虑改用ILIKEPostgreSQL或者在原列上建立函数索引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_SETMySQL或者拆表。我处理过一条慢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而不是FALSESELECT * FROM user_info WHERE remark LIKE %项目经理%;如果LIKE条件不满足按逻辑应该不返回这条数据结果也符合预期。但如果你写的是NOT LIKESELECT * 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...AGAINSTSQL Server用CONTAINSOracle也内置了全套方案。别在LIKE上硬耗性能差距是数量级的。如果是逗号分隔的ID列表MySQL用FIND_IN_SET最方便SQL Server用STRING_SPLIT拆行PostgreSQL可以用正则或者unnest函数。但根本上这种设计适合低频小表高频大表建议重建为关联表。如果是JSON字段内的数据MySQL 5.7有JSON_CONTAINSPostgreSQL有运算符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这些常用函数从头到尾过一遍看看它们在当前版本下的行为差异然后在笔记里记录归档。下次遇到跨库迁移或者新项目选型翻一眼笔记就能避开大部分坑省下的时间绝对对得起当初这几分钟。