Ruby on Rails百万级数据批量增改如何降低数据库与服务器负载?
1000万条数据批量Upsert的最优方案分析
先给结论:别用逐条处理,直接上数据库原生批量Upsert
不管你之前给出的两段代码是啥,先排除掉循环每条数据调用find_or_create_by或单独更新的方案——1000万条数据这么玩,数据库和服务器直接扛不住,光是千万级的SQL请求就能把数据库打崩。
常见方案的优劣对比
方案1:逐条查改(绝对别用)
循环每条数据,先查有没有对应fk的记录,没有就创建,有就更新。核心问题:
- 每条数据至少发1次查询,更新还要再加1次,1000万条就是2000万次数据库请求,负载直接拉满
- 服务器要处理大量循环和网络请求,内存、CPU直接飙到顶,大概率会出现内存溢出
方案2:先批量查现有数据再分组处理
先一次性(或分批)查出数据库里已有的fk列表,然后把待处理数据分成“要创建”和“要更新”两组,再分别批量创建和更新。这种比逐条强,但仍有缺点:
- 要额外占用内存存储分组数据,1000万条数据的分组逻辑对服务器内存压力不小
- 还是要发至少两次批量请求(查+创建+更新),效率不如原生Upsert
最优实现:用Rails的upsert_all分批次处理
Rails 6.0之后支持的upsert_all直接调用数据库的原生Upsert能力(PostgreSQL的ON CONFLICT,MySQL的ON DUPLICATE KEY UPDATE),所有操作在数据库端批量完成,是最低负载的方案。
代码示例
# 先把get_data返回的数组转成符合数据库字段的哈希结构 processed_data = get_data.map do |item| { fk: item[:fk], name: item[:name], detail: item[:detail] } end # 分批次处理,避免一次性加载1000万条数据到内存 batch_size = 1000 # 可根据数据库性能调整,比如PostgreSQL可以调到5000-10000 processed_data.each_slice(batch_size) do |batch| YourModel.upsert_all( batch, unique_by: :fk, # 指定唯一索引字段,用来判断冲突 update_only: [:name, :detail] # 冲突时只更新这两个字段,避免覆盖其他字段 ) end
为啥这方案最优?
- 数据库请求极少:1000万条按1000条一批,只需要1万次请求,和千万级请求比差了两个数量级
- 数据库原生高效处理:Upsert是数据库层面的原子操作,比应用层循环快N倍
- 内存可控:分批次加载数据,不会一次性把1000万条数据塞进内存
- 原子性保障:每一批的操作都是原子的,不会出现部分成功部分失败的情况(除非主动关掉事务)
额外优化小技巧
- 确认
fk字段是唯一索引——这是原生Upsert能工作的前提,你已经提到了,这点很重要 - 如果不需要跨批次的事务,可以加
transaction: false参数,减少事务开销 - 临时调大数据库的内存配置,比如PostgreSQL的
work_mem,MySQL的innodb_buffer_pool_size,提升批量操作速度 - 把这个操作放到Sidekiq之类的异步任务里跑,别阻塞Web进程
- 运行时盯着数据库的CPU、IO和连接数,根据实际情况调整批次大小
内容的提问来源于stack exchange,提问作者Test Title
相关产品推荐
相关产品推荐

