侧边栏壁纸
博主头像
Setyg的博客

当秋风染红落叶的时候,就是新生~

  • 累计撰写 86 篇文章
  • 累计创建 66 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

MySQL 日期格式化函数使用实践

Setyg
2026-09-20 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

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';              -- 输出中文星期 / 月名
0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区