ORDER BY RAND()在大表上极慢是因为每行都调用RAND()并全表排序,触发filesort导致I/O和CPU飙升;推荐主键范围抽样(如WHERE id >= FLOOR(RAND() * (MAX(id)-MIN(id)) + MIN(id)) ORDER BY id LIMIT N)替代。

别用 ORDER BY RAND() 抽数据,10 万行以上基本不可用;主键范围抽样才是生产环境可行方案。
为什么 ORDER BY RAND() 在大表上会卡死
MySQL 对每一行都调用一次 RAND(),再全表排序——哪怕你只 LIMIT 1。这个过程无法走索引,必然触发 filesort,I/O 和 CPU 双爆。实测 30 万行表,SELECT * FROM user ORDER BY RAND() LIMIT 10 耗时 0.6 秒以上;百万行常超 5 秒甚至连接超时。
常见错误现象:SHOW PROCESSLIST 里长期卡在 Sorting result 状态;慢查询日志频繁出现该语句;数据库负载突增,其他查询被拖慢。
-
RAND()是非确定性函数,MySQL 拒绝为其建立任何索引路径 - MyISAM 和 InnoDB 表表现一致,不存在引擎级优化空间
- 加了
id索引也没用,因为排序依据是随机值,不是索引列
主键范围抽样的标准写法(带容错)
前提:表有自增 id(或单调递增的数字主键),且删除不频繁(空洞少)。核心是避免排序,改用主键 B+ 树范围查找。
推荐语句:
SELECT * FROM user WHERE id >= FLOOR(RAND() * ((SELECT MAX(id) FROM user) - (SELECT MIN(id) FROM user)) + (SELECT MIN(id) FROM user)) ORDER BY id LIMIT 10;
关键点说明:
- 必须显式计算
MIN(id)和MAX(id),否则只用MAX(id)会导致结果集中在表头(比如id从 10000 开始,RAND() * MAX(id)仍可能生成 1~9999 的无效值) - 子查询
(SELECT MAX(id) FROM user)会被执行一次,但写成(SELECT MAX(id) FROM user) * RAND()会导致 MySQL 每行都重算子查询,性能更差 -
FLOOR比CEIL更稳妥,避免生成超出MAX(id)的值
空洞多时怎么补足行数
如果表经历过大量删除,id 不连续,单次查询可能返回少于预期行数(比如只查到 3 条)。不能靠“运气”一次查够,得主动兜底。
建议在应用层控制:
- 先执行一次主键范围查询,拿到实际返回行数
- 若不足 N 条,再生成新随机起点重试,最多 3 次
- 仍不够?改用两次采样
JOIN方案(见下条),或退化为ORDER BY RAND()(仅限极低频、小流量场景)
JOIN 写法(适合中等空洞):
SELECT t1.* FROM user AS t1 JOIN ( SELECT FLOOR(RAND() * ((SELECT MAX(id) FROM user) - (SELECT MIN(id) FROM user)) + (SELECT MIN(id) FROM user)) AS id ) AS t2 ON t1.id >= t2.id ORDER BY t1.id LIMIT 10;
注意:JOIN 版本仍可能返回不足 10 行,且 MAX(id) 子查询每次都会执行,高并发下建议用 Redis 缓存该值(每分钟刷新一次)。
UUID 或无主键表怎么办
上述所有基于 id 的方案对 UUID 主键、复合主键、无主键表完全失效——因为无法定义数值范围。
可选替代方案:
- 加一个自增辅助列(
AUTO_INCREMENT),仅用于随机抽样,业务不依赖 - 用
ROW_NUMBER() OVER(ORDER BY RAND())(MySQL 8.0+),但仍是全表扫描,仅比ORDER BY RAND()略优,不解决根本问题 - 应用层分页拉取:先
SELECT COUNT(*),再随机生成偏移量OFFSET,但OFFSET大时性能也差(如OFFSET 100000)
真正难的是“均匀随机”和“高性能”不可兼得。业务若要求抽奖级均匀性,就得接受额外开销;若只是内容推荐类弱随机,主键范围抽样已足够——别在 SQL 层强行追求数学意义的完美随机。











