SQL分组随机抽取一条记录

作者:技术拾光

时间:2026-07-15

浏览:94

MySQL8.0+用ROW_NUMBER()OVER(PARTITIONBYcategoryORDERBYRAND())实现分组随机抽样;PostgreSQL用DISTINCTON+ORDERBYRANDOM();MySQL5.7需关联子查询加LIMIT1但性能差。应避免外层ORDERBYRAND()或GROUPBY聚合函数,无法得到真随机。

文章目录

先抽个结论:在 MySQL 8.0 及以上版本里,用 ROW_NUMBER() OVER (PARTITION BY category ORDER BY RAND()) 做分组随机抽样,是最靠谱、语义最清晰的写法。它会先在每组内把记录顺序打乱,再编号,最后取每组 rn = 1 的那一行——干净利落,逻辑透明。

很多人容易踩的坑是:在最外层加个 ORDER BY RAND()。这其实只是把全表随机排序了一遍,然后分组——结果根本就不是“每组一条”,而是不可控的随机行数。还有人误用 GROUP BY 配合 MIN(id),本质是取最小 ID,跟随机八竿子打不着。

SQL分组随机抽取一条记录

MySQL 8.0+ 用 ROW_NUMBER() + RAND() 实现分组随机抽样

直接上正确写法,然后说几个关键点:

SELECT * FROM (  SELECT *,         ROW_NUMBER() OVER (PARTITION BY category ORDER BY RAND()) AS rn  FROM products) t WHERE rn = 1;
  • RAND() 必须放在窗口函数的 ORDER BY 里,而且不能加其他确定性排序字段。比如写成 ORDER BY category, RAND(),因为 category 是稳定的,同组内多次执行结果会一模一样,等于没随机。
  • 窗口函数只支持 MySQL 8.0+ 和 PostgreSQL;MariaDB 10.2+ 也支持,但旧版 MySQL(5.7 及之前)不行,得换方案。
  • 性能方面:全表扫描 + 每组内部排序,数据量大时会慢。如果只是偶尔跑一跑,或者数据在百万级以下,基本可以接受。

PostgreSQL 用 DISTINCT ON + ORDER BY RANDOM()

PostgreSQL 提供了一个更简洁的语法:DISTINCT ON 按指定列去重,配合 ORDER BY ... RANDOM() 控制保留哪一条。天生适合分组抽样,而且不需要子查询,清爽很多。

SELECT DISTINCT ON (category) *FROM productsORDER BY category, RANDOM();

这里有一个容易踩的坑:DISTINCT ON (category) 要求 ORDER BY 最左字段必须是 category,否则直接报错。另外 RANDOM() 必须出现在 ORDER BY 里,不能只靠 WHERE 过滤。

  • DISTINCT ON 返回的是“每组第一条”,所以 ORDER BY category, RANDOM() 才能保证每组内随机选一条。
  • 如果还想返回原表所有字段,category 必须放在 ORDER BY 开头,后面接 RANDOM(),其他字段顺序不影响结果。
  • 相比窗口函数,这个写法更轻量,执行计划通常更优,尤其在有 category 索引时。

MySQL 5.7 或 SQLite 等不支持窗口函数的场景

只能退回到“关联子查询 + LIMIT 1”模式。但注意:MySQL 5.7 不允许子查询里直接用 LIMIT 配合外部引用,必须包装成派生表;SQLite 则直接支持 LIMIT 写在子查询里。

性能差是最大问题——对主表每行都执行一次子查询,复杂度 O(n)。数据超 1 万行就会明显卡顿。线上环境慎用,仅限临时查或小表。

  • MySQL 5.7 正确写法:子查询要用双层括号,比如 (SELECT * FROM (SELECT ... ORDER BY RAND() LIMIT 1) AS tmp)。
  • SQLite 可以直接写 (SELECT * FROM products p2 WHERE p2.category = p1.category ORDER BY RANDOM() LIMIT 1)。
  • 所有方案中,这是唯一可能重复返回同一行(虽然概率低),因为 RAND() 在子查询里每次独立生成,无法保证组内真正“只选一次”。
-- MySQL 5.7 示例(注意双层括号)SELECT p1.* FROM products p1WHERE p1.id = (  SELECT id FROM (    SELECT id FROM products p2    WHERE p2.category = p1.category    ORDER BY RAND() LIMIT 1  ) AS tmp);

为什么不能用 GROUP BY + AGGREGATE(比如 MAX(id)) 模拟随机?

因为 MAX(id)、MIN(name) 这类聚合函数返回的是确定值,跟随机完全不沾边。有人试图用 GROUP_CONCAT(id ORDER BY RAND()) 再 SUBSTRING_INDEX 截取第一个,看似随机,实则隐患巨大:

  • GROUP_CONCAT 有长度限制(默认 1024 字符),超长组会截断,导致取不到真实 ID。
  • 字符串拼接后取子串,无法保证类型安全;如果 ID 是 UUID 或含逗号,解析直接失败。
  • MySQL 5.7 默认不开启 sql_mode=ONLY_FULL_GROUP_BY 时,SELECT * 配合 GROUP BY 会返回非确定性行,看起来像随机,其实是引擎随意选的,不可靠也不可移植。

真要兼容老版本,又不能接受性能损耗,优先考虑应用层分组后随机取——查出全部分组数据,用 Python/JS 在内存里按组 shuffle 再取首项,比写 SQL 黑魔法稳当得多。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章