Database Indexing adalah penggunaan struktur data khusus yang bertujuan untuk meningkatkan kinerja, dengan mencapai akses langsung ke halaman data. Indeks basis data bekerja seperti bagian indeks buku cetak:dengan melihat di bagian indeks, akan lebih cepat untuk mengidentifikasi halaman yang berisi istilah yang kita minati. Kita dapat dengan mudah menemukan halaman dan mengaksesnya secara langsung . Ini bukan memindai halaman buku secara berurutan, sampai kita menemukan istilah yang kita cari.
Indeks adalah alat penting di tangan DBA. Menggunakan indeks dapat memberikan peningkatan kinerja yang luar biasa untuk berbagai domain data. PostgreSQL dikenal karena ekstensibilitasnya yang luar biasa dan koleksi yang kaya dari add-on inti dan pihak ketiga, dan pengindeksan tidak terkecuali untuk aturan ini. Indeks PostgreSQL mencakup spektrum kasus yang kaya, dari indeks b-tree paling sederhana pada tipe skalar hingga indeks geospasial GiST hingga fts atau json atau array indeks GIN.
Indeks, bagaimanapun, seindah kelihatannya (dan sebenarnya!) Tidak datang secara gratis. Ada hukuman tertentu yang menyertai penulisan pada tabel yang diindeks. Jadi DBA, sebelum memeriksa opsinya untuk membuat indeks tertentu, pertama-tama harus memastikan bahwa indeks tersebut masuk akal sejak awal, yang berarti bahwa keuntungan dari pembuatannya akan lebih besar daripada kerugian kinerja pada penulisan.
Terminologi Indeks Dasar PostgreSQL
Sebelum menjelaskan jenis indeks di PostgreSQL dan penggunaannya, mari kita lihat beberapa terminologi yang cepat atau lambat akan ditemukan oleh DBA mana pun saat membaca dokumen.
- Metode Akses Indeks (juga disebut sebagai Metode Akses ):Jenis indeks (B-tree, GiST, GIN, dll)
- Jenis: tipe data kolom yang diindeks
- Operator: fungsi antara dua tipe data
- Keluarga Operator: operator tipe data lintas, dengan mengelompokkan operator tipe dengan perilaku serupa
- Kelas Operator (juga disebut sebagai strategi indeks ):mendefinisikan operator yang akan digunakan oleh indeks untuk kolom
Dalam katalog sistem PostgreSQL, metode akses disimpan di pg_am, kelas operator di pg_opclass, keluarga operator di pg_opfamily. Dependensi di atas ditunjukkan pada diagram di bawah ini:
Jenis Indeks di PostgreSQL
PostgreSQL menyediakan jenis Indeks berikut:
- B-pohon: indeks default, berlaku untuk jenis yang dapat diurutkan
- Hash: menangani kesetaraan saja
- Inti: cocok untuk tipe data non-skalar (misalnya bentuk geometris, ft, array)
- SP-GiST: ruang dipartisi GIST, sebuah evolusi dari GiST untuk menangani struktur tidak seimbang (quadtrees, k-d tree, radix tree)
- GIN: cocok untuk tipe kompleks (mis. jsonb, fts, array )
- BRIN: jenis indeks yang relatif baru yang mendukung data yang dapat diurutkan dengan menyimpan nilai min/maks di setiap blok
Rendah, kami akan mencoba mengotori tangan kami dengan beberapa contoh dunia nyata. Semua contoh yang diberikan dilakukan dengan PostgreSQL 10.0 (dengan klien psql 10 dan 9) di FreeBSD 11.1.
Indeks B-tree
Misalkan kita memiliki tabel berikut:
create table part (
id serial primary key,
partno varchar(20) NOT NULL UNIQUE,
partname varchar(80) NOT NULL,
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
Table "public.part"
Column | Type | Modifiers
------------+-----------------------+---------------------------------------------------
id | integer | not null default nextval('part_id_seq'::regclass)
partno | character varying(20)| not null
partname | character varying(80)| not null
partdescr | text |
machine_id | integer | not null
Indexes:
"part_pkey" PRIMARY KEY, btree (id)
"part_partno_key" UNIQUE CONSTRAINT, btree (partno)
Saat kita mendefinisikan tabel yang agak umum ini, PostgreSQL membuat dua indeks B-tree yang unik di belakang layar:part_pkey dan part_partno_key. Jadi setiap batasan unik di PostgreSQL diimplementasikan dengan INDEX unik. Mari kita isi tabel kita dengan sejuta baris data:
testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000
Sekarang mari kita coba melakukan beberapa pertanyaan di meja kita. Pertama, kami memberi tahu klien psql untuk melaporkan waktu kueri dengan mengetik \timing:
testdb=# select * from part where id=100000;
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,319 ms
Kami mengamati bahwa hanya dibutuhkan sepersekian milidetik untuk mendapatkan hasil kami. Kami mengharapkan ini karena untuk kedua kolom yang digunakan dalam kueri di atas, kami telah menentukan indeks yang sesuai. Sekarang mari kita coba query pada nama bagian kolom, yang tidak ada indeksnya.
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 89,173 ms
Di sini kita melihat dengan jelas bahwa untuk kolom yang tidak diindeks, kinerjanya turun secara signifikan. Sekarang mari kita buat indeks pada kolom itu, dan ulangi kuerinya:
testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,525 ms
Indeks part_partname_idx kami yang baru juga merupakan indeks B-tree (default). Pertama, kami mencatat bahwa pembuatan indeks pada tabel juta baris membutuhkan banyak waktu, sekitar 16 detik. Kemudian kami mengamati bahwa kecepatan kueri kami ditingkatkan dari 89 ms menjadi 0,525 ms. Indeks B-tree, selain memeriksa kesetaraan, juga dapat membantu kueri yang melibatkan operator lain pada tipe yang diurutkan, seperti <,<=,>=,>. Mari kita coba dengan <=dan>=
testdb=# select count(*) from part where partname>='Part 9999900';
count
-------
9
(1 row)
Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
count
--------
999991
(1 row)
Time: 355,618 ms
Permintaan pertama jauh lebih cepat daripada yang kedua, dengan menggunakan kata kunci EXPLAIN (atau EXPLAIN ANALYZE) kita dapat melihat apakah indeks yang sebenarnya digunakan atau tidak:
testdb=# explain select count(*) from part where partname>='Part 9999900';
QUERY PLAN
-----------------------------------------------------------------------------------------
Aggregate (cost=8.45..8.46 rows=1 width=8)
-> Index Only Scan using part_partname_idx on part (cost=0.42..8.44 rows=1 width=0)
Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)
Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
QUERY PLAN
----------------------------------------------------------------------------------------
Finalize Aggregate (cost=14603.22..14603.23 rows=1 width=8)
-> Gather (cost=14603.00..14603.21 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=13603.00..13603.01 rows=1 width=8)
-> Parallel Seq Scan on part (cost=0.00..12561.33 rows=416667 width=0)
Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)
Time: 0,461 ms
Dalam kasus pertama, perencana kueri memilih untuk menggunakan indeks part_partname_idx. Kami juga mengamati bahwa ini akan menghasilkan pemindaian indeks saja yang berarti tidak ada akses ke tabel data sama sekali. Dalam kasus kedua, perencana menentukan bahwa tidak ada gunanya menggunakan indeks karena hasil yang dikembalikan adalah sebagian besar tabel, dalam hal ini pemindaian berurutan dianggap lebih cepat.
Indeks Hash
Penggunaan indeks hash hingga dan termasuk PgSQL 9.6 tidak disarankan karena alasan yang berkaitan dengan kurangnya penulisan WAL. Pada PgSQL 10.0 masalah tersebut telah diperbaiki, tetapi indeks hash masih tidak masuk akal untuk digunakan. Ada upaya dalam PgSQL 11 untuk menjadikan indeks hash sebagai metode indeks kelas satu bersama dengan saudara-saudaranya yang lebih besar (B-tree, GiST, GIN). Jadi, dengan mengingat hal ini, mari kita coba menerapkan indeks hash.
Kami akan memperkaya tabel bagian kami dengan tipe kolom kolom baru dan mengisinya dengan nilai distribusi yang sama, lalu menjalankan kueri yang menguji tipe bagian yang sama dengan 'Kemudi':
testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 93,361 ms
Sekarang kita membuat indeks Hash untuk kolom baru ini, dan mencoba lagi kueri sebelumnya:
testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 63,634 ms
Kami mencatat peningkatan setelah menggunakan indeks hash. Sekarang kita akan membandingkan kinerja indeks hash pada bilangan bulat dengan indeks b-tree yang setara.
testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,297 ms
Seperti yang kita lihat, dengan penggunaan indeks hash, kecepatan kueri yang memeriksa kesetaraan sangat dekat dengan kecepatan indeks B-tree. Indeks hash dikatakan sedikit lebih cepat untuk kesetaraan daripada B-tree, sebenarnya kami harus mencoba setiap kueri dua atau tiga kali hingga indeks hash memberikan hasil yang lebih baik daripada yang setara dengan b-tree.
Unduh Whitepaper Hari Ini Pengelolaan &Otomatisasi PostgreSQL dengan ClusterControlPelajari tentang apa yang perlu Anda ketahui untuk menerapkan, memantau, mengelola, dan menskalakan PostgreSQLUnduh WhitepaperIndeks GiST
GiST (Generalized Search Tree) lebih dari satu jenis indeks, melainkan infrastruktur untuk membangun banyak strategi pengindeksan. Distribusi PostgreSQL default menyediakan dukungan untuk tipe data geometris, tsquery, dan tsvector. Dalam contrib ada implementasi dari banyak kelas operator lainnya. Dengan membaca dokumen dan direktori contrib, pembaca akan mengamati bahwa ada tumpang tindih yang agak besar antara kasus penggunaan GiST dan GIN:array int, pencarian teks lengkap untuk memberi nama kasus utama. Dalam kasus itu, GIN lebih cepat, dan dokumentasi resmi secara eksplisit menyatakan itu. Namun, GiST menyediakan dukungan tipe data geometris yang luas. Juga, pada saat penulisan ini, GiST (dan SP-GiST) adalah satu-satunya metode bermakna yang dapat digunakan dengan batasan pengecualian. Kita akan melihat contoh tentang ini. Mari kita misalkan (tetap di bidang teknik mesin) bahwa kita memiliki persyaratan untuk menentukan variasi jenis mesin untuk jenis mesin tertentu, yang berlaku untuk jangka waktu tertentu; dan bahwa untuk variasi tertentu, tidak ada variasi lain untuk jenis mesin yang sama yang periode waktunya tumpang tindih (bertentangan) dengan periode variasi tertentu.
create table machine_type (
id SERIAL PRIMARY KEY,
mtname varchar(50) not null,
mtvar varchar(20) not null,
start_date date not null,
end_date date,
CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);
Di atas kami memberi tahu PostgreSQL bahwa untuk setiap nama jenis mesin (mtname) hanya ada satu variasi (mtvar). Start_date menunjukkan tanggal mulai periode di mana variasi jenis mesin ini valid, dan end_date menunjukkan tanggal akhir periode ini. Null end_date berarti variasi jenis mesin saat ini valid. Sekarang kami ingin mengungkapkan persyaratan yang tidak tumpang tindih dengan kendala. Cara melakukannya adalah dengan batasan pengecualian:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
Sintaks EXCLUDE PostgreSQL memungkinkan kita untuk menentukan banyak kolom dengan tipe yang berbeda dan dengan operator yang berbeda untuk masing-masing kolom. &&adalah operator yang tumpang tindih untuk rentang tanggal, dan =adalah operator persamaan umum untuk varchar. Tapi selama kita menekan enter PostgreSQL mengeluh dengan pesan:
ERROR: data type character varying has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
Apa yang kurang di sini adalah dukungan opclass GiST untuk varchar. Asalkan kami telah berhasil membuat dan memasang ekstensi btree_gist, kami dapat melanjutkan dengan membuat ekstensi:
testdb=# create extension btree_gist ;
CREATE EXTENSION
Dan kemudian mencoba kembali untuk membuat batasan dan mengujinya:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).
Indeks SP-GiST
SP-GiST yang merupakan singkatan dari space-partitioned GiST, seperti GiST, adalah infrastruktur yang memungkinkan pengembangan berbagai strategi dalam domain struktur data berbasis disk yang tidak seimbang. Distribusi PgSQL default menawarkan dukungan untuk titik dua dimensi, rentang (tipe apa pun), teks, dan tipe inet. Seperti GiST, SP-GiST dapat digunakan dalam batasan pengecualian, dengan cara yang mirip dengan contoh yang ditunjukkan pada bab sebelumnya.
Indeks GIN
GIN (Generalized Inverted Index) seperti GiST dan SP-GiST dapat memberikan banyak strategi pengindeksan. GIN cocok ketika kita ingin mengindeks kolom tipe komposit. Distribusi PostgreSQL default menyediakan dukungan untuk semua jenis array, jsonb, dan pencarian teks lengkap (tsvector). Dalam contrib ada implementasi dari banyak kelas operator lainnya. Jsonb, fitur PostgreSQL yang sangat dipuji (dan pengembangan yang relatif baru (9.4+)) mengandalkan GIN untuk dukungan indeks. Penggunaan umum lainnya dari GIN adalah pengindeksan untuk pencarian teks lengkap. Pencarian teks lengkap di PgSQL layak mendapatkan artikel tersendiri, jadi kami hanya akan membahas bagian pengindeksan di sini. Pertama mari kita buat beberapa persiapan untuk tabel kita, dengan memberikan nilai not null ke kolom partdescr dan memperbarui satu baris dengan nilai yang berarti:
testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms
Kemudian kami melakukan pencarian teks pada kolom yang baru diperbarui:
testdb=# select * from part where partdescr @@ 'thermostat';
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 2015,690 ms (00:02,016)
Ini cukup lambat, hampir 2 detik untuk membawa hasil kami. Sekarang mari kita coba membuat indeks GIN pada tipe tsvector, dan ulangi kuerinya, menggunakan sintaks yang ramah indeks:
testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 0,952 ms
Dan kami mendapatkan kecepatan 2000 kali lipat. Kami juga dapat mencatat waktu yang relatif singkat yang dibutuhkan indeks untuk dibuat. Anda dapat bereksperimen dengan menggunakan GiST sebagai ganti GIN dalam contoh di atas, dan mengukur kinerja pembacaan, penulisan, dan pembuatan indeks untuk kedua metode akses.
Indeks BRIN
BRIN (Block Range Index) adalah tambahan terbaru untuk set jenis indeks PostgreSQL, sejak diperkenalkan di PostgreSQL 9.5, hanya memiliki beberapa tahun sebagai fitur inti standar. BRIN bekerja pada tabel yang sangat besar dengan menyimpan informasi ringkasan untuk satu set halaman yang disebut "Block Range". Indeks BRIN bersifat lossy (seperti GiST) dan ini membutuhkan logika ekstra dalam eksekutor kueri PostgreSQL, dan juga kebutuhan akan pemeliharaan ekstra. Mari kita lihat aksi BRIN:
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 5,461 ms
Di sini kita melihat rata-rata peningkatan ~ 18 kali lipat dengan menggunakan indeks BRIN. Namun, rumah asli BRIN adalah dalam domain data besar, jadi kami berharap untuk menguji teknologi yang relatif baru ini dalam skenario dunia nyata di masa mendatang.