news 2026/8/23 12:12:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 12:12:29

远程桌面与AI Agent开发实战:将高性能台式机变为便携云电脑

1. 先搞清楚“远程Ai agent”和“把台式机装进口袋”到底指什么看到这个标题&#xff0c;你可能会想&#xff0c;这又是哪个新出的黑科技&#xff1f;免费4K144Hz远程桌面&#xff1f;还能跑AI Agent&#xff1f;听起来像是把高性能台式机变成了一个随时随地能用的“云电脑”。…

作者头像 李华
网站建设 2026/8/23 12:09:02

编程思维四大核心与八种实战方法:从代码搬运工到系统设计者

1. 项目概述&#xff1a;从“写代码”到“设计代码”的思维跃迁干了十几年开发&#xff0c;带过不少新人&#xff0c;也面试过很多人&#xff0c;我发现一个挺普遍的现象&#xff1a;很多程序员&#xff0c;尤其是刚入行一两年的朋友&#xff0c;会把“编程能力”简单地等同于“…

作者头像 李华
网站建设 2026/8/23 12:07:36

Windows平台AI大模型本地部署:轻量化桌面应用开发实战

在 Windows 上折腾 AI 大模型&#xff0c;你是否也经历过这样的场景&#xff1a;好不容易找到一个心仪的模型&#xff0c;却因为复杂的 Python 环境、CUDA 版本冲突、命令行参数晦涩难懂而卡在第一步&#xff1f;或者&#xff0c;你只是想找一个开箱即用、界面友好、能快速体验…

作者头像 李华
网站建设 2026/8/23 12:05:32

协方差与相关矩阵:从概念到PCA与投资组合的实战应用

1. 从数据散点图到协方差矩阵&#xff1a;一个直观的起点 如果你刚开始接触数学建模或者数据分析&#xff0c;面对一堆多维数据时&#xff0c;可能会先画几个二维散点图看看关系。比如&#xff0c;你手头有某个地区过去十年的“降水量”和“农作物产量”数据&#xff0c;把它们…

作者头像 李华
网站建设 2026/8/23 12:02:06

多智能体系统中时序与结构信用分配的统一优化框架解析

1. 从“分锅”难题到统一框架&#xff1a;多智能体提示优化的核心挑战如果你尝试过用多个大语言模型&#xff08;LLM&#xff09;智能体协作完成一个复杂任务&#xff0c;比如让一个智能体负责规划&#xff0c;一个负责执行&#xff0c;再一个负责审核&#xff0c;你大概率会遇…

作者头像 李华
网站建设 2026/8/23 12:02:01

数学建模论文写作指南:从模型构建到高效表达的实战技巧

1. 从“看热闹”到“写门道”&#xff1a;数学建模论文的本质认知 如果你点开这个视频&#xff0c;大概率是正被数学建模竞赛的论文写作环节折磨得焦头烂额。看着别人提交的论文结构清晰、图表精美、论证有力&#xff0c;而自己团队熬了几个通夜做出来的模型和结果&#xff0c;…

作者头像 李华