Sebelum saya cerita migrasi 90 menit di artikel sebelumnya, ada satu tahap krusial yang men-trigger keputusan migrasi itu, investigation database query slow di shared hosting. Klien marketplace topup game online saya lapor 4 gejala konsisten selama 2 minggu di Maret 2026:
- Admin dashboard lambat, buka Posts list atau WooCommerce orders butuh 8-12 detik
- Frontend TTFB random spike, 90% request 700-900ms, tapi 10% lompat ke >2000ms
- WP-Cron yang seharusnya selesai 5 detik jadi 40 detik
- Customer complain checkout lambat, 1 dari 10 transaksi pending karena timeout di payment callback
Saya install Query Monitor plugin di shared. Hasilnya jelas: 5 query pattern konsisten muncul di tab Queries by Slowest, kontribusi 78% dari total query time per request.
Tapi di Rumahweb SMARTbiasa: tidak ada SSH, tidak ada akses slow query log MariaDB, tidak ada DDL grant untuk ALTER TABLE ADD INDEX. Investigation bisa, optimize tidak bisa.
Inilah yang men-trigger migrasi ke Contabo VPS, bukan cuma karena resource limit, tapi karena debuggability & tunability di shared = nol. Setelah migrasi, saya re-identify dengan toolkit lengkap (Query Monitor + slow query log + EXPLAIN + pt-query-digest), lalu optimize 5 query satu per satu.
Result: TTFB drop dari 612 ms (shared) → 187 ms (post-migrasi awal) → 94 ms (post-optimize). Query count per request: 387 → 142. Customer complaint pending hilang. Admin Posts list yang sebelumnya 8 detik jadi 1.2 detik.
Artikel ini saya tulis untuk Anda yang sedang di posisi yang sama: site WordPress lambat, sudah pakai page cache, sudah upgrade hosting, tapi TTFB tetap melonjak random. Kemungkinan besar penyebabnya bukan resource, tapi query pattern di database yang tidak Anda lihat.
1. Kenapa Query Slow di WordPress = Silent Killer
Query slow berbeda dengan server lambat atau network lambat:
- Server lambat: TTFB tinggi semua request, CPU/RAM exhausted, jelas terlihat di htop
- Network lambat: latency tinggi tapi konsisten, bisa dilihat di mtr ke origin
- Query slow: TTFB random spike, sebagian request cepat (cache hit), sebagian lambat 5-10x baseline. Tidak terlihat di server monitoring biasa karena CPU/RAM masih lega
Yang lebih bahaya: query slow tidak triggered Cloudflare error rate alert, tidak muncul di status page, dan tidak ke-log di nginx access log selain durasi yang sedikit lebih tinggi.
Tabel BEFORE vs AFTER (Marketplace Topup, 1 bulan)
| Metrik | Shared (Rumahweb) | VPS Migrasi Awal | VPS Post-Optimize |
|---|---|---|---|
| TTFB rata-rata | 612 ms | 187 ms | 94 ms |
| TTFB p95 (worst 5%) | 1.840 ms | 612 ms | 220 ms |
| Query count per request | 387 | 387 | 142 |
| Total query time per request | 2.150 ms | 720 ms | 180 ms |
| Admin Posts list load | 8.4 detik | 4.1 detik | 1.2 detik |
| WP-Cron duration | 40 detik | 18 detik | 3.8 detik |
| DB size | 124 MB | 124 MB | 42 MB |
| Customer complaint /day | 3-5 | 1-2 | 0 |
Catatan kolom VPS Migrasi Awal: angka di kolom ini lebih baik dari shared karena resource VPS lebih besar, tapi query count & query time per request TIDAK BERUBAH, buktinya bottleneck bukan cuma resource, tapi struktur query itu sendiri.
2. Setup Tooling: Query Monitor + Slow Query Log + EXPLAIN (10 menit)
Toolkit yang saya pakai untuk investigation:
Di sisi WordPress: Query Monitor plugin
Install via WP Admin atau WP-CLI:
wp plugin install query-monitor --activate
Buka frontend / admin page sebagai user dengan capability manage_options, toolbar Query Monitor muncul di footer. Tab yang paling sering saya pakai:
- Queries — semua query yang jalan di request itu, urut berdasarkan waktu eksekusi
- Queries by Caller — group by file PHP yang trigger query (untuk identify plugin/theme penyebab)
- Queries by Component — group by core/plugin/theme
Di sisi server: MariaDB slow query log + pt-query-digest
Edit /etc/mysql/mariadb.conf.d/50-server.cnf:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.1
log_queries_not_using_indexes = 1
Restart MariaDB:
sudo systemctl restart mariadb
sudo tail -f /var/log/mysql/slow.log
⚠️ Catatan #1
slow_query_log = ONproduces non-trivial IO load di production. Aktifkan saat investigation (24-48 jam), lalu disable kembali denganslow_query_log = 0+ restart MariaDB. Untuk monitoring permanen pakai performance_schema (built-in MySQL/MariaDB) yang IO impact lebih ringan.
Setelah log terkumpul 1 hari, aggregate dengan pt-query-digest (dari Percona Toolkit):
sudo apt install percona-toolkit -y
sudo pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
Output slow_report.txt mengelompokkan query yang pattern-nya sama (mengabaikan literal value), urut by total impact (waktu eksekusi × frekuensi).
Pro tip 💡 #1 — Selalu jalankan
EXPLAINdi query yang ditemukan slow:EXPLAIN SELECT * FROM wp_postmeta WHERE post_id=4521 AND meta_key='_thumbnail_id';Yang Anda cek di output:
type—ALL= full table scan = bad.ref / eq_ref / const= good (pakai index).rows— estimated rows scanned. Kalau >1000 untuk query yang harusnya return <10 row, ada masalah.Extra— kalau munculUsing filesortatau Using temporary, query butuh optimasi tambahan.

3. Query #1: wp_options Autoload Bloat (Silent Performance Killer)
Gejala
Setiap request WordPress meload semua row wp_options dengan autoload='yes' ke memory PHP — happens di wp-includes/option.php function wp_load_alloptions(). Default install: ~150 row autoload, total ~50 KB. Bloat case di klien saya: 1.247 row autoload, total 87 MB, semua di-load setiap request.
TTFB konsisten 700-900ms tanpa pattern jelas karena bottleneck-nya memory allocation + transfer, bukan query speed sendiri.
Cara identifikasi
SELECT option_name, LENGTH(option_value) AS size_bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size_bytes DESC
LIMIT 20;
Output sample dari klien saya (top 10):
option_name | size_bytes
-----------------------------------------+------------
_transient_woocommerce_admin_processing | 12,847,232
_transient_jetpack_plugin_api_cache | 8,432,512
_site_transient_theme_roots | 4,128,256
abandoned_cart_logs | 3,847,168
_transient_wc_orders_count_pending | 2,981,440
wpforms_form_cache | 1,847,232
wp_seopress_metas | 847,168
yoast_indexable_cache | 612,832
_transient_external_ip_address | 12,288
...
8 dari top 10 adalah transient yang expired tapi autoload='yes', bukan default WordPress, kemungkinan plugin yang sudah uninstall meninggalkan orphan.
Cara optimize
# Hitung total autoload size dulu sebagai baseline
wp option list --autoload=on --format=count
# Output: 1247
# Bulk fix: matikan autoload untuk semua transient
wp db query "UPDATE wp_options SET autoload='no' WHERE option_name LIKE '_transient_%';"
# Targeted: hapus orphan dari plugin lama
wp option delete abandoned_cart_logs
wp option delete wpforms_form_cache
wp option delete yoast_indexable_cache # kalau Yoast sudah uninstall
# Cleanup expired transient
wp transient delete --expired
⚠️ Catatan #2 UPDATE
wp_optionsbulk seperti di atas TIDAK BISA dijalankan di shared hosting Rumahweb SMALL (no DDL/UPDATE grant via phpMyAdmin). Inilah satu lagi alasan migrasi ke VPS, DDL freedom. Di shared, opsi Anda terbatas pakai plugin “Autoload Manager” yang lebih lambat per-option.
BEFORE / AFTER
- Query
SELECT option_name, option_value FROM wp_options WHERE autoload='yes':380 ms → 18 ms (~21× faster) - Memory PHP per request: 94 MB → 18 MB
- TTFB rata-rata: 187 ms → 142 ms (improvement 24%)
4. Query #2: wp_postmeta Tanpa Composite Index
Gejala
WooCommerce product page lambat random, terutama untuk produk dengan banyak meta_key (variation, custom field, ACF). Query Monitor menunjukkan 1 query yang sama muncul 20-50× per request:
SELECT * FROM wp_postmeta WHERE post_id = 4521 AND meta_key = '_thumbnail_id';
Cara identifikasi
EXPLAIN SELECT * FROM wp_postmeta WHERE post_id = 4521 AND meta_key = '_thumbnail_id';
Output SEBELUM index:
+----+------+--------+------+------+---------+--------+
| id | type | table | rows | key | ref | Extra |
+----+------+--------+------+------+---------+--------+
| 1 | ALL | wp_p.. | 78432| NULL | NULL | Using where |
+----+------+--------+------+------+---------+--------+
type: ALL = full table scan. rows: 78,432 = scan seluruh table untuk ambil 1 row. Ini yang bikin 220 ms per query × 40× per request = 8.8 detik wasted.
WordPress default schema wp_postmeta punya index post_id saja dan meta_key (191 char) saja, tapi TIDAK punya composite index (post_id, meta_key). Untuk query yang filter pakai keduanya, MariaDB harus pilih salah satu index dan filter sisanya manual.
Cara optimize
CREATE INDEX idx_postmeta_post_key ON wp_postmeta (post_id, meta_key(32));
Catatan: meta_key(32) artinya partial index hanya 32 karakter pertama. Cukup untuk WordPress karena meta_key biasanya <32 char.
Verify dengan EXPLAIN ulang:
+----+------+--------+------+---------------------+---------+
| id | type | table | rows | key | Extra |
+----+------+--------+------+---------------------+---------+
| 1 | ref | wp_p.. | 1 | idx_postmeta_post_key| |
+----+------+--------+------+---------------------+---------+
type: ref (good!), rows: 1 (perfect).

BEFORE / AFTER
- Query individual: 220 ms → 12 ms (~18× faster)
- Total impact per WooCommerce product page (40 queries): 8.800 ms → 480 ms
5. Query #3: wp_usermeta Capability di Setiap Request (Object Cache Win)
Gejala
Setiap page load, frontend (untuk personalize) atau admin (untuk permission check), WordPress call current_user_can(). Function ini query wp_usermeta untuk fetch wp_capabilities
user. Di Query Monitor: query yang sama muncul 5-8× per request:
SELECT * FROM wp_usermeta WHERE user_id = 1 AND meta_key = 'wp_capabilities';
5 call × 145 ms = 725 ms wasted per request.
Cara identifikasi
Di Query Monitor tab Queries by Caller, search current_user_can, Anda akan lihat backtrace dari WordPress core, theme, dan plugin yang trigger query yang sama berulang. Setiap call sebenarnya bisa cache, tapi default WordPress non-persistent cache hilang per request.
Cara optimize: Pasang Redis Object Cache
Instalasi Redis di Ubuntu 24.04:
sudo apt install redis-server -y
# Set Redis untuk LAN-only + maxmemory + eviction policy
sudo sed -i 's/^# maxmemory <bytes>/maxmemory 512mb/' /etc/redis/redis.conf
sudo sed -i 's/^# maxmemory-policy noeviction/maxmemory-policy allkeys-lru/' /etc/redis/redis.conf
sudo systemctl restart redis-server
# Install + activate Redis Object Cache plugin
wp plugin install redis-cache --activate
wp redis enable
# Verify
wp redis info | head -10
Output target:
Status: Connected
Client: PhpRedis (v6.0.2)
Hits: 8,432
Misses: 412
Hit Ratio: 95.34%
Pro tip 💡 #2 Redis vs Memcached untuk WordPress: saya selalu pilih Redis karena
- Support persistence (AOF/RDB), kalau Redis restart, cache tidak hilang seluruh
- Support pub/sub & data types, bisa di-reuse untuk WooCommerce sessions, cart, dll
- Redis-CLI lebih friendly untuk debug (
KEYS *, MEMORY USAGE, OBJECT ENCODING)Memcached lebih ringan kalau Anda cuma butuh KV store, tapi diferensiasi-nya kecil.
BEFORE / AFTER
- Query
wp_capabilities(5 call/request): 725 ms → 2 ms total (Redis hit, ~360× faster) - Hit ratio Redis setelah 24 jam warmup: 95.34%

6. Query #4: wp_posts SELECT * di Admin Tanpa LIMIT
Gejala
Posts list di admin untuk Custom Post Type (shop_order, product, atau CPT custom) lambat 8-12 detik. Query Monitor menunjukkan satu query “fat”:
SELECT * FROM wp_posts WHERE post_type IN ('post','product','shop_order')
ORDER BY post_date DESC;
Return 12.300 row, di-sortir di PHP, lalu pagination di PHP juga, bukan di SQL. Plugin atau theme nakal sering override pre_get_posts dan strip LIMIT.
Cara identifikasi
Di Query Monitor, perhatikan kolom Rows untuk query yang call di admin context. Kalau >1.000 untuk query single-page admin list, ada masalah. Lihat Caller untuk identify plugin/theme yang trigger.
EXPLAIN-nya:
type: range
rows: 12300
Extra: Using where; Using filesort
type=range sebenarnya bukan terburuk, tapi rows=12300 + Using filesort mengindikasikan sort dilakukan di disk, bukan in-memory.
Cara optimize
Cari hook pre_get_posts yang strip LIMIT di kode plugin/theme. Atau bypass dengan custom WP_Query yang explicit set posts_per_page:
// File: wp-content/mu-plugins/admin-list-limiter.php
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() || ! $query->is_main_query() ) {
return;
}
if ( in_array( $query->get( 'post_type' ), [ 'shop_order', 'product' ], true ) ) {
$query->set( 'posts_per_page', 20 );
$query->set( 'no_found_rows', true ); // skip COUNT(*) FOUND_ROWS
}
}, 999 );
Drop file di wp-content/mu-plugins/ (auto-loaded, tidak butuh activate via admin).
BEFORE / AFTER
- Query
SELECT * FROM wp_posts ...:1.840 ms → 95 ms (~19× faster) - Admin Posts list page load: 8.4 detik → 1.2 detik
7. Query #5: Transient Bloat di wp_options (Hidden Cron Killer)
Gejala
- WP-Cron yang seharusnya 5 detik jadi 40 detik (firing setiap menit di traffic tinggi)
- Frontend random spike >2.000ms ketika cron fire — coincident dengan transient cleanup
- DB size growth 2-3 MB / day tanpa konten baru masuk
Cara identifikasi
SELECT
COUNT(*) AS total_transient,
SUM(LENGTH(option_value))/1024/1024 AS total_MB,
COUNT(CASE WHEN option_name LIKE '_transient_timeout_%' THEN 1 END) AS expired_metadata
FROM wp_options
WHERE option_name LIKE '_transient_%'
OR option_name LIKE '_site_transient_%';
Output dari klien saya:
total_transient | total_MB | expired_metadata
8432 | 47.23 | 4216
8.432 transient, 47 MB total. Setengah dari itu (4.216) adalah expired metadata yang tidak ke-cleanup karena cron yang harusnya hapus mereka berhenti error.
Cara optimize
# Cleanup expired transient
wp transient delete --expired
# Cleanup ALL transient (drastis, hati-hati — beberapa plugin re-create otomatis)
wp transient delete --all
# Setup cron daily 04:00 WIB untuk auto-cleanup
sudo crontab -u www-data -e
# Tambah baris:
0 4 * * * cd /var/www/marketplace && wp transient delete --expired --quiet
Kalau Redis Object Cache sudah aktif (lihat Section 5), Anda bisa pindahkan transient backend ke Redis, transient akan disimpan di Redis (memory) bukan di wp_options (disk). Ini otomatis kalau plugin Redis Object Cache aktif dengan filter wp_using_ext_object_cache return true.
Pro tip 💡 #3 Beberapa plugin “nakal” store data permanen sebagai transient untuk bypass autoload check:
- Plugin security tertentu store firewall rules sebagai
_transient_wf_rules_*tanpa expiry- Page builder tertentu store cache CSS rendered sebagai
_transient_pagebuilder_*dengan expiry 1 tahun- AI assistant / chatbot plugin store conversation history sebagai transient
Cek list transient dengan
wp transient list --alldan audit pattern nama yang mencurigakan.
BEFORE / AFTER
- Query
SELECT ... WHERE option_name LIKE '_transient_%':480 ms → 8 ms (~60× faster) - WP-Cron duration: 40 detik → 3.8 detik
- DB size: 124 MB → 42 MB (-66%)
8. Hasil Akhir: BEFORE vs AFTER Comprehensive
Setelah 5 query di atas optimized, ini hasil 14 hari monitoring (7 sebelum vs 7 setelah):
| Metrik | Post-Migrasi Awal | Post-Optimize 5 Query | A |
|---|---|---|---|
| TTFB rata-rata | 187 ms | 94 ms | -50% |
| TTFB p50 (median) | 178 ms | 87 ms | -51% |
| TTFB p95 | 612 ms | 220 ms | -64% |
| TTFB p99 | 1.840 ms | 412 ms | -78% |
| Query count per request | 387 | 142 | -63% |
| Total query time per request | 720 ms | 180 ms | -75% |
| Admin Posts list load | 4.1 detik | 1.2 detik | -71% |
| WP-Cron full run | 18 detik | 3.8 detik | -79% |
| DB size | 124 MB | 42 MB | -66% |
| Customer complaint /day | 1-2 | 0 | -100% |

Total kontribusi 5 query yang dioptimize: 78% dari query time per request.
9. Maintenance: Cron + Monitor (Cegah Bloat Lagi)
Ada gunanya optimize kalau 3 bulan lagi DB Anda bloat lagi. Saya setup 3 cron berikut di Contabo VPS:
# /etc/cron.d/wp-maintenance — drop di Contabo VPS
# Daily 04:00 WIB — cleanup expired transient
0 4 * * * www-data cd /var/www/marketplace && wp transient delete --expired --quiet
# Weekly Sunday 04:30 WIB — wp db optimize (OPTIMIZE TABLE)
30 4 * * 0 www-data cd /var/www/marketplace && wp db optimize --quiet
# Monthly 1st 05:00 WIB — pt-query-digest report dari slow log → email
0 5 1 * * root pt-query-digest /var/log/mysql/slow.log | mail -s "Monthly Slow Query Report" [email protected]
Untuk monitoring kontinyu (opsional):
- Grafana + MariaDB exporter (Prometheus-based, gratis), query/sec, slow query count, buffer pool hit ratio. Setup 1-2 jam.
- Simpler alternative: Uptime Kuma yang sudah Anda pakai untuk monitor uptime, tambah HTTP check ke
/wp-admin/edit.phpsetiap 5 menit dengan timeout 3 detik. Kalau timeout 2× berturut-turut, alert, biasanya tanda query slow regression.
10. Anti-Pattern: 4 “Optimasi” yang Sebenarnya Bikin Lambat
Beberapa “best practice” yang viral tapi sebenarnya counter-productive:
| Anti-Pattern | Kenapa Salah | Yang Benar |
|---|---|---|
| Plugin “WP Database Cleaner All-in-One” yang bersih segala | Sering hapus row penting (woocommerce_sessions, wp_term_taxonomy_count) tanpa whitelist | Pakai WP-CLI dengan targeted command + cron, jangan plugin GUI |
| Disable autoload SEMUA transient tanpa pikir | AABeberapa transient legit perlu autoload (theme_mods, current_theme_root) — kalau di-disable, theme broken | Disable autoload selective berdasarkan pattern nama + ukuran (>10 KB) |
| ALTER TABLE add index berlebihan (“biar aman”) | Setiap index nambah cost untuk INSERT/UPDATE. >5 index per table = write performance turun signifikan | Add index hanya untuk query yang muncul di slow log + verify dengan EXPLAIN |
| Pasang Redis tanpa set maxmemory + eviction policy | Redis bisa konsumsi RAM tanpa batas → OOM kill MySQL/PHP-FPM → site down total | AASelalu set maxmemory (50-70% available RAM) + maxmemory-policy allkeys-lruAA |
11. FAQ
Apakah saya bisa optimize query slow di shared hosting tanpa migrasi VPS?
Sebagian bisa: cleanup autoload via plugin (slower than WP-CLI), cleanup transient via plugin. Tapi ALTER TABLE ADD INDEX, slow query log, dan Redis Object Cache butuh akses root yang tidak ada di shared SMALL. Hasilnya: optimize ~30-40%, sisa-nya butuh migrasi.
Berapa lama interval cron cleanup transient yang optimal?
Daily 04:00 WIB cukup untuk site dengan traffic ribuan visit/day. Untuk site puluhan ribu visit/day, kurangi jadi 6 jam sekali. Kalau di bawah ribuan visit/day, weekly Sunday juga aman.
Composite index sebaiknya (post_id, meta_key) atau (meta_key, post_id)?
Tergantung selektivitas. Untuk WordPress: (post_id, meta_key) karena query biasanya filter spesifik post_id dulu (selectivity tinggi), baru meta_key. Verify dengan EXPLAIN di workload Anda, kalau row scan masih tinggi, swap order.
Redis Object Cache vs Memcached, mana yang lebih cocok untuk WooCommerce?
Redis. Selain object cache, bisa di-reuse untuk WooCommerce sessions (wc_session_* keys) dan future feature seperti rate limiting. Memcached cuma cocok kalau infrastruktur Anda sudah pakai Memcached untuk service lain.
Apakah Query Monitor aman di-leave aktif di production?
Aman secara fungsional, Query Monitor hanya display data ke user dengan capability manage_options. Tapi ada overhead ~5-10ms per request untuk capture data. Saran: aktif saat investigation, deactivate saat production stable. Kalau ingin permanen, gunakan filter qm/dispatchers untuk disable display ke browser, hanya log ke file.
Kalau shared hosting saya kasih SSH (misalnya Niagahoster Cloud / Cloudways), bisa optimize selevel VPS?
Hampir bisa. SSH = bisa pakai WP-CLI untuk autoload + transient cleanup. Tapi ALTER TABLE, slow query log, dan custom my.cnf masih butuh root access yang biasanya tidak diberikan di “managed shared”. Cloudways memberikan akses lebih dalam dari shared standar tapi tetap di bawah VPS unmanaged.
12. Penutup
5 query pattern di atas (wp_options autoload, wp_postmeta tanpa composite index, wp_usermeta capability, wp_posts SELECT *, transient bloat) bertanggung jawab 78% query time di site WordPress production manapun yang saya audit selama 2 tahun terakhir — apakah WooCommerce, marketplace, atau blog dengan traffic ribuan.
Yang harus Anda ingat: migrasi VPS bukan obat ajaib untuk site WordPress lambat. Resource lebih besar membantu, tapi kalau pattern query Anda tidak dioptimize, Anda cuma mindahin masalah ke server yang lebih mahal. Yang saya lakukan di klien marketplace: investigate dulu di shared (limited), pindah VPS, lalu optimize 5 query satu per satu. TTFB drop dari 612ms → 94ms, 6.5× lebih cepat, bukan dari resource tapi dari struktur query.
Saya rekomendasikan urutan baca artikel saya untuk konteks lengkap WordPress production:
- Migrasi WordPress dari Shared Hosting ke VPS dalam 90 Menit
- Setup VPS Ubuntu 24.04 dari Nol untuk WordPress
- Tembus PageSpeed 100 via Nginx FastCGI Cache
- Audit Plugin WordPress: 5 Plugin yang Saya BANNED
Last Updated on Mei 25, 2026 by larhtechBro



