刚入行那会儿,我最头疼的就是处理数据库里的日期时间数据。产品经理要"最近30天活跃用户数",运营要"按周统计订单增长率",财务要"每月5号生成报表"——同样的时间数据,在不同场景下需要完全不同的呈现形式。MySQL虽然内置了丰富的日期函数,但实际项目中我发现,90%的开发者只用到DATE_FORMAT()这个函数,而剩下的10%甚至还在用程序代码处理格式化。
上周排查一个性能问题,发现某接口每次都要查询10万条记录,然后在Java里用SimpleDateFormat逐个转换日期格式。其实完全可以用数据库层的DATE_FORMAT直接输出目标格式,网络传输量减少40%,响应时间从800ms降到300ms。这就是为什么我们需要深入掌握MySQL日期格式化的原因——它不仅是字符串转换,更是影响系统性能的关键操作。
先看这张对比表,这是我用多个生产项目踩坑后整理的:
| 类型 | 格式 | 范围 | 存储空间 | 适用场景 |
|---|---|---|---|---|
| DATE | YYYY-MM-DD | 1000-01-01 ~ 9999-12-31 | 3字节 | 生日、注册日期等纯日期 |
| TIME | HH:MM:SS | -838:59:59 ~ 838:59:59 | 3字节 | 持续时间、间隔 |
| DATETIME | YYYY-MM-DD HH:MM:SS | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 8字节 | 需要完整时间戳的记录创建时间 |
| TIMESTAMP | YYYY-MM-DD HH:MM:SS | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 | 4字节 | 需要自动更新的最后修改时间 |
| YEAR | YYYY | 1901 ~ 2155 | 1字节 | 只需要年份的场景 |
关键经验:TIMESTAMP有2038年问题,做长期系统尽量用DATETIME。我曾参与过一个政府项目迁移,就是把TIMESTAMP字段批量改成DATETIME。
去年双十一大促时,我们遇到过新加坡用户看到的活动开始时间比实际晚了8小时。这是因为:
解决方法:
sql复制-- 会话级别设置时区
SET time_zone = '+08:00';
-- 查询时显式转换
SELECT CONVERT_TZ(created_at, '+00:00', '+08:00') FROM orders;
DATE_FORMAT(date, format) 是核心武器,format参数支持30+种格式符。分享几个最实用的组合:
sql复制-- 中文场景常用
SELECT
DATE_FORMAT(NOW(), '%Y年%m月%d日 %H时%i分') AS 格式化日期1,
DATE_FORMAT(NOW(), '%Y-%m-%d %T') AS 格式化日期2,
DATE_FORMAT(NOW(), '%W, %M %e %Y') AS 格式化日期3;
-- 输出结果:
-- 格式化日期1 → "2023年08月15日 14时30分"
-- 格式化日期2 → "2023-08-15 14:30:45"
-- 格式化日期3 → "Tuesday, August 15 2023"
完整格式符速查表:
| 格式符 | 说明 | 示例 |
|---|---|---|
| %Y | 四位年份 | 2023 |
| %y | 两位年份 | 23 |
| %m | 月份(01-12) | 08 |
| %c | 月份(1-12) | 8 |
| %M | 月份名称(January-December) | August |
| %b | 缩写月份(Jan-Dec) | Aug |
| %d | 日期(01-31) | 15 |
| %e | 日期(1-31) | 15 |
| %H | 小时(00-23) | 14 |
| %h | 小时(01-12) | 02 |
| %i | 分钟(00-59) | 30 |
| %s | 秒(00-59) | 45 |
| %T | 24小时制时间 | 14:30:45 |
| %r | 12小时制时间(带AM/PM) | 02:30:45 PM |
| %W | 星期名称(Sunday-Saturday) | Tuesday |
| %a | 缩写星期(Sun-Sat) | Tue |
| %w | 星期几(0=周日) | 2 |
sql复制-- 方式1:按自然周(周一至周日)
SELECT
DATE_FORMAT(DATE_SUB(created_at, INTERVAL (DAYOFWEEK(created_at)-2) DAY), '%Y-%m-%d') AS week_start,
COUNT(*) AS order_count
FROM orders
GROUP BY week_start;
-- 方式2:按7天滚动周期
SELECT
FLOOR(DATEDIFF(created_at, '2023-01-01')/7) AS week_num,
COUNT(*) AS order_count
FROM orders
GROUP BY week_num;
sql复制SELECT
CONCAT(YEAR(created_at), 'Q', QUARTER(created_at)) AS quarter,
SUM(amount) AS total_amount
FROM transactions
GROUP BY quarter;
在电商项目中,我们曾有个慢查询:
sql复制-- 错误示范:对字段使用函数会导致索引失效
SELECT * FROM orders
WHERE DATE_FORMAT(created_at, '%Y-%m-%d') = '2023-08-15';
-- 正确做法:使用日期范围查询
SELECT * FROM orders
WHERE created_at BETWEEN '2023-08-15 00:00:00' AND '2023-08-15 23:59:59';
这是我们在ERP系统中封装的日期处理函数:
sql复制DELIMITER //
CREATE FUNCTION format_chinese_date(d DATETIME)
RETURNS VARCHAR(50)
BEGIN
RETURN DATE_FORMAT(d, '%Y年%m月%d日 %H时%i分');
END //
DELIMITER ;
方案对比表:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 应用层转换 | 灵活,可动态调整 | 增加网络传输量 | 多时区混合系统 |
| 数据库会话时区 | 查询简单 | 需要维护连接池配置 | 单一固定时区系统 |
| 存储时区偏移量 | 数据自包含时区信息 | 需要额外存储空间 | 需要历史时区记录 |
| CONVERT_TZ函数 | 精确到每行数据 | 性能开销较大 | 少量关键记录的转换 |
问题1:Invalid datetime format错误
sql复制-- 错误:月份和分钟格式符混淆
SELECT DATE_FORMAT(NOW(), '%Y-%m-%i');
-- 正确:分钟应该用%i
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i');
问题2:时区导致的日期偏差
sql复制-- 在UTC+8时区执行
SELECT DATE_FORMAT('2023-01-01 00:00:00', '%Y-%m-%d %H:%i:%s');
-- 输出:2023-01-01 08:00:00 (自动加了8小时)
-- 解决方案:先用CONVERT_TZ处理
SELECT DATE_FORMAT(CONVERT_TZ('2023-01-01 00:00:00', '+00:00', '+08:00'), '%Y-%m-%d %H:%i:%s');
闰年问题:
sql复制-- 错误:直接加365天可能出错
UPDATE coupons SET expire_date = DATE_ADD(issue_date, INTERVAL 365 DAY);
-- 正确:使用YEAR_INTERVAL更安全
UPDATE coupons SET expire_date = DATE_ADD(issue_date, INTERVAL 1 YEAR);
月末日期处理:
sql复制-- 获取某月最后一天的安全方法
SELECT LAST_DAY('2023-02-01'); -- 返回2023-02-28
SELECT LAST_DAY('2024-02-01'); -- 返回2024-02-29
这是我们去年双十一实时看板的SQL片段:
sql复制SELECT
DATE_FORMAT(create_time, '%Y-%m-%d %H:00') AS hour_time,
COUNT(DISTINCT user_id) AS uv,
COUNT(*) AS pv,
SUM(CASE WHEN payment_time IS NOT NULL THEN amount ELSE 0 END) AS gmv
FROM order_data
WHERE create_time BETWEEN '2023-11-11 00:00:00' AND '2023-11-11 23:59:59'
GROUP BY hour_time
ORDER BY hour_time;
将原来的Java端处理改为数据库直接生成Excel友好格式:
sql复制SELECT
CONCAT(YEAR(payment_date), '年第', QUARTER(payment_date), '季度') AS quarter,
DATE_FORMAT(MIN(payment_date), '%m/%d') AS start_date,
DATE_FORMAT(MAX(payment_date), '%m/%d') AS end_date,
FORMAT(SUM(amount), 2) AS total_amount,
COUNT(*) AS order_count
FROM financial_transactions
GROUP BY quarter;
7日留存率的计算方案:
sql复制SELECT
DATE_FORMAT(register_date, '%Y-%m-%d') AS reg_date,
COUNT(DISTINCT user_id) AS reg_users,
COUNT(DISTINCT CASE WHEN DATEDIFF(login_date, register_date) = 1 THEN user_id END) AS day1_retention,
COUNT(DISTINCT CASE WHEN DATEDIFF(login_date, register_date) = 7 THEN user_id END) AS day7_retention
FROM user_register
LEFT JOIN user_login ON user_register.user_id = user_login.user_id
GROUP BY reg_date;
虽然DATE_FORMAT已经很强大,但在处理国际化项目时还是有些不足。比如日本日历系统、伊斯兰历法等特殊需求。我们的解决方案是:
sql复制CREATE FUNCTION format_japanese_date(d DATETIME)
RETURNS VARCHAR(50)
BEGIN
DECLARE era VARCHAR(10);
DECLARE year_in_era INT;
-- 计算日本年号逻辑
IF d >= '2019-05-01' THEN
SET era = '令和';
SET year_in_era = YEAR(d) - 2018;
ELSEIF d >= '1989-01-08' THEN
SET era = '平成';
SET year_in_era = YEAR(d) - 1988;
END IF;
RETURN CONCAT(era, year_in_era, '年', MONTH(d), '月', DAY(d), '日');
END;