做SQL Server开发这些年,我越来越发现一个规律:日常工作中真正能拉开效率差距的,往往不是那些被吹得神乎其神的高级特性,而是最基础的函数用得熟不熟。日期字段要不要转成字符串?字符串怎么截取、怎么拼接才不踩坑?分组统计的结果怎么算才是对的?这些问题几乎天天都在遇到。这篇文章我把SQL Server最常用的日期转换、字符串、数学、聚合四类函数全部梳理了一遍,每条都配有实际使用场景、语法示例和避坑细节,适合刚入门SQL Server的新手系统学习,也适合中高级开发者在日常工作中随手查阅。你可以把它当成一份可以反复回看的函数手册,也可以直接照着例子复制到自己的环境里跑一遍。
1. 内容整体设计与思路拆解
1.1 为什么这四类函数最值得系统整理
在实际的SQL Server开发中,我做过报表、写过存储过程、也做过不少数据迁移的脏活累活,回头总结发现一个很有意思的现象:你写的每一条SQL几乎都逃不开四种操作——把时间转换成想要的格式、把文本切一切拼一拼、把数字算一算、把数据聚合起来做统计。这四种操作对应的正是日期函数、字符串函数、数学函数和聚合函数。
刚入行的时候我也犯过傻,喜欢把函数零零散散地记在本地笔记里,需要用的时候翻出来看一眼,结果过两天又忘。后来我换了个思路,先把SQL Server的函数体系在脑子里梳理成一张地图,把每个函数归类、做对比、看差异,再配合实际业务场景去练,记忆效率一下子提升了不少。这篇文章本质上就是我把自己梳理过的这张函数地图摊开给你看,每个函数都有语法、有例子、有坑点,照着走一遍,比孤立地背公式要牢固得多。
1.2 学习函数库的正确思路
不少初学者学函数最大的误区是一个一个孤立地背。背LEN、背SUBSTRING、背CHARINDEX,背完就忘,因为不知道它们之间的组合关系。
其实SQL Server函数更像是一套积木,单个函数往往解决不了复杂问题,组合起来才是王道。比如说“提取邮箱地址中的用户名”,你至少需要CHARINDEX定位@的位置,再用LEFT截取前面的部分,这就同时用到了定位和截取两类函数。再比如常见的“按月统计销售额”,需要用到DATEPART把日期归一到月份,再配合GROUP BY和SUM做聚合,这又涉及日期函数和聚合函数的协作。
所以我在下文的讲解中,不会只列函数的定义和语法,而是把每个函数的典型业务场景、容易踩的坑、和相邻函数的对比都带上。这样你看一遍就会有真实的使用记忆,而不是对着文档抄完就扔。
2. 日期转换与处理函数:最容易踩坑的一类
2.1 取当前时间的函数:GETDATE、SYSDATETIME、CURRENT_TIMESTAMP
先讲最基础的一类:获取当前时间。SQL Server里日常用得最多的是这三个——GETDATE()、SYSDATETIME()、CURRENT_TIMESTAMP。
- GETDATE() 返回当前日期和时间,精度到毫秒
- SYSDATETIME() 精度更高,能到100纳秒
- CURRENT_TIMESTAMP 是ANSI标准写法,效果等同GETDATE()
我在实际项目中通常默认用GETDATE(),因为它老牌、稳定,团队里人人一看就懂。如果对时间精度有硬性要求,比如日志表需要记录更精确的操作时间,才换成SYSDATETIME()。CURRENT_TIMESTAMP一般在写跨数据库兼容脚本时使用,因为它更符合SQL标准,将来换数据库引擎时改动最小。
这里有一个非常容易忽略的细节:GETDATE()返回的是SQL Server所在服务器的本地时间。如果你的应用服务器和数据库服务器不在同一个时区,或者数据库服务器有夏令时调整,取出来的时间可能和你预期差好几个小时。这时候要么统一约定各端都使用UTC时间存储,要么在取数时显式做时区换算,千万别默认“两边时间一定一样”。
2.2 CONVERT与CAST:日期转字符串的核心
日期转换是我见过报错最多的函数类别之一。SQL Server里最常用的两个转换函数是CAST和CONVERT。
CAST是ANSI标准的写法,语法简单:
SELECT CAST(GETDATE() AS VARCHAR(20));但CAST在日期转字符串时有个痛点——它只能输出固定的默认格式,比如“2025-01-05 14:23:45”这种。如果你想要“2025年01月05日”,或者“20250105”这种紧凑格式,CAST就无能为力了。
这时候就要用到CONVERT,它比CAST多了一个样式码参数:
SELECT CONVERT(VARCHAR(10), GETDATE(), 23); -- 2025-01-05 SELECT CONVERT(VARCHAR(8), GETDATE(), 112); -- 20250105 SELECT CONVERT(VARCHAR(23), GETDATE(), 121); -- 2025-01-05 14:23:45.123我整理了几个最高频的样式码,强烈建议保存下来:
| 样式码 | 输出格式 | 典型用途 |
|---|---|---|
| 101 | 01/05/2025 | 美国格式,老系统常见 |
| 103 | 05/01/2025 | 英式日/月/年 |
| 111 | 2025/01/05 | 斜杠分隔日期 |
| 112 | 20250105 | 纯数字日期,适合文件名、存储键 |
| 120 | 2025-01-05 14:23:45 | 标准日志格式 |
| 121 | 2025-01-05 14:23:45.123 | 带毫秒的完整时间 |
| 23 | 2025-01-05 | ISO日期 |
| 20 | 2025-01-05 14:23:45 | 与120等价 |
我在做数据接口的时候,最喜欢用112生成日期后缀给文件命名,比如“订单_20250105.xlsx”,排序和识别都非常直观。写日志、做报表展示时用120或121,因为可读性最好。
一个高频坑:有人写CONVERT(VARCHAR(3), GETDATE(), 112),以为通过指定VARCHAR长度就能控制输出位数。实际上样式码112输出8位字符,声明VARCHAR(3)只会把结果截断成“202”,既不是年份也不是月份,纯粹是截断后的垃圾值。年份截取应该用DATEPART或者RIGHT(CONVERT(VARCHAR(8), GETDATE(), 112), 4)之类的组合,而不是靠缩短VARCHAR长度。
2.3 DATEADD、DATEDIFF、DATEPART:日期运算三兄弟
日期运算在SQL Server里基本是三兄弟的天下:DATEADD、DATEDIFF、DATEPART。
DATEADD给日期加(或减)指定的时间间隔:
SELECT DATEADD(DAY, 30, GETDATE()); -- 30天后 SELECT DATEADD(MONTH, -3, GETDATE()); -- 3个月前 SELECT DATEADD(YEAR, 1, GETDATE()); -- 一年后DATEDIFF计算两个日期之间的差值:
SELECT DATEDIFF(DAY, '2025-01-01', '2025-01-31'); -- 30 SELECT DATEDIFF(MONTH, '2024-01-01', '2025-01-01'); -- 12DATEPART提取日期的某个部分:
SELECT DATEPART(YEAR, GETDATE()); -- 年 SELECT DATEPART(MONTH, GETDATE()); -- 月这组函数在报表统计里特别常用。比如按月统计销售额,最简洁的写法之一就是:
SELECT DATEPART(YEAR, OrderDate) AS 年份, DATEPART(MONTH, OrderDate) AS 月份, SUM(Amount) AS 销售总额 FROM Orders GROUP BY DATEPART(YEAR, OrderDate), DATEPART(MONTH, OrderDate);注意DATEPART(WEEKDAY)的返回值跟数据库的DATEFIRST设置有关,同样是星期一,在不同设置下可能返回1也可能返回2。如果你要在脚本里判断“今天是不是周一”,建议用DATENAME(WEEKDAY, GETDATE())取星期名称,再和固定字符串比较。虽然性能略低,但结果稳定、可读性更强,谁看谁知道。
2.4 FORMAT:方便的格式化利器,但生产环境要慎用
SQL Server 2012开始提供FORMAT函数,它可以使用.NET的格式字符串来格式化日期:
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd'); -- 2025-01-05 SELECT FORMAT(GETDATE(), 'yyyy年MM月dd日'); -- 2025年01月05日 SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss');好用吗?非常好用,尤其面对中文日期、自定义格式需求的时候,比CONVERT的样式码灵活太多。但我要给你一句忠告:生产环境不要滥用。
FORMAT底层走的是.NET CLR,执行速度比CONVERT慢得多。我自己做过粗略对比,在大数据量查询里把100万行逐行做FORMAT,耗时可能是CONVERT的几十倍。正确做法是:能用CONVERT样式码解决的绝不上FORMAT,只有样式码满足不了、又必须在SQL里生成特定格式字符串的时候才用,而且尽量控制在数据量小或一次性转换的场景。
FORMAT还有一个隐藏的坑:它依赖当前会话的语言和区域设置。同样一行FORMAT(GETDATE(), 'MMMM'),在中文环境输出“一月”,在英文环境输出“January”。如果你的应用程序连接串没有显式指定区域,不同客户端可能看到不同结果。要指定就带上第三个参数,比如FORMAT(GETDATE(), 'MMMM', 'en-US')。
3. 字符串函数:文本处理的全套工具
3.1 长度、截取、定位:一网打尽
字符串函数是日常用得最多的函数,没有之一。先看定位和截取这两组。
LEN()返回字符串长度,注意它返回的是字符个数而不是字节数,而且会忽略末尾空格。如果你需要精确到字节,用DATALENGTH()。这个区别在处理中文、表情符号等Unicode字符时特别容易出问题。比如:
SELECT LEN('abc '); -- 3,末尾空格被忽略 SELECT DATALENGTH('abc '); -- 6很多新手用LEN做数据校验,发现长度“变短”了,其实就是末尾空格被忽略导致的。
CHARINDEX()定位子串出现的位置:
SELECT CHARINDEX('@', 'user@example.com'); -- 5CHARINDEX在定位不到时返回0,这个特性经常用来做“是否包含”的判断。如果需要从右边开始找或者支持通配符,就轮到PATINDEX()登场。PATINDEX支持用%和_做模糊匹配,比如判断一个字符串是否包含数字:
SELECT PATINDEX('%[0-9]%', 'abc123'); -- 4SUBSTRING()按位置截取子串:
SELECT SUBSTRING('SQL Server函数大全', 5, 6); -- ServerLEFT()和RIGHT()则分别从字符串一端截取:
SELECT LEFT('SQL Server', 3); -- SQL SELECT RIGHT('SQL Server', 6); -- Server实际业务里最常见的需求是“从邮箱里提取用户名”和“从身份证号里提取出生日期”。前者可以这样写:
SELECT LEFT(Email, CHARINDEX('@', Email) - 1) FROM Users;后者可以这样写:
SELECT SUBSTRING(IDCard, 7, 8) -- 身份证第7位到第14位是出生日期 FROM Employees;3.2 替换、去空格、大小写:数据清洗的基本功
数据清洗是几乎所有SQL开发都躲不过的活儿。REPLACE()完成简单替换:
SELECT REPLACE('SQL-Server-教程', '-', '_'); -- SQL_Server_教程LTRIM()、RTRIM()分别去掉左侧和右侧空格,SQL Server 2017起引入的TRIM()一步到位去两侧空格:
SELECT TRIM(' hello '); -- helloUPPER()、LOWER()做大小写转换,常用于规范化比较。比如登录查询时希望不区分用户名大小写,就可以统一转成大写再比对。
这里我要专门说一个很多人都会犯的错误:REPLACE并不会更新原表的数据,它只是在查询结果里返回替换后的字符串。如果你想把清洗结果真正写回表,必须额外加UPDATE。类似地,TRIM、UPPER这些函数都是纯函数,不修改源数据。搞清楚这一点,你就不会在调试时四处找“数据去哪了”。
另一个经典坑是去空格不完全。你肉眼看见的“空格”可能不是普通空格,而是制表符、换行符或者全角空格。处理从Excel或网页导入的数据时,字符串里可能夹杂着CHAR(9)(制表符)、CHAR(13)(回车)、CHAR(10)(换行)。需要组合使用:
SELECT REPLACE(REPLACE(REPLACE(col, CHAR(13), ''), CHAR(10), ''), CHAR(9), '') FROM SomeTable;这条组合替换是我做数据导入时常用的套路,直接把不可见字符全部清掉,之后再去做格式校验会省心很多。
3.3 拼接与拆分:CONCAT、STRING_AGG、STRING_SPLIT
字符串拼接的经典写法是加号:
SELECT '姓名:' + Name FROM Users;但加号有个老毛病:如果任何一边是NULL,整个结果就变成NULL。很多新人查出来整列为空,排查半天也找不到原因。
SQL Server 2012推出了CONCAT(),它会自动把NULL当空字符串处理:
SELECT CONCAT('姓名:', Name) FROM Users; -- NULL不会让结果变NULLSQL Server 2017又加了CONCAT_WS(),可以用第一个参数作分隔符,把多个字段拼起来:
SELECT CONCAT_WS('-', '2025', '01', '05'); -- 2025-01-05如果想把分组结果里的多行数据拼到一个字段,就要用STRING_AGG,SQL Server 2017引入:
SELECT Department, STRING_AGG(EmployeeName, '、') AS 员工列表 FROM Employees GROUP BY Department;这个函数极大简化了“一对多拼接”的需求。在它出现之前,实现同样的效果只能靠FOR XML PATH绕来绕去,写起来痛苦,读起来也痛苦。
反向的拆分也有现成的STRING_SPLIT()(SQL Server 2016引入):
SELECT value FROM STRING_SPLIT('a,b,c', ',');它返回一个包含三行的结果集。注意STRING_SPLIT只支持单字符分隔符,如果你要按多字符分隔符比如',,'去拆,就拆不了,这是很多人遇到的第一道坎。另外,STRING_SPLIT输出的value列是NVARCHAR类型,实际使用时通常需要再CAST一下。
3.4 字符串与数字的互转:CAST、CONVERT与TRY_*
字符串转数字也是高频操作。最直接的是CAST和CONVERT:
SELECT CAST('123.45' AS DECIMAL(10,2)); SELECT CONVERT(INT, '567');但这两个函数在遇到非数字字符串时会直接报错。比如'12a3'转INT,一转换就抛“转换失败”的异常,整个查询中断。这种情况在导入外部数据、清洗脏数据时极其常见。
SQL Server 2012起提供了一组TRY_系列函数:TRY_CAST、TRY_CONVERT、TRY_PARSE。转换失败时返回NULL而不是抛错:
SELECT TRY_CAST('12a3' AS INT); -- NULL,不报错 SELECT TRY_CONVERT(DECIMAL(10,2), '12.34'); -- 12.34利用这个特性,可以快速定位脏数据:
SELECT 原始值 FROM ImportData WHERE TRY_CAST(原始值 AS INT) IS NULL;这个方法可以说是清洗数据时的首选武器。等确认完哪些数据有问题、修好之后,再替换成普通CAST去正式入库,效率会高很多。
4. 数学函数:数值计算的常用武器
4.1 舍入三兄弟:ROUND、CEILING、FLOOR
数值计算里最容易被误会的是舍入。“四舍五入”是很多业务需求,但SQL Server的ROUND支持第三个参数,可以控制是四舍五入还是纯截断:
SELECT ROUND(3.14159, 2); -- 3.14 SELECT ROUND(3.14159, 2, 1); -- 第三个参数=1时截断 SELECT ROUND(3.146, 2, 1); -- 3.14,直接截断不进位第三参数为1时,只保留指定位数,不进位。这在某些财务场景反而更安全,因为截断是确定性的,不会因为边界值产生“是否该进位”的争议。
CEILING()向上取整,FLOOR()向下取整:
SELECT CEILING(4.1); -- 5 SELECT FLOOR(4.9); -- 4注意,这两个函数对负数也遵循“向上”和“向下”的方向,而不是简单的绝对值取整。CEILING(-4.1)返回-4,因为-4比-4.1大;FLOOR(-4.1)返回-5,因为-5比-4.1小。新手很容易在这里栽跟头。
还有一个容易忽略的细节:ROUND返回的结果可能带末尾的0。比如ROUND(123.45, 1)结果是123.50,末尾的0会保留。如果需要去掉末尾0,通常要再配合字符串函数处理,或者用FORMAT控制展示格式。
4.2 常用计算函数:ABS、POWER、SQRT、SIGN
ABS()取绝对值,没什么好说的:
SELECT ABS(-8); -- 8POWER()做幂运算:
SELECT POWER(2, 10); -- 1024SQRT()取平方根:
SELECT SQRT(16); -- 4SIGN()返回数字的符号,正数返回1、负数返回-1、0返回0。这个函数在做趋势判断时很实用,比如判断两个月的销售额变化方向:
SELECT SIGN(本月销售额 - 上月销售额) AS 趋势 FROM Sales;数学函数单独使用都不难,真正的难点在于和业务逻辑结合。比如计算复利、汇率换算、距离计算,都是多个数学函数的组合。以汇率换算为例:
SELECT Amount * CONVERT(DECIMAL(10,4), Rate) FROM Transactions;这里需要注意数据类型的精度。DECIMAL(p,s)的设置很关键,如果精度设置不当,乘法结果可能被隐式转换,造成精度丢失,甚至结果变成科学计数法。我做财务类报表时,对金额字段一律用DECIMAL(18,4)或DECIMAL(18,2),不用FLOAT。FLOAT是浮点数,二进制表示方式在累加时会产生微小误差,最终影响对账结果。这一点在金额计算上绝对是红线级别的禁忌。
4.3 RAND:随机数的正确打开方式
RAND()生成0到1之间的随机小数:
SELECT RAND(); -- 0.735465...常见需求是生成指定范围的随机整数,比如1到100之间的数,公式是:
SELECT FLOOR(RAND() * 100) + 1;但有一个细节经常被忽略:如果没有指定种子,RAND()每次调用都可能返回不同的值;如果在同一批查询里多次调用,还可能产生重复值。在高并发场景、或者需要生成唯一随机码时,不要指望RAND,更可靠的方式是用NEWID()作为随机源:
SELECT ABS(CHECKSUM(NEWID())) % 100 + 1;NEWID()生成GUID,每次互不相同,CHECKSUM把它映射成一个整数,再取模得到范围内的数。这个方法在测试数据填充、随机抽样场景里很常用。
不过无论是RAND还是NEWID方案,都不能单独用来生成加密级随机数或者业务主键随机码,那需要更严谨的机制。这类任务建议放到应用层处理,而不是在SQL里硬啃。
5. 聚合函数与分组统计实战
5.1 五大基础聚合:SUM、AVG、COUNT、MIN、MAX
聚合函数是SQL查询里统计分析的支柱。最基础的是这五个:
- SUM() 求和,只能作用于数值类型
- AVG() 求平均值,同样只能用于数值类型
- COUNT() 统计行数,COUNT(*)统计所有行,COUNT(列名)统计该列非NULL的行数
- MIN()、MAX() 求最小值和最大值,可用于数值、日期、字符串
一句话提醒:COUNT(列名)和COUNT()是两回事。COUNT()包含NULL行的数量,COUNT(列名)只计数非NULL值。如果你的列允许NULL,统计结果可能显著不同。比如统计订单表里“有多少用户填写了备注”,用COUNT(备注)是对的,用COUNT(*)就会把没写的也算进去。
AVG也一样,它在计算时会排除NULL值。如果你业务上希望把NULL当作0参与平均,需要提前处理:
SELECT AVG(ISNULL(Score, 0)) FROM StudentScores;这个需求在做绩效统计时经常遇到。但要注意,处理方式会影响结果的业务含义,得先和需求方确认清楚,是“有成绩的人的平均分”,还是“所有人的平均分(没成绩算0)”。
5.2 GROUP BY与HAVING:分组过滤的逻辑
聚合函数单独用比较简单,一旦配合GROUP BY,“维度+指标”的报表结构就出来了:
SELECT 城市, COUNT(*) AS 客户数, SUM(消费金额) AS 总消费额 FROM 客户表 GROUP BY 城市;GROUP BY之后,查询结果里只能出现分组列和聚合函数结果,其他列都不能直接select出来。这是新手最常见的报错来源:“列'xxx'在select列表中无效,因为它既不包含在聚合函数中,也不包含在GROUP BY子句中”。遇到这个报错,先检查select列表里的非聚合列有没有都放进GROUP BY。
HAVING是专门针对分组后的条件过滤用的,它跟WHERE最大的区别是:WHERE在分组之前执行,不能用聚合函数;HAVING在分组之后执行,可以用聚合函数:
SELECT 城市, SUM(消费金额) AS 总消费额 FROM 客户表 GROUP BY 城市 HAVING SUM(消费金额) > 10000;想过滤“总消费额大于1万的城市”,你绝对不能用WHERE SUM(消费金额) > 10000,那会直接报错。逻辑顺序是:先把数据按城市分组,计算完聚合结果之后再做HAVING条件筛选。
5.3 高级分组:GROUPING SETS、ROLLUP、CUBE
基础分组之外,SQL Server还提供了一些“高级分组”的扩展能力,做报表时非常好用。
ROLLUP生成小计和总计:
SELECT 年份, 季度, SUM(销售额) FROM 销售表 GROUP BY ROLLUP(年份, 季度);得到的每一组层级组合都会有对应的汇总行,最后还有一行全表总计。做分层报表时特别实用。
CUBE则按所选列的所有可能组合都生成汇总行,输出的组合数会指数级增长,适合多维度交叉统计,但要小心结果集膨胀。数据量一大,CUBE的输出行数可能会远超预期。
GROUPING SETS最灵活,可以精确指定需要哪些维度的汇总:
SELECT 年份, 季度, SUM(销售额) FROM 销售表 GROUP BY GROUPING SETS ((年份, 季度), (年份), ());最后一个空括号()表示总计行。这三者的区别我在实际项目里这样总结:ROLLUP适合有明显层级关系的维度,比如年-月-日;CUBE适合平级多维度交叉;GROUPING SETS适合你已经知道要哪些组合、不想多算的情况。数据量大时,用GROUPING SETS还能省下不少计算资源。
5.4 窗口函数:聚合的进阶形态
除了GROUP BY,SQL Server从2012年起支持完整的窗口函数体系,也就是OVER()。它能在不减少行数的情况下计算聚合值,非常适合做累计、排名、移动平均:
SELECT 姓名, 销售额, SUM(销售额) OVER (ORDER BY 月份) AS 累计销售额 FROM 销售表;再比如ROW_NUMBER()做分组排名:
SELECT 姓名, 部门, 销售额, ROW_NUMBER() OVER (PARTITION BY 部门 ORDER BY 销售额 DESC) AS 排名 FROM 销售表;这个写法就是经典的“按部门内销售额排名”。窗口函数是聚合函数的天生搭档,强烈建议所有做报表的人都熟练掌握。它和GROUP BY不是替代关系,而是互补关系:GROUP BY压缩行数,OVER保留行数同时附加聚合信息。两者各有适用场景,用对地方效率才会高。
6. 常见问题与排查技巧实录
6.1 日期转换失败的典型场景
遇到“从字符串转换日期和/或时间字符串时,转换失败”这个报错,先不要慌。90%的情况是字符串里的日期格式跟当前会话的日期格式不匹配。
默认语言是英语(us_english)时,SQL Server对'01/05/2025'的理解可能和你的预期完全相反。最稳妥的解决方案是用无歧义的格式:'20250105'或者'2025-01-05T00:00:00'。这种格式不受会话语言的影响,谁解析结果都一样。
另外,日期字符串里藏了看不见的字符也很常见。数据从Excel复制过来,可能有全角空格、不可见字符。排查时先把字符串的长度和ASCII值打出来看看:
SELECT LEN(日期字符串), ASCII(SUBSTRING(日期字符串, 1, 1)), ASCII(SUBSTRING(日期字符串, 2, 1)) FROM 问题表;ASCII值一旦不符合预期,你就知道里面混进了什么样的特殊字符。这种问题肉眼根本看不出来,必须用函数拆解。
6.2 字符串函数使用时的常见坑
字符串的坑集中在这三处:长度单位、NULL传播、隐式类型转换。
长度单位的问题前面说过,LEN返回字符数,DATALENGTH返回字节数,处理中文时尤其明显。一个中文字符在UTF-8编码下占3个字节,在NVARCHAR下占2个字节,混在一起统计必然出错。
NULL传播的问题就是加号拼接时遇到NULL就整体变NULL,解决办法是ISNULL包裹或者改用CONCAT。
隐式类型转换的坑则体现在性能上。当你在WHERE子句里对一个索引列做函数处理时,比如WHERE LEFT(Code, 3) = 'ABC',SQL Server就没法正常走索引了,因为索引键被函数改变了。正确的写法是用Code LIKE 'ABC%',这样既能利用索引,语义上也完全一样。这是一个性能差异可能在百倍级别的细节,值得记一辈子。
6.3 聚合结果对不上的排查思路
如果你发现GROUP BY统计的结果跟业务口径对不上,先检查三件事:NULL是否被正确处理、是否有重复数据、维度组合是否有遗漏。
比如COUNT(列名)遇到的NULL不算数、SUM遇到NULL自动跳过,但平均值却可能因为NULL数量而变化。重复数据可以通过COUNT(*)对比COUNT(DISTINCT 主键)来快速发现。维度遗漏则往往出现在多表JOIN时,内连接把没有匹配上的数据直接过滤掉了,统计结果自然和业务预期差一大截。
再一个高频场景:查询用了DISTINCT或TOP,又同时用了GROUP BY,统计口径会变得非常绕。遇到“数量多了/少了”的情况,建议把语句拆开,分步验证每一步的行数变化,缩小问题范围。这是排查SQL问题的基本方法论,比盯着整条语句空想高效得多。
6.4 性能相关的细节提醒
最后说几个性能细节。
聚合统计时,尽量不要对索引列套函数再分组。前面提到的LIKE替代LEFT就是典型例子。同理,DATEADD和DATEDIFF在条件里使用也可能阻断索引,能改写成范围条件就尽量改写。
字段类型的选择也影响性能。纯中文或字母数字内容,如果不需要存Unicode特殊字符,VARCHAR比NVARCHAR占用空间更小,读写性能更好。但如果系统未来可能扩展到多语言场景,还是建议用NVARCHAR,避免后期编码转换踩坑。
还有一点很实用:字符串拼接场景下的批量更新,要避免在循环里逐行UPDATE。能一次性批量完成的,尽量使用临时表加JOIN的方式,或者用CASE WHEN批量赋值。循环逐行更新在大数据量下慢到怀疑人生。我亲眼见过同事用WHILE循环更新30万行,跑了将近一小时;改成基于JOIN的整批更新后,十几秒就完成了。这个对比就是“不懂性能细节”和“懂性能细节”之间的真实差距。
写到这里,四大类函数基本都过了一遍。我个人在实际操作中的体会是:函数这东西,背是背不完的,也不需要背完。真正重要的是建立“遇到问题知道该查哪一类函数”的敏感度,然后把最常用的那20个练到肌肉记忆,剩下的用到时再查完全来得及。我自己的一个小习惯是,每过一段时间就把近期写的SQL翻出来复盘一遍,看看哪些地方还能用更好的函数替代,比如把FOR XML PATH换成STRING_AGG、把逐行循环改成窗口函数。这个习惯坚持了几年,写查询的速度和代码的可读性都有了明显提升。希望这份整理也能成为你日常写SQL时的参考手册,踩过的坑别再踩,写出来的代码一次跑通。