ORDER BY RAND() 性能极差,因全表扫描、每行调用 RAND()、临时表排序;主键范围法通过定位随机替代排序随机,走索引实现毫秒响应,但需处理空洞和偏置问题。

因为 MySQL 会为每一行都调用一次 RAND(),再把全表数据加载进临时表排序——哪怕你只要 1 条,它也得扫完百万行、生成百万个随机数、做全局排序。
ORDER BY RAND() 的执行模型就是反性能的
这不是写法错,是 MySQL 对这个语法的处理逻辑天生不适合大数据量。你写 SELECT * FROM users ORDER BY RAND() LIMIT 1,MySQL 实际干了五件事:
- 全表扫描(
type: ALL),不走任何索引 - 对每一行调用一次
RAND(),CPU 消耗随行数线性增长 - 把所有行 + 随机值塞进内存临时表;内存不够就落盘
- 对整个临时表做
filesort(磁盘排序),IO 爆涨 - 最后才取第 1 行
EXPLAIN 里一定出现 Using temporary; Using filesort —— 这俩标志同时存在,基本等于“慢查询确诊”。10 万行表可能 2 秒,100 万行直接 30 秒+,且无法靠加索引缓解。
主键范围法为什么快:绕开排序,直奔索引
核心是放弃“排序随机”,改用“定位随机”:利用主键(最好是自增整型)的有序性和索引能力,跳到一个大致随机的位置再取行。
安全写法(防偏置):
SELECT * FROM users WHERE id >= ( SELECT FLOOR(RAND() * ((SELECT MAX(id) FROM users) - (SELECT MIN(id) FROM users)) + (SELECT MIN(id) FROM users)) ) ORDER BY id LIMIT 1;
关键点:
-
EXPLAIN显示type: range,走主键索引,毫秒级响应 - 漏掉
MIN(id)偏移会导致结果严重偏向开头(比如总 ID 范围是 1000–10000,但只用RAND() * MAX(id),实际起点永远在 0–10000,大量命中 1000 附近) - ID 空洞太多(如删了 40% 中间记录)时可能查不到数据,需应用层加重试或 fallback
取多条时别反复执行单条查询
要取 10 条,执行 10 次上面的单条语句?不行。每次都是独立随机起点,大概率重复、遗漏,且网络和解析开销翻倍。
更稳的做法:
- 先查
COUNT(*)得总数N - 在应用层生成 10 个不重复的随机整数
r ∈ [1, N] - 用这些数字去查稠密序号(比如额外维护一列
seq_id),或转成主键范围后批量IN查询 - 如果主键空洞严重,直接建一张
random_id_map表存打乱后的有效 ID,小表上跑ORDER BY RAND()就不疼了
别踩这些常见坑
很多看似“优化”的写法其实没解决问题:
-
JOIN (SELECT FLOOR(RAND() * MAX(id)) AS r FROM t) t2 ON t1.id >= t2.r——RAND()在 JOIN 条件里仍会被多次计算,没本质改善 -
WHERE id IN (SELECT id FROM t ORDER BY RAND() LIMIT 10)—— 子查询还是全表ORDER BY RAND(),只是换了个位置 - 用
UNIX_TIMESTAMP(created_at) % N做哈希过滤时,N 太小(如 10)会导致桶分布不均,N 太大(如 10000)又可能返回不足预期条数
真正有效的优化,永远围绕两点:**避开全表扫描**、**绕过全局排序**。其他所有技巧,都是在这两个约束下做的取舍。空洞率、数据分布、QPS 要求——这些细节不确认清楚,方案就容易在线上翻车。


















