lag(order_time) over (partition by user_id order by order_time, order_id) 可安全获取用户上一次购买时间,需配合复合索引 idx_user_time 及标准时间差函数(如 postgresql 的 extract、mysql 的 timestampdiff)处理间隔计算。

用LAG()按用户分组排序取上一次购买时间
直接用 LAG() 窗口函数是最干净的解法。关键不是“怎么算差值”,而是“怎么拿到上一笔的时间”——必须按 user_id 分组、按 order_time 排序,否则跨用户错位,结果全乱。
常见错误是漏写 PARTITION BY user_id,导致所有用户混在一起排序,A用户的第二单可能被当成B用户的前一单;或者排序字段用错(比如用 order_id 代替 order_time),在订单乱序插入时出错。
-
LAG(order_time) OVER (PARTITION BY user_id ORDER BY order_time)返回上一行的下单时间,类型和order_time一致(通常是TIMESTAMP或DATETIME) - 如果某用户只有1笔订单,
LAG()返回NULL,对应行的间隔自然为NULL,不用额外过滤 - PostgreSQL 和 MySQL 8.0+、SQL Server 2012+ 都支持;SQLite 3.25+ 也行,但旧版不支持
计算间隔时注意时间单位和空值处理
时间相减的结果因数据库而异:order_time - prev_time 在 PostgreSQL 返回 INTERVAL,MySQL 返回秒数(如果字段是 DATETIME),SQL Server 可能报错或需用 DATEDIFF()。别硬写减法,用标准函数更稳。
- PostgreSQL:用
EXTRACT(EPOCH FROM (order_time - prev_time)) / 3600得小时数,或order_time - prev_time直接得INTERVAL - MySQL:用
TIMESTAMPDIFF(HOUR, prev_time, order_time),单位可选SECOND、DAY等,自动处理时区和闰秒 - SQL Server:用
DATEDIFF(minute, prev_time, order_time),注意参数顺序不能反 - 所有情况都要用
WHERE prev_time IS NOT NULL过滤首单(除非你真需要保留NULL)
性能陷阱:大表必须有复合索引
当用户量超百万、订单表几千万行时,LAG() 的窗口排序会变慢——它本质要对每个 user_id 子集单独排序。没索引的话,执行计划里常出现 WindowAgg on disk 或大量临时文件。
- 必须建复合索引:
CREATE INDEX idx_user_time ON orders (user_id, order_time) - 不要只建
(user_id)单列索引——排序仍要回表读order_time,效率低 - 如果查询还带时间范围(如“近30天”),索引可扩展为
(user_id, order_time, created_at),但优先保证前两列顺序 - MySQL 中若用
ORDER BY user_id, order_time全局排序再开窗,反而比分区排序慢,别这么干
业务逻辑补漏:同一时间多次下单怎么算?
真实场景里,用户可能1秒内下3单(比如购物车多商品提交)。此时 LAG() 默认按物理顺序取“上一行”,但 ORDER BY order_time 相同就无法保证稳定排序,间隔可能为0甚至负数。
- 加二级排序:
ORDER BY order_time, order_id(假设order_id自增),确保顺序唯一 - 如果业务要求“同一秒只算一次”,得先去重:
ROW_NUMBER() OVER (PARTITION BY user_id, DATE_TRUNC('second', order_time) ORDER BY order_id)然后筛rn = 1 - 别依赖数据库默认排序——哪怕看起来有序,优化器可能换执行路径,结果突变
间隔计算看着简单,真正上线时卡在索引缺失、时间单位混淆、或同一秒多单没兜住,才是常态。











