Tutup Indeks untuk GiST

Indeks penutup bukan hanya fitur lain yang mungkin berguna. Hal ini murni praktis. Tanpa mereka, Index Only Scan tidak akan memberikan kemenangan. Meskipun indeks penutup dalam situasi yang berbeda efektif dengan cara yang berbeda.

Ini sebenarnya bukan tentang membahas indeks: sebenarnya, indeks inklusif telah muncul di Postgres. Namun, secara berurutan: indeks penutup adalah indeks yang berisi semua nilai kolom yang diperlukan oleh kueri; Namun, akses ke tabel itu sendiri tidak lagi diperlukan. Hampir. Anda dapat membaca tentang "hampir" dan nuansa lain dalam sebuah artikel oleh Yegor Rogov , termasuk dalam seri indeksnya 10 (!) Bagian. Dan indeks inklusif dibuat khusus untuk pencarian pada permintaan tipikal: nilai bidang yang tidak dapat dicari ditambahkan ke indeks pencarian, mereka diperlukan hanya agar tidak merujuk ke tabel lagi. Indeks tersebut dibentuk dengan kata kunci TERMASUK.

Anastasia Lubennikova (Postgres Professional) menyelesaikan metode btree sehingga kolom tambahan dapat dimasukkan dalam indeks. Patch ini dimasukkan dalam PostgreSQL 11. Tetapi patch untuk metode akses GiST / SP-GiST tidak punya waktu untuk matang sebelum rilis versi ini. Pada 12 GiST matang.

Keinginan konstruktif untuk memiliki indeks inklusif untuk GiST muncul sejak lama: tes tempel oleh Andrey Borodin ditawarkan kepada masyarakat pada pertengahan April 2018. Dia melakukan semua pekerjaan dasar, sangat sulit.

Pada awal Agustus 2019, Alexander Korotkov menambahkan perbaikan kosmetik dan melakukan perbaikan.

Untuk demonstrasi dan penelitian, kami akan menghasilkan satu set 3 juta persegi panjang. Pada saat yang sama beberapa kata tentang tipe kotak, karena tidak semua manipulasi dengan itu adalah intuitif.

Jenis kotak - yaitu, persegi panjang - telah lama berada di Postgres, didefinisikan oleh 2 titik (titik tipe geometris) - simpul yang berlawanan dari persegi panjang (yaitu, persegi panjang tidak dapat miring, berserakan ke samping). Kita membaca dalam dokumentasi : โ€œnilai-nilai kotak jenis ditulis dalam salah satu bentuk berikut:

( ( x1 , y1 ) , ( x2 , y2 ) ) ( x1 , y1 ) , ( x2 , y2 ) x1 , y1 , x2 , y2 

Dalam praktiknya, Anda harus menulis, katakanlah, seperti ini:

 SELECT box('1,2', '3,4'); box ------------- (3,4),(1,2) (1 row) 

Pertama, Postgres menunjukkan verteks kanan atas, lalu kiri bawah. Jika kita menulis seperti ini,

 SELECT box('5,2', '3,4'); box ------------- (5,4),(3,2) (1 row) 

maka kami akan memastikan bahwa Postgres tidak memberikan puncak yang mereka berikan kepadanya. Dia menghitung kanan atas dan kiri bawah dari kiri atas dan kanan bawah kami. Ini adalah properti yang nyaman ketika lokasi simpul tidak diketahui sebelumnya - dalam kasus generasi acak, misalnya. Notasi '1,2', '3,4' setara dengan poin (1,2), poin (3,4). Bentuk ini terkadang lebih nyaman.


Untuk bisnis: cari dalam 3 juta persegi panjang


 CREATE TABLE boxes(id serial, thebox box, name text); 

Kami akan menghasilkan 3 juta persegi panjang acak. Kami ingin distribusi normal, tetapi agar tidak menggunakan ekstensi tablefunc , kami menggunakan pendekatan "miskin": kami menggunakan acak () - acak (), yang juga memberikan gambar yang bagus (lihat gambar). Dengan persegi panjang, semakin besar semakin besar semakin dekat ke pusat. Pusat gravitasi mereka juga acak. Distribusi semacam itu adalah karakteristik dari beberapa jenis data kota nyata. Dan mereka yang ingin mempelajari hukum statistik atau ingatan yang menyegarkan dapat membaca tentang perbedaan variabel acak, misalnya di sini .




 INSERT INTO boxes(thebox, name) SELECT box( point( random()-random(), random()-random() ), point( random()-random(), random()-random() ) ), 'box no.' || x FROM generate_series(1,3000000) AS g(x); 


Ukuran tabel yang menunjukkan \dt+ adalah 242MB. Sekarang Anda dapat memulai pencarian.

Kami mencari tanpa indeks:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------------- Gather (cost=1000.00..47853.00 rows=3000 width=46) (actual time=0.140..246.998 rows=139189 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on boxes (cost=0.00..46553.00 rows=1250 width=46) (actual time=0.011..106.708 rows=46396 loops=3) Filter: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Rows Removed by Filter: 953604 Planning Time: 0.040 ms Execution Time: 259.262 ms (8 rows) 

Kami melihat bahwa ada Pemindaian Seq Paralel - pemindaian sekuensial (meskipun diparalelkan).

Buat indeks reguler dan non-inklusif:

 CREATE INDEX ON boxes USING gist(thebox); 

Ukuran indeks boxes_thebox_idx , yang menunjukkan \di+ , 262MB. Menanggapi permintaan yang sama, kami mendapatkan:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on boxes (cost=159.66..9033.30 rows=3000 width=46) (actual time=29.101..80.283 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_idx (cost=0.00..158.91 rows=3000 width=0) (actual time=25.029..25.029 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 0.053 ms Execution Time: 86.206 ms (7 rows) 

Waktu pencarian dikurangi dengan faktor tiga dan bukannya Pemindaian Seq Paralel, mereka menerima Pemindaian Indeks Bitmap. Itu tidak sejajar, tetapi bekerja lebih cepat.

Sekarang bunuh indeks lama dan buat yang inklusif:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); 

Indeks boxes_thebox_name_idx fatter: 356MB. Ayo pergi:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on boxes (cost=207.66..9081.30 rows=3000 width=46) (actual time=86.822..152.014 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_name_idx (cost=0.00..206.91 rows=3000 width=0) (actual time=83.044..83.044 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 3.807 ms Execution Time: 157.997 ms (7 rows) 


Index Only Scan digunakan, tetapi gambarnya menyedihkan: waktunya hampir 2 kali lebih lama daripada tanpa itu. Kita membaca buku pegangan pembuat indeks, di bagian I :

โ€นRang PostgreSQL indeks tidak mengandung informasi yang memungkinkan Anda menilai visibilitas baris. Oleh karena itu, metode akses mengembalikan semua versi baris yang termasuk dalam kondisi pencarian, terlepas dari apakah mereka terlihat oleh transaksi saat ini atau tidak. Namun, jika mekanisme pengindeksan harus melihat ke tabel setiap kali untuk menentukan visibilitas, metode pemindaian ini tidak akan berbeda dari pemindaian indeks biasa. Masalahnya dipecahkan oleh fakta bahwa PostgreSQL mendukung apa yang disebut peta visibilitas untuk tabel, di mana proses vakum menandai halaman di mana data tidak berubah cukup lama untuk semua transaksi untuk melihatnya, terlepas dari waktu mulai dan tingkat isolasi. Jika pengidentifikasi baris yang dikembalikan oleh indeks merujuk ke halaman seperti itu, maka visibilitas tidak dapat diperiksa. โ€บโ€บ

Kami melakukan VACUUM. Ulangi:

 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Index Only Scan using boxes_thebox_name_idx on boxes (cost=0.41..236.91 rows=3000 width=46) (actual time=0.104..38.651 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Fetches: 0 Planning Time: 0.052 ms Execution Time: 44.337 ms (5 rows) 

Masalah yang sama sekali berbeda! Dua kali lipat dibandingkan dengan indeks non-inklusif.


Selektivitas dan perolehan


Kinerja indeks inklusif sangat tergantung pada selektivitas kondisi dalam kueri. Untuk menyelidiki sedikit ketergantungan ini, kami akan memecahkan masalah terbalik: kami akan menghasilkan label dengan indeks titik ketik dan kami akan mencari berapa banyak poin yang akan jatuh dalam kotak yang diberikan. Sebarkan titik-titik kuadrat secara merata.

 CREATE TABLE test_covergist(id serial, tochka point, name text); 

 INSERT INTO test_covergist(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,3000000) AS g(x); 

Ukuran tabel adalah 211 MB.

 CREATE INDEX on test_covergist USING gist(tochka); 

Ukuran 213 MB.

Kami jelas akan mengambil semua poin yang tersedia menjadi kotak:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1087.964..1864.059 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared read=54287 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1084.949..1084.949 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared read=27262 Planning Time: 0.102 ms Execution Time: 2029.501 ms (9 rows) 

Kami meminta EXPLAIN untuk menunjukkan buffer. Ini akan berguna. Sekarang waktu eksekusi permintaan lebih dari 2 detik, dapat dilihat bahwa Buffer: shared read = 54287. Dalam situasi lain, kita bisa melihat campuran dari read read dan shared hit - yaitu, beberapa buffer dibaca dari disk (atau dari cache OS), dan beberapa dari cache buffer. Kami tahu perkiraan ukuran tabel dan indeks, jadi kami akan melindungi diri sendiri dengan mengatur buffer bersama sehingga semuanya cocok - restart Postgres dengan opsi

 -o "-c shared_buffers=1GB" 

Sekarang:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=231.032..613.326 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared hit=54248 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=228.068..228.068 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=27223 Planning Time: 0.070 ms Execution Time: 755.915 ms (9 rows) 

Artinya, baca bersama menjadi hit bersama, dan waktu dikurangi tiga kali.

Detail penting lainnya dalam MENJELASKAN: 3 juta poin dikembalikan, dan perkiraan jumlah rekaman yang dikembalikan adalah 3.000 Spoiler: angka ini tidak akan berubah dengan selektivitas apa pun. Pengoptimal tidak tahu bagaimana mengevaluasi kardinalitas ketika bekerja dengan tipe kotak atau titik. Dan rencananya tidak akan berubah: untuk ukuran persegi panjang apa pun, akan ada Pemindaian Indeks Bitmap di test_covergist_tochka_idx.

Berikut adalah dua pengukuran lagi dengan jumlah catatan yang dikeluarkan, berbeda dengan urutan besarnya:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=27.889..134.054 rows=269882 loops=1) Recheck Cond: ('(300000,300000),(0,0)'::box @> tochka) Heap Blocks: exact=27024 Buffers: shared hit=29534 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=24.847..24.847 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Buffers: shared hit=2510 Planning Time: 0.074 ms Execution Time: 151.269 ms (9 rows) 

Ini mengembalikan 10-aneh kali lebih sedikit catatan (aktual ... baris = 26.988), waktu telah berkurang sekitar 5 kali.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','30000,30000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1.882..16.095 rows=2780 loops=1) Recheck Cond: ('(30000,30000),(0,0)'::box @> tochka) Heap Blocks: exact=2624 Buffers: shared hit=2655 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1.035..1.035 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Buffers: shared hit=31 Planning Time: 0.154 ms Execution Time: 16.702 ms (9 rows) 

Isi persegi 30K ร— 30K (2780) dihitung hanya dalam 16 ms. Dan ketika ada lusinan catatan, mereka sudah diekstraksi dalam fraksi ms, dan pengukuran seperti itu tidak terlalu bisa diandalkan.

Terakhir, ukur hal yang sama dengan indeks inklusif:

 CREATE INDEX on test_covergist USING gist(tochka) INCLUDE(name); 

Ukuran 316 MB.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.160..568.707 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=40492 Planning Time: 0.090 ms Execution Time: 709.837 ms (6 rows) 

Waktunya hampir sama dengan indeks konvensional, meskipun Index Only Scan.

Tapi:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.083..53.277 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=3735 Planning Time: 0.077 ms Execution Time: 66.162 ms (6 rows) 

Dan itu 151 ms. Dan, sesuai:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.043..0.639 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=52 Planning Time: 0.053 ms Execution Time: 0.791 ms (6 rows) 

Ini sudah sebagian kecil dari ms untuk catatan 2780 titik yang sama.

Buffer menyukai senjata


Penjelasan dapat dicari dan ditemukan di senapan yang belum menembak tetapi yang tergantung di dinding: jumlah blok dibaca. Dalam kasus indeks inklusif, hanya blok indeks itu sendiri yang dibaca (Heap Fetches: 0). Dalam tiga kasus, ini adalah angka 40492, 3735, dan 52. Tetapi ketika menggunakan indeks reguler, blok yang dibaca terdiri dari jumlah bit yang dibaca dalam indeks Bitmap Heap Scan (54248 dengan 3 juta catatan) dan yang harus dibaca dari heap (27223) , karena bidang nama tidak dapat diekstraksi dari indeks reguler. 54248 + 27223 = 81471. Eksklusif adalah 40492. Untuk dua kasus lain: 29534 + 2510 = 31044 dan 2655 + 31 = 2686. Dalam kasus indeks reguler, bagaimanapun, lebih banyak blok dibaca, tetapi dengan peningkatan dalam selektivitas, jumlah blok yang dibaca mulai berbeda dengan urutan besarnya daripada 2 kali karena fakta bahwa jumlah blok yang diperlukan dari tumpukan menurun lebih lambat daripada membaca blok indeks.

Total catatan yang dikembalikan (ribuan)30002702.7
Baca blok (Normal / Inklusif)81471/4049231044/37352686/52
Waktu755/710151/6616 / 0.7


Tapi mungkin intinya bukan selektivitas sama sekali, tetapi hanya ukuran tabel? Untuk berjaga-jaga, kami mengulangi langkah yang sama, menghasilkan tabel dengan 300 ribu, dan bukan 3 juta catatan:

 CREATE TABLE test_covergist_small(id serial, tochka point, name text); INSERT INTO test_covergist_small(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,300000) AS g(x); CREATE INDEX ON test_covergist_small USING gist(tochka); EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist_small WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist_small (cost=14.61..867.19 rows=300 width=31) (actual time=36.115..130.435 rows=300000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=2500 Buffers: shared hit=5225 -> Bitmap Index Scan on test_covergist_small_tochka_idx (cost=0.00..14.53 rows=300 width=0) (actual time=35.894..35.895 rows=300000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=2725 Planning Time: 0.060 ms Execution Time: 158.580 (9 rows) 

Selanjutnya, ulangi yang sama untuk indeks inklusif. Inilah hasilnya:

Total catatan yang dikembalikan (ribuan)300270,25
Baca blok (Normal / Inklusif)5225/37263026/352270/8
Waktu158/17820/130,4 / 0,2


Dalam hal cakupan 100% poin, kueri itu bahkan sedikit lebih lambat daripada dengan indeks biasa. Selanjutnya, seperti dalam kasus 3 juta, semuanya jatuh ke tempatnya. Artinya, selektivitas itu penting.

Perusahaan kami menguji indeks GiST inklusif pada data riil - satu set dengan beberapa juta persegi panjang pada peta Moskow. Kesimpulannya sama: dalam banyak situasi, indeks seperti itu secara nyata mempercepat permintaan. Tetapi artikel tersebut tidak dapat diilustrasikan dengan gambar dan jumlah tes: data ini tidak ada dalam domain publik.

Alih-alih sebuah kesimpulan


Mari kita kembali sejenak ke persegi panjang acak. Mari kita coba melakukan hal yang sama dengan spgist. Anda dapat mengingat atau mencari tahu apa artinya memahami perbedaan antara SP-GiST dan GiST dengan membaca artikel Indeks di PostgreSQL - 6 . Buat indeks inklusif:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); ERROR: access method "spgist" does not support included columns 

Sayangnya, untuk SP-GiST, indeks inklusif belum diterapkan.
Jadi ada ruang untuk perbaikan!



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


All Articles