在关系型数据库里,时间区间的查询几乎是每天都会遇到的需求,无论是统计某天订单量,还是计算用户近七天的活跃情况,核心都在于如何正确使用SQL日期函数去框定起点和终点。不同的数据库虽然函数名称略有差异,但底层逻辑都围绕日期时间类型的存储精度与比较规则展开。如果直接拿字符串去和日期字段比较,优化器往往无法走索引,还会引入隐式转换错误。

常见日期函数与区间界定方式
在 MySQL 中,CURDATE() 返回当前日期,DATE_ADD() 可以对日期做偏移,而 BETWEEN 是最直观的区间写法。不过 BETWEEN 是闭区间,意味着起止时间点都会被包含。当字段是 DATETIME 类型且含有时分秒时,写 BETWEEN '2023-01-01' AND '2023-01-02' 实际只覆盖到凌晨零点,漏掉了第二天全天,这是非常典型的边界错误。
更稳妥的做法是采用左闭右开区间,例如用 created_at >= '2023-01-01' AND created_at < '2023-01-03' 来表示一号和二号两天。PostgreSQL 提供了 generate_series 配合 date_trunc 来按天切分,SQL Server 则常用 DATEADD(day, DATEDIFF(day, 0, getdate()), 0) 来截断时间部分。理解这些函数返回的是日期还是时间戳,决定了比较时是否要补零。
下面是一段 MySQL 中安全统计两天的写法,利用函数明确区间边界:
SELECT COUNT(*)
FROM orders
WHERE created_at >= DATE('2023-01-01')
AND created_at < DATE_ADD(DATE('2023-01-01'), INTERVAL 2 DAY);
这种写法不依赖 BETWEEN,也避免了末尾秒数丢失。在大数据量下,如果 created_at 有索引,范围扫描能精准命中两天内的数据页,不会像函数包裹字段那样导致索引失效。
时区与精度带来的区间偏差
很多系统把时间存成 TIMESTAMP 且带时区,但查询时却用本地字符串做条件,这时数据库会按会话时区转换,容易让区间偏移数小时。比如在东八区写 WHERE ts >= '2023-01-01 00:00:00',实际在 UTC 存储里可能是前一日十六点,统计自然错位。正确方式是用 CONVERT_TZ 或统一用 UTC_TIMESTAMP() 先规范化。
另一个容易被忽视的是毫秒与微秒精度。当业务用 DATETIME(3) 记录毫秒,而区间上限用 < '2023-01-02 00:00:00' 时,那一秒内的 999 毫秒都被正确排除,但若误写成 <= 就会把次日的零毫秒也并进来,造成边界重叠。通过 EXTRACT 函数单独取天数再做分组,也能绕开精度干扰。
以下 PostgreSQL 示例先将时间转 UTC 再按天截断,适合跨时区报表:
SELECT date_trunc('day', ts AT TIME ZONE 'UTC') AS day,
COUNT(*)
FROM events
WHERE ts >= '2023-01-01'::timestamptz
AND ts < '2023-02-01'::timestamptz
GROUP BY 1
ORDER BY 1;
这样无论会话时区如何,聚合出来的天级区间都基于同一基准,不会出现某地多算一天的情况。对于全球业务,这一步在日期函数处理时间区间时不可忽视。
性能优化与分区表配合
当时间区间查询涉及上亿行,仅靠函数索引不够,通常要引入分区表。以 MySQL _RANGE 分区按月份切分后,带有明确日期下限和开区间上限的 SQL,能让优化器做分区裁剪,只扫描相关月份。若写法是 WHERE DATE(created_at) = '2023-01-01',因为字段被函数包裹,分区裁剪往往失效。
使用原生区间比较配合 EXPLAIN 观察,应看到 range 或 ref 访问类型而非 ALL。对连续区间还可借助 WITH 递归生成日期序列,再 LEFT JOIN 事实表,补齐无数据的空区间,这比在应用层循环查库更省资源。日期函数在此扮演了生成维度表的角色。
示例展示如何用通用表表达式生成最近七天区间并左接统计:
WITH days AS (
SELECT DATE_SUB(CURDATE(), INTERVAL n DAY) AS d
FROM (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2
UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6) t
)
SELECT days.d, COUNT(o.id) AS cnt
FROM days
LEFT JOIN orders o
ON o.created_at >= days.d
AND o.created_at < DATE_ADD(days.d, INTERVAL 1 DAY)
GROUP BY days.d
ORDER BY days.d;
该写法把时间区间展开成驱动表,每个区间独立对齐,既清晰又利于数据库复用执行计划。面对复杂报表,这种以日期函数构建区间维度再关联的思路,比零散的 BETWEEN 更易维护和调优。