简介:SQL Server数据库开发与维护中,函数是完成数据计算、转换与聚合的核心工具,这份精析文档系统讲解了聚合、配置、转换、加密、游标、日期时间、数学、元数据、排名、行集、安全、字符串、系统、系统统计、文本图像等十五大类函数的用途与调用方式。同时,它对确定性函数与非确定性函数给出了明确区分,结合AVG、CAST、GETDATE、RAND等典型示例,并通过DECLARE、SET与SELECT语句演示用户变量在函数中的输入输出用法,帮助读者规避在视图和存储过程中误用非确定性函数的风险。文档为doc格式,共1个文件,压缩包约859KB,文字内容紧凑,适合直接阅读或摘录到开发笔记中。目前已有1190人学习下载,对正在梳理SQL Server函数体系或准备面试的开发人员来说,具有较高的参考价值。透过实例与分类结合的方式,读者可以快速定位所需函数,理解其参数、返回类型与确定性特性,并在实际查询、存储过程和报表开发中更准确地选用函数。
1. SQL Server 函数大全到底在解决什么问题:从一次慢查询说起
SQL Server 函数大全(精析)这个标题,我第一反应不是工具书,而是一段线上血泪排查:当时一个订单查询突然从 100 毫秒涨到 5 秒,查看执行计划发现条件列被写成了SUBSTRING(OrderNo, 1, 2) = 'AB',函数作用在索引列上,非聚集索引无法 Seek,只能全表扫描。生产环境几十万行一扫描,延迟就上来了。这个事故让我意识到:函数大全的意义,不是把函数名背下来,而是知道每个函数能干什么、返回什么类型、在 WHERE 里用会不会让索引废掉。
下面这套拆解适合每天写存储过程、跑报表、排查慢 SQL 的工程师,也适合刚接触 SQL Server 的人照着练一遍。我会把常用函数按字符串、日期时间、聚合窗口、系统与类型转换四组来精析,每一组都会给可复制的最小代码,并解释参数含义和容易踩的坑。想彻底搞懂 SQL Server 内置函数,不是靠收藏那份几百行的 SQL 脚本,而是靠理解函数在真实查询里的行为边界。
2. 字符串函数精析:截取、拼接、拆分时最容易翻车的几个细节
字符串函数是 SQL Server 日常使用频率最高的一类,也是最容易出现“结果对,速度慢”的一类。很多坑都集中在返回类型、NULL 处理、以及函数是否会导致索引失效上。我们先从基础列表看起,再进入行转列这个经典场景,最后说说为什么同一个功能换一种写法会有完全不同执行计划。
2.1 常用字符串函数的功能边界与返回值类型
先放一份可以抄作业的示例脚本,把最常用的八个函数放在同一个 SELECT 里跑:
DECLARE @str NVARCHAR(50) = N' SQL Server '; SELECT LEN(@str) AS len_chars, -- 返回字符数,不统计尾随空格 DATALENGTH(@str) AS data_bytes, -- 返回字节数,NVARCHAR 每字符占 2 字节 CHARINDEX(N'Server', @str) AS char_pos, -- 找到子串起始位,找不到返回 0 SUBSTRING(@str, 2, 3) AS sub_part, -- 从第 2 个字符开始,取 3 个字符 LEFT(@str, 3) AS left_part, -- 取左边 3 个字符 RIGHT(@str, 3) AS right_part, -- 取右边 3 个字符 REPLACE(@str, N' ', N'') AS no_space, -- 去掉所有空格 STUFF(@str, 1, 1, N'【') AS stuff_rep; -- 第 1 位开始删 1 字符,再插入【这条语句里需要注意三件事。第一,LEN返回的是字符数而不是字节数,而且它会忽略尾随空格;DATALENGTH返回字节数,对 NVARCHAR 每字符 2 字节,对 VARCHAR 每字符 1 字节。如果你要按字节截断字段,必须用DATALENGTH而不是LEN。第二,CHARINDEX的搜索是受排序规则影响的,默认大小写不敏感时,'server' 也能匹配到 'Server';如果要用二进制精确匹配,需要加COLLATE。第三,这些字符串函数遇到NULL时,除了少数专门处理 NULL 的函数,其他都会返回NULL,所以在拼接之前要用ISNULL兜底。
2.2 用 STUFF + FOR XML PATH 做行转列:一句 SQL 背后的参数陷阱
行转列大概是字符串场景里最常用的需求:把多行课程名拼成一列,用顿号分隔。常见做法是STUFF + FOR XML PATH,但不少人第一次写都会输出一堆乱码或转义字符。看下面的写法:
SELECT s.StudentID, STUFF( (SELECT N'、' + c.CourseName FROM Score sc JOIN Course c ON sc.CourseID = c.CourseID WHERE sc.StudentID = s.StudentID ORDER BY c.CourseID FOR XML PATH('')), 1, 1, '' ) AS CourseList FROM Student s;FOR XML PATH('')会让子查询产生的行序列拼成一个没有根节点的 XML 字符串;外层 SELECT 每行执行一次。拼接时故意在每个课程名前加顿号,然后用STUFF从位置 1 开始删掉 1 个字符,也就是删掉最前面的顿号,再替换成空串。三个参数很好记:STUFF(原表达式, 删除起始位置, 删除长度, 替换表达式)。
但这里有一个很容易翻车的点:FOR XML PATH会自动对结果做 XML 转义,如果课程名里包含&、<、>,你会发现拼出来的字符串变成&等。我一般会在内层 SELECT 里用TYPE转成 XML 类型后再取值,或者干脆用更现代的STRING_AGG,前提是你的数据库版本支持。此外,如果CourseName本身是NULL,N'、' + NULL的结果会是NULL,这一行就会消失,需要提前用ISNULL(CourseName, N'')处理。
2.3 字符串函数与索引失效:LEFT、LIKE、SUBSTRING 的差别
在 WHERE 条件里使用字符串函数,最直接影响的是执行计划能否从 Seek 变成 Scan。下面三种写法,只有一种能合理利用索引:
-- 不推荐:函数包住列,索引无法 Seek SELECT * FROM Orders WHERE SUBSTRING(OrderNo, 1, 2) = 'AB'; SELECT * FROM Orders WHERE LEFT(OrderNo, 2) = 'AB'; -- 推荐:LIKE 前缀匹配,能转换成范围查找 SELECT * FROM Orders WHERE OrderNo LIKE 'AB%';原因在于 SQL Server 的索引是以整个列值为排序键的,对列做SUBSTRING或LEFT后,本来有序的值被打散,优化器只能扫描。LIKE 'AB%'之所以有机会走索引,是因为它本质上可以改写成>= 'AB' AND < 'AC',成为一个范围条件,B 树索引可以定位到起点再顺序读取。这个规则不仅适用于字符串,也适用于日期、数值:凡是把函数放在列这一侧的,都要高度警惕。
知道这个原理后,遇到“必须按某一段字符串查”的需求,常见做法有三种:一是把这段值抽成单独一列,比如加一个计算列 RTRIM(OrderNo 前两位),并在计算列上建索引;二是如果数据量小且并发低,接受扫描;三是改业务把前缀作为独立编码字段。不要把希望寄托在写一个更花哨的函数上,函数放在列上就是玄学——你看着返回了正确结果,实际执行计划可能是全表扫描。
3. 日期与时间函数精析:时区、精度和默认值的三个坑
日期函数是报表 SQL 的刚需,但“结果对不对”和“查询快不快”往往是两码事。这一章先弄清几个返回时间点的函数差异,再处理月首月末和年龄计算的边界,最后讨论日期类型与隐式转换的经典问题。
3.1 GETDATE、SYSDATETIME、GETUTCDATE 的精度与用途
先看一组返回当前日期时间的内置函数:
SELECT GETDATE() AS getdate_val, -- 精度约 3.33 毫秒 SYSDATETIME() AS sysdatetime_val, -- 精度 100 纳秒 GETUTCDATE() AS getutcdate_val, -- UTC 时间,精度同 GETDATE SYSUTCDATETIME() AS sysutcdatetime, -- UTC 时间,高精度 CURRENT_TIMESTAMP AS current_ts, -- ANSI 标准,等价 GETDATE DATEPART(TZOFFSET, SYSDATETIMEOFFSET()) AS tz_offset; -- 当前会话时区偏移分钟数如果你的业务只关心“当前时间”,GETDATE就够了;如果需要在同一毫秒内记录多条日志的先后顺序,就要用SYSDATETIME或SYSDATETIMEOFFSET。需要明确的是,SQL Server 实例本身没有可配置的时区概念,它依赖操作系统时区;如果你要存储跨时区的业务时间,常见做法是统一用 UTC 写入,展示层再做换算,而不要依赖GETDATE。DATEPART(TZOFFSET, ...)可以读出当前时区偏移,配合AT TIME ZONE新语法可以实现时区换算,这是后续功能的基础。
3.2 DATEADD/DATEDIFF 的边界语义:第一天、最后一天、月初月末怎么算
日期计算最常用的两个函数是DATEADD和DATEDIFF,但它们的边界语义很容易混。下面这段脚本给出当月首日、当月末日、今年首日等标准算法:
DECLARE @today DATETIME2 = SYSDATETIME(); SELECT DATEADD(day, DATEDIFF(day, 0, @today), 0) AS today_start, -- 当天零点 DATEADD(month, DATEDIFF(month, 0, @today), 0) AS month_start, -- 当月1日零点 EOMONTH(@today) AS month_end, -- 当月最后一天 DATEADD(year, DATEDIFF(year, 0, @today), 0) AS year_start, -- 今年1月1日零点 DATEDIFF(year, '2000-01-01', @today) AS years_since_2000; -- 只看年份差这里的核心技巧是“用DATEDIFF数出从基准日 0 到当前日期之间跨越了多少个边界,再用DATEADD把边界加回去”。比如DATEDIFF(month, 0, @today)得到的是从 1900-01-01 到今天的整月数,再加回 0 日期,就精准落在当月 1 日 0 点。EOMONTH是专门计算月末的,返回类型与传入参数有关。注意DATEDIFF(year, ...)计算的是跨年次数,不是实际年龄——如果一个人出生于 2000-12-31,到 2024-01-01 时DATEDIFF(year, ...)已经是 24,但实际周岁只有 23 岁。这类业务上要按生日加减一天后再比较。
另一个容易翻车的点是DATEADD超出目标类型范围。比如DATEADD(day, 100000, GETDATE())对DATETIME会直接报溢出(datetime 最大到 2079 年),换成DATETIME2就能延伸到 9999 年。所以设计表时,记录“过期时间”这类字段尽量用DATETIME2,不要为了兼容老系统继续用DATETIME。
3.3 日期函数的隐式转换:为什么你的 DateTime 查询走了全表扫描
日期列上的隐式转换是慢查询的高发区。最常见的是参数类型不匹配,或者为了让日期格式好看而把列转成字符串比较。看两组写法:
-- 场景1:OrderDate 是 datetime,@date 是 varchar DECLARE @date VARCHAR(10) = '2024-05-01'; SELECT * FROM Orders WHERE OrderDate = @date; -- 通常还能走索引 -- 场景2:OrderDate 是 varchar,@date 是 datetime,就会在列上隐式转换 DECLARE @date2 DATETIME = '2024-05-01'; SELECT * FROM Orders WHERE OrderDate = @date2; -- 执行计划出现 CONVERT_IMPLICIT -- 场景3:在列上做格式转换,索引必定失效 SELECT * FROM Orders WHERE CONVERT(VARCHAR(10), OrderDate, 120) = '2024-05-01';场景 1 里,数据类型优先级决定优化器会把@date转成datetime,转换发生在参数一侧,列本身没有被加工,所以仍然能用索引。场景 2 反过来,列被隐式转成datetime,等于对每一行都执行了一次转换,索引自然失效。场景 3 是显式对列做CONVERT,同样直接摧毁 Seek。判断方法很简单:打开实际执行计划,找“CONVERT_IMPLICIT”运算,如果它在 Index Seek 之前就不是好事。
正确的写法是:先把参数声明成与列一致的类型,再用范围条件替代精确匹配。比如查某一天的数据,写成OrderDate >= '2024-05-01 00:00:00' AND OrderDate < '2024-05-02 00:00:00',而不是CONVERT(VARCHAR(10), OrderDate, 120) = '2024-05-01'。这是我在报表优化里用过最多次的修改,改完 100 万行的表,查询从 2 秒掉到几十毫秒,血泪经验。
4. 聚合与窗口函数精析:从 GROUP BY 到 OVER() 的进阶逻辑
聚合函数是分组统计的主力,窗口函数则是 SQL Server 里很值得深挖的一类函数。这章先从 GROUP BY 的语义说起,再讲 OVER() 的边界参数,最后用移动平均和同比环比的案例来对比窗口函数与自连接。
4.1 聚合函数与 GROUP BY 的关系:NULL、DISTINCT 和空表返回
COUNT、SUM、AVG、MIN、MAX看起来很简单,但很多细节直到报表出了错你才会注意到。下面这段代码一次跑出所有行为:
SELECT COUNT(*) AS total_rows, -- 包括所有行,NULL 也算 COUNT(Amount) AS has_amount, -- 统计非 NULL 的 Amount SUM(Amount) AS sum_amount, -- 忽略 NULL,空集返回 NULL AVG(Amount) AS avg_amount, -- 忽略 NULL,不是除以总行数 MIN(Amount) AS min_amount, -- NULL 不参与比较 MAX(Amount) AS max_amount, SUM(DISTINCT Amount) AS distinct_sum -- 先去重再求和 FROM Orders;这里最容易被忽视的是空表语义。如果 Orders 表一行都没有,COUNT(*)返回 0,而SUM(Amount)、AVG(Amount)返回NULL,直接拿这个值写进报表前端会显示成空,而不是 0,所以要么在 SQL 里ISNULL(SUM(Amount), 0),要么在前端做空值处理。AVG计算时只把非 NULL 的行数作为分母,比如三行数据是 1、2、NULL,平均数是 1.5 而不是 1。若要按 NULL 也算分母,得自己写SUM(Amount) / COUNT(*)。另外DISTINCT放在聚合函数里可以避免重复值干扰,但会让计算成本增加,不要在百万级明细里滥用。
在GROUP BY里还要记住一个规则:WHERE子句在聚合之前过滤,不能引用聚合结果;想过滤聚合结果要用HAVING。例如“金额大于 100 的客户”是WHERE Amount > 100,而“总金额大于 10000 的客户”是HAVING SUM(Amount) > 10000。这两者执行计划也会不同,WHERE能利用索引提前筛掉行,HAVING只能等分组完再筛。
4.2 窗口函数 OVER() 的必调参数:PARTITION BY、ORDER BY、ROWS/RANGE
窗口函数是“可以在明细行上同时看到聚合值”的工具,不会像GROUP BY那样折叠行数。OVER()里的三个部分:PARTITION BY控制分组,ORDER BY控制窗口内排序,可选的ROWS/RANGE控制窗口帧范围。下面是一个典型的排名加累计求和:
SELECT OrderID, CustomerID, Amount, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate DESC) AS rn, RANK() OVER (PARTITION BY CustomerID ORDER BY Amount DESC) AS rk, DENSE_RANK() OVER (PARTITION BY CustomerID ORDER BY Amount DESC) AS drk, SUM(Amount) OVER (PARTITION BY CustomerID ORDER BY OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_sum FROM Orders;ROW_NUMBER给出一个不间断的行号,RANK遇到相同排序值会给相同名次,但下一个名次会跳号,比如 1、1、3;DENSE_RANK不跳号,是 1、1、2。三者的选择取决于业务要“分配唯一位置”还是“并列排名”。SUM OVER加ORDER BY以后,默认的窗口帧是从分组第一行到当前行,你可以用ROWS BETWEEN显式指定。这里有个很多书不讲的细节:RANGE和ROWS的差异。如果ORDER BY列存在并列值,RANGE会把所有并列行并入同一帧,可能让“当前行”看到后面行的值;ROWS则严格按物理行号取。做精确的逐行累计时,用ROWS更可控。
4.3 用窗口函数实现移动平均和同比环比,避免自连接性能暴跌
报表里经常需要“近 N 周期均值”和“去年同期对比”。传统自连接写法要先把同一张表读两次再关联,数据量大时很难受;窗口函数只需要一次扫描就能完成。看下面这个按月销售分析的例子:
SELECT SalesMonth, Amount, AVG(Amount) OVER (ORDER BY SalesMonth ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7, LAG(Amount, 12) OVER (ORDER BY SalesMonth) AS last_year_amount, CASE WHEN LAG(Amount, 12) OVER (ORDER BY SalesMonth) = 0 THEN NULL ELSE (Amount - LAG(Amount, 12) OVER (ORDER BY SalesMonth)) * 1.0 / LAG(Amount, 12) OVER (ORDER BY SalesMonth) * 100 END AS yoy_percent FROM MonthlySales;ROWS BETWEEN 6 PRECEDING AND CURRENT ROW是“近 7 期移动平均”的标准写法,它把从当前行往前 6 行到当前行作为窗口。LAG(Amount, 12)取同分组内排序后上方第 12 行的值,正好用来算去年同月。如果写成自连接,要么用关联子查询对每个月执行一次排序,要么对两张派生表做 JOIN,代码长度翻倍,执行计划里会出现多个 Sort 和 Table Scan。窗口函数压入到同一份数据扫描里,性能通常好得多。要注意的是,OVER (ORDER BY SalesMonth)里的ORDER BY只决定窗口内排序,不会决定最终结果集的返回顺序;如果需要按月份输出,最外层还要再加一次ORDER BY。
5. 系统函数与类型转换函数精析:避坑清单与排查方案
这一章把类型转换和元数据函数放在一起,因为它们往往在开发时不起眼,上线后却制造出各种问题。这里不仅给函数用法,还给出五个实际踩坑记录,方便直接照着排查。
5.1 类型转换:CAST/CONVERT 与隐式转换的五个血泪经验
CAST和CONVERT是解决类型冲突的基础,但真正的难点不是语法,而是“什么时候做转换、在哪一侧做转换”。先看一组转换写法:
SELECT CAST('123' AS INT) AS cast_int, -- 字符串转整数 CONVERT(DATETIME2, '2024-05-01 10:30:00', 120) AS convert_dt, -- style 120 指定格式 TRY_CAST('abc' AS INT) AS try_int, -- 转换失败返回 NULL TRY_CONVERT(INT, '12.5') AS try_decimal, -- 小数点字符串不能转 INT CONVERT(VARCHAR(10), GETDATE(), 23) AS date_ymd; -- 仅取出 yyyy-mm-ddTRY_CAST/TRY_CONVERT是后悔药,转换失败返回NULL而不是抛异常,特别适合处理外部导入的脏数据。CONVERT比CAST多一个 style 参数,用于日期格式化,但 SQL Server 的 style 编号很长,记不住没关系,用到时查sys.sysconv或者官方格式表,不要凭记忆写错 style 导致结果偏移。五个血泪经验,我总结为:
- 不要让列出现在转换函数里,把转换放在参数一侧;
- 参数声明类型尽量与目标列一致,避免隐式转换优先级带来的索引失效;
- 处理不可靠字符串时,一律用
TRY_CAST/TRY_CONVERT,再用WHERE ... IS NOT NULL过滤坏行; - 比较
NVARCHAR与VARCHAR时,SQL Server 会把VARCHAR先转成NVARCHAR,可能让字符集不能匹配,以及影响索引使用,建议统一使用同一类型; - 转换结果给前端展示时,不要依赖默认格式,显式用 style。
5.2 系统元数据函数:OBJECT_NAME、COL_LENGTH、DB_NAME 等的使用场景
写通用存储过程或做数据库巡检时,系统函数能让你少查不少 DMV。下面这段代码演示几个常见的组合:
IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL PRINT 'Orders 表存在'; SELECT DB_NAME() AS current_db, OBJECT_SCHEMA_NAME(OBJECT_ID('dbo.Orders')) AS table_schema, OBJECT_NAME(OBJECT_ID('dbo.Orders')) AS table_name, COL_LENGTH('dbo.Orders', 'Amount') AS amount_col_len;OBJECT_ID('dbo.Orders', 'U')返回对象 ID,第二个参数 'U' 表示用户表,如果对象不存在返回 NULL。OBJECT_SCHEMA_NAME和OBJECT_NAME能从 ID 反查名字,经常用在日志记录里。COL_LENGTH返回列的定义长度,对VARCHAR就是最大字节数,对NVARCHAR是字节数,这个值可以用来判断是否会出现截断。我一般在动态 SQL 执行前用这些函数做防呆:如果表不存在就不再往下执行,避免拼接出错。
另一个常用的系统函数是@@ROWCOUNT,它返回上一条语句影响的行数。注意它非常脆弱,执行任何语句都会重置,所以如果你需要保存这个值,要立刻赋给局部变量。写存储过程时用IF @@ROWCOUNT > 0判断是否继续,是很成熟的套路,但一定要在紧跟 DML 之后判断,不能隔一条无关语句。
5.3 避坑清单:函数使用中常见的 5 个踩坑点
这一条清单里的场景都是真实反复出现过的,按“现象 → 原因 → 解决”整理成笔记。
现象1:LEN('ABC ')返回 3,但前端看到字符串明明有 4 个字符。原因是LEN不统计尾随空格。解决:需要保留空格时改用DATALENGTH,对VARCHAR直接得到字节数,对NVARCHAR要除以 2。
现象2:在字符串里查找子串,CHARINDEX('Server', @str)返回 0,但字符串里明明有 Server。原因是大小写敏感性由排序规则决定,默认排序规则可能不区分大小写,也可能区分;还可能是字符串带了不可见字符。解决:先用LTRIM/RTRIM,再强制COLLATE,或者用LIKE '%Server%'验证。
现象3:用DATEADD(day, 50000, GETDATE())报“超出 datetime 范围”。原因是DATETIME上限是 2079 年。解决:改用DATETIME2,或者先确认业务值域。
现象4:FOR XML PATH行转列后出现&。原因是 XML 自动转义特殊字符。解决:使用FOR XML PATH(''), TYPE,再用.value('.', 'nvarchar(max)')取结果,或者改用较新版本的STRING_AGG。
现象5:ISNUMERIC('$')返回 1,但CAST('$' AS DECIMAL(10,2))直接报错。原因是ISNUMERIC会把货币符号、小数点、正负号等都视为可识别数字。解决:用TRY_CONVERT而不是ISNUMERIC,或者先按业务做模式筛选(SQL Server 里用LIKE带数字模式)。
每一条都是运行时才痛的教训。把这些抄在自己的笔记本上,遇到类似 SQL 时先倒着排查一遍,能省不少夜班时间。
6. 把你自己的“函数大全”变成工作流:验证方法与性能排查习惯
函数大全不是一节节背出来的,而是在一个个翻车现场里攒出来的。最实用的习惯是给“不确定函数行为”一个固定的验证流程:建一张一行数据的临时表,把函数放进去,同时打开SET STATISTICS IO ON和SET STATISTICS TIME ON,跑完看执行计划里有没有 CONVERT_IMPLICIT,有没有 Scan,再决定要不要改写成参数侧表达式。
我自己的做法是维护一份个人速查表,里面记录每个函数的真实行为,而不是网上抄来的手册。比如用TRY_CONVERT而不是ISNUMERIC判断可转性;用DATALENGTH而不是LEN判断存储长度;在STUFF行转列前先查TYPE转义。遇到新的坑就加一行。这份速查表比任何大全都有用,因为它记录的是你实际环境里的排序规则、数据类型和硬件特点。
另外一个验证技巧是对比新旧写法在相同数据上的执行计划。比如怀疑LEFT(OrderNo, 2)慢,就原样写一条,再写一条OrderNo LIKE 'AB%',用SET STATISTICS IO ON读逻辑读取次数。逻辑读取从几万降到几百,说明改写成功。优化一个函数调用,往往比对整套查询调优更立竿见影。
最后分享一个习惯:每写完一条带函数的 SQL,我都会自问一句“如果这张表有 1000 万行,这条 SQL 还能不能活着回来?”这个提问救过我很多次。真正决定 SQL Server 性能的不是函数本身,而是函数出现的位置——在列那侧,还是在参数那侧。这个认知希望帮到你。
本文还有配套的精品资源,点击获取