-
Notifications
You must be signed in to change notification settings - Fork 0
Description
CURSOR (游标) 简单介绍
游标(Cursor)是一种用于在数据库中遍历和处理大量查询结果集的机制。它允许应用程序逐条或分批地获取数据,而不是一次性加载所有结果到内存中,从而避免内存溢出。游标的主要作用是优化内存使用并提高性能,特别适用于处理大结果集或需要逐步传输数据的场景,例如数据分页、批量数据处理或与应用程序的交互式数据访问。
在PostgreSQL中,在SQL场景允许使用DECLARE语句去定义一个游标,然后使用FETCH语句去操作先前定义的游标,同时在plpgsql中又存在另一种方式去定义和使用游标。
今天不打算在这里赘述游标的定义和使用(比方说游标向前移、向后移,以二进制的形式返回还是什么的),打算写一点个人感觉相对冷门的用法。
场景构建
其实还是和上篇文章息息相关,就是将一个游标中的数据插入到另一张表中,并且在这其中使用了事务控制语句,如commit/rollback。如果我们使用下面的语句就会出现这个cursor "<unnamed portal xx>" does not exist现象。
postgres=# CREATE TABLE test(a INT);
CREATE TABLE
postgres=# DO $$
DECLARE
p_curdata refcursor;
val int;
BEGIN
open p_curdata for SELECT generate_series(1,10);
LOOP
FETCH p_curdata INTO val;
EXIT WHEN val IS NULL;
INSERT INTO test VALUES(val);
IF val % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
END; $$;
ERROR: cursor "<unnamed portal 1>" does not exist
CONTEXT: PL/pgSQL function inline_code_block line 8 at FETCH原因在于在plpgsql中使用事务控制语句,会将当前打开的cursor进行自动关闭,该怎么解决呢?
解决方案
参考上一篇文章——PL/pgSQL事务控制场景
上一篇文章中我们有介绍过在FOR循环中使用只读游标,plpgsql内部会自动帮我们pin住这个只读游标,因此我们可以使用下面的两种写法。
隐式游标变量
postgres=# TRUNCATE test;
TRUNCATE TABLE
postgres=# DO $$
DECLARE
val int;
cursor_info record;
is_print bool DEFAULT false;
BEGIN
FOR val IN SELECT generate_series(1,10) LOOP
-- 查看一下pg_cursors中的信息
IF is_print is false THEN
is_print = true;
SELECT * INTO cursor_info FROM pg_cursors;
RAISE NOTICE 'cursor_info = %', cursor_info;
END IF;
-- 插入数据
INSERT INTO test VALUES(val);
IF val % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
END; $$;
NOTICE: cursor_info = ("<unnamed portal 2>","SELECT generate_series(1,10)",f,f,f,"2025-11-10 16:34:27.8992+08")
DO
postgres=# -- 查看表中数据
postgres=# SELECT * FROM test;
a
----
2
4
6
8
10
(5 rows)显式游标变量
postgres=# TRUNCATE test;
TRUNCATE TABLE
postgres=# DO $$
DECLARE
val record;
cursor_info record;
is_print bool DEFAULT false;
p_curdata CURSOR FOR SELECT id FROM generate_series(1,10) t(id);
BEGIN
FOR val IN p_curdata LOOP
-- 查看一下pg_cursors中的信息
IF is_print is false THEN
is_print = true;
SELECT * INTO cursor_info FROM pg_cursors;
RAISE NOTICE 'cursor_info = %', cursor_info;
END IF;
-- 插入数据
INSERT INTO test(a) VALUES(val.id);
IF val.id % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
END; $$;
NOTICE: cursor_info = ("<unnamed portal 11>","SELECT id FROM generate_series(1,10) t(id)",f,f,t,"2025-11-10 16:50:09.976953+08")
DO
postgres=# SELECT * FROM test;
a
----
2
4
6
8
10
(5 rows)相对冷门的写法
这个写法需要配合SQL层面的DECLARE语句来实现,且需要将cursor定义成HOLD CURSOR.
postgres=# TRUNCATE test;
TRUNCATE TABLE
postgres=# DO $$
DECLARE
val INT;
cursor_info record;
is_print bool DEFAULT false;
p_curdata refcursor = 'hold_cursor';
BEGIN
EXECUTE 'DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT generate_series(1,10)';
LOOP
FETCH p_curdata INTO val;
EXIT WHEN val IS NULL;
-- 查看一下pg_cursors中的信息
IF is_print is false THEN
is_print = true;
SELECT * INTO cursor_info FROM pg_cursors;
RAISE NOTICE 'cursor_info = %', cursor_info;
END IF;
IF val % 2 = 0 THEN
INSERT INTO test(a) VALUES(val); -- 插入数据
COMMIT;
-- ELSE
-- ROLLBACK; -- 这里不能使用ROLLBACK 如果使用会导致hold_cursor关闭
END IF;
END LOOP;
-- 需要手动关闭
CLOSE p_curdata;
END; $$;
NOTICE: cursor_info = (hold_cursor,"DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT generate_series(1,10)",t,f,f,"2025-11-11 00:47:57.064936+08")
DO
postgres=# SELECT * FROM test;
a
----
2
4
6
8
10
(5 rows)
postgres=# SELECT * FROM pg_cursors;
name | statement | is_holdable | is_binary | is_scrollable | creation_time
------+-----------+-------------+-----------+---------------+---------------
(0 rows)这里指的注意的是,和上面的FOR循环中的显式/隐式游标不同,这种写法是不能使用ROLLBACK的,如果使用了,即使定义的这个游标为HOLD状态也会直接导致跟随实际的事务处理掉,现象如下:
postgres=# do $$
declare
p_CurData refcursor := 'hold_cursor';
val int;
begin
execute 'DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT 42';
loop
fetch p_CurData into val;
exit when val is null;
raise notice 'val = %', val;
rollback;
end loop;
close p_CurData;
end; $$;
NOTICE: val = 42
ERROR: cursor "hold_cursor" does not exist
CONTEXT: PL/pgSQL function inline_code_block line 8 at FETCH额外的一点补充(hold cursor)
如果声明的hold cursor,数据量不多,那么数据将会存放至内存之中;如果数据量过多,则会存放至临时文件中。可通过命令ls -lh $PGDATA/base/pgsql_tmp/,查看临时文件来判断,其实也就是物化。
postgres=# CREATE TABLE test_cursor_mat (
id SERIAL PRIMARY KEY,
data TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE
postgres=# -- 插入1000000行数据
postgres=# INSERT INTO test_cursor_mat (data)
SELECT 'Test data row ' || i || repeat('x', 500)
FROM generate_series(1, 1000000) i;
INSERT 0 1000000
postgres=# ANALYZE test_cursor_mat;
ANALYZE
postgres=# SELECT pg_size_pretty(pg_total_relation_size('test_cursor_mat')) AS total_table_size;
total_table_size
------------------
580 MB
(1 row)
postgres=# DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT * FROM test_cursor_mat; -- hold_cursor
DECLARE CURSOR
postgres=# -- 查看当前的PID
postgres=# SELECT pg_backend_pid();
pg_backend_pid
----------------
18392
(1 row)
postgres=# -- 查看是否存在对应的临时文件
postgres=# \! ls -lh $PGDATA/base/pgsql_tmp/
total 525M
-rw------- 1 postgres postgres 525M Nov 10 14:24 pgsql_tmp18392.3
postgres=# -- 释放该hold cursor
postgres=# discard all;
DISCARD ALL
postgres=# -- 再次查看是否存在对应的临时文件
postgres=# \! ls -lh $PGDATA/base/pgsql_tmp/
total 0少量数据 (这里的100条数据估计不超过8k)
postgres=# DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT generate_series(1,100);
DECLARE CURSOR
postgres=# \! ls -lh $PGDATA/base/pgsql_tmp/
total 0
postgres=# select * from pg_cursors;
name | statement | is_holdable | is_binary | is_scrollable | creation_time
-------------+-------------------------------------------------------------------------+-------------+-----------+---------------+-------------------------------
hold_cursor | DECLARE hold_cursor CURSOR WITH HOLD FOR SELECT generate_series(1,100); | t | f | f | 2025-11-10 14:36:38.913502+08
(1 row)
