大规模数据随机均匀分配的两种SQL实现方案

目录

引言

数据准备

需求一:简单随机均匀分配

业务需求

解决方案

算法解析

[1. 随机化阶段](#1. 随机化阶段)

[2. 分配阶段](#2. 分配阶段)

[3. 均匀性验证](#3. 均匀性验证)

性能特点

需求二:按语言条件分组分配

业务需求

解决方案

关键技术解析

[1. 分区随机化](#1. 分区随机化)

[2. 智能轮转算法](#2. 智能轮转算法)

分步执行流程

性能优化

为什么这个方案更快?

实际性能对比

两种方案的对比分析

生产环境优化建议

[1. 分批处理](#1. 分批处理)

[2. 内存优化](#2. 内存优化)

[3. 监控与验证](#3. 监控与验证)

常见问题与解决方案

[Q1: t2用户数变化怎么办?](#Q1: t2用户数变化怎么办?)

[Q2: 如何处理语言不匹配的记录?](#Q2: 如何处理语言不匹配的记录?)

[Q3: 如何避免重复分配?](#Q3: 如何避免重复分配?)

总结


引言

在数据处理中,我们常遇到这样的需求:将一张大表的记录随机且均匀地分配给另一张表的用户。最近我遇到了两个具体场景:

  1. 简单随机分配:将385万行数据随机分配给30万用户

  2. 条件随机分配:按语言条件进行分组随机分配

本文将分享这两种需求的优化实现方案,展示如何利用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 更分散的分配模式

智能轮转算法通过添加偏移量,实现了更均匀的分布。

分步执行流程

  1. 数据准备阶段

    • 按语言分区,组内随机编号

    • 统计每个语言的用户数量

  2. 匹配阶段

    • 对每个语言的t1记录

    • 使用轮转算法匹配同语言的t2用户

    • 处理语言不匹配的情况(返回NULL)

性能优化

为什么这个方案更快?
  1. 分区排序:只在语言分组内排序,数据量大幅减少

  2. 索引利用:t2表的songlang索引加速语言匹配

  3. 流式处理:窗口函数支持流式处理,内存占用可控

实际性能对比
  • 方案一(全表排序):约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是唯一键,分配结果不会重复

总结

通过这两种方案,我们展示了如何优雅地解决大规模数据随机分配问题:

  1. 方案一:简单直接的模运算分配,适合无条件随机分配场景

  2. 方案二:复杂但高效的分区轮转分配,适合有条件分组分配

核心思想

  • 利用窗口函数实现高效的随机排序

  • 通过数学算法保证分配的均匀性

  • 分区处理优化性能

这两种方案不仅解决了具体的技术问题,更提供了一种思路:将复杂业务需求分解为可计算的数学模型,再用现代SQL特性高效实现

在实际工作中,理解业务需求、选择合适的算法、充分利用数据库特性,是解决复杂数据处理问题的关键。希望这两种方案能为类似场景提供有价值的参考。

相关推荐
卿雪1 小时前
MySQL【数据库的三大范式】:1NF 原子、2NF 完全依赖、3NF 不可传递
数据库·mysql
不想画图2 小时前
数据库基础操作和权限管理
数据库·mysql
照物华2 小时前
MySQL 软删除 (Soft Delete) 与唯一索引 (Unique Constraint) 的冲突与解决
java·mysql
踢球的打工仔3 小时前
mysql数据备份
数据库·mysql
问道飞鱼3 小时前
【数据库知识】MySQL 数据类型详解:选型指南与实战最佳实践
数据库·mysql·数据类型
Xyz996_3 小时前
MySQL试验部署
数据库·mysql
小趴菜不能喝3 小时前
MySQL UTC时间
数据库·mysql
hanyi_qwe4 小时前
Mysql备份与还原
数据库·mysql
w***15314 小时前
【MySQL数据库】Ubuntu下的mysql
数据库·mysql·ubuntu