做开发这些年,被日期时间类型折磨的次数,恐怕比被我吐槽过的需求还多。不是时间本身多复杂,而是很多人在设计表结构时,根本没把日期时间当成一种“有自己脾气”的数据类型,而是随手存个字符串、存个时间戳完事。等到排序、比较、跨时区统计的时候,才发现全都拧巴了。
这篇东西就聚焦一个问题:MySQL里的日期时间类型到底该怎么选、怎么用。我会从底层的存储规格讲起,把四个核心类型(DATE、TIME、DATETIME、TIMESTAMP)掰开揉碎,再把计算函数、隐式转换、索引设计这些关键点串起来,最后附上我踩过几次坑之后的建模建议。目标是让新手能照着选型,让写过几年的开发也能查漏补缺。
1. 为什么日期不能直接用字符串存:类型设计背后的真实意图
早几年接手过一个内部报表系统,那时候我刚从“万物皆可字符串”的阶段过来,建表时很自然地把订单日期存成VARCHAR,格式是2024-07-21 12:33:18。前两周相安无事,直到第三周产品说要按“日期倒序”展示最近三十天的订单,我写了个ORDER BY order_time DESC,结果排出来一塌糊涂:2024-09-01排在了2024-07-30前面,因为字符串字典序里字符9比字符7大。
这不是个例。字符串存日期至少有四个绕不开的问题:
第一,排序规则跟着字符串走。'2024-01-05'和'2024-1-05'在字典序里是两条路,前者正常,后者被丢到最前面。你指望数据库帮你按时间序排,它却在高高兴兴按字符码表排。
**第二,日期运算变成烫手山芋。**想算“三个月前的订单”,字符串只能先转出来再运算,转的时候格式稍有不慎就出错。而原生日期时间类型可以直接做DATE_ADD、DATE_SUB、TIMESTAMPDIFF,天然支持interval语义。
**第三,无效数据防不住。**字符串列里插个'2024-02-30',MySQL笑纳不误。但你要是把列定义成DATE,默认SQL模式下一插就报错,在宽松模式下至少也会给个警告。数据质量是靠类型约束出来的,不是靠业务代码自觉。
**第四,存储与索引效率有差距。**字符串存日期,一个'2024-07-21 12:33:18'就是19个字节起步,而DATETIME只占8字节,TIMESTAMP占4字节。同样的表,数据页数量差一倍,索引树的层级也更浅,扫描成本自然更低。
MySQL专门做这些日期时间类型,目的是把“日历语义”下沉到存储引擎层,让排序、比较、运算都有确定规则。你不需要告诉数据库“这个月有28天或29天”,当用到DATE_ADD('2024-01-31', INTERVAL 1 MONTH)时,数据库自己知道该返回2月29日。这些逻辑如果靠应用层做,每张表每个接口都要重复实现一次,迟早出错。
所以第一课很简单:**日期时间数据就交给日期时间类型,别用字符串、别用整型裸奔。**整型存时间戳不是完全不行,但可读性和区间计算的便利性都打折扣,除非你做的系统对存储体积极端敏感,否则没必要。
2. 四种核心类型:DATE、TIME、DATETIME、TIMESTAMP的真实存储规格
MySQL里的日期时间类型不止四个,但日常核心就是DATE、TIME、DATETIME、TIMESTAMP,顺便还有那个存在感不高的YEAR。先看一张我整理的对照表,把最关键的参数都摆出来:
| 类型 | 存储字节 | 取值范围 | 默认格式 | 适用场景 |
|---|---|---|---|---|
| DATE | 3 | 1000-01-01 ~ 9999-12-31 | YYYY-MM-DD | 生日、结算日、节假日等纯日历日期 |
| TIME | 3 | -838:59:59 ~ 838:59:59 | HH:MM:SS | 每天的时刻,也可表示时间段/时长 |
| DATETIME | 8 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | YYYY-MM-DD HH:MM:SS | 业务单据时间、跨世纪数据、需要保留历史时区信息 |
| TIMESTAMP | 4 | 1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC | YYYY-MM-DD HH:MM:SS | 日志、埋点、审计字段,强依赖时区自动换算 |
| YEAR | 1 | 1901 ~ 2155,以及0000 | YYYY | 只关心年份的场景 |
这张表有几个容易被忽略的细节,我逐个说。
DATE只有3字节,不是你以为的字符串转储,它内部是压缩的整数表示。取值范围可以到公元9999年,单就“日历日期”而言,绝大多数业务都用不到上限,放心用。
TIME的取值上界是838:59:59,不是24小时。这地方特别容易让新人懵:为什么TIME能存25:00:00?因为MySQL的这个TIME类型,既表示“一天里的某个时刻”,也可以表示“两个事件之间的时间跨度”。比如统计一段任务从开始到结束跑了多少小时,可能超过24小时,TIME类型的设计上就预留了这个空间。不过需要注意,TIME虽然能装大跨度,但它本身不区分你是“25点”还是“次日1点”,语义解释要靠业务自己定。
DATETIME和TIMESTAMP表面上看都能存YYYY-MM-DD HH:MM:SS,但它们的底层机制差异巨大。DATETIME是个“日历值”,存进去是什么就是什么,不跟着时区变;TIMESTAMP内部按UTC存储,展示时按会话时区换算。也就是说,同一个TIMESTAMP值,你在北京时间看是一个字符串,在伦敦时间看是另一个字符串。这个差异是实际项目里最大的坑,后面单独开一节讲。
YEAR我多说一句。它只有1字节,占空间极小,但别拿它存跨世纪的年份。1901到2155这个区间,对考古和科幻都够呛。普通建表时,能不用YEAR就不用,DATE才是日历日期的正解。
还有一个大家容易踩的点:微秒精度。MySQL 5.6.4之后,日期时间类型支持fsp参数,也就是DATETIME(3)表示毫秒精度,DATETIME(6)表示微秒精度。如果你做的是高频交易、秒杀活动、性能监控这类对时间精度敏感的业务,建表时一定记得把fsp写出来。不写默认是0,也就是秒级,毫秒会被直接丢弃。我曾经在一个埋点表里漏写了(3),结果前端传过来的毫秒时间戳全被截断成整秒,不同事件之间的先后顺序都排反了。
3. TIMESTAMP的时区魔法与2038年边界:两个最容易被忽视的致命细节
TIMESTAMP是这几个类型里最“聪明”也最“娇气”的。先说它聪明的地方:存入时自动把会话时区的本地时间转换成UTC,查询时再自动转回当前会话的时区。这意味着只要你的应用明确设好了会话时区,TIMESTAMP天然支持全球化的日志记录。
举个例子,你在上海的业务系统里写入一行日志:
SET time_zone = '+08:00'; INSERT INTO access_log(request_time) VALUES ('2024-07-21 12:00:00');如果你把这个表的dump文件拿到一个时区为UTC的服务器上导入,再查询:
SET time_zone = '+00:00'; SELECT request_time FROM access_log;你看到的会变成2024-07-21 04:00:00。数据没坏,只是换了身衣服。
这也是为什么很多DBA坚持“数据库统一存UTC时间,应用层做时区转换”。TIMESTAMP的自动换算机制,可以把“换机房”“换服务器”的时区调整成本消解在数据库引擎里。但前提是——你必须理解并且掌控它。
再说自动初始化。MySQL允许给TIMESTAMP列设置DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,这个特性在审计字段上非常好用:
CREATE TABLE audit_log ( id INT PRIMARY KEY, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, detail VARCHAR(512) );这样插入时自动写当前时间,更新时自动刷新updated_at,应用层完全不用管。但有个细节:ON UPDATE CURRENT_TIMESTAMP仅在这一行发生了字段值的实际变更时才触发;如果你执行一条UPDATE,但所有值都和原来一样,MySQL不会刷新它(部分版本行为略有差异,但大体是这样)。别为了触发update时间而故意拼接一个相同的值,那是自欺欺人。
可能有人会问:那DATETIME也能这样自动初始化吗?可以,MySQL 5.6.5之后,DATETIME也支持DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP。所以“自动更新”不是TIMESTAMP的专利,别再拿这个当作选TIMESTAMP的理由。
接下来说“娇气”的地方。TIMESTAMP的存储只有4字节,它的取值范围被限制在1970年到2038年之间。对,就是那个著名的2038年问题。严格来说上限是2038-01-19 03:14:07 UTC,一旦过了这个时刻,32位时间戳溢出,TIMESTAMP就装不下了。
对一个2024年的系统来说,2038年听起来还很遥远,但HR系统存员工退休日期、金融产品存到期日、基础设施存设备维保周期,这些都是二三十年后的日期。一旦数据量积累跨过这个边界,你连INSERT都执行不了,报错说“value out of range”。我见过一套老系统因为养了一张TIMESTAMP存客户合同终止日期的表,结果在2023年评估续约时,发现2038年后的合同根本写不进去,只能紧急做表结构迁移,4000万行数据整整跑了一晚上。
所以选型建议很直接:
- 只需要短生命周期、高并发、对存储空间敏感的数据——日志、行为埋点、会话心跳——用TIMESTAMP没问题,4字节香。
- 涉及长期业务、跨世纪日期、需要和多个时区历史数据比对——老老实实DATETIME。
- 如果你无法保证所有服务器时区一致,建议干脆全局用DATETIME存UTC时间,应用层统一换算。虽然多花4字节,但语义可控。
TIMESTAMP还有一个“阴间设定”:不写入时,默认值为当前时间,除非列显式允许NULL。这曾经让很多从别的数据库转过来的开发吃过大亏。另外,如果你的列定义为TIMESTAMP NOT NULL DEFAULT '2024-01-01 00:00:00',而这个时间在目标时区里换算后不能在TIMESTAMP范围内,插入就会直接失败。边界情况很多,用之前一定要把时区会话打开测一遍。
4. 日期时间计算的函数工具箱:按场景分组,别再把公式背混
日期时间函数是面试八股,也是实战里用得最密的一层。我先按使用场景归个类,避免大家一头扎进函数清单里。
场景一:拿到“当前时间”。
NOW()和CURRENT_TIMESTAMP是同义词,返回当前语句开始执行时的日期时间;CURDATE()返回当前日期;CURTIME()返回当前时刻。特别注意SYSDATE()这个函数,它返回的是“该函数执行的瞬间”,而不是语句开始时间。一条长SQL里调用多次SYSDATE(),可能得到不同值,这在事务对比里会造成逻辑错乱。默认能用NOW()就别用SYSDATE()。
场景二:日期加减“一段时间”。
核心函数是DATE_ADD和DATE_SUB,以及对应的别名ADDDATE、SUBDATE。它们接受一个INTERVAL表达式,这个INTERVAL才是精髓:
SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH); -- 结果:2024-02-29 SELECT DATE_SUB('2024-07-21 12:00:00', INTERVAL '2:30' HOUR_MINUTE); -- 结果:2024-07-21 09:30:00INTERVAL后面可以跟各种单位:DAY、MONTH、YEAR、HOUR、MINUTE、SECOND,还能组合成DAY_HOUR、DAY_MINUTE、HOUR_MINUTE、SECOND_MICROSECOND等等。这里容易犯的错是单位名写错,比如用INTERVAL 1 DAYS,MySQL会直接报语法错误。凡是要有复数概念的,MySQL里都只要单数形式。
很多人不知道的细节:DATE_ADD('2024-01-31', INTERVAL 1 MONTH)的结果是2024-02-29,不是2024-02-28。MySQL采用“溢出则跳到月末”的策略。如果你的业务期望是“如果2月没有31日就报错”,那么数据库默认行为可能不符合你的预期,需要在应用层自己判断。
场景三:计算“两个日期之间差了多少”。
DATEDIFF(expr1, expr2)只返回天数差异,且只比较日期部分,不比较时间部分。比如:
SELECT DATEDIFF('2024-07-21 23:59:59', '2024-07-20 00:00:00'); -- 结果是1,不是接近2TIMESTAMPDIFF(unit, start, end)更灵活,可以指定SECOND、MINUTE、HOUR、DAY、MONTH、YEAR。注意参数顺序是“结束时间-开始时间”,写反了会得到负数,这个负号往往还是你想要的,但可读性很差。
一个真实的坑:算“两个日期间隔几个月”时,TIMESTAMPDIFF(MONTH, '2024-01-31', '2024-02-28')返回1,你没看错,是1,不是0。它是按月份边界判断,不关心1月31日这个日号在2月不存在。如果业务要求“满一个自然月才算1个月”,就得额外处理。
场景四:格式化输出和反向解析。
DATE_FORMAT(date, format)是输出端主力,写法:
SELECT DATE_FORMAT('2024-07-21 12:33:18', '%Y-%m-%d %H:%i:%s'); -- 2024-07-21 12:33:18STR_TO_DATE(str, format)是输入端主力,常用于把各种乱七八糟的字符串转成日期:
SELECT STR_TO_DATE('21/07/2024 10:20', '%d/%m/%Y %H:%i'); -- 2024-07-21 10:20:00%Y是四位年份,%y是两位年份;%m是两位月份,%c是一位或两位月份;%H是24小时制,%h是12小时制。输错格式不会报错,只会得到NULL,这类问题排查起来极其耗时。我建议所有转换格式统一在一张格式映射表里维护,别让每个接口各自为政。
场景五:提取年、月、日、季度。
最碎片化的一组:YEAR()、MONTH()、DAY()、HOUR()、QUARTER()、DAYOFWEEK()、WEEKDAY()、DAYOFYEAR()。还有一个通用函数EXTRACT(unit FROM date)。
日报、月报天然要用到它们。比如按月份聚合销量:
SELECT DATE_FORMAT(order_time, '%Y-%m') AS month_label, COUNT(*) AS order_cnt FROM orders WHERE order_time >= '2024-01-01' AND order_time < '2025-01-01' GROUP BY month_label;注意这里GROUP BY用的是别名,在MySQL里允许,但在某些其他数据库里会有限制。更稳妥的写法是GROUP BY DATE_FORMAT(order_time, '%Y-%m')。另外我见过不少人写WHERE MONTH(order_time) = 7来查7月的数据,这样不是不行,但函数套在索引列上会让索引失效。正确姿势是写成范围条件:order_time >= '2024-07-01' AND order_time < '2024-08-01'。这个点后面还会再踩,属于高性能SQL的基本素养。
5. 日期时间字面量与隐式转换:SQL里的隐形坑,踩到你怀疑人生
这里要聊的,是那些“看起来SQL写对了,但结果就是不对”的重灾区。
坑一:数字上下文里,字符串不是日期。
SELECT '2024-01-02' + 1的结果是什么?在一个不懂MySQL的人看来可能是“2024-01-03”,实际上MySQL把'2024-01-02'转成了数字2024,然后加1得2025。因为在这个表达式里没有日期语义,MySQL只会高高兴兴按数字处理字符串。同理,SELECT '2024-01-02' - '2024-01-01'的结果不是1天,而是0,因为2024-2024等于0。
所以,如果你想明确表达“这是一个日期字面量”,要么写标准格式的字符串并依赖列类型去转,要么用MySQL的日期字面量语法:
SELECT DATE '2024-01-02'; SELECT TIMESTAMP '2024-01-02 12:00:00';坑二:DATE类型与字符串比较,时间部分消失了。
假设有一张表,order_date DATE NOT NULL,里面存的都是整天的日期。你执行:
WHERE order_date = '2024-07-21 08:30:00'MySQL不会报错,它会尝试把字符串转成DATE,然后order_date里的2024-07-21和转出来的2024-07-21相等,返回全天的数据。这往往不是你想要的结果。如果你想匹配具体时刻,列类型就不该是DATE,而应该是DATETIME或TIMESTAMP。反过来,如果你的列是DATETIME,你执行WHERE order_time = '2024-07-21',MySQL把它解析为2024-07-21 00:00:00,当天的其他时刻全部被排除。这些行为都源于隐式转换,表面看着灵活,实则暗地里帮你做了决定。
坑三:宽松模式与零日期。
MySQL有几种sql_mode,最相关的是STRICT_TRANS_TABLES和ALLOW_INVALID_DATES。默认情况下,5.7以上版本基本都开启了严格模式,插入'2024-02-30'这样的无效日期会直接报错。但有些老项目或者迁移过来的库,sql_mode里没有严格模式,MySQL会宽容地把无效日期存成'0000-00-00',只给出Warning。
零日期0000-00-00是个大坑:JDBC驱动默认不允许把零日期映射为Java时间对象,查询时直接抛异常。别问我怎么知道的——某次我为了一张历史表写统计接口,线上一直报“Cannot convert value '0000-00-00' from column”,查了半天才发现是几年前初始化脚本在宽松模式下把一批坏数据存成了零日期。
治疗手段很简单:把sql_mode显式固定为STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,让数据库在入口就拒绝脏数据。同时,代码里对于“暂未确定”的日期,可以做成空字段或者业务约定的最大值(比如得用9999-12-31表示“永久”),别用零日期。
坑四:字符串与索引列的比较导致类型转换。
如果查询列是VARCHAR,你给它传一个日期时间字符串,也可能触发隐式转换,但这转换是在列上还是在参数上,要看具体语境。比较危险的场景是:列是字符串,你按日期判断大小,比如WHERE order_date_str > '2024-07-01',MySQL把双方都转成字符串比较,结果字符串'2024-07-01'排在一起,看着没问题。可一旦存在'2024-7-1'这种非标准格式,大小关系立刻乱掉。所以在设计层面,VARCHAR日期列必须尽早清剿,别让它在生产环境里继续坑人。
坑五:时间范围查询的BETWEEN边界。
BETWEEN在MySQL里是闭区间,BETWEEN '2024-07-21 00:00:00' AND '2024-07-21 23:59:59'看似覆盖一整天,但如果某条记录的时间精确到毫秒,就会漏掉23:59:59.500那条。稳妥做法是使用半开区间:
WHERE order_time >= '2024-07-21 00:00:00' AND order_time < '2024-07-22 00:00:00';这不仅是习惯问题,也是能让优化器更好利用索引的写法。
6. 日期时间列的建模与优化:精度、时区、索引和分区怎么一起落地
最后聊实战建模。一个表里的日期时间列,往往不止一两个字段。我的习惯是下单之前先确认三件事:精度能不能接受秒?业务时区要不要跟着服务器变?查询模式主要是范围扫还是点查?
精度选择:秒够用就用DATETIME,不够用就用DATETIME(3)毫秒;只有做性能剖析、测量延时这种场景才考虑DATETIME(6)微秒。默认建表时不要写DATETIME(6),看着很全,实际上存储空间没变,但很多ORM框架处理微秒时会出幺蛾子。另外MySQL 8.0.29之前,插入带小数秒的值时,默认是四舍五入的,8.0.29开始可以通过系统变量time_format控制,但多数人不需要关心,只要记住不要拿带微秒的字符串给秒级列赋值。
时区约定:我的推荐始终是“数据库统一存UTC,应用层按用户时区转换”。这样做的好处是,服务器从北京迁到新加坡、从新加坡迁到东京,数据库层面完全无感知。写数据时由API网关统一把业务时间转成UTC,读数据时再由前端或中间层转回用户本地时间。TIMESTAMP的自动换算虽然方便,但它在5.7老版本里,DEFAULT CURRENT_TIMESTAMP只能用于TIMESTAMP,且一张表只能有一个自动更新的TIMESTAMP列,约束太多。DATETIME反而更省心,因为它不参与时区换算,你存什么就是什么。
索引优化:日期时间列加索引是常规操作,但要注意几点。
不要在索引列上套函数,前面已经说过:WHERE DATE_FORMAT(create_time, '%Y-%m') = '2024-07'基本等于全表扫。改成create_time >= '2024-07-01' AND create_time < '2024-08-01',即使没有专门给月份建索引,也能用上create_time上的普通索引。
时间范围查询时,尽量写成半开区间。这样优化器做range scan最舒服。另外,如果一张表里同时有start_time和end_time,而查询经常是“某个时刻是否落在这个区间”,可以考虑给这两个字段建立复合索引,配合WHERE start_time <= ? AND end_time >= ?,再配合覆盖索引(start_time, end_time)可以避免回表。
分区:日志型、事件型的表,经常按月或按年分区。MySQL的分区类型里有RANGE COLUMNS,可以直接用日期列做分区键:
CREATE TABLE access_log_2024 ( id INT NOT NULL, log_time DATETIME NOT NULL, detail VARCHAR(255) ) PARTITION BY RANGE COLUMNS(log_time) ( PARTITION p202401 VALUES LESS THAN ('2024-02-01 00:00:00'), PARTITION p202402 VALUES LESS THAN ('2024-03-01 00:00:00'), PARTITION p202403 VALUES LESS THAN ('2024-04-01 00:00:00') );分区的好处不只是“大表更好管理”,更重要的是可以通过WHERE log_time < '2024-04-01'触发分区裁剪,只读相关分区,查询速度肉眼可见地提升。缺点也不少:分区数量过多反而拖慢插入性能;分区键必须包含在主键里;后续做分区合并、删除时要小心锁表。所以分区的优先级低于“一个大表加索引”的常规方案,只有确认查询模式稳定、数据增速明确时才启用。
还有一点关于默认值的设计。业务表里常见的created_at、updated_at字段,我建议统一用:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这个写法在MySQL 5.7和8.0都兼容。注意列名尽量用_at后缀,别一会儿create_time一会儿created_time,接口对接的时候前后端对字段名对半天。
建完表之后,最后一道防线是在应用层再加一道校验。MySQL的日期类型能够拦住格式错误,但拦不住“逻辑错误”。比如一个订单的支付时间比下单时间早两小时,Max久注定藏在这种地方。这类问题不是数据库能处理的,需要业务代码在写入前兜底。
7. 一次真实的排障记录:时区配置引发的线上时间错乱
纸上谈兵再多,都不如真出事的时候背脊发凉。去年年中我们重构了一套国际化订单系统,数据库从单机房迁到了跨地域的云环境,数据表里时间字段大多是DATETIME。因为之前一直运行在同一个时区的服务器上,表里直接存着北京时间的日期时间。
迁移后,新的数据库实例使用的是UTC时区。有一张报表需要展示“当日订单数”,代码里用的是本地凌晨零点转成时间字符串去数据库里查。结果上线第一天,早高峰过后数据迟迟不刷新,查看日志后发现:应用按北京时间传了'2024-06-01 00:00:00',数据库却把它当成UTC时间存储,等应用再读出来时,比北京时间慢了8个小时,当天的订单全部被归到了“前一天”。
排查过程没有用任何高端工具,就是把线上一条订单的create_time,和应用日志里收到的原始请求时间打印出来对比,立刻发现8小时的偏移。根因就是DATETIME虽然不会自动换算,但应用在生成查询条件时,默认用了服务器本地时区作为时钟源。
最后我把所有数据库连接串默认时区强制成serverTimezone=Asia/Shanghai,同时在配置里说明“数据库内统一存北京时间,不随机器时区改变”。这个方案可能让时区纯化主义者皱眉——为什么不干脆全存UTC?因为历史数据已经是北京时间,全量转换成本高、风险大。真正的经验是:**无论你选择存UTC还是存本地时间,都必须把“约定”写进配置和文档,并确保应用层读写的时区始终一致。**最怕的就是同一套系统,一部分表存UTC,一部分表存本地,那才是灾难。
排障结束后,我把日期时间字段的规范整理成一条简单原则:**同一系统内,所有时间字段要么全部存UTC,要么全部存同一个业务时区;读取展示时,由统一的时间组件转换成用户时区;写入时,由统一的时间组件完成反向转换。**谁都不许自己偷偷调用new Date()乱打时间。
这个原则听起来平平无奇,但真正能坚持下来的团队不多。多的是今天图省事在SQL里写NOW(),明天又因为服务器时区变了,全部错乱。
我自己的体会是,MySQL的日期时间类型,每一个设计细节背后都有历史包袱和现实场景。TIMESTAMP省空间但扛不住2038,DATETIME稳重但不自动带时区语义,DATE轻巧但装不下时刻,TIME既能表时刻又能表时长。没有绝对的好坏,只有跟业务匹配不匹配。你把这篇里的坑都避过去,再遇到“日期时间”四个字,就不会条件反射地焦虑了。