作为一名长期与MySQL打交道的数据库工程师,我经常看到开发者在处理日期时间格式化时走弯路。DATE_FORMAT()函数虽然基础,但它的灵活性和实用性远超大多数人的想象。今天,我将结合多年实战经验,带你全面掌握这个函数的精髓。
DATE_FORMAT()是MySQL中用于日期时间格式化的核心函数,它能够将DATETIME、TIMESTAMP、DATE等类型的字段按照我们指定的格式输出为字符串。不同于简单的日期提取函数,它提供了近乎无限的格式化可能性。
在实际项目中,这个函数最常见的三大用途:
提示:虽然DATE_FORMAT()很强大,但在处理海量数据时要注意性能影响,我们会在后面的注意事项部分详细讨论优化策略。
sql复制DATE_FORMAT(date, format)
这个看似简单的结构,实则暗藏玄机。让我们拆解每个参数的真实特性:
date参数:
format参数:
下表是我整理的完整格式化字符速查表,包含官方文档中很少提及的实用技巧:
| 格式化符 | 说明 | 示例 | 特殊场景注意事项 |
|---|---|---|---|
| %Y | 4位年份 | 2024 | 支持0000-9999 |
| %y | 2位年份 | 24 | 00-69视为2000-2069 |
| %m | 月份(01-12) | 03 | 不足两位补零 |
| %c | 月份(1-12) | 3 | 不补零的简写 |
| %M | 月份全名 | March | 英文环境依赖 |
| %b | 月份缩写 | Mar | 同受语言设置影响 |
| %d | 日期(01-31) | 15 | 自动补零 |
| %e | 日期(1-31) | 15 | 不补零版本 |
| %H | 24小时制(00-23) | 14 | 午夜为00 |
| %k | 24小时制(0-23) | 14 | 不补零的24小时制 |
| %h/%I | 12小时制(01-12) | 02 | 需要配合%p使用 |
| %l | 12小时制(1-12) | 2 | 不补零版本 |
| %i | 分钟(00-59) | 05 | 注意不是%m |
| %s | 秒(00-59) | 08 | 闰秒不适用 |
| %f | 微秒(000000-999999) | 123456 | 仅限5.6+版本 |
| %p | AM/PM | PM | 需配合12小时制 |
| %W | 星期全名 | Friday | 语言相关 |
| %a | 星期缩写 | Fri | 同上 |
| %w | 星期数字(0=周日) | 5 | 中美周序差异 |
| %U | 周数(00-53) | 11 | 周日为一周起始 |
| %u | 周数(01-53) | 11 | 周一为一周起始 |
| %j | 年天数(001-366) | 075 | 闰年判断准确 |
| %T | 24小时时间 | 14:05:08 | 等价于%H:%i:%s |
| %r | 12小时时间 | 02:05:08 PM | 包含AM/PM |
场景一:标准化输出
sql复制SELECT
order_id,
DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS create_time
FROM orders;
这是最基础的用法,将DATETIME转为标准格式字符串。注意这里的分隔符(-和:)都是可以自定义的。
场景二:中文友好格式
sql复制SELECT
CONCAT(
DATE_FORMAT(payment_time, '%Y年%m月%d日'),
' ',
DATE_FORMAT(payment_time, '%H时%i分')
) AS china_style_time
FROM transactions;
通过CONCAT组合多个DATE_FORMAT结果,实现完全自定义的本地化格式。
周报统计(ISO标准周)
sql复制SELECT
DATE_FORMAT(log_date, '%x年第%v周') AS week_name,
COUNT(*) AS pv_count
FROM user_behavior
GROUP BY week_name
ORDER BY log_date;
这里%x对应ISO年份,%v对应ISO周数,确保每年的周数计算符合国际标准。
季度报表生成
sql复制SELECT
CONCAT(YEAR(create_time), 'Q', QUARTER(create_time)) AS quarter,
SUM(amount) AS total_sales
FROM orders
GROUP BY quarter;
虽然QUARTER不是DATE_FORMAT的直接功能,但展示了如何结合其他日期函数实现复杂需求。
按格式化后的日期筛选
sql复制SELECT *
FROM events
WHERE DATE_FORMAT(start_time, '%Y-%m-%d') = '2024-03-15';
警告:这种写法会导致索引失效!正确做法是:
sql复制SELECT *
FROM events
WHERE start_time BETWEEN '2024-03-15 00:00:00' AND '2024-03-15 23:59:59';
DATE_FORMAT()用在WHERE条件左侧时,MySQL无法使用日期索引。这是我见过最常犯的错误之一。
错误示范:
sql复制-- 全表扫描警告!
SELECT * FROM logs
WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-03-15';
正确做法:
sql复制-- 使用范围查询保持索引有效性
SELECT * FROM logs
WHERE create_time >= '2024-03-15 00:00:00'
AND create_time < '2024-03-16 00:00:00';
DATE_FORMAT()输出受系统时区设置影响。在跨时区应用中,务必显式处理时区转换:
sql复制SET time_zone = '+00:00';
SELECT DATE_FORMAT(UTC_TIMESTAMP(), '%Y-%m-%d %H:%i:%s') AS utc_time;
SET time_zone = '+08:00';
SELECT DATE_FORMAT(UTC_TIMESTAMP(), '%Y-%m-%d %H:%i:%s') AS beijing_time;
月份和星期名称默认跟随系统语言设置。如需指定语言,可以:
sql复制SET lc_time_names = 'zh_CN';
SELECT DATE_FORMAT(NOW(), '%W %M') AS chinese_date;
SET lc_time_names = 'en_US';
SELECT DATE_FORMAT(NOW(), '%W %M') AS english_date;
DATE_FORMAT的反向操作是STR_TO_DATE,两者配合可以实现日期字符串的转换:
sql复制SELECT DATE_FORMAT(
STR_TO_DATE('15/03/2024', '%d/%m/%Y'),
'%Y-%m-%d'
) AS standard_date;
结合日期加减函数实现复杂逻辑:
sql复制SELECT
DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), '%Y-%m-%d') AS next_month,
DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 10 DAY), '%Y-%m-%d') AS ten_days_ago;
在存储过程中使用变量存储格式字符串,实现动态格式化:
sql复制DELIMITER //
CREATE PROCEDURE format_order_dates(IN date_format VARCHAR(32))
BEGIN
SET @sql = CONCAT('
SELECT
order_id,
DATE_FORMAT(created_at, "', date_format, '") AS formatted_date
FROM orders
');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
CALL format_order_dates('%Y年%m月%d日 %H时%i分');
不同MySQL版本对DATE_FORMAT的支持有细微差别:
在编写跨版本兼容的SQL时,建议先检查版本:
sql复制SHOW VARIABLES LIKE 'version%';
对于需要微秒精度的应用,应当添加版本判断:
sql复制SELECT
IF(VERSION() >= '5.6.0',
DATE_FORMAT(NOW(6), '%Y-%m-%d %H:%i:%s.%f'),
DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')
) AS precise_time;
去年我主导优化过一个日均订单量50万+的电商系统,其中的日期格式化曾是性能瓶颈之一。原始实现如下:
sql复制-- 旧方案(执行时间:2.3秒)
SELECT
DATE_FORMAT(paid_time, '%Y-%m-%d') AS pay_date,
COUNT(*) AS order_count
FROM orders
WHERE paid_time BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY pay_date;
优化后的方案:
sql复制-- 新方案(执行时间:0.4秒)
SELECT
DATE(paid_time) AS pay_date, -- 改用DATE函数
COUNT(*) AS order_count
FROM orders
WHERE paid_time BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY pay_date;
关键改进点:
额外收获:发现当使用DATE_FORMAT(paid_time, '%Y-%m')按月分组时,改用以下方案更快:
sql复制SELECT
EXTRACT(YEAR_MONTH FROM paid_time) AS year_month,
COUNT(*) AS order_count
FROM orders
GROUP BY year_month;