多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SQL Server函数大全精析:从能跑到跑对,避开日期、字符串与窗口函数常见坑

SQL Server函数大全精析:从能跑到跑对,避开日期、字符串与窗口函数常见坑 简介这份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 BETWEENSQL Server 默认使用 RANGE遇到相同排序值时累计结果会和预期不同这是窗口函数最常见的翻车点之一。3. 字符串与日期函数高频场景的写法与参数边界3.1 字符串拼接、截取与空值处理字符串函数里最容易出问题的是 NULL 传播。拼接遇到 NULL整个结果变 NULLCONCAT则把 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 结果就是 NULLCONCAT内部把 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,3DENSE_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;两个值一致才说明窗口逻辑正确。这个习惯帮我省掉了很多后悔药函数本身不会骗人骗人的是对边界和精度的假设。把校验写进脚本比事后拍脑袋靠谱。希望帮到你。本文还有配套的精品资源点击获取
返回列表