
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 姓名 OBrien;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 转成整数; -- 方式二CONVERTSQLServer扩展语法可指定样式 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会是20LEN和DATALENGTH的差异在中文场景很关键。LEN按字符算数据库的长度是3DATALENGTH按字节算如果列是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-DDSELECT 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 手机号 13800138000SQLServer要把手机号列每个值都转成数字再比较索引就用不上了导致全表扫描。这是性能杀手。第二隐式转换的规则不符合直觉。SQLServer有类型优先级低优先级的类型会转成高优先级。比如int和decimal比较int转decimalvarchar和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用LIMITSQLServer用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 TIESdatetime溢出日期字符串格式不对用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的脾性就熟了。