Search code examples
postgresqltransactionslockingpostgresql-11select-for-update

Postgres: SELECT FOR UPDATE does not see new rows after lock release


Trying to support PostgreSQL DB in my application, found this strange behaviour.

Preparation:

CREATE TABLE test(id INTEGER, flag BOOLEAN);
INSERT INTO test(id, flag) VALUES (1, true);

Assume two concurrent transactions (Autocommit=false, READ_COMMITTED) TX1 and TX2:

TX1:

UPDATE test SET flag = FALSE WHERE id = 1;
INSERT INTO test(id, flag) VALUES (2, TRUE);
-- (wait, no COMMIT yet)

TX2:

SELECT id FROM test WHERE flag=true FOR UPDATE;
-- waits for TX1 to release lock

Now, if I COMMIT in TX1, the SELECT in TX2 returns empty cursor.

It is strange to me, because same experiment in Oracle and MariaDB results in selecting newly created row (id=2).

I could not find anything about this behaviour in PG documentation. Am I missing something? Is there any way to force PG server to "refresh" statement visibility after acquiring lock?

PS: PostgreSQL version 11.1


Solution

  • TX2 scans the table and tries to lock the results.

    The scan sees the snapshot of the database from the start of the query, so it cannot see any rows that were inserted (or made eligible in some other way) by concurrent modifications that started after that snapshot was taken.

    That is why you cannot see the row with the id 2.

    For id 1, that is also true, so the scan finds that row. But the query has to wait until the lock is released. When that finally happens, it fetches that latest committed version of the row and performs the check again, so that row is excluded as well.

    This “EvalPlanQual” recheck (to use PostgreSQL jargon) is only performed for rows that were found during the scan, but were locked. The second row isn't even found during the scan, so no such processing happens there.

    This is a bit odd, admitted. But it is not a bug, it is just the way PostgreSQL wirks.

    If you want to avoid such anomalies, use the REPEATABLE READ isolation level. Then you will get a serialization error in such a case and can retry the transaction, thus avoiding inconsistencies like that.