postgres mvcc visibility check mechanism - ghdrako/doc_snipets GitHub Wiki

With MVCC, reading from the database never has to wait. Writing to the DB doesn’t block reading, and reading doesn’t block writing.

MVCC system works is that each write creates a new version of a tuple (row). We therefore have what’s called snapshot isolation: each transaction has a point-in-time consistent view (or snapshot) of the data. Postgres uses timestamps and transaction IDs (XIDs) to enable the activity tracking that makes enforcing these consistent views possible, that is, determine which tuples are visible from each transaction. A snapshot, which contains the earliest still-active transaction, the first as-yet-unassigned transaction, and the list of active transactions, is obtained by each transaction from the Transaction Manager. You can get a fresh snapshot via the function pg_current_snapshot().

MVCC is implemented by creating multiple versions of individual transaction data with two virtual columns, xmin and xmax. xmin has the identity (transaction ID) of the inserting transaction for this row version and xmax has the identity (transaction ID) of the deleting transaction, or zero for an undeleted row version [1]. With these virtual columns, PostgreSQL tracks changes that occurred during insert, updates, and deletes inside the transaction block for commit as well as for rollback and to provide a consistent view of data for select statements used for long- running reports. The WAL files are used for rollback and recovery, which roll backs aborted transactions and replays the committed transactions. The main advantage of MVCC concurrency control is that its readers don’t block writers and vice versa where user action is not required and is managed by PostgreSQL Server.

The default isolation level is READ COMMITTED.

Example

tabela

 id | produkt |    date    
----+---------+------------
  1 |         | 

Sesja NR 1. | Sesja NR 2.

postgres=# start transaction ;
START TRANSACTION
postgres=*# select *,xmin,xmax from test where id=1; 
 id | produkt | date | xmin | xmax 
----+---------+------+------+------
  1 |         |      |  860 |    0
(1 row)

postgres=*# update test set produkt='czajnik' where id=1;
UPDATE 1
postgres=*# select *,xmin,xmax from test where id=1; 
 id | produkt | date | xmin | xmax 
----+---------+------+------+------
  1 | czajnik |      |  864 |    0
(1 row)

Sesja NR 2.

W tym samym czasie:

postgres=# start transaction ;
START TRANSACTION
postgres=*# select *,xmin,xmax from test where id=1;
 id | produkt | date | xmin | xmax 
----+---------+------+------+------
  1 |         |      |  860 |    0
(1 row)
-- po update w sesji 1
postgres=*# select *,xmin,xmax from test where id=1;
 id | produkt | date | xmin | xmax 
----+---------+------+------+------
  1 |         |      |  860 |  864
(1 row)

Sesja NR 2 ciagle widzi wartość ID=1. PostgreSQL, żeby rozpoznać jakie dane powinien pokazać używa kolumn xmin i xmax. Popatrzmy na nie, w transakcji NR 1 mamy xmin 860 oraz xmax 0, w NR 2 xmin 860 i xmax 864. Dzięku temu PG wie, żę ktoś dokonały zmian ale ich nie pokazuje.

Sesja NR 1

postgres=*# commit;
COMMIT

Sesja Nr 2

postgres=*# select *,xmin,xmax from test where id=1;
 id | produkt | date | xmin | xmax 
----+---------+------+------+------
  1 | czajnik |      |  864 |    0
(1 row)

Każda tuple w heapie ma:

  • xmin → ID transakcji, która ją utworzyła
  • xmax → ID transakcji, która ją usunęła (0, jeśli aktywna)

Nie ma tu flagi „frozen” ani informacji o tym, że tuple jest stara. PostgreSQL utrzymuje status każdej transakcji w pamięci (pg_xact / ProcArray):

Status TX Co oznacza
in-progress transakcja trwa
committed transakcja zatwierdzona
aborted transakcja wycofana
ProcArray

To struktura w shared memory, która trzyma tylko informacje o aktywnych transakcjach w danej chwili Każdy proces backendu ma wpis w ProcArray, zawierający m.in.:

  • jego TransactionId,
  • subtransakcje,
  • snapshot XID-ów aktywnych,
  • minimalne XID, które jeszcze nie mogą być wyczyszczone (dla vacuum). W ProcArray znajdują się tylko:
  • aktualnie trwające transakcje,
  • kilka metadanych potrzebnych do tworzenia snapshotów.

Dzialanie

  • Gdy zaczynasz nową transakcję:
    • Postgres odczytuje listę aktywnych transakcji z ProcArray i z tego buduje snapshot.
    • Snapshot mówi: „te TX-y były aktywne w momencie mojego startu”. Gdy czytasz tuple:
    • Jeśli xmin/xmax jest w zakresie snapshotu lub ProcArray, Postgres wie z pamięci, że ta TX jest jeszcze aktywna → bez I/O.
    • Jeśli xmin/xmax jest starsze niż wszystko w ProcArray, to znaczy, że transakcja dawno się skończyła → Postgres sprawdza status w pg_xact.

Rozstrzygsnie widocznosci:

tuple → snapshot → ProcArray → pg_xact → hint bits
pg_xact
  • pg_xact – globalny rejestr stanu transakcji - plik
    • to plik binarny, 1 bajt na 4 transakcje (2 bity per TX).
    • Zwykle buforowany w pamięci (pg_xact jest często w cache).
  • pg_xact (dawniej pg_clog) to systemowy plik na dysku, który przechowuje dla każdego TransactionId jego status:
    • In progress (jeszcze trwa),
    • Committed (zatwierdzona),
    • Aborted (wycofana),
    • czasem dodatkowy bit „subxact”.
  • Jest to źródło prawdy o stanie transakcji – PostgreSQL używa go, gdy nie ma tej informacji w pamięci (ProcArray), np. dla starszych TX.
  • pg_xact / ProcArray w pamięci serwera:
    • Każda transakcja ma ID i flagę: in-progress, committed, aborted
    • Silnik PostgreSQL odczytuje to przy każdym MVCC check, żeby zdecydować, czy tuple jest widoczna Snapshot transakcji:
    • Zawiera ID transakcji aktywnych w momencie startu
    • Dzięki temu transakcja widzi spójny obraz danych (snapshot isolation)

przyklad:

Tuple:
xmin = TX1
xmax = TX2

Transakcja TX3 startuje:
- patrzy na snapshot
- sprawdza status TX1 i TX2 w pg_xact / snapshot
- decyduje, czy tuple widoczna
  • Jeśli TX1 committed → xmin widoczny
  • Jeśli TX2 in-progress → tuple nadal widoczna dla TX3
  • Jeśli TX2 committed → tuple niewidoczna dla TX3
Snapshot
  • Snapshot to lokalna struktura w pamięci, utworzona w momencie startu transakcji ktora zawiera
    • xmin – najstarszy aktywny TX przy starcie,
    • xmax – pierwszy wolny TXID (jeszcze nieprzydzielony),
    • active_tx[] – lista wszystkich transakcji aktywnych przy starcie.
  • Zawiera listę ID transakcji, które były aktywne wtedy — czyli te, które nie były jeszcze w pg_xact jako committed lub aborted.
  • Snapshot nie sprawdza pg_xact przy każdym odczycie — to byłoby zbyt wolne.
  • Każda transakcja (lub zapytanie w READ COMMITTED) tworzy snapshot przy starcie.
  • Snapshot „zamraża” widoczność danych — daje spójny obraz bazy, nawet jeśli inne transakcje coś modyfikują.
  • Dzięki snapshotowi PostgreSQL nie potrzebuje blokować odczytów, a mimo to zapewnia izolację transakcji.
Snapshot:
xmin = 101
xmax = 110
active_tx = {102, 104, 108}

To znaczy:

  • transakcje 101 i mniejsze są zakończone (committed lub aborted),
  • 102, 104, 108 trwały w momencie snapshotu,
  • transakcje 110+ jeszcze nie istnieją.

Snapshot startującej transakcji zawiera listę ID transakcji, które były aktywnie w momencie startu.

Podczas odczytu tuple silnik sprawdza:

  • jeśli xmin jest mniejsze niż xmin_snapshot → tuple pochodzi od transakcji zakończonej dawno temu → widoczna,
  • jeśli xmin jest w active_tx[] → tuple pochodzi od transakcji, która jeszcze trwała przy starcie → niewidoczna,
  • jeśli xmax = aktywna TX → tuple nadal widoczna (bo jeszcze nie commit usuwającego).
  • xmin tuple → czy TX zatwierdzona/abortowana/aktywna?
  • xmax tuple → czy TX zatwierdzona/abortowana/aktywna?
  • Czy TX jest w snapshot startowym tej trzanzakcji?
    • xmin była aktywna w momencie startu → tuple niewidoczna,
    • xmin jest mniejsza niż xmin_snapshot → tuple starsza niż snapshot → prawdopodobnie widoczna,
    • xmax = TX aktywna w snapshot → tuple jeszcze nieusunięta, więc widoczna.
  • Jeśli snapshot nie zawiera danej transakcji (bo była już zakończona wcześniej lub jest bardzo stara),wtedy sięga do pg_xact i sprawdza, czy ta transakcja była committed czy aborted.

przyklad

Transakcja A (TXID=10) startuje → buduje snapshot:
xmin=5, xmax=12, active={8,9,11}
Tuple xmin xmax Widoczna dla A? Powód
R1 4 0 ✅ TAK bardzo stara, musi sprawdzic w pg_xact i ma status c - zatwierdzona/commited
R2 8 0 ❌ NIE utworzona przez TX8 – aktywna przy starcie
R3 9 0 ❌ NIE też aktywna
R4 5 11 ✅ TAK TX11 jeszcze trwa, więc usunięcie niewidoczne
R5 12 0 ❌ NIE jeszcze nie istniała przy starcie

przyklad 1

Tuple: xmin=120, xmax=130
Aktualny snapshot transakcji: active={125,126,127}, xmin=120, xmax=135

Sprawdzanie:

  • xmin=120 → jest mniejsze niż xmin_snapshot, więc bardzo stara → sprawdź w pg_xact, czy 120 jest committed
  • xmax=130 → 130 < xmax_snapshot, ale nie w active list → sprawdź w pg_xact, czy 130 committed → jeśli tak, tuple niewidoczna

Podsumowanie:

  • Snapshot mówi: „ta transakcja była aktywna, gdy zaczynałem, więc jej zmian nie widzę”.
  • pg_xact mówi: „ta transakcja jest commit / abort – czyli tuple jest (nie)widoczna”. PostgreSQL najpierw patrzy w snapshot (pamięć), a dopiero w razie potrzeby w pg_xact (na dysk lub cache).

Kiedy snapshot powstaje

Zależnie od poziomu izolacji transakcji:

Poziom izolacji Kiedy snapshot się tworzy Czy się zmienia w trakcie
READ COMMITTED na początku każdego zapytania TAK — nowe snapshoty przy każdym SELECT/UPDATE
REPEATABLE READ na początku transakcji NIE — cały czas ten sam snapshot
SERIALIZABLE również na początku transakcji NIE — ale dodatkowo rejestrowane są zależności między transakcjami

Na tej podstawie decyduje, czy tuple jest widoczna.

Widoczność = xmin/xmax + status transakcji w pg_xact + snapshot startowy
Optymalizacja – „hint bits”
  • Żeby nie sięgać do pg_xact za każdym razem, Postgres robi trick:
    • Kiedy raz sprawdzi status w pg_xact,
    • To zapisuje ten wynik bezpośrednio w tuple (tzw. hint bit):
      • HEAP_XMIN_COMMITTED
      • HEAP_XMIN_INVALID
      • HEAP_XMAX_COMMITTED
      • HEAP_XMAX_INVALID zięki temu przy kolejnym odczycie tuple nie trzeba już sprawdzać pg_xact — informacja jest „zahintowana” w samym rekordzie.
Freeze / autovacuum
  • Freeze nie jest znacznikiem w tuple, tylko zamianą xmin na FrozenTransactionId, co oznacza:
    • Tuple jest zawsze widoczna dla wszystkich nowych transakcji
    • System nie musi już sprawdzać statusu starej transakcji, bo wiadomo, że jest „stara” i zatwierdzona
  • Mechanizm jest transparentny dla MVCC: snapshot i status transakcji nadal działają tak samo, tylko tuple z frozen xmin traktujemy jak zatwierdzoną zawsze.
  • W kodzie źródłowym PostgreSQL:
#define FrozenTransactionId 2
  • Wartości:

    • InvalidTransactionId = 0 → brak transakcji / nieważny ID
    • BootstrapTransactionId = 1 → używany przy inicjalizacji bazy systemowej
    • FrozenTransactionId = 2 → tuple uznawana za zawsze widoczną
  • Gdy tuple jest „stara” i autovacuum decyduje o freeze:

    • xmin tuple zostaje ustawione na 2 (FrozenTransactionId)
    • Interpretacja: tuple jest zatwierdzona dla wszystkich transakcji, niezależnie od snapshotu startowego
    • Nie trzeba sprawdzać statusu pierwotnej transakcji w pg_xact
    • Tuple z xmin = 2 → traktowana jak zatwierdzona i widoczna dla wszystkich nowych transakcji
  • Mechanizm pozwala bezpiecznie chronić MVCC przed wraparound 32-bitowego TransactionId

  • Każda nowa transakcja dostaje unikalny TransactionId > 3 (ID 0 i 1 zarezerwowane: InvalidTransactionId i BootstrapTransactionId)

  • Podczas INSERT lub UPDATE:

    • xmin tuple = TransactionId tworzącej ją transakcji
    • xmax = 0 lub TransactionId transakcji usuwającej / zastępującej ją
    • Tuple bieżące: xmin >= 3, xmax = 0 (aktywne) lub xmax >= 3 (oznaczone do usunięcia)
    • Tuple „zamrożone”: xmin = 2, xmax = 0 → zawsze widoczna, niezależnie od bieżących TransactionId

Isolation level

For the alter system command, the scope is for the entire database, and with the set transaction command, the scope is for individual sessions. Here are examples:

postgres=# ALTER SYSTEM SET default_transaction_isolation TO 'REPEATABLE READ';
postgres=# ALTER SYSTEM SET default_transaction_isolation TO 'SERIALIZABLE';

To set isolation level for the session or for individual transactions, you must use the command inside a translation block of begin and end. Here is an example:

BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
– Run transations
END;

If you do not set inside the block, you will receive this error:

postgres=# SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
WARNING: SET TRANSACTION can only be used in transaction blocks

The default transaction level is displayed with the show command:

postgres=# SHOW default_transaction_isolation;
default_transaction_isolation
-------------------------------
read committed

With Postgres catalog views, pg_locks, pg_blocking_pids(),
pg_blocked_pid(), pgrowlocks, and pg_stat_activity, you can identify the row level, table level, page level, and block level lock. You have to determine the age of the long-running transactions and advise the application team to disconnect or terminate long-running queries to avoid “oldest x min is far in the past.” You can determine the long- running query by subtracting the current time with xact_start in the view pg_stat_activity.

Long-Running Status Check

SELECT substr(query,1,40),pid,backend_start,xact_start,now()-
xact_start seconds from pg_stat_activity where xact_start is 
not null;