LEAD(ip, 1) OVER (PARTITION BY user_id ORDER BY login_time) 可取下一次登录IP,须确保按用户和时间严格排序,重复时间需追加唯一字段排序,并用CASE或COALESCE处理NULL。

LEAD 函数怎么写才能拿到“下一次登录”的 IP
直接用 LEAD(ip, 1) 就能取下一行的 IP,但前提是数据已按用户 + 时间严格排序。很多人漏掉 ORDER BY 子句,导致结果完全错乱——LEAD 不会自动按时间排,它只按你指定的窗口排序走。
必须搭配 PARTITION BY user_id,否则张三的下一条可能是李四的记录。
-
LEAD(ip, 1) OVER (PARTITION BY user_id ORDER BY login_time)是基础写法 - 如果
login_time有重复(比如毫秒级精度丢失),建议追加主键或唯一字段:ORDER BY login_time, id - 别用
ORDER BY created_at DESC想“倒着取上一次”——那得用LAG,不是LEAD
计算 IP 变化:NULL 怎么处理、相等怎么标
两次 IP 相同,差异为 0;不同则为 1;若下一次登录不存在(如最后一次),LEAD 返回 NULL,直接和 IP 比较会得 NULL,不是 false。
- 用
COALESCE(LEAD(ip) OVER (...), ip)填补 NULL,再比较,但会把“末次登录”误判为“无变化” - 更安全的是显式判断:
CASE WHEN LEAD(ip) OVER (...) IS NULL THEN NULL WHEN LEAD(ip) OVER (...) = ip THEN 0 ELSE 1 END - 如果只要标记“是否更换”,布尔表达式更简洁:
ip != LEAD(ip) OVER (...)—— 注意多数数据库中NULL != 'x'结果是 NULL,不是 true/false
MySQL 8.0+ 和 PostgreSQL 的兼容细节
语法一致,但 MySQL 在 WHERE 里不能直接用窗口函数,PostgreSQL 可以。想筛出“IP 变了的记录”,得套一层子查询或 CTE。
- MySQL 必须写成:
SELECT * FROM (SELECT user_id, ip, login_time, LEAD(ip) OVER (...) AS next_ip FROM logins) t WHERE ip != t.next_ip AND t.next_ip IS NOT NULL
- PostgreSQL 允许:
SELECT * FROM logins WHERE ip != LEAD(ip) OVER (PARTITION BY user_id ORDER BY login_time)—— 但实际执行仍可能报错,稳妥起见也建议子查询 - SQLite 3.25+ 支持
LEAD,但不支持PARTITION BY,只能全表排序,多用户场景不可用
性能容易被忽略的点:索引怎么建
没索引时,LEAD 窗口排序会触发 filesort,千万级日志表可能秒变分钟级。
- 复合索引必须覆盖
(user_id, login_time),顺序不能颠倒 - 如果还查
ip字段,可考虑包含索引:CREATE INDEX idx_user_time_ip ON logins (user_id, login_time) INCLUDE (ip)(PostgreSQL)或INDEX (user_id, login_time, ip)(MySQL) - 避免在
login_time上用函数,比如DATE(login_time),会让索引失效,LEAD就没法利用排序优势
真实业务里,“IP 变化”常要关联地理信息或风控规则,这时候 LEAD 只是第一步;更麻烦的是登录时间跨天、代理 IP 池复用、或用户多端同时在线——这些不会在 LEAD 的两行之间体现出来。










