Długa migawka PostgreSQL może zachowywać pośrednie wersje wierszy, których nigdy nie odczytuje. Vacuum używa zachowawczego horyzontu transakcji, aby ustalić, co można usunąć. Ten przewodnik wyjaśnia dlaczego, a następnie odtwarza to zachowanie za pomocą syntetycznych wierszy artykułów i node-postgres na Macu.

Celem jest odróżnienie migawki, która wstrzymuje czyszczenie, od sytuacji, w której vacuum jeszcze się nie uruchomiło. Przykład dotyczy PostgreSQL 18 i używa dwóch połączeń z bazą danych: jednego czytelnika utrzymującego migawkę Repeatable Read i jednego zapisującego, który aktualizuje pojedynczy artykuł. Dokumentacja PostgreSQL wskazuje obecnie wersję 18 jako najnowszą obsługiwaną wersję główną; opisane niżej szczegóły implementacji odnoszą się wyraźnie do jej gałęzi 18. Dokumentacja PostgreSQL 18.

Dlaczego zachowywać wersje, których stary czytelnik nie widzi?

Aktualizacja tworzy nową wersję wiersza. PostgreSQL zachowuje poprzednią wersję, dopóki inna transakcja może jej potrzebować. Z czasem vacuum może odzyskać nieaktualne wersje i umożliwić ponowne wykorzystanie ich miejsca. To podstawowy cykl życia MVCC opisany w sekcji Rutynowe czyszczenie.

Sedno sprawy polega na tym, że widoczność migawki i fizyczna możliwość usunięcia odpowiadają na różne pytania. Widoczność określa, czy konkretne zapytanie powinno zwrócić daną wersję. Czyszczenie korzysta z zachowawczego progu, zamiast ustalać dla każdej wersji, które z istniejących migawek mogłyby ją odczytać.

W PostgreSQL 18 funkcja HeapTupleSatisfiesVacuum porównuje transakcję, która unieważniła wersję, z OldestXmin. Zatwierdzone usunięcie lub zastąpienie nie sprawia od razu, że poprzednią wersję można usunąć: wersje, których transakcja usuwająca przypada na ten próg lub później, nadal są klasyfikowane jako niedawno martwe. Ta reguła jest opisana bezpośrednio w oficjalnej implementacji widoczności krotek.

Rozważmy poniższą przykładową sekwencję. Każda aktualizacja jest zatwierdzana osobno i nie ma innych czytelników:

Krok Działanie Co widzi stary czytelnik Najnowsza zatwierdzona wersja
1 Wstawienie artykułu w wersji 0 — Wersja 0
2 Czytelnik ustanawia migawkę Repeatable Read Wersja 0 Wersja 0
3 Zapisujący zatwierdza wersję 1 Wersja 0 Wersja 1
4 Zapisujący zatwierdza wersję 2 Wersja 0 Wersja 2
5 Zapisujący zatwierdza wersję 3 Wersja 0 Wersja 3

Czytelnik bezpośrednio potrzebuje wersji 0. Wersje 1 i 2 są pośrednie: ten czytelnik nie może ich zobaczyć, ponieważ powstały po utworzeniu jego migawki. Mimo to transakcje zastępujące te wersje są również nowsze niż zachowany próg czyszczenia. Zastosowanie opisanej reguły progowej wyjaśnia, dlaczego te wersje mogą pozostać obok wersji 0. Ich zachowanie nie oznacza, że czytelnik może przechodzić przez nie jako przez historyczne wyniki zapytań. Implementacja widoczności krotek w PostgreSQL 18.

Horyzont obejmuje więcej niż wiersz wybrany przez czytelnika. PostgreSQL 18 oblicza horyzonty właściwe dla relacji na podstawie identyfikatorów transakcji backendów i horyzontów migawek; w przypadku zwykłych danych tabeli uwzględniane są zasadniczo sesje w tej samej bazie danych. Nie śledzi wyłącznie wierszy zwróconych przez konkretne SELECT. Wyjaśnia to, dlaczego małe zapytanie może wpływać na czyszczenie w innych miejscach tej bazy danych. Relacje współdzielone i inne typy relacji podlegają odrębnym regułom, więc stwierdzenie „jedna migawka zatrzymuje każde vacuum wszędzie” jest zbyt daleko idące. Implementacja horyzontu w PostgreSQL 18.

Która transakcja faktycznie utrzymuje długą migawkę?

Poziom izolacji Repeatable Read ustanawia migawkę przy pierwszej instrukcji, która nie jest poleceniem sterującym transakcją, a następnie używa jej w kolejnych zapytaniach. Samo wykonanie BEGIN nie wystarczy do tej demonstracji; początkowe zapytanie o artykuł ustanawia widok, który musi zostać zachowany. Read Committed, domyślny poziom izolacji w PostgreSQL, pobiera nową migawkę dla każdego polecenia. Izolacja transakcji.

W związku z tym bezczynna transakcja Repeatable Read, która wykonała zapytanie do tabeli, może utrzymywać migawkę także po zakończeniu zapytania. Długo wykonywana instrukcja Read Committed również może utrzymywać migawkę podczas wykonania, ale osobne zakończone instrukcje na tym poziomie nie korzystają z tego samego niezmiennego widoku. Sam czas trwania transakcji nie pozwala więc ustalić, która migawka jest zachowywana. Izolacja transakcji.

Transakcja tylko do odczytu może wpływać na horyzont, nawet jeśli nie ma własnego identyfikatora transakcji najwyższego poziomu. Implementacja uwzględnia zarówno identyfikatory transakcji backendów, jak i horyzonty migawek. W tym przykładzie wartość null w backend_xid obok wartości niebędącej null w backend_xmin jest zatem zgodna z sytuacją, w której czytelnik utrzymuje migawkę. Implementacja horyzontu w PostgreSQL 18.

Odtworzenie zachowania z Terminala na Macu

Potrzebujesz działającego lokalnego serwera PostgreSQL 18, jego narzędzi wiersza poleceń oraz Node.js z npm. W dokumentacji zgodności node-postgres wymieniono Node.js w wersji 20 lub 24. Ten przewodnik zakłada, że istniejące ustawienia lokalnego połączenia działają. Tworzy osobną bazę danych i używa wyłącznie syntetycznego tekstu artykułów. Wprowadzenie i instalacja node-postgres.

W aplikacji Terminal utwórz katalog roboczy, zainstaluj sterownik i utwórz przykładową bazę danych:

mkdir article-snapshot-lab
cd article-snapshot-lab
npm init -y
npm install pg
createdb snapshot_lab

Zapisz cały poniższy program jako snapshot-demo.cjs. Jego połączenia dziedziczą ustawienia środowiska PostgreSQL, w tym PGDATABASE, PGHOST i PGPORT, zgodnie z dokumentacją Łączenie w node-postgres. Użyj tych samych lokalnych ustawień połączenia dla createdb i programu.

Czytelnik utrzymuje wszystkie instrukcje transakcji na jednym dedykowanym kliencie. Jest to wymagane przez node-postgres: transakcja musi przez cały czas korzystać z tego samego klienta, więc wysyłanie jej instrukcji za pomocą pool.query() jest nieodpowiednie. Drugi klient niezależnie wykonuje aktualizacje i polecenia vacuum. Transakcje w node-postgres.

Przykład indeksuje również revision, której wartość zmienia się przy każdej aktualizacji. Dzięki temu te aktualizacje celowo nie korzystają z HOT, co ułatwia interpretację demonstracji. PostgreSQL dokumentuje, że HOT wymaga, aby aktualizacje nie zmieniały kolumn objętych zwykłymi indeksami oraz aby na pierwotnej stronie było wystarczająco dużo miejsca. Krotki tylko w stercie.

const { Client } = require('pg');

async function main() {
  const reader = new Client({
    application_name: 'article_snapshot_reader',
    connectionTimeoutMillis: 5000
  });
  const writer = new Client({
    application_name: 'article_snapshot_writer',
    connectionTimeoutMillis: 5000
  });
  let readerOpen = false;
  writer.on('notice', notice => console.log(notice.message));

  try {
    await reader.connect();
    await writer.connect();
    const version = await writer.query('SHOW server_version_num');
    const major = Math.floor(Number(version.rows[0].server_version_num) / 10000);
    if (major !== 18) throw new Error('This walkthrough targets PostgreSQL 18.');

    await writer.query(
      'CREATE TABLE public.article_snapshot_demo (' +
      'id integer PRIMARY KEY, revision integer NOT NULL, title text NOT NULL)'
    );
    await writer.query(
      'CREATE INDEX article_snapshot_demo_revision_idx ' +
      'ON public.article_snapshot_demo (revision)'
    );
    await writer.query(
      'INSERT INTO public.article_snapshot_demo VALUES (1, 0, $1)',
      ['Article draft']
    );

    await reader.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
    readerOpen = true;
    const selectRow = 'SELECT id, revision, title FROM public.article_snapshot_demo';
    console.log('Reader before:', (await reader.query(selectRow)).rows);

    for (const revision of [1, 2, 3]) {
      await writer.query(
        'UPDATE public.article_snapshot_demo SET revision = $1, title = $2 WHERE id = 1',
        [revision, 'Article revision ' + revision]
      );
    }

    const pid = (await reader.query('SELECT pg_backend_pid() AS pid')).rows[0].pid;
    console.log('Reader activity:', (await writer.query(
      'SELECT state, backend_xid, backend_xmin FROM pg_stat_activity WHERE pid = $1',
      [pid]
    )).rows);
    console.log('Writer latest:', (await writer.query(selectRow)).rows);
    console.log('Reader still:', (await reader.query(selectRow)).rows);

    console.log('Vacuum with snapshot open:');
    await writer.query('VACUUM (VERBOSE) public.article_snapshot_demo');

    await reader.query('COMMIT');
    readerOpen = false;

    console.log('Vacuum after reader commit:');
    await writer.query('VACUUM (VERBOSE) public.article_snapshot_demo');
    console.log('Reader fresh:', (await reader.query(selectRow)).rows);
  } finally {
    if (readerOpen) await reader.query('ROLLBACK').catch(console.error);
    await Promise.allSettled([reader.end(), writer.end()]);
  }
}

main().catch(error => {
  console.error(error.message);
  process.exitCode = 1;
});

Uruchom program, jawnie wskazując bazę danych:

PGDATABASE=snapshot_lab node snapshot-demo.cjs

Składnię JavaScript w programie sprawdzono za pomocą Node.js. Sekwencję operacji na bazie danych zweryfikowano w odniesieniu do pobranej dokumentacji; na potrzeby tego artykułu nie wykonano jej na serwerze PostgreSQL.

Jak interpretować dwa przebiegi vacuum

Oczekiwane wyniki zapytań to wersja 0 dla Reader before, wersja 3 dla Writer latest i ponownie wersja 0 dla Reader still. Po zatwierdzeniu transakcji przez czytelnika jego następne, samodzielne zapytanie powinno zwrócić wersję 3. Są to oczekiwane wyniki syntetycznej sekwencji, a nie zarejestrowane rezultaty testu.

Wynik dotyczący aktywności powinien wskazywać czytelnika jako idle in transaction, gdy zapisujący go sprawdza, oraz pokazywać zachowane backend_xmin. PostgreSQL definiuje ten stan jako oczekiwanie transakcji na kolejne polecenie, a backend_xmin jako bieżący horyzont backendu. System statystyk zbiorczych.

W pierwszym raporcie vacuum odszukaj nieaktualne wersje, których nie można usunąć. Po zatwierdzeniu transakcji przez czytelnika porównaj go z drugim raportem, aby sprawdzić, czy te wersje zostały usunięte. W bazie danych, na której nie zachodzą inne istotne operacje, trzy zatwierdzone zastąpienia dają trzy nieaktualne wersje do sprawdzenia. Inne zachowane horyzonty lub równoległe prace konserwacyjne mogą wpłynąć na dokładną treść raportu, dlatego porównuj wzorzec przed i po, zamiast wymagać identycznego tekstu wyniku.

VACUUM (VERBOSE) generuje szczegółowy raport aktywności. Program wypisuje komunikaty serwera za pośrednictwem udokumentowanego zdarzenia node-postgres notice. Oba wywołania vacuum odbywają się poza jawnymi blokami transakcji, ponieważ PostgreSQL nie pozwala uruchamiać VACUUM wewnątrz takiej bloku. VACUUM w PostgreSQL, API Client w node-postgres.

Skorzystaj z tej wskazówki, jeśli wynik różni się od oczekiwań:

Obserwacja Co sprawdzić dalej
Czytelnik widzi wersję 3 przed zatwierdzeniem transakcji Potwierdź, że rozpoczęto transakcję Repeatable Read i że pierwsze zapytanie o artykuł wykonano przed aktualizacjami.
Pierwsze vacuum pozostawia nieaktualne wersje Porównaj je z przebiegiem po zatwierdzeniu transakcji przez czytelnika; zachowanie ich podczas otwartej migawki jest oczekiwane.
Wersje pozostają po zatwierdzeniu transakcji przez czytelnika Sprawdź, czy nie ma innego zachowanego horyzontu; zakończenie tej transakcji usuwa tylko jej wpływ.
Liczba w statystykach tabeli wydaje się niespójna Traktuj n_dead_tup jako wartość szacunkową i porównaj raporty vacuum.
Plik tabeli się nie zmniejsza Sprawdź, czy miejsce stało się możliwe do ponownego wykorzystania; zmniejszenie pliku to odrębny rezultat.

PostgreSQL wyraźnie opisuje n_dead_tup jako wartość szacunkową. Zapisany czas vacuum również potwierdza, że konserwacja się odbyła, ale nie dowodzi, że każdą nieaktualną wersję można było usunąć. Te pola pomagają w diagnostyce, lecz żadne z nich nie zastępuje sprawdzenia zachowanej migawki i raportu vacuum. System statystyk zbiorczych.

Zwykłe vacuum zazwyczaj udostępnia miejsce do ponownego użycia wewnątrz tabeli, zamiast zwracać je macOS. Udokumentowanym wyjątkiem są całkowicie wolne strony na końcu pliku. Po czyszczeniu niezmienny rozmiar pliku nie dowodzi więc, że czyszczenie się nie powiodło. Rutynowe czyszczenie.

Co należy zmienić w aplikacji?

Jeśli kilka zapytań dotyczących artykułów musi korzystać z tego samego, spójnego widoku bazy danych, ogranicz transakcję do tych zapytań i zakończ ją po ich wykonaniu. Wnioskiem dla projektu aplikacji wynikającym z zachowywania migawek jest to, aby — jeśli zadanie na to pozwala — wykonywać niezwiązane z nimi przetwarzanie i działania na komputerze po zakończeniu tej transakcji.

Jeśli każde zapytanie może niezależnie korzystać z najnowszego zatwierdzonego widoku, poziom Read Committed zapewnia migawki na poziomie poleceń. Zmiana poziomu izolacji lub zatwierdzanie transakcji między partiami zmienia semantykę spójności: późniejsze zapytania mogą widzieć nowo zatwierdzone wersje artykułów. Wybierz takie podejście tylko wtedy, gdy zmieniające się wyniki odpowiadają potrzebom czytelnika. Izolacja transakcji.

Częstsze uruchamianie vacuum nie sprawi, że wersja przekroczy niezmieniony próg możliwości usunięcia. Zakończenie niepotrzebnej migawki usuwa jej wpływ na zachowywanie wersji. Podobnie VACUUM FULL przepisuje tabelę i wymaga blokady wyłącznej oraz dodatkowego miejsca; nie jest rutynową reakcją na opisany przykład. Implementacja widoczności krotek w PostgreSQL 18, VACUUM w PostgreSQL.

Skrypt pozostawia syntetyczną tabelę do sprawdzenia. Celowo kończy się błędem, jeśli tabela już istnieje, zamiast zastępować jej zawartość. Po jej sprawdzeniu, jeśli chcesz usunąć przykładową tabelę, uruchom poniższe polecenie w sesji zapytań połączonej z snapshot_lab. Jeśli uruchomienie nie powiodło się w trakcie konfiguracji, przed usunięciem i ponownym uruchomieniem sprawdź tę samą tabelę.

DROP TABLE public.article_snapshot_demo;

Przydatna diagnostycznie jest następująca sekwencja: ustanów starą migawkę, zatwierdź kilka zastąpień, uruchom vacuum, zakończ migawkę i uruchom vacuum ponownie. Zachowanie czytelności pierwotnej wersji przy jednoczesnym oporze wersji pośrednich przed usunięciem konkretnie pokazuje różnicę. Reguły widoczności PostgreSQL określają, co zwróci zapytanie; horyzont czyszczenia określa, kiedy można ponownie wykorzystać miejsce zajmowane przez nieaktualne wersje.

Źródła