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 #

AspekPostgreSQLMySQL
FilosofiKebenaran data dan kekayaan fitur duluKecepatan dan kesederhanaan dulu
Model pengembanganKomunitas global, tanpa pemilik tunggalMilik Oracle Corporation
LisensiPostgreSQL License (permisif, mirip BSD)GPL v2 atau lisensi komersial (dual licensing)
Kepatuhan standar SQLSebagian besar standar SQL:ISO dipegang konsistenBanyak penyimpangan historis; kepatuhan dikendalikan sql_mode
Tipe data unggulanJSONB, array, range, UUID, alamat jaringan, compositeJSON tanpa padanan JSONB; tanpa array dan range native
Fitur query lanjutanCTE, window function, FILTER, materialized view, partial indexCTE dan window function sejak 8.0; tanpa FILTER dan materialized view native
MVCCVersi baris lama tetap di tabel; dibereskan VACUUMInnoDB menyimpan versi lama di undo log; data tersusun oleh primary key
EkstensibilitasFramework 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:

sql fitur-array.sql
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:

text
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:

  1. 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 kolom JSON.
  2. Baris 9-11 (ARRAY['alat tulis', 'diskon']): konstruktor ARRAY[...] membangun nilai array saat insert. Output SELECT memperlihatkan format penyimpanannya: {"alat tulis",diskon} — elemen yang mengandung spasi dibungkus kutip ganda, sisanya ditulis polos.
  3. Baris 21 (WHERE 'diskon' = ANY (tag)): operator ANY memeriksa apakah satu nilai ada di dalam array. Query ini mengembalikan dua produk ber-tag diskon — 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:

sql fitur-jsonb.sql
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:

text
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:

  1. jsonb (baris 4): JSON yang diurai menjadi format biner — diindeks, bisa dibandingkan, dan tidak menyimpan whitespace maupun urutan key input. Tipe json biasa hanya menyimpan teks mentah, dan itulah yang dimiliki MySQL: teks JSON dengan sekumpulan fungsi untuk membacanya.
  2. detail->>'aksi' (baris 15): operator ->> mengambil nilai key sebagai text. Kolom hasil bisa langsung diberi alias aksi seolah-olah kolom biasa.
  3. detail @> '{"aksi": "login"}' (baris 21): operator containment — “apakah detail memuat 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 #

SituasiPilihan yang lebih natural
Aplikasi web CRUD sederhana, stack sudah LAMP, tim terbiasa MySQLMySQL
Query analitik kompleks: CTE bersarang, window function, FILTERPostgreSQL
Data semi-terstruktur: tag, atribut produk, event JSONPostgreSQL (JSONB, array)
Kebutuhan geospasial, full-text search lanjutan, ekstensi khususPostgreSQL
Produk komersial yang tidak ingin kewajiban pembukaan kodePostgreSQL (lisensi permisif)
Hosting bersama murah yang hanya menyediakan MySQLMySQL

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 berkas
sql
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;

fitur-jsonb.sql

Unduh berkas
sql
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;