OPTIONAL MATCH

Padanan LEFT JOIN pada graf dan penanganan null yang menyertainya.

Daftar isi

Klausa OPTIONAL MATCH pada Cypher membuat kegagalan pencocokan tidak menghilangkan baris, melainkan mengisinya dengan nilai null. Topik ini membahas perilaku tersebut sebagai padanan LEFT JOIN pada graf, mencakup perbedaan hasil antara MATCH biasa dan OPTIONAL MATCH, posisi WHERE yang menempel pada masing-masing klausa, serta penanganan null dengan predikat IS NULL dan fungsi coalesce(). Kasus yang digunakan sepanjang topik adalah daftar orang beserta hobinya pada dataset jaringan sosial modul ini, dengan lima dari tujuh orang belum memiliki hobi.

Dataset jaringan sosial#

Seluruh contoh pada modul ini menggunakan satu dataset kecil yang sama, yaitu jaringan sosial berisi tujuh node berlabel :Person dengan properti name dan kota, ditambah dua node :Hobi bernama Fotografi dan Bersepeda. Relasi :KENAL menghubungkan orang-orang tersebut dalam beberapa rantai pertemanan berarah, sedangkan relasi :PUNYA_HOBI hanya menghubungkan Hana ke Fotografi serta Fajar ke Bersepeda. Ketimpangan inilah bahan utama topik ini, karena lima orang lainnya tidak memiliki relasi hobi sama sekali dan pencocokan pada bagian graf itu akan gagal secara wajar.

Skrip setup.cypher berikut membangun seluruh dataset dengan MERGE, sehingga aman dijalankan berulang tanpa menduplikasi data. Isinya identik dengan skrip dataset pada topik lain di modul ini, sehingga keluaran antartopik dapat dibandingkan secara langsung. Cara termudah memuat skrip tersebut adalah mengalirkannya ke cypher-shell melalui pipa, atau mengeksekusinya dari Neo4j Browser, dengan kata sandi yang diminta interaktif. Setelah skrip dijalankan, dataset yang sama sudah siap digunakan untuk seluruh kueri pada topik ini.

cypher setup.cypher
// Dataset modul Pola kueri: jaringan sosial dengan relasi KENAL dan hobi.
// Skrip ini idempoten, sehingga aman dijalankan lebih dari satu kali.

// Tujuh orang beserta kota asalnya
MERGE (p:Person {name: 'Eko'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Fajar'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Gita'}) SET p.kota = 'Semarang';
MERGE (p:Person {name: 'Hana'}) SET p.kota = 'Surabaya';
MERGE (p:Person {name: 'Iwan'}) SET p.kota = 'Semarang';
MERGE (p:Person {name: 'Jihan'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Rara'}) SET p.kota = 'Yogyakarta';

// Dua hobi
MERGE (h:Hobi {name: 'Fotografi'});
MERGE (h:Hobi {name: 'Bersepeda'});

// Rantai KENAL, kedua ujung diikat variabel agar MERGE hanya menangani relasinya
MATCH (a:Person {name: 'Eko'}), (b:Person {name: 'Fajar'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Person {name: 'Gita'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Gita'}), (b:Person {name: 'Hana'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Eko'}), (b:Person {name: 'Iwan'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Iwan'}), (b:Person {name: 'Hana'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Gita'}), (b:Person {name: 'Jihan'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Person {name: 'Rara'}) MERGE (a)-[:KENAL]->(b);

// Hobi hanya dimiliki sebagian orang, sehingga null muncul secara alami
MATCH (a:Person {name: 'Hana'}), (b:Hobi {name: 'Fotografi'}) MERGE (a)-[:PUNYA_HOBI]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Hobi {name: 'Bersepeda'}) MERGE (a)-[:PUNYA_HOBI]->(b);
bash
# tidak-dijalankan
# memuat dataset melalui cypher-shell, kata sandi diminta interaktif
cypher-shell -a bolt://localhost:7687 -u neo4j -f setup.cypher

OPTIONAL MATCH sebagai padanan LEFT JOIN#

Klausa MATCH mengembalikan satu baris untuk setiap pola yang ditemukan, dan node yang polanya tidak cocok tidak pernah muncul di dalam hasil. Kueri berikut meminta nama orang beserta hobinya melalui relasi :PUNYA_HOBI. Karena hanya dua orang yang memiliki hobi, keluarannya hanya berisi dua baris dan lima orang lainnya tidak ikut tampil.

cypher
MATCH (p:Person)-[:PUNYA_HOBI]->(h:Hobi)
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Fajar", "Bersepeda"
"Hana", "Fotografi"

Perilaku tersebut setara dengan inner join pada SQL, yaitu hanya pasangan lengkap yang dilaporkan. Untuk memperoleh perilaku LEFT JOIN, yaitu baris sisi kiri tetap keluar meskipun sisi kanannya tidak menemukan pasangan, Cypher menyediakan klausa OPTIONAL MATCH. Apabila pola opsional tidak ditemukan, variabel pada pola itu diisi null, yakni nilai khusus yang menyatakan ketiadaan data, dan barisnya tetap dikembalikan. Kueri berikut hanya menambahkan satu kata kunci pada kueri sebelumnya, tetapi hasilnya berubah dari dua baris menjadi tujuh baris.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Eko", NULL
"Fajar", "Bersepeda"
"Gita", NULL
"Hana", "Fotografi"
"Iwan", NULL
"Jihan", NULL
"Rara", NULL

Keluaran kini memuat satu baris untuk setiap orang, dan kolom hobi bernilai NULL bagi Eko, Gita, Iwan, Jihan, serta Rara. Daftar orang dengan atau tanpa hobi terjawab dalam satu kueri tanpa kueri susulan, dan identitas orang yang tidak memiliki hobi tetap terjaga. Data dari sisi kiri juga tidak hilang, sebab kolom orang terisi lengkap pada semua baris.

Posisi WHERE pada OPTIONAL MATCH#

Predikat pada WHERE yang menempel langsung setelah MATCH menghapus baris yang tidak memenuhi syarat. Kueri berikut menyaring hobi Fotografi dari pola yang wajib cocok, sehingga Fajar yang bersepeda ikut tersaring dan hasilnya tinggal satu baris.

cypher
MATCH (p:Person)-[:PUNYA_HOBI]->(h:Hobi)
WHERE h.name = 'Fotografi'
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Hana", "Fotografi"

Susunan yang tampak serupa pada OPTIONAL MATCH membawa makna berbeda. WHERE yang ditulis tepat setelah OPTIONAL MATCH menjadi bagian dari pencocokan opsional itu, sehingga predikatnya tidak menghapus baris, melainkan membatasi bagian opsionalnya saja. Setiap baris yang pola opsionalnya gagal memenuhi predikat tetap dikembalikan, tetapi variabel opsionalnya diisi null.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
WHERE h.name = 'Fotografi'
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Eko", NULL
"Fajar", NULL
"Gita", NULL
"Hana", "Fotografi"
"Iwan", NULL
"Jihan", NULL
"Rara", NULL

Dua keluaran terakhir menggunakan pola dan predikat yang identik, tetapi hasilnya berubah dari satu baris menjadi tujuh baris karena pencocokan opsional disisipkan. Fajar kini tampil dengan kolom hobi NULL, sebab Bersepeda tidak lolos predikat Fotografi. Orang-orang tanpa hobi juga tetap tampil dengan nilai NULL yang sama.

Cara menghapus baris diperlihatkan kueri berikut. Klausa WITH meneruskan variabel p dan h ke bagian kueri berikutnya, dan WHERE yang menyertainya bekerja sebagai penyaring baris biasa. Predikat yang sama kini kembali menghasilkan satu baris, dan klausa ini dibahas lebih lanjut pada topik agregasi di modul yang sama.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
WITH p, h WHERE h.name = 'Fotografi'
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Hana", "Fotografi"

Menangani nilai null dengan IS NULL dan coalesce()#

Perbandingan yang melibatkan null tidak menghasilkan benar atau salah, melainkan null itu sendiri, dan dokumentasi Cypher menyebut logika semacam ini sebagai logika tiga nilai (three-valued logic). Predikat h.name = 'Fotografi' bernilai null ketika h.name tidak ada, sehingga barisnya tidak pernah lolos penyaringan perbandingan biasa. Untuk menguji keberadaan nilai secara eksplisit, Cypher menyediakan predikat IS NULL dan IS NOT NULL.

Kueri berikut menghasilkan kebalikan daftar pada bagian pertama, yaitu orang-orang yang tidak memiliki hobi. Variabel p dan h diteruskan melalui WITH terlebih dahulu agar WHERE bekerja sebagai penyaring baris, kemudian predikat IS NULL memilih baris dengan kolom hobi yang kosong. Hasilnya memuat lima nama sesuai susunan dataset.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
WITH p, h WHERE h IS NULL
RETURN p.name AS orang
ORDER BY orang;
text Keluaran
orang
"Eko"
"Gita"
"Iwan"
"Jihan"
"Rara"

Predikat IS NULL menjawab siapa saja yang datanya tidak ada, sedangkan fungsi coalesce() menutup celah itu langsung pada proyeksi hasil. Fungsi ini menerima beberapa argumen dan mengembalikan argumen pertama yang bukan null, sehingga cocok untuk memberi nilai default pada kolom yang dapat kosong. Kueri berikut mengganti setiap null pada kolom hobi dengan teks “tidak ada”.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
RETURN p.name AS orang, coalesce(h.name, 'tidak ada') AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Eko", "tidak ada"
"Fajar", "Bersepeda"
"Gita", "tidak ada"
"Hana", "Fotografi"
"Iwan", "tidak ada"
"Jihan", "tidak ada"
"Rara", "tidak ada"

Lima baris kini menampilkan teks pengganti alih-alih NULL, sehingga daftarnya siap digunakan sebagai laporan tanpa penanganan tambahan pada sisi aplikasi. Penggantian ini bersifat tampilan semata karena node dan relasi di dalam graf tidak berubah. Apabila nilai null justru perlu dihitung atau disaring, predikat IS NULL pada kueri sebelumnya tetap menjadi cara yang tepat.

Kesalahan umum#

Kesalahan pertama adalah menempelkan predikat IS NULL langsung pada OPTIONAL MATCH dengan harapan mendapat daftar orang tanpa hobi. Kueri berikut memperlihatkan hasilnya, yaitu tujuh baris yang seluruh kolom hobinya NULL, termasuk milik Hana dan Fajar yang sebenarnya memiliki hobi.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h:Hobi)
WHERE h IS NULL
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
"Eko", NULL
"Fajar", NULL
"Gita", NULL
"Hana", NULL
"Iwan", NULL
"Jihan", NULL
"Rara", NULL

Penyebabnya adalah semantik WHERE pada pencocokan opsional, yaitu predikat yang gagal tidak menghapus baris, melainkan memutus bagian opsionalnya. Orang yang memiliki hobi gagal memenuhi predikat, sehingga variabel h yang sebelumnya terisi justru diubah menjadi null dan setiap baris lolos. Perbaikannya mengikuti kueri pada bagian sebelumnya, yaitu meneruskan variabel melalui WITH terlebih dahulu agar predikat bekerja pada baris yang sudah terbentuk.

Kesalahan kedua adalah membaca baris yang seluruhnya NULL sebagai tanda data rusak. Ketika pola opsional ditulis di awal kueri dan tidak menemukan node apa pun, OPTIONAL MATCH tetap mengembalikan satu baris dengan seluruh variabelnya null, sebagaimana kueri berikut atas nama Budi yang tidak ada di dataset.

cypher
OPTIONAL MATCH (p:Person {name: 'Budi'})-[:PUNYA_HOBI]->(h:Hobi)
RETURN p.name AS orang, h.name AS hobi
ORDER BY orang;
text Keluaran
orang, hobi
NULL, NULL

Baris semacam itu bukan error, melainkan perilaku yang dijanjikan klausa ini, yaitu setiap pencocokan opsional berakhir dengan paling sedikit satu baris. Perlakuannya sama dengan kasus lainnya, yaitu diuji dengan IS NULL atau diberi nilai default dengan coalesce().

Kesalahan ketiga adalah menggunakan OPTIONAL MATCH pada setiap pola sebagai tindakan pencegahan. Klausa ini menambah langkah pada rencana kueri dan menyembunyikan data yang seharusnya ada, sehingga kueri menjadi lebih lambat dan cacat data tidak terdeteksi. Pencocokan opsional hanya dikehendaki ketika ketiadaan pola memang informasi yang sah, seperti hobi yang belum dipilih, bukan ketika relasi yang hilang berarti kesalahan data.

Ringkasan#

  • MATCH hanya melaporkan pola yang ditemukan, sedangkan OPTIONAL MATCH tetap mengembalikan barisnya dengan null pada bagian yang tidak cocok, sebagaimana LEFT JOIN pada SQL.
  • WHERE yang menempel pada OPTIONAL MATCH membatasi bagian opsional saja, sehingga predikat yang gagal menghasilkan null dan bukan baris yang terhapus.
  • Penyaringan baris penuh setelah pencocokan opsional dikerjakan pada WITH, misalnya WITH p, h WHERE h IS NULL untuk mendaftar orang tanpa hobi.
  • Predikat IS NULL dan IS NOT NULL menguji ketiadaan atau keberadaan nilai, sedangkan coalesce() memberi nilai default pada proyeksi hasil.

Referensi#