The WordPress database structure is the system WordPress uses to store almost all of the dynamic information that makes a website what it is.
Posts, pages, users, comments, settings, menu data, metadata, taxonomies, revisions and plugin configuration are not stored inside the PHP files that make up WordPress.
They normally live in a MySQL or MariaDB database.
A useful mental model is:
WordPress files
→ application code
Database
→ site data and application state
Uploads
→ physical media files
That distinction becomes important whenever you:
- back up a WordPress site;
- move it to another server;
- debug a plugin;
- investigate slow queries;
- clean database bloat;
- write custom SQL;
- develop custom post types;
- work with metadata;
- manage a large WooCommerce or membership installation.
WordPress Core provides a defined database schema and exposes its tables through the wpdb class. The official wp_get_db_schema() documentation contains the SQL WordPress uses to define its database tables.
This guide explains the structure of a standard WordPress database, what each Core table stores, how the tables relate to each other, how metadata and taxonomies work, what changes in Multisite, how plugins extend the database and why understanding the relationships matters before you optimize or modify anything.
The standard WordPress database at a glance
A standard single-site installation includes tables such as:
wp_posts
wp_postmeta
wp_users
wp_usermeta
wp_comments
wp_commentmeta
wp_terms
wp_term_taxonomy
wp_term_relationships
wp_termmeta
wp_options
wp_links
The official wpdb class reference exposes these tables through properties including:
$wpdb->posts
$wpdb->postmeta
$wpdb->users
$wpdb->usermeta
$wpdb->comments
$wpdb->commentmeta
$wpdb->terms
$wpdb->term_taxonomy
$wpdb->term_relationships
$wpdb->termmeta
$wpdb->options
$wpdb->links
The relationships can be simplified as:
Posts
├── Post Meta
├── Comments
│ └── Comment Meta
└── Term Relationships
└── Term Taxonomy
├── Terms
└── Term Meta
Users
└── User Meta
Site
└── Options
This is not a perfect entity-relationship diagram, but it is a useful map for understanding how WordPress separates primary objects from flexible associated data.
The wp_ prefix is not guaranteed
Throughout this guide, table names use:
wp_
because it is the traditional default prefix.
However, an installation might instead use:
client_
site_
prod_
abc123_
The actual value comes from:
$table_prefix
in wp-config.php.
WP-CLI can display the current prefix with:
wp db prefix
The official wp db prefix documentation describes this command.
When developing against WordPress, prefer:
$wpdb->posts
instead of:
wp_posts
so your code does not assume a particular prefix.
wp_posts is the central content table
The name wp_posts is slightly misleading because the table contains much more than blog posts.
WordPress uses a unified content model in which many different objects can exist as post records.
Typical values of:
post_type
include:
post
page
attachment
revision
nav_menu_item
and plugins or themes can register custom post types such as:
product
event
property
portfolio
course
For a deeper explanation of this architecture, see WordPress Post Types vs. Custom Post Types.
Important wp_posts columns
Common fields include:
ID
post_author
post_date
post_date_gmt
post_content
post_title
post_excerpt
post_status
comment_status
ping_status
post_password
post_name
post_modified
post_modified_gmt
post_parent
guid
menu_order
post_type
post_mime_type
comment_count
ID
The:
ID
column is the primary identifier for the object.
Other tables frequently refer back to this ID.
For example:
wp_postmeta.post_id
→ wp_posts.ID
post_type
The post_type column determines what kind of content object the row represents.
For example:
ID: 25
post_type: page
ID: 40
post_type: post
ID: 81
post_type: attachment
post_status
The status can distinguish states such as:
publish
draft
pending
private
trash
inherit
The meaning can depend on the post type.
Attachments, for example, commonly use:
post_status = inherit
post_parent
The post_parent column can model parent-child relationships.
It may be used for:
- hierarchical pages;
- attachments;
- revisions;
- other post-based relationships.
Revisions also live in wp_posts
WordPress revisions are not stored in a dedicated:
wp_revisions
table.
They exist as rows in:
wp_posts
with:
post_type = revision
and normally reference the original content through:
post_parent
For example:
Published page
ID = 100
Revision
ID = 145
post_type = revision
post_parent = 100
This is why large revision histories can substantially increase the number of rows in the posts table.
See How WordPress Post Revisions Work and What Are WordPress Post Revisions, Really? for the revision architecture itself.
wp_postmeta extends posts with arbitrary metadata
Not every possible property belongs as a dedicated column in wp_posts.
WordPress therefore has:
wp_postmeta
for flexible key-value information associated with post objects.
The official WordPress Metadata API documentation describes metadata as key-value information attached to WordPress objects.
The basic structure
A post-meta record normally contains:
meta_id
post_id
meta_key
meta_value
For example:
meta_id: 901
post_id: 100
meta_key: _featured
meta_value: 1
The relationship is:
wp_posts.ID
↑
│
wp_postmeta.post_id
One post can have many metadata rows
A single post might have:
Post 100
_featured = 1
_subtitle = Example
_external_id = 8274
_layout = full-width
_rating = 5
This makes WordPress highly extensible because plugins can add information without modifying the structure of wp_posts.
The flexibility has a cost
A website containing:
50,000 posts
may easily contain:
5,000,000 postmeta rows
if plugins or page builders attach many metadata records to each object.
This is why a database performance investigation should rarely look at wp_posts without also considering wp_postmeta.
For a deeper performance discussion, see Optimizing the WordPress Posts Table.
wp_users stores core user records
Registered WordPress accounts live primarily in:
wp_users
Common fields include:
ID
user_login
user_pass
user_nicename
user_email
user_url
user_registered
user_activation_key
user_status
display_name
User passwords are not stored as plain text
The:
user_pass
column contains password-hash data rather than the user’s original plaintext password.
Application code should use WordPress authentication APIs rather than manually comparing values against this field.
wp_usermeta extends user accounts
Additional user information is stored in:
wp_usermeta
The relationship is:
wp_users.ID
↑
│
wp_usermeta.user_id
The structure follows the same general metadata pattern:
umeta_id
user_id
meta_key
meta_value
User metadata can contain information such as:
- profile preferences;
- administration settings;
- capability data;
- plugin-specific values;
- application-specific attributes.
See WordPress User Meta, Explained for a deeper look at how this system works.
Roles and capabilities use user metadata
A useful example of WordPress’s database architecture is its permissions system.
User roles are not stored in a simple column such as:
wp_users.role
Instead, capability-related information participates in the broader options and user-meta architecture.
This demonstrates an important WordPress design principle:
Core object
+
extensible metadata
rather than continually adding new database columns for every feature.
For the permission model itself, see WordPress User Roles and Capabilities, Explained.
wp_options stores site-level configuration
The:
wp_options
table stores site-level settings rather than data belonging to an individual post, comment or user.
The official WordPress Options API documentation describes options as WordPress’s standardized system for persistent configuration storage.
Typical information includes:
- site URLs;
- site title;
- active theme information;
- plugin configuration;
- rewrite settings;
- scheduled-event state;
- serialized application settings;
- transient data in some environments.
Important columns
option_id
option_name
option_value
autoload
option_name
The option name acts as the key.
For example:
siteurl
home
blogname
option_value
The value may contain:
- plain strings;
- numbers;
- serialized arrays;
- serialized objects;
- other application data.
This is why manually changing option values with SQL can be dangerous when serialized structures are involved.
See Why Serialized Data Breaks Naive WordPress Migrations.
autoload
The autoload setting influences whether an option is loaded automatically as part of normal WordPress initialization.
Frequently required configuration may benefit from autoloading.
Large amounts of unnecessary autoloaded data can instead contribute to request overhead.
This is one reason wp_options deserves special attention during a WordPress Database Bloat audit.
Transients may also involve wp_options
WordPress provides the Transients API for temporary cached data.
Without a persistent external object cache, transients are commonly stored through:
wp_options
An expiring transient may involve entries representing:
transient value
+
expiration timestamp
However, when a persistent object cache such as Redis is active, transient storage can behave differently.
This means:
not every transient
=
database row on every site
See WordPress Transients Explained for the complete mechanism.
The taxonomy system uses several tables
WordPress taxonomy storage is slightly more complex than posts, users or comments because it separates:
term identity
taxonomy context
object relationship
across several tables:
wp_terms
wp_term_taxonomy
wp_term_relationships
wp_termmeta
wp_terms
This table stores the basic term identity.
Fields include:
term_id
name
slug
term_group
A term might be:
News
with:
slug = news
wp_term_taxonomy
This table describes how a term is used within a taxonomy.
Important fields include:
term_taxonomy_id
term_id
taxonomy
description
parent
count
Examples of taxonomy values include:
category
post_tag
nav_menu
and any registered custom taxonomy.
Why separate terms from taxonomy?
Conceptually:
term
→ what is it called?
taxonomy
→ what classification system does it belong to?
wp_term_relationships
This table connects WordPress objects to taxonomy terms.
Conceptually:
Post 100
↓
Term relationship
↓
Category: News
Its primary relationship fields include:
object_id
term_taxonomy_id
wp_termmeta
Terms can also have metadata.
The pattern is familiar:
term_id
meta_key
meta_value
This can store taxonomy-specific settings such as:
- images;
- colors;
- display options;
- plugin-defined information.
WordPress uses metadata tables consistently
The metadata model can be summarized as:
Object
↓
Object ID
↓
Metadata table
↓
key + value
Core provides metadata for:
posts
users
comments
terms
through tables such as:
wp_postmeta
wp_usermeta
wp_commentmeta
wp_termmeta
The official Metadata API documents this common architecture.
wp_comments stores comments and related records
WordPress comments live in:
wp_comments
Typical fields include:
comment_ID
comment_post_ID
comment_author
comment_author_email
comment_author_url
comment_author_IP
comment_date
comment_date_gmt
comment_content
comment_approved
comment_agent
comment_type
comment_parent
user_id
Comments connect back to posts
The primary relationship is:
wp_comments.comment_post_ID
→ wp_posts.ID
A post can therefore have many associated comments.
Comment threading uses comment_parent
A reply can point to another comment through:
comment_parent
This allows nested discussions.
wp_commentmeta extends comments
Additional comment information can live in:
wp_commentmeta
using:
meta_id
comment_id
meta_key
meta_value
The relationship is:
wp_comments.comment_ID
↑
│
wp_commentmeta.comment_id
wp_links still exists for historical compatibility
A standard WordPress schema also includes:
wp_links
This table comes from WordPress’s historical Links/Blogroll functionality.
Modern installations may barely use it, but it remains part of the Core database schema.
The existence of an old or lightly used Core table is not evidence that it should be deleted manually.
WordPress relationships are often logical rather than enforced foreign keys
This is a very important architectural characteristic.
You may see relationships such as:
wp_postmeta.post_id
→ wp_posts.ID
but WordPress generally does not rely on database-level foreign-key constraints to enforce these relationships.
The official Metadata API documentation explicitly notes this pattern for metadata object IDs.
The application itself is responsible for maintaining much of the consistency.
This is why bypassing WordPress APIs can create orphaned data
Imagine running:
DELETE FROM wp_posts
WHERE ID = 100;
without handling associated records.
You may leave behind:
wp_postmeta
post_id = 100
or other relationships associated with the deleted object.
WordPress-aware deletion functions can perform additional cleanup and trigger relevant hooks.
For direct database work, see How to Safely Run SQL Queries in WordPress.
The database is relational even without extensive foreign keys
WordPress relationships still matter conceptually.
A simplified structure looks like:
wp_posts
│
├── wp_postmeta
│
├── wp_comments
│ └── wp_commentmeta
│
└── wp_term_relationships
└── wp_term_taxonomy
├── wp_terms
└── wp_termmeta
and separately:
wp_users
└── wp_usermeta
with:
wp_options
acting primarily as site-level configuration storage.
Plugins can store data in several different ways
A plugin is not limited to one database strategy.
It may use:
wp_options
wp_postmeta
wp_usermeta
wp_termmeta
wp_commentmeta
custom post types
custom database tables
The correct approach depends on the data model.
Options are appropriate for site configuration
Examples:
API endpoint
feature enabled
plugin settings
display preferences
Post meta is appropriate for properties belonging to a post object
Examples:
property price
product external ID
article subtitle
portfolio client
User meta is appropriate for properties belonging to users
Examples:
profile preference
external CRM ID
custom account attribute
Custom tables can be appropriate for specialized high-volume data
Examples might include:
- large event logs;
- analytics;
- specialized transactional records;
- queues;
- application-specific datasets.
Trying to place millions of structured transactional records into generic metadata purely because metadata is available can create difficult query patterns.
Custom plugin tables are part of the real database too
A production WordPress database rarely contains only Core tables.
You might see:
wp_posts
wp_options
wp_users
wp_plugin_events
wp_plugin_logs
wp_plugin_jobs
wp_wc_orders
...
A database audit should therefore ask:
Which component owns each table?
rather than:
Does this table begin with wp_?
Do not delete unfamiliar custom tables blindly
A table belonging to a plugin that appears inactive may still contain:
- historical orders;
- customer data;
- forms;
- logs required for compliance;
- configuration;
- migration state.
Identify the owner and purpose before cleanup.
TheOneWP Database Manager can help inspect database tables, their structures and stored records before making maintenance decisions.
How plugins create or update custom tables
WordPress provides the:
dbDelta()
function for comparing table definitions and applying supported schema changes.
The official dbDelta() documentation covers this mechanism.
Plugin developers may use it during installation or version upgrades to manage custom table schemas.
Database structure can evolve during updates
WordPress Core and plugins can change database structure as software evolves.
Updates may:
- create new tables;
- add columns;
- modify indexes;
- migrate stored data;
- update schema versions.
This is one reason backups matter before significant software upgrades.
How WordPress itself defines the schema
Core exposes:
wp_get_db_schema()
which returns the SQL used to create relevant WordPress tables.
The official wp_get_db_schema() reference accepts scopes such as:
all
global
ms_global
blog
This becomes especially useful when understanding WordPress Multisite.
WordPress Multisite changes the database structure
A WordPress Multisite network does not simply place every site’s content into one shared wp_posts table.
Instead, many content tables are created per site.
For example, the main site may use:
wp_posts
wp_postmeta
wp_options
wp_terms
...
while another site may use:
wp_2_posts
wp_2_postmeta
wp_2_options
wp_2_terms
...
and a third:
wp_3_posts
wp_3_postmeta
wp_3_options
...
Some tables remain global
WordPress distinguishes blog-level tables from global and Multisite-global tables.
The official wpdb::tables() documentation exposes scopes including:
blog
global
ms_global
all
Users are normally network-global
The Core wpdb class identifies:
users
usermeta
as global tables.
Multisite also introduces tables such as:
wp_blogs
wp_blogmeta
wp_signups
wp_site
wp_sitemeta
wp_registration_log
depending on the current WordPress schema.
WP-CLI can reveal site-specific tables
The official wp db tables command can list the tables associated with the current site or a selected Multisite site.
For example:
wp db tables --scope=blog
$wpdb is the application interface to the database
WordPress developers should generally work through:
global $wpdb;
rather than opening a separate connection to the same database.
The class knows about:
- the database connection;
- table prefixes;
- WordPress table names;
- Multisite context;
- query results;
- database errors.
For example:
global $wpdb;
$posts_table = $wpdb->posts;
$options_table = $wpdb->options;
The database schema and the WordPress API are different layers
Knowing that posts exist in:
wp_posts
does not mean every post operation should use direct SQL.
For example:
wp_insert_post()
wp_update_post()
wp_delete_post()
can perform application-level behavior around the database operation.
Likewise:
get_option()
update_option()
get_post_meta()
update_post_meta()
provide supported interfaces to stored data.
The official WordPress Database API documentation groups APIs such as Options, Transients and Metadata around the database layer.
Understanding structure does not mean bypassing the API
A strong WordPress developer should understand both:
logical WordPress API
+
physical database structure
The API helps maintain application semantics.
The database structure helps you:
- debug problems;
- design efficient systems;
- understand migrations;
- investigate data growth;
- write carefully controlled custom queries.
Why database tables grow at different rates
Two WordPress installations with the same number of pages can have dramatically different databases.
Site A:
500 posts
2,000 postmeta rows
small comments table
few options
Site B:
500 posts
300,000 postmeta rows
50,000 revisions
200,000 comments
large plugin log tables
thousands of options
The visible content count tells only a small part of the story.
wp_postmeta can grow quickly
Page builders, ecommerce plugins and complex content systems can attach many metadata records to one object.
wp_posts can grow through revisions
Historical revisions and custom post types can dramatically increase row counts.
wp_options can grow through plugins and temporary data
Settings, transients and abandoned configuration can accumulate.
Comment tables can grow through spam
Sites with public comments may accumulate substantial comment and comment-meta data.
Custom tables may become the largest tables
Ecommerce, analytics, logging and security plugins can create tables far larger than any Core table.
See WordPress Database Bloat Explained before assuming database size itself represents a problem.
A large database is not automatically unhealthy
Compare:
2 GB legitimate WooCommerce orders
with:
2 GB abandoned plugin logs
The storage size is identical.
The value of the data is not.
Database health is therefore better understood in terms of:
- useful data;
- query performance;
- retention policy;
- table structure;
- indexes;
- application behavior.
Indexes are part of the database structure
A table does not consist only of columns and rows.
Indexes help the database find records efficiently.
For example, Core’s database schema defines indexes appropriate to common WordPress query patterns.
The exact definitions can be inspected through:
wp_get_db_schema()
or directly through database tools.
Do not add indexes merely because a table is large
An index should serve an actual query pattern.
A better workflow is:
identify slow query
↓
inspect execution plan
↓
inspect existing indexes
↓
determine whether a new index helps
↓
test on representative data
rather than:
large table
↓
add random index
Relationships matter during deletion
If you remove one WordPress object directly from the database, related data may remain elsewhere.
For example:
Delete wp_posts row
↓
wp_postmeta may remain
term relationships may remain
plugin-specific relationships may remain
Likewise, deleting users can have implications for:
- user metadata;
- content ownership;
- plugin data;
- orders or memberships.
Use application-aware deletion where possible
For Core objects, prefer functions such as:
wp_delete_post()
wp_delete_user()
wp_delete_term()
wp_delete_comment()
when they match the operation you are performing.
Direct SQL is appropriate only when you understand what application behavior you are intentionally bypassing.
Database structure matters during migration
A WordPress migration transfers more than published posts.
The database may contain:
- site URLs;
- plugin settings;
- serialized values;
- revision history;
- users;
- comments;
- custom plugin tables;
- transient state;
- scheduled tasks;
- environment-specific configuration.
See Preparing a WordPress Database for Migration before treating the database as a portable collection of independent rows.
Serialized values make naive replacements dangerous
Some WordPress values contain PHP-serialized structures.
A serialized string can include explicit string lengths.
For example, changing:
old.example.com
to a longer domain with a naive SQL:
REPLACE()
can corrupt the structure.
Serialization-aware tools such as appropriate WP-CLI search-and-replace workflows should be used instead.
See Why Serialized Data Breaks Naive WordPress Migrations.
Database backups do not include WordPress files
A database backup can preserve:
- content;
- settings;
- users;
- comments;
- database-based plugin state.
It does not normally include:
- themes;
- plugins;
- uploaded images;
- custom PHP files;
wp-config.php.
The official WordPress database backup documentation makes this distinction explicit.
A complete WordPress backup needs database and files
The recovery model is:
Database
+
WordPress files
+
uploads
+
configuration
=
recoverable site
TheOneWP Backup Manager can provide complete, database-only or files-only backup workflows depending on the recovery requirement.
How to inspect a WordPress database safely
Before modifying anything, start with observation.
Useful questions include:
- Which tables exist?
- Which tables are Core?
- Which tables belong to plugins?
- Which tables are largest?
- How many rows do they contain?
- Which tables are growing quickly?
- What indexes exist?
- Which tables contain temporary data?
- Which tables contain irreplaceable business data?
WP-CLI can list WordPress tables
Use:
wp db tables
The official wp db tables documentation supports different scopes and Multisite targeting.
WordPress code can list tables too
The wpdb class provides:
$wpdb->tables()
For example:
global $wpdb;
$tables = $wpdb->tables(
'all',
true
);
The official wpdb::tables() reference documents the available scopes.
TheOneWP Database Manager
TheOneWP Database Manager provides direct visibility into WordPress database tables, structures and records.
This is useful when investigating questions such as:
Why is this table large?
Which plugin owns it?
What columns does it contain?
What kind of data is stored here?
Inspection should come before cleanup.
TheOneWP Database Optimizer
TheOneWP Database Optimizer provides targeted cleanup for supported categories of unnecessary WordPress data.
This is different from blindly attempting to make every table smaller.
A sensible process is:
understand structure
↓
identify disposable data
↓
create backup
↓
preview cleanup
↓
clean known categories
↓
verify site
Do not optimize tables you do not understand
A database may contain unfamiliar tables because:
- a plugin created them;
- a previous plugin used them;
- the site is Multisite;
- an ecommerce system stores transactional data there;
- a custom application depends on them.
Unknown does not mean disposable.
Database privileges also affect WordPress
The WordPress database user needs sufficient permissions to perform normal application work.
WordPress documentation on hardening WordPress notes that ordinary data operations depend on privileges such as:
SELECT
INSERT
UPDATE
DELETE
while Core or plugin upgrades may also need to modify the schema.
Overly restrictive database privileges can therefore cause update failures when legitimate structural changes are required.
Do not confuse table prefix changes with database security
Changing:
wp_
to:
xyz_
does not transform an insecure WordPress installation into a secure one.
The prefix primarily namespaces tables and allows multiple logical WordPress installations or Multisite table groups to coexist within a database.
Security should rely on:
- secure authentication;
- least privilege;
- updated software;
- prepared SQL;
- access control;
- server security;
- tested backups.
A practical map of the WordPress database
WORDPRESS DATABASE
│
├── CONTENT
│ ├── wp_posts
│ └── wp_postmeta
│
├── USERS
│ ├── wp_users
│ └── wp_usermeta
│
├── COMMENTS
│ ├── wp_comments
│ └── wp_commentmeta
│
├── TAXONOMIES
│ ├── wp_terms
│ ├── wp_term_taxonomy
│ ├── wp_term_relationships
│ └── wp_termmeta
│
├── CONFIGURATION
│ └── wp_options
│
├── LEGACY CORE DATA
│ └── wp_links
│
└── PLUGINS
├── Core tables through APIs
└── Custom plugin tables
WordPress database audit checklist
- Identify the actual table prefix.
- List all Core tables.
- Identify plugin-created tables.
- Identify Multisite tables where applicable.
- Measure database size.
- Measure individual table sizes.
- Inspect row counts.
- Review
wp_postsfor post types and revisions. - Review
wp_postmetaalongsidewp_posts. - Review
wp_optionsfor plugin settings, transients and autoloaded data. - Review users and user metadata together.
- Review comments and comment metadata together.
- Understand the four-table taxonomy architecture.
- Identify custom-table owners before modification.
- Check indexes before adding new ones.
- Profile real queries before attempting performance optimization.
- Do not delete unknown tables.
- Do not delete WordPress objects with direct SQL unless you understand related data.
- Use WordPress APIs where appropriate.
- Use
$wpdbrather than hardcoded prefixes in custom code. - Use prepared statements for dynamic SQL.
- Be careful with serialized values.
- Understand Multisite global versus per-site tables.
- Create a database backup before destructive maintenance.
- Remember that a database backup does not include site files.
- Test significant database changes on staging.
- Verify the application after cleanup or migration.
Related guides
- How to Safely Run SQL Queries in WordPress
- How to Manage the WordPress Database without phpMyAdmin
- Optimizing the WordPress Posts Table
- WordPress Database Bloat Explained
- Preparing a WordPress Database for Migration
- Why Serialized Data Breaks Naive WordPress Migrations
Final recommendation
The WordPress database becomes much easier to understand once you stop thinking of it as a collection of unrelated tables and start thinking in terms of objects and relationships.
The basic model is:
Posts
→ wp_posts
→ wp_postmeta
Users
→ wp_users
→ wp_usermeta
Comments
→ wp_comments
→ wp_commentmeta
Taxonomies
→ wp_terms
→ wp_term_taxonomy
→ wp_term_relationships
→ wp_termmeta
Site configuration
→ wp_options
Plugins can extend those systems through WordPress APIs or create their own database tables when a specialized data model requires them.
Understanding this structure is useful even if you never write a manual SQL statement. It explains why a plugin can create hundreds of thousands of metadata rows, why revisions increase wp_posts, why deleting one database row can leave related information behind and why a WordPress migration involves substantially more than copying published posts.
Use the WordPress API when it already expresses the operation you need. Use $wpdb when custom database access is justified. Inspect relationships before deleting data, treat unknown plugin tables as meaningful until proven otherwise and create a recoverable backup before structural or destructive maintenance.
The goal of understanding the WordPress database is not to manipulate it more aggressively. It is to know what the data represents well enough to modify only what actually needs to change.

