Kita telah menguji seluruh kotak perkakas JSON — dari penyimpanan tervalidasi hingga kolom generated ber-index — namun satu pertanyaan desain belum terjawab: kapan sebaiknya kita memakai JSON, dan kapan kolom relasional klasik tetap pilihan yang benar? JSON menjanjikan kelenturan skema, tetapi kelenturan itu dibayar dengan ketiadaan relasi, ketiadaan tipe ketat, dan jalur query yang lebih lambat. Kolom relasional menjanjikan integritas dan kecepatan, tetapi kaku terhadap perubahan struktur data. Pada chapter penutup section ini kita menimbang kedua kubu secara adil, menyusun tabel keputusan, membangun skema hibrida yang memakai keduanya sekaligus, lalu membedah anti-pattern yang paling sering menjerat tim pemula: menjadikan JSON tempat menampung segalanya.
Kapan JSON Menjadi Pilihan Tepat #
JSON bersinar ketika struktur data memang tidak bisa dijanjikan seragam. Tiga situasi berikut adalah alasan paling kuat untuk memakainya:
- Skema bervariasi per baris. Katalog produk adalah contoh klasiknya: kopi punya varietas dan tinggi kemasan, mug punya bahan dan kapasitas, teh hanya punya berat. Memaksa keduanya ke kolom tetap menghasilkan tabel lebar penuh
NULL, dan setiap jenis produk baru menuntutALTER TABLE. - Data eksternal sebagai passthrough. Payload dari webhook, respons API pihak ketiga, atau log event sering cukup disimpan dan dibaca ulang utuh tanpa perlu diurai. Kolom JSON menjamin teks tersebut valid tanpa memaksa kita mendesain ratusan kolom untuk field yang bahkan mungkin belum kita kenal.
- Prototipe dan eksplorasi cepat. Saat struktur data masih bergerak — field bertambah dan berganti nama tiap minggu — menulis ulang dokumen jauh lebih murah daripada mengubah skema tabel berulang kali. Setelah struktur stabil, nilai-nilai penting bisa dipromosikan menjadi kolom.
Ciri umum ketiganya sama: data dibaca sebagai dokumen utuh atau lewat sedikit path, jarang di-join, jarang diagregasi massal. Selama pola aksesnya seperti itu, kelenturan JSON tidak menagih harga yang mahal.
Kapan Kolom Relasional Lebih Baik #
Sebaliknya, begitu data menjadi warga kelas satu dari database — di-join, disaring, diagregasi, dijaga integritasnya — kolom relasional klasik hampir selalu menang:
- Relasi dan integritas. Foreign key hanya bisa menghubungkan kolom bertipe dan ter-index; tidak ada mekanisme integritas yang menjangkau ke dalam dokumen JSON. Relasi pelanggan–pesanan yang kita rancang di section desain relasional tidak mungkin diwujudkan dengan menyimpan
pelanggan_iddi dalam dokumen. - Agregasi berat dan sering.
SUM(harga)pada kolomDECIMALberjalan pada data terstruktur siap pakai; agregasi atas nilai JSON harus menulusuri dokumen per baris atau melewatiJSON_TABLEdanCASTlebih dulu. - Tipe ketat. Kolom
DECIMAL(10,2)menolak teks; dokumen JSON menerima"harga": "murah"tanpa keluhan — longgarnya tipe JSON akan kita buktikan sebentar lagi. - Index dan kendala langsung.
UNIQUE,CHECK, dan index kolom bekerja out-of-the-box; padanan JSON-nya harus lewat kolom generated yang kita rawat sendiri.
Tabel Keputusan #
| Pertanyaan | Jika ya | Jika tidak |
|---|---|---|
| Struktur data bervariasi tiap baris? | Pertimbangkan JSON | Kolom relasional |
| Nilai perlu di-join atau dirujuk FK? | Kolom relasional | JSON memadai |
Nilai diagregasi (SUM, AVG) secara rutin? | Kolom relasional | JSON memadai |
| Tipe nilai harus dijaga ketat? | Kolom relasional | JSON memadai |
| Data hanya disimpan dan dibaca utuh? | JSON memadai | Kolom relasional |
| Skema masih berubah cepat? | Pertimbangkan JSON | Kolom relasional |
Jawaban praktisnya jarang hitam-putih: sebagian besar sistem produksi memakai keduanya dalam satu tabel — inti yang stabil sebagai kolom relasional, atribut yang bervariasi sebagai dokumen JSON.
Contoh Skema Hibrida #
Mari kita bangun skema nyata untuk katalog toko. Identitas dan angka bisnis — nama, kategori, harga — menjadi kolom relasional karena selalu hadir, sering disaring, dan sering diagregasi. Atribut pelengkap yang berbeda tiap produk menjadi dokumen atribut:
DROP DATABASE IF EXISTS belajar_json;
CREATE DATABASE belajar_json;
USE belajar_json;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE produk_hibrid (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
kategori VARCHAR(50) NOT NULL,
harga DECIMAL(10,2) NOT NULL,
atribut JSON NULL
);
INSERT INTO produk_hibrid (nama, kategori, harga, atribut) VALUES
('Kopi Gayo 250g', 'kopi', 55000.00, '{"asal": "Aceh", "tag": ["arabika", "toraja"]}'),
('Mug Keramik', 'peralatan', 45000.00, '{"warna": "putih", "spesifikasi": {"bahan": "keramik"}}'),
('Teh Melati 100g', 'teh', 25000.00, '{"asal": "Tegal"}'),
('Gula Aren 500g', 'pemanis', 15000.00, NULL);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0Perhatikan dua keputusan sadar di skema tersebut: kolom harga memakai DECIMAL(10,2) karena uang butuh presisi dan tipe ketat, sedangkan atribut dibolehkan NULL karena produk seperti Gula Aren boleh saja tanpa atribut tambahan. Query sehari-hari lalu bebas memakai keduanya sekaligus — menyaring lewat kolom relasional, menampilkan detail dari dokumen:
SELECT nama, harga, atribut->>'$.asal' AS asal
FROM produk_hibrid
WHERE kategori = 'kopi'
ORDER BY harga;Output yang Dihasilkan:
+----------------+----------+------+
| nama | harga | asal |
+----------------+----------+------+
| Kopi Gayo 250g | 55000.00 | Aceh |
+----------------+----------+------+
1 row in set (0.00 sec)Agregasi per kategori berjalan murni di kolom relasional — cepat dan ber-tipe benar:
SELECT kategori, COUNT(*) AS jumlah, SUM(harga) AS total
FROM produk_hibrid
GROUP BY kategori
ORDER BY kategori;Output yang Dihasilkan:
+-----------+--------+----------+
| kategori | jumlah | total |
+-----------+--------+----------+
| kopi | 1 | 55000.00 |
| pemanis | 1 | 15000.00 |
| peralatan | 1 | 45000.00 |
| teh | 1 | 25000.00 |
+-----------+--------+----------+
4 rows in set (0.00 sec)Tip Ketat versus Longgar: Sebuah Percobaan #
Mari kita buktikan klaim soal longgarnya tipe JSON dengan percobaan kecil. Kolom harga yang bertipe DECIMAL menolak teks mentah — coba masukkan string ke sana:
-- @error
INSERT INTO produk_hibrid (nama, kategori, harga) VALUES
('Kopi Luwak', 'kopi', 'murah');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1366 (HY000): Incorrect decimal value: 'murah' for column 'harga' at row 1
ByeServer menolak dengan ERROR 1366 — nilai desimal salah — karena kolom relasional mempertahankan kontrak tipenya. Sekarang bandingkan dengan dokumen JSON, tempat kunci harga bisa diisi string apa pun:
UPDATE produk_hibrid
SET atribut = '{"harga": "murah", "asal": "Bogor"}'
WHERE id = 3;
SELECT id, atribut FROM produk_hibrid WHERE id = 3;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
+----+-------------------------------------+
| id | atribut |
+----+-------------------------------------+
| 3 | {"asal": "Bogor", "harga": "murah"} |
+----+-------------------------------------+
1 row in set (0.00 sec)Dokumen JSON menerima "harga": "murah" tanpa keluhan — teks tersebut adalah string JSON yang sah. Masalah baru muncul ketika angka itu ingin dipakai sebagai angka:
SELECT SUM(harga) AS total_kolom_relasional FROM produk_hibrid;Output yang Dihasilkan:
+------------------------+
| total_kolom_relasional |
+------------------------+
| 140000.00 |
+------------------------+
1 row in set (0.00 sec)SELECT SUM(atribut->>'$.harga') AS total_dari_json FROM produk_hibrid;Output yang Dihasilkan:
+-----------------+
| total_dari_json |
+-----------------+
| 0 |
+-----------------+
1 row in set, 1 warning (0.00 sec)Kedua SUM menghasilkan cerita yang sangat berbeda. Kolom relasional menjumlahkan seluruh harga menjadi 140000.00 dengan tipe yang terjamin. Versi JSON menghasilkan 0 — satu-satunya baris yang memiliki kunci harga di dokumennya menyimpan string "murah", yang dikonversi menjadi nol saat dijumlahkan, dan konversi itu hanya menghasilkan peringatan, bukan galat. Kesalahan kategori data di kolom relasional tertangkap saat tulis; kesalahan yang sama di dokumen JSON lolos hingga saat baca, dan mungkin baru terasa di angka laporan keuangan.
Anti-Pattern: JSON untuk Segalanya #
Percobaan di atas adalah bibit dari anti-pattern yang lebih besar: menjadikan JSON satu-satunya wadah data karena “biar fleksibel”. Tabel satu kolom JSON raksasa memang valid secara sintaks, namun setiap operasi inti database menjadi bermasalah — relasi tak bisa dijaga FK, tipe tak bisa dipegang, agregasi menulusuri dokumen per baris, dan perubahan kecil pun menulis ulang dokumen besar. Dampak jalur lambatnya bisa kita lihat langsung lewat EXPLAIN:
CREATE INDEX idx_harga ON produk_hibrid (harga);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0EXPLAIN SELECT id, nama FROM produk_hibrid WHERE harga > 30000;Output yang Dihasilkan:
+----+-------------+---------------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | produk_hibrid | NULL | range | idx_harga | idx_harga | 5 | NULL | 2 | 100.00 | Using index condition |
+----+-------------+---------------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)EXPLAIN SELECT id, nama FROM produk_hibrid WHERE atribut->>'$.asal' = 'Aceh';Output yang Dihasilkan:
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | produk_hibrid | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | Using where |
+----+-------------+---------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)Filter pada kolom relasional ter-index berjalan lewat type range dengan index idx_harga. Filter pada path JSON jatuh ke type ALL — full scan — karena tanpa kolom generated ber-index, server harus membuka setiap dokumen. Pada empat baris latihan selisihnya tak terasa; pada jutaan baris, selisihnya adalah detik lawan milidetik.
Yang terakhir dan paling mendasar: JSON bukan penggantian normalisasi. Duplikasi data di dalam dokumen JSON tidak bisa dibersihkan oleh bentuk normal seperti yang kita latih di section desain relasional — MySQL tidak punya jendela untuk melihat isi dokumen saat merancang relasi antartabel. Normalisasi tetap menjadi kerangka; JSON adalah pelengkap untuk bagian data yang memang layak semi terstruktur.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan:
DROP DATABASE belajar_json;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- The JSON Data Type — MySQL 8.4 Reference Manual
- CREATE TABLE Statement — MySQL 8.4 Reference Manual
- JSON Data Types — MySQL 8.4 Reference Manual
Keputusan kini ada di tangan kita: JSON untuk data yang bervariasi dan dibaca sebagai dokumen, kolom relasional untuk data yang dihubungkan, disaring, dan dijaga integritasnya — serta pola hibrida yang memadukan keduanya ketika dunia nyata menuntut keduanya sekaligus. Section JSON dan data semi terstruktur telah tuntas. Section berikutnya berpindah ke kemampuan pencarian yang berbeda sifatnya: full-text search, cara MySQL menemukan kata di dalam teks panjang secara alami — bukan dengan LIKE per karakter.