SQL Function Overuse #
Dalam praktik sehari-hari, kita sering menemukan query SQL yang menggunakan function (misalnya LOWER(), DATE(), CAST(), SUBSTRING(), TRIM(), dan sejenisnya) langsung di klausa WHERE atau JOIN. Contohnya terlihat sederhana dan works, tetapi di balik itu terdapat dampak performa yang sangat serius, terutama ketika data membesar.
Artikel ini akan membahas secara mendetail:
- Kenapa function di WHERE dan JOIN itu buruk
- Hubungannya dengan index dan full table scan
- Dampak jika praktik ini di-overuse
- Contoh nyata kasus di sistem produksi
- Best practice yang seharusnya dilakukan
Artikel ini relevan untuk engineer backend, data engineer, maupun software engineer yang berurusan dengan database relasional seperti MySQL / MariaDB / PostgreSQL / SQL Server.
Konsep Dasar: Bagaimana Database Mengeksekusi Query #
Sebelum masuk ke masalah, kita perlu paham bagaimana database bekerja saat mengeksekusi query.
Query Planner & Index #
Database memiliki query planner / optimizer yang bertugas menentukan cara paling efisien untuk mengeksekusi query:
- Apakah bisa menggunakan index?
- Index mana yang paling optimal?
- Apakah perlu scan seluruh tabel?
Index bekerja optimal ketika kondisi query bersifat:
column OPERATOR constant
Contoh:
WHERE email = '[email protected]'
WHERE created_at >= '2025-01-01'
Dalam kondisi ini, database bisa:
- Melakukan index seek (langsung lompat ke data yang relevan)
- Menghindari membaca seluruh tabel
Masalah Utama: Function di WHERE #
Contoh Query yang Bermasalah #
SELECT * FROM users
WHERE LOWER(email) = '[email protected]';
Sekilas terlihat wajar, tetapi ini sangat berbahaya.
Kenapa Ini Buruk? #
Ketika kamu menulis:
LOWER(email) = '[email protected]'
Artinya:
- Database HARUS menghitung
LOWER(email)untuk SETIAP ROW - Index pada kolom
emailtidak bisa digunakan
Akibatnya:
- Database terpaksa melakukan FULL TABLE SCAN
- Setiap baris diproses satu per satu
Dampak Nyata #
Jika tabel users berisi:
- 10 ribu data → masih terasa ringan
- 1 juta data → mulai lambat
- 50 juta data → query bisa memakan detik hingga timeout
Dan ini baru 1 query.
Function di JOIN: Lebih Berbahaya Lagi #
Contoh JOIN yang Bermasalah #
SELECT *
FROM orders o
JOIN users u ON LOWER(o.user_email) = LOWER(u.email);
Apa yang Terjadi di Balik Layar? #
Database tidak bisa menggunakan index di
user_emailmaupunemailUntuk setiap baris di
orders, database harus:- Menghitung function
- Membandingkan dengan setiap baris di
users
Ini sering berujung pada:
- Nested Loop Join + Full Scan
- Kompleksitas mendekati
O(n × m)
Dampak Produksi #
Dalam sistem nyata:
- CPU database naik drastis
- Query latency melonjak
- Koneksi lain ikut melambat
- Bisa menyebabkan cascading failure di service lain
Kenapa Database Tidak Bisa Pakai Index? #
Index menyimpan nilai as-is sesuai kolom.
Contoh:
Index email:
- "[email protected]"
- "[email protected]"
Ketika kamu menulis:
LOWER(email)
Database tidak punya index untuk hasil LOWER(email).
⚠️ Index TIDAK bekerja pada hasil function, kecuali:
- Database mendukung functional index / expression index
- Dan index tersebut memang dibuat
Overuse Function: Bom Waktu di Sistem Besar #
Pola yang Sering Terjadi #
- Banyak endpoint API
- Setiap endpoint punya query dengan function
- Data terus tumbuh
- Traffic meningkat
Awalnya:
- Semua terasa normal
Setelah beberapa bulan:
- Query lambat tanpa sebab yang jelas
- CPU DB tinggi meskipun QPS rendah
- Perlu scale DB lebih besar (biaya naik)
⚠️ Ini bukan masalah hardware, tapi masalah desain query.
Contoh Function yang Paling Sering Menyebabkan Masalah #
Beberapa function yang sering “membunuh” index:
LOWER()/UPPER()DATE()/CAST()/TO_CHAR()SUBSTRING()/LEFT()/RIGHT()TRIM()/REPLACE()COALESCE()(tergantung konteks)
Contoh buruk:
WHERE DATE(created_at) = '2025-01-01'
Best Practice yang Benar #
Normalisasi Data Sejak Awal #
Jika pencarian case-insensitive:
- Simpan data dalam bentuk lowercase
email VARCHAR(255) NOT NULL
Saat insert:
Query menjadi:
WHERE email = '[email protected]'
✅ Index tetap digunakan
Pindahkan Logic ke Application Layer #
❌ Buruk:
WHERE LOWER(username) = LOWER(?)
✅ Baik:
- Lowercase input di application
- Query tetap sederhana
Gunakan Range Query untuk Date #
❌ Buruk:
WHERE DATE(created_at) = '2025-01-01'
✅ Baik:
WHERE created_at >= '2025-01-01 00:00:00'
AND created_at < '2025-01-02 00:00:00'
Index di created_at akan bekerja optimal.
Gunakan Functional / Expression Index (Jika Terpaksa) #
Jika memang tidak bisa dihindari:
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
⚠️ Catatan:
- Tidak semua DB mendukung ini
- Index tambahan = storage + write cost
- Gunakan hanya jika benar-benar perlu
Pastikan JOIN Menggunakan Kolom Asli #
❌ Buruk:
JOIN users u ON LOWER(o.email) = LOWER(u.email)
✅ Baik:
- Gunakan
user_idsebagai foreign key
JOIN users u ON o.user_id = u.id
Ini adalah best practice desain schema, bukan hanya query.
Selalu Gunakan EXPLAIN / EXPLAIN ANALYZE #
Biasakan:
EXPLAIN ANALYZE SELECT ...
Perhatikan:
Seq Scan/Full ScanIndex Scan- Estimated vs actual rows
Ini adalah alat wajib engineer backend.
Ringkasan #
| Praktik | Dampak |
|---|---|
| Function di WHERE | Index tidak dipakai, full scan |
| Function di JOIN | Query sangat mahal, CPU tinggi |
| Overuse function | Scalability buruk, cost naik |
| Query sederhana + index | Performa stabil & scalable |
Penutup #
Menggunakan function di WHERE dan JOIN bukan sekadar masalah gaya penulisan SQL, tapi masalah arsitektur performa.
Query yang terlihat “rapi” bisa menjadi pembunuh sistem ketika data dan traffic bertumbuh.
Ingat prinsip sederhana:
Biarkan database menggunakan index semaksimal mungkin.