news 2026/9/13 7:23:56

SQL Server模糊查询LIKE用法详解:通配符、转义与索引优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL Server模糊查询LIKE用法详解:通配符、转义与索引优化

做SQL Server开发的人,十有八九都会遇到这种场景:运营那边甩来一句“帮我查一下姓名里带‘伟’的人”,或者“日志表里关键字包含‘timeout’的记录有多少”。第一反应基本都是where name like '%伟%',一条语句甩过去,结果出来了,大家也就散了。但like真正的用法,远不止一个“两头带百分号”这么简单。我在一个运行了十来年的业务系统上做过大量的数据维护和查询优化,吃过不注重排序规则的亏,也踩过通配符转义没处理导致误查的坑。这篇就围绕SQL Server里的模糊查询(like用法)和方向各异的查询函数,把每个细节掰开揉碎讲清楚,包括为什么这样写、什么时候那样写、写错了会发生什么。

这篇文章适合刚接触SQL Server的初学者,也适合写了不少SQL但一直没系统整理过`

like `和函数用法的开发或运维朋友。读的时候建议打开SSMS顺手执行一遍,光看是记不住的。

1. LIKE 的三种匹配模式:% _ 和字符集各自解决什么问题

1.1 LIKE 的基本结构

LIKE在SQL Server里的定位是一个谓词(Predicate),负责判断某个字段的值是否符合一个模式字符串。基本写法固定在以下形态:

SELECT 列1, 列2 FROM 表名 WHERE 列名 LIKE '模式串';

模式串里允许放两类东西:一类是普通字符,要求字段值原样包含这些字符;另一类是通配符,表示“任意内容”。SQL Server支持的通配符只有三个半,分别是%_[]以及[^],很多人只用了%,剩下几个基本没碰过,但这几个恰恰是提高查询精度、减少多余结果的关键。

1.2 %:匹配任意长度字符,含零个字符

%是使用频率最高的通配符,含义是“匹配任意数量(包括0个)的任意字符”。三个最常见的写法:

-- 以“张”开头的姓名 SELECT * FROM student WHERE name LIKE '张%'; -- 以“伟”结尾的姓名 SELECT * FROM student WHERE name LIKE '%伟'; -- 姓名中任意位置含“小” SELECT * FROM student WHERE name LIKE '%小%';

很多人觉得%小%%伟张%三个写法差不多,但实际在索引使用上差别很大,这个我在第2部分详细讲。这里先记住一个原则:只要%出现在模式串的最前面(前缀位置),数据库就没法用常规索引做范围扫描。像LIKE '%小%'这种写法,SQL Server等于要把整列数据全部拉出来逐个比对。

%还能匹配零个字符,所以LIKE '张%'能查出姓名为“张”的人,因为“张”后面跟零个字符也满足模式。

1.3 _:匹配单个字符,做定长筛选

_匹配且仅匹配一个字符,多一个少一个都不行。它在处理“XX届XX班”这类定长编码时特别有用。举几个实际例子:

-- 学号总共6位,查倒数第3位是5的 SELECT * FROM student WHERE student_id LIKE '__5___'; -- 查姓氏为张,且名字恰好只有一个字(如“张伟”) SELECT * FROM student WHERE name LIKE '张_';

这里有个坑特别值得提一下:对中文来说,一个_只能匹配一个汉字吗?是的。在SQL Server里,_匹配一个字符,而中文字符也是单字符。但是如果你用的是nvarchar且启用了某些补充字符集(比如代理对形式的生僻字),一个_可能只匹配半个补充字符,这个概率低,遇到别慌,知道有这回事就行。

1.4 字符集匹配 [ ] 与排除 [^]

[]允许在方括号内指定一组字符,匹配其中任意一个;[^]则反过来,匹配“不在这个集合里的任意一个字符”。示例:

-- 姓氏是“张、李、王”其中之一的 SELECT * FROM student WHERE name LIKE '[张李王]%'; -- 查第一位数字是1、2、3的电话 SELECT * FROM contact WHERE phone LIKE '[1-3]%'; -- 查姓名不以“张”或“李”开头的人 SELECT * FROM student WHERE name LIKE '[^张李]%';

[1-3]这类写法属于范围表示,等价于[123]。字母范围如[a-z]也比较常用,不过要注意大小写是否敏感受排序规则影响,这个下一节专门展开。[]这个符号在正则表达式里也出现,但SQL Server的[]并不是完整正则,只是简化版字符集匹配,别把正则那套量词* + ?拿进来用,通配符只有%_两个。

2. 模糊查询最容易翻车的地方:排序规则、转义与索引失效

2.1 排序规则(COLLATE)决定大小写是否敏感

先看一个让很多人抓头的场景:

SELECT * FROM users WHERE username LIKE '%admin%';

表里明明有一条Admin,结果却没查出来?不对,多数情况下恰好相反——你只想查小写admin,结果AdminADMIN全冒出来了。SQL Server默认装的排序规则通常长这样:Chinese_PRC_CI_AS,中间那个CICase Insensitive的缩写,也就是“大小写不敏感”。在这种规则下,LIKE '%admin%'等价于LIKE '%ADMIN%',也等价于LIKE '%Admin%'

如果业务上必须区分大小写,可以在查询级别临时指定排序规则:

SELECT * FROM users WHERE username LIKE '%admin%' COLLATE Chinese_PRC_CS_AS;

注意COLLATE要写在模式串所在的那一侧,即username LIKE '%admin%' COLLATE Chinese_PRC_CS_AS或者username COLLATE Chinese_PRC_CS_AS LIKE '%admin%',效果一样。CS表示Case Sensitive

这个方法在排查“为什么我 where 条件写对了还是查多数据”时非常有用。除了大小写,还要注意全角半角问题,SQL Server的默认规则通常也不区分全角半角,若必须区分,可以使用Chinese_PRC_CS_AS_KS_WS,其中KS区分假名类型,WS区分全半角。

2.2 通配符转义:字段里真的存在 % 或 _ 怎么办

这是业务系统里特别容易被忽视的场景。假设有个商品编码规则,允许编码本身包含_字符,比如ABC_123。你想查所有以ABC开头、后跟任意内容的编码:

SELECT * FROM product WHERE code LIKE 'ABC%';

这条会把ABC_123ABCX999ABC全部查出来。但如果你要查“编码里确实包含下划线_的记录”,情况就不一样了:

-- 错误示范:这里面 _ 会被当成“匹配单个字符”的通配符 SELECT * FROM product WHERE code LIKE '%_%';

这条会返回全部记录,因为所有编码都至少有一个字符可以被_匹配。解决办法用ESCAPE关键字指定一个转义符:

-- 转义符设为感叹号,表示紧跟其后的 _ 是普通字符 SELECT * FROM product WHERE code LIKE '%!_%' ESCAPE '!';

同理,要查包含百分号%的记录:

SELECT * FROM product WHERE code LIKE '%!%%' ESCAPE '!';

这里转义符是个人为约定的字符,选哪个都行,只要不在模式串中产生歧义。字符集[]内如果要匹配]本身,写法稍微绕一点,可以借道ESCAPE或者把]放第一位,但实际业务中遇到不多,了解即可。

2.3 索引失效:前缀通配符为什么让查询变慢

直接给结论:LIKE 'abc%'在sql server里有可能用上索引,LIKE '%abc%'LIKE '%abc'基本走不了索引。原因在于B树索引是按值排序存储的,前缀确定时,SQL Server可以把“abc之后的所有值”视为一个有序区间去扫描;一旦开头就是%,就失去了区间定位的依据,只能全表扫描或者全索引扫描。

但这里有个常见的误区:模糊查询变慢就怪%。实际生产环境里,数据量只有几万行的话,全表扫描也就几十毫秒,根本不用紧张。真正需要担心的是几百万行、上千万行的大表。我遇到过一个日志表,超过一亿行,开发在界面上做关键字搜索,每个用户点一次查询,数据库CPU立刻飙到100%,后来把这类搜索改造成全文索引(FULLTEXT),情况才缓解。

如果不想上全文索引,还能用的替代思路有两个:

  1. %关键字%拆成两个条件,配合计算列和索引,比如在写入时把文本反转存储,然后用LIKE '关键字%'查反转列,实现后缀匹配加速。
  2. CHARINDEX函数代替LIKE做包含匹配,不过要注意,CHARINDEX同样无法使用索引,只适合小表。
-- 等价于 LIKE '%timeout%' SELECT * FROM log_table WHERE CHARINDEX('timeout', log_message) > 0;

还有一点容易被忽略:LIKE不会匹配NULL。如果字段值是NULL,不管模式串写什么,结果都是不匹配。所以模糊查询前要先想好,是否需要把NULL数据也纳入统计。

3. 查询函数里的字符串派:SUBSTRING、CHARINDEX 这些比 LIKE 更精准

3.1 截取函数:LEFT、RIGHT、SUBSTRING

LIKE 负责筛选,但“把字段里的某一段取出来”得靠截取函数。三个核心函数的参数如下:

  • LEFT(字符串, 长度):从左侧截取指定长度。
  • RIGHT(字符串, 长度):从右侧截取。
  • SUBSTRING(字符串, 起始位置, 长度):从任意位置截取,起始位置从1开始。
SELECT name, LEFT(name, 1) AS surname, -- 姓 RIGHT(name, LEN(name) - 1) AS given_name, -- 名字(去掉姓) SUBSTRING(name, 2, 2) AS mid_two -- 第2个字符起的两个字符 FROM student;

这里有人会踩一个坑:中文一个字符占一个位置,LEN('张伟')返回2,没问题。但如果用的是varchar且存了生僻字或表情符号,长度计算会出现偏差。LEN返回字符数,而DATALENGTH返回字节数。对于一个汉字,DATALENGTHvarchar下通常是2,在nvarchar下是4。需要明确到底按字符取还是按字节取,别混用。

3.2 定位函数:CHARINDEX 和 PATINDEX

CHARINDEX(要查找的子串, 原字符串)返回子串在原字符串中的起始位置,没找到返回0。它和LIKE的定位不一样:LIKE告诉你“匹不匹配”,CHARINDEX还告诉你“匹配在哪个位置”。典型应用是拆分字符串:

-- 取邮箱 @ 前面的用户名部分 SELECT email, LEFT(email, CHARINDEX('@', email) - 1) AS username FROM users WHERE CHARINDEX('@', email) > 0;

PATINDEXCHARINDEX的加强版,允许在查找串中使用%_通配符,但注意PATINDEX返回的是“第一次匹配的位置”。当LIKE '%关键字%'只是做布尔判断时,PATINDEX可以进一步拿到位置,这在解析字符串场景里非常有用:

-- 找到第一个数字出现的位置 SELECT PATINDEX('%[0-9]%', '订单号A123B'); -- 返回4

3.3 替换与拼接:REPLACE、STUFF、CONCAT

处理脏数据时REPLACE是利器。比如手机号、身份证号脱敏:

-- 把手机号第4到第7位变成* SELECT phone, STUFF(phone, 4, 4, '****') AS masked_phone FROM users;

STUFF(原字符串, 开始位置, 删除长度, 插入字符串)的逻辑是:先删掉指定位置的若干个字符,再把新字符串插进去。这个函数在做脱敏、拼接时比SUBSTRINGCONCAT更简洁。

字符串拼接要注意NULL问题。直接使用+拼接时,只要有一方为NULL,结果整体就是NULLCONCAT函数会自动把NULL转成空字符串:

SELECT CONCAT(first_name, last_name) AS full_name FROM users; -- 等价但更安全:即使 last_name 为 NULL 也不会导致全名变 NULL

我刚工作那会儿经常因为没处理NULL,拼接出来的地址少了一段,排查了半天才发现在第5个字段上有个空值。自那之后,涉及拼接我一律优先CONCAT

3.4 用字符串函数做模糊查询的进阶组合

实际业务里,LIKE 和字符串函数经常配合使用。比如要查“身份证号倒数第二位是奇数”的人:

SELECT * FROM citizen WHERE RIGHT(id_card, 2) LIKE '[13579]_';

再比如,按名字长度过滤:查所有名字只有两个字的用户:

SELECT * FROM users WHERE LEN(real_name) = 2;

这种方式虽然能用,但要提醒:LEN(real_name) = 2写在 where 里会让该列的索引失效(因为对列做了函数运算),小表无所谓,大表要谨慎。

4. 聚合函数与 GROUP BY:从“查出来”到“算出来”

4.1 五大基础聚合函数与 NULL 的坑

COUNTSUMAVGMINMAX是查询函数里数据统计的中坚力量。它们的作用范围是一组行,返回一个汇总值。

SELECT COUNT(*) AS total_users, COUNT(phone) AS users_with_phone, -- 自动忽略 NULL AVG(age) AS avg_age, MIN(create_time) AS earliest_time, MAX(amount) AS max_amount FROM users;

NULL 对聚合函数的影响常常让人意外。COUNT(列名)会跳过NULLCOUNT(*)则统计所有行;AVG(列名)同样忽略NULL。假设10行数据里有2行的amountNULLAVG(amount)计算的是剩下8行的平均值,而不是把2个NULL当0算。如果业务上需要把NULL当0处理,必须先ISNULL(amount, 0)COALESCE(amount, 0)再聚合。

还有一点:SUM作用于空结果集会返回NULL而不是0。很多报表程序因为这个出现显示空白。稳妥做法是:

SELECT ISNULL(SUM(amount), 0) AS total_amount FROM orders WHERE order_date > '2099-01-01'; -- 查不到数据时也返回0,而不是NULL

4.2 GROUP BY 的执行逻辑

GROUP BY干了什么事?把同一个分组键的值合成一组,然后对每一组分别执行聚合函数。理解这个逻辑对写对SQL很重要。

SELECT category_id, COUNT(*) AS cnt, SUM(price * quantity) AS category_total FROM order_detail GROUP BY category_id ORDER BY category_total DESC;

这里有个常被忽略的约束:SELECT子句中出现的列,要么是分组键(GROUP BY后面的列),要么必须包在聚合函数里。比如上面查了category_id,它是分组键,没毛病;如果还想查product_name,除非它也在GROUP BY里,或者用MAX(product_name)包起来,否则SQL Server会直接报错。这个报错不是SQL Server矫情,是为了防止“每组里有多行,到底取哪一行的 product_name”这种语义不明确的问题。

4.3 HAVING 与 WHERE:过滤时机的差异

WHERE在分组前过滤原始行,HAVING在分组后过滤聚合结果。这个顺序差异决定了写法。

-- 查订单数量大于等于100的客户 SELECT customer_id, COUNT(*) AS order_cnt FROM orders WHERE order_status = 'completed' -- 先剔除无效订单 GROUP BY customer_id HAVING COUNT(*) >= 100; -- 再筛高频客户

如果把HAVING COUNT(*) >= 100换成WHERE COUNT(*) >= 100会直接报错,因为COUNT(*)WHERE阶段还没计算出来。反过来,如果条件能下推到WHERE就尽量下推,比如上面的order_status = 'completed'写在WHERE里比写在HAVING里效率高,因为可以在分组前就缩小数据量。

4.4 聚合函数与 LIKE 组合的典型场景

汇总统计经常需要先模糊筛选再聚合。比如统计“姓名包含‘张’”的客户的下单总金额:

SELECT COUNT(DISTINCT customer_id) AS customer_cnt, SUM(order_amount) AS total_amount FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE c.name LIKE '%张%';

COUNT(DISTINCT 列)是容易忽略的变体,用于计算去重后的数量。模糊查询筛选出的人有重复订单时,用它可以准确统计人数。这个写法在大数据量下性能一般,但对中小系统完全够用。

5. 时间函数、连接查询与行转列:这几类函数和模糊查询配合得最多

5.1 时间函数:GETDATE、DATEADD、DATEDIFF 的正确用法

业务系统里时间字段是查询条件中出现频率最高的字段类型之一。SQL Server 的时间函数很多,但实际工作中常用的就那么几个。

GETDATE()返回当前系统时间。DATEADD(日期部分, 增量, 日期)用于日期加减。DATEDIFF(日期部分, 开始日期, 结束日期)用于计算两个日期的差值。

-- 近7天的订单 SELECT * FROM orders WHERE order_date >= DATEADD(DAY, -7, GETDATE()); -- 按月统计近12个月的订单数 SELECT YEAR(order_date) AS order_year, MONTH(order_date) AS order_month, COUNT(*) AS order_cnt FROM orders WHERE order_date >= DATEADD(MONTH, -12, GETDATE()) GROUP BY YEAR(order_date), MONTH(order_date) ORDER BY order_year, order_month;

5.2 时间字段与 LIKE 组合的坑

有些初学者会把日期字段直接跟LIKE配合:

-- 错误示范 SELECT * FROM orders WHERE order_date LIKE '%2024-06%';

这通常会报错,因为order_datedatetime类型,不能直接跟字符串模式比。正确做法有两种:

-- 方式一:转成字符串再匹配(不推荐,慢) SELECT * FROM orders WHERE CONVERT(VARCHAR(7), order_date, 120) = '2024-06'; -- 方式二:用日期范围(推荐,可用索引) SELECT * FROM orders WHERE order_date >= '2024-06-01' AND order_date < '2024-07-01';

方式二是最稳的写法。>=<把6月整月框进区间,既能走到索引,又避免了BETWEEN在 datetime 精度下可能漏掉23:59:59.997之后数据的问题。

5.3 连接查询:LEFT JOIN 和 CROSS JOIN 别搞混

虽然标题核心是模糊查询和查询函数,但查询函数最终要服务于实际的取数逻辑,连接是绕不开的。LEFT JOIN返回左表全部记录,右表没有匹配时用NULL补齐;INNER JOIN只返回两边能匹配上的行。

实际业务中一定要想清楚“以哪边为主表”。比如统计每个客户的订单数,即便某客户没有订单,也希望显示数量0,这时必须LEFT JOIN

SELECT c.customer_name, COUNT(o.order_id) AS order_cnt FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id GROUP BY c.customer_name;

很多人在这里犯的错是用了LEFT JOIN却把o.order_id的条件写在WHERE里,结果左连接被“抹平”成了内连接。原因很简单:一旦在WHERE中过滤右表字段为某个非NULL值,左表中无匹配的行就全被剔除了。要加过滤条件就放在ON子句中,或者把过滤条件一起放进AND

SELECT c.customer_name, COUNT(o.order_id) AS order_cnt FROM customers c LEFT JOIN orders o ON c.customer_id = o.customer_id AND o.order_status = 'completed' GROUP BY c.customer_name;

这样能统计出“每个客户的已完成订单数”,同时保留没有已完成订单的客户。

CROSS JOIN则是笛卡尔积,左表每行都乘上右表每行。在没有明确需求时少用,行数爆炸是分分钟的事。

5.4 行转列:PIVOT 和条件聚合

很多人一听到行转列就觉得难,其实本质是“把某个列里的不同值变成多个列”,然后对每个值做聚合。SQL Server 提供了PIVOT操作符,但语法有点反直觉,我平时更推荐用条件聚合实现,逻辑更清晰:

-- 按月份转列统计订单金额 SELECT product_name, SUM(CASE WHEN MONTH(order_date) = 1 THEN amount END) AS Jan_amount, SUM(CASE WHEN MONTH(order_date) = 2 THEN amount END) AS Feb_amount, SUM(CASE WHEN MONTH(order_date) = 3 THEN amount END) AS Mar_amount FROM orders GROUP BY product_name;

CASE WHEN配合聚合函数的写法可读性强,也方便扩展,推荐新手优先掌握。

5.5 查询函数使用频率排序与个人建议

按我在实际项目里的使用频率,给这些查询函数排个序,方便你判断优先级:GETDATEDATEADDDATEDIFF这类时间函数用得最多,因为报表查询永远带着时间范围;其次是COUNTSUMISNULL这类聚合与空值处理;再是LEFTSUBSTRINGCHARINDEX这类字符串函数;最后才是PIVOT这类进阶操作。

写SQL查询函数时,我自己的习惯是先在草稿纸上写出“我想从哪些原始列,经过什么变换,汇出哪些结果列”,再落成SQL。这样写出来的查询通常结构清晰,不容易在GROUP BYSELECT的对应关系上翻车。

另外提醒一句,函数虽好用,但查询中尽量别对索引列套函数。像WHERE DATEADD(DAY, 1, order_date) > GETDATE()这种写法,会让order_date上的索引失效。遇到这种情况,调整为WHERE order_date > DATEADD(DAY, -1, GETDATE()),效果相同,但引擎能正常用索引。

我最初调优一个报表接口的时候,就是把三个这种“列上套函数”的条件全部改写成“在常量侧运算”,整个查询从 8 秒降到了 0.3 秒。这段经验比背一百个函数签名都管用——函数本身不难,难的是知道在哪些位置用、哪些位置不用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 7:23:13

Rust迭代器:原理、应用与性能优化

1. Rust迭代器基础概念Rust中的迭代器是一种设计模式&#xff0c;它提供了一种顺序访问集合元素的方法&#xff0c;而不需要暴露集合的内部表示。迭代器模式将遍历元素的责任从集合对象转移到迭代器对象上&#xff0c;这使得我们可以用统一的方式处理不同的集合类型。1.1 迭代器…

作者头像 李华
网站建设 2026/9/13 7:22:28

AC !DC:一款拒绝联网的离线空调控制器设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:20:33

因果学习入门:从相关到因果,揭开因果推断的核心方法与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:18:01

点云采集原理与PCD格式避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:16:45

Qt C++网盘开发实战:从TCP协议帧到断点续传

简介&#xff1a;这份基于Qt框架开发的C网盘项目源码&#xff0c;面向毕业设计、课程设计及需要快速搭建带通信与文件管理功能系统的开发者。项目实现了网盘基础功能&#xff0c;包括用户注册登录、好友系统、私聊与群聊、文件上传下载、分享管理&#xff0c;并配有数据库脚本及…

作者头像 李华
网站建设 2026/9/13 7:14:53

ESP32-S3 N16R8开发实战:PSRAM+USB Device+PlatformIO一体化配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华