Alice and the curser (fortunately not WITH HOLD)
© Laurenz Albe 2021

 

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.

Cursors in PostgreSQL

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:

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.

Cursors and transactions

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.

Cursors in SQL

A special feature of PostgreSQL is that you can use cursors in SQL. You create a cursor with the DECLARE statement:

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 strings
  • SCROLL means that you can move the cursor position backwards to fetch the same rows several times
  • WITH HOLD creates a cursor that is not automatically closed at the end of a transaction
  • ASENSITIVE and INSENSITIVE are redundant in PostgreSQL and are there for SQL standard compatibility

There 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 cursors

Some 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:

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;

Cursor sensitivity

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 <cursor>”.

WITH HOLD corsors

Since 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:

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

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:

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 procedures

Procedures, 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:

  • create the cursor in SQL and pass it as a refcursor argument to the procedure
  • use dynamic SQL to declare an SQL cursor

Here is sample code that illustrates the second technique:

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!

Conclusion

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.