Indeks

Indeks rentang, constraint keunikan, dan menghentikan pemindaian seluruh graf.

Daftar isi

Kueri yang menyaring node berdasarkan properti berjalan lambat apabila mesin harus memeriksa seluruh isi graf. Topik ini membahas indeks pada Neo4j, yaitu struktur tambahan yang membuat pencarian properti tidak lagi menyentuh node yang tidak relevan. Lingkupnya mencakup alasan pemindaian seluruh graf mahal, pembuatan indeks rentang (range index) dengan CREATE INDEX ... IF NOT EXISTS, constraint keunikan yang sekaligus berlaku sebagai indeks, pemeriksaan daftar indeks melalui SHOW INDEXES, serta syarat predikat agar indeks benar-benar digunakan oleh planner, yaitu komponen yang menyusun rencana eksekusi kueri. Seluruh contoh dijalankan pada dataset 10000 artikel, dan perbedaan biayanya dibuktikan melalui angka db hits pada PROFILE.

Dataset contoh#

Seluruh contoh pada topik ini menggunakan label :Artikel yang berisi 10000 node artikel. Setiap node membawa properti slug yang unik, properti judul, serta properti tahun yang menyebar pada rentang 2015 sampai 2024. Tiga node :Tag bernama graf, basis-data, dan cypher berperan sebagai penanda topik, dan relasi :BERTAG menghubungkan setiap artikel bernomor kelipatan 100 dengan tepat satu tag sehingga jumlah relasinya 100. Dataset sebesar ini dipilih agar perbedaan biaya antara pemindaian dan pencarian indeks tampak pada angka, bukan pada kesan.

Dataset dimuat dari berkas setup.cypher berikut. Skripnya idempoten (idempotent), artinya dijalankan berulang kali pun keadaan akhir grafnya tetap sama. Sifat itu dicapai dengan dua keputusan. Pertama, constraint keunikan pada properti slug dipasang lebih dahulu, sehingga setiap pencocokan MERGE didukung oleh indeks dan slug ganda mustahil terbentuk meskipun penulisan berlangsung serentak. Kedua, pembuatan node menggunakan pola MERGE per baris alih-alih CREATE, karena CREATE yang dijalankan ulang akan melempar error pelanggaran constraint, sedangkan MERGE menemukan node yang sudah ada lalu menuliskan properti yang sama.

cypher setup.cypher
// Dataset modul Performa: 10000 artikel untuk pengujian indeks.
// Skrip ini idempoten, artinya aman dijalankan lebih dari satu kali.

// Constraint keunikan dipasang lebih dahulu agar setiap pencocokan MERGE
// di bawahnya didukung indeks dan slug ganda mustahil terbentuk.
CREATE CONSTRAINT artikel_slug_m6 IF NOT EXISTS
  FOR (a:Artikel) REQUIRE a.slug IS UNIQUE;

// 10000 artikel dengan slug, judul, dan tahun terbit yang deterministik.
UNWIND range(1, 10000) AS n
MERGE (a:Artikel {slug: 'artikel-' + n})
SET a.judul = 'Artikel ' + n, a.tahun = 2015 + (n % 10);

// Tiga tag, masing-masing menempel pada artikel bernomor kelipatan 100.
UNWIND range(1, 100) AS m
MERGE (a:Artikel {slug: 'artikel-' + (m * 100)})
MERGE (t:Tag {name: ['graf', 'basis-data', 'cypher'][(m - 1) % 3]})
MERGE (a)-[:BERTAG]->(t);
bash
# tidak-dijalankan
# memuat dataset modul performa, kata sandi diminta interaktif
cypher-shell -a bolt://localhost:7687 -u neo4j -f setup.cypher

Kueri berikut memverifikasi isi dataset, yaitu jumlah artikel beserta sebaran relasi tag per namanya.

cypher
MATCH (a:Artikel) RETURN count(a) AS artikel;
MATCH (t:Tag)<-[r:BERTAG]-(:Artikel)
RETURN t.name AS tag, count(r) AS jumlah
ORDER BY tag;
text Keluaran
artikel
10000
tag, jumlah
"basis-data", 33
"cypher", 33
"graf", 34

Keluaran menegaskan 10000 artikel dengan sebaran tag 33, 33, dan 34, sesuai pola perulangannya.

Pemindaian seluruh graf#

Tanpa indeks, satu-satunya cara bagi mesin untuk menjawab predikat pada properti adalah memeriksa setiap node berlabel :Artikel satu per satu. Pola eksekusi ini disebut pemindaian label, yaitu seluruh node dengan label yang diminta diambil terlebih dahulu, kemudian properti pada setiap node diperiksa terhadap predikatnya. Biayanya tumbuh linier terhadap jumlah node, sehingga pencarian satu artikel pada 10000 node menuntut 10000 kali pemeriksaan, dan beban itu berulang pada setiap eksekusi kueri. Karena itu, kueri yang cepat pada data kecil dapat melambat seiring data bertambah, meskipun perintahnya tidak berubah.

Kueri berikut mencari satu artikel dari properti judul, lalu dijalankan dengan PROFILE, yaitu kata kunci yang membuat mesin melaporkan rencana eksekusi beserta biayanya dalam satuan db hits, yakni hitungan operasi baca dan tulis yang dikerjakan mesin.

cypher
PROFILE MATCH (a:Artikel)
WHERE a.judul = 'Artikel 7777'
RETURN a.slug AS slug;
text Keluaran
slug
"artikel-7777"
Plan: "PROFILE"
Statement: "READ_ONLY"
Version: "25"
Planner: "COST"
Runtime: "SLOTTED"
Time: 106
DbHits: 20002
Rows: 1
Memory (Bytes): 64

Angka DbHits bernilai 20002 untuk hasil satu baris. Nilai itu mencerminkan pemindaian seluruh 10000 node berlabel :Artikel ditambah pemeriksaan properti judul pada setiap node yang dipindai. Pembacaan rencana baris demi baris dibahas pada topik rencana kueri, sehingga bagian ini cukup membandingkan angka DbHits antarkueri.

Membuat indeks rentang#

Indeks rentang adalah struktur terurut yang menyimpan nilai sebuah properti untuk seluruh node berlabel tertentu, dan merupakan indeks bawaan Neo4j untuk predikat umum. Karena nilainya tersusun rapi, mesin dapat melompat langsung ke nilai yang dicari tanpa membaca sisanya. Pernyataan berikut membuat indeks rentang pada properti judul untuk label :Artikel, kemudian menampilkan indeks milik modul ini melalui SHOW INDEXES.

cypher
CREATE INDEX artikel_judul_m6 IF NOT EXISTS
  FOR (a:Artikel) ON (a.judul);
SHOW INDEXES YIELD name, type, entityType
WHERE name IN ['artikel_slug_m6', 'artikel_judul_m6']
RETURN name, type
ORDER BY name;
text Keluaran
name, type
"artikel_judul_m6", "RANGE"
"artikel_slug_m6", "RANGE"

Klausa IF NOT EXISTS membuat pernyataan itu aman dijalankan ulang, karena indeks dengan definisi sama tidak dibuat ulang dan tidak memicu error. Keluaran memuat dua baris. Baris pertama adalah indeks artikel_judul_m6 yang baru saja dibuat, sedangkan baris kedua adalah artikel_slug_m6, yaitu indeks yang hadir bersama constraint keunikan pada setup dan dibahas pada bagian akhir topik ini. Keduanya bertipe RANGE dan menautkan label dengan satu properti.

Kapan planner menggunakan indeks#

Planner hanya memilih indeks apabila predikat pada kueri dapat dilayani oleh indeks tersebut. Predikat kesamaan nilai penuh seperti a.judul = 'Artikel 7777' termasuk yang dilayani oleh indeks rentang, demikian pula perbandingan <, <=, >, >=, serta pencarian awalan dengan STARTS WITH. Kueri yang sama dengan bagian sebelumnya kini dijalankan ulang dengan PROFILE.

cypher
PROFILE MATCH (a:Artikel)
WHERE a.judul = 'Artikel 7777'
RETURN a.slug AS slug;
text Keluaran
slug
"artikel-7777"
Plan: "PROFILE"
Statement: "READ_ONLY"
Version: "25"
Planner: "COST"
Runtime: "SLOTTED"
Time: 73
DbHits: 3
Rows: 1
Memory (Bytes): 64

DbHits turun dari 20002 menjadi 3. Mesin tidak lagi membaca 10000 node, melainkan melompat ke satu posisi pada indeks, kemudian membaca node yang dirujuknya. Perilaku berbeda muncul pada predikat akhiran. Operator ENDS WITH mencari string yang berakhiran teks tertentu, dan indeks rentang tidak dapat menjawabnya karena struktur terurut hanya membantu pencarian dari sisi awal nilai. Kueri berikut menggunakan ENDS WITH dengan teks pencarian yang sama.

cypher
PROFILE MATCH (a:Artikel)
WHERE a.judul ENDS WITH '7777'
RETURN a.slug AS slug;
text Keluaran
slug
"artikel-7777"
Plan: "PROFILE"
Statement: "READ_ONLY"
Version: "25"
Planner: "COST"
Runtime: "SLOTTED"
Time: 48
DbHits: 10002
Rows: 1
Memory (Bytes): 64

Angka DbHits bernilai 10002 karena planner meninggalkan indeks dan memindai seluruh label :Artikel dari awal. Perilaku serupa berlaku pada operator CONTAINS. Tabel berikut merangkum ketiga pengukuran pada dataset yang sama.

KondisiPredikatDbHits
Tanpa indeksa.judul = 'Artikel 7777'20002
Dengan indeks rentanga.judul = 'Artikel 7777'3
Dengan indeks rentanga.judul ENDS WITH '7777'10002

Constraint keunikan sebagai indeks#

Constraint keunikan (uniqueness constraint) adalah aturan mesin yang menjamin tidak ada dua node berlabel sama membawa nilai properti yang sama. Pernyataan berikut memasangnya pada properti slug. Pernyataan ini sudah dijalankan saat setup, dan klausa IF NOT EXISTS membuatnya aman dijalankan ulang tanpa memicu error.

cypher
CREATE CONSTRAINT artikel_slug_m6 IF NOT EXISTS
  FOR (a:Artikel) REQUIRE a.slug IS UNIQUE;
SHOW CONSTRAINTS YIELD name, type
WHERE name = 'artikel_slug_m6'
RETURN name, type;
text Keluaran
name, type
"artikel_slug_m6", "NODE_PROPERTY_UNIQUENESS"

Setiap penulisan yang melanggar aturan itu ditolak oleh mesin dengan pesan error, sehingga duplikat slug tidak pernah lolos masuk ke graf. Selain menegakkan aturan, constraint keunikan juga memasang indeks pendamping yang namanya sama dengan constraintnya, sehingga pencarian pada properti yang sama otomatis didukung oleh indeks. Kueri berikut menampilkan kedua indeks milik modul ini beserta pemiliknya melalui kolom owningConstraint.

cypher
SHOW INDEXES YIELD name, type, entityType, owningConstraint
WHERE name IN ['artikel_slug_m6', 'artikel_judul_m6']
RETURN name, type, entityType, owningConstraint
ORDER BY name;
text Keluaran
name, type, entityType, owningConstraint
"artikel_judul_m6", "RANGE", "NODE", NULL
"artikel_slug_m6", "RANGE", "NODE", "artikel_slug_m6"

Baris artikel_slug_m6 menunjukkan owningConstraint bernilai artikel_slug_m6, artinya indeks itu dimiliki oleh constraint keunikan. Nilai NULL pada baris artikel_judul_m6 menandakan indeks yang berdiri sendiri. Untuk membuktikan indeks tersebut bekerja, pencarian berdasarkan slug dijalankan dengan PROFILE.

cypher
PROFILE MATCH (a:Artikel)
WHERE a.slug = 'artikel-7777'
RETURN a.judul AS judul;
text Keluaran
judul
"Artikel 7777"
Plan: "PROFILE"
Statement: "READ_ONLY"
Version: "25"
Planner: "COST"
Runtime: "SLOTTED"
Time: 28
DbHits: 3
Rows: 1
Memory (Bytes): 64

DbHits bernilai 3, setara dengan pencarian melalui indeks rentang biasa. Satu constraint keunikan dengan demikian memberi dua manfaat sekaligus, yaitu jaminan keunikan data pada setiap penulisan dan percepatan pencarian pada setiap pembacaan. Karena itu, properti kunci seperti slug, alamat surel, atau nomor induk lazim diberi constraint keunikan alih-alih indeks biasa.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat topik ini dipraktikkan.

  1. Mengharapkan indeks rentang melayani ENDS WITH atau CONTAINS. Kedua operator itu tidak dilayani oleh struktur terurut, sehingga planner memindai seluruh label sebagaimana terlihat pada DbHits 10002. Perbaikannya adalah menuliskan predikat yang dilayani indeks rentang, misalnya kesamaan nilai atau STARTS WITH, atau menggunakan indeks teks untuk kebutuhan pola string yang lebih bebas.
  2. Menulis CREATE INDEX tanpa IF NOT EXISTS pada skrip yang dijalankan ulang. Eksekusi kedua berhenti dengan error karena indeks dengan definisi yang sama sudah ada sejak eksekusi pertama. Perbaikannya adalah menambahkan klausa IF NOT EXISTS sehingga pernyataan yang sama aman pada setiap eksekusi.
  3. Membuat indeks untuk properti yang tidak pernah difilter. Setiap indeks menambah pekerjaan tulis pada setiap penulisan node, sedangkan manfaat bacanya tidak pernah terpakai. Perbaikannya adalah membuat indeks hanya untuk properti yang memang menjadi predikat kueri, kemudian memeriksa pemanfaatannya dengan PROFILE pada kueri yang ada.

Ringkasan#

  • Tanpa indeks, predikat pada properti diselesaikan dengan memindai seluruh node berlabel terkait, sehingga pencarian satu artikel pada 10000 node mencatat 20002 db hits.
  • Indeks rentang menyusun nilai properti secara terurut, sehingga predikat kesamaan, perbandingan, dan STARTS WITH diselesaikan dengan beberapa db hits alih-alih pemindaian penuh.
  • Indeks hanya digunakan oleh planner apabila predikatnya dilayani, sedangkan ENDS WITH dan CONTAINS membuat planner kembali memindai seluruh label, dan PROFILE memperlihatkannya melalui lonjakan DbHits.
  • Constraint keunikan menolak nilai ganda pada setiap penulisan sekaligus memasang indeks pendamping bernama sama, dan daftar indeks diperiksa melalui SHOW INDEXES.

Referensi#