Benchmark MemCP vs. MariaDB on Wordpress: Difference between revisions
No edit summary |
|||
| Line 1: | Line 1: | ||
<!-- Copyright (C) 2026 MemCP contributors --> | <!-- Copyright (C) 2026 MemCP contributors --> | ||
<!-- SPDX-License-Identifier: AGPL-3.0-or-later --> | <!-- SPDX-License-Identifier: AGPL-3.0-or-later --> | ||
'''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'''. MemCP-JIT has lower measured warm latency across all seven query shapes in this run. | |||
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. | |||
'''Dataset size:''' 22,063 rows in wp_posts (including 2,000 generated articles and 20,000 revisions), 14,013 post-metadata rows, 6,001 comments and 503 options. Taxonomy data comprises 175 terms, 175 term-taxonomy entries and 7,701 object-to-taxonomy relationships. These are the measured snapshot's table counts, not the number of rows returned by each query. | |||
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" | |||
! 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''' || 1.58× || Equal | |||
|- | |||
| 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 | |||
|} | |||
Timing values are from master 43d0c8140. Taxonomy result equality (all 15 rows, including their order) was verified separately with [https://github.com/launix-de/memcp/pull/871 PR #871]. | |||
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 == | |||
'''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'''. MemCP-JIT has lower measured warm latency across all seven query shapes in this run. | '''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'''. MemCP-JIT has lower measured warm latency across all seven query shapes in this run. | ||
| Line 42: | Line 78: | ||
With a PHP-enabled build and an external ZTS PHP development installation: | With a PHP-enabled build and an external ZTS PHP development installation: | ||
< | <pre> | ||
make php PHP_CONFIG=/path/to/zts/php-config | 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 | ./memcp-php --no-repl -data /path/to/data --api-port=8098 --mysql-port=3307 --serve=/path/to/php-app lib/main.scm | ||
</ | </pre> | ||
The application is mounted at /; /dashboard remains available. Scheme code can mount additional applications through servePHP. No WordPress source is bundled into MemCP. | The application is mounted at /; /dashboard remains available. Scheme code can mount additional applications through servePHP. No WordPress source is bundled into MemCP. | ||
| Line 64: | Line 100: | ||
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. | 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" | |||
! 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. | |||
{| class="wikitable" | |||
! Table !! Contents !! Rows | |||
|- | |||
| wp_posts || Posts, pages, revisions and other post types || 22,063 | |||
|- | |||
| wp_postmeta || Post metadata || 14,013 | |||
|- | |||
| wp_comments || Comments || 6,001 | |||
|- | |||
| wp_options || Site options || 503 | |||
|- | |||
| wp_terms || Terms (category/tag names) || 175 | |||
|- | |||
| wp_term_taxonomy || Term assignments to taxonomy types || 175 | |||
|- | |||
| wp_term_relationships || Object-to-taxonomy 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,763). 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. | |||
== 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, corpus.json, text-parity.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 === | |||
<pre> | |||
-- 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; | |||
</pre> | |||
=== Elementor metadata join + LIMIT === | |||
<pre> | |||
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; | |||
</pre> | |||
=== Taxonomy DISTINCT/join, ordered by name === | |||
<pre> | |||
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; | |||
</pre> | |||
=== Comment count with join === | |||
<pre> | |||
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'); | |||
</pre> | |||
=== WordPress search + FOUND_ROWS === | |||
<pre> | |||
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; | |||
</pre> | |||
=== Yoast CAST metadata join + FOUND_ROWS === | |||
<pre> | |||
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; | |||
</pre> | |||
=== Monthly archive DISTINCT === | |||
<pre> | |||
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; | |||
</pre> | |||
== Reproducible setup == | == Reproducible setup == | ||
Revision as of 23:44, 9 September 2026
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. MemCP-JIT has lower measured warm latency across all seven query shapes in this run.
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.
Dataset size: 22,063 rows in wp_posts (including 2,000 generated articles and 20,000 revisions), 14,013 post-metadata rows, 6,001 comments and 503 options. Taxonomy data comprises 175 terms, 175 term-taxonomy entries and 7,701 object-to-taxonomy relationships. These are the measured snapshot's table counts, not the number of rows returned by each query.
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 | 1.58× | Equal |
| 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 |
Timing values are from master 43d0c8140. Taxonomy result equality (all 15 rows, including their order) was verified separately with PR #871.
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
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. MemCP-JIT has lower measured warm latency across all seven query shapes in this run.
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.
Dataset size: 22,063 rows in wp_posts (including 2,000 generated articles and 20,000 revisions), 14,013 post-metadata rows, 6,001 comments and 503 options. Taxonomy data comprises 175 terms, 175 term-taxonomy entries and 7,701 object-to-taxonomy relationships. These are the measured snapshot's table counts, not the number of rows returned by each query.
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 | 1.58× | Equal |
| 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 |
Timing values are from master 43d0c8140. Taxonomy result equality (all 15 rows, including their order) was verified separately with PR #871.
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:
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
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 | Contents | Rows |
|---|---|---|
| wp_posts | Posts, pages, revisions and other post types | 22,063 |
| wp_postmeta | Post metadata | 14,013 |
| wp_comments | Comments | 6,001 |
| wp_options | Site options | 503 |
| wp_terms | Terms (category/tag names) | 175 |
| wp_term_taxonomy | Term assignments to taxonomy types | 175 |
| wp_term_relationships | Object-to-taxonomy 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,763). 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.
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, corpus.json, text-parity.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
-- 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;
Elementor metadata join + LIMIT
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;
Taxonomy DISTINCT/join, ordered by name
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;
Comment count with join
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');
WordPress search + FOUND_ROWS
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;
Yoast CAST metadata join + FOUND_ROWS
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;
Monthly archive DISTINCT
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;
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 | Contents | Rows |
|---|---|---|
| wp_posts | Posts, pages, revisions and other post types | 22,063 |
| wp_postmeta | Post metadata | 14,013 |
| wp_comments | Comments | 6,001 |
| wp_options | Site options | 503 |
| wp_terms | Terms (category/tag names) | 175 |
| wp_term_taxonomy | Term assignments to taxonomy types | 175 |
| wp_term_relationships | Object-to-taxonomy 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,763). 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.
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, corpus.json, text-parity.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>