1. 写在前面
做 SQL Server 开发或者运维的朋友,日常工作里打交道最多的,除了表结构和存储过程,就是系统函数。日期转来转去、字符串拼来拆去、统计时聚合数据、报表里四舍五入,几乎每一个查询背后都站着几个函数。很多朋友学的时候零散地用过几个,遇到真正复杂的业务需求,要么去网上搜一段代码,要么翻官方文档,效率很低。有些函数其实是一条 SQL 解决问题的关键,但不知道它的存在,硬用别的方式绕了很多弯路。
这篇文章我把 SQL Server 里常用函数分门别类地整理一遍,重点不是把函数清单抄一遍,而是结合真实业务场景说清楚每个函数的用途、语法、常见陷阱和替代方案。覆盖范围包括日期转换、字符串处理、数学计算、聚合统计,还有平时容易被忽视的细节坑。无论你是刚接触 SQL Server 的新手,还是写了好几年 T-SQL 的开发者,这篇文章都值得花半小时认真读一遍,很多细节是你平时写代码时没注意过的。
2. 日期与时间处理
2.1 获取当前日期时间的正确姿势
SQL Server 里获取当前日期时间有多个函数,最常用的是 GETDATE(),它返回当前数据库服务器的日期时间,精度到毫秒。另一个是 SYSDATETIME(),它返回精度更高的日期时间,精确到 100 纳秒。如果是跨时区应用,还有 SYSDATETIMEOFFSET(),它带时区偏移量,比如 +08:00。实际开发中,绝大多数场景用 GETDATE() 就够了,但如果你在做日志审计或者需要高精度时间戳的应用,建议用 SYSDATETIME()。
还有一个容易踩坑的地方:GETDATE() 返回的是数据库服务器本地时间,不是应用服务器时间,更不是客户端电脑时间。如果应用部署在多台服务器上,数据库服务器的操作系统时间不准,所有日志记录的时间也会跟着偏。我曾经排查过一个问题,应用日志显示凌晨三点产生了大量数据,但业务人员坚持说凌晨没人操作,最后发现是数据库服务器时区设置错了,时间快了五个小时。所以做跨时区或者多地区部署的系统,存时间最好统一存 UTC 时间,用 GETUTCDATE(),显示时再转换。
SELECT GETDATE() AS 本地时间, GETUTCDATE() AS UTC时间, SYSDATETIME() AS 高精度时间;2.2 日期格式化与转换:FORMMAT 还是 CONVERT?
日期转字符串是高频需求。很多新手喜欢用 FORMAT(),因为它语法简单,格式表达式和 .NET 一致。比如 FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss')。但这里我要泼一盆冷水:FORMAT() 是个性能杀手,它运行时依赖 .NET CLR,在海量数据上使用会导致严重的 CPU 开销。数据量小、报表一两千行的场景没问题,但在上百万行的表里用 FORMAT() 做筛选或者分组,性能下降是肉眼可见的。
高效的做法是用 CONVERT()。CONVERT() 内置了很多格式编码,常见的 120 就是 yyyy-mm-dd hh:mi:ss,112 是 yyyymmdd,101 是 mm/dd/yyyy,103 是 dd/mm/yyyy。用 CONVERT 还有一个额外的好处,它输出的字符串格式固定、可预测,而且可以直接参与排序。
-- FORMAT 写法(简单但慢,适合小数据量) SELECT FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss') AS 格式化时间; -- CONVERT 写法(推荐,性能好) SELECT CONVERT(VARCHAR(19), GETDATE(), 120) AS 格式化时间; SELECT CONVERT(VARCHAR(8), GETDATE(), 112) AS 日期紧凑格式;再提醒一个静态鉴权很常见的陷阱:如果你把日期字段和字符串直接比较,比如 WHERE CreateTime = '2025-01-15',SQL Server 会发生隐式转换。如果 CreateTime 是 datetime 类型,'2025-01-15' 会被转成 2025-01-15 00:00:00.000,那么当天 00:00 之后的所有记录都会被漏掉。正确的写法是:
WHERE CreateTime >= '2025-01-15 00:00:00' AND CreateTime < '2025-01-16 00:00:00'或者 CAST('2025-01-15' AS datetime2) 之后比较。总之,处理日期时间段范围,必须遵循"大于等于起始时间,小于结束时间"的原则,否则查询结果十有八九是有偏差的。
2.3 DATEADD、DATEDIFF 与日期计算
DATEADD 用来做日期的加减运算,DATEDIFF 用来计算两个日期之间相差的间隔数量。比如计算上个月第一天和最后一天,就是典型的 DATEADD 和 DATEDIFF 组合应用。DATEDIFF 有个细节要注意,它计算的是间隔的边界次数,不是两个时间点之间的绝对差值精确到小数。举个例子,DATEDIFF(MINUTE, '2025-01-01 09:59:59', '2025-01-01 10:00:01') 的结果是 1,因为从 9:59:59 到 10:00:01,分钟数从 0 变成了 1,只有一次边界跨越,但实际时间差是 2 秒。如果要精确计算两个时间点差距,要用 DATEDIFF_BIG 或做更高精度的计算。
DATEADD 的常见应用场景是定时任务窗口、报表周期划分。比如计算当前时间所在周的周一,可以用 DATEADD(WEEK, DATEDIFF(WEEK, 0, GETDATE()), 0)。这条语句的原理是以 1900-01-01 为参照点,先算出从参照点到当前时间经过了多少周,然后再加回那么多个周,得到本周周一的零点。理解这个思路比记住语句更重要,以后遇到"上月的第一天""下季度的最后一天"都能推出来。
-- 上月第一天 SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0); -- 本月最后一天 SELECT EOMONTH(GETDATE());EOMONTH() 这个函数是专门用来取月最后一天的,SQL Server 2012 及以上版本才有。如果线上库是老版本 2008,就只能用 DATEADD 的写法模拟。
2.4 DATEPART 与 DATENAME 的实际用途
DATEPART 返回日期的指定部分,比如年、月、日、星期几、第几周。DATENAME 返回的是对应的名称字符串,比如星期几返回的是 "星期一" 这种中文文本或英文。这里的坑在于 DATENAME 的语言设置与排序规则有关。如果服务器是英文环境,DATENAME(WEEKDAY, GETDATE()) 返回 "Wednesday";如果是中文环境,返回 "星期三"。多语言应用如果要保证输出一致,最好用 DATEPART 取数值,然后自己映射中文或英文名称。DATEPART(WEEKDAY, GETDATE()) 返回的数值在默认情况下 1 表示周日,7 表示周六,这也是容易弄混的地方。
例如一个场景:统计每个小时的新增订单数。可以用 DATEPART(HOUR, OrderTime) 分组。但如果订单时间是 datetime 类型,分组时要注意小时分组后日期其实已经混在一起,如果跨天了,需要同时带上日期条件,否则统计结果会错乱。我遇到过一位同业朋友,他直接按小时分组统计了半个月的数据,结果每天下午某几个小时的订单数异常多,排查了半天才发现是没有加上 DATEPART(DAY, OrderTime) 作为分组维度。
2.5 日期时间类型的选择建议
SQL Server 的日期类型有 datetime、smalldatetime、datetime2、date、time、datetimeoffset。老系统里通常是 datetime,精度到毫秒,但最小值是 1753 年,而且精度有舍入误差。datetime2 精度更高,范围更广,可以精确到 100 纳秒。date 和 time 是分开存的类型,适合只关心日期或者只关心时间的业务。 新项目我强烈建议用 datetime2 替代 datetime,存时间统一用 datetime2(3) 或 datetime2(7),不要再用 datetime 这种老类型了。关于精度,datetime 的舍入误差偶尔会在边界计算中导致结果差异。比如某个时间点刚好是 997 毫秒之后继续进位的情况,用 datetime2 就没这些破事。
3. 字符串函数深度解析
3.1 长度与查找:LEN、DATALENGTH、CHARINDEX
LEN() 返回字符串的字符数,注意它是去掉两端空格后的字符数,也就是说 LEN(' abc ') 返回的是 3,不是 7。这跟 LEN 在很多其他语言中的行为不同。DATALENGTH() 返回的是字节数,对于普通字符和中文,一个中文字符在 UTF-8 编码下占 3 字节,在 GBK 编码下占 2 字节,SQL Server 里取决于排序规则和数据类型。如果存的是 nvarchar,DATALENGTH 会把每个字符按 2 字节计算。 所以说,如果你要判断一个字段是否为空字符串还是全空格,用 LEN(field) = 0 不够严谨,因为 LEN 已经去掉了两端空格,全空格字符串的 LEN 结果是 0。要用 DATALENGTH(field) = 0 或者直接 field = ''。
CHARINDEX 用来查子字符串的起始位置,从 1 开始计。另一个类似函数是 PATINDEX,它支持通配符匹配。比如要从一个版本号字符串 'V2025.01.15.8' 里提取最后一段数字,CHARINDEX 需要两层嵌套,PATINDEX 可以用通配符直接定位。日常开发中 CHARINDEX 用得多,但遇到模糊匹配定位,PATINDEX 更省事。注意 CHARINDEX 找不到时返回 0,不会报错,所以可以用返回值判断是否存在,不用先写 IF EXISTS。
SELECT CHARINDEX(',', 'SQL Server,字符串,函数') AS 逗号位置; -- 返回 11 SELECT PATINDEX('%[0-9]%', 'ABC123DEF') AS 首个数字位置; -- 返回 43.2 截取与拼接:SUBSTRING、LEFT、RIGHT、STUFF
SUBSTRING 按位置截取子串,LEFT 和 RIGHT 分别是取左侧、右侧 N 个字符。截取时最容易出错的是长度参数,SUBSTRING 的第三个参数如果超出了字符串的实际长度,它能正常返回,不会报错,只是返回尽可能多的字符。这点跟其他语言不太一样。
STUFF 是一个非常有用的函数,它的语法是 STUFF(原字符串, 开始位置, 删除长度, 插入字符串)。它先把指定位置的若干字符删掉,再把新的字符串放到那个位置。常见的应用是把手机号中间四位打码、把身份证号中间位数隐藏。LEFT 加 STUFF 配合,一套组合拳。
还有一种拼接需求是用表内多行记录转成一行逗号分隔的字符串。SQL Server 2017 之前要靠 FOR XML PATH 配合 STUFF 曲线救国。2017 之后有了 STRING_AGG(),这个函数极大地简化了这类语句。要注意 STRING_AGG 在 SQL Server 2017 及以上才有,如果你的生产环境还是 SQL Server 2016,就还是得用 FOR XML PATH 的方案。
3.3 REPLACE 与字符串清洗的实际案例
日常数据清洗,最常见的就是去掉换行符、回车符、制表符、多余空格。REPLACE 可以分别处理这些字符:REPLACE(REPLACE(字段, CHAR(13), ''), CHAR(10), '')。很多从 Excel 导入的数据都带着换行,显示在表格里感觉没事,导到数据库之后拼接、导出就乱了。
这里特别说下 TRIM、LTRIM、RTRIM。SQL Server 2017 之前只有 LTRIM 和 RTRIM,分别去掉左右空格。2017 之后新增了 TRIM 函数,默认去掉两端空格,也可以指定要去除的字符,比如 TRIM('#' FROM '#SQL Server#'),这个非常有用的场景是处理带分隔符标记的文本。
REPLACE 在替换时是全字匹配的,不会用正则。如果你希望做正则替换,SQL Server 原生没有直接函数,常见做法是写 CLR 函数,或者用 LIKE 做模糊匹配之后分步骤处理。CLR 函数部署和维护成本高,一般只在特殊场景才用。绝大多数常见清洗需求 REPLACE 加 SUBSTRING 组合就足够。
3.4 LIKE 与通配符的避坑指南
LIKE 的四个通配符:% 匹配任意多个字符,_ 匹配单个字符,[ ] 匹配范围内单个字符,[^] 匹配不在范围内单个字符。最容易忽略的是转义问题。如果业务数据本身包含下划线、百分号,比如存储的商品型号是 AB_100 和 AB%100,用 LIKE 'AB_100' 会把 ABX100 之类的数据也匹配进来,因为下划线代表单个任意字符。正确的写法是用 ESCAPE 子句指定转义符。
WHERE ProductCode LIKE 'AB\_100' ESCAPE '\'用 [ ] 的方式也可以:LIKE 'AB[_]100'。
在大量文本字段上执行 LIKE '%关键字%',索引会失效,全表扫描。如果业务上经常要模糊查询,可以考虑全文索引,或者做反向抽取字段。比如一列地址是多级拼接的,可能需要省份、城市、区县分别存储,而不是都在一个大字段里用 LIKE 去筛。
3.5 处理 NULL 的字符串函数用法
ISNULL、COALESCE 和 NULLIF 虽然不算专门的字符串函数,但在字符串处理中极其常用。ISNULL 是 SQL Server 的写法,两个参数,如果第一个参数为 NULL,返回第二个参数。COALESCE 是标准 SQL 的写法,可以接受多个参数,返回第一个非 NULL 的值。推荐用 COALESCE,但不推荐嵌套多层。
NULLIF 比较有意思,它接收两个参数,如果相等就返回 NULL,否则返回第一个参数。常用场景是防除零,或者把特殊的标志值转成 NULL 处理。比如 NULLIF(字段, '') 可以把空字符串转成 NULL。然后配合 ISNULL 再次转换回 NULL,会让数据统计时把空字符串统统计入 NULL 分组,规避了 GROUP BY 之后空格分一组、NULL 分一组的问题。我自己写统计报表时经常用这个组合处理脏数据。
4. 数学函数的应用边界
4.1 四舍五入:ROUND、CEILING、FLOOR 的差别
ROUND 是四舍五入,但注意它的第三个参数。默认是四舍五入,如果第三个参数是非零值,它不四舍五入,而是直接截断。比如 ROUND(123.4567, 2) 返回 123.46,ROUND(123.4567, 2, 1) 返回 123.45。这个细节经常被忽略,特别是从其他数据库迁移过来的开发者。
CEILING 和 FLOOR 分别向上取整和向下取整,它们不接收小数位参数。CEILING(4.01) 返回 5,FLOOR(4.99) 返回 4。注意负数的取整方向和直觉相反:CEILING(-4.01) 返回 -4,FLOOR(-4.99) 返回 -5。处理金额的时候,分转元、按分成整数后四舍五入,要确认业务规则是银行家舍入还是四舍五入,ROUND 的行为更接近大多数人预期的四舍五入,但遇到 .5 的场景,底层二进制浮点的表示可能导致意外的结果。如果对精度有强要求,用 DECIMAL 或 NUMERIC 类型,不要用 FLOAT。
4.2 ABS、SIGN 与幂运算
ABS 求绝对值,SIGN 返回 1、0、-1 表示正负。这两个函数在金融对账场景很常用。比如核对借贷方抵消,用 SIGN(差额) 来判断方向。POWER 做幂运算,SQRT 是平方根。平方根在计算标准差时用得较多。注意 SQRT 不能对负数使用,会报错。如果不确定输入范围,可以先 ABS 再 SQRT。
4.3 RAND 的随机数与 NEWID 控制
RAND 生成 0 到 1 之间的随机数。如果每次都要随机排序或抽样,单纯用 RAND() 配合 ORDER BY 会有问题。因为 RAND() 在查询中可能被多次计算,导致每条记录获得多个随机值,排序结果不稳定。更常见做法是 ORDER BY NEWID(),每行生成一个全局唯一标识符做随机排序。这个方案在数据量非常大时开销较高,但通常可以接受。
SELECT TOP 100 * FROM Orders WITH (NOLOCK) ORDER BY NEWID();要给每条记录生成固定的随机数,可以给 RAND 传一个整数种子,同一种子在同一会话中会得到相同的序列。这在测试数据生成、抽样比较时很实用。
4.4 REPLICATE、SPACE 和数字对齐
REPLICATE(字符串, 次数) 可以重复生成指定次数的字符。这在账务展示里很有用。比如把数字格式化为固定长度,前面补零:REPLICATE('0', 8 - LEN(CardNo)) + CardNo,可以处理卡号、流水号的对齐。SPACE(n) 生成 n 个空格。还有 PRINCE 相关格式,把数字变成字符串再拼接。注意,在大量数据里做这种字符串拼接,性能开销不小,如果条件允许,推荐在应用层做格式化,数据库里只负责返回原始数值。
4.5 数学计算中的精度和数据类型陷阱
DECIMAL 和 FLOAT 的混合运算要特别注意。FLOAT 和 REAL 是近似类型,DECIMAL 是精确类型。两个不同类型的数值做运算,SQL Server 会按照数据类型优先级隐式转换,FLOAT 的优先级高于 DECIMAL,结果就变成 FLOAT,可能丢失精度。 所以在做金额计算、税率计算时,不要用 FLOAT。另外,SUM 一个 decimal 字段时,结果类型会提升精度和规模,如果字段本身精度不够,SUM 结果有溢出可能。比如 SUM(Decimal(9, 2)) 的返回类型是 Decimal(38, 2),这个要注意,如果字段是 Decimal(38, 2) 再 SUM,结果类型仍是 Decimal(38, 2),精度可能不够。处理这类问题的通用做法是先把数据 CAST 成更高精度的 DECIMAL 再聚合。
5. 聚合函数与分组统计
5.1 COUNT、SUM、AVG、MIN、MAX 的基本用法与变体
COUNT() 统计行数,COUNT(字段) 统计该字段非 NULL 的个数,COUNT(DISTINCT 字段) 统计去重后的非 NULL 个数。三者之间的差别一定要分清。COUNT() 性能在多数情况下没有 COUNT(1) 和 COUNT(主键) 有明显的差别,但 COUNT(字段) 会额外跳过 NULL 值,如果统计逻辑没有意识到这一点,结果容易出错。
SUM 呢,如果数值列存在 NULL,SUM 不会把 NULL 包含进来,返回非 NULL 值的和,如果全为 NULL,则返回 NULL。如果希望全 NULL 时返回 0,建议用 ISNULL(SUM(字段), 0) 包一层。AVG 要小心,它计算的是非 NULL 值的平均值。如果某个分组里有很多 NULL 值,平均值会偏大,因为 NULL 不参与分母,看起来可能有点反直觉。
MIN 和 MAX 在字符串上是按照排序规则的顺序取最小最大,对中文的排序规则尤其敏感。比如按某种拼音排序规则,'张' 和 '李' 的先后可能和你想的不同。日期字段的 MIN 和 MAX 分别是最早和最晚时间,用它们取时间范围非常常见。
5.2 GROUP BY 与 HAVING 的配合
GROUP BY 分组,HAVING 筛选分组后的条件。看似简单,实际写 SQL 时最大的问题是 SELECT 列的规范性:GROUP BY 后面的字段必须出现在 SELECT 列表里,或者被聚合函数包裹,否则会报错。HAVING 和 WHERE 的区别是执行时机不同:WHERE 在分组前过滤行,HAVING 在分组后过滤组。能用 WHERE 的条件尽量用 WHERE,因为分组前过滤可大幅减小分组计算的数据量,效率更高。
HAVING 里面可以用聚合函数,比如 HAVING COUNT(*) > 10,但不能直接用 SELECT 里的别名来引用聚合结果。有些场景下,对 SUM 结果做条件过滤需要套一层子查询或者 CTE。CTE 在处理"先汇总再筛选"的复杂统计时比多层子查询可读性好很多。
5.3 经典统计案例:每类商品的销售情况
假设订单明细表 OrderDetails,有商品分类、商品名称、销售数量、单价。要统计每个分类下商品数量、总销售额、最大订单金额。写出来基本都是分组加聚合的套路,但要注意一个常见问题:如果某分类没有销售记录,查询结果中就看不到这个分类。如果想要所有分类都展示,需要一个维度表做 LEFT JOIN,否则统计结果少行。
SELECT c.CategoryName , COUNT(od.OrderID) AS 订单数 , ISNULL(SUM(od.Quantity * od.UnitPrice), 0) AS 总销售额 , MAX(od.Quantity * od.UnitPrice) AS 最大单笔金额 FROM dbo.Categories c LEFT JOIN dbo.OrderDetails od ON c.CategoryID = od.CategoryID GROUP BY c.CategoryName ORDER BY 总销售额 DESC;这些基础模式在实际报表中会反复出现,掌握好 JOIN 与 GROUP BY 的关系比背函数更重要。
5.4 窗口函数与聚合函数的关系
窗口函数是在不合并行的情况下,对每组数据做聚合计算,OVER( PARTITION BY ) 是它的核心语法。比如想要每一行的销售额、在分类内的销售占比,用 GROUP BY 做不出这样的效果,因为会合并所有行。窗口函数可以保留每一条明细,同时带上聚合结果到每行。
窗口函数中常用于排名的是 ROW_NUMBER、RANK、DENSE_RANK。这三者经常被搞混:ROW_NUMBER 不管有没有并列,都生成连续编号;RANK 有并列时编号相同,后续编号跳号;DENSE_RANK 并列时编号相同,后续编号不跳号。要取每类商品销量第一名的记录,一般用 ROW_NUMBER 加子查询筛选,或者用 TOP 1 WITH TIES,后者查询质量偶尔有心智负担,建议多用 ROW_NUMBER。业务场景里 "分组 TopN" 的需求基本都是窗口函数配合 CTE 解决的。
WITH SalesRanked AS ( SELECT ProductID, SalesQty , ROW_NUMBER() OVER (PARTITION BY CategoryID ORDER BY SalesQty DESC) AS rn FROM ProductSales ) SELECT * FROM SalesRanked WHERE rn = 1;注意,窗口函数的计算发生在 WHERE、GROUP BY、HAVING 之后,所以如果要用聚合结果作为行筛选,能放窗口就放窗口。
5.5 GROUPING SETS、ROLLUP、CUBE 的高级统计
GROUP BY 之后如果想同时看到小计和总计,可以用 ROLLUP。比如按年月日汇总销售额,ROLLUP 会给出月小计、年小计和总计。CUBE 给出所有维度的交叉小计,数据维度过高时生成的行数量非常多,注意风险。GROUPING SETS 是 SQL Server 2008 引入的,可以定制需要的分组组合,比 CUBE 更可控。
在这些结果集里,小计行的分组字段会是 NULL。如果 NULL 本身有业务含义,可以用 GROUPING() 函数来判断:GROUPING(字段) 返回 1 表示这行是该字段的汇总行,0 表示正常数据。这个函数是区分汇总行和原始 NULL 数据的关键。否则你可能会把汇总 NULL 当成原始 NULL 处理,统计结果偏差。
SELECT CategoryID, ProductID, SUM(Quantity) AS TotalQty FROM OrderDetails GROUP BY GROUPING SETS ((CategoryID, ProductID), (CategoryID), ());6. 类型转换与隐式转换的坑
6.1 CAST 与 CONVERT 的选择与差异
CAST 是标准 SQL 语法,CONVERT 是 SQL Server 特有。两者功能类似,但 CONVERT 在日期转字符串时支持更多格式编码,CAST 做不到。反过来,CAST 在代码跨数据库移植时兼容性更好。实际项目中,建议统一使用 CAST 做常规类型转换,只有在需要特定日期格式时才用 CONVERT。千万不要一个项目里两种混用得太随意,写出来的代码可维护性会降低。
6.2 隐式转换的风险
SQL Server 在做不同类型比较或运算时会发生隐式转换,这种转换如果发生在索引列上,会导致索引失效。最经典的问题是 VARCHAR 字段和 NVARCHAR 字段进行比较:如果字段是 VARCHAR,参数是 NVARCHAR,SQL Server 会把 VARCHAR 字段统一转成 NVARCHAR 来比较,索引就失效了。反过来把 NVARCHAR 参数转成 VARCHAR,如果参数有非英文字符就会乱码。所以存储和查询参数的数据类型要保持一致,程序里传入参数尽量用 SqlDbType 明确指定,不要全都走字符串。
6.3 数值类型转换时的溢出与截断
CAST('123.456' AS INT) 会报错,CAST('123.456' AS DECIMAL(5, 2)) 是正常舍入到 123.46。把一个大数字字符串转换成 INT 会溢出报错,遇到这种场景需要判断数据范围后决定用 BIGINT 或 DECIMAL。在转换时如果明知字段值可能不合法,可以先做 LEN、ISNUMERIC 之类的预检查。ISNUMERIC 这个函数可以判断字符串是否能转成数值,但它在某些场景会返回 1,比如空字符串、货币符号。登记筛查时用 TRY_CAST 更安全。
6.4 TRY_CONVERT、TRY_PARSE 的容错处理
SQL Server 2012 引入了 TRY_CONVERT 和 TRY_PARSE,它们在做转换失败时返回 NULL 而不是报错,这非常有用。处理用户导入的 Excel 数据时,日期格式千奇百怪,有 '2025/01/15'、'01-15-2025'、'2025年1月15日'。用普通 CONVERT 遇到不合法的值整个批处理会失败,用 TRY_CONVERT 就能把非法值转成 NULL,然后统一处理。需要注意,TRY_ 开头函数的性能通常比普通转换差一些,但在数据量可控时,安全性远重要于这点差异。
SELECT TRY_CONVERT(DATE, '2025-13-45'); -- 返回 NULL,不报错 SELECT TRY_CONVERT(INT, '12abc'); -- 返回 NULL7. 函数使用中高频踩坑总结
7.1 在 WHERE 子句中对列使用函数导致索引失效
这是一个非常常见的性能问题:WHERE CONVERT(VARCHAR, CreateTime, 112) = '20250115' 或 WHERE YEAR(CreateTime) = 2025,这种写法对时间列的函数操作会导致该列索引失效,SQL Server 必须全表扫描。 每年到 1 月份,运维群里经常看到生产环境的查询因为这个写法把数据库 CPU 打满。正确的写法是改写成范围条件:WHERE CreateTime >= '2025-01-01' AND CreateTime < '2026-01-01',这样既可以走索引,也不会漏掉数据。
7.2 用 UPPER/LOWER 做条件匹配导致索引失效
处理英文大小写匹配时,新手喜欢写 WHERE UPPER(LastName) = 'ZHANG'。同 YEAR(日期列) 一样,这会导致索引失效。如果字段本身需要大小写不敏感的匹配,改变排序规则或者在建表时就把列设为大小写不敏感排序规则,效果远好于在查询中加 UPPER 函数。还有一点,如果使用 LIKE '%zhang%',在文本大字段上同样无法走普通索引,B+ 树索引对这个模式没什么优化能力,数据量大时要考虑全文检索,不能硬着头皮全表 LIKE。
7.3 NULL 与其他值的运算行为
在 SQL Server 中,NULL 与任何值做算术运算的结果都是 NULL,NULL 与任何字符串拼接结果都是 NULL。不注意这点的后果是拼出的地址少了一块,数值汇总缺行。字符串拼接时,建议用 CONCAT() 函数,SQL Server 2012 及以后版本中 CONCAT 会自动把 NULL 当作空字符串处理。如果没有 CONCAT,需要先用 ISNULL 转成空字符串,否则会出现某个本来就该显示部分数据的地方整个字段变 NULL 的情况。
7.4 字符串中的不可见字符
平时发现一些 SQL 明明看着条件写对了,就是查不到数据,尤其是一些从外部系统导入过来的数据,表面看起来是空格,实际可能是全角空格、不间断空格 CHAR(160) 或者零宽空格。要排查不可见字符,可以用:
SELECT Unicode(SUBSTRING(Field, 位置, 1))或直接用 LEN 对比字符数。真正清理时用 REPLACE(Field, CHAR(160), '') 处理全角空格,用 DATALENGTH 对比字节数确认是否还有残留。
7.5 日期区域设置导致的转换错误
使用 CONVERT 或 FORMAT 时,格式代码受语言区域影响。DD/MM/YYYY 和 MM/DD/YYYY 的歧义在跨地域协作项目中经常导致数据错位。 如果你统一用格式代码 112 的 'yyyyMMdd',这是一个无歧义的纯数字日期格式,几乎不会出错。在存储过程参数校验时,建议把外部传入的日期字符串先用 TRY_CONVERT(DATE, @param, 112) 尝试解析,再判断是否为 NULL,可以有效拦截非法输入。
8. 版本差异与兼容性
8.1 各版本新增函数速查
SQL Server 不同版本新增了很多函数,如果你的代码是跨版本部署,要先确认目标版本。常见版本差异如下:
- SQL Server 2008:新增 TRIM 系函数较少,WITH 更早已有。
- SQL Server 2012:新增 EOMONTH、CONCAT、FORMAT、TRY_CAST、TRY_CONVERT、TRY_PARSE、IIF、CHOOSE。
- SQL Server 2016:新增 STRING_SPLIT、STRING_ESCAPE、DROP IF EXISTS 等,并支持 JSON 函数。
- SQL Server 2017:新增 STRING_AGG、TRIM(原生函数)和 TRIM 的其他形式。
- SQL Server 2019:增加了 STRING_AGG 的一些增强行为(比如 WITHIN GROUP 排序),以及 APPROX_COUNT_DISTINCT 等大数据近似聚合函数。
- SQL Server 2022:新增 GREATEST、LEAST 等函数,这正好能解决 MAX/MIN 对多个列进行取值的繁琐问题。
8.2 老版本兼容性处理方案
如果生产环境还是 2008 或 2016,某些新函数不能用。字符串聚合在没有 STRING_AGG 的情况下只能用 FOR XML PATH 方案;多列取最大值普遍用 CASE WHEN 嵌套取值,或者 VALUES 表值构造函数结合表连接。
举个例子,在 SQL Server 2017 之前做 "分组拼接逗号分隔字符串" 的经典写法:
SELECT CategoryID , STUFF(( SELECT ',' + ProductName FROM Product p WHERE p.CategoryID = c.CategoryID ORDER BY p.ProductName FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS ProductList FROM Category c;这个写法很长,容易出错,特别是 XML 路径中含有特殊字符时会被转义。所以在升级 SQL Server 版本之前,明确把这些兼容性代码整理出来,后续替换成新函数时才能节省时间。
8.3 排序规则与中文字符处理
数据库排序规则影响字符串的排序、比较、大小写等行为。常用的简体中文排序规则是 Chinese_PRC_CI_AS。CI 表示大小写不敏感,AS 表示重音敏感。如果你的数据库是从英文环境迁移而来的,中文排序中文可能不符合预期。在创建数据库或列的时候,应当显式指定排序规则。 如果应用层要求同一字段支持大小写敏感的比较,但又不能改列排序规则的时候,可以在查询中用 COLLATE 关键字临时指定排序规则。
WHERE LastName = 'zhang' COLLATE Chinese_PRC_CS_AS要记住 COLLATE 在列上使用也会导致索引失效。所以如果业务对大小写敏感是稳定需求,设计表结构时就选择对应的排序规则,不要每次查询都 COLLATE。
9. 实际项目中积累的性能优化细节
9.1 避免函数嵌套过深
多层函数嵌套不仅影响可读性,更会影响性能。比如 SUBSTRING(CHARINDEX(...)) 各种套娃,写的时候很爽,读的时候痛苦。嵌套过多还有一个坏处:SQL Server 的查询优化器有时很难对函数包裹的列做有效的统计信息估算。如果确实要处理复杂逻辑,尽量在 SELECT 列表中用别名或 CTE 分层处理,把函数计算的中间结果逐层传递。这样利于理解,也便于后续维护。
9.2 尽量不要在谓词中使用非确定性函数
RAND()、NEWID()、GETDATE() 这类非确定性函数如果出现在 WHERE 条件中,每次执行结果都不同,无法缓存执行计划,对复杂查询影响很大。日常常规查询中把它们计算出来放到变量里,再去 WHERE 条件中引用变量,执行计划更稳定。
DECLARE @today DATE = CAST(GETDATE() AS DATE); SELECT * FROM Orders WHERE OrderDate = @today;这比直接把 GETDATE() 写到 WHERE 中更合理,变量语义清晰,计划也更稳。
9.3 结合索引优化字符串匹配
LIKE 'abc%' 这种前缀匹配是可以走索引的,但 LIKE '%abc' 和 LIKE '%abc%' 不行。业务中如果有根据编号前缀查询的需求,尽量设计成前缀匹配。 比如订单号的前几位有业务含义--日期加序号,查询当天的订单就是 OrderNo LIKE '20250115%',这种写法若字段上有索引是可以高效利用的。如果非要全模糊匹配,可以考虑反向存储字段:把字符串的倒序存一列,并建索引,以支持后缀查询。这方法有点偏,但解决部分场景非常实用。
9.4 CASE 表达式与函数选择的权衡
CASE 表达式在 SQL Server 里有时比专门的函数(如 IIF、CHOOSE)更灵活,它支持任意复杂条件和多种数据类型分支。IIF 是 SQL Server 2012 引入的简化写法,本质上是 CASE WHEN 的语法糖。CHOOSE 是根据索引返回列表中的某一项,用起来简单,但尽量不要在数据量大的查询里选择它,因为它的实现可读性和优化性有限。 遇到复杂多分支逻辑,老老实实写 CASE,别指望 IIF 嵌套能写得优雅。
9.5 临时表、表变量与函数配合
函数计算后如果数据量大,不要直接把它放在子查询里反复调用。比如一个表值函数返回几万行,外层 JOIN 又关联它几次,每执行一次性能开销都累计一次。合理做法是先把函数结果插入临时表,加上主键和索引,再作为驱动表做 JOIN。很多 SQL 性能问题并不出在函数本身,而出在函数结果集被反复使用。
10. 一些值得注意的小技巧
实际工作中,有几个小技巧让我节省了不少时间,统一分享出来,算是压箱底的东西:
第一个是统一时间粒度的思路。报表周期统计时,先通过 CAST(GETDATE() AS DATE) 把 datetime 转成今天零点,再配合 DATEADD,能避免很多边界条件的噩梦。同理,生成 2025-01-01 00:00:00 这样的起点,DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 0) 比手写字符串靠谱得多。
第二个是对日期时间做减法时,计算年龄这类业务需求,不要简单用 DATEDIFF(YEAR, 生日, 现在) 算,因为这样没考虑过没过生日。正确逻辑是先用 DATEADD 加上年龄,再跟当前日期比较。直接用 DATEDIFF 会在生日还没到时多算一年。我见过很多统计报表年龄算错,就是因为这个细节:
DECLARE @birthdate DATE = '2000-06-15'; DECLARE @now DATE = '2025-01-15'; SELECT DATEDIFF(YEAR, @birthdate, @now) - CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, @birthdate, @now), @birthdate) > @now THEN 1 ELSE 0 END;第三个是去除重复字符串。如果需要在逗号分隔的字符串里去重,比较麻烦,通用做法是先 STRING_SPLIT 再聚合。SQL Server 2016 及以上版本支持 STRING_SPLIT,之后配 STRING_AGG 去重:
SELECT STRING_AGG(value, ',') FROM (SELECT DISTINCT value FROM STRING_SPLIT('A,B,A,C,B', ',')) t;第四个是用 FORMAT 前先问自己,是否真的需要这种格式。如果要传给前端展示,尽量让前端格式化,数据库只负责返回原生日期时间。如果必须 SQL 层格式化,优先 CONVERT,数据量小时 FORMAT 勉强接受,数据量大时别用。
11. 最后的经验之谈
写了这么多 SQL Server 常用函数,最想提醒大家的一句话是:函数是工具,关键在于用得合适和安全。 我在日常项目里见过太多因为使用了不合适的函数写法把查询写崩的场景,比如用 FORMAT 做日期过滤导致大批量数据的 CPU 飙升,或者在 WHERE 中对列套函数导致索引失效跑全表扫描。这些问题的本质不是函数难学,而写代码时没有考虑数据量和执行计划。
学习函数的过程,我建议按场景归纳而不是按函数名死记。比如你脑中建立"日期区间查询应该怎样写"、"字符串模糊匹配怎样做才不走火入魔"、"聚合统计用什么维度组合" 这样的场景,再遇到业务需求,自然能想到最合适的函数和处理方式。真正的高效,不是记住每个函数的所有参数,而是清楚它能解决哪个场景下的问题,上限和下限是什么。
如果这篇文章对你有帮助,可以边看边打开数据库实操,把文中这些例子跑一遍。实际操作中踩几次坑,比看十篇文章记得更牢。遇到具体问题,也别怕查官方文档,SQL Server 的官方文档现在写得已经很清楚,函数列表、语法、示例、兼容性信息都有。结合文档和自己的实操经验,熟练只是时间问题。