Kita sudah memakai operator ->, ->>, dan #> untuk membaca nilai, serta @> sekilas saat menyaring baris. Namun keluarga operator JSON PostgreSQL jauh lebih lengkap: ada operator keanggotaan untuk menguji kunci, operator penampung dua arah, operator penggabungan dokumen, sampai operator penghapusan kunci dan path. Chapter ini menyusun semuanya menjadi satu peta di atas katalog produk Toko Maju Jaya, lalu menguji tiap kelompok dengan contoh yang sengaja memuat jebakannya — karena hampir setiap operator JSON punya satu perilaku tak terduga yang menghantam pemula tepat di produksi.
Peta Lengkap Operator #
Seluruh operator di tabel berikut bekerja pada tipe jsonb (operator akses dasar juga tersedia untuk json, seperti dibahas di chapter JSON vs JSONB):
| Operator | Tugas | Hasil |
|---|---|---|
-> | ambil nilai kunci/indeks | jsonb |
->> | ambil nilai kunci/indeks sebagai teks | text |
#> | ambil nilai lewat path text[] | jsonb |
#>> | ambil nilai lewat path sebagai teks | text |
@> | sisi kiri memuat seluruh struktur sisi kanan | boolean |
<@ | sisi kiri terkandung dalam sisi kanan | boolean |
? | string hadir sebagai kunci (objek) atau elemen (array) | boolean |
?& | semua string pada array hadir | boolean |
| `? | ` | salah satu string pada array hadir |
|| | gabungkan dua dokumen | jsonb |
- | hapus kunci (objek) atau indeks (array) | jsonb |
#- | hapus nilai di ujung path text[] | jsonb |
-- Katalog produk untuk peta operator JSON dan JSONB.
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}}');Output yang Dihasilkan:
CREATE TABLE
INSERT 0 8Penampung @> dan <@ #
Operator @> menjawab pertanyaan “apakah dokumen ini memuat struktur itu?” — dan jawabannya ditentukan arah operator. Semantiknya juga bukan kesamaan persis melainkan penampungan parsial: dokumen kiri boleh membawa kunci tambahan selama seluruh pasangan sisi kanan hadir dengan nilai yang sama.
-- Arah menentukan: @> menguji "sisi kiri memuat sisi kanan".
SELECT '{"merek": "Elang", "harga": 750000}'::jsonb @> '{"merek": "Elang"}' AS kiri_memuat_kanan;
-- Dibalik arahnya, dokumen kecil tidak memuat dokumen besar.
SELECT '{"merek": "Elang"}'::jsonb @> '{"merek": "Elang", "harga": 750000}' AS kanan_memuat_kiri;
-- <@ adalah @> dengan arah dibalik: kiri terkandung dalam kanan.
SELECT '{"merek": "Elang"}'::jsonb <@ '{"merek": "Elang", "harga": 750000}' AS kiri_terkandung;
-- Angka dibandingkan secara NUMERIK: 25 dan 25.0 dianggap sama.
SELECT '{"stok": 25}'::jsonb @> '{"stok": 25.0}'::jsonb AS angka_sama_numerik;
-- Teks "25" dan angka 25 adalah dua nilai JSON yang berbeda.
SELECT '{"stok": 25}'::jsonb @> '{"stok": "25"}'::jsonb AS teks_vs_angka;
-- Penampung objek bersarang bersifat PARSIAL:
-- cukup pasangan yang disebut hadir, kunci lain boleh ikut.
SELECT info @> '{"spesifikasi": {"berat": 280}}' AS spek_cocok
FROM produk
WHERE id = 1;
-- Bentuk pemakaian paling umum: penyaringan baris di WHERE.
SELECT nama
FROM produk
WHERE info @> '{"tag": ["olahraga"]}'
ORDER BY id;
SELECT nama
FROM produk
WHERE info @> '{"spesifikasi": {"tahan_air": true}}'
ORDER BY id;Output yang Dihasilkan:
kiri_memuat_kanan
-------------------
t
(1 row)
kanan_memuat_kiri
-------------------
f
(1 row)
kiri_terkandung
-----------------
t
(1 row)
angka_sama_numerik
--------------------
t
(1 row)
teks_vs_angka
---------------
f
(1 row)
spek_cocok
------------
t
(1 row)
nama
----------------------
Sepatu Lari
Tas Ransel
Sarung Tangan Sepeda
Kacamata Renang
(4 rows)
nama
------------
Tas Ransel
Termos 1L
(2 rows)Empat query pertama memetakan perilakunya. {"merek": "Elang", "harga": 750000} memuat {"merek": "Elang"} — benar; dibalik, dokumen kecil tidak memuat dokumen yang lebih kaya — salah; dan <@ hanyalah @> yang diputar arah. Query keempat dan kelima memperlihatkan aturan tipe nilainya: angka dibandingkan secara numerik sehingga 25 dan 25.0 dianggap sama, tetapi teks "25" dan angka 25 adalah dua nilai JSON yang berbeda dan tidak saling memuat.
Dua query terakhir adalah pemakaian nyatanya. info @> '{"tag": ["olahraga"]}' mengembalikan produk yang array tag-nya memuat elemen "olahraga" — operator tidak menuntut arraynya identik, cukup elemennya hadir. Penampungan juga menembus struktur bersarang secara parsial: {"spesifikasi": {"berat": 280}} dianggap terkandung meskipun spesifikasi aslinya jauh lebih kaya — cukup pasangan yang disebut hadir. Bentuk WHERE inilah yang diakselerasi index GIN, topik chapter Index pada JSONB
.
Keanggotaan ?, ?&, dan ?| #
Keluarga ? menguji keberadaan string — sebagai kunci pada objek, atau sebagai elemen pada array. Batasannya satu dan tegas: pemeriksaan hanya terjadi pada level teratas nilai yang diuji.
-- ? menguji keberadaan kunci pada LEVEL TERATAS dokumen.
SELECT nama
FROM produk
WHERE info ? 'garansi'
ORDER BY id;
-- 'berat' hanya hidup di dalam objek spesifikasi, bukan level teratas;
-- hasilnya nol baris -- inilah jebakan klasik operator ?.
SELECT count(*) AS punya_berat_level_atas
FROM produk
WHERE info ? 'berat';
-- Turun dulu satu tingkat, baru uji keanggotaan pada objek itu.
SELECT count(*) AS punya_berat_di_spek
FROM produk
WHERE info -> 'spesifikasi' ? 'berat';
-- Pada array, ? menguji KEANGGOTAN ELEMEN, bukan kunci.
SELECT nama
FROM produk
WHERE info -> 'tag' ? 'diskon'
ORDER BY id;
-- ?& menuntut SEMUA kunci hadir.
SELECT nama
FROM produk
WHERE info ?& ARRAY['garansi', 'tag']
ORDER BY id;
-- ?| cukup SALAH SATU kunci hadir.
SELECT nama
FROM produk
WHERE info ?| ARRAY['garansi', 'stok']
ORDER BY id;Output yang Dihasilkan:
nama
------------
Tas Ransel
Termos 1L
(2 rows)
punya_berat_level_atas
------------------------
0
(1 row)
punya_berat_di_spek
---------------------
8
(1 row)
nama
--------------
Sepatu Lari
Kemeja Batik
Topi Pantai
(3 rows)
nama
------------
Tas Ransel
Termos 1L
(2 rows)
nama
----------------------
Sepatu Lari
Kemeja Batik
Tas Ransel
Sendok Set 6
Sarung Tangan Sepeda
Topi Pantai
Termos 1L
Kacamata Renang
(8 rows)info ? 'garansi' menemukan dua produk bergaransi. Namun info ? 'berat' menemukan nol baris — padahal semua delapan produk punya berat di dalam spesifikasi. Itu bukan bug: ? tidak menuruni struktur; ia hanya melihat kunci level atas. Solusinya dua langkah: turun dulu dengan -> lalu uji keanggotaan pada objek tersebut — info -> 'spesifikasi' ? 'berat' menemukan kedelapan produk. Pada array, ? berpindah peran menjadi uji keanggotaan elemen: info -> 'tag' ? 'diskon' menemukan tiga produk yang tag-nya memuat "diskon".
Dua operator terakhir menerima array string SQL sebagai operand kanan. ?& (dan) menuntut semua kunci hadir — hanya dua produk yang punya garansi sekaligus tag. ?| (atau) cukup salah satu — karena semua produk punya stok, hasilnya seluruh tabel.
Penggabungan || #
Operator || menggabungkan dua nilai jsonb. Untuk dua objek, hasilnya adalah union kunci-kuncinya; untuk array dan nilai, nilai kanan menempel di ujung array kiri.
-- || menggabungkan dua objek jsonb;
-- kunci yang bentrok: nilai sisi KANAN yang menang.
SELECT '{"a": 1, "b": 2}'::jsonb || '{"b": 99, "c": 3}' AS kanan_menang;
-- Penggabungan bersifat DANGKAL: objek bersarang diganti utuh,
-- isi lamanya tidak dilebur dengan isi yang baru.
SELECT '{"spesifikasi": {"berat": 280, "bahan": "mesh"}}'::jsonb
|| '{"spesifikasi": {"bahan": "kulit"}}' AS merge_dangkal;
-- Perbarui satu kunci pada dokumen produk.
SELECT info || '{"stok": 10}' AS stok_diperbarui
FROM produk
WHERE id = 1;
-- Array || nilai: elemen baru ditambahkan di ujung array.
SELECT info -> 'tag' || '"murah"' AS tag_diperluas
FROM produk
WHERE id = 1;Output yang Dihasilkan:
kanan_menang
---------------------------
{"a": 1, "b": 99, "c": 3}
(1 row)
merge_dangkal
-------------------------------------
{"spesifikasi": {"bahan": "kulit"}}
(1 row)
stok_diperbarui
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"tag": ["olahraga", "diskon"], "stok": 10, "harga": 750000, "merek": "Elang", "spesifikasi": {"bahan": "mesh", "berat": 280, "dimensi": {"lebar": 11, "tinggi": 9, "panjang": 31}}}
(1 row)
tag_diperluas
---------------------------------
["olahraga", "diskon", "murah"]
(1 row)Query pertama menunjukkan aturan bentrok: kunci b hadir di kedua sisi, dan nilai sisi kanan (99) yang menang. Query kedua memperlihatkan sifat yang jarang disadari — penggabungan || bersifat dangkal (shallow): objek bersarang diganti utuh, bukan dilebur. spesifikasi yang seharusnya bisa mempertahankan berat: 280 lenyap diganti objek baru yang hanya berisi bahan. Dua query terakhir memperlihatkan pemakaian sehari-hari: memperbarui satu kunci (info || '{"stok": 10}') dan menambah elemen array (info -> 'tag' || '"murah"').
Penghapusan - dan #- #
Dua operator penutup peta justru paling sering dicari: cara membuang bagian dokumen. Operator - menerima kunci (untuk objek) atau indeks (untuk array); operator #- menerima path text[] dan membuang nilai di ujungnya.
-- - 'kunci': buang pasangan kunci-nilai dari objek.
SELECT info - 'tag' AS tanpa_tag
FROM produk
WHERE id = 1;
-- - indeks: buang elemen array pada posisi tersebut.
-- Tanda kurung wajib: tanpa itu, ekspresi 'tag' - 0 yang terparse lebih dulu.
SELECT (info -> 'tag') - 0 AS tag_tanpa_pertama
FROM produk
WHERE id = 1;
-- #- path text[]: buang nilai yang berdiri di ujung path.
SELECT info #- '{spesifikasi,berat}' AS tanpa_berat
FROM produk
WHERE id = 1;
-- Path boleh memuat indeks array setelah nama kunci.
SELECT info #- '{tag,0}' AS tag_tanpa_pertama
FROM produk
WHERE id = 1;
-- Penghapusan berlapis: path masuk dua tingkat ke dalam objek.
SELECT '{"a": {"b": {"c": 1}}}'::jsonb #- '{a,b}' AS jalur_dihapus;Output yang Dihasilkan:
tanpa_tag
-------------------------------------------------------------------------------------------------------------------------------------------------------
{"stok": 25, "harga": 750000, "merek": "Elang", "spesifikasi": {"bahan": "mesh", "berat": 280, "dimensi": {"lebar": 11, "tinggi": 9, "panjang": 31}}}
(1 row)
tag_tanpa_pertama
-------------------
["diskon"]
(1 row)
tanpa_berat
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"tag": ["olahraga", "diskon"], "stok": 25, "harga": 750000, "merek": "Elang", "spesifikasi": {"bahan": "mesh", "dimensi": {"lebar": 11, "tinggi": 9, "panjang": 31}}}
(1 row)
tag_tanpa_pertama
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"tag": ["diskon"], "stok": 25, "harga": 750000, "merek": "Elang", "spesifikasi": {"bahan": "mesh", "berat": 280, "dimensi": {"lebar": 11, "tinggi": 9, "panjang": 31}}}
(1 row)
jalur_dihapus
---------------
{"a": {}}
(1 row)Lima pengamatan dari hasil di atas:
info - 'tag'membuang seluruh pasangantagbeserta array-nya dari objek.(info -> 'tag') - 0membuang elemen ke-0 dari array — hasil["diskon"]. Tanda kurung di sini wajib: operator+/-terikat lebih erat daripada->, jadi tanpa kurung, parser membaca'tag' - 0lebih dulu dan gagal dengan errorinvalid input syntax for type integer: "tag".info #- '{spesifikasi,berat}'menghapusberatdi dalam objek bersarang, menyisakanspesifikasiyang lebih muda.info #- '{tag,0}'memadukan kunci dan indeks dalam satu path.'{"a": {"b": {"c": 1}}}'::jsonb #- '{a,b}'membuangbdua tingkat di bawah akar, menyisakan{"a": {}}.
Semua operator pengubahan ini mengembalikan dokumen baru — kolom aslinya tidak berubah sampai Anda menjalankan UPDATE. Teknik mengubah dokumen secara penuh (memasukkan nilai di tengah array, mengganti nilai pada path, sampai pola UPDATE yang aman untuk concurrency) dibahas khusus di chapter Mengubah Data JSON.
Dengan peta operator lengkap di tangan, membaca dan menyaring dokumen jsonb menjadi soal memilih alat yang tepat: -> untuk satu nilai, @> untuk penyaringan berbasis index, ? untuk keanggotaan level atas, || dan - untuk merakit-ulang dokumen. Pengetahuan ini adalah fondasi bagi topik lanjutan section ini: mengubah dokumen secara terarah dengan jsonb_set, lalu merangkai baris tabel menjadi dokumen dengan json_agg.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Katalog produk untuk peta operator JSON dan JSONB.
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}}');
containment.sql
Unduh berkas-- Arah menentukan: @> menguji "sisi kiri memuat sisi kanan".
SELECT '{"merek": "Elang", "harga": 750000}'::jsonb @> '{"merek": "Elang"}' AS kiri_memuat_kanan;
-- Dibalik arahnya, dokumen kecil tidak memuat dokumen besar.
SELECT '{"merek": "Elang"}'::jsonb @> '{"merek": "Elang", "harga": 750000}' AS kanan_memuat_kiri;
-- <@ adalah @> dengan arah dibalik: kiri terkandung dalam kanan.
SELECT '{"merek": "Elang"}'::jsonb <@ '{"merek": "Elang", "harga": 750000}' AS kiri_terkandung;
-- Angka dibandingkan secara NUMERIK: 25 dan 25.0 dianggap sama.
SELECT '{"stok": 25}'::jsonb @> '{"stok": 25.0}'::jsonb AS angka_sama_numerik;
-- Teks "25" dan angka 25 adalah dua nilai JSON yang berbeda.
SELECT '{"stok": 25}'::jsonb @> '{"stok": "25"}'::jsonb AS teks_vs_angka;
-- Penampung objek bersarang bersifat PARSIAL:
-- cukup pasangan yang disebut hadir, kunci lain boleh ikut.
SELECT info @> '{"spesifikasi": {"berat": 280}}' AS spek_cocok
FROM produk
WHERE id = 1;
-- Bentuk pemakaian paling umum: penyaringan baris di WHERE.
SELECT nama
FROM produk
WHERE info @> '{"tag": ["olahraga"]}'
ORDER BY id;
SELECT nama
FROM produk
WHERE info @> '{"spesifikasi": {"tahan_air": true}}'
ORDER BY id;
keanggotaan.sql
Unduh berkas-- ? menguji keberadaan kunci pada LEVEL TERATAS dokumen.
SELECT nama
FROM produk
WHERE info ? 'garansi'
ORDER BY id;
-- 'berat' hanya hidup di dalam objek spesifikasi, bukan level teratas;
-- hasilnya nol baris -- inilah jebakan klasik operator ?.
SELECT count(*) AS punya_berat_level_atas
FROM produk
WHERE info ? 'berat';
-- Turun dulu satu tingkat, baru uji keanggotaan pada objek itu.
SELECT count(*) AS punya_berat_di_spek
FROM produk
WHERE info -> 'spesifikasi' ? 'berat';
-- Pada array, ? menguji KEANGGOTAN ELEMEN, bukan kunci.
SELECT nama
FROM produk
WHERE info -> 'tag' ? 'diskon'
ORDER BY id;
-- ?& menuntut SEMUA kunci hadir.
SELECT nama
FROM produk
WHERE info ?& ARRAY['garansi', 'tag']
ORDER BY id;
-- ?| cukup SALAH SATU kunci hadir.
SELECT nama
FROM produk
WHERE info ?| ARRAY['garansi', 'stok']
ORDER BY id;
penggabungan.sql
Unduh berkas-- || menggabungkan dua objek jsonb;
-- kunci yang bentrok: nilai sisi KANAN yang menang.
SELECT '{"a": 1, "b": 2}'::jsonb || '{"b": 99, "c": 3}' AS kanan_menang;
-- Penggabungan bersifat DANGKAL: objek bersarang diganti utuh,
-- isi lamanya tidak dilebur dengan isi yang baru.
SELECT '{"spesifikasi": {"berat": 280, "bahan": "mesh"}}'::jsonb
|| '{"spesifikasi": {"bahan": "kulit"}}' AS merge_dangkal;
-- Perbarui satu kunci pada dokumen produk.
SELECT info || '{"stok": 10}' AS stok_diperbarui
FROM produk
WHERE id = 1;
-- Array || nilai: elemen baru ditambahkan di ujung array.
SELECT info -> 'tag' || '"murah"' AS tag_diperluas
FROM produk
WHERE id = 1;
penghapusan.sql
Unduh berkas-- - 'kunci': buang pasangan kunci-nilai dari objek.
SELECT info - 'tag' AS tanpa_tag
FROM produk
WHERE id = 1;
-- - indeks: buang elemen array pada posisi tersebut.
-- Tanda kurung wajib: tanpa itu, ekspresi 'tag' - 0 yang terparse lebih dulu.
SELECT (info -> 'tag') - 0 AS tag_tanpa_pertama
FROM produk
WHERE id = 1;
-- #- path text[]: buang nilai yang berdiri di ujung path.
SELECT info #- '{spesifikasi,berat}' AS tanpa_berat
FROM produk
WHERE id = 1;
-- Path boleh memuat indeks array setelah nama kunci.
SELECT info #- '{tag,0}' AS tag_tanpa_pertama
FROM produk
WHERE id = 1;
-- Penghapusan berlapis: path masuk dua tingkat ke dalam objek.
SELECT '{"a": {"b": {"c": 1}}}'::jsonb #- '{a,b}' AS jalur_dihapus;