JSON ke SQL: Pernyataan INSERT, Desain Tabel, dan Jebakan Pemetaan Tipe
Intinya: Mengubah JSON menjadi SQL INSERT menyembunyikan pemecahan tabel bersarang, NULL versus field hilang, escaping, dan perbedaan dialek. Pernyataan yang dapat dijalankan dan tabel perbandingan untuk setiap jebakan.

Intinya: tentukan entitas bisnis apa yang diwakili satu baris sebelum mendesain tabel, lalu tulis multi-baris VALUES eksplisit di dalam satu transaksi; selesaikan penamaan field dan pemetaan tipe sebelum impor alih-alih menambalnya di SQL.
JSON mendeskripsikan bentuk; SQL juga mendeskripsikan batasan. Saat Anda memindahkan yang pertama ke yang kedua, setiap batasan yang tidak Anda tuliskan akan kembali kemudian sebagai data kotor.
1. Tentukan dulu: satu tabel atau satu record per baris JSON
Lihat lapisan terluar data sebelum mendesain tabel apa pun. Salah dalam keputusan ini dan setiap INSERT setelahnya harus ditulis ulang.
| Struktur JSON | Saran desain tabel | Risiko utama |
|---|---|---|
Array object yang rata [{...},{...}] |
Satu tabel, banyak baris | Kunci tidak konsisten antar record membuat sebagian besar kolom gabungan NULL |
Object yang membungkus array {"data":[...],"total":100} |
Satu tabel, ambil hanya data |
Menulis total ke setiap baris menempatkan makna field di tempat yang salah |
Object bersarang {"user":{"name":"x"}} |
Tabel induk plus kolom yang diratakan, atau induk plus tabel terkait | Ekspansi tak terkendali, nama kolom terduplikasi |
Field array {"tags":["a","b"]} |
Tabel anak, atau kolom JSON/TEXT | Relasi satu-ke-banyak dipaksakan ke satu sel dan tidak lagi dapat dikueri per tag |
| Kunci tidak konsisten | Kolom gabungan, atau pecah tabel berdasarkan tipe | "Kunci hilang" dan "nilai null" runtuh menjadi NULL yang sama |
Hanya ada satu kriteria: entitas bisnis apa yang diwakili satu baris? Jika satu baris adalah order, item baris harus berada di tabelnya sendiri. Jika satu baris adalah user, tag berada di tabel anak atau kolom JSON, bukan di string yang digabung koma.
2. Dari array ke INSERT: templat terkecil yang dapat dijalankan
Bentuk teraman selalu mendaftar nama kolom secara eksplisit dan tidak pernah bergantung pada urutan kolom:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
INSERT INTO users (id, name, email) VALUES
(1, 'Ada', 'ada@example.com'),
(2, 'Linus', NULL);
Tiga detail penting:
- Tulis setiap nama kolom. Saat schema nanti bertambah kolom, tidak ada yang bergeser ke field yang salah.
VALUESmulti-baris bekerja di SQLite, PostgreSQL, dan MySQL, dan menghemat banyak round trip dibandingkan loop insert satu baris.NULLadalah kata kunci; jangan mengutipnya. Menulis'NULL'menyimpan string empat karakter.
Jika kunci JSON tidak cocok dengan nama kolom (userId versus user_id), tambahkan lapisan pemetaan. Jangan menebak dengan COALESCE di dalam SQL. Memperbaiki nama field di lapisan data jauh lebih murah daripada menambalnya di kueri.
3. Object bersarang: tabel induk plus tabel anak
Order adalah kasus satu-ke-banyak klasik:
INSERT INTO orders (id, customer_name, total_cents) VALUES
(1001, 'Ada', 12900);
INSERT INTO order_items (order_id, sku, quantity) VALUES
(1001, 'A-1', 2),
(1001, 'B-7', 1);
Ini sesuai dengan JSON {"id":1001,"customer":"Ada","total":129.00,"items":[{"sku":"A-1","quantity":2},{"sku":"B-7","quantity":1}]}.
Poin kunci:
- Panjang array
itemsmenentukan jumlah baris anak; tabel induk mendapat tepat satu baris. - Simpan uang sebagai integer sen (
total_cents), jangan pernah sebagaiFLOAT. - Berikan tabel anak foreign key
order_iddan commit kedua insert dalam satu transaksi, jika tidak induk bisa ada sementara itemnya hilang. - Jika data bersifat read-only dan tidak ada yang memfilter per item baris, kolom JSON dapat diterima (PostgreSQL
jsonb, MySQLJSON, atau SQLiteTEXTdengan fungsi json1), tetapi terima biayanya: tidak ada indeks dan kueri yang canggung.
Ratalkan object bersarang tetap seperti user.address menjadi kolom (address_city, address_zip). Pecah menjadi tabel hanya ketika penyarangan bisa terus bertambah; memecah bentuk tetap hanya menambah JOIN.
4. NULL versus field hilang: SQL tidak punya "kunci tidak ada"
JSON memungkinkan kunci benar-benar tidak ada. Kolom SQL bersifat tetap. Setelah impor kedua kasus tampak identik — NULL — meskipun bisa bermakna berbeda:
| Situasi JSON | Yang ditampilkan basis data | Saran penanganan |
|---|---|---|
| Kunci tidak ada | NULL |
Tambahkan default pada definisi tabel, atau kolom penanda _present |
Kunci ada dengan null |
NULL |
Berarti pengguna secara eksplisit mengosongkan nilai |
| Kunci ada dengan string kosong | '' |
Dapat dibedakan dari NULL, tetapi mudah tertukar dalam kueri |
| Field numerik hilang | NULL |
Jangan pernah membiarkan 0 berpura-pura menjadi "hilang" |
Dua kesalahan umum:
- Menggunakan string kosong alih-alih NULL. Nanti,
WHERE email IS NULLmelewatkan baris-baris itu, dan metrik "pengguna tanpa email" Anda diam-diam menghitung kurang. - Menguji dengan
col = NULL. Dalam logika tiga-nilai SQL,NULL = NULLdievaluasi menjadiNULL, bukanTRUE. Anda harus menulisIS NULL:
SELECT COUNT(*) FROM users WHERE email IS NULL;
Jika Anda perlu mempertahankan apakah kunci pernah ada, tambahkan kolom penanda boolean alih-alih menyimpulkannya dari NULL. Kontrak API juga harus menyatakan ini; lihat Desain API JSON dan penamaan field.
5. Quoting dan escaping: jangan pernah menyambung string SQL
String JSON dapat berisi kutip tunggal, kutip ganda, backslash, baris baru, dan bahkan \u0000. Dengan string yang dirakit manual, satu escape yang terlewat menggeser sisa pernyataan, dan dalam kasus terburuk menjadi SQL injection.
Dalam SQL standar, kutip tunggal di-escape dengan menggandakannya:
INSERT INTO notes (id, content) VALUES (1, 'It''s a ''quoted'' value');
Kode produksi harus menggunakan parameter binding dan membiarkan driver menangani escaping dan tipe:
// PostgreSQL uses $1/$2, MySQL uses ?; this example uses node-postgres
await client.query('INSERT INTO users (id, name) VALUES ($1, $2)', [row.id, row.name]);
Lakukan escape manual hanya saat Anda harus mengekspor file .sql, dan setidaknya:
- ganti
'dengan''; - jika baris baru dipertahankan, pastikan basis data target menerima literal string multi-baris;
- hapus
\u0000, karena sebagian besar basis data menolak NUL di dalam string; - MySQL memperlakukan backslash sebagai karakter escape secara default, sehingga
\harus menjadi\\, atau beralih ke mode standar.
Kutip ganda dari JSON tidak perlu di-escape di dalam string SQL berkutip tunggal. Menambahkan lapisan escaping ekstra adalah cara backslash liar berakhir di data Anda.
6. Pemetaan tipe: di mana enam nilai JSON mendarat di SQL
JSON hanya punya string, number, boolean, null, object, dan array. SQL memiliki sistem tipe yang jauh lebih rinci. Kesalahan biasanya terjadi pada presisi dan semantik, bukan sintaks:
| Nilai JSON | Saran tipe SQL | Di mana ia rusak |
|---|---|---|
| Integer | INTEGER / BIGINT |
Di atas 2^53 sisi JavaScript sudah kehilangan presisi, sehingga nilai yang disimpan juga salah |
| Desimal, uang | NUMERIC / DECIMAL |
FLOAT adalah floating point biner; 0.1 + 0.2 tidak eksak |
| String | TEXT / VARCHAR(n) |
Batas panjang memotong; collation mengubah perbandingan |
| Boolean | BOOLEAN / TINYINT(1) |
SQLite tidak punya tipe boolean dan menyimpan 0/1 |
| null | NULL |
Tidak dapat dibedakan dari field yang hilang |
| String tanggal | DATE / TIMESTAMP / TIMESTAMPTZ |
Zona waktu hilang, milidetik terpotong, format tidak valid |
| String enum | TEXT dengan CHECK, atau tabel enum |
Perbedaan huruf besar-kecil dan nilai kotor tidak memiliki batasan |
| Object / array | Tabel anak, atau JSON / JSONB |
Disimpan sebagai string tidak dapat diindeks atau dikueri |
Boolean menyembunyikan bug paling halus: false di JSON menjadi 0 di MySQL TINYINT(1), round-trip tampak baik, dan tidak ada yang menyadari. Tetapi begitu beberapa kode melakukan if (value) pada string "false", ia dievaluasi sebagai truthy. Skrip konversi harus memanggil Boolean(value) secara eksplisit.
PostgreSQL JSONB mem-parsing dan menghapus duplikat kunci, menyimpan kemunculan terakhir, yang cocok untuk kueri; JSON mempertahankan teks asli dan urutan kunci. SQLite JSON hanyalah TEXT plus sekumpulan fungsi, tanpa batasan tipe nyata. Sebelum memilih, tanyakan apakah tabel ini perlu memfilter field di dalam JSON.
7. Insert massal dan transaksi: batch, commit, buat dapat dijalankan ulang
Saat volume bertambah, tiga hal menentukan apakah impor tetap terkendali:
- Batch. Loop N pernyataan
INSERTsatu baris membutuhkan N round trip jaringan. Gabungkan 500 hingga 1000 baris menjadi satuVALUESmulti-baris, atau gunakan bulk loader milik basis data (PostgreSQLCOPY, MySQLLOAD DATA, insert multi-baris di dalam transaksi SQLite). - Gunakan transaksi. Semua atau tidak sama sekali, sehingga tabel yang setengah termuat tidak pernah bertahan setelah error:
BEGIN;
INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Linus');
INSERT INTO order_items (order_id, sku, quantity) VALUES (1, 'A-1', 2);
COMMIT;
- Buat idempoten. Skrip impor terus-menerus dijalankan ulang. Gunakan upsert alih-alih delete-then-insert:
| Basis data | Bentuk idempoten |
|---|---|
| PostgreSQL / SQLite | INSERT ... ON CONFLICT (id) DO UPDATE SET ... |
| MySQL | INSERT ... ON DUPLICATE KEY UPDATE ... |
ON CONFLICT membutuhkan unique constraint atau primary key, jika tidak ia tidak pernah terpicu. Ketika tidak ada kunci alami, buat indeks unik pada kunci bisnis seperti email. Ketika tidak ada kunci sama sekali, tambahkan id stabil di sisi JSON — jangan pernah bergantung pada id auto-increment untuk idempotensi, karena menjalankan ulang akan menyisipkan duplikat.
8. Perbedaan dialek sekilas
| Kemampuan | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
VALUES multi-baris |
Ya | Ya | Ya |
| Upsert | ON CONFLICT |
ON CONFLICT |
ON DUPLICATE KEY UPDATE |
| Kunci auto-increment | INTEGER PRIMARY KEY |
GENERATED ... AS IDENTITY |
AUTO_INCREMENT |
| Tipe boolean | Tidak ada, menyimpan 0/1 | boolean |
TINYINT(1) |
| Impor massal | Insert multi-baris dalam transaksi | COPY |
LOAD DATA |
| Dukungan JSON | TEXT plus fungsi json1 |
json / jsonb |
JSON |
| Quoting pengenal | Kutip ganda | Kutip ganda | Backtick (default) |
Agar satu file SQL berfungsi di ketiganya, setidaknya hindari ON CONFLICT, backtick, dan SERIAL. Untuk proyek lintas basis data, hasilkan schema dan pernyataan upsert secara terpisah; jangan berharap satu skrip cocok untuk semua.
9. Tiga pemeriksaan sekilas setelah impor
Sebelum menghapus file sumber, verifikasi tiga hal:
- Jumlah baris.
SELECT COUNT(*) FROM users;harus sama dengan panjang array JSON. Satu baris yang hilang biasanya berarti record terakhir terpotong. - Kolom dengan tingkat NULL tertinggi. Itu menunjuk ke nama kunci yang tidak konsisten atau penyarangan yang tidak diperluas. Kembali dan periksa apakah sumber memakai
userIddanuser_idsekaligus. - Uang dan pengenal panjang. Ambil nilai maksimum dan minimum dan pastikan konversi floating point maupun integer tidak memotongnya.
Jika Anda perlu memahami bentuk tabularnya lebih dulu, gunakan pendekatan perataan di Konversi JSON, CSV, dan data tabular. Jika audiensinya adalah rekan kerja yang hanya ingin membaca data, empat cara mengonversi JSON ke Excel lebih sedikit kerja daripada mendirikan basis data.
Mengapa angka kehilangan presisi setelah mengimpor JSON ke SQL?
Integer di atas 2^53 sudah kehilangan presisi selama JSON.parse JavaScript, dan menuliskannya ke BIGINT tidak dapat memulihkannya. id seperti itu harus berupa string di JSON dan dikonversi ke BIGINT atau TEXT saat impor.
Apa yang harus saya gunakan sebagai primary key ketika JSON tidak punya id?
Biarkan basis data menghasilkannya, atau buat UUID stabil sebelum impor. Jangan pernah menggunakan indeks array sebagai id: menjalankan ulang akan mengurutkan ulang data dan mengarahkan baris yang salah ke record yang salah. Indeks unik pada kunci bisnis (email, nomor order) juga memungkinkan upsert.
Apa yang harus saya lakukan saat INSERT massal mengalami konflik kunci unik?
Ganti INSERT biasa dengan upsert yang sesuai untuk dialek Anda, dan pastikan kolom yang berkonflik memiliki unique constraint. Menghapus dulu lalu menyisipkan juga bisa, tetapi harus terjadi dalam satu transaksi dan Anda menerima jendela singkat tanpa baris.
Apakah array bersarang selalu membutuhkan tabel anak?
Tidak. Untuk tampilan read-only tanpa kueri per sub-item, kolom JSON lebih sederhana. Begitu Anda perlu memfilter atau mengagregasi per tag atau SKU, tabel anak dengan indeks adalah pilihan yang berkelanjutan. Faktor penentunya adalah bagaimana Anda mengueri, bukan bagaimana data dibentuk.