Benchmark MemCP vs. MariaDB on Wordpress: Difference between revisions
(Created page with "<!-- Copyright (C) 2026 MemCP contributors --> <!-- SPDX-License-Identifier: AGPL-3.0-or-later --> = Benchmark MemCP vs. MariaDB on Wordpress = WordPress does more than fetch individual posts. Category pages, comment counters, search, archive lists, and plugin metadata queries repeatedly join and aggregate data. These are the places where a different database engine can make a noticeable difference. We compared '''MariaDB, MemCP, and MemCP with JIT''' using seven SQL...") |
No edit summary |
||
| Line 4: | Line 4: | ||
= Benchmark MemCP vs. MariaDB on Wordpress = | = Benchmark MemCP vs. MariaDB on Wordpress = | ||
'''MemCP makes repeated WordPress database work faster.''' On current master, MemCP-JIT completes this fixture's search plus total-count query '''36.0× faster''' than MariaDB, its comment count '''21.1× faster''', and its archive query '''6.2× faster'''. All six query shapes with matching results have lower warm latency with JIT in this run. The seventh, a taxonomy query, is retained in the matrix with a clearly marked collation mismatch. | |||
MemCP speaks the MySQL protocol and can also '''host PHP directly in the database process'''. WordPress is our example application: the hosting interface is general purpose and is also intended for MediaWiki and other PHP software. Try MemCP with your PHP hosting and measure the queries your application actually runs. | |||
== MariaDB vs. MemCP vs. MemCP-JIT == | |||
Measured on master [https://github.com/launix-de/memcp/commit/43d0c81401616fafeeb456b22f4a47d6e25361d6 43d0c8140], including the SQL literal-parameterization improvement in PR #873. All three columns use the same seven WordPress query shapes and data snapshot. These are '''warm database round trips''', not HTTP page-load times. Lower is better. | |||
Each value is the median of 11 batch means, each containing 100 complete query executions, after 30 warmups. For SQL_CALC_FOUND_ROWS statements, the measured operation also includes the subsequent SELECT FOUND_ROWS() on the same connection. | |||
{| class="wikitable sortable" | {| class="wikitable sortable" | ||
! | ! Query !! MariaDB (ms) !! MemCP (ms) !! MemCP-JIT (ms) !! MariaDB / JIT !! Result check | ||
! MariaDB | |||
! MemCP | |||
! MemCP | |||
! | |||
! Result check | |||
|- | |- | ||
| Option lookup | | Option lookup: siteurl || 0.079 || 0.069 || '''0.056''' || 1.39× || Equal; 1 row | ||
| 0. | |||
| 0. | |||
| 0. | |||
| | |||
|- | |- | ||
| | | Elementor metadata join + LIMIT || 0.152 || 0.108 || '''0.079''' || 1.93× || Equal; 0 rows | ||
| 0. | |||
| 0. | |||
| 0. | |||
| | |||
| | |||
|- | |- | ||
| Taxonomy join | | Taxonomy DISTINCT/join, ordered by name || 0.224 || 0.245 || '''0.142''' || Diagnostic only || See correctness notes below | ||
| 0. | |||
| 0. | |||
| 0. | |||
| | |||
| | |||
|- | |- | ||
| Comment | | Comment count with join || 3.240 || 0.222 || '''0.153''' || 21.14× || Equal; 1 row | ||
| | |||
| 0. | |||
| '''0. | |||
| | |||
|- | |- | ||
| WordPress search | | WordPress search + FOUND_ROWS || 30.405 || 1.278 || '''0.844''' || 36.01× || Equal; 10 rows | ||
| | |||
| | |||
| | |||
| | |||
| | |||
|- | |- | ||
| | | Yoast CAST metadata join + FOUND_ROWS || 0.197 || 0.248 || '''0.144''' || 1.37× || Equal; 0 rows | ||
| 0. | |||
| 0. | |||
| 0. | |||
| | |||
| | |||
|- | |- | ||
| DISTINCT | | Monthly archive DISTINCT || 0.718 || 0.168 || '''0.116''' || 6.21× || Equal; 10 rows | ||
| 0. | |||
| 0. | |||
| '''0. | |||
| | |||
|} | |} | ||
The empty | The two metadata queries return zero rows on this snapshot. Their times describe proving an empty result, not retrieving a populated plugin workload. Tiny differences in the smallest queries are sensitive to scheduling on this shared development machine. We do not average these seven unrelated query shapes into a single WordPress speedup. | ||
== Host PHP directly: remove a round trip for every small query == | |||
The optional FrankenPHP dependency supplies the PHP runtime; only MemCP's own bridge and PDO addon live in this repository. PHP requests use ZTS threads, with OPcache enabled. PHP folders are attached through the existing HTTP handler, alongside the SQL API and dashboard. | |||
With a PHP-enabled build and an external ZTS PHP development installation: | |||
<syntaxhighlight lang="bash"> | |||
make php PHP_CONFIG=/path/to/zts/php-config | |||
./memcp-php --no-repl -data /path/to/data --api-port=8098 --mysql-port=3307 --serve=/path/to/php-app lib/main.scm | |||
</syntaxhighlight> | |||
The application is mounted at /; /dashboard remains available. Scheme code can mount additional applications through servePHP. No WordPress source is bundled into MemCP. | |||
The PDO addon supports authenticated memcp: connections and retains ordinary MySQL wire connections. A mysql: connection to localhost/127.0.0.1 and this MemCP server's own MySQL port can be routed internally; other destinations continue using their original driver. Both paths require database credentials. WordPress's use of mysqli is a separate connector question: PDO routing alone is not a claim that every WordPress installation or plugin uses PDO. | |||
A separate microbenchmark measures SELECT 1 inside PHP, using an already established connection. Each round performs 100 warmups and 2,000 measured executions; seven rounds alternate the two transports. The timer includes query(), fetchColumn(), and closeCursor(), but excludes HTTP, connection establishment, and PHP startup. Pdo\Mysql::connect forces the real wire driver for the control, so automatic PDO routing cannot accidentally turn both measurements into RAM calls. | |||
{| class="wikitable" | |||
! PHP SELECT 1 transport !! Median of round medians | |||
|- | |||
| MySQL wire over local TCP || 37.19 µs | |||
|- | |||
| MemCP in-process PDO || '''11.57 µs''' | |||
|} | |||
This removes '''69% of the measured latency''', or 25.61 µs per tiny query. At 100 such queries per request, the arithmetic saving is approximately 2.56 ms; at 1,000 queries it is 25.61 ms. Those are extrapolations, not measured WordPress page-load improvements. | |||
For a workload-specific budgeting example, halving 100–200 µs of overhead would save '''50–100 µs per call (50%)'''. Across 100 database calls, that would add up to 5–10 ms per request. Those figures illustrate how small savings accumulate; they were not measured here. The measured transport comparison above is the evidence for this setup; measure your own query mix before assigning a per-request saving. | |||
== Reproducible setup == | |||
{| class="wikitable" | {| class="wikitable" | ||
! | ! Item !! Configuration | ||
! | |||
! | |||
|- | |- | ||
| | | Measurement date || 9 September 2026 (Europe/Berlin) | ||
| | |||
| | |||
|- | |- | ||
| | | CPU || AMD Ryzen 9 7900X3D, 12 physical cores / 24 logical CPUs | ||
| | |||
| | |||
|- | |- | ||
| | | Memory / OS || Approximately 62 GiB RAM; Ubuntu 24.04.4 LTS, x86-64 | ||
| | |||
| | |||
|- | |- | ||
| | | MemCP || Master 43d0c81401616fafeeb456b22f4a47d6e25361d6; unchanged source for both SQL builds | ||
| | |- | ||
| 14, | | SQL compiler/toolchain || Patched Go 1.27.0, revision 84fe25ee45468f5e3920c11c24e2bbb926fc3a72; GOEXPERIMENT empty vs. GOEXPERIMENT=jit | ||
| 0. | |- | ||
| MariaDB || 10.11.14; query cache OFF; InnoDB buffer pool 128 MiB | |||
|- | |||
| PHP microbenchmark || Same MemCP master, built with Go 1.26.0; PHP 8.5.10 ZTS, Zend Engine 4.5.10, OPcache enabled; FrankenPHP v1.12.7; no Go JIT | |||
|- | |||
| SQL client || Python 3.12 / PyMySQL, established TCP connections to 127.0.0.1; complete result fetching; no connection setup timed | |||
|- | |||
| Database fixture || Read-only consistent snapshot of wordpress_bench_v2, copied to a separate MemCP database; MariaDB InnoDB vs. MemCP sloppy engine; rebuild before measurements | |||
|- | |||
| Measurement order || MariaDB, MemCP, MemCP-JIT; one client, no concurrent query load | |||
|- | |||
| Runtime settings || Query-plan cache, autoindexing and normal optimizer enabled; TracePrint and ScanDebugging disabled while timing | |||
|} | |} | ||
This is a warm read comparison, not a matched-memory or durability benchmark. The host is shared with development services; cooperating builds/tests were paused for timing, but CPUs and memory were not reserved or pinned. Autoindexing must have time to activate: the warmup count deliberately exceeds its initial observation threshold. Compiled plans, indexes and maintained group/prejoin structures may all contribute to warm results. Their cold build cost and write-maintenance cost are outside the latency table. | |||
Cold startup and initial automatic structure building are not represented by this table. Both MemCP modes reuse the same freshly imported, rebuilt directory sequentially; persisted structures and OS caches remain available. An earlier preparation pass populated some structures before the final measured run. This is deliberately a warm comparison, not a cold-start A/B test. | |||
== | === Fixture === | ||
The queries | The seven captured queries exercise option lookups, plugin metadata, taxonomy joins, comment counts, search and archives. The data is synthetic, with real WordPress query shapes. It is not a million-post or concurrent-production benchmark. The source WordPress installation identifies itself as 7.1; WordPress/PHP rendering does not execute in the SQL matrix, and installed plugin versions are not performance inputs once these exact queries are captured. | ||
The | |||
{| class="wikitable" | {| class="wikitable" | ||
! Table | ! Table !! Rows | ||
! Rows | |||
|- | |- | ||
| | | wp_posts || 22,063 | ||
| 22,063 | |||
|- | |- | ||
| | | wp_postmeta || 14,013 | ||
| 14,013 | |||
|- | |- | ||
| | | wp_comments || 6,001 | ||
| 6,001 | |||
|- | |- | ||
| | | wp_options || 503 | ||
| 503 | |||
|- | |- | ||
| | | wp_terms || 175 | ||
| 175 | |||
|- | |- | ||
| | | wp_term_taxonomy || 175 | ||
| 175 | |||
|- | |- | ||
| | | wp_term_relationships || 7,701 | ||
| 7,701 | |||
|} | |} | ||
The corpus contains 2,000 varied generated articles across 16 topics and 20,000 revisions. Titles, excerpts and bodies vary; body lengths range from 396 to 6,996 bytes (median 1,764). The search query uses realistic, present in 80 generated articles; the full query also considers other post types. Low-cardinality fields such as status and categories naturally repeat. This version-2 corpus must not be mixed with older version-1 measurements. | |||
== Correctness and scope == | |||
Six of the seven exact queries return matching results in all three modes, including the total counts: '''83''' for the search and '''0''' for the Yoast query. After import, rebuild and restart, all 22,063 post IDs, titles, excerpts and body texts also match a canonical SHA-256 comparison. The previously reported blob-content mismatch and search total of 82 were '''not reproduced in this fresh master run'''; they are not carried forward as current failures. | |||
The remaining difference is the taxonomy query. Its exact captured SQL '''does contain ORDER BY t.name ASC'''. Both engines return the same 15 projected rows, but in a different name order. MariaDB exposes wp_terms.name as utf8mb4_unicode_520_ci; this fresh MemCP import exposes utf8mb4. Therefore its timing is diagnostic and excluded from accepted speedup claims. This statement applies to this saved query, not to every WordPress taxonomy query. Without an ORDER BY, a different row order alone would not be a correctness failure. | |||
This | This benchmark does not include the unmerged type/collation fix in PR #871. It reports current master as measured. The taxonomy collation discrepancy remains a migration issue for this schema and should be resolved before relying on equivalent ordering. | ||
These are query-level correctness checks and a post-text integrity check, not an exhaustive application compatibility test. INSERT/UPDATE workloads, parallel PHP requests, maintenance costs, cold starts and HTTP page-load latency are outside this matrix. | |||
== Try MemCP with your PHP hosting == | |||
'''MemCP is faster on the substantial repeated SQL work measured here. Try it with your own PHP hosting.''' Start with a copy of your site's data and replay its frequent queries: counts, searches, archives and plugin joins are useful starting points. Compare complete results and total counts, then measure full uncached HTTP requests and representative writes. You can retain the MySQL wire interface or explore direct PHP hosting to remove transport overhead from many small calls. | |||
WordPress is the example, not the product boundary. The same PHP-folder handler and PDO bridge can host other PHP applications; application and plugin compatibility still needs to be checked for the software you deploy. | |||
== Measurement artifacts and exact queries == | |||
Local artifact directory: <code>../wordpress-db-lab/measurements/wiki-master-43d0c8140/</code>. It contains results-master.json (raw batch means and complete query results), php-latency.json (all PHP samples), manifest.json, counts.json, server logs, and a copied benchmark.py. The SQL runner can be repeated against the same fixture; its WP_BINARY and WP_WORKTREE overrides are for separately labelled candidate checks. Candidate measurements are not included in this master matrix. | |||
Source queries: <code>../wordpress-db-lab/profiles/queries/</code>. They are reproduced below so that the benchmark shapes, including actual ORDER BY clauses, are unambiguous. | |||
=== Option lookup: siteurl === | |||
<syntaxhighlight lang="sql"> | |||
-- Copyright (C) 2026 MemCP WordPress lab contributors | |||
-- SPDX-License-Identifier: AGPL-3.0-or-later | |||
SELECT option_value FROM wp_options WHERE option_name = 'siteurl' LIMIT 1; | |||
</syntaxhighlight> | |||
=== Elementor metadata join + LIMIT === | |||
<syntaxhighlight lang="sql"> | |||
SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = '_elementor_template_type' AND wp_postmeta.meta_value = 'landing-page' ) ) AND wp_posts.post_type = 'e-landing-page' AND ((wp_posts.post_status <> 'trash' AND wp_posts.post_status <> 'auto-draft')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 1; | |||
</syntaxhighlight> | |||
=== Taxonomy DISTINCT/join, ordered by name === | |||
<syntaxhighlight lang="sql"> | |||
SELECT DISTINCT t.term_id, tr.object_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id INNER JOIN wp_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('category', 'post_tag', 'post_format') AND tr.object_id IN (1, 21993, 21982, 21971, 21960) ORDER BY t.name ASC; | |||
</syntaxhighlight> | |||
== | === Comment count with join === | ||
<syntaxhighlight lang="sql"> | |||
SELECT COUNT(*) FROM wp_comments LEFT JOIN wp_posts AS wp_posts_to_exclude_reviews ON comment_post_ID = wp_posts_to_exclude_reviews.ID WHERE ( comment_approved = '1' ) AND comment_type NOT IN ('note') AND comment_type != 'order_note' AND comment_type != 'webhook_delivery' AND comment_type != 'action_log' AND wp_posts_to_exclude_reviews.post_type NOT IN ('product'); | |||
</syntaxhighlight> | |||
=== WordPress search + FOUND_ROWS === | |||
<syntaxhighlight lang="sql"> | |||
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ( wp_posts.ID NOT IN ( SELECT object_id FROM wp_term_relationships WHERE term_taxonomy_id IN (166) ) ) AND (((wp_posts.post_title LIKE '%realistic%') OR (wp_posts.post_excerpt LIKE '%realistic%') OR (wp_posts.post_content LIKE '%realistic%'))) AND ((wp_posts.post_type = 'attachment' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'e-floating-buttons' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'page' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'product' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private'))) GROUP BY wp_posts.ID ORDER BY wp_posts.post_title LIKE '%realistic%' DESC, wp_posts.post_date DESC LIMIT 0, 10; | |||
</syntaxhighlight> | |||
=== Yoast CAST metadata join + FOUND_ROWS === | |||
<syntaxhighlight lang="sql"> | |||
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = '_yoast_wpseo_linkdex' AND CAST(wp_postmeta.meta_value AS SIGNED) BETWEEN '1' AND '40' ) ) AND wp_posts.post_type = 'post' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 10; | |||
</syntaxhighlight> | |||
=== Monthly archive DISTINCT === | |||
<syntaxhighlight lang="sql"> | |||
SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month FROM wp_posts WHERE post_type = 'post' AND post_status != 'auto-draft' AND post_status != 'trash' ORDER BY post_date DESC; | |||
</syntaxhighlight> | |||
Revision as of 23:34, 9 September 2026
Benchmark MemCP vs. MariaDB on Wordpress
MemCP makes repeated WordPress database work faster. On current master, MemCP-JIT completes this fixture's search plus total-count query 36.0× faster than MariaDB, its comment count 21.1× faster, and its archive query 6.2× faster. All six query shapes with matching results have lower warm latency with JIT in this run. The seventh, a taxonomy query, is retained in the matrix with a clearly marked collation mismatch.
MemCP speaks the MySQL protocol and can also host PHP directly in the database process. WordPress is our example application: the hosting interface is general purpose and is also intended for MediaWiki and other PHP software. Try MemCP with your PHP hosting and measure the queries your application actually runs.
MariaDB vs. MemCP vs. MemCP-JIT
Measured on master 43d0c8140, including the SQL literal-parameterization improvement in PR #873. All three columns use the same seven WordPress query shapes and data snapshot. These are warm database round trips, not HTTP page-load times. Lower is better.
Each value is the median of 11 batch means, each containing 100 complete query executions, after 30 warmups. For SQL_CALC_FOUND_ROWS statements, the measured operation also includes the subsequent SELECT FOUND_ROWS() on the same connection.
| Query | MariaDB (ms) | MemCP (ms) | MemCP-JIT (ms) | MariaDB / JIT | Result check |
|---|---|---|---|---|---|
| Option lookup: siteurl | 0.079 | 0.069 | 0.056 | 1.39× | Equal; 1 row |
| Elementor metadata join + LIMIT | 0.152 | 0.108 | 0.079 | 1.93× | Equal; 0 rows |
| Taxonomy DISTINCT/join, ordered by name | 0.224 | 0.245 | 0.142 | Diagnostic only | See correctness notes below |
| Comment count with join | 3.240 | 0.222 | 0.153 | 21.14× | Equal; 1 row |
| WordPress search + FOUND_ROWS | 30.405 | 1.278 | 0.844 | 36.01× | Equal; 10 rows |
| Yoast CAST metadata join + FOUND_ROWS | 0.197 | 0.248 | 0.144 | 1.37× | Equal; 0 rows |
| Monthly archive DISTINCT | 0.718 | 0.168 | 0.116 | 6.21× | Equal; 10 rows |
The two metadata queries return zero rows on this snapshot. Their times describe proving an empty result, not retrieving a populated plugin workload. Tiny differences in the smallest queries are sensitive to scheduling on this shared development machine. We do not average these seven unrelated query shapes into a single WordPress speedup.
Host PHP directly: remove a round trip for every small query
The optional FrankenPHP dependency supplies the PHP runtime; only MemCP's own bridge and PDO addon live in this repository. PHP requests use ZTS threads, with OPcache enabled. PHP folders are attached through the existing HTTP handler, alongside the SQL API and dashboard.
With a PHP-enabled build and an external ZTS PHP development installation:
<syntaxhighlight lang="bash"> make php PHP_CONFIG=/path/to/zts/php-config ./memcp-php --no-repl -data /path/to/data --api-port=8098 --mysql-port=3307 --serve=/path/to/php-app lib/main.scm </syntaxhighlight>
The application is mounted at /; /dashboard remains available. Scheme code can mount additional applications through servePHP. No WordPress source is bundled into MemCP.
The PDO addon supports authenticated memcp: connections and retains ordinary MySQL wire connections. A mysql: connection to localhost/127.0.0.1 and this MemCP server's own MySQL port can be routed internally; other destinations continue using their original driver. Both paths require database credentials. WordPress's use of mysqli is a separate connector question: PDO routing alone is not a claim that every WordPress installation or plugin uses PDO.
A separate microbenchmark measures SELECT 1 inside PHP, using an already established connection. Each round performs 100 warmups and 2,000 measured executions; seven rounds alternate the two transports. The timer includes query(), fetchColumn(), and closeCursor(), but excludes HTTP, connection establishment, and PHP startup. Pdo\Mysql::connect forces the real wire driver for the control, so automatic PDO routing cannot accidentally turn both measurements into RAM calls.
| PHP SELECT 1 transport | Median of round medians |
|---|---|
| MySQL wire over local TCP | 37.19 µs |
| MemCP in-process PDO | 11.57 µs |
This removes 69% of the measured latency, or 25.61 µs per tiny query. At 100 such queries per request, the arithmetic saving is approximately 2.56 ms; at 1,000 queries it is 25.61 ms. Those are extrapolations, not measured WordPress page-load improvements.
For a workload-specific budgeting example, halving 100–200 µs of overhead would save 50–100 µs per call (50%). Across 100 database calls, that would add up to 5–10 ms per request. Those figures illustrate how small savings accumulate; they were not measured here. The measured transport comparison above is the evidence for this setup; measure your own query mix before assigning a per-request saving.
Reproducible setup
| Item | Configuration |
|---|---|
| Measurement date | 9 September 2026 (Europe/Berlin) |
| CPU | AMD Ryzen 9 7900X3D, 12 physical cores / 24 logical CPUs |
| Memory / OS | Approximately 62 GiB RAM; Ubuntu 24.04.4 LTS, x86-64 |
| MemCP | Master 43d0c81401616fafeeb456b22f4a47d6e25361d6; unchanged source for both SQL builds |
| SQL compiler/toolchain | Patched Go 1.27.0, revision 84fe25ee45468f5e3920c11c24e2bbb926fc3a72; GOEXPERIMENT empty vs. GOEXPERIMENT=jit |
| MariaDB | 10.11.14; query cache OFF; InnoDB buffer pool 128 MiB |
| PHP microbenchmark | Same MemCP master, built with Go 1.26.0; PHP 8.5.10 ZTS, Zend Engine 4.5.10, OPcache enabled; FrankenPHP v1.12.7; no Go JIT |
| SQL client | Python 3.12 / PyMySQL, established TCP connections to 127.0.0.1; complete result fetching; no connection setup timed |
| Database fixture | Read-only consistent snapshot of wordpress_bench_v2, copied to a separate MemCP database; MariaDB InnoDB vs. MemCP sloppy engine; rebuild before measurements |
| Measurement order | MariaDB, MemCP, MemCP-JIT; one client, no concurrent query load |
| Runtime settings | Query-plan cache, autoindexing and normal optimizer enabled; TracePrint and ScanDebugging disabled while timing |
This is a warm read comparison, not a matched-memory or durability benchmark. The host is shared with development services; cooperating builds/tests were paused for timing, but CPUs and memory were not reserved or pinned. Autoindexing must have time to activate: the warmup count deliberately exceeds its initial observation threshold. Compiled plans, indexes and maintained group/prejoin structures may all contribute to warm results. Their cold build cost and write-maintenance cost are outside the latency table.
Cold startup and initial automatic structure building are not represented by this table. Both MemCP modes reuse the same freshly imported, rebuilt directory sequentially; persisted structures and OS caches remain available. An earlier preparation pass populated some structures before the final measured run. This is deliberately a warm comparison, not a cold-start A/B test.
Fixture
The seven captured queries exercise option lookups, plugin metadata, taxonomy joins, comment counts, search and archives. The data is synthetic, with real WordPress query shapes. It is not a million-post or concurrent-production benchmark. The source WordPress installation identifies itself as 7.1; WordPress/PHP rendering does not execute in the SQL matrix, and installed plugin versions are not performance inputs once these exact queries are captured.
| Table | Rows |
|---|---|
| wp_posts | 22,063 |
| wp_postmeta | 14,013 |
| wp_comments | 6,001 |
| wp_options | 503 |
| wp_terms | 175 |
| wp_term_taxonomy | 175 |
| wp_term_relationships | 7,701 |
The corpus contains 2,000 varied generated articles across 16 topics and 20,000 revisions. Titles, excerpts and bodies vary; body lengths range from 396 to 6,996 bytes (median 1,764). The search query uses realistic, present in 80 generated articles; the full query also considers other post types. Low-cardinality fields such as status and categories naturally repeat. This version-2 corpus must not be mixed with older version-1 measurements.
Correctness and scope
Six of the seven exact queries return matching results in all three modes, including the total counts: 83 for the search and 0 for the Yoast query. After import, rebuild and restart, all 22,063 post IDs, titles, excerpts and body texts also match a canonical SHA-256 comparison. The previously reported blob-content mismatch and search total of 82 were not reproduced in this fresh master run; they are not carried forward as current failures.
The remaining difference is the taxonomy query. Its exact captured SQL does contain ORDER BY t.name ASC. Both engines return the same 15 projected rows, but in a different name order. MariaDB exposes wp_terms.name as utf8mb4_unicode_520_ci; this fresh MemCP import exposes utf8mb4. Therefore its timing is diagnostic and excluded from accepted speedup claims. This statement applies to this saved query, not to every WordPress taxonomy query. Without an ORDER BY, a different row order alone would not be a correctness failure.
This benchmark does not include the unmerged type/collation fix in PR #871. It reports current master as measured. The taxonomy collation discrepancy remains a migration issue for this schema and should be resolved before relying on equivalent ordering.
These are query-level correctness checks and a post-text integrity check, not an exhaustive application compatibility test. INSERT/UPDATE workloads, parallel PHP requests, maintenance costs, cold starts and HTTP page-load latency are outside this matrix.
Try MemCP with your PHP hosting
MemCP is faster on the substantial repeated SQL work measured here. Try it with your own PHP hosting. Start with a copy of your site's data and replay its frequent queries: counts, searches, archives and plugin joins are useful starting points. Compare complete results and total counts, then measure full uncached HTTP requests and representative writes. You can retain the MySQL wire interface or explore direct PHP hosting to remove transport overhead from many small calls.
WordPress is the example, not the product boundary. The same PHP-folder handler and PDO bridge can host other PHP applications; application and plugin compatibility still needs to be checked for the software you deploy.
Measurement artifacts and exact queries
Local artifact directory: ../wordpress-db-lab/measurements/wiki-master-43d0c8140/. It contains results-master.json (raw batch means and complete query results), php-latency.json (all PHP samples), manifest.json, counts.json, server logs, and a copied benchmark.py. The SQL runner can be repeated against the same fixture; its WP_BINARY and WP_WORKTREE overrides are for separately labelled candidate checks. Candidate measurements are not included in this master matrix.
Source queries: ../wordpress-db-lab/profiles/queries/. They are reproduced below so that the benchmark shapes, including actual ORDER BY clauses, are unambiguous.
Option lookup: siteurl
<syntaxhighlight lang="sql"> -- Copyright (C) 2026 MemCP WordPress lab contributors -- SPDX-License-Identifier: AGPL-3.0-or-later SELECT option_value FROM wp_options WHERE option_name = 'siteurl' LIMIT 1; </syntaxhighlight>
Elementor metadata join + LIMIT
<syntaxhighlight lang="sql"> SELECT wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = '_elementor_template_type' AND wp_postmeta.meta_value = 'landing-page' ) ) AND wp_posts.post_type = 'e-landing-page' AND ((wp_posts.post_status <> 'trash' AND wp_posts.post_status <> 'auto-draft')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 1; </syntaxhighlight>
Taxonomy DISTINCT/join, ordered by name
<syntaxhighlight lang="sql"> SELECT DISTINCT t.term_id, tr.object_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id INNER JOIN wp_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('category', 'post_tag', 'post_format') AND tr.object_id IN (1, 21993, 21982, 21971, 21960) ORDER BY t.name ASC; </syntaxhighlight>
Comment count with join
<syntaxhighlight lang="sql"> SELECT COUNT(*) FROM wp_comments LEFT JOIN wp_posts AS wp_posts_to_exclude_reviews ON comment_post_ID = wp_posts_to_exclude_reviews.ID WHERE ( comment_approved = '1' ) AND comment_type NOT IN ('note') AND comment_type != 'order_note' AND comment_type != 'webhook_delivery' AND comment_type != 'action_log' AND wp_posts_to_exclude_reviews.post_type NOT IN ('product'); </syntaxhighlight>
WordPress search + FOUND_ROWS
<syntaxhighlight lang="sql"> SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND ( wp_posts.ID NOT IN ( SELECT object_id FROM wp_term_relationships WHERE term_taxonomy_id IN (166) ) ) AND (((wp_posts.post_title LIKE '%realistic%') OR (wp_posts.post_excerpt LIKE '%realistic%') OR (wp_posts.post_content LIKE '%realistic%'))) AND ((wp_posts.post_type = 'attachment' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'e-floating-buttons' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'page' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private')) OR (wp_posts.post_type = 'product' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'acf-disabled' OR wp_posts.post_status = 'private'))) GROUP BY wp_posts.ID ORDER BY wp_posts.post_title LIKE '%realistic%' DESC, wp_posts.post_date DESC LIMIT 0, 10; </syntaxhighlight>
Yoast CAST metadata join + FOUND_ROWS
<syntaxhighlight lang="sql"> SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts INNER JOIN wp_postmeta ON ( wp_posts.ID = wp_postmeta.post_id ) WHERE 1=1 AND ( ( wp_postmeta.meta_key = '_yoast_wpseo_linkdex' AND CAST(wp_postmeta.meta_value AS SIGNED) BETWEEN '1' AND '40' ) ) AND wp_posts.post_type = 'post' AND ((wp_posts.post_status = 'publish')) GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 10; </syntaxhighlight>
Monthly archive DISTINCT
<syntaxhighlight lang="sql"> SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month FROM wp_posts WHERE post_type = 'post' AND post_status != 'auto-draft' AND post_status != 'trash' ORDER BY post_date DESC; </syntaxhighlight>