DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

MySQL BETWEEN 按日期筛选漏掉结束日记录

DATETIME 与日期上界比较时,结束边界落在当天零点。改用包含起点、不包含次日零点的范围,保留小数秒,并核对 TIMESTAMP 会话时区。

日期筛选选了整个月,MySQL 查询却漏掉最后一天的大部分记录:

SELECT id, created_at
FROM events
WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31';

如果 created_at 是 DATETIME,结束日期对应的是 2026-01-31 00:00:00,不是这一天结束时刻。BETWEEN 包含两端,但中午或晚上的时间已经超过上界。月末零点恰好命中的记录仍可能返回,因此不能把现象概括成“BETWEEN 不包含结束值”。

下面按 MySQL 8.4 的时间比较规则说明,示例字段为 DATETIME。真正的 DATE 列没有时分秒,不应直接套用这个归因。

先确认字段和实际边界

对照表结构中的类型,以及应用最终绑定的起止参数。界面只显示日期,不代表数据库字段也是日期;日志中也要确认应用有没有把上界转换成次日零点,避免再加一天。

下面只读查询可观察日期转成时间后的值:

SELECT CAST('2026-01-31' AS DATETIME) AS upper_bound;

预期时间部分是 00:00:00。再核对一条被漏掉记录的完整时间及小数秒。若它实际早于上界,或字段本身是 DATE,应继续检查其他条件、参数绑定和时区,而不是继续调整结束日期。

用次日零点作为不包含的上界

需要“包含 1 月 31 日全天”时,查询范围写成左闭右开:

SELECT id, created_at
FROM events
WHERE created_at >= CAST('2026-01-01 00:00:00' AS DATETIME)
  AND created_at <  CAST('2026-02-01 00:00:00' AS DATETIME)
ORDER BY created_at, id;

下界包含当天零点,上界排除下一天零点。两处边界都明确为时间类型,列本身不做日期转换。events、created_at 和 id 是占位表列;实际查询应保留原有权限、租户和业务筛选。

用户输入的结束日期仍可保留为 1 月 31 日。应用将它转换成“下一日零点”用于查询,不需要改变界面上的选择。要按日历计算下一天并处理月末、闰年以及输入范围,不应把日期字符串最后两位简单加一。最终参数仍通过驱动绑定,不能把用户输入直接拼接成 SQL。

如果原接口已经约定上界不包含,就直接使用传入边界;不要在数据库层和应用层各加一天。

不用 23:59:59 猜测最后一个时刻

DATETIME 和 TIMESTAMP 支持小数秒。把上界改为 23:59:59 会漏掉 23:59:59.500000 这样的值。

反过来,把 BETWEEN 的结束值直接改为次日零点,又会把下一天零点也包括进来。半开区间同时避开这两处边界问题,不依赖字段当前是否保存毫秒或微秒。

对列使用 DATE(created_at) 再比较日期,可能改变普通时间列索引的使用方式。这里无需逐行改写列值,只要修正查询参数即可。是否采用索引仍取决于索引定义、数据分布和执行计划,不能只凭改用了比较符号就保证提速。

TIMESTAMP 还要确认会话时区

MySQL 的 TIMESTAMP 涉及会话时区与 UTC 的转换;DATETIME 不会以同样的方式自动转换。若业务把 UTC 时刻存进 DATETIME,也必须按该约定生成对应的 UTC 边界,不能直接拿本地日期零点比较。

可以只读检查当前连接:

SELECT @@session.time_zone;

“某地日历的一天”应先在业务时区确定开始和次日开始,再转换为存储或查询使用的时间表示。存在夏令时的地区,一天不一定等于固定的 86400 秒。不要为了修一条筛选悄悄修改全局数据库时区,影响其他连接。

验收要覆盖两侧零点

在隔离数据中核对开始日零点、结束日中午、结束日最后一秒内的小数时间、次日零点。前三类应按目标范围返回,次日零点应排除;相邻两段日期范围也不应在交界处重复。

保留原查询和参数转换逻辑,比较代表性结果与执行计划。调整不需要修改字段类型、截断已存时间或批量更新数据。需要回退时恢复查询代码,但回退后原来的末日漏选也会回来,应保留检查结果继续修正。