写这篇文章的起因,是前两天一位刚转岗做数据报表的朋友问我:SQL Server里想查某个客户名字里带“华”字的所有记录,为什么LIKE '%华%'有时候查得出来,有时候查不出来,还有时候慢得要命?这个问题看着基础,真要讲清楚其实能扯出一大串:通配符的写法、大小写排序规则、索引能不能命中、函数搭配怎么用、NULL值陷阱、转义字符……所以我干脆把SQL Server里模糊查询和常用查询函数这块儿完整梳理一遍,既是给新手一份能“抄作业”的实操手册,也算给我自己做个备忘。
这篇文章没有任何版本歧视,SQL Server 2008 R2到2022我都用过,文里的SQL语法在主流版本上基本通用,遇到个别版本差异我会单独标注。内容涵盖LIKE通配符的完整用法、模糊查询的性能优化思路、字符串/聚合/日期三大类查询函数,以及我这些年实际踩过的坑。适合刚入门SQL的在校生、天天写报表的数据分析师,以及正在维护老系统的开发同学。
1. 模糊查询的核心:LIKE 用法拆解
1.1 四种通配符,先把这个记死
LIKE的核心价值就四个字:模糊匹配。它靠通配符去匹配“包含关系”“开头关系”“位置关系”,比等号=那种“非黑即白”的死匹配灵活太多。我做培训的时候喜欢打一个比方:等号匹配就像找一栋楼必须门牌号完全一样,LIKE匹配则像只告诉快递员“小区名里有‘江’字”,他就能找出一圈候选地址。
SQL Server里的LIKE通配符一共四种:
| 通配符 | 作用 | 示例 | 匹配结果 |
|---|---|---|---|
% | 匹配任意长度的字符,包括0个字符 | WHERE name LIKE '张%' | 张三、张伟、张 |
_ | 匹配单个任意字符 | WHERE name LIKE '张_' | 张三、张飞(仅两个字的) |
[] | 匹配指定范围内的单个字符 | WHERE name LIKE '[张李王]三' | 张三、李三、王三 |
[^] | 匹配不在指定范围内的单个字符 | WHERE name LIKE '[^张李王]三' | 赵三、刘三(排除张/李/王) |
初学阶段最容易混的是_和%。_严格占一个字符位,你写LIKE '张_'就绝对匹配不到“张伟强”,因为伟后面还有“强”,长度不匹配。%则不然,它代表的是零到无数个字符,LIKE '张%'既能匹配“张”,也能匹配“张三”、“张伟强”、“张无忌的大舅子”。
方括号[]的使用频率没那么高,但某些场景特别好用。比如你要查所有名字里带数字的客户,可以直接WHERE 客户名 LIKE '%[0-9]%',一条语句把所有包含阿拉伯数字的名称全捞出来,这个比写一堆OR条件干净得多。字母范围也同理,LIKE '[A-Z]%'能匹配所有以大写字母开头的数据。
1.2 大小写敏感与排序规则,查不出来多半是这个原因
我见过太多人写LIKE '%abc%'却发现大写ABC的记录一条也查不出来,第一反应是数据有问题,其实九成情况是排序规则(Collation)在作祟。
SQL Server的排序规则分两大类:_CI_表示大小写不敏感(Case Insensitive),_CS_表示大小写敏感(Case Sensitive)。比如安装时默认常见的Chinese_PRC_CI_AS和SQL_Latin1_General_CP1_CI_AS都是CI(不区分大小写),所以你的LIKE查询里大小写都能匹配上。但如果数据库或者字段级别把排序规则设置成了Latin1_General_CS_AS,那LIKE '%abc%'就只会匹配小写abc,ABC一条都出不来。
你可以在查询里这样确认:
-- 查看当前数据库的排序规则 SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS DatabaseCollation; -- 直接在查询时临时指定大小写不敏感(推荐方式) SELECT * FROM 用户表 WHERE 用户名 COLLATE Latin1_General_CI_AS LIKE '%abc%';这个坑在接口对接场景尤其常见:业务系统表是CS排序规则,前端搜索框传过来的是小写关键字,两边一碰就查漏数据。我的建议是:不要在LIKE的查询条件里过度依赖默认排序规则,拿不准的时候直接显式指定COLLATE,一次把规则定死,别让环境差异背锅。
1.3 转义字符:让通配符变成普通字符
继续讨论数据里真的包含%或_的情况。比如产品编码规则是“前缀%后缀”格式,你要查编码里含%的记录,直接写LIKE '%%%'肯定乱套——三个%到底哪个是通配符,哪个是普通字符,SQL Server分不清。
解决办法是转义。SQL Server用ESCAPE关键字指定转义字符,推荐用感叹号!或反斜杠\,不推荐用方括号语法,因为可读性太差:
-- 查编码中包含“%”字符的记录 SELECT * FROM 产品表 WHERE 产品编码 LIKE '%!%%' ESCAPE '!'; -- 查编码中包含“_”下划线的记录 SELECT * FROM 产品表 WHERE 产品编码 LIKE '%!_%' ESCAPE '!';ESCAPE的实际含义就是告诉SQL Server:感叹号后面的那个字符请当作普通字符处理,不要当通配符。这里的ESCAPE '!'语法在SQL Server 2005以后的版本都支持,老项目也完全能用,不存在兼容性问题。
还有一种老式语法是用方括号把通配符包起来,LIKE '%[%]%'也能匹配到包含%的记录。这种写法在PostgreSQL、MySQL里也通用,但我个人还是更推荐ESCAPE,因为方括号语法一旦和字符集范围混在一起,很容易写出LIKE '%[[]%'这种自己都看不懂的表达式。
1.4 LIKE + 前端搜索框的最常见拼接写法
在正式开发一个带搜索框的页面时,常用的做法是后端收到关键字参数后,把它拼进LIKE条件。这里我最想提醒的是:不要在EF Core、Dapper或者存储过程里用字符串拼接的方式直接怼参数,而是优先用参数化查询。比如C#加Dapper的经典写法:
var keyword = "%" + input.Trim() + "%"; var list = connection.Query<User>( "SELECT * FROM UserInfo WHERE UserName LIKE @keyword", new { keyword }).ToList();注意,输入过滤不只是为了防注入,LIKE条件还有个隐藏问题:如果用户输入的内容里带了%、_、[这些通配符,你的搜索会被带偏。比如用户搜“A%”,你拼成LIKE '%A%%',他会把所有包含“A”的记录全查出来。所以更严谨的做法是先把用户输入里的通配符转义掉再拼通配:
input = input.Replace("[", "[[]").Replace("%", "[%]").Replace("_", "[_]"); var keyword = "%" + input + "%";这样用户搜“A%”的时候,%被当作普通文本处理,查出来的就是真正包含“A%”字样的记录,而不是把全表以A开头的都算上。我把这个处理逻辑封装在工具方法里,每个新项目都是直接复制过去。
2. 模糊查询性能调优:LIKE会不会走索引
2.1 前缀匹配走索引,中间匹配全表扫
先给结论:LIKE 'abc%'这种前缀匹配(通配符在最后),在SQL Server里是可以使用索引的;LIKE '%abc%'这种中间匹配(通配符在最前),索引就帮不上忙了,只能全表扫描。
原理并不难理解:索引是按照B+树的顺序排列的,类似于字典按拼音排好序。你查“以abc开头”的数据,等同于在字典里翻到“abc”的区间,效率极高。但你要查“所有包含abc的数据”,就像要求你在字典里找出所有正文里出现过“abc”这三个字母的条目——单靠目录排序做不到,只能一页一页翻。
看一个实际例子。假设订单表的订单号列上建了索引,下面两种写法性能差距巨大:
-- 能高效走索引,百万级数据毫秒级返回 SELECT * FROM 订单表 WHERE 订单号 LIKE 'SO2024%'; -- 索引失效,全表扫描,数据量大时直接卡死 SELECT * FROM 订单表 WHERE 订单号 LIKE '%SO2024%';SEO时代大家喜欢把搜索框做成含任意位置的模糊匹配,但对大表来说这是性能毒药。如果业务上确实需要从任意位置匹配,有几个替代方案:第一,数据量小(十万行以内)且并发低,直接全表扫其实没多大事;第二,数据量大,可以引入全文索引(Full-Text Index),用CONTAINS代替LIKE;第三,如果只是固定几个前缀规则,加冗余字段存前缀,用前缀匹配。
2.2 CHARINDEX 与 PATINDEX:模糊匹配的替代函数
LIKE是条件匹配,但有时候你需要在SELECT结果里直接返回“关键字出现的位置”,那就要用到CHARINDEX和PATINDEX了,恰好在“查询函数”范畴里。
CHARINDEX用于查找一个字符串在另一个字符串中的起始位置,找不到就返回0;PATINDEX则支持用通配符去查找模式串的起始位置,是“函数版LIKE”。
-- 返回3,因为“sql”从第3个字符开始 SELECT CHARINDEX('sql', 'my sql server'); -- 返回7,第7-9位是“123”,符合[0-9][0-9][0-9]格式 SELECT PATINDEX('%[0-9][0-9][0-9]%', 'abc123def'); -- 用CHARINDEX判断包含关系,等价于LIKE SELECT * FROM 产品表 WHERE CHARINDEX('华为', 产品名称) > 0;这里有个性能相关的经验:WHERE CHARINDEX(关键字, 字段) > 0的写法也是无法走索引的,和LIKE '%关键字%'是难兄难弟。但从写法上看,CHARINDEX适合在关键字本身也是动态变量、需要进一步参与计算的场景。比如你要“找出商品描述里第二次出现‘优惠’的位置”,用LIKE很难优雅实现,CHARINDEX三参数版本直接解决:
-- 第三个参数2表示从第2位开始找 SELECT CHARINDEX('优惠', 商品描述, 2) AS 第二次出现位置 FROM 商品表;2.3 全文索引:大文本模糊搜索的终极方案(附适用条件)
如果某张表的body字段存的是长篇文章,你还要按文章内容做搜索,LIKE '%关键词%'在大数据量下基本跑不动。这时候SQL Server的全文索引节点就非常值得研究。
全文索引的基本使用套路是:先建索引,再用CONTAINS或FREETEXT查询。
-- 创建全文索引(需要先有一个唯一索引) CREATE FULLTEXT CATALOG ft_catalog AS DEFAULT; CREATE FULLTEXT INDEX ON 文章表(正文) KEY INDEX PK_文章表 WITH STOPLIST = SYSTEM; -- 查询正文中包含“数据库优化”的记录 SELECT * FROM 文章表 WHERE CONTAINS(正文, '数据库优化'); -- 按词形变化匹配,能匹配到“running”等变形 SELECT * FROM 文章表 WHERE FREETEXT(正文, 'run');我的实际体感:全文索引对中文分词的支持没有云搜索那么聪明,但对付固定词组查询完全够用。注意,全文索引的对象是词而不是字符,所以CONTAINS(正文, '数据')查不到“数据库优化”里单独的“数据”,除非启用中文分词特性。如果你的需求只是字段前缀匹配,比如邮编、订单号,那全文索引是杀鸡用牛刀,老老实实用LIKE '前缀%'就好。
3. 查询函数的第一梯队:字符串函数全解析
3.1 截取类:LEFT、RIGHT、SUBSTRING
做数据清洗时,字符串截取是三板斧。比如订单号SO2024123456,想取年份和流水号,用函数直接拆:
SELECT 订单号, LEFT(订单号, 2) AS 前缀, SUBSTRING(订单号, 3, 4) AS 年份, RIGHT(订单号, 6) AS 流水号 FROM 订单表;LEFT(字符串, 长度):从左边截取指定长度RIGHT(字符串, 长度):从右边截取指定长度SUBSTRING(字符串, 起始位置, 长度):从任意位置截取指定长度,起始位置从1开始计数
这里最值得提醒的是SUBSTRING的边界问题。很多人把起始位置和长度搞混,尤其是从1开始还是从0开始,SQL Server是从1开始,SUBSTRING('abcd', 1, 2)返回的是ab而不是abc。我做报表时曾经因为从0开始取,导致所有编码第一位被吞掉,排查半天才反应过来。另外,SUBSTRING配合CHARINDEX可以轻松提取两个分隔符之间的内容,比如从“品牌-型号-容量”中提取型号:
SELECT SUBSTRING( 商品全名, CHARINDEX('-', 商品全名) + 1, CHARINDEX('-', 商品全名, CHARINDEX('-', 商品全名) + 1) - CHARINDEX('-', 商品全名) - 1 ) AS 型号 FROM 商品表;这段代码看着绕,本质就是先找到第一个-的位置,再找到第二个-的位置,两者中间的长度就是型号部分。刚接触会觉得嵌套很深,但这是字符串解析的经典模式,用熟了顺手得很。
3.2 查找定位类:LEN、CHARINDEX、PATINDEX
前面提到过CHARINDEX和PATINDEX,这里和LEN、DATALENGTH放到一起看。
LEN返回字符串的字符数,但要注意它不计算尾随空格。LEN('abc ')返回3,而不是5。这经常成为数据验证里的盲区:你看着字符串有空格,LEN却告诉你没空格。如果需要精确的字节数,尤其是存储中文字符(一个汉字占2字节)的场景,就得用DATALENGTH:
-- LEN不数尾随空格,DATALENGTH数实际字节 SELECT LEN('abc ') AS len_val, -- 3 DATALENGTH('abc ') AS datalen_val, -- 6(含3个空格) DATALENGTH('数据库') AS chinese_byte; -- 6(一个汉字2字节)PATINDEX是模糊匹配的函数版,它对大小写是否敏感同样遵循所在数据库的排序规则。用它判断一段文本是否符合特定字符模式,比LIKE更有优势,因为它能返回值而不是布尔逻辑,比如判断手机号是否纯数字开头:
SELECT 手机号, PATINDEX('%[^0-9]%', 手机号) AS 首个非数字位置 FROM 用户表;如果查询结果全是0,说明手机号全部由数字组成;如果返回某个正整数,说明该位置出现了非数字字符。这种“数据质量体检”写法,比嵌套一堆REPLACE高效太多。
3.3 替换改造类:REPLACE、STUFF、LTRIM/RTRIM
清洗脏数据时REPLACE出镜率最高,比如把历史录入错误的全角逗号统一改成半角、把电话号码里的横杠去掉:
SELECT REPLACE(电话号码, '-', '') AS 去横杠号码, REPLACE(REPLACE(地址, ',', ','), '。', '.') AS 规范地址 FROM 客户表;STUFF是个更隐蔽但超有用的字符串缝合函数。它的语法是STUFF(原字符串, 起始位置, 删除长度, 插入字符串),作用是把指定位置的内容替换掉,并且插入新内容。最经典的场景是手机号脱敏,或者银行卡中四位打星:
-- 把手机号第4位开始的4位替换成**** SELECT STUFF(手机号, 4, 4, '****') AS 脱敏手机号 FROM 用户表; -- 13812345678 -> 138****5678LTRIM和RTRIM分别去掉左边和右边的空格。SQL Server 2017以后出了个TRIM函数可以同时去两边空格,而且还支持指定字符,但考虑到还有大量2016及以前的存量系统,我建议写脚本时还是老老实实用LTRIM(RTRIM(字段))组合,兼容性最稳。
再补充一下大小写转换LOWER和UPPER,适合在邮箱匹配和一些业务代码归一化的场景。注意它们对中文没有影响,因为中文没有大小写概念。
SELECT UPPER('abc@qq.com'); -- ABC@QQ.COM3.4 拼接类:加号 + 与 CONCAT 的区别
字符串拼接这块儿有个经典大坑:SQL Server里用+拼接,遇到NULL就整体变成NULL。比如客户表里姓氏和名字分开存储,其中一个为NULL,你SELECT 姓 + 名 AS 全名查出来就是NULL,应用层展示直接空一列。
CONCAT函数(SQL Server 2012+)则不同,它会自动把NULL当成空字符串处理:
-- 老写法:只要有一个NULL,全名为NULL SELECT 姓 + 名 AS 全名 FROM 客户表; -- 新写法:NULL自动忽略,拼接结果更符预期 SELECT CONCAT(姓, 名) AS 全名 FROM 客户表;我这里想强调两个实操建议。其一,如果你的服务器版本是2012以上,直接默认用CONCAT,少一个NULL坑就少一次线上事故;其二,如果项目还在2008 R2,那只能用ISNULL先把NULL转成空串:
SELECT ISNULL(姓, '') + ISNULL(名, '') AS 全名 FROM 客户表;另外,+拼接数字时会先把数字隐式转换成字符串,但需要注意顺序。SELECT 1 + 2 + '3'在不同数据库里可能会得到33(字符串'33')或6(整数6),SQL Server遵循表达式从左到右的类型转换规则,这里也容易出隐性bug。我建议混合拼接时一律先转成VARCHAR:
SELECT CAST(订单数量 AS VARCHAR(10)) + '件' FROM 订单表;4. 查询函数的第二梯队:聚合函数与分组统计实战
4.1 五大聚合函数:SUM、AVG、COUNT、MAX、MIN
聚合函数的价值是把多行数据浓缩成一行统计结果。SQL Server中最常用的五个:
SUM(字段):求和,只适用于数值类型AVG(字段):求平均值,自动忽略NULLCOUNT(字段):计数,COUNT(*)统计所有行,COUNT(列名)统计该列非NULL的行数MAX(字段)/MIN(字段):求最大/最小值,适用于数值、字符串、日期
我特别想强调COUNT(*)和COUNT(列)的差别。举例,客户表有100条记录,其中手机号字段有3条是空值,那么COUNT(*)返回100,COUNT(手机号)返回97。这个差异在写报表时非常容易踩坑——你想统计“有多少客户填了手机号”,结果写了个COUNT(*),把没填手机号的也一起算进去了,数据直接虚高。
另外一个隐蔽点是AVG自动忽略NULL,这会导致平均值的“分母”变小。比如3笔订单金额分别是100、200、NULL,AVG(金额)结果是150,而不是100。理由也合理:NULL代表未知,不该参与计算。但业务上你可能希望NULL按0参与分母计算,这时得用AVG(ISNULL(金额, 0)),把NULL先转换成0。
4.2 GROUP BY 分组统计的黄金搭配
聚合函数不配GROUP BY,就只能输出一行总计,真正日常报表大多需要按维度分组,比如“每个月的销售总额”“每个部门的平均工资”。
SELECT 部门ID, COUNT(*) AS 员工数, AVG(工资) AS 平均工资, MAX(工资) AS 最高工资, MIN(工资) AS 最低工资, SUM(工资) AS 工资总和 FROM 员工表 GROUP BY 部门ID;GROUP BY的语法约束是:SELECT子句里出现的列,要么出现在GROUP BY里,要么被聚合函数包裹,否则SQL Server直接报错。这条规则看似死板,其实是帮你守住逻辑边界——你想统计部门维度,就不能在明细里露员工姓名,否则语义说不通。
还有个频率极高的坑:GROUP BY之后想过滤分组结果,用WHERE是无效的。WHERE在分组之前执行,只能过滤原始行;分组之后的条件要用HAVING:
-- 找出订单数超过100个的客户 SELECT 客户ID, COUNT(*) AS 订单数 FROM 订单表 GROUP BY 客户ID HAVING COUNT(*) > 100;如果同时有WHERE和HAVING,执行顺序是:WHERE先过滤原始行,然后分组,再做HAVING过滤分组结果,最后SELECT输出。理解这个顺序,写复杂统计语句基本不会乱。
4.3 一个完整的多函数组合案例
现在串一个贴近业务的例子:统计每个产品分类下,价格超过100元的产品数量,以及其中最便宜的价格。这个需求既要用WHERE过滤明细,又要GROUP BY分组,还要用聚合函数统计:
SELECT 分类ID, COUNT(*) AS 高价产品数, MIN(单价) AS 最低价, MAX(单价) AS 最高价, ROUND(AVG(单价), 2) AS 平均价 FROM 产品表 WHERE 单价 > 100 GROUP BY 分类ID ORDER BY 高价产品数 DESC;这里ROUND(AVG(单价), 2)是嵌套函数——先用AVG求平均值,再用ROUND保留两位小数,防止原始平均价出现一长串小数。SQL的嵌套函数很常见,运算顺序是从内向外,和数学公式一样。这种嵌套逻辑你写得越多,读别人的复杂查询就越轻松。
5. 时间与转换函数:查询函数里最容易被忽略的角落
5.1 日期函数:GETDATE、DATEADD、DATEDIFF
业务查询里日期过滤是家常便饭,但很多人写日期条件时特别喜欢WHERE 下单时间 = '2024-01-01',然后发现白天下的单一条都查不出来,因为下单时间是datetime类型,精确到了时分秒,而’2024-01-01‘是零点时刻,两者对不上。
更稳的写法是用范围条件:
-- 推荐:左闭右开区间 SELECT * FROM 订单表 WHERE 下单时间 >= '2024-01-01' AND 下单时间 < '2024-01-02'; -- 或者用CONVERT去掉时间部分再比较 SELECT * FROM 订单表 WHERE CONVERT(date, 下单时间) = '2024-01-01';用函数包住字段(如CONVERT(date, 下单时间))会让索引失效,大数据量下不推荐;所以首选还是范围条件法。
动态日期计算需要用到DATEADD和DATEDIFF。DATEADD给日期增加或减去一个时间间隔,DATEDIFF计算两个日期的间隔数:
-- 查询最近7天的订单 SELECT * FROM 订单表 WHERE 下单时间 >= DATEADD(day, -7, GETDATE()); -- 计算客户注册至今的天数 SELECT 客户名, DATEDIFF(day, 注册日期, GETDATE()) AS 注册天数 FROM 客户表; -- 查询本周的订单:本周从周一开始(取决于@@DATEFIRST) SELECT * FROM 订单表 WHERE 下单时间 >= DATEADD(day, 1 - DATEPART(weekday, GETDATE()), CAST(GETDATE() AS date));这里DATEPART(weekday, GETDATE())返回今天是本周第几天,比如周日返回1(默认设置下)、周一直返2。用它反推本周起始日的写法是SQL Server里标准的周报统计套路,值得直接抄走。
5.2 格式转换三件套:CAST、CONVERT、FORMAT
数据导入导出时,字符串和日期、数字之间互相转换是高频操作。CAST是标准SQL语法,CONVERT是SQL Server扩展,支持更多样式码,FORMAT则是2012以后才有的花活,灵活但性能差。
-- CAST 基础用法 SELECT CAST(下单时间 AS date) AS 下单日期 FROM 订单表; -- CONVERT 转字符串并指定样式 112=yyyymmdd SELECT CONVERT(varchar(10), 下单时间, 112) AS 日期键 FROM 订单表; -- FORMAT 自定义格式(中文环境下好用但慢) SELECT FORMAT(下单时间, 'yyyy-MM-dd') AS 日期字符串 FROM 订单表;我的建议是:数字和短日期转换用CONVERT,因为它有丰富的样式码;跨数据库移植性要求高时用CAST;只有需要非常规格式(比如“2024年01月”这种中文日月格式)才用FORMAT,而且数据量大时必须谨慎——FORMAT是基于.NET CLR实现的,性能比CONVERT差一到两个数量级,我在百万级数据上实测过,慢得让人怀疑人生。
5.3 时间函数与模糊查询的联动场景
字符串函数和日期函数经常和LIKE绑在一起用。比如很多老系统会把日期存成YYYYMMDD格式的字符串,你要按月份模糊查询,直接LIKE '202401%'就能命中所有1月的数据:
-- 月份维度统计,日期存储为字符串 yyyyMMdd SELECT LEFT(订单日期, 6) AS 年月, COUNT(*) AS 订单数 FROM 订单流水表 WHERE 订单日期 LIKE '2024%' GROUP BY LEFT(订单日期, 6);这种用LIKE处理定长字符串日期的写法,能避开各种日期格式转换带来的坑。但提醒一句:它只适用于“存储格式严格统一”的表。如果这个字符串列偶尔出现2024-01-05或者2024015这种脏数据,LIKE查询就会漏计或者错计,前置的数据质量检查跑不掉。
6. 常见问题与排查技巧实录
6.1 模糊查询慢到爆炸,怎么查原因
先看执行计划。SQL Server Management Studio里按Ctrl+M开启“包含实际执行计划”,再执行查询,看有没有表扫描(Table Scan)或聚集索引扫描(Clustered Index Scan),以及扫描预估行数是不是几十万起步。
如果确认是LIKE '%关键字%'导致的全表扫描,按数据量分三个档处理:两万行以内,别折腾,直接扫,成本可忽略;两万到两百万行,考虑改造成前缀LIKE或者用冗余字段缓存一份可前缀匹配的数据;超过两百万行且搜索并发高,直接上全文索引或者迁到专门的搜索引擎组件,不要在SQL Server里硬扛。
还有个人为制造的慢查询:在LIKE关键字两端加%之前,应用层忘了Trim,导致传入的条件里带着前后空格,LIKE '% 关键字 %'自然匹配不到预期结果,但执行的代价却是全表扫。处理方式很简单,抓SQL日志看实际执行语句即可快速定位。
6.2 明明数据存在,LIKE却查不到,排查方向清单
这种问题基本可以按顺序排查:第一,看是否有不可见字符,比如全角空格、回车换行,用DATALENGTH对比字段长度,或者SELECT '[' + 字段 + ']'把字符串前后边界标出来;第二,看排序规则,按文章开头提到的方法检查CS/CI,必要时临时加COLLATE验证;第三,看字段是否真的是字符串类型,有时候数值类型字段被隐式转换,LIKE '%123%'匹配0结果,改用CAST(字段 AS VARCHAR)再查;第四,确认不是NULL,因为LIKE碰上NULL既不算匹配也不算不匹配,直接返回UNKNOWN,结果就是查不到,这种场景用IS NULL或ISNULL处理。
NULL这个坑我多说一句:WHERE 字段 NOT LIKE '%xxx%'在字段值NULL时也返回UNKNOWN,也就是说这一行既不会被NOT LIKE条件保留下来。如果你期望“非匹配”的集合里包含NULL行,得显式加OR 字段 IS NULL:
SELECT * FROM 客户表 WHERE 备注 NOT LIKE '%黑名单%' OR 备注 IS NULL;6.3 如何快速验证一个LIKE查询是否会走索引
临时用一个低代价的实验最快:找一张50万行以上的表,分别执行带前缀匹配和中间匹配的语句,开启SET STATISTICS IO ON,观察逻辑读取次数。前缀匹配的LIKE 'ABC%'可能只有几十次逻辑读,中间匹配的LIKE '%ABC%'往往直接几千次起步,这个数字差就是性能和代价的最直观体现。
SET STATISTICS IO ON; SELECT * FROM 订单表 WHERE 订单号 LIKE 'SO2024%'; -- 逻辑读较少 SELECT * FROM 订单表 WHERE 订单号 LIKE '%SO2024%'; -- 逻辑读暴涨如果前缀LIKE也走不了索引,大概率是字段上根本没建索引,或者查询字段上套了函数(比如WHERE LEFT(订单号, 2) = 'SO'),后者属于典型的索引杀手,改成LIKE 'SO%'才能救回来。
6.4 排序规则冲突:多表关联LIKE报错
两个表关联后做模糊查询,如果两边字段的排序规则不同(比如一个是Chinese_PRC_CI_AS,一个是SQL_Latin1_General_CP1_CI_AS),SQL Server会直接报“无法解决排序规则冲突”的错误。解决办法是给其中一个字段显式指定COLLATE:
SELECT * FROM 表A a JOIN 表B b ON a.名称 COLLATE Chinese_PRC_CI_AS = b.名称 WHERE a.名称 LIKE '%测试%';这种冲突常见于把老库和新库的表做跨库关联的场景。为了不动底表结构,查询时手动调整字段的COLLATE是最安全和最小的改动方案。
最后再分享一个配套的小技巧
我在实际项目里还发现一个高频需求:同时查多个关键词,且关键词之间是“且”的关系。直接用AND嵌套LIKE没问题,但逻辑一多就乱。我会先在应用层把关键词拆成数组,然后拼多个LIKE条件,每个条件都加上转义处理,前文已讲。如果是“或”的关系,我习惯用OR或者借助临时表存关键词再JOIN去判断:
-- 查询名称同时包含“旗舰”和“2024”的型号 SELECT * FROM 产品表 WHERE 产品名称 LIKE '%旗舰%' ESCAPE '!' AND 产品名称 LIKE '%2024%' ESCAPE '!';这个写法看着没什么特别,但配合转义字符后,用户输入再怎么刁钻都不会把查询搞崩。我接手过的系统里,有好几处线上问题就是关键词里的%号直接拼进LIKE导致的——有人查“进度100%完成”这种含百分号的关键词,因为没转义,直接把全表都搜了出来,返回几万条数据,应用层直接超时。这些坑说大不大,但每次都能让人加班到深夜。
LIKE和查询函数这块儿,其实就是熟练度的问题。先记住四种通配符的区别,再搞明白%放哪里影响索引,最后把字符串函数、聚合函数、日期函数串联起来用,日常开发里的数据查询需求基本能全覆盖。如果你在实际操作中遇到什么灵异现象,多半都逃不出NULL、排序规则、隐式转换这三大元凶,照着上面的排查路径走一遍,大概率能定位到问题。