Pay For Results. Our new Performance Model is live.

wp_postmeta Is Huge: What That Actually Means, and What to Do

wp_postmeta is meant to be your biggest table. The size is rarely the problem — unindexed meta_value lookups and orphaned rows are. How to tell them apart.

Picture of Adam Walters
Adam Walters

Founder

wp_postmeta. Every plugin, every revision and every post you deleted leaves rows here.
IN THIS ARTICLE

You sorted your tables by size and wp_postmeta came out on top with a few million rows, an order of magnitude larger than wp_posts. That is normal. On almost every WordPress site wp_postmeta is the biggest table in the database, and on a healthy site it should be.

Which means the size on its own tells you nothing useful. There are two questions worth asking instead: is anything querying it in a way MySQL cannot answer efficiently, and how much of it is rows that nothing will ever read again. Those have different fixes, and only one of them involves deleting anything.

What is actually in there

Four columns. That is the entire table.

meta_id     bigint(20) unsigned   PRIMARY, auto_increment
post_id     bigint(20) unsigned   indexed
meta_key    varchar(255)          indexed
meta_value  longtext              not indexed

post_id points at a row in wp_posts. meta_key is the field name, meta_value holds the data as longtext regardless of what it really is. One row per field, per post.

A product with 40 custom fields is 40 rows. A page builder storing its whole layout as one JSON blob is a single row that might be 400KB. Both are normal. And “post” here means every row in wp_posts: revisions, autosaves, menu items, attachments and every custom post type. That is why the ratio to wp_posts looks so lopsided.

Why size is the wrong thing to look at

WordPress ships this table with three indexes and no more.

IndexAnswers this fastCannot help with
PRIMARY on meta_idFetching one known rowAnything you would realistically ask for
post_id“Give me every field on post 47”, the common caseAny lookup that starts from a key or a value
meta_key (first 191 chars)“Which posts have _my_field set?”Telling apart two keys sharing a 191-character prefix
nothing on meta_valueNoneEvery meta_value = 'x' comparison. Row by row.
The last row is the one that costs you. WordPress cannot index an unbounded text column, so filtering by value means reading rows.

meta_value is a longtext column and it is not indexed, because MySQL cannot usefully index an unbounded text column. So any query filtering on the value (which is exactly what a meta_query in WP_Query generates) compares rows one at a time.

At 50,000 rows nobody notices. At 4 million, one unindexed comparison is the difference between a page that renders in 200ms and one that times out. A filtered shop archive doing three of them per request is the classic version of this.

There is a subtler catch in that table too. The meta_key index covers only the first 191 characters of the key. WordPress reduced it to 191 in version 4.2, when it moved to utf8mb4: InnoDB caps an index prefix at 767 bytes, which is 191 characters at four bytes each, against 255 at one. Keys are rarely that long, so it seldom bites. But if two plugins generate keys sharing a 191-character prefix, the index alone cannot separate them.

Diagram showing a meta_query hitting the unindexed meta_value column and forcing a full table scan

Find out what is filling it

Group by key and sort by the space each one occupies. Adjust the wp_ prefix to match your install:

SELECT meta_key,
       COUNT(*) AS row_count,
       ROUND(SUM(LENGTH(meta_value)) / 1048576, 1) AS mb
FROM wp_postmeta
GROUP BY meta_key
ORDER BY SUM(LENGTH(meta_value)) DESC
LIMIT 25;

The output almost always sorts into three groups: page-builder layout blobs, which are large but few; plugin bookkeeping, which is small but in the millions of rows; and keys belonging to something you removed years ago.

Then count the rows that point at nothing:

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

On a site that has had plugins come and go for a few years, this number is regularly in the hundreds of thousands.

Comparison showing unfamiliar meta keys should be left alone while orphaned rows whose post no longer exists are safe to delete

What is safe to delete

Orphaned rows. A row whose post_id no longer exists in wp_posts is unreachable by any WordPress function. Nothing can read it, ever. Back up first, then:

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

The backup instruction is not decoration here. There is no trash and no undo.

Meta belonging to a plugin you have permanently removed, once you are certain it is not coming back. Delete by exact meta_key, never by a LIKE pattern, because prefixes are shared far more often than people expect.

What is not safe is any meta_key you cannot identify on a post that still exists. An unfamiliar key is not clutter until you have proved it is; it may be the only copy of a field somebody’s template reads. Search the key name before you touch it, and if nothing comes back, leave it.

Stopping it filling up again

Revisions are the one source you can cap with a single line. Left alone, WordPress keeps every revision indefinitely, and each one is a row in wp_posts that can carry meta of its own. In wp-config.php:

define( 'WP_POST_REVISIONS', 5 );

Setting it to false disables revisions entirely, which is usually a step too far. Five is plenty for recovering from a bad edit, and it puts a ceiling on the growth rather than clearing it once.

The rest is plugin hygiene. Uninstall rather than deactivate when you are finished with something, and check afterwards whether the plugin actually cleaned up, because a great many do not.

Where this sits with the rest of the database

wp_postmeta is core WordPress, so it applies everywhere. The other tables that reliably get out of hand belong to specific plugins and behave differently: the Action Scheduler actions table is a queue, so it grows when something stops draining it, and the WooCommerce sessions table holds live state you must not truncate. Knowing which kind you are looking at decides what you are allowed to do to it, which is the whole argument in our guide to WordPress database optimization.

Keeping all of this in check without anyone having to remember is what managed WordPress hosting is for.

Frequently asked questions

How big should wp_postmeta be?

There is no target number, because it scales with content and with how many plugins store data per post. It is normally the largest table in a WordPress database and often ten to fifty times the row count of wp_posts. A better question than size is the ratio of orphaned rows to live ones, and whether any of your templates filter posts by meta_value.

Will deleting orphaned postmeta rows break anything?

No, provided the query is the one that joins against wp_posts and keeps only rows whose post_id has no matching post. Those rows are unreachable through get_post_meta() or any other WordPress function, because there is no post to ask about. Take a backup anyway; the postmeta table has no trash and the delete cannot be undone.

Does cleaning wp_postmeta make my site faster?

Sometimes, and less than people hope. Removing orphaned rows shrinks backups and speeds up full-table operations, but if the site was slow because a template filters posts by meta_value, it will still be slow afterwards with fewer rows to scan. Fix the query pattern first and treat cleanup as housekeeping.

Why is wp_postmeta so much larger than wp_posts?

Because it holds one row per field per post, and because “post” in WordPress terms includes revisions, autosaves, attachments, menu items and every custom post type. A single product with 40 fields plus 20 revisions can account for several hundred rows on its own.

Can I add an index on meta_value to speed up meta queries?

You can add a prefix index, for example on the first 20 characters, and MySQL will use it for exact comparisons that fit within that prefix. It is a real option for a specific known query, but it slows writes on a table that is written to constantly, it will not help LIKE searches with a leading wildcard, and it is a custom schema change that nobody after you will expect to find. Changing how the site queries the data is almost always the better fix.

Should I use a plugin to clean wp_postmeta?

A cleanup plugin is fine for orphaned rows and revision trimming, which are the safe, well-defined operations. Be much more careful with anything offering to remove “unused” meta keys, because it cannot know which of your templates or snippets read a given key. Run the grouping query yourself first so you recognise what is being offered up for deletion.

Is your competitor stealing traffic?

Get a free competitor spy report. See their keywords, backlinks, and map rankings.

SHARE:
WP-Cron only fires when somebody loads a page. Here is how to confirm it has…
September 15, 2026
The autoload column decides what WordPress loads on every single request. Here is what the…
September 8, 2026
WordPress MCP lets AI tools like Claude and ChatGPT act on your site as a…
September 5, 2026
Get the Blueprint

Join 5,000+ local business owners receiving weekly growth tactics.

Performance Partner Application

We invest our own capital to generate your leads. To ensure this partnership is profitable for both of us, we need to verify a few details about your business.

Let's Connect.

Have a specific question or need a custom proposal? Fill out the form and our team will get back to you within 24 hours.

Get Your Free Audit

See where you stand before you launch.