Table of Contents
Both cursors and transactions are basic ingredients for developing a database application. This article describes how cursors and transactions interact and how WITH HOLD
can work around their limitations. We will also see the dangers involved and how to properly use WITH HOLD
cursors in a PL/pgSQL procedure.
When a query is ready for execution, PostgreSQL creates a portal from which the result rows can be fetched. During normal query execution, you receive the whole result set in one step. In contrast, a cursor allows you to fetch the result rows one by one. A cursor marks a position within a result set. Cursors are particularly useful in procedural code on the client or in the database, because they allow you to loop through the query results. Another advantage is that a cursor allows you to have more than one SQL statement running at the same time, which is normally not possible in a single database session.
A simple example for PL/pgSQL code that uses a cursor would be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
DO LANGUAGE plpgsql $$DECLARE /* declare and open a cursor */ c CURSOR FOR SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'mydata' AND table_name LIKE 'old_%'; v_schema text; v_name text; BEGIN LOOP /* get next result row */ FETCH c INTO v_schema, v_name; /* system variable FOUND is set by FETCH */ EXIT WHEN NOT FOUND; /* avoid SQL injection */ EXECUTE format( 'DROP TABLE %I.%I', v_schema, v_name ); END LOOP; /* not necessary */ CLOSE c; END;$$; |
In this example, the SELECT
is executed concurrently with the DROP TABLE
statements.
The above is not the most readable way to write this in PL/pgSQL (you could have used “FOR v_schema, v_name IN SELECT ... LOOP ... END LOOP;
”, which uses a cursor “under the hood”), but I wanted to make the cursor explicit.
Note that it is often possible to avoid a cursor loop by using a join in the database. Such a join is more efficient, because it does all the work in a single statement. However, we have to use a cursor in our case, since we need to execute a dynamic SQL statement inside the loop.
One basic property of a PostgreSQL cursor is that it only exists for the duration of a database transaction. That is not surprising, since a cursor is a single SQL statement, and an SQL statement is always part of one transaction. In the above example we had no problem, because a DO
statement is always executed in a single transaction anyway.
Cursors are automatically closed at the end of a transaction, so it is usually not necessary to explicitly close them, unless they are part of a long-running transaction and you want to free the resources allocated by the statement.
A special feature of PostgreSQL is that you can use cursors in SQL. You create a cursor with the DECLARE
statement:
1 2 |
DECLARE name [ BINARY ] [ ASENSITIVE | INSENSITIVE ] [ [ NO ] SCROLL ] CURSOR [ { WITH | WITHOUT } HOLD ] FOR query |
Here is a short description of the different options:
BINARY
will fetch the results in the internal binary format, which may be useful if you want to read bytea
columns and avoid the overhead of escaping them as stringsSCROLL
means that you can move the cursor position backwards to fetch the same rows several timesWITH HOLD
creates a cursor that is not automatically closed at the end of a transactionASENSITIVE
and INSENSITIVE
are redundant in PostgreSQL and are there for SQL standard compatibilityThere is also an SQL statement FETCH
that is more powerful than its PL/pgSQL equivalent, in that it can fetch more than one row at a time. Like PL/pgSQL, SQL also has a MOVE
statement that moves the cursor position without retrieving rows.
SQL cursors are closed with the CLOSE
statement, or by the end of the transaction.
SCROLL
cursorsSome execution plans, like a B-tree index scan or a sequential scan, can be executed in both directions. A cursor for a query with such an execution plan is implicitly scrollable, that is, you can move the cursor position backwards in the result set. PostgreSQL calculates query result rows “on demand” and streams them to the client, so scrollable cursors for such queries come with no extra cost.
Other, more complicated execution plans require the explicit keyword SCROLL
for the cursor to become scrollable. Such cursors incur an overhead, because the server must cache the entire result set.
Here is a little example that showcases scrollable cursors:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
BEGIN; /* this cursor would be implicitly scrollable */ DECLARE c SCROLL CURSOR FOR SELECT * FROM generate_series(1, 10); FETCH 5 FROM c; generate_series ═════════════════ 1 2 3 4 5 (5 rows) MOVE BACKWARD 2 FROM c; FETCH BACKWARD 2 FROM c; generate_series ═════════════════ 2 1 (2 rows) /* sixth result row */ FETCH ABSOLUTE 6 FROM c; generate_series ═════════════════ 6 (1 row) FETCH ALL FROM c; generate_series ═════════════════ 7 8 9 10 (4 rows) COMMIT; |
The SQL standard distinguishes SENSITIVE
, INSENSITIVE
and ASENSITIVE
cursors. A sensitive cursor reflects modifications of the underlying data; one consequence of this is that scrolling back to a previous row might fetch a different result. PostgreSQL does not implement sensitive cursors: that would be difficult, because a statement always sees a stable snapshot of the data in PostgreSQL.
PostgreSQL cursors are always insensitive, which means that changes in the underlying data after the cursor has started processing are not visible in the data fetched from the cursor. “Asensitive”, which means that the sensitivity is implementation dependent, is the same as “insensitive” in PostgreSQL.
Note that this insensitivity also applies if you modify a table via the special statements “UPDATE/DELETE ... WHERE CURRENT OF
”.
WITH HOLD
corsorsSince WITH HOLD
cursors live longer than a transaction, but statements don't, PostgreSQL must calculate the complete result set at COMMIT
time and cache it on the server. This can result in COMMIT
taking an unusually long time.
Moreover, WITH HOLD
cursors are not automatically closed at the end of the transaction, so you must not forget to CLOSE
them if you don't want the result set to hog server resources until the end of the database session.
Here is an example of a WITH HOLD
cursor in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
BEGIN; DECLARE c CURSOR WITH HOLD FOR SELECT i FROM generate_series(1, 10) AS i; FETCH 3 FROM c; i ═══ 1 2 3 (3 rows) COMMIT; FETCH 3 FROM c; i ═══ 4 5 6 (3 rows) /* important */ CLOSE c; |
Cursors in PL/pgSQL are variables of the special data type refcursor
. The value of such a variable is actually a string. That string is the name of the portal that is opened when a query is bound to the cursor variable and the cursor is opened.
Using refcursor
variables, you can also pass cursors between PL/pgSQL functions or procedures:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
CREATE FUNCTION c_open(n integer) RETURNS refcursor LANGUAGE plpgsql AS $$DECLARE /* a query is bound to the cursor variable */ c CURSOR (x integer) FOR SELECT * FROM generate_series(1, x); BEGIN /* the cursor is opened */ OPEN c(n); RETURN c; END;$$; CREATE FUNCTION c_fetch(cur refcursor) RETURNS TABLE (r integer) LANGUAGE plpgsql AS $$BEGIN LOOP FETCH cur INTO r; EXIT WHEN NOT FOUND; RETURN NEXT; END LOOP; END;$$; SELECT c_fetch(c_open(5)); c_fetch ═════════ 1 2 3 4 5 (5 rows) |
Cursor declarations in PL/pgSQL support SCROLL
, but not WITH HOLD
, for the historical reason that PostgreSQL functions always run inside a single transaction. Also, you can only FETCH
a single row at a time from a PL/pgSQL cursor.
WITH HOLD
cursors in PL/pgSQL proceduresProcedures, introduced in PostgreSQL v11, support transaction commands like COMMIT
and ROLLBACK
under certain circumstances. Consequently, it would be useful to have WITH HOLD
cursors in procedures. There are two ways to work around the lack of WITH HOLD
cursors in PL/pgSQL:
refcursor
argument to the procedureHere is sample code that illustrates the second technique:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
CREATE PROCEDURE del_old() LANGUAGE plpgsql AS $$DECLARE /* assign the portal name */ c refcursor := 'curs'; v_schema text; v_name text; BEGIN /* dynamic SQL to create the cursor */ EXECUTE $_$DECLARE curs CURSOR WITH HOLD FOR SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'mydata' AND table_name LIKE 'old_%'$_$; LOOP FETCH c INTO v_schema, v_name; EXIT WHEN NOT FOUND; /* * We need to make sure that the cursor is closed * in the case of an error. For that, we need an * extra block, because COMMIT cannot be used in * a block with an EXCEPTION clause. */ BEGIN /* avoid SQL injection */ EXECUTE format( 'DROP TABLE %I.%I', v_schema, v_name ); EXCEPTION WHEN OTHERS THEN CLOSE c; RAISE; WHEN query_canceled THEN CLOSE c; RAISE; END; /* reduce deadlock risk when dropping many tables */ COMMIT; END LOOP; /* we need to close the cursor */ CLOSE c; END;$$; |
Note how the code makes dead sure that the cursor cannot “leak” from the procedure!
Both cursors and transactions are well-known database features. Normally, cursors exist only within a single database transaction. But by using WITH HOLD
, you can escape that limitation. Useful as this feature is, you have to be aware of the performance impact during COMMIT
, and you have to make sure that you close the cursor to free the server's resources.
In order to receive regular updates on important changes in PostgreSQL, subscribe to our newsletter, or follow us on Twitter, Facebook, or LinkedIn.
You need to load content from reCAPTCHA to submit the form. Please note that doing so will share data with third-party providers.
More InformationYou are currently viewing a placeholder content from Facebook. To access the actual content, click the button below. Please note that doing so will share data with third-party providers.
More InformationYou are currently viewing a placeholder content from X. To access the actual content, click the button below. Please note that doing so will share data with third-party providers.
More Information
Awesome post, man!! Congrats and thanks for sharing your knowledge!!
nice posts, good help for understand in depth use of cursors