Snapshot PostgreSQL yang lama dapat menyimpan versi baris perantara yang tidak pernah dibacanya. Vacuum menggunakan horizon transaksi yang konservatif untuk menentukan apa yang dapat dihapus. Panduan ini menjelaskan alasannya, lalu mereproduksi perilakunya dengan baris artikel sintetis dan node-postgres di Mac.

Tugasnya adalah membedakan snapshot yang menahan pembersihan dari vacuum yang belum dijalankan. Contoh ini ditujukan untuk PostgreSQL 18 dan menggunakan dua koneksi basis data: satu pembaca yang menahan snapshot Repeatable Read dan satu penulis yang memperbarui satu artikel. Dokumentasi PostgreSQL saat ini menyebut versi 18 sebagai versi mayor yang didukung saat ini; detail implementasi di bawah secara eksplisit mengacu pada cabang 18. Dokumentasi PostgreSQL 18.

Mengapa menyimpan versi yang tidak dapat dilihat pembaca lama?

Pembaruan membuat versi baris baru. PostgreSQL menyimpan versi sebelumnya selama transaksi lain mungkin masih membutuhkannya. Pada akhirnya, vacuum dapat mengambil kembali versi yang sudah usang dan membuat ruang penyimpanannya dapat digunakan kembali. Inilah siklus hidup MVCC dasar yang dijelaskan dalam Routine Vacuuming.

Hal rumitnya, visibilitas snapshot dan kemungkinan penghapusan fisik menjawab pertanyaan yang berbeda. Visibilitas menanyakan apakah kueri tertentu seharusnya mengembalikan suatu versi. Pembersihan menggunakan batas konservatif, bukan membuktikan untuk setiap versi snapshot mana yang ada saat ini yang dapat membacanya.

Di PostgreSQL 18, HeapTupleSatisfiesVacuum membandingkan transaksi yang membuat suatu versi menjadi usang dengan OldestXmin. Penghapusan atau penggantian yang sudah di-commit tidak langsung membuat versi sebelumnya dapat dihapus: versi yang transaksi penghapusnya berada pada atau setelah batas tersebut tetap diklasifikasikan sebagai baru saja mati. Aturan ini didokumentasikan langsung dalam implementasi visibilitas tuple resmi.

Perhatikan urutan ilustratif ini. Setiap pembaruan di-commit secara terpisah, dan tidak ada pembaca lain yang terlibat:

Langkah Tindakan Yang dilihat pembaca lama Versi terbaru yang sudah di-commit
1 Masukkan revisi artikel 0 — Revisi 0
2 Pembaca membuat snapshot Repeatable Read Revisi 0 Revisi 0
3 Penulis meng-commit revisi 1 Revisi 0 Revisi 1
4 Penulis meng-commit revisi 2 Revisi 0 Revisi 2
5 Penulis meng-commit revisi 3 Revisi 0 Revisi 3

Revisi 0 dibutuhkan secara langsung oleh pembaca. Revisi 1 dan 2 adalah versi perantara: pembaca ini tidak dapat melihatnya karena versi tersebut dibuat setelah snapshot-nya. Meskipun demikian, transaksi yang menggantikannya juga lebih baru daripada batas pembersihan yang dipertahankan. Penerapan aturan batas yang didokumentasikan menjelaskan mengapa versi-versi ini dapat tetap tersimpan bersama revisi 0. Penyimpanannya tidak berarti pembaca dapat menelusurinya sebagai hasil kueri historis. Implementasi visibilitas tuple PostgreSQL 18.

Horizon ini cakupannya lebih luas daripada baris yang dipilih pembaca. PostgreSQL 18 menghitung horizon yang sesuai untuk relasi berdasarkan ID transaksi backend dan horizon snapshot; data tabel biasa umumnya memperhitungkan sesi dalam basis data yang sama. PostgreSQL tidak hanya melacak baris yang dikembalikan oleh SELECT tertentu. Inilah sebabnya kueri kecil dapat memengaruhi pembersihan di bagian lain dalam basis data tersebut. Relasi bersama dan jenis relasi lain memiliki aturan tersendiri, jadi pernyataan “satu snapshot menghentikan semua vacuum di mana-mana” terlalu luas. Implementasi horizon PostgreSQL 18.

Transaksi mana yang sebenarnya menahan snapshot lama?

Repeatable Read menetapkan snapshot pada pernyataan pertama yang bukan kendali transaksi, lalu menggunakan snapshot itu untuk kueri berikutnya. Sekadar menjalankan BEGIN tidak cukup untuk demonstrasi ini; kueri artikel pertama menetapkan tampilan yang harus dipertahankan. Read Committed, tingkat isolasi default PostgreSQL, mengambil snapshot baru untuk setiap perintah. Isolasi Transaksi.

Akibatnya, transaksi Repeatable Read yang tidak aktif tetapi sudah mengkueri tabel dapat mempertahankan snapshot-nya setelah kueri selesai. Pernyataan Read Committed yang berjalan lama juga dapat mempertahankan snapshot selama eksekusi, tetapi pernyataan-pernyataan terpisah yang sudah selesai pada tingkat ini tidak berbagi tampilan tetap yang sama. Karena itu, durasi transaksi saja tidak menunjukkan snapshot mana yang dipertahankan. Isolasi Transaksi.

Transaksi hanya-baca dapat berkontribusi pada horizon meskipun tidak memiliki ID transaksi tingkat atas sendiri. Implementasi mempertimbangkan ID transaksi backend sekaligus horizon snapshot. Dalam contoh ini, backend_xid yang null bersama backend_xmin yang tidak null konsisten dengan pembaca yang mempertahankan snapshot. Implementasi horizon PostgreSQL 18.

Mereproduksi perilaku dari Terminal Mac

Anda memerlukan server PostgreSQL 18 lokal yang berjalan, alat baris perintahnya, serta Node.js dengan npm. Node.js 20 atau 24 tercantum dalam dokumentasi kompatibilitas node-postgres. Panduan ini mengasumsikan pengaturan koneksi lokal yang sudah ada berfungsi. Panduan ini membuat basis data terpisah dan hanya menggunakan teks artikel sintetis. Pengenalan dan pemasangan node-postgres.

Di Terminal, buat direktori kerja, pasang driver, dan buat basis data contoh:

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

Simpan program lengkap di bawah sebagai snapshot-demo.cjs. Koneksinya mewarisi pengaturan lingkungan PostgreSQL, termasuk PGDATABASE, PGHOST, dan PGPORT, sebagaimana didokumentasikan di Menghubungkan dengan node-postgres. Gunakan pengaturan koneksi lokal yang sama untuk createdb dan program tersebut.

Pembaca mempertahankan semua pernyataan transaksi pada satu klien khusus. node-postgres mewajibkan hal ini: transaksi harus menggunakan klien yang sama dari awal hingga akhir, jadi mengirim pernyataannya melalui pool.query() tidak cocok. Klien kedua menjalankan pembaruan dan perintah vacuum secara terpisah. Transaksi node-postgres.

Contoh ini juga mengindeks revision, yang berubah pada setiap pembaruan. Dengan sengaja, hal ini membuat pembaruan tersebut tidak dapat menggunakan HOT sehingga demonstrasinya lebih mudah ditafsirkan. PostgreSQL mendokumentasikan bahwa HOT mensyaratkan pembaruan tidak mengubah kolom yang dirujuk oleh indeks biasa, serta tersedianya ruang yang cukup pada halaman asal. 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;
});

Jalankan dengan memilih basis data secara eksplisit:

PGDATABASE=snapshot_lab node snapshot-demo.cjs

Sintaks JavaScript program ini diperiksa dengan Node.js. Urutan basis datanya ditelaah berdasarkan dokumentasi yang ditemukan; urutan tersebut tidak dijalankan pada server PostgreSQL untuk artikel ini.

Cara menafsirkan dua tahap vacuum

Hasil kueri yang diharapkan adalah revisi 0 untuk Reader before, revisi 3 untuk Writer latest, dan revisi 0 lagi untuk Reader still. Setelah pembaca melakukan commit, kueri mandirinya berikutnya seharusnya mengembalikan revisi 3. Ini adalah hasil yang diharapkan dari urutan sintetis tersebut, bukan keluaran uji yang direkam.

Hasil aktivitas seharusnya menunjukkan pembaca dalam status idle in transaction saat penulis memeriksanya, dengan backend_xmin yang dipertahankan. PostgreSQL mendefinisikan status tersebut sebagai transaksi yang menunggu perintah lain, dan backend_xmin sebagai horizon saat ini milik backend. Sistem Statistik Kumulatif.

Baca laporan vacuum pertama untuk melihat versi usang yang masih belum dapat dihapus. Setelah pembaca melakukan commit, bandingkan laporan kedua untuk melihat penghapusan versi-versi tersebut. Dalam basis data yang tidak mengalami aktivitas lain, tiga penggantian yang sudah di-commit menyediakan tiga versi usang untuk diperiksa. Horizon lain yang masih dipertahankan atau pemeliharaan serentak dapat memengaruhi laporan persisnya, jadi gunakan pola sebelum dan sesudah, bukan mengharuskan teks keluaran yang identik.

VACUUM (VERBOSE) menghasilkan laporan aktivitas terperinci. Program mencetak pemberitahuan server melalui event notice node-postgres yang terdokumentasi. Kedua pemanggilan vacuum berjalan di luar blok transaksi eksplisit karena PostgreSQL tidak mengizinkan VACUUM di dalamnya. VACUUM PostgreSQL, API Klien node-postgres.

Gunakan panduan keputusan ini jika hasilnya berbeda dari perkiraan:

Pengamatan Pemeriksaan berikutnya
Pembaca melihat revisi 3 sebelum melakukan commit Pastikan Repeatable Read dimulai dan kueri artikel pertama dijalankan sebelum pembaruan.
Vacuum pertama mempertahankan versi usang Bandingkan dengan tahap setelah pembaca melakukan commit; versi yang tetap tersimpan saat snapshot terbuka adalah hal yang diharapkan.
Versi tetap ada setelah pembaca melakukan commit Periksa apakah ada horizon lain yang masih dipertahankan; mengakhiri transaksi pembaca ini hanya menghilangkan kontribusinya.
Jumlah statistik tabel tampak tidak konsisten Anggap n_dead_tup sebagai perkiraan dan bandingkan laporan vacuum.
Berkas tabel tidak mengecil Periksa apakah ruang penyimpanan menjadi dapat digunakan kembali; penyusutan berkas adalah hasil yang terpisah.

PostgreSQL secara eksplisit menjelaskan n_dead_tup sebagai jumlah perkiraan. Waktu vacuum yang tercatat juga memastikan bahwa pemeliharaan telah berjalan, tetapi tidak membuktikan bahwa setiap versi usang dapat dihapus. Kolom-kolom ini membantu diagnosis, tetapi tidak satu pun menggantikan pemeriksaan snapshot yang dipertahankan dan laporan vacuum. Sistem Statistik Kumulatif.

Vacuum biasa umumnya membuat ruang dapat digunakan kembali di dalam tabel, alih-alih mengembalikannya ke macOS. Halaman kosong sepenuhnya di bagian akhir merupakan pengecualian yang terdokumentasi. Karena itu, ukuran berkas yang tidak berubah setelah pembersihan tidak membuktikan bahwa pembersihan gagal. Routine Vacuuming.

Apa yang perlu diubah di aplikasi?

Jika beberapa kueri artikel harus berbagi tampilan basis data yang konsisten, batasi transaksi pada kueri-kueri tersebut dan akhiri saat semuanya selesai. Sebagai konsekuensi desain aplikasi dari dipertahankannya snapshot, lakukan pemrosesan yang tidak terkait dan interaksi desktop setelah transaksi berakhir, jika tugasnya memungkinkan.

Jika setiap kueri dapat menggunakan tampilan terbaru yang sudah di-commit secara terpisah, Read Committed menyediakan snapshot pada tingkat perintah. Mengubah isolasi atau melakukan commit di antara kelompok kueri mengubah semantik konsistensi: kueri berikutnya mungkin melihat revisi artikel yang baru di-commit. Pilih pendekatan itu hanya jika hasil yang berubah sesuai dengan kebutuhan pembaca. Isolasi Transaksi.

Meningkatkan frekuensi vacuum tidak dapat membuat suatu versi melewati batas penghapusan yang tidak berubah. Mengakhiri snapshot yang tidak diperlukan menangani kontribusinya terhadap penyimpanan versi. Demikian pula, VACUUM FULL menulis ulang tabel dan memerlukan kunci eksklusif, serta membutuhkan ruang tambahan; perintah ini bukan respons rutin untuk contoh ini. Implementasi visibilitas tuple PostgreSQL 18, VACUUM PostgreSQL.

Skrip ini membiarkan tabel sintetisnya tersedia untuk diperiksa. Skrip sengaja gagal jika tabel tersebut sudah ada, alih-alih mengganti isinya. Setelah memeriksanya, jalankan perintah berikut dalam sesi kueri yang terhubung ke snapshot_lab jika Anda ingin menghapus tabel contoh. Jika proses gagal di tengah penyiapan, periksa tabel yang sama sebelum menghapusnya dan menjalankan ulang skrip.

DROP TABLE public.article_snapshot_demo;

Diagnosis yang berguna terletak pada peralihannya: buat snapshot lama, commit beberapa penggantian, jalankan vacuum, akhiri snapshot, lalu jalankan vacuum lagi. Melihat revisi asli tetap dapat dibaca sementara versi perantara juga sulit dibersihkan membuat perbedaannya menjadi konkret. Aturan visibilitas PostgreSQL menentukan hasil yang dikembalikan kueri; horizon pembersihannya menentukan kapan ruang penyimpanan usang dapat digunakan kembali.

Sumber