SQLite 时区陷阱:一字之差,38 条会话归错日期
一、症状:三个页面,两个时间线
venxine.vip 的 /token/ 看板有三个子页面:
| 页面 | 数据来源 | 日期基准 |
|---|---|---|
| summary(用量总览) | Python datetime |
Asia/Shanghai |
| usage(用量明细) | Python datetime |
Asia/Shanghai |
| database(数据明细) | SQLite GROUP BY date() |
UTC |
summary 和 usage 用的是 Python 代码里的 datetime.now(),天然按服务器本地时区(Asia/Shanghai)。但 database 页面用的是 SQL 的 GROUP BY date(started_at):
SELECT date(datetime(started_at, 'unixepoch')) as date,
COUNT(*) as sessions,
SUM(input_tokens + output_tokens) as tokens
FROM sessions
GROUP BY date
ORDER BY date DESC
早上 06:00~08:00 的 cron 会话(华尔街日报、Crypto 日报、AI 新闻),在北京时间已经是"今天",但在 UTC 时间还在"昨天"。
结果:同样一笔 token 消耗,在 summary 归入 7月3日,在 database 归入 7月2日。
二、根因:unixepoch 的默认时区
SQLite 的 datetime() 函数签名:
datetime(timestring, modifier, modifier, ...)
unixepoch 修饰符的含义是"把前面的数字当作 Unix 时间戳"。但它默认输出 UTC 时间,不会自动转换时区。
-- unixepoch = 1751500800(2026-07-03 00:00:00 UTC = 2026-07-03 08:00:00 BJT)
-- 默认 UTC
SELECT datetime(1751500800, 'unixepoch');
-- → '2026-07-03 00:00:00'
-- 加 localtime 转北京时区
SELECT datetime(1751500800, 'unixepoch', 'localtime');
-- → '2026-07-03 08:00:00'
不加 localtime,SQLite 当 UTC 处理。加了 localtime,SQLite 读取操作系统的时区设置(/etc/timezone 或 TZ 环境变量)做转换。
三、修复:一行代码
# app.py 第 2013 行
# 改前(UTC)
date(datetime(started_at, 'unixepoch')) as date
# 改后(北京时区)
date(datetime(started_at, 'unixepoch', 'localtime')) as date
只加了一个参数 'localtime'。
四、影响范围
修复前统计:
SELECT COUNT(*) FROM sessions
WHERE date(datetime(started_at, 'unixepoch'))
!= date(datetime(started_at, 'unixepoch', 'localtime'));
-- → 38 条
38 条会话(占总数的 9.5%)之前归错了日期。全部是 06:00~08:00 之间启动的早间 cron 任务。
修复后三个页面完全统一:
| 页面 | 时区 | 修复前 | 修复后 |
|---|---|---|---|
| summary | Asia/Shanghai | ✅ 正确 | ✅ 正确 |
| usage | Asia/Shanghai | ✅ 正确 | ✅ 正确 |
| database | UTC → Asia/Shanghai | ❌ 错位 | ✅ 正确 |
五、SQLite 时区速查表
| 需求 | 写法 | 说明 |
|---|---|---|
| 存时间戳(推荐) | INTEGER 存 Unix 秒 |
无时区歧义,前端转换 |
| 读时间戳为 UTC | datetime(ts, 'unixepoch') |
默认 UTC |
| 读时间戳为本地时区 | datetime(ts, 'unixepoch', 'localtime') |
跟随 OS 时区 |
| 当前 UTC 时间 | datetime('now') |
等价于 UTC now |
| 当前本地时间 | datetime('now', 'localtime') |
跟随 OS 时区 |
| 存文本时间(不推荐) | TEXT 存 '2026-07-03 08:00:00' |
有歧义,不知道是哪个时区 |
最佳实践:永远用 INTEGER 存 Unix 时间戳,查询时显式指定 'unixepoch' + 'localtime'。
六、教训
- SQLite 没有原生时区类型:
datetime()只是格式化函数,不做时区推断 unixepoch默认 UTC:不加localtime就是 UTC 输出- Python 和 SQLite 的时区不一致是常见陷阱:Python
datetime.now()拿本地时间,SQLitedatetime(ts, 'unixepoch')输出 UTC,同一个"今天"可能差 8 小时 - 三页面统一验证法:如果一个数据源有多处展示,抽查同一天的数值是否能对上,这是发现时区 bug 的最快方法