Benchmark MemCP vs. MariaDB on Wordpress: Difference between revisions

From MemCP
Jump to navigation Jump to search
No edit summary
No edit summary
Line 2: Line 2:
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->


= 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'''. 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'''. 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.
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.
Line 21: Line 19:
| Elementor metadata join + LIMIT || 0.152 || 0.108 || '''0.079''' || 1.93× || Equal; 0 rows
| 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
| Taxonomy DISTINCT/join, ordered by name || 0.224 || 0.245 || '''0.142''' || Master timing || Fix: [https://github.com/launix-de/memcp/pull/871 PR #871] (in CI)
|-
|-
| Comment count with join || 3.240 || 0.222 || '''0.153''' || 21.14× || Equal; 1 row
| Comment count with join || 3.240 || 0.222 || '''0.153''' || 21.14× || Equal; 1 row
Line 118: Line 116:


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.
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.
== 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 ==
== Try MemCP with your PHP hosting ==

Revision as of 23:36, 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.

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 Master timing Fix: PR #871 (in CI)
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,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>