在MySQL数据库里,日期和时间相关的查询几乎出现在每一张业务表中。订单创建时间、用户注册日期、日志记录时间,这些字段的筛选、统计和格式化需求非常普遍。但很多查询看似简单,一旦涉及日期类型选择、时区处理或索引利用,就会出现结果偏差或性能瓶颈。本文从日期类型、常用函数、范围查询、分组统计和时区转换几个方面,系统梳理MySQL日期查询的实用方法。

一、先分清MySQL的日期类型
MySQL提供多种日期和时间类型,常见的有DATE、DATETIME、TIMESTAMP、TIME和YEAR。DATE只存储年月日,范围从1000-01-01到9999-12-31;DATETIME存储年月日时分秒,范围与DATE一致;TIMESTAMP范围较小,从1970-01-01 00:00:01 UTC到2038-01-19 03:14:07 UTC,且与时区相关。TIME可以表示时间间隔或一天中的时间,范围从-838:59:59到838:59:59;YEAR只存储年份。
很多人容易混淆DATETIME和TIMESTAMP。核心差异是TIMESTAMP在写入和读取时会根据会话时区自动转换,而DATETIME存什么读出来就是什么。因此记录业务发生时间且需要跨时区展示时,TIMESTAMP更灵活,但要注意2038年限制;如果只是记录一个固定时间点且不关心时区,用DATETIME更直观。
最不推荐的做法是用VARCHAR类型保存日期字符串。这样做会导致日期函数无法直接使用,比较和排序也只能按字典序进行,例如字符串的10月会排在2月前面。下面给出一个规范的表结构示例:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_date DATE,
created_at DATETIME,
updated_at TIMESTAMP
);
二、掌握核心日期函数
MySQL内置了大量日期函数,最常用的包括NOW()、CURDATE()、CURTIME()、DATE()、YEAR()、MONTH()、DAY()等。NOW()返回当前日期和时间,CURDATE()只返回当前日期,CURTIME()只返回当前时间。DATE()可以从DATETIME中截取日期部分,YEAR()、MONTH()、DAY()则分别提取年、月、日。
格式化日期通常使用DATE_FORMAT()函数,它支持类似%Y代表四位年份、%m代表两位月份、%d代表两位日期、%H代表24小时制小时、%i代表分钟、%s代表秒的格式符。反过来,STR_TO_DATE()可以把字符串按指定格式解析成日期。下面是一个典型示例,同时展示提取和格式化:
SELECT
id,
created_at,
DATE(created_at) AS only_date,
YEAR(created_at) AS order_year,
MONTH(created_at) AS order_month,
DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS formatted_time
FROM orders;
日期计算方面,DATE_ADD()和DATE_SUB()可以在日期上加减指定的时间间隔,DATEDIFF()可以计算两个日期之间的天数差。比如要查询最近7天内的数据,可以先算好起始日期,再配合范围条件使用。
三、按日期范围查询的正确姿势
按日期范围查询是业务中最常见的需求,但写法不当会造成结果错误或索引失效。比如要查询某一天创建的订单,如果直接使用DATE(created_at) = '2025-01-01',逻辑上没错,但DATE()函数作用在created_at列上,MySQL无法使用该列上的索引,数据量大时查询会非常慢。
更合理的写法是使用半开区间,即大于等于当天零点并小于次日零点。这样既可以利用created_at上的索引,也能准确包含当天所有时间记录。示例如下:
SELECT * FROM orders WHERE created_at >= '2025-01-01 00:00:00' AND created_at < '2025-01-02 00:00:00';
对于BETWEEN ... AND ...,虽然写起来简单,但它在日期范围查询中容易把边界值重复包含或误包含。尤其是当字段带有时间部分时,BETWEEN '2025-01-01' AND '2025-01-31' 会漏掉1月31日当天除了零点以外的数据。因此涉及日期范围时,优先使用大于等于和小于组合。
四、日期分组统计并避开性能坑
按日期分组统计是报表系统的刚需。要统计每天的订单量,可以使用GROUP BY DATE(created_at);如果要按月统计,可以用DATE_FORMAT(created_at, '%Y-%m')作为分组键。下面是一个按月统计订单数量的例子:
SELECT
DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS order_count
FROM orders
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;
不过这种分组方式在数据量很大时性能一般,因为DATE_FORMAT()也是函数操作,会阻止索引的有序性利用。更优的做法是先按日期范围切分,再在应用层汇总,或者引入一张日期维度表,关联后按日期维度字段分组。日期维度表通常包含连续日期、所属周、月、季度、是否工作日等属性,既能提升查询效率,也能简化统计逻辑。
如果只需要统计某个月的数据,可以先确定该月的起止时间,再用范围条件过滤后COUNT,而不是对整个表做GROUP BY。例如统计2025年1月的订单量,可以写成:
SELECT COUNT(*) AS order_count FROM orders WHERE created_at >= '2025-01-01 00:00:00' AND created_at < '2025-02-01 00:00:00';
五、时区与格式转换注意事项
时区处理是日期查询中另一个容易踩坑的地方。TIMESTAMP类型的值在存储时会从当前会话时区转换为UTC,读取时再从UTC转换回当前会话时区。这意味着同一个TIMESTAMP字段在不同时区的客户端可能显示不同时间。DATETIME则不会自动转换。如果需要把某个时间从一时区转换到另一时区,可以使用CONVERT_TZ()函数。
例如把UTC时间转换为北京时间,可以写成:
SELECT
CONVERT_TZ('2025-01-01 08:00:00', '+00:00', '+08:00') AS beijing_time;
处理从外部系统导入的日期字符串时,STR_TO_DATE()非常有用,但要注意格式必须与字符串完全匹配,否则返回NULL。对于无效日期,MySQL默认在严格模式下会报错,在非严格模式下可能转换为零日期0000-00-00。建议在应用层或导入前先做好日期格式校验,减少数据库侧的异常。
最后提醒一点,日期函数虽强大,但在WHERE条件中应尽量避免对列使用函数,除非该列没有索引或数据量很小。否则应改写为范围条件,让索引发挥作用。这个原则适用于DATE()、DATE_FORMAT()、YEAR()等所有函数操作。