I think this is due to the way that PL/pgSQL's SELECT INTO
works under the hood.
The apparent underlying cause is mentioned in the docs:
if a locking clause is used in a cursor's query, only rows actually
fetched or stepped past by the cursor will be locked.
...and this effect flows through to any CTE in the query, where rows will only be fetched (and therefore locked) when the outer query needs them.
This is pretty easy to demonstrate. First, some setup:
CREATE TABLE t (x INT);
INSERT INTO t VALUES (1),(2),(3);
This query will lock all rows, as expected:
WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
SELECT x FROM cte WHERE x = 1
However, this will produce the behaviour you observed, locking only the first record:
DECLARE c CURSOR FOR
WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
SELECT x FROM cte WHERE x = 1;
FETCH NEXT FROM c;
My guess is that a SELECT INTO
in PL/pgSQL operates in much the same way; as mentioned in the docs, if the query returns multiple rows, then the first row is assigned to the target variable, and the rest are ignored (so there is never a need for it to fetch more than one record).
This statement, however, will lock all rows in t
:
DO $$
DECLARE i INT;
BEGIN
WITH cte AS (SELECT x FROM t WHERE x IN (1,2,3) FOR UPDATE)
SELECT x FROM cte WHERE x = 1 INTO STRICT i;
END
$$ LANGUAGE plpgsql;
Note the use of SELECT INTO STRICT
. The STRICT
keyword causes an error to be thrown if multiple rows are returned by the query. But in order to discover that a second row was returned, PL/pgSQL must attempt to fetch it, which causes the CTE query to fetch the remaining records (locking them in the process).
All of this is a non-issue most of the time in a PL/pgSQL function, as you'd typically fetch all records from a FOR UPDATE
query, and pass them along to whoever is actually updating them. On the other hand, if you want a query's side-effects but aren't interested in its output, then (as you observed) you can use PERFORM
, which will run a query to completion, but discard the results.
In the rare case where you need to (i.e. lock) all rows, while only returning a subset of them, you'll probably need to write your own loop.