SQL Function Overuse

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 email tidak 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_email maupun email

  • Untuk 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:

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_id sebagai 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 Scan
  • Index Scan
  • Estimated vs actual rows

Ini adalah alat wajib engineer backend.


Ringkasan #

PraktikDampak
Function di WHEREIndex tidak dipakai, full scan
Function di JOINQuery sangat mahal, CPU tinggi
Overuse functionScalability buruk, cost naik
Query sederhana + indexPerforma 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.

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact