WordPress DB Optimization

Share
WordPress DB Optimization

wp_options Health Check

-- Row count (healthy: 500–2000, bad: 50k+)
SELECT COUNT(*) FROM wp_options;

-- Autoload payload size (>1MB = problem, >3MB = serious, >10MB = broken)
SELECT SUM(LENGTH(option_value)) / 1024 / 1024 AS autoload_mb
FROM wp_options WHERE autoload = 'yes';

-- Biggest rows overall
SELECT option_name,
       LENGTH(option_value) / 1024 AS size_kb,
       autoload
FROM wp_options
ORDER BY LENGTH(option_value) DESC
LIMIT 30;

-- Biggest autoloaded rows above 100KB
SELECT option_name,
       LENGTH(option_value) / 1024 AS size_kb
FROM wp_options
WHERE autoload = 'yes'
AND LENGTH(option_value) > 102400
ORDER BY LENGTH(option_value) DESC;

Transients

-- Count expired transients still in DB
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP();
# Delete expired only (safe for production)
wp transient delete --expired --path=/var/www/html

# Delete ALL transients (only if Redis is active)
wp transient delete --all --path=/var/www/html

Warning

Only run --all if Redis object cache is confirmed working.
Without Redis, the next traffic wave will hammer the DB as everything recalculates.


Autoload Fixes

-- Disable autoload on known-safe computed/cache rows
UPDATE wp_options SET autoload = 'no'
WHERE option_name = 'elementor_css_print_method';

UPDATE wp_options SET autoload = 'no'
WHERE option_name LIKE 'learndash_course_info_%';

UPDATE wp_options SET autoload = 'no'
WHERE option_name LIKE 'bb_platform_%'
AND option_name NOT IN ('bb_platform_core', 'bb_platform_settings');

Warning

Never blindly mass-set autoload = 'no'. Options like siteurl, blogname,
active_plugins must stay autoloaded or the site crashes.


Orphaned Data Cleanup

-- Orphaned postmeta
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;

-- Orphaned usermeta
DELETE um FROM wp_usermeta um
LEFT JOIN wp_users u ON um.user_id = u.ID
WHERE u.ID IS NULL;

-- Old BuddyBoss activity (discuss retention period with client first)
DELETE FROM wp_activity
WHERE date_recorded < DATE_SUB(NOW(), INTERVAL 1 YEAR);
# Delete post revisions
wp post delete $(wp post list --post_type=revision --format=ids) \
  --force --path=/var/www/html

Table Fragmentation

-- Check fragmentation across key tables
SELECT table_name,
       data_length / 1024 / 1024 AS data_mb,
       data_free / 1024 / 1024 AS free_mb,
       ROUND(data_free / (data_length + data_free) * 100, 1) AS fragmentation_pct
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name IN ('wp_options', 'wp_postmeta', 'wp_usermeta');

-- Reclaim space after bulk deletes
OPTIMIZE TABLE wp_options;
OPTIMIZE TABLE wp_postmeta;
OPTIMIZE TABLE wp_usermeta;

Warning

OPTIMIZE TABLE on large InnoDB tables can take minutes and causes a lock.
Run during off-peak or use pt-online-schema-change for zero-downtime.
Requires ~2x table size in free disk space temporarily.


Orphaned Plugin Data Detection

-- Find high-volume prefixes (cross-ref against active plugins)
SELECT DISTINCT SUBSTRING_INDEX(option_name, '_', 1) AS prefix,
       COUNT(*) AS count,
       SUM(LENGTH(option_value)) / 1024 AS total_kb
FROM wp_options
GROUP BY prefix
ORDER BY total_kb DESC
LIMIT 40;

Redis Verification

redis-cli ping                          # expect PONG
redis-cli info stats | grep keyspace    # hits vs misses ratio
wp redis status --path=/var/www/html    # plugin connection status

# Watch BuddyPress cache writes in real time
redis-cli monitor | grep buddypress

Query Monitor (Temporary — Never Leave on Production)

wp plugin install query-monitor --activate --path=/var/www/html
# Load activity feed → check query count
# 50+ queries with near-identical patterns = N+1 confirmed
wp plugin deactivate query-monitor --path=/var/www/html

Slow Query Log

-- Enable at runtime
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';

-- Disable when done
SET GLOBAL slow_query_log = 'OFF';
SET GLOBAL log_queries_not_using_indexes = 'OFF';
# /etc/mysql/mysql.conf.d/mysqld.cnf (permanent)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
tail -f /var/log/mysql/slow.log
# Red flag: Rows_examined in the hundreds of thousands for small Rows_sent

wp_bp_activity Index Check

SHOW INDEX FROM wp_bp_activity;
SHOW INDEX FROM wp_bp_activity_meta;

-- Verify query uses index (look for type=ALL = full table scan = bad)
EXPLAIN SELECT * FROM wp_bp_activity
WHERE component = 'activity'
ORDER BY date_recorded DESC LIMIT 20;

Automated Weekly Cleanup Script

#!/bin/bash
# /usr/local/bin/wp-db-cleanup.sh

WP_PATH="/var/www/html"
LOG="/var/log/wp-cleanup.log"
DATE=$(date '+%Y-%m-%d %H:%M')
DB_NAME="wp_database"

echo "[$DATE] Starting cleanup" >> $LOG

# Backup wp_options before touching anything
mysqldump $DB_NAME wp_options > /backups/wp_options_$(date +%Y%m%d).sql

# Cleanup
wp transient delete --expired --path=$WP_PATH >> $LOG 2>&1
wp post delete $(wp post list --post_type=revision --format=ids --path=$WP_PATH) \
  --force --path=$WP_PATH >> $LOG 2>&1
wp cache flush --path=$WP_PATH >> $LOG 2>&1

# Report autoload payload size
AUTOLOAD_SIZE=$(wp eval \
  'echo round(strlen(serialize(wp_load_alloptions())) / 1024, 1);' \
  --path=$WP_PATH 2>/dev/null)
echo "[$DATE] Autoload payload: ${AUTOLOAD_SIZE}KB" >> $LOG

echo "[$DATE] Done" >> $LOG
# Add to crontab — weekly Sunday 3AM
0 3 * * 0 /usr/local/bin/wp-db-cleanup.sh

PHP Settings for LearnDash + BuddyBoss

memory_limit = 512M
max_execution_time = 300
upload_max_filesize = 256M
max_input_vars = 5000

Note

max_input_vars below 3000 causes LearnDash settings page to silently drop fields.


Quick Reference — What's Safe to Delete

Target Method Safe?
Expired transients wp transient delete --expired ✅ Always
All transients wp transient delete --all ✅ Only with Redis active
Post revisions wp post delete by type ✅ After confirming with client
Orphaned postmeta LEFT JOIN delete ✅ Exclude sfwd-quiz post type
Orphaned usermeta LEFT JOIN delete
bb_platform_* options Manual review only ⚠️ Check each prefix
learndash_course_info_* Set autoload=no
Old BP activity DATE_SUB delete ⚠️ Client approval required
siteurl, active_plugins Never touch

Autoload Size Thresholds

Size Status Action
< 1 MB Healthy Monitor quarterly
1–3 MB Degraded Clean transients, audit large rows
3–10 MB Serious Immediate cleanup + autoload audit
> 10 MB Critical Emergency cleanup, check for runaway plugin