Chapter JSON dan JSONB
pada section tipe data sudah memperkenalkan kedua tipe tersebut secara ringkas: json menyimpan teks, jsonb menyimpan bentuk biner hasil parsing. Sekarang kita bedah keputusan itu pada level yang menentukan arsitektur tabel: apa yang benar-benar berubah di penyimpanan, operator mana yang tersedia, bagaimana konsekuensi performanya, dan kapan json justru pilihan yang lebih tepat daripada jsonb. Katalog produk Toko Maju Jaya kembali menjadi arena uji — kali ini setiap dokumen disimpan dua kali, sekali di kolom json dan sekali di kolom jsonb, supaya setiap perbedaan terlihat berdampingan dalam query yang sama.
Format Penyimpanan: Teks Apa Adanya vs Biner Terurai #
Mekanismenya berbeda sejak baris pertama masuk. Kolom json menyimpan salinan teks input persis apa adanya; setiap kali operator atau fungsi memproses nilai tersebut, server harus mengurai ulang teksnya dari awal. Kolom jsonb sebaliknya mengurai dokumen saat input, lalu menyimpannya dalam format biner terstruktur — mirip bedanya string SQL dengan pohon sintaks yang sudah siap pakai. Konsekuensinya: jsonb sedikit lebih lambat saat penyimpanan karena biaya konversi, tetapi jauh lebih cepat saat diproses karena tidak perlu mengurai ulang.
-- Produk Toko Maju Jaya: dokumen yang sama disimpan dua kali,
-- sekali sebagai json (teks apa adanya), sekali sebagai jsonb (biner terurai).
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
info_json json NOT NULL,
info_jsonb jsonb NOT NULL
);
INSERT INTO produk (id, nama, info_json, info_jsonb) VALUES
(1, 'Sepatu Lari',
'{ "merek" : "Elang", "harga" : 750000, "tag" : [ "olahraga", "diskon" ] }',
'{ "merek" : "Elang", "harga" : 750000, "tag" : [ "olahraga", "diskon" ] }'),
(2, 'Kemeja Batik',
'{"spesifikasi": {"bahan": "katun", "berat": 220}, "merek": "Nusantara"}',
'{"spesifikasi": {"bahan": "katun", "berat": 220}, "merek": "Nusantara"}');
SELECT id, info_json, info_jsonb
FROM produk
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
id | info_json | info_jsonb
----+---------------------------------------------------------------------------+-------------------------------------------------------------------------
1 | { "merek" : "Elang", "harga" : 750000, "tag" : [ "olahraga", "diskon" ] } | {"tag": ["olahraga", "diskon"], "harga": 750000, "merek": "Elang"}
2 | {"spesifikasi": {"bahan": "katun", "berat": 220}, "merek": "Nusantara"} | {"merek": "Nusantara", "spesifikasi": {"bahan": "katun", "berat": 220}}
(2 rows)Baris pertama hasil SELECT memperlihatkan perbedaan itu secara langsung. Teks input sengaja ditulis dengan spasi longgar di sekitar tanda titik dua dan spasi jamak di dalam array; kolom info_json mengembalikannya persis seperti diketik, sedangkan kolom info_jsonb mengembalikan versi rapit tanpa spasi. Perhatikan juga urutan kunci: input menulis "merek" paling depan, tetapi info_jsonb menatanya ulang menjadi "tag", "harga", "merek" — jsonb mengurutkan kunci menurut panjang lalu urutan byte saat normalisasi. Pada baris kedua, spesifikasi yang ditulis paling depan bergeser ke belakang oleh aturan yang sama.
Normalisasi Dokumen: Kunci Kembar dan Tipe Hasil #
Dua perilaku jsonb lainnya mengikuti logika normalisasi yang sama. Pertama, dokumen dengan kunci kembar: standar JSON memperbolehkan kunci yang sama muncul lebih dari sekali, dan kedua tipe menyikapinya berbeda. Kedua, tipe data SQL dari kolom itu sendiri — json dan jsonb adalah dua tipe yang berbeda, bukan satu tipe dengan dua mode.
-- Kunci kembar: json menyimpan SEMUA pasangan dalam teks,
-- jsonb hanya menyimpan nilai terakhir.
SELECT '{"harga": 1000, "harga": 2000}'::json AS json_duplikat;
SELECT '{"harga": 1000, "harga": 2000}'::jsonb AS jsonb_duplikat;
-- Tipe data yang berbeda menghasilkan tipe hasil yang berbeda pula.
SELECT pg_typeof(info_json) AS tipe_kolom_json,
pg_typeof(info_jsonb) AS tipe_kolom_jsonb
FROM produk
WHERE id = 1;
-- jsonb menyimpan dokumen terurai, jadi bisa dicetak rapi;
-- json tidak punya fungsi serupa karena teksnya memang tidak pernah diurai.
SELECT jsonb_pretty(info_jsonb) AS info_jsonb_cantik
FROM produk
WHERE id = 2;Output yang Dihasilkan:
json_duplikat
--------------------------------
{"harga": 1000, "harga": 2000}
(1 row)
jsonb_duplikat
-----------------
{"harga": 2000}
(1 row)
tipe_kolom_json | tipe_kolom_jsonb
-----------------+------------------
json | jsonb
(1 row)
info_jsonb_cantik
---------------------------
{ +
"merek": "Nusantara",+
"spesifikasi": { +
"bahan": "katun",+
"berat": 220 +
} +
}
(1 row)Dokumen {"harga": 1000, "harga": 2000} disimpan json lengkap dengan kedua pasangan kembar, sementara jsonb hanya menyimpan pasangan terakhir: {"harga": 2000}. Fungsi pemrosesan json tetap membaca nilai terakhir saat mengekstrak kunci kembar, jadi hasil pembacaan sama — yang berbeda adalah isi penyimpanan di disk. Query pg_typeof menegaskan bahwa json dan jsonb adalah tipe SQL yang berbeda. Terakhir, jsonb_pretty mencetak dokumen biner dengan indentasi — fitur yang hanya masuk akal untuk jsonb karena bentuk teks aslinya memang sudah dibuang; untuk json, teks rapi atau berantakan tergantung bagaimana ia ditulis, bukan bagaimana ia dicetak.
Dukungan Operator dan Fungsi #
Karena json dan jsonb adalah tipe berbeda, keduanya tidak mendukung operator yang sama. Operator akses dasar — ->, ->>, #>, #>> — tersedia untuk kedua tipe. Selebihnya milik eksklusif jsonb:
| Operator / fungsi | json | jsonb |
|---|---|---|
->, ->>, #>, #>> (akses nilai) | Ya | Ya |
@>, <@, ?, ?&, `? | ` (penampung dan keanggotaan) | Tidak |
||, -, #- (penggabungan dan penghapusan) | Tidak | Ya |
jsonb_set, jsonb_insert, jsonb_strip_nulls | Tidak | Ya |
jsonb_path_query dan keluarga JSONPath | Tidak | Ya |
| Index GIN pada kolom | Tidak | Ya |
Bukti langsungnya bisa dijalankan. Operator -> bekerja pada kedua kolom dengan hasil yang sama persis, dan @> berjalan mulus di kolom jsonb:
-- Operator akses dasar (->, ->>) tersedia untuk json DAN jsonb.
SELECT info_json -> 'merek' AS dari_json,
info_jsonb -> 'merek' AS dari_jsonb
FROM produk
WHERE id = 1;
-- Operator penampung @> hanya ada untuk jsonb.
SELECT info_jsonb @> '{"merek": "Elang"}' AS jsonb_memuat
FROM produk
WHERE id = 1;Output yang Dihasilkan:
dari_json | dari_jsonb
-----------+------------
"Elang" | "Elang"
(1 row)
jsonb_memuat
--------------
t
(1 row)Saat operator yang sama dicoba pada kolom json, server menolaknya sejak parsing — operator tersebut memang tidak pernah didefinisikan untuk tipe json:
-- Kolom json tidak mengenal operator @>:
-- operator penampung hanya didefinisikan untuk jsonb.
SELECT info_json @> '{"merek": "Elang"}'
FROM produk
WHERE id = 1;Output yang Dihasilkan:
psql:operator-json-gagal.sql:5: ERROR: operator does not exist: json @> unknown
LINE 1: SELECT info_json @> '{"merek": "Elang"}'
^
HINT: No operator matches the given name and argument types. You might need to add explicit type casts.Pesan operator does not exist: json @> unknown lengkap dengan HINT tentang explicit cast adalah penolakan tingkat parser, bukan kesalahan data. Kalau Anda benar-benar butuh operasi penampung pada kolom json, satu-satunya jalan adalah mengonversi dulu lewat cast ::jsonb — yang berarti membayar biaya parsing ulang pada setiap baris, setiap kali query dijalankan.
Performa dan Index #
Trade-off performanya mengikuti format penyimpanannya. Menulis ke kolom jsonb lebih mahal karena server harus mengurai teks, menormalkan urutan kunci, membuang duplikat, lalu menyusun bentuk binernya; menulis ke kolom json hampir cuma biaya validasi sintaks dan penyimpanan teks. Namun di sisi pembacaan posisinya terbalik: setiap operator pada json harus mengurai ulang seluruh teks dokumen, sedangkan jsonb bekerja langsung pada struktur biner yang sudah siap. Semakin sering dokumen dikueri — bukan sekadar disimpan — semakin besar keunggulan jsonb.
Keunggulan terbesarnya ada di index: hanya jsonb yang bisa diindeks dengan GIN sehingga query penampung seperti info @> '{"tag": ["diskon"]}' tidak lagi memindai seluruh tabel. Pembahasan lengkap strategi index untuk JSONB ada di chapter Index pada JSONB
; untuk sekarang, cukup dicatat bahwa opsi tersebut sama sekali tidak tersedia untuk json.
Kapan json Tetap Tepat: Jejak Audit Payload #
Ada satu pola pemakaian tempat json justru unggul: saat bentuk asli payload harus terpelihara. Contoh klasiknya tabel jejak audit webhook — aplikasi Anda menerima payload dari pihak ketiga lalu menyimpannya sebagai bukti, termasuk spasi dan urutan kuncinya. Saat terjadi sengketa integrasi, Anda bisa menunjukkan payload persis seperti yang diterima, bukan versi yang sudah dinormalkan server Anda.
-- Kasus pemakaian json: jejak audit payload webhook yang harus
-- tersimpan PERSIS seperti diterima, spasi dan urutan kunci termasuk.
CREATE TABLE log_payload (
id integer PRIMARY KEY,
diterima timestamp NOT NULL,
payload json NOT NULL
);
INSERT INTO log_payload (id, diterima, payload) VALUES
(1, TIMESTAMP '2026-01-15 08:00:00',
'{ "event" : "order.created", "versi" : "2.1", "total" : 750000 }'),
(2, TIMESTAMP '2026-01-15 08:05:00',
'{"event":"order.paid","versi":"2.1","total":750000}');
SELECT id, payload
FROM log_payload
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
id | payload
----+-------------------------------------------------------------------
1 | { "event" : "order.created", "versi" : "2.1", "total" : 750000 }
2 | {"event":"order.paid","versi":"2.1","total":750000}
(2 rows)Dua baris payload sengaja ditulis dengan gaya berbeda — baris pertama longgar dengan spasi, baris kedua padat tanpa spasi — dan keduanya kembali persis seperti disimpan. Pola serupa berlaku untuk pass-through: proxy atau integrasi yang hanya meneruskan dokumen ke sistem lain tanpa memproses isinya tidak memperoleh keuntungan apa pun dari normalisasi jsonb, jadi menyimpan sebagai json menghemat biaya konversi di kedua arah.
Kesimpulan praktisnya satu kalimat: kecuali Anda punya kebutuhan khusus mempertahankan bentuk asli teks — audit payload, arsip, pass-through — gunakan jsonb sebagai pilihan default, persis seperti rekomendasi dokumentasi resmi PostgreSQL. Keputusan tipe sudah mantap; langkah berikutnya adalah praktik menyimpan dokumen ke kolom jsonb dengan benar: dari literal, dari cast teks, sampai membangun dokumen lewat fungsi — materi chapter Menyimpan Data JSON.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Produk Toko Maju Jaya: dokumen yang sama disimpan dua kali,
-- sekali sebagai json (teks apa adanya), sekali sebagai jsonb (biner terurai).
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
info_json json NOT NULL,
info_jsonb jsonb NOT NULL
);
INSERT INTO produk (id, nama, info_json, info_jsonb) VALUES
(1, 'Sepatu Lari',
'{ "merek" : "Elang", "harga" : 750000, "tag" : [ "olahraga", "diskon" ] }',
'{ "merek" : "Elang", "harga" : 750000, "tag" : [ "olahraga", "diskon" ] }'),
(2, 'Kemeja Batik',
'{"spesifikasi": {"bahan": "katun", "berat": 220}, "merek": "Nusantara"}',
'{"spesifikasi": {"bahan": "katun", "berat": 220}, "merek": "Nusantara"}');
SELECT id, info_json, info_jsonb
FROM produk
ORDER BY id;
dukungan-operator.sql
Unduh berkas-- Operator akses dasar (->, ->>) tersedia untuk json DAN jsonb.
SELECT info_json -> 'merek' AS dari_json,
info_jsonb -> 'merek' AS dari_jsonb
FROM produk
WHERE id = 1;
-- Operator penampung @> hanya ada untuk jsonb.
SELECT info_jsonb @> '{"merek": "Elang"}' AS jsonb_memuat
FROM produk
WHERE id = 1;
kapan-json.sql
Unduh berkas-- Kasus pemakaian json: jejak audit payload webhook yang harus
-- tersimpan PERSIS seperti diterima, spasi dan urutan kunci termasuk.
CREATE TABLE log_payload (
id integer PRIMARY KEY,
diterima timestamp NOT NULL,
payload json NOT NULL
);
INSERT INTO log_payload (id, diterima, payload) VALUES
(1, TIMESTAMP '2026-01-15 08:00:00',
'{ "event" : "order.created", "versi" : "2.1", "total" : 750000 }'),
(2, TIMESTAMP '2026-01-15 08:05:00',
'{"event":"order.paid","versi":"2.1","total":750000}');
SELECT id, payload
FROM log_payload
ORDER BY id;
normalisasi.sql
Unduh berkas-- Kunci kembar: json menyimpan SEMUA pasangan dalam teks,
-- jsonb hanya menyimpan nilai terakhir.
SELECT '{"harga": 1000, "harga": 2000}'::json AS json_duplikat;
SELECT '{"harga": 1000, "harga": 2000}'::jsonb AS jsonb_duplikat;
-- Tipe data yang berbeda menghasilkan tipe hasil yang berbeda pula.
SELECT pg_typeof(info_json) AS tipe_kolom_json,
pg_typeof(info_jsonb) AS tipe_kolom_jsonb
FROM produk
WHERE id = 1;
-- jsonb menyimpan dokumen terurai, jadi bisa dicetak rapi;
-- json tidak punya fungsi serupa karena teksnya memang tidak pernah diurai.
SELECT jsonb_pretty(info_jsonb) AS info_jsonb_cantik
FROM produk
WHERE id = 2;
operator-json-gagal.sql
Unduh berkas-- Kolom json tidak mengenal operator @>:
-- operator penampung hanya didefinisikan untuk jsonb.
SELECT info_json @> '{"merek": "Elang"}'
FROM produk
WHERE id = 1;