LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Blog Overview /WordPress Plugins & Tricks / WP-CLI Delete Commands: Which Tables Each One...
Read this article in other languages:

WP-CLI Delete Commands: Which Tables Each One Touches and What It Leaves Behind

WP-CLI Delete Commands: Which Tables Each One Touches and What It Leaves Behind
Contents
  1. The Two-Step Pattern That Replaces the Dry Run
  2. Two Ways to Remove the Same Row
  3. What Each Command Actually Touches
  4. The Trash Is Not a Backup
  5. Expired Is Not the Same as All
  6. Optimising Is a Rebuild, Not a Tidy-Up
  7. Questions and answers
  8. Sources

Database housekeeping in WordPress is a handful of commands that all look alike and behave nothing alike. One goes through the application and cleans up after itself; the next one touches a single table and leaves four others holding rows that point at nothing.

What makes the difference matter is that WP-CLI, with a few exceptions, has no dry run. There is no way to ask a delete command what it would do. There is only the discipline of asking a different command first.

Two table maps side by side: wp post delete removes the post row together with meta, comments, comment meta and term relationships; a direct SQL DELETE removes only the post row and leaves 43 orphaned rows
The same row, two ways of removing it. The right-hand version reports success just as cheerfully.

The Two-Step Pattern That Replaces the Dry Run

Every destructive command has a harmless sibling that takes the same filter. wp post delete has wp post list; wp comment delete has wp comment list. Running the list first with --format=count answers the only question a dry run would have answered: how many rows is this about to remove.

# 0. a backup, outside the document root
wp db export /home/pi/backup-$(date +%F).sql

# 1. count, with exactly the filter that will be used
wp post list --post_type=revision --format=count

# 2. delete, with exactly the same filter
wp post list --post_type=revision --format=ids | xargs -n 100 wp post delete --force

The xargs in the last line is not decoration. Substituting several thousand IDs directly into the command line runs into the argument length limit, and the failure arrives after some of the work is done rather than before any of it – which is the one situation the whole exercise was meant to avoid.

The backup line has a detail worth keeping: without a path, wp db export writes the dump into the current directory, and the current directory during a WordPress command is usually the document root. A full database dump with a guessable name, served over HTTP, is a worse outcome than anything the cleanup was going to fix.

Two Ways to Remove the Same Row

wp post delete calls the same function the editor calls. It removes the post meta, the comments, the comment meta and the term relationships, and it fires the hooks that let plugins remove whatever they attached elsewhere – a search index entry, a cache, a row in their own table.

A direct DELETE FROM wp_posts removes one row. Everything that referred to it stays, unreachable and invisible: no query joins to a post that is gone, so the rows simply never appear again. That is tolerable until an ID is reused, and post IDs are reused after a database import – at which point the new post silently inherits the old one’s custom fields and comments.

What Each Command Actually Touches

Command Removes Leaves behind
wp post delete <id> --force post, meta, comments, term links the attached media files and their attachment rows
wp post delete <attachment> --force the row and the files on disk, all sizes references to the URL inside other posts
wp post delete <revisions> the revisions and their own meta rows the parent post, untouched
wp comment delete --status=spam comments and comment meta the per-post comment counts, until a recount
wp transient delete --expired only transients whose expiry has passed every transient stored without an expiry
wp db optimize nothing a table lock for the duration of the rebuild

The first row is the one that surprises people. Deleting a post does not delete the images in it – attachments are posts in their own right, with their own IDs, and they survive their parent. A site cleaned of a thousand old posts therefore still carries a thousand posts’ worth of media, which is where the disk space actually went.

The comment count row is the cheapest to fix and the easiest to forget: after any bulk comment deletion, wp comment recount puts the numbers back in agreement with the rows. Nothing breaks without it; the counts are simply wrong, everywhere, quietly.

The Trash Is Not a Backup

Without --force, deleting moves a post to the trash, which is a status change and nothing more. The row stays, the meta stays, the media stays, and the space is not freed – a trash emptied once a month is a housekeeping habit, not a safety net.

With --force the post skips the trash entirely and there is no undo inside WordPress at all. The only thing standing between a mistyped filter and a lost month is the dump written in step zero, which is the reason it is step zero.

Expired Is Not the Same as All

Transients are a cache with an expiry stored alongside the value. wp transient delete --expired removes those whose time has passed and is safe by construction: anything it deletes was already invalid.

--all is a different command wearing the same name. It also removes transients stored without an expiry, and some plugins use exactly those as their only storage for something that was expensive to compute – a licence check, an inventory, a rendered menu. Nothing breaks visibly; the site simply spends the next hour rebuilding things nobody asked it to rebuild, and one or two plugins behave as if they had just been installed.

Optimising Is a Rebuild, Not a Tidy-Up

wp db optimize runs OPTIMIZE TABLE, and for InnoDB that is not an optimisation but a full rebuild of the table plus its indexes. The table is locked for writes while it happens, which on a small blog is a second and on a large wp_postmeta is long enough for visitors to notice.

It is also the step that most reliably reclaims nothing. Space freed by deleted rows is already reused by InnoDB for new rows; the file on disk only shrinks if a great deal was deleted and nothing has been written since. Worth running once after a large cleanup, at a quiet hour, and not worth putting in a nightly cron – which is precisely where it usually ends up.

Questions and answers

How can rows be found that no longer point at any post after a direct DELETE?

With a join that asks explicitly for absence. For post meta, the counting step looks like this:

wp db query 'SELECT COUNT(*) FROM wp_postmeta pm LEFT JOIN wp_posts p ON p.ID = pm.post_id WHERE p.ID IS NULL'

The same pattern checks wp_comments via comment_post_ID and wp_term_relationships via object_id. The last table needs care, because object_id does not only point at posts: the link categories of the old link manager refer to wp_links, and some plugins attach terms to users or to objects of their own. There the check has to be limited to the taxonomies that are actually attached to posts.

The deletion comes only after the backup and with exactly the same condition, as DELETE pm FROM wp_postmeta pm LEFT JOIN wp_posts p ON p.ID = pm.post_id WHERE p.ID IS NULL. That is the two-step pattern from the article, only in SQL. Both statements read all of wp_postmeta, so on a large installation they are better run at a quiet hour. The wp_ prefix is the default and may differ on a given installation.

Can the number of revisions be capped permanently instead of deleting them again and again?

Yes. Set WP_POST_REVISIONS in wp-config.php: a number specifies how many revisions to keep per post, while false disables new revisions. A numeric cap is applied when a post is next saved with changes; WordPress then removes that post’s oldest revisions above the cap. Setting the constant does not immediately clean up existing revisions. With false, they are left untouched entirely, so the one-off cleanup from this article is still needed for that stock.

What does a persistent object cache change about wp transient delete?

It moves the transients out of the database and into the cache. With a persistent object cache such as Redis or Memcached, new transients no longer end up in wp_options, and wp transient delete only reaches the rows that remain there, usually leftovers from before the cache was installed. WP-CLI points this out with a warning.

Inside the cache, transients with an expiry lapse on their own, because the expiry becomes the entry’s expiration time. Usually the only way to remove all of them at once is wp cache flush, which empties the entire object cache, not just the transients. The effect is the same as with --all, only wider: nothing breaks, but the site spends a while rebuilding everything, and if several installations share the same cache, their entries may be hit too, depending on the setup.

What is different on a multisite installation?

Every site in the network has its own tables with their own prefix, such as wp_2_posts, and a WP-CLI command without --url only works on the main site. Covering the whole network therefore means running every step once per site, the count included, for instance with wp site list --field=url feeding xargs. wp db export, by contrast, backs up the whole database without further options, and with it every site at once.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Write a comment

Experience with other plugins or hosting environments and questions about the setup are welcome here.

The email address is not published. Required fields are marked with an asterisk.

Articles & categories

CCTV

All 6 articles in this category Follow this category by RSS

Cloud & AI

All 18 articles in this category Follow this category by RSS

Data Privacy

All 20 articles in this category Follow this category by RSS

Digital Analytics

All 60 articles in this category Follow this category by RSS

Digital Marketing

All 38 articles in this category Follow this category by RSS

IT & Networks

All 19 articles in this category Follow this category by RSS

Music Production

All 18 articles in this category Follow this category by RSS

Raspberry PI

All 11 articles in this category Follow this category by RSS

SaaS & Internet Earning

Follow this category by RSS

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

All 11 articles in this category Follow this category by RSS

WordPress Plugins & Tricks

All 15 articles in this category Follow this category by RSS