☰
SQL Server函数大全精析:从能跑到跑对,避开日期、字符串与窗口函数常见坑
2026/10/9 15:19:11 网站建设 项目流程

简介:这份SQL Server函数大全精析文档面向数据库开发、运维人员及SQL初学者,系统梳理T-SQL函数的分类与使用要点,帮助读者解决数据计算、类型转换、聚合统计与日期处理等常见问题。资源包内含1个doc文档,约859KB,内容涵盖聚合、配置、转换、加密、游标、日期时间、数学、元数据、排名、行集、安全、字符串、系统、系统统计、文本图像等十五类函数,并重点讲解确定性函数与非确定性函数的区别,如AVG、CAST、CONVERT、DATEADD、DATEDIFF、ASCII、CHAR、SUBSTRING属于确定性函数,而GETDATE、@@ERROR、@@SERVICENAME、CURSORSTATUS、RAND属于非确定性函数。文档还通过DECLARE、SET与SELECT赋值示例,演示用户变量在函数中的输入输出用法,如SQRT(@MyNumber)返回12。目前已有1190人学习,适合需要系统掌握SQL Server函数体系、优化查询与编写高效数据库代码的读者参考。

1. sql server函数大全(精析):从“能跑”到“跑对”的分水岭

很多人第一次接触 sql server函数大全(精析),是在一段能跑但结果诡异的 SQL 里:明明写了ISNULL,报表还是出现空白;明明用了GETDATE(),跨时区汇总就错了一天。函数本身没有玄学,问题出在“用哪个、传什么、返回什么类型”这三件事上。这篇笔记不打算把几百个函数罗列一遍,而是按真实落地路径拆开:先讲清楚函数分类和选型逻辑,再给出可直接抄的写法、参数含义和排错方法,最后落到几个只有踩过坑才知道的细节。适合已经会写基本 SELECT、但被日期、字符串、聚合和窗口函数反复折磨的从业者,也适合想系统梳理 T-SQL 函数边界的熟手。

2. 先分清函数家族:标量、聚合、窗口到底怎么选

2.1 三类函数的执行位置和返回形态

在 SQL Server 里,函数按调用方式大致分三类。标量函数对每一行输入返回一个值,比如UPPER、DATEADD、ISNULL,它们出现在 SELECT 列表或 WHERE 条件里,逐行计算。聚合函数把多行压成一行,比如SUM、COUNT、AVG,通常配合 GROUP BY 使用。窗口函数则是在保留明细行的同时做分组计算,比如ROW_NUMBER()、RANK()、SUM() OVER(),它不压缩行数,而是给每行附加一个计算结果。

选型的第一原则是:先问“我要不要保留明细”。要保留明细又要分组排名或累计,就用窗口函数;不要明细只要汇总,就用聚合函数;单行转换或格式化,用标量函数。很多性能问题不是函数写错,而是把窗口函数当聚合用,或者把聚合函数塞进 WHERE 里导致逻辑错误。

第二原则是看数据量。标量函数在 SELECT 列表里逐行调用,数据量大时 CPU 开销明显;聚合和窗口函数在引擎内部有优化,但窗口函数如果分区字段没索引,排序开销会很大。常见做法是:能在子查询或 CTE 里先过滤再调用函数,就不要在最终结果集上对全量数据逐行处理。

2.2 一个最小可复现的对比环境

下面这段脚本建一张模拟订单表,插入少量数据,用来对比三类函数的输出差异。字段包括订单号、客户、金额、下单时间,足够覆盖后面大部分示例。

-- 建一张模拟订单表,用于函数对比 CREATE TABLE dbo.DemoOrders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, Customer NVARCHAR(50) NOT NULL, Amount DECIMAL(10,2) NOT NULL, OrderTime DATETIME2(0) NOT NULL ); -- 插入几条跨客户、跨日期的数据 INSERT INTO dbo.DemoOrders (Customer, Amount, OrderTime) VALUES (N'客户A', 120.00, '2024-03-01T09:15:00'), (N'客户A', 80.50, '2024-03-01T18:40:00'), (N'客户B', 200.00, '2024-03-02T10:05:00'), (N'客户B', 50.00, '2024-03-03T14:30:00'), (N'客户C', 300.00, '2024-03-03T20:00:00');

逻辑说明:IDENTITY(1,1)让 OrderId 自增,省去手工维护主键;DECIMAL(10,2)保证金额精度,避免 FLOAT 带来的舍入误差;DATETIME2(0)精确到秒,减少毫秒对日期截断的干扰。参数上,NVARCHAR用于客户名,防止中文乱码;时间统一用 ISO 8601 格式写入,避免受会话语言设置影响。

有了这张表,后面所有示例都可以直接跑。注意每次重跑前先DROP TABLE IF EXISTS dbo.DemoOrders;,否则重复建表会报错。

2.3 聚合与窗口的典型写法对照

先看聚合:按客户汇总金额。

-- 聚合:每个客户一行汇总 SELECT Customer, SUM(Amount) AS TotalAmount, COUNT(*) AS OrderCount FROM dbo.DemoOrders GROUP BY Customer;

再看窗口:保留每笔订单明细,同时给出该客户的累计金额和排名。

-- 窗口:保留明细,附加分组累计与排名 SELECT OrderId, Customer, Amount, OrderTime, SUM(Amount) OVER (PARTITION BY Customer ORDER BY OrderTime ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RunningTotal, ROW_NUMBER() OVER (PARTITION BY Customer ORDER BY Amount DESC) AS AmountRank FROM dbo.DemoOrders;

逻辑说明:PARTITION BY Customer按客户分组,ORDER BY OrderTime决定累计顺序,ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW明确窗口范围是从分组第一行到当前行,避免默认 RANGE 在重复值上的差异。ROW_NUMBER()按金额降序给每个客户内部排名。参数上,如果省略ROWS BETWEEN,SQL Server 默认使用 RANGE,遇到相同排序值时累计结果会和预期不同,这是窗口函数最常见的翻车点之一。

3. 字符串与日期函数:高频场景的写法与参数边界

3.1 字符串拼接、截取与空值处理

字符串函数里最容易出问题的是 NULL 传播。+拼接遇到 NULL,整个结果变 NULL;CONCAT则把 NULL 当空字符串处理。下面这段对比两种写法。

-- 演示 NULL 在拼接中的不同表现 DECLARE @a NVARCHAR(10) = N'订单'; DECLARE @b NVARCHAR(10) = NULL; SELECT @a + @b AS PlusResult, -- 结果为 NULL CONCAT(@a, @b) AS ConcatResult, -- 结果为 N'订单' ISNULL(@b, N'') AS IsnullResult;-- 结果为 N''

逻辑说明:+是严格拼接,任何操作数为 NULL 结果就是 NULL;CONCAT内部把 NULL 转成空串,适合展示层拼接;ISNULL是显式替换,适合需要控制默认值的场景。参数上,CONCAT接受最多 254 个参数,类型会自动转成字符串,但隐式转换可能带来性能损耗,大字段拼接时要注意。

截取方面,SUBSTRING(expression, start, length)的 start 从 1 开始,不是 0。如果 start 超过字符串长度,返回空串;如果 length 为负数会报错。常见做法是先用LEN判断长度再截取,避免越界。

-- 安全截取:先判断长度,再取前 3 个字符 SELECT Customer, CASE WHEN LEN(Customer) >= 3 THEN SUBSTRING(Customer, 1, 3) ELSE Customer END AS ShortName FROM dbo.DemoOrders;

参数说明:LEN不计算尾部空格,DATALENGTH计算字节数,两者在 NVARCHAR 上结果不同。如果业务要求包含尾部空格,用DATALENGTH除以 2 更准确。

3.2 日期加减、截断与格式转换

日期函数的核心是“精度”和“边界”。DATEADD(day, 1, @d)加一天,DATEDIFF(day, @a, @b)算天数差,EOMONTH(@d)取月末。下面这段演示按天截断和按月汇总。

-- 日期截断与按月汇总 SELECT OrderId, OrderTime, CAST(OrderTime AS DATE) AS OrderDate, -- 截断到天 DATEADD(day, 1, CAST(OrderTime AS DATE)) AS NextDay, EOMONTH(OrderTime) AS MonthEnd FROM dbo.DemoOrders; -- 按月汇总金额 SELECT DATEFROMPARTS(YEAR(OrderTime), MONTH(OrderTime), 1) AS MonthStart, SUM(Amount) AS MonthAmount FROM dbo.DemoOrders GROUP BY DATEFROMPARTS(YEAR(OrderTime), MONTH(OrderTime), 1);

逻辑说明:CAST(... AS DATE)是最直接的截断方式,比CONVERT加样式码更易读;EOMONTH返回当月最后一天,参数可选偏移月数;DATEFROMPARTS用年、月、日拼出日期,适合做分组键。参数上,DATEDIFF的边界是“跨过多少个边界”,比如DATEDIFF(year, '2023-12-31', '2024-01-01')返回 1,不是 0,这个反直觉结果在算年龄、账期时经常导致 off-by-one。

格式转换用FORMAT最灵活,但它依赖 CLR,性能较差,大结果集上慎用。常见做法是展示层用FORMAT,计算层用CONVERT加样式码。

-- 两种格式化方式对比 SELECT OrderTime, FORMAT(OrderTime, 'yyyy-MM-dd HH:mm') AS Formatted, CONVERT(VARCHAR(16), OrderTime, 120) AS Converted FROM dbo.DemoOrders;

参数说明:样式码 120 对应yyyy-mm-dd hh:mi:ss,截取前 16 位得到分钟精度。FORMAT的格式串区分大小写,MM是月,mm是分钟,写错会得到完全不同的结果。

3.3 类型转换与隐式转换的坑

CAST和CONVERT是显式转换,TRY_CAST和TRY_CONVERT在失败时返回 NULL 而不报错。下面演示安全转换。

-- 安全转换:失败返回 NULL,不中断查询 SELECT TRY_CAST('2024-03-01' AS DATE) AS OkDate, TRY_CAST('not-a-date' AS DATE) AS BadDate, TRY_CONVERT(INT, '123abc') AS BadInt;

逻辑说明:TRY_CAST适合清洗外部导入的数据,避免一条脏数据导致整个查询失败。参数上,TRY_CONVERT多一个样式码参数,用法与CONVERT一致。注意TRY_CAST返回 NULL 后,后续计算仍可能因 NULL 传播出问题,需要配合ISNULL或COALESCE兜底。

隐式转换更隐蔽:字符串和数字比较时,SQL Server 按数据类型优先级把字符串转成数字,如果字符串不是合法数字就报错。常见做法是显式转换,别依赖引擎猜。

4. 聚合、窗口与排名函数:分组逻辑和性能边界

4.1 GROUP BY 与 HAVING 的执行顺序

聚合查询里,WHERE 在分组前过滤,HAVING 在分组后过滤。下面这段先过滤再分组,最后用 HAVING 筛掉小额客户。

-- WHERE 先过滤,HAVING 后过滤 SELECT Customer, SUM(Amount) AS TotalAmount FROM dbo.DemoOrders WHERE OrderTime >= '2024-03-01' GROUP BY Customer HAVING SUM(Amount) > 100;

逻辑说明:WHERE 条件作用在原始行上,能减少参与分组的行数;HAVING 作用在聚合结果上,不能用未分组的列。参数上,HAVING 里可以引用聚合函数,也可以引用分组列,但不能引用 SELECT 别名,因为别名在 HAVING 阶段还未生效。

4.2 窗口函数的 PARTITION、ORDER 与 FRAME

窗口函数的三要素是分区、排序和帧。下面用SUM() OVER()演示不同帧对结果的影响。

-- 不同帧的累计效果 SELECT OrderId, Customer, Amount, SUM(Amount) OVER (PARTITION BY Customer ORDER BY OrderId) AS DefaultFrame, SUM(Amount) OVER (PARTITION BY Customer ORDER BY OrderId ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS RowFrame, SUM(Amount) OVER (PARTITION BY Customer) AS WholePartition FROM dbo.DemoOrders;

逻辑说明:DefaultFrame在 ORDER BY 存在时默认是 RANGE UNBOUNDED PRECEDING AND CURRENT ROW,遇到相同 OrderId 会合并计算;RowFrame明确按行累计,结果更符合直觉;WholePartition不带 ORDER BY,对整个分区求和。参数上,ROWS和RANGE的区别在于对重复排序值的处理,做累计时建议显式写ROWS。

4.3 排名函数的选择:ROW_NUMBER、RANK、DENSE_RANK

三个排名函数在重复值上的表现不同。下面用金额排名演示。

-- 三种排名函数对比 SELECT OrderId, Amount, ROW_NUMBER() OVER (ORDER BY Amount DESC) AS RowNum, RANK() OVER (ORDER BY Amount DESC) AS RankNum, DENSE_RANK() OVER (ORDER BY Amount DESC) AS DenseNum FROM dbo.DemoOrders;

逻辑说明:ROW_NUMBER永远连续不重复;RANK遇到相同值跳号,比如 1,1,3;DENSE_RANK遇到相同值不跳号,比如 1,1,2。参数上,三者都支持 PARTITION BY,做分组内排名时记得加上分区列,否则排名会跨组计算。

性能方面,窗口函数需要排序,如果 ORDER BY 的列上有索引,排序开销会降低。常见做法是把窗口函数放在 CTE 里,外层再过滤,避免在窗口计算后再做大量筛选。

5. 避坑与排查:函数用错时的五个典型现场

5.1 现象:ISNULL 替换后结果仍为 NULL

原因:ISNULL只替换第一个参数为 NULL 的情况,如果表达式本身返回 NULL 且类型不匹配,替换可能不生效。更常见的是在聚合里用ISNULL(SUM(x), 0),但 SUM 返回 NULL 是因为没有行,而不是行内值为 NULL。

解决:先确认 NULL 来源。用COUNT判断是否有行,再决定是否替换。多列兜底用COALESCE,它支持多个参数,按顺序返回第一个非 NULL 值。

-- 区分“无行”和“行内 NULL” SELECT Customer, ISNULL(SUM(Amount), 0) AS TotalAmount, COUNT(Amount) AS NonNullCount, COUNT(*) AS AllCount FROM dbo.DemoOrders GROUP BY Customer;

5.2 现象:DATEDIFF 算出的天数差总是多一天

原因:DATEDIFF计算的是跨过的边界数,不是精确时间差。比如DATEDIFF(day, '2024-03-01 23:59', '2024-03-02 00:01')返回 1,尽管只差两分钟。

解决:需要精确时间差时用DATEDIFF_BIG配合更小单位,或者直接做日期减法。算账期、年龄时明确业务口径,是“跨天”还是“满 24 小时”。

5.3 现象:窗口函数累计结果和预期不一致

原因:省略了ROWS BETWEEN,默认 RANGE 在重复排序值上会合并计算。

解决:累计场景显式写ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。如果排序值可能重复,再加一个唯一列做次级排序,比如ORDER BY OrderTime, OrderId。

5.4 现象:FORMAT 在大结果集上查询变慢

原因:FORMAT依赖 CLR,逐行调用开销大,且无法有效利用索引。

解决:计算层用CONVERT加样式码,展示层再格式化。如果必须用FORMAT,先过滤再格式化,别对全表用。

5.5 现象:TRY_CAST 返回 NULL 后后续计算全变 NULL

原因:NULL 在算术和拼接中会传播,一个 NULL 能让整行结果失效。

解决:在TRY_CAST外层包ISNULL或COALESCE,给默认值。清洗数据时先统计 NULL 比例,再决定默认值是否合理。

6. 进阶技巧:用 APPLY 和 CTE 把函数调用收敛到可控范围

6.1 用 CROSS APPLY 做逐行函数调用

当需要对每行调用一个表值函数或子查询时,CROSS APPLY比标量子查询更清晰,也更容易优化。下面演示对每个客户取金额最高的订单。

-- CROSS APPLY:每个客户取金额最高的一笔 SELECT c.Customer, o.OrderId, o.Amount, o.OrderTime FROM (SELECT DISTINCT Customer FROM dbo.DemoOrders) AS c CROSS APPLY ( SELECT TOP (1) OrderId, Amount, OrderTime FROM dbo.DemoOrders AS d WHERE d.Customer = c.Customer ORDER BY d.Amount DESC, d.OrderId ) AS o;

逻辑说明:外层取客户列表,CROSS APPLY对每个客户执行一次子查询,取金额最高的一笔。参数上,TOP (1)配合ORDER BY保证确定性,加OrderId做次级排序避免金额相同时结果不稳定。相比窗口函数,CROSS APPLY在客户数少、每客户数据多时更高效,因为可以利用客户列上的索引做查找。

6.2 用 CTE 分步收敛函数调用

复杂查询里,把函数调用拆到 CTE 里,每步只做一件事,既好读也好排查。

-- CTE 分步:先算每日汇总,再算累计 WITH DailySum AS ( SELECT CAST(OrderTime AS DATE) AS OrderDate, SUM(Amount) AS DayAmount FROM dbo.DemoOrders GROUP BY CAST(OrderTime AS DATE) ), RunningSum AS ( SELECT OrderDate, DayAmount, SUM(DayAmount) OVER (ORDER BY OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS TotalSoFar FROM DailySum ) SELECT OrderDate, DayAmount, TotalSoFar FROM RunningSum ORDER BY OrderDate;

逻辑说明:第一个 CTE 按天汇总,第二个 CTE 在汇总结果上做累计。这样窗口函数的输入行数从明细行降到天数行,排序开销大幅减少。参数上,CTE 不物化,每次引用会重新计算,如果被多次引用,考虑用临时表落地。

6.3 验证函数结果的一个习惯

我一般会在开发时加一段校验查询,把函数结果和手工计算对比。比如验证累计金额是否等于总和。

-- 校验:累计最后一行应等于总和 WITH DailySum AS ( SELECT CAST(OrderTime AS DATE) AS OrderDate, SUM(Amount) AS DayAmount FROM dbo.DemoOrders GROUP BY CAST(OrderTime AS DATE) ), RunningSum AS ( SELECT OrderDate, DayAmount, SUM(DayAmount) OVER (ORDER BY OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS TotalSoFar FROM DailySum ) SELECT (SELECT SUM(Amount) FROM dbo.DemoOrders) AS ManualTotal, (SELECT TOP (1) TotalSoFar FROM RunningSum ORDER BY OrderDate DESC) AS WindowTotal;

两个值一致才说明窗口逻辑正确。这个习惯帮我省掉了很多后悔药:函数本身不会骗人,骗人的是对边界和精度的假设。把校验写进脚本,比事后拍脑袋靠谱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询