Rantai -> dan path #> dari chapter sebelumnya mengambil satu nilai di satu jalur yang sudah diketahui. Namun pertanyaan dunia nyata sering berbentuk lain: elemen array mana saja yang jumlahnya dua atau lebih, produk mana yang harganya di atas seratus ribu, dokumen mana yang punya tag diskon di mana pun posisinya. Menulis kondisi seperti itu dengan operator per-langkah menuntut subquery dan perulangan yang canggung. PostgreSQL menjawabnya dengan bahasa query tersendiri untuk dokumen — JSONPath, bagian dari standar SQL/JSON — yang dioperasikan lewat keluarga fungsi jsonb_path_*. Kita akan memakainya atas katalog produk dan daftar pesanan Toko Maju Jaya: dari path sederhana, filter per elemen, sampai metode size() dan last.
Bahasa JSONPath dalam Satu Pandangan #
Sebuah ekspresi JSONPath selalu berangkat dari $ — akar dokumen — lalu menempuh kunci, indeks, dan penyaringan. Simbol @ di dalam penyaring merujuk pada nilai yang sedang diperiksa. Tabel berikut merangkum semua komponen yang dipakai di chapter ini:
| Komponen | Arti | Contoh |
|---|---|---|
$ | akar dokumen | '$' |
$.kunci | nilai kunci pada objek | '$.merek' |
$.a.b | path bertingkat | '$.spesifikasi.berat' |
$[*] | semua elemen array | '$[*].produk' |
$.kunci[*] | semua elemen array di kunci | '$.tag[*]' |
@ | nilai yang sedang diperiksa filter | ? (@.harga > 100000) |
? (...) | filter kondisi | '$[*] ? (@.jumlah >= 2)' |
exists (...) | uji keberadaan path dalam filter | ? (exists (@.garansi)) |
last | indeks elemen terakhir | '$.tag[last]' |
size() | jumlah elemen array | '$.tag.size()' |
like_regex | pencocokan pola regex | ? (@.produk like_regex "^S") |
Empat fungsi menjalankan ekspresi tersebut, masing-masing dengan bentuk hasil yang berbeda:
| Fungsi | Hasil | Kapan dipakai |
|---|---|---|
jsonb_path_query | nol atau lebih baris (set) | mengambil semua nilai yang cocok |
jsonb_path_query_first | satu nilai jsonb atau NULL | nilai pertama yang cocok |
jsonb_path_query_array | satu array jsonb | semua hasil dikumpulkan jadi array |
jsonb_path_exists | boolean | sekadar menguji ada-tidaknya kecocokan |
-- Katalog produk dan daftar pesanan untuk contoh JSONPath.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
info jsonb NOT NULL
);
INSERT INTO produk (id, nama, info) VALUES
(1, 'Sepatu Lari',
'{"merek": "Elang", "harga": 750000, "stok": 25, "tag": ["olahraga", "diskon"], "spesifikasi": {"berat": 280, "bahan": "mesh", "dimensi": {"panjang": 31, "lebar": 11, "tinggi": 9}}}'),
(2, 'Kemeja Batik',
'{"merek": "Nusantara", "harga": 320000, "stok": 40, "tag": ["formal", "diskon"], "spesifikasi": {"berat": 220, "bahan": "katun"}}'),
(3, 'Tas Ransel',
'{"merek": "Gunung Jaya", "harga": 450000, "stok": 15, "tag": ["olahraga", "outdoor"], "garansi": "12 bulan", "spesifikasi": {"berat": 850, "kapasitas": 30, "tahan_air": true}}'),
(4, 'Sendok Set 6',
'{"merek": "Sinar Pagi", "harga": 65000, "stok": 120, "tag": ["dapur"], "spesifikasi": {"berat": 300, "bahan": "stainless"}}'),
(5, 'Sarung Tangan Sepeda',
'{"merek": "Elang", "harga": 85000, "stok": 60, "tag": ["olahraga"], "spesifikasi": {"berat": 90, "bahan": "kulit sintetis"}}'),
(6, 'Topi Pantai',
'{"merek": "Kasih", "harga": 45000, "stok": 200, "tag": ["outdoor", "diskon"], "spesifikasi": {"berat": 60, "bahan": "katun", "lipat": true}}'),
(7, 'Termos 1L',
'{"merek": "Sinar Pagi", "harga": 185000, "stok": 35, "tag": ["dapur", "bestseller"], "garansi": "12 bulan", "spesifikasi": {"berat": 540, "kapasitas": 1, "tahan_air": true}}'),
(8, 'Kacamata Renang',
'{"merek": "Elang", "harga": 95000, "stok": 75, "tag": ["olahraga", "bestseller"], "spesifikasi": {"berat": 120, "anti_uv": true}}');
-- Tiap pesanan membawa items: array objek produk yang dibeli.
CREATE TABLE pesanan (
id integer PRIMARY KEY,
nama_pemesan text NOT NULL,
items jsonb NOT NULL
);
INSERT INTO pesanan (id, nama_pemesan, items) VALUES
(1, 'Budi',
'[{"produk": "Sepatu Lari", "jumlah": 1, "harga": 750000}, {"produk": "Kacamata Renang", "jumlah": 2, "harga": 95000}]'),
(2, 'Siti',
'[{"produk": "Kemeja Batik", "jumlah": 3, "harga": 320000}, {"produk": "Topi Pantai", "jumlah": 1, "harga": 45000}, {"produk": "Termos 1L", "jumlah": 2, "harga": 185000}]'),
(3, 'Tono',
'[{"produk": "Tas Ransel", "jumlah": 1, "harga": 450000}]');
SELECT id, nama_pemesan, items
FROM pesanan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 8
CREATE TABLE
INSERT 0 3
id | nama_pemesan | items
----+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | Budi | [{"harga": 750000, "jumlah": 1, "produk": "Sepatu Lari"}, {"harga": 95000, "jumlah": 2, "produk": "Kacamata Renang"}]
2 | Siti | [{"harga": 320000, "jumlah": 3, "produk": "Kemeja Batik"}, {"harga": 45000, "jumlah": 1, "produk": "Topi Pantai"}, {"harga": 185000, "jumlah": 2, "produk": "Termos 1L"}]
3 | Tono | [{"harga": 450000, "jumlah": 1, "produk": "Tas Ransel"}]
(3 rows)Tabel pesanan menjadi arena utama kita karena kolom items-nya berisi array objek — struktur yang paling melelahkan bila dijangkau dengan rantai panah: jumlah elemennya berbeda per baris, sementara -> 0 selalu terpatok pada satu indeks.
Menyusun Path Dasar #
Path paling sederhana meniru apa yang sudah biasa kita lakukan dengan ->: mengambil kunci dari akar dokumen. Bedanya, seluruh jalur — berapa pun tingkatnya — ditulis sebagai satu ekspresi $.spesifikasi.berat, dan fungsi jsonb_path_query_first mengembalikan nilai pertama yang ditemukan.
-- $.kunci: nilai tunggal dari akar dokumen.
SELECT nama, jsonb_path_query_first(info, '$.merek') AS merek
FROM produk
ORDER BY id;
-- Path bertingkat ditulis flat dalam satu ekspresi.
SELECT nama, jsonb_path_query_first(info, '$.spesifikasi.berat') AS berat
FROM produk
ORDER BY id;
-- jsonb_path_exists: menguji keberadaan path tanpa mengambil nilainya.
SELECT nama, jsonb_path_exists(info, '$.spesifikasi.dimensi') AS punya_dimensi
FROM produk
ORDER BY id;
-- Wildcard $[*]: menelusuri semua elemen array;
-- jsonb_path_query mengembalikan satu baris per hasil.
SELECT id, jsonb_path_query(items, '$[*].produk') AS produk_dibeli
FROM pesanan
ORDER BY id, 2;
-- jsonb_path_query_array: seluruh hasil wildcard dikumpulkan
-- kembali menjadi satu array jsonb.
SELECT id, jsonb_path_query_array(items, '$[*].produk') AS daftar_produk
FROM pesanan
ORDER BY id;Output yang Dihasilkan:
nama | merek
----------------------+---------------
Sepatu Lari | "Elang"
Kemeja Batik | "Nusantara"
Tas Ransel | "Gunung Jaya"
Sendok Set 6 | "Sinar Pagi"
Sarung Tangan Sepeda | "Elang"
Topi Pantai | "Kasih"
Termos 1L | "Sinar Pagi"
Kacamata Renang | "Elang"
(8 rows)
nama | berat
----------------------+-------
Sepatu Lari | 280
Kemeja Batik | 220
Tas Ransel | 850
Sendok Set 6 | 300
Sarung Tangan Sepeda | 90
Topi Pantai | 60
Termos 1L | 540
Kacamata Renang | 120
(8 rows)
nama | punya_dimensi
----------------------+---------------
Sepatu Lari | t
Kemeja Batik | f
Tas Ransel | f
Sendok Set 6 | f
Sarung Tangan Sepeda | f
Topi Pantai | f
Termos 1L | f
Kacamata Renang | f
(8 rows)
id | produk_dibeli
----+-------------------
1 | "Kacamata Renang"
1 | "Sepatu Lari"
2 | "Kemeja Batik"
2 | "Termos 1L"
2 | "Topi Pantai"
3 | "Tas Ransel"
(6 rows)
id | daftar_produk
----+----------------------------------------------
1 | ["Sepatu Lari", "Kacamata Renang"]
2 | ["Kemeja Batik", "Topi Pantai", "Termos 1L"]
3 | ["Tas Ransel"]
(3 rows)Empat pengamatan dari hasil di atas:
$.merekdan$.spesifikasi.beratmenghasilkan delapan nilai — sama sepertiinfo -> 'merek'daninfo -> 'spesifikasi' -> 'berat', tetapi ditulis dalam satu string path.jsonb_path_exists(info, '$.spesifikasi.dimensi')hanya bernilaituntuk Sepatu Lari: satu-satunya produk yang dokumennya memuatdimensi— menguji keberadaan path jauh lebih ringkas daripada membandingkan hasil->dengan NULL.- Wildcard
'$[*].produk'menelusuri setiap elemen arrayitems; karenajsonb_path_querymengembalikan satu baris per hasil, pesanan Budi menghasilkan dua baris dan pesanan Siti tiga baris — total enam baris dari tiga pesanan. jsonb_path_query_arraymengumpulkan hasil wildcard yang sama kembali menjadi satu array — bentuk yang pas saat aplikasi butuh daftar lengkap per pesanan tanpa baris terpisah.
Menyaring dengan Filter ?() #
Inilah fitur yang tidak dimiliki operator panah: kondisi di dalam path. Filter ? (...) dievaluasi terhadap setiap kandidat — elemen array saat wildcard sedang berjalan, atau keseluruhan dokumen saat filter menempel langsung pada $.
-- Filter ?(): saring elemen array berdasarkan kondisi per elemen.
-- Simbol @ merujuk elemen yang sedang diperiksa.
SELECT id, jsonb_path_query(items, '$[*] ? (@.jumlah >= 2).produk') AS dibeli_banyak
FROM pesanan
ORDER BY id, 2;
-- Filter pada dokumen: hanya produk dengan harga di atas 100000.
SELECT nama, info ->> 'harga' AS harga
FROM produk
WHERE jsonb_path_exists(info, '$ ? (@.harga > 100000)')
ORDER BY id;
-- Wildcard di dalam kondisi: cukup SALAH SATU tag bernilai "diskon".
SELECT nama
FROM produk
WHERE jsonb_path_exists(info, '$ ? (@.tag[*] == "diskon")')
ORDER BY id;
-- exists: menguji keberadaan path di dalam filter.
SELECT nama, info ->> 'garansi' AS garansi
FROM produk
WHERE jsonb_path_exists(info, '$ ? (exists (@.garansi))')
ORDER BY id;Output yang Dihasilkan:
id | dibeli_banyak
----+-------------------
1 | "Kacamata Renang"
2 | "Kemeja Batik"
2 | "Termos 1L"
(3 rows)
nama | harga
--------------+--------
Sepatu Lari | 750000
Kemeja Batik | 320000
Tas Ransel | 450000
Termos 1L | 185000
(4 rows)
nama
--------------
Sepatu Lari
Kemeja Batik
Topi Pantai
(3 rows)
nama | garansi
------------+----------
Tas Ransel | 12 bulan
Termos 1L | 12 bulan
(2 rows)Mari kita bedah keempat query tersebut:
'$[*] ? (@.jumlah >= 2).produk'— wildcard menawarkan tiap elemen array ke filter; elemen denganjumlahdua atau lebih lolos, lalu.produkmengambil nilainya. Hasilnya tiga produk dari dua pesanan; item berjumlah satu tidak pernah muncul.'$ ? (@.harga > 100000)'— filter menempel pada akar, jadi@merujuk seluruh dokumen tiap baris tabel. Empat produk lolos ambang harga.'$ ? (@.tag[*] == "diskon")'— wildcard di dalam kondisi: perbandingan bernilai benar bila salah satu elementagsama dengan"diskon". Perilaku ini dijamin mode lax bawaan PostgreSQL, yang secara otomatis membuka array saat dibandingkan dengan skalar.'$ ? (exists (@.garansi))'—existsmenguji keberadaan path; hasilnya dua produk bergaransi tanpa perlu membandingkan nilainya dengan apa pun.
Metode Bawaan: size, last, like_regex #
JSONPath membawa segelintir metode siap pakai. Metode size() menghitung jumlah elemen array; indeks last menunjuk elemen terakhir; dan like_regex mencocokkan nilai string dengan pola regular expression, lengkap dengan opsi flag.
-- size(): jumlah elemen array dalam dokumen.
SELECT id, jsonb_path_query_first(items, '$.size()') AS jumlah_item
FROM pesanan
ORDER BY id;
SELECT nama, jsonb_path_query_first(info, '$.tag.size()') AS jumlah_tag
FROM produk
WHERE id IN (1, 4)
ORDER BY id;
-- Indeks last: elemen terakhir tanpa perlu tahu panjang array.
SELECT nama, jsonb_path_query_first(info, '$.tag[last]') AS tag_terakhir
FROM produk
ORDER BY id;
-- like_regex: pencocokan pola terhadap nilai string.
-- Tanpa flag, pencocokan bersifat case-sensitive.
SELECT id, jsonb_path_query(items, '$[*] ? (@.produk like_regex "sepatu").produk') AS cocok_peka_huruf
FROM pesanan
ORDER BY id, 2;
-- flag "i" mengabaikan perbedaan huruf besar-kecil.
SELECT id, jsonb_path_query(items, '$[*] ? (@.produk like_regex "sepatu" flag "i").produk') AS cocok_abai_huruf
FROM pesanan
ORDER BY id, 2;Output yang Dihasilkan:
id | jumlah_item
----+-------------
1 | 2
2 | 3
3 | 1
(3 rows)
nama | jumlah_tag
--------------+------------
Sepatu Lari | 2
Sendok Set 6 | 1
(2 rows)
nama | tag_terakhir
----------------------+--------------
Sepatu Lari | "diskon"
Kemeja Batik | "diskon"
Tas Ransel | "outdoor"
Sendok Set 6 | "dapur"
Sarung Tangan Sepeda | "olahraga"
Topi Pantai | "diskon"
Termos 1L | "bestseller"
Kacamata Renang | "bestseller"
(8 rows)
id | cocok_peka_huruf
----+------------------
(0 rows)
id | cocok_abai_huruf
----+------------------
1 | "Sepatu Lari"
(1 row)Query pertama menghitung item tiap pesanan lewat '$.size()' — angka 2, 3, 1 — dan query kedua menghitung tag produk lewat '$.tag.size()'. Indeks last mengembalikan tag terakhir tiap produk tanpa perlu tahu panjang arraynya; bandingkan hasil kolom ini dengan info -> 'tag' -> -1 dari chapter sebelumnya — keduanya setara. Dua query terakhir memperlihatkan sensitivitas huruf pada like_regex: pola "sepatu" huruf kecil tidak menemukan apa pun karena nama produk ditulis "Sepatu Lari", sedangkan flag "i" mengabaikan perbedaan huruf besar-kecil dan langsung menemukannya.
Tiga Cara Menempuh Dokumen #
Kita kini punya tiga teknik membaca nilai. Pilihan di antaranya adalah soal bentuk pertanyaan, bukan kekuatan:
| Teknik | Contoh | Hasil | Medan terbaik |
|---|---|---|---|
Rantai -> | info -> 'spesifikasi' -> 'berat' | satu nilai | path pendek yang selalu tetap |
Path array #> | info #> '{spesifikasi,berat}' | satu nilai | path dalam/dinamis sebagai data |
| JSONPath | jsonb_path_query(info, '$.spesifikasi.berat') | nol atau lebih nilai | filter, wildcard, array multi-elemen |
Ekspresi path dikirim sebagai string — jaga agar pola dari pengguna akhir tidak ditempel langsung dengan penyambungan teks: JSONPath punya sintaksnya sendiri, dan string yang tidak valid akan gagal saat parsing. Pada driver aplikasi, kirim path sebagai parameter terpisah sebagaimana Anda mengirim nilai SQL lainnya.
Dengan JSONPath, pertanyaan “dokumen mana yang memuat apa” terjawab dalam satu ekspresi. Chapter berikutnya melengkapi kotak perkakas dari arah yang berbeda: peta lengkap seluruh operator JSON dan JSONB — penampung @>, keanggotaan ?, penggabungan ||, sampai penghapusan kunci - dan #- — beserta jebakan masing-masing.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Katalog produk dan daftar pesanan untuk contoh JSONPath.
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
info jsonb NOT NULL
);
INSERT INTO produk (id, nama, info) VALUES
(1, 'Sepatu Lari',
'{"merek": "Elang", "harga": 750000, "stok": 25, "tag": ["olahraga", "diskon"], "spesifikasi": {"berat": 280, "bahan": "mesh", "dimensi": {"panjang": 31, "lebar": 11, "tinggi": 9}}}'),
(2, 'Kemeja Batik',
'{"merek": "Nusantara", "harga": 320000, "stok": 40, "tag": ["formal", "diskon"], "spesifikasi": {"berat": 220, "bahan": "katun"}}'),
(3, 'Tas Ransel',
'{"merek": "Gunung Jaya", "harga": 450000, "stok": 15, "tag": ["olahraga", "outdoor"], "garansi": "12 bulan", "spesifikasi": {"berat": 850, "kapasitas": 30, "tahan_air": true}}'),
(4, 'Sendok Set 6',
'{"merek": "Sinar Pagi", "harga": 65000, "stok": 120, "tag": ["dapur"], "spesifikasi": {"berat": 300, "bahan": "stainless"}}'),
(5, 'Sarung Tangan Sepeda',
'{"merek": "Elang", "harga": 85000, "stok": 60, "tag": ["olahraga"], "spesifikasi": {"berat": 90, "bahan": "kulit sintetis"}}'),
(6, 'Topi Pantai',
'{"merek": "Kasih", "harga": 45000, "stok": 200, "tag": ["outdoor", "diskon"], "spesifikasi": {"berat": 60, "bahan": "katun", "lipat": true}}'),
(7, 'Termos 1L',
'{"merek": "Sinar Pagi", "harga": 185000, "stok": 35, "tag": ["dapur", "bestseller"], "garansi": "12 bulan", "spesifikasi": {"berat": 540, "kapasitas": 1, "tahan_air": true}}'),
(8, 'Kacamata Renang',
'{"merek": "Elang", "harga": 95000, "stok": 75, "tag": ["olahraga", "bestseller"], "spesifikasi": {"berat": 120, "anti_uv": true}}');
-- Tiap pesanan membawa items: array objek produk yang dibeli.
CREATE TABLE pesanan (
id integer PRIMARY KEY,
nama_pemesan text NOT NULL,
items jsonb NOT NULL
);
INSERT INTO pesanan (id, nama_pemesan, items) VALUES
(1, 'Budi',
'[{"produk": "Sepatu Lari", "jumlah": 1, "harga": 750000}, {"produk": "Kacamata Renang", "jumlah": 2, "harga": 95000}]'),
(2, 'Siti',
'[{"produk": "Kemeja Batik", "jumlah": 3, "harga": 320000}, {"produk": "Topi Pantai", "jumlah": 1, "harga": 45000}, {"produk": "Termos 1L", "jumlah": 2, "harga": 185000}]'),
(3, 'Tono',
'[{"produk": "Tas Ransel", "jumlah": 1, "harga": 450000}]');
SELECT id, nama_pemesan, items
FROM pesanan
ORDER BY id;
filter.sql
Unduh berkas-- Filter ?(): saring elemen array berdasarkan kondisi per elemen.
-- Simbol @ merujuk elemen yang sedang diperiksa.
SELECT id, jsonb_path_query(items, '$[*] ? (@.jumlah >= 2).produk') AS dibeli_banyak
FROM pesanan
ORDER BY id, 2;
-- Filter pada dokumen: hanya produk dengan harga di atas 100000.
SELECT nama, info ->> 'harga' AS harga
FROM produk
WHERE jsonb_path_exists(info, '$ ? (@.harga > 100000)')
ORDER BY id;
-- Wildcard di dalam kondisi: cukup SALAH SATU tag bernilai "diskon".
SELECT nama
FROM produk
WHERE jsonb_path_exists(info, '$ ? (@.tag[*] == "diskon")')
ORDER BY id;
-- exists: menguji keberadaan path di dalam filter.
SELECT nama, info ->> 'garansi' AS garansi
FROM produk
WHERE jsonb_path_exists(info, '$ ? (exists (@.garansi))')
ORDER BY id;
metode.sql
Unduh berkas-- size(): jumlah elemen array dalam dokumen.
SELECT id, jsonb_path_query_first(items, '$.size()') AS jumlah_item
FROM pesanan
ORDER BY id;
SELECT nama, jsonb_path_query_first(info, '$.tag.size()') AS jumlah_tag
FROM produk
WHERE id IN (1, 4)
ORDER BY id;
-- Indeks last: elemen terakhir tanpa perlu tahu panjang array.
SELECT nama, jsonb_path_query_first(info, '$.tag[last]') AS tag_terakhir
FROM produk
ORDER BY id;
-- like_regex: pencocokan pola terhadap nilai string.
-- Tanpa flag, pencocokan bersifat case-sensitive.
SELECT id, jsonb_path_query(items, '$[*] ? (@.produk like_regex "sepatu").produk') AS cocok_peka_huruf
FROM pesanan
ORDER BY id, 2;
-- flag "i" mengabaikan perbedaan huruf besar-kecil.
SELECT id, jsonb_path_query(items, '$[*] ? (@.produk like_regex "sepatu" flag "i").produk') AS cocok_abai_huruf
FROM pesanan
ORDER BY id, 2;
path-dasar.sql
Unduh berkas-- $.kunci: nilai tunggal dari akar dokumen.
SELECT nama, jsonb_path_query_first(info, '$.merek') AS merek
FROM produk
ORDER BY id;
-- Path bertingkat ditulis flat dalam satu ekspresi.
SELECT nama, jsonb_path_query_first(info, '$.spesifikasi.berat') AS berat
FROM produk
ORDER BY id;
-- jsonb_path_exists: menguji keberadaan path tanpa mengambil nilainya.
SELECT nama, jsonb_path_exists(info, '$.spesifikasi.dimensi') AS punya_dimensi
FROM produk
ORDER BY id;
-- Wildcard $[*]: menelusuri semua elemen array;
-- jsonb_path_query mengembalikan satu baris per hasil.
SELECT id, jsonb_path_query(items, '$[*].produk') AS produk_dibeli
FROM pesanan
ORDER BY id, 2;
-- jsonb_path_query_array: seluruh hasil wildcard dikumpulkan
-- kembali menjadi satu array jsonb.
SELECT id, jsonb_path_query_array(items, '$[*].produk') AS daftar_produk
FROM pesanan
ORDER BY id;