📑 Daftar Isi
Pernah kepikiran gak kenapa website kamu tiba-tiba lemot padahal servernya RAM 4GB, CPU jarang nyampe 50%, tapi response time nembus 5 detik? Kok bisa ya? Aku juga heran pas pertama kali ngalamin ini. Server-secondary cuakep, resource lega, tapi MySQL-nya kayak orang ngantuk — lambat banget ngejawab query. Nah, ini nih yang bikin penasaran, dan ternyata root cause-nya seringkali bukan di hardware, tapi di query-query yang gak optimal jalan di production. Jadi gini, cerita sithik ya — kemarin aku lagi handle server client, shared hosting biasa. Ada ticket masuk: “Website kok lambat banget, padahal kemarin fine-fine aja.” Aku cek server, load average masih di bawah 2, RAM sisa 1.2GB, disk I/O normal. Tapi ternyata, satu query SELECT yang jalan setiap page load itu-nya 3.8 detik. Gila. Dan itu terjadi 500 kali per jam. Siapa yang gak pusing?
MySQL slow query sebenarnya bukan masalah baru. Bahkan di server sekelas dedicated enterprise pun, ini bisa muncul kalau query-nya gak di-tune dengan benar. Yang bikin masalah ini frustrasi adalah: kadang server kamu terasa powerful, tapi karena satu query nakal yang gak di-index dengan baik, seluruh user experience berantakan. Aku udah handle kasus ini di berbagai environment — dari shared hosting sampe bare metal dedicated server — dan polanya selalu mirip: ada query yang seharusnya bisa selesai dalam 50ms, tapi karena missing index atau ambiguous join, jadi makan waktu berhari-hari query time-nya.
Masalahnya dudu mung server down wae, lho. Dampak-e iso nggawe revenue loss, trust user mudhun, lan nek iki production environment, iso-iso sampeyan entuk surat cinta seko management. Kalo ecommerce, tiap detik delay itu artinya conversion rate turun. Kalo SaaS, SLA bisa breach. Kalo blog personal sih santai-santai aja sih, tapi tetep gak enak dilihatnya kan. Common causes-nya biasanya: missing index, improper JOIN, SELECT * tanpa filter, subquery yang bisa di-replace pakai JOIN lebih efficient, atau bahkan cuma karena table-nyaFragmented setelah berbulan-bulan insert-delete routine. Dan yang paling sering, orang gak sadar ada query lambat karena gak pernah nge-check slow query log.

Kenapa Query Lambat? Ini Akar Masalahnya
Oke, jadi sebelum kita gas ke solusi, penting banget buat ngerti kenapa query bisa jadi lambat. Bayangin kayak ini: kamu lagi di mall, mau beli kopi. Kalo tokonya udah ditandain dengan jelas di papan petunjuk (index), kamu langsung ke sana. Tapi kalo gak ada petunjuk, kamu muter-muter satu-satu tiap lantai, cek tiap toko satu-satu. Itulah yang terjadi ketika MySQL harus full table scan karena gak ada index yang tepat.
Pertama, kita perlu identifikasi dulu query mana yang bermasalah. MySQL punya fitur built-in yang namanya slow query log. Sayangnya, fitur ini sering disabled di banyak server production karena”takut bikin disk penuh” atau sekadar gak pernah di-enable karena gak tau cara pakainya. Nah, ini yang harus kamu aktifkan dulu. Tanpa slow query log, kamu cuma bermain tebak-tebakan soal query mana yang bikin masalah.
Kedua, setelah kamu tau query mana yang lambat, kamu harus bisa baca EXPLAIN output dari query tersebut. Ini kayak X-ray buat query MySQL. Kamu bisa lihat: apakah query-nya melakukan full table scan (type: ALL), berapa banyak rows yang harus di-scan (rows column), apakah ada Using temporary atau Using filesort — dua red flag besar yang biasanya menandakan query gak optimal. Semua ini bisa kamu pelajari di artikel analisis MySQL EXPLAIN yang udah kita bahas sebelumnya.
Ketiga, masalah seringkali datang dari aplikasi yang pakai ORM atau framework tanpa sadar nge-generate query gak efisien. Laravel Eloquent misalnya, kalo kamu pakai eager loading yang salah, bisa-bisa generate N+1 query yang padahal cukup 1 query aja. Atau di WordPress, plugin yang gak dioptimasi bisa generate query dengan LEFT JOIN ke 5 tabel sekaligus padahal datanya cuma butuh 2 tabel. Ini semua kontributor utama slow query di production. Dan yang paling bikin kesel, kadang developer yang nulis query-nya gak pernah nge-test di data volume production — mereka ngetest di local dengan 100 rows, terus deploy ke production yang udah punya 5 juta rows. Hasilnya? Request timeout di production.
Step 1: Aktifkan dan Cek Slow Query Log
Oke, skip basa-basi, langsung ke langkah pertama. Pertama-tama, kita musti cek apakah slow query log udah aktif di server kamu. Caranya gampang banget — kamu tinggal nge-run SQL command berikut ini:
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
Kalau slow_query_log-nya OFF, itu berarti kamu buta — gak tau query mana yang bermasalah. Well, not literally buta, tapi kamu basically guessing. Dan guessing di production environment itu bahaya. Jadi pastikan kamu nge-enable slow query log dengan cara ini:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
SET GLOBAL long_query_time = 1;
Angka 1 di situ artinya: setiap query yang eksekusinya lebih dari 1 detik akan dicatat ke slow query log. Kalau di server kamu traffic-nya tinggi, kamu bisa naikkin ke 2 atau 3 detik biar log-nya gak terlalu besar. Tapi kalau lagi troubleshoot, set ke 0.5 atau bahkan 0.1 biar kamu bisa catch semua query yang butuh waktu lebih dari setengah detik. Ingat, di production, setiap query yang lebih dari 200ms itu udah patut dipertanyakan.
Selain itu, pastikan juga kamu nge-cek slow query log yang udah ada. Kadang log-nya udah aktif tapi file-nya kosong — bisa jadi karena long_query_time-nya kegedean, atau emang server-nya udah optimal. Tapi yang paling sering: log-nya aktif tapi file-nya gak pernah di-rotate, jadi size-nya udah 5GB dan disk mulai penuh. Kamu bisa pakai panduan rotasi log MySQL yang udah kita bahas sebelumnya buat handle ini.
Step 2: Analisis Query dengan EXPLAIN
Nah, setelah kamu punya daftar slow query dari log, saatnya kita bongkar satu-satu. Ambil query yang paling sering muncul di slow query log — biasanya itu yang paling berdampak ke performa. Terus, jalankan EXPLAIN sebelum query-nya:
EXPLAIN SELECT u.name, o.total, o.created_at
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active'
AND o.created_at > '2026-01-01'
ORDER BY o.total DESC
LIMIT 20;
Lihat output EXPLAIN-nya dengan teliti. Yang harus kamu perhatikan: type column — kalau ALL, itu full table scan, berarti MySQL nge-scan seluruh rows di table. Ini red flag besar. Idealnya type harus ref, eq_ref, atau range. Terus, rows column — kalau angkanya gede banget (jutaan), tapi LIMIT kamu cuma 20, itu artinya MySQL harus scan jutaan rows dulu baru dapat 20 rows yang kamu mau. Gak efisien kan? Dan perhatiin juga Extra column — kalau muncul Using temporary atau Using filesort, itu biasanya berarti ada ORDER BY atau GROUP BY yang gak di-support oleh index yang ada.
Beberapa hal yang sering muncul di EXPLAIN output dan harus kamu waspadai:
| EXPLAIN Extra | Artinya | Solusi |
|---|---|---|
| Using filesort | MySQL harus sort result di memory karena gak ada index yang cover ORDER BY | Buat composite index yang include kolom ORDER BY |
| Using temporary | MySQL bikin temporary table — biasanya karena GROUP BY atau DISTINCT | Optimasi GROUP BY, pakai index yang tepat |
| Using where | Filter dilakukan setelah fetch dari table | Pastikan WHERE clause pakai indexed column |
| Using index | Covering index — gak perlu fetch dari table | Good! Pertahankan ini |
| type: ALL | Full table scan | Buat index di kolom yang di-WHERE atau JOIN |
Step 3: Optimasi dengan Index yang Tepat
Ya, index adalah jawaban untuk sebagian besar masalah slow query. Tapi hati-hati — bikin index itu kayak nyalain kipas angin: terlalu sedikit gak ngefek, terlalu banyak malah bikin panas. Eh, analoginya kurang tepat. Lebih tepatnya: bikin index yang tepat itu seperti punya papan petunjuk yang jelas di mall — kamu langsung tau harus ke mana. Tapi kalo papan petunjuknya terlalu banyak dan saling konflik, malah bikin bingung kan.
Buat kamu yang belum familiar, begini cara bikin index yang efektif berdasarkan EXPLAIN output kita tadi:
-- Composite index untuk query di atas
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at, total);
Kenapa composite index? Karena query kita punya WHERE di dua kolom (user_id dan created_at), plus ORDER BY di total. Dengan composite index, MySQL bisa langsung cari rows yang match tanpa harus sort ulang. InnoDB di MySQL 8.0 juga support descending index, jadi kamu bisa bikin index yang cover ORDER BY total DESC secara native.
Tapi jangan asal bikin index juga. Tiap index yang kamu tambah itu artinya: lebih banyak disk space, lebih lambat INSERT/UPDATE/DELETE (karena index harus di-update juga), dan lebih lama backup time. Jadi sebelum bikin index, tanya dulu: “Apakah query ini benar-benar sering dijalankan?” Kalau cuma sekali sehari, mungkin gak worth it bikin index khusus. Tapi kalau query-nya jalan 500 kali per jam, ya udah, bikin index-nya sekarang juga.
Di artikel strategi index MySQL production, kita udah bahas secara lebih detail soal kapan harus bikin index dan kapan harus skip. Intinya: prioritaskan query yang paling sering muncul di slow query log, dan pastikan index-nya cover semua kolom yang di-WHERE, JOIN, ORDER BY, dan SELECT (kalau memungkinkan, bikin covering index supaya MySQL gak perlu fetch dari table utama).
Step 4: Review Query Pattern dan Rewrite
Oke, sekarang kita masuk ke bagian yang agak tricky — rewrite query. Kadang, masalahnya bukan di index, tapi di cara query-nya ditulis. Contoh klasik: SELECT * di table yang punya 50 kolom padahal kamu cuma butuh 3 kolom. Atau subquery yang bisa di-replace pakai JOIN lebih efficient. Atau WHERE clause dengan fungsi yang gak sargable (artinya MySQL gak bisa pakai index untuk filter-nya).
Contoh query yang gak efisien:
-- INI lambat (gak sargable)
SELECT * FROM orders WHERE YEAR(created_at) = 2026;
-- INI lebih cepat (sargable)
SELECT * FROM orders
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';
Kenapa yang pertama lambat? Karena YEAR(created_at) itu fungsi yang dijalankan di setiap row — MySQL harus baca setiap row, extract tahun-nya, terus bandingin sama 2026. Gak bisa pakai index. Yang kedua langsung compare raw value, jadi bisa pakai range scan di index. Beda banget kan performanya?
Contoh lain yang sering muncul di application code — N+1 query:
-- INI N+1 (lambat)
SELECT * FROM users WHERE status = 'active';
-- Loop results, for each:
SELECT * FROM orders WHERE user_id = ?;
-- INI lebih efisien (eager loading / JOIN)
SELECT u.*, o.id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'active';
N+1 query itu killer banget di production. Bayangin kamu punya 1000 active users — artinya kamu execute 1 (initial query) + 1000 (loop query) = 1001 queries. Dengan JOIN, cukup 1 query aja. Bedanya? Bisa puluhan kali lebih cepat. Di WordPress, kamu bisa pakai panduan optimasi database WordPress buat handle query pattern yang umum di CMS ini, karena banyak plugin WordPress yang tanpa sadar generate N+1 query.
Step 5: Optimasi Server Configuration MySQL
Selain query-nya sendiri, konfigurasi MySQL juga berpengaruh banget ke performa. Beberapa setting yang sering di-skip padahal impact-nya besar:
# /etc/mysql/mysql.conf.d/mysqld.cnf atau /etc/my.cnf
# Buffer pool - set ke 50-70% dari RAM dedicated
innodb_buffer_pool_size = 3G
# Query cache (MySQL 8.0: deprecated, MariaDB 10.6: masih bisa)
# query_cache_type = 1
# query_cache_size = 64M
# Temporary table size
max_heap_table_size = 256M
tmp_table_size = 256M
# Join buffer
join_buffer_size = 4M
# Sort buffer
sort_buffer_size = 4M
# Thread cache
thread_cache_size = 64
Yang paling krusial itu innodb_buffer_pool_size. Ini adalah memory yang digunakan MySQL buat caching data dan index di RAM. Kalau buffer pool-nya kecil, MySQL bakal sering baca dari disk — dan disk I/O itu jauh lebih lambat dari RAM. Di dedicated MySQL server, buffer pool biasanya di-set ke 50-70% dari total RAM. Tapi hati-hati, jangan sampe kegedean juga karena OS juga butuh RAM. Di VPS dengan RAM terbatas, kamu mungkin perlu baca artikel optimasi MySQL di VPS RAM kecil buat tau cara allocate memory yang efisien.
Selain itu, perhatiin juga query cache. Di MySQL 8.0, query cache udah di-deprecate dan dihapus karena malah bikin bottleneck di high-concurrency environment. Tapi di MariaDB, query cache masih bisa dipakai dan cukup helpful untuk read-heavy workload. Kalau kamu pakai MySQL 8.0, alternatifnya pakai ProxySQL atau aplikasi-level caching (Redis/Memcached) buat cache hasil query yang sering diakses.
Step 6: Monitoring dan Maintenance Berkala
Satu hal yang sering dilupakan: optimasi MySQL itu bukan one-time job. Server kamu berubah seiring waktu — data bertambah, traffic naik, query pattern berubah. Jadi kamu perlu monitoring berkala. Beberapa cara yang bisa kamu lakukan:
- Schedule periksa slow query log — minimal seminggu sekali, cek query mana yang masuk slow query log. Kalau ada query baru yang sering muncul, langsung investigate.
- Pakai performance_schema — MySQL 8.0 punya performance_schema yang bisa kasih kamu detail statistik per query tanpa harus nge-check slow query log.
- ANALYZE TABLE berkala — tabel yang udah dipakai bertahun-tahun bisaFragmented, bikin index gak optimal. Jalankan ANALYZE TABLE untuk update statistik index.
- OPTIMIZE TABLE — kalau ada tabel yang udah banyak delete-nya, OPTIMIZE TABLE bisa rebuild table dan index biar lebih compact.
-- Check slow queries from performance_schema
SELECT
DIGEST_TEXT AS query_pattern,
COUNT_STAR AS exec_count,
ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_time_ms,
SUM_ROWS_EXAMINED AS total_rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
Output di atas bakal kasih kamu 10 query dengan average terlama. Dari situ, kamu bisa langsung target mana yang harus dioptimasi dulu. Biasanya, cukup 2-3 query yang dioptimasi dengan benar udah bisa bikin perbedaan signifikan ke overall server performance. Ingat: gak semua query harus perfect — yang penting query yang paling sering dijalankan dan paling lambat itu yang harus diprioritasin.
Q: Bagaimana cara tahu kalau MySQL butuh index baru?
Cek slow query log dan perhatikan query yang sering muncul dengan exec_time tinggi. Jalankan EXPLAIN di query tersebut — kalau type-nya ALL (full table scan) atau ada Using filesort/Using temporary, itu tanda kuat butuh index baru. Kamu juga bisa pakai performance_schema buat lihat query mana yang paling banyak examine rows.
Q: Apakah semua query lambat harus di-index?
Tidak juga. Kalau query-nya cuma dijalankan sekali sehari atau beberapa kali sehari, mungkin gak worth it bikin index khusus karena index juga punya overhead untuk INSERT/UPDATE/DELETE. Prioritasin query yang sering dijalankan (high frequency) dan punya dampak besar ke user experience.
Q: Berapa idealnya long_query_time di production?
Untuk production web server, 1 detik adalah angka yang umum dipakai. Kalau kamu lagi troubleshoot spesifik, turunkan ke 0.5 atau 0.2 detik biar bisa catch lebih banyak query. Tapi kalau server-nya high-traffic, terlalu rendah bisa bikin log-nya gede banget. Jadi sesuaikan saja dengan kebutuhanmu.
Nah, udah tau kan kenapa MySQL slow query itu bisa jadi masalah besar dan gimana cara handle-nya? Mulai dariaktifkan slow query log, baca EXPLAIN output, bikin index yang tepat, rewrite query nakal, sampe optimasi server config — semua ini langkah-langkah yang bisa langsung kamu praktekin di production server. Gak perlu bongkar hardware, gak perlu upgrade RAM, gak perlu migrate ke server baru. Yang kamu butuhkan cuma sedikit waktu buat investigate dan optimasi.
Coba praktekin step-step di atas satu-satu. Mulai dari langkah paling gampang: nyalain slow query log dulu. Setelah itu, cek log-nya tiap beberapa hari. Pastiin kamu udah cek: 1) slow query log aktif, 2) long_query_time sesuai kebutuhan, 3) EXPLAIN untuk query lambat, 4) index sudah optimal, 5) query pattern sudah efisien. Kalau semua oke, kamu bakal lihat perbedaan yang cukup signifikan di response time website. Semangat ya! Udah pasti bisa!