当前位置: 首页 > news >正文

postgresql_cursor vs find_in_batches:深扒批量读取的4大致命缺陷,find_each为何不够用

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_eachfind_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 的原生游标操作,伪代码如下(摘自 README):

SET 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.0):

gem '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("id>0").order("name").each_row { |row| Product.process(row) } # 逐行返回模型实例(需要调用模型方法时用) Product.where("id>0").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每次从数据库取回的行数(默认 1000)
while: 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_instance,ActiveRecord 只在你读取属性时才惰性转换,效率已经不错;
  • 只需要几列?配合select(:id, :name)收窄返回列;
  • 只要值不要行?pluck_rows(:id)/pluck_instances(:id, :quantity)可代替传统pluck,且仍是分批惰性加载。

进阶:边遍历边加锁更新(FOR UPDATE)

Product.lock.each_instance(block_size: 100) do |p| p.update(price: p.price * 1.05) end

lock会为每个 FETCH 块加FOR UPDATE行锁,块处理完即释放——大表逐行更新时既安全又不阻塞并发。注意:繁忙表或单行处理耗时较长时,block_size建议 ≤ 10,避免死锁。

选型对比:一张表看懂差异

维度find_in_batches / find_eachpostgresql_cursor
排序支持❌ 仅主键序✅ 任意 order
主键类型❌ 必须数字✅ 无限制
查询执行次数每批重跑一次仅执行一次
复杂查询开销随批次数放大无额外重放开销
内存占用恒定(单批)恒定(单块)
行锁更新需手动实现原生lock支持
额外依赖仅限 PostgreSQL 数据库

⚠️ 注意:游标有数据库侧开销,只用于大数据量场景;小结果集直接用常规查询即可。另外它依赖to_sql,无法像 ActiveRecord 那样做关联预加载的表 JOIN 拆分,设计查询时请把所需字段 join 好。

常见问题(FAQ)

Q1:我用的不是 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),仅供参考

http://www.cnnetsun.cn/news/4173608.html

相关文章:

  • 远程桌面与AI Agent开发实战:将高性能台式机变为便携云电脑
  • 编程思维四大核心与八种实战方法:从代码搬运工到系统设计者
  • Windows平台AI大模型本地部署:轻量化桌面应用开发实战
  • 协方差与相关矩阵:从概念到PCA与投资组合的实战应用
  • 多智能体系统中时序与结构信用分配的统一优化框架解析
  • 数学建模论文写作指南:从模型构建到高效表达的实战技巧
  • fastapi-permissions 进阶技巧:自定义403异常、All 通配权限与 ACL 归一化的6个关键点
  • 认识Pink:面向关节机器人的Python逆运动学库完全入门指南
  • 确定性AI:实现可复现输出的工程实践与CIYA项目解析
  • FlexLabs.Upsert 排错清单:InvalidMatchColumnsException 与 UnsupportedExpressionException 全解
  • Core Data与CollectionView UI实时同步:CompositionalDiffablePlayground Jokes示例收藏、上下文菜单与骨架屏动画完整实现
  • BreezeJS快速上手指南:在CustomerManagerStandard中掌握EntityManager、元数据获取与saveChanges完整工作流
  • 嵌入式学习路线全解析:从51单片机到STM32,新手避坑指南与核心技能构建
  • 数学建模实战:线性回归的核心假设、特征工程与模型诊断全解析
  • Vortigern 样式方案拆解:CSS Modules + PostCSS-Assets 完整配置指南
  • 深入react-native-app-tour源码:findNodeHandle与NativeModules如何打通JS与原生App Tour视图
  • 为什么DebugKit是Android开发者必备的悬浮调试神器?完整概览与功能解析
  • noteForOpenGL PBO像素缓冲对象:Pack/Unpack机制与CPU-GPU数据通道完整指南
  • 函数设计四大核心特性:从内置函数到模板重载的工程实践
  • OpCore-Simplify 快速上手指南:从硬件报告到 OpenCore EFI
  • Android开发者必学:从file_operations入门Linux驱动开发
  • 如何测试行级权限控制?用 pytest 与 pytest-mock 构建 fastapi-permissions 单元测试完全指南
  • 数学建模实战指南:从思维转变到模型落地的全流程解析
  • 开发者知识体系重构:从碎片化学习到系统化升级的工程实践
  • 5分钟跑通pymavlink:mavlink_connection连接Pixhawk并接收心跳的保姆级实战
  • 多智能体集群架构:构建公平、自适应的心理健康支持系统
  • 彻底解决链接器报错:从原理到实战的完整指南
  • RogueViz引擎深度剖析:HyperRogue背后的非欧几何游戏引擎
  • 30 分钟跑通 openAUTOSAR 经典平台:3 个核心模块与 1 个必踩的坑
  • 人形机器人落地实战:工业、商用、家庭三大场景技术评估与集成指南