先说句大实话:ORDER BY RAND() 这玩意儿,要是你的表里只有几百行数据,那用用也无妨。可一旦数据量突破万级,它的性能问题就暴露无遗——不是“有点慢”,而是直接拖垮整个查询线程,百万级数据量下基本不可用。

为什么 ORDER BY RAND() 在大表上必须避开
当你执行 SELECT * FROM t ORDER BY RAND() LIMIT 5 时,MySQL 会为每一行调用一次 RAND(),再对全表进行排序。100 万行意味着 100 万个随机数生成加上一次百万级排序操作。磁盘 I/O 和 CPU 资源都会被占满,响应时间从毫秒级飙升到秒级甚至直接超时。通过 EXPLAIN 可以看到 Using filesort,并且无法利用任何索引进行优化。
一个常见的性能陷阱:SELECT * FROM orders WHERE status = 'paid' ORDER BY RAND() LIMIT 10 在订单表记录超过 50 万后,频繁触发慢查询告警或连接超时。
这种写法的适用场景极其有限:仅限配置表、字典表、测试用的小表(COUNT(*) );生产环境中的 user、log、order 等核心业务表应一律禁用。
用主键范围采样:快但得防 ID 空洞
该方法要求表拥有自增 id,且删除操作不频繁(ID 分布相对均匀)。核心思想是绕过排序,直接利用主键索引进行定位。
- 先获取边界值:
SELECT MIN(id), MAX(id) FROM your_table WHERE condition - 在应用层生成 N 个
[min_id, max_id]范围内的随机整数(注意去重) - 构建查询:
SELECT * FROM your_table WHERE id IN (123, 456, 789)
需要注意的细节:
- 如果 ID 空洞较多(例如删除了 30% 的数据),
IN子句可能无法返回足够数量的记录,建议多生成 20%~30% 的候选 ID,或补充一次查询 - 不要写成
WHERE id >= FLOOR(RAND() * (SELECT MAX(id) FROM t)) LIMIT 5—— 这会返回连续的 ID 段,而非均匀随机 - 此方法依赖主键索引,查询耗时基本恒定,属于
O(1)级别
用 COUNT(*) + LIMIT ... OFFSET:均匀但偏移越大越慢
适用于对随机性要求严格(如抽奖、A/B 测试),且能承受一定性能下降的场景。
- 先获取总行数:
SELECT COUNT(*) FROM your_table WHERE condition - 在应用层生成随机偏移量:
offset = random_int(0, total_count - N) - 执行查询:
SELECT * FROM your_table WHERE condition LIMIT N OFFSET offset
风险提示:
COUNT(*)在 InnoDB 大表上可能较慢(尤其当没有 WHERE 条件或条件无法走索引时),可考虑使用information_schema.TABLES.TABLE_ROWS近似值(如果允许一定误差)- 当
OFFSET超过 10 万后,MySQL 仍需扫描前面所有行,耗时明显增加 - 该方式天然均匀,不依赖主键连续性,但两次查询加上应用层计算,逻辑比单条 SQL 稍重
真正难的不是选择哪种方法,而是判断你的表是否真的“连续”、空洞率有多高、业务能否容忍少量重复采样或偏移偏差——这些无法靠 SQL 自动感知,需要你通过 SELECT COUNT(*), COUNT(id) FROM t 和 SELECT MAX(id)-MIN(id)+1 - COUNT(*) 来明确空洞比例。
