order by rand() 在大表上不可扩展,因需全表扫描、逐行计算rand()并排序;应改用主键范围随机采样(如where id >= ceil(rand() * max(id))),性能提升数十倍。

别用 ORDER BY RAND() 抽大表,它不是“慢一点”,是根本不可扩展。
为什么 ORDER BY RAND() 在大表上会卡死
MySQL 对每一行都调用一次 RAND(),再对全部结果做全表排序(filesort),最后才 LIMIT。哪怕只要 1 条,也要扫描、计算、排序全部数据。
- 10 万行表:大概率触发磁盘临时文件,I/O 爆涨
- 百万行表:常见超时、连接池打满、
Sort_merge_passes指标飙升 - 错误现象示例:
SELECT * FROM user ORDER BY RAND() LIMIT 5执行 8 秒以上,或直接被max_execution_time中断 - 加了主键索引也没用——
ORDER BY RAND()完全不走索引
用主键范围随机采样:最常用且有效的替代方案
前提是表有自增整型主键(如 id),且删除不频繁(空洞少)。核心是绕过排序,直接查。
- 单次查询写法:
SELECT * FROM user WHERE id >= CEIL(RAND() * (SELECT MAX(id) FROM user)) ORDER BY id LIMIT 5 - 注意:
(SELECT MAX(id) FROM user)必须提前算好或缓存,不能写成CEIL(RAND() * (SELECT MAX(id) FROM user))—— 否则子查询每行都执行一遍 - 如果
id有大量空洞(比如删过 50% 数据),可能返回不足 5 行;建议应用层最多重试 3 次,或生成 8~10 个随机 ID 再IN查询去重补足 - 性能对比:30 万行表,
ORDER BY RAND() LIMIT 5平均 0.66s;同样条件用该方案平均 0.015s
主键不连续又不想重试?用 JOIN 子查询构造起点
比纯范围采样更抗空洞,但语法稍复杂,仍需处理返回行数不足的问题。
- 推荐写法:
SELECT t1.* FROM user AS t1 JOIN (SELECT CEIL(RAND() * (SELECT MAX(id) FROM user)) AS id) AS t2 ON t1.id >= t2.id ORDER BY t1.id LIMIT 5 - 关键点:
JOIN把随机值只生成一次,避免子查询重复执行 - 不要写成
WHERE id > (SELECT ... RAND() ...)—— 这会导致 MySQL 对每行都重新算子查询,性能反而更差 - 高并发下
MAX(id)可用 Redis 缓存(TTL 设 60 秒),避免每次查都压数据库
真正需要严格均匀随机时的现实约束
如果你在做抽奖、审计抽样这类要求统计学意义均匀的场景,所有基于主键范围的方法本质上都是“近似随机”——ID 空洞会让低 ID 区域被选中概率偏高。
-
ORDER BY RAND()是唯一能保证严格均匀的 SQL 原生方案,但它代价是不可接受的资源消耗 - 折中做法:应用层先
SELECT id FROM user WHERE ...拿全部 ID(加SQL_CALC_FOUND_ROWS或分页拉取),再用random.sample()或Fisher–Yates洗牌取 N 个,最后WHERE id IN (...) - 这个方案 IO 和内存开销转移到应用,但数据库压力可控;适合日活不高、ID 总量在几十万以内的业务











