不能直接用 order by rand() 再 group by,因为 group by 要求 select 列必须是分组键或聚合结果,而 order by rand() 是非确定性操作,无法在分组后合法取每组一行;正确解法是用 row_number() over (partition by ... order by rand()) 为每组随机编号后取 rn=1。

为什么不能直接用 ORDER BY RAND() 再 GROUP BY
因为 SQL 标准里,GROUP BY 后的 SELECT 列必须是分组键或聚合函数结果;ORDER BY RAND() 是非确定性排序,无法在分组上下文中直接“取每组一条”。强行写会报错,比如 MySQL 8.0+ 提示 Expression #1 of ORDER BY contains aggregate function and applies to the result of a non-aggregated query,或者在 PostgreSQL 中直接拒绝语法。
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY RAND()) 是核心解法
窗口函数能绕过 GROUP BY 的限制,在分组内部独立编号,再筛选编号为 1 的行即可实现每组随机抽一条。关键点在于:ORDER BY RAND() 必须写在 OVER 子句里,且不能有其他非确定性操作干扰排序稳定性。
- MySQL 8.0+、PostgreSQL、SQL Server、BigQuery 均支持该写法
- SQLite 不支持窗口函数(除非 3.25+ 且编译时启用了
ENABLE_WINDOW_FUNCTIONS) - 旧版 MySQL(
- 示例:从用户订单表中,对每个
user_id随机抽一条订单记录
SELECT user_id, order_id, amount
FROM (
SELECT user_id, order_id, amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY RAND()) AS rn
FROM orders
) t
WHERE rn = 1;
用 RAND() 抽多条?小心重复和性能陷阱
如果要每组抽 N 条(N > 1),把 WHERE rn 即可,但要注意:
-
RAND()在同一查询中多次调用可能生成相同值(尤其在 WHERE/HAVING 中),导致抽样偏差;应只在OVER子句中调用一次 - 大数据量时,
ORDER BY RAND()会强制全表排序,性能极差;可先用WHERE id IN (SELECT id FROM ... ORDER BY RAND() LIMIT N)预过滤,再关联原表 - 若需可复现的随机结果,用
ORDER BY RAND(42)(MySQL)或ORDER BY RANDOM(42)(PostgreSQL)传入种子
替代方案:用 GROUP_CONCAT + SUBSTRING_INDEX(仅限 MySQL)
适用于小数据量、兼容老版本 MySQL 的场景,但有隐含限制:
-
GROUP_CONCAT默认长度上限为 1024 字符(由group_concat_max_len控制),超长会被截断 - 只能抽字符串字段,数值/时间等需显式
CAST,否则拼接后类型丢失 - 无法控制“随机性强度”,本质是按物理存储顺序拼接后取首段,不是真随机
SELECT user_id,
SUBSTRING_INDEX(GROUP_CONCAT(order_id ORDER BY RAND()), ',', 1) AS sample_order_id,
SUBSTRING_INDEX(GROUP_CONCAT(amount ORDER BY RAND()), ',', 1) AS sample_amount
FROM orders
GROUP BY user_id;
真正难的是在千万级订单表上稳定跑出每组抽样——RAND() 触发全排序,ROW_NUMBER() 需要足够内存,而很多人忽略的是:不同数据库对 RAND() 在窗口函数中的求值时机定义并不完全一致,测试时务必用真实数据集验证结果分布是否均匀。










