Dua nama paling sering disandingkan di dunia database open source adalah PostgreSQL dan MySQL. Keduanya relational, keduanya gratis, dan keduanya menangani jutaan aplikasi produksi — sehingga pertanyaan “pilih yang mana?” hampir pasti muncul di awal setiap proyek. Chapter ini membandingkan keduanya secara adil: filosofi yang membentuknya, perbedaan fitur yang bisa kita uji langsung, dan skenario ketika masing-masing menjadi pilihan yang lebih masuk akal.
Dua Filosofi yang Berbeda #
PostgreSQL dibesarkan komunitas akademik dengan prioritas correctness: kepatuhan pada standar SQL, transaksi yang ketat, dan fitur yang ditambahkan perlahan setelah teruji. MySQL lahir pada era web tahun 1990-an dengan prioritas kebalikannya — kecepatan dan kesederhanaan untuk aplikasi web yang membaca jauh lebih sering daripada menulis. Dari akar itu pula model tumbuhnya berbeda: PostgreSQL dikembangkan PostgreSQL Global Development Group tanpa pemilik tunggal, sedangkan MySQL kini dimiliki Oracle Corporation dan didistribusikan dengan skema lisensi ganda.
Tidak ada filosofi yang “benar” di sini. Keduanya berhasil di bidangnya masing-masing, dan keduanya saling mengejar: MySQL 8 sudah mendukung common table expression dan window function, sementara PostgreSQL terus memperbaiki kecepatan tulisnya dari rilis ke rilis. Perbandingan yang jujur karena itu menyoroti sisa perbedaan yang bertahan sampai hari ini.
Perbandingan Sekilas #
| Aspek | PostgreSQL | MySQL |
|---|---|---|
| Filosofi | Kebenaran data dan kekayaan fitur dulu | Kecepatan dan kesederhanaan dulu |
| Model pengembangan | Komunitas global, tanpa pemilik tunggal | Milik Oracle Corporation |
| Lisensi | PostgreSQL License (permisif, mirip BSD) | GPL v2 atau lisensi komersial (dual licensing) |
| Kepatuhan standar SQL | Sebagian besar standar SQL:ISO dipegang konsisten | Banyak penyimpangan historis; kepatuhan dikendalikan sql_mode |
| Tipe data unggulan | JSONB, array, range, UUID, alamat jaringan, composite | JSON tanpa padanan JSONB; tanpa array dan range native |
| Fitur query lanjutan | CTE, window function, FILTER, materialized view, partial index | CTE dan window function sejak 8.0; tanpa FILTER dan materialized view native |
| MVCC | Versi baris lama tetap di tabel; dibereskan VACUUM | InnoDB menyimpan versi lama di undo log; data tersusun oleh primary key |
| Ekstensibilitas | Framework extension kaya (PostGIS, pg_stat_statements) | Arsitektur storage engine yang bisa diganti-ganti |
Fitur yang Bisa Kita Uji: Array #
Tabel di atas menyebut array sebagai keunggulan PostgreSQL — mari kita buktikan. Skenarionya nyata: Toko Maju Jaya ingin memberi setiap produk sejumlah tag (misalnya diskon, hadiah) tanpa membuat tabel relasi tambahan. Di PostgreSQL, kolom bertipe text[] cukup:
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
tag text[] NOT NULL,
harga numeric(10, 2) NOT NULL
);
INSERT INTO produk (id, nama, tag, harga) VALUES
(1, 'Pulpen Hitam', ARRAY['alat tulis', 'diskon'], 3000.00),
(2, 'Kertas HVS A4', ARRAY['kertas', 'diskon'], 55000.00),
(3, 'Stapler Besar', ARRAY['alat tulis', 'hadiah'], 24000.00);
SELECT id, nama, tag
FROM produk
ORDER BY id;
SELECT id, nama, harga
FROM produk
WHERE 'diskon' = ANY (tag)
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
id | nama | tag
----+---------------+-----------------------
1 | Pulpen Hitam | {"alat tulis",diskon}
2 | Kertas HVS A4 | {kertas,diskon}
3 | Stapler Besar | {"alat tulis",hadiah}
(3 rows)
id | nama | harga
----+---------------+----------
1 | Pulpen Hitam | 3000.00
2 | Kertas HVS A4 | 55000.00
(2 rows)Bedah baris-per-baris untuk bagian-bagian kuncinya:
- Baris 4 (
tag text[] NOT NULL): deklarasi kolom array —text[]berarti “array dari text”. Tidak ada padanan langsung untuk deklarasi ini di MySQL; pilihan yang tersedia adalah tabel relasi terpisah atau kolomJSON. - Baris 9-11 (
ARRAY['alat tulis', 'diskon']): konstruktorARRAY[...]membangun nilai array saat insert. Output SELECT memperlihatkan format penyimpanannya:{"alat tulis",diskon}— elemen yang mengandung spasi dibungkus kutip ganda, sisanya ditulis polos. - Baris 21 (
WHERE 'diskon' = ANY (tag)): operatorANYmemeriksa apakah satu nilai ada di dalam array. Query ini mengembalikan dua produk ber-tagdiskon— filter bersih tanpa tabel tambahan dan tanpa manipulasi string.
Fitur yang Bisa Kita Uji: JSONB #
Kasus kedua: aplikasi web mencatat event pengguna yang strukturnya berbeda-beda — login punya field perangkat, checkout punya field total. PostgreSQL menampungnya dalam kolom jsonb, lalu meng-query isinya dengan operator khusus:
CREATE TABLE aktivitas (
id integer PRIMARY KEY,
pengguna text NOT NULL,
detail jsonb NOT NULL
);
INSERT INTO aktivitas (id, pengguna, detail) VALUES
(1, 'Budi', '{"aksi": "login", "perangkat": "android"}'),
(2, 'Budi', '{"aksi": "checkout", "total": 125000}'),
(3, 'Siti', '{"aksi": "login", "perangkat": "ios"}');
SELECT id, pengguna, detail->>'aksi' AS aksi
FROM aktivitas
ORDER BY id;
SELECT id, pengguna
FROM aktivitas
WHERE detail @> '{"aksi": "login"}'
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
id | pengguna | aksi
----+----------+----------
1 | Budi | login
2 | Budi | checkout
3 | Siti | login
(3 rows)
id | pengguna
----+----------
1 | Budi
3 | Siti
(2 rows)Tiga operator dan tipe di atas pantas diingat:
jsonb(baris 4): JSON yang diurai menjadi format biner — diindeks, bisa dibandingkan, dan tidak menyimpan whitespace maupun urutan key input. Tipejsonbiasa hanya menyimpan teks mentah, dan itulah yang dimiliki MySQL: teks JSON dengan sekumpulan fungsi untuk membacanya.detail->>'aksi'(baris 15): operator->>mengambil nilai key sebagaitext. Kolom hasil bisa langsung diberi aliasaksiseolah-olah kolom biasa.detail @> '{"aksi": "login"}'(baris 21): operator containment — “apakahdetailmemuat objek ini?”. Dipadukan dengan index GIN, filter seperti ini cepat pada tabel berisi jutaan event.
MVCC: Dua Jalur Menuju Tujuan yang Sama #
Kedua database menjanjikan hal yang sama lewat MVCC: transaksi pembaca tidak memblokir transaksi penulis. Jalurnya berbeda. PostgreSQL menyimpan versi lama baris tetap di dalam tabel itu sendiri, lalu proses VACUUM membersihkan baris mati secara berkala — mekanisme ini dibedah tuntas di Section Transactions. MySQL (InnoDB) menyimpan informasi undo di undo log terpisah dan menyusun seluruh tabel mengikuti urutan primary key (clustered index).
Konsekuensi praktisnya terasa di desain skema dan operasional. Di PostgreSQL, primary key bebas dipilih tanpa memengaruhi tata letak fisik data, tetapi kita wajib memahami VACUUM dan bloat. Di MySQL, primary key menentukan urutan fisik baris sehingga akses berurutan lewat primary key sangat cepat, tetapi primary key yang buruk langsung merugikan semua index lain. Tidak ada pemenang mutlak — ada trade-off yang harus dipahami.
Kapan Memilih yang Mana #
| Situasi | Pilihan yang lebih natural |
|---|---|
| Aplikasi web CRUD sederhana, stack sudah LAMP, tim terbiasa MySQL | MySQL |
Query analitik kompleks: CTE bersarang, window function, FILTER | PostgreSQL |
| Data semi-terstruktur: tag, atribut produk, event JSON | PostgreSQL (JSONB, array) |
| Kebutuhan geospasial, full-text search lanjutan, ekstensi khusus | PostgreSQL |
| Produk komersial yang tidak ingin kewajiban pembukaan kode | PostgreSQL (lisensi permisif) |
| Hosting bersama murah yang hanya menyediakan MySQL | MySQL |
Pilihan Anda saat ini adalah PostgreSQL — dan selebihnya course ini mengoptimalkan pemahaman itu. Langkah berikutnya: membuka kap mesinnya. Bagaimana persisnya satu baris SELECT berjalan dari koneksi sampai hasil kembali? Chapter Cara Kerja PostgreSQL menelusurinya tahap demi tahap.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
fitur-array.sql
Unduh berkasCREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
tag text[] NOT NULL,
harga numeric(10, 2) NOT NULL
);
INSERT INTO produk (id, nama, tag, harga) VALUES
(1, 'Pulpen Hitam', ARRAY['alat tulis', 'diskon'], 3000.00),
(2, 'Kertas HVS A4', ARRAY['kertas', 'diskon'], 55000.00),
(3, 'Stapler Besar', ARRAY['alat tulis', 'hadiah'], 24000.00);
SELECT id, nama, tag
FROM produk
ORDER BY id;
SELECT id, nama, harga
FROM produk
WHERE 'diskon' = ANY (tag)
ORDER BY id;
fitur-jsonb.sql
Unduh berkasCREATE TABLE aktivitas (
id integer PRIMARY KEY,
pengguna text NOT NULL,
detail jsonb NOT NULL
);
INSERT INTO aktivitas (id, pengguna, detail) VALUES
(1, 'Budi', '{"aksi": "login", "perangkat": "android"}'),
(2, 'Budi', '{"aksi": "checkout", "total": 125000}'),
(3, 'Siti', '{"aksi": "login", "perangkat": "ios"}');
SELECT id, pengguna, detail->>'aksi' AS aksi
FROM aktivitas
ORDER BY id;
SELECT id, pengguna
FROM aktivitas
WHERE detail @> '{"aksi": "login"}'
ORDER BY id;