JSON di SQL: Cara Query Data Semi Terstruktur
Blog/Tutorial SQL/JSON di SQL: Cara Query Data Semi Terstruktur

JSON di SQL: Cara Query Data Semi Terstruktur

BimaBima
·7 Juni 2026·11 menit baca

Penulis

Bima

Bima

Founder & Data Professional

Bagikan

TL;DR

Query JSON di SQL artinya ngambil nilai dari kolom yang isinya dokumen JSON, pakai operator kayak ->> di PostgreSQL atau fungsi JSON_VALUE di MySQL dan SQL Server. Kamu bisa ambil field tunggal, turun ke struktur bersarang, dan ngurai array jadi baris terpisah pakai JSON_TABLE atau jsonb_array_elements. Kuncinya: hasil ekstraksi JSON itu teks, jadi harus di-cast ke angka atau tanggal sebelum dihitung.

Query JSON di SQL artinya ngambil nilai dari kolom yang isinya dokumen JSON, tanpa perlu ngeluarin datanya dulu ke Python.

Skenarionya kayak gini. Kamu buka tabel pesanan_umkm, dan ada satu kolom namanya detail yang isinya begini:

{
  "channel": "whatsapp",
  "ongkir": 12000,
  "alamat": { "kota": "Bandung", "provinsi": "Jawa Barat" },
  "items": [
    { "nama": "Keripik Singkong", "qty": 3, "harga": 18000 },
    { "nama": "Sambal Roa",       "qty": 1, "harga": 45000 }
  ]
}

Reaksi pertama kebanyakan analis: "waduh, ini harus minta tolong backend."

Gak perlu. SQL modern udah bisa baca isi JSON langsung, dan sintaksnya lebih gampang dari yang kamu kira.

Di tutorial ini aku pakai PostgreSQL sebagai contoh utama, plus padanan sintaksnya di MySQL dan SQL Server. Datanya: tabel pesanan_umkm berisi 3.200 pesanan dari toko oleh-oleh online selama 6 bulan.

Kenapa ada kolom JSON di database?

Karena gak semua data punya bentuk yang sama tiap barisnya.

Toko UMKM tadi jualan di WhatsApp, Shopee, dan Tokopedia. Tiap kanal ngirim metadata yang beda: Shopee ngasih voucher_id, WhatsApp gak punya itu tapi punya nomor_pengirim.

Kalau tiap field dibikin kolom sendiri, tabelnya bakal punya 40 kolom yang 30-nya NULL terus. Jadi orang milih nyimpen semuanya di satu kolom JSON.

Trade-off-nya: fleksibel di sisi nyimpen, ribet di sisi baca. Bagian ribetnya itu yang mau kita selesaikan.

Aturan praktisnya: kalau kamu sering filter atau agregasi suatu field, pindahin dia ke kolom biasa. JSON buat yang jarang dipakai dan bentuknya berubah-ubah.

Gimana cara ambil satu field dari JSON?

Di PostgreSQL, ada dua operator yang harus kamu hafalin:

  • -> balikin JSON (buat lanjut turun ke dalam)
  • ->> balikin teks (buat berhenti dan ambil nilainya)

Gampangnya: pakai -> di tengah jalan, ->> di ujung.

SELECT
    id_order,
    detail ->> 'channel'           AS channel,
    (detail ->> 'ongkir')::numeric AS ongkir
FROM pesanan_umkm
WHERE detail ->> 'channel' = 'whatsapp';

Perhatiin ::numeric itu. Ini bagian yang paling sering bikin orang kepeleset.

Hasil ->> selalu teks. Kalau kamu langsung SUM(detail ->> 'ongkir'), PostgreSQL bakal ngeluh. Dan di database yang lebih longgar, dia mungkin gak ngeluh — tapi ngasih hasil yang salah.

Cast dulu, baru hitung. Tiap kali.

Kalau kamu belum familiar sama casting tipe data, cek fungsi CAST dulu.

Gimana cara turun ke JSON yang bersarang?

Field alamat tadi isinya objek lagi. Buat ambil kotanya, kamu turun satu level pakai ->, terus ambil pakai ->>:

SELECT
    id_order,
    detail -> 'alamat' ->> 'kota'     AS kota,
    detail -> 'alamat' ->> 'provinsi' AS provinsi
FROM pesanan_umkm;

Kalau nested-nya dalam banget, ada jalan pintas pakai #>> dengan path array:

SELECT
    detail #>> '{alamat,kota}' AS kota
FROM pesanan_umkm;

Hasilnya sama. Pilih yang mana aja yang kamu ngerti pas baca ulang 3 bulan lagi.

Gimana cara ngurai array JSON jadi baris?

Ini bagian yang paling berguna, dan paling bikin orang stuck.

Field items isinya array. Satu pesanan bisa punya 1 item atau 8 item. Buat ngitung produk mana yang paling laku, kamu butuh tiap item jadi barisnya sendiri.

PostgreSQL punya jsonb_array_elements:

SELECT
    p.id_order,
    item ->> 'nama'             AS produk,
    (item ->> 'qty')::int       AS qty,
    (item ->> 'harga')::numeric AS harga
FROM pesanan_umkm p,
     jsonb_array_elements(p.detail -> 'items') AS item;

Satu pesanan dengan 2 item jadi 2 baris. Pesanan dengan 5 item jadi 5 baris.

Habis diurai, datanya jadi tabel biasa. Agregasi jalan normal:

SELECT
    item ->> 'nama' AS produk,
    SUM((item ->> 'qty')::int) AS total_qty,
    SUM((item ->> 'qty')::int * (item ->> 'harga')::numeric) AS omzet
FROM pesanan_umkm p,
     jsonb_array_elements(p.detail -> 'items') AS item
GROUP BY 1
ORDER BY omzet DESC
LIMIT 10;

Satu catatan penting. Kalau ada baris yang items-nya kosong atau NULL, baris itu bakal hilang dari hasil. Ini perilaku join implisit.

Kalau kamu butuh baris itu tetap muncul, pakai LEFT JOIN LATERAL:

SELECT p.id_order, item ->> 'nama' AS produk
FROM pesanan_umkm p
LEFT JOIN LATERAL jsonb_array_elements(p.detail -> 'items') AS item ON true;

Aku pernah kehilangan 47 pesanan dari laporan gara-gara lupa ini. Angkanya jadi meleset 1,5% dan baru ketahuan seminggu kemudian.

Contoh kasus: nyari kanal paling untung

Dari 3.200 pesanan toko oleh-oleh tadi, pemiliknya nanya: kanal mana yang paling menguntungkan?

Pertanyaan itu belum bisa dijawab langsung, karena "menguntungkan" bisa berarti omzet total atau nilai per pesanan. Aku ambil dua-duanya:

WITH item_flat AS (
    SELECT
        p.id_order,
        p.detail ->> 'channel' AS channel,
        (p.detail ->> 'ongkir')::numeric AS ongkir,
        (item ->> 'qty')::int * (item ->> 'harga')::numeric AS nilai_item
    FROM pesanan_umkm p,
         jsonb_array_elements(p.detail -> 'items') AS item
),
per_order AS (
    SELECT
        id_order,
        channel,
        MAX(ongkir)     AS ongkir,
        SUM(nilai_item) AS nilai_order
    FROM item_flat
    GROUP BY id_order, channel
)
SELECT
    channel,
    COUNT(*)                     AS jumlah_order,
    SUM(nilai_order)             AS omzet_total,
    ROUND(AVG(nilai_order))      AS rata_per_order,
    ROUND(AVG(ongkir))           AS rata_ongkir
FROM per_order
GROUP BY channel
ORDER BY omzet_total DESC;

Perhatiin CTE per_order. Aku pakai MAX(ongkir) bukan SUM(ongkir), karena setelah array diurai, nilai ongkir keduplikat di tiap baris item. Kalau di-SUM, ongkir pesanan dengan 5 item bakal kehitung 5 kali.

Ini jebakan klasik pas kerja sama JSON array. Field di level atas selalu keduplikat setelah unnest.

Hasilnya:

ChannelJumlah orderOmzet totalRata per order
shopee1.684Rp 142.100.000Rp 84.400
whatsapp892Rp 118.700.000Rp 133.100
tokopedia624Rp 51.300.000Rp 82.200

Shopee menang di omzet total. Tapi WhatsApp punya nilai per pesanan 58% lebih tinggi — Rp 133.100 versus Rp 84.400.

Waktu aku urai lebih jauh, ketahuan kenapa: pesanan WhatsApp rata-rata isinya 3,8 item, sementara Shopee cuma 1,9 item. Orang yang chat langsung cenderung nanya-nanya dulu terus nambah barang.

Keputusan yang keluar dari sini: pemiliknya nambah 1 admin WhatsApp, bukan naikin budget iklan Shopee.

Angka itu gak akan pernah kelihatan kalau kolom JSON-nya cuma dianggurin.

Sintaksnya di MySQL dan SQL Server

Konsepnya identik. Yang beda cuma nama fungsinya.

TugasPostgreSQLMySQLSQL Server
Ambil field (teks)detail ->> 'channel'detail ->> '$.channel'JSON_VALUE(detail, '$.channel')
Field bersarangdetail #>> '{alamat,kota}'detail ->> '$.alamat.kota'JSON_VALUE(detail, '$.alamat.kota')
Urai arrayjsonb_array_elements()JSON_TABLE()OPENJSON()
Cek key adadetail ? 'ongkir'JSON_CONTAINS_PATH()JSON_PATH_EXISTS()

Detail lengkap sintaks PostgreSQL ada di dokumentasi resmi fungsi JSON PostgreSQL.

Gimana bikin query JSON gak lemot?

JSON itu mahal buat dibaca. Ada tiga hal yang bikin bedanya kerasa.

Pakai JSONB, bukan JSON. Tipe json nyimpen teks mentah, jadi database harus parse ulang tiap kali dibaca. Tipe jsonb udah disimpen dalam bentuk terurai. Buat tabel yang sering di-query, jsonb selalu menang.

Index GIN cuma jalan buat operator containment. Ini yang bikin banyak orang bingung:

CREATE INDEX idx_pesanan_detail ON pesanan_umkm USING GIN (detail);

-- Index KEPAKAI
SELECT * FROM pesanan_umkm WHERE detail @> '{"channel": "whatsapp"}';

-- Index GAK kepakai (tetap full scan)
SELECT * FROM pesanan_umkm WHERE detail ->> 'channel' = 'whatsapp';

Dua query itu ngasih hasil yang sama. Tapi cuma yang pertama yang pakai index.

Kalau kamu tetap mau pakai ->> dan tetep cepat, bikin expression index:

CREATE INDEX idx_channel ON pesanan_umkm ((detail ->> 'channel'));

Jangan unnest kalau gak perlu. Kalau kamu cuma butuh channel dan ongkir, jangan ikut ngurai items. Tiap unnest ngalikan jumlah baris yang harus diproses.

Cek selalu pakai EXPLAIN ANALYZE sebelum nyalahin JSON-nya.

Kesalahan umum

Lupa cast tipe data. SUM(detail ->> 'ongkir') itu error atau salah hasil. Selalu SUM((detail ->> 'ongkir')::numeric).

Salah pakai -> vs ->>. Kalau hasilmu keluar dengan tanda kutip ganda ("whatsapp" bukan whatsapp), berarti kamu pakai -> padahal harusnya ->>.

Nge-SUM field level atas setelah unnest. Ongkir keduplikat per item. Pakai MAX atau agregasi dulu per order sebelum dijumlah.

Nganggap key selalu ada. Kalau 200 baris gak punya key ongkir, hasilnya NULL — dan NULL diem-diem bikin rata-ratamu meleset. Pakai COALESCE(detail ->> 'ongkir', '0') kalau memang nol itu masuk akal.

Nyimpen semuanya di JSON. Kalau tiap query kamu selalu filter pakai channel, keluarin dia jadi kolom beneran. JSON bukan alasan buat males bikin skema.

FAQ

Kapan sebaiknya simpan data sebagai JSON di database?

Simpan sebagai JSON kalau strukturnya beda-beda per baris dan sering berubah. Contohnya metadata pesanan dari beberapa marketplace yang formatnya gak seragam. Kalau semua baris punya field yang sama dan kamu sering filter atau agregasi field itu, bikin kolom biasa. Kolom biasa lebih cepat, lebih ketat tipenya, dan lebih gampang dibaca orang lain.

Bedanya JSON sama JSONB di PostgreSQL apa?

Tipe JSON nyimpen teks aslinya persis, termasuk spasi dan urutan key. Tipe JSONB nyimpen dalam bentuk biner yang udah diurai, jadi urutan key hilang tapi query-nya jauh lebih cepat. JSONB juga bisa diindex pakai GIN. Buat hampir semua kasus analisis, pakai JSONB. JSON biasa cuma masuk akal kalau kamu butuh nyimpen dokumen persis apa adanya.

Kenapa hasil query JSON-ku selalu bertipe teks?

Karena operator ->> di PostgreSQL dan fungsi JSON_VALUE memang didesain balikin teks. Kalau kamu langsung jumlahin hasilnya, kamu bakal dapat error atau hasil yang salah karena teks digabung, bukan dijumlah. Selalu cast dulu: pakai ::numeric atau ::int di PostgreSQL, atau CAST(... AS DECIMAL) di MySQL. Ini kesalahan nomor satu waktu mulai kerja sama JSON.

Gimana cara query JSON di MySQL dan SQL Server?

MySQL pakai JSON_EXTRACT atau operator ->> yang mirip PostgreSQL, plus JSON_TABLE buat ngurai array jadi baris. SQL Server pakai JSON_VALUE buat field tunggal dan OPENJSON buat array. Konsepnya sama persis: ambil field, cast tipenya, urai array kalau perlu. Yang beda cuma nama fungsinya.

Query JSON-ku lemot banget, gimana benerinnya?

Tiga penyebab paling umum. Pertama, kamu filter pakai ->> tapi index GIN cuma jalan buat operator containment @>. Kedua, kamu ngurai array besar di tiap baris padahal cuma butuh satu field. Ketiga, kolomnya bertipe JSON bukan JSONB, jadi database harus parse ulang teksnya tiap kali dibaca. Cek dulu dengan EXPLAIN ANALYZE sebelum nebak.

Penutup

Yang perlu kamu bawa pulang:

  • -> buat turun, ->> buat ambil. Hasilnya selalu teks, jadi cast dulu sebelum dihitung.
  • Array diurai pakai jsonb_array_elements (atau JSON_TABLE / OPENJSON). Habis itu, dia jadi tabel biasa.
  • Hati-hati duplikasi field level atas setelah unnest. Itu penyebab angka meleset paling senyap.

Kolom JSON bukan alasan buat nyerah dan minta tolong backend. Lima operator tadi udah cukup buat 90% kerjaan.

Mau latihan query kayak gini di data beneran? Mulai dari COALESCE buat nanganin key yang kosong, terus lanjut ke CTE di SQL — query JSON yang panjang jauh lebih kebaca kalau dipecah pakai CTE.

Coba Langsung

Mau praktek langsung? Mulai latihan SQL gratis

Latihan interaktif, langsung di browser.

Buka NgulikSQL →
Bagikan:
Bima
Ditulis oleh

Bima

Founder & Data Professional

Founder Ngulik Data. Passionate about making data analysis accessible for everyone.

Artikel Terkait

7 SQL Client Gratis Terbaik buat Analis Data (2026)
Tutorial SQL
10 Juli 2026•9 menit baca

7 SQL Client Gratis Terbaik buat Analis Data (2026)

Nggak perlu bayar buat nulis query. Ini 7 SQL client gratis yang beneran dipakai analis, plus kelebihan dan kekurangannya masing-masing.

BimaBima
SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim
Tutorial SQL
7 Juli 2026•9 menit baca

SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim

Sepuluh aturan SQL style guide yang bikin query kamu kebaca sama tim — dan sama kamu sendiri 6 bulan lagi. Lengkap dengan contoh sebelum-sesudah.

BimaBima
Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya
Tutorial SQL
4 Juli 2026•11 menit baca

Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya

Contoh soal take-home test SQL yang beneran dipakai perusahaan, dikerjain step by step — dari baca soal, nulis query, sampai nyusun insight yang bikin recruiter nengok.

BimaBima
Kembali ke Blog
Ngulik Data logoNgulik Data

Platform edukasi data lengkap untuk professionals Indonesia. Belajar SQL, Data Analysis, dan lebih banyak lagi dengan praktek langsung dan feedback real-time.

Copyright © 2026 - All rights reserved

LINKS
SupportPricingDatasetBlogAffiliates
LEGAL
Terms of servicesPrivacy policy
Ngulik Data
DatasetLeaderboardBlogStore