如何筛选最后登录会话更新时间在指定日期区间内的用户?
筛选最后一次登录会话更新时间在指定区间的用户:性能优化方案
嘿,咱们好好梳理下这个用户会话筛选的问题——尤其是你担心的百万级用户+海量登录记录场景下的性能问题。先明确下核心需求,确保咱们方向一致:
核心目标:找出最后一条登录会话记录的更新时间落在指定日期区间内的用户。
为什么要盯着最后一条会话的updated_at?因为登录会话可能跨天,同一用户还可能在多设备登录产生多条会话,咱们要确保筛选的是用户最近一次活跃会话的时间符合要求的情况。
举个例子就清楚了:
如果我们要筛选2019-09-15 00:00:00到2019-09-15 23:59:59区间的用户:
- Bill的最后一条会话更新时间在这个区间里,所以他应该被选中;
- John虽然有一条会话在区间内,但他最新的会话已经更新到
2019-09-18 12:00:00,不符合“最后一次会话在区间内”的要求,所以不能被筛选出来。
现有两个方案的性能隐患
先看看你给出的两个方案,在大流量场景下都存在明显的性能瓶颈:
方案1
SELECT * FROM ( SELECT * FROM ( SELECT * FROM `user_logins` WHERE `updated_at` >= '2019-09-15 00:00:00' ORDER BY `user_logins`.`id` DESC ) as sub GROUP BY `sub`.user_id ) as sub WHERE `sub`.updated_at BETWEEN '2019-09-15 00:00:00' and '2019-09-15 23:59:59'
这个方案的问题不小:
- 它依赖
ORDER BY id DESC+GROUP BY user_id的隐式特性(取分组后的第一条记录),这个行为在不同MySQL版本里可能有兼容性问题,不是标准SQL写法; - 内层先把所有
updated_at>=目标区间开始时间的记录全量排序,当登录记录达到千万级时,这个排序操作会吃掉大量内存和CPU; - 最后再加一层过滤,多了一次不必要的计算,进一步拖慢性能。
方案2
SELECT * FROM ( SELECT * FROM `user_logins` WHERE `updated_at` BETWEEN '2019-09-15 00:00:00' and '2019-09-15 23:59:59' ORDER BY `user_logins`.`id` DESC ) as sub WHERE NOT EXISTS ( SELECT 1 FROM user_logins WHERE user_id = sub.user_id and updated_at > '2019-09-15 23:59:59' );
这个方案比方案1稍好,但依然有硬伤:
- 外层的
NOT EXISTS子查询会对目标区间内的每条记录做一次关联查询,当区间内的记录很多时,相当于做了N次单表查询; - 哪怕给
user_id+updated_at加了索引,百万级用户下,这个关联的开销依然会让查询变得很慢。
大流量场景下的优化方案
要解决这个问题,核心思路是快速定位每个用户的最后一条会话记录,避免全量排序和多次关联。这里给你两个靠谱的优化方案:
方案A:窗口函数(MySQL 8.0+推荐)
如果你的MySQL版本是8.0及以上,窗口函数是处理这类“分组取最值”场景的最优解,性能远超嵌套分组或关联查询:
SELECT ul.* FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) AS rn FROM user_logins ) ul WHERE ul.rn = 1 AND ul.updated_at BETWEEN '2019-09-15 00:00:00' AND '2019-09-15 23:59:59';
性能优化点:
- 给
user_id+updated_at DESC建立复合索引:CREATE INDEX idx_userid_updatedat_desc ON user_logins(user_id, updated_at DESC); - 窗口函数可以直接利用这个索引快速完成分组并获取每个用户的最新会话,不需要全表排序,一次索引扫描就能完成所有计算。
方案B:关联查询(兼容低版本MySQL)
如果你的MySQL版本低于8.0,不支持窗口函数,可以用关联查询先找出每个用户的最新会话时间,再关联原表获取完整记录:
SELECT ul.* FROM user_logins ul INNER JOIN ( SELECT user_id, MAX(updated_at) AS latest_updated FROM user_logins GROUP BY user_id ) ul_latest ON ul.user_id = ul_latest.user_id AND ul.updated_at = ul_latest.latest_updated WHERE ul_latest.latest_updated BETWEEN '2019-09-15 00:00:00' AND '2019-09-15 23:59:59';
性能优化点:
- 给
user_id+updated_at建立复合索引:CREATE INDEX idx_userid_updatedat ON user_logins(user_id, updated_at); - 子查询可以直接通过索引快速计算每个用户的最新更新时间,不需要全表扫描;关联操作只针对每个用户的唯一最新记录,数据量小,效率极高。
关键索引建议
无论你选择哪个方案,复合索引都是必须的——它能让数据库跳过全表扫描和排序,直接定位到需要的记录,在百万级用户+千万级登录记录的场景下,性能会有数量级的提升。
内容的提问来源于stack exchange,提问作者Vlad Vladimir Hercules
相关产品推荐
相关产品推荐

