Most WordPress developers treat wp_postmeta as a black box. They trust that get_post_meta() will return whatever Advanced Custom Fields or a page builder stuffed into the database. That trust breaks the moment you inherit a site where the original developer is gone, the ACF field groups are corrupted, or a migration stripped serialized data. You are left staring at a table with millions of rows and no map. This article is that map. It is a direct, unsentimental method for reading the raw wp_postmeta table to reverse-engineer the exact custom field strategy a site uses, so you can rebuild it, migrate it, or simply understand what the previous person did.

Why wp_postmeta Becomes a Liability for Small-to-Mid Publishers
Small and mid-sized publishing teams rarely have a dedicated database administrator. The person who built the custom post types and meta boxes three years ago might have left. The site runs on a stack of plugins that each dump their own serialized arrays into wp_postmeta. Over time, the table becomes the single largest object in the database, often exceeding the size of wp_posts by a factor of ten. When performance degrades, or when you need to migrate content to a new theme, you cannot rely on the original field-group definitions. They may not exist. You have to read the meta table directly.
Understanding the Four-Column Structure Before You Query
The wp_postmeta table has exactly four columns: meta_id, post_id, meta_key, and meta_value. The meta_id is a surrogate primary key that you will almost never use in reconstruction work. The post_id links each row to a specific post, page, or custom post type entry in wp_posts. The meta_key is the name of the custom field. The meta_value holds the data, and this is where the real trouble lives. A single field can store a plain string, an integer, a JSON-encoded object, or a PHP-serialized array. You cannot tell which by looking at the column type because it is always LONGTEXT. You have to inspect the content.
Serialized Data: The Silent Schema Killer
When a plugin or theme stores an array or object, WordPress uses PHP’s serialize() function. The resulting string looks like a:2:{s:5:"width";i:800;s:6:"height";i:600;}. This format encodes the type and length of each value. If you perform a search-and-replace on the entire database without adjusting the serialized length markers, you will corrupt the data. I have seen entire media libraries break because someone changed the domain in a serialized attachment metadata string without recalculating the byte counts. When you read wp_postmeta to reconstruct a strategy, you must identify which keys hold serialized data and decode them before analysis.
Step 1: Inventory Every Distinct Meta Key
Start with a simple aggregate query. This gives you the complete vocabulary of custom fields on the site, along with how many posts use each key. The query is straightforward:
SELECT meta_key, COUNT(DISTINCT post_id) AS post_count
FROM wp_postmeta
GROUP BY meta_key
ORDER BY post_count DESC;
Run this and you will see a mix of internal WordPress keys (prefixed with an underscore, like _thumbnail_id or _edit_lock) and keys that belong to themes or plugins. The underscore prefix is a WordPress convention for hidden fields that do not appear in the default custom field metabox. Do not ignore them. Keys like _wp_page_template tell you which page templates are assigned. Keys like _yoast_wpseo_title reveal SEO plugin usage. The inventory alone often exposes plugins that are no longer active but whose data still bloats the table.
Separating Active from Orphaned Keys
Once you have the key list, cross-reference it against the active theme’s codebase and active plugins. Use grep or an IDE to search for get_post_meta and update_post_meta calls. Keys that appear nowhere in the current code are orphans. They may be safe to delete, but never delete from wp_postmeta directly without a backup. Orphaned keys often belong to deactivated plugins that might be reactivated later. Instead, flag them and decide whether to clean them with a WP-CLI command or a targeted SQL DELETE after confirming they are not referenced by any active code.
Step 2: Decode the Value Patterns
Knowing the keys is not enough. You need to understand what each key stores. Query a sample of values for a specific key:
SELECT meta_value
FROM wp_postmeta
WHERE meta_key = 'my_custom_field'
LIMIT 10;
Look at the output. If every value starts with a:, O:, s:, i:, or b:, it is serialized PHP. If it starts with { or [, it is likely JSON. If it is a plain string or number, you have a simple field. This distinction matters because it tells you how the field was originally registered. ACF stores most of its repeater and flexible content fields as serialized arrays. A plugin like Meta Box might store data as a serialized array or as separate rows with a naming convention like group_subfield. You need to know this to rebuild the field groups correctly.
Detecting Repeater and Flexible Content Structures
Repeater fields are the most painful to reconstruct. ACF stores them as a single serialized array under one meta key, with a numeric index for each row. For example, a:2:{i:0;a:2:{s:4:"name";s:5:"Alice";s:3:"age";i:30;}i:1;a:2:{s:4:"name";s:3:"Bob";s:3:"age";i:25;}}. To understand the structure, you need to unserialize the value and inspect the array keys. Write a small PHP script that runs on a staging environment, not production. Loop through all posts that have the key, unserialize the value, and print the array structure. This reveals the sub-fields and their data types. Document these as you would a database schema.

Step 3: Map Keys to Post Types and Templates
A custom field strategy is not just a list of keys; it is the relationship between keys and the content types they belong to. You need to know which post types use which meta keys. This query joins wp_postmeta with wp_posts to show the distribution:
SELECT p.post_type, pm.meta_key, COUNT(DISTINCT p.ID) AS post_count
FROM wp_postmeta pm
JOIN wp_posts p ON pm.post_id = p.ID
WHERE pm.meta_key NOT LIKE '\_%'
GROUP BY p.post_type, pm.meta_key
ORDER BY p.post_type, post_count DESC;
The output tells you, for example, that the key author_bio appears only on posts of type review, while event_date appears on tribe_events. This is the foundation for rebuilding field groups: you now know which post types need which fields. If you are migrating away from a plugin like Advanced Custom Fields to a code-based solution, this query gives you the exact specification.
Identifying Template-Specific Fields
Some fields are only relevant when a specific page template is assigned. The template itself is stored in wp_postmeta under the key _wp_page_template. To find fields that are only used with a particular template, join on that condition:
SELECT pm.meta_key, COUNT(DISTINCT p.ID) AS post_count
FROM wp_postmeta pm
JOIN wp_posts p ON pm.post_id = p.ID
JOIN wp_postmeta tpl ON tpl.post_id = p.ID AND tpl.meta_key = '_wp_page_template'
WHERE tpl.meta_value = 'template-custom.php'
AND pm.meta_key NOT LIKE '\_%'
GROUP BY pm.meta_key;
This reveals fields that are only meaningful in a specific template context. When you rebuild the site, you can scope your field registration to that template, reducing clutter in the editor for other pages.
Step 4: Reconstructing Field Groups for Code-Based Registration
If you are moving away from a plugin-based custom field solution, you need to translate the discovered schema into PHP arrays for register_meta() or a library like Carbon Fields. The raw data from wp_postmeta gives you the field name, the post type association, and the data type. What it does not give you is the field label, description, or input type (text, wysiwyg, select). You have to infer these from the stored values and the original context. For example, if a meta value is consistently a short string without HTML, it was likely a text input. If it contains HTML tags, it was a WYSIWYG or textarea. If it is a serialized array with a limited set of values, it was probably a checkbox group or multi-select.
Rebuilding Select, Radio, and Checkbox Options
For fields that store a limited set of values, run a distinct query to recover the original options:
SELECT DISTINCT meta_value
FROM wp_postmeta
WHERE meta_key = 'my_select_field';
If the values are serialized, you will need to unserialize them in a script and collect the distinct sub-values. This is tedious but essential. Without it, you will lose data integrity when you re-register the field with a new set of allowed values.

Step 5: Cleaning the Table Without Breaking the Site
Once you have documented the active field strategy, you can safely remove orphaned rows. The most common orphans are leftover from deleted posts, plugin uninstalls that did not clean up, and old revisions. WordPress stores revision meta in wp_postmeta with post_id values that no longer exist in wp_posts. This query finds them:
SELECT pm.*
FROM wp_postmeta pm
LEFT JOIN wp_posts p ON pm.post_id = p.ID
WHERE p.ID IS NULL;
Before deleting, always export the rows to a SQL file. Then run a DELETE using the same join condition. On a site with years of accumulated cruft, this can reclaim gigabytes of space and significantly speed up postmeta queries.
Handling Serialized Data During Search-Replace
If you need to change domain names or file paths stored in serialized arrays, do not use a naive SQL REPLACE(). The string length markers in serialized data will become incorrect, and WordPress will silently return empty values when it tries to unserialize them. Use WP-CLI’s search-replace command with the --all-tables flag, or a dedicated tool like WP-CLI Search-Replace that correctly handles serialized data. If you must do it manually, unserialize, modify, and re-serialize each value in a PHP script.
Step 6: Document the Reconstructed Schema
Your final output should be a living document that any developer on the team can read. Include the meta key, the post types it applies to, the data type, the inferred field type, and any template or conditional logic. Store this in the theme repository as docs/custom-fields.md or in a shared wiki. When the next person inherits the site, they will not have to repeat your reverse-engineering work. This is not documentation for documentation’s sake; it is a direct defense against the exact failure mode that forced you to read wp_postmeta in the first place.
Common Failure Modes When Reading wp_postmeta
I have made, and seen, every one of these mistakes. They are avoidable if you know what to look for.
Assuming All Keys Without Underscores Are Custom
Some plugins store public-facing data without the underscore prefix. WooCommerce uses keys like _price but also total_sales. Always verify the key’s origin before assuming it is a custom field you created.
Ignoring the post_id Context
A meta key like location might store a city name on a post but a full address array on a user profile (if user meta is involved). Always filter by post_type when analyzing.
Deleting Rows Based on Key Name Alone
Just because a key looks like it belongs to a deactivated plugin does not mean another plugin has not adopted it. Check the active codebase before deleting anything.
FAQ
How can I tell if a meta value is serialized or JSON without writing code?
Look at the first character. If it is a, O, s, i, d, or b followed by a colon, it is PHP serialized. If it is { or [, it is JSON. If it is anything else, it is a plain string or number. You can verify by running a query like SELECT meta_value FROM wp_postmeta WHERE meta_key = 'your_key' LIMIT 1; and inspecting the output.
What is the safest way to delete orphaned postmeta rows?
First, export the rows you plan to delete using a SELECT ... INTO OUTFILE or a manual export. Then run a DELETE with a LEFT JOIN on wp_posts where the post ID is null. Always test on a staging copy of the database first. After deletion, run OPTIMIZE TABLE wp_postmeta; to reclaim disk space.
Why do some of my custom fields not appear in the post editor even though they exist in wp_postmeta?
This usually happens when the field was registered by a plugin that is now deactivated, or when the field group’s location rules no longer match the post. The data remains in the database, but WordPress does not expose it in the editor without an active field registration. Check the active plugins and theme functions to see if the field is still being registered.
How do I handle meta keys that store large amounts of data, like page builder content?
Page builders like Elementor or Beaver Builder store entire page layouts in a single meta key, often as JSON. These rows can be several megabytes each. When analyzing the table, exclude these keys from aggregate queries that might time out. Focus on the structural keys first, then handle the content-heavy keys separately. If you need to migrate away from a page builder, you will need a dedicated parsing script that understands the builder’s specific JSON schema.
Next Steps for Your Site’s Content Architecture
Once you have reconstructed the custom field strategy, the natural next step is to audit how those fields are being queried on the front end. A common performance bottleneck is the use of get_post_meta() inside loops without proper caching. The same wp_postmeta table you just read is being hit on every page load. Understanding the schema is the first half; optimizing the queries is the second. If you found this article because you are troubleshooting a site that returns nothing on certain archive pages, the field strategy might be part of the problem—or the query itself might be broken. Either way, the database does not lie. You just have to know how to ask it the right questions.