Nearly every plugin stores data in post meta: SEO titles, WooCommerce prices, page builder layouts, custom fields. Understanding how it’s stored helps you debug plugins, speed up slow sites and clean up a bloated database.
The table
Post meta lives in wp_postmeta (the prefix may differ):
| Column | Meaning |
|---|---|
meta_id |
Primary key |
post_id |
The post, page or custom post type it belongs to |
meta_key |
The field name, e.g. _yoast_wpseo_title, _price |
meta_value |
The value, stored as LONGTEXT |
Keys starting with _ are “protected” — hidden from the Custom Fields box in the editor.
There are matching tables for other objects: wp_usermeta, wp_termmeta and wp_commentmeta.
Everything is text
meta_value is a text column. Numbers are stored as strings, and arrays or objects are serialized by PHP:
a:2:{s:5:"width";i:800;s:6:"height";i:600;}
Consequences:
- Sorting numbers needs
'orderby' => 'meta_value_num'or'type' => 'NUMERIC'in meta queries. - You can’t reliably search inside serialized values with SQL.
- Never edit serialized data with a plain search-and-replace — changing a string’s length breaks it. Use
wp search-replace, which handles serialization.
How WordPress loads meta
When you call get_post_meta() for a post, WordPress loads all meta for that post in one query and caches it for the rest of the request. In loops, WP_Query primes the meta cache for every post at once (update_post_meta_cache), so you don’t get one query per post.
Without a persistent object cache, that work repeats on every request. Adding Redis (with an object-cache plugin) keeps meta in memory between requests — a big win for WooCommerce and meta-heavy sites.
Inspecting meta
With WP-CLI:
wp post meta list 123
wp post meta get 123 _price
wp post meta update 123 _price 19.99
With SQL, find which plugins store the most meta:
SELECT meta_key, COUNT(*) AS rows_count
FROM wp_postmeta
GROUP BY meta_key
ORDER BY rows_count DESC
LIMIT 20;
Cleaning up
Deleting a plugin usually leaves its meta behind. Over years, wp_postmeta can grow to millions of rows.
Back up first: wp db export backup.sql.
Orphaned meta (rows whose post no longer exists):
DELETE pm FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;
Meta for old revisions is removed when revisions are deleted — limit revisions in wp-config.php:
define( 'WP_POST_REVISIONS', 10 );
Leftover keys from a removed plugin (check the key names first!):
wp db query "DELETE FROM wp_postmeta WHERE meta_key LIKE '\_oldplugin\_%';"
Performance notes
meta_keyis indexed (first 191 characters);meta_valueis not. Queries filtering on values scan many rows.- For data you filter constantly (e.g. thousands of events by date), a custom table or taxonomy is faster.
- Give MySQL/MariaDB enough memory (
innodb_buffer_pool_size) to keepwp_postmetain RAM. On a VPS you control this setting — see Linux server performance.
Summary
Post meta is a flexible key/value store attached to posts. Know that values are text (and sometimes serialized), use WP-CLI for safe edits, add an object cache on busy sites, and clean up orphaned rows periodically. For the coding side — registering meta, meta boxes and queries — see WordPress custom post meta.