Biarkan saya mengingatkan Anda bahwa kami telah memeriksa masalah yang berkaitan dengan
isolasi , membuat penyimpangan tentang
pengorganisasian data pada tingkat rendah , dan kemudian berbicara secara rinci
tentang versi baris dan bagaimana
snapshot diperoleh dari versi.
Hari ini kita akan membahas dua masalah yang agak berkaitan:
pembersihan intra-halaman dan
pembaruan-HOT . Kedua mekanisme dapat diklasifikasikan sebagai optimisasi; mereka penting, tetapi hampir tidak tercakup dalam dokumentasi pengguna.
Pembersihan dalam halaman dengan pembaruan rutin
Saat mengakses halaman - baik saat memperbarui maupun saat membaca - pembersihan intra-halaman cepat dapat terjadi jika PostgreSQL memahami bahwa halaman tersebut kehabisan ruang. Ini terjadi dalam dua kasus.
- Pembaruan yang sebelumnya dilakukan pada halaman ini (PEMBARUAN) tidak menemukan cukup ruang untuk menempatkan versi baru dari baris pada halaman yang sama. Situasi ini diingat dalam judul halaman, dan saat berikutnya halaman dihapus.
- Halaman diisi lebih dari pada fillfactor. Dalam hal ini, pembersihan terjadi segera, tanpa menunda waktu berikutnya.
Fillfactor adalah parameter penyimpanan yang dapat didefinisikan untuk tabel (dan untuk indeks). PostgreSQL menyisipkan baris baru (INSERT) pada halaman hanya jika halaman ini kurang dari mengisi persen atau penuh. Ruang yang tersisa dicadangkan untuk versi string baru yang dihasilkan dari pembaruan (PEMBARUAN). Nilai default untuk tabel adalah 100, yaitu, ruang tidak dicadangkan (dan nilai untuk indeks adalah 90).
Pembersihan intra-halaman menghapus versi baris yang tidak terlihat dalam gambar apa pun (terletak di luar "horizon peristiwa" dari basis data, kami membicarakan ini
terakhir kali ), tetapi ini bekerja secara ketat di dalam halaman tabel yang sama. Pointer ke versi string yang digosok tidak dibebaskan, karena mereka dapat dirujuk dari indeks, dan indeks adalah halaman lain. Pembersihan dalam halaman tidak pernah melampaui satu halaman tabular, tetapi sangat cepat.
Untuk alasan yang sama, peta ruang bebas tidak diperbarui; itu juga menghemat ruang untuk pembaruan, bukan untuk sisipan. Peta visibilitas juga tidak diperbarui.
Fakta bahwa sebuah halaman dapat dihapus saat membaca berarti bahwa permintaan baca (SELECT) dapat menyebabkan halaman berubah. Ini adalah kasus lain, di samping perubahan bit petunjuk yang sebelumnya ditangguhkan.
Mari kita lihat bagaimana ini bekerja, menggunakan contoh. 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 hanya huruf latin yang disimpan dalam kolom s, maka setiap versi baris akan menempati 2004 byte plus 24 byte header. Kami mengatur parameter penyimpanan fillfactor ke 75% - akan ada ruang yang cukup untuk tiga baris.
Untuk kenyamanan, kami membuat kembali fungsi yang sudah akrab, melengkapi output dengan dua bidang:
=> 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;
Dan mari kita buat fungsi untuk melihat ke dalam 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;
Kami akan memeriksa cara kerja pembersihan intra-halaman. Untuk melakukan ini, masukkan satu baris dan ubah 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 versi baris di halaman:
=> 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 ditunjukkan oleh perbedaan antara pageize dan nilai-nilai atas: melebihi ambang batas 75% dari ukuran halaman, yaitu 6144 byte.
=> SELECT lower, upper, pagesize FROM page_header(get_raw_page('hot',0));
lower | upper | pagesize -------+-------+---------- 40 | 64 | 8192 (1 row)
Jadi, saat berikutnya Anda mengakses halaman, pembersihan dalam halaman akan terjadi. Lihat 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 versi garis yang tidak relevan (0,1), (0,2) dan (0,3) dihapus; setelah itu, versi baru dari garis (0,5) ditambahkan ke ruang kosong.
Versi garis yang tersisa setelah pembersihan secara fisik bergeser ke sisi alamat halaman senior sehingga semua ruang kosong diwakili oleh satu fragmen berkelanjutan. Nilai pointer berubah sesuai. Berkat ini, tidak ada masalah dengan fragmentasi ruang kosong di halaman.
Pointer ke versi string yang dihapus tidak dapat dibebaskan karena direferensikan dari halaman indeks. Mari kita lihat halaman pertama indeks hot_s (karena nol sibuk dengan informasi-meta):
=> 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)
Kita akan 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 dapat melihat bahwa pointer ke baris tabel pergi ke sini "mundur", tetapi itu tidak masalah, karena di semua versi baris nilai yang sama adalah id = 1. Tetapi dalam indeks sebelumnya pointer diarahkan oleh nilai s, dan ini secara substansial.
Dengan akses indeks, PostgreSQL bisa mendapatkan (0,1), (0,2), atau (0,3) sebagai pengidentifikasi versi baris. Kemudian dia akan mencoba untuk mendapatkan baris yang sesuai dari halaman tabel, tetapi berkat status mati dari pointer, dia akan menemukan bahwa versi seperti itu tidak ada lagi dan akan mengabaikannya. (Faktanya, pertama kali mendeteksi kekurangan versi dari baris tabel, PostgreSQL juga akan mengubah status pointer di halaman indeks sehingga tidak mengakses halaman tabel lagi.)
Penting bahwa pembersihan intra-halaman hanya berfungsi dalam satu halaman tabular dan tidak menghapus halaman indeks.
Pembaruan HOT
Mengapa buruk menyimpan tautan ke semua versi string dalam indeks?
Pertama, dengan perubahan baris apa pun, Anda harus memperbarui semua indeks yang dibuat untuk tabel: karena versi baru telah muncul, Anda harus memiliki tautan ke sana. Dan Anda perlu melakukan ini dalam hal apa pun, bahkan jika bidang yang tidak termasuk dalam indeks berubah. Jelas, ini tidak terlalu efektif.
Kedua, indeks mengumpulkan tautan ke versi historis dari string, yang kemudian harus dihapus bersama dengan versi itu sendiri (kita akan melihat ini sedikit kemudian).
Selain itu, ada fitur implementasi B-tree di PostgreSQL. Jika tidak ada cukup ruang pada halaman indeks untuk memasukkan baris baru, halaman dibagi menjadi dua dan semua data didistribusikan kembali di antara mereka. Ini disebut halaman terpisah. Namun, saat menghapus baris, dua halaman indeks tidak lagi "menyatu" menjadi satu. Karena itu, ukuran indeks mungkin tidak berkurang bahkan jika sebagian besar data dihapus.
Secara alami, semakin banyak indeks dibuat di atas meja, semakin besar kesulitan yang harus Anda hadapi.
Namun, jika nilai kolom yang bukan milik perubahan indeks, maka tidak ada gunanya membuat catatan tambahan di B-tree yang berisi nilai kunci yang sama. Ini adalah cara kerja pengoptimalan, yang disebut pembaruan HOT - Pembaruan Tuple Hanya Heap-Only.
Dengan pembaruan ini, hanya ada satu entri di halaman indeks yang merujuk ke versi paling pertama dari baris di halaman tabel. Dan sudah ada di dalam halaman tabel ini, serangkaian versi disusun:
- string yang diubah dan dimasukkan dalam rantai ditandai dengan bit Heap Hot Updated;
- baris yang tidak direferensikan dari indeks ditandai dengan bit Heap Only Tuple (yaitu, "hanya versi tabular dari baris");
- tautan reguler versi string melalui bidang ctid didukung.
Jika, ketika memindai indeks, PostgreSQL masuk ke halaman tabel dan menemukan versi yang ditandai sebagai Heap Hot Updated, ia mengerti bahwa itu tidak perlu berhenti dan melangkah lebih jauh di sepanjang seluruh rantai pembaruan. Tentu saja, untuk semua versi string yang diperoleh dengan cara ini, visibilitas diperiksa sebelum dikembalikan ke klien.
Untuk melihat operasi pembaruan HOT, hapus satu indeks dan kosongkan tabel.
=> DROP INDEX hot_s; => TRUNCATE TABLE hot;
Ulangi sisipan dan perbarui baris.
=> INSERT INTO hot VALUES (1, 'A'); => UPDATE hot SET s = 'B';
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)
Di halaman ada rantai perubahan:
- bendera Heap Hot Updated menunjukkan bahwa Anda harus mengikuti rantai ctid,
- bendera Heap Only Tuple menunjukkan bahwa tidak ada tautan indeks ke versi baris ini.
Dengan perubahan lebih lanjut, rantai akan tumbuh (di dalam halaman):
=> 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)
Selain itu, dalam indeks ada satu referensi tunggal ke "kepala" rantai:
=> SELECT * FROM index_page('hot_id',1);
itemoffset | ctid ------------+------- 1 | (0,1) (1 row)
Kami menekankan bahwa pembaruan PANAS berfungsi jika bidang yang diperbarui tidak termasuk dalam indeks apa pun. Jika tidak, dalam beberapa indeks akan ada tautan langsung ke versi baru dari string, yang bertentangan dengan gagasan optimasi ini.
Optimasi hanya berfungsi dalam batas satu halaman, oleh karena itu, bypass tambahan rantai tidak memerlukan akses ke halaman lain dan tidak mengganggu kinerja.
Pembersihan dalam halaman dengan pembaruan HOT
Kasus pembersihan intra-halaman yang khusus namun penting adalah pembersihan selama pembaruan HOT.
Seperti yang terakhir kali, kami sudah melampaui ambang fillfactor, sehingga pembaruan berikutnya akan mengarah pada pembersihan dalam halaman. Tapi kali ini di halaman adalah rantai pembaruan. "Kepala" rantai PANAS ini harus selalu tetap di tempatnya, karena indeks merujuk padanya, dan sisa petunjuk dapat dibebaskan: diketahui bahwa mereka tidak dirujuk dari luar.
Agar tidak menyentuh "head", pengalamatan ganda digunakan: pointer yang merujuk indeks - dalam hal ini (0,1) - menerima status "redirect", mengarahkan ke versi string yang diinginkan.
=> 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)
Harap dicatat bahwa:
- versi (0,1), (0,2) dan (0,3) dihapus,
- Header pointer (0,1) tetap, tetapi menerima status redirect,
- versi baru dari baris ditulis di tempat (0.2), karena versi ini dijamin tidak memiliki tautan dari indeks dan penunjuk dibebaskan (tidak digunakan).
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 berikut lagi menyebabkan pembersihan intra-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 versi dihapus, dan penunjuk ke "head" dengan demikian digeser.
Kesimpulan: dengan seringnya pembaruan ke kolom di luar indeks, mungkin masuk akal untuk mengurangi parameter fillfactor untuk menyimpan ruang pada halaman untuk pembaruan. Tentu saja, kita harus memperhitungkan bahwa semakin rendah fillfactor, semakin banyak ruang yang tidak terisi tetap pada halaman dan, dengan demikian, ukuran fisik tabel meningkat.
Rantai putus panas
Jika tidak ada cukup ruang kosong pada halaman untuk mengirim versi baru dari sebuah baris, rantai akan terputus. Versi baris yang diposting di halaman lain harus membuat tautan terpisah dari indeks.
Untuk mendapatkan situasi ini, kami memulai transaksi paralel dan membuat snapshot data di dalamnya.
| => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT count(*) FROM hot;
| count | ------- | 1 | (1 row)
Cuplikan tidak akan menghapus versi garis pada halaman. Sekarang kami melakukan 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)
Lain kali halaman di-refresh, tidak akan ada cukup ruang di halaman, tetapi pembersihan di halaman tidak akan bisa membebaskan apa pun:
=> UPDATE hot SET s = 'L';
| => COMMIT;
=> 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 versi (0,5) kita melihat tautan ke (1.1) yang mengarah ke 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, informasi tentang pembersihan dalam-halaman dan pembaruan-HOT praktis tidak ada dalam dokumentasi, dan kebenaran harus dicari dalam kode sumber. Saya sarankan memulai dengan README.HOT .
Untuk dilanjutkan .