
先讲一个我自己的经历。去年年底帮一个团队排查报表问题两张表 JOIN 之后数据对不上明明都是同一个业务单号结果做聚合的时候差了上千条。查到最后原因特别“低级”A 表的时间字段是 datetimeB 表的时间字段是 varchar存的是“2025-01-02 14:30:00”两边 JOIN 条件里直接写等号数据库在后台做隐式转换把 A 表的 datetime 全部转成了字符串去匹配。转换本身没报错但数据格式肉眼看着一样实际却包含了不同的尾部空格和精度差异匹配自然就丢了。最后统一改成显式转换问题立刻消失。这个案例让我感触很深——SQL 里的日期和字符串互转绝大多数人觉得“不就是 CAST 一下嘛”可恰恰是这种“太简单”的地方埋着最多的雷。今天我把这几年在 SQL Server、MySQL、PostgreSQL、Oracle 上踩过的坑和整理过的写法完整梳理一遍希望能让正在写 SQL 的人少走点弯路。1. 日期转字符串三大数据库的格式化语法对比日期转字符串是数据导出、报表标题、日志文件名、前后端接口传参里最频繁的操作。但“转成字符串”这句话本身就太模糊了——你要的是“2025-06-15”还是“20250615”还是“June 15, 2025”还是“2025-06-15 14:23:45.123”不同的库语法体系完全不一样如果用混了那真是会当场傻眼。1.1 SQL Server 的 CONVERT 与 FORMAT别做只会写 CAST 的人很多 SQL Server 新手写日期转字符串第一反应是CAST(GETDATE() AS VARCHAR)。这条语句能跑但结果取决于服务器的语言设置和日期格式你大概率会得到类似“Jun 15 2025 2:23PM”这种既不像人能读的、也不好排序的字符串。所以我在 SQL Server 里几乎从来不用裸CAST做日期转字符串而是用CONVERT加格式码。-- 最常用的三个格式码 SELECT CONVERT(VARCHAR(10), GETDATE(), 120) AS date_str; -- 2025-06-15 SELECT CONVERT(VARCHAR(19), GETDATE(), 120) AS datetime_str; -- 2025-06-15 14:23:45 SELECT CONVERT(VARCHAR(8), GETDATE(), 112) AS compact_str; -- 20250615格式码 120、121、112 这三个我基本背熟了120 是yyyy-mm-dd hh:mi:ss121 是带毫秒的完整格式112 是纯数字的yyyymmdd。如果要跟导出文件的文件名配合我经常用 112如果要写日志或接口用 120 最稳妥。SQL Server 还支持 101美式、103英式、110月日年等等一堆格式码但说实话真正生产环境里用得最多的就是 120 和 112。SQL Server 2012 之后又多了一个FORMAT函数SELECT FORMAT(GETDATE(), yyyy-MM-dd) AS date_str; -- 2025-06-15 SELECT FORMAT(GETDATE(), yyyy年MM月dd日 dddd) AS cn_str; -- 2025年06月15日 星期日FORMAT写起来非常直观而且能直接输出“2025年06月15日 星期日”这种本地化文案这是CONVERT完全做不到的。但它的底层调用的是 .NET 的格式化引擎性能比CONVERT差一个量级。我曾经对一个 10 万行结果集做FORMAT执行时间从几十毫秒直接飙到 2 秒多。所以我的原则是少量数据、对可读性有要求的场合用FORMAT大批量导出、ETL 流水线里还是老老实实用CONVERT 格式码。1.2 MySQL 的 DATE_FORMAT百分号说明符要记牢MySQL 没有CONVERT那一套格式码它的日期转字符串走的是DATE_FORMAT函数格式说明符以百分号开头。这个设计跟 C 语言的printf有点神似跟 SQL Server 完全是两套世界观。SELECT DATE_FORMAT(NOW(), %Y-%m-%d) AS date_str; -- 2025-06-15 SELECT DATE_FORMAT(NOW(), %Y-%m-%d %H:%i:%s) AS datetime_str; -- 2025-06-15 14:23:45 SELECT DATE_FORMAT(NOW(), %Y%m%d) AS compact_str; -- 20250615这里有几个特别容易写错的点分钟是%i不是%m秒是%s24 小时制小时是%H12 小时制是%h月份是%m分钟和月份很容易搞混。我见过不下三次同事把分钟写成%m结果出来的“时分秒”里冒出一个“月份”来比如14:06:45变成了14:06:45之外的奇怪值。%Y是四位年份%y是两位年份导数据的时候如果年份变成25而不是2025多半是把%y当%Y用了。另外 MySQL 也支持从日期里直接提取部分内容比如YEAR、MONTH、DAY、HOUR、MINUTE、SECOND、DAYOFWEEK这些函数。它们返回的是数值不是字符串。如果想做“今天是这个月的第几天”或者“按月份分组”用提取函数比先转字符串再截取要干净得多也能少走一层转换。1.3 PostgreSQL 和 Oracle 的 TO_CHAR一个家族的习惯PostgreSQL 和 Oracle 在日期格式化上走的同一条路都用TO_CHAR模式串的风格又和上面两家完全不同四位年份是YYYY月份是MM日是DD24 小时制小时是HH24分钟是MI秒是SS。-- PostgreSQL 示例 SELECT TO_CHAR(NOW(), YYYY-MM-DD) AS date_str; -- 2025-06-15 SELECT TO_CHAR(NOW(), YYYY-MM-DD HH24:MI:SS) AS datetime_str; -- 2025-06-15 14:23:45 -- Oracle 写法一致只是把 NOW() 换成 SYSDATE SELECT TO_CHAR(SYSDATE, YYYY-MM-DD) FROM dual;这个家族还有一个常用的EXTRACT/EXTRACT(YEAR FROM ...)用来取年、月、日、星期几等数值。PostgreSQL 的TO_CHAR功能非常丰富比如TO_CHAR(NOW(), FMMonth DD, YYYY)能输出“June 15, 2025”FM前缀可以去掉前导空格。不过“丰富”的另一面是容易踩坑——MM是月份MI才是分钟一旦把分钟写成MI写成MM出来的时间就是错的。这和 MySQL 的%i问题如出一辙不同数据库用不同符号表达同一个概念真是考验记忆力。1.4 跨库对比格式代码差异与我的选择为了让大家一眼看明白我把最常见的四种需求跨库列个对照表需求SQL ServerMySQLPostgreSQL / Oracle年-月-日CONVERT(VARCHAR(10), GETDATE(), 120)DATE_FORMAT(NOW(), %Y-%m-%d)TO_CHAR(NOW(), YYYY-MM-DD)年-月-日 时:分:秒CONVERT(VARCHAR(19), GETDATE(), 120)DATE_FORMAT(NOW(), %Y-%m-%d %H:%i:%s)TO_CHAR(NOW(), YYYY-MM-DD HH24:MI:SS)纯数字CONVERT(VARCHAR(8), GETDATE(), 112)DATE_FORMAT(NOW(), %Y%m%d)TO_CHAR(NOW(), YYYYMMDD)月份英文名FORMAT(GETDATE(), MMMM)DATE_FORMAT(NOW(), %M)TO_CHAR(NOW(), Month)跨库经验多了之后我的个人选择非常明确只要不是必须用本地化文案的场合日期转字符串一律统一成YYYY-MM-DD或YYYY-MM-DD HH24:MI:SS这种 ISO 风格。原因是这个格式有两个天然优势第一按字符串排序时字典序和时间序完全一致可以放心用字符串排序代替日期排序第二各个数据库对它的解析都最不容易产生歧义后面讲字符串转日期时这一点会体现得非常明显。2. 字符串转日期解析方向才是真正的难点日期转字符串方向很明确拿着格式码去格式化就行。字符串转日期就难多了——同样是2025-01-02有的库认为是 1 月 2 日有的库在特定语言环境下会认为是 2 月 1 日这不是数据库“笨”而是字符串本身缺少语义信息。所以字符串转日期的核心不是“怎么转”而是“你告诉数据库怎么解读它”。2.1 显式转换四种解析语法先看显式转换的家族谱系。SQL Server 支持CAST、CONVERT、PARSE三套-- CAST 依赖会话默认格式不建议直接用 SELECT CAST(2025-06-15 AS DATE) AS d1; -- CONVERT 指定格式码最推荐 SELECT CONVERT(DATE, 2025-06-15, 120) AS d2; SELECT CONVERT(DATETIME, 2025-06-15 14:23:45, 120) AS d3; -- PARSE 基于 .NET 的文化设置可以用但性能差 SELECT PARSE(June 15, 2025 AS DATE USING en-US) AS d4;MySQL 这边是STR_TO_DATE和CASTSELECT STR_TO_DATE(2025-06-15, %Y-%m-%d) AS d1; SELECT STR_TO_DATE(2025-06-15 14:23:45, %Y-%m-%d %H:%i:%s) AS d2; SELECT CAST(2025-06-15 AS DATE) AS d3;PostgreSQL 和 Oracle 用TO_DATESELECT TO_DATE(2025-06-15, YYYY-MM-DD); SELECT TO_DATE(2025-06-15 14:23:45, YYYY-MM-DD HH24:MI:SS);显式转换的意义在于你明确告诉数据库“这个字符串我按什么模式解读”而不让数据库去猜。猜的结果往往和你的预期不一致。我在实际项目里要求团队写字符串转日期时必须带格式参数禁止裸写CAST(2025-01-02 AS DATE)因为裸写等于把解读权交给了会话参数哪天换个连接池、换个区域设置结果就变了。2.2 隐式转换数据库是怎么背着你干活的隐式转换是这里最大的坑。先看 SQL Server 的一个典型场景-- create_time 是 datetime 类型2025-06-15 是字符串 SELECT * FROM orders WHERE create_time 2025-06-15;这条 SQL 看起来很正常。数据库在比较时会把字符串2025-06-15转换成 datetime然后再和create_time比较。因为转换的是右边的输入值不是左边列所以索引还能用性能没有大问题结果通常也是对的。但反过来就完全不一样了-- 如果 order_date 是 varchar 类型存的是 2025-06-15 14:23:45 SELECT * FROM orders WHERE order_date 2025-06-15;这条 SQL 会把左边order_date这一整列都转换成 datetime再做比较。于是每一行都要执行一次字符串转日期索引直接失效而且一旦某一行的字符串格式不规范转换直接报错。更隐蔽的是如果order_date里存的是2025-6-15这种不规范的字符串也可能被“宽容”地解析成功但解析结果和预期未必一致。MySQL 的隐式转换也有类似问题。MySQL 里如果一个列是字符串、另一个是日期或数字优化器会按照类型优先级把字符串转成数字或日期。这种“帮忙”经常帮倒忙。你写WHERE date_col 2025-06-15右边的字符串会转成日期这没问题但如果你写WHERE varchar_date_col 2025-06-15列是字符串、输入是字符串两边直接按字符串比较“2025-06-15”和“2025-6-15”就会被当成完全不同的两个值。所以我的结论是隐式转换不是不能用但你必须知道它发生在哪一侧。发生在输入值一侧多数时候问题不大发生在列一侧一定要警惕索引失效和解析不确定性。2.3 TRY_类函数转换失败的兜底策略SQL Server 从 2012 开始提供了TRY_CONVERT、TRY_CAST、TRY_PARSE三个函数MySQL 有STR_TO_DATE加严格模式控制PostgreSQL 有TO_DATE的严格报错体系。其中 SQL Server 的TRY_系列我在数据清洗场景里用得特别多。SELECT TRY_CONVERT(DATE, 2025-06-15, 120) AS ok; -- 2025-06-15 SELECT TRY_CONVERT(DATE, not-a-date, 120) AS bad; -- NULLTRY_CONVERT在转换失败时返回NULL而不是抛异常。这在处理上游脏数据时非常有用。举个例子导入外部 Excel 文件时日期列里可能混着“2025/6/15”“15-06-2025”“手工备注”等各种内容如果直接CONVERT一条脏数据就能让整个插入事务回滚。用TRY_CONVERT先过滤一遍把转换不了的行单独记录只插入干净数据是常规做法。MySQL 里做了类似的判断通常要配合CASE WHEN或正则PostgreSQL 则可以用CASE WHEN加TO_DATE的异常捕获但写法会笨重不少。所以如果你是 SQL Server 用户TRY_系列一定要用熟其他库的用户更推荐在程序入口就做好数据清洗而不是在 SQL 里层层 try。3. 最容易翻车的三种场景格式暧昧、空值解析与月末边界讲完基础语法来聊几个我在实际开发和运维里反复撞到的坑。这三个坑几乎每个项目都会遇到而且网上能查到资料但讲得分散我干脆集中写在一块。3.1 01/02/2025 到底是几月几号这是字符串转日期里最经典的歧义问题。01/02/2025在美国是 1 月 2 日在英国是 2 月 1 日在 ISO 标准里它根本不算合法日期格式。同样的字符串在不同语言环境下解析结果完全不同。在 SQL Server 里做个实验就能看得很清楚SET LANGUAGE us_english; SELECT CONVERT(DATE, 01/02/2025) AS d; -- 2025-01-02 SET LANGUAGE British; SELECT CONVERT(DATE, 01/02/2025) AS d; -- 2025-02-01同一个字符串换个语言设置结果差了整整一个月。这种问题在跨国项目里尤其致命。我见过两个海外系统对接一个用美式格式mm/dd/yyyy一个用英式格式dd/mm/yyyy日期字段在接口文档里写得模棱两可最后对账差了三十天。解决方案很简单但也容易被忽视第一接口和表结构定义里强制使用YYYY-MM-DD或 ISO 8601 格式不接受那种斜杠分隔的日期字符串第二SQL 里做转换时必须显式指定格式码比如 SQL Server 用CONVERT(DATE, 01/02/2025, 103)明确告诉它这是英式dd/mm/yyyy或者用CONVERT(DATE, 01/02/2025, 101)明确是美式。显式指定后无论会话语言怎么变结果都不会漂。3.2 空字符串、NULL 和 0000-00-00日期转换对“空”的处理也是个大坑各库表现还很不一样。SQL Server 里把空字符串转成日期会直接报错SELECT CONVERT(DATE, , 120); -- 错误从字符串转换日期和/或时间时转换失败MySQL 则更“宽容”——在非严格模式下空字符串转日期会得到一个0000-00-00日期INSERT INTO t (date_col) VALUES (STR_TO_DATE(, %Y-%m-%d)); -- 非严格模式0000-00-00严格模式报错0000-00-00这个值在 MySQL 里是个“合法的非法值”不仔细看根本发现不了但它会导致后续所有日期比较都变得不可预测。PostgreSQL 则比较严格空字符串直接转日期会报错没有太多回旋余地。所以我处理字符串日期清洗时第一步永远是先处理空字符串-- SQL Server 做法 SELECT TRY_CONVERT(DATE, NULLIF(COLUMN, ), 120) FROM table_a; -- MySQL 做法 SELECT NULLIF(STR_TO_DATE(COLUMN, %Y-%m-%d), 0000-00-00) FROM table_a;NULLIF(col, )把空字符串变成 NULLTRY_CONVERT再兜底转换失败返回 NULL。这样进入目标表的日期列就不会出现脏值。关于NULL和空字符串还有一个容易混淆的点NULL是“没有值”空字符串是“有值但内容是空”两者在日期语境下语义完全不同但很多编程语言里经常把它们混为一谈。录入数据时日期字段宁可给 NULL也绝对不要给空字符串或0000-00-00。3.3 月末最后一天和闰年边界值测试清单日期转换还有一个隐蔽问题来自边界值。最常见的几个闰年判断规则是“能被 4 整除且不能被 100 整除或者能被 400 整除”。2000 年是闰年1900 年不是2100 年也不是。如果你的系统要处理存储到 2100 年的数据这个逻辑必须在应用层或数据库函数里正确处理。月末2025-01-31加一个月应该是2025-02-28还是2025-03-03大多数数据库的日期算术会返回2025-02-28但不同数据库对这个行为的定义有细微差异。如果依赖这个结果做业务逻辑比如每月订单截止日一定要提前测试。一天的最后一刻条件WHERE create_time 2025-06-15 23:59:59在绝大多数 SQL 里都有隐患。因为23:59:59不是一天的真正终点数据库的datetime精度可能到毫秒甚至微秒真正的一天内最大值是23:59:59.999或23:59:59.999999。最稳妥的写法是-- 不要用 某天 23:59:59 WHERE create_time 2025-06-15 AND create_time 2025-06-16用半开区间[start, end)既不会漏掉 23:59:59 之后的数据也不会因为那 1 毫秒的误差把下一天的数据卷进来。这个写法在日期转字符串、字符串转日期、索引优化里都适用可以说是 SQL 日期处理的“通用公式”之一。4. 转换函数与查询性能索引是怎么被你写没的前面讲的都是“结果对不对”的问题这一节讲“快不快”的问题。日期和字符串转换影响性能最常见的方式就是让索引失效。很多人优化慢查询从头到尾看执行计划、看表大小就是没注意到自己某个 WHERE 条件里给列套了个转换函数。4.1 对列套函数SARGable 原则SQL Server 和 MySQL 社区对“能不能用到索引”有一个核心概念叫SARGableSearch Argumentable意思是谓语条件是否“可搜索”。可搜索的写法是列 操作符 表达式表达式这一侧可以随便折腾不可搜索的写法是函数(列) 操作符 表达式这一写索引就废了。来看一个典型的“反 SARGable”例子-- 想把 create_time 转成字符串然后按日期筛选 SELECT * FROM orders WHERE CONVERT(VARCHAR(10), create_time, 120) 2025-06-15;这条语句逻辑上没毛病但它会对create_time这一列的每一行都先执行转换再做字符串比较。数据库无法直接跳到索引里定位到 2025-06-15 这个范围只能全表扫描或索引扫描。数据量小还好几千万行的时候直接卡死。MySQL 也有同样的坑SELECT * FROM orders WHERE DATE(create_time) 2025-06-15;DATE()函数套在列上索引同样失效。很多 MySQL 开发写“查当天数据”时都爱这么写其实改一下就好。4.2 改用范围条件把谓词改写干净正确写法是用范围条件代替函数包裹-- SQL Server / MySQL / PostgreSQL 通用写法 SELECT * FROM orders WHERE create_time 2025-06-15 AND create_time 2025-06-16;这样改写后create_time列没有被任何函数包裹优化器可以直接利用索引做范围查找Index Seek / Range Scan而不是全表扫描。如果你有一个endTime参数是完整的2025-06-15 23:59:59也建议改写-- 不推荐包含秒甚至毫秒的边界很容易出问题 WHERE create_time endTime -- 推荐用日期函数只处理输入值不处理列 WHERE create_time startTime AND create_time DATEADD(DAY, 1, endDate) -- SQL Server AND create_time DATE_ADD(endDate, INTERVAL 1 DAY) -- MySQL AND create_time endDate INTERVAL 1 day -- PostgreSQL关键原则是函数、计算、转换都放在“值”的那一侧列本身保持干净。4.3 隐式转换也会让索引失效一个真实的排查案例前面提到隐式转换这里举个真实案例。有次我帮人看一条慢 SQL执行计划显示全表扫描但表上明明有索引。SQL 大概长这样SELECT * FROM user_login_log WHERE login_date 2025-06-15;login_date在表里的定义是varchar(10)存的是2025-06-15这种字符串。2025-06-15在 SQL Server 里会被当成 datetime 类型所以比较时SQL Server 会把login_date这一整列转成 datetime再和右边的 datetime 比较。这一转索引就废了同时还有转换失败的风险。解决办法有两种。第一种把表字段的类型改成date一劳永逸第二种保持字段类型不变但让比较发生在字符串之间WHERE login_date 2025-06-15;这里如果把右边的字符串也明确转成字符串或直接保持字符串类型SQL Server 就会按字符串比较索引就能用。虽然字段设计成 varchar 来存日期本身是个反模式但很多遗留系统就是这么建的你只能在查询写法上迁就它。一个简单的判断方法是看执行计划的Seek还是Scan如果明明有索引却走Scan并且 WHERE 条件涉及日期时间字段一旦检查到列上有CONVERT、DATE()、TRUNC、EXTRACT或者隐式类型转换第一时间往这个方向排查。5. 落地建议字段选型、规范代码与团队自查前面讲了语法、坑和性能最后这一节说点“工程化”的东西。日期和字符串转换这个问题很多团队反复踩坑根子不在技术能力而在缺少统一约定。我把自己项目里沉淀下来的几条规范列一下供参考。5.1 存日期还是存字符串我的结论先说字段选型。只要你的字段要进行日期比较、加减运算、排序或者要被拿来按时间聚合那我强烈建议存数据库原生的日期类型date、datetime、timestamp等不要存字符串。字符串日期有三个致命弱点无法保证格式统一有人存2025-06-15有人存2025-6-15有人存2025/06/15排序和比较都会出问题而且不像日期类型那样有数据库帮你校验。排序语义可能出错字符串排序是字典序2025-06-15这种 ISO 格式碰巧和日期序一致但换成2025/6/15或者15-06-2025以后字典序就完全不等于日期序了。隐式转换损失性能如前面所说字符串字段和日期参数比较时要么全表扫描要么转换报错。什么时候可以考虑存字符串比如你只是需要一个“年月日”的展示值而且这个值永远不会参与计算或者它是一个外部系统传来的 ID 型日期比如20250615作为业务编号的一部分。即便如此我也建议另起一个隐藏的日期列用于排序和筛选展示列才用字符串。5.2 常用场景格式速查表为了方便团队直接抄作业我把不同场景推荐的格式写成一个速查表场景推荐格式示例对应关系日志文件名/导出文件名YYYYMMDD20250615紧凑、无分隔符、适合文件名接口传参/报表筛选YYYY-MM-DD2025-06-15ISO 风格、无歧义、可按字符串排序带时间的接口传参YYYY-MM-DD HH24:MI:SS2025-06-15 14:23:45可读性高、多数据库通用前端展示用户可读文案根据业务需要2025年06月15日用 SQL Server FORMAT / MySQL DATE_FORMAT跨库数据迁移YYYY-MM-DD HH24:MI:SS2025-06-15 14:23:45各库解析都最友好这套速查表不复杂但能避免很多“我觉得这个格式挺常见的”这种主观判断。团队里一旦有了明确约定代码 review 的时候直接拿表格对照效率高很多。5.3 团队 SQL 规范里我会写的三条规则最后是我在项目管理中沉淀下来的几条规则新产品或新项目的 SQL 规范里我会直接写进去规则一所有字符串转日期必须显式带格式参数禁止裸写CAST(2025-01-02 AS DATE)目的是一旦会话语言改动不会影响结果。规则二WHERE 条件里禁止对日期时间列套用转换函数统一改写为范围条件保证 SARGable。规则三日期时间字段优先用数据库原生类型存储必须用字符串存日期的场景要强制YYYY-MM-DD格式并且需经团队评审确认没有日期运算需求。这三条规则谈不上多玄妙但执行下来确实能明显减少前面说的各种隐性故障。尤其是规则二执行之前慢 SQL 隔三差五出现执行之后基本绝迹。最后再分享一个小技巧如果你们团队的代码里经常出现日期字符串拼接比如把2025 - 06 - 15这种写法拼出一个日期字符串再去比较那大概率可以改写成日期函数直接构造日期值比如 SQL Server 的DATEFROMPARTS(2025, 6, 15)、MySQL 的STR_TO_DATE(CONCAT(...,-,...),%Y-%m-%d)这类做法。让数据库自己处理日期构造既安全又省一层转换。日期和字符串互转这件事说到底不是你不会写某个函数而是你有没有在每个项目里把格式约定、转换边界和性能影响都考虑清楚。把这些想明白了你写出的 SQL 会稳定不少。