Rails读1000万行数据直接OOMpostgresql_cursor大结果集处理的终极方案【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursorpostgresql_cursor是一款免费的 Rails/ActiveRecord 扩展 gem专为 PostgreSQL 数据库打造。它利用 PostgreSQL 原生游标Cursor将大结果集按“块”返回默认每次 1000 行让你在 Ruby 进程中完整处理上千万行数据也不 OOM。如果你的后台任务曾因为一句Model.where(...).to_a而内存爆炸这个 gem 就是答案。为什么 Rails 读大结果集会 OOMActiveRecord 天生为 Web 场景优化——一个页面通常只返回 20 行左右的数据。但当你在后台任务里这样写Product.where(id 0).each { |product| product.process }数据库会把所有匹配行一次性返回给 ActiveRecord 并实例化成模型对象。处理千万级行时内存瞬间被打满更糟的是 Ruby 在数组处理完后并不归还内存进程不断“膨胀”最终抛出 OOM 异常 Rails 官方的find_each/find_in_batches虽能分块处理但有明显限制限制点说明排序不可控只能按主键顺序读取无法order(name)主键要求主键必须是数字类型重复查询每块1000 行都要重新执行一次 SQL复杂查询开销大带 join、子查询的 SQL 会被反复重跑postgresql_cursor 游标原理一次查询、分批取数游标的思路完全不同把 SQL 在数据库侧“挂起”只声明一次然后循环分批取数。其核心逻辑等价于见 cursor 核心实现 中的open、fetch_block、close方法SET cursor_tuple_fraction TO 1.0; DECLARE cursor_xxx CURSOR FOR SELECT * FROM products; LOOP rows FETCH 1000 FROM cursor_xxx; -- 每次只取 1000 行 rows.each { |row| 处理 } END LOOP; CLOSE cursor_xxx;两个关键细节cursor_tuple_fraction设为 1.0PostgreSQL 默认 0.1按只取 10% 结果集优化执行计划而游标要读完全部数据该库自动改为 1.0让查询规划器为“全量读取”选择最优路径。事务包裹游标必须在事务中运行该库会自动帮你包一层transaction除非指定with_hold: true。对应用户而言这一切被封装成了 4 个 ActiveRecord 方法扩展定义在 cursor_iterators.rb 中几乎零学习成本。三步上手安装 postgresql_cursor在Gemfile中加入gem postgresql_cursor执行bundle install完成 ✅ 无需任何额外配置——它在 ActiveRecord 加载时自动挂载见 postgresql_cursor.rb 的on_load钩子。环境要求PostgreSQL ActiveRecordRails 3.2~5.x 均支持含 5.0Ruby 1.9。核心用法三种方式遍历百万行数据① each_row返回 Hash速度最快Product.where(id 0).order(name).each_row { |hash| Product.process(hash) }每行返回字符串键的 Hash不实例化模型。官方非正式基准测试显示比实例化模型快约 4 倍。代价是值均为字符串需自行做类型转换。② each_instance返回模型实例Product.where(id 0).each_instance { |product| product.process! } Product.where(id 0).each_instance(block_size: 100_000) { |p| p.process }需要调用模型回调、关联或自定义方法时选它。ActiveRecord 只在你真正读取属性时才做类型转换效率也不错。③ 原生 SQL 游标复杂报表、跨库查询直接传 SQLProduct.each_row_by_sql(select * from products) { |hash| Product.process(hash) } Product.each_instance_by_sql(select * from products) { |p| p.process } 所有方法都支持选项参数block_size每批行数默认 1000、cursor_name自定义游标名、while/until按块返回值提前终止循环、connection指定连接等。想要完全手动控制 fetch 节奏把游标包在事务里即可Product.transaction do cursor Product.all.each_row row cursor.fetch # {id1} row cursor.fetch(symbolize_keys: true) # {:id2} cursor.close end进阶技巧Enumerable 链式与 FOR UPDATE 锁天然支持 Enumerable不传 block 时返回的游标对象已混入Enumerable且本身就是“懒”的可继续链式调用Product.each_row.map { |r| r[id].to_i } Product.each_instance.lazy.inject(0) { |sum, r| sum r.quantity }逐行更新 行级锁配合lockFOR UPDATE每批行在处理期间被锁定、处理完自动释放实现并发安全的大表更新Product.lock.each_instance(block_size: 100) do |p| p.update(price: p.price * 1.05) end⚠️ 繁忙表或单行处理耗时长时建议block_size 10避免长时间持锁引发死锁。实用建议与避坑清单只在大结果集场景用游标小查询直接select更轻游标有额外数据库开销✂️少取列用.select(:id, :name)限定字段只要单列值时pluck往往更合适批量场景选 batch 版本each_row_batch/each_instance_batch按批 yield方便攒批写库实验性功能pluck_rows/pluck_instances仍会构建数组超大结果集慎用 想读源码核心游标逻辑在 cursor.rbActiveRecord 集成在 cursor_iterators.rb可运行示例见 app.rb总结方案内存排序自由复杂查询直接each❌ 全量加载易 OOM✅✅find_each✅ 分批❌ 仅主键序❌ 每批重跑postgresql_cursor✅ 分批✅ 任意 order✅ 只声明一次一句话Web 请求继续用 ActiveRecord百万行以上的后台处理交给 postgresql_cursor——一次声明、分批 FETCH、内存恒定你的 Rails 应用从此告别 OOM。【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考