Bagaimana gagasan inti Jeffrey Ullman mendasari basis data modern: aljabar relasional, aturan optimisasi, join, dan perencanaan ala kompiler yang membantu sistem berskala.

Kebanyakan orang yang menulis SQL, membuat dashboard, atau menyetel kueri lambat telah mendapat manfaat dari pekerjaan Jeffrey Ullman—bahkan jika mereka belum pernah mendengar namanya. Ullman adalah ilmuwan komputer dan pendidik yang riset serta buku-bukunya membantu mendefinisikan bagaimana basis data menggambarkan data, menganalisis kueri, dan mengeksekusinya secara efisien.
Saat mesin basis data mengubah SQL Anda menjadi sesuatu yang bisa dijalankan dengan cepat, ia mengandalkan ide-ide yang harus presisi sekaligus adaptif. Ullman membantu memformalkan makna kueri (agar sistem bisa menulis ulang dengan aman), dan menghubungkan pemikiran basis data dengan pemikiran kompiler (agar kueri dapat diparse, dioptimalkan, dan diterjemahkan menjadi langkah-langkah yang dapat dieksekusi).
Pengaruh itu tenang karena tidak muncul sebagai tombol di alat BI Anda atau fitur terlihat di konsol cloud. Ia muncul sebagai:
JOINTulisan ini menggunakan ide inti Ullman sebagai tur berpemandu mengenai internal basis data yang paling berguna: bagaimana aljabar relasional ada di bawah SQL, bagaimana penulisan ulang kueri mempertahankan makna, mengapa pengoptimal berbasis biaya membuat pilihan tertentu, dan bagaimana algoritma join sering menentukan apakah pekerjaan selesai dalam hitungan detik atau jam.
Kita juga akan menarik beberapa konsep ala kompiler—parsing, penulisan ulang, dan perencanaan—karena mesin basis data berperilaku lebih seperti kompiler canggih daripada yang banyak orang pikirkan.
Janji singkat: diskusi akan tetap akurat, tetapi menghindari bukti berat-matematika. Tujuannya memberi Anda model mental yang bisa diterapkan di tempat kerja saat performa, skala, atau perilaku kueri yang membingungkan muncul.
Jika Anda pernah menulis kueri SQL dan mengira ia “hanya berarti satu hal”, Anda bergantung pada gagasan yang dipopulerkan dan diformalkan oleh Jeffrey Ullman: model data yang bersih, plus cara presisi untuk menggambarkan apa yang diminta sebuah kueri.
Inti model relasional memperlakukan data sebagai tabel (relasi). Setiap tabel memiliki baris (tuple) dan kolom (atribut). Sekilas terdengar jelas sekarang, tetapi bagian pentingnya adalah disiplin yang diciptakan:
Pembingkaian ini memungkinkan penalaran tentang kebenaran dan performa tanpa basa-basi. Saat Anda tahu apa yang direpresentasikan tabel dan bagaimana baris diidentifikasi, Anda bisa memprediksi apa yang seharusnya dilakukan join, apa arti duplikasi, dan mengapa filter tertentu mengubah hasil.
Pengajaran Ullman sering menggunakan aljabar relasional sebagai semacam kalkulator kueri: satu set operasi kecil (select, project, join, union, difference) yang bisa digabungkan untuk mengekspresikan apa yang Anda inginkan.
Mengapa ini penting untuk bekerja dengan SQL: basis data menerjemahkan SQL ke bentuk aljabar dan kemudian menulis ulangnya ke bentuk ekuivalen lain. Dua kueri yang terlihat berbeda bisa secara aljabar sama—itulah cara optimizer bisa mengubah urutan join, memajukan filter, atau menghapus kerja berulang sambil mempertahankan makna.
SQL sebagian besar adalah “apa”, tetapi mesin sering melakukan optimisasi menggunakan aljabar yang bersifat “bagaimana.”
Dialek SQL berbeda (Postgres vs. Snowflake vs. MySQL), tetapi dasar-dasarnya tidak. Memahami kunci, relasi, dan ekuivalensi aljabar membantu Anda melihat kapan kueri secara logis salah, kapan cuma lambat, dan perubahan mana yang mempertahankan makna lintas platform.
Aljabar relasional adalah “matematika di bawah” SQL: satu set operator kecil yang menggambarkan hasil yang Anda inginkan. Karya Ullman membantu membuat pandangan operator ini tajam dan mudah diajarkan—dan itu masih model mental yang dipakai sebagian besar optimizer.
Kueri basis data bisa diekspresikan sebagai rangkaian blok bangunan:
WHERE dalam SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT di banyak dialek SQL)Karena jumlah operator kecil, lebih mudah menalar tentang kebenaran: bila dua ekspresi aljabar ekuivalen, keduanya mengembalikan tabel yang sama untuk setiap keadaan basis data yang valid.
Ambil kueri yang familiar:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Secara konseptual, ini adalah:
customers ⋈ ordersσ(o.total > 100)(...)π(c.name)(...)Itu bukan notasi internal yang persis dipakai setiap engine, tapi idenya: SQL menjadi pohon operator.
Banyak pohon berbeda bisa berarti hasil yang sama. Misalnya, filter sering bisa dipindahkan lebih awal (terapkan σ sebelum join besar), dan proyeksi sering bisa menjatuhkan kolom yang tidak dipakai lebih awal (terapkan π lebih cepat).
Aturan-aturan ekuivalensi itulah yang memungkinkan basis data menulis ulang kueri Anda menjadi rencana yang lebih murah tanpa mengubah makna. Setelah Anda melihat kueri sebagai aljabar, “optimisasi” berhenti jadi sulap dan menjadi pembentukan ulang yang aman berdasarkan aturan.
Ketika Anda menulis SQL, basis data tidak mengeksekusinya “sebagaimana tertulis.” Ia menerjemahkan pernyataan Anda menjadi rencana kueri: representasi terstruktur dari pekerjaan yang harus dilakukan.
Model mental yang baik adalah pohon operator. Daun membaca tabel atau index; node internal mentransformasikan dan menggabungkan baris. Operator umum termasuk scan, filter (selection), project (pilih kolom), join, group/aggregate, dan sort.
Basis data biasanya memisahkan perencanaan menjadi dua lapis:
Pengaruh Ullman terlihat pada penekanan transformasi yang mempertahankan makna: tata ulang rencana logis dengan banyak cara tanpa mengubah jawaban, lalu pilih strategi fisik yang efisien.
Sebelum memilih pendekatan eksekusi akhir, optimizer menerapkan aturan "pembersihan" aljabar. Penulisan ulang ini tidak mengubah hasil; mereka mengurangi kerja yang tidak perlu.
Contoh umum:
Misalkan Anda ingin order untuk pengguna di suatu negara:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Interpretasi naif mungkin menggabungkan semua users dengan semua orders lalu memfilter ke Canada. Penulisan ulang yang mempertahankan makna memajukan filter sehingga join menyentuh lebih sedikit baris:
country = 'CA'order_id dan totalDalam istilah rencana, optimizer mencoba mengubah:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
menjadi sesuatu seperti:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Jawaban sama. Kerja lebih sedikit.
Penulisan ulang ini mudah diabaikan karena Anda tidak mengetiknya—namun mereka alasan utama mengapa SQL yang sama bisa berjalan cepat di satu basis data dan lambat di basis lain.
Saat Anda menjalankan kueri SQL, basis data mempertimbangkan beberapa cara valid untuk memperoleh jawaban yang sama, lalu memilih yang diperkirakan paling murah. Proses pengambilan keputusan ini disebut optimisasi berbasis biaya—dan ini salah satu tempat paling praktis di mana teori ala Ullman muncul dalam performa sehari-hari.
Model biaya adalah sistem penilaian yang dipakai optimizer untuk membandingkan rencana alternatif. Sebagian besar engine memperkirakan biaya menggunakan beberapa sumber daya inti:
Model tidak harus sempurna; yang penting arahnya cukup sering benar untuk memilih rencana yang bagus.
Sebelum menilai rencana, optimizer menanyakan pada setiap langkah: berapa banyak baris yang akan dihasilkan ini? Itu estimasi kardinalitas.
Jika Anda memfilter WHERE country = 'CA', engine memperkirakan proporsi tabel yang cocok. Jika Anda menggabungkan customers dengan orders, engine memperkirakan berapa banyak pasangan yang cocok pada kunci join. Tebakan jumlah baris inilah yang menentukan apakah ia memilih index scan atau full scan, hash join atau nested loop, atau apakah sort akan kecil atau sangat besar.
Tebakan optimizer didorong oleh statistik: hitungan, distribusi nilai, tingkat null, dan kadang korelasi antar kolom.
Ketika statistik kadaluarsa atau hilang, engine dapat salah memperkirakan jumlah baris hingga beberapa order besaran. Rencana yang tampak murah di atas kertas bisa menjadi mahal di kenyataan—gejala klasik termasuk perlambatan tiba-tiba setelah pertumbuhan data, perubahan rencana “acak”, atau join yang tak terduga harus menulis ke disk.
Perkiraan lebih baik sering membutuhkan lebih banyak kerja: statistik yang lebih rinci, sampling, atau menjelajahi lebih banyak kandidat rencana. Tetapi perencanaan sendiri memakan waktu, terutama untuk kueri kompleks.
Jadi optimizer menyeimbangkan dua tujuan:
Memahami trade-off ini membantu Anda menginterpretasi keluaran EXPLAIN: optimizer bukan mencoba jadi pintar—ia mencoba konsisten benar di bawah informasi yang terbatas.
Ullman membantu mempopulerkan gagasan sederhana namun kuat: SQL bukan sekadar “dijalankan” melainkan diterjemahkan menjadi rencana eksekusi. Tidak ada tempat yang lebih jelas daripada pada join. Dua kueri yang mengembalikan baris yang sama bisa berbeda drastis waktu eksekusinya tergantung algoritma join yang dipilih engine—dan urutan penggabungan tabel.
Nested loop join sederhana secara konseptual: untuk setiap baris di kiri, cari baris yang cocok di kanan. Ini bisa cepat saat sisi kiri kecil dan sisi kanan punya index yang berguna.
Hash join membangun tabel hash dari salah satu input (sering yang lebih kecil) dan melakukan probe dengan input lain. Ia unggul untuk input besar yang tidak diurutkan dengan kondisi kesetaraan (mis. A.id = B.id), tapi membutuhkan memori; spill-ke-disk bisa menghapus keunggulannya.
Merge join berjalan pada dua input yang diurutkan. Ia cocok ketika kedua sisi sudah terurut (atau bisa diurutkan dengan murah), misalnya saat index bisa mengembalikan baris dalam urutan kunci join.
Dengan tiga tabel atau lebih, jumlah kemungkinan urutan join membengkak. Menggabungkan dua tabel besar lebih dulu dapat menghasilkan intermediate yang sangat besar yang memperlambat semua langkah berikutnya. Urutan yang lebih baik sering mulai dari filter paling selektif (paling sedikit baris) dan bergabung ke luar, menjaga intermediate tetap kecil.
Index tidak hanya mempercepat lookup—mereka membuat strategi join tertentu jadi layak. Index pada kunci join dapat mengubah nested loop mahal menjadi pola “seek per row” yang cepat. Sebaliknya, index yang hilang atau tidak bisa dipakai bisa memaksa engine ke hash join atau sort besar untuk merge join.
Basis data tidak sekadar “menjalankan SQL.” Mereka mengompilasinya. Pengaruh Ullman meluas di kedua domain—teori basis data dan pemikiran kompiler—dan hubungan itu menjelaskan mengapa mesin kueri mirip dengan toolchain bahasa pemrograman: mereka menerjemahkan, menulis ulang, dan mengoptimalkan sebelum melakukan kerja apapun.
Saat Anda mengirim kueri, langkah pertama mirip front end kompiler. Engine men-token-kan kata kunci dan identifier, memeriksa tata bahasa, dan membangun sebuah parse tree (sering disederhanakan menjadi abstract syntax tree). Di sinilah kesalahan dasar ditangkap: koma yang hilang, nama kolom ambigu, aturan pengelompokan yang tidak valid.
Model mental yang berguna: SQL adalah bahasa pemrograman yang “program”-nya kebetulan menggambarkan relasi data alih-alih loop.
Kompiler mengubah sintaks menjadi representasi antara (IR). Basis data melakukan hal serupa: menerjemahkan sintaks SQL menjadi operator logis seperti:
GROUP BY)Bentuk logis ini lebih dekat ke aljabar relasional daripada teks SQL, sehingga lebih mudah menalar tentang makna dan ekuivalensi.
Optimisasi kompiler menjaga hasil program identik sambil membuat eksekusi lebih murah. Optimizer basis data melakukan hal yang sama, menggunakan sistem aturan seperti:
Ini versi basis data dari “dead code elimination”: bukan teknik identik, tetapi filosofi yang sama—pertahankan semantik, kurangi biaya.
Jika kueri Anda lambat, jangan hanya menatap SQL. Lihat rencana kueri seperti Anda memeriksa output kompiler. Rencana memberi tahu apa yang dipilih mesin sebenarnya: urutan join, pemakaian index, dan di mana waktu dihabiskan.
Inti praktis: pelajari membaca EXPLAIN sebagai "daftar assembly performa." Itu mengubah penyetelan dari tebak-tebakan menjadi debugging berbasis bukti. Untuk lebih lanjut tentang mengubah itu menjadi kebiasaan, lihat /blog/practical-query-optimization-habits.
Performa kueri yang baik sering dimulai sebelum Anda menulis SQL. Teori desain skema Ullman (khususnya normalisasi) tentang mengatur data agar basis data dapat menjaga kebenaran, prediktabilitas, dan efisiensi saat bertumbuh.
Normalisasi bertujuan untuk:
Keuntungan kebenaran itu menerjemah ke keuntungan performa nanti: lebih sedikit kolom duplikat, index yang lebih kecil, dan update yang lebih murah.
Anda tidak perlu menghafal bukti untuk menggunakan ide-idenya:
Denormalisasi bisa jadi pilihan cerdas ketika:
Kuncinya adalah denormalisasi dilakukan sengaja, dengan proses untuk menjaga duplikat tetap sinkron.
Desain skema membentuk apa yang bisa dilakukan optimizer. Kunci dan foreign key yang jelas memungkinkan strategi join yang lebih baik, penulisan ulang yang lebih aman, dan estimasi jumlah baris yang lebih akurat. Sementara itu, duplikasi berlebihan dapat membengkakkan index dan memperlambat penulisan, dan kolom multi-nilai menghalangi predikat yang efisien. Saat volume data tumbuh, keputusan modeling awal ini seringkali lebih penting daripada mengoptimalkan kueri tunggal secara mikro.
Saat sebuah sistem "berskala", jarang hanya soal menambah mesin yang lebih besar. Seringkali, bagian sulitnya adalah makna kueri yang sama harus dipertahankan sementara mesin memilih strategi fisik yang sangat berbeda agar waktu berjalan tetap dapat diprediksi. Penekanan Ullman pada ekuivalensi formal adalah tepat yang memungkinkan perubahan strategi itu tanpa mengubah hasil.
Pada ukuran kecil, banyak rencana "bekerja". Pada skala, perbedaan antara memindai tabel, memakai index, atau menggunakan hasil yang sudah dihitung sebelumnya bisa berarti selisih detik dan jam. Sisi teori penting karena optimizer butuh kumpulan aturan penulisan ulang yang aman (mis. memajukan filter, merombak urutan join) yang tidak mengubah jawaban—walau mereka radikal mengubah kerja yang dilakukan.
Partisi (berdasarkan tanggal, pelanggan, wilayah, dll.) mengubah satu tabel logis menjadi banyak potongan fisik. Itu memengaruhi perencanaan:
Teks SQL mungkin tidak berubah, tetapi rencana terbaik kini bergantung pada di mana baris-barisan tersebut berada.
Materialized view pada dasarnya adalah “sub-ekspresi yang disimpan.” Jika engine bisa membuktikan kueri Anda cocok (atau bisa ditulis ulang agar cocok) dengan hasil yang tersimpan, ia bisa mengganti kerja mahal—seperti join berulang dan agregasi—dengan lookup cepat. Ini pemikiran aljabar relasional dalam praktik: kenali ekuivalensi, lalu pakai ulang.
Caching bisa mempercepat baca berulang, tetapi ia tidak akan menyelamatkan kueri yang harus memindai terlalu banyak data, mengocok intermediate besar, atau menghitung join raksasa. Ketika masalah skala muncul, perbaikannya sering kali: kurangi jumlah data yang disentuh (layout/partisi), kurangi komputasi yang diulang (materialized views), atau ubah rencana—bukan sekadar “tambah cache.”
Pengaruh Ullman terlihat di pola pikir sederhana: perlakukan kueri lambat sebagai pernyataan maksud yang bebas ditulis ulang oleh basis data, lalu verifikasi apa yang sebenarnya diputuskan untuk dilakukan. Anda tidak perlu menjadi teoritikus untuk mendapat manfaat—cukup rutinitas yang bisa diulang.
Mulailah dengan bagian yang biasanya mendominasi runtime:
Jika Anda hanya melakukan satu hal, identifikasi operator pertama di mana jumlah baris meledak. Itu biasanya akar masalah.
Ini mudah ditulis dan mengejutkan mahal:
WHERE LOWER(email) = ... bisa mencegah pemakaian index (gunakan kolom ter-normalisasi atau functional index bila didukung).Aljabar relasional mendorong dua langkah praktis:
WHERE sebelum join bila memungkinkan untuk mengecilkan input.Hipotesis yang baik berbunyi: “Join ini mahal karena kita menggabungkan terlalu banyak baris; jika kita memfilter orders ke 30 hari terakhir dulu, input join menyusut.”
Gunakan aturan keputusan sederhana:
EXPLAIN menunjukkan kerja yang bisa dihindari (join tidak perlu, filter terlambat, predikat non-sargable).Tujuannya bukan “SQL yang cerdik.” Tujuannya hasil intermediate yang dapat diprediksi dan kecil—persis jenis perbaikan yang mempertahankan ekuivalensi yang dipermudah ide Ullman.
Konsep-konsep ini bukan hanya untuk administrator basis data. Jika Anda mengirim aplikasi, Anda membuat keputusan basis data dan perencanaan kueri—baik Anda sadar atau tidak: bentuk skema, pilihan kunci, pola kueri, dan lapisan akses data semuanya memengaruhi apa yang bisa dilakukan optimizer.
Jika Anda memakai workflow vibe-coding (misalnya, menghasilkan aplikasi React + Go + PostgreSQL dari antarmuka chat di Koder.ai), model mental ala Ullman adalah jaring pengaman praktis: Anda bisa meninjau skema yang dihasilkan untuk kunci dan relasi yang bersih, memeriksa kueri yang diandalkan aplikasi, dan memvalidasi performa dengan EXPLAIN sebelum masalah muncul di produksi. Semakin cepat Anda mengiterasi “maksud kueri → rencana → perbaikan”, semakin besar nilai yang Anda dapat dari pengembangan yang dipercepat.
Anda tidak perlu “mempelajari teori” sebagai hobi terpisah. Cara tercepat mendapat manfaat dari dasar-dasar ala Ullman adalah mempelajari secukupnya untuk membaca rencana kueri dengan percaya diri—lalu berlatih pada basis data Anda sendiri.
Cari buku dan topik kuliah ini (tanpa afiliasi—hanya titik awal yang sering dikutip):
Mulailah kecil dan kaitkan setiap langkah ke sesuatu yang bisa Anda amati:
Pilih 2–3 kueri nyata dan iterasikan:
IN menjadi EXISTS, majukan predikat, hilangkan kolom tak perlu, dan bandingkan hasil.Gunakan bahasa yang jelas dan berbasis rencana:
Itu keuntungan praktis dari fondasi Ullman: Anda mendapat kosakata bersama untuk menjelaskan performa—tanpa menebak.
Jeffrey Ullman membantu memformalkan bagaimana basis data merepresentasikan arti kueri dan bagaimana sistem dapat mentransformasikan kueri secara aman menjadi ekuivalen yang lebih cepat. Fondasi itu muncul setiap kali mesin menulis ulang kueri, mengubah urutan join, atau memilih rencana eksekusi berbeda sambil menjamin hasil yang sama.
Aljabar relasional adalah sekumpulan operator kecil (select, project, join, union, difference) yang mendeskripsikan hasil kueri secara presisi. Mesin biasanya menerjemahkan SQL ke bentuk seperti aljabar—sebuah pohon operator—sehingga bisa menerapkan aturan ekuivalensi (mis. memajukan filter lebih awal) sebelum memilih strategi eksekusi.
Karena optimisasi bergantung pada bukti bahwa kueri yang ditulis ulang mengembalikan hasil yang sama. Aturan ekuivalensi memungkinkan optimizer untuk:
WHERE sebelum joinPerubahan ini dapat memangkas kerja secara drastis tanpa mengubah makna.
Rencana logis menjelaskan apa operasi yang diperlukan (filter, join, agregat) terlepas dari detail penyimpanan. Rencana fisik memilih bagaimana menjalankannya (index scan vs full scan, hash join vs nested loop, paralelisme, strategi sort). Sebagian besar perbedaan performa muncul dari pilihan fisik, yang dimungkinkan setelah penulisan ulang logis.
Optimisasi berbasis biaya membandingkan beberapa rencana valid dan memilih yang diperkirakan paling murah. Biaya biasanya dipengaruhi oleh faktor praktis seperti jumlah baris yang diproses, I/O, CPU, dan memori (termasuk apakah hash atau sort harus ditulis ke disk).
Estimasi kardinalitas adalah tebakan optimizer tentang “berapa banyak baris yang akan dihasilkan pada langkah ini?” Perkiraan ini menentukan urutan join, jenis join, dan apakah index scan layak. Saat perkiraan salah (seringnya karena statistik yang kadaluarsa atau tidak ada), Anda bisa melihat perlambatan tiba-tiba, spill besar ke disk, atau perubahan rencana yang mengejutkan.
Fokus pada beberapa petunjuk bernilai tinggi:
Anggap rencana seperti output kompilasi: ia menunjukkan apa yang sebenarnya diputuskan mesin.
Normalisasi mengurangi penggandaan fakta dan anomali update, yang biasanya berarti tabel dan index lebih kecil serta join lebih dapat diandalkan. Denormalisasi bisa tepat untuk analitik atau pola baca berat yang berulang, tapi harus dilakukan sengaja (aturan refresh jelas, duplikasi terkendali) agar konsistensi tidak memburuk.
Skala sering mengharuskan mengubah strategi fisik sambil menjaga makna kueri identik. Alat umum meliputi:
Caching mempercepat baca berulang, tapi tidak memperbaiki kueri yang harus menyentuh terlalu banyak data atau menghasilkan join antar-intermediate yang besar.