Your Database Is a Ticking Time Bomb
Most WordPress crashes are not caused by bad plugins or cheap hosting. They start in the database. A single unindexed query, a bloated options table, or a missing cleanup routine can bring a site to its knees under real traffic.
After reviewing hundreds of WordPress databases, the same 11 mistakes appear over and over. Here is what they are, why they happen, and exactly how to fix each one before it takes your site down. For more speed and caching guides, explore our performance hub.
WordPress loads every autoloaded option on every single page request. When plugins dump megabytes of serialized data into wp_options with autoload set to yes, your site loads that data whether it needs it or not.
The Wrong Way
Saving large arrays, logs, or cache data directly into wp_options without setting autoload to no.
The Right Way
Audit your autoloaded options regularly. Run this query to find the biggest offenders:
SELECT option_name, LENGTH(option_value) AS size
FROM wp_options
WHERE autoload = 'yes'
ORDER BY size DESC
LIMIT 20;
Any option larger than 100KB that does not need to load on every page should be switched to autoload=no. For plugin developers, always use the fourth parameter in add_option() or call update_option( $key, $value, false ) to disable autoloading.
WordPress stores every revision of every post by default. A site with 500 posts and 50 revisions each has 25,000 extra rows in wp_posts. That is dead weight slowing down every post query.
The Wrong Way
Leaving WP_POST_REVISIONS at its default unlimited value and never purging old revisions.
The Right Way
Limit revisions in wp-config.php:
define( 'WP_POST_REVISIONS', 5 );
Then clean up existing revisions with WP-CLI:
wp post delete $(wp post list --post_type=revision --format=ids) --force
Or be selective and delete revisions older than 90 days using a direct query on wp_posts where post_type is revision and post_date is older than your cutoff.
Transients are temporary cached values stored in wp_options. When they expire, WordPress does not automatically delete them. Over months, thousands of expired transients pile up, bloating the options table.
The Wrong Way
Assuming WordPress handles transient cleanup automatically. It does not unless you have an object cache.
The Right Way
Delete expired transients periodically:
wp transient delete --expired
Better yet, install a persistent object cache like Redis. When you use Redis, transients are stored in memory instead of the database, and Redis handles expiration natively.
Using $wpdb->query() with raw user input is the fastest way to get SQL injected. It also bypasses WordPress caching and can cause unexpected results with special characters.
The Wrong Way
$wpdb->query( "DELETE FROM wp_posts WHERE ID = " . $_GET['id'] );
The Right Way
Always use $wpdb->prepare():
$wpdb->query(
$wpdb->prepare( "DELETE FROM wp_posts WHERE ID = %d", absint( $_GET['id'] ) )
);
The prepare method escapes inputs and prevents injection. There is never a valid reason to skip it when user data touches your queries.
When plugins create custom tables, they often skip adding proper indexes. The table works fine with 100 rows. At 100,000 rows, every query becomes a full table scan.
The Wrong Way
Creating a table with only a primary key and no additional indexes on columns you filter or sort by.
The Right Way
Add indexes on any column used in WHERE, ORDER BY, or JOIN clauses:
ALTER TABLE wp_custom_logs ADD INDEX idx_user_date (user_id, created_at);
Check for missing indexes by running EXPLAIN on slow queries. If you see “Using filesort” or “Using temporary” with large row counts, you need an index.
MyISAM uses table-level locking. When one query writes to a table, every other query on that table waits. Under any real traffic, this creates a queue that brings your site down.
The Wrong Way
Running older tables on MyISAM because “it has always worked.”
The Right Way
Convert all tables to InnoDB:
ALTER TABLE wp_posts ENGINE=InnoDB;
ALTER TABLE wp_postmeta ENGINE=InnoDB;
ALTER TABLE wp_options ENGINE=InnoDB;
InnoDB supports row-level locking, crash recovery, and foreign keys. There is no modern reason to use MyISAM for WordPress. Check all your tables with SHOW TABLE STATUS; and convert any that are still MyISAM.
wp_postmeta is the most abused table in WordPress. Plugins store everything there because it is easy. A WooCommerce store with 10,000 orders can have millions of postmeta rows.
The Wrong Way
Querying postmeta with multiple JOINs on unindexed meta_key and meta_value columns.
The Right Way
Add a composite index on meta_key and post_id:
ALTER TABLE wp_postmeta ADD INDEX idx_meta_key_post (meta_key(191), post_id);
For high-volume sites, consider moving frequently queried meta into a custom table with proper columns instead of the EAV (Entity-Attribute-Value) pattern that postmeta uses.
When you delete a post, WordPress deletes the post row but sometimes leaves its postmeta, term relationships, and comments behind. Plugins that store custom post types are especially bad at cleanup.
The Wrong Way
Deleting posts without checking if their metadata gets cleaned up.
The Right Way
Periodically clean orphaned postmeta:
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;
Do the same for wp_term_relationships and wp_comments. On a site with years of plugin installs and removals, this can reclaim thousands of rows.
Counting all posts, calculating statistics, or running complex JOINs on every page load is a performance killer. These queries hit the database when the result rarely changes.
The Wrong Way
Putting a complex aggregate query inside a shortcode or widget that runs on every page view.
The Right Way
Cache expensive query results using transients or object cache:
$stats = get_transient( 'site_stats' );
if ( false === $stats ) {
$stats = expensive_stats_query();
set_transient( 'site_stats', $stats, HOUR_IN_SECONDS );
}
Invalidate the cache only when the underlying data changes, not on every page load.
The InnoDB buffer pool is where MySQL keeps table and index data in memory. The default is often 128MB, which is tiny for a WordPress site with any traffic.
The Wrong Way
Running MySQL with default settings on a server with 4GB+ RAM.
The Right Way
Set innodb_buffer_pool_size to 50-70% of available RAM for a dedicated database server:
[mysqld]
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
Monitor buffer pool hit rate with SHOW STATUS LIKE 'Innodb_buffer_pool_read%';, you want a hit rate above 99%. If reads from disk are high, increase the buffer pool.
Tables fragment over time. Deleted rows leave gaps. Indexes get stale. Without regular maintenance, performance degrades slowly until one day the site crawls.
The Wrong Way
Never running OPTIMIZE TABLE or checking table health.
The Right Way
Set up a monthly maintenance routine:
wp db optimize
Or run it per-table for large databases:
OPTIMIZE TABLE wp_posts;
OPTIMIZE TABLE wp_postmeta;
OPTIMIZE TABLE wp_options;
Combine this with monitoring. Set up slow query logging (slow_query_log = 1 and long_query_time = 1) to catch degrading queries before users notice.
Run through this monthly:
- Audit autoloaded options, nothing over 100KB unless essential
- Purge post revisions beyond your limit
- Delete expired transients
- Clean orphaned postmeta, term relationships, and comments
- Check all tables are InnoDB
- Verify indexes on custom tables
- Review slow query log for new offenders
- Run OPTIMIZE TABLE on large tables
- Confirm buffer pool hit rate is above 99%
A healthy database is the single biggest performance lever for WordPress. Fix these 11 mistakes and your site will handle traffic spikes that would crash most WordPress installations. If you encounter issues along the way, our fixing WordPress errors guides can help you troubleshoot.
database cleanup database optimization
Last modified: March 26, 2026