Een lange PostgreSQL-snapshot kan tussenliggende rijversies behouden die hij nooit leest. Vacuum gebruikt een conservatieve transactiehorizon om te bepalen wat kan worden verwijderd. Deze uitleg laat zien waarom en reproduceert het gedrag vervolgens met synthetische artikelrijen en node-postgres op een Mac.

De taak is onderscheid te maken tussen een snapshot die opschoning tegenhoudt en een vacuum dat nog niet is uitgevoerd. Het voorbeeld is bedoeld voor PostgreSQL 18 en gebruikt twee databaseverbindingen: één lezer die een Repeatable Read-snapshot vasthoudt en één schrijver die één artikel bijwerkt. De documentatie van PostgreSQL vermeldt momenteel versie 18 als de huidige ondersteunde hoofdversie; de implementatiedetails hieronder zijn expliciet gekoppeld aan de 18-branch. PostgreSQL 18-documentatie.

Waarom versies behouden die de oude lezer niet kan zien?

Een update maakt een nieuwe rijversie aan. PostgreSQL behoudt de vorige versie zolang een andere transactie die mogelijk nog nodig heeft. Uiteindelijk kan vacuum verouderde versies terugwinnen, zodat de opslagruimte opnieuw kan worden gebruikt. Dat is de basislevenscyclus van MVCC die wordt beschreven in Regulier vacuumen.

De subtiliteit is dat snapshotzichtbaarheid en fysieke verwijderbaarheid verschillende vragen beantwoorden. Zichtbaarheid bepaalt of een bepaalde query een versie moet retourneren. Opschoning gebruikt een conservatieve grens in plaats van voor elke versie precies vast te stellen welke bestaande snapshots die kunnen lezen.

In PostgreSQL 18 vergelijkt HeapTupleSatisfiesVacuum de transactie die een versie verouderd maakte met OldestXmin. Een vastgelegde verwijdering of vervanging maakt de vorige versie niet meteen verwijderbaar: versies waarvan de verwijderende transactie op of na die grens valt, blijven geclassificeerd als recent dood. Deze regel is rechtstreeks gedocumenteerd in de officiële implementatie van tuplezichtbaarheid.

Bekijk deze illustratieve reeks. Elke update wordt afzonderlijk vastgelegd en er zijn geen andere lezers:

Stap Actie Wat de oude lezer ziet Nieuwste vastgelegde versie
1 Artikelrevisie 0 invoegen — Revisie 0
2 Lezer stelt een Repeatable Read-snapshot vast Revisie 0 Revisie 0
3 Schrijver legt revisie 1 vast Revisie 0 Revisie 1
4 Schrijver legt revisie 2 vast Revisie 0 Revisie 2
5 Schrijver legt revisie 3 vast Revisie 0 Revisie 3

Revisie 0 is rechtstreeks nodig voor de lezer. Revisies 1 en 2 zijn tussenliggende versies: deze lezer kan ze niet zien, omdat ze na zijn snapshot zijn aangemaakt. Toch zijn de transacties die ze vervingen ook nieuwer dan de vastgehouden opschoningsgrens. De gedocumenteerde grensregel verklaart waarom deze versies naast revisie 0 kunnen blijven bestaan. Dat ze behouden blijven, betekent niet dat de lezer ze als historische queryresultaten kan doorlopen. Implementatie van tuplezichtbaarheid in PostgreSQL 18.

De horizon is ruimer dan de rij die de lezer heeft geselecteerd. PostgreSQL 18 berekent horizons die passen bij de relatie, op basis van transactie-ID's van backends en snaphothorizons; voor gewone tabelgegevens wordt doorgaans rekening gehouden met sessies in dezelfde database. Er wordt niet alleen bijgehouden welke rijen door een bepaalde SELECT zijn geretourneerd. Dit verklaart waarom een kleine query de opschoning elders in die database kan beïnvloeden. Gedeelde relaties en andere relatietypen hebben eigen regels, dus “één snapshot stopt alle vacuum overal” is te algemeen. Horizonimplementatie in PostgreSQL 18.

Welke transactie houdt een snapshot daadwerkelijk lang vast?

Repeatable Read stelt de snapshot vast bij de eerste instructie die geen transactiebeheer betreft en gebruikt die snapshot vervolgens voor latere queries. Alleen BEGIN uitvoeren volstaat niet voor deze demonstratie; de eerste artikelquery stelt de weergave vast die behouden moet blijven. Read Committed, het standaardisolatieniveau van PostgreSQL, neemt daarentegen voor elke opdracht een nieuwe snapshot. Transactie-isolatie.

Daarom kan een inactieve Repeatable Read-transactie die de tabel heeft opgevraagd haar snapshot behouden nadat de query is voltooid. Een langlopende Read Committed-instructie kan tijdens de uitvoering ook een snapshot vasthouden, maar afzonderlijke, voltooide instructies op dat niveau delen niet dezelfde vaste weergave. De duur van een transactie zegt dus op zichzelf niet welke snapshot wordt behouden. Transactie-isolatie.

Een alleen-lezen-transactie kan bijdragen aan de horizon zonder zelf een transactie-ID op hoofdniveau te hebben. De implementatie houdt rekening met zowel transactie-ID's van backends als snaphothorizons. In dit voorbeeld is een null backend_xid naast een niet-null backend_xmin dus verenigbaar met een lezer die een snapshot vasthoudt. Horizonimplementatie in PostgreSQL 18.

Reproduceer het gedrag vanuit Mac Terminal

Je hebt een actieve lokale PostgreSQL 18-server, de opdrachtregelprogramma's en Node.js met npm nodig. Node.js 20 of 24 wordt vermeld in de compatibiliteitsdocumentatie van node-postgres. Deze uitleg gaat ervan uit dat je bestaande lokale verbindingsinstellingen werken. Er wordt een aparte database aangemaakt en alleen synthetische artikeltekst gebruikt. Inleiding tot en installatie van node-postgres.

Maak in Terminal een werkmap aan, installeer het stuurprogramma en maak de voorbeelddatabase aan:

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

Sla het volledige programma hieronder op als snapshot-demo.cjs. De verbindingen nemen PostgreSQL-omgevingsinstellingen over, waaronder PGDATABASE, PGHOST en PGPORT, zoals beschreven in Verbinden met node-postgres. Gebruik dezelfde lokale verbindingsinstellingen voor createdb en het programma.

De lezer voert alle transactiestatements uit op één toegewezen client. Dat is vereist door node-postgres: een transactie moet gedurende de hele transactie dezelfde client gebruiken, dus de statements ervan via pool.query() uitvoeren is ongeschikt. De tweede client voert de updates en vacuum-opdrachten onafhankelijk uit. Transacties met node-postgres.

Het voorbeeld indexeert ook revision, dat bij elke update verandert. Daardoor kunnen deze updates opzettelijk geen HOT gebruiken, wat de demonstratie makkelijker te interpreteren maakt. PostgreSQL documenteert dat HOT vereist dat updates kolommen die in gewone indexen worden gebruikt ongemoeid laten, en dat er voldoende ruimte op de oorspronkelijke pagina beschikbaar is. Heap-Only Tuples.

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;
});

Voer het programma uit met de database expliciet geselecteerd:

PGDATABASE=snapshot_lab node snapshot-demo.cjs

De JavaScript-syntaxis van het programma is gecontroleerd met Node.js. De databasevolgorde is getoetst aan de opgehaalde documentatie; voor dit artikel is ze niet uitgevoerd op een PostgreSQL-server.

De twee vacuum-rondes interpreteren

De verwachte queryresultaten zijn revisie 0 voor Reader before, revisie 3 voor Writer latest en opnieuw revisie 0 voor Reader still. Nadat de lezer de transactie heeft vastgelegd, hoort zijn volgende zelfstandige query revisie 3 op te leveren. Dit zijn verwachte resultaten van de synthetische reeks, geen vastgelegde testuitvoer.

Het activiteitresultaat hoort de lezer als idle in transaction te tonen terwijl de schrijver deze controleert, met een vastgehouden backend_xmin. PostgreSQL definieert die status als een transactie die op een andere opdracht wacht, en backend_xmin als de huidige horizon van de backend. Het cumulatieve statistieksysteem.

Lees in het eerste vacuumrapport welke verouderde versies niet verwijderbaar blijven. Nadat de lezer de transactie heeft vastgelegd, vergelijk je het tweede rapport om te zien of die versies zijn verwijderd. In een verder rustige database leveren de drie vastgelegde vervangingen drie verouderde versies op om te onderzoeken. Andere vastgehouden horizons of gelijktijdig onderhoud kunnen het precieze rapport beïnvloeden; let daarom op het patroon voor en na, en eis geen identieke uitvoertekst.

VACUUM (VERBOSE) maakt een gedetailleerd activiteitenrapport. Het programma toont servermeldingen via het gedocumenteerde node-postgres-event notice. Beide vacuum-aanroepen worden buiten expliciete transactieblokken uitgevoerd, omdat PostgreSQL VACUUM daarin niet toestaat. PostgreSQL VACUUM, node-postgres Client API.

Gebruik dit hulpmiddel om te bepalen wat je moet controleren als het resultaat afwijkt van de verwachting:

Waarneming Volgende controle
De lezer ziet revisie 3 voordat de transactie wordt vastgelegd Controleer of Repeatable Read is gestart en de eerste artikelquery vóór de updates is uitgevoerd.
Het eerste vacuum behoudt verouderde versies Vergelijk het met de ronde na het vastleggen van de lezerstransactie; behoud zolang de snapshot openstaat is te verwachten.
Versies blijven behouden nadat de lezer de transactie heeft vastgelegd Controleer op een andere vastgehouden horizon; deze lezer beëindigen verwijdert alleen zijn bijdrage.
Een tabelstatistiek lijkt niet te kloppen Beschouw n_dead_tup als een schatting en vergelijk de vacuumrapporten.
Het tabelbestand wordt niet kleiner Controleer of de opslagruimte opnieuw bruikbaar is geworden; het verkleinen van het bestand is een afzonderlijk resultaat.

PostgreSQL beschrijft n_dead_tup uitdrukkelijk als een geschatte telling. Een geregistreerd vacuumtijdstip bevestigt ook dat onderhoud heeft plaatsgevonden, maar niet dat elke verouderde versie verwijderd kon worden. Deze velden helpen bij de diagnose, maar geen van beide vervangt het onderzoeken van de behouden snapshot en het vacuumrapport. Het cumulatieve statistieksysteem.

Gewoonlijk maakt plain vacuum ruimte binnen de tabel opnieuw bruikbaar, in plaats van die aan macOS terug te geven. Volledig vrije pagina's aan het einde vormen een gedocumenteerde uitzondering. Een onveranderde bestandsgrootte na opschoning bewijst dus niet dat de opschoning is mislukt. Regulier vacuumen.

Wat moet er in de applicatie veranderen?

Als meerdere artikelqueries dezelfde consistente databaseweergave moeten delen, beperk de transactie dan tot die queries en beëindig haar zodra ze klaar zijn. Als gevolg van snapshotbehoud voor het applicatieontwerp geldt dat je, als de taak het toelaat, ongerelateerde verwerking en interactie met de desktop uitvoert nadat die transactie is beëindigd.

Als elke query zelfstandig de nieuwste vastgelegde weergave kan gebruiken, levert Read Committed snapshots op opdrachtniveau. Het isolatieniveau wijzigen of tussen batches vastleggen verandert de consistentiesemantiek: latere queries kunnen nieuw vastgelegde artikelrevisies zien. Kies die aanpak alleen als zulke veranderende resultaten passen bij de taak van de lezer. Transactie-isolatie.

De vacuümfrequentie verhogen zorgt er niet voor dat een versie een ongewijzigde verwijderbaarheidsgrens passeert. De onnodige snapshot beëindigen neemt die bijdrage aan het behoud weg. Ook is VACUUM FULL een tabelherschrijving waarvoor een exclusieve vergrendeling en extra opslagruimte nodig zijn; het is geen gebruikelijke reactie op dit voorbeeld. Implementatie van tuplezichtbaarheid in PostgreSQL 18, PostgreSQL VACUUM.

Het script laat de synthetische tabel beschikbaar voor inspectie. Het stopt opzettelijk als die tabel al bestaat, in plaats van de bestaande inhoud te vervangen. Nadat je de tabel hebt geïnspecteerd, kun je de volgende opdracht uitvoeren in een querysessie die is verbonden met snapshot_lab als je de voorbeeldtabel wilt verwijderen. Als een uitvoering halverwege de configuratie is mislukt, inspecteer dan eerst dezelfde tabel voordat je die verwijdert en het programma opnieuw uitvoert.

DROP TABLE public.article_snapshot_demo;

De nuttige diagnostische stap is de overgang: stel een oude snapshot vast, leg verschillende vervangingen vast, voer vacuum uit, beëindig de snapshot en voer opnieuw vacuum uit. Als de oorspronkelijke revisie leesbaar blijft terwijl tussenliggende versies ook niet kunnen worden opgeschoond, wordt het onderscheid concreet. De zichtbaarheidsregels van PostgreSQL bepalen wat de query retourneert; de opschoningshorizon bepaalt wanneer verouderde opslag opnieuw kan worden gebruikt.

Bronnen