
postgresql_cursor vs find_in_batches深扒批量读取的4大致命缺陷find_each为何不够用【免费下载链接】postgresql_cursorActiveRecord PostgreSQL Adapter extension for using a cursor to return a large result set项目地址: https://gitcode.com/gh_mirrors/po/postgresql_cursor处理百万行级数据时Rails 开发者常陷入内存暴涨的困境。postgresql_cursor是一款扩展 ActiveRecord PostgreSQL 适配器的 Ruby 开源库它借助 PostgreSQL 游标Cursor机制将超大结果集按块默认 1000 行分批取回让应用内存占用始终保持在可控范围。本文对比find_in_batches/find_each的 4 大致命缺陷讲清为什么批量读取场景下它才是更优解。为什么 find_each / find_in_batches 不够用先说结论find_each和find_in_batches是 Rails 提供的分批读方案它们按batch_size默认 1000 行分块遍历避免了把整张表一次性装进内存。看起来很美好但在真实业务里它们有 4 个硬伤——缺陷 1无法指定排序只能按主键顺序返回find_each/find_in_batches强制按主键通常是id顺序返回不支持自定义order。如果你需要按创建时间、价格或任意业务字段排序遍历它们直接出局。而 postgresql_cursor 基于真实游标order(name)等任意排序都能正常生效。缺陷 2主键必须是数字类型分页机制依赖上一批最大 id 1这种数字区间推进因此主键必须是数值型。字符串主键、UUID 主键抱歉用不了。游标方案完全没有这个限制因为它由数据库侧维护结果集位置。缺陷 3每个批次都要重新执行查询每取一批1000 行Rails 都要重新跑一次带id last_seen_id LIMIT 1000的查询。100 万行就意味着 1000 次完整的查询规划与执行数据库压力随数据量线性放大。游标则不同只声明一次查询DECLARE后续反复 FETCH 取块数据库侧只执行一遍。缺陷 4复杂查询扛不住性能开销翻倍由于查询会重放任何复杂的 JOIN、子查询都会在每个批次重复付出编译与执行代价数据量越大浪费越明显。README 在 README.md 中也明确指出复杂查询配合重放机制会带来额外开销这正是游标要解决的痛点。 小结4 个缺陷的共同根源——它们不是真正的流式读取而是伪流式的重放分页。postgresql_cursor 如何做到真·流式读取它直接调用 PostgreSQL 的原生游标操作伪代码如下摘自 READMESET cursor_tuple_fraction TO 1.0; DECLARE cursor_1 CURSOR WITH HOLD FOR select * from widgets; loop rows FETCH 100 FROM cursor_1; -- 每次只取一块 rows.each {|row| yield row} until rows.size 100; CLOSE cursor_1;关键机制查询只执行一次结果集位置由数据库游标维护每次 FETCH 只拉取 block_size 行默认 1000内存恒定支持任意 order、任意复杂 SQL、任意类型主键with_hold: true时游标甚至能在事务提交后保持打开。核心迭代器实现在lib/postgresql_cursor/active_record/relation/cursor_iterators.rb底层游标封装在lib/postgresql_cursor/cursor.rb你可以直接阅读源码理解细节。安装与 3 分钟上手一键安装步骤在 Gemfile 中添加依赖即可要求 ActiveRecord 6.0gem postgresql_cursor本地开发也可以从源码安装git clone https://gitcode.com/gh_mirrors/po/postgresql_cursor gem build postgresql_cursor.gemspec gem install postgresql_cursor.gem最快配置方法3 行代码开始流式读取# 逐行返回 Hash最快适合批量处理 Product.where(id0).order(name).each_row { |row| Product.process(row) } # 逐行返回模型实例需要调用模型方法时用 Product.where(id0).each_instance { |product| product.process! }不想写块直接拿游标对象它是 Enumerable可以自由链式操作Product.each_row.map { |r| r[id].to_i } # [1, 2, 3, ...] Product.each_instance.lazy.inject(0) { |sum, r| sum r.quantity }常用配置项options 速查表选项说明block_size: n每次从数据库取回的行数默认 1000while: value块返回该值时继续循环until: value块返回该值时停止循环connection: conn指定使用的数据库连接with_hold: true提交后保持游标打开cursor_name: str给游标命名fraction: 1.0设置 cursor_tuple_fraction不建议改动性能优化Hash vs 实例差出 4 倍速度README 中的非正式基准测试显示返回 Hash 比实例化模型快约 4 倍。选型建议只做数据加工、写库、导出用each_roweach_hash拿到的是字符串值 Hash注意自行做类型转换需要调用模型方法、依赖类型自动转换用each_instanceActiveRecord 只在你读取属性时才惰性转换效率已经不错只需要几列配合select(:id, :name)收窄返回列只要值不要行pluck_rows(:id)/pluck_instances(:id, :quantity)可代替传统pluck且仍是分批惰性加载。进阶边遍历边加锁更新FOR UPDATEProduct.lock.each_instance(block_size: 100) do |p| p.update(price: p.price * 1.05) endlock会为每个 FETCH 块加FOR UPDATE行锁块处理完即释放——大表逐行更新时既安全又不阻塞并发。注意繁忙表或单行处理耗时较长时block_size建议 ≤ 10避免死锁。选型对比一张表看懂差异维度find_in_batches / find_eachpostgresql_cursor排序支持❌ 仅主键序✅ 任意 order主键类型❌ 必须数字✅ 无限制查询执行次数每批重跑一次仅执行一次复杂查询开销随批次数放大无额外重放开销内存占用恒定单批恒定单块行锁更新需手动实现原生lock支持额外依赖无仅限 PostgreSQL 数据库⚠️ 注意游标有数据库侧开销只用于大数据量场景小结果集直接用常规查询即可。另外它依赖to_sql无法像 ActiveRecord 那样做关联预加载的表 JOIN 拆分设计查询时请把所需字段 join 好。常见问题FAQQ1我用的不是 PostgreSQL 能用吗不能。该库扩展的是 PostgreSQL 适配器见lib/postgresql_cursor/active_record/connection_adapters/postgresql_type_map.rb的类型映射MySQL 等数据库没有对应游标语义支持。Q2需要包在事务里吗只有当你手动cursor.fetch逐行取数、自己控制节奏时才必须放在事务中直接使用each_row/each_instance遍历则无需。Q3如何本地跑测试项目自带测试应用运行test-app/run.sh setup创建测试库再用test-app/run.sh irb进入交互式控制台体验示例代码见test-app/app.rb。总结如果只需按 id 顺序简单翻页find_each已经够用但只要你遇到要自定义排序、主键不是数字、查询很复杂、数据量上百万中任意一条find_in_batches的 4 大致命缺陷就会逐个爆出来。postgresql_cursor 用查询执行一次 游标分块 FETCH的数据库级方案一次性解决了全部问题还能顺手获得 4 倍速的 Hash 遍历和行锁更新能力。批量读取场景值得把游标纳入你的技术清单。【免费下载链接】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),仅供参考