<p>ORDER BY RAND() 在百万级表上卡死,因其需为每行计算 RAND() 并全表排序,触发 filesort 和全表扫描;应改用主键范围随机查询(如 WHERE id >= FLOOR(RAND() * (MAX(id)-MIN(id)) + MIN(id)) ORDER BY id LIMIT 1),避免全表计算与排序。</p>

为什么 ORDER BY RAND() 在百万级表上会卡死
它不是“随机”慢,而是执行逻辑注定低效:MySQL 必须为每一行调用 RAND(),再把全部结果放进临时表做 filesort,最后才取 LIMIT。哪怕只要 1 条,也得扫全表、生成百万个随机数、排序百万行——EXPLAIN 显示 type: ALL 和 Extra: Using filesort,索引完全失效。
主键范围随机查询:最常用且见效快的替代方案
前提是你有单调递增、空洞不严重的自增主键(比如 id)。核心是跳过全表扫描,直接定位到一个近似随机的位置再向后取值。
-
FLOOR(RAND() * ((SELECT MAX(id) FROM t) - (SELECT MIN(id) FROM t)) + (SELECT MIN(id) FROM t))比CEIL(RAND() * MAX(id))更安全,避免越界或集中打在开头 - 必须显式加上
MIN(id)偏移,否则随机数从 0 开始,大量请求会挤在 id=1 附近 - 语句要写成
WHERE id >= ? ORDER BY id LIMIT 1,才能走主键索引,EXPLAIN显示type: range - 如果表里删过大量记录(id 空洞多),这条可能返回空——这不是 bug,是设计取舍;应用层最多重试 2–3 次即可补足
带 WHERE 条件的随机抽样怎么写
不能直接在原表上套 ORDER BY RAND(),也不能对子查询结果再 ORDER BY RAND()。正确做法是先限定条件,再在其 id 范围内采样。
- 先查符合条件的最小和最大 id:
SELECT MIN(id), MAX(id) FROM question WHERE level = 1 - 生成随机起点:
FLOOR(RAND() * (max_id - min_id) + min_id) - 主查询用:
SELECT * FROM question WHERE level = 1 AND id >= ? ORDER BY id LIMIT 1 - 别写成
WHERE id > (SELECT ... RAND() ...)这种嵌套,MySQL 可能对每行都重算子查询,性能反而更差
真正需要严格均匀随机时,该选哪条路
如果你在做抽奖、AB 测试分组这类对概率精度敏感的场景,那所有基于主键偏移的方法都有偏差——空洞导致某些记录被选中概率更低。
- 小表(ORDER BY RAND() LIMIT N,加覆盖索引减少 I/O
- 中大表但允许轻微偏差:主键范围法 + 应用层重试,95% 场景够用
- 大表且要求等概率:用应用层预取 ID 列表(
SELECT id FROM t WHERE ...),Collections.shuffle()后取前 N 个,再IN回查——内存换精度,注意缓存 ID 列表 - 千万别在高并发接口里反复查
MAX(id),用 Redis 缓存,每分钟更新一次即可
空洞问题永远存在,没有银弹;关键是你愿为“绝对均匀”付出多少性能和工程成本。


















