当一条 SELECT 语句返回的结果集大到无法一次性装进内存时,游标就成了 PostgreSQL 提供的标准解决方案。它允许应用程序分批取回数据,而不是等待整个结果集生成完毕。不过在声明游标时,有两个容易被忽视的属性会直接影响使用方式:滚动性(SCROLL)和敏感度(INSENSITIVE)。前者决定了游标能否反向遍历,后者决定了游标能否感知底层数据的并发修改。本文将结合示例详细讲解这两个属性的用法和陷阱。

游标滚动性:SCROLL 与 NO SCROLL 的区别
滚动性控制的是游标在结果集中的移动方向。声明为 SCROLL 的游标支持 FETCH PRIOR、FETCH FIRST、FETCH LAST、FETCH RELATIVE 以及 FETCH ABSOLUTE 等全方位的移动操作;而 NO SCROLL 游标只能从头到尾单向前进,也就是仅支持 FETCH NEXT。这个看似简单的区别背后,其实涉及 PostgreSQL 存储结果集的方式。
需要特别注意的是,通过 SQL 层面 DECLARE ... CURSOR 声明的普通查询游标,默认就是 NO SCROLL 的。如果尝试在一个默认游标上执行 FETCH BACKWARD,PostgreSQL 会直接报错,提示游标不能反向取数据。原因在于 PostgreSQL 对简单查询采用了流式执行策略——结果行是边执行边产生的,执行过的行如果没有显式缓存,就无法回退重新读取。要在 SQL 层获得滚动能力,必须在声明时显式写上 SCROLL 关键字,此时 PostgreSQL 会物化一份临时结果集,代价是额外的内存或临时文件开销。
-- 声明一个可滚动的游标
DECLARE cur_order SCROLL CURSOR FOR
SELECT order_id, amount FROM orders ORDER BY order_id;
-- 取最后一行
FETCH LAST FROM cur_order;
-- 向前回退两行
FETCH RELATIVE -2 FROM cur_order;
-- 直接跳转到第 3 行
FETCH ABSOLUTE 3 FROM cur_order;
CLOSE cur_order;在 PL/pgSQL 存储过程中,情况正好相反:过程语言里的游标默认是可滚动的(相当于 SCROLL),如果明确知道只需要单向遍历,建议显式声明为 NO SCROLL,避免数据库为回退能力预留物化缓冲,从而降低内存占用。下面是一个 PL/pgSQL 中的声明示例:
DO $$
DECLARE
cur NO SCROLL CURSOR FOR SELECT id FROM t_limit_test;
rec RECORD;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
RAISE NOTICE '当前行 id = %', rec.id;
END LOOP;
CLOSE cur;
END $$;敏感度设置:INSENSITIVE 与 SENSITIVE 的行为差异
敏感度描述的是游标对底层数据并发变化的可见性。按照 SQL 标准,INSENSITIVE 游标看到的是声明时刻的数据快照,事务内其他语句对源表的修改不会反映到游标结果中;SENSITIVE 游标则相反,能够感知这些变化。PostgreSQL 在文档中声明其游标默认且仅支持 INSENSITIVE 语义——即使在声明时写上 SENSITIVE 或 INSENSITIVE 关键字,实际行为仍然是 INSENSITIVE,SENSITIVE 会被静默接受而不报错。
这背后的原因是 PostgreSQL 的一致性读机制。游标读取数据时依赖 MVCC 多版本快照,整个游标生命周期内使用同一个快照,因此自然隔离了后续修改。举例来说,先声明一个游标,然后在同一事务中 UPDATE 源表并提交部分逻辑,再通过游标 FETCH,取到的依然是声明时的旧数据。这一点从其他数据库(如 SQL Server 的动态游标)迁移过来的开发者尤其要留意,行为预期可能完全不同。
BEGIN;
DECLARE cur_ins INSENSITIVE CURSOR FOR
SELECT name FROM users WHERE active = true;
-- 即使这里修改了 users 表,游标读到的仍是声明时的数据
UPDATE users SET active = false WHERE id = 1;
FETCH ALL FROM cur_ins; -- 返回结果仍包含 id = 1 的记录
COMMIT;如果业务上确实需要感知最新数据,可行的替代方案有两个:一是修改完成后重新声明游标并重新打开;二是放弃游标,改用普通查询直接读取当前快照。试图依赖游标的敏感性来实现实时读取,在 PostgreSQL 中是行不通的。
实际使用中的注意事项与性能建议
首先是滚动性与性能的权衡。SCROLL 游标需要物化结果集,当原始查询返回千万行数据时,物化开销可能非常大,甚至撑爆临时文件空间。生产环境中,只有确实需要随机跳转或回退遍历时才使用 SCROLL,普通的分批拉取场景(例如 ETL 导出)用 NO SCROLL 就够了。
其次是事务边界问题。不带 WITH HOLD 选项的游标必须在一个事务块内使用,事务提交或回滚后游标自动销毁。如果希望游标跨事务存活,需要使用 WITH HOLD,但此时 PostgreSQL 会强制在事务提交时物化整个结果集,同样会带来不小的存储开销。另外,WITH HOLD 游标必须是只读的,不能配合 FOR UPDATE 使用。
最后给出一个综合示例,展示跨事务的滚动游标用法:
BEGIN;
DECLARE cur_hold SCROLL CURSOR WITH HOLD FOR
SELECT id, title FROM articles ORDER BY created_at DESC;
FETCH NEXT 10 FROM cur_hold; -- 先取前 10 条
COMMIT; -- 游标仍然存活
BEGIN;
FETCH PRIOR FROM cur_hold; -- 回退一行
CLOSE cur_hold;
COMMIT;总结一下:PostgreSQL 的游标默认不可滚动且不敏感,SQL 层游标需要显式声明 SCROLL 才能反向移动,而 PL/pgSQL 游标默认可滚动、建议按需声明 NO SCROLL;敏感度方面 PostgreSQL 只实现 INSENSITIVE 语义,需要最新数据时应重新打开游标。理解这两个属性,能帮助你在报表分页、批量导出、流式处理等场景中写出更可靠高效的代码。
修改时间:2026-09-12 07:00:29