mysql本身不记录last_login_time字段,需依赖业务表中自建的last_login_at等字段查询;典型语句为select id,username,last_login_at from users where last_login_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
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
- 把“从未登录但注册超 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 这类固定哨兵值替代空值存储- 执行前一定跑
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'比物理删除更可逆,也避免外键级联断裂。












