Benchmark MemCP vs. MariaDB on Wordpress: Difference between revisions

From MemCP
Jump to navigation Jump to search
(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
 
(6 intermediate revisions by the same user not shown)
Line 1: Line 1:
<!-- Copyright (C) 2026 MemCP contributors -->
<!-- Copyright (C) 2026 Carl-Philip Hänsch -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
= Editing 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'''. MemCP-JIT has lower measured warm latency across all seven query shapes in this run.


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


We compared '''MariaDB, MemCP, and MemCP with JIT''' using seven SQL queries captured from a WordPress installation. On the tested master revision, JIT-enabled MemCP completed the comment-count query '''25.5 times faster''' and the archive query '''4.8 times faster''' than MariaDB. A simple option lookup was effectively tied. Two small metadata queries still favored MariaDB.
== MariaDB vs. MemCP vs. MemCP-JIT ==


For administrators, the takeaway is specific: '''MemCP offers substantial potential for recurring aggregates, but the right migration decision depends on your site's query mix, correctness, and cache behavior—not one headline multiplier.'''
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.


{| class="wikitable"
'''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.
| '''Status of this measurement — 8 September 2026'''
The tested master revision has two known correctness issues exposed by this fixture: a blob-reference decoding problem and lost collation in the grouped taxonomy plan. Search also returns an incorrect total count. The affected timings below are diagnostic only, not accepted speedup claims. This is a development benchmark supporting a staging evaluation, '''not a recommendation to replace a production WordPress database with this revision'''.
|}


== The results at a glance ==
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.
 
The following values are '''warm end-to-end database latency''': sending SQL over an established TCP/MySQL connection and receiving the complete result. For the search and SEO queries, the time also includes a subsequent <code>SELECT FOUND_ROWS()</code> on the same connection.
 
'''Lower is better.''' The comparison column is MariaDB versus MemCP JIT—not JIT versus vanilla.


{| class="wikitable sortable"
{| class="wikitable sortable"
! WordPress workload
! Query !! MariaDB (ms) !! MemCP (ms) !! MemCP-JIT (ms) !! MariaDB / JIT !! Result check
! MariaDB
! MemCP
! MemCP + JIT
! MemCP JIT compared with MariaDB
! Result check
|-
|-
| Option lookup (<code>siteurl</code>)
| Option lookup: siteurl || 0.079 || 0.069 || '''0.056''' || 1.39× || Equal; 1 row
| 0.082 ms
| 0.116 ms
| 0.081 ms
| Effectively tied; approximately 1% lower latency
| Same single row
|-
|-
| Plugin metadata join with LIMIT
| Elementor metadata join + LIMIT || 0.152 || 0.108 || '''0.079''' || 1.93× || Equal; 0 rows
| 0.141 ms
| 0.211 ms
| 0.200 ms
| 41.9% higher latency
| Same empty result
|-
|-
| Taxonomy join and DISTINCT
| Taxonomy DISTINCT/join, ordered by name || 0.224 || 0.245 || '''0.142''' || 1.58× || Equal
| 0.222 ms
| 0.222 ms
| 0.179 ms
| Not scored: ordering differs
| Same 15 rows, different order
|-
|-
| Comment COUNT with exclusion join
| Comment count with join || 3.240 || 0.222 || '''0.153''' || 21.14× || Equal; 1 row
| 4.053 ms
| 0.173 ms
| '''0.159 ms'''
| '''25.5× faster; 96.1% lower latency'''
| Same aggregate result
|-
|-
| WordPress search and total count
| WordPress search + FOUND_ROWS || 30.405 || 1.278 || '''0.844''' || 36.01× || Equal; 10 rows
| 31.457 ms
| 2.192 ms
| 1.766 ms
| Not scored: total count differs
| Same first 10 rows; total 82 instead of 83
|-
|-
| SEO metadata CAST/range join
| Yoast CAST metadata join + FOUND_ROWS || 0.197 || 0.248 || '''0.144''' || 1.37× || Equal; 0 rows
| 0.177 ms
| 0.225 ms
| 0.198 ms
| 11.8% higher latency
| Same empty result and total of zero
|-
|-
| DISTINCT year/month archive
| Monthly archive DISTINCT || 0.718 || 0.168 || '''0.116''' || 6.21× || Equal; 10 rows
| 0.666 ms
| 0.158 ms
| '''0.138 ms'''
| '''4.8× faster; 79.2% lower latency'''
| Same 10 ordered rows
|}
|}


The empty metadata results are intentional disclosures, not successful-hit performance tests. They measure how quickly each engine establishes that there are no matching records in this snapshot. They do not establish performance for populated plugin result sets.
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].


There is no combined “WordPress speedup” score. The frequency of these queries differs between sites, and a correctness failure must not be averaged into a favorable result.
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.


== What this means for a WordPress administrator ==
== Host PHP directly: remove a round trip for every small query ==


=== Look for expensive repeated database work ===
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.


A site dominated by indexed point lookups has less to gain from the results here: the option lookup took about 0.08 ms in both MariaDB and JIT-enabled MemCP. A site spending database time on counts, joins, and archive aggregates has a more promising starting point.
With a PHP-enabled build and an external ZTS PHP development installation:


For example, the measured comment-count query saves approximately '''3.89 ms per execution'''. That can matter if the query is frequent or occurs repeatedly while rendering an uncached request. It does not imply that the entire PHP request becomes 25 times faster.
<pre>
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
</pre>


Use your own slow-query trace or query-monitoring output to identify the statements that dominate database time. Include frontend pages, administration screens, searches, and scheduled plugin work.
The application is mounted at /; /dashboard remains available. Scheme code can mount additional applications through servePHP. No WordPress source is bundled into MemCP.


=== Understand what is warm ===
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.


MemCP can reuse compiled query plans and incrementally maintained relational structures such as group caches and prejoins. These structures help avoid repeating a join or aggregate from scratch.
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.


A warm read benchmark benefits from that reuse. It does '''not''' measure the complete cost of maintaining those structures under writes. New comments, imports, metadata updates, and bulk edits therefore belong in a separate staging test.
{| 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'''
|}


JIT is another part of execution: eligible code is compiled to native instructions. It is not permission to change SQL results, and it does not make every query faster. The vanilla column shows that much of the aggregate advantage is already present without JIT.
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.


=== Budget for the first request ===
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.


Fast warm responses do not imply that the first request is equally fast. In this run, selected first-query and warm values were:
== Reproducible setup ==


{| class="wikitable"
{| class="wikitable"
! Query
! Item !! Configuration
! MemCP first query
! MemCP + JIT first query
! MemCP + JIT warm median
|-
|-
| Plugin metadata join
| Measurement date || 9 September 2026 (Europe/Berlin)
| 42.3 ms
| 33.2 ms
| 0.200 ms
|-
|-
| Comment COUNT
| CPU || AMD Ryzen 9 7900X3D, 12 physical cores / 24 logical CPUs
| 52.1 ms
| 66.2 ms
| 0.159 ms
|-
|-
| WordPress search — correctness issue
| Memory / OS || Approximately 62 GiB RAM; Ubuntu 24.04.4 LTS, x86-64
| 1,957.7 ms
| 2,553.0 ms
| 1.766 ms
|-
|-
| Archive
| MemCP || Master 43d0c81401616fafeeb456b22f4a47d6e25361d6; unchanged source for both SQL builds
| 13,652.3 ms
|-
| 14,560.0 ms
| SQL compiler/toolchain || Patched Go 1.27.0, revision 84fe25ee45468f5e3920c11c24e2bbb926fc3a72; GOEXPERIMENT empty vs. GOEXPERIMENT=jit
| 0.138 ms
|-
| 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
|}
|}


Preparation can also occur after the first execution. During the 20 warmup requests, the plugin metadata query had an approximately 650 ms outlier in each MemCP mode; the search had approximately 580 ms outliers.
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.


These are '''observed first-query latencies after restarting each MemCP process''', not a controlled empty-cache comparison: persisted structures and operating-system caches were retained, and vanilla ran before JIT against the same data directory. Server startup itself is not included in these values.
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.


For a migration rehearsal, measure first requests after restart, cache eviction, and representative writes separately. Do not use the warm table to set cold-start service-level expectations.
=== Fixture ===


== A more useful WordPress 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.
 
The queries come from a WordPress installation using common plugins, including WooCommerce, Elementor, Yoast SEO, Advanced Custom Fields, and Contact Form 7. No plugin receives a special place in the scoring; the seven statements cover distinct kinds of database work.
 
The measured '''version-2 queryset fixture''' contains these seven tables—not the complete live WordPress installation:


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


Version 1 repeated essentially the same search-bearing paragraph in every generated article. Version 2 replaces that with 2,000 different titles, excerpts, and article bodies across 16 topics. Article bodies range from 396 to 6,996 bytes, with a median of 1,764 bytes. Revisions retain their parent document's content with an editorial change; low-cardinality fields such as status and categories still repeat naturally.
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 generator deliberately provides several search frequencies over the 2,000 generated articles:
 
* <code>nightjar</code>: 10 matches, or 0.5%;
* <code>realistic</code>: 80 matches, or 4%;
* <code>performance</code>: 240 matches, or 12%;
* <code>quasarless</code>: no matches.
 
These are corpus checks, '''not four separately timed search workloads'''. The captured search query uses <code>realistic</code>. Its SQL also considers pages and other post types and applies additional predicates, so its complete MariaDB total is 83 rather than 80.
 
This is a controlled synthetic dataset with real captured query shapes. It is not evidence of performance at millions of posts or under concurrent production traffic. The gains on counts and archives justify testing larger datasets; they do not replace that test.
 
== Correctness before speed ==
 
Results were compared across engines, including <code>FOUND_ROWS()</code> where applicable. Five query shapes agreed; two did not.
 
=== Taxonomy ordering ===
 
Both databases expose <code>wp_terms.name</code> with <code>utf8mb4_unicode_520_ci</code>. A direct ordered scan agrees, but the full DISTINCT/join plan loses that collation at its grouped sort. For example, MemCP places <code>Uncategorized</code> before lower-case tag names.


This is '''not an unspecified tie order'''. It is a correctness issue, and the taxonomy timing is excluded from speedup claims.
== Try MemCP with your PHP hosting ==


=== Blob content and search totals ===
'''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.


Full table contents matched checksums immediately after import. After MemCP compaction, 33 long post-content values were observed as 32-byte blob hashes instead of their text. A hash beginning with <code>!</code> collides with the legacy escaped-string encoding.
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.


For one affected post, MariaDB reports a text length of 5,327 bytes and MemCP reports 32. The search's first ten rows agree, but MemCP reports 82 total matches versus MariaDB's 83. Consequently the search timing is also excluded from accepted speedup claims.
== Measurement artifacts and exact queries ==


These findings must be resolved and the affected workloads revalidated before using this revision for migration. No query was removed from the table to conceal them. All 21 measurements completed without a process crash.
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.


== How the comparison was run ==
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.
 
{| class="wikitable"
! Item
! Configuration
|-
| MemCP source
| [https://github.com/launix-de/memcp/commit/ce26bfb3deb883efc09272965cc1a3b9eb177e24 Master ce26bfb3d], including the merged Q1/Q3 planner improvements
|-
| Date
| 8 September 2026
|-
| MariaDB
| 10.11.14; query cache disabled; InnoDB buffer pool 128 MiB
|-
| MemCP builds
| Identical source, both built with the patched Go 1.27.0 toolchain; <code>GOEXPERIMENT</code> empty versus <code>GOEXPERIMENT=jit</code>
|-
| Host
| AMD Ryzen 9 7900X3D, 12 cores / 24 logical CPUs; approximately 62 GiB RAM
|-
| Client
| Python/PyMySQL over local TCP; established connections, autocommit
|-
| Sampling
| 20 warmup executions, then 11 batches of 100 executions per query and engine
|-
| Reported statistic
| Median of the 11 batch means, in milliseconds per query or query/FOUND_ROWS pair
|-
| Execution order
| MariaDB, MemCP vanilla, MemCP JIT
|-
| Run duration
| Approximately 93 seconds, excluding builds, fixture import, and investigation
|}


The host was shared with development work and was not isolated for benchmarking. Small percentage differences should not be overinterpreted. The results are not a memory-budget-matched or concurrency-throughput comparison.
=== 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>


The query-plan cache and normal execution behavior remained enabled. We did not substitute hand-written plans or force a special prejoin before timing. The setup contains no application-level page-cache measurement: this is database latency, not HTTP page-render time.
=== 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>


The measurements are specific to this master snapshot and fixture. Earlier version-1 numbers should not be mixed with this table or used to calculate an improvement attributable solely to a planner patch.
=== 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>


== A practical staging checklist ==
=== 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>


Before deciding whether MemCP will speed up your WordPress installation:
=== 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>


# '''Capture your workload.''' Include expensive frontend queries, admin pages, search, and the plugins your site actually uses.
=== Yoast CAST metadata join + FOUND_ROWS ===
# '''Use one consistent data snapshot.''' Preserve IDs, relationships, text, and collations. Compare contents after import and after compression/restart—not just table row counts.
<pre>
# '''Check complete results.''' Validate ordering, pagination, NULL behavior, aggregates, and total counts, including <code>FOUND_ROWS()</code>.
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;
# '''Separate first use from steady state.''' Record startup/cache-build spikes, warm medians, and tail latency.
</pre>
# '''Exercise writes and concurrency.''' Test comments, edits, imports, plugin jobs, cache maintenance, and recovery. This read-only query benchmark does not establish their cost or compatibility.
# '''Measure whole requests too.''' PHP, plugin execution, network requests, and page caching can dominate total page time even when SQL becomes faster.
# '''Keep production unchanged until validation passes.''' Retain your MariaDB deployment and a tested backup/rollback path during evaluation. The known correctness issues above must be closed first.


MemCP's strongest result here is the reduction of recurring aggregate work from milliseconds to a fraction of a millisecond. For WordPress administrators, that is a concrete reason to evaluate it—but equivalent results and predictable operations remain the entry requirements for any speed claim.
=== 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>

Latest revision as of 23:54, 9 September 2026

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