Kita telah memakai fungsi pembaca path, fungsi modifikasi dokumen, dan fungsi pencarian — namun keluarga fungsi JSON MySQL masih menyimpan satu rak lagi yang perlu kita kenal. Fungsi-fungsi ini bukan pembaca maupun penulis, melainkan alat ukur, alat uji, alat gabung, dan alat olah bentuk: berapa dalam dokumen itu, kunci apa saja yang dimiliknya, apakah dokumen berisi nilai tertentu, bagaimana menggabungkan dua dokumen tanpa kehilangan data, dan bagaimana mengubah array menjadi baris supaya bisa dikenakan WHERE biasa. Pada chapter ini kita mengulasnya satu per satu dengan contoh pada tabel produk, menutup dengan JSON_TABLE yang menjadi pintu antara dunia JSON dan dunia relasional.

Menyiapkan Tabel Latihan #

Tabel produk(id, nama, atribut) kembali menjadi arena latihan di database belajar_json. Tiga dokumen sengaja dibuat bertingkat berbeda: Kopi Gayo datar, Mug Keramik punya object bersarang spesifikasi, dan Teh Melati paling ramping:

sql
DROP DATABASE IF EXISTS belajar_json;
CREATE DATABASE belajar_json;
USE belajar_json;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL,
  atribut JSON NOT NULL
);

INSERT INTO produk (nama, atribut) VALUES
  ('Kopi Gayo 250g', '{"warna": "coklat", "berat": 250, "harga": 55000, "tag": ["arabika", "toraja"]}'),
  ('Mug Keramik', '{"warna": "putih", "berat": 350, "harga": 45000, "tag": ["keramik", "microwave"], "spesifikasi": {"bahan": "keramik", "kapasitas": "350 ml"}}'),
  ('Teh Melati 100g', '{"berat": 100, "harga": 25000, "tag": ["teh", "melati"]}');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

Mengukur Dokumen: JSON_LENGTH, JSON_DEPTH, JSON_KEYS #

Tiga fungsi pengukur menjawab tiga pertanyaan bentuk dokumen. JSON_LENGTH menghitung jumlah anggota tingkat teratas, JSON_DEPTH menghitung tingkat bersarang paling dalam, dan JSON_KEYS mendaftar kunci tingkat teratas. Mari kita uji perilaku dasarnya pada literal kecil lebih dulu:

sql
SELECT
  JSON_LENGTH('17')                        AS panjang_skalar,
  JSON_LENGTH('{"a": [1, 2], "b": 3}')     AS panjang_object,
  JSON_DEPTH('17')                         AS kedalaman_skalar,
  JSON_DEPTH('{"a": [1, 2]}')              AS kedalaman_bersarang;

Output yang Dihasilkan:

text
+----------------+----------------+------------------+---------------------+
| panjang_skalar | panjang_object | kedalaman_skalar | kedalaman_bersarang |
+----------------+----------------+------------------+---------------------+
|              1 |              2 |                1 |                   3 |
+----------------+----------------+------------------+---------------------+
1 row in set (0.00 sec)

Hasilnya memperlihatkan aturan yang penting dipahami. JSON_LENGTH pada skalar menghasilkan 1, sedangkan pada object menghasilkan jumlah pasangan kunci-nilai di tingkat teratas — nilai 2 untuk {"a": [1, 2], "b": 3} karena array [1, 2] dihitung sebagai satu anggota, bukan dua. JSON_DEPTH pada skalar menghasilkan 1, dan setiap lapis pembungkus menambah satu: object pembungkus plus array di dalamnya menghasilkan kedalaman 3. Terapkan ketiga pengukur pada tabel produk:

sql
SELECT
  nama,
  JSON_LENGTH(atribut) AS jumlah_anggota,
  JSON_DEPTH(atribut)  AS kedalaman,
  JSON_KEYS(atribut)   AS daftar_kunci
FROM produk
ORDER BY id;

Output yang Dihasilkan:

text
+-----------------+----------------+-----------+---------------------------------------------------+
| nama            | jumlah_anggota | kedalaman | daftar_kunci                                      |
+-----------------+----------------+-----------+---------------------------------------------------+
| Kopi Gayo 250g  |              4 |         3 | ["tag", "berat", "harga", "warna"]                |
| Mug Keramik     |              5 |         3 | ["tag", "berat", "harga", "warna", "spesifikasi"] |
| Teh Melati 100g |              3 |         3 | ["tag", "berat", "harga"]                         |
+-----------------+----------------+-----------+---------------------------------------------------+
3 rows in set (0.00 sec)

Kopi Gayo memiliki empat anggota dengan kedalaman tiga — terdalamnya adalah elemen string di dalam array tag. Mug Keramik memiliki lima anggota dan kedalaman yang sama, hanya bahwa lapisan ketiganya berada di dalam spesifikasi. JSON_KEYS mengembalikan kunci dalam urutan internal biner yang telah kita bahas di chapter pertama: tag, warna, berat, harga — kunci pendek selalu lebih dulu. Ketiga pengukur ini berguna untuk audit kualitas data, misalnya menemukan baris yang dokumennya ternyata lebih dangkal dari seharusnya.

Menguji Keanggotaan dengan JSON_CONTAINS #

Fungsi JSON_CONTAINS menguji apakah sebuah nilai kandidat terkandung dalam dokumen atau bagian dokumen. Argumen pertama adalah dokumen target, argumen kedua nilai kandidat dalam bentuk teks JSON, dan argumen ketiga opsional berupa path yang mempersempit area pencarian:

sql
SELECT
  nama,
  JSON_CONTAINS(atribut, '"arabika"', '$.tag') AS bertag_arabika,
  JSON_CONTAINS(atribut, '350', '$.berat')     AS beratnya_350
FROM produk
ORDER BY id;

Output yang Dihasilkan:

text
+-----------------+----------------+--------------+
| nama            | bertag_arabika | beratnya_350 |
+-----------------+----------------+--------------+
| Kopi Gayo 250g  |              1 |            0 |
| Mug Keramik     |              0 |            1 |
| Teh Melati 100g |              0 |            0 |
+-----------------+----------------+--------------+
3 rows in set (0.00 sec)

Kolom pertama menghasilkan 1 hanya untuk Kopi Gayo karena string "arabika" menjadi salah satu elemen array yang ditunjuk path $.tag. Kolom kedua menghasilkan 1 hanya untuk Mug Keramik karena path $.berat menunjuk nilai 350 pada dokumen tersebut. Perhatikan bahwa kandidat selalu ditulis sebagai teks JSON: string harus berkutip ('"arabika"'), angka ditulis polos ('350'). Perlu diketahui pula bahwa kandidat skalar tidak pernah ditemukan pada dokumen akar bertipe object tanpa path — skalar diuji terhadap array lewat $.tag, atau terhadap satu nilai lewat path spesifik. Kandidat berupa object pun sah, dan pengujian bersifat parsial — cukup anggota kandidat ditemukan di dalam object target:

sql
SELECT
  JSON_CONTAINS('{"warna": "putih", "berat": 350}', '{"berat": 350}') AS objek_parsial,
  JSON_CONTAINS('{"warna": "putih", "berat": 350}', '{"berat": 250}') AS objek_tak_cocok;

Output yang Dihasilkan:

text
+---------------+-----------------+
| objek_parsial | objek_tak_cocok |
+---------------+-----------------+
|             1 |               0 |
+---------------+-----------------+
1 row in set (0.00 sec)

Kandidat {"berat": 350} ditemukan dalam dokumen yang memuat pasangan yang sama, sedangkan kandidat dengan nilai 250 gagal. Pola “object parsial” ini menjawab kebutuhan menyaring array of object tanpa filter path: uji keberadaan kombinasi kunci-nilai tertentu, bukan menulusuri satu per satu.

Menggabungkan Dokumen: JSON_MERGE_PATCH versus JSON_MERGE_PRESERVE #

Dua fungsi merge menggabungkan dua dokumen menjadi satu, namun dengan filosofi berlawanan ketika kunci yang sama muncul di kedua sisi. JSON_MERGE_PATCH menganut semantik PATCH ala RFC 7396: nilai dari dokumen kedua selalu menggantikan nilai pertama, dan kunci dengan nilai null menghapus kunci dari hasil. JSON_MERGE_PRESERVE lebih pelindung: nilai lama dipertahankan, nilai array digabung menjadi satu array, dan nilai skalar dikemas menjadi array berisi kedua nilai. Mari kita adu keduanya pada pasangan dokumen yang sama:

sql
SELECT
  JSON_MERGE_PATCH(
    '{"tag": ["arabika"], "harga": 55000, "warna": "coklat"}',
    '{"tag": ["organik"], "harga": 60000, "stok": 10}') AS hasil_patch,
  JSON_MERGE_PRESERVE(
    '{"tag": ["arabika"], "harga": 55000, "warna": "coklat"}',
    '{"tag": ["organik"], "harga": 60000, "stok": 10}') AS hasil_preserve;

Output yang Dihasilkan:

text
+---------------------------------------------------------------------+-----------------------------------------------------------------------------------------+
| hasil_patch                                                         | hasil_preserve                                                                          |
+---------------------------------------------------------------------+-----------------------------------------------------------------------------------------+
| {"tag": ["organik"], "stok": 10, "harga": 60000, "warna": "coklat"} | {"tag": ["arabika", "organik"], "stok": 10, "harga": [55000, 60000], "warna": "coklat"} |
+---------------------------------------------------------------------+-----------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

Perbedaannya tegas pada dua kunci yang bertabrakan. JSON_MERGE_PATCH membuang array lama: tag menjadi ["organik"] dan harga menjadi 60000 — kunci warna dan stok yang tidak bertabrakan tetap utuh. JSON_MERGE_PRESERVE justru menjaga keduanya: tag menjadi ["arabika", "organik"] dan harga menjadi array [55000, 60000] — dua nilai yang sama-sama tersimpan. Pilih JSON_MERGE_PATCH saat dokumen kedua memang pembaruan yang berhak menimpa; pilih JSON_MERGE_PRESERVE saat kedua dokumen adalah sumber setara yang datanya tidak boleh hilang, misalnya menggabungkan tag dari dua marketplace.

Konstruktor Dokumen: JSON_OBJECT dan JSON_ARRAY #

Dua fungsi pembangun dokumen memungkinkan kita menyusun JSON dari nilai SQL, bukan mengetik teks JSON mentah. JSON_OBJECT menerima pasangan kunci-nilai bergantian, sedangkan JSON_ARRAY menerima daftar nilai. Keduanya dapat bersarang dan langsung dipakai di INSERT:

sql
INSERT INTO produk (nama, atribut) VALUES
  ('Susu Kental 375g',
   JSON_OBJECT('berat', 375, 'harga', 18000, 'tag', JSON_ARRAY('susu', 'kondensasi')));

SELECT nama, atribut FROM produk WHERE id = 4;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+------------------+---------------------------------------------------------------+
| nama             | atribut                                                       |
+------------------+---------------------------------------------------------------+
| Susu Kental 375g | {"tag": ["susu", "kondensasi"], "berat": 375, "harga": 18000} |
+------------------+---------------------------------------------------------------+
1 row in set (0.00 sec)

Konstruktor menangani konversi tipe secara otomatis: angka 375 menjadi angka JSON, dan string 'susu' menjadi string JSON tanpa perlu kita menulis kutip ganda manual. Manfaat terbesarnya muncul saat nilai berasal dari kolom SQL — misalnya merakit dokumen laporan dari data relasional:

sql
SELECT JSON_OBJECT('nama', nama, 'harga', atribut->>'$.harga') AS kartu_produk
FROM produk
ORDER BY id;

Output yang Dihasilkan:

text
+------------------------------------------------+
| kartu_produk                                   |
+------------------------------------------------+
| {"nama": "Kopi Gayo 250g", "harga": "55000"}   |
| {"nama": "Mug Keramik", "harga": "45000"}      |
| {"nama": "Teh Melati 100g", "harga": "25000"}  |
| {"nama": "Susu Kental 375g", "harga": "18000"} |
+------------------------------------------------+
4 rows in set (0.00 sec)

Setiap baris produk kini terbungkus object JSON yang rapi, dengan nama diambil dari kolom relasional dan harga ditarik dari dalam dokumen atribut. Pola konstruktor-plus-path seperti ini adalah jembatan dua arah antara kolom biasa dan JSON.

Menampilkan Dokumen Rapi dengan JSON_PRETTY #

Dokumen JSON tampil default satu baris panjang — hemat ruang tetapi melelahkan mata. Fungsi JSON_PRETTY mencetak ulang dokumen dengan indentasi dan baris per tingkat:

sql
SELECT JSON_PRETTY(atribut) AS dokumen_rapi
FROM produk
WHERE id = 2;

Output yang Dihasilkan:

text
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| dokumen_rapi                                                                                                                                                                      |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {
  "tag": [
    "keramik",
    "microwave"
  ],
  "berat": 350,
  "harga": 45000,
  "warna": "putih",
  "spesifikasi": {
    "bahan": "keramik",
    "kapasitas": "350 ml"
  }
} |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

Setiap anggota object kini menempati barisnya sendiri, dan object spesifikasi ter-indent dua spasi lebih dalam. Fungsi ini hanya memengaruhi tampilan, bukan isi — dokumen yang tersimpan di kolom tetap dalam format biner ringkas. JSON_PRETTY paling sering dipakai untuk inspeksi manual dokumen besar di hasil SELECT, log, atau tahap debugging.

Mengubah Array menjadi Baris: Pengantar JSON_TABLE #

Fungsi JSON_TABLE menempati kelas tersendiri: ia dipakai di klausa FROM, bukan di daftar SELECT. Tugasnya memecah array JSON menjadi baris-tabel semu yang bisa di-WHERE, di-JOIN, dan di-agregasi seperti tabel biasa — inilah jawaban resmi atas ketiadaan filter path yang kita buktikan di chapter JSON Path. Bentuk paling sederhananya mengubah array tag setiap produk menjadi baris:

sql
SELECT p.nama, t.tag
FROM produk p,
JSON_TABLE(p.atribut, '$.tag[*]'
  COLUMNS (tag VARCHAR(50) PATH '$')) AS t
ORDER BY p.nama, t.tag;

Output yang Dihasilkan:

text
+------------------+------------+
| nama             | tag        |
+------------------+------------+
| Kopi Gayo 250g   | arabika    |
| Kopi Gayo 250g   | toraja     |
| Mug Keramik      | keramik    |
| Mug Keramik      | microwave  |
| Susu Kental 375g | kondensasi |
| Susu Kental 375g | susu       |
| Teh Melati 100g  | melati     |
| Teh Melati 100g  | teh        |
+------------------+------------+
8 rows in set (0.00 sec)

Mari kita bedah bagiannya satu per satu. FROM produk p, JSON_TABLE(...) meletakkan fungsi berdampingan dengan tabel produk — koma di sini menandakan pemrosesan per baris: untuk setiap baris produk, JSON_TABLE menjalankan satu kali terhadap dokumen baris tersebut. Argumen p.atribut adalah sumber dokumen dan '$.tag[*]' path yang menunjuk seluruh elemen array. Klausa COLUMNS (tag VARCHAR(50) PATH '$') mendefinisikan kolom hasil: setiap elemen array dimuat ke kolom tag bertipe VARCHAR(50) lewat path $ — akar dari elemen yang sedang diproses. Hasil akhirnya empat produk × dua tag = delapan baris; Susu Kental menyumbang dua baris karena dokumennya juga punya array tag.

Karena hasilnya adalah baris biasa, klausa agregasi bekerja penuh — menghitung jumlah tag per produk kini sesederhana GROUP BY:

sql
SELECT p.nama, COUNT(*) AS jumlah_tag
FROM produk p,
JSON_TABLE(p.atribut, '$.tag[*]'
  COLUMNS (tag VARCHAR(50) PATH '$')) AS t
GROUP BY p.nama
ORDER BY p.nama;

Output yang Dihasilkan:

text
+------------------+------------+
| nama             | jumlah_tag |
+------------------+------------+
| Kopi Gayo 250g   |          2 |
| Mug Keramik      |          2 |
| Susu Kental 375g |          2 |
| Teh Melati 100g  |          2 |
+------------------+------------+
4 rows in set (0.00 sec)

Membersihkan Database Latihan #

Tur fungsi JSON selesai dan database latihan kita jatuhkan:

sql
DROP DATABASE belajar_json;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Kotak perkakas JSON kini lengkap di tangan kita: pengukur dokumen, pengujis keanggotaan, penggabung dengan dua semantik, konstruktor, pemformat, dan JSON_TABLE sebagai jembatan ke dunia relasional. Satu masalah besar masih tersisa: semua query di atas menulusuri dokumen per baris — jalur lambat yang tidak bisa memakai index. Chapter berikutnya memperkenalkan kolom generated dari JSON, pintu untuk meng-index nilai di dalam dokumen dan menyaringnya secepat kolom biasa.