MVCC di PostgreSQL-5. Vakum dalam halaman dan pembaruan HOT

Sekadar mengingatkan Anda, kami sudah membahas masalah yang berkaitan dengan isolasi , membuat penyimpangan mengenai struktur data tingkat rendah , dan kemudian mengeksplorasi versi baris dan mengamati bagaimana snapshot data diperoleh dari versi baris.

Sekarang kita akan melanjutkan ke dua masalah yang berhubungan erat: vakum di halaman dan pembaruan HOT . Kedua teknik dapat disebut sebagai optimasi; mereka penting, tetapi sebenarnya tidak tercakup dalam dokumentasi.

Vakum dalam halaman selama pembaruan rutin


Saat mengakses halaman untuk pembaruan atau membaca, jika PostgreSQL memahami bahwa halaman tersebut kehabisan ruang, itu dapat melakukan vakum cepat di dalam halaman. Ini terjadi dalam salah satu kasus:

  1. Pembaruan sebelumnya di halaman ini tidak menemukan ruang yang cukup untuk mengalokasikan versi baris baru di halaman yang sama. Situasi seperti itu diingat di tajuk halaman, dan lain kali halaman itu disedot.
  2. Halaman lebih dari fillfactor persen penuh. Dalam hal ini, vakum dilakukan segera tanpa menunda sampai berikutnya.

fillfactor adalah parameter penyimpanan yang dapat didefinisikan untuk tabel (dan untuk indeks). PostgresSQL menyisipkan baris baru di halaman hanya jika halaman kurang dari fillfactor persen penuh. Ruang yang tersisa disediakan untuk tupel baru yang dibuat sebagai hasil dari pembaruan. Nilai default untuk tabel adalah 100, yaitu, tidak ada ruang yang dipesan (dan nilai default untuk indeks adalah 90).

Vakum dalam halaman menghapus tupel yang tidak terlihat dalam snapshot apa pun (yang berada di luar cakrawala transaksi dari basis data, yang telah dibahas terakhir kali ), tetapi melakukan hal ini secara ketat dalam satu halaman tabel. Pointer ke tupel vakum tidak dirilis karena mereka dapat dirujuk dari indeks, dan indeks ada di halaman lain. Vakum dalam halaman tidak pernah mencapai lebih dari satu halaman tabel, tetapi bekerja dengan sangat cepat.

Untuk alasan yang sama, peta ruang bebas tidak diperbarui; ini juga menyimpan ruang ekstra untuk pembaruan daripada untuk memasukkan. Peta visibilitas juga tidak diperbarui.

Fakta bahwa sebuah halaman dapat dikosongkan selama membaca berarti bahwa permintaan SELECT dapat menyebabkan perubahan halaman. Ini adalah satu lagi kasus seperti ini, di samping perubahan bit petunjuk yang ditangguhkan, dibahas sebelumnya.

Mari kita perhatikan contoh cara kerjanya. Mari kita buat tabel dan indeks pada kedua kolom.

 => CREATE TABLE hot(id integer, s char(2000)) WITH (fillfactor = 75); => CREATE INDEX hot_id ON hot(id); => CREATE INDEX hot_s ON hot(s); 

Jika kolom s hanya menyimpan karakter Latin, setiap versi baris akan menempati 2004 byte plus 24 byte header. Kami menetapkan parameter penyimpanan fillfactor menjadi 75%, yang mencadangkan ruang yang cukup untuk tiga baris.

Untuk melihat isi halaman tabel dengan mudah, mari kita buat kembali fungsi yang sudah akrab dengan menambahkan dua bidang ke output:

 => CREATE FUNCTION heap_page(relname text, pageno integer) RETURNS TABLE(ctid tid, state text, xmin text, xmax text, hhu text, hot text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, CASE WHEN (t_infomask2 & 16384) > 0 THEN 't' END AS hhu, CASE WHEN (t_infomask2 & 32768) > 0 THEN 't' END AS hot, t_ctid FROM heap_page_items(get_raw_page(relname,pageno)) ORDER BY lp; $$ LANGUAGE SQL; 

Mari kita juga membuat fungsi untuk melihat ke halaman indeks:

 => CREATE FUNCTION index_page(relname text, pageno integer) RETURNS TABLE(itemoffset smallint, ctid tid) AS $$ SELECT itemoffset, ctid FROM bt_page_items(relname,pageno); $$ LANGUAGE SQL; 

Mari kita periksa bagaimana vakum dalam halaman bekerja. Untuk melakukan ini, kami menyisipkan satu baris dan mengubahnya beberapa kali:

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; 

Ada empat tupel di halaman sekarang:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3979 (c) | 3980 (c) | | | (0,2) (0,2) | normal | 3980 (c) | 3981 (c) | | | (0,3) (0,3) | normal | 3981 (c) | 3982 | | | (0,4) (0,4) | normal | 3982 | 0 (a) | | | (0,4) (4 rows) 

Seperti yang diharapkan, kami baru saja melampaui ambang fillfactor . Ini jelas dari perbedaan antara pagesize dan nilai-nilai upper : ini melebihi ambang sama dengan 75% dari ukuran halaman, yang menghasilkan 6144 byte.

 => SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0)); 
  lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row) 

Jadi, ketika halaman diakses lain kali, kekosongan dalam halaman harus terjadi. Mari kita periksa ini.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | dead | | | | | (0,2) | dead | | | | | (0,3) | dead | | | | | (0,4) | normal | 3982 (c) | 3983 | | | (0,5) (0,5) | normal | 3983 | 0 (a) | | | (0,5) (5 rows) 

Semua tupel mati (0,1), (0,2) dan (0,3) disedot; setelah itu tuple baru (0,5) ditambahkan di ruang yang dibebaskan.

Tuple yang selamat dari penyedotan secara fisik dipindahkan ke alamat yang tinggi dari halaman sehingga semua ruang bebas diwakili oleh satu area kontinu. Nilai dari pointer diubah sesuai. Berkat ini, tidak ada masalah yang muncul dengan fragmentasi ruang kosong di halaman.

Pointer ke tupel vakum tidak dapat dilepaskan karena direferensikan dari halaman indeks. Mari kita melihat halaman pertama dari indeks hot_s (karena halaman nol ditempati oleh metainformation):

 => SELECT * FROM index_page('hot_s',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) 2 | (0,2) 3 | (0,3) 4 | (0,4) 5 | (0,5) (5 rows) 

Kami juga melihat gambar yang sama di indeks lain:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,5) 2 | (0,4) 3 | (0,3) 4 | (0,2) 5 | (0,1) (5 rows) 

Anda mungkin memperhatikan bahwa pointer ke baris tabel mengikuti di sini dalam urutan terbalik, tetapi ini tidak ada bedanya karena semua tupel memiliki nilai yang sama: id = 1. Tetapi dalam indeks sebelumnya, pointer diurutkan berdasarkan nilai s , dan ini penting.

Dengan akses indeks, PostgreSQL bisa mendapatkan (0,1), (0,2) atau (0,3) sebagai pengidentifikasi tuple. Kemudian akan mencoba untuk mendapatkan versi baris yang sesuai dari halaman tabel, tetapi karena status pointer "mati", PostgreSQL akan menemukan bahwa versi seperti itu tidak ada lagi dan akan mengabaikannya. (Sebenarnya, setelah menemukan bahwa versi baris tabel tidak tersedia, PostgreSQL akan mengubah status pointer di halaman indeks agar tidak mengakses halaman tabel lagi.)

Sangat penting bahwa vakum dalam halaman hanya berfungsi dalam satu halaman tabel dan tidak vakum halaman indeks.

Pembaruan HOT


Mengapa tidak baik menyimpan referensi ke semua versi baris dalam indeks?

Pertama, untuk setiap perubahan baris, semua indeks yang dibuat untuk tabel harus diperbarui: setelah versi baru dibuat, itu perlu direferensikan. Dan kita perlu melakukan ini dalam hal apa pun, bahkan jika bidang diubah yang tidak diindeks. Ini jelas sangat tidak efisien.

Kedua, indeks mengumpulkan referensi ke tupel historis, yang kemudian perlu disedot bersama dengan tupel itu sendiri (kita akan membahas sedikit kemudian bagaimana ini dilakukan).

Selain itu, B-tree di PostgreSQL memiliki spesifikasi implementasi. Jika halaman indeks tidak memiliki ruang yang cukup untuk menyisipkan baris baru, halaman dibagi menjadi dua, dan semua data didistribusikan di antara mereka. Ini disebut pemisahan halaman. Namun, ketika baris dihapus, dua halaman indeks tidak digabung menjadi satu. Karena itu, ukuran indeks mungkin gagal untuk mengurangi bahkan jika sebagian besar data dihapus.

Secara alami, semakin banyak indeks dibuat di atas meja, semakin banyak kompleksitas yang ditemui.

Namun, jika nilai diubah dalam kolom yang tidak diindeks sama sekali, tidak masuk akal untuk membuat baris B-tree tambahan yang berisi nilai kunci yang sama. Inilah cara optimalisasi yang disebut pembaruan PANAS ( pembaruan Heap-Only Tuple) bekerja.

Selama pembaruan ini, halaman indeks hanya berisi satu baris, yang mereferensikan versi pertama dari baris di halaman tabel. Dan sudah ada di dalam halaman tabel, bahwa rantai tupel diatur:

  • Baris yang diperbarui yang ada dalam rantai diberi label dengan bit Heap Hot Updated.
  • Baris yang tidak direferensikan dari indeks diberi label dengan Heap Only Tuple bit.
  • Seperti biasa, versi baris ditautkan melalui bidang ctid .

Jika selama pemindaian indeks, PostgreSQL mengakses halaman tabel dan menemukan tupel berlabel Heap Hot Updated, ia memahami bahwa itu tidak boleh berhenti, tetapi harus mengikuti rantai HOT, mengingat setiap tuple di dalamnya. Tentu saja, untuk semua tupel yang didapat dengan cara ini, visibilitas diperiksa sebelum mengembalikannya ke klien.

Untuk mengamati cara kerja pembaruan PANAS, mari kita hapus satu indeks dan kosongkan tabel.

 => DROP INDEX hot_s; => TRUNCATE TABLE hot; 

Sekarang kita ulangi sisipan dan perbarui satu baris.

 => INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B'; 

Dan inilah yang kita lihat di halaman tabel:

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+-------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 | t | | (0,2) (0,2) | normal | 3987 | 0 (a) | | t | (0,2) (2 rows) 

Ada rantai perubahan di halaman:

  • Bendera Heap Hot Updated menunjukkan bahwa rantai ctid harus diikuti.
  • Bendera Heap Only Tuple menunjukkan bahwa tupel ini tidak dirujuk dari indeks.

Rantai akan tumbuh (di dalam halaman) dengan perubahan lebih lanjut:

 => UPDATE hot SET s = 'C'; => UPDATE hot SET s = 'D'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+----------+----------+-----+-----+-------- (0,1) | normal | 3986 (c) | 3987 (c) | t | | (0,2) (0,2) | normal | 3987 (c) | 3988 (c) | t | t | (0,3) (0,3) | normal | 3988 (c) | 3989 | t | t | (0,4) (0,4) | normal | 3989 | 0 (a) | | t | (0,4) (4 rows) 

Tetapi hanya ada satu referensi ke kepala rantai dalam indeks:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (0,1) (1 row) 

Untuk menekankan, pembaruan HOT bekerja dalam kasus di mana bidang untuk memperbarui tidak diindeks sama sekali. Jika tidak, beberapa indeks akan berisi referensi langsung ke versi baris baru, dan ini tidak sesuai dengan konsep pengoptimalan ini.

Optimalisasi hanya berfungsi dalam satu halaman, dan karenanya, jalan tambahan melalui rantai tidak memerlukan akses ke halaman lain dan tidak mempengaruhi kinerja.

Vakum dalam halaman selama pembaruan HOT


Vakum selama pembaruan HOT adalah kasus khusus, tetapi penting dari vakum di-halaman.

Seperti sebelumnya, kami telah melampaui ambang fillfactor , sehingga pembaruan berikutnya harus menyebabkan kekosongan dalam-halaman. Tapi kali ini ada rantai pembaruan di halaman. Kepala rantai HOT ini harus selalu tetap di tempatnya semula karena direferensikan oleh indeks, sementara sisa pointer dapat dirilis: mereka diketahui tidak memiliki referensi dari luar.

Agar tidak menyentuh pointer kepala, pengalamatan tidak langsung digunakan: pointer yang dirujuk oleh indeks - (0,1) dalam kasus ini - memperoleh status "redirect", yang mengarahkan ke tuple yang sesuai.

 => UPDATE hot SET s = 'E'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | normal | 3989 (c) | 3990 | t | t | (0,2) (4 rows) 

Perhatikan bahwa:

  • Tuples (0,1), (0,2) dan (0,3) disedot pergi.
  • Header pointer (0,1) tetap, tetapi memperoleh status "redirect".
  • Versi baris yang baru ditimpa (0,2) karena tidak ada referensi untuk tuple itu, dan penunjuknya dilepaskan (status "tidak digunakan").

Mari lakukan pembaruan beberapa kali lagi:

 => UPDATE hot SET s = 'F'; => UPDATE hot SET s = 'G'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 4 | | | | | (0,2) | normal | 3990 (c) | 3991 (c) | t | t | (0,3) (0,3) | normal | 3991 (c) | 3992 | t | t | (0,5) (0,4) | normal | 3989 (c) | 3990 (c) | t | t | (0,2) (0,5) | normal | 3992 | 0 (a) | | t | (0,5) (5 rows) 

Pembaruan berikutnya menyebabkan penyedotan kembali halaman:

 => UPDATE hot SET s = 'H'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+-------+-----+-----+-------- (0,1) | redirect to 5 | | | | | (0,2) | normal | 3993 | 0 (a) | | t | (0,2) (0,3) | unused | | | | | (0,4) | unused | | | | | (0,5) | normal | 3992 (c) | 3993 | t | t | (0,2) (5 rows) 

Sekali lagi, beberapa tuple disedot pergi, dan penunjuk ke kepala rantai dipindahkan sesuai.

Kesimpulan: jika kolom yang tidak diindekskan sering diperbarui, mungkin masuk akal untuk mengurangi parameter fillfactor untuk memesan beberapa ruang halaman untuk pembaruan. Namun, kita harus memperhitungkan bahwa semakin sedikit fillfactor , semakin banyak ruang kosong yang tersisa dalam satu halaman, sehingga ukuran fisik tabel meningkat.

Istirahat dari rantai PANAS


Jika halaman tidak memiliki ruang kosong untuk mengalokasikan tuple baru, rantai akan putus. Dan kita harus membuat referensi terpisah dari indeks ke versi baris yang terletak di halaman yang berbeda.

Untuk mereproduksi situasi ini, mari kita mulai transaksi bersamaan dan buat snapshot data di dalamnya.

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot; 
 | count | ------- | 1 | (1 row) 

Snapshot tidak akan memungkinkan menyedot tupel di halaman. Sekarang mari kita lakukan pembaruan di sesi pertama:

 => UPDATE hot SET s = 'I'; => UPDATE hot SET s = 'J'; => UPDATE hot SET s = 'K'; => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 | t | t | (0,5) (0,5) | normal | 3996 | 0 (a) | | t | (0,5) (5 rows) 

Pada pembaruan berikutnya, halaman tidak akan memiliki cukup ruang, tetapi vakum di-halaman tidak akan dapat menyedot apa pun:

 => UPDATE hot SET s = 'L'; 

 | => COMMIT; -- snapshot no longer needed 

 => SELECT * FROM heap_page('hot',0); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+---------------+----------+----------+-----+-----+-------- (0,1) | redirect to 2 | | | | | (0,2) | normal | 3993 (c) | 3994 (c) | t | t | (0,3) (0,3) | normal | 3994 (c) | 3995 (c) | t | t | (0,4) (0,4) | normal | 3995 (c) | 3996 (c) | t | t | (0,5) (0,5) | normal | 3996 (c) | 3997 | | t | (1,1) (5 rows) 

Dalam tupel (0,5), ada referensi ke (1,1), yang ada di halaman 1.

 => SELECT * FROM heap_page('hot',1); 
  ctid | state | xmin | xmax | hhu | hot | t_ctid -------+--------+------+-------+-----+-----+-------- (1,1) | normal | 3997 | 0 (a) | | | (1,1) (1 row) 

Sekarang ada dua baris dalam indeks, yang masing-masing menunjuk ke awal rantai HOT-nya:

 => SELECT * FROM index_page('hot_id',1); 
  itemoffset | ctid ------------+------- 1 | (1,1) 2 | (0,1) (2 rows) 

Sayangnya, dokumentasi ini hampir tidak memiliki informasi tentang pembaruan vakum dan panas dalam-halaman, dan Anda harus mencari jawaban dalam kode sumber. Saya menyarankan Anda untuk mulai dengan README.HOT .

Baca terus .

Source: https://habr.com/ru/post/id483768/


All Articles