WordPress Database Structure Explained (All 12 Tables Decoded)
Most people who run WordPress sites have absolutely no idea what’s happening three layers below their theme. That’s fine — until it isn’t. Understanding the wordpress database structure is the difference between a developer who can debug a broken migration in 20 minutes and one who reinstalls WordPress from scratch and prays. Every single one of the sites making up WordPress’s 42.5% share of the entire web runs on this same skeleton underneath.
The wordpress database structure isn’t complicated — but it is specific. Twelve tables, a handful of key relationships, zero foreign keys, and an architecture that’s been quietly evolving since 2003. Once you actually understand it, you’ll read plugin documentation differently, approach site migrations without sweating, and write SQL queries that don’t destroy production databases.
⚡ Key Takeaways
- The wordpress database structure consists of 12 default tables in a single-site install, expanding to 18 in multisite configurations.
- WordPress uses MySQL or MariaDB — and deliberately avoids foreign keys, enforcing all table relationships through application-layer code.
wp_postsis the most powerful table in the entire structure — it stores posts, pages, custom post types, revisions, nav menus, and media metadata all at once.- Changing the default
wp_table prefix adds a layer of defense-in-depth but is not a silver bullet — SQL injection bots can still enumerate tables viaINFORMATION_SCHEMA. - Plugins like WooCommerce can add ~38 additional tables on top of the core wordpress database structure — knowing this matters when you’re debugging, migrating, or cleaning up.
This guide breaks it all down — tables, relationships, security trade-offs, plugin extensions, and real SQL queries you’ll actually use. No fluff, no handholding. Let’s get into it.
What Is the WordPress Database and Why Should You Care?

At its core, the wordpress database structure is a relational database — specifically MySQL 5.0.15 or higher, or MariaDB as a drop-in replacement. Every post you’ve ever published, every user who’s ever logged in, every plugin setting ever saved — it all lives in that database. Your WordPress files are mostly just logic; the database is the memory.
The credentials that connect WordPress to its database live in wp-config.php — the DB_NAME, DB_USER, DB_PASSWORD, and DB_HOST constants. If you haven’t read our wp-config.php guide, that’s your next stop after this one. That file is the keystone of the entire setup.
You’ve got three main ways to interact with the wordpress database structure directly:
- phpMyAdmin — the browser-based GUI your host probably already gives you. Great for exploration, dangerous for production edits.
- WP-CLI — command-line interface that lets you run
wp db query, export, import, and search-replace without touching a browser. - The
$wpdbclass — WordPress’s built-in PHP class for programmatic database access. This is the right way to query the DB from within WordPress code.
For the official schema reference, bookmark the WordPress Developer Resources. That’s ground truth.
The 12 Default Tables — Every WordPress Database Structure Decoded

A fresh WordPress install drops exactly 12 tables into your database. Every single WordPress site on the planet starts with this same foundation — that’s the beauty and the constraint of a standardized wordpress database structure. Here’s the full roster:
| Table | Purpose |
|---|---|
wp_posts |
All content — posts, pages, CPTs, revisions, nav menus, attachments |
wp_postmeta |
Key-value metadata for any post (EAV pattern) |
wp_users |
User accounts — login, email, hashed passwords |
wp_usermeta |
User metadata — roles, preferences, capabilities |
wp_options |
Site settings, plugin configs, autoloaded data |
wp_comments |
All comments — approved, spam, trash |
wp_commentmeta |
Metadata attached to individual comments |
wp_terms |
Tags, categories, and any taxonomy terms |
wp_term_taxonomy |
Associates terms with their taxonomy type |
wp_term_relationships |
Maps posts to their taxonomy terms |
wp_termmeta |
Metadata for taxonomy terms (added in WP 4.4) |
wp_links |
Deprecated since WP 3.5 — legacy blogroll feature |
12
default tables in every WordPress installation
Source: WordPress Codex
The real workhorse of the wordpress database structure is wp_posts. It’s not just blog posts — it stores pages, custom post types, revisions (yes, every auto-save), navigation menu items, and media attachment metadata. The post_type column is what differentiates them all. One table to rule them all.
wp_postmeta runs on the Entity-Attribute-Value (EAV) pattern — each row is just a post ID, a meta key, and a meta value. It’s wildly flexible and notoriously slow at scale. If you’ve ever seen a query pulling 400 rows of postmeta to render a single page, you’ve felt the EAV tax firsthand.
wp_options is where site settings live — but the dangerous part is the autoload column. Every option with autoload = yes gets loaded into memory on every single page request, regardless of whether it’s needed. Bloated autoloaded data is one of the most common causes of slow WordPress sites. As for wp_links — it exists, it’s still created, and it hasn’t been meaningfully used since WordPress 3.5 killed the blogroll feature. A ghost table that just won’t leave.
For the complete schema with field-level details, the WordPress Codex Database Description is still the most comprehensive reference available.
🏴☠️ PIRATE TIP: Run SELECT option_name, length(option_value) FROM wp_options WHERE autoload = 'yes' ORDER BY length(option_value) DESC LIMIT 20; on any WordPress site and watch the horror show unfold. That’s your autoload bloat, right there.
How WordPress Tables Talk to Each Other

Here’s one of the most important things to understand about the wordpress database structure: there are no foreign keys. None. Zero. If you come from a traditional relational database background, this will feel like a crime against nature. But it’s a deliberate architectural choice with real consequences.
The key relationships in the wordpress database structure are all enforced by application code alone:
wp_posts.ID→wp_postmeta.post_id(post to its metadata)wp_posts.post_author→wp_users.ID(post to its author)wp_comments.comment_post_ID→wp_posts.ID(comment to its post)wp_terms↔wp_term_taxonomy↔wp_term_relationships→wp_posts(the full taxonomy chain)
“Code is poetry.”
— Matt Mullenweg, Co-founder, WordPress
The reason there are no foreign keys goes back to WordPress’s MyISAM legacy. Early WordPress used MyISAM storage engine, which didn’t support foreign key constraints at all. When InnoDB became the default, the architectural decision to enforce relationships through code was already baked into the DNA of the project. That ship had sailed.
The practical consequence? Orphaned records are absolutely possible. Delete a user directly from the database without using WordPress’s API and you’ll leave behind ghost rows in wp_posts and wp_usermeta. The same risk applies if you manually delete posts and skip the cleanup hooks. This is why WordPress’s own deletion functions — and its system of WordPress hooks and filters — exist to handle cascading cleanup in application code rather than at the database level.
💡 We build WordPress tools for people who want to understand what’s under the hood. Check the Arsenal.
The wp_ Prefix — Security Theater or Defense in Depth?

By default, every table in the wordpress database structure starts with wp_. That prefix is set in wp-config.php via the $table_prefix variable, and you can change it to anything — xk92_, myblog_, whatever you want. The question is whether it actually matters for security.
The argument for changing it: automated SQL injection scripts specifically target wp_users and wp_options using the default prefix. If your tables are named xk92_users, those dumb bots walk straight past you. It’s a speed bump against the least sophisticated attacks.
The counter-argument — and Wordfence’s analysis on table prefix changes makes this point clearly — is that any attacker who has actually achieved SQL injection access can query INFORMATION_SCHEMA.TABLES to enumerate every table name in your database instantly. The prefix is invisible to them. So calling it a security silver bullet is wishful thinking.
🏴☠️ PIRATE TIP: Change your table prefix during a fresh install — it costs you nothing and stops script-kiddie bots cold. But don’t kid yourself that it’s real armor. Pair it with everything in our WordPress security hardening guide or you’re just wearing a costume.
The real verdict on the wordpress database structure prefix: change it during install as one layer of defense-in-depth, not as your primary defense. Changing it after install on a live site is painful — you have to update every table name AND update the wp_options and wp_usermeta rows that store the old prefix. If you forget even one, your site breaks. Not worth the risk on a live site unless you really know what you’re doing.
How Plugins Extend the WordPress Database Structure

The wordpress database structure doesn’t stay at 12 tables for long on most real-world sites. Plugins can — and do — add their own tables on top of the core schema. The right way to do this is with WordPress’s dbDelta() function, which compares your desired schema against what already exists and creates or alters tables accordingly without destroying existing data.
When writing a plugin that creates tables, you always use $wpdb->prefix to prepend the correct table prefix. Hard-coding wp_ directly is a rookie mistake that breaks every site using a custom prefix — and a good reason why code review matters. The wordpress database structure is only as clean as the plugins running on top of it.
The extreme example: WooCommerce adds approximately 38 custom tables to the wordpress database structure — tables for orders, order items, order meta, product lookups, sessions, and more. That’s more than triple the default table count from a single plugin. It’s technically necessary for the performance WooCommerce needs at scale, but it also means a WooCommerce migration isn’t just a standard WordPress migration anymore.
Best practice for plugin developers: create your tables on plugin activation using register_activation_hook(), and clean them up on uninstall via register_uninstall_hook(). Most plugins skip the cleanup step, which is why deactivated plugins leave ghost tables rotting in databases for years. And yes — plugin updates can modify those tables, which is exactly why you need a pre-update backup strategy that includes your database.
Common SQL Queries Every WordPress Developer Should Know

Knowing the wordpress database structure is only useful if you can actually query it. Here are the SQL queries that come up again and again in real development and migration work — all targeting the default table structure.
1. Get the site URL from wp_options:
SELECT option_value FROM wp_options WHERE option_name = 'siteurl';
2. Find all published posts:
SELECT ID, post_title, post_date
FROM wp_posts
WHERE post_status = 'publish'
AND post_type = 'post'
ORDER BY post_date DESC;
3. Change site URL after migration (the big one):
UPDATE wp_options SET option_value = 'https://yournewdomain.com' WHERE option_name IN ('siteurl', 'home');
4. Search and replace in post content (use with extreme caution — back up first):
UPDATE wp_posts
SET post_content = REPLACE(post_content, 'http://olddomain.com', 'https://newdomain.com');
5. Find all users with administrator role:
SELECT u.ID, u.user_login, u.user_email
FROM wp_users u
INNER JOIN wp_usermeta um ON u.ID = um.user_id
WHERE um.meta_key = 'wp_capabilities'
AND um.meta_value LIKE '%administrator%';
Understanding WordPress user roles helps you understand why capabilities live in wp_usermeta as serialized data — the role system is entirely metadata-driven in the wordpress database structure.
6. Count published posts per category:
SELECT t.name, COUNT(tr.object_id) as post_count
FROM wp_terms t
INNER JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
INNER JOIN wp_posts p ON tr.object_id = p.ID
WHERE tt.taxonomy = 'category'
AND p.post_status = 'publish'
GROUP BY t.name
ORDER BY post_count DESC;
A word on safety: when you’re writing PHP code that queries the wordpress database structure, always use $wpdb->prepare() to sanitize inputs. Direct interpolation of user-supplied data into SQL queries is how you get owned. The prepare() method uses parameterized queries — it’s not optional, it’s table stakes.
How many tables does WordPress create by default?
A standard single-site installation creates exactly 12 tables as part of the core wordpress database structure. On a WordPress Multisite network, that number jumps to 18 — the standard 12 tables plus 6 additional network-level tables that handle sites, blogs, and network-wide users. Every one of those tables is prefixed with whatever you’ve set as your $table_prefix in wp-config.php.
What database does WordPress use?
The wordpress database structure runs on MySQL (version 5.0.15 or higher) or MariaDB as a compatible alternative. Both use the same SQL dialect for the purposes of core WordPress, though there are minor differences at the engine level. Most modern hosts default to MariaDB. WordPress uses InnoDB as its default storage engine for new tables, though older installations may still have tables on MyISAM — which is part of why the wordpress database structure has no foreign keys.
How do I access my WordPress database?
You have three main options for accessing the wordpress database structure directly. First, phpMyAdmin — a browser-based GUI available through most hosting control panels like cPanel or Plesk. Second, WP-CLI via the command line using wp db query or wp db cli for interactive access. Third, programmatically through WordPress’s $wpdb global object within your PHP code — the safest approach for plugin and theme development since it respects your configured table prefix and offers built-in query preparation.
Should I change the default wp_ table prefix?
Yes — but only during a fresh install, and only as one part of a larger security strategy. Changing the default prefix stops automated bots that target the standard wordpress database structure table names, but it doesn’t protect against a determined attacker who has already achieved SQL injection access, since they can enumerate tables through INFORMATION_SCHEMA regardless of prefix. Changing the prefix on a live site is risky and requires careful updates to all table names, plus specific rows in wp_options and wp_usermeta that reference the old prefix. The juice is barely worth the squeeze post-install.
What is the wp_postmeta table used for?
The wp_postmeta table is the flexible metadata store in the wordpress database structure — it holds any arbitrary key-value data associated with a post, page, or custom post type. It uses the Entity-Attribute-Value (EAV) pattern: each row links a post ID to a single meta key and its value. Custom fields, featured image IDs, page template assignments, SEO metadata from plugins, and WooCommerce product data all live here. The trade-off is flexibility versus performance — complex wp_postmeta queries with multiple meta_key joins can get expensive fast on large sites.
⚔️ Pirate Verdict
The wordpress database structure is 12 tables, zero foreign keys, and a philosophy that trusts application code over database constraints. That’s either brilliant or insane depending on your background — but it works for 42.5% of the entire internet, so maybe stop arguing about it and start understanding it. Learn these tables, learn the relationships, write your queries with $wpdb->prepare(), and never let a broken migration catch you off guard again. That’s the pirate way.
The wordpress database structure is the foundation every WordPress site is built on — and now you know exactly what’s down there. Whether you’re debugging a broken site, migrating to a new host, or building a plugin that needs its own tables, this knowledge pays dividends every single time. What’s the gnarliest database query you’ve ever had to write? Drop it in the comments.