Benchmark MemCP vs. MariaDB on Wordpress: Difference between revisions

From MemCP
Jump to navigation Jump to search
No edit summary
No edit summary
Line 9: Line 9:


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.
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.
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.
Line 100: Line 102:


{| class="wikitable"
{| class="wikitable"
! Table !! Rows
! Table !! Contents !! Rows
|-
|-
| wp_posts || 22,063
| wp_posts || Posts, pages, revisions and other post types || 22,063
|-
|-
| wp_postmeta || 14,013
| wp_postmeta || Post metadata || 14,013
|-
|-
| wp_comments || 6,001
| wp_comments || Comments || 6,001
|-
|-
| wp_options || 503
| wp_options || Site options || 503
|-
|-
| wp_terms || 175
| wp_terms || Terms (category/tag names) || 175
|-
|-
| wp_term_taxonomy || 175
| wp_term_taxonomy || Term assignments to taxonomy types || 175
|-
|-
| wp_term_relationships || 7,701
| wp_term_relationships || Object-to-taxonomy relationships || 7,701
|}
|}



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

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 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>