
1. 为什么你需要一份函数速查手册先从场景说起做SQL Server开发或者运维的兄弟应该都有过这种经历写报表查询时要按月份格式化日期、要处理客户名称里的空格、要把逗号分隔的字符串拆成多行、要计算客单价并做同比环比……每次都要打开浏览器翻博客翻了半天还未必能找到面面俱到的版本。今天我花了两天时间把SQL Server里最常用的日期转换、字符串处理、数学计算、聚合统计函数连同我自己踩过的坑一起整理出来有的地方直接给出了可以直接抄的写法。这份东西适合正在学SQL的初学者、写报表写到头大的数据分析师以及需要维护老项目的后端开发——不管你是用Management Studio还是Navicat函数用法都是一样的只是连接工具不同而已。就算你已经熟悉大部分函数我也建议你重点看一下第7部分有些坑我保证你大概率遇到过只是当时没想明白原因。SQL Server函数这个事说难不难说简单也不简单。关键在于两点第一你要知道有哪些函数可用别自己写复杂的嵌套表达式去实现现成功能第二你要知道每个函数的“脾气”比如CONVERT的样式参数、字符串函数的索引习惯、聚合函数遇到NULL的行为否则出了bug排查起来极费劲。下面我从几个最常用的分类开始逐个拆解。2. 日期转换与日期处理函数最容易被格式绕晕的部分2.1 CONVERT 与 FORMAT两种转换思路的取舍日期转换可能是SQL Server里被问到最多的问题。常见需求把getdate()得到的时间改成“2025-01-15”或者“2025年01月15日”这种格式。很多人直接想到FORMAT函数它确实方便但性能隐患很大。FORMAT依赖.NET的CLR在数据量大的查询里被调用无数次速度会明显慢。我的建议是能不用FORMAT就不用优先用CONVERT配合样式参数。CONVERT的基本写法是CONVERT(date_type, expression, style)第三个参数style决定了输出格式。常用的几个我列个表style值输出格式典型场景101mm/dd/yyyy美国习惯少用102yyyy.mm.dd某些导出的固定格式103dd/mm/yyyy英国/欧洲格式104dd.mm.yyyy德式105dd-mm-yyyy今天很多业务系统上报格式111yyyy/mm/dd日文系统常见112yyyymmdd最推荐排序、比较都非常好用120yyyy-mm-dd hh:mi:ss日志、时间点记录121yyyy-mm-dd hh:mi:ss.mmm带毫秒调试用CONVERT(varchar(10), GETDATE(), 112)就能得到20250115这样的字符串要做成2025-01-15可以再用STUFF插入横杠或者直接用CONVERT(varchar(10), GETDATE(), 120)也会得到2025-01-15。我自己写报表的时候日期格式统一用yyyyMMdd或yyyy-MM-dd前者适合做索引关联后者适合人眼阅读。有条件的话直接用DATE类型做参数留给类型转换的次数越少越好。FORMAT也不是完全不能用。它真正的价值在于需要中文化格式时比如FORMAT(GETDATE(),yyyy年MM月dd日)或者输出周几FORMAT(GETDATE(),dddd,zh-CN)。如果只是几千行数据展示随便用但你要是把它放在几百万行的大表SELECT列里报表能慢到你怀疑人生。遇到这种场景我的经验是宁可先用CONVERT转成标准格式再在应用端做格式化也别把压力都扔给SQL Server。2.2 GETDATE、DATEADD、DATEDIFF等常用日期计算除了转换日期的加减计算也特别常用。比如查最近7天的订单或算两个日期之间隔了多少天。这里有几个必备函数GETDATE()/SYSDATETIME()取当前时间后者精度更高带毫秒和时区偏移但一般GETDATE够用。DATEADD(interval, number, date)给日期加上/减去一段间隔。间隔单位可以是year、quarter、month、day、hour、minute、second等。DATEDIFF(interval, startdate, enddate)计算两个日期之间的间隔数。注意是“结束日期减开始日期”。DATEPART(interval, date)提取日期的某一部分比如年、月、日。DATENAME(interval, date)返回字符串形式比如月份名称或星期几受语言设置影响。举个简单的例子查上月第一天到今天的数据可以先算出上月第一天SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0);这个写法的原理是DATEDIFF(MONTH, 0, GETDATE())算出1900-01-01到现在的月份数减1表示回到上个月再通过DATEADD(MONTH, ..., 0)把它映射回日期。很多人不理解为什么用0其实0在SQL Server里会被隐式转换成1900-01-01这是个很老的技巧理解之后别乱用但确实非常高效。还有人喜欢用DATEPART(WEEKDAY, GETDATE())来判断今天是星期几但要注意默认情况下周日是1周一才是2。如果你想按中国的习惯让周一1、周日7可以用DATEPART(WEEKDAY, GETDATE()) DATEFIRST - 1再取模之类的方法。不过更直接的做法是用SET DATEFIRST 1这样一周从周一开始。2.3 日期函数实战工龄计算、月初月末、周几判断光讲理论没意思我直接给几个写报表时高频使用的例子。场景一计算员工工龄精确到年月日很多人拿到工龄需求就写DATEDIFF直接DATEDIFF(YEAR, HireDate, GETDATE())结果会出现“入职满5年但实际差一天”的错误因为DATEDIFF是按年份边界跳的。正确做法是SELECT EmpName, HireDate, DATEDIFF(YEAR, HireDate, GETDATE()) - CASE WHEN DATEADD(YEAR, DATEDIFF(YEAR, HireDate, GETDATE()), HireDate) GETDATE() THEN 1 ELSE 0 END AS FullYears FROM Employees;思路是先用年份差再检查还没有到今年的对应日期没到就减1。场景二获取当前月的第一天和最后一天SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AS FirstDayOfMonth, DATEADD(DAY, -1, DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) 1, 0)) AS LastDayOfMonth;这里用到“先跳到下个月第一天再减一天”的技巧。注意如果数据库的日期类型涉及时间部分最后一天往往带有23:59:59.xxx有些人会专门写成DATEADD(MS, -3, ...)来凑一个“小于X”的条件但我觉得更稳妥的是直接用月初 AND 下月月初避开对毫秒的依赖。场景三判断某个日期所在的周一SELECT DATEADD(DAY, -(DATEPART(WEEKDAY, someDate) DATEFIRST - 2) % 7, someDate);这个公式可以把任意日期归到它所在周的周一前提是已经SET DATEFIRST 1。具体推演不写了留下备注即可用的时候复制测几个边界值再上生产。3. 字符串函数处理脏数据的必修课3.1 截取、拼接与替换SUBSTRING、LEFT、RIGHT、REPLACE、CONCAT业务系统里的数据从来不会干干净净最常见的脏数据就是字段里混着多余字符、换行、逗号分隔等。字符串函数就是专门拿来对付这些的。先说截取三兄弟LEFT(str, n)取左边n个字符。比如LEFT(ABC123, 3)返回ABC。RIGHT(str, n)取右边n个字符。比如RIGHT(ABC123, 3)返回123。SUBSTRING(str, start, length)从第start个字符开始取length个。注意索引是从1开始的不是从0开始。SUBSTRING(ABC123, 3, 2)返回C1。再说字符串拼接。早期版本用号非常方便但存在一个隐患如果其中一个参数是NULL整个结果就是NULL。比如A NULL返回NULL。如果字段里可能有NULL你得先ISNULL(字段,)。如果使用的是SQL Server 2012及以上推荐用CONCAT(str1, str2, ...)它会自动把NULL当作空字符串处理不用手动加ISNULL。但要注意CONCAT不会自动处理分隔符你仍然要自己在参数中间加,或-。最后是替换REPLACE(str, old, new);这个没什么难的但实际使用中很容易犯一个错用REPLACE去清理多余空格结果不管几个空格都被替换成一个或者用了REPLACE(str, ,)把句子里的所有空格都删了。正确做法是如果只想保留单个空格可以先用REPLACE(REPLACE(str, ,CHAR(39)),CHAR(39), )这种组合但注意未必适合所有情况。更常用的场景是从地址里去掉“省/市/区”比如REPLACE(REPLACE(address,省,),市,)。3.2 查找、去空格、大小写CHARINDEX、LTRIM/RTRIM/UPPER/LOWER有截取就要有查找。CHARINDEX(substr, str)用来返回子串第一次出现的位置找不到就返回0。还有一个PATINDEX(%pattern%, str)支持通配符比如PATINDEX(%[0-9]%, str)可以找到第一个数字的位置。去空格这一块很多人只记得LTRIM和RTRIM但常常忽略中间空格。2017之后SQL Server新增了TRIM(str)函数能去掉首尾空格但去不掉中间的空格。如果你的数据是Excel导入的经常会有前后空格这时候TRIM就能派上用场。2016之前的版本只能用LTRIM(RTRIM(字段))的组合。大小写转换没什么好说的UPPER和LOWER两个函数。不过要注意做大小写敏感比较时你也可以不调用UPPER而是直接指定排序规则比如WHERE name name COLLATE SQL_Latin1_General_CP1_CS_AS这样能利用索引。还有几个冷门但好用的LEN(str)返回字符数注意不包含尾随空格但包含前导空格。这个坑也要记住LEN( abc )返回5而不是7。DATALENGTH(str)返回字节数会包含尾部空格。对中文字符尤其要小心一个中文在varchar里占2个字节在nvarchar里占2个字节但仍是一个字符。3.3 字符串函数实战拆分逗号分隔、提取数字等拆分逗号分隔字符串在旧版本SQL Server里比较痛苦因为没有现成的STRING_SPLIT2016及以上才有。早期大家都是写递归CTE或者自定义函数。现在简单了SELECT value FROM STRING_SPLIT(A,B,C,D, ,);STRING_SPLIT返回一个名为value的列默认不保证顺序。如果需要保序可以用SELECT value, ROW_NUMBER() OVER(ORDER BY (SELECT 0)) ...但严格来说它的顺序仍不保证所以如果顺序重要你要在原始数据里额外带上序号或者用JSON方式拆分。在SQL Server 2022里STRING_SPLIT增加了ordinal参数可以直接返回序号但老版本还是别指望了。提取数字是另一个常见需求。比如字段里是“订单号123456”你要把数字部分取出来。可以用PATINDEX定位第一个数字SELECT SUBSTRING(字段, PATINDEX(%[0-9]%, 字段), 6);如果数字位置不固定、长度不固定可以先用PATINDEX(%[^0-9]%, 字段)找第一个非数字字符两个位置一减就是连续数字的长度。更暴力的做法是写一个正则提取的自定义函数但正则函数在SQL Server里是CLR层面的性能和使用门槛都比较高。我对简单场景的建议是宁可多写几层嵌套的REPLACE和PATINDEX也别轻易上正则除非你能接受额外的部署和维护成本。4. 数学函数与类型转换别让计算精度坑了你4.1 ROUND、CEILING、FLOOR、ABS、POWER等数学函数看着简单用错了照样出笑话。比如ROUND的四舍五入行为其实受数据类型影响。ROUND(number, length)按指定小数位数做四舍五入。但注意ROUND返回的结果类型取决于第一个参数而且它只对数值的小数部分做舍入不会改变数值类型。比如ROUND(12.34, 1)返回12.3ROUND(12.35, 1)返回12.4。CEILING(number)向上取整到最近的整数CEILING(12.01)返回13CEILING(-12.1)返回-12。FLOOR(number)向下取整到最近的整数FLOOR(12.9)返回12FLOOR(-12.1)返回-13。ABS(number)绝对值。POWER(a, b)a的b次方。SQRT(number)平方根。RAND(seed)返回0到1之间的随机数相同的seed会得到相同的结果。想要随机整数可以FLOOR(RAND() * 100)。我特别想说一下ROUND和数据类型的问题。假设你有个数值是DECIMAL(18,2)类型字段值12.345在没有转换前其实存不下第三位小数。你在SELECT里写ROUND(12.345, 2)结果可能是12.34也可能12.35取决于编译器隐式转换的方式。最稳妥的方式是先把字段强转成高精度的DECIMAL(18,3)再去ROUND这样能得到预期的12.35。4.2 CAST与TRY_CAST、类型转换的坑类型转换是SQL Server里最容易被忽视的地雷。常见的写法CAST(expression AS target_type)标准SQL。CONVERT(target_type, expression [, style])带样式参数。TRY_CAST(expression AS target_type)2012及以上版本提供转换失败返回NULL而不是报错。TRY_CONVERT(target_type, expression [, style])同理。PARSE/TRY_PARSE把字符串解析成日期/数字基于.NET文化信息性能最差尽量少用。我踩过的坑CAST(20250115 AS DATE)是可以成功的因为SQL Server能识别yyyymmdd的字符串。但CAST(15/01/2025 AS DATE)在有些服务器上可能成功在另一些服务器上会失败这取决于会话的语言设置。所以处理非标准日期字符串我都是先用CONVERT加style显式指定格式比如CONVERT(DATE, 15/01/2025, 103)。另一类坑是隐式转换。当你把VARCHAR和NVARCHAR比较时排序规则可能导致索引失效。当你把INT和VARCHAR比较时SQL Server会把VARCHAR转成INT如果那一列有非数字字符查询直接报转换错误。这类问题排查起来非常烦最好的办法是设计表的时候就让关联字段类型一致实在不行就在查询里显式CAST到同一类型。还有一种情况是空字符串转数值。CAST( AS INT)在SQL Server里返回0而不是报错。这在从Excel导入数据时经常造成假象明明是脏数据结果转成了0后面的统计就被污染了。我处理这种字段时都是先判断LTRIM(RTRIM(字段)) 再赋值NULL或排除。4.3 数学函数实战四舍五入与百分比计算百分比计算有个经典精度坑。比如要算“完成率 完成数 / 总数”如果两个字段都是INTSQL Server的除法规则是整数除法5 / 10得到0而不是0.5。你必须在除法前把其中一个数转成小数比如SELECT 完成数 * 1.0 / 总数 AS 完成率, CAST(完成数 * 100.0 / NULLIF(总数, 0) AS DECIMAL(5,2)) AS 完成率百分比;这里用了NULLIF(总数,0)是为了防止除零错误。NULLIF在处理分母可能为0的场景下太好用了下面还会提到。向上取整在分页里的妙用如果你要“每页20条总共123条问有几页”用CEILINGSELECT CEILING(123 * 1.0 / 20); -- 返回7注意如果直接写CEILING(123 / 20)由于整数除法先算出了6CEILING结果还是6所以一定要先把另一个操作数变成小数。这个细节也是无数新手倒过的地方。随机抽样的注意事项RAND()如果放在WHERE里SQL Server会把每一行当成独立的RAND调用结果每一行的随机数都不同导致抽样结果不稳定。如果你想要一个稳定的随机序号推荐CHECKSUM(NEWID())生成每行固定但随机的值比如取10%样本SELECT * FROM Orders WHERE ABS(CHECKSUM(NEWID())) % 100 10;但这个方法在表很大时性能不太好不是所有场景都能用。真正稳定的抽样最好用TABLESAMPLE或ORDER BY NEWID() OFFSET但前者在其他版本里行为有点怪按物理页抽样后者在大表上开销巨大没有银弹。5. 聚合函数与分组统计从明细到汇总5.1 SUM、AVG、MIN、MAX、COUNT的常规用法聚合函数的作用是把多行汇总成一行的结果。最常用的五个SUM(列)求和忽略NULL只有数值类型能用。AVG(列)求平均也忽略NULL。但注意这里会有逻辑陷阱如果某一行的值为NULL它不算平均数分母但0值算。比如有三行10, NULL, 20AVG结果是15而不是10。很多报表对“缺失值”的处理有不同预期你要搞清楚业务想要的是“忽略缺失”还是“当作0”不要拿着AVG瞎用。COUNT(*)统计行数包含NULL行。COUNT(列)统计该列的非NULL值个数。COUNT(DISTINCT 列)统计去重后的非NULL值个数。MIN(列)/MAX(列)取最小/最大值适用于数字、字符串、日期NULL会被忽略。还有一个容易被忽略的SUM(DISTINCT 列)它是对去重后的值求和用的场景很少但偶尔处理一些重复统计需求时能救命。5.2 GROUP BY与HAVING的配合GROUP BY把行按分组列聚合成组。写GROUP BY的时候有两条铁律SELECT里出现的非聚合列必须出现在GROUP BY里。WHERE在分组前过滤HAVING在分组后过滤。实际写报表我习惯写成这样SELECT CustomerID, COUNT(*) AS OrderCount, SUM(OrderAmount) AS TotalAmount FROM Orders WHERE OrderDate 2025-01-01 GROUP BY CustomerID HAVING SUM(OrderAmount) 10000;WHERE先过滤了日期范围HAVING再筛掉总金额不足1万的客户。这里有个性能细节如果过滤条件针对的是明细行就放在WHERE如果针对聚合结果只能放HAVING。但HAVING通常不能利用索引所以尽量用WHERE把数据量先压下去。还要提一个容易绕进去的问题GROUP BY和DISTINCT的区分。单纯去重用DISTINCT需要分组统计时用GROUP BY。但两者在语义上并不完全等价GROUP BY还会触发聚合逻辑而且结果集的排序可能不同虽然排序从来不该被依赖。5.3 聚合函数实战统计订单、分组占比等案例一统计每天订单数和销售额并算每日占比。有几种写法。一种是用子查询先算总数然后关联SELECT CONVERT(varchar(10), OrderDate, 120) AS OrderDay, COUNT(*) AS DayOrders, SUM(Amount) AS DayAmount, CAST(SUM(Amount) * 100.0 / (SELECT SUM(Amount) FROM Orders) AS DECIMAL(5,2)) AS Pct FROM Orders GROUP BY CONVERT(varchar(10), OrderDate, 120);另一种是用窗口函数后面会讲直接算不需要两次扫描。案例二每个客户最近一笔订单。这个需求用传统的GROUP BY MAX(OrderDate)只能得到日期拿不到订单号等更多字段。所以要会配合窗口函数SELECT CustomerID, OrderID, OrderDate FROM ( SELECT CustomerID, OrderID, OrderDate, ROW_NUMBER() OVER(PARTITION BY CustomerID ORDER BY OrderDate DESC) AS rn FROM Orders ) t WHERE rn 1;这是典型的“分组后取组内排名第一”的问题。如果你只需要MAX日期那GROUP BY就够了但一旦要该行其他字段必须用窗口。5.4 窗口函数与OVER()不分组也能聚合窗口函数是SQL Server 2005之后才有的到2012以后用法越来越普及。SUM(列) OVER(PARTITION BY 分组列 ORDER BY 排序列)可以让你在不丢失明细的情况下看到每个分组内的累计值。比如累计销售额SELECT OrderDate, Amount, SUM(Amount) OVER(ORDER BY OrderDate) AS RunningTotal FROM Orders;这个RunningTotal是逐日累加的。如果你写PARTITION BY CustomerID ORDER BY OrderDate就是每个客户内部的累计。窗口函数比用子查询做累计效率高很多因为SQL Server只需要扫描一次数据。常用窗口函数有四类聚合窗口SUM() OVER()、AVG() OVER()、COUNT() OVER()排序窗口ROW_NUMBER() OVER()、RANK() OVER()、DENSE_RANK() OVER()偏移窗口LAG(列, n)向前取第n行LEAD(列, n)向后取第n行取值窗口FIRST_VALUE(列)、LAST_VALUE(列)RANK和DENSE_RANK的区别RANK在并列名次后会跳过名次比如两个第1名下一个是第3名DENSE_RANK不跳下一个是第2名。用了这么多年我做报表更常用DENSE_RANK因为名次连续性更直观。LAG常用的场景是计算环比。比如拿今天的订单金额和前一天的比SELECT OrderDate, Amount, LAG(Amount, 1, 0) OVER(ORDER BY OrderDate) AS PrevAmount, Amount - LAG(Amount, 1, 0) OVER(ORDER BY OrderDate) AS Diff FROM Orders;LAG第三个参数是默认值没有前一条记录时返回0避免NULL。窗口函数里的ORDER BY只是定义窗口内的顺序不会改变结果的物理顺序要保证输出顺序还是得在外面加ORDER BY。6. 系统函数与元数据让脚本更灵活6.1 ISNULL、COALESCE、NULLIF处理空值空值处理是SQL项目里绕不开的事。三个常用函数我做个对比ISNULL(expr, replacement)如果expr是NULL返回replacement。它只接受两个参数且返回类型更偏向第一个参数的类型。COALESCE(expr1, expr2, ...)返回参数列表中第一个非NULL值。可以传多个参数标准SQL写法。NULLIF(expr1, expr2)如果两个参数相等返回NULL否则返回第一个参数。最经典的用法在前面提到的除以0防范中NULLIF(分母, 0)。ISNULL和COALESCE看起来像但有个差异值得注意ISNULL会在类型推断时做更多隐式转换。比如ISNULL(intColumn, 0)的结果类型就是int而COALESCE(intColumn, 0)会尝试按第一个参数的类型做推导在某些复杂场景下可能会有略微不同的类型或精度。实际写代码我倾向用COALESCE因为它的行为更标准而且参数不受两个限制。NULLIF还有个妙用统计“非零值被当作NULL排除”的聚合场景。比如要统计某个指标里不为0的平均值可以直接AVG(NULLIF(列, 0))因为NULL会被AVG忽略0则会影响均值。这个写法比AVG(CASE WHEN 列 0 THEN 列 END)简洁不少。6.2 CASE WHEN表达式严格来说CASE WHEN不是函数而是表达式。但它在实际中的使用频率堪比函数。它的两种写法简单表达式CASE 列 WHEN 值1 THEN 结果1 WHEN 值2 THEN 结果2 ELSE 结果N END搜索表达式CASE WHEN 条件1 THEN 结果1 WHEN 条件2 THEN 结果2 ELSE 结果N END大部分场景推荐用搜索表达式因为它更灵活条件可以是范围、子查询、多列组合。CASE WHEN在SELECT列里做“列转行”非常常用比如SELECT Region, SUM(CASE WHEN OrderMonth 2025-01 THEN Amount END) AS JanAmount, SUM(CASE WHEN OrderMonth 2025-02 THEN Amount END) AS FebAmount FROM Orders GROUP BY Region;这就是行转列的雏形。注意我在THEN后面没写ELSE当条件不满足时CASE返回NULL而SUM会忽略NULL所以不会有影响。如果你用COUNT(CASE...THEN 1 END)注意COUNT(列)不数NULL但如果写COUNT(CASE WHEN... THEN 1 ELSE NULL END)NULL被忽略可以被当作条件计数来用。不过更直观的是用SUM(CASE WHEN 条件 THEN 1 ELSE 0 END)这样统计值更明确也不会漏数。CASE WHEN在UPDATE语句里也能做条件更新比如工资分级更新UPDATE Employee SET Salary CASE WHEN Salary 5000 THEN Salary * 1.1 WHEN Salary 10000 THEN Salary * 1.05 ELSE Salary * 1.02 END;一次UPDATE操作就完成多级调整避免写多个UPDATE语句导致中间状态不一致。6.3 系统函数与元数据DB_NAME、OBJECT_NAME等有时候写动态SQL或者做自动化运维脚本需要知道当前数据库名、对象名、行数之类的信息。几个实用的系统函数DB_NAME(database_id)当前数据库名也可以根据ID查名字。OBJECT_NAME(object_id)根据对象ID找表名或视图名。OBJECT_ID(schema.table)根据表名取对象ID。ROWCOUNT上一条语句影响的行数。ERROR上一条语句的错误号0表示无错不过新版有THROW和TRY...CATCH不太需要用这个。SCOPE_IDENTITY()返回当前会话当前作用域内最后一个插入的自增ID。很多人问它和IDENTITY的区别区别在于IDENTITY可能被触发器里的插入覆盖SCOPE_IDENTITY()不会所以请永远用SCOPE_IDENTITY()。这些函数在写存储过程的时候特别有用。比如你要对一个表做INSERT后马上拿新ID去插入子表必须用SCOPE_IDENTITY()。有些老项目用了SELECT MAX(ID)1并发下必出问题千万别学。7. 常用函数避坑指南与调试技巧7.1 函数性能问题避免在WHERE中使用函数这是我在一线踩坑最多的地方。很多人在WHERE条件里对列应用函数比如SELECT * FROM OrderDetail WHERE CONVERT(varchar(10), OrderDate, 120) 2025-01-15;这个写法看起来没问题但致命的是它让OrderDate列上的索引完全失效。因为每一行都要先做转换再比较SQL Server只能全表扫描。正确写法是SELECT * FROM OrderDetail WHERE OrderDate 2025-01-15 00:00:00 AND OrderDate 2025-01-16 00:00:00;这是一个半开区间精确命中仅当天的所有记录还能用索引。类似的WHERE YEAR(OrderDate) 2025也会让索引失效应该写成OrderDate 2025-01-01 AND OrderDate 2026-01-01。我的原则是能对常量做函数就别对列做函数。还有一个容易被忽略的隐式转换性能问题假设你有个VARCHAR列存手机号查询时传入了INT类型参数SQL Server会把整个列转成INT去比较索引也失效了极端情况下还会报转换错误。排查这种问题时先检查一下参数类型和列类型是否一致。7.2 隐式转换与排序规则地狱隐式转换带来的坑五花八门。我举一个真实案例有个订单表订单号字段是VARCHAR(50)实际存的都是纯数字后来数据量大了用订单号去关联另一个表时另一个表里该字段是INT类型。一执行查询慢得不可接受而且偶尔报错“将varchar转换为int时失败”。原因是某些订单号里混入了个别字母导致不能转换。最后只能把那个表字段也改成VARCHAR并统一清理脏数据。排序规则的问题更隐蔽。数据库实例默认排序规则是SQL_Latin1_General_CP1_CI_AS不区分大小写但有些表的字段用了Chinese_PRC_CI_AS中文不区分大小写的排序规则。当这两个字段做JOIN时SQL Server会尝试让排序规则兼容通常临时转换一边但这可能阻止索引使用。如果你遇到“JOIN突然很慢”“字符串比较结果不对”这种诡异的事先检查两边字段的COLLATION是否一致。不一致可以用COLLATE DATABASE_DEFAULT来强转SELECT ... FROM A INNER JOIN B ON A.Code B.Code COLLATE DATABASE_DEFAULT;但这只是临时方案根因还是设计时统一COLLATION。7.3 自定义函数的安全与限制SQL Server里自定义函数分三种标量函数返回单值、表值函数返回表、多语句表值函数。里面坑不少我挑几个重点标量函数会严重拖慢查询。如果你写一个自定义标量函数然后在SELECT列表里调用它SQL Server往往会对每一行都调用一次函数而且函数内部无法利用表索引性能堪比逐行计算。所以能用内联表达式解决的就别包一层函数。实在要复用逻辑优先考虑内联表值函数inline TVF它会像视图一样展开执行计划。函数内不能用动态SQL、不能有副作用不能用INSERT/UPDATE/DELETE除了少数特定场景。这个约束让很多想用函数做循环操作的开发者很痛苦。遇到要修改数据的需求请改用存储过程。系统函数与用户自定义函数的冲突。不要起名为fn_系统名或者跟系统函数重名否则调用时可能有歧义而且一旦有依赖关系后面改起来特别麻烦。命名习惯我建议用ufn_前缀函数名尽量表明用途。调试技巧。如果函数结果不对先用简单参数单独调用检查每个中间变量。也可以把函数体里的核心逻辑单独复制出来跑排除权限、排序规则等干扰。SQL Server Management Studio里可以直接用“函数脚本”生成CREATE语句执行后马上用SELECT dbo.ufn_test(...)验证别等构建完整个存储过程再查。8. 关于函数使用习惯的个人建议整理到这儿我回顾了一下自己这些年写的SQL发现一个规律用得好的人不是背的函数最多而是对数据行为理解最深。比如知道NULL对聚合的影响、知道格式转换不只是显示问题还影响性能、知道分组和窗口的区别这些比记住100个函数都值钱。希望大家收藏这份速查的时候也顺带把我提的“为什么”想一遍。我实操中还有个小习惯分享一下在任何写日期和字符串处理的脚本里都先用一个临时查询验证LANGUAGE、DATEFIRST及字段COLLATION因为这三个设置会悄悄改变函数的行为。特别是维护老系统时不要假设所有环境都是默认配置。如果这篇文章里的某个写法在你们的测试环境里结果不对不妨朝这些方向查。SQL Server的函数世界并不复杂复杂的是它和真实环境之间的各种边界条件。把这些边界摸清楚你的SQL水平自然就上了一个台阶。