mysql 的 order by rand() 不能用于大表,因其需全表扫描并为每行计算随机数后内存排序,导致 using filesort 和 using temporary,qps骤降;百万级表执行耗时2~5秒且并发易卡死,仅适用于count(*)

MySQL 的 ORDER BY RAND() 为什么不能直接用在大表上
因为它是全表扫描+内存排序:MySQL 对每一行生成一个随机数,再对所有行按这个随机数排序。10 万行就要算 10 万次随机数、搬运并比较全部数据——EXPLAIN 里会看到 Using filesort 和 Using temporary,QPS 下降明显,还容易拖垮主库。
常见错误现象:SELECT * FROM users ORDER BY RAND() LIMIT 10 在百万级表上执行要 2~5 秒,且并发一高就卡死。
- 只适合预估
COUNT(*) 的小表或缓存层兜底 - MyISAM 表比 InnoDB 略快(因统计行数更准),但差距微乎其微
- 无法利用索引,
WHERE条件再强也救不了排序阶段的开销
Laravel 中用 inRandomOrder() 的真实行为
它只是封装了 ORDER BY RAND()(MySQL)或 ORDER BY RANDOM()(PostgreSQL),底层没做任何优化。调用 User::inRandomOrder()->limit(5)->get(),生成的 SQL 就是原生的随机排序语句。
使用场景:管理后台抽样查日志、测试数据填充、低频运营活动页——这些地方 QPS 低、数据量可控,可以接受。
- 不支持链式
where后再加inRandomOrder()的“条件内随机”,它总是在最终结果集上随机 - 如果先
where('status', 1)再inRandomOrder(),MySQL 仍需扫描所有status = 1的行再排序 - 在 SQLite 中可用,但同样慢;在 SQL Server 上不支持,会抛出
NotSupportedException
真正能落地的替代方案:ID 范围采样 + 两次查询
核心思路是避开全表排序,用主键(通常是自增 id)的分布特性做概率采样。适用于主键连续或基本连续的表。
实操步骤:
- 先用
SELECT MIN(id), MAX(id) FROM users WHERE status = 1拿到有效范围 - PHP 里生成 N 个
rand($min, $max),去重后作为候选 ID 数组 - 再查
SELECT * FROM users WHERE id IN (?,?,?) AND status = 1,结果不足时补一次
示例片段:
[$min, $max] = DB::table('users')->where('status', 1)->selectRaw('MIN(id), MAX(id)')->first()->toArray();
$ids = array_unique(array_map(fn() => rand($min, $max), range(1, 15)));
$users = User::whereIn('id', $ids)->where('status', 1)->get();
注意:如果表中存在大量删除导致 ID 空洞,成功率会下降,建议配合 count() 校验返回数量,不足时重试或 fallback 到 inRandomOrder()(仅限极低频)。
更稳但需额外字段:添加 random_hash 并建索引
给表加一个 CHAR(16) 字段(如用 Str::random(16) 填充),每次插入时生成唯一随机值,并为该字段建普通索引。查询时用 WHERE random_hash > ? ORDER BY random_hash LIMIT N —— 利用 B+ 树索引的有序性实现伪随机流式读取。
优势和代价:
- 查询稳定在
O(log n),不受数据量影响;EXPLAIN显示Using index - 写入稍慢(多一次随机字符串生成+索引更新),但远好于读时全表排序
- 需要迁移脚本批量补全历史数据的
random_hash,且要避免重复(可加UNIQUE约束) - 不适合频繁更新
random_hash的场景(比如用户每天重置一次“今日推荐”)
复杂点在于首次上线要权衡迁移成本和查询性能拐点——如果日均随机查询超 100 次、表行数超 10 万,这个字段就值得加。











