MySQL本身不记录last_login_time字段,需依赖业务表中自建的last_login_at等字段查询;典型语句为SELECT id,username,last_login_at FROM users WHERE last_login_at < DATE_SUB(NOW(), INTERVAL 180 DAY) OR last_login_at IS NULL,但须结合created_at、索引优化及归档策略谨慎处理。

查出 last_login_time 字段为空或过期的账号
MySQL 本身不记录用户登录时间,所谓“僵尸账号”必须依赖业务表(比如 users 表)中自建的登录时间字段,常见名是 last_login_at、updated_at 或 login_time。如果字段不存在,直接查 MySQL 系统库 mysql.user 是无效的——它只存账号元信息,不存行为日志。
典型查询语句:
SELECT id, username, last_login_at FROM users WHERE last_login_at < DATE_SUB(NOW(), INTERVAL 180 DAY) OR last_login_at IS NULL;
-
INTERVAL 180 DAY可按需改成 90、365,注意别用DATE_ADD写反方向 - 务必确认
last_login_at是DATETIME或TIMESTAMP类型;若为字符串(如VARCHAR),需要先用STR_TO_DATE()转换,否则索引失效且结果错乱 - 如果业务用软删除(
is_deleted = 1),记得加AND is_deleted = 0
避免误删:区分“真僵尸”和“首次注册未登录”用户
刚注册但还没登录过的用户,last_login_at 也是 NULL,和长期未登录用户混在一起。仅靠空值判断会误伤。
更稳妥的做法是结合注册时间:
SELECT id, username, created_at, last_login_at
FROM users
WHERE (last_login_at < DATE_SUB(NOW(), INTERVAL 180 DAY)
AND last_login_at IS NOT NULL)
OR (last_login_at IS NULL
AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY));
- 把“从未登录但注册超 7 天”的用户也纳入统计,排除掉刚注册几分钟的测试账号
- 如果注册后有邮箱/手机验证流程,可额外加
AND verified_at IS NOT NULL过滤掉未激活账号 - 别只看
created_at:有些系统允许后台代创建用户,这类账号可能created_at很早但实际从未交付使用
用 EXPLAIN 验证查询是否走索引
当用户量上百万时,没索引的 last_login_at IS NULL 或范围查询会全表扫描,执行可能卡住几十秒甚至超时。
检查索引是否存在:
SHOW INDEX FROM users WHERE Key_name = 'idx_last_login_at';
- 推荐复合索引:
ALTER TABLE users ADD INDEX idx_last_login_at (last_login_at, created_at); - 单列索引也行,但
last_login_at上的索引必须是NOT NULL字段才高效;若允许 NULL,MySQL 对IS NULL的索引支持较弱,建议改用last_login_at < '1970-01-01'这类固定哨兵值替代空值存储 - 执行前一定跑
EXPLAIN,确认type是range或ref,不是ALL
导出结果并对接清理流程(不是直接 DELETE)
统计只是第一步,真正清理要走审批和备份流程。别在生产库直接 DELETE FROM users WHERE ...。
- 先导出 ID 列表:
SELECT id FROM users WHERE ... INTO OUTFILE '/tmp/zombie_ids.txt';(注意 MySQL 的secure_file_priv路径限制) - 更安全的做法是生成带时间戳的 SQL 文件:
SELECT CONCAT('UPDATE users SET status = ''archived'', archived_at = NOW() WHERE id = ', id, ';') FROM users WHERE ...; - 如果账号关联订单、日志等外键数据,先查
SELECT COUNT(*) FROM orders WHERE user_id IN (...),确认无关键业务依赖再归档
最常被忽略的一点:很多系统把“禁用账号”和“删除账号”混为一谈。归档到 status = 'archived' 比物理删除更可逆,也避免外键级联断裂。


















