在数据处理工作中,经常需要针对大表按照某个维度分组,再从每个分组中随机抽取少量样本记录,用于后续分析或者测试。如果使用传统的ORDER BY RAND()方式实现,大表场景下全表排序的代价极高,查询很容易超时。而借助OFFSET和LIMIT的特性,可以高效完成这个需求。
核心实现思路
要实现分组后随机取样本,核心逻辑分为三步:首先统计每个分组的总记录数,其次为每个分组生成随机的偏移量,最后用OFFSET和LIMIT获取对应分组的随机记录。这样可以避免全表随机排序,只需要针对单个分组做少量偏移计算即可。
通用基础实现逻辑
以用户行为表user_action为例,假设要按照user_id分组,每个分组随机取1条记录,表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 用户ID,分组字段 |
| action_time | datetime | 行为时间 |
| action_type | varchar | 行为类型 |
首先统计每个分组的总记录数,同时为每个分组生成0到总记录数-1之间的随机偏移量:
-- 统计每个分组的记录数,生成随机偏移量
WITH group_count AS (
SELECT
user_id,
COUNT(*) AS total_count,
FLOOR(RAND() * COUNT(*)) AS random_offset
FROM user_action
GROUP BY user_id
)
SELECT
gc.user_id,
ua.id,
ua.action_time,
ua.action_type
FROM group_count gc
-- 关联原表,按照分组和偏移量取记录
INNER JOIN user_action ua
ON ua.user_id = gc.user_id
-- 利用窗口函数给每个分组的记录编号,从0开始
INNER JOIN (
SELECT
user_id,
id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) - 1 AS rn
FROM user_action
) ua_rn
ON ua_rn.user_id = gc.user_id
AND ua_rn.rn = gc.random_offset
AND ua_rn.id = ua.id;
MySQL场景下的简化实现
MySQL支持直接在查询中使用变量,可以简化分组编号的逻辑:
-- MySQL分组随机取1条记录
SET @prev_user_id = NULL;
SET @row_num = 0;
SELECT
user_id,
id,
action_time,
action_type
FROM (
SELECT
user_id,
id,
action_time,
action_type,
-- 按user_id分组,组内按id排序编号
IF(@prev_user_id = user_id, @row_num := @row_num + 1, @row_num := 0) AS rn,
@prev_user_id := user_id,
-- 每个分组的总记录数
COUNT(*) OVER (PARTITION BY user_id) AS total_count,
-- 随机偏移量
FLOOR(RAND() * COUNT(*) OVER (PARTITION BY user_id)) AS random_offset
FROM user_action
ORDER BY user_id, id
) t
WHERE rn = random_offset;
取多个随机样本的扩展实现
如果需要每个分组取N个随机样本,只需要调整逻辑,让偏移量可以覆盖多个随机位置即可。以每个分组取2个随机记录为例:
-- 每个分组取2个随机样本
WITH group_info AS (
SELECT
user_id,
COUNT(*) AS total_count,
-- 生成两个不同的随机偏移量,避免重复
FLOOR(RAND() * COUNT(*)) AS offset1,
FLOOR(RAND() * COUNT(*)) AS offset2
FROM user_action
GROUP BY user_id
),
ua_rn AS (
SELECT
user_id,
id,
action_time,
action_type,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id) - 1 AS rn
FROM user_action
)
SELECT
gi.user_id,
ua.id,
ua.action_time,
ua.action_type
FROM group_info gi
INNER JOIN ua_rn
ON ua_rn.user_id = gi.user_id
AND (ua_rn.rn = gi.offset1 OR ua_rn.rn = gi.offset2)
INNER JOIN user_action ua
ON ua.id = ua_rn.id;
性能优化建议
- 给分组字段
user_id建立索引,可以大幅提升分组统计和关联查询的速度 - 如果只需要取少量分组,可以在统计分组总数的时候先过滤目标分组,减少后续处理的数据量
- 随机偏移量生成时,如果分组总记录数很小,可以直接用ORDER BY RAND() LIMIT N,性能差异可以忽略
- 对于超大规模大表,可以结合分库分表策略,先定位到对应的分片再执行随机取样逻辑
注意事项
使用RAND()函数生成的随机数是伪随机,在单次查询中同一个分组内的RAND()调用如果多次执行可能返回相同值,建议先生成随机偏移量存到临时表或者CTE中,再关联查询,避免随机偏移量重复计算导致结果不符合预期。
如果使用的是PostgreSQL数据库,可以将RAND()替换为RANDOM(),窗口函数语法和MySQL基本一致,只需要调整随机函数的名称即可适配。