SQLite 时间窗统计静默假数字排查
> 一句话:「查询不报错」不等于「查询是对的」。 同一个问题,两种写法给出「全表命中」和「恒为 0」两个互相矛盾的假数字,全程零报错、退出码 0——差点把假数字写进报告再传播出去。
症状(按你会搜的词写)
用一条 SQL 数「某时间窗内有多少条记录」,打印出来的数字完全不合常理,但查询不报错:
- 第一版:「某时刻之后 = 377 条」——可总数才 377,等于把全表都算进时间窗了
- 第二版:「今日 00:00 之后 = 377 条」「近 14 天 = 377 条」——清一色全表命中
- 第三版(换写法后):「今天 = 0 条」——清一色 0 行,跟第一版正好相反
同一个问题,三种写法给出「全表命中」和「恒为 0」两个互相矛盾的假数字,全程零报错。
搜索词:sqlite 时间窗 统计 全表命中、created_at 字符串比较、sqlite 毫秒 时间戳 比较、or and 优先级 sqlite、静默假数字
真因(两处,叠加才暴露)
① INTEGER 毫秒列 与 字符串日期字面量 比较 → 条件恒假
时间列存的是 INTEGER 毫秒(如 1781525416446),查询却拿它与字符串字面量比较:
WHERE created_at > '2026-10-24 21:19:00' -- 恒假!
SQLite 比较时按类型优先级(NULL < INTEGER/REAL < TEXT < BLOB),TEXT 永远大于 INTEGER → 条件恒为假 → 恒 0 行。
最小复现(已实测):
字符串比较 ts > '2026-10-24 21:19:00' → 0 (共 2 行)
字符串比较 ts > '2000-01-01' → 0 (共 2 行)
INTEGER 毫秒比较 ts > 1781525416446 → 1
注意第 2 行:连 '2000-01-01' 这种「显然该全中」的字面量也是 0——恒假不只针对未来时间,是所有字符串字面量。
② WHERE 里 or 与 and 混用不加括号 → 整行通过
WHERE a.owner LIKE 'x%' or a.owner LIKE 'y%'
or a.owner LIKE 'z%' and r.created_at > 1781525416446
SQL 里 and 优先级高于 or → 实际解析成 A or B or (C and 时间条件),前两个分支已经恒真 → 整行通过 → 时间条件形同虚设 → 全表命中。
最小复现(已实测):
无括号 WHERE A or B or (C and D) → 2 (全表)
有括号 WHERE (A or B or C) and D → 0
两个 bug 方向相反(一个恒假、一个恒真),所以「换个写法」往往得到相反的假数字,更容易让人误以为是数据库坏了或结论随机。
影响面
- ✅ 高危场景:临时核验脚本 / 一次性排查查询。这类脚本的输出会直接进交付物和汇报 → 杀伤力在「报告数字不可信」,而且因为不报错,读者无法从退出码发现。
- ❌ 相对安全:生产判据层如果时间窗用参数化查询(
WHERE created_at > ?传入 int),owner 过滤在应用层分支而不是 SQL or 拼接,则口径正确。
解法(六条,按顺序)
- 先看 schema 再写查询:
PRAGMA table_info(<表>)确认时间列是 INTEGER 毫秒 / INTEGER 秒 / TEXT ISO,再决定字面量形态。 - 时间阈值一律 int + 参数化,不写字面量:
python
since = int(time.time() - N * 86400) * 1000 # 毫秒
cur.execute("SELECT count(*) FROM runs WHERE created_at > ?", (since,))
- or / and 混用必须加括号:集合条件用
(A or B or C)包起来再and时间条件。 - 反证自检(最有效):把时间阈值推到未来(或推到极远的过去),命中数必须变成 0;若不变 → 条件恒真(拼写/括号错了)。
- 双算法对拍:SQL 结果 vs 应用层过滤(Python 端
datetime.fromtimestamp(ms/1000)逐行判),不一致时以应用层为准,并把这条 SQL 判为有 bug。本轮靠这招把 377(全表命中)和 0(恒假)定位成 SQL 写法问题。 - 出数字前先问一句:这个数字如果恒等于全表行数 / 恒等于 0,说明不是数据问题,是查询问题。
通用教训
- 静默假数字比报错危险得多——报错会逼你停下核对,假数字会让你把结论写进报告再传播出去。
- 同一问题写出互相矛盾的两种数字时,先怀疑查询本身,再怀疑数据。 矛盾本身就是最强的信号:真实数据不会因为换一种等价写法就全表命中和恒零对撞。
- 判据层的 SQL 与一次性核验的 SQL 是两套东西——前者出问题会系统性污染体检结论(重),后者污染一轮汇报(轻)但出现概率更高,所以每次写核验查询都要自带反证。