EXISTS子查询是判断24小时内状态变更的最常用写法,需关联外层表、用BETWEEN限定时间范围,并注意数据库间datetime函数差异;LAG()窗口函数更高效但需按record_id和updated_at排序且过滤NULL。

WHERE子句里用EXISTS判断24小时内状态变更
直接在主查询中嵌套一个子查询,用EXISTS检查同一record_id是否存在另一条时间差在24小时内的不同status记录。这是最常用也最易读的写法,避免了自连接带来的笛卡尔积风险。
- 子查询必须关联外层表(例如用
t1.record_id = t2.record_id),否则会变成全表扫描 - 时间比较要用
BETWEEN或AND显式限定范围,别只写t2.updated_at > t1.updated_at——漏掉上限会导致查出几天前的变更 - PostgreSQL/MySQL 8.0+/SQL Server都支持,但SQLite需注意
datetime函数语法差异(比如SQLite用datetime(t1.updated_at, '+24 hours')) - 示例:
SELECT * FROM logs t1<br>WHERE EXISTS (<br> SELECT 1 FROM logs t2<br> WHERE t2.record_id = t1.record_id<br> AND t2.status != t1.status<br> AND t2.updated_at BETWEEN t1.updated_at AND datetime(t1.updated_at, '+24 hours')<br>);
用窗口函数LAG()对比相邻状态(推荐用于有序流水日志)
如果数据按record_id和updated_at天然有序,LAG()比嵌套查询更高效——它只扫一遍表,且能精准定位“上一条”记录的状态变化。
- 必须配合
PARTITION BY record_id ORDER BY updated_at,否则跨ID比较会出错 -
LAG(status)返回的是前一行的值,所以要判断status != LAG(status) OVER (...)是否为真 - 时间差验证不能省:再加一列
LAG(updated_at),然后用updated_at - LAG(updated_at)算秒数或小时数(不同数据库单位函数不同,PostgreSQL用EXTRACT(EPOCH FROM ...),MySQL用TIMESTAMPDIFF(HOUR, ...)) - 注意NULL:首条记录的
LAG()结果是NULL,需用WHERE prev_status IS NOT NULL过滤
自连接容易踩的三个坑
有人习惯用JOIN logs t1 ON t1.record_id = t2.record_id方式,但实际执行时极易翻车。
- 没加
t1.updated_at 条件 → 每条记录和自己匹配,状态永远相等,结果全空或全错 - 只写
t1.updated_at 不加时间上限 → 查出所有历史变更,不是“24小时内” - 没去重 → 同一记录多次变更会生成多行重复结果,需加
DISTINCT t1.id或GROUP BY t1.id - 性能差:自连接数据量是N²级,1万行日志可能产生1亿次比较;而
EXISTS或窗口函数基本是N级
时间字段类型不一致导致查询失效
看似逻辑正确,但查不到结果?大概率是updated_at字段类型和函数不匹配。
- MySQL里
DATETIME和TIMESTAMP行为不同:后者受时区影响,NOW()和字段值可能不在同一时区 - PostgreSQL若字段是
text存时间字符串(如'2024-05-20 14:30:00'),updated_at + INTERVAL '24 hours'会报错,必须先CAST(updated_at AS TIMESTAMP) - Oracle需用
TO_DATE()或TO_TIMESTAMP()显式转换,否则隐式转换可能丢失精度 - 验证方法:单独查
SELECT updated_at, TYPEOF(updated_at) FROM logs LIMIT 1(SQLite)或pg_typeof()(PG)确认类型
时间范围计算本身不难,难的是让数据库真正按你的意图执行——类型、索引、关联条件缺一不可。尤其当表没在(record_id, updated_at)上建复合索引时,哪怕SQL写对了,也可能跑十分钟才出结果。

















