目录
[1. 随机化阶段](#1. 随机化阶段)
[2. 分配阶段](#2. 分配阶段)
[3. 均匀性验证](#3. 均匀性验证)
[1. 分区随机化](#1. 分区随机化)
[2. 智能轮转算法](#2. 智能轮转算法)
[1. 分批处理](#1. 分批处理)
[2. 内存优化](#2. 内存优化)
[3. 监控与验证](#3. 监控与验证)
[Q1: t2用户数变化怎么办?](#Q1: t2用户数变化怎么办?)
[Q2: 如何处理语言不匹配的记录?](#Q2: 如何处理语言不匹配的记录?)
[Q3: 如何避免重复分配?](#Q3: 如何避免重复分配?)
引言
在数据处理中,我们常遇到这样的需求:将一张大表的记录随机且均匀地分配给另一张表的用户。最近我遇到了两个具体场景:
-
简单随机分配:将385万行数据随机分配给30万用户
-
条件随机分配:按语言条件进行分组随机分配
本文将分享这两种需求的优化实现方案,展示如何利用MySQL 8的窗口函数高效解决实际问题。
数据准备
sql
-- 表1:385万行的歌曲数据
create table t1 (
songid bigint not null,
language varchar(100) default null,
userid bigint default null,
primary key (songid)
) engine=innodb default charset=utf8mb4;
-- 数据量:3,850,170 行
-- 表2:30万行的用户数据
create table t2 (
songuser bigint not null,
songnick varchar(50) not null,
songlang varchar(50) default null,
primary key (songuser),
key songlang (songlang)
) engine=innodb default charset=utf8mb4;
-- 数据量:300,000 行
需求一:简单随机均匀分配
业务需求
-
为t1表的每一行随机分配一个t2.songuser
-
每个t2.songuser会被分配到多个t1行
-
分配数量要尽可能平均
-
t1.userid初始为NULL,需要批量更新
解决方案
sql
select t1.songid, t1.language, t2.songuser
from (
select songid, language, row_number() over (order by rand()) rn
from t1
) t1
join (
select songuser, row_number() over (order by rand()) rn
from t2
) t2 on (t1.rn - 1) % 300000 + 1 = t2.rn;
算法解析
1. 随机化阶段
sql
-- t1随机排序编号
select songid, language, row_number() over (order by rand()) rn
from t1
-- t2随机排序编号
select songuser, row_number() over (order by rand()) rn
from t2
-
使用
ROW_NUMBER() OVER (ORDER BY RAND())为两张表生成随机序列 -
t1的385万行被打乱为1-3850170的随机编号
-
t2的30万行被打乱为1-300000的随机编号
2. 分配阶段
sql
(t1.rn - 1) % 300000 + 1 = t2.rn
这是一个巧妙的模运算轮询分配:
-
第1行t1 → 第1个t2用户
-
第2行t1 → 第2个t2用户
-
...
-
第300001行t1 → 第1个t2用户(重新开始循环)
3. 均匀性验证
sql
-- 统计每个用户分配到的行数
with distribution as (
select t2.songuser, count(*) as assigned_count
from (
select songid, row_number() over (order by rand()) rn
from t1
) t1
join (
select songuser, row_number() over (order by rand()) rn
from t2
) t2 on (t1.rn - 1) % 300000 + 1 = t2.rn
group by t2.songuser
)
select
min(assigned_count) as 最少分配,
max(assigned_count) as 最多分配,
avg(assigned_count) as 平均分配,
max(assigned_count) - min(assigned_count) as 最大差异
from distribution;
数学结果:
-
总行数:3,850,170
-
用户数:300,000
-
每个用户最少:floor(3,850,170 / 300,000) = 12行
-
每个用户最多:ceil(3,850,170 / 300,000) = 13行
-
差异:仅1行!
实际分布:
-
49,830个用户分配到12行
-
250,170个用户分配到13行
性能特点
-
时间复杂度:O(n log n),主要开销在随机排序
-
内存使用:窗口函数需要内存或临时表存储排序结果
-
优点:分配均匀性数学上得到保证
需求二:按语言条件分组分配
业务需求
在需求一的基础上增加条件:
-
要求
t1.language = t2.songlang -
只在同语言范围内进行随机分配
-
每个语言的分配要独立且均匀
解决方案
sql
-- 使用CTE和窗口函数的优化方案
with
-- 为每个语言内的t1记录生成随机序列
tt1_final as (
select
songid,
language,
row_number() over (partition by language order by rand()) as rn_in_lang
from t1
),
-- 为每个语言内的t2用户生成随机序列
tt2_final as (
select
songuser,
songlang,
row_number() over (partition by songlang order by rand()) as seq_num
from t2
),
-- 统计每个语言的用户数量
lang_counts as (
select songlang, count(*) as user_count
from t2
group by songlang
),
-- 改进的轮转分配逻辑
final_assignment as (
select t1.songid, t1.language, t2.songuser
from tt1_final t1
left join lang_counts lc on t1.language = lc.songlang
left join tt2_final t2 on t1.language = t2.songlang
and t2.seq_num = (
case
when lc.user_count is not null
then (((t1.rn_in_lang - 1) + floor((t1.rn_in_lang - 1) / lc.user_count)) % lc.user_count) + 1
else null
end
)
)
-- 导出结果
select songid, language, songuser
into outfile '/home/mysql/a.txt'
from final_assignment;
关键技术解析
1. 分区随机化
sql
row_number() over (partition by language order by rand())
-
只在每个语言分组内部进行随机排序
-
避免了全表3,850,170行的大排序
-
当语言种类较多时,性能优势明显
2. 智能轮转算法
核心分配公式:
sql
(((t1.rn_in_lang - 1) + floor((t1.rn_in_lang - 1) / lc.user_count)) % lc.user_count) + 1
与传统模运算的对比:
假设某语言有10行t1数据,3个t2用户:
| 方法 | 分配序列 | 特点 |
|---|---|---|
| 简单模运算 | 1,2,3,1,2,3,1,2,3,1 | 严格的循环分配 |
| 智能轮转 | 1,2,3,2,3,1,3,1,2,1 | 更分散的分配模式 |
智能轮转算法通过添加偏移量,实现了更均匀的分布。
分步执行流程
-
数据准备阶段:
-
按语言分区,组内随机编号
-
统计每个语言的用户数量
-
-
匹配阶段:
-
对每个语言的t1记录
-
使用轮转算法匹配同语言的t2用户
-
处理语言不匹配的情况(返回NULL)
-
性能优化
为什么这个方案更快?
-
分区排序:只在语言分组内排序,数据量大幅减少
-
索引利用:t2表的songlang索引加速语言匹配
-
流式处理:窗口函数支持流式处理,内存占用可控
实际性能对比
-
方案一(全表排序):约25秒
-
方案二(分区排序):约18秒
-
性能提升:约28%
两种方案的对比分析
| 特性 | 方案一:简单分配 | 方案二:条件分配 |
|---|---|---|
| 适用场景 | 无条件随机分配 | 按条件分组分配 |
| 分配算法 | 简单模运算轮询 | 智能轮转分配 |
| 排序方式 | 全表随机排序 | 分区内随机排序 |
| 性能表现 | 相对较慢 | 更快(分区优化) |
| 内存使用 | 较高 | 较低 |
| 代码复杂度 | 简单 | 中等 |
| 分配均匀性 | 数学上完美均匀 | 组内尽可能均匀 |
生产环境优化建议
1. 分批处理
对于超大数据集,考虑分批处理:
sql
-- 分批处理示例
declare batch_size int default 100000;
declare start_id bigint default 0;
declare max_id bigint;
select max(songid) into max_id from t1;
while start_id <= max_id do
insert into temp_assignment
select ... -- 分配逻辑
from t1
where songid between start_id and start_id + batch_size - 1;
set start_id = start_id + batch_size;
end while;
2. 内存优化
调整MySQL配置:
ini
# my.cnf配置
tmp_table_size=256M
max_heap_table_size=256M
sort_buffer_size=8M
3. 监控与验证
分配后验证均匀性:
sql
-- 检查分配结果
select songuser, count(*) as cnt
from assignment_results
group by songuser
order by cnt desc
limit 10;
-- 检查语言匹配情况
select language, count(*) as total,
sum(case when songuser is null then 1 else 0 end) as unmatched
from assignment_results
group by language;
常见问题与解决方案
Q1: t2用户数变化怎么办?
方案:使用动态计数
sql
-- 替换硬编码的300000
on (t1.rn - 1) % (select count(*) from t2) + 1 = t2.rn
Q2: 如何处理语言不匹配的记录?
方案:添加默认分配
sql
-- 在final_assignment后补充
union all
select t1.songid, t1.language,
(select songuser from t2 order by rand() limit 1)
from t1
where language not in (select distinct songlang from t2);
Q3: 如何避免重复分配?
方案:确保t1.songid是唯一键,分配结果不会重复
总结
通过这两种方案,我们展示了如何优雅地解决大规模数据随机分配问题:
-
方案一:简单直接的模运算分配,适合无条件随机分配场景
-
方案二:复杂但高效的分区轮转分配,适合有条件分组分配
核心思想:
-
利用窗口函数实现高效的随机排序
-
通过数学算法保证分配的均匀性
-
分区处理优化性能
这两种方案不仅解决了具体的技术问题,更提供了一种思路:将复杂业务需求分解为可计算的数学模型,再用现代SQL特性高效实现。
在实际工作中,理解业务需求、选择合适的算法、充分利用数据库特性,是解决复杂数据处理问题的关键。希望这两种方案能为类似场景提供有价值的参考。