做 SQL Server 开发的人,几乎没有不跟 CONVERT 打交道的。尤其是日期转换,“CONVERT(varchar, GETDATE(), 23)”这种写法在公司的存储过程、日常查询、数据导出脚本里到处都是。可说实话,真正能把这个函数讲清楚、用明白的人并不多。大多数情况是:抄来一个格式能用,换一个日期类型或者换一台服务器就报错,报错之后只能一个个试 style 参数,试出一个看着对的就交差。这篇文章我想站在一个多年写 SQL 的开发者角度,把“SQL Server 中 CONVERT 函数转换日期”这件事从头到尾说透,包括 style 参数怎么选、哪些场景容易踩坑、怎么写查询才不影响性能。不管你是刚入门的新人,还是被日期格式坑过好几次的熟手,都能从里面找到直接能用的东西。
1. CONVERT函数到底在做什么?先弄懂转换的本质
1.1 三个参数,每个参数到底在管什么
CONVERT 的完整语法是:
CONVERT(data_type[(length)], expression[, style])第一个参数是目标类型,第二个参数是源表达式,第三个参数 style 是可选参数,也是日期转换中最关键、最容易被忽略的一个。很多人把 CONVERT 当“格式化工具”用,记住几个固定写法就完事,这其实没搞清楚它真正的执行逻辑。
日期在 SQL Server 内部并不是人眼看到的“2025-06-01 14:30:00”,而是一组数值结构。datetime 类型内部表示其实是一个从 1900-01-01 开始计算的天数和小数部分组合,date、datetime2、smalldatetime 也各有各的存储规则。也就是说,当我们说“转换日期”时,本质上是在做翻译:把内部结构翻译成另一种类型或一种字符串呈现方式。可以拿成绩单来类比:同一场考试的成绩,内部存储是分数,你可以选择把它显示成百分制还是等级制。CONVERT 的 style 参数,就是决定“按哪种方式显示”的选项。
为什么必须要有这个函数?大概有三个原因。第一,数据源拿到的可能是一段日期字符串,比如接口传过来的“2025-06-01”,要和表里的日期字段比较或插入,必须先转成日期类型。第二,报表、导出脚本需要把日期列格式化成固定字符串,否则用户看到的是“Jun 1 2025 10:30AM”这种很难受的默认值。第三,拼文件名、流水号时需要“20250601”这样的字符串,日期类型本身做不到,必须转一次。
还有一个容易忽略的点:style 不仅对“日期转字符串”有用,对“字符串转日期”同样有意义。很多转换失败都出在这里。比如 CONVERT(datetime, '15/06/2025', 103),这里的 103 就是在告诉 SQL Server:这段字符串按“日/月/年”来读。如果不指定,SQL Server 会按当前会话的 DATEFORMAT 去猜,猜错了就报错。理解这一点,后面很多坑都能避开。
1.2 style 参数速查:一张表看懂常用格式
SQL Server 的 style 参数看起来像玄学,其实背后有规律可循。编号在 0-14 之间的是旧风格,年份只有两位;编号在 100-114 之间的是对应的新风格,年份是四位。比如 style 1 是 mm/dd/yy,style 101 是 mm/dd/yyyy;style 20 和 style 120 在视觉上很接近,但一个来自旧编号体系,一个来自新编号体系。既然处理现代场景,我们基本只用 100 以上的风格或者几个特定编号。
下面这张表我整理的是实际工作中最高频的几个:
| style | 输出样例 | 说明 |
|---|---|---|
| 20 | 2025-06-01 14:30:00 | 日期时间,24小时制,四位年份 |
| 21 | 2025-06-01 14:30:00.123 | 日期时间,带毫秒 |
| 23 | 2025-06-01 | 只有日期部分 |
| 108 | 14:30:00 | 只有时间部分 |
| 112 | 20250601 | 紧凑日期,适合文件名、流水号 |
| 120 | 2025-06-01 14:30:00 | 与20显示效果相似,新代码更常见 |
| 121 | 2025-06-01 14:30:00.123 | 与21显示效果相似,新代码更常见 |
| 101 | 06/01/2025 | 美式格式,月/日/年 |
| 103 | 01/06/2025 | 英式格式,日/月/年 |
你可以把这个表存在手边,绝大多数转换需求就是从中选一个编号的事。需要注意的是,style 只有配合“字符串目标类型”时才发挥完整的格式化效果;如果目标类型是 datetime、date 这类日期类型,style 的作用主要是影响字符串解析规则或在小数秒上有细微表现,而不是把结果显示成某个样式。不少新手在这里理解错了,以为 CONVERT(date, 某列, 23) 能强行得到“2025-06-01”这种字符串,最后发现值类型还是 date,显示的时候照样带时间或按客户端默认格式展示。
2. 实战场景:把你的日期换成想要的样子
2.1 标准日期字符串 yyyy-MM-dd,最常用的 23 号 style
我写存储过程时,用得最多的就是 style 23。SELECT CONVERT(varchar(10), GETDATE(), 23),结果就是“2025-06-01”。这个格式在行业里算通用,日志文件名、接口报文、报表查询条件、导出 Excel 的单元格,几乎都认它。
这里有两个细节要提醒。第一,varchar 的长度尽量写明确,不要只写 CONVERT(varchar, GETDATE(), 23)。虽然例子很短看不出问题,但一旦后续代码改动、把 GETDATE() 换成带时间的 datetime 列,varchar 的默认长度可能不够,截断行为会变得不可控。我通常写成 varchar(10),因为 yyyy-MM-dd 正好是 10 个字符,写大了反而容易掩盖问题。
第二,不要把 style 23 的转换结果再拿去存到 datetime 列里。我见过有同事把“2025-06-01”这种字符串当作标准值反存回日期字段,理由是“这样干净”。但这种做法会让所有后续查询都多一次字符串和日期之间的隐式转换,得不偿失。正确的做法是:存储层只保留日期类型,只是在展示、拼接、输出时再做 CONVERT。
2.2 紧凑日期 yyyyMMdd,用于文件名和流水号
另一个高频场景是拼文件名、批次号、分区表后缀等,这时候需要不加横杠的“20250601”。style 112 可以直接输出这个格式:SELECT CONVERT(varchar(8), GETDATE(), 112)。
不过我个人的习惯是优先用 REPLACE 的方式:
SELECT REPLACE(CONVERT(varchar(10), GETDATE(), 23), '-', '')为什么绕一下?因为 style 112 在底层列类型是 datetime 时表现稳定,但如果哪天表结构里把列改成了 datetime2,部分场景下 112 的输出行为和 datetime 不完全一致,结果里可能带出时间部分或者格式走样。用 23 先转成最干净的日期字符串,再统一去掉分隔符,不管底层是 date、datetime 还是 datetime2,输出都稳定可靠。这个“绕”不是没有道理的。
实际项目里最常见的是拼文件名和导出文件名:
SELECT 'order_' + REPLACE(CONVERT(varchar(10), GETDATE(), 23), '-', '') + '.csv' -- order_20250601.csv如果你是做批处理任务,还可以把这个字符串当作任务标识、日志标记,避免日志和文件名格式混乱。记住一个原则:凡是要参与拼接的日期,一定先转成字符串,不要依赖隐式转换。
2.3 只要日期、只要时间:先转类型,再定格式
经常会遇到“我只要日期部分”“我只要时间部分”的需求,比如统计截止到当天、或者某个班次的时间段。最容易想到的办法是 CONVERT(varchar(10), 日期时间列, 23),确实能取到日期字符串,但如果你想继续做日期运算,比如加一天、比较大小,字符串就不方便了。这时候更推荐先转类型:
SELECT CONVERT(date, GETDATE()) -- 得到 date 类型,只有日期 SELECT CONVERT(time, GETDATE()) -- 得到 time 类型,只有时间先转成 date 类型,再按需要做 CONVERT 格式化,这个顺序比直接截字符串更安全。举个例子,你想筛出“今天创建的订单”,如果 CreateTime 是 datetime,最稳妥的写法不是 CONVERT(varchar(10), CreateTime, 23) = 某个字符串,而是先把变量转成 date 再比较,或者用范围查询。这个在后面的性能部分会展开讲。
还有一个老写法:CONVERT(datetime, CONVERT(varchar(10), 日期, 23)),这种写法在旧代码里随处可见,本质上是先转字符串再转回日期,绕了一大圈。放到今天,直接用 CONVERT(date, 日期) 就能达到同样目的,没必要再扛着这个历史包袱。我遇到这类老代码都会顺手改掉。
3. style参数背后的语言与文化差异,最容易出问题的一块
3.1 同一个style,换个环境结果可能完全不同
这一节是很多人的盲区。你会不会遇到过这种诡异现象:同一个 CONVERT 语句,在自己电脑上跑得好好的,部署到生产服务器上结果就变了,甚至直接报“从字符串转换日期和/或时间时,转换失败”。问题往往不是函数写错了,而是登录语言的默认设置不同。
SQL Server 的 style 编号不是在所有语言环境下都有一致表现。典型的如 style 6,会输出“Jun 1 2025”这种英文月份缩写;你在中文环境里看很不习惯,但服务器默认语言是英文的话,转换出来的样子就是这样。反过来,如果某段代码靠默认 style 输出,换到不同语言环境的服务器,格式也可能跟着变。
更麻烦的是字符串转日期的方向。同样一段字符串“15/06/2025”,在很多语言环境下会被按“月/日/年”解析,15 被当成月份直接报错;在另一些环境下按“日/月/年”解析,能正常转成 2025 年 6 月 15 日。CONVERT(datetime, '15/06/2025', 103) 之所以要带 103,就是为了明确告诉 SQL Server“这里不是美式格式,是日/月/年”。
如果你在写一些给内部系统用的公共查询,我的建议是:日期字符串统一用 yyyyMMdd,比如“20250601”。这是 SQL Server 解析字符串时兼容性最好的一个格式,基本不受 DATEFORMAT 和语言设置影响。其次就是用带横杠的“yyyy-MM-dd”配合 style 23,但这种写法在个别语言环境下仍有可能被以其他顺序解析,没那么绝对安全。能避开就避开。
3.2 团队规范比函数本身更重要
比起纠结 style,团队里更大的问题是五花八门混用。我见过一个项目里,日期格式同时存在 style 101、103、120、23 四种写法,导出的 CSV 有时候“06/01/2025”,有时候“01/06/2025”,下游同事根本分不清到底是 6 月 1 日还是 1 月 6 日。这种混乱不是函数造成的,是规范缺失。
我的建议很简单:一张表一条规范。日期型字段在 SQL 结果里统一转成“yyyy-MM-dd”,日期时间型字段统一转成“yyyy-MM-dd HH:mm:ss”,跨系统传输或拼接文件名时统一用“yyyyMMdd”。代码里凡是 CONVERT 日期,都显式写出 varchar 长度和 style,并在旁边加一行注释说明为什么选这个 style。这样后来人接手时不用猜,也不会自己再造一个格式出来。
另外要小心一些“长得像”的 style。style 20 和 120 看起来都是“yyyy-mm-dd hh:mi:ss”,style 21 和 121 都带毫秒,新的项目里用哪个都行,但团队统一选一个就好,不要这次写 120,下次写 21,相互改代码的时候容易看漏。这种细节维护成本不高,但收益很大。
3.3 CAST 和 CONVERT,什么时候用哪个
SQL Server 里还有一个 CAST 函数,很多人纠结到底用哪个。其实选择标准很清楚:
CAST 是标准 SQL 函数,写法是 CAST(expression AS data_type),跨数据库迁移时更通用,但它没法指定格式。CONVERT 是 SQL Server 专属,最大的优势就是带 style 参数,可以控制日期格式。所以结论是:如果你只是想把一个类型变成另一个类型,比如把 varchar 数字转成 int,用 CAST;如果你想把日期变成指定样式的字符串,就必须用 CONVERT。
-- 类型转换,用 CAST 就够 SELECT CAST('123' AS INT) -- 日期格式化,必须用 CONVERT SELECT CONVERT(varchar(10), GETDATE(), 23)还有一个常见误解是“CONVERT 比 CAST 性能好”。实际上在日期转换这个场景,两者性能差异可以忽略不计,真正的性能杀手是把转换函数用在索引列上,这个下面专门讲。
4. 性能与替代方案:别让CONVERT拖垮你的查询
4.1 WHERE条件里不要包住日期列
这是我在代码评审里最常指出的问题。很多开发写“查某天数据”时习惯写成:
SELECT * FROM orders WHERE CONVERT(varchar(10), CreateTime, 23) = '2025-06-01'这个写法结果可能没错,但它会让 CreateTime 上的索引完全失效。原因很简单:SQL Server 要比较“转换后的值”和“2025-06-01”,可转换动作是作用在每一行上的,优化器没法直接按索引定位,只能把整张表的行都读出来,逐行做 CONVERT 再比对。数据量一大,这个查询就能把数据库拖垮。
正确的姿势是用范围查询:
SELECT * FROM orders WHERE CreateTime >= '2025-06-01 00:00:00' AND CreateTime < '2025-06-02 00:00:00'这样写,优化器知道要在一段连续区间里找数据,可以走索引 seek。我会在团队评审时把这条作为硬性规范:任何日期字符串比较,都不允许把 CONVERT 包在列上,必须用范围条件。哪怕现在表很小、感觉无所谓,等数据量上来了再想改,代价就大了。
4.2 查某天数据:用 >= 当天 且 < 次日,别用 =
既然提到范围查询,顺便把“查某天”这个经典场景说透。假设订单表 CreateTime 是 datetime 类型,有人这样写:
SELECT * FROM orders WHERE CreateTime = '2025-06-01'看到这里你可能会觉得没问题,但实际上它查不到 6 月 1 日当天大多数记录。为什么?因为 CreateTime 是 datetime,包含时分秒,等于号要求变量的值精确匹配到某个时刻。“2025-06-01” 会被隐式转换成 2025-06-01 00:00:00.000,只有恰好在这个时刻创建的订单才匹配,其他时间点全部漏掉。
更隐蔽的是 datetime 的精度特性。datetime 的精度是 3.33 毫秒,存储时会把时间舍入到 0、0.003、0.007 秒这样的间隔,有些边界值甚至会出现 23:59:59.997 这样的数据。如果你拿列的最大值去设条件,很容易在边界上漏数据。所以查某天数据,最稳妥的写法永远是半开区间:大于等于当天零点,小于次日零点。这个习惯能根治很多莫名其妙的“少一天”问题。
如果列本身是 date 类型,直接等于某个日期是可以的,因为 date 没有时间部分。但为了兼容旧表、避免以后改表类型就翻车,我还是会建议团队统一用范围写法。统一的好处是:改变量时不用再考虑列类型。
4.3 FORMAT 函数看着香,但性能代价不小
SQL Server 2012 之后多了一个 FORMAT 函数,写法非常漂亮:
SELECT FORMAT(GETDATE(), 'yyyy-MM-dd')它之所以诱人,是因为可以用 .NET 的格式字符串,想要什么样式几乎随手就能写,不用记 style 编号。但代价是性能。FORMAT 底层通过 CLR 调用 .NET 的格式化逻辑,开销比 CONVERT 大得多。在十万、百万行级别的数据里跑一下对比,差距非常直观,用 FORMAT 的查询慢上一大截是常事。
我的使用建议很明确:临时脚本、报表控件、几次性的数据修复,随便用 FORMAT 无所谓;但存储过程里高频执行的路径、大批量循环、需要和 JOIN 配合的场景,一律用 CONVERT。扩展性和性能都要兼顾,就别把 FORMAT 当成默认选择。我见过有同事用 FORMAT 给整个报表所有日期列做格式化,最后报表接口慢到被下游投诉,改成 CONVERT 后立刻恢复。工具本身不分好坏,放在合适的地方才有价值。
5. 常见问题速查:那些年我们踩过的日期坑
5.1 “从字符串转换日期和/或时间时,转换失败”
这个报错基本是 SQL Server 开发者的入门必修课。常见原因有三个:字符串格式和会话的 DATEFORMAT 不匹配;日期本身不合法,比如 2025-02-30;字符串里混入了中文或其他语言环境下的星期表示。排查时别急着改代码,先看当前会话的语言和日期格式:
SELECT @@LANGUAGE, @@DATEFORMAT如果是“15/06/2025”这种格式,直接加上 style 103 转换,或者干脆把输入改成“20250615”。我还有一个排查技巧:用 TRY_CONVERT 代替 CONVERT,转换失败时它返回 NULL 而不是抛错,可以快速定位是哪条记录有问题:
SELECT CreateTimeString, TRY_CONVERT(date, CreateTimeString, 103) AS ConvertedDate FROM staging_table WHERE TRY_CONVERT(date, CreateTimeString, 103) IS NULL这样能直接把“坏数据”捞出来,比对着报错日志猜快得多。跑完这批坏数据,你才能真正理解:报错很多时候不是函数的问题,是上游数据的质量在捣乱。
5.2 没指定 style,得到“Jun 1 2025 10:30AM”这类怪格式
很多新手第一次执行 CONVERT(varchar, GETDATE()) 时会懵掉,因为结果不是想要的“yyyy-mm-dd hh:mi:ss”,而是类似“Jun 1 2025 10:30AM”的英文风格。原因是没指定 style 时默认按 style 0 处理,而 style 0 恰好是这种带英文月份缩写的写法,并且还会受服务器默认语言影响。
解决办法是显式指定 20、23、120 这些 style,不要省略。写 SQL 时省略 style,等于把格式的控制权完全交出去,生产环境、测试环境和本地机器的语言设置稍有差异,结果就会不一致。显式指定 style 是成本最低的稳定方案。我在团队里有一条不成文规定:CONVERT 日期时,没有 style 的写法不许提交。
5.3 拼接字符串时日期变成了“2025-06-01 00:00:00.000”
这个问题常见于把日期和字符串拼接。比如想拼一句“创建时间:2025-06-01”,写成了:
SELECT '创建时间:' + CreateTime你会得到“创建时间:2025-06-01 00:00:00.000”或者直接报错,因为字符串不能直接和 datetime 拼接,SQL Server 会做隐式转换,而这个隐式转换默认用的是当前语言环境下的完整日期时间格式。要避免这个问题,先把日期转成字符串再接:
SELECT '创建时间:' + CONVERT(varchar(10), CreateTime, 23)这个坑的变种还特别多,比如把 date 类型的值放到 XML、JSON、CSV 里拼接,只要没先转字符串,都会出现格式不可控的情况。凡是日期要参与字符串操作,第一件事永远是 CONVERT 成字符串并指定 style。
5.4 排序规则与隐式转换引起的“怪现象”
最后聊一个不太起眼但很麻烦的问题:排序规则。当你把日期转成 varchar 后去和其他字符串列比较、关联时,可能因为两个字段的 Collation 不同,出现“无法解决排序规则冲突”的报错,或者明明看着一样的字符串,JOIN 不上。这在跨库查询、临时表和正式表关联时尤其常见。
我的建议是,日期相关字段能存成 date/datetime 就绝不要存成 varchar。有些项目为了“显示方便”,把日期直接存成 yyyy-MM-dd 字符串,短时间看确实省了转换,但后面要排序时会发现字符串排序和日期排序完全两码事,要按月、按年分组时更是痛苦,还得再转换回来。存储层的日期就该用日期类型,展示层的格式化交给查询或应用层,不要让一个字符串字段背负双重职责。
6. 一些我自己的使用习惯
写了这么多,最后分享几个我这么多年调 SQL 总结下来的小习惯。
第一,需要日期字符串的存储过程,我通常在最前面定义一个统一变量。比如:
DECLARE @todayStr varchar(10) = CONVERT(varchar(10), GETDATE(), 23)后面的逻辑全部引用这个变量,不要在每个语句里重复写 CONVERT。好处是整个存储过程只有一个地方控制日期格式,哪天要改成 yyyyMMdd 或加上时间,只改一行。
第二,凡是从外部接口、文件、Excel 里接收日期字符串,一定先做合法性校验。要么用 TRY_CONVERT 先过滤,要么把字符串统一转成 yyyyMMdd 再进入业务流程。上游数据“看着是日期”和“真是日期”是两回事,让异常数据尽早暴露在导入环节,而不是等业务计算结果出来才发现,能省太多排查时间。
第三,能不用 CONVERT 的地方就尽量不用。查询条件里需要的是日期范围,就直接写范围;展示层需要格式化,尽量放在报表端或程序端做。SQL 里过度使用转换函数,不仅让语句变长,还容易引入隐式转换和索引失效的隐患。这个取舍做对了,查询性能通常不会差。
日期转换这件事本身不复杂,但细节非常多。希望这篇文章能帮你少走一些弯路,至少在下次遇到 CONVERT 报错的时候,不用再一脸茫然地试风格编号了。