MySQL 里跟日期格式化相关的事情主要靠 DATE_FORMAT() 和 STR_TO_DATE() 这一对函数完成,再加上 UNIX_TIMESTAMP() / FROM_UNIXTIME() 处理时间戳。看着简单,但格式占位符记混一次(%m 是月还是分?)就要排查半天,把函数套在 WHERE 的字段上更会直接让索引失效。本文把格式占位符、互转方式和实战场景整理清楚。
一、核心函数速查
| 函数 | 作用 | 示例 |
|---|---|---|
DATE_FORMAT(d, fmt) |
日期 → 指定格式的字符串 | DATE_FORMAT(NOW(), '%Y-%m-%d') → 2026-09-20 |
STR_TO_DATE(s, fmt) |
字符串 → 日期(按格式解析) | STR_TO_DATE('2026/09/20', '%Y/%m/%d') |
UNIX_TIMESTAMP([d]) |
日期 → 秒级时间戳(不传参取当前) | UNIX_TIMESTAMP('2026-09-20 00:00:00') |
FROM_UNIXTIME(ts[, fmt]) |
秒级时间戳 → 日期,可顺手格式化 | FROM_UNIXTIME(ts, '%Y-%m-%d %H:%i') |
DATE(d) / TIME(d) |
取日期部分 / 时间部分 | DATE('2026-09-20 13:45:00') |
YEAR/MONTH/DAY/HOUR/MINUTE/SECOND(d) |
取单个部件(返回数字) | MONTH(NOW()) → 9 |
CONVERT_TZ(d, from, to) |
时区转换 | CONVERT_TZ(d, '+00:00', '+08:00') |
TIMESTAMPDIFF(unit, a, b) |
两个日期的差值(按指定单位) | TIMESTAMPDIFF(YEAR, birth, CURDATE()) |
DATEDIFF(a, b) |
相差多少天 | DATEDIFF(NOW(), create_time) |
二、DATE_FORMAT 格式占位符
这套占位符同时被 DATE_FORMAT()、STR_TO_DATE()、FROM_UNIXTIME() 的第二个参数使用,记一次就够。以 2026-09-20 13:45:06.123456(周日)为例:
年
| 占位符 | 含义 | 输出 |
|---|---|---|
%Y |
四位年份 | 2026 |
%y |
两位年份 | 26 |
%X / %x |
与 %V / %v 配套的「周所属年份」 |
2026 |
月、日
| 占位符 | 含义 | 输出 |
|---|---|---|
%m |
月,补零 | 09 |
%c |
月,不补零 | 9 |
%M / %b |
月名 / 月名缩写 | September / Sep |
%d |
日,补零 | 20 |
%e |
日,不补零 | 20 |
%j |
一年中的第几天(001~366) | 263 |
%D |
带英文序数后缀的日 | 20th |
时、分、秒
| 占位符 | 含义 | 输出 |
|---|---|---|
%H |
小时,24 小时制补零 | 13 |
%k |
小时,24 小时制不补零 | 13 |
%h / %I / %l |
小时,12 小时制(补零 / 补零 / 不补零) | 01 / 01 / 1 |
%i |
分钟(不是月!) | 45 |
%s / %S |
秒(两者等价) | 06 |
%f |
微秒,固定 6 位 | 123456 |
%p |
AM / PM | PM |
%r / %T |
完整时间,12 小时制 / 24 小时制 | 01:45:06 PM / 13:45:06 |
周、星期与其他
| 占位符 | 含义 | 输出 |
|---|---|---|
%W / %a |
星期名 / 缩写 | Sunday / Sun |
%w |
星期几数字(0 = 周日) | 0 |
%U / %u |
年内第几周(周日 / 周一为一周开始),00~53 | 38 |
%V / %v |
年内第几周(01~53),分别与 %X / %x 配套 |
38 |
%% |
输出一个字面量 % |
% |
关于周的两个细节:%X-%V 是「周日为一周第一天」的组合,真正的 ISO 8601 周(周一为一周开始、第一周含 4 天以上)应该用 %x-%v,做周报统计时别写错。另外 %M、%W、%b 这类文字输出受 lc_time_names 影响,默认是英文,要输出中文名需要先设置:
SET lc_time_names = 'zh_CN';
SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日 %W'); -- 2026年09月20日 星期日三、日期与字符串、时间戳互转
1. 日期 → 字符串
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'); -- 2026-09-20 13:45:06
SELECT DATE_FORMAT(NOW(), '%Y-%m'); -- 2026-09(按月分组常用)
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i'); -- 到分钟
SELECT DATE_FORMAT(NOW(), '%c月%e日'); -- 9月20日(不补零)2. 字符串 → 日期
库里存的是 2026/09/20、20260920 这类非标准格式时,用 STR_TO_DATE() 解析成真正的日期类型再比较或运算:
SELECT STR_TO_DATE('2026/09/20', '%Y/%m/%d'); -- 2026-09-20
SELECT STR_TO_DATE('2026-09-20 13:45:06', '%Y-%m-%d %H:%i:%s');
SELECT STR_TO_DATE('20260920', '%Y%m%d');
SELECT STR_TO_DATE('20/09/2026', '%d/%m/%Y'); -- 非「年月日」顺序也能解析
-- 转成日期后就能正常做范围比较和运算
SELECT * FROM orders
WHERE create_time >= STR_TO_DATE('2026/09/01', '%Y/%m/%d');格式不匹配时它返回 NULL 而不是报错,所以导入数据时建议顺手校验一下:
SELECT id, raw_date FROM tmp_import
WHERE STR_TO_DATE(raw_date, '%Y-%m-%d') IS NULL AND raw_date IS NOT NULL;3. 时间戳互转
-- 以下示例以会话时区为 UTC(time_zone = '+00:00')为准
SELECT UNIX_TIMESTAMP('2026-09-20 13:45:06'); -- 1789911906
SELECT FROM_UNIXTIME(1789911906); -- 2026-09-20 13:45:06
SELECT FROM_UNIXTIME(1789911906, '%Y-%m-%d %H:%i:%s'); -- 第二参数就是 DATE_FORMAT 的格式
-- 毫秒时间戳(很多接口返回 13 位)要先除以 1000
SELECT FROM_UNIXTIME(1789911906123 / 1000, '%Y-%m-%d %H:%i:%s');注意这两个函数按当前会话的时区解释:UNIX_TIMESTAMP('2026-09-20 00:00:00') 在 +08:00 和 +00:00 下得到的结果相差 8 小时。跨时区系统一定要先确认 @@session.time_zone/@@global.time_zone。
4. 时区转换
-- 库里存 UTC,展示时转成东八区
SELECT CONVERT_TZ(create_time, '+00:00', '+08:00') FROM orders;
-- 查看当前时区
SELECT @@global.time_zone, @@session.time_zone, NOW(), UTC_TIMESTAMP();坑:用 'Asia/Shanghai' 这类命名时区(而不是 '+08:00' 偏移量)时,必须先把时区表导入 mysql 库,否则 CONVERT_TZ() 静默返回 NULL。导入方法:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql四、实战场景
1. 按天 / 按月分组统计
-- 按天统计订单量
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS cnt
FROM orders
WHERE create_time >= '2026-09-01' AND create_time < '2026-10-01'
GROUP BY day ORDER BY day;
-- 按月统计(跨年也不会混在一起,因为带了年份)
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, SUM(amount) AS total
FROM orders
GROUP BY month ORDER BY month;只是要「按天」时 DATE(create_time) 比 DATE_FORMAT() 更直观也更快一点,两者结果一致。
2. 查「某一天」的数据:别把函数套在字段上
-- ✗ 不推荐:字段被函数包住,索引失效,等于全表扫描
SELECT * FROM orders
WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2026-09-20';
-- ✓ 推荐:写成范围条件,能走 create_time 上的索引
SELECT * FROM orders
WHERE create_time >= '2026-09-20 00:00:00'
AND create_time < '2026-09-21 00:00:00';如果确实需要按「格式化后的值」查询、且条件无法改写成范围,MySQL 8.0.13+ 可以用函数索引:
ALTER TABLE orders
ADD INDEX idx_create_date ((DATE(create_time)));
-- 表达式与索引定义完全一致时才会命中
SELECT * FROM orders WHERE DATE(create_time) = '2026-09-20';3. 计算时间差与年龄
SELECT TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age,
TIMESTAMPDIFF(HOUR, create_time, NOW()) AS hours_passed,
DATEDIFF(NOW(), create_time) AS days_passed
FROM users;
-- 最近 7 天(用日期运算,不要用函数包字段)
SELECT * FROM orders WHERE create_time >= CURDATE() - INTERVAL 7 DAY;4. 日期加减与截断到月初
SELECT DATE_ADD(NOW(), INTERVAL 1 MONTH);
SELECT DATE_SUB(NOW(), INTERVAL 30 DAY);
SELECT LAST_DAY(NOW()); -- 本月最后一天
SELECT DATE_FORMAT(NOW(), '%Y-%m-01'); -- 本月第一天
SELECT DATE_SUB(DATE(NOW()), INTERVAL DAYOFMONTH(NOW()) - 1 DAY); -- 同样效果五、常见坑清单
%i才是分钟,%m是月。写成'%Y-%m-%d %m:%s'会得到2026-09-20 09:06这种「看起来对但完全错」的结果。%H是 24 小时制,%h是 12 小时制,需要 AM/PM 请配合%p。%s和%S都是秒,但%S容易和「月」混淆,统一用%s更清晰。- WHERE 里给字段套
DATE_FORMAT()/DATE()会导致索引失效,改写成范围条件或加函数索引。 STR_TO_DATE()解析失败返回NULL,不报错,数据导入时容易静默丢数据。UNIX_TIMESTAMP()/FROM_UNIXTIME()受会话时区影响,跨时区场景先确认time_zone。- 命名时区没导入时区表时
CONVERT_TZ()返回 NULL,而不是报错;用'+08:00'偏移量则不受影响。 NOW()与SYSDATE()不同:NOW()返回语句开始执行的时间(同一语句内恒定),SYSDATE()返回函数实际执行的那一刻;主从复制下SYSDATE()也不安全,默认建议用NOW()。- 字符串比较不等于日期比较:
'2026-9-20' > '2026-10-01'按字符串比是成立的,别拿格式化后的字符串做时间先后判断。 TIMESTAMP与DATETIME语义不同:TIMESTAMP存取时按会话时区自动转换、范围只到 2038 年,DATETIME原样存储、范围到 9999 年。存业务时间通常用DATETIME,存「时刻」并需要自动时区转换才用TIMESTAMP。
六、速查卡
-- 最常用的格式化
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') -- 2026-09-20 13:45:06
DATE_FORMAT(NOW(), '%Y-%m-%d') -- 2026-09-20
DATE_FORMAT(NOW(), '%Y-%m') -- 2026-09 按月分组
DATE_FORMAT(NOW(), '%x-%v') -- ISO 周(配合年份用)
-- 字符串 → 日期 / 时间戳 → 日期
STR_TO_DATE('2026/09/20', '%Y/%m/%d')
FROM_UNIXTIME(1789911906, '%Y-%m-%d %H:%i:%s')
UNIX_TIMESTAMP('2026-09-20 13:45:06')
-- 取部件
DATE(d), YEAR(d), MONTH(d), DAY(d), HOUR(d), MINUTE(d), SECOND(d)
-- 运算
DATE_ADD(NOW(), INTERVAL 7 DAY), DATE_SUB(NOW(), INTERVAL 1 MONTH)
DATEDIFF(NOW(), create_time), TIMESTAMPDIFF(YEAR, birth_date, CURDATE())
LAST_DAY(NOW())
-- 查某一天(走索引的写法)
WHERE create_time >= '2026-09-20' AND create_time < '2026-09-21'
-- 时区
CONVERT_TZ(create_time, '+00:00', '+08:00')
SET lc_time_names = 'zh_CN'; -- 输出中文星期 / 月名
评论区