Find and Remove Orphaned Post Meta (WP-CLI)
Adds a "wp orphaned-meta" command that counts custom field rows left behind by deleted posts, grouped by meta key, and removes them in small batches when you add --delete.
Opens WPIDE on
Code
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
WP_CLI::add_command( 'orphaned-meta', function ( $args, $assoc_args ) {
global $wpdb;
$rows = $wpdb->get_results(
"SELECT pm.meta_key, COUNT(*) AS rows_found FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.ID IS NULL GROUP BY pm.meta_key ORDER BY rows_found DESC",
ARRAY_A
);
if ( ! $rows ) {
WP_CLI::success( 'No orphaned post meta found.' );
return;
}
WP_CLI\Utils\format_items( 'table', $rows, array( 'meta_key', 'rows_found' ) );
if ( empty( $assoc_args['delete'] ) ) {
WP_CLI::log( 'Add --delete to remove these rows.' );
return;
}
$deleted = 0;
do {
// Batches keep table locks short on big sites.
$ids = $wpdb->get_col(
"SELECT pm.meta_id FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE p.ID IS NULL LIMIT 1000"
);
if ( $ids ) {
$deleted += (int) $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_id IN (" . implode( ',', array_map( 'absint', $ids ) ) . ')' );
}
} while ( count( $ids ) === 1000 );
wp_cache_flush();
WP_CLI::success( sprintf( 'Deleted %d orphaned meta rows.', $deleted ) );
}, array(
'shortdesc' => 'Reports or deletes post meta whose post no longer exists.',
'synopsis' => array(
array( 'type' => 'flag', 'name' => 'delete', 'optional' => true ),
),
) );
Notes
Back up the database before running with --delete.
How to add this snippet
- Install WPIDE on your WordPress site (it is free) and open Code Snippets.
- Use snippet above adds it from the library in one click. Or copy the code into a new snippet with the same code type.
- It is added inactive: review the code and settings, then switch it on.