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 indexedpost_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.
| Index | Answers this fast | Cannot help with |
|---|---|---|
PRIMARY on meta_id | Fetching one known row | Anything you would realistically ask for |
post_id | “Give me every field on post 47”, the common case | Any 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_value | None | Every meta_value = 'x' comparison. Row by row. |
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.

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.

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.




