
我一直觉得SQL Server的函数库是入门到进阶绕不过去的一关。不管你是做报表、写存储过程还是日常取数分析都得跟日期、字符串和聚合逻辑打交道。很多人在群里问“这个日期怎么转格式”“这个字符串怎么截取”“为什么COUNT出来不对”其实翻翻函数文档就能解决但文档写得零散查起来费劲。这篇文章就把我平时用得最多的SQL Server常用函数一次性整理出来按日期、字符串、数学、聚合四大类拆开讲每个函数都给语法、示例、还有我踩过的坑。这篇内容适合所有正在用SQL Server的朋友不管是刚学T-SQL的新手还是写了好几年SQL但想查漏补缺的老手都能找到能直接抄走的写法。我会尽量用大白话解释每个函数背后的逻辑同时附上实际可运行的代码段。建议你打开SSMS边看边试光看不练容易忘动手敲一遍印象才深。1. 内容整体设计与思路拆解1.1 为什么按“日期、字符串、数学、聚合”这四类拆SQL Server的函数有上百个如果一股脑全列出来反而让人记不住。我把日常开发里命中率最高的函数分成四个维度基本覆盖了90%的T-SQL编写场景日期时间报表统计离不开“昨天”“本月”“上季度”这类时间窗口还有日期显示格式的转换。字符串处理清洗脏数据、截取代码、拼接姓名、拆分标签全是字符串活。数学计算保留小数、向上取整、生成随机数主要用在金额计算和抽样统计上。聚合汇总COUNT、SUM、AVG、GROUP BY这是所有分组统计的基石。这个分类思路其实和SQL Server自带的“函数分类”是吻合的。官方文档把函数分成“日期和时间函数”“字符串函数”“数学函数”“聚合函数”另外还有“转换函数”和“逻辑函数”等。我刻意把CONVERT、CAST这类转换函数塞进日期章节是因为大家在日常工作里碰上转换函数十有八九都是为了处理日期格式。字符串和数学两个部分边界很清楚聚合则单独拿出来讲因为聚合函数经常配合GROUP BY和OVER子句单独列出来更容易说明白。1.2 先学会查系统自带函数列表每次忘了函数名我都不太推荐去搜索引擎乱翻直接在SQL Server里查字典是最快的。系统视图sys.all_objects里存了所有内置函数你也可以用INFORMATION_SCHEMA.ROUTINES但我更喜欢下面这个写法SELECT o.name AS 函数名, o.type_desc AS 类型 FROM sys.all_objects o WHERE o.type IN (FN, AF, FS, FT, IF, TF) ORDER BY o.name;这会把标量函数、聚合函数、表值函数都列出来。你想看某一类函数的语法直接点击SSMS左侧“可编程性-函数”目录也能展开看到系统函数的清单。不过要注意目录树刷新可能不及时还是用上面这段SQL查最准。2. 日期时间转换与处理函数详解2.1 取当前日期时间GETDATE 和 SYSDATETIMEGETDATE()返回当前数据库实例的系统时间精度到毫秒3.33毫秒的精度这是最常用的。另外还有SYSDATETIME()它返回精度更高的时间100纳秒还有一个CURRENT_TIMESTAMP它是ANSI标准写法本质上跟GETDATE()一样。SELECT GETDATE() AS 当前时间, SYSDATETIME() AS 高精度时间, CURRENT_TIMESTAMP AS ANSI时间;这里有个容易忽略的坑GETDATE()用的是SQL Server所在服务器的操作系统时间不是客户端电脑的时间。有时候开发机在北京、服务器在美国你取出来的时间和本地差一截这正常别慌。要改的话得改服务器时区或操作系统时间通常不是SQL层面的调整。2.2 日期格式化我为什么不推荐 FORMAT但偶尔也用FORMAT()函数是SQL Server 2012引入的可以按照.NET的格式串来格式化日期比如这样SELECT FORMAT(GETDATE(), yyyy-MM-dd HH:mm:ss) AS 格式化时间;这个函数的优势是格式串很直观yyyy就是四位年份MM是两位月份跟我们在C#里写的一样。但是我要说一个性能坑FORMAT()走的是.NET CLR处理大量数据时特别慢。一万行数据用FORMAT()格式化日期比用CONVERT()慢几十倍都很正常这是因为每次调用都要启动CLR上下文。所以我的习惯是小型报表、临时取数随便用但如果是大型数据仓库的ETL或者高频查询能用CONVERT()解决的绝不用FORMAT()。配合递归生成连续日期序列时也能看到明显的性能差别。如果只是展示用FORMAT()确实香例如做月度报表标题SELECT FORMAT(GETDATE(), yyyy年MM月) AS 月份标题;2.3 日期转换的两种主力CAST 和 CONVERTCAST是ANSI标准语法简单CAST(表达式 AS 数据类型)。CONVERT是SQL Server特有可以带样式编号比如直接指定日期显示格式。SELECT CAST(GETDATE() AS date) AS 纯日期, CONVERT(varchar(10), GETDATE(), 120) AS 标准年月日, CONVERT(varchar(8), GETDATE(), 112) AS 紧凑年月日, CONVERT(varchar(20), GETDATE(), 121) AS 带毫秒;常用样式编号我列一下样式编号输出格式示例101mm/dd/yyyy 美式12/06/2025103dd/mm/yyyy 英式06/12/2025110mm-dd-yyyy12-06-2025111yyyy/mm/dd2025/12/06112yyyymmdd 紧凑数字20251206120yyyy-mm-dd hh:mi:ss2025-12-06 14:23:05121yyyy-mm-dd hh:mi:ss.mmm 带毫秒2025-12-06 14:23:05.123最常用的是120和112。120适合存储varchar日期字段112适合按日期排序和比较因为它是纯数字字典序就是时间顺序。103和101容易混淆国际项目里一定要搞清楚对方习惯否则年月日反了数据就全乱了。2.4 日期加减与间隔计算DATEADD 和 DATEDIFFDATEADD(单位, 数量, 日期)用于给日期加一段间隔DATEDIFF(单位, 开始日期, 结束日期)用于计算两个日期之间的间隔。SELECT DATEADD(day, -1, GETDATE()) AS 昨天, DATEADD(month, 1, GETDATE()) AS 下个月今天, DATEADD(year, -3, GETDATE()) AS 三年前, DATEDIFF(day, 2025-01-01, GETDATE()) AS 今年已过天数;单位可以是year、quarter、month、day、week、hour、minute、second、millisecond等。需要注意DATEDIFF计算的是两个日期之间跨越了多少个边界不是精确的24小时差。比如DATEDIFF(day, 2025-01-01 23:59:59, 2025-01-02 00:00:01)结果是1但实际只过了2秒。这在计算年龄时特别典型SELECT DATEDIFF(year, 2000-12-31, 2025-01-01) AS 按年跨度的年龄;这个结果会是25因为只看年份边界但实际上这个人只过了24年零1天。要精确计算年龄推荐先判断今年生日过了没有或者在计算后减一个修正量。2.5 提取日期部件DATEPART、DATENAMEDATEPART(单位, 日期)返回日期部件的整数值DATENAME返回名称字符串。SELECT DATEPART(year, GETDATE()) AS 年份, DATEPART(month, GETDATE()) AS 月份, DATEPART(day, GETDATE()) AS 日, DATEPART(weekday, GETDATE()) AS 星期几数值, DATENAME(weekday, GETDATE()) AS 星期几名称;如果你把DATEFIRST默认7表示周日的值为1考虑进去DATEPART(weekday, ...)返回的是1到7代表周日到周六。想切成周一为一周的开始通常用DATEPART(week, ...)配合SET DATEFIRST或者用DATEDIFF(day, 0, 日期)/7来分组。拿周一分组的实用写法是SELECT DATEADD(day, DATEDIFF(day, 0, GETDATE()) / 7 * 7, 0) AS 当前周周一;2.6 日期字符串解析与常见报错把字符串转成日期最省事的是用CONVERT配样式编号SELECT CONVERT(datetime, 2025-12-06 14:23:05, 120) AS 转换结果;但如果你遇到2025/12/06、20251206、12/06/2025这类格式最保险的做法是先把字符串替换成标准yyyy-MM-dd形式再用CONVERT(..., 120)。我见过太多“从字符串转换日期和/或时间时转换失败”的报错基本都是因为字符串里混进了空格、反斜杠、或者月份缩写。这里有个排查心法把所有日期字符串统一成yyyymmdd纯数字格式再转换几乎不会出错。-- 把 2025/12/06 转成日期 SELECT CONVERT(date, REPLACE(2025/12/06, /, -), 120);3. 字符串函数详解与实战玩法3.1 字符串截取全家桶LEFT、RIGHT、SUBSTRING这三个是切片神器各有分工LEFT(字符串, n)从左侧取n个字符。RIGHT(字符串, n)从右侧取n个字符。SUBSTRING(字符串, 起始位置, 长度)从中间任意位置取。SELECT LEFT(SQL Server, 3) AS 左三, RIGHT(SQL Server, 6) AS 右六, SUBSTRING(SQL Server, 5, 6) AS 从第5位取6位;字符串的位置从1开始这是SQL Server的老规矩跟很多编程语言从0开始不一样新手容易懵。比如SUBSTRING(abc, 1, 2)的结果是ab不是b。想取最后一个字符可以用RIGHT(字符串, 1)。如果不知道字符串长度配合LEN使用SELECT SUBSTRING(SQL Server, LEN(SQL Server) - 3, 4); -- 取后四位3.2 查找字符位置CHARINDEX 和 PATINDEXCHARINDEX(要查找的子串, 字符串, 起始位置)返回第一次出现的位置找不到返回0。PATINDEX(%模式%, 字符串)支持通配符匹配。SELECT CHARINDEX(Server, SQL Server, 1) AS 位置, PATINDEX(%[0-9]%, 订单号20251206备注) AS 第一个数字位置;常用场景是截取某个分隔符后面的内容。比如从“姓名:张三”里提取张三DECLARE str NVARCHAR(100) N姓名:张三; SELECT SUBSTRING(str, CHARINDEX(:, str) 1, LEN(str)) AS 姓名;这里有个细节LEN()会去掉末尾空格所以截取时不用担心末尾有多余空格。但如果你用了VARCHAR而不是NVARCHAR字符串里有中文时按字符算位置CHARINDEX依然能正确工作。3.3 替换与删除空格REPLACE、TRIM、LTRIM、RTRIMREPLACE(字符串, 旧子串, 新子串)是清洗脏数据的神器。比如用户输入的电话号码里有横线或空格要统一去掉SELECT REPLACE(REPLACE(138-1234-5678, -, ), , ) AS 清洗后号码;LTRIM去掉左空格RTRIM去掉右空格。SQL Server 2017引入了TRIM(字符串)可以直接去掉两端空格或者指定要去的字符比如SELECT LTRIM( abc), RTRIM(abc ), TRIM( abc ), TRIM(###abc### FROM #abc#);在SQL Server 2017之前TRIM函数不可用所以老项目里最常见的写法是LTRIM(RTRIM(字段))。新项目直接用TRIM就行。要注意TRIM默认只去空格不去换行符或制表符。遇到换行符要配合REPLACE(字段, CHAR(10), )和REPLACE(字段, CHAR(13), )。3.4 拼接字符串运算符、CONCAT 和 CONCAT_WS向来用拼接但必须有意识处理NULL。NULL abc的结果是NULL不是abc。SQL Server 2012引入了CONCAT函数它自动把NULL当成空字符串处理这个改进非常实用。SELECT SQL Server AS 加号拼接, CONCAT(SQL, NULL, Server) AS CONCAT拼接, CONCAT_WS(-, 2025, 12, 06) AS 带分隔符拼接;CONCAT_WS是SQL Server 2017新增的第一个参数是分隔符后面是要拼接的内容自动忽略NULL。这个函数在拼地址、拼姓名、拼ID列表时特别舒服。3.5 大小写与去重UPPER、LOWER、REVERSE、STRING_AGG字符串的大小写转换没什么好说的UPPER全变大写LOWER全变小写。REVERSE倒序输出可以用来做回文判断或反转字符串。STRING_AGG(字段, 分隔符)是SQL Server 2017新增的聚合连接函数替代了老式的STUFF FOR XML PATH写法。它的作用是把分组内的多行字符串拼接成一行SELECT 部门ID, STRING_AGG(姓名, 、) AS 员工名单 FROM 员工表 GROUP BY 部门ID;没有STRING_AGG的老版本里大家普遍用这样一段经典的黑魔法SELECT 部门ID, STUFF(( SELECT 、 姓名 FROM 员工表 e2 WHERE e2.部门ID e1.部门ID FOR XML PATH() ), 1, 1, ) AS 员工名单 FROM 员工表 e1 GROUP BY 部门ID;虽然能跑但可读性很差。如果你还在维护2016之前的库还是得懂这段写法如果是2017以上直接拥抱STRING_AGG吧。3.6 字符串分割STRING_SPLIT 与替代方案SQL Server 2016引入了STRING_SPLIT(字符串, 分隔符)把分隔符拆成多行。SELECT value FROM STRING_SPLIT(apple,banana,orange, ,);这个函数可以配合WHERE做“传ID列表”的查询比如前端传进来一串“1,2,3,4”要查出这些ID对应的订单SELECT * FROM 订单表 WHERE 订单ID IN (SELECT CAST(value AS INT) FROM STRING_SPLIT(1,2,3,4, ,));在2016之前没有这个函数通常的做法是自己写一个拆分函数或用递归CTE。STRING_SPLIT的输出列名叫value这个不能改注意别写错。另外它不支持同时返回序号如果你需要每个元素的原始顺序还得再想办法比如配合JSON函数或者用OPENJSON。4. 数学函数与聚合函数4.1 数学计算核心ROUND、CEILING、FLOOR、ABS、RANDROUND(数值, 小数位数)是四舍五入CEILING(数值)是向上取整到最接近的整数FLOOR(数值)是向下取整ABS取绝对值。RAND(种子)返回0到1之间的随机小数每次运行如果不用种子生成的结果不同。SELECT ROUND(123.4567, 2) AS 四舍五入两位, ROUND(123.4567, 0) AS 四舍五入整数, CEILING(123.456) AS 向上取整, FLOOR(123.456) AS 向下取整, ABS(-100) AS 绝对值, RAND() AS 随机小数;随机整数取法SELECT CAST(RAND() * 100 AS INT) AS 0到99随机整数;不过RAND()每次生成的序列不稳定如果需要每条记录一个独立的随机数推荐用NEWID()来生成随机排序SELECT TOP 10 * FROM 订单表 ORDER BY NEWID();CEILING和FLOOR在分页计算里很常用。比如1000条数据每页20条需要CEILING(1000.0/20)得到50页。4.2 真正的随机值NEWID 和 RAND 的取舍RAND()有一个大家都知道的行为同一查询里多次调用RAND()可能返回相同序列。这是因为它是基于种子按顺序生成的。如果你在UPDATE语句里对每一行设置随机数RAND()往往给所有行赋予同一个值这不是你想要的。这时候改用NEWID()作为种子SELECT TOP 5 ABS(CHECKSUM(NEWID())) % 100 AS 随机数 FROM sys.objects;CHECKSUM(NEWID())能生成非常大的随机整数值对100取余后得到一个比较均匀的0-99随机数。这个方法在抽样检查中很实用比如随机抽10个订单做审计。4.3 聚合函数基础SUM、AVG、COUNT、MIN、MAX聚合函数的作用是把多行数据汇总成一行。最常见的五个SUM(列)求和只用于数值列。AVG(列)求平均只用于数值列。COUNT(*)统计所有行数包括NULL行COUNT(列)统计该列非NULL值的个数。MIN(列)取最小值适用于数值、日期、字符串。MAX(列)取最大值。SELECT COUNT(*) AS 全部订单数, COUNT(发货日期) AS 已发货数, SUM(金额) AS 总金额, AVG(金额) AS 平均金额, MIN(订单日期) AS 最早订单, MAX(订单日期) AS 最晚订单 FROM 订单表;这里最容易出错的是COUNT的语义。COUNT(*)和COUNT(1)在MySQL里常被争论其实在SQL Server里两者没有本质区别都是统计行数。COUNT(列名)则跳过NULL如果你以为COUNT(发货日期)能统计所有订单数结果会因为部分订单没发货而少算。想统计非空唯一值可以用COUNT(DISTINCT 列)。4.4 分组统计GROUP BY 的细节和升级版 ROLLUP / CUBE聚合函数单独用是全部数据的汇总配合GROUP BY就能按维度分块统计。基础写法SELECT 城市, COUNT(*) AS 客户数, SUM(消费金额) AS 总消费 FROM 客户表 GROUP BY 城市;GROUP BY也可以用ROLLUP生成小计和总计。比如按城市分组后还想看所有城市的总计SELECT 城市, COUNT(*) AS 客户数, SUM(消费金额) AS 总消费 FROM 客户表 GROUP BY ROLLUP(城市);ROLLUP会在结果集最后加一行城市NULL的小计行。CUBE则是多维度的全组合汇总适用于多列交叉统计。要注意GROUP BY后面的列必须是查询中出现的非聚合列否则会报错。另外带ROLLUP的结果里小计行的城市为NULL如果你还要继续输出到报表通常用ISNULL或COALESCE替换掉。4.5 窗口聚合OVER 子句让聚合不丢明细窗口函数是进阶的必备技能能把聚合结果和每一行明细同时展示。SQL Server 2005就支持了基本的OVER2012后加了ORDER BY和ROWS BETWEEN等更完整的窗口框架。SELECT 订单ID, 城市, 金额, SUM(金额) OVER (PARTITION BY 城市) AS 城市总金额, RANK() OVER (ORDER BY 金额 DESC) AS 金额排名 FROM 订单表;PARTITION BY类似于GROUP BY的分组逻辑但它不会折叠行数。SUM(...) OVER(PARTITION BY 城市)会给每一行都附上该城市的总金额。这种写法在计算“每个城市的销售占比”时特别方便SELECT 订单ID, 金额, 城市, 金额 * 1.0 / SUM(金额) OVER (PARTITION BY 城市) AS 城市内占比 FROM 订单表;窗口函数还有ROW_NUMBER()、LEAD()、LAG()等它们在去重、环比计算中非常有用。比如抽每个城市最近一笔订单SELECT 城市, 订单ID, 订单日期 FROM ( SELECT 城市, 订单ID, 订单日期, ROW_NUMBER() OVER (PARTITION BY 城市 ORDER BY 订单日期 DESC) AS rn FROM 订单表 ) t WHERE rn 1;这个写法比GROUP BY后自连接简单得多。5. 实操过程与核心环节实现5.1 实战场景一生成连续日期序列补全报表缺失日期做数据补全时经常需要一张“从今天往前推30天”的日期表。经典写法是递归CTEWITH DateRange AS ( SELECT CAST(GETDATE() AS date) AS 日期 UNION ALL SELECT DATEADD(day, -1, 日期) FROM DateRange WHERE DATEADD(day, -1, 日期) DATEADD(day, -29, GETDATE()) ) SELECT 日期 FROM DateRange ORDER BY 日期;如果数据量特别大或者日期跨度特别长推荐维护一张物理日期维度表把未来几年的日期都生成好字段带上年、月、季度、星期几、是否工作日等。查询时直接JOIN性能远高于递归CTE。日常临时用用CTE足够。5.2 实战场景二字符串清洗把“工号-姓名-部门”拆成三列假设有一个不规范字符串字段DECLARE source NVARCHAR(50) NA001-张三-研发部; SELECT SUBSTRING(source, 1, CHARINDEX(-, source) - 1) AS 工号, SUBSTRING( source, CHARINDEX(-, source) 1, CHARINDEX(-, source, CHARINDEX(-, source) 1) - CHARINDEX(-, source) - 1 ) AS 姓名, REVERSE(SUBSTRING(REVERSE(source), 1, CHARINDEX(-, REVERSE(source)) - 1)) AS 部门;这里用一个临时变量跑通了拆列逻辑。如果是真实表里的批量数据可以直接把变量替换成字段名。这种处理方式在对接ERP导出文件时经常用到特别是那些“编号-名称-备注”挤在一列的坏数据。5.3 实战场景三统计各部门月度销售并输出“2025年12月”格式需求统计各部门在指定月份的销售额结果里要带中文月份名称并且按销售额从高到低排。SELECT 部门名称, FORMAT(订单日期, yyyy年MM月) AS 月份, SUM(订单金额) AS 销售额 FROM 订单表 WHERE 订单日期 2025-12-01 AND 订单日期 2026-01-01 GROUP BY 部门名称, FORMAT(订单日期, yyyy年MM月) ORDER BY 销售额 DESC;这个写法直接在GROUP BY里用了表达式SQL Server是允许的。但要注意如果数据量大FORMAT在分组和排序中会被反复计算性能较差。更推荐先算出月份字符串再分组SELECT 部门名称, 月份, SUM(订单金额) AS 销售额 FROM ( SELECT 部门名称, 订单金额, FORMAT(订单日期, yyyy年MM月) AS 月份 FROM 订单表 WHERE 订单日期 2025-12-01 AND 订单日期 2026-01-01 ) t GROUP BY 部门名称, 月份 ORDER BY 销售额 DESC;写成子查询至少让FORMAT的调用次数减少不过根本优化方案还是换成CONVERT(varchar(7), 订单日期, 120)得到2025-12这种格式再自己拼上年月文字。5.4 实战场景四用聚合语句做数据质量检查调试数据时我会用一段“万金油”SQL快速检查表的完整性和边界值SELECT COUNT(*) AS 总行数, COUNT(客户ID) AS 非空客户数, COUNT(DISTINCT 客户ID) AS 去重客户数, MIN(注册日期) AS 最早注册, MAX(注册日期) AS 最近注册, SUM(CASE WHEN 手机号 IS NULL OR 手机号 THEN 1 ELSE 0 END) AS 手机号缺失数 FROM 客户表;这条语句能一眼看出有没有重复客户ID、日期是否异常、关键字段缺失多少。数据质量检查是上生产环境前最该做的事别等报表算出来对不上账再回头查。6. 常见问题与排查技巧实录6.1 报错“从字符串转换日期和/或时间时转换失败”这个错误遇见的频率实在太高了。通常原因是字符串里有非法格式比如-- 这里会报错 SELECT CONVERT(datetime, 2025年12月6日);解决办法是先把字符串清洗成yyyy-MM-dd这样的标准格式再做转换。用TRY_CONVERT可以先测试如果转换失败返回NULL便于排查SELECT TRY_CONVERT(datetime, 2025年12月6日) AS 结果; -- 返回NULL排查思路把原始数据捞出来用LEN和ASCII检查隐藏字符尤其是从文本文件导入的数据容易混入全角空格。6.2 聚合结果比预期大或小如果你发现SUM结果不对先查是不是有NULL值。SUM会忽略NULL如果整列全是NULL返回值是NULL而不是0。这种情况要用ISNULL(SUM(列), 0)来兜底。另外COUNT(列)只统计非空值如果列里有空值结果当然比行数小。还有一个常见陷阱SUM小数列的结果有时候出现小数点后十几位这是浮点误差。在财务系统里应该用decimal类型避免用float存金额。6.3 字符串拼接结果太长被截断VARCHAR类型有长度限制VARCHAR(10)拼接时多余部分会被截断这可能导致生成的结果莫名变短。尤其是STUFF FOR XML PATH拼接长文本时注意要把中间变量声明成NVARCHAR(MAX)不然超过8000字符NVARCHAR(MAX)是2GB就会被静默截断。用STRING_AGG时也有默认长度限制的风险不过它是按返回类型最大长度计算的一般写VARCHAR(MAX)作为表达式的一部分更稳妥SELECT STRING_AGG(CONVERT(NVARCHAR(MAX), 姓名), 、) FROM 员工表;6.4 函数性能问题速查表现象原因解决思路用了FORMAT后查询极慢CLR开销大换CONVERT或预先格式化聚合查询很慢没有索引或扫描大表检查WHERE条件是否能走索引考虑建覆盖索引字符串拼接超时循环拼接且未使用MAX长度用STRING_AGG或提前物化窗口函数导致内存溢出OVER里的PARTITION BY选择不合理优化分组键减小数据范围日期比较慢对日期列用了函数导致索引失效不要写WHERE CONVERT(date, 日期列) ...直接WHERE 日期列 ...并调整边界这条索引失效的坑值得重点说。很多人喜欢写WHERE CONVERT(varchar(10), 订单日期, 120) 2025-12-06这样在订单日期上加了函数索引就用不上了。正确写法WHERE 订单日期 2025-12-06 00:00:00 AND 订单日期 2025-12-07 00:00:006.5 字符集和排序规则带来的坑SQL Server默认排序规则通常是Chinese_PRC_CI_AS这决定了字符串比较是否区分大小写、是否把全角半角视为相同。CI表示不区分大小写AS表示区分重音。如果你对大小写敏感要么改排序规则要么用COLLATE指定SELECT * FROM 用户表 WHERE 用户名 admin COLLATE Chinese_PRC_CS_AS;还有一种容易忽略的情况VARCHAR存中文容易产生乱码只要源头是NVARCHAR或字符串直接赋值最好统一字段类型为NVARCHAR。用N前缀指定Unicode字符串SELECT N中文;6.6 排查函数逻辑的调试习惯最后分享几个我自己的排错思路。第一把复杂查询拆成小段每段只做一件事先验证函数结果再做关联。第二善用PRINT和临时表把中间结果打出来尤其是写存储过程时在关键步骤后SELECT调试信息。第三写函数表达式时多放几个边界值测试比如空字符串、NULL、0、负值、日期边界往往能提前发现隐藏bug。SQL Server的函数体系看着庞杂但只要抓住日期、字符串、数学、聚合这四个主干配合官方文档和实际工作中的场景慢慢就能形成自己的函数库。平时写SQL时可以刻意总结“我每次取数最常用的函数是什么”过一阵子你就能把这些函数的语法和行为细节烂熟于心遇到问题直接能写出稳定靠谱的代码。我个人在实际操作中体会最深的一点是函数是死的场景是活的。同一函数在不同业务里用法千差万别关键是理解每个函数的边界行为和性能代价。比如FORMAT虽好但别在核心统计上用STRING_AGG再方便也要留意长度限制和版本兼容。多写几轮真实数据你自然会形成属于自己的“避坑清单”。