news 2026/9/17 12:15:49

SQLServer查询基石:SELECT语法、WHERE过滤与常用函数实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQLServer查询基石:SELECT语法、WHERE过滤与常用函数实战

1. SELECT基础:从“把表打开看一眼”到精准取数

很多人学SQLServer,前面建库建表都很顺利,一到查询就觉得脑子不够用。原因很简单:建表是有固定格式的,照着写就行;但查询是开放的,同样的需求能写出十种不同的语句,效率可能差几十倍。这一章我们系统地把查询讲透,第一篇先打地基——把SELECT这条语句的每一个零件都拆开看明白。

学习查询之前,你先建立一个认知:SELECT语句的本质是“按条件从表中取数据”,但很多人在这个“条件”上栽跟头——不是语法不会写,而是搞不清SQLServer到底按什么顺序执行你的语句。先记住一件事:你写的顺序是SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY,但SQLServer执行的顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。这个顺序差异直接决定了你哪些地方能用别名、哪些地方不能用。

比如你想在WHERE里写SELECT子句定义的别名,在很多人的认知里“我明明定义了”,但SQLServer就是报“列名无效”——因为执行WHERE的时候,SELECT还没轮到执行。这种问题在实际工作中特别容易遇到。

本文适合刚学完建库建表的入门者,也适合写了一段时间SQL但总感觉“不踏实”的开发同学。我会把查询拆成几个递进的层次:单表基础查询、条件过滤、函数处理、排序去重,每一层都配上实际场景和容易踩的坑。

再看一个基础中的基础——SELECT的语法结构:

SELECT [ALL | DISTINCT] [TOP (n)] 列名列表 FROM 表名 [WHERE 过滤条件] [GROUP BY 分组依据] [HAVING 分组后过滤] [ORDER BY 排序依据 [ASC | DESC]]

中括号里的内容是可选的,但每个可选部分背后都对应一类查询场景。这篇文章先解决FROM、WHERE、SELECT这三层的核心问题,GROUP BY和HAVING我建议你在完全掌握了WHERE之后再学——因为很多人的分组查不准,根本不是分组逻辑的问题,而是WHERE没写对,导致分组前的数据就已经是错的。

查询的“零件”逐个理解之后,你会发现SQLServer的查询并不难,难的是建立一个正确的思维方式:你要清楚每一部分在什么时候执行、对数据做了什么操作,而不是把整条SQL当黑盒。

1.1 逻辑执行顺序:为什么你的别名在WHERE里用不了

先把这个最重要的问题单独拿出来说。我们写一条完整查询:

SELECT 学生姓名, YEAR(入学日期) AS 入学年份 FROM 学生信息 WHERE YEAR(入学日期) = 2023 ORDER BY 入学年份;

这条语句你看着很顺,因为WHERE和SELECT里都用了YEAR函数,ORDER BY里用了别名“入学年份”。实际上SQLServer执行过程是这样的:

第一步,FROM——确定数据源,把学生信息表整个读入内存(当然这只是逻辑上的,实际有索引优化,但逻辑上要先理解这步)。

第二步,WHERE——逐行过滤。这时候SELECT还没执行,所以SELECT里头定义的别名“入学年份”压根不存在。为什么WHERE里你写了YEAR(入学日期)能行?因为“入学日期”是表里的真实列,WHERE可以直接操作;但“入学年份”是SELECT阶段才计算出来的结果,WHERE阶段根本看不见它。

第三步,SELECT——投影,决定最终输出哪些列,如果有计算表达式、函数,在此时执行,别名也是此时生效。

第四步,ORDER BY——这一步比较特殊,它是唯一一个“可以用别名”的子句。因为ORDER BY在逻辑上排在SELECT之后,所以别名此时已经生成了。

所以你记住这个口诀:WHERE只能用原始列,ORDER BY可以用别名,GROUP BY和HAVING介于两者之间(GROUP BY也不能用SELECT别名,HAVING理论上可以但建议别用)。

实际工作里,有些人在WHERE里写别名报错,就想着“把别名换成原表达式”——这是对的;但也有人反而把WHERE里的表达式搬到SELECT里做了一次,SQL变成“SELECT YEAR(入学日期) AS 入学年份 FROM 学生信息 WHERE 入学年份=2023”,还是报错。根源就是对执行顺序没概念。

1.2 最简单的查询:SELECT和FROM的配合

回到最基础的场景。你刚拿到一张员工表,想知道里面存了什么,最简单的写法是:

SELECT * FROM 员工表;

星号表示“所有列”。但这个写法有三个问题需要注意。

第一,生产环境里不推荐。因为如果表结构变了(加了列),你的查询结果列也会跟着变,程序里如果按列位置取值就会出问题。更好的习惯是明确列出你需要的列名。

第二,星号会让SQLServer先去查一下“这个表有哪些列”,多了一次元数据访问。数据量小无所谓,但千万行级别的表,列特别多的时候会拖慢首次查询速度。

第三,当你只关心三五个字段的时候,用星号会把所有列都捞出来,网络传输的数据量也变大。

SELECT 工号, 姓名, 部门, 入职日期 FROM 员工表;

明确列名的好处是:查询结果结构稳定、传输数据量小、后续维护的人一眼就能看出你要哪些数据。这也是所有专业开发团队代码规范里都会要求的一条。

还有一个SELECT的细节:SELECT后面可以直接跟常量,不需要FROM。比如:

SELECT GETDATE() AS 当前时间, 1 + 1 AS 计算结果;

这个在测试连接、快速查看数据库时间时非常有用。SQLServer允许无FROM的SELECT,这在MySQL和Oracle里也支持,可以作为快速验证的小工具。

另外,SELECT DISTINCT是热点词里很多人问的去重方法,但我要提醒一句:DISTINCT不是万能的。它只对“你选出来的那一串列组合”做完全相同的去重,如果你SELECT了三列,那么三列完全相同才会被去重。很多人想对单列去重,却把后面的列也带出来了,结果发现去重没生效——因为其他列的值不同。

正确理解DISTINCT的粒度:它作用于SELECT列表的整体,而非单独的某一列。如果只想看某个列有哪些不重复值,就只SELECT那一列再加DISTINCT。这个后面排序去重那节还会细讲。

2. WHERE条件过滤:从“全表扫描”到“精准定位”

真正让查询有价值的,是WHERE。没有WHERE的SELECT就是“把整表捞出来”,数据库系统设计者最希望看到的查询是“用尽量少的IO取到尽量精确的数据”。所以这一节重点讲清楚WHERE的各类条件写法,以及各自潜在的问题。

WHERE后面跟的是一个逻辑表达式,对每一行判断真(TRUE)或假(FALSE),只有为真的行才会进入下一步。表达式里可以用比较运算符、逻辑运算符、范围判断、列表判断、模糊匹配、空值判断等。

我把它们分成四类来讲:基础比较、范围与列表、模糊匹配、NULL特殊处理。这四类几乎覆盖日常查询90%以上的过滤需求。

2.1 比较运算符:=、>、<、>=、<=、<>的用法细节

最常见的过滤就是比较:

-- 查工资大于5000的员工 SELECT 姓名, 工资 FROM 员工表 WHERE 工资 > 5000; -- 查部门不等于'销售部'的员工 SELECT 姓名, 部门 FROM 员工表 WHERE 部门 <> '销售部'; -- 查入职日期在2020年1月1日之后的员工 SELECT 姓名, 入职日期 FROM 员工表 WHERE 入职日期 >= '2020-01-01';

这里有个坑:日期和时间。如果你存的是datetime类型,那么“入职日期 >= '2020-01-01'”会把2020年1月1日当天所有记录都包括进来,同时还包括了1月1日0点之后的所有时间点——比如2020-01-01 09:30。很多人以为这样会漏掉当天,但其实没有——因为'2020-01-01'在比较时被自动转成了'2020-01-01 00:00:00'。

如果想只查1月1日这一天的数据,不能只写“= '2020-01-01'”,要写成:

WHERE 入职日期 >= '2020-01-01' AND 入职日期 < '2020-01-02'

或者用CONVERT把datetime转成date再比(但这会牺牲索引,后面讲性能时再展开)。这里先记住第一条:对日期范围查询,“左闭右开”是最安全的写法

还有一个细节字符串比较。SQLServer默认排序规则下,字符串比较通常不区分大小写,但区分全角半角。另外,字符串要用单引号括起来,不能省略。如果你在字符串里要包含单引号,需要写成两个单引号转义,比如:

SELECT * FROM 员工表 WHERE 姓名 = 'O''Brien';

2.2 范围与列表:BETWEEN和IN的正确打开方式

BETWEEN用来判断某个值是否在某个范围内,注意它是包含边界的:

SELECT 姓名, 工资 FROM 员工表 WHERE 工资 BETWEEN 5000 AND 8000;

等价于“工资 >= 5000 AND 工资 <= 8000”。初学者容易忽略的是BETWEEN包含两端,导致统计结果和预期不一致。

更隐蔽的一个坑是BETWEEN和日期。还是前面的问题,如果你写:

WHERE 入职日期 BETWEEN '2020-01-01' AND '2020-01-31'

你以为查了1月整月,但实际上这会把1月31日0点到23点59分的记录都包含进来,而且1月31日的数据不会漏(因为包含边界)——真正的问题在于,如果数据里有“2020-01-31 08:00:00”这种,仍然没问题;但如果哪天有人录入了“2020-01-31 23:59:59.500”,也在范围内。这其实正是你想要的。真正需要小心的是:BETWEEN把结束点理解成当天零点还是当天最后一毫秒?答案是“你写的那个值本身”。你写'2020-01-31',它就当'2020-01-31 00:00:00'处理,所以1月31日的记录其实还包括了1月31日零点前的部分?不对,这里重新理一下:

  • 写“BETWEEN '2020-01-01' AND '2020-01-31'”,SQLServer把后面的字符串转成“2020-01-31 00:00:00”。
  • 结果:包含1月1日00:00:00 到 1月30日23:59:59.999 的所有记录。
  • 1月31日0点之后的记录会被漏掉

这是一个经典错误:想查整月,结果少了最后一天。因为日期类型不带时间,但存到datetime列后,哪怕你录入的是“2020-01-31”,实际值是“2020-01-31 00:00:00”,而你BETWEEN的范围到“2020-01-31 00:00:00”,所以只有刚好等于零点的那条记录被包含,当天其他时间全部丢失。除非你的数据全是纯date类型(SQLServer 2008以后的date类型),否则一定会踩这个坑。

所以我的建议是:查日期范围,尽量用“>=起始日期 AND < 结束日期+1天”这种左闭右开写法,别用BETWEEN处理日期。

IN用来判断某列值是否在给定列表内:

SELECT 姓名, 部门 FROM 员工表 WHERE 部门 IN ('销售部', '市场部', '技术部');

等价于三个=条件用OR连接。IN的写法更清晰,但要注意列表不能太长。如果列表上千个值,SQLServer会产生大量OR条件,查询计划会变得很复杂。这时候要么想想能不能用关联表,要么把列表放到临时表再JOIN。从性能角度看,几百个以内的IN列表问题不大,上千就要警惕了。

“IN查询语句报错”这个热词基本就是这么来的——要么列表太长、要么列表里有NULL导致结果集不对(IN遇到NULL不会报错,但会“少数据”,后面NULL小节细讲),要么类型不匹配,比如列是int,列表里写了'abc',转换时报错。

2.3 LIKE模糊匹配:通配符与搜索的坑

LIKE用于模糊匹配,配合通配符使用。SQLServer支持的通配符有四个:

通配符作用示例
%匹配任意长度字符串(含空)'张%' 匹配 张三、张伟、张无忌
_匹配单个字符'张_' 匹配 张三、张飞,但不匹配 张无忌
[]匹配括号内任意单个字符'[张李王]三' 匹配 张三、李三、王三
[^]匹配不在括号内的任意单个字符'[^张李王]三' 匹配 赵三,但不匹配张三

实际工作中的经典场景:

-- 查所有姓张的员工 SELECT 姓名 FROM 员工表 WHERE 姓名 LIKE '张%'; -- 查手机号第二位是5的客户 SELECT 姓名, 手机号 FROM 客户表 WHERE 手机号 LIKE '_5%';

LIKE的性能问题要特别说明:前导通配符会杀死索引。WHERE 姓名 LIKE '%张%' 这种写法,因为%在最前面,SQLServer无法利用索引来加速查找,只能做全表扫描。如果数据量几十万,这倒还好;如果几千万行,一次这种查询能把数据库IO打满。我的建议是:能避免就避免前导通配符;实在要查,可以考虑全文索引方案(但那是另一套技术)。

再补一个转义技巧:如果要查的数据本身包含%或_,需要用ESCAPE指定转义符:

-- 查折扣率为50%的记录 SELECT * FROM 商品表 WHERE 折扣率 LIKE '50!%' ESCAPE '!';

这里!是转义符,表示跟在后面的%是普通字符而不是通配符。ESCAPE子句可以被很多人忽略,直到某一天要查带百分号的错误数据时才发现SQLServer有这功能。

2.4 NULL:最容易让查询结果“悄悄不对”的元凶

NULL代表“不知道”“没填”“不存在”,它不是0,也不是空字符串,它是“一个不知道是什么的值”。这个哲学层面的区别在查询中体现得非常明显。

-- 错误的用法:查没有手机号的客户 SELECT * FROM 客户表 WHERE 手机号 = NULL; -- 正确用法 SELECT * FROM 客户表 WHERE 手机号 IS NULL; -- 查有手机号的客户 SELECT * FROM 客户表 WHERE 手机号 IS NOT NULL;

很多新手第一次写“= NULL”发现一行都查不到,就是因为NULL和任何值比较的结果都是“未知”(UNKNOWN),而WHERE只保留结果为TRUE的行。“未知”不等于“FALSE”,但都不进入结果集。

更有意思的坑在NOT IN和NULL的配合。比如你查“部门不在A、B、C里的员工”:

SELECT * FROM 员工表 WHERE 部门 NOT IN ('销售部', '市场部', NULL);

这条语句的结果是空集。为什么?因为“部门 <> '销售部' AND 部门 <> '市场部' AND 部门 <> NULL”这个复合条件里,最后一项的结果永远是UNKNOWN,整个AND的结果在多数情况下会变成UNKNOWN或FALSE,最终不返回行。很多人查不到数据时怀疑是权限问题、数据问题,折腾半天,其实就是一个隐藏的NULL。

建议:写NOT IN之前,先确认列表里不会有NULL;或者改用NOT EXISTS方式(这个后面子查询部分再讲)。总而言之,NULL不是麻烦,不了解NULL才是麻烦。

3. 函数处理:让查询能回答更复杂的问题

原始列的值往往不能直接满足业务需求:日期要算年份,字符串要截取,数字要转格式。SQLServer内置了大量函数,这篇文章先覆盖最常用的几类,包括热搜词里大家高频在找的“字符串转数字”和“日期函数”。

3.1 字符串函数:截取、拼接、转换一条龙

先讲字符串转数字,这是一个非常日常的需求。比如手机号、身份证号这类数据如果建表时用了varchar,或者外部导入的数据全是字符串格式,你要做数值比较或计算时就得先把它们转成数字类型。SQLServer提供了三种方式:CAST、CONVERT和TRY_CAST。

-- 方式一:CAST(标准SQL语法) SELECT CAST('123' AS INT) AS 转成整数; -- 方式二:CONVERT(SQLServer扩展语法,可指定样式) SELECT CONVERT(INT, '123') AS 转成整数; -- 方式三:TRY_CAST(转换失败返回NULL,不报错) SELECT TRY_CAST('abc' AS INT) AS 失败时返回NULL;

CAST和CONVERT的区别在转日期时特别明显——CONVERT能带102、103、120这些样式参数,把各种格式的字符串转成日期:

SELECT CONVERT(DATE, '2024-03-15', 120) AS 标准格式, CONVERT(DATE, '15/03/2024', 103) AS 英国格式;

实际工作中“sqlserver字符串转数字”最常见的坑:列里混入了非数字字符,CAST直接报错。比如你有一列手机号,业务上以'0'开头,但某几条录入了字母。这时用TRY_CAST就比CAST安全。另一个坑是整数溢出:'99999999999'转INT会超出范围,同样报错,可以先转BIGINT看看。

字符串截取方面,最常用的三个函数:

SELECT LEFT('SQLServer教程', 3) AS 取左边3个字符, -- 结果:SQL RIGHT('SQLServer教程', 2) AS 取右边2个字符, -- 结果:教程 SUBSTRING('SQLServer教程', 4, 6) AS 从第4位开始取6个, -- 结果:Server LEN('SQLServer') AS 字符串长度, -- 结果:10,注意不计算尾随空格 DATALENGTH('SQLServer') AS 字节数; -- 结果:10,但如果存的是NVARCHAR会是20

LEN和DATALENGTH的差异在中文场景很关键。LEN按字符算,'数据库'的长度是3;DATALENGTH按字节算,如果列是VARCHAR,'数据库'是6字节(每个汉字2字节,与排序规则有关),如果是NVARCHAR则固定是每个字符2字节。这个差异在涉及存储空间判断、字符串截取边界时非常容易踩坑。

字符串拼接,SQLServer用加号或者CONCAT函数:

SELECT 姓名 + '-' + 部门 AS 用加号拼接 FROM 员工表; SELECT CONCAT(姓名, '-', 部门) AS 用CONCAT拼接 FROM 员工表;

加号拼接有个隐患:只要其中一个是NULL,整个结果就是NULL。CONCAT函数则会把NULL当空字符串处理,不产生NULL结果。所以我的习惯是:拼接人名字符串场景用CONCAT,需要保留NULL特质的用加号。

3.2 日期函数:日期运算与格式化的核心方法

日期函数是查询中使用频率最高的一类函数。热点词“sqlserver 日期函数”背后反映出大家常遇到三类需求:取当前日期时间、日期加减、提取日期中的某部分。

-- 当前日期时间 SELECT GETDATE() AS 当前时间, -- 返回datetime类型 SYSDATETIME() AS 更精确的时间, -- 返回datetime2(7) CAST(GETDATE() AS DATE) AS 当前日期; -- 只要日期,不带时间 -- 日期加减 SELECT DATEADD(DAY, 30, GETDATE()) AS 30天后, DATEADD(MONTH, -1, GETDATE()) AS 1个月前, DATEADD(YEAR, 1, GETDATE()) AS 1年后; -- 两个日期的差值 SELECT DATEDIFF(DAY, '2024-01-01', '2024-03-15') AS 间隔天数, -- 74 DATEDIFF(MONTH, '2024-01-01', '2024-03-15') AS 间隔月数; -- 2 -- 提取日期部分 SELECT DATEPART(YEAR, 入职日期) AS 年份, MONTH(入职日期) AS 月份, DAY(入职日期) AS 日期 FROM 员工表;

DATEDIFF有个容易误解的点:它计算的是“跨过多少个边界”,而不是“相差多少完整单位”。比如从'2024-01-01 23:59'到'2024-01-02 00:01',差2分钟,但DATEDIFF(DAY, ...)的结果是1——因为跨过了日期的边界。如果你要算“真正满24小时才算一天”,需要自己用秒数除,或者用DATEDIFF_BIG配合高精度时间。

还有DATEADD和DATEDIFF组合的经典用法:算某个日期所在周的周一。

-- 假设 @d 是任意日期,DATEADD(DAY, 1 - DATEPART(WEEKDAY, @d), @d) -- 在不同DATEFIRST设置下结果不同,这里需加上@@DATEFIRST修正 SELECT DATEADD(DAY, 1 - DATEPART(WEEKDAY, GETDATE()) - @@DATEFIRST + 1, GETDATE()) AS 本周周一;

这个语句在不同语言环境下容易出错,因为WEEKDAY的起始日取决于服务器设置。如果没有特殊要求,我一般建议直接用DATEPART(WEEKDAY)配合业务日历表来处理“自然周”逻辑,否则调试起来很费神。

日期格式化,最常见的是把日期转成'YYYY-MM-DD':

SELECT CONVERT(VARCHAR(10), GETDATE(), 120) AS 标准日期, -- 2024-03-15 CONVERT(VARCHAR(8), GETDATE(), 112) AS 紧凑日期, -- 20240315 FORMAT(GETDATE(), 'yyyy年MM月dd日') AS 中文格式; -- 2024年03月15日

FORMAT函数很灵活但性能较差,因为它走.NET的格式引擎。少量数据无所谓,几十万行以上做格式化时建议用CONVERT代替。

3.3 类型转换的原则:显式优于隐式

前面提到CAST/CONVERT/TRY_CAST,这里展开讲一个更重要的原则:显式转换优于隐式转换

SQLServer在比较不同类型时,会自动做隐式转换。比如int列和varchar列比较:'123'会被转成123再比。听起来方便,但有两个致命问题。

第一,隐式转换可能导致索引失效。如果表里的手机号列是varchar,你写WHERE 手机号 = 13800138000,SQLServer要把手机号列每个值都转成数字再比较,索引就用不上了,导致全表扫描。这是性能杀手。

第二,隐式转换的规则不符合直觉。SQLServer有类型优先级,低优先级的类型会转成高优先级。比如int和decimal比较,int转decimal;varchar和int比较,varchar转int(如果varchar里存的是'abc',直接转换报错)。

我的原则是:建表时就把类型定对,数字就是int/decimal,日期就是date/datetime,字符串就是varchar/nvarchar。如果因为各种历史原因数据类型混乱了,查询时显式转换比隐式转换安全得多——至少报错时你知道是自己转的,排查起来快。

4. 排序与去重:让结果有序、让数据不重复

查询结果默认是无序的,这个“无序”不是随机的,而是“不确定的”——取决于SQLServer的查询计划、存储顺序、索引情况,每次执行可能都不一样。想要稳定的结果顺序,必须显式使用ORDER BY。

4.1 ORDER BY的用法与执行细节

SELECT 姓名, 工资 FROM 员工表 ORDER BY 工资 DESC;

ORDER BY后面可以跟列名、别名、表达式,也可以用数字表示“按SELECT列表里第几列排序”——比如ORDER BY 2表示按第二列排序。用数字虽然快,但可读性差,别人看代码还得回去数你SELECT了哪些列,不推荐。

排序规则上,SQLServer默认升序(ASC),想降序要写DESC。多列排序从左到右逐级生效:

SELECT 部门, 工资, 姓名 FROM 员工表 ORDER BY 部门 ASC, 工资 DESC;

表示先按部门升序,同部门内再按工资降序。这里最容易犯的错是每列都写ASC/DESC,比如“ORDER BY 部门, 工资 DESC”——这个写法正确;但“ORDER BY 部门 ASC, 工资”意味着工资按默认升序排,有些人以为写了ASC以后影响了后面的DESC,其实没有,每个排序键独立设置。

NULL在排序中的位置比较特殊。SQLServer默认升序时NULL排最前,降序时NULL排最后。但不同数据库不一样(比如Oracle默认升序NULL排最后),如果你要精确控制NULL的位置,可以用CASE表达式:

SELECT 姓名, 工资 FROM 员工表 ORDER BY CASE WHEN 工资 IS NULL THEN 1 ELSE 0 END, 工资 DESC;

个性化排序需求,比如“按固定顺序排列部门”,用CASE或CHARINDEX都可以。有时候这种查询场景比普通排序更常见——比如报表要求“按公司组织架构的顺序显示部门”,这个顺序在业务上有明确意义,但表里没有编号字段。可以用:

SELECT 姓名, 部门 FROM 员工表 ORDER BY CHARINDEX(部门, '总裁办-技术部-市场部-销售部');

4.2 DISTINCT去重:什么时候能用、什么时候不能用

DISTINCT是热搜词里高频出现的一个词。它解决的问题是:查询结果中出现重复行时,只保留一条。经典场景:查这个月有消费记录的客户编号。

SELECT DISTINCT 客户编号 FROM 订单表 WHERE 下单日期 >= '2024-01-01';

注意这里说的是“重复行”——如果你同时SELECT了客户编号和客户名称,那么“同一个人但名称写法不完全一致”也会被视为不同行。比如“张三”和“张 三”,DISTINCT会认为这是两条不同的记录。

再提醒一次:DISTINCT作用于你SELECT出来的整个列组合。把DISTINCT放在一列上,其他列照选,在SQLServer里直接报错:

-- 错误示例 SELECT DISTINCT 客户编号, 客户名称 FROM 订单表;

如果你想查“每个客户编号对应的所有客户名称”,这不是去重问题,是分组或关联问题。DISTINCT只是“结果集层面去重”,不是“列级别去重”。

另一个被问得很多的热搜词“sqlserver删除重复数据只保留一条 无id”,这也是DISTINCT无法解决的场景——因为删除操作需要定位到具体行,DISTINCT只能查询不能删。这种情况的通用解法是用ROW_NUMBER()窗口函数,方法是给重复记录编个序号,保留序号为1的行,删除其余。这个属于进阶内容,后续讲到窗口函数时会专门展开。

4.3 TOP:限制返回行数的强大武器

TOP是SQLServer的一个特色语法,用来限制返回的行数。其他数据库(如MySQL)用LIMIT,SQLServer用TOP,注意别搞混。基本用法:

-- 返回前10条 SELECT TOP 10 姓名, 工资 FROM 员工表 ORDER BY 工资 DESC; -- 返回前1%条 SELECT TOP 1 PERCENT 姓名, 工资 FROM 员工表 ORDER BY 工资 DESC; -- 包含并列排名(即工资相同都算) SELECT TOP 10 WITH TIES 姓名, 工资 FROM 员工表 ORDER BY 工资 DESC;

TOP常和ORDER BY配合使用——没有ORDER BY的TOP 10只是“任意10条”,并不保证是“前10”。你想查工资最高的10个人,必须写“TOP 10 ... ORDER BY 工资 DESC”。

WITH TIES是个容易被忽略但有奇效的语法。比如第10名和第11名工资一样,普通TOP 10只返回10条,第11名被截掉。加上WITH TIES后,会把这些并列的都带上,结果可能多于10条。这在报表“前10大客户”这种场景特别实用,避免出现“我和别人工资相同,凭什么他上榜我被刷掉”的尴尬。

还有一个很多人不知道的:TOP也可以在UPDATE和DELETE里用。比如删除“前100条测试数据”:

DELETE TOP (100) FROM 日志表 WHERE 日志级别 = '调试';

注意这个语法在SQLServer里不需要ORDER BY,它删除的是物理上前100条。缺点是结果不确定——如果想精确控制先删哪100条,得配合子查询。这个属于DELETE篇的话题了,这里先提一句,大家知道有这回事。

4.4 去重与排序的配合:一次搞懂“取每组最大值”的基础版

严格来说“每组最大值”要窗口函数才方便,但用排序加去重思路也能实现一部分。比如你有订单表,想查每个客户最近的一单:

SELECT 客户编号, 下单日期 FROM 订单表 AS o WHERE 下单日期 = (SELECT MAX(下单日期) FROM 订单表 WHERE 客户编号 = o.客户编号);

这种“自关联子查询”是下一章的范围,这里先不展开。但要建立需求分析的意识:先分清“去重”(去掉完全重复的行)和“分组取极值”(每个分类下取一条代表)是两种不同的需求,你用DISTINCT解决不了后者。

理解了这个区别,你再看到“sql语句去重查询”这个热搜词时,就会知道提问者可能真正想要的是“每个分类下取一条”,而不只是简单的DISTINCT。作为博主,我建议初学者先扎实掌握DISTINCT,再进阶到窗口函数,因为窗口函数建立在对基础查询语法的完全理解之上。

5. 查询语句的经典报错与排查:踩过的坑都在这了

前面每节都穿插了一些坑,这一节把它们集中整理成速查表,方便实际报错时对照排查。

报错信息(或现象)原因解决办法
列名无效WHERE/GROUP BY中用了SELECT别名替换为原始表达式或原始列名
在关键字ORDER附近有语法错误ORDER BY位置不对,或漏写了逗号检查语句各子句顺序
将varchar转换为int时失败字符串里有非数字字符用TRY_CAST返回NULL,再筛选排查脏数据
结果集与预期不一致NULL参与比较/IN里有NULL用IS NULL/IS NOT NULL,或改用NOT EXISTS
查询特别慢LIKE前导通配符、隐式转换、缺索引改写语句,避免左侧通配,给过滤列建索引
日期查不到最后一天BETWEEN错误匹配时间范围用左闭右开写法 >= 起始 AND < 截止+1天
TOP数量比预期多使用了WITH TIES且有并列确认业务是否需要并列,不需要就去掉WITH TIES
datetime溢出日期字符串格式不对用CONVERT带样式参数转日期
字符串截断目标列长度不够用LEFT/RIGHT/SUBSTRING明确截取
去重没生效多列组合后才重复只SELECT需要去重的列

这里额外说两个我在实际运维中见到的“典型非典型”问题。

第一个是“in查询语句报错”中常见的子查询返回多列问题:

-- 错误:子查询返回了两列 SELECT * FROM 员工表 WHERE 部门 IN (SELECT 部门, 备注 FROM 部门表);

IN后面的子查询只能返回一列。如果想用两列来匹配,得用EXISTS或关联条件。

第二个是“timer执行查询是报空指针”这个热词,看起来是Java代码问题——定时任务里执行SQL,但数据库连接为NULL。本质原因是连接没有在定时器执行前初始化。这种问题排查思路别老盯着SQL看,先看连接对象是否为空、事务是否已提交、数据库连接池是否被回收。SQL语句本身没问题的时候,检查运行环境才是最有效的方向。

再说一个经典“慢查询日志”相关的话题。SQLServer没有像MySQL那样默认开启慢查询日志(除非用扩展事件),所以排查慢查询时,我的做法是先抓执行计划:

SET STATISTICS TIME ON; SET STATISTICS IO ON; SELECT * FROM 大表 WHERE 某列 = '某值'; SET STATISTICS TIME OFF; SET STATISTICS IO OFF;

通过输出的“CPU时间、占用时间、扫描次数、逻辑读”等指标来判断查询是否高效。如果扫描次数特别多、逻辑读特别高,大概率是缺索引或者写完杀了索引的查询。这是一套可以自学的排查路径,后续讲索引优化时再写专题。

6. 从“会写”到“写好”:查询思维的进阶路径

写完了查询基础语法,最后我一定要多啰嗦几句“思维”层面的东西。因为带过太多新人,我太清楚从“能跑出结果”到“能稳定、高效地跑出正确结果”之间隔着什么。

第一,先想清楚要什么,再写语法。很多新手拿到需求就写SQL,写一半发现漏了条件,再加;再跑发现字段不对,再改。正确流程是:先把数据结构看清楚(有哪些表、哪些列、表间关系),再用自然语言把需求拆成“从哪张表、过滤什么条件、取哪些列、怎么排序”,最后才翻译成SQL。前面三步想清楚了,SQL一般一遍就能写对。

第二,写SQL时保持“每步能解释”。如果你写的每个子句都能说出“这一步在做什么过滤/转换”,你的SQL就不会出错。反过来,如果某段SQL你自己都说不清为什么这么写,那它大概率藏着隐患。

第三,不要迷信“能跑就行”。一个查询在几百行数据上跑得快,不代表在百万行上也能跑得快。我见过太多上线后才爆发性能问题的SQL,当初都是“测试环境数据量小,看起来没问题”就放过去了。从第一天写SQL就养成看执行计划的习惯,对你后面的成长帮助极大。

第四,NULL意识要刻在骨子里。SQL里与NULL相关的坑能写一本书。每次写比较条件、写IN、写NOT IN、写拼接、写聚合,都先问一句:如果这里有NULL会怎样?养成这个习惯以后,你在数据质量处理上会少踩一半的坑。

这一章的内容到这里只是一个起点。下一篇会讲GROUP BY分组聚合、HAVING过滤、以及更复杂的多表JOIN和子查询。但请你务必先把本文的基础打牢——尤其是执行顺序、NULL处理、日期边界这三个最容易被忽略的细节,因为这些恰恰是区分“会写SQL”和“能查好数据”的关键。

我自己带新人的时候有个习惯:让他们把本文里的每一条语句,都拿SQLServer Management Studio亲手敲一遍,再故意改错几个地方,看看报错信息和预期是否一致。这种“故意写错再观察”的练习,比单纯照着敲十遍更有效——因为报错信息也是一种学习材料,多读几次,你对SQLServer的脾性就熟了。

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

四个工业级O(nlogn)排序算法选型与调优实战

1. 这不是算法课件&#xff0c;而是四个真正能用、敢用、用了不踩坑的nlogn排序方案你翻过《算法导论》第6章&#xff0c;也刷过LeetCode的排序题&#xff0c;但真到写业务代码时——数据库查出来的订单列表要按创建时间金额双字段排序&#xff0c;前端传来的用户行为日志要按时…

作者头像 李华
网站建设 2026/9/17 12:11:58

STM32CubeMX生成IAR工程实战指南:配置、编译与常见坑

今天聊聊嵌入式开发里一个挺常见的需求&#xff1a;用STM32CubeMX生成IAR工程。网上有个词叫“STM32CubeMX2”&#xff0c;其实就是我们平时说的STM32CubeMX&#xff0c;可能版本号写顺了多打了个2。最近在一个老项目里接手了一批IAR工程&#xff0c;代码维护全靠CubeMX重新生成…

作者头像 李华
网站建设 2026/9/17 12:11:37

Security Report for {{Workspace}}

Security Report for {{Workspace}} 【免费下载链接】osmedeus A Modern Orchestration Engine for Security 项目地址: https://gitcode.com/GitHub_Trending/os/osmedeus Generated: {{TaskDate}} Target: {{Target}} Run UUID: {{RunUUID}} ## 二、三种语法形态&…

作者头像 李华
网站建设 2026/9/17 12:11:02

汽车行业数字化转型顶层规划:从价值链对齐到量化指标落地

简介&#xff1a;一份关于汽车行业数字化转型的顶层规划设计报告&#xff0c;以PPTX演示文稿形式呈现&#xff0c;适合车企高层、战略规划人员及数字化转型咨询从业者用于内部汇报、现状研判与路径设计。报告系统梳理了科技创新、政策推动、产业与市场驱动等背景&#xff0c;并…

作者头像 李华
网站建设 2026/9/17 12:07:13

WSL2启动报HCS_E_HYPERV_NOT_INSTALLED解决

1. 先把这个报错的底细摸清楚敲下wsl命令的那一刻&#xff0c;屏幕没给你 Ubuntu 的欢迎信息&#xff0c;反而甩回来一串红字&#xff1a;Error code: Wsl/Service/CreateVm/HCS/HCS_E_HYPERV_NOT_INSTALLED。这个报错我第一次见到的时候也愣了一下&#xff0c;因为这行信息拆开…

作者头像 李华