BuddyX

13 min read · 2,606 words

Why Does WordPress Use MySQL?

why does WordPress use MySQL?

Same story as PHP, told from a different angle: WordPress uses MySQL because b2/cafelog, the blogging tool it forked from in 2003, already used MySQL, and two decades of accumulated schema design, query patterns, and plugin ecosystem built directly against that specific database engine have made it essentially permanent infrastructure at this point. But unlike the PHP question, where the “why” is mostly historical accident that turned out fine, the MySQL question has a more interesting technical answer once you actually look at what WordPress’s database schema is doing, because it reveals both why MySQL fits reasonably well and where the fit gets genuinely awkward.

The schema itself, and why it matters more than the engine choice

WordPress’s core database schema is built around a small number of tables: wp_posts holds everything that behaves like content, posts, pages, attachments, revisions, nav menu items, and any custom post type a plugin registers, all in the same table distinguished by a post_type column. wp_postmeta holds arbitrary key-value metadata attached to any post, structured as one row per meta key per post rather than dedicated columns. wp_options is a single flat table holding site-wide settings, again as key-value pairs. This design, sometimes called an Entity-Attribute-Value or EAV-adjacent pattern for the meta tables, is a deliberate trade-off: it lets any plugin attach arbitrary structured data to any post without ever needing to run an ALTER TABLE on a live production database, which would be a genuinely risky operation to ask tens of thousands of independently developed plugins to coordinate around. The cost of that flexibility is that wp_postmeta can become enormous on large sites, and querying against meta values (rather than dedicated columns) is inherently less efficient than querying a properly normalized column, because the database has to treat every value as an unstructured string rather than a typed, indexed field.

Why a relational database made sense for this shape of data in the first place

Despite the EAV pattern’s awkwardness, the core relationships in a CMS are genuinely relational: a post belongs to an author, has zero or more categories and tags through a shared taxonomy system, has zero or more comments, and comments themselves can have parent-child threading relationships. MySQL’s foreign-key-adjacent relational model (WordPress core doesn’t actually declare formal foreign key constraints, notably, relying on application logic instead) handles this kind of interconnected, structured data more naturally than a pure document store would, where you’d either duplicate related data across documents or build your own joining logic in application code. The wp_term_relationships table, linking posts to taxonomy terms through a clean many-to-many junction table, is a good example of relational modeling doing exactly what it’s good at: representing a relationship as its own first-class row rather than nesting arrays inside a document.

ACID transactions and why “it just works” matters more than it sounds

MySQL, when running the InnoDB storage engine (the default since MySQL 5.5, and what virtually every modern WordPress host uses), provides ACID guarantees, atomicity, consistency, isolation, durability, meaning a multi-step database operation either completes entirely or rolls back entirely, with no partial, corrupted intermediate state left behind if something fails mid-write. For a CMS handling concurrent writes from multiple editors, scheduled publishing, WooCommerce order processing, or plugin operations that touch several tables in sequence, that guarantee matters more than it might seem from the outside; without it, a server crash or a network hiccup mid-write could leave a post half-published, an order half-recorded, or metadata out of sync with the content it describes. Many popular NoSQL document databases historically traded away some of these guarantees for horizontal scalability, which is a reasonable trade for certain workloads but a worse fit for a content-editing system where data integrity during concurrent writes is a baseline requirement, not a nice-to-have.

MariaDB: the fork that quietly became the default

When Oracle acquired Sun Microsystems (and with it, MySQL) in 2010, a meaningful part of the open-source community grew concerned about Oracle’s long-term commitment to keeping MySQL genuinely open and actively developed, given Oracle’s history with other acquired projects. MariaDB emerged as a community-driven fork led by some of MySQL’s original developers, designed to remain a drop-in replacement, same wire protocol, same core SQL dialect, same client libraries, while developing independently under a foundation structure less exposed to any single company’s commercial priorities. Most major WordPress hosts today default new accounts to MariaDB rather than Oracle’s MySQL specifically because of this history, and from WordPress’s perspective the two are functionally interchangeable for the vast majority of sites; WordPress core doesn’t care which one it’s talking to, because both implement the same SQL surface WordPress’s database layer, wpdb, was built against.

The SQLite experiment, and what it reveals about the constraint that actually matters

WordPress 6.4 introduced official, though still developing, support for running on SQLite instead of MySQL or MariaDB, primarily through the sqlite-database-integration plugin and as the default storage layer for WordPress Playground, the in-browser WordPress environment used for quick demos and testing. This is worth paying attention to because it demonstrates something important: WordPress’s database layer was never fundamentally tied to MySQL’s specific SQL dialect at the deepest level, it was tied to having a reliable, transactional, relational SQL database available. SQLite, a serverless, single-file database engine, fits certain use cases MySQL doesn’t, a lightweight demo environment, a low-traffic personal site on hosting that doesn’t want to run a separate database server process, an embedded or offline scenario, but it isn’t a wholesale replacement for MySQL on any site with meaningful concurrent traffic, because SQLite’s file-level locking model handles simultaneous writes far less gracefully than MySQL’s row-level locking under InnoDB. The experiment is best understood as WordPress broadening where it can run at the low end, not as evidence that MySQL was ever the wrong choice for its actual target range of sites.

No foreign keys: a deliberate, debated omission

Despite modeling clearly relational data, WordPress core’s schema doesn’t declare formal foreign key constraints between tables, wp_posts.post_author references a row in wp_users, but the database itself doesn’t enforce that relationship the way a strict relational design typically would. This is a long-running point of criticism from developers coming from more rigorously modeled systems, and the practical reason is backward compatibility and plugin flexibility again: enforced foreign keys would reject any insert or update that violates referential integrity, and across an ecosystem of plugins with wildly varying code quality, historically, the risk of a legitimate but slightly unusual write getting rejected outright was judged worse than the risk of an occasional orphaned row that application-level logic simply needs to tolerate gracefully. WordPress’s own core code and cleanup routines are generally written to handle these orphaned rows without much drama, but it’s a real, intentional trade-off between raw data integrity guarantees and the flexibility of thirteen thousand plugins written by people who never coordinated a shared schema design with each other.

A concrete example: diagnosing a slow query with EXPLAIN

When a specific page is slow and the database is the suspect, MySQL’s EXPLAIN command is the actual diagnostic tool, not guesswork. Prefixing any query with EXPLAIN shows the query planner’s execution strategy without actually running it, whether it’s using an index or scanning the full table, roughly how many rows it expects to examine, and which join strategy it picked. A common real-world finding: a plugin’s meta_query filtering posts by a custom field with no index on meta_key forces MySQL into a full table scan across every row in wp_postmeta, which might be a fast, invisible cost on a young site with a few thousand rows and a genuinely painful multi-second query once that table grows into the millions of rows a mature, actively used site accumulates. Query Monitor, a free debugging plugin, surfaces exactly this kind of slow, unindexed query directly in the WordPress admin toolbar without needing to touch phpMyAdmin or run EXPLAIN by hand, which makes it one of the higher-value diagnostic tools available for free in the ecosystem.

Scaling MySQL as a WordPress site grows, in practical terms

A small site rarely stresses MySQL at all; the database becomes a genuine bottleneck once you’re dealing with meaningful traffic, large post counts, or heavy plugin activity generating lots of postmeta and options rows. The standard toolkit for scaling here, in roughly the order most sites hit them: proper indexing on every column actually used in WHERE, JOIN, and ORDER BY clauses (a shockingly large share of “WordPress is slow” complaints trace back to a plugin running unindexed meta queries against a large wp_postmeta table); an object cache (Redis or Memcached, via a persistent object cache drop-in) so repeated reads of the same data don’t hit the database at all; query caching or full-page caching at a layer above the database entirely, so most requests never generate a database query in the first place; and, for genuinely large, high-traffic installs, read replicas that offload the read-heavy queries (the vast majority of WordPress traffic) to secondary database servers while a single primary handles writes. WooCommerce stores and BuddyPress-style community sites with heavy write activity from orders, comments, and activity feeds tend to hit these limits earlier than a typical content blog, precisely because they generate proportionally more write traffic that can’t be cached away the same way read-heavy page views can.

Full-text search: the honest limitation

MySQL’s built-in MATCH() AGAINST() full-text search, which powers WordPress’s default search box, is genuinely weak compared to purpose-built search engines, it doesn’t handle relevance ranking, typo tolerance, or stemming nearly as well as something like Elasticsearch, Algolia, or even the more capable SearchWP and Relevanssi plugins that layer smarter indexing on top of MySQL or bypass it with their own external index. This is one of the clearer places where “why does WordPress use MySQL” runs into a real, not just historical, answer: MySQL is a general-purpose relational database, not a search engine, and expecting it to perform like one is asking it to do a job it was never particularly designed for. Sites with search as a genuinely important feature, rather than an afterthought, are well served by adding a dedicated search layer rather than trying to force MySQL’s full-text capability further than it naturally goes. This is a case where the underlying database engine simply wasn’t designed for the job being asked of it, and no amount of indexing or configuration fully closes that gap; the honest fix is a purpose-built tool sitting alongside MySQL, not a workaround forced through it.

The autoload trap: a real, common failure mode specific to this schema

Every row in wp_options has an autoload column, and when it’s set to “yes,” WordPress loads that option’s entire value into memory on every single page load, regardless of whether the current request actually needs it, because pulling all autoloaded options in one query up front is faster than querying them individually as needed. This is a genuinely clever optimization for a database with a reasonable number of small options. It becomes a serious liability when a poorly written plugin stores a large serialized array (cached API responses, a big settings blob, accumulated log data) with autoload left on, because now every page load on the entire site pays the cost of loading and unserializing that oversized value even on pages that never touch that plugin’s functionality at all. Sites that have run for years and accumulated dozens of plugins over time frequently develop an autoloaded options table bloated into tens of megabytes, and it’s one of the most common, most fixable causes of a site that “just feels sluggish everywhere” without any single obviously slow page. Running wp option list --autoload=yes via WP-CLI, sorted by size, is a fast way to spot the culprit.

Why not PostgreSQL, given how much developers outside WordPress prefer it

Postgres has a strong reputation among developers for more sophisticated data types, more standards-compliant SQL, and arguably more robust handling of complex queries and JSON data than MySQL historically offered. WordPress doesn’t use it for the same reason it doesn’t use anything else that wasn’t available or chosen in 2003: b2/cafelog was built on MySQL, and switching database engines at this point would require rewriting wpdb, WordPress’s low-level database abstraction layer, along with auditing every plugin that runs raw SQL rather than going through that abstraction, which a meaningful number do. It’s not that Postgres would be a worse technical fit in the abstract; it’s that the migration cost for an ecosystem this size, with this much unaudited third-party code touching the database directly, is high enough that the case for switching would need to be overwhelming, and “some developers have a stylistic preference for Postgres” doesn’t clear that bar against two decades of accumulated MySQL-specific tooling, hosting defaults, and plugin assumptions.

The utf8mb4 migration and why it mattered more than it sounds

For years, WordPress’s default database character set was utf8, which despite the name only supports a subset of Unicode using up to 3 bytes per character, enough for most languages but not enough for emoji or certain rarer characters, which need 4 bytes. WordPress 4.2, released in 2015, migrated the default character set to utf8mb4, MySQL’s genuinely full Unicode implementation, specifically so a comment or post containing an emoji wouldn’t get silently truncated or throw a database error. This sounds like a minor cosmetic fix, but it’s a good illustration of how deeply WordPress’s database schema decisions ripple outward into real user-facing behavior; sites that migrated hosts or upgraded from a very old install sometimes still carry legacy utf8 tables today, and converting them (via wp db commands or a plugin) is worth checking if you’ve ever seen emoji or certain international characters mysteriously vanish or error out on save.

What database choice actually protects you from, and what it doesn’t

None of MySQL’s relational integrity, ACID guarantees, or two decades of WordPress-specific optimization protect a site from bad query patterns written by a careless plugin, an unindexed meta_query running against 500,000 rows, or a theme running the same expensive query on every single page load with no caching layer in front of it. The database engine sets the ceiling on what’s possible; it doesn’t guarantee anything about how a specific site’s code actually uses it. This is the same lesson that applies to most “why does WordPress use X” questions once you dig past the historical answer: the underlying technology choice from 2003 turned out to be a reasonable, durable fit for a relational content-management workload, but the performance and reliability any individual site actually experiences depends far more on how well its specific plugins, queries, and caching are built on top of that foundation than on the database engine itself.

The practical takeaway for anyone running a WordPress site rather than developing WordPress core itself is that the database engine choice made in 2003 is essentially a solved question at this point, not something worth reconsidering. What’s worth actively managing is everything sitting on top of it: keeping the autoloaded options table lean, adding indexes for any custom query pattern a plugin introduces, layering an object cache in front of repeated reads, and reaching for a dedicated search solution once search becomes a genuinely important feature rather than a decorative search box. Those are the levers that actually determine whether a given WordPress site’s database performs well at scale, and every one of them is something a site owner or developer can act on directly, regardless of which storage engine sits underneath.


Best Managed WordPress Hosting Providers

SearchWP vs Relevanssi: Which WordPress Search Plugin Is Right for You?

MySQL Documentation: InnoDB Storage Engine

Reading
13 min · 2,606 words
Published
Aug 9, 2024
Shashank Dubey
BuddyX contributor

Writing about WordPress communities, BuddyPress, BuddyBoss, LMS plugins, and the business of paid communities.

Keep reading

More from the BuddyX blog

Browse all posts on community, WordPress, BuddyPress and the studio of plugins behind BuddyX.