In the first part of this series (see here), I discussed the integer-based FOR loop and described some of the nitty-gritty details that you really should know to ensure that your code works properly in a world festering with NULL values.
I promised that part 2 (this article) would describe how to loop through the result sets produced a SELECT, INSERT, and UPDATE statements (along with a few oddities).
I lied.
Part 2 will instead provide a mini-reference (or perhaps, a supplement to the official documentation) for the query-based FOR loop, the cursor-based FOR loop, and the EXECUTE-based FOR loop.
Part 3 (which is already here) will discuss what I had originally planned to cover in part 2 – looping through SELECT, INSERT, UPDATE, DELETE, and other utility commands.
As I mentioned in part 1, the FOR loop comes in a number of different flavors. In this article, we will examine three of those flavors: the query-based FOR loop, the cursor-based FOR loop, and the EXECUTE-based FOR loop. In each case, PL/pgSQL executes a statement and then iterates through the results produced by that statement, executing the body of the loop once for each row.
Query-based FOR loop
The general form of the query-based FOR loop is:
FOR iterator IN query LOOP loopBody END LOOP [ label ];
The query-based FOR loop executes the loopBody once for each row returned by the given query (unless the loop is terminated by an EXIT, scoped CONTINE, RETURN, or unhandled exception).
Unlike an integer-based FOR loop, PL/pgSQL does not declare the iterator for you – you must declare iterator before executing the FOR loop. iterator can be any of the following:
- A comma-separated list of variables
- A row variable
- A record variable
When PL/pgSQL executes a query-based FOR loop, it evaluates the given query and loops through the result set, assigning one row to the iterator for each iteration.
Now, what happens if the shape of the result set does not match the shape of the iterator? PL/pgSQL matches up the columns in the result set against the columns in the iterator proceeding from left to right (in other words, the first column in the result set gets copied into the first column in the iterator). If the iterator contains more attributes than the query returns, the FOR loop sets the extra iterator variables to NULL. If the query returns more attributes than you’ve defined in the iterator, the extra values (returned by the query) are simply ignored. Of course, if the iterator is a record variable, the shape of the record variable will exactly match the shape of the row returned by the query. The data type of each column in the iterator must be compatible with the data type of the corresponding column in the result set (“compatible” means that the types must be identical or that an implicit cast is defined that will convert the type in the result set to the type in the iterator).
PL/pgSQL will pre-fetch from the query to improve performance. When the FOR loop begins, PL/pgSQL fetches up to 10 rows from the result set and then iterates through those values. After processing the first 10 rows, PL/pgSQL fetches the remainder of the result set 50 rows at a time.
When the loop terminates, PL/pgSQL sets the FOUND pre-defined variable to TRUE if the query returns at least one row; otherwise FOUND is set to FALSE. After the loop terminates, the variables in the iterator contain the values in the last row of the result set. If the query returns no data, the variables in the iterator are set to NULL. Remember that PL/pgSQL will raise an exception (22004 - null_value_not_allowed) if you try to assign NULL to a variable declared as NOT NULL.
Like any other loop construct, you can terminate the loop early by executing an EXIT statement, a RETURN statement, a scoped CONTINUE statement, or an unhandled exception.
Cursor-based FOR loop
The general form of the cursor-based FOR loop is:
FOR iterator IN cursorVar [ ( argument_values ) ] LOOP loopBody END LOOP [ label ];
When PL/pgSQL executes a cursor-based FOR loop, it opens the cursor and loops through the result set, assigning one row to the iterator for each iteration. When the loop terminates, PL/pgSQL closes the cursor.
The cursorVar must specify the name of a bound cursor variable (that is, the cursor variable must be declared for a specific query). If the query bound to the cursorVar is a parameterized query, you may specify the query arguments in the argument_values clause. cursorVar is typically bound to a SELECT query, but it may, in fact, be bound to any command which returns a result set. For example, you can bind cursorVar to an INSERT…RETURNING, DELETE…RETURNING, or UPDATE…RETURNING statement (see part 3 of this series).
You can refer to the cursor (within the loop) in an UPDATE…WHERE CURRENT OF cursorVar statement or a DELETE…WHERE CURRENT OF cursorVar statement. Note that, unlike the query-based and EXECUTE-based FOR loops, PL/pgSQL will not prefetch from a cursor-based FOR loop.
The cursor must not be open at the time the FOR loop begins (the FOR loop will automatically open the cursor on your behalf).
PL/pgSQL automatically declares the iterator for you – the iterator is a record variable accessible only within the loopBody. The data type and name of each attribute in the iterator is determined by the types and names in the cursor’s result set. By default, PostgreSQL assigns the name “?column?” to each computed column in the result set; you may want to alias computed columns. If you don’t, you may end up with more than one “?column?” in the result set and you won’t be able to access any computed column other than the left-most.
When the loop terminates, PL/pgSQL sets the FOUND pre-defined variable to TRUE if the cursor returns at least one row; otherwise FOUND is set to FALSE.
You can terminate the loop early by executing an EXIT statement, a RETURN statement, a scoped CONTINUE statement, or an unhandled exception. In each case, PL/pgSQL closes the cursor.
EXECUTE-based FOR loop
The general form of the EXECUTE-based FOR loop is:
FOR iterator IN EXECUTE expression [ USING expression [, ... ] ] LOOP loopBody END LOOP [ label ];
An execute-based FOR loop is similar to a query-based FOR loop; each loop evaluates a query and executes the loopBody once for each row returned by the query. The primary difference between an execute-based FOR loop and a query-based FOR loop is that you use an execute-based FOR loop to iterate through the result set of a dynamically generated query. In an execute-based FOR loop, you specify the query in the form of a string (or an expression of string type). If the expression contains parameter markers ($1, $2, …), you must specify a USING clause that includes one value for each marker.
The iterator may be any of the following:
- A comma-separated list of variables
- A row variable
- A record variable
If the iterator contains more attributes than the query returns, the FOR loop sets the extra iterator variables to NULL. If the query returns more attributes than you’ve defined in the iterator, the extra values (returned by the query) are simply ignored. If the iterator is a record variable, the shape of the record variable will exactly match the shape of the row returned by the query.
PL/pgSQL will pre-fetch from the query to improve performance. When the FOR loop begins, PL/pgSQL fetches up to 10 rows from the result set and then iterates through those values. After processing the first 10 rows, PL/pgSQL fetches the remainder of the result set 50 rows at a time.
When the loop terminates, PL/pgSQL sets the FOUND pre-defined variable to TRUE if the query returns at least one row; otherwise FOUND is set to FALSE. After the loop terminates, the variables in the iterator contain the values in the last row of the result set. If the query returns no data, the variables in the iterator are set to NULL. Remember that PL/pgSQL will raise an exception (22004 – null_value_not_allowed) if you try to assign NULL to a variable declared as NOT NULL.
You can terminate the loop early by executing an EXIT statement, a RETURN statement, a scoped CONTINUE statement, or an unhandled exception.
Next Time
In the last part of this series, I will show you how to loop over the result sets produced by SELECT, INSERT, UPDATE, DELETE, and even FETCH statements.
