order by rand()在大表上卡死是因为mysql需为每一行调用rand()并全表排序,触发filesort导致i/o和cpu飙升;推荐主键范围抽样(如where id >= floor(rand() * (max-min) + min) order by id limit n)替代。

ORDER BY RAND() 为什么在大表上会卡死
因为 MySQL 必须为 SELECT * FROM t ORDER BY RAND() LIMIT 10 中的**每一行都调用一次 RAND()**,再对全表结果做排序(使用临时表 + 文件排序),哪怕你只要 10 条。I/O 和 CPU 双高,5 万行以上就明显延迟,100 万行可能秒变 3 秒以上。
它不“随机得不够”,而是“算得太满”。小表(≤1 万行)可直接用;中大表必须绕开这个逻辑。
-
ORDER BY RAND()的不重复性是天然的——每行随机值几乎唯一,取前 N 条自然无重复 - 但性能瓶颈不在
RAND()函数本身,而在后续的全表排序动作 - 加
WHERE条件(如WHERE status = 1)后,仍需先过滤再逐行算RAND(),无法跳过无效行
主键区间采样:用 MAX(id) + FLOOR(RAND() * ...) 快速生成候选 ID
前提是表有自增 id(或类似单调递增主键),且空洞不多(比如删过大量数据后 id 不连续,就会漏行)。
最常用写法是借 information_schema.tables 当驱动表,避免低版本不支持 FROM DUAL:
SELECT t1.* FROM tbl t1 INNER JOIN ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM tbl)) + 1 AS id FROM information_schema.tables LIMIT 10 ) t2 ON t1.id = t2.id;
-
FLOOR(RAND() * MAX(id)) + 1保证生成 ID 落在[1, MAX(id)]区间内 - 若表中实际缺失某些 ID(比如删过记录),
JOIN后结果可能少于 10 条 -
information_schema.tables仅作“空行源”,不查真实数据,兼容 MySQL 5.6+
WHERE id IN (...) 替代 JOIN 更简洁,但要注意长度和空洞
适合 N 较小(≤50)、ID 分布尚可的场景,语义更直白:
SELECT * FROM tbl WHERE id IN ( SELECT FLOOR(RAND() * (SELECT MAX(id) FROM tbl)) + 1 FROM information_schema.tables LIMIT 10 );
- MySQL 8.0.19+ 原生支持子查询出现在
IN右侧;旧版本需改写为JOIN或先建临时表 -
IN列表受max_allowed_packet限制,拼太长会报错Packet too large - 同样面临空洞问题:生成的 10 个随机 ID 中若有 3 个不存在,最终只返回 7 行
要求严格返回 N 条时,必须补漏或换策略
上面所有基于主键采样的方法,都无法 100% 保证返回 N 条——这是设计使然,不是写错了。
如果业务强依赖“必须拿到 N 条”,有两个务实选择:
- 应用层循环重试:执行一次,检查返回行数,不足则再查,最多重试 3 次(适合 N 小、空洞率
- 两步兜底法:先用
ORDER BY RAND() LIMIT N*2快速捞出冗余样本,再用应用层去重并截取前 N 条(N ≤ 100 时较稳) - 彻底放弃 ID 采样,改用
WHERE id % k = FLOOR(RAND() * k)类似哈希抽样(适合离线分析,非强实时)
真正难的不是写出一条“看起来随机”的 SQL,而是清楚知道它在什么数据分布、什么并发压力、什么 MySQL 版本下会掉链子——尤其是空洞率和 MAX(id) 是否真实反映有效行数,这点最容易被忽略。











