5.8. Preventing Lost Updates

A Lost Update, also known as a ww-conflict, is an anomaly that occurs when concurrent transactions update the same rows, and it must be prevented in both the REPEATABLE READ and SERIALIZABLE levels. (Note that the READ COMMITTED level does not need to prevent Lost Updates.) This section describes how PostgreSQL prevents Lost Updates and shows examples.

5.8.1. Behavior of Concurrent UPDATE Commands

When an UPDATE command is executed, the function ExecUpdate is internally invoked. The pseudocode of the ExecUpdate is shown below:

Pseudocode: ExecUpdate
(1)   FOR each row that will be updated by this UPDATE command
(2)        WHILE true

                /*
                 * The First Block
                 */
(3)             IF the target row is 'being updated' THEN
(4)	             WAIT for the termination of the transaction that updated the target row

(5)                  IF (the status of the terminated transaction is COMMITTED)
   	               AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN
(6)	                  ABORT this transaction  /* First-Updater-Win */
                     ELSE 
(7)                       GOTO step (2)
                     END IF

                /*
                 * The Second Block
                 */
(8)             ELSE IF the target row has been updated by another concurrent transaction THEN
(9)                  IF (the isolation level of this transaction is READ COMMITTED THEN
(10)                      UPDATE the target row
                     ELSE
(11)                      ABORT this transaction  /* First-Updater-Win */
                     END IF

                /*
                 * The Third Block
                 */
                ELSE  /* The target row is not yet modified               */
                      /* or has been updated by a terminated transaction. */
(12)                  UPDATE the target row
                END IF
           END WHILE 
      END FOR 
  • (1) Get each row that will be updated by this UPDATE command.

  • (2) Repeat the following process until the target row has been updated (or this transaction is aborted).

  • (3) If the target row is being updated, go to step (3); otherwise, go to step (8).

  • (4) Wait for the termination of the transaction that updated the target row, because PostgreSQL uses first-updater-win scheme in SI.

  • (5) If the status of the transaction that updated the target row is COMMITTED and the isolation level of this transaction is REPEATABLE READ (or SERIALIZABLE), go to step (6); otherwise, go to step (7).

  • (6) Abort this transaction to prevent Lost Updates.

  • (7) Go to step (2) and attempt to update the target row in the next round.

  • (8) If the target row has been updated by another concurrent transaction, go to step (9); otherwise, go to step (12).

  • (9) If the isolation level of this transaction is READ COMMITTED, go to step (10); otherwise, go to step (11).

  • (10) UPDATE the target row, and go to step (1).

  • (11) Abort this transaction to prevent Lost Updates.

  • (12) UPDATE the target row, and go to step (1), because the target row is not yet modified or has been updated by a terminated transaction, i.e. there is ww-conflict.

This function performs update operations for each of the target rows. It has a while loop to update each row, and the inside of the while loop branches to three blocks according to the conditions shown in Fig. 5.11.

Fig. 5.11. Three internal blocks in ExecUpdate.
  • [1] The target row is being updated (Fig. 5.11[1])
    ‘Being updated’ means that the row is being updated by another concurrent transaction and its transaction has not terminated.
    In this case, the current transaction must wait for termination of the transaction that updated the target row because PostgreSQL’s SI uses the first-updater-win scheme.
    For example, assume that transactions Tx_A and Tx_B run concurrently, and Tx_B attempts to update a row; however, Tx_A has updated it and is still in progress. In this case, Tx_B waits for the termination of Tx_A.
    After the transaction that updated the target row commits, the update operation of the current transaction proceeds.
    If the current transaction is in the READ COMMITTED level, the target row will be updated; otherwise (REPEATABLE READ or SERIALIZABLE), the current transaction is aborted immediately to prevent lost updates.

  • [2] The target row has been updated by the concurrent transaction (Fig. 5.11[2])
    The current transaction attempts to update the target tuple; however, the other concurrent transaction has updated the target row and has already been committed.
    In this case, if the current transaction is in the READ COMMITTED level, the target row will be updated; otherwise, the current transaction is aborted immediately to prevent lost updates.

  • [3] There is no conflict (Fig. 5.11[3])
    When there is no conflict, the current transaction can update the target row.

First-updater-win / First-commiter-win

As mentioned in this section, PostgreSQL’s concurrency control based on SI uses the first-updater-win scheme to avoid lost update anomalies. In contrast, as explained in the next section, PostgreSQL’s SSI uses the first-committer-win scheme to avoid serialization anomalies.

5.8.2. Examples

Three examples are shown in the following. The first and second examples show behaviours when the target row is being updated, and the third example shows the behaviour when the target row has been updated.

5.8.2.1. Example 1

Transactions Tx_A and Tx_B update the same row in the same table, and their isolation level is READ COMMITTED.

testdb=# -- Tx_A
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL READ COMMITTED;
START TRANSACTION


testdb=# UPDATE tbl SET name = 'Hyde';
UPDATE 1





testdb=# COMMIT;
COMMIT
testdb=#
testdb=# -- Tx_B
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL READ COMMITTED;
START TRANSACTION


testdb=# UPDATE tbl SET name = 'Utterson';


(this transaction is being blocked)


UPDATE 1

Tx_B is executed as follows.

  1. After executing the UPDATE command, Tx_B should wait for the termination of Tx_A, because the target tuple is being updated by Tx_A (Step (4) in ExecUpdate).

  2. After Tx_A is committed, Tx_B attempts to update the target row (Step (7) in ExecUpdate).

  3. In the second round of ExecUpdate, the target row is updated again by Tx_B (Steps (2),(8),(9),(10) in ExecUpdate).

5.8.2.2. Example 2

Tx_A and Tx_B update the same row in the same table, and their isolation levels are READ COMMITTED and REPEATABLE READ, respectively.

testdb=# -- Tx_A
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL READ COMMITTED;
START TRANSACTION


testdb=# UPDATE tbl SET name = 'Hyde';
UPDATE 1





testdb=# COMMIT;
COMMIT
testdb=#
testdb=# -- Tx_B
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION


testdb=# UPDATE tbl SET name = 'Utterson';


(this transaction is being blocked)


ERROR:couldn't serialize access due to concurrent update

The behaviour of Tx_B is described as follows.

  1. After executing the UPDATE command, Tx_B should wait for the termination of Tx_A (Step (4) in ExecUpdate).

  2. After Tx_A is committed, Tx_B is aborted to resolve conflict because the target row has been updated and the isolation level of this transaction is REPEATABLE READ (Steps (5) and (6) in ExecUpdate).

5.8.2.3. Example 3

Tx_B (REPEATABLE READ) attempts to update the target row that has been updated by the committed Tx_A. In this case, Tx_B is aborted (Steps (2),(8),(9), and (11) in ExecUpdate).

testdb=# -- Tx_A
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL READ COMMITTED;
START TRANSACTION


testdb=# UPDATE tbl SET name = 'Hyde';
UPDATE 1

testdb=# COMMIT;
COMMIT
testdb=#
testdb=#
testdb=# -- Tx_B
testdb=# START TRANSACTION
testdb-#    ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
testdb=# SELECT * FROM tbl;
  name  
--------
 Jekyll
(1 row)
testdb=# UPDATE tbl SET name = 'Utterson';
ERROR:couldn't serialize access due to concurrent update