SQLite 按天查询为什么漏记录:用左闭右开区间覆盖最后一秒
按日期筛选日志、提交记录或操作历史时,结束时间经常被拼成“当天 23:59:59”。这个写法看起来覆盖了一整天,但当存储值包含时区标记或小数秒时,最后一秒的记录仍可能被漏掉。下面用 Python 标准库和内存 SQLite,把问题与修正方式完整跑一遍。所有数据均为合成数据,没有连接线上数据库。
同一天的记录为什么被排除
先准备 5 条时间:当天零点、23:59:58Z、23:59:59Z、23:59:59.500000Z,以及次日零点。这里明确按 UTC 自然日演示;业务中的北京时间日期需要先转换查询边界。
WHERE created_at >= "2026-11-12T00:00:00"
AND created_at <= "2026-11-12T23:59:59"
当列按 TEXT 比较时,SQL 比较的是文本值。23:59:59 后面还有 Z 或小数秒的字符串,会大于没有后缀的上界。在本地这组样例中,上面的查询只匹配 2 条,漏掉了当天最后一秒的两条记录。SQLite 不会因为字段名字叫 created_at,就自动把所有字符串统一为某个时区。
先统一格式,再使用左闭右开区间
把查询写成“大于等于当天零点,小于次日零点”,可以避免寻找某种精度下的“最后一刻”。下面还把时间统一为 UTC、固定六位小数及 Z 后缀,使同一字段的文本比较有一致前提。
import sqlite3
from datetime import datetime, timezone
samples = [
"2026-11-12T00:00:00Z",
"2026-11-12T23:59:58Z",
"2026-11-12T23:59:59Z",
"2026-11-12T23:59:59.500000Z",
"2026-11-13T00:00:00Z",
]
def canonical(value):
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
return dt.astimezone(timezone.utc).isoformat(
timespec="microseconds"
).replace("+00:00", "Z")
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE events(created_at TEXT)")
db.executemany("INSERT INTO events VALUES(?)",
[(canonical(x),) for x in samples])
start = canonical("2026-11-12T00:00:00Z")
end = canonical("2026-11-13T00:00:00Z")
rows = db.execute(
"SELECT created_at FROM events "
"WHERE created_at >= ? AND created_at < ? "
"ORDER BY created_at", (start, end)
).fetchall()
assert len(rows) == 4
print("matched:", len(rows))
这段代码本地运行输出 matched: 4。当天零点被保留,带小数秒的最后一秒被保留,次日零点被排除。参数通过占位符传入,避免在查询语句中拼接业务输入。
把日期、时区和存储约定一起检查
“2026-11-12”是业务日期,不天然代表 UTC 日期。如果产品按北京时间统计,应该先得到北京时间当天与次日的两个零点,再分别转换为数据库使用的时区。不要把没有时区的本地时间直接与带 Z 的 UTC 字符串混着比较。
如果继续用 TEXT 存储,需约定相同的时区、格式和精度;如果使用整数时间戳,也要约定秒或微秒单位。左闭右开的区间设计不能替代格式规范。线上已有数据格式不统一时,应先梳理数据和迁移方案,再改查询条件。
适合保留的回归用例有:当天零点、最后一秒、最后一秒的小数部分、次日零点,以及跨月、跨年。日期筛选的验收应检查具体记录集合,而不只看总数是否“差不多”。本例只验证筛选逻辑,没有验证生产数据库性能或某个云服务的部署。
参考:SQLite 官方数据类型与比较说明;Python datetime 官方文档。相关开源问题复现:eduSwarm Issue #1628。
- 点赞
- 收藏
- 关注作者
评论(0)