ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SQL Server模糊查询LIKE通配符与常用函数详解

SQL Server模糊查询LIKE通配符与常用函数详解 我做了几年SQL Server相关的开发和运维发现一个很有意思的现象很多刚接触数据库的同学能把增删改查写得飞起但一遇到“模糊查询”就开始懵要么只会用死磕精确匹配要么LIKE写出来却查不到数据更别提那些藏在系统里的字符串函数、日期函数、聚合函数了。这篇就专门聊聊LIKE的完整用法再顺手把日常最常用的查询函数梳理一遍。不管你是刚入门的新手还是写了好几年SQL想查漏补缺的老手这篇的内容都值得收藏。1. LIKE模糊查询的核心套路——通配符是灵魂1.1 四个通配符逐个拆解LIKE在SQL Server里是专门做模糊匹配的关键字它配合通配符使用。很多人在这一步就栽了因为通配符看着简单用起来却有不少细节。第一个是百分号%它代表任意长度的字符包括零个字符。比如我想查所有姓“张”的客户SELECT * FROM Customers WHERE CustomerName LIKE 张%;这段SQL会把“张三”、“张伟”、“张晓明”全部捞出来。这里要注意LIKE 张%和LIKE %张%是两回事。前者要求字符串必须以“张”开头后者只要字符串里包含“张”就行位置不限。这个区别在性能上和业务含义上都有讲究后面我单独展开。第二个是下划线_它代表任意单个字符。比如你要查名字是三个字、且中间那个字是“小”的人SELECT * FROM Users WHERE UserName LIKE _小_;这条会把“王小二”、“李小三”查出来但“张小”或者“张小三四”这种长度不对的就不会出现。_严格占一位不能多也不能少。第三个是方括号[]它用来匹配指定范围内的任意单个字符。这个功能在筛选数据时特别实用。比如我要查所有姓“赵”、“钱”、“孙”的员工SELECT * FROM Employees WHERE EmpName LIKE [赵钱孙]%;也可以写成范围形式比如查姓氏拼音首字母在A到M之间的记录SELECT * FROM Employees WHERE EmpName LIKE [A-M]%;注意一点[]在英文状态下也能匹配数字比如LIKE [0-9]%查以数字开头的数据这在处理一些脏数据的时候非常好用。第四个是方括号加尖号[^]它代表不在指定范围内的任意单个字符。还是拿姓氏举例我想排除姓“赵”、“钱”、“孙”的员工SELECT * FROM Employees WHERE EmpName LIKE [^赵钱孙]%;这四个通配符配合起来几乎能覆盖日常所有模糊匹配的场景。我自己实际用下来发现最容易被忽略的是_和[]的组合用法比如查手机号倒数第二位不是4的记录就可以写成LIKE %[^4]配合RIGHT函数之类的手法。这属于进阶玩法了基础先打牢。1.2 ESCAPE转义与特殊字符处理——最容易被忽略的坑通配符虽好但有个问题如果数据里本身就包含%或者_这些字符呢比如你的产品编码是“A%B_C”你想查包含这个字符串的所有记录直接写LIKE %A%B_C%就出事了因为%和_都会被当成通配符查出来的结果一片混乱。这时就需要ESCAPE关键字来指定转义符。比如我用!作为转义符SELECT * FROM Products WHERE ProductCode LIKE %A!%B!_C% ESCAPE !;这段的意思是!%代表字面量的百分号!_代表字面量的下划线其余位置的%仍然是通配符。这样就能精准匹配包含“A%B_C”这个字符串的产品编码了。这个知识点在面试里经常出现实际工作中处理路径、编码、日志之类的数据时也经常碰到。比如Windows文件路径里的\在LIKE语句里也可能引发奇怪的行为如果存储的路径中包含反斜杠建议直接用ESCAPE指定一个转义符来包住它。1.3 大小写、中文排序与排序规则Collation的微妙影响再说一个很多人踩过的坑SQL Server中LIKE的大小写敏感问题完全取决于数据库的排序规则Collation。默认情况下SQL Server安装时选的排序规则一般是Chinese_PRC_CI_AS这个CI就是Case Insensitive的意思即大小写不敏感。所以在默认设置下LIKE abc%和LIKE ABC%查出来的结果是一样的。但如果你用的数据库排序规则是Chinese_PRC_CS_ASCS即Case Sensitive大小写敏感那LIKE abc%就查不到“ABC”开头的记录。很多老项目突然出现“查不到数据”的诡异问题最后定位到是某个环境重新安装时改了排序规则这种案例我见过不止一次。处理方式有两种一是在查询时用COLLATE关键字临时指定排序规则SELECT * FROM Users WHERE UserName LIKE abc% COLLATE Chinese_PRC_CS_AS;二是从源头统一排序规则保证所有环境的Collation一致。我个人建议在项目初期就把这个定下来否则后期数据迁移、同步时会痛不欲生。中文排序这块也提一下SQL Server对中文默认是按拼音排序的所以ORDER BY中文列时“张三”会排在“李四”前面因为Z在L后面不对这里我想说的是拼音张(Zhang)的首字母是Z李(Li)的首字母是L按字母表排序Z在L之后所以“张”应该排在“李”后面。这其实是个常见的认知混乱点但和LIKE无关顺带提一句提醒大家别搞混了。2. LIKE实用场景与查询函数配合实战2.1 动态拼接LIKE条件——参数化是底线在实际业务系统里LIKE条件很少是写死的大多数情况是用户在搜索框里输入关键字后端拼SQL去查。比如一个简单的用户搜索string keyword txtKeyword.Text.Trim(); string sql SELECT * FROM Users WHERE UserName LIKE % keyword %;这段代码在演示和教学里到处都是但在真实项目里绝对不能这么写。这不仅是SQL注入的问题还有性能隐患。正确的做法是使用参数化查询string sql SELECT * FROM Users WHERE UserName LIKE kw; command.Parameters.AddWithValue(kw, % keyword %);这里有个细节通配符%是拼在参数值里的而不是拼在SQL语句里。这样既安全又清晰。如果你用的是存储过程道理一样CREATE PROCEDURE sp_SearchUsers Keyword NVARCHAR(50) AS BEGIN SELECT * FROM Users WHERE UserName LIKE % Keyword %; END很多人会问为什么不能写成LIKE %Keyword%因为SQL Server会把%Keyword%当成一个字符串字面量而不是把Keyword变量的值拼接进去。这个坑我见过新人踩过特意说一下。2.2 结合日期函数实现“模糊时间范围”查询在业务系统中最常见的组合拳之一就是“时间段关键字”的复合查询。比如查2023年5月所有备注里包含“退款”的订单SELECT * FROM Orders WHERE Note LIKE %退款% AND OrderDate 2023-05-01 AND OrderDate 2023-06-01;这里用和而不是BETWEEN AND是因为OrderDate如果是datetime类型本身就带时分秒。BETWEEN 2023-05-01 AND 2023-05-31会漏掉5月31日当天23点之后的订单因为2023-05-31 23:59:59大于2023-05-31。这是日期查询里非常经典的边界问题切记。如果想按“年份月份”直接过滤可以用YEAR和MONTH函数SELECT * FROM Orders WHERE Note LIKE %退款% AND YEAR(OrderDate) 2023 AND MONTH(OrderDate) 5;不过这种写法有个代价就是对OrderDate列用了函数导致索引失效。如果订单表数据量大建议还是用和的范围写法既准确又能走索引。2.3 数据清洗场景中的LIKE判断LIKE不光能做正向匹配还能做反向判断。比如清理脏数据查找所有电话号码列里包含非数字字符的记录SELECT * FROM Customers WHERE Phone LIKE %[^0-9]%;这个思路很巧妙利用[^0-9]匹配任意非数字字符只要电话号码里有任何非数字就会被查出来。同理查包含字母的记录就是LIKE %[A-Za-z]%。我去年做一个数据迁移项目时就靠这几条LIKE语句把几万条带格式符、带空格、带字母的手机号全捞了出来效率非常高。这种“反向清洗”的思路比一个个写正则去匹配要简单直观得多。3. 常用查询函数实例手册——拿来即用3.1 聚合函数GROUP BY与HAVING的搭档关系聚合函数Aggregate Functions是统计查询的核心。最基础的五个是COUNT、SUM、AVG、MIN、MAX。先看COUNT的两个写法区别SELECT COUNT(*) FROM Orders; SELECT COUNT(OrderID) FROM Orders; SELECT COUNT(DISTINCT CustomerID) FROM Orders;COUNT(*)统计所有行数包括NULLCOUNT(OrderID)只统计OrderID不为NULL的行数COUNT(DISTINCT CustomerID)统计去重后的客户数。三者含义不同用的时候一定搞清楚业务要的是什么。聚合函数通常配合GROUP BY使用。比如统计每个客户的订单数和总金额SELECT CustomerID, COUNT(OrderID) AS OrderCount, SUM(OrderAmount) AS TotalAmount FROM Orders GROUP BY CustomerID;这里有个很多新手会犯的错用了GROUP BY之后SELECT列表里只能出现分组列和聚合函数列。比如上面这段SQL里不能再出现OrderDate因为OrderDate没有参与分组SQL Server会直接报错。如果需要过滤分组后的结果不能使用WHERE而是要用HAVING。WHERE是对明细行过滤HAVING是对分组后的结果过滤。这个执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。理解这个顺序很多SQL逻辑问题都会迎刃而解。举个例子查订单数超过5个且总金额超过10000的客户SELECT CustomerID, COUNT(OrderID) AS Cnt, SUM(OrderAmount) AS Amt FROM Orders GROUP BY CustomerID HAVING COUNT(OrderID) 5 AND SUM(OrderAmount) 10000;3.2 字符串函数从左、右、中截取与替换字符串函数是日常处理数据时的高频工具。先列几个最常用的LEN()返回字符串长度LEFT(字符串, 位数)从左边截取RIGHT(字符串, 位数)从右边截取SUBSTRING(字符串, 开始位置, 长度)从中间截取CHARINDEX(子串, 字符串)返回子串在字符串中的起始位置REPLACE(字符串, 旧子串, 新子串)替换LTRIM()/RTRIM()去掉左右空格UPPER()/LOWER()大小写转换这些函数里最常用也最灵活的其实是CHARINDEX配合SUBSTRING的组合。比如手机号脱敏把中间四位换成星号SELECT Phone, LEFT(Phone, 3) **** RIGHT(Phone, 4) AS MaskedPhone FROM Customers;再比如从“张三-北京-销售部”这种复合字段中拆出城市SELECT EmpInfo, SUBSTRING(EmpInfo, CHARINDEX(-, EmpInfo) 1, CHARINDEX(-, EmpInfo, CHARINDEX(-, EmpInfo) 1) - CHARINDEX(-, EmpInfo) - 1) AS City FROM Employees;这段SQL的逻辑是先找到第一个-的位置再加1作为起始位置然后从起始位置往后找第二个-的位置两者相减得到城市字符串的长度。写法稍微绕但在没有拆分函数的老版本SQL Server中这是标准写法。REPLACE也有一个很实用的场景就是批量清洗换行符。文本数据经常会带\r\n回车换行在SQL Server里表现为CHAR(13)CHAR(10)清洗时可以这样UPDATE Articles SET Content REPLACE(Content, CHAR(13) CHAR(10), );3.3 日期函数GETDATE、DATEADD、DATEDIFF与时间段计算日期函数用得最多的场景是统计报表和定时任务。核心函数有这么几个GETDATE()获取当前系统时间DATEADD(间隔单位, 数量, 日期)给日期加上指定间隔DATEDIFF(间隔单位, 开始日期, 结束日期)计算两个日期之间的间隔数YEAR(日期)/MONTH(日期)/DAY(日期)提取年月日CONVERT(数据类型, 表达式, 格式码)类型转换常用于日期格式化比如查最近7天内注册的用户SELECT * FROM Users WHERE RegisterDate DATEADD(DAY, -7, GETDATE());再比如计算每个用户从注册到现在的天数SELECT UserName, RegisterDate, DATEDIFF(DAY, RegisterDate, GETDATE()) AS DaysSinceRegister FROM Users;日期格式化在报表里很常用。SQL Server的CONVERT可以用格式码控制日期输出样式比如CONVERT(VARCHAR(10), GETDATE(), 120)会输出2025-01-01这种格式CONVERT(VARCHAR(10), GETDATE(), 23)同理但略有差异。实际项目中用120ODBC标准格式较多因为它是YYYY-MM-DD HH:MI:SS这种最直观的格式。有一个日期分组统计的经典写法按天统计每天的订单量SELECT CONVERT(VARCHAR(10), OrderDate, 120) AS OrderDay, COUNT(OrderID) AS OrderCount FROM Orders GROUP BY CONVERT(VARCHAR(10), OrderDate, 120);这里把datetime截断到天再按天分组。很多人会纠结要不要用CAST(OrderDate AS DATE)在SQL Server 2008以上版本确实更简洁但如果兼容老版本CONVERT到字符串的写法更稳妥。3.4 CASE WHEN逻辑判断与行列转换基础CASE WHEN是SQL里最接近“写逻辑”的功能它让查询具备了条件分支能力。最常见的场景是把离散值翻译成业务含义比如订单状态SELECT OrderID, OrderStatus, CASE OrderStatus WHEN 0 THEN 待付款 WHEN 1 THEN 已付款 WHEN 2 THEN 已发货 WHEN 3 THEN 已完成 ELSE 未知状态 END AS StatusDesc FROM Orders;这是简单CASE表达式的写法还有一个搜索CASE表达式支持范围判断SELECT UserName, TotalAmount, CASE WHEN TotalAmount 10000 THEN VIP客户 WHEN TotalAmount 5000 THEN 优质客户 WHEN TotalAmount 1000 THEN 普通客户 ELSE 新客户 END AS CustomerLevel FROM Users;CASE WHEN配合聚合函数还可以实现“条件计数”。比如统计每个省份下订单金额超过1000的客户数和总客户数SELECT Province, COUNT(CASE WHEN TotalAmount 1000 THEN 1 END) AS HighValueCount, COUNT(*) AS TotalCount FROM Customers GROUP BY Province;这里的关键写法是COUNT(CASE WHEN ... THEN 1 END)——不满足条件时返回NULL而COUNT会自动忽略NULL所以这个写法等价于“只统计满足条件的行数”。这个技巧在报表开发中非常实用。4. 易踩的坑SQL Server查询常见问题与排查技巧实录4.1 查不到数据排查思路要清晰LIKE查询查不到数据90%的情况出在通配符使用错误或数据本身包含不可见字符。我排查这类问题的方法是先去掉LIKE条件用精确条件查出所有可能相关的数据再用LEN()和LTRIM/RTRIM确认有没有空格必要时用CAST(字段 AS VARBINARY(MAX))来看字段里到底存的什么字符。比如查用户名包含“admin”的记录直接写LIKE %admin%查不到但数据里可能是admin后面跟了个换行符或者admin和后面的字之间有个不常见的空格字符。这种情况下把WHERE条件改成SELECT * FROM Users WHERE UserName LIKE %admin% OR UserName LIKE %admin CHAR(10) % OR UserName LIKE %admin CHAR(13) %先确认问题在哪再做数据清洗不要一开始就怀疑数据出错了。4.2 通配符转义不当导致的误匹配前面说了转义的问题这里再补充一个具体场景。电商平台上商品名称经常包含%、_和[如果用户在搜索框里输入50%off后端直接拼LIKE %50%off%那会把所有包含50和off的商品全查出来结果就没法看了。规避方案是搜索前把用户输入里的通配符全部替换成带转义符的形式keyword keyword.Replace(!, !!) .Replace(%, !%) .Replace(_, !_) .Replace([, ![);然后SQL写成LIKE % kw % ESCAPE !。这样用户输入什么就查什么通配符全部变成字面量不会误匹配。这个处理在对外开放的搜索接口里一定要做否则不仅结果不准还有可能被恶意用户用通配符构造特殊条件试探数据。4.3 性能隐患LIKE前缀匹配走索引其他情况全表扫描这是模糊查询最核心的性能问题。SQL Server的普通非聚集索引是有序排列的所以LIKE abc%这种前缀匹配可以走索引查找Index Seek效率很高。但LIKE %abc%这种包含匹配因为不知道匹配的字符从哪个位置开始SQL Server只能做索引扫描Index Scan或全表扫描Table Scan性能随数据量急剧下降。看执行计划时Index Seek和Index Scan是两种完全不同的成本级别。LIKE abc%通常能看到Index Seek而LIKE %abc%大概率是Index Scan。如果你的业务必须要做包含匹配且数据量大有几个优化方向一是用全文索引Full-Text Index它专门针对包含匹配做了优化配合CONTAINS关键字查询效率远高于LIKE %xxx%。二是考虑引入搜索引擎或专门的模糊匹配方案这属于架构层面的改动适合数据量真的很大的场景。三是在写SQL时避免对索引列用函数包裹。前面提到的YEAR(OrderDate) 2023这类写法即便列上有索引也走不了因为索引是基于原始值构建的不是基于函数结果。4.4 参数嗅探与查询计划缓存这是一个比较隐蔽的性能问题。存储过程首次执行时SQL Server会根据传入的参数值生成执行计划并缓存在内存中。如果第一次执行时参数的值选择性很差比如查出一个超大数据集生成的就是全表扫描计划之后即使传入选择性很好的参数SQL Server也倾向于复用旧计划导致性能问题。遇到这种情况常规手段是给存储过程参数加上OPTION (RECOMPILE)强制每次重新编译或者用OPTION (OPTIMIZE FOR UNKNOWN)让优化器不依赖具体参数值。但这两招都有副作用前者增加编译开销后者可能不是最优计划。实际项目中我更倾向于在开发阶段就把LIKE查询写成不会失控的形式比如限制必须加前缀、增加其他过滤条件来缩小数据集。4.5 常见报错速查小结在实际运行中有些报错反复出现这里整理一个小表格方便对照报错信息常见原因处理思路数据类型转换失败字符串拼接时隐式转换出问题显式用CAST或CONVERT指定类型列名不明确多表JOIN时两个表都有同名字段给字段加表别名前缀无效的列名拼SQL时字段写错或表别弄混检查表结构和字段拼写LIKE查询结果异常通配符未转义或排序规则不同ESCAPE转义检查Collation字符串或二进制数据将被截断插入或更新的值超过字段长度检查字段定义与传入数据长度5. 几个拿来就用的综合查询模板5.1 客户信息订单汇总统计模板这个模板适合做客户分析报表把客户基本信息和订单聚合数据联合起来同时还筛选了客户名称的关键字SELECT c.CustomerID, c.CustomerName, c.Phone, COUNT(o.OrderID) AS OrderCount, ISNULL(SUM(o.OrderAmount), 0) AS TotalAmount, MAX(o.OrderDate) AS LastOrderDate FROM Customers c LEFT JOIN Orders o ON c.CustomerID o.CustomerID WHERE c.CustomerName LIKE keyword % GROUP BY c.CustomerID, c.CustomerName, c.Phone HAVING ISNULL(SUM(o.OrderAmount), 0) 1000 ORDER BY LastOrderDate DESC;这里用了LEFT JOIN因为要保留没有下过单的客户用ISNULL处理NULL金额避免统计结果出现空白HAVING放在分组之后对聚合结果做过滤。如果要查“名字中包含某字”的客户就改成LIKE % keyword %但要注意索引使用问题。5.2 日志表的时间段关键字复合查询模板做系统维护时分析日志是家常便饭。一般我会写一个这样的查询来在日志表里搜索某个时间区域内包含特定关键词的记录DECLARE StartTime DATETIME 2025-01-01 00:00:00; DECLARE EndTime DATETIME 2025-01-02 00:00:00; DECLARE Keyword NVARCHAR(100) ERROR; SELECT LogTime, Module, Message FROM SystemLogs WHERE LogTime StartTime AND LogTime EndTime AND Message LIKE % Keyword % ORDER BY LogTime DESC;这里特别强调LogTime范围用了和而不是BETWEEN就是为了避免把2025-01-02 00:00:00当作新一天的起点而漏掉前一秒的日志。如果你确实想包含EndTime那一瞬间可以写成AND LogTime DATEADD(SECOND, 1, EndTime)这样对最终用户来说选到哪个时间点那条记录就会被包含进来。6. 关于LIKE与查询函数我的一点经验总结技术文章写到最后我习惯说点真正来自实操的心得而不是干巴巴复述文档。接触SQL Server这么多年我踩过最大的坑就是初期太依赖LIKE %xxx%觉得什么模糊查询都能一把梭。后来在几张几百万行的业务表上这种写法直接把查询拖到了十几秒才意识到模糊查询的性能问题不是等数据量大了再考虑的事而是在设计表结构和查询时就要提前规划的。另外查询函数看起来简单但真正熟练需要靠场景练习。我建议每个做数据相关工作的朋友都养成一个习惯遇到一个SQL问题先不要急着搜答案自己画一遍逻辑想想有没有其他函数能实现同样的需求。比如拆字符串有人只会用SUBSTRING硬切位置而熟悉CHARINDEX和REVERSE用法的人可以写出更通用的拆分方案。这种能力积累起来之后处理复杂报表和数据分析会顺手很多。还有一点小建议写SQL之前一定先确认数据库的排序规则、字段类型、索引情况这些环境信息决定了你写出来的SQL能不能快、能不能准。我见过太多“这段SQL在本地正常一上生产就慢”的案例最后定位出问题都在这些基础配置上。以上就是这段实际探索中的一点积累希望对读到这里的人能有帮助。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进