<?xml version="1.0" encoding="utf-8" ?>
<rss version="2.0"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:content="http://purl.org/rss/1.0/modules/content/">

<channel>
  <title>Planet MySQL</title>
  <link>https://planet.mysql.com</link>
  <pubDate>Sun, 25 Oct 2026 08:32:55 +0000</pubDate>
  <language>en</language>
  <description>Planet MySQL - https://planet.mysql.com</description>

  <item>
    <title>Optimize OLTP and OLAP Workloads with MySQL HeatWave Autopilot</title>
    <guid isPermaLink="false">6dbe256dac1ed36c0e79d740107a974f</guid>
    <link>https://blogs.oracle.com/mysql/optimize-oltp-and-olap-workloads-with-mysql-heatwave-autopilot</link>
    <description>As businesses grow, their databases must support both fast transactions and timely analytics. OLTP workloads handle everyday operations such as orders and payments, while OLAP workloads analyze large datasets for reporting and planning. Without appropriate optimization, growing data volumes and competing resource demands can slow transactions and delay insights. MySQL HeatWave supports online transaction processing […]</description>
    <pubDate>Fri, 23 Oct 2026 18:27:17 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Basics/How-To</category>
    <category>MySQL HeatWave</category>
    <category>OCI</category>
  </item>

  <item>
    <title>What’s New in MySQL NDB Cluster 9.x – Better Visibility and Control in MySQL NDB Cluster</title>
    <guid isPermaLink="false">5088bd809dc1d42c81aeb98bca818a45</guid>
    <link>https://blogs.oracle.com/mysql/whats-new-in-mysql-ndb-cluster-9-x-better-visibility-and-control-in-mysql-ndb-cluster</link>
    <description>MySQL NDB Cluster 9.x continues to focus on the qualities that matter most in production: clear operational insight, predictable behaviour under failure, efficient application access, and faster recovery. This release series adds practical observability across the cluster, strengthens several exceptional paths, and expands the control available to users. Better visibility into cluster operation Diagnosing a […]</description>
    <pubDate>Thu, 22 Oct 2026 17:05:48 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>MySQL NDB</category>
    <category>News</category>
    <category>mysql</category>
    <category>MySQL 9.7 LTS</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>Benchmarking vector indexes in MySQL with vector-dev-bench</title>
    <guid isPermaLink="false">6ad905f36a70ab0001bf83eb</guid>
    <link>https://villagesql.com/blog/vector-dev-bench/</link>
    <description>PostgreSQL has a rapidly evolving vector ecosystem. MySQL is earlier on that road, but the community is hard at work catching up. MariaDB ships its own HNSW index, Google engineers have shared a ScaNN-based vector index on MySQL's own vector index proposal, Percona is building an HNSW index inside InnoDB, and VillageSQL is building vector search as an extension with vsql-vector.
We want vector search on MySQL to be something that teams can choose with confidence (and not have to switch to PostgreSQL to get). Getting there takes more than one good implementation. The teams building these indexes need to measure them the same way, check each other's numbers, and fix what the numbers show.
Working with Percona, we published vector-dev-bench. It runs on a laptop, it drives VillageSQL, MariaDB, PostgreSQL with pgvector, and MySQL built from Google's ScaNN pull request, and it gives an answer in minutes.
Doing things in the open
A benchmark number is only as good as the method behind it. A public method lets anyone rerun numbers, read the exact SQL each engine received, and tell us where the method may be unfair.
It also makes collaboration practical. When another team can run the same harness against their own build, we can compare results without a bunch of upfront coordination. With vector-dev-bench, an engine is one entry in a Python dictionary plus a start script, so adding a new one is a small change.
We've seen this work already. Percona's vector-bench is a full comparison framework, and we added VillageSQL to our copy of it. Along the way, we noticed that pgvector loaded its rows with COPY while the MySQL engines used INSERT, so the build times were measuring two different kinds of statements. We switched pgvector's incremental build to batched INSERTs so that every engine loads rows the same way. vector-bench adopted the change, and its code credits our commit. That is the kind of exchange we want more of.
Two tools for two jobs
vector-bench and vector-dev-bench answer different questions, and we use both.
vector-bench is built to produce a comparison that you can publish. It runs each engine in Docker, built from pinned sources where it compiles them, runs real datasets such as SIFT, GloVe, and GIST, and measures recall against throughput, build cost, concurrency from 1 to 32 clients, filtered search, and recall after deletes and reinserts. It runs every engine twice, once with identical resources and once with each vendor's recommended settings, and it records the hardware and versions with every result. Its main profile takes about two days, which is the right cost for a number that other people will cite.
vector-dev-bench is built for the quick, iterative loop in which you change the index, rebuild it, and want to know whether recall moved. It runs against engines you've already built, does not use containers, and uses synthetic data by default. It builds a 20,000-vector index in about 7 seconds. Its README is plain about the trade-off: the numbers are for relative comparisons, and a published comparison belongs in a framework like vector-bench.
vector-dev-bench Measurement
vector-dev-bench is three command-line tools over one shared module:



Tool
What it measures




recall_bench.py
It measures recall@k and queries per second across a sweep of ef_search values. It exits with an error when recall falls below a threshold (0.95 by default), so a script can use it as a pass/fail check.


build_bench.py
It measures index build time. --build-threads N builds the index from N client connections at once.


rw_bench.py
It measures reads during writes. Reader threads run nearest-neighbor queries while writer threads insert into the same index, and the tool reports read throughput second by second along with p50, p95, and p99 latency.



Recall is the measure that matters here, because an approximate nearest-neighbor index returns approximately the right answer by design. A test that compares exact output, the way MySQL's own test framework does, fails on a correct index. vector-dev-bench computes the true nearest neighbors itself with NumPy and reports what fraction of them each engine returned. You can also pass --dataset to use a standard ann-benchmarks dataset with its published ground truth.
The engines are profiles:



Profile
Engine




vsql_vector
VillageSQL with vsql-vector (SVECTOR column, HNSW index)


mariadb
MariaDB 11.7 or later (MHNSW)


pgvector
PostgreSQL 15 or later with pgvector


google
MySQL 9.7.1 built from Google's vector search commit (ScaNN index), which its authors describe as an experimental proof of concept


varchar_ctrl
A plain VARCHAR column, as a control that separates a server problem from a harness problem



VillageSQL, MariaDB, and the Google build all speak the MySQL protocol, and the harness drives all three through the same PyMySQL connection code. The client cost is the same for each engine, but it isn't zero. A gap between two of these engines holds for the machine that measured it, and it can change on different hardware, so rerun it on the hardware you care about before you trust it.
pgvector takes a different path. The harness sends each batch of pgvector inserts or queries through the psql command-line client, so the timed window also includes starting psql and opening its connection. The parallel options (--readers and --build-threads) fall back to one serial client, and rw_bench.py does not run against pgvector. A VillageSQL-versus-pgvector number from vector-dev-bench measures the two clients as well as the two engines. Treat it as a hint about where to look, and don't quote it as a verdict on either engine.
What a run looks like
We ran a recall sweep against VillageSQL 0.0.7 with the vsql-vector-pk build of the extension (whose HNSW index stores each row's primary key), on 20,000 random 32-dimension vectors. The tool builds the index once and then queries it at four ef_search values. The README and RUNBOOK cover building each engine and running the tools.
Here is the output from an Apple M5 laptop with 32 GB of memory:
build_time_s=7.03  (index built once; sweeping ef_search)
ef_search  recall@10    qps
50         0.7870       8829.3
100        0.9035       5903.0
200        0.9645       3526.0
400        0.9875       2044.4

This is the curve every HNSW index trades along. A wider search finds more of the true neighbors and answers fewer queries per second. The shape is also a check on the run itself: recall that stays at exactly 1.0 when ef_search drops usually means the optimizer chose a full table scan and the index wasn't used.
MariaDB and PostgreSQL run the same sweep through their own profiles, and run_three_way.sh runs all three engines one after another so that only one server is busy at a time.
Reads while the index is changing
Benchmarks such as ann-benchmarks build the index once and then query a corpus that never changes. Production tables change all the time. rw_bench.py covers that case: it runs readers alone, adds writers for a window in the middle, and then runs readers alone again. This run used 4 readers and 4 writers against the same 20,000-vector index:
total reads=47700  writes=2000 rows  write_rate=432 rows/s over 4.6s
read-only mean=4564 qps  read+write mean=2738 qps  impact=40% drop
read latency (us)   p50      p95      p99      max      n
  read-only             797     1399     1805    87018    32038
  read+write           1440     1803     1908     8991    10988

The per-second table in the full output shows reads dropping when the writers start at second 4 and recovering once they stop at second 8. For an index that lives inside a transactional engine, this is one of the numbers we care about most, because it shows what a busy table costs the queries that read it.
When to use it, and when not to
Use vector-dev-bench when you're changing an index and want to know within minutes whether recall, build time, or read throughput moved. Use it as a recall check in a script, since recall_bench.py exits with an error below its threshold. Use it to try an idea against MariaDB or pgvector on the same machine (for pgvector, recall and build time only) before you invest in a full run.
Use a full framework such as vector-bench when the number is going to leave the room. That includes anything published, anything that compares absolute latency, and anything that depends on filtered search or recall after deletes. Latency under concurrency counts as absolute latency. A comparison with pgvector needs the full framework most of all, because pgvector is the engine whose client differs. Every number from vector-dev-bench passes through a client, so absolute latency includes the client and the round trip. Synthetic vectors give a trustworthy relative signal on the machine that ran them, but they don't predict recall on real embeddings.
Add your engine
The repository is Apache-2.0 licensed, and an AGENTS.md file walks a coding agent through running it. If you're building a vector index for MySQL, or for anything that speaks the MySQL or PostgreSQL protocol, add a profile and a start script and send a pull request. If you think our method is unfair to your engine, open an issue and tell us why. Getting vector search on MySQL to the point where teams can pick it with confidence is work for everyone in the MySQL community, and it starts with numbers we can all reproduce.
You can get VillageSQL from the website, read how custom indexes work in the custom indexes documentation, and see our Percona Live demo in Vector search in MySQL: an early look at HNSW and custom indexes in VillageSQL.</description>
    <content:encoded><![CDATA[<img src="https://storage.ghost.io/c/db/c7/dbc78ca4-dcfe-468b-b2fa-030396d2a98e/content/images/2026/10/IMG_0536.JPG" alt="Benchmarking vector indexes in MySQL with vector-dev-bench"><p>PostgreSQL has a rapidly evolving vector ecosystem. MySQL is earlier on that road, but the community is hard at work catching up. MariaDB ships its own HNSW <a href="https://mariadb.com/docs/server/reference/sql-structure/vectors/vector-overview">index</a>, Google engineers have <a href="https://github.com/mysql/mysql-community/issues/3#issuecomment-5642853937">shared a ScaNN-based vector index</a> on MySQL's own vector index proposal, <a href="https://www.percona.com/blog/vector-distance-vector-similarity-search-in-percona-server-for-mysql/">Percona is building</a> an HNSW index inside InnoDB, and VillageSQL is <a href="https://villagesql.com/blog/vector-search-hnsw/">building vector search</a> as an extension with <a href="https://github.com/villagesql/vsql-vector"><code>vsql-vector</code></a>.</p>
<p>We want vector search on MySQL to be something that teams can choose with confidence (and not have to switch to PostgreSQL to get). Getting there takes more than one good implementation. The teams building these indexes need to measure them the same way, check each other's numbers, and fix what the numbers show.</p>
<p>Working with Percona, we published <a href="https://github.com/villagesql/vector-dev-bench">vector-dev-bench</a>. It runs on a laptop, it drives VillageSQL, MariaDB, PostgreSQL with pgvector, and MySQL built from Google's ScaNN pull request, and it gives an answer in minutes.</p>
<h2>Doing things in the open</h2>
<p>A benchmark number is only as good as the method behind it. A public method lets anyone rerun numbers, read the exact SQL each engine received, and tell us where the method may be unfair.</p>
<p>It also makes collaboration practical. When another team can run the same harness against their own build, we can compare results without a bunch of upfront coordination. With vector-dev-bench, an engine is one entry in a Python dictionary plus a start script, so adding a new one is a small change.</p>
<p>We've seen this work already. Percona's <a href="https://github.com/EvgeniyPatlan/vector-bench">vector-bench</a> is a full comparison framework, and we added VillageSQL to our copy of it. Along the way, we noticed that pgvector loaded its rows with COPY while the MySQL engines used INSERT, so the build times were measuring two different kinds of statements. We switched pgvector's incremental build to batched INSERTs so that every engine loads rows the same way. vector-bench adopted the change, and its code credits our commit. That is the kind of exchange we want more of.</p>
<h2>Two tools for two jobs</h2>
<p>vector-bench and vector-dev-bench answer different questions, and we use both.</p>
<p>vector-bench is built to produce a comparison that you can publish. It runs each engine in Docker, built from pinned sources where it compiles them, runs real datasets such as SIFT, GloVe, and GIST, and measures recall against throughput, build cost, concurrency from 1 to 32 clients, filtered search, and recall after deletes and reinserts. It runs every engine twice, once with identical resources and once with each vendor's recommended settings, and it records the hardware and versions with every result. Its <code>main</code> profile takes about two days, which is the right cost for a number that other people will cite.</p>
<p>vector-dev-bench is built for the quick, iterative loop in which you change the index, rebuild it, and want to know whether recall moved. It runs against engines you've already built, does not use containers, and uses synthetic data by default. It builds a 20,000-vector index in about 7 seconds. Its <a href="https://github.com/villagesql/vector-dev-bench">README</a> is plain about the trade-off: the numbers are for relative comparisons, and a published comparison belongs in a framework like vector-bench.</p>
<h2>vector-dev-bench Measurement</h2>
<p>vector-dev-bench is three command-line tools over one shared module:</p>
<table>
<thead>
<tr>
<th>Tool</th>
<th>What it measures</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>recall_bench.py</code></td>
<td>It measures recall@k and queries per second across a sweep of <code>ef_search</code> values. It exits with an error when recall falls below a threshold (0.95 by default), so a script can use it as a pass/fail check.</td>
</tr>
<tr>
<td><code>build_bench.py</code></td>
<td>It measures index build time. <code>--build-threads N</code> builds the index from N client connections at once.</td>
</tr>
<tr>
<td><code>rw_bench.py</code></td>
<td>It measures reads during writes. Reader threads run nearest-neighbor queries while writer threads insert into the same index, and the tool reports read throughput second by second along with p50, p95, and p99 latency.</td>
</tr>
</tbody>
</table>
<p>Recall is the measure that matters here, because an approximate nearest-neighbor index returns approximately the right answer by design. A test that compares exact output, the way MySQL's own test framework does, fails on a correct index. vector-dev-bench computes the true nearest neighbors itself with NumPy and reports what fraction of them each engine returned. You can also pass <code>--dataset</code> to use a standard ann-benchmarks dataset with its published ground truth.</p>
<p>The engines are profiles:</p>
<table>
<thead>
<tr>
<th>Profile</th>
<th>Engine</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>vsql_vector</code></td>
<td>VillageSQL with <code>vsql-vector</code> (<code>SVECTOR</code> column, HNSW index)</td>
</tr>
<tr>
<td><code>mariadb</code></td>
<td>MariaDB 11.7 or later (MHNSW)</td>
</tr>
<tr>
<td><code>pgvector</code></td>
<td>PostgreSQL 15 or later with pgvector</td>
</tr>
<tr>
<td><code>google</code></td>
<td>MySQL 9.7.1 built from Google's <a href="https://github.com/mysql/mysql-server/commit/51e4510c44b1e039321ff6e91ab56d62a0d50aac">vector search commit</a> (ScaNN index), which its authors describe as an experimental proof of concept</td>
</tr>
<tr>
<td><code>varchar_ctrl</code></td>
<td>A plain <code>VARCHAR</code> column, as a control that separates a server problem from a harness problem</td>
</tr>
</tbody>
</table>
<p>VillageSQL, MariaDB, and the Google build all speak the MySQL protocol, and the harness drives all three through the same PyMySQL connection code. The client cost is the same for each engine, but it isn't zero. A gap between two of these engines holds for the machine that measured it, and it can change on different hardware, so rerun it on the hardware you care about before you trust it.</p>
<p>pgvector takes a different path. The harness sends each batch of pgvector inserts or queries through the <code>psql</code> command-line client, so the timed window also includes starting <code>psql</code> and opening its connection. The parallel options (<code>--readers</code> and <code>--build-threads</code>) fall back to one serial client, and <code>rw_bench.py</code> does not run against pgvector. A VillageSQL-versus-pgvector number from vector-dev-bench measures the two clients as well as the two engines. Treat it as a hint about where to look, and don't quote it as a verdict on either engine.</p>
<h2>What a run looks like</h2>
<p>We ran a recall sweep against VillageSQL 0.0.7 with the <code>vsql-vector-pk</code> build of the extension (whose HNSW index stores each row's primary key), on 20,000 random 32-dimension vectors. The tool builds the index once and then queries it at four <code>ef_search</code> values. The <a href="https://github.com/villagesql/vector-dev-bench">README</a> and <a href="https://github.com/villagesql/vector-dev-bench/blob/main/RUNBOOK.md">RUNBOOK</a> cover building each engine and running the tools.</p>
<p>Here is the output from an Apple M5 laptop with 32 GB of memory:</p>
<pre><code>build_time_s=7.03  (index built once; sweeping ef_search)
ef_search  recall@10    qps
50         0.7870       8829.3
100        0.9035       5903.0
200        0.9645       3526.0
400        0.9875       2044.4
</code></pre>
<p>This is the curve every HNSW index trades along. A wider search finds more of the true neighbors and answers fewer queries per second. The shape is also a check on the run itself: recall that stays at exactly 1.0 when <code>ef_search</code> drops usually means the optimizer chose a full table scan and the index wasn't used.</p>
<p>MariaDB and PostgreSQL run the same sweep through their own profiles, and <code>run_three_way.sh</code> runs all three engines one after another so that only one server is busy at a time.</p>
<h2>Reads while the index is changing</h2>
<p>Benchmarks such as ann-benchmarks build the index once and then query a corpus that never changes. Production tables change all the time. <code>rw_bench.py</code> covers that case: it runs readers alone, adds writers for a window in the middle, and then runs readers alone again. This run used 4 readers and 4 writers against the same 20,000-vector index:</p>
<pre><code>total reads=47700  writes=2000 rows  write_rate=432 rows/s over 4.6s
read-only mean=4564 qps  read+write mean=2738 qps  impact=40% drop
read latency (us)   p50      p95      p99      max      n
  read-only             797     1399     1805    87018    32038
  read+write           1440     1803     1908     8991    10988
</code></pre>
<p>The per-second table in the full output shows reads dropping when the writers start at second 4 and recovering once they stop at second 8. For an index that lives inside a transactional engine, this is one of the numbers we care about most, because it shows what a busy table costs the queries that read it.</p>
<h2>When to use it, and when not to</h2>
<p>Use vector-dev-bench when you're changing an index and want to know within minutes whether recall, build time, or read throughput moved. Use it as a recall check in a script, since <code>recall_bench.py</code> exits with an error below its threshold. Use it to try an idea against MariaDB or pgvector on the same machine (for pgvector, recall and build time only) before you invest in a full run.</p>
<p>Use a full framework such as vector-bench when the number is going to leave the room. That includes anything published, anything that compares absolute latency, and anything that depends on filtered search or recall after deletes. Latency under concurrency counts as absolute latency. A comparison with pgvector needs the full framework most of all, because pgvector is the engine whose client differs. Every number from vector-dev-bench passes through a client, so absolute latency includes the client and the round trip. Synthetic vectors give a trustworthy relative signal on the machine that ran them, but they don't predict recall on real embeddings.</p>
<h2>Add your engine</h2>
<p>The repository is Apache-2.0 licensed, and an <code>AGENTS.md</code> file walks a coding agent through running it. If you're building a vector index for MySQL, or for anything that speaks the MySQL or PostgreSQL protocol, add a profile and a start script and send a pull request. If you think our method is unfair to your engine, open an issue and tell us why. Getting vector search on MySQL to the point where teams can pick it with confidence is work for everyone in the MySQL community, and it starts with numbers we can all reproduce.</p>
<p>You can get <a href="https://villagesql.com/">VillageSQL</a> from the website, read how custom indexes work in the <a href="https://villagesql.com/docs/mysql-8.4/stable/custom-indexes">custom indexes documentation</a>, and see our Percona Live demo in <a href="https://villagesql.com/blog/vector-search-hnsw/">Vector search in MySQL: an early look at HNSW and custom indexes in VillageSQL</a>.</p>]]></content:encoded>
    <pubDate>Thu, 22 Oct 2026 14:00:00 +0000</pubDate>
    <dc:creator>VillageSQL</dc:creator>
    <category>AI</category>
    <category>MySQL</category>
    <category>Extension</category>
    <category>Open Source</category>
    <category>VillageSQL</category>
  </item>

  <item>
    <title>Village News: MySQL News + Events (21 October 2026)</title>
    <guid isPermaLink="false">6ad915d56a70ab0001bf842a</guid>
    <link>https://villagesql.com/blog/village-news-mysql-news-events-21-october-2026/</link>
    <description>Welcome back to Village News, our curated roundup of MySQL and database news. This issue covers two weeks, with MySQL 26.10 landing at the end of them.
If you want to get these updates, just subscribe to the blog.
Enjoy!
MySQL News
Note: Aggregated MySQL news can be found at Planet for MySQL Community and Planet MySQL (Oracle curated)
MySQL 26.10 Is Here: New Capabilities for Developers and DBAs
Oracle MySQL Blog
TL;DR - MySQL 26.10 brings MySQL REST Service into the server as a component, so you no longer need MySQL Router to serve its HTTPS endpoints.
Four Attempts and One RESET MASTER — Migrating a Terabyte of Production MySQL to Kubernetes (MOCO)
Eugene Dimov, OurSQL Foundation Blog
TL;DR - Ivinco rolled back three cutovers to MOCO, caused by a hardcoded VIP and Ceph write latency. After the move, a passwordless root account from the donor made the operator run RESET MASTER and wipe a replica's GTID history.
MySQL vs PostgreSQL Internals (Part 3) – Row Locking
Zhao Song
TL;DR - Traces one SELECT … ORDER BY … FOR UPDATE through both engines. InnoDB locks rows as it scans and keeps the locks in memory. Postgres sorts a snapshot first, then locks and rechecks each tuple, recording the lock in the tuple header.
Taming MySQL 8.4 Background Histogram Lock Wait Timeouts on Amazon RDS
Umesh Shastry, MySQL :: under_the_hood
TL;DR - RDS's monitoring agent holds a near-constant write lock on mysql.rds_heartbeat2, so 8.4's background histogram collection keeps timing out on it. The fix is to drop the histograms on that one table, not to change global settings.
How fast is vector search inside MySQL?
Alkin Tezuysal, AskDBA
TL;DR - Benchmarks the MyVector HNSW plugin on three MySQL versions against MariaDB's native vector type. lefred's reply argues that MariaDB comes out ahead once you compare at matching recall.
Announcing VillageSQL Server 0.0.7
VillageSQL Blog
TL;DR - Builds now track MySQL 8.4.11, 9.7.2 and Percona Server 8.4.11-11. Extensions can now define custom indexes, including nearest-neighbor search, which lays the groundwork for vector search.
“MySQL Is Still the Backbone”: Zongzhi Chen on Hot Rows, PolarDB, and the Agent Era
OurSQL Foundation Blog
TL;DR - An interview with a longtime MySQL contributor and early PolarDB engineer about why China runs on MySQL, what Singles Day does to hot rows, and where MySQL goes next.
MySQL Online DDL &quot;Duplicate Key&quot; Optimization
Libing Song
TL;DR - Since 5.6, an online DDL can fail with a false Duplicate Entry error when concurrent DML fails on a unique key and leaves entries in the row log. This explains the cause and the fix shipped in AliSQL.
InnoDB for the fast. DuckDB for the heavy. Same connection for both.
Daniel Guzmán Burgos, dbtrail Blog
TL;DR - dbtrail (Apache 2.0) reads the binlog into Parquet copies and routes each SELECT to InnoDB or DuckDB. 13 of 14 reports ran about 10x faster, while point lookups got slower. A companion post uses it for row-level recovery.
How a Routine Linux fstrim Job Took Down MySQL: An InnoDB Crash Analysis
Vinoth Kanna, Mydbops Blog
TL;DR - A weekly fstrim sent about 5.2 TiB of discards to thick-provisioned disks and stalled I/O until InnoDB's 600-second semaphore watchdog killed mysqld. The fix is to disable fstrim.timer on such storage.
MySQL Workbench 26.7
Dave Stokes, Dave's MySQL Stuff
TL;DR - The new Workbench is a rewrite, not an update of the version retired in January. It lacks relationship maps and reverse engineering, and servers 8.0 and older are no longer supported.
PGO or not PGO this is the dilemma. Step 1
Marco Tusa, Percona Blog
TL;DR - A three-part look at profile-guided builds of MySQL 9.7.2. Steps 2 and 3 show why a build trained only on sysbench loses at high concurrency and recommend mixing MTR runs with moderate-concurrency training.
Database News
Neki update: $50 Metal, read-only replicas, Azure, and more
Nick Van Wiggeren, PlanetScale Blog
TL;DR - A month into preview, PlanetScale's sharded Postgres has added read-only replicas, Metal instances from $50/month, Azure regions, and Terraform and CLI controls.
Make pg_stat_monitor stable again!
Percona Community Blog
TL;DR - pg_stat_monitor 2.4.0 fixes memory leaks that could end in an OOM kill, along with buffer overflows, a use-after-free, a lock-ordering bug and incorrect statistics.
CedarDB Cloud Is Now Generally Available
CedarDB Blog
TL;DR - CedarDB, a Postgres-compatible database for both transactional and analytical workloads, is now available as a fully managed service.
What gets TOASTed in Postgres
Ahmed Darwich, PlanetScale Blog
TL;DR - Postgres 19 makes LZ4 the default TOAST compression. LZ4 is usually the right choice, but pglz still compresses repetitive JSON smaller and reads large-value previews faster.
In MariaDB 13.0, new-style optimizer hints go beyond MySQL’s
Sergei Petrunia
TL;DR - MariaDB 13.0 is stable, and its optimizer hints now go beyond MySQL's syntax, starting with path-based query-block addressing in QB_NAME.
Load terabytes of data in minutes into Lakebase Postgres
Databricks Blog
TL;DR - Spark builds valid Postgres heap and index pages in parallel and writes them to storage, then the primary publishes them with one compact WAL record. Databricks reports loading 1 TB in under 5 minutes.
Upcoming Database Events
PGConf.EU
October 20–23, 2026 (PostgreSQL Europe)
Valencia, Spain
MySQL Public Discussion #6
October 22, 2026, 17:00–18:00 UTC
Online
Oracle AI World
October 25–28, 2026
Las Vegas, NV
PG Down Under 2026
October 30, 2026
Surry Hills
Sydney, Australia
MySQL Contributor Summit
November 4-5, 2026
Virtual
KubeCon + CloudNativeCon North America (VillageSQL is a sponsor)
November 9-12, 2026
Salt Lake City, Utah
PASS Data Community Summit
November 9-11, 2026
Hyatt Regency Seattle
Seattle, WA
DuckDB × MySQL Day
November 17, 2026
Online
PGConf.Asia 2026
November 17-18, 2026
Hong Kong
PGConf.PL 2026
November 24, 2026
ARCHE Dwór Uphagena
Gdańsk, Poland
AWS re:Invent 2026
November 30 – December 4, 2026
Las Vegas, NV
Open Source Summit Japan
December 7-9, 2026
Tokyo, Japan
CIDR 2027
January 24-27, 2027
Mövenpick Hotel Amsterdam City Centre
Amsterdam, The Netherlands
MySQL Belgian Days 2027
January 28-29, 2027
ICAB Incubator
Brussels, Belgium
FOSDEM PGDay 2027
January 28-29, 2027
Brussels Marriott Hotel Grand Place
Brussels, Belgium
FOSDEM 2027
January 30-31, 2027
ULB Solbosch Campus
Brussels, Belgium
CERN PGDay 2027
February 12, 2027
CERN Council Chamber
Geneva, Switzerland
PGConf India 2027
March 2-5, 2027
Sheraton Grand Hotel at Brigade Gateway
Bengaluru, India
FOSSASIA PGDay 2027
March 9, 2027
Bangkok, Thailand
KubeCon + CloudNativeCon Europe 2027
March 15-18, 2027
Barcelona, Spain
Nordic PGDay 2027
March 16, 2027
Courtyard Kungsholmen
Stockholm, Sweden
SCaLE 24x
April 1-4, 2027
Pasadena Convention Center
Pasadena, CA
PgDay Boston 2027
April 10, 2027
Tufts University Joyce Cummings Center
Boston, MA
PGConf.dev 2027
May 11-14, 2027
Plaza Centre-Ville
Montréal, QC, Canada</description>
    <content:encoded><![CDATA[<img src="https://storage.ghost.io/c/db/c7/dbc78ca4-dcfe-468b-b2fa-030396d2a98e/content/images/2026/10/village-news-banner-1.jpg" alt="Village News: MySQL News + Events (21 October 2026)"><p>Welcome back to Village News, our curated roundup of MySQL and database news. This issue covers two weeks, with MySQL 26.10 landing at the end of them.</p>
<p>If you want to get these updates, just <a href="https://villagesql.com/blog/blog#/portal/signup">subscribe</a> to the blog.</p>
<p>Enjoy!</p>
<h2>MySQL News</h2>
<p><em>Note: Aggregated MySQL news can be found at <a href="https://planet.oursqlcommunity.org/?source=villagesql">Planet for MySQL Community</a> and <a href="https://planet.mysql.com/">Planet MySQL</a> (Oracle curated)</em></p>
<p><strong><a href="https://blogs.oracle.com/mysql/mysql-26-10-is-here">MySQL 26.10 Is Here: New Capabilities for Developers and DBAs</a></strong><br>
<em>Oracle MySQL Blog</em></p>
<p><strong>TL;DR -</strong> MySQL 26.10 brings MySQL REST Service into the server as a component, so you no longer need MySQL Router to serve its HTTPS endpoints.</p>
<p><strong><a href="https://oursqlfoundation.org/blog/four-attempts-one-reset-master/">Four Attempts and One RESET MASTER — Migrating a Terabyte of Production MySQL to Kubernetes (MOCO)</a></strong><br>
<em>Eugene Dimov, OurSQL Foundation Blog</em></p>
<p><strong>TL;DR -</strong> Ivinco rolled back three cutovers to MOCO, caused by a hardcoded VIP and Ceph write latency. After the move, a passwordless root account from the donor made the operator run RESET MASTER and wipe a replica's GTID history.</p>
<p><strong><a href="https://kernelmaker.github.io/mysql-vs-pg-rowlocking">MySQL vs PostgreSQL Internals (Part 3) – Row Locking</a></strong><br>
<em>Zhao Song</em></p>
<p><strong>TL;DR -</strong> Traces one <code>SELECT … ORDER BY … FOR UPDATE</code> through both engines. InnoDB locks rows as it scans and keeps the locks in memory. Postgres sorts a snapshot first, then locks and rechecks each tuple, recording the lock in the tuple header.</p>
<p><strong><a href="https://ushastry.blogspot.com/2026/10/taming-mysql-84-background-histogram.html">Taming MySQL 8.4 Background Histogram Lock Wait Timeouts on Amazon RDS</a></strong><br>
<em>Umesh Shastry, MySQL :: under_the_hood</em></p>
<p><strong>TL;DR -</strong> RDS's monitoring agent holds a near-constant write lock on <code>mysql.rds_heartbeat2</code>, so 8.4's background histogram collection keeps timing out on it. The fix is to drop the histograms on that one table, not to change global settings.</p>
<p><strong><a href="https://askdba.net/how-fast-is-vector-search-inside-mysql/">How fast is vector search inside MySQL?</a></strong><br>
<em>Alkin Tezuysal, AskDBA</em></p>
<p><strong>TL;DR -</strong> Benchmarks the MyVector HNSW plugin on three MySQL versions against MariaDB's native vector type. lefred's <a href="https://lefred.be/content/vector-search-in-mysql-vs-mariadb-look-beyond-qps/">reply</a> argues that MariaDB comes out ahead once you compare at matching recall.</p>
<p><strong><a href="https://villagesql.com/blog/007/">Announcing VillageSQL Server 0.0.7</a></strong><br>
<em>VillageSQL Blog</em></p>
<p><strong>TL;DR -</strong> Builds now track MySQL 8.4.11, 9.7.2 and Percona Server 8.4.11-11. Extensions can now define custom indexes, including nearest-neighbor search, which lays the groundwork for vector search.</p>
<p><strong><a href="https://oursqlfoundation.org/blog/zongzhi-chen-interview/">“MySQL Is Still the Backbone”: Zongzhi Chen on Hot Rows, PolarDB, and the Agent Era</a></strong><br>
<em>OurSQL Foundation Blog</em></p>
<p><strong>TL;DR -</strong> An interview with a longtime MySQL contributor and early PolarDB engineer about why China runs on MySQL, what Singles Day does to hot rows, and where MySQL goes next.</p>
<p><strong><a href="https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem-en/">MySQL Online DDL "Duplicate Key" Optimization</a></strong><br>
<em>Libing Song</em></p>
<p><strong>TL;DR -</strong> Since 5.6, an online DDL can fail with a false Duplicate Entry error when concurrent DML fails on a unique key and leaves entries in the row log. This explains the cause and the fix shipped in AliSQL.</p>
<p><strong><a href="https://blog.dbtrail.com/innodb-and-duckdb-same-connection/">InnoDB for the fast. DuckDB for the heavy. Same connection for both.</a></strong><br>
<em>Daniel Guzmán Burgos, dbtrail Blog</em></p>
<p><strong>TL;DR -</strong> dbtrail (Apache 2.0) reads the binlog into Parquet copies and routes each SELECT to InnoDB or DuckDB. 13 of 14 reports ran about 10x faster, while point lookups got slower. A <a href="https://oursqlfoundation.org/blog/time-travel-for-mysql/">companion post</a> uses it for row-level recovery.</p>
<p><strong><a href="https://www.mydbops.com/blog/how-linux-fstrim-caused-mysql-innodb-crash">How a Routine Linux fstrim Job Took Down MySQL: An InnoDB Crash Analysis</a></strong><br>
<em>Vinoth Kanna, Mydbops Blog</em></p>
<p><strong>TL;DR -</strong> A weekly fstrim sent about 5.2 TiB of discards to thick-provisioned disks and stalled I/O until InnoDB's 600-second semaphore watchdog killed mysqld. The fix is to disable <code>fstrim.timer</code> on such storage.</p>
<p><strong><a href="https://davesmysqlstuff.blogspot.com/2026/10/mysql-workbench-267.html">MySQL Workbench 26.7</a></strong><br>
<em>Dave Stokes, Dave's MySQL Stuff</em></p>
<p><strong>TL;DR -</strong> The new Workbench is a rewrite, not an update of the version retired in January. It lacks relationship maps and reverse engineering, and servers 8.0 and older are no longer supported.</p>
<p><strong><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1/">PGO or not PGO this is the dilemma. Step 1</a></strong><br>
<em>Marco Tusa, Percona Blog</em></p>
<p><strong>TL;DR -</strong> A three-part look at profile-guided builds of MySQL 9.7.2. Steps 2 and 3 show why a build trained only on sysbench loses at high concurrency and recommend mixing MTR runs with moderate-concurrency training.</p>
<h2>Database News</h2>
<p><strong><a href="https://planetscale.com/blog/neki-updates-and-preparing-for-production">Neki update: $50 Metal, read-only replicas, Azure, and more</a></strong><br>
<em>Nick Van Wiggeren, PlanetScale Blog</em></p>
<p><strong>TL;DR -</strong> A month into preview, PlanetScale's sharded Postgres has added read-only replicas, Metal instances from $50/month, Azure regions, and Terraform and CLI controls.</p>
<p><strong><a href="https://percona.community/blog/2026/10/19/pg-stat-monitor-2-4-0/">Make pg_stat_monitor stable again!</a></strong><br>
<em>Percona Community Blog</em></p>
<p><strong>TL;DR -</strong> pg_stat_monitor 2.4.0 fixes memory leaks that could end in an OOM kill, along with buffer overflows, a use-after-free, a lock-ordering bug and incorrect statistics.</p>
<p><strong><a href="https://cedardb.com/blog/cedardb_cloud/">CedarDB Cloud Is Now Generally Available</a></strong><br>
<em>CedarDB Blog</em></p>
<p><strong>TL;DR -</strong> CedarDB, a Postgres-compatible database for both transactional and analytical workloads, is now available as a fully managed service.</p>
<p><strong><a href="https://planetscale.com/blog/what-gets-toasted-in-postgres">What gets TOASTed in Postgres</a></strong><br>
<em>Ahmed Darwich, PlanetScale Blog</em></p>
<p><strong>TL;DR -</strong> Postgres 19 makes LZ4 the default TOAST compression. LZ4 is usually the right choice, but pglz still compresses repetitive JSON smaller and reads large-value previews faster.</p>
<p><strong><a href="https://petrunia.net/2026/10/10/in-mariadb-13-0-new-style-optimizer-hints-go-beyond-mysqls/">In MariaDB 13.0, new-style optimizer hints go beyond MySQL’s</a></strong><br>
<em>Sergei Petrunia</em></p>
<p><strong>TL;DR -</strong> MariaDB 13.0 is stable, and its optimizer hints now go beyond MySQL's syntax, starting with path-based query-block addressing in <code>QB_NAME</code>.</p>
<p><strong><a href="https://www.databricks.com/blog/load-terabytes-data-minutes-lakebase-postgres">Load terabytes of data in minutes into Lakebase Postgres</a></strong><br>
<em>Databricks Blog</em></p>
<p><strong>TL;DR -</strong> Spark builds valid Postgres heap and index pages in parallel and writes them to storage, then the primary publishes them with one compact WAL record. Databricks reports loading 1 TB in under 5 minutes.</p>
<h2>Upcoming Database Events</h2>
<p><strong><a href="http://pgconf.eu/">PGConf.EU</a></strong><br>
October 20–23, 2026 (PostgreSQL Europe)<br>
Valencia, Spain</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Public Discussion #6</a></strong><br>
October 22, 2026, 17:00–18:00 UTC<br>
Online</p>
<p><strong><a href="https://www.oracle.com/ai-world/">Oracle AI World</a></strong><br>
October 25–28, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://conf.pgdu.org/conferences/2026/">PG Down Under 2026</a></strong><br>
October 30, 2026<br>
Surry Hills<br>
Sydney, Australia</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Contributor Summit</a></strong><br>
November 4-5, 2026<br>
Virtual</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-north-america/">KubeCon + CloudNativeCon North America</a></strong> (<strong>VillageSQL is a sponsor</strong>)<br>
November 9-12, 2026<br>
Salt Lake City, Utah</p>
<p><strong><a href="https://passdatacommunitysummit.com/">PASS Data Community Summit</a></strong><br>
November 9-11, 2026<br>
Hyatt Regency Seattle<br>
Seattle, WA</p>
<p><strong><a href="https://oursqlfoundation.org/events/duckdb-mysql-day/">DuckDB × MySQL Day</a></strong><br>
November 17, 2026<br>
Online</p>
<p><strong><a href="https://www.pgconf.asia/EN/2026/">PGConf.Asia 2026</a></strong><br>
November 17-18, 2026<br>
Hong Kong</p>
<p><strong><a href="https://2026.pgconf.pl/">PGConf.PL 2026</a></strong><br>
November 24, 2026<br>
ARCHE Dwór Uphagena<br>
Gdańsk, Poland</p>
<p><strong><a href="https://aws.amazon.com/events/reinvent">AWS re:Invent 2026</a></strong><br>
November 30 – December 4, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://events.linuxfoundation.org/open-source-summit-japan/">Open Source Summit Japan</a></strong><br>
December 7-9, 2026<br>
Tokyo, Japan</p>
<p><strong><a href="https://www.cidrdb.org/cidr2027/">CIDR 2027</a></strong><br>
January 24-27, 2027<br>
Mövenpick Hotel Amsterdam City Centre<br>
Amsterdam, The Netherlands</p>
<p><strong><a href="https://sessionize.com/mysql-belgian-days-2027/">MySQL Belgian Days 2027</a></strong><br>
January 28-29, 2027<br>
ICAB Incubator<br>
Brussels, Belgium</p>
<p><strong><a href="https://2027.fosdempgday.org/">FOSDEM PGDay 2027</a></strong><br>
January 28-29, 2027<br>
Brussels Marriott Hotel Grand Place<br>
Brussels, Belgium</p>
<p><strong><a href="https://fosdem.org/2027/">FOSDEM 2027</a></strong><br>
January 30-31, 2027<br>
ULB Solbosch Campus<br>
Brussels, Belgium</p>
<p><strong><a href="https://indico.cern.ch/event/1720940/">CERN PGDay 2027</a></strong><br>
February 12, 2027<br>
CERN Council Chamber<br>
Geneva, Switzerland</p>
<p><strong><a href="https://pgconf.in/">PGConf India 2027</a></strong><br>
March 2-5, 2027<br>
Sheraton Grand Hotel at Brigade Gateway<br>
Bengaluru, India</p>
<p><strong><a href="https://www.postgresql.org/about/event/fossasia-pgday-2027-returns-to-thailand-on-march-9-cfp-open-2611/">FOSSASIA PGDay 2027</a></strong><br>
March 9, 2027<br>
Bangkok, Thailand</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/">KubeCon + CloudNativeCon Europe 2027</a></strong><br>
March 15-18, 2027<br>
Barcelona, Spain</p>
<p><strong><a href="https://2027.nordicpgday.org/">Nordic PGDay 2027</a></strong><br>
March 16, 2027<br>
Courtyard Kungsholmen<br>
Stockholm, Sweden</p>
<p><strong><a href="https://www.socallinuxexpo.org/scale/24x">SCaLE 24x</a></strong><br>
April 1-4, 2027<br>
Pasadena Convention Center<br>
Pasadena, CA</p>
<p><strong><a href="https://2027.pgdayboston.org/">PgDay Boston 2027</a></strong><br>
April 10, 2027<br>
Tufts University Joyce Cummings Center<br>
Boston, MA</p>
<p><strong><a href="https://2027.pgconf.dev/">PGConf.dev 2027</a></strong><br>
May 11-14, 2027<br>
Plaza Centre-Ville<br>
Montréal, QC, Canada</p>]]></content:encoded>
    <pubDate>Wed, 21 Oct 2026 19:52:23 +0000</pubDate>
    <dc:creator>VillageSQL</dc:creator>
  </item>

  <item>
    <title>MySQL October 2026 GA Releases Now Available</title>
    <guid isPermaLink="false">4d572f7104ea86e92da7ac3cfbf6b13a</guid>
    <link>https://blogs.oracle.com/mysql/mysql-october-2026-ga-releases-now-available</link>
    <description>The October 2026 MySQL releases are now available, including: MySQL 26.10 brings additional capabilities to Community Edition, new options for building REST applications, and improvements to database operations and developer tools. MySQL 9.7.4 and MySQL 8.4.13 continue maintenance updates for the supported LTS release lines. October MySQL Server releases MySQL 26.10 Innovation delivers new functionality […]</description>
    <pubDate>Wed, 21 Oct 2026 19:10:48 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>MySQL Enterprise</category>
    <category>News</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>Introducing the MySQL REST Service Component: Now Generally Available</title>
    <guid isPermaLink="false">7cd64365aa2c9e72a381c1cead93efac</guid>
    <link>https://blogs.oracle.com/mysql/introducing-the-mysql-rest-service-component-now-generally-available</link>
    <description>With MySQL 26.10, we’re announcing the general availability of the MySQL REST Service (MRS) Component, an installable component that runs MRS directly inside MySQL Server. MRS: the MySQL REST Service Many modern applications, well, not just applications, but also tools, CLIs, CI pipelines, and automation, use HTTP and JSON to communicate with other systems. For […]</description>
    <pubDate>Wed, 21 Oct 2026 10:02:39 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>News</category>
    <category>components</category>
    <category>JSON</category>
    <category>MySQL 26.10</category>
    <category>MySQL REST Service</category>
    <category>MySQL Server</category>
    <category>REST API</category>
  </item>

  <item>
    <title>MySQL 26.10 Is Here: New Capabilities for Developers and DBAs</title>
    <guid isPermaLink="false">a415ef7d26b753ed46b5277a5a69ad3b</guid>
    <link>https://blogs.oracle.com/mysql/mysql-26-10-is-here</link>
    <description>MySQL 26.10 is here! This release brings MySQL REST Service into the server, expands Community Edition capabilities, and adds more control for applications using JSON Relational Duality Views. Here’s a look at the highlights. MySQL REST Service joins the server MySQL REST Service (MRS) is now available as a MySQL Server component, enabling HTTPS endpoints […]</description>
    <pubDate>Tue, 20 Oct 2026 22:31:29 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>MySQL 26.10</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>MySQL JSON Duality Views: Now with granular write and ETAG controls</title>
    <guid isPermaLink="false">1ea30820382a65862e65841b77121977</guid>
    <link>https://blogs.oracle.com/mysql/mysql-json-duality-views-now-with-granular-write-and-etag-controls</link>
    <description>JSON Duality Views (JDVs) let applications read and write JSON documents backed by relational tables (see our introductory JDV blog). MySQL handles the document-to-table mapping, reducing application code while preserving relational integrity. We still need to decide which parts of a document applications can change. Earlier JDVs offered limited choices: table writes were generally all-or-nothing, and […]</description>
    <pubDate>Tue, 20 Oct 2026 12:59:00 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Basics/How-To</category>
    <category>MySQL Community</category>
    <category>JSON Duality</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
    <category>OCI</category>
  </item>

  <item>
    <title>Troubleshooting MySQL with OpenTelemetry Metrics</title>
    <guid isPermaLink="false">ed5189806daa7bd6f1b67b0863c877a2</guid>
    <link>https://blogs.oracle.com/mysql/troubleshooting-mysql-with-opentelemetry-metrics</link>
    <description>In the first post, we exported native MySQL telemetry to Prometheus. In the second, we organized those metrics around DBA questions. In the third, we built a Grafana dashboard that makes those questions visible. This final post shows how to use that dashboard when an application slows down or a database signal changes. The goal […]</description>
    <pubDate>Mon, 19 Oct 2026 23:48:23 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>Monitoring</category>
    <category>MySQL</category>
    <category>MySQL Enterprise</category>
  </item>

  <item>
    <title>Upgrading MySQL HeatWave High Availability from MySQL 8.0.46 to MySQL 8.4 LTS</title>
    <guid isPermaLink="false">277c5ccffa8a3fb074d561b46825ee5c</guid>
    <link>https://blogs.oracle.com/mysql/upgrading-mysql-heatwave-high-availability-from-mysql-8-0-46-to-mysql-8-4-lts</link>
    <description>Planning, compatibility testing, performance validation, application readiness, and rollback strategies for a large production deployment MySQL 8.0 reached its official End of Life (EOL) in April 2026 alongside the final release of version 8.0.46, but specific support extensions are available depending on your hosting environment.  Oracle extended support for MySQL 8.0 instances in MySQL HeatWave […]</description>
    <pubDate>Mon, 19 Oct 2026 19:50:31 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Basics/How-To</category>
    <category>MySQL HeatWave</category>
    <category>mysql</category>
    <category>OCI</category>
  </item>

  <item>
    <title>Devart ODBC Drivers update brings SQL Server 2025, MySQL 9.7, and new Snowflake authentication methods</title>
    <guid isPermaLink="false">https://www.devart.com/blog/?p=184660</guid>
    <link>https://www.devart.com/blog/devart-odbc-drivers-update-brings-sql-server-2025-mysql-9-7-and-new-snowflake-authentication-methods.html</link>
    <description>The latest Devart ODBC Drivers release is here, adding support for SQL Server 2025, MySQL 9.7.x, and NexusDB 4.75.20. The update also introduces new authentication options, improved data protection, and broader compatibility with development tools, operating systems, and other data sources.
The post Devart ODBC Drivers update brings SQL Server 2025, MySQL 9.7, and new Snowflake authentication methods appeared first on Devart Blog.</description>
    <content:encoded><![CDATA[<p>The latest Devart ODBC Drivers release is here, adding support for SQL Server 2025, MySQL 9.7.x, and NexusDB 4.75.20. The update also introduces new authentication options, improved data protection, and broader compatibility with development tools, operating systems, and other data sources.</p>
<p>The post <a href="https://www.devart.com/blog/devart-odbc-drivers-update-brings-sql-server-2025-mysql-9-7-and-new-snowflake-authentication-methods.html">Devart ODBC Drivers update brings SQL Server 2025, MySQL 9.7, and new Snowflake authentication methods</a> appeared first on <a href="https://www.devart.com/blog">Devart Blog</a>.</p>]]></content:encoded>
    <pubDate>Fri, 16 Oct 2026 15:28:13 +0000</pubDate>
    <dc:creator>Alena Subotina</dc:creator>
    <category>ODBC Drivers</category>
    <category>What’s New</category>
    <category>Devart ODBC Driver for SQL Server</category>
    <category>odbc</category>
    <category>odbc driver</category>
    <category>odbc drivers</category>
  </item>

  <item>
    <title>When a Small Change Writes Too Much – How A Linux Kernel Improvement Can Strike You in PXC/Galera Workloads</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54716</guid>
    <link>https://www.percona.com/blog/when-a-small-change-writes-too-much-how-a-linux-kernel-improvement-can-strike-you-in-pxc-galera-workloads/</link>
    <description>We’ve recently been dealing with an interesting customer case where a PXC version upgrade from 5.7 to 8.0 seemed to trigger a completely unexpected problem. Even though the workload and MySQL settings did not change, all PXC members began exhibiting sparse but regular bursts of massive disk writes. Yet the MySQL side workload statistics that we gathered during those bursts did not explain the volume of them at all!

Analyzing these bursts further with pidstat and iostat confirmed that the mysqld process was indeed the sole source of excessive disk writes, and digging further showed the responsible threads to be the Galera receiver thread responsible for subscribing to the replication stream, as well as the main connection threads on the writer node, or Galera applier threads on the secondary members. And what’s most important is that there was a huge disparity between InnoDB engine threads writes (redo, undo, tablespace, etc) and binary log writes versus what was accounted to the Galera threads.
At first, it looked like during the burst, several GB of write-sets were received and applied, yet InnoDB barely changed any pages. All InnoDB-related write activity was completely flat, so they did not participate in these high IO load spikes at all! An example mysqld per-thread breakdown for a few-second burst looked like this:




Thread group
Accounted Writes
Share




applier (Galera write-set appliers, many threads)
13.0 GB
61%


galera_recv-0 (Galera receiver thread)
5.2 GB
24%


ib_pg_flush* (InnoDB page cleaners)
2.0 GB
9%


ib_log_writer (redo log)
0.76 GB
4%


Galera_srvc (Galera service thread)
0.29 GB
1%




The quick thought on the small redo log vs potentially huge writeset volume was that maybe transactions changing a single column would bloat the binlog events with other large blob/text/json columns, due to binlog_row_image=full, as the ROW image format applies to both Galera writesets and binary logs. But, analyzing the binary logs (with binlogsum) showed moderate and completely flat volume, nothing related to those bursts.
Looking for clues, I asked myself, so what are those Galera threads exactly writing? The receiver thread copies all replicated writesets into the Galera cache file(s), but also reclaims the space when the ring wraps (the ring-buffer allocator walks forward from the oldest buffers and discards released ones).
Then, the applier or connection threads also update the cache according to the transaction’s state. Still, when there are no spikes with big writesets (MySQL write load stays flat), why would we see such massive IO write bursts only accounted to the Galera threads?
And why would that weird behavior start to happen after the PXC upgrade? We could not find any clues in the source code; there were no changes to the Galera plugin or wsrep extensions that would explain these bursts.
Later, we found out additional information, which turned out to be crucial – not only the PXC version, but also the OS was upgraded! Now, the previous one: Debian 11, uses Linux 5.10 LTS kernel series, while the upgraded version 12 uses the Linux 6.1 LTS kernel series. And as the main suspect at this point was the Gcache store, how would the new kernel change the way this file is handled, though? It turns out that it does indeed, and it is related to the fact that the Galera plugin uses the mmap() path to handle the cache!
Page Folios in Modern Linux Kernels
In pursuit of handling large data on machines with big RAM and disk space more efficiently in Linux, in addition to huge pages, something called page folios was introduced in 5.16. What is relevant to this case is that large folios were implemented to also handle the page cache, initially only for the XFS filesystem in kernel 5.18, and yes, the affected cluster kept Galera cache on the XFS partition. Then, large folios support is what makes the kernel 6.1 different from the previous 5.10.
OK, but why would “folios” cause bursts, and why do they not affect InnoDB writes, but only the Galera threads? Now, unlike any other data or log files in MySQL (except TempTable storage) or MariaDB, Galera cache files do not use the write() or pwrite() path, but mmap rather.
This difference pointed me to some reports complaining about the systemd journal’s excessive IO problems, which turns out to be using mmap() path as well!
Funny enough, the problem is not very well known, and the root cause is still being confused to this day, even in the most affected systemd community. Some reports describe the journal IO pressure being higher than expected, but never define the real culprit. The systemd issue that currently covers this problem best is:
https://github.com/systemd/systemd/issues/40262
Related kernel thread discussions:
https://lwn.net/Articles/1092530/
https://lists.openwall.net/linux-kernel/2026/08/20/1169
And despite many good blog posts about optimizing journal IO, I found only this one that points to the folio impact:
https://www.cnblogs.com/32bin/p/22929557
The problem even reached the general tech press, as still unsolved in 2026:
https://northeasttimes.com/2026/08/14/a-single-log-line-eats-49kb-of-disk-and-linux-admins-are-furious/
In short: on a large folios-enabled kernel and filesystem, page cache entries are grouped in folios. In early kernel implementations (5.18–6.5), write()/pwrite() paths were also affected, but InnoDB typically uses O_DIRECT for table spaces (so not using page cache at all), and redo and binlog use small appends, which create order-0 (4 kB) small folios. Then in Linux 6.6, a patch was implemented that added per-block dirty tracking, as now iomap records the overlapping filesystem blocks (filesystem-block granularity): see RFCv3 for details. With mmap writes, any update only tracks a whole folio. Thus, writeback page cache iteration, in the mmap case, will sync the whole folio (up to 2MB) instead of the exact fs block (4kB). In other words, the mmap() path cannot benefit from the improved I/O tracking loops in 6.6+.
So, yes, the difference may be potentially huge, and small random writes to a mmapped file can induce huge IO bursts when the related page cache gets flushed to disk.
There are important variables here:



Readahead size via optimal_io_size → read_ahead_kb, per block device, influences the sizes of large folios allocated through it (from 128kB up to even 2MB)
Bursts are specifically periodic due to how writeback of dirty data gets invoked – every dirty_writeback_centisecs (default 5 seconds) the kernel checks if the dirty data (tracked per inode) age is old enough via dirty_expire_centisecs (default 30 seconds) – the observed roughly 33-second bursts on the affected PXC nodes, are consistent with this mechanism (although these settings do not guarantee a fixed flush period) when the object, here the Gcache inode, gets flushed when receiving regular writes.
Workload characteristics matter a lot – writes accumulated within a single folio will result in way smaller write amplifications as compared to a file being updated in many random places, hence touching many different large folios.



The whole concept is damn complex, so let me share a more technical deep dive schematic in a separate document. Bear in mind, I am not a Linux kernel expert, so I hope there are not too many errors. The document is available here: https://przemekmalkowski.github.io/folios-mmap/
Galera Cache Impact
Galera’s writeset cache has been historically a mmapped file from the very beginning. And there are a couple of factors that make the large folio-induced write amplifications particularly severe here:

The cache file is by default 128MB, but very often a much bigger size is used to provide long enough IST coverage.
In addition to sequential appends of new writesets, there are many small and random updates that change their status to RELEASED when the transaction is already applied and ready to be overwritten.
Quite often, MySQL data, together with the galera.cache file, is placed on a large storage, like RAID arrays. Specifically for software RAID, the Linux kernel automatically increases the readahead size proportionally to the array geometry and the advertised optimal I/O size. For hardware RAID and dm/LVM stacks, it may be the case too. This leads to even larger folios, and that’s exactly what happened for the said customer case.
During the PXC member startup, the whole gcache file is scanned sequentially during the recovery process (enabled by default), which can immediately populate the large page cache areas.

Having said that, every PXC/Galera cluster may be affected by extra write amplifications for the galera.cache (and gcache.page.X), if kernel and filesystem versions support large folios. But the impact may be small enough to slip through unnoticed when the default readahead of 128kB is set for the disk block device. The extra IO write pressure may be wrongly assumed to be related to regular (and complex enough) database writes. But when the storage uses bigger readahead, and large folios get to even 2MB in size, the amplification is huge and really difficult to miss.
Possible workarounds
As currently there is no Linux kernel side solution implemented for the mmap() path, there are a couple of workarounds possible, and they apply to any applications using mmapped files, not only PXC/Galera:

Depending on the kernel version, using the EXT4 filesystem may be an option, as folio support was introduced there much later (in 6.16) than in XFS (5.18) – if applicable, this allows us to avoid the said amplifications altogether. It helped in this customer’s case!
Lower the readahead size for the disk device – for the price of potentially limited performance, the maximum size of large folios will be impacted by it, making the amplifications smaller.
Modify the application mmap calls to use MADV_RANDOM via madvise() hints. It will basically disable readahead via enforced random access.
Stop using mmap() path in favor of pwrite() on Linux 6.6+.
Finally, use a customized modern kernel, compiled without THP support via the disabled CONFIG_TRANSPARENT_HUGEPAGE option – large folios are not created on recent kernels when THP is not available. (Note that disabling THP in GRUB is not enough to disable large folios!)

For PXC, I reported the problem here: https://perconadev.atlassian.net/browse/PXC-5340
How to check if you’re affected

Kernel and filesystem: XFS on 5.18+, or ext4 on 6.16+.
Readahead size: cat /sys/block/&amp;lt;dev&amp;gt;/queue/{read_ahead_kb,optimal_io_size}.
Large mappings: FilePmdMapped in /proc/meminfo or smaps for the gcache mapping, and thp_file_mapped climbing in /proc/vmstat.
Bursts: about every 30–35 s (unless different timers in vm.dirty_expire_centisecs and vm.dirty_writeback_centisecs) in iostat, with per-thread pidstat -d charges in up to 2 MB multiples.

More on Large Folios Impact Cases
During research, I found another interesting story. It turns out that mixing active write workloads using both page cache and bypassing it with O_DIRECT may lead to additional unexpected stalls. This time it was visible with RocksDB, as described here:
https://blog.wegenz.in/posts/page-cache-reclaim-foliotune-direct-io-stalemate
The article was created by a human.
The post When a Small Change Writes Too Much – How A Linux Kernel Improvement Can Strike You in PXC/Galera Workloads appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>We’ve recently been dealing with an interesting customer case where a PXC version upgrade from 5.7 to 8.0 seemed to trigger a completely unexpected problem. Even though the workload and MySQL settings did not change, all PXC members began exhibiting sparse but regular <strong>bursts of massive disk writes</strong>. Yet the MySQL side workload statistics that we gathered during those bursts did not explain the volume of them at all!</p>
<p><!-- IMAGE: upload images/image1.png to the Media Library and insert it here --></p>
<p>Analyzing these bursts further with pidstat and iostat confirmed that the mysqld process was indeed the sole source of excessive disk writes, and digging further showed the responsible threads to be the <a href="https://github.com/percona/galera/blob/a6b54d887d58bae43ae3f46f5696195329405b7e/gcs/src/gcs.cpp#L1688-L1704">Galera receiver thread</a> responsible for subscribing to the replication stream, as well as the main connection threads on the writer node, or <strong>Galera applier threads</strong> on the secondary members. And what’s most important is that there was a <strong>huge disparity</strong> between InnoDB engine threads writes (redo, undo, tablespace, etc) and binary log writes versus what was accounted to the Galera threads.</p>
<p>At first, it looked like during the burst, several GB of write-sets were received and applied, yet InnoDB barely changed any pages. All InnoDB-related write activity was completely flat, so they did not participate in these high IO load spikes at all! An example mysqld per-thread breakdown for <strong>a few-second burst</strong> looked like this:</p>
<div>
<table>
<thead>
<tr>
<th>Thread group</th>
<th>Accounted Writes</th>
<th>Share</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>applier</code> (Galera write-set appliers, many threads)</td>
<td>13.0 GB</td>
<td>61%</td>
</tr>
<tr>
<td><code>galera_recv-0</code> (Galera receiver thread)</td>
<td>5.2 GB</td>
<td>24%</td>
</tr>
<tr>
<td><code>ib_pg_flush*</code> (InnoDB page cleaners)</td>
<td>2.0 GB</td>
<td>9%</td>
</tr>
<tr>
<td><code>ib_log_writer</code> (redo log)</td>
<td>0.76 GB</td>
<td>4%</td>
</tr>
<tr>
<td><code>Galera_srvc</code> (Galera service thread)</td>
<td>0.29 GB</td>
<td>1%</td>
</tr>
</tbody>
</table>
</div>
<p>The quick thought on the small redo log vs potentially huge writeset volume was that maybe transactions changing a single column would bloat the binlog events with other large blob/text/json columns, due to <code>binlog_row_image=full</code>, as the ROW image format applies to both Galera writesets and binary logs. But, analyzing the binary logs (with <a href="https://github.com/PrzemekMalkowski/binlogsum">binlogsum</a>) showed moderate and completely flat volume, nothing related to those bursts.</p>
<p>Looking for clues, I asked myself, so what are those Galera threads exactly writing? The receiver thread copies all replicated writesets into the Galera cache file(s), but also reclaims the space when the ring wraps (the ring-buffer allocator walks forward from the oldest buffers and discards released ones).</p>
<p>Then, the applier or connection threads also update the cache according to the transaction’s state. Still, when there are no spikes with big writesets (MySQL write load stays flat), why would we see such massive IO write bursts only accounted to the Galera threads?</p>
<p>And why would that weird behavior start to happen after the PXC upgrade? We could not find any clues in the source code; there were no changes to the Galera plugin or wsrep extensions that would explain these bursts.</p>
<p>Later, we found out additional information, which turned out to be crucial – not only the PXC version, but also the OS was upgraded! Now, the previous one: Debian 11, uses Linux <strong>5.10</strong> LTS kernel series, while the upgraded version 12 uses the Linux <strong>6.1 LTS kernel</strong> series. And as the main suspect at this point was the Gcache store, how would the new kernel change the way this file is handled, though? It turns out that it does indeed, and it is related to the fact that the Galera plugin uses the <a href="https://kernel-internals.org/io/mmap-io/"><strong>mmap() path</strong></a> to handle the cache!</p>
<h2>Page Folios in Modern Linux Kernels</h2>
<p>In pursuit of handling large data on machines with big RAM and disk space more efficiently in Linux, in addition to <a href="https://docs.kernel.org/admin-guide/mm/hugetlbpage.html">huge pages</a>, something called <strong>page folios</strong> was <a href="https://kernelnewbies.org/Linux_5.16#Memory_folios_infrastructure_for_a_faster_memory_management">introduced in 5.16</a>. What is relevant to this case is that large folios were implemented to also handle the page cache, initially only for the XFS filesystem in <a href="https://kernelnewbies.org/Linux_5.18#Memory_management">kernel <strong>5.18</strong></a>, and yes, the affected cluster kept Galera cache on the XFS partition. Then, large folios support is what makes the kernel 6.1 different from the previous 5.10.</p>
<p>OK, but why would “folios” cause bursts, and why do they not affect InnoDB writes, but only the Galera threads? Now, unlike any other data or log files in MySQL (except <a href="https://dev.mysql.com/doc/refman/9.7/en/server-system-variables.html#sysvar_temptable_max_mmap">TempTable</a> storage) or MariaDB, Galera cache files do not use the <code>write()</code> or <code>pwrite()</code> path, but <strong><code>mmap</code></strong> rather.</p>
<p>This difference pointed me to some reports complaining about the <strong>systemd journal’s</strong> excessive IO problems, which turns out to be <strong><a href="https://systemd.io/JOURNAL_FILE_FORMAT/">using <code>mmap()</code></a> path as well</strong>!</p>
<p>Funny enough, the problem is not very well known, and the root cause is still being confused to this day, even in the most affected systemd community. Some reports describe the journal IO pressure being higher than expected, but never define the real culprit. The systemd issue that currently covers this problem best is:<br>
<a href="https://github.com/systemd/systemd/issues/40262">https://github.com/systemd/systemd/issues/40262</a><br>
Related kernel thread discussions:<br>
<a href="https://lwn.net/Articles/1092530/">https://lwn.net/Articles/1092530/</a><br>
<a href="https://lists.openwall.net/linux-kernel/2026/08/20/1169">https://lists.openwall.net/linux-kernel/2026/08/20/1169</a><br>
And despite many good blog posts about optimizing journal IO, I found only this one that points to the folio impact:<br>
<a href="https://www.cnblogs.com/32bin/p/22929557">https://www.cnblogs.com/32bin/p/22929557</a><br>
The problem even reached the general tech press, as still unsolved in 2026:<br>
<a href="https://northeasttimes.com/2026/08/14/a-single-log-line-eats-49kb-of-disk-and-linux-admins-are-furious/">https://northeasttimes.com/2026/08/14/a-single-log-line-eats-49kb-of-disk-and-linux-admins-are-furious/</a></p>
<p>In short: on a large folios-enabled kernel and filesystem, page cache entries are grouped in folios. In early kernel implementations (5.18–6.5), <code>write()</code>/<code>pwrite()</code> paths were also affected, but InnoDB typically uses <code>O_DIRECT</code> for table spaces (so not using page cache at all), and redo and binlog use small appends, which create order-0 (4 kB) small folios. Then in Linux 6.6, a patch was implemented that added per-block dirty tracking, as now <a href="https://docs.kernel.org/filesystems/iomap/design.html">iomap</a> records the overlapping filesystem blocks (filesystem-block granularity): <a href="https://lore.kernel.org/linux-xfs/874jr53ab4.fsf@doe.com/T/">see RFCv3 for details</a>. With mmap writes, any update only tracks a whole folio. Thus, writeback page cache iteration, in the mmap case, will sync the whole folio (up to 2MB) instead of the exact fs block (4kB). In other words, the <code>mmap()</code> path cannot benefit from the improved I/O tracking loops in 6.6+.</p>
<p>So, yes, the difference may be potentially huge, and small random writes to a mmapped file can induce huge IO bursts when the related page cache gets flushed to disk.</p>
<p>There are important variables here:</p>
<ul>
<li>
<ul>
<li>Readahead size via <code>optimal_io_size</code> → <code>read_ahead_kb</code>, per block device, influences the sizes of large folios allocated through it (from 128kB up to even 2MB)</li>
<li>Bursts are specifically periodic due to how writeback of dirty data gets invoked – every <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/vm.html#dirty-writeback-centisecs"><code>dirty_writeback_centisecs</code></a> (default 5 seconds) the kernel checks if the dirty data (tracked per inode) age is old enough via <a href="https://www.kernel.org/doc/html/latest/admin-guide/sysctl/vm.html#dirty-expire-centisecs"><code>dirty_expire_centisecs</code></a> (default 30 seconds) – the observed roughly 33-second bursts on the affected PXC nodes, are consistent with this mechanism (although these settings do not guarantee a fixed flush period) when the object, here the Gcache inode, gets flushed when receiving regular writes.</li>
<li>Workload characteristics matter a lot – writes accumulated within a single folio will result in way smaller write amplifications as compared to a file being updated in many random places, hence touching many different large folios.</li>
</ul>
</li>
</ul>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/folio-writeback-truck-1.svg"><img decoding="async" class="size-full wp-image-54721 aligncenter" src="https://www.percona.com/wp-content/uploads/2026/10/folio-writeback-truck-1.svg" alt=""></a>The whole concept is damn complex, so let me share a more technical deep dive schematic in a separate document. Bear in mind, I am not a Linux kernel expert, so I hope there are not too many errors. The document is available here: <a href="https://przemekmalkowski.github.io/folios-mmap/">https://przemekmalkowski.github.io/folios-mmap/</a></p>
<h2>Galera Cache Impact</h2>
<p>Galera’s writeset cache has been historically a mmapped file from the very beginning. And there are a couple of factors that make the large folio-induced write amplifications particularly severe here:</p>
<ul>
<li>The cache file is by <a href="https://docs.percona.com/percona-xtradb-cluster/8.0/wsrep-provider-index.html#gcachesize">default 128MB</a>, but very often a much bigger size is used to provide long enough IST coverage.</li>
<li>In addition to sequential appends of new writesets, there are many <strong>small and random updates</strong> that change their <a href="https://deepwiki.com/percona/galera/5-storage-system-(gcache)#buffer-management">status</a> to <a href="https://github.com/percona/galera/blob/13ff9ed6577de1aa01e6e296f87ba439cde70b98/gcache/src/gcache_bh.hpp#L104-L108">RELEASED</a> when the transaction is already applied and ready to be overwritten.</li>
<li>Quite often, MySQL data, together with the galera.cache file, is placed on a large storage, like RAID arrays. Specifically for software RAID, the Linux kernel automatically increases the readahead size proportionally to the array geometry and the advertised optimal I/O size. For hardware RAID and dm/LVM stacks, it may be the case too. This leads to even larger folios, and that’s exactly what happened for the said customer case.</li>
<li>During the PXC member startup, the whole gcache file is scanned sequentially during the <a href="https://docs.percona.com/percona-xtradb-cluster/8.4/wsrep-provider-index.html#gcacherecover">recovery process</a> (enabled by default), which can immediately populate the large page cache areas.</li>
</ul>
<p>Having said that, every PXC/Galera cluster may be affected by extra write amplifications for the galera.cache (and gcache.page.X), if kernel and filesystem versions support large folios. But the impact may be small enough to slip through unnoticed when the default readahead of 128kB is set for the disk block device. The extra IO write pressure may be wrongly assumed to be related to regular (and complex enough) database writes. But when the storage uses bigger readahead, and large folios get to even 2MB in size, the amplification is huge and really difficult to miss.</p>
<h2>Possible workarounds</h2>
<p>As currently there is no Linux kernel side solution implemented for the mmap() path, there are a couple of workarounds possible, and they apply to any applications using mmapped files, not only PXC/Galera:</p>
<ul>
<li>Depending on the kernel version, using the <strong>EXT4</strong> filesystem may be an option, as folio support was introduced there much later (<a href="https://kernelnewbies.org/Linux_6.16#Some_Ext4_performance_improvements">in 6.16</a>) than in XFS (5.18) – if applicable, this allows us to avoid the said amplifications altogether. It helped in this customer’s case!</li>
<li>Lower the <a href="https://kernel-internals.org/io/readahead/">readahead</a> size for the disk device – for the price of potentially limited performance, the maximum size of large folios will be impacted by it, making the amplifications smaller.</li>
<li>Modify the <a href="https://github.com/percona/galera/blob/007c7363b74cc2626c54569d9830a12bc2c665db/galerautils/src/gu_mmap.cpp#L67">application mmap calls</a> to use <a href="https://kernel-internals.org/io/mmap-io/?h=madv_random#hints-that-affect-io-behaviour">MADV_RANDOM via madvise() hints</a>. It will basically disable readahead via enforced random access.</li>
<li>Stop using <code>mmap()</code> path in favor of <code>pwrite()</code> on Linux 6.6+.</li>
<li>Finally, use a customized modern kernel, <strong>compiled without THP support</strong> via the disabled <code>CONFIG_TRANSPARENT_HUGEPAGE</code> option – <a href="https://github.com/ctrliq/kernel-src-tree/commit/421f1ab48452af48b64e205de1caca3d1ba415f4">large folios are not created on recent kernels when THP is not available</a>. (Note that disabling THP in GRUB is not enough to disable large folios!)</li>
</ul>
<p>For PXC, I reported the problem here: <a href="https://perconadev.atlassian.net/browse/PXC-5340">https://perconadev.atlassian.net/browse/PXC-5340</a></p>
<h2>How to check if you’re affected</h2>
<ul>
<li>Kernel and filesystem: XFS on 5.18+, or ext4 on 6.16+.</li>
<li>Readahead size: <code>cat /sys/block/&lt;dev&gt;/queue/{read_ahead_kb,optimal_io_size}</code>.</li>
<li>Large mappings: <code>FilePmdMapped</code> in <code>/proc/meminfo</code> or <code>smaps</code> for the gcache mapping, and <code>thp_file_mapped</code> climbing in <code>/proc/vmstat</code>.</li>
<li>Bursts: about every 30–35 s (unless different timers in <code>vm.dirty_expire_centisecs</code> and <code>vm.dirty_writeback_centisecs</code>) in iostat, with per-thread <code>pidstat -d</code> charges in up to 2 MB multiples.</li>
</ul>
<h2>More on Large Folios Impact Cases</h2>
<p>During research, I found another interesting story. It turns out that mixing active write workloads using both page cache and bypassing it with <code>O_DIRECT</code> may lead to additional unexpected stalls. This time it was visible with RocksDB, as described here:</p>
<p><a href="https://blog.wegenz.in/posts/page-cache-reclaim-foliotune-direct-io-stalemate">https://blog.wegenz.in/posts/page-cache-reclaim-foliotune-direct-io-stalemate</a></p>
<p><em>The article was created by a human.</em></p>
<p>The post <a href="https://www.percona.com/blog/when-a-small-change-writes-too-much-how-a-linux-kernel-improvement-can-strike-you-in-pxc-galera-workloads/">When a Small Change Writes Too Much – How A Linux Kernel Improvement Can Strike You in PXC/Galera Workloads</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Fri, 16 Oct 2026 12:58:08 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Hardware and Storage</category>
    <category>Insight for DBAs</category>
    <category>MariaDB</category>
    <category>MySQL</category>
    <category>Uncategorized</category>
    <category>XtraDB Cluster (PXC)</category>
    <category>galera</category>
    <category>large folios</category>
    <category>MMAP</category>
    <category>page cache</category>
  </item>

  <item>
    <title>Just Because You Can SELECT Everything Doesn’t Mean You Should</title>
    <guid isPermaLink="false">8d747a8e361a94a5dcd3ef653d0e4e0c</guid>
    <link>https://blogs.oracle.com/mysql/mysql-select-star-best-practices</link>
    <description>Last week, I shared a meme in the MySQL Group on LinkedIn about using SELECT * in MySQL queries. Today, I’m backing up the punchline with something every joke desperately needs: a technical explanation. We’ve all written queries like this: It’s quick. It’s convenient. It lets you postpone remembering whether the column is called customer_name, […]</description>
    <pubDate>Thu, 15 Oct 2026 15:19:36 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>MySQL Best Practices</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>JavaScript in MySQL: Extending SQL, Not Moving the Application</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54696</guid>
    <link>https://www.percona.com/blog/javascript-in-mysql-extending-sql-not-moving-the-application/</link>
    <description>At Percona Live Europe 2026, Dmitry Lenev and I presented JavaScript in My Database? Over My Dead Query, the story of component_js_lang, the V8-based JavaScript engine for Percona Server for MySQL 8.4. That experience told us few things:

The presentation was seen as “for developers”
What most DBA or DevOps heard was “JavaScript in the database” = “application logic in the database”

This let most of the people pictured developers shipping their business logic into stored procedures. Order workflows, pricing rules, REST calls, all running inside mysqld. 
That picture is wrong, and in this post I want to replace it with the right one, using the same four demos we showed on stage.
The fear is reasonable, the target is wrong
DBAs have earned their skepticism. Many of us have inherited a schema where thousands of lines of stored procedures were the application: impossible to test, impossible to version, impossible to scale out, and owned by nobody who still works there.
So when a new language arrives inside the server, the reflex is to protect the database from becoming an application server again. 
We share that reflex. 

The point of this post is that JavaScript routines, used as designed, do not move us in that direction. 
They move the opposite way: they make SQL stronger so that less logic needs to be pulled out of the database and re-implemented somewhere else.
The engine itself also enforces the boundary. Inside component_js_lang a routine has:

No network I/O. No fetch(), no sockets. A routine cannot call your payment provider or another microservice.
No file system access. It cannot read configs, keys or files.
No Node.js or DOM APIs. No require(), no process, no npm ecosystem.
No useful async. Everything runs synchronously inside the statement.

You cannot build an application with that toolbox. However you can build very good functions!
What JavaScript routines are actually for
A JavaScript routine extends the SQL vocabulary. It is a new function you call from SELECT, JOIN, WHERE, GROUP BY or a trigger, exactly like LOWER() or JSON_EXTRACT(). The only new syntax is LANGUAGE JS:CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
  const walk = (j) =&amp;gt; {
    if (j === null || typeof j !== 'object') return j
    if (Array.isArray(j)) return j.map(walk)
    const out = {}
    for (const [k, v] of Object.entries(j))
      out[k.toLowerCase()] = walk(v)
    return out
  }
  return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$; 
The rule we kept repeating on stage is simple: SQL owns the set, JavaScript owns the value. SQL is unbeatable at filtering, joining, ordering, windowing and aggregating. Where SQL and its procedural language stop is a short, very specific list:

Recursion over a shape not known in advance. JSON_TABLE needs every path as a literal.
Arrays. SQL has no array type, so any algorithm that fills a grid has nowhere to live.
Big integers. BIGINT stops at 19 digits; an IBAN checksum needs 29.
Real iteration with aggregates. A recursive CTE cannot contain an aggregate or window function (ERROR 3575), and a WHILE loop in PL pays an executor round trip on every statement.
Rich string handling. UPPER() and LOWER() do not know that McDonald has a capital D.

None of these is business logic. They are missing building blocks. JavaScript supplies them and hands the result straight back to SQL.

The JavaScript routine lives inside the SQL layer, not beside the application: a query calls it, and it returns a value to that query.
Four demos, one pattern

Each demo in the pleu_2026 repository follows the same shape: SQL does the set work, a small JavaScript function does the one step SQL cannot phrase, and the decision that matters to the business stays in the application.



Demo
What SQL could not do
The JavaScript piece
Result
What stays in the application


Normalize JSON keys
Walk a JSON tree of unknown shape; four partners spell customer.id four ways
norm_keys(), 9 lines
One report went from 51 lines with four UNION ALL branches to 19 lines with no partner names
Which partners to onboard and what the report means


Validate input at the door
Proper-case names, normalise phones, run an IBAN mod-97 on a 29-digit number
proper_name(), clean_email(), clean_phone(), valid_iban(), clean_contact()
460 of 2,000 rows caught, each with a list of what was wrong
What to do with a rejected contact: fix, call the customer, quarantine


Deduplicate customers
Levenshtein distance needs a grid, and SQL has no arrays
lev_distance() inside a JOIN predicate
Name equality found 21.7% of duplicates; blocking in SQL plus two distances found about 80% at 99.7% precision
Whether to merge two records, and how


Where should Federico live?
Iterate to a geometric median; recursive CTEs refuse aggregates
Weiszfeld procedure
11.00 s in procedural SQL vs 0.52 s in JavaScript; 327,033 statements vs 6
Whether Federico actually moves



A few details are worth calling out, because they are exactly where DBAs and developers should meet.
Dedupe: SQL narrows, JavaScript decides the distance. Comparing 3,000 customers with 2,000 signups is six million pairs. We never let JavaScript see them all. Ordinary join predicates on city and initials cut the work to 21,352 pairs, and only those reach lev_distance(). The function returns a number. Merging two customers is a business decision, and it stays in the application.
Validation: the function reports, the policy decides. clean_contact() returns the cleaned values and an array of errors. A trigger uses it to guarantee that nothing dirty lands in contacts, whoever does the insert: web app, mobile app, nightly import, or someone at a mysql prompt at 2am. What happens next to a rejected row is still the application’s job.
Federico: three steps, one of them JavaScript. Turning 44,347 raw stops into 16,232 weighted endpoints is two window functions, and nothing beats SQL there. Turning the resulting coordinate into a district is plain joins. JavaScript only runs the inner loop in the middle. Note that this demo uses session.runSql(), so today it runs on MySQL Enterprise; SQL from JavaScript is the top roadmap item for Percona Server.
Where we draw the line
A JavaScript routine belongs in the database when it makes a data operation possible or cheaper. It belongs in the application when it decides what the business does. In practice we use five questions before accepting a routine:

Does it work on values the database already holds? If it needs an external service, a file or a user session, it is application code.
Is it deterministic, with no side effects outside the transaction? DETERMINISTIC NO SQL is not only a binlog requirement; it is a good design filter.
Is it called from SQL? A good routine sits inside a SELECT, a JOIN, a JSON_TABLE or a trigger. If nothing in SQL calls it, ask why it is in the database.
Is the result smaller than the input, or does it save a round trip? Two numbers out instead of 5,000 names, emails and cities is the kind of win we want.
Would every client need the same rule? Validation that the web app, the mobile app and the importer all must apply is a data rule. Pricing for a campaign that changes next week is business policy.




Belongs in a JS routine
Stays in the application


Normalising and validating values (keys, names, phones, IBAN)
Workflows, approvals, notifications


Distance, similarity, checksum and parsing functions
Decisions based on those scores: merge, reject, refund


The inner loop of a numeric algorithm over rows SQL selected
Orchestration across services and external APIs


Integrity rules every writer must obey
UI, sessions, authentication flows


Reshaping JSON so SQL can query it
Business rules owned by product and changed often



If a routine starts to need configuration tables, feature flags or calls to another system, that is our signal that it has crossed the line.
Why developers should ask for it
For developers this is not about giving up control of the application. It is about no longer having to pull data out of MySQL just because SQL could not express one step.

It is the language you already write. ECMAScript on V8, the same runtime as Chrome and Node.js. JSON columns arrive as native JS objects, no JSON.parse() needed.
Fewer round trips. In the commute demo one CALL read 16,232 rows and wrote 2,000 answers with zero rows crossing the network. The speed-up from 11.00 s to 0.52 s came from not crossing the executor boundary 327,033 times, not from JavaScript being a faster language.
Less personal data in motion. The dedupe query returns two numbers instead of shipping 5,000 names, emails and cities to an app server, and into its logs. Your GDPR scope stays in the database.
One rule, written once. Validation that lives in five clients becomes four out-of-date copies. A function and a trigger are one copy nobody can bypass.
Simpler queries in your code. norm_keys() turned a 51-line, partner-specific report into 19 lines. Partner five becomes a data change, not a code change.
Pure functions are easy to test. Deterministic, no I/O, no hidden state: the same unit-testing habits you already have.

How we suggest developers and DBAs work together:

The developer proposes the function, with the SQL that will call it.
The DBA reviews it against the five questions above, and checks if the call site uses blocking or filtering so the function runs on as few rows as possible.
Grant CREATE_JS_ROUTINE deliberately; it is required on top of CREATE ROUTINE, so who can write JavaScript is an explicit choice.
Keep the routine in version control next to the schema migrations.

SQL did not get worse, it got a companion
JavaScript in MySQL is not an invitation to rebuild the application inside the server. 
It is a set of sharp tools for the few places where SQL and PL run out: recursion, arrays, big numbers and tight loops. 
The set work stays in SQL, the business logic stays in the application, and JavaScript fills the gap between them.
If you want to try it, component_js_lang is available as a tech preview in Percona Server for MySQL 8.4.5-5 and later, free and open source, while the equivalent feature from Oracle requires MySQL Enterprise. 
You can easily use the code available here Demo code from Percona Live Europe 2026 to perform some tests, just doing cut and paste. 
Tell us what you think on the Percona Community Forum or in Jira with the label PS-JS-LANG. Your feedback decides what goes into GA, starting with SQL execution from JavaScript routines.
Resources

Percona Server js_lang documentation
Demo code from Percona Live Europe 2026
Source code on GitHub

The post JavaScript in MySQL: Extending SQL, Not Moving the Application appeared first on Percona.</description>
    <content:encoded><![CDATA[<p><span>At Percona Live Europe 2026, Dmitry Lenev and I presented </span><i><span>JavaScript in My Database? Over My Dead Query</span></i><span>, the story of </span><span>component_js_lang</span><span>, the V8-based JavaScript engine for Percona Server for MySQL 8.4. That experience told us few things:</span></p>
<ul>
<li aria-level="1"><span>The presentation was seen as “for developers”</span></li>
<li aria-level="1"><span>What most DBA or DevOps heard was “JavaScript in the database” = “application logic in the database”</span></li>
</ul>
<p><span>This let most of the people pictured developers shipping their business logic into stored procedures. Order workflows, pricing rules, REST calls, all running inside mysqld. </span></p>
<p><span>That picture is wrong, and in this post I want to replace it with the right one, using the same four demos we showed on stage.</span></p>
<p><em><strong>The fear is reasonable, the target is wrong</strong></em></p>
<p><span>DBAs have earned their skepticism. Many of us have inherited a schema where thousands of lines of stored procedures </span><i><span>were</span></i><span> the application: impossible to test, impossible to version, impossible to scale out, and owned by nobody who still works there.</span></p>
<p><span>So when a new language arrives inside the server, the reflex is to protect the database from becoming an application server again. </span></p>
<h3><span>We share that reflex.</span> <span><br>
</span></h3>
<p><span>The point of this post is that JavaScript routines, used as designed, do not move us in that direction. </span><span><br>
</span><span>They move the opposite way: they make SQL stronger so that </span><i><span>less</span></i><span> logic needs to be pulled out of the database and re-implemented somewhere else.</span></p>
<p><span>The engine itself also enforces the boundary. Inside </span><span>component_js_lang</span><span> a routine has:</span></p>
<ul>
<li aria-level="1"><span>No network I/O. No </span><span>fetch()</span><span>, no sockets. A routine cannot call your payment provider or another microservice.</span></li>
<li aria-level="1"><span>No file system access. It cannot read configs, keys or files.</span></li>
<li aria-level="1"><span>No Node.js or DOM APIs. No </span><span>require()</span><span>, no </span><span>process</span><span>, no npm ecosystem.</span></li>
<li aria-level="1"><span>No useful async. Everything runs synchronously inside the statement.</span></li>
</ul>
<p><span>You cannot build an application with that toolbox. However you can build very good functions!</span></p>
<h2><span>What JavaScript routines are actually for</span></h2>
<p><span>A JavaScript routine extends the SQL vocabulary. It is a new function you call from </span><span>SELECT</span><span>, </span><span>JOIN</span><span>, </span><span>WHERE</span><span>, </span><span>GROUP BY</span><span> or a trigger, exactly like </span><span>LOWER()</span><span> or </span><span>JSON_EXTRACT()</span><span>. The only new syntax is </span><span>LANGUAGE JS</span><span>:</span></p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
  const walk = (j) =&gt; {
    if (j === null || typeof j !== 'object') return j
    if (Array.isArray(j)) return j.map(walk)
    const out = {}
    for (const [k, v] of Object.entries(j))
      out[k.toLowerCase()] = walk(v)
    return out
  }
  return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$;</pre><p> </p>
<p><span>The rule we kept repeating on stage is simple: SQL owns the set, JavaScript owns the value. SQL is unbeatable at filtering, joining, ordering, windowing and aggregating. Where SQL and its procedural language stop is a short, very specific list:</span></p>
<ul>
<li aria-level="1"><span>Recursion over a shape not known in advance. </span><span>JSON_TABLE</span><span> needs every path as a literal.</span></li>
<li aria-level="1"><span>Arrays. SQL has no array type, so any algorithm that fills a grid has nowhere to live.</span></li>
<li aria-level="1"><span>Big integers. </span><span>BIGINT</span><span> stops at 19 digits; an IBAN checksum needs 29.</span></li>
<li aria-level="1"><span>Real iteration with aggregates. A recursive CTE cannot contain an aggregate or window function (ERROR 3575), and a </span><span>WHILE</span><span> loop in PL pays an executor round trip on every statement.</span></li>
<li aria-level="1"><span>Rich string handling. </span><span>UPPER()</span><span> and </span><span>LOWER()</span><span> do not know that McDonald has a capital D.</span></li>
</ul>
<p><span>None of these is business logic. They are missing building blocks. JavaScript supplies them and hands the result straight back to SQL.</span></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/schema.png"><img fetchpriority="high" decoding="async" class="size-medium_large wp-image-54699 alignnone" src="https://www.percona.com/wp-content/uploads/2026/10/schema-768x403.png" alt="" width="768" height="403" srcset="https://www.percona.com/wp-content/uploads/2026/10/schema-768x403.png 768w, https://www.percona.com/wp-content/uploads/2026/10/schema-300x158.png 300w, https://www.percona.com/wp-content/uploads/2026/10/schema-1024x538.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/schema.png 1344w" sizes="(max-width: 768px) 100vw, 768px"></a></p>
<p><span>The JavaScript routine lives inside the SQL layer, not beside the application: a query calls it, and it returns a value to that query.</span></p>
<h2><span>Four demos, one pattern</span></h2>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/slide1.png"><img decoding="async" class="size-medium_large wp-image-54700 alignnone" src="https://www.percona.com/wp-content/uploads/2026/10/slide1-768x372.png" alt="" width="768" height="372" srcset="https://www.percona.com/wp-content/uploads/2026/10/slide1-768x372.png 768w, https://www.percona.com/wp-content/uploads/2026/10/slide1-300x145.png 300w, https://www.percona.com/wp-content/uploads/2026/10/slide1-1024x495.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/slide1.png 1054w" sizes="(max-width: 768px) 100vw, 768px"></a></p>
<p><span>Each demo in the </span><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>pleu_2026 repository</span></a><span> follows the same shape: SQL does the set work, a small JavaScript function does the one step SQL cannot phrase, and the decision that matters to the business stays in the application.</span></p>
<table>
<tbody>
<tr>
<td><span>Demo</span></td>
<td><span>What SQL could not do</span></td>
<td><span>The JavaScript piece</span></td>
<td><span>Result</span></td>
<td><span>What stays in the application</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/normalize_key"><span>Normalize JSON keys</span></a></td>
<td><span>Walk a JSON tree of unknown shape; four partners spell </span><span>customer.id</span><span> four ways</span></td>
<td><span>norm_keys()</span><span>, 9 lines</span></td>
<td><span>One report went from 51 lines with four </span><span>UNION ALL</span><span> branches to 19 lines with no partner names</span></td>
<td><span>Which partners to onboard and what the report means</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/validation_input"><span>Validate input at the door</span></a></td>
<td><span>Proper-case names, normalise phones, run an IBAN mod-97 on a 29-digit number</span></td>
<td><span>proper_name()</span><span>, </span><span>clean_email()</span><span>, </span><span>clean_phone()</span><span>, </span><span>valid_iban()</span><span>, </span><span>clean_contact()</span></td>
<td><span>460 of 2,000 rows caught, each with a list of what was wrong</span></td>
<td><span>What to do with a rejected contact: fix, call the customer, quarantine</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/deupe_data_cpu_bound_algorithm"><span>Deduplicate customers</span></a></td>
<td><span>Levenshtein distance needs a grid, and SQL has no arrays</span></td>
<td><span>lev_distance()</span><span> inside a </span><span>JOIN</span><span> predicate</span></td>
<td><span>Name equality found 21.7% of duplicates; blocking in SQL plus two distances found about 80% at 99.7% precision</span></td>
<td><span>Whether to merge two records, and how</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/where_should_I_live"><span>Where should Federico live?</span></a></td>
<td><span>Iterate to a geometric median; recursive CTEs refuse aggregates</span></td>
<td><span>Weiszfeld procedure</span></td>
<td><span>11.00 s in procedural SQL vs 0.52 s in JavaScript; 327,033 statements vs 6</span></td>
<td><span>Whether Federico actually moves</span></td>
</tr>
</tbody>
</table>
<p><span>A few details are worth calling out, because they are exactly where DBAs and developers should meet.</span></p>
<p><span>Dedupe: SQL narrows, JavaScript decides the distance. Comparing 3,000 customers with 2,000 signups is six million pairs. We never let JavaScript see them all. Ordinary join predicates on city and initials cut the work to 21,352 pairs, and only those reach </span><span>lev_distance()</span><span>. The function returns a number. Merging two customers is a business decision, and it stays in the application.</span></p>
<p><span>Validation: the function reports, the policy decides. </span><span>clean_contact()</span><span> returns the cleaned values </span><i><span>and</span></i><span> an array of errors. A trigger uses it to guarantee that nothing dirty lands in </span><span>contacts</span><span>, whoever does the insert: web app, mobile app, nightly import, or someone at a </span><span>mysql</span><span> prompt at 2am. What happens next to a rejected row is still the application’s job.</span></p>
<p><span>Federico: three steps, one of them JavaScript. Turning 44,347 raw stops into 16,232 weighted endpoints is two window functions, and nothing beats SQL there. Turning the resulting coordinate into a district is plain joins. JavaScript only runs the inner loop in the middle. Note that this demo uses </span><span>session.runSql()</span><span>, so today it runs on MySQL Enterprise; SQL from JavaScript is the top roadmap item for Percona Server.</span></p>
<h2><span>Where we draw the line</span></h2>
<p><span>A JavaScript routine belongs in the database when it makes a data operation possible or cheaper. It belongs in the application when it decides what the business does. In practice we use five questions before accepting a routine:</span></p>
<ol>
<li aria-level="1"><span>Does it work on values the database already holds? If it needs an external service, a file or a user session, it is application code.</span></li>
<li aria-level="1"><span>Is it deterministic, with no side effects outside the transaction? </span><span>DETERMINISTIC NO SQL</span><span> is not only a binlog requirement; it is a good design filter.</span></li>
<li aria-level="1"><span>Is it called from SQL? A good routine sits inside a </span><span>SELECT</span><span>, a </span><span>JOIN</span><span>, a </span><span>JSON_TABLE</span><span> or a trigger. If nothing in SQL calls it, ask why it is in the database.</span></li>
<li aria-level="1"><span>Is the result smaller than the input, or does it save a round trip? Two numbers out instead of 5,000 names, emails and cities is the kind of win we want.</span></li>
<li aria-level="1"><span>Would every client need the same rule? Validation that the web app, the mobile app and the importer all must apply is a data rule. Pricing for a campaign that changes next week is business policy.</span></li>
</ol>
<table>
<tbody>
<tr>
<td><span>Belongs in a JS routine</span></td>
<td><span>Stays in the application</span></td>
</tr>
<tr>
<td><span>Normalising and validating values (keys, names, phones, IBAN)</span></td>
<td><span>Workflows, approvals, notifications</span></td>
</tr>
<tr>
<td><span>Distance, similarity, checksum and parsing functions</span></td>
<td><span>Decisions based on those scores: merge, reject, refund</span></td>
</tr>
<tr>
<td><span>The inner loop of a numeric algorithm over rows SQL selected</span></td>
<td><span>Orchestration across services and external APIs</span></td>
</tr>
<tr>
<td><span>Integrity rules every writer must obey</span></td>
<td><span>UI, sessions, authentication flows</span></td>
</tr>
<tr>
<td><span>Reshaping JSON so SQL can query it</span></td>
<td><span>Business rules owned by product and changed often</span></td>
</tr>
</tbody>
</table>
<p><span>If a routine starts to need configuration tables, feature flags or calls to another system, that is our signal that it has crossed the line.</span></p>
<h2><span>Why developers should ask for it</span></h2>
<p><span>For developers this is not about giving up control of the application. It is about no longer having to pull data out of MySQL just because SQL could not express one step.</span></p>
<ul>
<li aria-level="1"><span>It is the language you already write. ECMAScript on V8, the same runtime as Chrome and Node.js. JSON columns arrive as native JS objects, no </span><span>JSON.parse()</span><span> needed.</span></li>
<li aria-level="1"><span>Fewer round trips. In the commute demo one </span><span>CALL</span><span> read 16,232 rows and wrote 2,000 answers with zero rows crossing the network. The speed-up from 11.00 s to 0.52 s came from not crossing the executor boundary 327,033 times, not from JavaScript being a faster language.</span></li>
<li aria-level="1"><span>Less personal data in motion. The dedupe query returns two numbers instead of shipping 5,000 names, emails and cities to an app server, and into its logs. Your GDPR scope stays in the database.</span></li>
<li aria-level="1"><span>One rule, written once. Validation that lives in five clients becomes four out-of-date copies. A function and a trigger are one copy nobody can bypass.</span></li>
<li aria-level="1"><span>Simpler queries in your code. </span><span>norm_keys()</span><span> turned a 51-line, partner-specific report into 19 lines. Partner five becomes a data change, not a code change.</span></li>
<li aria-level="1"><span>Pure functions are easy to test. Deterministic, no I/O, no hidden state: the same unit-testing habits you already have.</span></li>
</ul>
<h3><span>How we suggest developers and DBAs work together:</span></h3>
<ol>
<li aria-level="1"><span>The developer proposes the function, with the SQL that will call it.</span></li>
<li aria-level="1"><span>The DBA reviews it against the five questions above, and checks if the call site uses blocking or filtering so the function runs on as few rows as possible.</span></li>
<li aria-level="1"><span>Grant </span><span>CREATE_JS_ROUTINE</span><span> deliberately; it is required on top of </span><span>CREATE ROUTINE</span><span>, so who can write JavaScript is an explicit choice.</span></li>
<li aria-level="1"><span>Keep the routine in version control next to the schema migrations.</span></li>
</ol>
<p><span>SQL did not get worse, it got a companion</span></p>
<p><span>JavaScript in MySQL is not an invitation to rebuild the application inside the server. </span></p>
<p><span>It is a set of sharp tools for the few places where SQL and PL run out: recursion, arrays, big numbers and tight loops. </span><span><br>
</span><span>The set work stays in SQL, the business logic stays in the application, and JavaScript fills the gap between them.</span></p>
<p><span>If you want to try it, </span><span>component_js_lang</span><span> is available as a tech preview in Percona Server for MySQL 8.4.5-5 and later, free and open source, while the equivalent feature from Oracle requires MySQL Enterprise. </span></p>
<p><span>You can easily use the code available here </span><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>Demo code from Percona Live Europe 2026</span></a><span> to perform some tests, just doing cut and paste. </span></p>
<p><span>Tell us what you think on the </span><a href="https://forums.percona.com/"><span>Percona Community Forum</span></a><span> or in </span><a href="https://jira.percona.com/"><span>Jira</span></a><span> with the label </span><span>PS-JS-LANG</span><span>. Your feedback decides what goes into GA, starting with SQL execution from JavaScript routines.</span></p>
<p><span>Resources</span></p>
<ul>
<li aria-level="1"><a href="https://docs.percona.com/percona-server/8.4/js-lang-overview.html"><span>Percona Server js_lang documentation</span></a></li>
<li aria-level="1"><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>Demo code from Percona Live Europe 2026</span></a></li>
<li aria-level="1"><a href="https://github.com/percona/percona-server/tree/release-8.4.11-11/components/js_lang"><span>Source code on GitHub</span></a></li>
</ul>
<p>The post <a href="https://www.percona.com/blog/javascript-in-mysql-extending-sql-not-moving-the-application/">JavaScript in MySQL: Extending SQL, Not Moving the Application</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Thu, 15 Oct 2026 15:12:30 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>Database Trends</category>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MySQL</category>
    <category>Open Source</category>
    <category>SQL</category>
    <category>Uncategorized</category>
    <category>Application Logic vs Database Logic</category>
    <category>component_js_lang</category>
    <category>Data Validation</category>
    <category>Database Administration (DBA)</category>
    <category>Database Architecture</category>
    <category>Javascript</category>
    <category>JSON Processing</category>
    <category>Percona Live E</category>
  </item>

  <item>
    <title>JavaScript in MySQL: Extending SQL, Not Moving the Application</title>
    <guid isPermaLink="false">http://www.tusacentral.com/joomla/index.php/mysql-blogs/272-javascript-in-mysql-extending-sql-not-moving-the-application</guid>
    <link>http://www.tusacentral.com/joomla/index.php/mysql-blogs/272-javascript-in-mysql-extending-sql-not-moving-the-application</link>
    <description>At Percona Live Europe 2026, Dmitry Lenev and I presented JavaScript in My Database? Over My Dead Query, the story of component_js_lang, the V8-based JavaScript engine for Percona Server for MySQL 8.4. That experience told us few things:

The presentation was seen as “for developers”
What most DBA or DevOps heard was &quot;JavaScript in the database&quot; = &quot;application logic in the database&quot;

This let most of the people pictured developers shipping their business logic into stored procedures. Order workflows, pricing rules, REST calls, all running inside mysqld. 
That picture is wrong, and in this post I want to replace it with the right one, using the same four demos we showed on stage.
The fear is reasonable, the target is wrong
DBAs have earned their skepticism. Many of us have inherited a schema where thousands of lines of stored procedures were the application: impossible to test, impossible to version, impossible to scale out, and owned by nobody who still works there.
So when a new language arrives inside the server, the reflex is to protect the database from becoming an application server again. 
 
We share that reflex.  
The point of this post is that JavaScript routines, used as designed, do not move us in that direction.  They move the opposite way: they make SQL stronger so that less logic needs to be pulled out of the database and re-implemented somewhere else.
The engine itself also enforces the boundary. Inside component_js_lang a routine has:

No network I/O. No fetch(), no sockets. A routine cannot call your payment provider or another microservice.
No file system access. It cannot read configs, keys or files.
No Node.js or DOM APIs. No require(), no process, no npm ecosystem.
No useful async. Everything runs synchronously inside the statement.

You cannot build an application with that toolbox. However you can build very good functions!
What JavaScript routines are actually for
A JavaScript routine extends the SQL vocabulary. It is a new function you call from SELECT, JOIN, WHERE, GROUP BY or a trigger, exactly like LOWER() or JSON_EXTRACT(). The only new syntax is LANGUAGE JS:
CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
  const walk = (j) =&amp;gt; {
    if (j === null || typeof j !== 'object') return j
    if (Array.isArray(j)) return j.map(walk)
    const out = {}
    for (const [k, v] of Object.entries(j))
      out[k.toLowerCase()] = walk(v)
    return out
  }
  return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$;

 
The rule we kept repeating on stage is simple: SQL owns the set, JavaScript owns the value. SQL is unbeatable at filtering, joining, ordering, windowing and aggregating. Where SQL and its procedural language stop is a short, very specific list:

Recursion over a shape not known in advance. JSON_TABLE needs every path as a literal.
Arrays. SQL has no array type, so any algorithm that fills a grid has nowhere to live.
Big integers. BIGINT stops at 19 digits; an IBAN checksum needs 29.
Real iteration with aggregates. A recursive CTE cannot contain an aggregate or window function (ERROR 3575), and a WHILE loop in PL pays an executor round trip on every statement.
Rich string handling. UPPER() and LOWER() do not know that McDonald has a capital D.

None of these is business logic. They are missing building blocks. JavaScript supplies them and hands the result straight back to SQL.

The JavaScript routine lives inside the SQL layer, not beside the application: a query calls it, and it returns a value to that query.
Four demos, one pattern

Each demo in the pleu_2026 repository follows the same shape: SQL does the set work, a small JavaScript function does the one step SQL cannot phrase, and the decision that matters to the business stays in the application.



Demo
What SQL could not do
The JavaScript piece
Result
What stays in the application


Normalize JSON keys
Walk a JSON tree of unknown shape; four partners spell customer.id four ways
norm_keys(), 9 lines
One report went from 51 lines with four UNION ALL branches to 19 lines with no partner names
Which partners to onboard and what the report means


Validate input at the door
Proper-case names, normalise phones, run an IBAN mod-97 on a 29-digit number
proper_name(), clean_email(), clean_phone(), valid_iban(), clean_contact()
460 of 2,000 rows caught, each with a list of what was wrong
What to do with a rejected contact: fix, call the customer, quarantine


Deduplicate customers
Levenshtein distance needs a grid, and SQL has no arrays
lev_distance() inside a JOIN predicate
Name equality found 21.7% of duplicates; blocking in SQL plus two distances found about 80% at 99.7% precision
Whether to merge two records, and how


Where should Federico live?
Iterate to a geometric median; recursive CTEs refuse aggregates
Weiszfeld procedure
11.00 s in procedural SQL vs 0.52 s in JavaScript; 327,033 statements vs 6
Whether Federico actually moves



A few details are worth calling out, because they are exactly where DBAs and developers should meet.
Dedupe: SQL narrows, JavaScript decides the distance. Comparing 3,000 customers with 2,000 signups is six million pairs. We never let JavaScript see them all. Ordinary join predicates on city and initials cut the work to 21,352 pairs, and only those reach lev_distance(). The function returns a number. Merging two customers is a business decision, and it stays in the application.
Validation: the function reports, the policy decides. clean_contact() returns the cleaned values and an array of errors. A trigger uses it to guarantee that nothing dirty lands in contacts, whoever does the insert: web app, mobile app, nightly import, or someone at a mysql prompt at 2am. What happens next to a rejected row is still the application's job.
Federico: three steps, one of them JavaScript. Turning 44,347 raw stops into 16,232 weighted endpoints is two window functions, and nothing beats SQL there. Turning the resulting coordinate into a district is plain joins. JavaScript only runs the inner loop in the middle. Note that this demo uses session.runSql(), so today it runs on MySQL Enterprise; SQL from JavaScript is the top roadmap item for Percona Server.
Where we draw the line
A JavaScript routine belongs in the database when it makes a data operation possible or cheaper. It belongs in the application when it decides what the business does. In practice we use five questions before accepting a routine:

Does it work on values the database already holds? If it needs an external service, a file or a user session, it is application code.
Is it deterministic, with no side effects outside the transaction? DETERMINISTIC NO SQL is not only a binlog requirement; it is a good design filter.
Is it called from SQL? A good routine sits inside a SELECT, a JOIN, a JSON_TABLE or a trigger. If nothing in SQL calls it, ask why it is in the database.
Is the result smaller than the input, or does it save a round trip? Two numbers out instead of 5,000 names, emails and cities is the kind of win we want.
Would every client need the same rule? Validation that the web app, the mobile app and the importer all must apply is a data rule. Pricing for a campaign that changes next week is business policy.




Belongs in a JS routine
Stays in the application


Normalising and validating values (keys, names, phones, IBAN)
Workflows, approvals, notifications


Distance, similarity, checksum and parsing functions
Decisions based on those scores: merge, reject, refund


The inner loop of a numeric algorithm over rows SQL selected
Orchestration across services and external APIs


Integrity rules every writer must obey
UI, sessions, authentication flows


Reshaping JSON so SQL can query it
Business rules owned by product and changed often



If a routine starts to need configuration tables, feature flags or calls to another system, that is our signal that it has crossed the line.
Why developers should ask for it
For developers this is not about giving up control of the application. It is about no longer having to pull data out of MySQL just because SQL could not express one step.

It is the language you already write. ECMAScript on V8, the same runtime as Chrome and Node.js. JSON columns arrive as native JS objects, no JSON.parse() needed.
Fewer round trips. In the commute demo one CALL read 16,232 rows and wrote 2,000 answers with zero rows crossing the network. The speed-up from 11.00 s to 0.52 s came from not crossing the executor boundary 327,033 times, not from JavaScript being a faster language.
Less personal data in motion. The dedupe query returns two numbers instead of shipping 5,000 names, emails and cities to an app server, and into its logs. Your GDPR scope stays in the database.
One rule, written once. Validation that lives in five clients becomes four out-of-date copies. A function and a trigger are one copy nobody can bypass.
Simpler queries in your code. norm_keys() turned a 51-line, partner-specific report into 19 lines. Partner five becomes a data change, not a code change.
Pure functions are easy to test. Deterministic, no I/O, no hidden state: the same unit-testing habits you already have.

How we suggest developers and DBAs work together:

The developer proposes the function, with the SQL that will call it.
The DBA reviews it against the five questions above, and checks if the call site uses blocking or filtering so the function runs on as few rows as possible.
Grant CREATE_JS_ROUTINE deliberately; it is required on top of CREATE ROUTINE, so who can write JavaScript is an explicit choice.
Keep the routine in version control next to the schema migrations.

SQL did not get worse, it got a companion
JavaScript in MySQL is not an invitation to rebuild the application inside the server. 
It is a set of sharp tools for the few places where SQL and PL run out: recursion, arrays, big numbers and tight loops.  The set work stays in SQL, the business logic stays in the application, and JavaScript fills the gap between them.
If you want to try it, component_js_lang is available as a tech preview in Percona Server for MySQL 8.4.5-5 and later, free and open source, while the equivalent feature from Oracle requires MySQL Enterprise. 
You can easily use the code available here Demo code from Percona Live Europe 2026 to perform some tests, just doing cut and paste. 
Tell us what you think on the Percona Community Forum or in Jira with the label PS-JS-LANG. Your feedback decides what goes into GA, starting with SQL execution from JavaScript routines.
Resources

Percona Server js_lang documentation
Demo code from Percona Live Europe 2026
Source code on GitHub
</description>
    <content:encoded><![CDATA[<p><span><img src="http://www.tusacentral.com/joomla/images/stories/javascript/scaled_goat.jpeg" alt="scaled goat" width="414" height="231">At Percona Live Europe 2026, Dmitry Lenev and I presented </span><i><span>JavaScript in My Database? Over My Dead Query</span></i><span>, the story of </span><span>component_js_lang</span><span>, the V8-based JavaScript engine for Percona Server for MySQL 8.4. That experience told us few things:</span></p>
<ul>
<li aria-level="1"><span>The presentation was seen as “for developers”</span></li>
<li aria-level="1"><span>What most DBA or DevOps heard was "JavaScript in the database" = "application logic in the database"</span></li>
</ul>
<p><span>This let most of the people pictured developers shipping their business logic into stored procedures. Order workflows, pricing rules, REST calls, all running inside mysqld. </span></p>
<p><span>That picture is wrong, and in this post I want to replace it with the right one, using the same four demos we showed on stage.</span></p>
<p><em><strong>The fear is reasonable, the target is wrong</strong></em></p>
<p><span>DBAs have earned their skepticism. Many of us have inherited a schema where thousands of lines of stored procedures </span><i><span>were</span></i><span> the application: impossible to test, impossible to version, impossible to scale out, and owned by nobody who still works there.</span></p>
<p><span>So when a new language arrives inside the server, the reflex is to protect the database from becoming an application server again. </span></p>
<p> </p>
<h3><span>We share that reflex.</span> <span> </span></h3>
<p><span>The point of this post is that JavaScript routines, used as designed, do not move us in that direction. </span><span> </span><span>They move the opposite way: they make SQL stronger so that </span><i><span>less</span></i><span> logic needs to be pulled out of the database and re-implemented somewhere else.</span></p>
<p><span>The engine itself also enforces the boundary. Inside </span><span>component_js_lang</span><span> a routine has:</span></p>
<ul>
<li aria-level="1"><span>No network I/O. No </span><span>fetch()</span><span>, no sockets. A routine cannot call your payment provider or another microservice.</span></li>
<li aria-level="1"><span>No file system access. It cannot read configs, keys or files.</span></li>
<li aria-level="1"><span>No Node.js or DOM APIs. No </span><span>require()</span><span>, no </span><span>process</span><span>, no npm ecosystem.</span></li>
<li aria-level="1"><span>No useful async. Everything runs synchronously inside the statement.</span></li>
</ul>
<p><span>You cannot build an application with that toolbox. However you can build very good functions!</span></p>
<h2><span>What JavaScript routines are actually for</span></h2>
<p><span>A JavaScript routine extends the SQL vocabulary. It is a new function you call from </span><span>SELECT</span><span>, </span><span>JOIN</span><span>, </span><span>WHERE</span><span>, </span><span>GROUP BY</span><span> or a trigger, exactly like </span><span>LOWER()</span><span> or </span><span>JSON_EXTRACT()</span><span>. The only new syntax is </span><span>LANGUAGE JS</span><span>:</span></p>
<pre class="lang:default decode:true">CREATE FUNCTION norm_keys(d JSON) RETURNS JSON
DETERMINISTIC NO SQL LANGUAGE JS AS $$
  const walk = (j) =&gt; {
    if (j === null || typeof j !== 'object') return j
    if (Array.isArray(j)) return j.map(walk)
    const out = {}
    for (const [k, v] of Object.entries(j))
      out[k.toLowerCase()] = walk(v)
    return out
  }
  return walk(typeof d === 'string' ? JSON.parse(d) : d)
$$;
</pre>
<p> </p>
<p><span>The rule we kept repeating on stage is simple: SQL owns the set, JavaScript owns the value. SQL is unbeatable at filtering, joining, ordering, windowing and aggregating. Where SQL and its procedural language stop is a short, very specific list:</span></p>
<ul>
<li aria-level="1"><span>Recursion over a shape not known in advance. </span><span>JSON_TABLE</span><span> needs every path as a literal.</span></li>
<li aria-level="1"><span>Arrays. SQL has no array type, so any algorithm that fills a grid has nowhere to live.</span></li>
<li aria-level="1"><span>Big integers. </span><span>BIGINT</span><span> stops at 19 digits; an IBAN checksum needs 29.</span></li>
<li aria-level="1"><span>Real iteration with aggregates. A recursive CTE cannot contain an aggregate or window function (ERROR 3575), and a </span><span>WHILE</span><span> loop in PL pays an executor round trip on every statement.</span></li>
<li aria-level="1"><span>Rich string handling. </span><span>UPPER()</span><span> and </span><span>LOWER()</span><span> do not know that McDonald has a capital D.</span></li>
</ul>
<p><span>None of these is business logic. They are missing building blocks. JavaScript supplies them and hands the result straight back to SQL.</span></p>
<p><a href="http://www.tusacentral.com/joomla/images/stories/javascript/schema.png"><img src="http://www.tusacentral.com/joomla/images/stories/javascript/schema.png" alt="schema" width="700" height="368" class="size-medium_large wp-image-54699 alignnone"></a></p>
<p><span>The JavaScript routine lives inside the SQL layer, not beside the application: a query calls it, and it returns a value to that query.</span></p>
<h2><span>Four demos, one pattern</span></h2>
<p><a href="http://www.tusacentral.com/joomla/images/stories/javascript/slide1.png"><img src="http://www.tusacentral.com/joomla/images/stories/javascript/slide1.png" alt="slide1" width="700" height="339" class="size-medium_large wp-image-54700 alignnone"></a></p>
<p><span>Each demo in the </span><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>pleu_2026 repository</span></a><span> follows the same shape: SQL does the set work, a small JavaScript function does the one step SQL cannot phrase, and the decision that matters to the business stays in the application.</span></p>
<table>
<tbody>
<tr>
<td><span>Demo</span></td>
<td><span>What SQL could not do</span></td>
<td><span>The JavaScript piece</span></td>
<td><span>Result</span></td>
<td><span>What stays in the application</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/normalize_key"><span>Normalize JSON keys</span></a></td>
<td><span>Walk a JSON tree of unknown shape; four partners spell </span><span>customer.id</span><span> four ways</span></td>
<td><span>norm_keys()</span><span>, 9 lines</span></td>
<td><span>One report went from 51 lines with four </span><span>UNION ALL</span><span> branches to 19 lines with no partner names</span></td>
<td><span>Which partners to onboard and what the report means</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/validation_input"><span>Validate input at the door</span></a></td>
<td><span>Proper-case names, normalise phones, run an IBAN mod-97 on a 29-digit number</span></td>
<td><span>proper_name()</span><span>, </span><span>clean_email()</span><span>, </span><span>clean_phone()</span><span>, </span><span>valid_iban()</span><span>, </span><span>clean_contact()</span></td>
<td><span>460 of 2,000 rows caught, each with a list of what was wrong</span></td>
<td><span>What to do with a rejected contact: fix, call the customer, quarantine</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/deupe_data_cpu_bound_algorithm"><span>Deduplicate customers</span></a></td>
<td><span>Levenshtein distance needs a grid, and SQL has no arrays</span></td>
<td><span>lev_distance()</span><span> inside a </span><span>JOIN</span><span> predicate</span></td>
<td><span>Name equality found 21.7% of duplicates; blocking in SQL plus two distances found about 80% at 99.7% precision</span></td>
<td><span>Whether to merge two records, and how</span></td>
</tr>
<tr>
<td><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026/where_should_I_live"><span>Where should Federico live?</span></a></td>
<td><span>Iterate to a geometric median; recursive CTEs refuse aggregates</span></td>
<td><span>Weiszfeld procedure</span></td>
<td><span>11.00 s in procedural SQL vs 0.52 s in JavaScript; 327,033 statements vs 6</span></td>
<td><span>Whether Federico actually moves</span></td>
</tr>
</tbody>
</table>
<p><span>A few details are worth calling out, because they are exactly where DBAs and developers should meet.</span></p>
<p><span>Dedupe: SQL narrows, JavaScript decides the distance. Comparing 3,000 customers with 2,000 signups is six million pairs. We never let JavaScript see them all. Ordinary join predicates on city and initials cut the work to 21,352 pairs, and only those reach </span><span>lev_distance()</span><span>. The function returns a number. Merging two customers is a business decision, and it stays in the application.</span></p>
<p><span>Validation: the function reports, the policy decides. </span><span>clean_contact()</span><span> returns the cleaned values </span><i><span>and</span></i><span> an array of errors. A trigger uses it to guarantee that nothing dirty lands in </span><span>contacts</span><span>, whoever does the insert: web app, mobile app, nightly import, or someone at a </span><span>mysql</span><span> prompt at 2am. What happens next to a rejected row is still the application's job.</span></p>
<p><span>Federico: three steps, one of them JavaScript. Turning 44,347 raw stops into 16,232 weighted endpoints is two window functions, and nothing beats SQL there. Turning the resulting coordinate into a district is plain joins. JavaScript only runs the inner loop in the middle. Note that this demo uses </span><span>session.runSql()</span><span>, so today it runs on MySQL Enterprise; SQL from JavaScript is the top roadmap item for Percona Server.</span></p>
<h2><span>Where we draw the line</span></h2>
<p><span>A JavaScript routine belongs in the database when it makes a data operation possible or cheaper. It belongs in the application when it decides what the business does. In practice we use five questions before accepting a routine:</span></p>
<ol>
<li aria-level="1"><span>Does it work on values the database already holds? If it needs an external service, a file or a user session, it is application code.</span></li>
<li aria-level="1"><span>Is it deterministic, with no side effects outside the transaction? </span><span>DETERMINISTIC NO SQL</span><span> is not only a binlog requirement; it is a good design filter.</span></li>
<li aria-level="1"><span>Is it called from SQL? A good routine sits inside a </span><span>SELECT</span><span>, a </span><span>JOIN</span><span>, a </span><span>JSON_TABLE</span><span> or a trigger. If nothing in SQL calls it, ask why it is in the database.</span></li>
<li aria-level="1"><span>Is the result smaller than the input, or does it save a round trip? Two numbers out instead of 5,000 names, emails and cities is the kind of win we want.</span></li>
<li aria-level="1"><span>Would every client need the same rule? Validation that the web app, the mobile app and the importer all must apply is a data rule. Pricing for a campaign that changes next week is business policy.</span></li>
</ol>
<table>
<tbody>
<tr>
<td><span>Belongs in a JS routine</span></td>
<td><span>Stays in the application</span></td>
</tr>
<tr>
<td><span>Normalising and validating values (keys, names, phones, IBAN)</span></td>
<td><span>Workflows, approvals, notifications</span></td>
</tr>
<tr>
<td><span>Distance, similarity, checksum and parsing functions</span></td>
<td><span>Decisions based on those scores: merge, reject, refund</span></td>
</tr>
<tr>
<td><span>The inner loop of a numeric algorithm over rows SQL selected</span></td>
<td><span>Orchestration across services and external APIs</span></td>
</tr>
<tr>
<td><span>Integrity rules every writer must obey</span></td>
<td><span>UI, sessions, authentication flows</span></td>
</tr>
<tr>
<td><span>Reshaping JSON so SQL can query it</span></td>
<td><span>Business rules owned by product and changed often</span></td>
</tr>
</tbody>
</table>
<p><span>If a routine starts to need configuration tables, feature flags or calls to another system, that is our signal that it has crossed the line.</span></p>
<h2><span>Why developers should ask for it</span></h2>
<p><span>For developers this is not about giving up control of the application. It is about no longer having to pull data out of MySQL just because SQL could not express one step.</span></p>
<ul>
<li aria-level="1"><span>It is the language you already write. ECMAScript on V8, the same runtime as Chrome and Node.js. JSON columns arrive as native JS objects, no </span><span>JSON.parse()</span><span> needed.</span></li>
<li aria-level="1"><span>Fewer round trips. In the commute demo one </span><span>CALL</span><span> read 16,232 rows and wrote 2,000 answers with zero rows crossing the network. The speed-up from 11.00 s to 0.52 s came from not crossing the executor boundary 327,033 times, not from JavaScript being a faster language.</span></li>
<li aria-level="1"><span>Less personal data in motion. The dedupe query returns two numbers instead of shipping 5,000 names, emails and cities to an app server, and into its logs. Your GDPR scope stays in the database.</span></li>
<li aria-level="1"><span>One rule, written once. Validation that lives in five clients becomes four out-of-date copies. A function and a trigger are one copy nobody can bypass.</span></li>
<li aria-level="1"><span>Simpler queries in your code. </span><span>norm_keys()</span><span> turned a 51-line, partner-specific report into 19 lines. Partner five becomes a data change, not a code change.</span></li>
<li aria-level="1"><span>Pure functions are easy to test. Deterministic, no I/O, no hidden state: the same unit-testing habits you already have.</span></li>
</ul>
<h3><span>How we suggest developers and DBAs work together:</span></h3>
<ol>
<li aria-level="1"><span>The developer proposes the function, with the SQL that will call it.</span></li>
<li aria-level="1"><span>The DBA reviews it against the five questions above, and checks if the call site uses blocking or filtering so the function runs on as few rows as possible.</span></li>
<li aria-level="1"><span>Grant </span><span>CREATE_JS_ROUTINE</span><span> deliberately; it is required on top of </span><span>CREATE ROUTINE</span><span>, so who can write JavaScript is an explicit choice.</span></li>
<li aria-level="1"><span>Keep the routine in version control next to the schema migrations.</span></li>
</ol>
<p><span>SQL did not get worse, it got a companion</span></p>
<p><span>JavaScript in MySQL is not an invitation to rebuild the application inside the server. </span></p>
<p><span>It is a set of sharp tools for the few places where SQL and PL run out: recursion, arrays, big numbers and tight loops. </span><span> </span><span>The set work stays in SQL, the business logic stays in the application, and JavaScript fills the gap between them.</span></p>
<p><span>If you want to try it, </span><span>component_js_lang</span><span> is available as a tech preview in Percona Server for MySQL 8.4.5-5 and later, free and open source, while the equivalent feature from Oracle requires MySQL Enterprise. </span></p>
<p><span>You can easily use the code available here </span><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>Demo code from Percona Live Europe 2026</span></a><span> to perform some tests, just doing cut and paste. </span></p>
<p><span>Tell us what you think on the </span><a href="https://forums.percona.com/"><span>Percona Community Forum</span></a><span> or in </span><a href="https://jira.percona.com/"><span>Jira</span></a><span> with the label </span><span>PS-JS-LANG</span><span>. Your feedback decides what goes into GA, starting with SQL execution from JavaScript routines.</span></p>
<p><span>Resources</span></p>
<ul>
<li aria-level="1"><a href="https://docs.percona.com/percona-server/8.4/js-lang-overview.html"><span>Percona Server js_lang documentation</span></a></li>
<li aria-level="1"><a href="https://github.com/Tusamarco/blogs/tree/master/pleu_2026"><span>Demo code from Percona Live Europe 2026</span></a></li>
<li aria-level="1"><a href="https://github.com/percona/percona-server/tree/release-8.4.11-11/components/js_lang"><span>Source code on GitHub</span></a></li>
</ul>]]></content:encoded>
    <pubDate>Thu, 15 Oct 2026 15:04:29 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>MySQL</category>
  </item>

  <item>
    <title>Visualizing MySQL Telemetry with Grafana</title>
    <guid isPermaLink="false">6ed583627c0febbc5ca7f6bdaa813a36</guid>
    <link>https://blogs.oracle.com/mysql/visualizing-mysql-telemetry-with-grafana</link>
    <description>In the first post, we exported MySQL OpenTelemetry metrics directly to Prometheus. In the second, we used DBA questions to decide which metrics matter. This post turns those questions into a Grafana dashboard. The objective is not to display every available metric. It is to make the next investigation obvious. When workload changes, a DBA […]</description>
    <pubDate>Thu, 15 Oct 2026 04:10:08 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>Monitoring</category>
    <category>MySQL</category>
    <category>MySQL Enterprise</category>
  </item>

  <item>
    <title>Announcing VillageSQL Server 0.0.7</title>
    <guid isPermaLink="false">6abfdfed4f4d7f0001b09df8</guid>
    <link>https://villagesql.com/blog/007/</link>
    <description>We are pleased to announce the launch of VillageSQL Server 0.0.7. You now have a License to Query (please note this post contains lots of James Bond 007 database puns). In this release, the mysql-8.4 build tracks MySQL Server 8.4.11, the mysql-9.7 build tracks MySQL Server 9.7.2, and the percona-8.4 build carries Percona Server 8.4.11-11.
Highlights of this release include the following:

The server can now describe a custom index, store its data, report it, and run a nearest-neighbor search through it. This is the foundation of custom indexing and vector search we demoed at Percona Live Amsterdam 2026.
CLONE now checks that the recipient has your extensions before it copies anything.
An extension can give every connection its own copy of a setting.
Every bundled extension, and the server itself, runs under Valgrind and ThreadSanitizer every night.

Custom indexes defined by extensions
MySQL gives you B-tree, R-tree, hash, and full-text indexes, and that list is fixed. If you want a vector index, you need to wait for a future MySQL release that ships one, and MySQL Community Server doesn't ship one in any version. VillageSQL Server custom indexes let you define an index yourself as an extension. The extension supplies the index type, the storage, the build, and the search, and the server treats the result as a first-class index. This release lays the foundation for that (the Quantum of SQL).
SHOW CREATE TABLE renders a custom index as DDL, with its type, the distance profile for each column (hnsw_cosine here), and the WITH parameters the extension defined:
  KEY `idx_title` (`title_vec` `vsql_vector`.`hnsw_cosine`) USING EXTENDED(`vsql_vector`.`hnsw`) WITH (`ef_construction` = 64, `m` = 8),
  KEY `idx_body` (`body_vec` `vsql_vector`.`hnsw_l1`) USING EXTENDED(`vsql_vector`.`hnsw`),

The server doesn't interpret m or ef_construction on its own. It stores them because the extension asked it to, and prints them back as DDL you can replay. SHOW INDEX and INFORMATION_SCHEMA.STATISTICS now report the extension's index type name. The server also stores the data a custom index needs. A vector column lives outside the row, in storage the extension manages, and InnoDB now indexes it through a reference, so you can index a column far wider than the usual key-length limit.
The server can also plan a search through a custom index. The query shape is a distance function over the indexed column in ORDER BY, with a LIMIT. The server routes that query to the extension's index instead of sorting the whole table. The classic optimizer reports the access type as index_distance, and EXPLAIN FORMAT=TREE prints Custom index distance scan on idx_title. The hypergraph optimizer picks the same plan.
The server runs the whole scan: it picks the index, asks the extension for the nearest entries, and fetches each row by the primary key the index stored. That means the index has to store the primary key. The vsql-vector-pk extension does, and its HNSW index answers these queries end to end on this release. It is a variant of vsql-vector whose index stores the primary key alongside each entry.
The remaining work on custom indexing is index maintenance. On a table with a custom index, the server currently refuses an UPDATE that changes the primary key or a column in any index, ordinary indexes included, and an ALTER TABLE that changes the primary key. It also refuses a DELETE (DELETE Another Day) against an index that identifies its entries by its own handles rather than by row key, because the server can't yet tell the index which entry to drop. The HNSW indexes in vsql-vector and vsql-vector-pk both work this way, so neither extension supports DELETE yet. Other ALTER TABLE operations, including a full table rebuild, keep the index.
At Percona Live in Amsterdam on September 9 we demoed vector search in MySQL on a development build. An HNSW index defined entirely by the vsql-vector extension answered a nearest-neighbor query from the graph instead of scanning the table, and we measured recall against the same rows. The metadata, the storage, the DDL, and the server side of that scan all ship in this release.
CLONE now checks for your extensions
CLONE INSTANCE copies a running server's data into another server. We extended its compatibility checks to cover extensions, alongside the existing checks for plugins, character sets, and configuration. Extension bundles (.veb files) live outside the data directory, so a clone doesn't copy them, but the system table that records which extensions are installed is copied.
We check the donor's extension list during the handshake, before any data moves. Every extension installed on the donor has to be present in the recipient's veb_dir with a matching version and a matching .veb checksum, which is the same bar the server applies to its own extensions at startup.
This check matters beyond clone. An extension can define a column type whose stored bytes mean nothing without the extension that wrote them. The extension is part of the server's state, so every operational path that copies, moves, or upgrades a server has to account for it. Clone is the first path we covered. Backup and restore, upgrades across codebases, and dependencies between extensions come next.
Extension functions can compute their own return type
The vsql_tvector example extension defines TVECTOR(N), a fixed-width vector type. A function that joins a TVECTOR(3) and a TVECTOR(4) end to end should return a TVECTOR(7). The server's built-in type rules can't produce that answer: they copy one argument's parameters onto the return value, and they reject a call whose arguments disagree.
An extension can now register a function with bind_and_check_types, and the server calls that hook while it resolves the statement:
make_func&amp;lt;&amp;amp;tvector_concat&amp;gt;(&quot;tvector_concat&quot;)
    .bind_and_check_types&amp;lt;&amp;amp;tvector_concat_bind&amp;gt;()

The hook receives each argument's kind, the type parameters already resolved for it, and the literal text when an argument is a string constant. It returns the type parameters for the result, optionally new parameters for each argument, or an error. The hook replaces the built-in rules for that function, so a call the server would have rejected is allowed and the return type is the one the extension computed.
vsql_tvector gains 7 functions that use it, covering a computed return dimension, 2 that fix the dimension and inherit the element type, 1 that inherits the dimension and fixes the element type, 2 reductions to a scalar, and an accessor. You can use these as examples when building your own extension.
Per-session extension settings
An extension can declare per-session variables through the new vsql::session_var capability. Every connection holds its own value, SET SESSION changes it, and the value the extension declared is the global default. INT and STR are supported. Extension system variables, which hold one value for the whole server, work as they did before.
The vector work is why this exists. HNSW search has a dial that trades recall against speed, and that choice belongs to the query, so vsql-vector declares the dial per session:
SET SESSION vsql_vector.ef_search = 100;

For vsql-vector, the default is 40, and a value outside 1 to 65536 is clamped to the nearest bound. Turn it up and the search reads more of the graph, returns better answers, and takes longer. One connection can run an analytics query at 400 while every other connection stays at the default. A function reads the calling session's value through int_var(). The capability is preview, so it needs vsql_allow_preview_extensions=ON.
System variables with a fixed list of values
For Your Enums Only. A vsql::sys_var variable can now be an enum: a fixed list of names, stored as an index. The server validates the name at SET time and rejects anything off the list, so the extension no longer has to check a free-form string after the fact.
Statement events identify the client
The Spy Who Logged Me. The vsql::preview::statement_event capability now reports the client process id, the connector library, and the application name, each NULL when the client didn't send it. If you're trying to be The Man with the Golden Query and writing per-query telemetry, that's what attributes a slow query to the service that ran it.
Breaking changes
DDL that the server used to accept can now be rejected. The server validates type parameters before it hands them to an extension, and for the TYPE, TYPE(N), and TYPE('k=v,...') forms it rejects a duplicate parameter name, a nameless parameter, and a parameter with no value. After the extension's resolve_params callback returns, the server also rejects a rewrite that dropped a parameter. Adding parameters and changing values is still legal. Check your DDL and your extensions against these rules before you upgrade.
The preview authentication capability changed too. auto_grant is gone, replaced by roles_mode, which tells the server whether to activate, grant, or sync the roles an identity provider supplies. An authentication extension built against the 0.0.6 preview needs a source change to compile. vsql-oauth2 already has, and its own vsql_oauth2.auto_grant setting still exists.
Strengthening testing
Tomorrow Never Deadlocks. Every night we run the unit tests, the full VillageSQL test suite, and every bundled extension under both Valgrind memcheck and ThreadSanitizer, so the server and the extensions you get with a release have been through both. We also used gcov to find code the suites never reached and wrote tests for it, and moving our CI runners to Ubuntu 26.04 turned up undefined behavior in our type-parameter parsing along with an upstream bug in the Group Replication plugin.
New extensions
Three extensions joined this cycle:

vsql-duckdb (From Amsterdam with Select): query Parquet, CSV, and JSON files from an ordinary MySQL connection, without exporting the data first or standing up a second query engine. It embeds DuckDB in the server process and reads the files where they already live, in an s3:// bucket or behind an https:// URL. Reading files off the server's own disk works too, once you turn on vsql_duckdb.allow_local_files. It's a port of the PostgreSQL extension pg_duckdb, narrowed to what the extension framework supports today. It uses preview capabilities, so it needs vsql_allow_preview_extensions=ON, and you build it from source. It also needs a server newer than 0.0.6, so this is the first release it runs on.
vsql-vector (Dr. No-Scan): the SVECTOR vector type, stored as packed floats in InnoDB pages through the column storage interface, the distance functions to go with it, and the HNSW index type the demo ran on. Its index does not store a row reference, so on this release the server builds and keeps it current on INSERT but cannot answer a query through it. It needs vsql_allow_preview_extensions=ON, and you build it from source.
vsql-vector-pk (The Vector Who Loved Me): the same SVECTOR type and HNSW index, with the primary key stored alongside each entry. That is what the nearest-neighbor scan described above needs, so this is the variant that answers queries on this release. A server runs either vsql-vector or vsql-vector-pk, never both. It needs vsql_allow_preview_extensions=ON, and you build it from source.

What's ahead
The next release is more hardening and production readiness. We're testing VillageSQL against popular ORMs, the JDBC and ODBC connectors, the Windows client, and backup and restore. We're building performance tests for a server running extensions, so that when someone asks what an extension costs in terms of performance we have a number instead of an opinion. Custom indexes carry on in parallel, on the query side and the write side, until an extension's index runs end to end on a released server without the preview flag.
Special thanks
The native RPM and DEB packaging in this release came from Evgeniy Patlan at Percona. The packages take over an existing MySQL install (On Her Majesty's Secret Server) rather than sitting beside one, sharing its binary names, data directory, systemd unit, and port, and the build dependencies resolve on EL8, EL9, EL10, Ubuntu 24.04, Ubuntu 26.04, Debian 12, and Debian 13. No workflow publishes them yet, so the release artifacts are unchanged for now, but the sources and the build scripts are in the tree.
There's more in this release than fits here. The release notes carry the per-PR detail. Our roadmap is published on GitHub, and we'd like to hear what you're building and what we should work on next, either on GitHub or on Discord. If you want to support VillageSQL, please star the repo.</description>
    <content:encoded><![CDATA[<img src="https://storage.ghost.io/c/db/c7/dbc78ca4-dcfe-468b-b2fa-030396d2a98e/content/images/2026/10/Gemini_Generated_Image_1ppnm1ppnm1ppnm1.jpeg" alt="Announcing VillageSQL Server 0.0.7"><p>We are pleased to announce the launch of VillageSQL Server 0.0.7. You now have a <em>License to Query</em> (please note this post contains lots of James Bond 007 database puns). In this release, the <code>mysql-8.4</code> build tracks MySQL Server 8.4.11, the <code>mysql-9.7</code> build tracks MySQL Server 9.7.2, and the <code>percona-8.4</code> build carries Percona Server 8.4.11-11.</p>
<p>Highlights of this release include the following:</p>
<ul>
<li>The server can now describe a custom index, store its data, report it, and run a nearest-neighbor search through it. This is the foundation of custom indexing and vector search we <a href="https://villagesql.com/blog/vector-search-hnsw/">demoed at Percona Live Amsterdam 2026</a>.</li>
<li><code>CLONE</code> now checks that the recipient has your extensions before it copies anything.</li>
<li>An extension can give every connection its own copy of a setting.</li>
<li>Every bundled extension, and the server itself, runs under Valgrind and ThreadSanitizer every night.</li>
</ul>
<h2>Custom indexes defined by extensions</h2>
<p>MySQL gives you B-tree, R-tree, hash, and full-text indexes, and that list is fixed. If you want a vector index, you need to wait for a future MySQL release that ships one, and MySQL Community Server doesn't ship one in any version. VillageSQL Server custom indexes let you define an index yourself as an extension. The extension supplies the index type, the storage, the build, and the search, and the server treats the result as a first-class index. This release lays the foundation for that (the <em>Quantum of SQL</em>).</p>
<p><code>SHOW CREATE TABLE</code> renders a custom index as DDL, with its type, the distance profile for each column (<code>hnsw_cosine</code> here), and the <code>WITH</code> parameters the extension defined:</p>
<pre><code class="language-sql">  KEY `idx_title` (`title_vec` `vsql_vector`.`hnsw_cosine`) USING EXTENDED(`vsql_vector`.`hnsw`) WITH (`ef_construction` = 64, `m` = 8),
  KEY `idx_body` (`body_vec` `vsql_vector`.`hnsw_l1`) USING EXTENDED(`vsql_vector`.`hnsw`),
</code></pre>
<p>The server doesn't interpret <code>m</code> or <code>ef_construction</code> on its own. It stores them because the extension asked it to, and prints them back as DDL you can replay. <code>SHOW INDEX</code> and <code>INFORMATION_SCHEMA.STATISTICS</code> now report the extension's index type name. The server also stores the data a custom index needs. A vector column lives outside the row, in storage the extension manages, and InnoDB now indexes it through a reference, so you can index a column far wider than the usual key-length limit.</p>
<p>The server can also plan a search through a custom index. The query shape is a distance function over the indexed column in <code>ORDER BY</code>, with a <code>LIMIT</code>. The server routes that query to the extension's index instead of sorting the whole table. The classic optimizer reports the access type as <code>index_distance</code>, and <code>EXPLAIN FORMAT=TREE</code> prints <code>Custom index distance scan on idx_title</code>. The hypergraph optimizer picks the same plan.</p>
<p>The server runs the whole scan: it picks the index, asks the extension for the nearest entries, and fetches each row by the primary key the index stored. That means the index has to store the primary key. The <a href="https://github.com/villagesql/vsql-vector-pk">vsql-vector-pk</a> extension does, and its HNSW index answers these queries end to end on this release. It is a variant of <a href="https://github.com/villagesql/vsql-vector">vsql-vector</a> whose index stores the primary key alongside each entry.</p>
<p>The remaining work on custom indexing is index maintenance. On a table with a custom index, the server currently refuses an <code>UPDATE</code> that changes the primary key or a column in any index, ordinary indexes included, and an <code>ALTER TABLE</code> that changes the primary key. It also refuses a <code>DELETE</code> (<em>DELETE Another Day</em>) against an index that identifies its entries by its own handles rather than by row key, because the server can't yet tell the index which entry to drop. The HNSW indexes in vsql-vector and vsql-vector-pk both work this way, so neither extension supports <code>DELETE</code> yet. Other <code>ALTER TABLE</code> operations, including a full table rebuild, keep the index.</p>
<p>At Percona Live in Amsterdam on September 9 we demoed <a href="https://villagesql.com/blog/vector-search-hnsw/">vector search in MySQL</a> on a development build. An HNSW index defined entirely by the <a href="https://github.com/villagesql/vsql-vector">vsql-vector</a> extension answered a nearest-neighbor query from the graph instead of scanning the table, and we measured recall against the same rows. The metadata, the storage, the DDL, and the server side of that scan all ship in this release.</p>
<h2>CLONE now checks for your extensions</h2>
<p><code>CLONE INSTANCE</code> copies a running server's data into another server. We extended its compatibility checks to cover extensions, alongside the existing checks for plugins, character sets, and configuration. Extension bundles (<code>.veb</code> files) live outside the data directory, so a clone doesn't copy them, but the system table that records which extensions are installed is copied.</p>
<p>We check the donor's extension list during the handshake, before any data moves. Every extension installed on the donor has to be present in the recipient's <code>veb_dir</code> with a matching version and a matching <code>.veb</code> checksum, which is the same bar the server applies to its own extensions at startup.</p>
<p>This check matters beyond clone. An extension can define a column type whose stored bytes mean nothing without the extension that wrote them. The extension is part of the server's state, so every operational path that copies, moves, or upgrades a server has to account for it. Clone is the first path we covered. Backup and restore, upgrades across codebases, and dependencies between extensions come next.</p>
<h2>Extension functions can compute their own return type</h2>
<p>The <code>vsql_tvector</code> example extension defines <code>TVECTOR(N)</code>, a fixed-width vector type. A function that joins a <code>TVECTOR(3)</code> and a <code>TVECTOR(4)</code> end to end should return a <code>TVECTOR(7)</code>. The server's built-in type rules can't produce that answer: they copy one argument's parameters onto the return value, and they reject a call whose arguments disagree.</p>
<p>An extension can now register a function with <code>bind_and_check_types</code>, and the server calls that hook while it resolves the statement:</p>
<pre><code class="language-cpp">make_func&lt;&amp;tvector_concat&gt;("tvector_concat")
    .bind_and_check_types&lt;&amp;tvector_concat_bind&gt;()
</code></pre>
<p>The hook receives each argument's kind, the type parameters already resolved for it, and the literal text when an argument is a string constant. It returns the type parameters for the result, optionally new parameters for each argument, or an error. The hook replaces the built-in rules for that function, so a call the server would have rejected is allowed and the return type is the one the extension computed.</p>
<p><code>vsql_tvector</code> gains 7 functions that use it, covering a computed return dimension, 2 that fix the dimension and inherit the element type, 1 that inherits the dimension and fixes the element type, 2 reductions to a scalar, and an accessor. You can use these as examples when building your own extension.</p>
<h2>Per-session extension settings</h2>
<p>An extension can declare per-session variables through the new <code>vsql::session_var</code> capability. Every connection holds its own value, <code>SET SESSION</code> changes it, and the value the extension declared is the global default. <code>INT</code> and <code>STR</code> are supported. Extension system variables, which hold one value for the whole server, work as they did before.</p>
<p>The vector work is why this exists. HNSW search has a dial that trades recall against speed, and that choice belongs to the query, so <code>vsql-vector</code> declares the dial per session:</p>
<pre><code class="language-sql">SET SESSION vsql_vector.ef_search = 100;
</code></pre>
<p>For vsql-vector, the default is 40, and a value outside 1 to 65536 is clamped to the nearest bound. Turn it up and the search reads more of the graph, returns better answers, and takes longer. One connection can run an analytics query at 400 while every other connection stays at the default. A function reads the calling session's value through <code>int_var()</code>. The capability is preview, so it needs <code>vsql_allow_preview_extensions=ON</code>.</p>
<h2>System variables with a fixed list of values</h2>
<p><em>For Your Enums Only</em>. A <code>vsql::sys_var</code> variable can now be an enum: a fixed list of names, stored as an index. The server validates the name at <code>SET</code> time and rejects anything off the list, so the extension no longer has to check a free-form string after the fact.</p>
<h2>Statement events identify the client</h2>
<p><em>The Spy Who Logged Me.</em> The <code>vsql::preview::statement_event</code> capability now reports the client process id, the connector library, and the application name, each <code>NULL</code> when the client didn't send it. If you're trying to be <em>The Man with the Golden Query</em> and writing per-query telemetry, that's what attributes a slow query to the service that ran it.</p>
<h2>Breaking changes</h2>
<p>DDL that the server used to accept can now be rejected. The server validates type parameters before it hands them to an extension, and for the <code>TYPE</code>, <code>TYPE(N)</code>, and <code>TYPE('k=v,...')</code> forms it rejects a duplicate parameter name, a nameless parameter, and a parameter with no value. After the extension's <code>resolve_params</code> callback returns, the server also rejects a rewrite that dropped a parameter. Adding parameters and changing values is still legal. Check your DDL and your extensions against these rules before you upgrade.</p>
<p>The preview authentication capability changed too. <code>auto_grant</code> is gone, replaced by <code>roles_mode</code>, which tells the server whether to activate, grant, or sync the roles an identity provider supplies. An authentication extension built against the 0.0.6 preview needs a source change to compile. <code>vsql-oauth2</code> already has, and its own <code>vsql_oauth2.auto_grant</code> setting still exists.</p>
<h2>Strengthening testing</h2>
<p><em>Tomorrow Never Deadlocks.</em> Every night we run the unit tests, the full VillageSQL test suite, and every bundled extension under both Valgrind memcheck and ThreadSanitizer, so the server and the extensions you get with a release have been through both. We also used gcov to find code the suites never reached and wrote tests for it, and moving our CI runners to Ubuntu 26.04 turned up undefined behavior in our type-parameter parsing along with an upstream bug in the Group Replication plugin.</p>
<h2>New extensions</h2>
<p>Three extensions joined this cycle:</p>
<ul>
<li><a href="https://github.com/villagesql/vsql-duckdb"><strong>vsql-duckdb</strong></a> (<em>From Amsterdam with Select</em>): query Parquet, CSV, and JSON files from an ordinary MySQL connection, without exporting the data first or standing up a second query engine. It embeds DuckDB in the server process and reads the files where they already live, in an <code>s3://</code> bucket or behind an <code>https://</code> URL. Reading files off the server's own disk works too, once you turn on <code>vsql_duckdb.allow_local_files</code>. It's a port of the PostgreSQL extension <code>pg_duckdb</code>, narrowed to what the extension framework supports today. It uses preview capabilities, so it needs <code>vsql_allow_preview_extensions=ON</code>, and you build it from source. It also needs a server newer than 0.0.6, so this is the first release it runs on.</li>
<li><a href="https://github.com/villagesql/vsql-vector"><strong>vsql-vector</strong></a> (<em>Dr. No-Scan</em>): the <code>SVECTOR</code> vector type, stored as packed floats in InnoDB pages through the column storage interface, the distance functions to go with it, and the HNSW index type the demo ran on. Its index does not store a row reference, so on this release the server builds and keeps it current on <code>INSERT</code> but cannot answer a query through it. It needs <code>vsql_allow_preview_extensions=ON</code>, and you build it from source.</li>
<li><a href="https://github.com/villagesql/vsql-vector-pk"><strong>vsql-vector-pk</strong></a> (<em>The Vector Who Loved Me</em>): the same <code>SVECTOR</code> type and HNSW index, with the primary key stored alongside each entry. That is what the nearest-neighbor scan described above needs, so this is the variant that answers queries on this release. A server runs either vsql-vector or vsql-vector-pk, never both. It needs <code>vsql_allow_preview_extensions=ON</code>, and you build it from source.</li>
</ul>
<h2>What's ahead</h2>
<p>The next release is more hardening and production readiness. We're testing VillageSQL against popular ORMs, the JDBC and ODBC connectors, the Windows client, and backup and restore. We're building performance tests for a server running extensions, so that when someone asks what an extension costs in terms of performance we have a number instead of an opinion. Custom indexes carry on in parallel, on the query side and the write side, until an extension's index runs end to end on a released server without the preview flag.</p>
<h2>Special thanks</h2>
<p>The native RPM and DEB packaging in this release came from <a href="https://github.com/EvgeniyPatlan">Evgeniy Patlan</a> at Percona. The packages take over an existing MySQL install (<em>On Her Majesty's Secret Server</em>) rather than sitting beside one, sharing its binary names, data directory, systemd unit, and port, and the build dependencies resolve on EL8, EL9, EL10, Ubuntu 24.04, Ubuntu 26.04, Debian 12, and Debian 13. No workflow publishes them yet, so the release artifacts are unchanged for now, but the sources and the build scripts are in the tree.</p>
<p>There's more in this release than fits here. The <a href="https://github.com/villagesql/villagesql-server/blob/main/Docs/release_notes/release_note_0_0_7.md">release notes</a> carry the per-PR detail. Our roadmap is published on <a href="https://github.com/orgs/villagesql/projects/1/views/4">GitHub</a>, and we'd like to hear what you're building and what we should work on next, either on <a href="https://github.com/villagesql/villagesql-server/issues">GitHub</a> or on <a href="https://discord.gg/KSr6whd3Fr">Discord</a>. If you want to support VillageSQL, <a href="https://github.com/villagesql/villagesql-server">please star the repo</a>.</p>]]></content:encoded>
    <pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
    <dc:creator>VillageSQL</dc:creator>
    <category>MySQL</category>
    <category>VillageSQL</category>
    <category>Open Source</category>
  </item>

  <item>
    <title>PHP và MySQL: 7 câu hỏi dev mới hay hỏi nhất</title>
    <guid isPermaLink="false">https://dbahire.com/php-va-mysql-7-cau-hoi-dev-moi-hay-hoi-nhat</guid>
    <link>https://dbahire.com/php-va-mysql-7-cau-hoi-dev-moi-hay-hoi-nhat</link>
    <description>Khi bắt đầu làm một website có tính năng động, gần như lúc nào bạn cũng sẽ phải đụng đến PHP và MySQL. Đây là phần lập trình xử lý logic và phần lưu trữ dữ liệu. Bài viết này tổng hợp top 7 câu hỏi thường gặp về PHP và MySQL mà người mới hay hỏi nhất.
PHP là phần xử lý logic chạy trên máy chủ. MySQL là nơi lưu trữ dữ liệu dưới dạng các bảng có liên kết với nhau. Chức năng đăng nhập, giỏ hàng, danh sách bài viết hay sản phẩm phần lớn là kết quả của việc hai thành phần này phối hợp.
Chúng tôi nhận thấy người mới thường hỏi gần như cùng một nhóm câu hỏi. Dưới đây là phần giải đáp cụ thể. Nếu bạn đang tự học, làm đồ án, hoặc là chủ shop nhỏ đang tự dựng website, phần này sẽ giúp bạn có nền tảng vững hơn.
PHP và MySQL khác nhau thế nào, có cần học cả hai không?
PHP là ngôn ngữ lập trình, dùng để viết các quyết định trong chương trình. Ví dụ: kiểm tra mật khẩu có đúng hay không, tính tổng tiền giỏ hàng, hay quyết định hiển thị nội dung nào cho người truy cập.
MySQL là hệ quản trị cơ sở dữ liệu, chuyên giữ dữ liệu và trả về đúng thứ bạn yêu cầu. Nếu chỉ làm trang giới thiệu tĩnh, ít thay đổi, bạn chưa cần MySQL. Khi có người đăng nhập hoặc bài viết cần thêm, sửa, xóa, bạn cần chỗ lưu dữ liệu đó. Với phần lớn dự án, học cả hai là cần thiết.
Nên chọn phiên bản PHP nào cho dự án hiện tại?
Tiêu chí quan trọng nhất là phiên bản đó còn được nhà phát triển vá lỗi bảo mật hay không. Khi phiên bản ngừng hỗ trợ, lỗ hổng mới sẽ không còn bản sửa chính thức. Tiêu chí thứ hai là thư viện bạn định dùng có tương thích với phiên bản đó không.
Danh sách phiên bản được hỗ trợ thay đổi theo thời gian, nên hãy kiểm tra trên trang chính thức của PHP trước khi quyết định. Đừng chỉ tin một bài viết cũ, kể cả bài này chỉ là điểm khởi đầu để bạn đối chiếu.
Đừng quên kiểm tra hosting. Một số gói giá rẻ chỉ hỗ trợ phiên bản khá thấp. Kiểm tra trước khi cài đặt sẽ tránh phiền phức khi nâng cấp về sau.
Kết nối PHP với MySQL sao cho an toàn?
Có hai cách kết nối phổ biến: PDO và MySQLi. PDO làm việc được với nhiều loại cơ sở dữ liệu, còn MySQLi chỉ dành cho MySQL. Với người mới, chúng tôi thường gợi ý PDO vì kiến thức này dễ mang sang dự án khác.
Quan trọng hơn cả là không ghép trực tiếp dữ liệu người dùng nhập vào câu lệnh SQL. Hãy dùng câu lệnh chuẩn bị sẵn (prepared statement) để tách lệnh khỏi dữ liệu. Thông tin kết nối như tên đăng nhập và mật khẩu cơ sở dữ liệu nên đặt trong tệp cấu hình nằm ngoài thư mục công khai.
Vì sao SQL Injection nguy hiểm và phòng tránh ra sao?
SQL Injection là kiểu tấn công mà kẻ xấu nhập mã SQL vào các ô nhập liệu như ô tìm kiếm hoặc form đăng nhập. Nếu chương trình ghép thẳng chuỗi đó vào truy vấn mà không kiểm tra, dữ liệu có thể bị đọc, sửa hoặc xóa.
Cách xử lý cốt lõi là dùng prepared statement và kiểm tra kiểu dữ liệu đầu vào. Ô nhập số lượng chỉ nhận số, còn ô email cần kiểm tra đúng định dạng. Bên cạnh đó, tài khoản kết nối cơ sở dữ liệu chỉ nên có những quyền thật sự cần thiết.
Thiết kế bảng MySQL cho shop nhỏ thế nào cho gọn?
Với shop nhỏ, bạn nên tách riêng bảng người dùng, bảng sản phẩm và bảng đơn hàng. Các bảng liên kết với nhau qua khóa ngoại, ví dụ mỗi đơn hàng trỏ về đúng một người mua. Cách này giúp tránh lặp thông tin và giúp việc sửa đổi chỉ cần làm ở một chỗ.
Đừng chuẩn hóa quá mức khi quy mô còn nhỏ. Thiết kế quá phức tạp sẽ làm việc viết truy vấn khó hơn. Chuẩn hóa vừa phải là đủ, và khi cửa hàng mở rộng bạn có thể điều chỉnh tiếp.
Website chậm vì truy vấn MySQL, nên xử lý từ đâu?
Điều đầu tiên là đừng đoán mò. Hãy bật slow query log để ghi lại các truy vấn chạy chậm, rồi xem xét từng câu. Thường thì thêm chỉ mục (index) cho các cột xuất hiện trong mệnh đề WHERE hoặc JOIN sẽ giúp cải thiện.
Một lỗi phổ biến là dùng SELECT * khi chỉ cần vài cột. Chỉ chọn đúng cột cần thiết cũng góp phần cải thiện tốc độ. Hãy đo lại sau mỗi lần chỉnh sửa để biết hiệu quả thực tế, vì kết quả phụ thuộc vào lượng dữ liệu và cấu hình máy chủ.
So sánh PDO, MySQLi và mysql_ cho người mới
Khi tìm tài liệu cũ, bạn sẽ gặp cả ba cách này. Dưới đây là tóm tắt nhanh:

mysql_: cách cũ, đã ngừng sử dụng và không hỗ trợ prepared statement. Không nên dùng cho dự án mới.
MySQLi: hỗ trợ prepared statement, phù hợp khi dự án chỉ dùng MySQL.
PDO: hỗ trợ prepared statement tốt và dùng được với nhiều hệ quản trị, thường là lựa chọn dễ mang theo kiến thức nhất cho người mới.

Quan trọng hơn là chọn một cách, dùng đúng và giữ nhất quán trong toàn bộ dự án.
Câu hỏi thường gặp
PHP và MySQL có miễn phí cho dự án thương mại không?
Cả hai đều là mã nguồn mở, bạn có thể dùng cho dự án kinh doanh. Tuy nhiên, công cụ đi kèm hoặc dịch vụ hosting có thể tính phí, nên hãy đọc kỹ giấy phép từng thành phần.
Hosting giá rẻ có chạy tốt PHP và MySQL không?
Đa số hosting phổ thông đều hỗ trợ PHP và MySQL. Cần để ý phiên bản PHP và giới hạn tài nguyên. Với website nhỏ thường đủ dùng, khi lượng truy cập tăng bạn sẽ cần nâng cấp gói.
Nên học MySQL trước hay PHP trước?
Người hoàn toàn mới nên học PHP căn bản trước để thấy dữ liệu được lấy ra và hiển thị như thế nào, từ đó hiểu MySQL nhanh hơn.
Nếu chưa biết bắt đầu từ đâu, hãy chọn bài toán nhỏ như trang đăng nhập hoặc danh sách sản phẩm rồi tự làm thử. Để làm quen công cụ quản trị, bạn có thể đọc tổng quan về hệ quản trị cơ sở dữ liệu MySQL và MySQL Workbench là gì và vì sao nên cài đặt. Nếu cần đơn vị tư vấn để dựng website đúng chuẩn, bạn có thể tham khảo dotnetguru.org.
The post PHP và MySQL: 7 câu hỏi dev mới hay hỏi nhất appeared first on DBAhire.</description>
    <content:encoded><![CDATA[<p>Khi bắt đầu làm một website có tính năng động, gần như lúc nào bạn cũng sẽ phải đụng đến PHP và MySQL. Đây là phần lập trình xử lý logic và phần lưu trữ dữ liệu. Bài viết này tổng hợp <strong>top 7 câu hỏi thường gặp về PHP và MySQL</strong> mà người mới hay hỏi nhất.</p>
<p>PHP là phần xử lý logic chạy trên máy chủ. MySQL là nơi lưu trữ dữ liệu dưới dạng các bảng có liên kết với nhau. Chức năng đăng nhập, giỏ hàng, danh sách bài viết hay sản phẩm phần lớn là kết quả của việc hai thành phần này phối hợp.</p>
<p>Chúng tôi nhận thấy người mới thường hỏi gần như cùng một nhóm câu hỏi. Dưới đây là phần giải đáp cụ thể. Nếu bạn đang tự học, làm đồ án, hoặc là chủ shop nhỏ đang tự dựng website, phần này sẽ giúp bạn có nền tảng vững hơn.</p>
<h2>PHP và MySQL khác nhau thế nào, có cần học cả hai không?</h2>
<p>PHP là ngôn ngữ lập trình, dùng để viết các quyết định trong chương trình. Ví dụ: kiểm tra mật khẩu có đúng hay không, tính tổng tiền giỏ hàng, hay quyết định hiển thị nội dung nào cho người truy cập.</p>
<p>MySQL là hệ quản trị cơ sở dữ liệu, chuyên giữ dữ liệu và trả về đúng thứ bạn yêu cầu. Nếu chỉ làm trang giới thiệu tĩnh, ít thay đổi, bạn chưa cần MySQL. Khi có người đăng nhập hoặc bài viết cần thêm, sửa, xóa, bạn cần chỗ lưu dữ liệu đó. Với phần lớn dự án, học cả hai là cần thiết.</p>
<h2>Nên chọn phiên bản PHP nào cho dự án hiện tại?</h2>
<p>Tiêu chí quan trọng nhất là phiên bản đó còn được nhà phát triển vá lỗi bảo mật hay không. Khi phiên bản ngừng hỗ trợ, lỗ hổng mới sẽ không còn bản sửa chính thức. Tiêu chí thứ hai là thư viện bạn định dùng có tương thích với phiên bản đó không.</p>
<p>Danh sách phiên bản được hỗ trợ thay đổi theo thời gian, nên hãy kiểm tra trên <a href="https://www.php.net/">trang chính thức của PHP</a> trước khi quyết định. Đừng chỉ tin một bài viết cũ, kể cả bài này chỉ là điểm khởi đầu để bạn đối chiếu.</p>
<p>Đừng quên kiểm tra hosting. Một số gói giá rẻ chỉ hỗ trợ phiên bản khá thấp. Kiểm tra trước khi cài đặt sẽ tránh phiền phức khi nâng cấp về sau.</p>
<h2>Kết nối PHP với MySQL sao cho an toàn?</h2>
<p>Có hai cách kết nối phổ biến: PDO và MySQLi. PDO làm việc được với nhiều loại cơ sở dữ liệu, còn MySQLi chỉ dành cho MySQL. Với người mới, chúng tôi thường gợi ý PDO vì kiến thức này dễ mang sang dự án khác.</p>
<p>Quan trọng hơn cả là không ghép trực tiếp dữ liệu người dùng nhập vào câu lệnh SQL. Hãy dùng câu lệnh chuẩn bị sẵn (prepared statement) để tách lệnh khỏi dữ liệu. Thông tin kết nối như tên đăng nhập và mật khẩu cơ sở dữ liệu nên đặt trong tệp cấu hình nằm ngoài thư mục công khai.</p>
<h2>Vì sao SQL Injection nguy hiểm và phòng tránh ra sao?</h2>
<p>SQL Injection là kiểu tấn công mà kẻ xấu nhập mã SQL vào các ô nhập liệu như ô tìm kiếm hoặc form đăng nhập. Nếu chương trình ghép thẳng chuỗi đó vào truy vấn mà không kiểm tra, dữ liệu có thể bị đọc, sửa hoặc xóa.</p>
<p>Cách xử lý cốt lõi là dùng prepared statement và kiểm tra kiểu dữ liệu đầu vào. Ô nhập số lượng chỉ nhận số, còn ô email cần kiểm tra đúng định dạng. Bên cạnh đó, tài khoản kết nối cơ sở dữ liệu chỉ nên có những quyền thật sự cần thiết.</p>
<h2>Thiết kế bảng MySQL cho shop nhỏ thế nào cho gọn?</h2>
<p>Với shop nhỏ, bạn nên tách riêng bảng người dùng, bảng sản phẩm và bảng đơn hàng. Các bảng liên kết với nhau qua khóa ngoại, ví dụ mỗi đơn hàng trỏ về đúng một người mua. Cách này giúp tránh lặp thông tin và giúp việc sửa đổi chỉ cần làm ở một chỗ.</p>
<p>Đừng chuẩn hóa quá mức khi quy mô còn nhỏ. Thiết kế quá phức tạp sẽ làm việc viết truy vấn khó hơn. Chuẩn hóa vừa phải là đủ, và khi cửa hàng mở rộng bạn có thể điều chỉnh tiếp.</p>
<h2>Website chậm vì truy vấn MySQL, nên xử lý từ đâu?</h2>
<p>Điều đầu tiên là đừng đoán mò. Hãy bật slow query log để ghi lại các truy vấn chạy chậm, rồi xem xét từng câu. Thường thì thêm chỉ mục (index) cho các cột xuất hiện trong mệnh đề WHERE hoặc JOIN sẽ giúp cải thiện.</p>
<p>Một lỗi phổ biến là dùng SELECT * khi chỉ cần vài cột. Chỉ chọn đúng cột cần thiết cũng góp phần cải thiện tốc độ. Hãy đo lại sau mỗi lần chỉnh sửa để biết hiệu quả thực tế, vì kết quả phụ thuộc vào lượng dữ liệu và cấu hình máy chủ.</p>
<h2>So sánh PDO, MySQLi và mysql_ cho người mới</h2>
<p>Khi tìm tài liệu cũ, bạn sẽ gặp cả ba cách này. Dưới đây là tóm tắt nhanh:</p>
<ul>
<li><strong>mysql_</strong>: cách cũ, đã ngừng sử dụng và không hỗ trợ prepared statement. Không nên dùng cho dự án mới.</li>
<li><strong>MySQLi</strong>: hỗ trợ prepared statement, phù hợp khi dự án chỉ dùng MySQL.</li>
<li><strong>PDO</strong>: hỗ trợ prepared statement tốt và dùng được với nhiều hệ quản trị, thường là lựa chọn dễ mang theo kiến thức nhất cho người mới.</li>
</ul>
<p>Quan trọng hơn là chọn một cách, dùng đúng và giữ nhất quán trong toàn bộ dự án.</p>
<h2>Câu hỏi thường gặp</h2>
<h3>PHP và MySQL có miễn phí cho dự án thương mại không?</h3>
<p>Cả hai đều là mã nguồn mở, bạn có thể dùng cho dự án kinh doanh. Tuy nhiên, công cụ đi kèm hoặc dịch vụ hosting có thể tính phí, nên hãy đọc kỹ giấy phép từng thành phần.</p>
<h3>Hosting giá rẻ có chạy tốt PHP và MySQL không?</h3>
<p>Đa số hosting phổ thông đều hỗ trợ PHP và MySQL. Cần để ý phiên bản PHP và giới hạn tài nguyên. Với website nhỏ thường đủ dùng, khi lượng truy cập tăng bạn sẽ cần nâng cấp gói.</p>
<h3>Nên học MySQL trước hay PHP trước?</h3>
<p>Người hoàn toàn mới nên học PHP căn bản trước để thấy dữ liệu được lấy ra và hiển thị như thế nào, từ đó hiểu MySQL nhanh hơn.</p>
<p>Nếu chưa biết bắt đầu từ đâu, hãy chọn bài toán nhỏ như trang đăng nhập hoặc danh sách sản phẩm rồi tự làm thử. Để làm quen công cụ quản trị, bạn có thể đọc <a href="https://dbahire.com/tong-quan-ve-he-quan-tri-co-so-du-lieu-mysql">tổng quan về hệ quản trị cơ sở dữ liệu MySQL</a> và <a href="https://dbahire.com/mysql-workbench-la-gi-tai-sao-phai-cai-dat-mysql-workbench">MySQL Workbench là gì và vì sao nên cài đặt</a>. Nếu cần đơn vị tư vấn để dựng website đúng chuẩn, bạn có thể tham khảo <a href="https://dotnetguru.org/">dotnetguru.org</a>.</p>
<p>The post <a rel="nofollow" href="https://dbahire.com/php-va-mysql-7-cau-hoi-dev-moi-hay-hoi-nhat">PHP và MySQL: 7 câu hỏi dev mới hay hỏi nhất</a> appeared first on <a rel="nofollow" href="https://dbahire.com/">DBAhire</a>.</p>]]></content:encoded>
    <pubDate>Wed, 14 Oct 2026 09:20:32 +0000</pubDate>
    <dc:creator>Jaime Crespo</dc:creator>
  </item>

  <item>
    <title>How fast is vector search inside MySQL?</title>
    <guid isPermaLink="false">https://askdba.net/?p=3009</guid>
    <link>https://askdba.net/how-fast-is-vector-search-inside-mysql/</link>
    <description>





Benchmarks · HNSW vector search

MyVector adds approximate nearest-neighbor search to stock MySQL — no separate vector database. We put it on identical hardware across three MySQL versions, added MariaDB’s native vector type as a reference, and measured build time, throughput, latency, and recall on the same data.

askdba/myvector · live dashboard at bench.myvector.online


  548peak ANN queries/sec (MySQL 9.7), ~1.8 ms median
  0.95best recall@10 (MariaDB 12.3) once tuned
  4×engines, one 50k×128 workload, identical 4 vCPU / 24 GB slices


Why benchmark this at all
Vector search usually means standing up a dedicated database next to your primary store, then keeping the two in sync. MyVector takes the other path: it lives inside MySQL as UDFs and stored procedures, builds an HNSW index over a column of vectors, and answers nearest-neighbor queries in SQL. The obvious question is whether that convenience costs you speed or accuracy.
To answer it honestly we needed a level field. Every engine runs in its own container on one Ampere A1 host, each pinned to the same 4 vCPU / 24 GB slice, loading the same 50,000 synthetic 128-dimensional vectors (seeded, so every engine indexes identical data), and builds an HNSW index with the same parameters (M=16, ef_construction=200, L2 distance). Three of the four are MySQL running MyVector — versions 8.4, 9.7 and 26.7. The fourth is MariaDB 12.3, which ships its own native VECTOR type, included as a cross-engine reference point.

What each number means
Five measurements, each isolating one thing:

Index build — wall-clock seconds to build the HNSW graph over all 50k rows.
Insert throughput — rows/sec written into a live, indexed table.
ANN QPS &amp;amp; latency — approximate nearest-neighbor queries per second through the index, and their median response time.
Recall@10 — of the true 10 nearest neighbors (found by exact brute-force search), how many the approximate search actually returns, averaged over held-out queries that were never inserted. 1.0 is perfect; 0.82 means it finds ~8 of the real 10.
ef_search sweep — the accuracy/throughput trade-off: searching more of the graph raises recall but lowers QPS.


Results
The headline is throughput. On identical A1 hardware, all three MySQL versions clear ~530–550 approximate queries per second at ~1.8 ms median — roughly 500× faster than the exact brute-force scan (~1 QPS) they fall back to without an index. 8.4, 9.7 and 26.7 land within a few percent of each other, so moving to a newer MySQL costs nothing here.


  
  
  Approximate nearest-neighbor throughput at the default operating point (ef_search=200). Higher is better. All four engines on identical Ampere A1 hardware.



  
  Recall@10 at the same operating point. The three MyVector builds land at 0.82; MariaDB 12.3 reaches 0.948 — it trades some speed for accuracy in how it walks the graph.


The real picture: accuracy vs throughput
A single QPS number hides the trade-off. The ef_search knob lets each query decide how hard to look: low values are fast but miss neighbors, high values are accurate but slow. Plotting recall against QPS across that sweep is the honest comparison — up and to the right is better.

  
  
  Each point is one ef_search setting (10 → 200). MariaDB’s curve sits higher on recall at a given speed; the MySQL/MyVector curves reach higher raw QPS. All share the same synthetic ground truth.




EngineBuild (s)Insert QPSANN QPSANN p50 (ms)Recall@10



Exact brute-force KNN measured ~1 QPS (~700 ms) on every engine — the baseline the index is beating.

The MariaDB surprise
The first MariaDB run looked grim: 29 ANN QPS, a 583-second build, recall aside. Taken at face value it was 15× behind MySQL. That gap was too large to be a real engine difference, so we dug in.
The cause was one default. MariaDB caches its HNSW graph in memory up to mhnsw_max_cache_size, which ships at 16 MB — but the index for this dataset is ~27 MB. The graph didn’t fit, so every search and the whole online build re-read nodes from disk. MyVector sidesteps this by loading its entire index into memory, so the default pitted a disk-bound MariaDB against an in-memory MyVector.
Sizing the cache above the index changed everything:

  
  
  MariaDB 12.3, same data and hardware, cache at its 16 MB default vs sized above the index. ANN throughput rose ~9×; build dropped from 583 s to 92 s; recall improved from 0.85 to 0.95. (Search alone, measured in isolation, was ~15× faster.)


The lesson, not the leaderboard
Properly configured, MariaDB’s native vector search is competitive with MyVector on speed and ahead on recall. The takeaway isn’t a winner — it’s that a single cache default can make one engine look an order of magnitude slower than it is. Benchmarks are only as fair as their configuration, which is why the setup here is in the open.


Takeaways

Native MySQL vector search is fast. ~530–550 QPS at single-digit-millisecond latency on 4 modest cores, ~500× over brute force — without a second database to run.
Newer MySQL isn’t slower. 8.4, 9.7 and 26.7 land within a few percent of each other; there’s no regression moving forward.
MariaDB 12.3 holds its own once its HNSW cache fits the index — higher recall, comparable throughput.
Configuration is part of the result. The most interesting finding wasn’t a speed number; it was a default that quietly handicapped an engine 15×.


Every number here is reproducible. The live, interactive dashboard is at bench.myvector.online, and the benchmark harness and raw results live in github.com/askdba/myvector under scripts/bench-dashboard. Workload: 50,000 × 128-d vectors, L2 distance, HNSW M=16 / ef_construction=200, held-out queries for recall, each engine pinned to 4 vCPU / 24 GB on Ampere.




</description>
    <content:encoded><![CDATA[<p><img src="https://askdba.net/wp-content/uploads/2026/10/myvector-featured.jpg" width="1200" height="630" alt="Bar chart showing MySQL vector search benchmark: 548 QPS for MySQL 9.7, 257 QPS for MariaDB 12.3"></p>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&amp;family=IBM+Plex+Mono:wght@400;500&amp;family=IBM+Plex+Sans:wght@400;450;500;600;700&amp;display=swap">


<div class="mv-post">

<p class="eyebrow">Benchmarks · HNSW vector search</p>

<p class="standfirst">MyVector adds approximate nearest-neighbor search to stock MySQL — no separate vector database. We put it on identical hardware across three MySQL versions, added MariaDB’s native vector type as a reference, and measured build time, throughput, latency, and recall on the same data.</p>

<p>askdba/myvector · live dashboard at <a href="https://bench.myvector.online/">bench.myvector.online</a></p>

<div class="stats">
  <div class="stat"><div class="n">548</div><div class="l">peak ANN queries/sec (MySQL 9.7), ~1.8 ms median</div></div>
  <div class="stat"><div class="n">0.95</div><div class="l">best recall@10 (MariaDB 12.3) once tuned</div></div>
  <div class="stat"><div class="n">4×</div><div class="l">engines, one 50k×128 workload, identical 4 vCPU / 24 GB slices</div></div>
</div>

<h2>Why benchmark this at all</h2>
<p>Vector search usually means standing up a dedicated database next to your primary store, then keeping the two in sync. MyVector takes the other path: it lives <em>inside</em> MySQL as UDFs and stored procedures, builds an HNSW index over a column of vectors, and answers nearest-neighbor queries in SQL. The obvious question is whether that convenience costs you speed or accuracy.</p>
<p>To answer it honestly we needed a level field. Every engine runs in its own container on one <strong>Ampere A1</strong> host, each pinned to the same <strong>4 vCPU / 24 GB</strong> slice, loading the <strong>same 50,000 synthetic 128-dimensional vectors</strong> (seeded, so every engine indexes identical data), and builds an HNSW index with the same parameters (M=16, ef_construction=200, L2 distance). Three of the four are MySQL running MyVector — versions 8.4, 9.7 and 26.7. The fourth is MariaDB 12.3, which ships its own native <code>VECTOR</code> type, included as a cross-engine reference point.</p>

<h3>What each number means</h3>
<p>Five measurements, each isolating one thing:</p>
<ul>
<li><strong>Index build</strong> — wall-clock seconds to build the HNSW graph over all 50k rows.</li>
<li><strong>Insert throughput</strong> — rows/sec written into a live, indexed table.</li>
<li><strong>ANN QPS &amp; latency</strong> — approximate nearest-neighbor queries per second through the index, and their median response time.</li>
<li><strong>Recall@10</strong> — of the true 10 nearest neighbors (found by exact brute-force search), how many the approximate search actually returns, averaged over held-out queries that were never inserted. 1.0 is perfect; 0.82 means it finds ~8 of the real 10.</li>
<li><strong>ef_search sweep</strong> — the accuracy/throughput trade-off: searching more of the graph raises recall but lowers QPS.</li>
</ul>

<h2>Results</h2>
<p>The headline is throughput. On identical A1 hardware, all three MySQL versions clear ~530–550 approximate queries per second at ~1.8 ms median — roughly 500× faster than the exact brute-force scan (~1 QPS) they fall back to without an index. 8.4, 9.7 and 26.7 land within a few percent of each other, so moving to a newer MySQL costs nothing here.</p>

<figure>
  <div class="legend"></div>
  <div class="chartbox"><canvas></canvas></div>
  <figcaption>Approximate nearest-neighbor throughput at the default operating point (ef_search=200). Higher is better. All four engines on identical Ampere A1 hardware.</figcaption>
</figure>

<figure>
  <div class="chartbox"><canvas></canvas></div>
  <figcaption>Recall@10 at the same operating point. The three MyVector builds land at 0.82; MariaDB 12.3 reaches 0.948 — it trades some speed for accuracy in how it walks the graph.</figcaption>
</figure>

<h3>The real picture: accuracy vs throughput</h3>
<p>A single QPS number hides the trade-off. The <code>ef_search</code> knob lets each query decide how hard to look: low values are fast but miss neighbors, high values are accurate but slow. Plotting recall against QPS across that sweep is the honest comparison — up and to the right is better.</p>
<figure>
  <div class="legend"></div>
  <div class="chartbox"><canvas></canvas></div>
  <figcaption>Each point is one ef_search setting (10 → 200). MariaDB’s curve sits higher on recall at a given speed; the MySQL/MyVector curves reach higher raw QPS. All share the same synthetic ground truth.</figcaption>
</figure>

<div class="tablewrap">
<table>
<thead><tr><th>Engine</th><th>Build (s)</th><th>Insert QPS</th><th>ANN QPS</th><th>ANN p50 (ms)</th><th>Recall@10</th></tr></thead>
<tbody></tbody>
</table>
</div>
<p>Exact brute-force KNN measured ~1 QPS (~700 ms) on every engine — the baseline the index is beating.</p>

<h2>The MariaDB surprise</h2>
<p>The first MariaDB run looked grim: 29 ANN QPS, a 583-second build, recall aside. Taken at face value it was 15× behind MySQL. That gap was too large to be a real engine difference, so we dug in.</p>
<p>The cause was one default. MariaDB caches its HNSW graph in memory up to <code>mhnsw_max_cache_size</code>, which ships at <strong>16 MB</strong> — but the index for this dataset is <strong>~27 MB</strong>. The graph didn’t fit, so every search and the whole online build re-read nodes from disk. MyVector sidesteps this by loading its entire index into memory, so the default pitted a disk-bound MariaDB against an in-memory MyVector.</p>
<p>Sizing the cache above the index changed everything:</p>
<figure>
  <div class="legend"></div>
  <div class="chartbox"><canvas></canvas></div>
  <figcaption>MariaDB 12.3, same data and hardware, cache at its 16 MB default vs sized above the index. ANN throughput rose ~9×; build dropped from 583 s to 92 s; recall improved from 0.85 to 0.95. (Search alone, measured in isolation, was ~15× faster.)</figcaption>
</figure>
<div class="callout">
<h3>The lesson, not the leaderboard</h3>
<p>Properly configured, MariaDB’s native vector search is competitive with MyVector on speed and ahead on recall. The takeaway isn’t a winner — it’s that a single cache default can make one engine look an order of magnitude slower than it is. Benchmarks are only as fair as their configuration, which is why the setup here is in the open.</p>
</div>

<h2>Takeaways</h2>
<ul>
<li><strong>Native MySQL vector search is fast.</strong> ~530–550 QPS at single-digit-millisecond latency on 4 modest cores, ~500× over brute force — without a second database to run.</li>
<li><strong>Newer MySQL isn’t slower.</strong> 8.4, 9.7 and 26.7 land within a few percent of each other; there’s no regression moving forward.</li>
<li><strong>MariaDB 12.3 holds its own</strong> once its HNSW cache fits the index — higher recall, comparable throughput.</li>
<li><strong>Configuration is part of the result.</strong> The most interesting finding wasn’t a speed number; it was a default that quietly handicapped an engine 15×.</li>
</ul>

<p class="foot">Every number here is reproducible. The live, interactive dashboard is at <a href="https://bench.myvector.online/">bench.myvector.online</a>, and the benchmark harness and raw results live in <a href="https://github.com/askdba/myvector">github.com/askdba/myvector</a> under <code>scripts/bench-dashboard</code>. Workload: 50,000 × 128-d vectors, L2 distance, HNSW M=16 / ef_construction=200, held-out queries for recall, each engine pinned to 4 vCPU / 24 GB on Ampere.</p>

</div>


]]></content:encoded>
    <pubDate>Wed, 14 Oct 2026 05:20:14 +0000</pubDate>
    <dc:creator>Alkin Tezuysal</dc:creator>
    <category>mysql</category>
    <category>Technical</category>
    <category>mariadb</category>
    <category>myvector</category>
    <category>search</category>
    <category>vector</category>
  </item>

  <item>
    <title>MySQL Online DDL &quot;Duplicate Key&quot; Optimization</title>
    <guid isPermaLink="false">https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem-en/</guid>
    <link>https://songlibing.github.io/posts/mysql-ddl-duplicate-key-problem-en/</link>
    <description>When you rebuild a table with MySQL’s Online DDL, the operation can fail partway through with a Duplicate Entry error:

1
2
mysql&amp;gt; alter table tt add c3 int, algorithm=inplace;
ERROR 1062 (23000): Duplicate entry '1' for key 'tt.uk_c2'


This is a long-standing, well-known problem — it has existed ever since Online DDL was introduced in MySQL 5.6, and it appears across several bug reports:


  BUG#76895 — Adding new column OR Drop column causes duplicate PK error
  BUG#77572 — The bogus duplicate key error in online ddl with incorrect key name
  BUG#98600 — Optimize table fails with duplicate entry on UNIQUE KEY
  BUG#104626 — Remove failure of Online ALTER because concurrent Duplicate entry


BUG#76895 was the first to report it. It called the problem a Duplicate PRIMARY KEY error, but that label was itself a bug: a separate 5.6 issue, BUG#77572, printed PRIMARY KEY in the message when it should have said UNIQUE KEY. Once BUG#77572 was fixed (in 5.6.28 and 5.7.10), the message correctly read Duplicate UNIQUE KEY — and BUG#98600 was filed later for what was really the same underlying problem.

MySQL closed BUG#76895 as Not a Bug, treating the error as an unavoidable side effect of how Online DDL works. But “not a bug” is of little consolation to a DBA: a failed DDL derails a carefully planned change and often pushes it to the next maintenance window. That inconvenience is what eventually produced BUG#104626 — this time filed as a feature request asking for a real fix.

So what actually causes this error, why does MySQL consider it working as intended, and can you do anything about it? Answering all three means looking at how Online DDL works internally.

How Online DDL Works

To keep a table writable while it’s being altered, Online DDL splits the job in two: a full copy of the rows that already exist, and an incremental replay of everything that changes while that copy runs. It copies the existing rows into the new table (when you’re adding an index there’s no new table — the rows go straight into the new index). The copy takes time, and DML keeps running throughout, so every change made along the way is captured to be applied afterward. Two things make this work:


  A precise cut-off in time, so the full copy takes only the rows that existed before it and nothing after.
  An incremental log that captures every change made after the cut-off, so replaying it brings the new table fully up to date.


Distinguishing Full Data from Incremental Changes

The full data lives in the clustered B+Tree, and Online DDL reads it by scanning that tree. Since DML runs during the scan, the tree also holds changes made after the cut-off, so Online DDL needs a way to separate the original rows from those later changes. It does this with InnoDB’s MVCC (multi-version concurrency control).

MVCC lets a transaction read from a consistent snapshot of the data — one that isn’t affected by changes other transactions make while it runs. The mechanism behind it is the read view: when one is created, it records the IDs of all currently active transactions, then uses that list to decide which version of each row the transaction may see. If a row is modified and committed after the read view was created, the view can still reach the earlier version through the undo log, even once the new version is sitting in the clustered index.

That is exactly what Online DDL relies on to set the boundary of the full copy: it opens a read view at one precise moment, and the copy reads only what that view can see. Whatever concurrent DML changes afterward — no matter when it commits — stays invisible to the copy.

The Three Phases of Online DDL

InnoDB runs an Online DDL in three phases, each holding a different level of Metadata Lock (MDL) to control precisely what concurrent DML is allowed to do.

Prepare

Prepare runs under MDL_EXCLUSIVE, which blocks every read and write on the table. That brief “writes-stopped” window is the cut-off between full and incremental data. During it, InnoDB:


  Creates a consistent read view via trx_assign_read_view().
  Sets up the row log (the incremental log) on the target index and marks the index ONLINE_INDEX_CREATION. From then on, each insert, update, or delete on that index’s B+tree checks the flag to decide whether it also needs to go into the row log.


Both steps happen under MDL_EXCLUSIVE, which guarantees two things:


  The read view and the row log line up perfectly, so together they cover the existing rows and every later change with no gap.
  It shuts out any in-flight transaction. Without the exclusive lock, a transaction could have modified the table but not yet committed at the moment of the cut-off. The row log isn’t installed yet, so its change goes unlogged; and because it’s still uncommitted, the read view marks it active and the full copy can’t see it either. The change would then be in neither the copy nor the log, and would be lost. MDL_EXCLUSIVE guarantees no such transaction is running, so the read view and row log meet at a clean boundary: everything committed before it belongs to the full copy; everything after it is captured by the row log.


Once Prepare finishes, the lock is downgraded from MDL_EXCLUSIVE to the shared MDL_SHARED_UPGRADABLE.

Execute

During Execute, Online DDL holds MDL_SHARED_UPGRADABLE, so concurrent DML runs normally. This is where the full copy happens, along with the first pass of incremental replay — and it is by far the longest phase, the one throughout which the table stays writable. Every DML on the table is recorded in the row log.

Commit

For Commit, the DDL upgrades the lock from MDL_SHARED_UPGRADABLE back to MDL_EXCLUSIVE, blocking all reads and writes again. Under that lock it replays whatever row log is left and commits the DDL transaction. Because Execute already replayed most of the log, there is little left to do here, so the exclusive lock is held only briefly.

The exclusive lock in this phase also guarantees two things:


  Every DML from the Execute phase that touched the table has committed.
  No new DML can start, so no new row log is produced.



  In short, Online DDL is not fully online — it blocks DML during Prepare and Commit. And because a metadata lock is held for the life of a transaction, a long-running transaction that is in progress when the DDL tries to acquire MDL_EXCLUSIVE will block it — and while the DDL waits, it blocks other DML too, leaving the table effectively unavailable.


The Full Copy

The full copy runs during Execute, under MDL_SHARED_UPGRADABLE, with concurrent DML allowed.

It uses row_merge_read_clustered_index() to scan the old table’s clustered index in ascending PRIMARY KEY order, checking each row’s visibility with MVCC as it goes. Only rows visible to the read view are copied into the new table; delete-marked rows are skipped.

Thanks to MVCC, even when concurrent DML rewrites a row mid-scan, the copy still sees the version as of the read view — reconstructed from the undo log if necessary. What it copies is always one consistent snapshot, unaffected by concurrent activity.

Recording the Incremental Log

The row log records page-level changes to a B+tree, and it behaves differently in the two kinds of DDL:


  Rebuilding the table: it logs only changes to the clustered B+tree — INSERT, UPDATE, and DELETE — and on replay each entry is applied to every index.
  Adding an index: it logs only changes to the new index’s B+tree — INSERT and DELETE, never UPDATE. Since the index is still being built, these operations are only written to the row log and never actually applied to the B+tree — the opposite of a table rebuild, where every existing index on the old table is maintained in real time.


One detail here is worth paying attention to: when an entry is logged. Because the row log tracks B+tree changes, an entry is written only after the B+tree operation succeeds. As the figure shows, for a table rebuild the entry is logged after the row is inserted into the clustered B+tree; for an added index it is logged at the point the row would go into the secondary index's B+tree — except that, for an added index, nothing is actually inserted, and only the log entry is written.



What Happens on a DML Rollback

A DML operates on the clustered B+tree first, then the secondary indexes. So what happens if the secondary-index step fails?


  The row-log entry remains — it isn’t cleaned up. It records one B+tree operation, and that operation really did happen.
  The statement then rolls back. Rolling back replays the change in reverse against the B+tree (driven by the undo log), and that reverse operation is logged too. So a failed INSERT leaves two row-log entries — a ROW_T_INSERT followed by a ROW_T_DELETE (below). Applied in order, the two cancel out, exactly as a rollback should.




A failed statement is just one way to reach this state; every rollback works the same way, including an explicit ROLLBACK from the user.

Replaying the Incremental Log

The row log is replayed twice:


  The first pass runs in Execute, right after the full copy. The lock is still MDL_SHARED_UPGRADABLE, so DML keeps running and keeps adding to the log. The purpose of this pass is to replay as much of the log as possible now, so the exclusive lock in Commit is held for as short a time as possible.
  The second pass runs in Commit, under MDL_EXCLUSIVE, with no new log being produced. It handles the small remainder, and once it is done the new table matches the old one exactly.


Both passes read the log in write order, one entry at a time. For a table rebuild, each entry is applied to every index on the new table; for an added index, only to the index being built.

What Actually Causes the Duplicate Entry

Recall the rollback case: even when an INSERT encounters a Duplicate Entry on a unique index, it still leaves two row-log entries behind (below).



Replaying the first entry, ROW_T_INSERT, does exactly what the original INSERT did: insert a row into the clustered B+tree, then add an entry to each index. When it reaches the unique index, it encounters the same Duplicate Entry — this time during replay, which is what fails the whole DDL.

Reproducing It

1
2
3
4
5
CREATE TABLE t1 (
  c1 INT AUTO_INCREMENT PRIMARY KEY,
  c2 INT,
  UNIQUE KEY (c2)
) ENGINE=InnoDB;


Run these statements in three separate sessions:

1
2
3
4
5
6
7
8
9
# Session 1
BEGIN;
INSERT INTO t1 VALUES(NULL, 1);

# Session 2
ALTER TABLE t1 ENGINE = InnoDB;

# Session 3
INSERT INTO t1 VALUES(NULL, 1);


Now commit Session 1, and both Session 2’s ALTER and Session 3’s INSERT fail with Duplicate Entry:

1
2
3
4
5
6
7
# Session 2
mysql&amp;gt; ALTER TABLE t1 ENGINE = InnoDB;
ERROR 1062 (23000): Duplicate entry '1' for key 't1.c2'

# Session 3
mysql&amp;gt; INSERT INTO t1 VALUES(2, 1);
ERROR 1062 (23000): Duplicate entry '1' for key 't1.c2'


The example relies on the MDL behavior described above:


  Session 1’s INSERT takes an MDL_SHARED_WRITE lock on t1 and holds it until it commits.
  Session 2’s ALTER needs MDL_EXCLUSIVE for its prepare phase, so it is blocked by Session 1.
  Session 3’s INSERT wants MDL_SHARED_WRITE, but is now blocked by Session 2.


You can observe this in performance_schema.metadata_locks — the first row below is Session 1’s lock.



When Session 1 commits, Session 2 acquires MDL_EXCLUSIVE, finishes Prepare, and downgrades to MDL_SHARED_UPGRADABLE. That lock doesn’t conflict with MDL_SHARED_WRITE, so Session 3 runs — and since c2 = 1 already exists, it fails with a Duplicate key error. That failure leaves a row-log entry, and replaying it is what makes Session 2’s ALTER fail in turn.



How to Avoid It

Once the mechanism is clear, I would call this a bug — which is why we fixed it in AliSQL. On community MySQL, the only option is to prevent DML from encountering Duplicate Entry while a DDL is running.

A common trigger is a table with an auto-increment primary key that has also gained a unique index. The application inserts a row, lets MySQL assign the auto-increment key, and — if the first INSERT is slow — retries the same statement from another session. That is perfectly sensible on its own: the unique index guarantees only one of the two can succeed. But a single failing INSERT is enough to make the DDL fail. So while a DDL is in progress, increasing the retry timeout is a practical way to avoid the problem.

The Fix in AliSQL

AliSQL takes a straightforward approach: when replay encounters a Duplicate Entry, ignore it.

Real Duplicates vs. False Ones

There is one caveat: not every Duplicate Entry is safe to ignore.

Online DDL has a genuine real duplicate case: the DDL introduces a new uniqueness constraint (a new or changed primary key, or a new UNIQUE index) while the existing data already contains duplicates. That new constraint isn’t enforced during the DDL, so concurrent DML can keep adding duplicates — and here the DDL must fail. Ignoring it would silently break the guarantee the user asked for.

But if the Duplicate Entry occurs on a unique index that already existed on the table, it is always a false duplicate and safe to skip. That is the common case, and the one AliSQL optimizes.

Cleaning Up the Rollback Entry

As we saw, a failed DML leaves two row-log entries. For an INSERT:

1
2
&amp;lt;ROW_T_INSERT, pk1, ...&amp;gt;
&amp;lt;ROW_T_DELETE, pk1&amp;gt;


On replay, the insert into the unique index fails and we ignore it — but the insert into the primary-key B+tree did succeed. When the second entry (ROW_T_DELETE) is replayed, deleting from the primary key is fine; deleting from the unique index is not, because nothing was ever inserted there, so the record can’t be found and the replay fails.

So whenever we skip a Duplicate Entry on a pre-existing unique index, we have to record it and skip the matching operation when the rollback’s row log arrives later, as shown below:



UPDATE is more complex. When an UPDATE changes a secondary-index column, the secondary index performs two operations:


  Delete the old entry.
  Insert the new one.


The replay fails at step 2, so on the rollback we cannot simply skip the whole index — we skip step 1 but still perform step 2:



With this handling, AliSQL eliminates the Duplicate Entry failures that Online DDL should never have raised in the first place.

Conclusion

Online DDL can fail with a Duplicate Entry, and because it is nondeterministic — it depends on the timing of concurrent DML — it is hard to rule out entirely, and each failure can disrupt a planned change. The failure happens in just a few steps: a concurrent DML encounters a Duplicate Entry, that error is carried through Online DDL’s row log, and replaying the log fails the DDL. AliSQL breaks this chain by ignoring Duplicate Entry errors that occur on an already-existing unique index, so Online DDL is no longer aborted by an error that did not originate from the DDL itself.</description>
    <content:encoded><![CDATA[<p>When you rebuild a table with MySQL’s Online DDL, the operation can fail partway through with a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> error:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="n">mysql</span><span class="o">&gt;</span> <span class="k">alter</span> <span class="k">table</span> <span class="n">tt</span> <span class="k">add</span> <span class="n">c3</span> <span class="nb">int</span><span class="p">,</span> <span class="n">algorithm</span><span class="o">=</span><span class="n">inplace</span><span class="p">;</span>
<span class="n">ERROR</span> <span class="mi">1062</span> <span class="p">(</span><span class="mi">23000</span><span class="p">):</span> <span class="n">Duplicate</span> <span class="n">entry</span> <span class="s1">'1'</span> <span class="k">for</span> <span class="k">key</span> <span class="s1">'tt.uk_c2'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This is a long-standing, well-known problem — it has existed ever since Online DDL was introduced in MySQL 5.6, and it appears across several bug reports:</p>

<ul>
  <li><a href="https://bugs.mysql.com/bug.php?id=76895">BUG#76895</a> — Adding new column OR Drop column causes duplicate PK error</li>
  <li><a href="https://bugs.mysql.com/bug.php?id=77572">BUG#77572</a> — The bogus duplicate key error in online ddl with incorrect key name</li>
  <li><a href="https://bugs.mysql.com/bug.php?id=98600">BUG#98600</a> — Optimize table fails with duplicate entry on UNIQUE KEY</li>
  <li><a href="https://bugs.mysql.com/bug.php?id=104626">BUG#104626</a> — Remove failure of Online ALTER because concurrent Duplicate entry</li>
</ul>

<p><a href="https://bugs.mysql.com/bug.php?id=76895">BUG#76895</a> was the first to report it. It called the problem a <code class="language-plaintext highlighter-rouge">Duplicate PRIMARY KEY</code> error, but that label was itself a bug: a separate 5.6 issue, <a href="https://bugs.mysql.com/bug.php?id=77572">BUG#77572</a>, printed <code class="language-plaintext highlighter-rouge">PRIMARY KEY</code> in the message when it should have said <code class="language-plaintext highlighter-rouge">UNIQUE KEY</code>. Once BUG#77572 was fixed (in 5.6.28 and 5.7.10), the message correctly read <code class="language-plaintext highlighter-rouge">Duplicate UNIQUE KEY</code> — and <a href="https://bugs.mysql.com/bug.php?id=98600">BUG#98600</a> was filed later for what was really the same underlying problem.</p>

<p>MySQL closed BUG#76895 as <code class="language-plaintext highlighter-rouge">Not a Bug</code>, treating the error as an unavoidable side effect of how Online DDL works. But “not a bug” is of little consolation to a DBA: a failed DDL derails a carefully planned change and often pushes it to the next maintenance window. That inconvenience is what eventually produced <a href="https://bugs.mysql.com/bug.php?id=104626">BUG#104626</a> — this time filed as a feature request asking for a real fix.</p>

<p>So what actually causes this error, why does MySQL consider it working as intended, and can you do anything about it? Answering all three means looking at how Online DDL works internally.</p>

<h2>How Online DDL Works</h2>

<p>To keep a table writable while it’s being altered, Online DDL splits the job in two: a <em>full copy</em> of the rows that already exist, and an <em>incremental replay</em> of everything that changes while that copy runs. It copies the existing rows into the new table (when you’re adding an index there’s no new table — the rows go straight into the new index). The copy takes time, and DML keeps running throughout, so every change made along the way is captured to be applied afterward. Two things make this work:</p>

<ul>
  <li>A precise cut-off in time, so the full copy takes only the rows that existed before it and nothing after.</li>
  <li>An incremental log that captures every change made after the cut-off, so replaying it brings the new table fully up to date.</li>
</ul>

<h3>Distinguishing Full Data from Incremental Changes</h3>

<p>The <em>full data</em> lives in the <em>clustered B+Tree</em>, and Online DDL reads it by scanning that tree. Since DML runs during the scan, the tree also holds changes made after the cut-off, so Online DDL needs a way to separate the original rows from those later changes. It does this with InnoDB’s <code class="language-plaintext highlighter-rouge">MVCC</code> (multi-version concurrency control).</p>

<p>MVCC lets a transaction read from a consistent snapshot of the data — one that isn’t affected by changes other transactions make while it runs. The mechanism behind it is the <code class="language-plaintext highlighter-rouge">read view</code>: when one is created, it records the IDs of all currently active transactions, then uses that list to decide which version of each row the transaction may see. If a row is modified and committed after the read view was created, the view can still reach the earlier version through the undo log, even once the new version is sitting in the clustered index.</p>

<p>That is exactly what Online DDL relies on to set the boundary of the full copy: <em>it opens a read view at one precise moment, and the copy reads only what that view can see. Whatever concurrent DML changes afterward — no matter when it commits — stays invisible to the copy.</em></p>

<h2>The Three Phases of Online DDL</h2>

<p>InnoDB runs an Online DDL in three phases, each holding a different level of <code class="language-plaintext highlighter-rouge">Metadata Lock (MDL)</code> to control precisely what concurrent DML is allowed to do.</p>

<h3>Prepare</h3>

<p>Prepare runs under <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code>, which blocks every read and write on the table. That brief “writes-stopped” window is the cut-off between full and incremental data. During it, InnoDB:</p>

<ol>
  <li>Creates a consistent read view via <code class="language-plaintext highlighter-rouge">trx_assign_read_view()</code>.</li>
  <li>Sets up the row log (the incremental log) on the target index and marks the index <code class="language-plaintext highlighter-rouge">ONLINE_INDEX_CREATION</code>. From then on, each insert, update, or delete on that index’s B+tree checks the flag to decide whether it also needs to go into the row log.</li>
</ol>

<p>Both steps happen under <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code>, which guarantees two things:</p>

<ol>
  <li>The read view and the row log line up perfectly, so together they cover the existing rows and every later change with no gap.</li>
  <li>It <em>shuts out any in-flight transaction</em>. Without the exclusive lock, a transaction could have modified the table but not yet committed at the moment of the cut-off. The row log isn’t installed yet, so its change goes unlogged; and because it’s still uncommitted, the read view marks it active and the full copy can’t see it either. The change would then be in neither the copy nor the log, and would be lost. <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code> guarantees no such transaction is running, so the read view and row log meet at a clean boundary: <em>everything committed before it belongs to the full copy; everything after it is captured by the row log.</em></li>
</ol>

<p>Once Prepare finishes, the lock is downgraded from <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code> to the shared <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code>.</p>

<h3>Execute</h3>

<p>During Execute, Online DDL holds <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code>, so <em>concurrent DML runs normally</em>. This is where the full copy happens, along with the first pass of incremental replay — and it is by far the longest phase, the one throughout which the table stays writable. Every DML on the table is recorded in the row log.</p>

<h3>Commit</h3>

<p>For Commit, the DDL upgrades the lock from <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code> back to <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code>, <em>blocking all reads and writes again</em>. Under that lock it replays whatever row log is left and commits the DDL transaction. Because Execute already replayed most of the log, there is little left to do here, so the exclusive lock is held only briefly.</p>

<p>The exclusive lock in this phase also guarantees two things:</p>

<ul>
  <li>Every DML from the Execute phase that touched the table has committed.</li>
  <li>No new DML can start, so no new row log is produced.</li>
</ul>

<blockquote class="prompt-tip">
  <p>In short, Online DDL is not <em>fully</em> online — it blocks DML during Prepare and Commit. And because a metadata lock is held for the life of a transaction, a long-running transaction that is in progress when the DDL tries to acquire <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code> will block it — and while the DDL waits, it blocks other DML too, leaving the table effectively unavailable.</p>
</blockquote>

<h2>The Full Copy</h2>

<p>The full copy runs during Execute, under <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code>, with concurrent DML allowed.</p>

<p>It uses <code class="language-plaintext highlighter-rouge">row_merge_read_clustered_index()</code> to scan the old table’s clustered index in ascending PRIMARY KEY order, checking each row’s visibility with MVCC as it goes. Only rows visible to the read view are copied into the new table; <code class="language-plaintext highlighter-rouge">delete-marked</code> rows are skipped.</p>

<p>Thanks to MVCC, even when concurrent DML rewrites a row mid-scan, the copy still sees the version as of the read view — reconstructed from the undo log if necessary. What it copies is always one consistent snapshot, unaffected by concurrent activity.</p>

<h2>Recording the Incremental Log</h2>

<p>The row log records page-level changes to a B+tree, and it behaves differently in the two kinds of DDL:</p>

<ul>
  <li><em>Rebuilding the table</em>: it logs only changes to the clustered B+tree — <code class="language-plaintext highlighter-rouge">INSERT</code>, <code class="language-plaintext highlighter-rouge">UPDATE</code>, and <code class="language-plaintext highlighter-rouge">DELETE</code> — and on replay each entry is applied to <em>every</em> index.</li>
  <li><em>Adding an index</em>: it logs only changes to the new index’s B+tree — <code class="language-plaintext highlighter-rouge">INSERT</code> and <code class="language-plaintext highlighter-rouge">DELETE</code>, never <code class="language-plaintext highlighter-rouge">UPDATE</code>. Since the index is still being built, these operations are <em>only</em> written to the row log and never actually applied to the B+tree — the opposite of a table rebuild, where every existing index on the old table is maintained in real time.</li>
</ul>

<p>One detail here is worth paying attention to: when an entry is logged. Because the row log tracks B+tree changes, an entry is written only after the B+tree operation succeeds. As the figure shows, <code class="language-plaintext highlighter-rouge">for a table rebuild the entry is logged after the row is inserted into the clustered B+tree; for an added index it is logged at the point the row would go into the secondary index's B+tree</code> — except that, for an added index, nothing is actually inserted, and only the log entry is written.</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-1-en.svg" alt=""></p>

<h3>What Happens on a DML Rollback</h3>

<p>A DML operates on the clustered B+tree first, then the secondary indexes. So what happens if the secondary-index step fails?</p>

<ul>
  <li>The row-log entry remains — it isn’t cleaned up. It records one B+tree operation, and that operation really did happen.</li>
  <li>The statement then rolls back. Rolling back replays the change in reverse against the B+tree (driven by the undo log), and that reverse operation is logged too. So a failed INSERT leaves <em>two</em> row-log entries — a <code class="language-plaintext highlighter-rouge">ROW_T_INSERT</code> followed by a <code class="language-plaintext highlighter-rouge">ROW_T_DELETE</code> (below). Applied in order, the two cancel out, exactly as a rollback should.</li>
</ul>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-2-en.svg" alt=""></p>

<p>A failed statement is just one way to reach this state; every rollback works the same way, including an explicit <code class="language-plaintext highlighter-rouge">ROLLBACK</code> from the user.</p>

<h2>Replaying the Incremental Log</h2>

<p>The row log is replayed twice:</p>

<ul>
  <li><em>The first pass</em> runs in Execute, right after the full copy. The lock is still <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code>, so DML keeps running and keeps adding to the log. The purpose of this pass is to <em>replay as much of the log as possible now, so the exclusive lock in Commit is held for as short a time as possible</em>.</li>
  <li><em>The second pass</em> runs in Commit, under <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code>, with no new log being produced. It handles the small remainder, and once it is done the new table matches the old one exactly.</li>
</ul>

<p>Both passes read the log in write order, one entry at a time. For a table rebuild, each entry is applied to every index on the new table; for an added index, only to the index being built.</p>

<h2>What Actually Causes the Duplicate Entry</h2>

<p>Recall the rollback case: even when an INSERT encounters a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> on a unique index, it still leaves two row-log entries behind (below).</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-3-en.svg" alt=""></p>

<p>Replaying the first entry, <code class="language-plaintext highlighter-rouge">ROW_T_INSERT</code>, does exactly what the original INSERT did: insert a row into the clustered B+tree, then add an entry to each index. When it reaches the unique index, it encounters the same <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> — this time during replay, which is what fails the whole DDL.</p>

<h3>Reproducing It</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">t1</span> <span class="p">(</span>
  <span class="n">c1</span> <span class="nb">INT</span> <span class="n">AUTO_INCREMENT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="n">c2</span> <span class="nb">INT</span><span class="p">,</span>
  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="p">(</span><span class="n">c2</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">InnoDB</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Run these statements in three separate sessions:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="o">#</span> <span class="k">Session</span> <span class="mi">1</span>
<span class="k">BEGIN</span><span class="p">;</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">t1</span> <span class="k">VALUES</span><span class="p">(</span><span class="k">NULL</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>

<span class="o">#</span> <span class="k">Session</span> <span class="mi">2</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">t1</span> <span class="n">ENGINE</span> <span class="o">=</span> <span class="n">InnoDB</span><span class="p">;</span>

<span class="o">#</span> <span class="k">Session</span> <span class="mi">3</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">t1</span> <span class="k">VALUES</span><span class="p">(</span><span class="k">NULL</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Now commit Session 1, and both Session 2’s ALTER and Session 3’s INSERT fail with <code class="language-plaintext highlighter-rouge">Duplicate Entry</code>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="o">#</span> <span class="k">Session</span> <span class="mi">2</span>
<span class="n">mysql</span><span class="o">&gt;</span> <span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">t1</span> <span class="n">ENGINE</span> <span class="o">=</span> <span class="n">InnoDB</span><span class="p">;</span>
<span class="n">ERROR</span> <span class="mi">1062</span> <span class="p">(</span><span class="mi">23000</span><span class="p">):</span> <span class="n">Duplicate</span> <span class="n">entry</span> <span class="s1">'1'</span> <span class="k">for</span> <span class="k">key</span> <span class="s1">'t1.c2'</span>

<span class="o">#</span> <span class="k">Session</span> <span class="mi">3</span>
<span class="n">mysql</span><span class="o">&gt;</span> <span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">t1</span> <span class="k">VALUES</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">ERROR</span> <span class="mi">1062</span> <span class="p">(</span><span class="mi">23000</span><span class="p">):</span> <span class="n">Duplicate</span> <span class="n">entry</span> <span class="s1">'1'</span> <span class="k">for</span> <span class="k">key</span> <span class="s1">'t1.c2'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The example relies on the MDL behavior described above:</p>

<ul>
  <li>Session 1’s INSERT takes an <code class="language-plaintext highlighter-rouge">MDL_SHARED_WRITE</code> lock on <code class="language-plaintext highlighter-rouge">t1</code> and holds it until it commits.</li>
  <li>Session 2’s ALTER needs <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code> for its prepare phase, so it is blocked by Session 1.</li>
  <li>Session 3’s INSERT wants <code class="language-plaintext highlighter-rouge">MDL_SHARED_WRITE</code>, but is now blocked by Session 2.</li>
</ul>

<p>You can observe this in <code class="language-plaintext highlighter-rouge">performance_schema.metadata_locks</code> — the first row below is Session 1’s lock.</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-4-en.png" alt=""></p>

<p>When Session 1 commits, Session 2 acquires <code class="language-plaintext highlighter-rouge">MDL_EXCLUSIVE</code>, finishes Prepare, and downgrades to <code class="language-plaintext highlighter-rouge">MDL_SHARED_UPGRADABLE</code>. That lock doesn’t conflict with <code class="language-plaintext highlighter-rouge">MDL_SHARED_WRITE</code>, so Session 3 runs — and since <code class="language-plaintext highlighter-rouge">c2 = 1</code> already exists, it fails with a <code class="language-plaintext highlighter-rouge">Duplicate key</code> error. That failure leaves a row-log entry, and replaying it is what makes Session 2’s ALTER fail in turn.</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-5-en.svg" alt=""></p>

<h3>How to Avoid It</h3>

<p>Once the mechanism is clear, I would call this a bug — which is why we fixed it in AliSQL. On community MySQL, the only option is to prevent DML from encountering <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> while a DDL is running.</p>

<p>A common trigger is a table with an auto-increment primary key that has also gained a unique index. The application inserts a row, lets MySQL assign the auto-increment key, and — if the first INSERT is slow — retries the same statement from another session. That is perfectly sensible on its own: the unique index guarantees only one of the two can succeed. But a single failing INSERT is enough to make the DDL fail. So while a DDL is in progress, increasing the retry timeout is a practical way to avoid the problem.</p>

<h2>The Fix in AliSQL</h2>

<p>AliSQL takes a straightforward approach: <em>when replay encounters a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code>, ignore it.</em></p>

<h3>Real Duplicates vs. False Ones</h3>

<p>There is one caveat: not every <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> is safe to ignore.</p>

<p>Online DDL has a genuine <em>real duplicate</em> case: <em>the DDL introduces a new uniqueness constraint (a new or changed primary key, or a new UNIQUE index) while the existing data already contains duplicates.</em> That new constraint isn’t enforced during the DDL, so concurrent DML can keep adding duplicates — and here the DDL <em>must</em> fail. Ignoring it would silently break the guarantee the user asked for.</p>

<p>But if the <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> occurs on a unique index that already existed on the table, it is always a <em>false duplicate</em> and safe to skip. That is the common case, and the one AliSQL optimizes.</p>

<h3>Cleaning Up the Rollback Entry</h3>

<p>As we saw, a failed DML leaves two row-log entries. For an INSERT:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>&lt;ROW_T_INSERT, pk1, ...&gt;
&lt;ROW_T_DELETE, pk1&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On replay, the insert into the unique index fails and we ignore it — but the insert into the primary-key B+tree did succeed. When the second entry (<code class="language-plaintext highlighter-rouge">ROW_T_DELETE</code>) is replayed, deleting from the primary key is fine; deleting from the unique index is not, because nothing was ever inserted there, so the record can’t be found and the replay fails.</p>

<p>So whenever we skip a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> on a pre-existing unique index, we have to record it and skip the matching operation when the rollback’s row log arrives later, as shown below:</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-6-en.svg" alt=""></p>

<p>UPDATE is more complex. When an UPDATE changes a secondary-index column, the secondary index performs two operations:</p>

<ol>
  <li>Delete the old entry.</li>
  <li>Insert the new one.</li>
</ol>

<p>The replay fails at step 2, so on the rollback we cannot simply skip the whole index — we skip step 1 but still perform step 2:</p>

<p><img src="https://songlibing.github.io/assets/img/ddl-dupkey-7-en.svg" alt=""></p>

<p>With this handling, AliSQL eliminates the <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> failures that Online DDL should never have raised in the first place.</p>

<h2>Conclusion</h2>

<p>Online DDL can fail with a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code>, and because it is nondeterministic — it depends on the timing of concurrent DML — it is hard to rule out entirely, and each failure can disrupt a planned change. The failure happens in just a few steps: a concurrent DML encounters a <code class="language-plaintext highlighter-rouge">Duplicate Entry</code>, that error is carried through Online DDL’s row log, and replaying the log fails the DDL. AliSQL breaks this chain by ignoring <code class="language-plaintext highlighter-rouge">Duplicate Entry</code> errors that occur on an already-existing unique index, so Online DDL is no longer aborted by an error that did not originate from the DDL itself.</p>]]></content:encoded>
    <pubDate>Tue, 13 Oct 2026 08:30:00 +0000</pubDate>
    <dc:creator>Libing Song</dc:creator>
    <category>MySQL</category>
    <category>DDL</category>
    <category>Online DDL</category>
    <category>InnoDB</category>
  </item>

  <item>
    <title>Thread Pool in Percona Server and MySQL (Part 2)</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54397</guid>
    <link>https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-2/</link>
    <description>Thread Pool in Percona Server and MySQL (Part 2)
The previous Part 1 focused on explaining the idea of thread pool, configuration options and analysis of thread pool efficiency in Percona Server for MySQL 9.7.1-1 against the base line, which is the same server without thread pooling.
We recommend reading the post with Part 1 before proceeding with reading Part 2 in the current post.
MySQL 26.7.0 with and without thread pool:
For better understanding of Percona Server 9.7.1-1 and MySQL 26.7.0 / 9.7.2 thread pool performance against each other we will start with a brief look at MySQL on its own.
The test runs did not find any significant differences in the performance of the thread pool between MySQL Server 26.7.0 and 9.7.2:
 Graph 1 – Comparison of MySQL 26.7.0 and 9.7.2 [ INTERACTIVE GRAPH ][ TABLE ]
As can be seen on the Graph 1 – the variation in performance between the versions is minimal. Configurations without thread pool go extremely close to each other, same as with the thread pool.
Lines that correspond to similar configurations converge to the same values across all server buffer tiers (2G / 12G / 32G) and across thread pool parameter ranges (thread_pool_size=[10/20/40/80/120/160] and thread_pool_oversubscribe=[2/3/4]).
NOTE: MySQL 26.7.0 is very slightly faster than 9.7.2 in the high connection threads count.
Therefore, to avoid the crowded graphs the data and performance comparisons will be presented for MySQL Version 26.7.0 only. The results for MySQL 9.7.2 can be viewed on the interactive graph page on GitHub.
 Graph 2 – MySQL with 2G buffer  [ INTERACTIVE GRAPH ][ TABLE ]
The thread pool implementation behaves differently in MySQL Server 26.7.0. Although it does not bring a huge performance boost as it was in Percona Server, the buffer pool for a high number of client connection threads (640 / 1280 / 2560) helps to keep TPS performance better than without the thread pool. Especially with 2560 connections the pooled TPS is three times the number of non-pooled TPS: 371 and 122 respectively.
The selection of the optimal value for thread_pool_size is important. The value itself (10) looks the same as it was with Percona Server. In the same way, higher values for thread_pool_size (20 / 40 / 80 / 120 / 160) do less TPS improvements.
Unlike Percona Server the oversubscription variations do not impact TPS much when innodb_buffer_pool_size=2G. However, thread_pool_oversubscribe=2 does slightly better than 3 or 4.
 Graph 3 – MySQL with 12G buffer  [ INTERACTIVE GRAPH ][ TABLE ]
Increasing buffer pool (innodb_buffer_pool_size=12G) brings the overall TPS performance up, but thread pool makes less impact at the highest number of connections: 1273 with thread pool and 718 without (TPS is improved by 77%).
With the fully buffered data set (innodb_buffer_pool_size=32G) MySQL thread pool configuration managed to achieve better TPS performance than non-pooled configuration.
 Graph 4 – MySQL with 32G buffer  [ INTERACTIVE GRAPH ][ TABLE ]
The pooled configuration yielded 18,975 TPS against 17,011 for non-pooled server configuration, which is 11.6% win for the thread pool.
The p95 latency in MySQL had a very similar characteristic to Percona Server with thread pool reducing the latency from 759ms down to 153 ms.
Comparison of Percona Server 9.7.1-1 for MySQL and MySQL Server 26.7.0:
Finally, we arrived at the direct TPS comparison.
Let’s start with innodb_buffer_pool_size=2G:
For the comparison we will pick the best performing thread pool configurations for Percona Server and MySQL Server along with baseline (thread pool disabled) configurations. It was established before that thread_pool_size=10 works best for both servers (TP 10 on the graph), but the oversubscription 4 (os4 on the graph) works better for Percona Server while the optimal oversubscription for MySQL Server is 2 (os2 on the graph).
 Graph 5 – Percona Server and MySQL with 2G buffer  [ INTERACTIVE GRAPH ][ TABLE ]

MySQL Server starts well, winning with 40 connections, but we do not consider thread pool efficiency to be good with low number of connections.
At 2560 connections mark the Percona Server implementation of the thread pool helps it to produce more TPS than MySQL server by a factor of 6.2 (2,304 against 371 TPS).
 Graph 6 – Percona Server and MySQL with 12G buffer  [ INTERACTIVE GRAPH ][ TABLE ]
Again, at a lower number of connections (40 and 80) MySQL Server goes faster than Percona Server.
Moving to the high number of connections (2560) with innodb_buffer_pool_size=12G the Percona thread pool performs better than MySQL.
On the next graph corresponding to innodb_buffer_pool_size=32G, the best performing configurations are different for Percona Server (thread_pool_size=160/thread_pool_oversubscribe=2) and MySQL Server (thread_pool_size=10/thread_pool_oversubscribe=2). It looks that in Percona Server increasing innodb_buffer_pool_size requires increasing thread_pool_size for the thread pool to work optimally.
 Graph 7 – Percona Server and MySQL with 32G buffer  [ INTERACTIVE GRAPH ][ TABLE ]
In the tier of fully buffered data (innodb_buffer_pool_size=32G) both servers performance was close, but MySQL Server was slightly faster. When it comes to the thread pool, MySQL is a clear winner with 19,054 TPS against 15,519 TPS from Percona Server (22% difference).
With regard to p95 the Percona Server showed slightly better (lower) latency than MySQL server in 2G and 12G buffer tiers. However, with 32G the MySQL Server is ahead of Percona Server:
 Graph 8 – p95 latency in Percona Server and MySQL with 32G buffer  [ INTERACTIVE GRAPH ][ TABLE ]


With disabled thread pool the latencies for both Percona Server and MySQL land close to each other with MySQL being slightly better (shorter).
When thread pool is enabled the p95 latency in MySQL is notably better (by ~25%) with 177ms against 235ms in Percona server.
Conclusion
Despite sharing the base server code, the thread pool implementations in Percona Server for MySQL and MySQL are very different. When the data set is too big to be fully buffered the Percona Server thread pool shows better performance compared to MySQL Server. However, MySQL Server and its thread pool wins with the configurations when data is fully buffered. Also, in the fully buffered scenario the p95 latency in MySQL Server is slightly shorter than in Percona Server.
Another important observation is that the thread pool inside Percona Server for MySQL is very sensitive to the number of thread groups (thread_pool_size) and the number of active threads in the same group (thread_pool_oversubscribe). These options had the biggest impact on the performance and setting optimal values is important to reach the full potential of the thread pool.
With MySQL Server change of these parameters has less dramatic effect. Also, the noticeable benefit of thread pooling in case of MySQL is visible only on high connection numbers such as 1280/2560 while Percona Server implementation does it much earlier with 80+ connections.
The post Thread Pool in Percona Server and MySQL (Part 2) appeared first on Percona.</description>
    <content:encoded><![CDATA[<h2><span>Thread Pool in Percona Server and MySQL (Part 2)</span></h2>
<p><span>The previous <a href="https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-1/">Part 1</a> focused on explaining the idea of thread pool, configuration options and analysis of thread pool efficiency in Percona Server for MySQL 9.7.1-1 against the base line, which is the same server without thread pooling.</span></p>
<p><span>We recommend reading the post with <a href="https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-1/">Part 1</a> before proceeding with reading Part 2 in the current post.</span></p>
<h4><span>MySQL 26.7.0 with and without thread pool:</span></h4>
<p><span>For better understanding of Percona Server 9.7.1-1 and MySQL 26.7.0 / 9.7.2 thread pool performance against each other we will start with a brief look at MySQL on its own.</span></p>
<p><span>The test runs did not find any significant differences in the performance of the thread pool between MySQL Server 26.7.0 and 9.7.2:</span></p>
<p><span> <b>Graph 1 – Comparison of MySQL 26.7.0 and 9.7.2 </b>[ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=mysql+26.7.0%2Cmysql+9.7.2&amp;mem=2&amp;tp=off%2C10&amp;os=2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=mysql+26.7.0%2Cmysql+9.7.2&amp;mem=2&amp;tp=off%2C10&amp;os=2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span><img decoding="async" class="alignnone size-full wp-image-54399" src="https://www.percona.com/wp-content/uploads/2026/10/101.png" alt="Graph 1 - Comparison of MySQL 26.7.0 and 9.7.2" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/101.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/101-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/101-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/101-768x485.png 768w" sizes="(max-width: 1235px) 100vw, 1235px"></span></p>
<p><span>As can be seen on the Graph 1 – the variation in performance between the versions is minimal. Configurations without thread pool go extremely close to each other, same as with the thread pool.</span></p>
<p><span>Lines that correspond to similar configurations converge to the same values across all server buffer tiers (2G / 12G / 32G) and across thread pool parameter ranges (<b>thread_pool_size=[10/20/40/80/120/160]</b> and <b>thread_pool_oversubscribe=[2/3/4]</b>).</span></p>
<p><span><b>NOTE</b>: MySQL 26.7.0 is very slightly faster than 9.7.2 in the high connection threads count.</span></p>
<p><span>Therefore, to avoid the crowded graphs the data and performance comparisons will be presented for MySQL Version 26.7.0 only. The results for MySQL 9.7.2 can be viewed on the </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html" target="_blank" rel="noopener"><span>interactive graph</span></a><span> page on GitHub.</span></p>
<p><span> <b>Graph 2 – MySQL with 2G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=2&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=2&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54401" src="https://www.percona.com/wp-content/uploads/2026/10/102.png" alt="Graph 2 - MySQL with 2G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/102.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/102-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/102-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/102-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></span></p>
<p><span>The thread pool implementation behaves differently in MySQL Server 26.7.0. Although it does not bring a huge performance boost as it was in Percona Server, the buffer pool for a high number of client connection threads (640 / 1280 / 2560) helps to keep TPS performance better than without the thread pool. Especially with 2560 connections the pooled TPS is three times the number of non-pooled TPS: 371 and 122 respectively.</span></p>
<p><span>The selection of the optimal value for <b>thread_pool_size</b> is important. The value itself (10) looks the same as it was with Percona Server. In the same way, higher values for <b>thread_pool_size</b> (20 / 40 / 80 / 120 / 160) do less TPS improvements.</span></p>
<p><span>Unlike Percona Server the oversubscription variations do not impact TPS much when </span><b>innodb_buffer_pool_size=2G</b><span>. However, <b>thread_pool_oversubscribe=2</b> does slightly better than 3 or 4.</span></p>
<p><span> <b>Graph 3 – MySQL with 12G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=12&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=12&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54403" src="https://www.percona.com/wp-content/uploads/2026/10/103.png" alt="Graph 3 - MySQL with 12G buffer " width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/103.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/103-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/103-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/103-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>Increasing buffer pool (<b>innodb_buffer_pool_size=12G</b>) brings the overall TPS performance up, but thread pool makes less impact at the highest number of connections: 1273 with thread pool and 718 without (TPS is improved by 77%).</span></p>
<p><span>With the fully buffered data set (<b>innodb_buffer_pool_size=32G</b>) MySQL thread pool configuration managed to achieve better TPS performance than non-pooled configuration.</span></p>
<p><span> <b>Graph 4 – MySQL with 32G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=32&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=mysql+26.7.0&amp;mem=32&amp;tp=all&amp;os=2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54404" src="https://www.percona.com/wp-content/uploads/2026/10/104.png" alt="Graph 4 - MySQL with 32G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/104.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/104-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/104-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/104-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>The pooled configuration yielded 18,975 TPS against 17,011 for non-pooled server configuration, which is 11.6% win for the thread pool.</span></p>
<p><span>The p95 latency in MySQL had a very similar characteristic to Percona Server with thread pool reducing the latency from 759ms down to 153 ms.</span></p>
<h4><span>Comparison of Percona Server 9.7.1-1 for MySQL and MySQL Server 26.7.0:</span></h4>
<p><span>Finally, we arrived at the direct TPS comparison.</span><span><br>
</span><span>Let’s start with <b>innodb_buffer_pool_size=2G</b>:</span></p>
<p><span>For the comparison we will pick the best performing thread pool configurations for Percona Server and MySQL Server along with baseline (thread pool disabled) configurations. It was established before that <b>thread_pool_size=10</b> works best for both servers (TP 10 on the graph), but the oversubscription 4 (os4 on the graph) works better for Percona Server while the optimal oversubscription for MySQL Server is 2 (</span><b>os2</b><span> on the graph).</span></p>
<p><span> <b>Graph 5 – Percona Server and MySQL with 2G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=2&amp;tp=off%2C10%2C20&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+2G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+2G+%7C+TP+20+os2%2CPercona-Server+9.7.1-1+%7C+2G+%7C+TP+20+os4%2Cmysql+26.7.0+%7C+2G+%7C+TP+10+os4%2Cmysql+26.7.0+%7C+2G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+2G+%7C+TP+20+os4%2Cmysql+9.7.2+%7C+2G+%7C+TP+off%2Cmysql+9.7.2+%7C+2G+%7C+TP+10+os2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=2&amp;tp=off%2C10%2C20&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+2G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+2G+%7C+TP+20+os2%2CPercona-Server+9.7.1-1+%7C+2G+%7C+TP+20+os4%2Cmysql+26.7.0+%7C+2G+%7C+TP+10+os4%2Cmysql+26.7.0+%7C+2G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+2G+%7C+TP+20+os4%2Cmysql+9.7.2+%7C+2G+%7C+TP+off%2Cmysql+9.7.2+%7C+2G+%7C+TP+10+os2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]<img loading="lazy" decoding="async" class="alignnone size-full wp-image-54406" src="https://www.percona.com/wp-content/uploads/2026/10/105b.png" alt="Graph 5 - Percona Server and MySQL with 2G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/105b.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/105b-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/105b-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/105b-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"><br>
</span></span></p>
<p><span>MySQL Server starts well, winning with 40 connections, but we do not consider thread pool efficiency to be good with low number of connections.</span></p>
<p><span>At 2560 connections mark the Percona Server implementation of the thread pool helps it to produce more TPS than MySQL server by a factor of 6.2 (2,304 against 371 TPS).</span></p>
<p><span> <b>Graph 6 – Percona Server and MySQL with 12G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=12&amp;tp=off%2C10%2C20&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+12G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+12G+%7C+TP+10+os4%2CPercona-Server+9.7.1-1+%7C+12G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+12G+%7C+TP+10+os4%2Cmysql+26.7.0+%7C+12G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+12G+%7C+TP+20+os4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=12&amp;tp=off%2C10%2C20&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+12G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+12G+%7C+TP+10+os4%2CPercona-Server+9.7.1-1+%7C+12G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+12G+%7C+TP+10+os4%2Cmysql+26.7.0+%7C+12G+%7C+TP+20+os2%2Cmysql+26.7.0+%7C+12G+%7C+TP+20+os4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54408" src="https://www.percona.com/wp-content/uploads/2026/10/106.png" alt="Graph 6 - Percona Server and MySQL with 12G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/106.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/106-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/106-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/106-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>Again, at a lower number of connections (40 and 80) MySQL Server goes faster than Percona Server.</span></p>
<p><span>Moving to the high number of connections (2560) with <b>innodb_buffer_pool_size=12G</b> the Percona thread pool performs better than MySQL.</span></p>
<p><span>On the next graph corresponding to <b>innodb_buffer_pool_size=32G</b>, the best performing configurations are different for Percona Server (<b>thread_pool_size=160/thread_pool_oversubscribe=2</b>) and MySQL Server (<b>thread_pool_size=10/thread_pool_oversubscribe=2</b>). It looks that in Percona Server increasing <b>innodb_buffer_pool_size</b> requires increasing <b>thread_pool_size</b> for the thread pool to work optimally.</span></p>
<p><span> <b>Graph 7 – Percona Server and MySQL with 32G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=32&amp;tp=off%2C10%2C160&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+32G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+32G+%7C+TP+10+os4%2CPercona-Server+9.7.1-1+%7C+32G+%7C+TP+160+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+10+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+160+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+160+os4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=32&amp;tp=off%2C10%2C160&amp;os=2%2C4&amp;shape=box&amp;points=all&amp;median=lines&amp;hide=Percona-Server+9.7.1-1+%7C+32G+%7C+TP+10+os2%2CPercona-Server+9.7.1-1+%7C+32G+%7C+TP+10+os4%2CPercona-Server+9.7.1-1+%7C+32G+%7C+TP+160+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+10+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+160+os2%2Cmysql+26.7.0+%7C+32G+%7C+TP+160+os4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54411" src="https://www.percona.com/wp-content/uploads/2026/10/107.png" alt="Graph 7 - Percona Server and MySQL with 32G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/107.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/107-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/107-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/107-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>In the tier of fully buffered data (<b>innodb_buffer_pool_size=32G</b>) both servers performance was close, but MySQL Server was slightly faster. When it comes to the thread pool, MySQL is a clear winner with 19,054 TPS against 15,519 TPS from Percona Server (22% difference).</span></p>
<p><span>With regard to p95 the Percona Server showed slightly better (lower) latency than MySQL server in 2G and 12G buffer tiers. However, with 32G the MySQL Server is ahead of Percona Server:</span></p>
<p><span> <b>Graph 8 – p95 latency in Percona Server and MySQL with 32G buffer </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=lat95&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=32&amp;tp=off%2C40&amp;os=2&amp;shape=box&amp;points=all&amp;median=lines" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=lat95&amp;server=Percona-Server+9.7.1-1%2Cmysql+26.7.0&amp;mem=32&amp;tp=off%2C40&amp;os=2&amp;shape=box&amp;points=all&amp;median=lines" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]<br>
<img loading="lazy" decoding="async" class="alignnone size-full wp-image-54412" src="https://www.percona.com/wp-content/uploads/2026/10/108.png" alt="Graph 8 - p95 latency in Percona Server and MySQL with 32G buffer" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/108.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/108-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/108-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/108-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"><br>
</span></span></p>
<p><span>With disabled thread pool the latencies for both Percona Server and MySQL land close to each other with MySQL being slightly better (shorter).</span></p>
<p><span>When thread pool is enabled the p95 latency in MySQL is notably better (by ~25%) with 177ms against 235ms in Percona server.</span></p>
<h2><span>Conclusion</span></h2>
<p><span>Despite sharing the base server code, the thread pool implementations in Percona Server for MySQL and MySQL are very different. When the data set is too big to be fully buffered the Percona Server thread pool shows better performance compared to MySQL Server. However, MySQL Server and its thread pool wins with the configurations when data is fully buffered. Also, in the fully buffered scenario the p95 latency in MySQL Server is slightly shorter than in Percona Server.</span></p>
<p><span>Another important observation is that the thread pool inside Percona Server for MySQL is very sensitive to the number of thread groups (<b>thread_pool_size</b>) and the number of active threads in the same group (<b>thread_pool_oversubscribe</b>). These options had the biggest impact on the performance and setting optimal values is important to reach the full potential of the thread pool.</span></p>
<p><span>With MySQL Server change of these parameters has less dramatic effect. Also, the noticeable benefit of thread pooling in case of MySQL is visible only on high connection numbers such as 1280/2560 while Percona Server implementation does it much earlier with 80+ connections.</span></p>
<p>The post <a href="https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-2/">Thread Pool in Percona Server and MySQL (Part 2)</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 12 Oct 2026 11:14:07 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Benchmarks</category>
    <category>MySQL</category>
    <category>benchmark</category>
    <category>Percona Server for MySQL</category>
    <category>Thread pool</category>
  </item>

  <item>
    <title>FOSS 2026 conference in Chernihiv: how it went</title>
    <guid isPermaLink="false">https://percona.community/blog/2026/10/12/foss-2026-conference-in-chernihiv-how-it-went/</guid>
    <link>https://percona.community/blog/2026/10/12/foss-2026-conference-in-chernihiv-how-it-went/</link>
    <description>On October 1-2, Chernihiv Polytechnic National University held the II International scientific and practical conference “Free/Open Source Software in Education, Science &amp;amp; Business”. The conference was online and free for everyone. The idea is to bring together people from universities, from industry and from open source communities and talk about how free software is used in education, science and business.
This year Percona was well represented. There were six speakers from Percona, and two more keynote speakers also worked at Percona in the past.
Day 1: keynotes
The first day started with welcome words from Volodymyr Bazylevych, Chair of the Organizing Committee, and Anatolii Prystupa, Vice-Rector for Research. After that there were five keynote talks.
Yevhen Patlan (Percona) spoke about embedding DuckDB into MySQL. MySQL works very well for transactions, but analytical queries over hundreds of millions of rows can take a long time on InnoDB. Usually people solve this by copying data to a separate column store, so they need to support two systems and a pipeline between them. Yevhen showed another way: run DuckDB inside MySQL. He talked about Percona’s experimental DuckDB engine for MySQL 9.7: how query pushdown works, how results stay the same as in InnoDB, and the TPC-H results on 500 GB. He also told about a two-phase commit bug that made a replica lose data without any error.
Frédéric Descamps (MariaDB Foundation), known in the community as “lefred”, worked many years at Percona and then in the MySQL Community Team. This year he joined the MariaDB Foundation. In his talk he showed MariaDB Server features that people often don’t know about: system-versioned tables, flashback, sequences, invisible columns and others. For every feature he explained which real problem it solves.
Sergii Shchaslyvyi (AWS) talked about virtualization. After the licensing changes, many companies started to look for alternatives to their current virtualization platform. Sergii explained what migration options exist and where open source technologies make sense.
Oleksiy Lukin (Percona) is not a new person for this conference. He worked at Chernihiv Polytechnic for more than 20 years and has been one of the organizers since 2009. Oleksiy presented kmipclient and kmipcore, an open source C++ library for KMIP (Key Management Interoperability Protocol). It supports KMIP 1.4 and 2.0, it is tested with five different KMIP servers, and Percona Server for MySQL already uses it.
Vinicius Grippa (Readyset, Oracle ACE Director) finished the first day. Before Readyset he worked at Percona for eight years. He presented myflames, an open source tool that builds flame graphs, treemaps and Visual Explain style diagrams from MySQL execution plans. With these pictures it is much easier to understand where a query spends time.
Day 2: track talks
On the second day there were 13 talks from researchers, teachers and students. A team from Chernihiv Polytechnic presented a browser-based simulator for STM32 firmware and electrical circuits. Other talks were about microgrid optimization with CVXPY, local AI on x86 NPUs, digital education in crisis conditions, cyber hygiene for students and teaching steganography with free software.
Four more Percona engineers spoke on this day:

Adnan Šupić - zero-downtime migration with Percona ClusterSync for MongoDB
Puneet Kala - a practical open source stack for running MongoDB
Fabio Silva - how PMM hosts another application inside an iframe
Briana Swift - how to manage public messaging for seven database technologies without a PMO, using a public GitHub repository and treating text like code

Some numbers

82 registered participants
51 submitted theses
18 talks
more than 90 people online
participants from Ukraine, Belgium, Brazil, The Gambia, India and other countries

Big thanks to the computer engineering students of Chernihiv Polytechnic. They were active during both days and asked speakers a lot of good questions.
Thanks to our partners
The conference would not be possible without our partners:

Chernihiv Polytechnic National University - for hosting and organizing the conference.
WIN-WIN EDIH - the conference was held as part of this project, co-funded by the European Union.
Percona - for the support and for six speakers.
MariaDB Foundation - for the support and the keynote about MariaDB Server.
Amazon Web Services (AWS) - for the support and the keynote about migration from proprietary virtualization.
Readyset - for the support and the keynote about myflames.
OurSQL Foundation - for the support of the conference.
PortaOne - for the support of the conference.
Chernihiv IT Cluster (Chernihiv.IT) - for helping to connect the university with the local IT community.

Thanks to you, the conference stays free, and Ukrainian students and researchers can listen to and talk with people from the international open source community.
Recordings and materials

Keynote recordings on YouTube: https://www.youtube.com/@foss-che
Program and slides: https://feit.stu.cn.ua/foss/
Conference proceedings will be published at the end of October.

Thanks to all speakers, authors and participants. See you at FOSS 2027!</description>
    <content:encoded><![CDATA[<p>On October 1-2, Chernihiv Polytechnic National University held the II International scientific and practical conference <a href="https://percona.community/events/2026-feit-stu-foss/">“Free/Open Source Software in Education, Science &amp; Business”</a>. The conference was online and free for everyone. The idea is to bring together people from universities, from industry and from open source communities and talk about how free software is used in education, science and business.</p>
<p>This year Percona was well represented. There were six speakers from Percona, and two more keynote speakers also worked at Percona in the past.</p>
<h2>Day 1: keynotes</h2>
<p>The first day started with welcome words from Volodymyr Bazylevych, Chair of the Organizing Committee, and Anatolii Prystupa, Vice-Rector for Research. After that there were five keynote talks.</p>
<p><strong>Yevhen Patlan (Percona)</strong> spoke about embedding DuckDB into MySQL. MySQL works very well for transactions, but analytical queries over hundreds of millions of rows can take a long time on InnoDB. Usually people solve this by copying data to a separate column store, so they need to support two systems and a pipeline between them. Yevhen showed another way: run DuckDB inside MySQL. He talked about Percona’s experimental DuckDB engine for MySQL 9.7: how query pushdown works, how results stay the same as in InnoDB, and the TPC-H results on 500 GB. He also told about a two-phase commit bug that made a replica lose data without any error.</p>
<p><strong>Frédéric Descamps (MariaDB Foundation)</strong>, known in the community as “lefred”, worked many years at Percona and then in the MySQL Community Team. This year he joined the MariaDB Foundation. In his talk he showed MariaDB Server features that people often don’t know about: system-versioned tables, flashback, sequences, invisible columns and others. For every feature he explained which real problem it solves.</p>
<p><strong>Sergii Shchaslyvyi (AWS)</strong> talked about virtualization. After the licensing changes, many companies started to look for alternatives to their current virtualization platform. Sergii explained what migration options exist and where open source technologies make sense.</p>
<p><strong>Oleksiy Lukin (Percona)</strong> is not a new person for this conference. He worked at Chernihiv Polytechnic for more than 20 years and has been one of the organizers since 2009. Oleksiy presented kmipclient and kmipcore, an open source C++ library for KMIP (Key Management Interoperability Protocol). It supports KMIP 1.4 and 2.0, it is tested with five different KMIP servers, and Percona Server for MySQL already uses it.</p>
<p><strong>Vinicius Grippa (Readyset, Oracle ACE Director)</strong> finished the first day. Before Readyset he worked at Percona for eight years. He presented myflames, an open source tool that builds flame graphs, treemaps and Visual Explain style diagrams from MySQL execution plans. With these pictures it is much easier to understand where a query spends time.</p>
<h2>Day 2: track talks</h2>
<p>On the second day there were 13 talks from researchers, teachers and students. A team from Chernihiv Polytechnic presented a browser-based simulator for STM32 firmware and electrical circuits. Other talks were about microgrid optimization with CVXPY, local AI on x86 NPUs, digital education in crisis conditions, cyber hygiene for students and teaching steganography with free software.</p>
<p>Four more Percona engineers spoke on this day:</p>
<ul>
<li><strong>Adnan Šupić</strong> - zero-downtime migration with Percona ClusterSync for MongoDB</li>
<li><strong>Puneet Kala</strong> - a practical open source stack for running MongoDB</li>
<li><strong>Fabio Silva</strong> - how PMM hosts another application inside an iframe</li>
<li><strong>Briana Swift</strong> - how to manage public messaging for seven database technologies without a PMO, using a public GitHub repository and treating text like code</li>
</ul>
<h2>Some numbers</h2>
<ul>
<li>82 registered participants</li>
<li>51 submitted theses</li>
<li>18 talks</li>
<li>more than 90 people online</li>
<li>participants from Ukraine, Belgium, Brazil, The Gambia, India and other countries</li>
</ul>
<p>Big thanks to the computer engineering students of Chernihiv Polytechnic. They were active during both days and asked speakers a lot of good questions.</p>
<h2>Thanks to our partners</h2>
<p>The conference would not be possible without our partners:</p>
<ul>
<li><strong>Chernihiv Polytechnic National University</strong> - for hosting and organizing the conference.</li>
<li><strong>WIN-WIN EDIH</strong> - the conference was held as part of this project, co-funded by the European Union.</li>
<li><strong>Percona</strong> - for the support and for six speakers.</li>
<li><strong>MariaDB Foundation</strong> - for the support and the keynote about MariaDB Server.</li>
<li><strong>Amazon Web Services (AWS)</strong> - for the support and the keynote about migration from proprietary virtualization.</li>
<li><strong>Readyset</strong> - for the support and the keynote about myflames.</li>
<li><strong>OurSQL Foundation</strong> - for the support of the conference.</li>
<li><strong>PortaOne</strong> - for the support of the conference.</li>
<li><strong>Chernihiv IT Cluster (Chernihiv.IT)</strong> - for helping to connect the university with the local IT community.</li>
</ul>
<p>Thanks to you, the conference stays free, and Ukrainian students and researchers can listen to and talk with people from the international open source community.</p>
<h2>Recordings and materials</h2>
<ul>
<li>Keynote recordings on YouTube: <a href="https://www.youtube.com/@foss-che" target="_blank" rel="noopener noreferrer">https://www.youtube.com/@foss-che</a></li>
<li>Program and slides: <a href="https://feit.stu.cn.ua/foss/" target="_blank" rel="noopener noreferrer">https://feit.stu.cn.ua/foss/</a></li>
<li>Conference proceedings will be published at the end of October.</li>
</ul>
<p>Thanks to all speakers, authors and participants. See you at FOSS 2027!</p>]]></content:encoded>
    <pubDate>Mon, 12 Oct 2026 10:00:00 +0000</pubDate>
    <dc:creator>Percona Community</dc:creator>
    <category>Percona</category>
    <category>Community</category>
    <category>Events</category>
    <category>Conferences</category>
    <category>Open Source</category>
    <category>Education</category>
    <category>MySQL</category>
    <category>MongoDB</category>
    <category>PMM</category>
  </item>

  <item>
    <title>Meet MySQL 26.7: A New Versioning Era Begins</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54581</guid>
    <link>https://www.percona.com/blog/meet-mysql-26-7-a-new-versioning-era-begins/</link>
    <description>Over the period, we have witnessed how MySQL versioning evolved from 8.0 to 9.7 LTS, and now it has entered a new era: a calendar-based model.
Let’s first take a brief look at how MySQL versioning has evolved over time.

Phase 1 – MySQL 8.0 (2018–2026) used a traditional single-track release model, with all new features and bug fixes rolled into the same 8.0 release line.
Phase 2 – Oracle launches an Innovative/LTS dual-track model (Mid 2023). Innovation releases focus on new features and changes to short-lived performance on a quarterly basis. The LTS (Long Term Stable) release is steadier and has a long lifespan, with a cycle of 5 years of Premier Support + 3 years of Extended Support, and it typically launches every 2 years.
Phase 3 – There are chains of Innovation releases following (8.1, 8.2, 8.3) between (2023- 2024), leading to the first LTS (8.4) in April 2024.
Phase 4 – There was a shift in the Innovative versioning numbering, and we moved in the next 9.7 series. v9.0 to v9.6 are part of the Innovative release model (2024-26). We got our second LTS (9.7) in April 2026, with a 5-year Premier Support cycle followed by 3 years of Extended Support.

After going through all those phases, we have finally reached a major shift in the overall versioning model. Instead of the next Innovation release to v9.8, Oracle switched to calendar versioning mode (Year.Month.Patch). So, the new Innovative release shipped as v26.7.0 instead of v9.8.
Now, let’s discuss the new versioning model a bit.
The new versioning model is denoted as Year.Month.Patch, or YY.M.P.

26 represents 2026(Year).
7 represents July (Month).
0 is the PATCH Number.

The same quarterly cycle is followed for each subsequent Innovative release; the only difference is that it now uses the calendar versioning model. For example,  the next innovation release would tentatively be v26.10.0, scheduled for launch in October 2026.
The LTS release can also be considered roughly every 2 years under the new versioning model. The next LTS release in the new scheme is expected in April 2028 as 28.4.0.
Why choose the new Versioning Model?
The real advantage we see is that with the format (Year.Month.Patch), we can better assess when releases/packages were shipped and how they relate to overall release planning (Innovative/LTS, etc.). This was a problem with the previous versioning model, say 8.4/9.7, as we don’t get much clue or information.
This has no impact on the existing LTS (8.4/9.7); they will get the usual quarterly maintenance updates (bug fixes and security patches) while all Innovation or new features are offered under the new Calendar series.
How does the upgrade cycle work?

MySQL 9.7.x LTS is the sole entry point into the first calendar-versioned lineage 26.7.0, so we must first upgrade to 9.7 LTS if we need to go with a calendar version upgrade to 26.7.0. 
8.4.x LTS or any previous version won’t simply work when considering upgrading to a new lineage.
Singe hope upgrade from v9.7.x LTS to 26.7.0 or any later release in that same lineage (e.g., 26.10.0, 27.1.0, 27.4.0) is still valid until the next LTS, v28.4.0.

E.g,8.4.x LTS -&amp;gt; 9.7.x LTS -&amp;gt; 26.7.0So if someone is still using an EOL version like 5.7, 8.0 has to follow the above path and upgrade to 8.4 LTS first.
What features and enhancements does v26.7 bring to the table?
1) Change Stream Applier for Multi-Threaded Replication
The new change stream applier (CSA) introduces further improvements to parallel transaction scheduling and execution. The original model, Multi-threaded Applier (MTA), basically uses a coordinator to read transactions sequentially from the relay files and then hand them off to worker threads for parallel applying and execution. This can be a limitation, especially when a worker thread is waiting/sitting Idle for a commit due to any prior or dependency transaction.
Change stream applier (CSA)  basically separates the transaction applying from commit ordering. For example, if a worker finishes applying a transaction but has to wait for the commit order, it can work on another dependency-ready transaction instead. 
The replication configurations below are introduced by CSR adoption. Please note that it works for each channel separately.APPLIER_VERSION  -  “1” denotes for MTA and “2“ denotes for CSA
APPLIER_WORKER_COUNT - different worker count based on each replication channel 
APPLIER_EVENT_MEMORY_LIMIT - Per-channel memory limits for cached binlog events
CSA has some more added advantages:

CSA workers can simultaneously read transaction events from the relay log and release event memory when events are consumed.
Relay log files can also be marked for concurrent purging once all transactions have been applied successfully and there is nothing left to read from the relay files.

How to apply the change stream applier?
mysql&amp;gt; STOP REPLICA FOR CHANNEL 'mysql1';mysql&amp;gt; CHANGE REPLICATION SOURCE TO
APPLIER_VERSION = 2,
APPLIER_WORKER_COUNT = 8,
APPLIER_EVENT_MEMORY_LIMIT = 1073741824,
REQUIRE_ROW_FORMAT = 1,
GTID_ONLY = 1
FOR CHANNEL 'mysql1';mysql&amp;gt; START REPLICA FOR CHANNEL 'mysql1';
Requirements to use CSA
Row-Based replication (binlog_format=ROW)
GTID enforcement (gtid_mode=ON)
2) Thread Pool Plugin
The good news is that the Thread Pool Plugin is now available in MySQL Community releases, removing the previous dependency on the Enterprise Edition to use the Thread Pool mechanism.
MySQL traditionally uses a one-thread-per-connection model, where each client connection creates a dedicated server thread. This approach can become a problem when an application establishes a very large number of concurrent connections, especially when many of those connections are Idle/Sleep(waiting for work).
The Thread Pool addresses this by managing a high volume of client connections through a smaller number of worker threads. So, instead of creating a new thread for each connection, the Thread Pool uses the connections from the existing pool of threads and schedules their work across a controlled number of worker threads.
This could be useful in an environment where applications trigger a very high number of concurrent database connections, and simply increasing max_connections is not an option, as it would lead to excessive thread creation and resource consumption during database setup.
How to enable the Thread Pool?

Load thread pool plugin

[mysqld]
plugin-load-add=thread_pool.so

Restart the database service

bash&amp;gt; Systemctl restart mysql

Verify plugin active

mysql &amp;gt;  SELECT PLUGIN_NAME, PLUGIN_STATUS
    -&amp;gt;        FROM INFORMATION_SCHEMA.PLUGINS
    -&amp;gt;        WHERE PLUGIN_NAME LIKE 'thread%';
+-------------+---------------+
| PLUGIN_NAME | PLUGIN_STATUS |
+-------------+---------------+
| thread_pool | ACTIVE        |
+-------------+---------------+
1 row in set (0.004 sec) 
Thread Pool configuration/Monitoring
The thread pool size and other related parameters can be listed and configured as needed.mysql&amp;gt; show variables like 'thread_pool%';
+----------------------------------------+-------+
| Variable_name                          | Value |
+----------------------------------------+-------+
| thread_pool_algorithm                  | 1     |
| thread_pool_connection_report_interval | 120   |
| thread_pool_dedicated_listeners        | OFF   |
| thread_pool_high_priority_connection   | 0     |
| thread_pool_longrun_trx_limit          | 2000  |
| thread_pool_max_active_query_threads   | 0     |
| thread_pool_max_transactions_limit     | 192   |
| thread_pool_max_unused_threads         | 32    |
| thread_pool_prio_kickup_timer          | 1000  |
| thread_pool_query_threads_per_group    | 2     |
| thread_pool_size                       | 6     |
| thread_pool_stall_limit                | 6     |
| thread_pool_transaction_delay          | 0     |
+----------------------------------------+-------+
13 rows in set (0.003 sec)Performance_schema provided a few tables to monitor thread pool activity, along with the status variables.mysql &amp;gt; show tables like 'tp%';
+------------------------------------+
| Tables_in_performance_schema (tp%) |
+------------------------------------+
| tp_connections                     |
| tp_thread_group_state              |
| tp_thread_group_stats              |
| tp_thread_state                    |
+------------------------------------+
4 rows in set (0.001 sec)mysql&amp;gt;  show status like 'thread_pool_%';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| Thread_pool_query_threads | 14    |
+---------------------------+-------+
1 row in set (0.001 sec)
3) Undo Log Truncation Improvement
MySQL 26.7 improves the robustness of InnoDB undo tablespace truncation. Previously, InnoDB used a separate undo_{space_number}_trunc.log file to keep track of an undo tablespace truncation operation. This external file was used to determine whether a truncation operation was in progress, particularly during recovery.
Now this approach has changed: instead of maintaining a separate truncate log file, the truncation state is captured directly in the undo tablespace header.
This improves both undo truncation and recovery as it does not rely on a separate dedicated file.
4)  InnoDB recovery and metadata infrastructure Improvement
There were some internal changes, or rather improvements, to the structure and efficiency of the core InnoDB recovery, metadata, redo-log, tablespace, and transaction-visibility code paths.
This doesn’t bring any changes to the usage point of view.  The goal is to make these components easier to maintain, test, and extend without changing user-visible SQL semantics.
5) Group Replication default communication stack
The group_replication_communication_stack now defaults to “MySQL” from “XCOM”. This change is more aligned with the MySQL Server authentication and connection security. 
This also means no separate communication port needs to be specified for group replication variables (group_replication_local_address and group_replication_group_seeds). The native MySQL port (3306)/ MEMBER_PORT or whatever is defined for MySQL nodes can be used instead. 
In short, the default MySQL communication stack provides the benefits below. 

Overall, it simplifies the creation of InnoDB Clusters by using MySQL Server’s connection security in place of the Group Replication implementation.
It removes the need for an extra network address or port for internal Group Replication communications.
Using the MySQL protocol means that standard user authentication methods can be used to grant or revoke access to the group, in place of the allow list.
Supports network namespaces for Group Replication.

Note – If migrating from the old environment or switching from “XCOM” to “MySQL” simply switching the communication stack wouldn’t work.mysql&amp;gt; set global group_replication_communication_stack = 'MySQL';Group replication must be stopped on all members, and then the relevant changes should be applied. Finally, the cluster needs to be bootstrapped. 
Basically, this activity requires downtime to successfully switch the communication stack.
Also, the XCom communication stack is deprecated and could be removed in future releases, so better to switch to the MySQL stack. Further complete release notes can be checked here – https://dev.mysql.com/doc/relnotes/mysql/26.7/en/news-26-7-0.html
Conclusion
The transition to MySQL 26.7 introduces a major shift to a calendar-based versioning scheme (Year.Month.Patch), providing clearer alignment with release timelines and future planning across both Innovation releases and long-term support cycles. Upgrading to this new series requires being on MySQL 9.7 LTS as the primary baseline before moving forward into the 26.x series.
Alongside versioning improvements, MySQL 26.7 delivers significant architecture and usability enhancements. Key features include the Change Stream Applier (CSA) for optimised parallel transaction execution and replication performance, as well as the native availability of the Thread Pool Plugin in Community editions to manage high-concurrency connections effectively without relying on Enterprise Edition extras.

The post Meet MySQL 26.7: A New Versioning Era Begins appeared first on Percona.</description>
    <content:encoded><![CDATA[<p><span>Over the period, we have witnessed how MySQL versioning evolved from 8.0 to 9.7 LTS, and now it has entered a new era: </span><b>a calendar-based model</b><span>.</span></p>
<p><span>Let’s first take a brief look at how MySQL versioning has evolved over time.</span></p>
<ul>
<li aria-level="1"><b>Phase 1</b><span><span> – MySQL 8.0 (2018–2026) used a traditional single-track release model, with all new features and bug fixes rolled into the same 8.0 release line.</span></span></li>
<li aria-level="1"><b>Phase 2</b><span> – Oracle launches an Innovative/LTS dual-track model (Mid 2023). </span><b>Innovation releases</b><span> focus on new features and changes to short-lived performance on a quarterly basis. The </span><b>LTS (Long Term Stable) release</b><span> is steadier and has a long lifespan, with a cycle of </span><b>5 years of Premier Support + 3 years of Extended Support</b><span>, and it typically launches every </span><b>2 years</b><span><span><span>.</span></span></span></li>
<li aria-level="1"><b>Phase 3</b><span> – There are chains of Innovation releases following (</span><b>8.1</b><span>, </span><b>8.2</b><span>, </span><b>8.3</b><span>) between (2023- 2024), leading to the </span><b>first LTS (8.4)</b><span><span><span> in April 2024.</span></span></span></li>
<li aria-level="1"><b>Phase 4 </b><span>– There was a shift in the Innovative versioning numbering, and we moved in the next </span><b>9.7 series</b><span>. </span><b>v9.0</b><span> to </span><b>v9.6</b><span> are part of the Innovative release model (2024-26). We got our </span><b>second LTS (9.7)</b><span> in April 2026, with a </span><b>5-year Premier Support cycle </b>followed by <b>3 years of Extended Support</b><span>.</span></li>
</ul>
<p><span>After going through all those phases, we have finally reached a major shift in the overall versioning model. Instead of the next </span><b>Innovation release </b>to<b> v9.8</b><span>, Oracle switched to calendar versioning mode (</span><b>Year</b><span>.</span><b>Month</b><span>.</span><b>Patch</b><span>). So, the new Innovative release shipped as </span><b>v26.7.0 </b><span>instead</span> <span>of</span><b> v9.8.</b></p>
<h3><span>Now, let’s discuss the new versioning model a bit.</span></h3>
<p><span>The new versioning model is denoted as </span><b>Year.Month.Patch</b><span>, or </span><b>YY.M.P.</b></p>
<ul>
<li aria-level="1"><span>26 represents </span><b>2026(Year)</b><span>.</span></li>
<li aria-level="1"><span>7 represents </span><b>July (Month)</b><span>.</span></li>
<li aria-level="1"><span>0 is the </span><b>PATCH Number</b><span>.</span></li>
</ul>
<p><span>The same quarterly cycle is followed for each subsequent Innovative release; the only difference is that it now uses the calendar versioning model. For example,  the next innovation release would tentatively be </span><b>v26.10.0</b><span>, scheduled for launch in </span><b>October 2026</b><span>.</span></p>
<p><span>The LTS release can also be considered roughly every 2 years </span><span>under the new versioning model. The next LTS release in the new scheme is expected in </span><b>April 2028</b><span> as </span><b>28.4.0</b><span>.</span></p>
<h3><span>Why choose the new Versioning Model?</span></h3>
<p><span>The real advantage we see is that with the format (</span><b>Year</b><span>.</span><b>Month</b><span>.</span><b>Patch</b><span>), we can better assess when releases/packages were shipped and how they relate to overall release planning (Innovative/LTS, etc.). This was a problem with the previous versioning model, say 8.4/9.7, as we don’t get much clue or information.</span></p>
<p><span>This has no impact on the existing LTS (8.4/9.7); they will get the usual quarterly maintenance updates (bug fixes and security patches) while all Innovation or new features are offered under the new Calendar series.</span></p>
<h3><span>How does the upgrade cycle work?</span></h3>
<ul>
<li aria-level="1"><b>MySQL 9.7.x LTS </b><span>is the sole entry point into the first </span><b>calendar-versioned lineage 26.7.0</b><span>, so we must first upgrade to 9.7 LTS if we need to go with a calendar version upgrade to 26.7.0. </span></li>
<li aria-level="1"><b>8.4.x LTS or any previous version </b><span>won’t simply work when considering upgrading to a new lineage.</span></li>
<li aria-level="1"><span>Singe hope upgrade from</span><b> v9.7.x LTS</b><span> to </span><b>26.7.0 </b><span>or any later release in that same lineage (e.g., 26.10.0, 27.1.0, 27.4.0) is still valid until the next LTS, v28.4.0.</span></li>
</ul>
<p>E.g,</p><pre class="urvanov-syntax-highlighter-plain-tag">8.4.x LTS -&gt; 9.7.x LTS -&gt; 26.7.0</pre><p><span>So if someone is still using an EOL version like 5.7, 8.0 has to follow the above path and upgrade to 8.4 LTS first.</span></p>
<h3><span>What features and enhancements does v26.7 bring to the table?</span></h3>
<h5><span>1) Change Stream Applier for Multi-Threaded Replication</span></h5>
<p><span>The new </span><b>change stream applier (CSA)</b><span> introduces further improvements to parallel transaction scheduling and execution. The original model, </span><b>Multi-threaded Applier (MTA)</b><span>, basically uses a </span><b>coordinator</b><span> to read transactions sequentially from the relay files and then hand them off to </span><b>worker threads </b><span>for parallel applying and execution</span><b>. </b><span>This can be a limitation, especially when a worker thread is waiting/sitting Idle for a commit due to any prior or dependency transaction.</span></p>
<p><b>Change stream applier (CSA)</b><span>  basically separates the transaction applying from commit ordering. For example, if a worker finishes applying a transaction but has to wait for the commit order, it can </span><b>work on another dependency-ready transaction instead</b><span>. </span></p>
<p><span>The replication configurations below are introduced by CSR adoption. Please note that it works for each channel separately.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">APPLIER_VERSION  -  “1” denotes for MTA and “2“ denotes for CSA
APPLIER_WORKER_COUNT - different worker count based on each replication channel 
APPLIER_EVENT_MEMORY_LIMIT - Per-channel memory limits for cached binlog events</pre><p></p>
<h4><span>CSA has some more added advantages:</span></h4>
<ul>
<li aria-level="1"><b>CSA workers</b><span> can </span><b>simultaneously read transaction </b><span>events from the relay log and release event memory when events are consumed.</span></li>
<li aria-level="1"><b>Relay log files</b><span> can also be marked for </span><b>concurrent</b> <b>purging</b><span> once all transactions have been applied successfully and there is nothing left to read from the relay files.</span></li>
</ul>
<h4><span>How to apply the change stream applier?</span></h4>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; STOP REPLICA FOR CHANNEL 'mysql1';</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; CHANGE REPLICATION SOURCE TO
APPLIER_VERSION = 2,
APPLIER_WORKER_COUNT = 8,
APPLIER_EVENT_MEMORY_LIMIT = 1073741824,
REQUIRE_ROW_FORMAT = 1,
GTID_ONLY = 1
FOR CHANNEL 'mysql1';</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; START REPLICA FOR CHANNEL 'mysql1';</pre><p></p>
<h4><span>Requirements to use CSA</span></h4>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">Row-Based replication (binlog_format=ROW)
GTID enforcement (gtid_mode=ON)</pre><p></p>
<h5><span>2) Thread Pool Plugin</span></h5>
<p><span>The good news is that the Thread Pool Plugin is now available in MySQL Community releases, removing the previous dependency on the Enterprise Edition to use the Thread Pool mechanism.</span></p>
<p><span>MySQL traditionally uses a </span><b>one-thread-per-connection</b><span> model, where each client connection creates a dedicated server thread. This approach can become a problem when an application establishes a very large number of concurrent connections, especially when many of those connections are Idle/Sleep(waiting for work).</span></p>
<p><span>The </span><b>Thread Pool</b><span> addresses this by managing a high volume of client connections through a</span><b> smaller number of worker threads</b><span>. So, instead of creating a new thread for each connection, the Thread Pool uses the connections from the existing pool of threads and schedules their work across a controlled number of worker threads.</span></p>
<p><span>This could be useful in an environment where applications trigger a very high number of concurrent database connections, and simply increasing </span><b>max_connections</b><span> is not an option, as it would lead to excessive thread creation and resource consumption during database setup.</span></p>
<h4><span>How to enable the Thread Pool?</span></h4>
<ul>
<li aria-level="1"><span>Load thread pool plugin</span></li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">[mysqld]
plugin-load-add=thread_pool.so</pre><p></p>
<ul>
<li aria-level="1"><span>Restart the database service</span></li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">bash&gt; Systemctl restart mysql</pre><p></p>
<ul>
<li aria-level="1"><span>Verify plugin active</span></li>
</ul>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql &gt;  SELECT PLUGIN_NAME, PLUGIN_STATUS
    -&gt;        FROM INFORMATION_SCHEMA.PLUGINS
    -&gt;        WHERE PLUGIN_NAME LIKE 'thread%';
+-------------+---------------+
| PLUGIN_NAME | PLUGIN_STATUS |
+-------------+---------------+
| thread_pool | ACTIVE        |
+-------------+---------------+
1 row in set (0.004 sec)</pre><p> </p>
<h4><span>Thread Pool configuration/Monitoring</span></h4>
<p><span>The thread pool size and other related parameters can be listed and configured as needed.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show variables like 'thread_pool%';
+----------------------------------------+-------+
| Variable_name                          | Value |
+----------------------------------------+-------+
| thread_pool_algorithm                  | 1     |
| thread_pool_connection_report_interval | 120   |
| thread_pool_dedicated_listeners        | OFF   |
| thread_pool_high_priority_connection   | 0     |
| thread_pool_longrun_trx_limit          | 2000  |
| thread_pool_max_active_query_threads   | 0     |
| thread_pool_max_transactions_limit     | 192   |
| thread_pool_max_unused_threads         | 32    |
| thread_pool_prio_kickup_timer          | 1000  |
| thread_pool_query_threads_per_group    | 2     |
| thread_pool_size                       | 6     |
| thread_pool_stall_limit                | 6     |
| thread_pool_transaction_delay          | 0     |
+----------------------------------------+-------+
13 rows in set (0.003 sec)</pre><p><b>Performance_schema</b><span> provided a few tables to monitor thread pool activity, along with the status variables.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql &gt; show tables like 'tp%';
+------------------------------------+
| Tables_in_performance_schema (tp%) |
+------------------------------------+
| tp_connections                     |
| tp_thread_group_state              |
| tp_thread_group_stats              |
| tp_thread_state                    |
+------------------------------------+
4 rows in set (0.001 sec)</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt;  show status like 'thread_pool_%';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| Thread_pool_query_threads | 14    |
+---------------------------+-------+
1 row in set (0.001 sec)</pre><p></p>
<h5><span>3) Undo Log Truncation Improvement</span></h5>
<p><span>MySQL 26.7 improves the robustness of InnoDB undo tablespace truncation. Previously, InnoDB used a separate </span><b>undo_{space_number}_trunc.log</b><span> file to keep track of an undo tablespace truncation operation. This external file was used to determine whether a truncation operation was in progress, particularly during recovery.</span></p>
<p><span>Now this approach has changed: instead of maintaining a separate truncate log file, the truncation state is captured directly in the </span><a href="https://dev.mysql.com/doc/relnotes/mysql/26.7/en/news-26-7-0.html"><span>undo tablespace header</span></a><span>.</span></p>
<p><span>This improves both undo truncation and recovery as it does not rely on a separate dedicated file.</span></p>
<h5><span>4)  InnoDB recovery and metadata infrastructure Improvement</span></h5>
<p><span>There were some internal changes, or rather improvements, to the structure and efficiency of the core InnoDB recovery, metadata, redo-log, tablespace, and transaction-visibility code paths.</span></p>
<p><span>This doesn’t bring any changes to the usage point of view.  The goal is to make these components easier to maintain, test, and extend without changing user-visible SQL semantics.</span></p>
<h5><span>5) Group Replication default communication stack</span></h5>
<p><span>The </span><b>group_replication_communication_stack</b><span> now defaults to “</span><b>MySQL</b><span>” from “</span><b>XCOM</b><span>”. This change is more aligned with the MySQL Server authentication and connection security. </span></p>
<p><span>This also means no separate communication port needs to be specified for group replication variables (</span><b>group_replication_local_address</b><span> and </span><b>group_replication_group_seeds</b><span>). The native MySQL port (3306)/ MEMBER_PORT or whatever is defined for MySQL nodes can be used instead. </span></p>
<p><span>In short, the default MySQL communication stack provides the benefits below. </span></p>
<ul>
<li aria-level="1"><span>Overall, it simplifies the creation of InnoDB Clusters by using MySQL Server’s connection security in place of the Group Replication implementation.</span></li>
<li aria-level="1"><span>It removes the need for an extra network address or port for internal Group Replication communications.</span></li>
<li aria-level="1"><span>Using the MySQL protocol means that standard user authentication methods can be used to grant or revoke access to the group, in place of the allow list.</span></li>
<li aria-level="1"><span>Supports network namespaces for Group Replication.</span></li>
</ul>
<p><b>Note</b><span> – If migrating from the old environment or switching from “XCOM” to “MySQL” simply switching the communication stack wouldn’t work.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; set global group_replication_communication_stack = 'MySQL';</pre><p><span>Group replication must be stopped on all members, and then the </span><a href="https://dev.mysql.com/doc/refman/8.4/en/group-replication-connection-security.html"><span>relevant changes </span></a><span>should be applied. Finally, the cluster needs to be bootstrapped. </span></p>
<p><span>Basically, this activity requires </span><a href="https://dev.mysql.com/doc/refman/26.7/en/group-replication-system-variables.html#sysvar_group_replication_communication_stack"><span>downtime</span></a><span> to successfully switch the communication stack.</span></p>
<p><span>Also, the XCom communication stack is deprecated and could be removed in future releases, so better to switch to the MySQL stack. Further complete release notes can be checked here – </span><a href="https://dev.mysql.com/doc/relnotes/mysql/26.7/en/news-26-7-0.html"><span>https://dev.mysql.com/doc/relnotes/mysql/26.7/en/news-26-7-0.html</span></a></p>
<h2><span>Conclusion</span></h2>
<p><span>The transition to MySQL 26.7 introduces a major shift to a </span><b>calendar-based versioning scheme </b><span>(</span><b>Year</b><span>.</span><b>Month</b><span>.</span><b>Patch</b><span>), providing clearer alignment with release timelines and future planning across both Innovation releases and long-term support cycles. Upgrading to this new series requires being on MySQL 9.7 LTS as the primary baseline before moving forward into the 26.x series.</span></p>
<p><span>Alongside versioning improvements, MySQL 26.7 delivers significant architecture and usability enhancements. Key features include the </span><a href="https://dev.mysql.com/doc/refman/26.7/en/change-stream-applier.html"><b>Change Stream Applier (CSA)</b></a><span> for optimised parallel transaction execution and replication performance, as well as the </span><a href="https://dev.mysql.com/doc/refman/26.7/en/thread-pool-installation.html"><b>native availability of the Thread Pool Plugin in Community editions</b></a><span> to manage high-concurrency connections effectively without relying on Enterprise Edition extras.</span></p>

<p>The post <a href="https://www.percona.com/blog/meet-mysql-26-7-a-new-versioning-era-begins/">Meet MySQL 26.7: A New Versioning Era Begins</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 12 Oct 2026 04:05:49 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Cloud</category>
    <category>Featured</category>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>Kubernetes</category>
    <category>MySQL</category>
    <category>Open Source</category>
    <category>Operator</category>
    <category>Percona Services</category>
    <category>Percona Software</category>
    <category>Storage Engine</category>
    <category>Uncategorized</category>
    <category>MySQL Asynchronous replication</category>
    <category>MySQL GTID</category>
    <category>MySQL Replication</category>
    <category>MySQL Thread Pool</category>
    <category>MySQL V26.7</category>
  </item>

  <item>
    <title>Docker DBA Lab</title>
    <guid isPermaLink="false">https://percona.community/blog/2026/10/12/docker-dba-lab/</guid>
    <link>https://percona.community/blog/2026/10/12/docker-dba-lab/</link>
    <description>Building a Database DBA Lab with Docker, PostgreSQL, MySQL, and Percona Monitoring and Management
How I built a local database playground, learned a few lessons the hard way, and created a practical command reference along the way.
Introduction
One of the best ways to learn database administration is to get your hands dirty.
Documentation is important, but there’s something different about building your own environment, breaking it, troubleshooting it, and eventually understanding why everything works.
Recently, I decided to create a local database lab using Docker Desktop on my Mac. My goal was straightforward: build an environment where I could experiment with MySQL, PostgreSQL, Percona XtraDB Cluster (PXC), and Percona Monitoring and Management (PMM), without worrying about disrupting a production environment.
What started as a few containers quickly became a much more interesting learning experience.
Along the way, I ran into Docker networking problems, container persistence challenges, cluster bootstrap issues, and PostgreSQL monitoring configuration. This article shows the setup I recommend after working through those problems, so you can start with the right foundation instead of discovering it at the end.
1. Building the Lab
The foundation of my environment is Docker Desktop.
The lab includes:

Percona Server for MySQL 8.4
A three-node Percona XtraDB Cluster
PostgreSQL 17
PMM Server and PMM Client
Persistent Docker volumes
A shared Docker network

The architecture looks like this:

text
Copy
Copied!



Docker Desktop
│
├── pmm-network
│ ├── pmm-server
│ ├── pmm-client
│ ├── percona-server
│ ├── postgres-server
│ ├── pxc1
│ ├── pxc2
│ └── pxc3
│
└── Persistent Docker Volumes
 ├── pmm-data
 ├── pmm-client-data
 ├── mysql-data
 ├── postgres-data
 ├── pxc1_data
 ├── pxc2_data
 └── pxc3_data


There are two ways to build this.
Option A (recommended): Docker Compose
Docker Compose lets you define every container, volume, and network attachment in one file. The whole lab becomes reproducible: one command brings it up, one command shuts it down, and nothing depends on remembering which docker run flags you used weeks ago.
I started out with individual docker run commands and later moved everything to Compose. If I were starting again, I would begin here.
First, keep passwords out of the Compose file by putting them in a .env file next to it (and out of version control):

bash
Copy
Copied!



# .env
MYSQL_ROOT_PASSWORD=change-me
PXC_ROOT_PASSWORD=change-me
POSTGRES_PASSWORD=change-me
PMM_ADMIN_PASSWORD=change-me
PG_MONITOR_PASSWORD=change-me
MYSQL_MONITOR_PASSWORD=change-me


Then define the lab in docker-compose.yml:

yaml
Copy
Copied!



services:
 pmm-server:
 image: percona/pmm-server:3.9.1
 platform: linux/amd64
 container_name: pmm-server
 ports:
 - &quot;443:8443&quot;
 volumes:
 - pmm-data:/srv
 networks: [pmm-network]

 pmm-client:
 image: percona/pmm-client:3.9.1
 container_name: pmm-client
 depends_on: [pmm-server]
 environment:
 PMM_AGENT_SERVER_ADDRESS: pmm-server:8443
 PMM_AGENT_SERVER_USERNAME: admin
 PMM_AGENT_SERVER_PASSWORD: ${PMM_ADMIN_PASSWORD}
 PMM_AGENT_SERVER_INSECURE_TLS: 1
 PMM_AGENT_SETUP: 1
 PMM_AGENT_CONFIG_FILE: config/pmm-agent.yaml
 volumes:
 - pmm-client-data:/usr/local/percona/pmm/config
 networks: [pmm-network]

 percona-server:
 image: percona/percona-server:8.4.11
 container_name: percona-server
 environment:
 MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
 ports:
 - &quot;3306:3306&quot;
 volumes:
 - mysql-data:/var/lib/mysql
 networks: [pmm-network]

 postgres-server:
 image: postgres:17
 container_name: postgres-server
 environment:
 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
 command: &amp;gt;
 postgres
 -c shared_preload_libraries=pg_stat_statements
 -c track_io_timing=on
 ports:
 - &quot;5432:5432&quot;
 volumes:
 - postgres-data:/var/lib/postgresql/data
 networks: [pmm-network]

 pxc1:
 image: percona/percona-xtradb-cluster:8.4.11
 container_name: pxc1
 environment:
 CLUSTER_NAME: pxc-lab
 MYSQL_ROOT_PASSWORD: ${PXC_ROOT_PASSWORD}
 volumes:
 - pxc1_data:/var/lib/mysql
 - ./certs:/etc/mysql/certs:ro
 - ./conf.d:/etc/percona-xtradb-cluster.conf.d:ro
 stop_grace_period: 120s
 healthcheck:
 test: [&quot;CMD&quot;, &quot;mysqladmin&quot;, &quot;ping&quot;, &quot;-uroot&quot;, &quot;-p${PXC_ROOT_PASSWORD}&quot;]
 interval: 10s
 retries: 30
 networks: [pmm-network]
 ports:
 - &quot;3307:3306&quot;

 pxc2:
 image: percona/percona-xtradb-cluster:8.4.11
 container_name: pxc2
 environment:
 CLUSTER_NAME: pxc-lab
 CLUSTER_JOIN: pxc1
 MYSQL_ROOT_PASSWORD: ${PXC_ROOT_PASSWORD}
 volumes:
 - pxc2_data:/var/lib/mysql
 - ./certs:/etc/mysql/certs:ro
 - ./conf.d:/etc/percona-xtradb-cluster.conf.d:ro
 stop_grace_period: 120s
 depends_on:
 pxc1:
 condition: service_healthy
 networks: [pmm-network]
 ports:
 - &quot;3308:3306&quot;

 pxc3:
 image: percona/percona-xtradb-cluster:8.4.11
 container_name: pxc3
 environment:
 CLUSTER_NAME: pxc-lab
 CLUSTER_JOIN: pxc1
 MYSQL_ROOT_PASSWORD: ${PXC_ROOT_PASSWORD}
 volumes:
 - pxc3_data:/var/lib/mysql
 - ./certs:/etc/mysql/certs:ro
 - ./conf.d:/etc/percona-xtradb-cluster.conf.d:ro
 stop_grace_period: 120s
 depends_on:
 - pxc2
 networks: [pmm-network]
 ports:
 - &quot;3309:3306&quot;

networks:
 pmm-network:
 name: pmm-network

volumes:
 pmm-data:
 pmm-client-data:
 mysql-data:
 postgres-data:
 pxc1_data:
 pxc2_data:
 pxc3_data:


A few design choices are worth calling out, because each one comes from a problem I hit:

Volumes and the network are declared up front. Every database has persistent storage and is attached to pmm-network from the moment it is created, so there is nothing to wire up afterward.
All PXC nodes share the same SSL certificates. Mismatched certificates were behind several of my early cluster join failures.
stop_grace_period: 120s gives each PXC node time to shut down cleanly instead of being killed (exit code 137).
depends_on defines the PXC order. Compose starts pxc1 first and waits for it to be healthy before the others join. On shutdown, Compose stops dependents first, so the nodes leave in the order pxc3, pxc2, pxc1.

Bring the lab up and down with:

bash
Copy
Copied!



docker compose up -d
docker compose stop # stops containers, keeps everything
docker compose down # removes containers, keeps named volumes


Option B (alternative): Individual docker run commands
If you prefer to build containers one at a time, or want to understand what Compose is doing for you, you can create the network yourself and attach containers to it:

bash
Copy
Copied!



docker network create pmm-network

docker run -d --name postgres-server \
 --network pmm-network \
 -e POSTGRES_PASSWORD=&quot;$POSTGRES_PASSWORD&quot; \
 -v postgres-data:/var/lib/postgresql/data \
 -p 5432:5432 \
 postgres:17


An existing container can be attached later:

bash
Copy
Copied!



docker network connect pmm-network postgres-server


This works, but you are responsible for recreating each container with exactly the same flags, volumes, and network settings every time. That is the main reason I recommend Option A. Whichever option you choose, avoid mixing the two for the same lab, because it becomes hard to tell which containers are managed by Compose and which are not.
A networking lesson that applies either way
At one point, PMM couldn’t resolve my PostgreSQL container:

text
Copy
Copied!



lookup postgres-server on 127.0.0.11:53: no such host


The problem wasn’t PostgreSQL or PMM. The PostgreSQL container was stopped, so it had no active network endpoint, and Docker’s internal DNS had nothing to return.
That was an important reminder: always verify the container and network state before troubleshooting the application.
2. Making Monitoring Persistent with PMM
Once the databases were running, I wanted visibility into their performance.
PMM provides dashboards and metrics for MySQL, PXC, and PostgreSQL. It consists of a server that stores and visualizes monitoring data and a client agent that collects database metrics.
The key thing to understand is that persistent storage and service registration are related, but they are not the same thing.
PMM Server maintains the monitoring inventory, while PMM Client has its own configuration and agent identity. If the client is recreated with a new identity, the association between the client and its registered services is lost. In my early setup, after recreating containers, the client listing showed only the basic agents:

text
Copy
Copied!



pmm_agent Connected
node_exporter Running
vmagent Running


Two things prevent this, and both should be in place from the start:

A persistent volume for the client configuration. In the Compose file above, pmm-client-data keeps the agent identity across restarts and recreations.
An idempotent registration script. It adds each database only if it isn’t already registered, so it is safe to run after every docker compose up.

Before registering, create a monitoring user in each database. For PostgreSQL, for example:

sql
Copy
Copied!



CREATE USER pmm WITH PASSWORD 'change-me';
GRANT pg_monitor TO pmm;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;


Then the registration script, which reads credentials from the .env file rather than hardcoding them:

bash
Copy
Copied!



#!/usr/bin/env bash
# register-pmm.sh
set -euo pipefail
source .env

registered() {
 docker exec pmm-client pmm-admin list | grep -qw &quot;$1&quot;
}

if ! registered postgres-server; then
 docker exec pmm-client pmm-admin add postgresql \
 --username=pmm \
 --password=&quot;$PG_MONITOR_PASSWORD&quot; \
 --host=postgres-server \
 --port=5432 \
 postgres-server
fi

for node in percona-server pxc1 pxc2 pxc3; do
 if ! registered &quot;$node&quot;; then
 docker exec pmm-client pmm-admin add mysql \
 --username=pmm \
 --password=&quot;$MYSQL_MONITOR_PASSWORD&quot; \
 --host=&quot;$node&quot; \
 --port=3306 \
 --query-source=perfschema \
 &quot;$node&quot;
 fi
done

docker exec pmm-client pmm-admin list


The takeaway: a monitoring system should be designed to survive a restart, not just work immediately after installation.
3. Learning PostgreSQL Through Real Workloads
With PostgreSQL running and connected to PMM, I wanted to do more than look at an idle dashboard.
I used sysbench to generate a workload against PostgreSQL 17.
My test environment included ten tables with approximately 750,000 rows each, giving me about 7.5 million rows to experiment with.
A sample workload command looked like this:

bash
Copy
Copied!



sysbench \
 --db-driver=pgsql \
 --pgsql-host=127.0.0.1 \
 --pgsql-port=5432 \
 --pgsql-user=postgres \
 --pgsql-db=sbtest \
 --tables=10 \
 --table-size=750000 \
 --threads=16 \
 --time=300 \
 --report-interval=5 \
 oltp_read_write run


With the workload running, PMM became much more interesting.
I could observe database activity, disk latency, query execution statistics, and changes in resource utilization.
This also helped me understand an important distinction: PostgreSQL tuple activity is not the same thing as SQL queries per second.
One query can read or return many tuples. Looking only at tuple counts without understanding the underlying queries can lead to incorrect conclusions about workload intensity.
4. Understanding PostgreSQL Query Performance
One of the most useful PostgreSQL features for troubleshooting is EXPLAIN.
For example:

sql
Copy
Copied!



EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM sbtest1
WHERE id BETWEEN 100 AND 200;


This shows the execution plan, actual execution measurements, and buffer activity.
I started paying attention to several key pieces of information:

Whether PostgreSQL used a sequential scan or an index
Estimated versus actual rows
Execution time
Buffer hits and reads
Number of execution loops

Because the Compose file loads pg_stat_statements at startup, query statistics are aggregated from the beginning.
A useful query is:

sql
Copy
Copied!



SELECT
 calls,
 total_exec_time,
 mean_exec_time,
 rows,
 LEFT(query, 100) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;


This provides a different perspective from looking at individual slow queries.
A query that takes only a fraction of a millisecond might still consume substantial cumulative execution time if it runs hundreds of thousands of times.
For a DBA, both individual latency and aggregate workload matter.
5. MVCC, Dead Tuples, and VACUUM
My next learning objective was understanding PostgreSQL’s Multi-Version Concurrency Control (MVCC).
Unlike an in-place overwrite model, PostgreSQL commonly creates a new row version when a row is updated.
Older versions may remain until PostgreSQL determines they are no longer needed.
That leads directly to one of PostgreSQL’s most important maintenance mechanisms: VACUUM.
I experimented with updating large numbers of rows:

sql
Copy
Copied!



UPDATE sbtest1
SET k = k + 1
WHERE id  300000;


Then checked the table statistics:

sql
Copy
Copied!



SELECT
 relname,
 n_live_tup,
 n_dead_tup,
 last_autovacuum,
 autovacuum_count
FROM pg_stat_user_tables
WHERE relname = 'sbtest1';


This gave me a practical way to observe dead tuple estimates and autovacuum activity.
I also explored what happens when a long-running transaction holds an old snapshot.
By starting a REPEATABLE READ transaction and leaving it open while another session updated rows, I could demonstrate how PostgreSQL may need to retain older row versions.
That helped connect several important DBA concepts:
Long-running transactions → old snapshots → delayed tuple cleanup → potential table bloat.
I also learned that normal VACUUM generally makes space reusable within PostgreSQL rather than immediately shrinking the table file.
VACUUM FULL can reclaim disk space by rewriting a table, but it requires an exclusive lock and is not something to run casually in production.
6. Operating the PXC Cluster Safely
My three-node Percona XtraDB Cluster taught me that database containers need database-aware operational procedures. That’s why startup and shutdown are built into the Compose file rather than left to chance.
Early on, I ran into certificate problems, donor timeouts, cluster membership issues, and shutdowns that ended with exit code 137 (the container being killed before MySQL finished). The shared certificate directory, the healthcheck on pxc1, and stop_grace_period address those directly.
The shutdown procedure is simply:

bash
Copy
Copied!



docker compose stop pxc3 pxc2 pxc1


If you use Option B, do the same thing explicitly, one node at a time, with a long timeout:

bash
Copy
Copied!



docker stop -t 120 pxc3
docker stop -t 120 pxc2
docker stop -t 120 pxc1


To start the cluster again, check grastate.dat on each node and understand the meaning of safe_to_bootstrap:

bash
Copy
Copied!



docker run --rm -v pxc1_data:/data alpine cat /data/grastate.dat


After a clean shutdown, the last node to leave the cluster (here, pxc1) will have safe_to_bootstrap: 1 and is the right node to start first.
After an unclean shutdown, selecting the correct bootstrap node requires recovery checks. Compare the recovered sequence numbers across nodes and bootstrap from the most advanced one. Simply changing safe_to_bootstrap to 1 without verifying that can lose data.
7. My Most Useful Commands
I kept a command cheat sheet from the start of the project, and it has become one of the most useful outcomes. It captures the commands I actually needed while learning and troubleshooting. I recommend starting one on day one.
For Docker:

bash
Copy
Copied!



# Running containers
docker ps

# All containers
docker ps -a

# Status of the Compose-managed lab
docker compose ps

# Follow logs
docker logs -f postgres-server

# Inspect network configuration
docker inspect postgres-server \
 --format '{{json .NetworkSettings.Networks}}'

# Resource usage
docker stats

# List Docker volumes
docker volume ls

# Check PMM services
docker exec pmm-client pmm-admin list


These commands are my first line of investigation whenever something isn’t behaving as expected.
For PostgreSQL, my daily reference includes:

sql
Copy
Copied!



-- Current sessions
SELECT pid, usename, state, query
FROM pg_stat_activity;

-- Long-running transactions
SELECT
 pid,
 state,
 now() - xact_start AS transaction_age,
 query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL;

-- Dead tuples
SELECT relname, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;

-- Blocking sessions
SELECT
 pid,
 pg_blocking_pids(pid) AS blocked_by
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) &amp;gt; 0;


These are simple queries, but they form a useful starting point for troubleshooting.
8. Design Principles Behind the Lab
To summarize, these are the principles the setup above is built on:

Define the whole lab in Docker Compose, rather than mixing docker run commands with Compose-managed containers.
Declare persistent volumes and network attachments before creating any databases.
Treat startup and shutdown as part of the design, especially for PXC.
Make PMM registration reproducible with a persistent client volume and an idempotent script, and keep credentials out of scripts and command history.
Keep a command cheat sheet from the beginning.

Final Thoughts
What began as an effort to build a small local database environment turned into a practical DBA learning experience.
I now have a place to explore PostgreSQL performance, MVCC, autovacuum, MySQL replication concepts, PXC clustering, and database monitoring without risking a production system.
Perhaps the biggest lesson is that the most valuable learning moments often happen when something breaks.
A Docker DNS failure teaches networking. A missing PMM registration teaches persistence and identity. A PXC bootstrap problem teaches cluster recovery. A growing dead tuple count teaches PostgreSQL MVCC.
And once you understand why something broke, you’re much better prepared to troubleshoot it the next time.
My next goal is to continue expanding the lab with PostgreSQL replication, backup and recovery, and more deliberate performance troubleshooting exercises.

This article describes a local learning environment. The commands and configurations are intended for experimentation and should be reviewed and adapted before use in production.</description>
    <content:encoded><![CDATA[<h1>Building a Database DBA Lab with Docker, PostgreSQL, MySQL, and Percona Monitoring and Management</h1>
<p><em>How I built a local database playground, learned a few lessons the hard way, and created a practical command reference along the way.</em></p>
<h2>Introduction</h2>
<p>One of the best ways to learn database administration is to get your hands dirty.</p>
<p>Documentation is important, but there’s something different about building your own environment, breaking it, troubleshooting it, and eventually understanding why everything works.</p>
<p>Recently, I decided to create a local database lab using Docker Desktop on my Mac. My goal was straightforward: build an environment where I could experiment with MySQL, PostgreSQL, Percona XtraDB Cluster (PXC), and Percona Monitoring and Management (PMM), without worrying about disrupting a production environment.</p>
<p>What started as a few containers quickly became a much more interesting learning experience.</p>
<p>Along the way, I ran into Docker networking problems, container persistence challenges, cluster bootstrap issues, and PostgreSQL monitoring configuration. This article shows the setup I recommend after working through those problems, so you can start with the right foundation instead of discovering it at the end.</p>
<h2>1. Building the Lab</h2>
<p>The foundation of my environment is Docker Desktop.</p>
<p>The lab includes:</p>
<ul>
<li>Percona Server for MySQL 8.4</li>
<li>A three-node Percona XtraDB Cluster</li>
<li>PostgreSQL 17</li>
<li>PMM Server and PMM Client</li>
<li>Persistent Docker volumes</li>
<li>A shared Docker network</li>
</ul>
<p>The architecture looks like this:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Docker Desktop
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├── pmm-network
</span></span><span class="line"><span class="cl">│ ├── pmm-server
</span></span><span class="line"><span class="cl">│ ├── pmm-client
</span></span><span class="line"><span class="cl">│ ├── percona-server
</span></span><span class="line"><span class="cl">│ ├── postgres-server
</span></span><span class="line"><span class="cl">│ ├── pxc1
</span></span><span class="line"><span class="cl">│ ├── pxc2
</span></span><span class="line"><span class="cl">│ └── pxc3
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">└── Persistent Docker Volumes
</span></span><span class="line"><span class="cl"> ├── pmm-data
</span></span><span class="line"><span class="cl"> ├── pmm-client-data
</span></span><span class="line"><span class="cl"> ├── mysql-data
</span></span><span class="line"><span class="cl"> ├── postgres-data
</span></span><span class="line"><span class="cl"> ├── pxc1_data
</span></span><span class="line"><span class="cl"> ├── pxc2_data
</span></span><span class="line"><span class="cl"> └── pxc3_data</span></span></code></pre></div>
</div>
</div>
<p>There are two ways to build this.</p>
<h3>Option A (recommended): Docker Compose</h3>
<p>Docker Compose lets you define every container, volume, and network attachment in one file. The whole lab becomes reproducible: one command brings it up, one command shuts it down, and nothing depends on remembering which <code>docker run</code> flags you used weeks ago.</p>
<p>I started out with individual <code>docker run</code> commands and later moved everything to Compose. If I were starting again, I would begin here.</p>
<p>First, keep passwords out of the Compose file by putting them in a <code>.env</code> file next to it (and out of version control):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># .env</span>
</span></span><span class="line"><span class="cl"><span class="nv">MYSQL_ROOT_PASSWORD</span><span class="o">=</span>change-me
</span></span><span class="line"><span class="cl"><span class="nv">PXC_ROOT_PASSWORD</span><span class="o">=</span>change-me
</span></span><span class="line"><span class="cl"><span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>change-me
</span></span><span class="line"><span class="cl"><span class="nv">PMM_ADMIN_PASSWORD</span><span class="o">=</span>change-me
</span></span><span class="line"><span class="cl"><span class="nv">PG_MONITOR_PASSWORD</span><span class="o">=</span>change-me
</span></span><span class="line"><span class="cl"><span class="nv">MYSQL_MONITOR_PASSWORD</span><span class="o">=</span>change-me</span></span></code></pre></div>
</div>
</div>
<p>Then define the lab in <code>docker-compose.yml</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">yaml</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pmm-server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/pmm-server:3.9.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">platform</span><span class="p">:</span><span class="w"> </span><span class="l">linux/amd64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pmm-server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"443:8443"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pmm-data:/srv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pmm-client</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/pmm-client:3.9.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pmm-client</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-server]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_SERVER_ADDRESS</span><span class="p">:</span><span class="w"> </span><span class="l">pmm-server:8443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_SERVER_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="l">admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_SERVER_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PMM_ADMIN_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_SERVER_INSECURE_TLS</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_SETUP</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">PMM_AGENT_CONFIG_FILE</span><span class="p">:</span><span class="w"> </span><span class="l">config/pmm-agent.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pmm-client-data:/usr/local/percona/pmm/config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">percona-server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-server:8.4.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">percona-server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${MYSQL_ROOT_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"3306:3306"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">mysql-data:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">postgres-server</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">postgres-server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd"> postgres
</span></span></span><span class="line"><span class="cl"><span class="sd"> -c shared_preload_libraries=pg_stat_statements
</span></span></span><span class="line"><span class="cl"><span class="sd"> -c track_io_timing=on</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"5432:5432"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">postgres-data:/var/lib/postgresql/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-xtradb-cluster:8.4.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pxc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CLUSTER_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">pxc-lab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PXC_ROOT_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pxc1_data:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./certs:/etc/mysql/certs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./conf.d:/etc/percona-xtradb-cluster.conf.d:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">120s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"CMD"</span><span class="p">,</span><span class="w"> </span><span class="s2">"mysqladmin"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ping"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-uroot"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-p${PXC_ROOT_PASSWORD}"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"3307:3306"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc2</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-xtradb-cluster:8.4.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pxc2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CLUSTER_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">pxc-lab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CLUSTER_JOIN</span><span class="p">:</span><span class="w"> </span><span class="l">pxc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PXC_ROOT_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pxc2_data:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./certs:/etc/mysql/certs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./conf.d:/etc/percona-xtradb-cluster.conf.d:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">120s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc1</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"3308:3306"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc3</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-xtradb-cluster:8.4.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">pxc3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CLUSTER_NAME</span><span class="p">:</span><span class="w"> </span><span class="l">pxc-lab</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">CLUSTER_JOIN</span><span class="p">:</span><span class="w"> </span><span class="l">pxc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PXC_ROOT_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pxc3_data:/var/lib/mysql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./certs:/etc/mysql/certs:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./conf.d:/etc/percona-xtradb-cluster.conf.d:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">120s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pxc2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">pmm-network]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"3309:3306"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pmm-network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">pmm-network</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pmm-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pmm-client-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">mysql-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">postgres-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc1_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pxc2_data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="l">pxc3_data:</span></span></span></code></pre></div>
</div>
</div>
<p>A few design choices are worth calling out, because each one comes from a problem I hit:</p>
<ul>
<li><strong>Volumes and the network are declared up front.</strong> Every database has persistent storage and is attached to <code>pmm-network</code> from the moment it is created, so there is nothing to wire up afterward.</li>
<li><strong>All PXC nodes share the same SSL certificates.</strong> Mismatched certificates were behind several of my early cluster join failures.</li>
<li><strong><code>stop_grace_period: 120s</code></strong> gives each PXC node time to shut down cleanly instead of being killed (exit code 137).</li>
<li><strong><code>depends_on</code> defines the PXC order.</strong> Compose starts <code>pxc1</code> first and waits for it to be healthy before the others join. On shutdown, Compose stops dependents first, so the nodes leave in the order <code>pxc3</code>, <code>pxc2</code>, <code>pxc1</code>.</li>
</ul>
<p>Bring the lab up and down with:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose stop <span class="c1"># stops containers, keeps everything</span>
</span></span><span class="line"><span class="cl">docker compose down <span class="c1"># removes containers, keeps named volumes</span></span></span></code></pre></div>
</div>
</div>
<h3>Option B (alternative): Individual <code>docker run</code> commands</h3>
<p>If you prefer to build containers one at a time, or want to understand what Compose is doing for you, you can create the network yourself and attach containers to it:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network create pmm-network
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker run -d --name postgres-server <span class="se">\
</span></span></span><span class="line"><span class="cl"> --network pmm-network <span class="se">\
</span></span></span><span class="line"><span class="cl"> -e <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span><span class="s2">"</span><span class="nv">$POSTGRES_PASSWORD</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> -v postgres-data:/var/lib/postgresql/data <span class="se">\
</span></span></span><span class="line"><span class="cl"> -p 5432:5432 <span class="se">\
</span></span></span><span class="line"><span class="cl"> postgres:17</span></span></code></pre></div>
</div>
</div>
<p>An existing container can be attached later:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker network connect pmm-network postgres-server</span></span></code></pre></div>
</div>
</div>
<p>This works, but you are responsible for recreating each container with exactly the same flags, volumes, and network settings every time. That is the main reason I recommend Option A. Whichever option you choose, avoid mixing the two for the same lab, because it becomes hard to tell which containers are managed by Compose and which are not.</p>
<h3>A networking lesson that applies either way</h3>
<p>At one point, PMM couldn’t resolve my PostgreSQL container:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">lookup postgres-server on 127.0.0.11:53: no such host</span></span></code></pre></div>
</div>
</div>
<p>The problem wasn’t PostgreSQL or PMM. The PostgreSQL container was stopped, so it had no active network endpoint, and Docker’s internal DNS had nothing to return.</p>
<p>That was an important reminder: <strong>always verify the container and network state before troubleshooting the application.</strong></p>
<h2>2. Making Monitoring Persistent with PMM</h2>
<p>Once the databases were running, I wanted visibility into their performance.</p>
<p>PMM provides dashboards and metrics for MySQL, PXC, and PostgreSQL. It consists of a server that stores and visualizes monitoring data and a client agent that collects database metrics.</p>
<p>The key thing to understand is that persistent storage and service registration are related, but they are not the same thing.</p>
<p>PMM Server maintains the monitoring inventory, while PMM Client has its own configuration and agent identity. If the client is recreated with a new identity, the association between the client and its registered services is lost. In my early setup, after recreating containers, the client listing showed only the basic agents:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pmm_agent Connected
</span></span><span class="line"><span class="cl">node_exporter Running
</span></span><span class="line"><span class="cl">vmagent Running</span></span></code></pre></div>
</div>
</div>
<p>Two things prevent this, and both should be in place from the start:</p>
<ol>
<li><strong>A persistent volume for the client configuration.</strong> In the Compose file above, <code>pmm-client-data</code> keeps the agent identity across restarts and recreations.</li>
<li><strong>An idempotent registration script.</strong> It adds each database only if it isn’t already registered, so it is safe to run after every <code>docker compose up</code>.</li>
</ol>
<p>Before registering, create a monitoring user in each database. For PostgreSQL, for example:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="n">pmm</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">PASSWORD</span><span class="w"> </span><span class="s1">'change-me'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="n">pg_monitor</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">pmm</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="p">;</span></span></span></code></pre></div>
</div>
</div>
<p>Then the registration script, which reads credentials from the <code>.env</code> file rather than hardcoding them:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="c1"># register-pmm.sh</span>
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl"><span class="nb">source</span> .env
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">registered<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl"> docker <span class="nb">exec</span> pmm-client pmm-admin list <span class="p">|</span> grep -qw <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! registered postgres-server<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> docker <span class="nb">exec</span> pmm-client pmm-admin add postgresql <span class="se">\
</span></span></span><span class="line"><span class="cl"> --username<span class="o">=</span>pmm <span class="se">\
</span></span></span><span class="line"><span class="cl"> --password<span class="o">=</span><span class="s2">"</span><span class="nv">$PG_MONITOR_PASSWORD</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --host<span class="o">=</span>postgres-server <span class="se">\
</span></span></span><span class="line"><span class="cl"> --port<span class="o">=</span><span class="m">5432</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> postgres-server
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> node in percona-server pxc1 pxc2 pxc3<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> ! registered <span class="s2">"</span><span class="nv">$node</span><span class="s2">"</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> docker <span class="nb">exec</span> pmm-client pmm-admin add mysql <span class="se">\
</span></span></span><span class="line"><span class="cl"> --username<span class="o">=</span>pmm <span class="se">\
</span></span></span><span class="line"><span class="cl"> --password<span class="o">=</span><span class="s2">"</span><span class="nv">$MYSQL_MONITOR_PASSWORD</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --host<span class="o">=</span><span class="s2">"</span><span class="nv">$node</span><span class="s2">"</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --port<span class="o">=</span><span class="m">3306</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --query-source<span class="o">=</span>perfschema <span class="se">\
</span></span></span><span class="line"><span class="cl"> <span class="s2">"</span><span class="nv">$node</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> pmm-client pmm-admin list</span></span></code></pre></div>
</div>
</div>
<p>The takeaway: <strong>a monitoring system should be designed to survive a restart, not just work immediately after installation.</strong></p>
<h2>3. Learning PostgreSQL Through Real Workloads</h2>
<p>With PostgreSQL running and connected to PMM, I wanted to do more than look at an idle dashboard.</p>
<p>I used sysbench to generate a workload against PostgreSQL 17.</p>
<p>My test environment included ten tables with approximately 750,000 rows each, giving me about 7.5 million rows to experiment with.</p>
<p>A sample workload command looked like this:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sysbench <span class="se">\
</span></span></span><span class="line"><span class="cl"> --db-driver<span class="o">=</span>pgsql <span class="se">\
</span></span></span><span class="line"><span class="cl"> --pgsql-host<span class="o">=</span>127.0.0.1 <span class="se">\
</span></span></span><span class="line"><span class="cl"> --pgsql-port<span class="o">=</span><span class="m">5432</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --pgsql-user<span class="o">=</span>postgres <span class="se">\
</span></span></span><span class="line"><span class="cl"> --pgsql-db<span class="o">=</span>sbtest <span class="se">\
</span></span></span><span class="line"><span class="cl"> --tables<span class="o">=</span><span class="m">10</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --table-size<span class="o">=</span><span class="m">750000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --threads<span class="o">=</span><span class="m">16</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --time<span class="o">=</span><span class="m">300</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> --report-interval<span class="o">=</span><span class="m">5</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"> oltp_read_write run</span></span></code></pre></div>
</div>
</div>
<p>With the workload running, PMM became much more interesting.</p>
<p>I could observe database activity, disk latency, query execution statistics, and changes in resource utilization.</p>
<p>This also helped me understand an important distinction: PostgreSQL tuple activity is not the same thing as SQL queries per second.</p>
<p>One query can read or return many tuples. Looking only at tuple counts without understanding the underlying queries can lead to incorrect conclusions about workload intensity.</p>
<h2>4. Understanding PostgreSQL Query Performance</h2>
<p>One of the most useful PostgreSQL features for troubleshooting is <code>EXPLAIN</code>.</p>
<p>For example:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="p">(</span><span class="k">ANALYZE</span><span class="p">,</span><span class="w"> </span><span class="n">BUFFERS</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">sbtest1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">BETWEEN</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="mi">200</span><span class="p">;</span></span></span></code></pre></div>
</div>
</div>
<p>This shows the execution plan, actual execution measurements, and buffer activity.</p>
<p>I started paying attention to several key pieces of information:</p>
<ul>
<li>Whether PostgreSQL used a sequential scan or an index</li>
<li>Estimated versus actual rows</li>
<li>Execution time</li>
<li>Buffer hits and reads</li>
<li>Number of execution loops</li>
</ul>
<p>Because the Compose file loads <code>pg_stat_statements</code> at startup, query statistics are aggregated from the beginning.</p>
<p>A useful query is:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">calls</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">total_exec_time</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">mean_exec_time</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">rows</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">LEFT</span><span class="p">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_statements</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">total_exec_time</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div>
</div>
</div>
<p>This provides a different perspective from looking at individual slow queries.</p>
<p>A query that takes only a fraction of a millisecond might still consume substantial cumulative execution time if it runs hundreds of thousands of times.</p>
<p>For a DBA, both individual latency and aggregate workload matter.</p>
<h2>5. MVCC, Dead Tuples, and VACUUM</h2>
<p>My next learning objective was understanding PostgreSQL’s Multi-Version Concurrency Control (MVCC).</p>
<p>Unlike an in-place overwrite model, PostgreSQL commonly creates a new row version when a row is updated.</p>
<p>Older versions may remain until PostgreSQL determines they are no longer needed.</p>
<p>That leads directly to one of PostgreSQL’s most important maintenance mechanisms: VACUUM.</p>
<p>I experimented with updating large numbers of rows:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">UPDATE</span><span class="w"> </span><span class="n">sbtest1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">k</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o"><span class="w"> </span><span class="mi">300000</span><span class="p">;</span></span></span></span></code></pre></div>
</div>
</div>
<p>Then checked the table statistics:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">relname</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">n_dead_tup</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">last_autovacuum</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">autovacuum_count</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">relname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'sbtest1'</span><span class="p">;</span></span></span></code></pre></div>
</div>
</div>
<p>This gave me a practical way to observe dead tuple estimates and autovacuum activity.</p>
<p>I also explored what happens when a long-running transaction holds an old snapshot.</p>
<p>By starting a <code>REPEATABLE READ</code> transaction and leaving it open while another session updated rows, I could demonstrate how PostgreSQL may need to retain older row versions.</p>
<p>That helped connect several important DBA concepts:</p>
<p><strong>Long-running transactions → old snapshots → delayed tuple cleanup → potential table bloat.</strong></p>
<p>I also learned that normal VACUUM generally makes space reusable within PostgreSQL rather than immediately shrinking the table file.</p>
<p><code>VACUUM FULL</code> can reclaim disk space by rewriting a table, but it requires an exclusive lock and is not something to run casually in production.</p>
<h2>6. Operating the PXC Cluster Safely</h2>
<p>My three-node Percona XtraDB Cluster taught me that <strong>database containers need database-aware operational procedures.</strong> That’s why startup and shutdown are built into the Compose file rather than left to chance.</p>
<p>Early on, I ran into certificate problems, donor timeouts, cluster membership issues, and shutdowns that ended with exit code 137 (the container being killed before MySQL finished). The shared certificate directory, the healthcheck on <code>pxc1</code>, and <code>stop_grace_period</code> address those directly.</p>
<p>The shutdown procedure is simply:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose stop pxc3 pxc2 pxc1</span></span></code></pre></div>
</div>
</div>
<p>If you use Option B, do the same thing explicitly, one node at a time, with a long timeout:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker stop -t <span class="m">120</span> pxc3
</span></span><span class="line"><span class="cl">docker stop -t <span class="m">120</span> pxc2
</span></span><span class="line"><span class="cl">docker stop -t <span class="m">120</span> pxc1</span></span></code></pre></div>
</div>
</div>
<p>To start the cluster again, check <code>grastate.dat</code> on each node and understand the meaning of <code>safe_to_bootstrap</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v pxc1_data:/data alpine cat /data/grastate.dat</span></span></code></pre></div>
</div>
</div>
<p>After a clean shutdown, the last node to leave the cluster (here, <code>pxc1</code>) will have <code>safe_to_bootstrap: 1</code> and is the right node to start first.</p>
<p>After an unclean shutdown, selecting the correct bootstrap node requires recovery checks. Compare the recovered sequence numbers across nodes and bootstrap from the most advanced one. Simply changing <code>safe_to_bootstrap</code> to <code>1</code> without verifying that can lose data.</p>
<h2>7. My Most Useful Commands</h2>
<p>I kept a command cheat sheet from the start of the project, and it has become one of the most useful outcomes. It captures the commands I actually needed while learning and troubleshooting. I recommend starting one on day one.</p>
<p>For Docker:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-18" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Running containers</span>
</span></span><span class="line"><span class="cl">docker ps
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># All containers</span>
</span></span><span class="line"><span class="cl">docker ps -a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Status of the Compose-managed lab</span>
</span></span><span class="line"><span class="cl">docker compose ps
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Follow logs</span>
</span></span><span class="line"><span class="cl">docker logs -f postgres-server
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Inspect network configuration</span>
</span></span><span class="line"><span class="cl">docker inspect postgres-server <span class="se">\
</span></span></span><span class="line"><span class="cl"> --format <span class="s1">'{{json .NetworkSettings.Networks}}'</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Resource usage</span>
</span></span><span class="line"><span class="cl">docker stats
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># List Docker volumes</span>
</span></span><span class="line"><span class="cl">docker volume ls
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check PMM services</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> pmm-client pmm-admin list</span></span></code></pre></div>
</div>
</div>
<p>These commands are my first line of investigation whenever something isn’t behaving as expected.</p>
<p>For PostgreSQL, my daily reference includes:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-19" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Current sessions
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">usename</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Long-running transactions
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">now</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">xact_start</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">transaction_age</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">xact_start</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Dead tuples
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">relname</span><span class="p">,</span><span class="w"> </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- Blocking sessions
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">pg_blocking_pids</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">blocked_by</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="k">cardinality</span><span class="p">(</span><span class="n">pg_blocking_pids</span><span class="p">(</span><span class="n">pid</span><span class="p">))</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span></span></span></code></pre></div>
</div>
</div>
<p>These are simple queries, but they form a useful starting point for troubleshooting.</p>
<h2>8. Design Principles Behind the Lab</h2>
<p>To summarize, these are the principles the setup above is built on:</p>
<ol>
<li><strong>Define the whole lab in Docker Compose</strong>, rather than mixing <code>docker run</code> commands with Compose-managed containers.</li>
<li><strong>Declare persistent volumes and network attachments before creating any databases.</strong></li>
<li><strong>Treat startup and shutdown as part of the design</strong>, especially for PXC.</li>
<li><strong>Make PMM registration reproducible</strong> with a persistent client volume and an idempotent script, and keep credentials out of scripts and command history.</li>
<li><strong>Keep a command cheat sheet from the beginning.</strong></li>
</ol>
<h2>Final Thoughts</h2>
<p>What began as an effort to build a small local database environment turned into a practical DBA learning experience.</p>
<p>I now have a place to explore PostgreSQL performance, MVCC, autovacuum, MySQL replication concepts, PXC clustering, and database monitoring without risking a production system.</p>
<p>Perhaps the biggest lesson is that the most valuable learning moments often happen when something breaks.</p>
<p>A Docker DNS failure teaches networking. A missing PMM registration teaches persistence and identity. A PXC bootstrap problem teaches cluster recovery. A growing dead tuple count teaches PostgreSQL MVCC.</p>
<p>And once you understand why something broke, you’re much better prepared to troubleshoot it the next time.</p>
<p><strong>My next goal is to continue expanding the lab with PostgreSQL replication, backup and recovery, and more deliberate performance troubleshooting exercises.</strong></p>
<hr>
<p><em>This article describes a local learning environment. The commands and configurations are intended for experimentation and should be reviewed and adapted before use in production.</em></p>]]></content:encoded>
    <pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate>
    <dc:creator>Percona Community</dc:creator>
    <category>OpenSource</category>
    <category>Percona</category>
    <category>MySQL</category>
    <category>Docker</category>
    <category>PostgreSQL</category>
    <category>PMM</category>
    <category>PXC</category>
  </item>

  <item>
    <title>SecureBridge 12.1, EntityDAC 4.1, and dbExpress drivers: Support for RAD Studio 13 Florence Release 2, SQL Server 2025, and MySQL 9.7</title>
    <guid isPermaLink="false">https://www.devart.com/blog/?p=184288</guid>
    <link>https://www.devart.com/blog/securebridge-12-1-entitydac-4-1-and-dbexpress-drivers-support-for-rad-studio-13-florence-release-2-sql-server-2025-and-mysql-9-7.html</link>
    <description>New versions of SecureBridge, EntityDAC, and dbExpress drivers are out. All three now work with RAD Studio 13 Florence Release 2 and offer improved support for the Arm64EC target platform. The dbExpress drivers also add support for Microsoft SQL Server 2025 and MySQL 9.7, while SecureBridge gains SFTP upload resume, multipart/form-data support, and a new SSL/TLS demo.
The post SecureBridge 12.1, EntityDAC 4.1, and dbExpress drivers: Support for RAD Studio 13 Florence Release 2, SQL Server 2025, and MySQL 9.7 appeared first on Devart Blog.</description>
    <content:encoded><![CDATA[<p>New versions of SecureBridge, EntityDAC, and dbExpress drivers are out. All three now work with RAD Studio 13 Florence Release 2 and offer improved support for the Arm64EC target platform. The dbExpress drivers also add support for Microsoft SQL Server 2025 and MySQL 9.7, while SecureBridge gains SFTP upload resume, multipart/form-data support, and a new SSL/TLS demo.</p>
<p>The post <a href="https://www.devart.com/blog/securebridge-12-1-entitydac-4-1-and-dbexpress-drivers-support-for-rad-studio-13-florence-release-2-sql-server-2025-and-mysql-9-7.html">SecureBridge 12.1, EntityDAC 4.1, and dbExpress drivers: Support for RAD Studio 13 Florence Release 2, SQL Server 2025, and MySQL 9.7</a> appeared first on <a href="https://www.devart.com/blog">Devart Blog</a>.</p>]]></content:encoded>
    <pubDate>Fri, 09 Oct 2026 17:00:52 +0000</pubDate>
    <dc:creator>Alena Subotina</dc:creator>
    <category>ORM Solutions</category>
    <category>Productivity Tools</category>
    <category>What’s New</category>
    <category>dbexpress</category>
    <category>dbExpress driver</category>
    <category>entitydac</category>
    <category>RAD Studio 13 Florence</category>
    <category>securebridge</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 3</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54438</guid>
    <link>https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-3/</link>
    <description>Step 3: Why my sysbench-trained build loses and how to do it right.

Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

Three compounding reasons:

Uncovered code gets pessimized. 
Sysbench-tpcc touches a narrow slice of mysqld. 
Every function with zero counts is treated as cold: GCC optimizes it for size, skips inlining, and shoves it into cold sections. 
But at runtime I still execute plenty of code my training never touched, such as purge, flushing, stats recalculation, error paths, different optimizer plans, connection churn. 
All of that is now running de-optimized code, and I pay icache penalties every time hot code calls into “cold” regions. 
This is why GCC added -fprofile-partial-training, without it, a narrow profile actively hurts everything outside it.
High-concurrency instrumented runs produce corrupted or skewed profiles. 
GCC’s profile counters are non-atomic by default. 
With 128–1024 threads hammering the same counters I get lost updates and internally inconsistent counts (I actually had “profile count data file corrupted/inconsistent” warnings at the -fprofile-use compile). 
I need to use -fprofile-update=atomic, which almost nobody sets and was at the beginning not aware of. 
In short my profile was garbage and a garbage profile is worse than no profile, consistent with my PGO build being slower than plain -O3 at 128 threads.
Instrumentation distorts what “hot” means under contention. 
The instrumented binary is 2–10x slower, which shifts where threads pile up. Spin loops in mutexes and rw-locks record enormous counts, so the compiler lavishes optimization on waiting code instead of useful work. While at high thread counts my real bottleneck is lock contention and memory latency things branch layout can’t fix. 

Do I have a way to merge the different profiles like MTR + sysbench?
The answer is yes. For GCC it’s simple to do so, because the runtime automatically merges profile data across multiple training runs against the same instrumented binary. I don’t need a separate merge step like Clang does.
What it does is that each time an instrumented binary exits, it writes its counters into files. If those files already exist (from a previous run), GCC’s runtime adds the new counts to the existing ones rather than overwriting them. 
So if I run MTR first, then run sysbench against the same instrumented build with the same FPROFILE_DIR, the second run’s counts accumulate on top of the first. The final profile reflects both workloads combined. 
Combining MTR with a moderate-thread sysbench run for PGO training is helpful because the two workloads cover different, complementary dimensions of mysqld’s behavior: 

MTR sweeps broad functional breadth parser, optimizer, DDL, replication, error paths. However it runs almost entirely single-connection, so it never exercises the branches that only exist under real concurrency, like the contended slow-path of a latch, MVCC visibility checks against in-flight writers, lock-wait queuing, or redo-log group-commit batching;
A moderate-concurrency sysbench run (something like 4–64 threads, enough to create actual simultaneous access without descending into the timing-distortion and counter-corruption problems), fills exactly that gap by giving those concurrency-only branches nonzero execution counts, which keeps the compiler from treating them as cold, since cold code paths get optimized for size instead of speed.

So the merged profile ends up with both the wide code coverage MTR provides and the concurrent-path coverage MTR structurally can’t, at the cost of only a modest, second-order improvement over MTR alone since PGO’s overall gains are already small and this specific slice of the binary is a narrow fraction of total execution.
 
However even where it does help, I am stacking a small effect on top of a small effect. I have already found PGO vs non-PGO gives me under 5%. 
The incremental gain from better-covering a narrow slice of concurrency-only code within that is a second-order refinement. 
It is plausibly in the sub-1% range, quite  smaller than the run-to-run noise I had seen just from benchmark variance.
Or at least that is what I now think, let me validate it.

Commands to build the code:cmake ../mysql-9.7.2 \
-DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-instrument \
-DCMAKE_BUILD_TYPE=Release \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_PACKAGE_FLAGS=OFF \
-DCOMPILATION_COMMENT_SERVER=&quot;Marco compile 9.7.2-PGO instrument&quot; \
-DCOMPILATION_COMMENT=&quot;Marco compile 9.7.2 PGO instrument&quot; \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_FLAGS=&quot;-fuse-ld=lld&quot; \
-DCMAKE_CXX_FLAGS=&quot;-fuse-ld=lld&quot; \
-DFPROFILE_GENERATE=ON \
-DWITH_LTO=OFF \
-DFPROFILE_DIR=/opt/mysql_source/profileRun the mtr:
perl mysql-test-run.pl --force --max-test-fail=0 --parallel=8 --suite=main,innodb,innodb_undo,binlog,rpl,perfschema,sys_vars 
Then run the sysbench-tpcc test with 64 threadssysbench /opt/tools/sysbench-tpcc/tpcc.lua --mysql-host=127.0.0.1 --mysql-port=3307  --mysql-user=app_test --mysql-password=test --mysql-db=tpcc --db-driver=mysql --tables=10 --scale=100 --rand-type=uniform --report-interval=1  --histogram --report_csv=yes  --stats_format=csv --db-ps-mode=disable --trx_level=RR --enable_purge=yes --time=600 --threads=64 --mysql-ssl=PREFERRED --mysql-ignore-errors=none  --reconnect=0  runAnd got the profile as[root@sm-blade03 profile]# llvm-profdata-20 show -detailed-summary /opt/mysql_source/profile/mysql.profdata
/opt/mysql_source/profile/mysql.profdata
Instrumentation level: IR entry_first = 0 instrument_loop_entries = 0
Total functions: 52403
Maximum function count: 289910820864
Maximum internal block count: 16881288068
Total number of blocks: 619050
Total count: 2473216025729
Detailed summary:
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 1% of the total counts.
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 10% of the total counts.
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 20% of the total counts.
11 blocks (0.00%) with count &amp;gt;= 16777125216 account for 30% of the total counts.
32 blocks (0.01%) with count &amp;gt;= 7806039615 account for 40% of the total counts.
77 blocks (0.01%) with count &amp;gt;= 3839409465 account for 50% of the total counts.
160 blocks (0.03%) with count &amp;gt;= 2273821592 account for 60% of the total counts.
312 blocks (0.05%) with count &amp;gt;= 1111890303 account for 70% of the total counts.
719 blocks (0.12%) with count &amp;gt;= 365989756 account for 80% of the total counts.
2165 blocks (0.35%) with count &amp;gt;= 85858392 account for 90% of the total counts.
4893 blocks (0.79%) with count &amp;gt;= 24359463 account for 95% of the total counts.
16760 blocks (2.71%) with count &amp;gt;= 2841200 account for 99% of the total counts.
45006 blocks (7.27%) with count &amp;gt;= 148881 account for 99.9% of the total counts.
96619 blocks (15.61%) with count &amp;gt;= 8640 account for 99.99% of the total counts.
157023 blocks (25.37%) with count &amp;gt;= 960 account for 99.999% of the total counts.
220001 blocks (35.54%) with count &amp;gt;= 91 account for 99.9999% of the total countsChecking on how counts concentrate: just 2 blocks account for 20% of all executed instructions across the entire training run (almost certainly a tight InnoDB buffer-pool/redo-log/lock_manager loop  counts in the hundreds of billions), while 90% of total execution volume is concentrated in only 2,165 blocks (0.35% of all covered blocks). 
That’s the “hot core” PGO is designed to find and optimize aggressively. Meanwhile the long tail,  the other 96%+ of blocks, still has nonzero counts (this is a sparse profile, so anything appearing here was actually executed at least once).
Meaning a huge amount of MTR’s functional-path breadth got captured even though it’s numerically dwarfed by sysbench’s tight hot loops.
This is the ideal shape for a merged profile: a small, extremely hot core (from sustained sysbench load) sitting on top of broad, low-frequency-but-nonzero coverage (from MTR’s functional sweep). 
If MTR had contributed nothing, I would have a much flatter, narrower distribution with far fewer total functions covered.
 If sysbench had swamped everything with no MTR contribution, I would have a similar shape but with a much smaller “Total functions” number, since sysbench-tpcc only touches a fraction of mysqld’s total surface.
Command to build the final optimized binaries:llvm-profdata-20 merge -sparse /opt/mysql_source/profile/*.profraw -o /opt/mysql_source/profile/mysql.profdata

cmake ../mysql-9.7.2 \
 -DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-optimized-MTR-sysbench \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLED_LOCAL_INFILE=1 \
  -DWITH_FEDERATED_STORAGE_ENGINE=1 \
  -DWITH_ARCHIVE_STORAGE_ENGINE=1 \
  -DWITH_PACKAGE_FLAGS=OFF \
  -DCOMPILATION_COMMENT_SERVER=&quot;Marco compile 9.7.2-PGO optimized MTR+sysbench&quot; \
  -DCOMPILATION_COMMENT=&quot;Marco compile 9.7.2 PGO optimized MTR+sysbench&quot; \
  -DCMAKE_C_COMPILER=clang-20 \
  -DCMAKE_CXX_COMPILER=clang++-20 \
  -DFPROFILE_USE=ON \
  -DFPROFILE_DIR=/opt/mysql_source/profile/mysql.profdata \
  -DWITH_SSL=system -DWITH_ZLIB=system -DWITH_LZ4=system -DWITH_ICU=system \
  -DWITH_NUMA=ON -DWITH_LTO=ON -DWITH_LD=lld -DWITH_SYSTEMD=1 \
  -DWITH_UNIT_TESTS=OFF -DWITH_ROUTER=OFF -DMYSQL_MAINTAINER_MODE=OFFRe-running the test I got this:

 
 
 
 
 
 
 
 
As expected the benefit I got was minimal, something was there but that disappeared while the concurrency increased.
 
Conclusions

Non-PGO vs PGO comparison: we had a 12% increase with low concurrency. But when simulating a more realistic load with higher contention the win becomes smaller and smaller, under 2%.
Training-workload choice mattered a lot: a sysbench-tpcc-trained PGO build ended up slower than a non-PGO.
After switching to a merged MTR + moderate-concurrency-sysbench training profile and re-running, the gain was still minimal, and it shrank further as concurrency increased.

 
Why the sysbench-only build lost
Sysbench-tpcc only exercises a narrow slice of mysqld, so everything outside that slice (purge, flushing, stats, error paths, alternate optimizer plans) gets pessimized as “cold” code.
At 128–1024 threads, GCC’s non-atomic profile counters get corrupted under contention unless I explicitly set -fprofile-update=atomic,a garbage profile is worse than no profile at all.
Instrumentation overhead (2–10x slowdown) distorts what looks “hot” under load mutex/rwlock spin loops dominate the counts, so the compiler optimizes waiting code instead of real work, while the actual bottleneck (lock contention, memory latency) is something branch layout can’t fix anyway.
Bottom line: PGO for MySQL/Percona Server binaries does work in the sense that the mechanism is sound whole-binary function reordering for something the size of mysqld is a legitimate, often the single biggest, win in PGO generally. 
But empirically here the payoff was consistently small (sub-5%, trending toward sub-1% for the concurrency-specific refinement), fragile to training-workload choice, fragile to build-flag correctness (atomic counters, -fprofile-partial-training), and it erodes further as thread count rises,  which is exactly what happens in production and what we care about most.
So my practical answer: it’s not a clear “yes, always compile with PGO.” It’s more a “maybe, and only if you get every detail right“. Correct broad-coverage training data (MTR, ideally merged with moderate-concurrency sysbench), atomic profile counters, and realistic expectations that the win is marginal and shrinks under heavy concurrency. 
Get any of those wrong and you can end up worse than a plain build. 
Given the size of the benefit versus the number of ways to mess up the training methodology, PGO reads more like a niche optimization for a well-controlled build pipeline than a default you’d flip on broadly.
I would be more than happy to prove wrong and I am eager to get other people’s feedback, so please test, test, test and let me know. 
 
Happy MySQL to everyone
Go to:
What is PGO
How PGO it works
The post PGO or not PGO this is the dilemma. Step 3 appeared first on Percona.</description>
    <content:encoded><![CDATA[<h2><span>Step 3: Why my sysbench-trained build loses and how to do it right.</span><span><br>
</span></h2>
<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></li>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></li>
<li aria-level="1"><span>Why my sysbench-trained build loses and how to do it right.</span></li>
</ol>
<h3><span>Three compounding reasons:</span></h3>
<ol>
<li aria-level="1"><i><span>Uncovered code gets pessimized.</span></i> <span><br>
</span><span>Sysbench-tpcc touches a narrow slice of mysqld. </span><span><br>
</span><span>Every function with zero counts is treated as cold: GCC optimizes it for size, skips inlining, and shoves it into cold sections. </span><span><br>
</span><span>But at runtime I still execute plenty of code my training never touched, such as purge, flushing, stats recalculation, error paths, different optimizer plans, connection churn. </span><span><br>
</span><span>All of that is now running de-optimized code, and I pay icache penalties every time hot code calls into “cold” regions. </span><span><br>
</span><span>This is why GCC added </span><span>-fprofile-partial-training</span><span>, without it, a narrow profile actively hurts everything outside it.</span></li>
<li aria-level="1"><i><span>High-concurrency instrumented runs produce corrupted or skewed profiles.</span></i> <span><br>
</span><span>GCC’s profile counters are non-atomic by default. </span><span><br>
</span><span>With 128–1024 threads hammering the same counters I get lost updates and internally inconsistent counts (I actually had “profile count data file corrupted/inconsistent” warnings at the </span><span>-fprofile-use</span><span> compile). </span><span><br>
</span><span>I need to use </span><span>-fprofile-update=atomic</span><span>, which almost nobody sets and was at the beginning not aware of. </span><span><br>
</span><span>In short my profile was garbage and a garbage profile is worse than no profile, consistent with my PGO build being </span><i><span>slower</span></i><span> than plain </span><span>-O3</span><span> at 128 threads.</span></li>
<li aria-level="1"><i><span>Instrumentation distorts what “hot” means under contention.</span></i> <span><br>
</span><span>The instrumented binary is 2–10x slower, which shifts where threads pile up. Spin loops in mutexes and rw-locks record enormous counts, so the compiler lavishes optimization on </span><i><span>waiting</span></i><span> code instead of useful work. While at high thread counts my real bottleneck is lock contention and memory latency things branch layout can’t fix. </span></li>
</ol>
<p><i><span>Do I have a way to merge the different profiles like MTR + sysbench?</span></i><span><br>
</span><span>The answer is yes. For GCC it’s simple to do so, because the runtime automatically merges profile data across multiple training runs against the same instrumented binary. I don’t need a separate merge step like Clang does.</span></p>
<p><span>What it does is that each time an instrumented binary exits, it writes its counters into files. If those files already exist (from a previous run), GCC’s runtime </span><i><span>adds</span></i><span> the new counts to the existing ones rather than overwriting them. </span><span><br>
</span><span>So if I run MTR first, then run sysbench against the same instrumented build with the same </span><span>FPROFILE_DIR</span><span>, the second run’s counts accumulate on top of the first. The final profile reflects both workloads combined. </span></p>
<p><span>Combining MTR with a moderate-thread sysbench run for PGO training is helpful because the two workloads cover different, complementary dimensions of mysqld’s behavior: </span></p>
<ul>
<li aria-level="1"><span>MTR sweeps broad functional breadth parser, optimizer, DDL, replication, error paths. However it runs almost entirely single-connection, so it never exercises the branches that only exist under real concurrency, like the contended slow-path of a latch, MVCC visibility checks against in-flight writers, lock-wait queuing, or redo-log group-commit batching;</span></li>
<li aria-level="1"><span>A moderate-concurrency sysbench run (something like 4–64 threads, enough to create actual simultaneous access without descending into the timing-distortion and counter-corruption problems), fills exactly that gap by giving those concurrency-only branches nonzero execution counts, which keeps the compiler from treating them as cold, since cold code paths get optimized for size instead of speed.</span></li>
</ul>
<p><span>So the merged profile ends up with both the wide code coverage MTR provides and the concurrent-path coverage MTR structurally can’t, at the cost of only a modest, second-order improvement over MTR alone since PGO’s overall gains are already small and this specific slice of the binary is a narrow fraction of total execution.</span></p>
<p> </p>
<p><span>However even where it does help, I am stacking a small effect on top of a small effect. I have already found PGO vs non-PGO gives me under 5%. </span><span><br>
</span><span>The incremental gain from better-covering a narrow slice of concurrency-only code within that is a second-order refinement. </span><span><br>
</span><span>It is plausibly in the sub-1% range, quite  smaller than the run-to-run noise I had seen just from benchmark variance.</span></p>
<p><span>Or at least that is what I now think, let me validate it.</span><span><br>
</span><span><br>
</span><span>Commands to build the code:</span></p><pre class="urvanov-syntax-highlighter-plain-tag">cmake ../mysql-9.7.2 \
-DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-instrument \
-DCMAKE_BUILD_TYPE=Release \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_PACKAGE_FLAGS=OFF \
-DCOMPILATION_COMMENT_SERVER="Marco compile 9.7.2-PGO instrument" \
-DCOMPILATION_COMMENT="Marco compile 9.7.2 PGO instrument" \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_FLAGS="-fuse-ld=lld" \
-DCMAKE_CXX_FLAGS="-fuse-ld=lld" \
-DFPROFILE_GENERATE=ON \
-DWITH_LTO=OFF \
-DFPROFILE_DIR=/opt/mysql_source/profile</pre><p><span>Run the mtr:</span><span><br>
</span></p><pre class="urvanov-syntax-highlighter-plain-tag">perl mysql-test-run.pl --force --max-test-fail=0 --parallel=8 --suite=main,innodb,innodb_undo,binlog,rpl,perfschema,sys_vars</pre><p> </p>
<p><span>Then run the sysbench-tpcc test with 64 threads</span></p><pre class="urvanov-syntax-highlighter-plain-tag">sysbench /opt/tools/sysbench-tpcc/tpcc.lua --mysql-host=127.0.0.1 --mysql-port=3307  --mysql-user=app_test --mysql-password=test --mysql-db=tpcc --db-driver=mysql --tables=10 --scale=100 --rand-type=uniform --report-interval=1  --histogram --report_csv=yes  --stats_format=csv --db-ps-mode=disable --trx_level=RR --enable_purge=yes --time=600 --threads=64 --mysql-ssl=PREFERRED --mysql-ignore-errors=none  --reconnect=0  run</pre><p><span>And got the profile as</span></p><pre class="urvanov-syntax-highlighter-plain-tag">[root@sm-blade03 profile]# llvm-profdata-20 show -detailed-summary /opt/mysql_source/profile/mysql.profdata
/opt/mysql_source/profile/mysql.profdata
Instrumentation level: IR entry_first = 0 instrument_loop_entries = 0
Total functions: 52403
Maximum function count: 289910820864
Maximum internal block count: 16881288068
Total number of blocks: 619050
Total count: 2473216025729
Detailed summary:
2 blocks (0.00%) with count &gt;= 289910820864 account for 1% of the total counts.
2 blocks (0.00%) with count &gt;= 289910820864 account for 10% of the total counts.
2 blocks (0.00%) with count &gt;= 289910820864 account for 20% of the total counts.
11 blocks (0.00%) with count &gt;= 16777125216 account for 30% of the total counts.
32 blocks (0.01%) with count &gt;= 7806039615 account for 40% of the total counts.
77 blocks (0.01%) with count &gt;= 3839409465 account for 50% of the total counts.
160 blocks (0.03%) with count &gt;= 2273821592 account for 60% of the total counts.
312 blocks (0.05%) with count &gt;= 1111890303 account for 70% of the total counts.
719 blocks (0.12%) with count &gt;= 365989756 account for 80% of the total counts.
2165 blocks (0.35%) with count &gt;= 85858392 account for 90% of the total counts.
4893 blocks (0.79%) with count &gt;= 24359463 account for 95% of the total counts.
16760 blocks (2.71%) with count &gt;= 2841200 account for 99% of the total counts.
45006 blocks (7.27%) with count &gt;= 148881 account for 99.9% of the total counts.
96619 blocks (15.61%) with count &gt;= 8640 account for 99.99% of the total counts.
157023 blocks (25.37%) with count &gt;= 960 account for 99.999% of the total counts.
220001 blocks (35.54%) with count &gt;= 91 account for 99.9999% of the total counts</pre><p><span>Checking on how counts concentrate: just 2 blocks account for 20% of </span><i><span>all</span></i><span> executed instructions across the entire training run (almost certainly a tight InnoDB buffer-pool/redo-log/lock_manager loop  counts in the hundreds of billions), while 90% of total execution volume is concentrated in only 2,165 blocks (0.35% of all covered blocks). </span></p>
<p><span>That’s the “hot core” PGO is designed to find and optimize aggressively. Meanwhile the long tail,  the other 96%+ of blocks, still has nonzero counts (this is a sparse profile, so anything appearing here was actually executed at least once).</span></p>
<p><span>Meaning a huge amount of MTR’s functional-path breadth got captured even though it’s numerically dwarfed by sysbench’s tight hot loops.</span></p>
<p><span>This is the ideal shape for a merged profile: a small, extremely hot core (from sustained sysbench load) sitting on top of broad, low-frequency-but-nonzero coverage (from MTR’s functional sweep). </span></p>
<p><span>If MTR had contributed nothing, I would have a much flatter, narrower distribution with far fewer total functions covered.</span><span><br>
</span><span> If sysbench had swamped everything with no MTR contribution, I would have a similar shape but with a much smaller “Total functions” number, since sysbench-tpcc only touches a fraction of mysqld’s total surface.</span></p>
<p><span>Command to build the final optimized binaries:</span></p><pre class="urvanov-syntax-highlighter-plain-tag">llvm-profdata-20 merge -sparse /opt/mysql_source/profile/*.profraw -o /opt/mysql_source/profile/mysql.profdata

cmake ../mysql-9.7.2 \
 -DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-optimized-MTR-sysbench \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLED_LOCAL_INFILE=1 \
  -DWITH_FEDERATED_STORAGE_ENGINE=1 \
  -DWITH_ARCHIVE_STORAGE_ENGINE=1 \
  -DWITH_PACKAGE_FLAGS=OFF \
  -DCOMPILATION_COMMENT_SERVER="Marco compile 9.7.2-PGO optimized MTR+sysbench" \
  -DCOMPILATION_COMMENT="Marco compile 9.7.2 PGO optimized MTR+sysbench" \
  -DCMAKE_C_COMPILER=clang-20 \
  -DCMAKE_CXX_COMPILER=clang++-20 \
  -DFPROFILE_USE=ON \
  -DFPROFILE_DIR=/opt/mysql_source/profile/mysql.profdata \
  -DWITH_SSL=system -DWITH_ZLIB=system -DWITH_LZ4=system -DWITH_ICU=system \
  -DWITH_NUMA=ON -DWITH_LTO=ON -DWITH_LD=lld -DWITH_SYSTEMD=1 \
  -DWITH_UNIT_TESTS=OFF -DWITH_ROUTER=OFF -DMYSQL_MAINTAINER_MODE=OFF</pre><p><span>Re-running the test I got this:</span></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-scaled.png"><img loading="lazy" decoding="async" class="alignleft size-medium_large wp-image-54432" src="https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-768x337.png" alt="" width="768" height="337" srcset="https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-768x337.png 768w, https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-300x132.png 300w, https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-1024x449.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-1536x674.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/pgo_not_pgo_mtr_vs_mtr_sysbench-2048x899.png 2048w" sizes="auto, (max-width: 768px) 100vw, 768px"></a></p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p><span>As expected the benefit I got was minimal, something was there but that disappeared while the concurrency increased.</span></p>
<p> </p>
<h2><span>Conclusions</span></h2>
<ul>
<li aria-level="1"><span>Non-PGO vs PGO comparison: we had a 12% increase with low concurrency. But when simulating a more realistic load with higher contention the win becomes smaller and smaller, under 2%.</span></li>
<li aria-level="1"><span>Training-workload choice mattered a lot: a sysbench-tpcc-trained PGO build ended up </span><i><span>slower</span></i><span> than a non-PGO.</span></li>
<li aria-level="1"><span>After switching to a merged MTR + moderate-concurrency-sysbench training profile and re-running, the gain was still minimal, and it shrank further as concurrency increased.</span></li>
</ul>
<p> </p>
<h3><span>Why the sysbench-only build lost</span></h3>
<p><span>Sysbench-tpcc only exercises a narrow slice of mysqld, so everything outside that slice (purge, flushing, stats, error paths, alternate optimizer plans) gets pessimized as “cold” code.</span></p>
<p><span>At 128–1024 threads, GCC’s non-atomic profile counters get corrupted under contention unless I explicitly set </span><span>-fprofile-update=atomic,</span><span>a garbage profile is worse than no profile at all.</span></p>
<p><span>Instrumentation overhead (2–10x slowdown) distorts what looks “hot” under load mutex/rwlock spin loops dominate the counts, so the compiler optimizes waiting code instead of real work, while the actual bottleneck (lock contention, memory latency) is something branch layout can’t fix anyway.</span></p>
<p><b>Bottom line:</b><span> PGO for MySQL/Percona Server binaries does work in the sense that the mechanism is sound whole-binary function reordering for something the size of mysqld is a legitimate, often the single biggest, win in PGO generally. </span></p>
<p><span>But empirically here the payoff was consistently small (sub-5%, trending toward sub-1% for the concurrency-specific refinement), fragile to training-workload choice, fragile to build-flag correctness (atomic counters, </span><span>-fprofile-partial-training</span><span>), and it erodes further as thread count rises,  which is exactly what happens in production and what we care about most.</span></p>
<p><span>So my practical answer: </span><b>it’s not a clear “yes, always compile with PGO.”</b><span> It’s more a “</span><b>maybe, and only if you get every detail right</b><span>“. Correct broad-coverage training data (MTR, ideally merged with moderate-concurrency sysbench), atomic profile counters, and realistic expectations that the win is marginal and shrinks under heavy concurrency. </span></p>
<p><span>Get any of those wrong and you can end up </span><i><span>worse</span></i><span> than a plain build. </span><span><br>
</span><span>Given the size of the benefit versus the number of ways to mess up the training methodology, PGO reads more like a niche optimization for a well-controlled build pipeline than a default you’d flip on broadly.</span></p>
<p><span>I would be more than happy to prove wrong and I am eager to get other people’s feedback, so please test, test, test and let me know. </span></p>
<p> </p>
<p><span>Happy MySQL to everyone</span></p>
<p>Go to:</p>
<p><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></p>
<p><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></p>
<p>The post <a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-3/">PGO or not PGO this is the dilemma. Step 3</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Fri, 09 Oct 2026 11:47:55 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MySQL</category>
    <category>Benchmarking</category>
    <category>Compiler Optimization</category>
    <category>MySQL Performance</category>
    <category>Percona Server for MySQL</category>
    <category>PGO</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 2</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54435</guid>
    <link>https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2/</link>
    <description>Step 2: How it works
Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

How PGO works: PGO is a two-pass build. First pass compiles with instrumentation (-fprofile-generate): every basic block and branch gets a counter. 
You run a training workload, counters are dumped to profraw files. 
Second pass recompiles using those counts to drive inlining decisions, branch layout (hot path falls through, cold path jumps away), hot/cold function splitting, code ordering for icache/iTLB locality, loop unrolling, and indirect-call promotion. 
Crucially, PGO is not “make the trained workload fast” it’s “tell the compiler which code is hot and which is cold, and let it reshape the whole binary accordingly.”
 
But how does it work?
Phase 1: what actually gets recorded. The instrumented binary has counters injected at compile time one per edge in the control-flow graph, not just per function. 
So for every branch, every loop back-edge, every call site, there’s a counter that increments each time execution takes that path. 
This is finer-grained than “function X was called N times” it’s “when we reached this branch, we went left 950,000 times and right 50 times.” 
That per-edge granularity is what lets the second phase make surgical decisions rather than just “function X is hot, function Y is cold.”
Phase 2: what the compiler does with those counts. Several distinct transformations, all driven by the same counter data:
Inlining. 
Normally the compiler inlines based on static heuristics:

function size call-site count
estimated cost/benefit. 

With profile data it can override those heuristics: a call site executed millions of times gets inlined even if it looks “too expensive” by static cost rules, because the runtime benefit clearly outweighs the code-size cost. 
A call site that’s technically inlinable but sits in dead-cold code gets left as a real call inlining it would only bloat the binary for no benefit.
Branch layout. 
Every “if” in our code compiles down to a branch instruction with two possible outcomes: 

continue straight to the next instruction
jump somewhere else. 

Continuing straight is basically free; the CPU is already fetching instructions in order, so there’s no extra cost. 
Jumping is not free: the CPU has to guess in advance which way a branch will go so it can keep fetching ahead of time, and if it guesses wrong, it has to throw away the work it already queued up and start over from the right place.
That is the “pipeline bubble,” a small stall. So the “straight through” path is cheap and the “jump elsewhere” path carries a penalty when the guess is wrong. 
The compiler arranges the hot path as the fall-through and pushes the cold path out of line; literally relocated to a separate location in the binary, often into a .text.unlikely section. 
So an if (unlikely_error_condition) { …rare handling… } block doesn’t sit inline interrupting the hot path anymore; it is moved somewhere else entirely, and the hot path becomes a straight run of instructions with no diversion.
To be clear, reordering our if/else in the source code usually doesn’t change how the compiler lays out the machine code. Optimizing compilers decide branch layout themselves based on either profile data (PGO) or static heuristics, not on which branch we happened to write first in the source. So swapping the order of our if blocks by hand generally has little to no effect on the compiled result.
Hot/cold function splitting. 
This is the same idea applied within a single function. 
A function might have a hot core loop and a rarely-hit error-handling tail. 
The compiler physically splits the function into two pieces: 

the hot part stays in .text.hot
the cold part moves to .text.unlikely. 

The function still works identically (a jump connects them when needed), but now the hot part is smaller and denser, so more of it fits in an instruction-cache line, and cold code that’s almost never touched isn’t wasting icache space sitting next to it.
Whole-binary function reordering.
This is where “reshape the whole binary” becomes literal. 
At link time (especially with LTO, which MySQL’s PGO build enables), functions get physically reordered in the final executable so that functions which call each other frequently, or execute in sequence during a hot workload, are placed near each other in memory. 
This maximizes instruction-cache and iTLB locality; the CPU’s fetch unit is pulling in a tight cluster of hot functions instead of jumping all over a 100+MB binary. 
For something the size of mysqld, this is often the single biggest win, because normal builds place functions in whatever order the source files happen to be compiled, which has no relationship to runtime call patterns.
Indirect-call promotion. 
If profile data shows a virtual call or function-pointer call resolves to the same target the overwhelming majority of the time (common in C++ with vtables, e.g. a storage-engine interface with basically only InnoDB registered), the compiler inserts a guarded direct call: “if target == this specific address, call it directly and skip the indirect jump; otherwise fall back to the indirect call.” 
Direct calls are cheaper and more predictable for the branch predictor than an indirect jump through a table.
Register allocation and code density trade-offs. 
Hot code gets compiled favoring speed, more aggressive unrolling, more registers dedicated to hot-path values. 
Cold code, especially with -fprofile-partial-training and cold-path treatment, gets compiled favoring size, fewer registers, less unrolling because it barely executes. So runtime cost there is irrelevant but its footprint in the binary is not free (it still occupies disk/page-cache space and can evict hot lines from cache if placed carelessly, which is exactly why it gets segregated into .text.unlikely rather than just left unoptimized in place).
Switch/jump-table lowering. 
A switch statement with many cases can be compiled as a jump table (fast, O(1), but requires a full table load and indirect jump) or as a cascade of compares (slower per-case but better branch prediction if one case dominates). 
Profile data tells the compiler which case actually dominates in practice and picks accordingly.
Given the above,  “reshape the whole binary” is not metaphorical. The compiler is redrawing the physical layout of machine code in the executable: which instructions are adjacent to which, which code sections are hot and packed tightly versus cold and shoved to the side, which calls are direct versus indirect, and where the CPU’s fetch/prediction effort gets spent. 
None of this requires the values processed during training to resemble production traffic. It only requires the shape of control flow. 
Which branches, functions, and paths are frequently exercised to resemble production traffic. 
That’s the core reason MTR’s broad-but-different-data coverage transfers well: it walks nearly every code path in mysqld even though the actual queries and data are nothing like a TPCC workload, and control-flow shape is exactly what PGO optimizes for.
 
Why my sysbench-trained build loses and how to do it right.
The post PGO or not PGO this is the dilemma. Step 2 appeared first on Percona.</description>
    <content:encoded><![CDATA[<h2><span>Step 2: How it works</span></h2>
<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></li>
<li aria-level="1"><span>How PGO it works</span></li>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></li>
</ol>
<p><b>How PGO works:</b><span> PGO is a two-pass build. First pass compiles with instrumentation (</span><i><span>-fprofile-generate</span></i><span>): every basic block and branch gets a counter. </span><span><br>
</span><span>You run a training workload, counters are dumped to </span><i><span>profraw </span></i><span>files. </span><span><br>
</span><span>Second pass recompiles using those counts to drive inlining decisions, branch layout (hot path falls through, cold path jumps away), hot/cold function splitting, code ordering for icache/iTLB locality, loop unrolling, and indirect-call promotion. </span><span><br>
</span><span>Crucially, PGO is not “make the trained workload fast” it’s “tell the compiler which code is hot and which is cold, and let it reshape the whole binary accordingly.”</span></p>
<p> </p>
<h3><span>But how does it work?</span></h3>
<p><b>Phase 1: what actually gets recorded.</b><span> The instrumented binary has counters injected at compile time one per edge in the control-flow graph, not just per function. </span><span><br>
</span><span>So for every branch, every loop back-edge, every call site, there’s a counter that increments each time execution takes that path. </span><span><br>
</span><span>This is finer-grained than “function X was called N times” it’s “when we reached this branch, we went left 950,000 times and right 50 times.” </span><span><br>
</span><span>That per-edge granularity is what lets the second phase make surgical decisions rather than just “function X is hot, function Y is cold.”</span></p>
<p><b>Phase 2: what the compiler does with those counts.</b><span> Several distinct transformations, all driven by the same counter data:</span></p>
<p><i><span>Inlining.</span></i> <span><br>
</span><span>Normally the compiler inlines based on static heuristics:</span></p>
<ul>
<li aria-level="1"><span>function size call-site count</span></li>
<li aria-level="1"><span>estimated cost/benefit. </span></li>
</ul>
<p><span>With profile data it can override those heuristics: a call site executed millions of times gets inlined even if it looks “too expensive” by static cost rules, because the runtime benefit clearly outweighs the code-size cost. </span></p>
<p><span>A call site that’s technically inlinable but sits in dead-cold code gets left as a real call inlining it would only bloat the binary for no benefit.</span></p>
<p><i><span>Branch layout.</span></i> <span><br>
</span><span>Every “if” in our code compiles down to a branch instruction with two possible outcomes: </span></p>
<ul>
<li aria-level="1"><span>continue straight to the next instruction</span></li>
<li aria-level="1"><span>jump somewhere else. </span></li>
</ul>
<p><span>Continuing straight is basically free; the CPU is already fetching instructions in order, so there’s no extra cost. </span><span><br>
</span><span>Jumping is not free: the CPU has to guess in advance which way a branch will go so it can keep fetching ahead of time, and if it guesses wrong, it has to throw away the work it already queued up and start over from the right place.</span></p>
<p><span>That is the “pipeline bubble,” a small stall. So the “straight through” path is cheap and the “jump elsewhere” path carries a penalty when the guess is wrong. </span></p>
<p><span>The compiler arranges the hot path as the fall-through and pushes the cold path out of line; literally relocated to a separate location in the binary, often into a </span><span>.text.unlikely</span><span> section. </span><span><br>
</span><span>So an </span><span>if (unlikely_error_condition) { …rare handling… }</span><span> block doesn’t sit inline interrupting the hot path anymore; it is moved somewhere else entirely, and the hot path becomes a straight run of instructions with no diversion.</span></p>
<p><i><span>To be clear, reordering our if/else in the source code usually doesn’t change how the compiler lays out the machine code. Optimizing compilers decide branch layout themselves based on either profile data (PGO) or static heuristics, not on which branch we happened to write first in the source. So swapping the order of our if blocks by hand generally has little to no effect on the compiled result.</span></i></p>
<p><i><span>Hot/cold function splitting.</span></i> <span><br>
</span><span>This is the same idea applied within a single function. </span><span><br>
</span><span>A function might have a hot core loop and a rarely-hit error-handling tail. </span><span><br>
</span><span>The compiler physically splits the function into two pieces: </span></p>
<ul>
<li aria-level="1"><span>the hot part stays in </span><span>.text.hot</span></li>
<li aria-level="1"><span>the cold part moves to </span><span>.text.unlikely</span><span>. </span></li>
</ul>
<p><span>The function still works identically (a jump connects them when needed), but now the hot part is smaller and denser, so more of it fits in an instruction-cache line, and cold code that’s almost never touched isn’t wasting icache space sitting next to it.</span></p>
<p><i><span>Whole-binary function reordering.</span></i><span><br>
</span><span>This is where “reshape the whole binary” becomes literal. </span><span><br>
</span><span>At link time (especially with LTO, which MySQL’s PGO build enables), functions get physically reordered in the final executable so that functions which call each other frequently, or execute in sequence during a hot workload, are placed near each other in memory. </span><span><br>
</span><span>This maximizes instruction-cache and iTLB locality; the CPU’s fetch unit is pulling in a tight cluster of hot functions instead of jumping all over a 100+MB binary. </span><span><br>
</span><span>For something the size of </span><span>mysqld</span><span>, </span><b>this is often the single biggest win</b><span>, because normal builds place functions in </span><b>whatever order the source files happen to be compiled,</b> <b>which has no relationship to runtime call patterns</b><span>.</span></p>
<p><i><span>Indirect-call promotion.</span></i> <span><br>
</span><span>If profile data shows a virtual call or function-pointer call resolves to the same target the overwhelming majority of the time (common in C++ with vtables, e.g. a storage-engine interface with basically only InnoDB registered), the compiler inserts a guarded direct call: “if target == this specific address, call it directly and skip the indirect jump; otherwise fall back to the indirect call.” </span><span><br>
</span><span>Direct calls are cheaper and more predictable for the branch predictor than an indirect jump through a table.</span></p>
<p><i><span>Register allocation and code density trade-offs.</span></i> <span><br>
</span><span>Hot code gets compiled favoring speed, more aggressive unrolling, more registers dedicated to hot-path values. </span><span><br>
</span><span>Cold code, especially with </span><span>-fprofile-partial-training</span><span> and cold-path treatment, gets compiled favoring size, fewer registers, less unrolling because it barely executes. So runtime cost there is irrelevant but its footprint in the binary is not free (it still occupies disk/page-cache space and can evict hot lines from cache if placed carelessly, which is exactly why it gets segregated into </span><span>.text.unlikely</span><span> rather than just left unoptimized in place).</span></p>
<p><i><span>Switch/jump-table lowering.</span></i> <span><br>
</span><span>A switch statement with many cases can be compiled as a jump table (fast, O(1), but requires a full table load and indirect jump) or as a cascade of compares (slower per-case but better branch prediction if one case dominates). </span><span><br>
</span><span>Profile data tells the compiler which case actually dominates in practice and picks accordingly.</span></p>
<p><b>Given the above</b><span>,  “reshape the whole binary” is not metaphorical. The compiler is redrawing the physical layout of machine code in the executable: which instructions are adjacent to which, which code sections are hot and packed tightly versus cold and shoved to the side, which calls are direct versus indirect, and where the CPU’s fetch/prediction effort gets spent. </span><span><br>
</span><span>None of this requires the </span><i><span>values</span></i><span> processed during training to resemble production traffic. It only requires the </span><i><span>shape</span></i><span> of control flow. </span><span><br>
</span><span>Which branches, functions, and paths are frequently exercised to resemble production traffic. </span><span><br>
</span><span>That’s the core reason MTR’s broad-but-different-data coverage transfers well: it walks nearly every code path in mysqld even though the actual queries and data are nothing like a TPCC workload, and control-flow shape is exactly what PGO optimizes for.</span></p>
<p> </p>
<p><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></p>
<p>The post <a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2/">PGO or not PGO this is the dilemma. Step 2</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Fri, 09 Oct 2026 11:47:32 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MySQL</category>
    <category>Benchmarking</category>
    <category>Compiler Optimization</category>
    <category>MySQL Performance</category>
    <category>Percona Server for MySQL</category>
    <category>PGO</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 1</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54428</guid>
    <link>https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1/</link>
    <description>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

Step 1: What is PGO
PGO (Profile-Guided Optimization) is a two-pass compilation technique. 
First, we build the program with instrumentation that adds counters to every branch, loop, and call site. 
Next, we run it against a representative training workload so these counters can record which code paths are actually hot versus cold. 
We then recompile the program using that data to drive the compiler’s decisions.
With this profiling data, the compiler inlines hot call sites more aggressively and lays out hot paths as straight-line, fall-through code while pushing cold paths out of the way. 
Finally, it physically reorders functions in the binary so frequently interacting hot code sits close together for better instruction-cache locality, and converts frequently resolved indirect calls into direct ones.

The result isn’t “make the training run fast” but rather “reshape the entire binary’s layout around real execution patterns,” which is why the quality and representativeness of the training workload matters so much to whether PGO actually helps. 
It is nice to be wrong
Not so far ago I was wondering if having Percona Server compiled with PGO default is a good idea or not. Then I started to do some tests and I end up with this:
If I compare non PGO with PGO release I can see an optimization, minimal below 5% but is there.
If I compile MySQL using PGO and use as sample the recording of a specific test say sysbench-tpcc my compile will always be slower than my compile without PGO, no mater how many threads I use during recording, I tried from 128 to 1024.

 
 
 
 
 
 
 
 
 
Given my understanding of PGO was that it should be the other way around I was a bit disoriented. So I decided to read a bit and get a better understanding of what PGO really means/does and if it makes sense or not.
 
How PGO it works
The post PGO or not PGO this is the dilemma. Step 1 appeared first on Percona.</description>
    <content:encoded><![CDATA[<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><span>What is PGO</span></li>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></li>
<li aria-level="1"><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></li>
</ol>
<h2><span>Step 1: What is PGO</span></h2>
<p><span>PGO (Profile-Guided Optimization) is a two-pass compilation technique. </span></p>
<p><span>First, we build the program with instrumentation that adds counters to every branch, loop, and call site. </span><span><br>
</span><span>Next, we run it against a representative training workload so these counters can record which code paths are actually hot versus cold. </span><span><br>
</span><span>We then recompile the program using that data to drive the compiler’s decisions.</span></p>
<p><span>With this profiling data, the compiler inlines hot call sites more aggressively and lays out hot paths as straight-line, fall-through code while pushing cold paths out of the way. </span><span><br>
</span><span>Finally, it physically reorders functions in the binary so frequently interacting hot code sits close together for better instruction-cache locality, and converts frequently resolved indirect calls into direct ones.</span><span><br>
</span><span><br>
</span><span>The result isn’t “make the training run fast” but rather “reshape the entire binary’s layout around real execution patterns,” which is why the quality and representativeness of the training workload matters so much to whether PGO actually helps. </span></p>
<h2><span>It is nice to be wrong</span></h2>
<p><span>Not so far ago I was wondering if having Percona Server compiled with PGO default is a good idea or not. Then I started to do some tests and I end up with this:</span></p>
<p><i><span>If I compare non PGO with PGO release I can see an optimization, minimal below 5% but is there.</span></i></p>
<p><i><span>If I compile MySQL using PGO and use as sample the recording of a specific test say sysbench-tpcc my compile will always be slower than my compile without PGO, no mater how many threads I use during recording, I tried from 128 to 1024.</span></i></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-scaled.png"><img loading="lazy" decoding="async" class="size-medium_large wp-image-54430 alignleft" src="https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-768x337.png" alt="" width="768" height="337" srcset="https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-768x337.png 768w, https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-300x132.png 300w, https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-1024x449.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-1536x674.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/Percona_server_no_pg_vs_ps_marco_sysbench_pgo-2048x899.png 2048w" sizes="auto, (max-width: 768px) 100vw, 768px"></a></p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p><span>Given my understanding of PGO was that it should be the other way around I was a bit disoriented. So I decided to read a bit and get a better understanding of what PGO really means/does and if it makes sense or not.</span></p>
<p> </p>
<p><a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></p>
<p>The post <a href="https://www.percona.com/blog/pgo-or-not-pgo-this-is-the-dilemma-step-1/">PGO or not PGO this is the dilemma. Step 1</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Fri, 09 Oct 2026 11:47:08 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MySQL</category>
    <category>Benchmarking</category>
    <category>Compiler Optimization</category>
    <category>MySQL Performance</category>
    <category>Percona Server for MySQL</category>
    <category>PGO</category>
  </item>

  <item>
    <title>Step by Step: ProxySQL HA with BGP ECMP Anycast</title>
    <guid isPermaLink="false">https://percona.community/blog/2026/10/08/step-by-step-proxysql-ha-with-bgp-ecmp-anycast/</guid>
    <link>https://percona.community/blog/2026/10/08/step-by-step-proxysql-ha-with-bgp-ecmp-anycast/</link>
    <description>In our previous blog post ProxySQL HA with BGP ECMP Anycast, we established why we would want to use BGP ECMP as a strategy for making our ProxySQL cluster highly available. Now let’s look at how to implement it technically.
For this scenario we assume that we have:

an OPNsense instance as our BGP Router with the IP address 10.5.8.251
Two ProxySQL nodes (proxysql-01 with IP 10.5.20.4 and proxysql-02 with IP 10.5.20.5)
10.5.200.1 as the anycast IP we want both ProxySQL nodes to accept connections on

Setup on the router
Enable BGP on OPNsense
The out-of-the-box installation of OPNsense does not come with BGP support. To add it, install the FRRouting package first (os-frr), which will enable available protocols for dynamic routing.
Install the os-frr plugin in System -&amp;gt; Firmware -&amp;gt; Plugins.


We need to activate the routing service on our OPNsense host.

In Routing -&amp;gt; General check “enable”



Next, assign an Autonomous System (AS) number to the OPNsense BGP peer.
An AS number identifies a network, or a group of routers, under a single administrative domain.
Choose a private AS number from the range 64512-65534.
In Routing -&amp;gt; BGP (advanced mode):

check “enable”.
set the chosen AS number (in this example we use 64512).
set Maximum Paths to 2 to match the number of ProxySQL nodes that we have.

Maximum Paths configures OPNsense to perform Equal-Cost Multi-Path (ECMP) load balancing across the two paths.
Any additional paths are kept as backups and only used if an active route is withdrawn.


Create a firewall rule for BGP traffic
BGP exchanges routing information through a TCP connection on port 179.
Our ProxySQL nodes will establish this connection towards OPNsense, so we need a firewall rule to allow this traffic.
First, we will create an alias for the ProxySQL nodes. This keeps the firewall rule readable, and new nodes only need to be added in one place.
In Firewall -&amp;gt; Aliases, add:

Name: proxysql_nodes
Type: Host(s)
Content: 10.5.20.4, 10.5.20.5 (The IP addresses of our ProxySQL nodes)



With the Alias in place, we can now continue to define the actual Firewall rule:
Navigate to Firewall -&amp;gt; Rules and create a new rule:

Description: Allow ProxySQL network to BGP peer with OPNsense
Interface: Select the interface the ProxySQL nodes are in.
Action: Pass
Direction: in
Version: IPv4
Protocol: TCP
Source: proxysql_nodes (the alias we created in the previous step)
Source port: any
Destination: the OPNsense address the ProxySQL nodes peer with (10.5.8.251)
Destination port: BGP (179)

Note, here we do not explicitly enable logging. It may be helpful to enable logging that if you need to debug the BGP peering.


Set up route filtering with a prefix list
We need to set up an inbound filter using a prefix list.
The prefix list defines the networks that OPNsense will accept routes for, and should be as narrow as possible to prevent a misbehaving peer from advertising routes that could re-route sensitive traffic through it.
Prefix lists are configured in Routing -&amp;gt; BGP -&amp;gt; Prefix Lists.
Create the prefix list:

Description: ProxySQL Anycast IP
Name: ANYCAST-PROXYSQL-IN
IP Version: IPV4
Sequence Number: for this example we use 10. Sequence numbers are used in order to set the order with which to apply the rules, in case you would have multiple rules for the same prefix list. We only need to create one entry for the ProxySQL Anycast IP.
Action: Permit, as we want to allow the ProxySQL Anycast IP.
Network: 10.5.200.1/32, the Anycast IP of our ProxySQL nodes.


Tip: Click the ⓘ icon next to a field in the OPNsense UI for more information.



Configure BGP neighbors
In this step we will configure OPNsense to know about our ProxySQL servers and their intent to peer.
Neighbor configuration defines which IPs are allowed to speak the BGP protocol with OPNsense. In our case, we need to define a neighbor for both of our ProxySQL hosts:
Choose a unique AS number for the cluster. All ProxySQL nodes belonging to this cluster will share this AS number, whilst separate clusters must use different, dedicated AS numbers.
Ensure that the cluster AS number is distinct from the one configured for OPNsense.
For this example we choose the AS number 64513.
Navigate to Routing -&amp;gt; BGP -&amp;gt; Neighbors and add a new one.

Description: ProxySQL-01
Peer-IP: 10.5.20.4
Remote AS: 64513
Local AS: 64512
Prefix-List In: ANYCAST-PROXYSQL-IN:10

Make sure you repeat this for the second ProxySQL.


All configurable BGP options can be found in the OPNsense documentation.
Setup on the ProxySQL nodes
Next, assign the anycast virtual IP (10.5.200.1) to the loopback interface of each ProxySQL node, so that the node accepts traffic intended for that IP address.

bash
Copy
Copied!



# Add the Anycast VIP to the loopback interface
sudo ip addr add 10.5.200.1/32 dev lo


Make the address persistent across reboots by adding it to netplan, systemd-networkd or /etc/network/interfaces.
Install ExaBGP
ExaBGP is a tool that can speak the BGP protocol with OPNsense and is able to announce / withdraw routes.
Install ExaBGP on the ProxySQL hosts with:

bash
Copy
Copied!



sudo apt-get update
sudo apt-get install exabgp


For information on installing ExaBGP on other distributions, you can check the ExaBGP wiki here.
Note that BIRD (BIRD Internet Routing Daemon) or FRR can be used as alternative tools to ExaBGP, but in this example we will go with ExaBGP.
Create the health check
As we only want to route traffic to a ProxySQL host when the ProxySQL process is running, we need a way to tell ExaBGP when to announce the route and when to withdraw it.
We will write a health check script that ExaBGP can execute to determine the state of our ProxySQL process.
If this health check fails, ExaBGP withdraws the node’s route from OPNsense. OPNsense removes that node as an anycast next hop, and routes the traffic to the remaining healthy nodes.
In our example, we will use a simple bash script that uses mysqladmin to send a PING to ProxySQL:

bash
Copy
Copied!



#!/usr/bin/env bash

ANYCAST_IP=&quot;10.5.200.1/32&quot;
FAILED=1

while true; do
 # Execute the health check command and depending on the return code
 # announce or withdraw the route.
 mysqladmin --defaults-extra-file=/etc/exabgp/proxysql-monitor.cnf --connect-timeout=2 ping &amp;amp;&amp;gt; /dev/null
 STATUS=$?

 if [ $STATUS -eq 0 ]; then
 if [ $FAILED -ne 0 ]; then
 # Recovered: Announce route
 echo &quot;announce route $ANYCAST_IP next-hop self&quot;
 FAILED=0
 fi
 else
 if [ $FAILED -eq 0 ]; then
 # Health check failed: Withdraw route
 echo &quot;withdraw route $ANYCAST_IP next-hop self&quot;
 FAILED=1
 fi
 fi
 sleep 2
done


Save the health check file as /etc/exabgp/healthcheck-proxysql, and make it executable with chmod +x /etc/exabgp/healthcheck-proxysql.
To avoid passwords in the check script, we tell mysqladmin to load them from a separate file. Create that file (/etc/exabgp/proxysql-monitor.cnf) with the credentials that you want the check to use to connect to ProxySQL:

bash
Copy
Copied!



[client]
user=user
password=password
host=127.0.0.1
port=6033


Ensure that you replace “user” and “password” with your actual user credentials. Restrict the file permissions with:

bash
Copy
Copied!



sudo chown exabgp:exabgp /etc/exabgp/proxysql-monitor.cnf
sudo chmod 600 /etc/exabgp/proxysql-monitor.cnf


In our example, the script only checks if connecting to ProxySQL succeeds, and not whether ProxySQL can reach any backend servers.
The checks you come up with should ideally only test the readiness of the ProxySQL process, regardless of the health of the MySQL nodes behind it. Otherwise you might get unwanted side effects. For example:
you could write a check to verify whether there are hosts with status ONLINE in the runtime_mysql_servers table. At first it might sound like a good idea, as a misconfigured ProxySQL server would be taken offline.
BUT: In case your MySQL cluster is experiencing a downtime, ALL ProxySQLs will withdraw their routes, causing clients to see timeouts instead of potentially helpful error messages. Define your health checks carefully based on your specific architecture.
Configuring ExaBGP
ExaBGP configuration lives in the /etc/exabgp/exabgp.conf file. You can find full details on what can be configured there in the ExaBGP documentation.
For the purpose of this blog post, we will define three blocks.

The process block defines the path to the health check script.
The template block defines the BGP settings and which health check process to run.
Each neighbor block defines the router we want ExaBGP to peer with.


bash
Copy
Copied!



# -------------------------------------------------------------------
# Process Definitions
# -------------------------------------------------------------------
process proxysql-healthcheck {
 run &quot;/etc/exabgp/healthcheck-proxysql&quot;;
 encoder text;
}

# -------------------------------------------------------------------
# Neighbor Templates
# -------------------------------------------------------------------
template {
 neighbor opnsense-nodes {
 router-id 10.5.20.4;
 local-as 64513;
 peer-as 64512;

 api {
 processes [ proxysql-healthcheck ];
 }
 }
}

# -------------------------------------------------------------------
# OPNsense nodes
# -------------------------------------------------------------------
neighbor 10.5.8.251 {
 inherit opnsense-nodes;
 local-address 10.5.20.4;
}


Create the file on proxysql-01 and proxysql-02, but make sure to update the neighbor and template block. The router-id and local-address should be set to 10.5.20.5 on proxysql-02.
Once you have created this file, restart ExaBGP.

bash
Copy
Copied!



systemctl restart exabgp


You can check the status of ExaBGP by running exabgpcli show neighbor summary on the ProxySQL hosts.
An overview of the workflow for a healthy ProxySQL node

ExaBGP starts the health check script that we configured in exabgp.conf
The script sees that ProxySQL is running, and outputs announce route 10.5.200.1/32 next-hop self.
ExaBGP receives this output and sends a BGP UPDATE message to OPNsense.
OPNsense adds the ProxySQL node as potential “next hop” to its routing table for 10.5.200.1.

If the health checks detects that ProxySQL is not running, it will output withdraw route 10.5.200.1/32 next-hop self, which tells ExaBGP to send a BGP update for OPNsense to remove that route from its routing table.
The traffic is redistributed to the remaining healthy ProxySQL node.
Check BGP ECMP is configured correctly
Check the firewall
Ensure that the firewall is not blocking traffic by navigating to Firewall -&amp;gt; Log Files -&amp;gt; Live View. Make sure you have enabled logging in the firewall rule you created.
Filter for “address” “is” “10.5.200.1” and check that the connections are not blocked.
Check ExaBGP on the ProxySQL
Run the health check on the ProxySQL to confirm that the ProxySQL reports that it is healthy, and announces the route.

bash
Copy
Copied!



sudo -u exabgp /etc/exabgp/healthcheck-proxysql


You should see output like:

bash
Copy
Copied!



announce route 10.5.200.1/32 next-hop self


Check that ExaBGP announces the route with:

bash
Copy
Copied!



exabgpcli show adj-rib out


You should see output like:

bash
Copy
Copied!



neighbor 10.5.8.251 ipv4 unicast 10.5.200.1/32 next-hop self


Check that ExaBGP has an established BGP session to OPNsense with:

bash
Copy
Copied!



exabgpcli show neighbor summary


You want to see that the state is established. State active means “ready to connect”, but no connection is actually made.

bash
Copy
Copied!



Peer AS up/down state | #sent #recvd
10.5.8.251 64512 0:00:57 established 2 6


Verify the BGP connections and routes on OPNsense.
Navigate to Routing -&amp;gt; Diagnostics -&amp;gt; BGP to check the routing status.
The Anycast IP should appear twice, one entry for each ProxySQL. Both entries should be marked valid.
The path should show the AS number 64513.


BGP always selects a single best path. With Maximum Paths set, the other equal-cost paths are also installed in the routing table and are flagged as multipath.
You can also run vtysh on the OPNsense CLI to check this:

bash
Copy
Copied!



vtysh -c &quot;show ip route 10.5.200.1/32&quot;


You should see an entry like:

bash
Copy
Copied!



Routing entry for 10.5.200.1/32
 Known via &quot;bgp&quot;, distance 20, metric 0, best
 Last update 00:05:12 ago
 Flags: Selected
 Status: Installed
 * 10.5.20.4, via vtnet1, weight 1
 * 10.5.20.5, via vtnet1, weight 1


The * next to each result shows that this is an active next-hop path.
Because the two results share the same weight (1), ECMP is active, and OPNsense will load balance the tcp connections equally across the two routes.
Summary
In this blog post we stepped through an example setup of BGP ECMP Anycast for ProxySQL. We configured OPNsense to accept anycast routes from our ProxySQL nodes and load-balance across them. On each node, ExaBGP announces the route whilst ProxySQL is healthy and withdraws the route when ProxySQL fails. To scale the cluster, add the new node to the proxysql_nodes alias, create a BGP neighbor for it, and raise Maximum Paths to match the new node count.
This post is part of the Percona Community Writers Program.</description>
    <content:encoded><![CDATA[<p>In our previous blog post <a href="https://percona.community/blog/2026/09/07/proxysql-ha-with-bgp-ecmp-anycast/">ProxySQL HA with BGP ECMP Anycast</a>, we established why we would want to use BGP ECMP as a strategy for making our ProxySQL cluster highly available. Now let’s look at how to implement it technically.</p>
<p>For this scenario we assume that we have:</p>
<ul>
<li>an OPNsense instance as our BGP Router with the IP address <code>10.5.8.251</code></li>
<li>Two ProxySQL nodes (proxysql-01 with IP <code>10.5.20.4</code> and proxysql-02 with IP <code>10.5.20.5</code>)</li>
<li><code>10.5.200.1</code> as the anycast IP we want both ProxySQL nodes to accept connections on</li>
</ul>
<h2>Setup on the router</h2>
<h3>Enable BGP on OPNsense</h3>
<p>The out-of-the-box installation of OPNsense does not come with BGP support. To add it, install the FRRouting package first (<code>os-frr</code>), which will enable available protocols for dynamic routing.</p>
<p>Install the <code>os-frr</code> plugin in System -&gt; Firmware -&gt; Plugins.</p>
<p>
<figure><img width="1412" height="264" sizes="(max-width: 1400px) 100vw, 1400px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-plugin_hu_861e792883f2c3b1.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-plugin_hu_aff8cfa24a625b1d.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-plugin_hu_7fe85d4e8a3fd223.webp 1400w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-plugin_hu_7fe85d4e8a3fd223.webp" alt="OPNsense Plugin" loading="lazy"></figure></p>
<p>We need to activate the routing service on our OPNsense host.</p>
<ul>
<li>In Routing -&gt; General check “enable”</li>
</ul>
<p>
<figure><img width="2984" height="698" sizes="(max-width: 1400px) 100vw, 1400px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-frr-enable-routing_hu_fde1cfc2b15622db.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-frr-enable-routing_hu_86824f479b3c9f86.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-frr-enable-routing_hu_28169352b14701e2.webp 1400w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-frr-enable-routing_hu_28169352b14701e2.webp" alt="OPNsense Enable Routing" loading="lazy"></figure></p>
<p>Next, assign an Autonomous System (AS) number to the OPNsense BGP peer.
An AS number identifies a network, or a group of routers, under a single administrative domain.
Choose a private AS number from the range 64512-65534.</p>
<p>In Routing -&gt; BGP (advanced mode):</p>
<ul>
<li>check “enable”.</li>
<li>set the chosen AS number (in this example we use <code>64512</code>).</li>
<li>set Maximum Paths to 2 to match the number of ProxySQL nodes that we have.</li>
</ul>
<p>Maximum Paths configures OPNsense to perform Equal-Cost Multi-Path (ECMP) load balancing across the two paths.
Any additional paths are kept as backups and only used if an active route is withdrawn.</p>
<p>
<figure><img width="756" height="648" sizes="(max-width: 756px) 100vw, 756px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-routing_hu_3c796f862044bc7a.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-routing_hu_9f441aaca5c632fe.webp 756w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-routing_hu_9f441aaca5c632fe.webp" alt="OPNsense BGP Routing" loading="lazy"></figure></p>
<h3>Create a firewall rule for BGP traffic</h3>
<p>BGP exchanges routing information through a TCP connection on port 179.
Our ProxySQL nodes will establish this connection towards OPNsense, so we need a firewall rule to allow this traffic.</p>
<p>First, we will create an alias for the ProxySQL nodes. This keeps the firewall rule readable, and new nodes only need to be added in one place.</p>
<p>In Firewall -&gt; Aliases, add:</p>
<ul>
<li><strong>Name:</strong> <code>proxysql_nodes</code></li>
<li><strong>Type:</strong> Host(s)</li>
<li><strong>Content:</strong> <code>10.5.20.4</code>, <code>10.5.20.5</code> (The IP addresses of our ProxySQL nodes)</li>
</ul>
<p>
<figure><img width="1195" height="580" sizes="(max-width: 1195px) 100vw, 1195px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-alias_hu_22140d235aca2c70.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-alias_hu_a9d93346f6379b16.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-alias_hu_a023e4fd085c6120.webp 1195w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-alias_hu_a023e4fd085c6120.webp" alt="OPNsense Alias" loading="lazy"></figure></p>
<p>With the Alias in place, we can now continue to define the actual Firewall rule:</p>
<p>Navigate to Firewall -&gt; Rules and create a new rule:</p>
<ul>
<li><strong>Description:</strong> Allow ProxySQL network to BGP peer with OPNsense</li>
<li><strong>Interface:</strong> Select the interface the ProxySQL nodes are in.</li>
<li><strong>Action:</strong> Pass</li>
<li><strong>Direction:</strong> in</li>
<li><strong>Version:</strong> IPv4</li>
<li><strong>Protocol:</strong> TCP</li>
<li><strong>Source:</strong> <code>proxysql_nodes</code> (the alias we created in the previous step)</li>
<li><strong>Source port:</strong> any</li>
<li><strong>Destination:</strong> the OPNsense address the ProxySQL nodes peer with (<code>10.5.8.251</code>)</li>
<li><strong>Destination port:</strong> BGP (179)</li>
</ul>
<p>Note, here we do not explicitly enable logging. It may be helpful to enable logging that if you need to debug the BGP peering.</p>
<p>
<figure><img width="896" height="1085" sizes="(max-width: 896px) 100vw, 896px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-firewall-rule-bgp-alias_hu_8f0598cdee47c736.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-firewall-rule-bgp-alias_hu_2e24e32a561e870d.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-firewall-rule-bgp-alias_hu_38efa2ee74b21ac9.webp 896w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-firewall-rule-bgp-alias_hu_38efa2ee74b21ac9.webp" alt="OPNsense Firewall Rule BGP Alias" loading="lazy"></figure></p>
<h3>Set up route filtering with a prefix list</h3>
<p>We need to set up an inbound filter using a prefix list.
The prefix list defines the networks that OPNsense will accept routes for, and should be as narrow as possible to prevent a misbehaving peer from advertising routes that could re-route sensitive traffic through it.</p>
<p>Prefix lists are configured in Routing -&gt; BGP -&gt; Prefix Lists.
Create the prefix list:</p>
<ul>
<li><strong>Description</strong>: <code>ProxySQL Anycast IP</code></li>
<li><strong>Name</strong>: <code>ANYCAST-PROXYSQL-IN</code></li>
<li><strong>IP Version</strong>: IPV4</li>
<li><strong>Sequence Number</strong>: for this example we use <code>10</code>. Sequence numbers are used in order to set the order with which to apply the rules, in case you would have multiple rules for the same prefix list. We only need to create one entry for the ProxySQL Anycast IP.</li>
<li><strong>Action</strong>: <code>Permit</code>, as we want to allow the ProxySQL Anycast IP.</li>
<li><strong>Network</strong>: <code>10.5.200.1/32</code>, the Anycast IP of our ProxySQL nodes.</li>
</ul>
<blockquote>
<p><strong>Tip:</strong> Click the ⓘ icon next to a field in the OPNsense UI for more information.</p>
</blockquote>
<p>
<figure><img width="894" height="693" sizes="(max-width: 894px) 100vw, 894px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-create-prefix-list_hu_52256a9312b5c0bb.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-create-prefix-list_hu_6beaff311168a216.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-create-prefix-list_hu_ef65b74d803e2cd9.webp 894w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-create-prefix-list_hu_ef65b74d803e2cd9.webp" alt="OPNsense Create Prefix List" loading="lazy"></figure></p>
<h3>Configure BGP neighbors</h3>
<p>In this step we will configure OPNsense to know about our ProxySQL servers and their intent to peer.
Neighbor configuration defines which IPs are allowed to speak the BGP protocol with OPNsense. In our case, we need to define a neighbor for both of our ProxySQL hosts:</p>
<p>Choose a unique AS number for the cluster. All ProxySQL nodes belonging to this cluster will share this AS number, whilst separate clusters must use different, dedicated AS numbers.
Ensure that the cluster AS number is distinct from the one configured for OPNsense.
For this example we choose the AS number <code>64513</code>.</p>
<p>Navigate to Routing -&gt; BGP -&gt; Neighbors and add a new one.</p>
<ul>
<li><strong>Description</strong>: ProxySQL-01</li>
<li><strong>Peer-IP</strong>: 10.5.20.4</li>
<li><strong>Remote AS</strong>: 64513</li>
<li><strong>Local AS</strong>: 64512</li>
<li><strong>Prefix-List In</strong>: ANYCAST-PROXYSQL-IN:10</li>
</ul>
<p>Make sure you repeat this for the second ProxySQL.</p>
<p>
<figure><img width="922" height="1006" sizes="(max-width: 922px) 100vw, 922px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-neighbor_hu_854329fcb6ce70cf.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-neighbor_hu_72d5df0155c5531a.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-neighbor_hu_2c994c0bbfd3a398.webp 922w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-neighbor_hu_2c994c0bbfd3a398.webp" alt="OPNsense BGP Neighbor" loading="lazy"></figure></p>
<p>All configurable BGP options can be found in the <a href="https://docs.opnsense.org/manual/dynamic_routing.html#bgp-section" target="_blank" rel="noopener noreferrer">OPNsense documentation</a>.</p>
<h2>Setup on the ProxySQL nodes</h2>
<p>Next, assign the anycast virtual IP (<code>10.5.200.1</code>) to the loopback interface of each ProxySQL node, so that the node accepts traffic intended for that IP address.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Add the Anycast VIP to the loopback interface</span>
</span></span><span class="line"><span class="cl">sudo ip addr add 10.5.200.1/32 dev lo</span></span></code></pre></div>
</div>
</div>
<p>Make the address persistent across reboots by adding it to netplan, systemd-networkd or <code>/etc/network/interfaces</code>.</p>
<h3>Install ExaBGP</h3>
<p>ExaBGP is a tool that can speak the BGP protocol with OPNsense and is able to announce / withdraw routes.</p>
<p>Install ExaBGP on the ProxySQL hosts with:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get update
</span></span><span class="line"><span class="cl">sudo apt-get install exabgp</span></span></code></pre></div>
</div>
</div>
<p>For information on installing ExaBGP on other distributions, you can check the <a href="https://github.com/Exa-Networks/exabgp/wiki/Installation-Guide" target="_blank" rel="noopener noreferrer">ExaBGP wiki here</a>.</p>
<p>Note that BIRD (BIRD Internet Routing Daemon) or FRR can be used as alternative tools to ExaBGP, but in this example we will go with ExaBGP.</p>
<h3>Create the health check</h3>
<p>As we only want to route traffic to a ProxySQL host when the ProxySQL process is running, we need a way to tell ExaBGP when to announce the route and when to withdraw it.
We will write a health check script that ExaBGP can execute to determine the state of our ProxySQL process.
If this health check fails, ExaBGP withdraws the node’s route from OPNsense. OPNsense removes that node as an anycast next hop, and routes the traffic to the remaining healthy nodes.</p>
<p>In our example, we will use a simple bash script that uses <code>mysqladmin</code> to send a <code>PING</code> to ProxySQL:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">ANYCAST_IP</span><span class="o">=</span><span class="s2">"10.5.200.1/32"</span>
</span></span><span class="line"><span class="cl"><span class="nv">FAILED</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># Execute the health check command and depending on the return code</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># announce or withdraw the route.</span>
</span></span><span class="line"><span class="cl"> mysqladmin --defaults-extra-file<span class="o">=</span>/etc/exabgp/proxysql-monitor.cnf --connect-timeout<span class="o">=</span><span class="m">2</span> ping <span class="p">&amp;</span>&gt; /dev/null
</span></span><span class="line"><span class="cl"> <span class="nv">STATUS</span><span class="o">=</span><span class="nv">$?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="o">[</span> <span class="nv">$STATUS</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="o">[</span> <span class="nv">$FAILED</span> -ne <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># Recovered: Announce route</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">"announce route </span><span class="nv">$ANYCAST_IP</span><span class="s2"> next-hop self"</span>
</span></span><span class="line"><span class="cl"> <span class="nv">FAILED</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"> <span class="k">else</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="o">[</span> <span class="nv">$FAILED</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl"> <span class="c1"># Health check failed: Withdraw route</span>
</span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">"withdraw route </span><span class="nv">$ANYCAST_IP</span><span class="s2"> next-hop self"</span>
</span></span><span class="line"><span class="cl"> <span class="nv">FAILED</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"> <span class="k">fi</span>
</span></span><span class="line"><span class="cl"> sleep <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div>
</div>
</div>
<p>Save the health check file as <code>/etc/exabgp/healthcheck-proxysql</code>, and make it executable with <code>chmod +x /etc/exabgp/healthcheck-proxysql</code>.</p>
<p>To avoid passwords in the check script, we tell mysqladmin to load them from a separate file. Create that file (<code>/etc/exabgp/proxysql-monitor.cnf</code>) with the credentials that you want the check to use to connect to ProxySQL:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span>client<span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="nv">user</span><span class="o">=</span>user
</span></span><span class="line"><span class="cl"><span class="nv">password</span><span class="o">=</span>password
</span></span><span class="line"><span class="cl"><span class="nv">host</span><span class="o">=</span>127.0.0.1
</span></span><span class="line"><span class="cl"><span class="nv">port</span><span class="o">=</span><span class="m">6033</span></span></span></code></pre></div>
</div>
</div>
<p>Ensure that you replace “user” and “password” with your actual user credentials. Restrict the file permissions with:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chown exabgp:exabgp /etc/exabgp/proxysql-monitor.cnf
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">600</span> /etc/exabgp/proxysql-monitor.cnf</span></span></code></pre></div>
</div>
</div>
<p>In our example, the script only checks if connecting to ProxySQL succeeds, and not whether ProxySQL can reach any backend servers.
The checks you come up with should ideally only test the readiness of the ProxySQL process, regardless of the health of the MySQL nodes behind it. Otherwise you might get unwanted side effects. For example:
you could write a check to verify whether there are hosts with status <code>ONLINE</code> in the <code>runtime_mysql_servers</code> table. At first it might sound like a good idea, as a misconfigured ProxySQL server would be taken offline.
BUT: In case your MySQL cluster is experiencing a downtime, ALL ProxySQLs will withdraw their routes, causing clients to see timeouts instead of potentially helpful error messages. Define your health checks carefully based on your specific architecture.</p>
<h3>Configuring ExaBGP</h3>
<p>ExaBGP configuration lives in the <code>/etc/exabgp/exabgp.conf</code> file. You can find full details on what can be configured there in the <a href="https://github.com/Exa-Networks/exabgp/wiki/Configuration-Syntax" target="_blank" rel="noopener noreferrer">ExaBGP documentation</a>.
For the purpose of this blog post, we will define three blocks.</p>
<ul>
<li>The <code>process</code> block defines the path to the health check script.</li>
<li>The <code>template</code> block defines the BGP settings and which health check process to run.</li>
<li>Each <code>neighbor</code> block defines the router we want ExaBGP to peer with.</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Process Definitions</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl">process proxysql-healthcheck <span class="o">{</span>
</span></span><span class="line"><span class="cl"> run <span class="s2">"/etc/exabgp/healthcheck-proxysql"</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> encoder text<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Neighbor Templates</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl">template <span class="o">{</span>
</span></span><span class="line"><span class="cl"> neighbor opnsense-nodes <span class="o">{</span>
</span></span><span class="line"><span class="cl"> router-id 10.5.20.4<span class="p">;</span>
</span></span><span class="line"><span class="cl"> local-as 64513<span class="p">;</span>
</span></span><span class="line"><span class="cl"> peer-as 64512<span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> api <span class="o">{</span>
</span></span><span class="line"><span class="cl"> processes <span class="o">[</span> proxysql-healthcheck <span class="o">]</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl"> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl"><span class="c1"># OPNsense nodes</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -------------------------------------------------------------------</span>
</span></span><span class="line"><span class="cl">neighbor 10.5.8.251 <span class="o">{</span>
</span></span><span class="line"><span class="cl"> inherit opnsense-nodes<span class="p">;</span>
</span></span><span class="line"><span class="cl"> local-address 10.5.20.4<span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></div>
</div>
</div>
<p>Create the file on proxysql-01 and proxysql-02, but make sure to update the neighbor and template block. The router-id and local-address should be set to <code>10.5.20.5</code> on proxysql-02.</p>
<p>Once you have created this file, restart ExaBGP.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">systemctl restart exabgp</span></span></code></pre></div>
</div>
</div>
<p>You can check the status of ExaBGP by running <code>exabgpcli show neighbor summary</code> on the ProxySQL hosts.</p>
<h3>An overview of the workflow for a healthy ProxySQL node</h3>
<ul>
<li>ExaBGP starts the health check script that we configured in exabgp.conf</li>
<li>The script sees that ProxySQL is running, and outputs <code>announce route 10.5.200.1/32 next-hop self</code>.</li>
<li>ExaBGP receives this output and sends a BGP UPDATE message to OPNsense.</li>
<li>OPNsense adds the ProxySQL node as potential “next hop” to its routing table for 10.5.200.1.</li>
</ul>
<p>If the health checks detects that ProxySQL is not running, it will output <code>withdraw route 10.5.200.1/32 next-hop self</code>, which tells ExaBGP to send a BGP update for OPNsense to remove that route from its routing table.
The traffic is redistributed to the remaining healthy ProxySQL node.</p>
<h3>Check BGP ECMP is configured correctly</h3>
<h4>Check the firewall</h4>
<p>Ensure that the firewall is not blocking traffic by navigating to Firewall -&gt; Log Files -&gt; Live View. Make sure you have enabled logging in the firewall rule you created.
Filter for “address” “is” “10.5.200.1” and check that the connections are not blocked.</p>
<h4>Check ExaBGP on the ProxySQL</h4>
<p>Run the health check on the ProxySQL to confirm that the ProxySQL reports that it is healthy, and announces the route.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -u exabgp /etc/exabgp/healthcheck-proxysql</span></span></code></pre></div>
</div>
</div>
<p>You should see output like:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">announce route 10.5.200.1/32 next-hop self</span></span></code></pre></div>
</div>
</div>
<p>Check that ExaBGP announces the route with:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">exabgpcli show adj-rib out</span></span></code></pre></div>
</div>
</div>
<p>You should see output like:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">neighbor 10.5.8.251 ipv4 unicast 10.5.200.1/32 next-hop self</span></span></code></pre></div>
</div>
</div>
<p>Check that ExaBGP has an established BGP session to OPNsense with:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">exabgpcli show neighbor summary</span></span></code></pre></div>
</div>
</div>
<p>You want to see that the state is <code>established</code>. State <code>active</code> means “ready to connect”, but no connection is actually made.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">Peer AS up/down state <span class="p">|</span> <span class="c1">#sent #recvd</span>
</span></span><span class="line"><span class="cl">10.5.8.251 <span class="m">64512</span> 0:00:57 established <span class="m">2</span> <span class="m">6</span></span></span></code></pre></div>
</div>
</div>
<h4>Verify the BGP connections and routes on OPNsense.</h4>
<p>Navigate to Routing -&gt; Diagnostics -&gt; BGP to check the routing status.
The Anycast IP should appear twice, one entry for each ProxySQL. Both entries should be marked <code>valid</code>.
The path should show the AS number <code>64513</code>.</p>
<p>
<figure><img width="4288" height="992" sizes="(max-width: 1400px) 100vw, 1400px" srcset="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-diagnostics-routing-table_hu_c7f2fff54813f6d9.webp 768w, https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-diagnostics-routing-table_hu_27739bba1f203b16.webp 480w, https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-diagnostics-routing-table_hu_ea9dd67f4d2ca62c.webp 1400w" src="https://percona.community/blog/2026/10/step-by-step-opnsense-bgp-diagnostics-routing-table_hu_ea9dd67f4d2ca62c.webp" alt="OPNsense BGP Diagnostics Routing Table" loading="lazy"></figure></p>
<p>BGP always selects a single best path. With Maximum Paths set, the other equal-cost paths are also installed in the routing table and are flagged as multipath.
You can also run <code>vtysh</code> on the OPNsense CLI to check this:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">vtysh -c <span class="s2">"show ip route 10.5.200.1/32"</span></span></span></code></pre></div>
</div>
</div>
<p>You should see an entry like:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard">
<span class="code-block__copy-default">Copy</span>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span>
</button>
</div>
<div class="code-block__content">
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">Routing entry <span class="k">for</span> 10.5.200.1/32
</span></span><span class="line"><span class="cl"> Known via <span class="s2">"bgp"</span>, distance 20, metric 0, best
</span></span><span class="line"><span class="cl"> Last update 00:05:12 ago
</span></span><span class="line"><span class="cl"> Flags: Selected
</span></span><span class="line"><span class="cl"> Status: Installed
</span></span><span class="line"><span class="cl"> * 10.5.20.4, via vtnet1, weight <span class="m">1</span>
</span></span><span class="line"><span class="cl"> * 10.5.20.5, via vtnet1, weight <span class="m">1</span></span></span></code></pre></div>
</div>
</div>
<p>The <code>*</code> next to each result shows that this is an active next-hop path.
Because the two results share the same weight (<code>1</code>), ECMP is active, and OPNsense will load balance the tcp connections equally across the two routes.</p>
<h3>Summary</h3>
<p>In this blog post we stepped through an example setup of BGP ECMP Anycast for ProxySQL. We configured OPNsense to accept anycast routes from our ProxySQL nodes and load-balance across them. On each node, ExaBGP announces the route whilst ProxySQL is healthy and withdraws the route when ProxySQL fails. To scale the cluster, add the new node to the proxysql_nodes alias, create a BGP neighbor for it, and raise Maximum Paths to match the new node count.</p>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>]]></content:encoded>
    <pubDate>Thu, 08 Oct 2026 18:50:00 +0000</pubDate>
    <dc:creator>Percona Community</dc:creator>
    <category>MySQL</category>
    <category>ProxySQL</category>
    <category>Opensource</category>
    <category>BGP</category>
    <category>Percona Server</category>
    <category>DevOps</category>
  </item>

  <item>
    <title>Your Galera cluster’s hardest problem was never the replication library</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54528</guid>
    <link>https://www.percona.com/blog/your-galera-clusters-hardest-problem-was-never-the-replication-library/</link>
    <description>Ask anyone who runs a multi-node MySQL Galera Cluster in production about their worst night. Almost nobody says write-set replication was wrong. They say something closer to this:
“All queries piling up… no error messages in logs.”
Twenty minutes of downtime, ended only when a human picked a node to kill. (codership-team)
Or the flow-control stall where writes time out and wsrep_flow_control_sent still reads 0, so there’s nothing to alert on and nothing to write in the post-mortem. (codership-team)
You have your own version of these. You didn’t need mine.
And here’s the thing about all of them: the root cause is usually not Galera. It’s flashcache, or a shadowed wsrep_provider_options line, or DNS resolving to the wrong interface, or a one-way firewall path. One team diagnosed a Kubernetes crash loop by reading the operator’s source code.
So your MTTR isn’t dominated by repair. It’s dominated by diagnosis. The cluster is not hard to fix once you know what’s wrong, but it’s hard to know what’s wrong at 3am with writes stalled and clean logs.
That’s what changed on 30 September 2026.
What the EOL actually takes away
MariaDB plc acquired Codership in May 2025 and set the end of support for all current MySQL Galera Cluster versions at 30 September 2026. No development, no maintenance, no binary releases after that.
Your cluster will behave on 1 October exactly as it did the day before. Nothing new breaks. What disappears is the two things that used to bound those incidents: someone to escalate to, and a release that eventually contains the fix.
MariaDB Enterprise’s recommended way out is migrating to MariaDB Galera Cluster. That’s a different database: distinct system table structure, a data dictionary that differs fundamentally from MySQL’s, and user privileges that MariaDB’s own migration notes say you have to systematically recreate. You built this HA layer on open source MySQL because you wanted control. Changing database vendor to keep a support contract is the opposite of control.
Two paths that don’t ask you to:
Keep the cluster, buy back the escalation path
If diagnosis is the expensive part, what shortens it is a senior engineer who has seen your failure mode before. Not a new binary.
Percona MySQL Support covers your existing MySQL Galera Cluster as it is. No migration, no re-platforming, no privilege rebuild. Senior MySQL engineers 24x7x365 on follow-the-sun, consultative support as well as incident triage (wsrep tuning, DDL strategy, write distribution, quorum design), and contractual SLAs down to 15-minute initial response for Severity 1 incidents on Premium tier. Details in the support datasheet.
Or move to PXC, where the cluster layer still gets fixed
Percona XtraDB Cluster is Percona Server for MySQL plus the Galera library – our own fork, in a public repo, compatible with MySQL Community Edition.
The difference from the MariaDB path is mechanical: your schema, system tables, and user accounts carry over as they are. Same wsrep behavior, same ecosystem, same tools, same connectors. The ten-step walkthrough is public and free to follow on your own.
The reason to pick PXC isn’t that it’s another Galera build. It’s that the failure modes at the top of this post are what we ship fixes for: IST and SST behavior, node eviction, flow control, DDL under concurrent writes. Public release notes, Jira IDs attached, every quarter:

That cadence goes back to 2012, across 5.5, 5.6, 5.7, 8.0 and now 8.4, with the 8.0 line still shipping alongside. In the last 90 days, nearly 4,000 PXC host instances reported telemetry outside Kubernetes, and the PXC Operator runs across thousands more Kubernetes deployments. The engineers you’d escalate to are in this cluster layer every week.- See the recent work on gcache inspection and cross-site replication in the Operator.
If you’d rather not migrate alone, our migration service includes a test environment on the target cluster with a wsrep behavior comparison against your current one: you watch it handle the same failures before you commit, plus a documented rollback plan, scheduled cutover with off-hours support, and a health audit a few weeks after go-live.
What’s not an option
Running unmaintained. Not because of the date since your cluster won’t notice the date. Because the next time all the queries pile up and the logs say nothing, there’s no ticket to open and no release to wait for.
Support buys time. Migration ends the exposure. Doing both, in that order, is a perfectly good plan. Either way your clustering layer stays open source, stays on MySQL, and stays supported.
Talk to us about which one fits.
The post Your Galera cluster’s hardest problem was never the replication library appeared first on Percona.</description>
    <content:encoded><![CDATA[<p><span>Ask anyone who runs a multi-node MySQL Galera Cluster in production about their worst night. Almost nobody says write-set replication was wrong. They say something closer to this:</span></p>
<p><i><span>“All queries piling up… no error messages in logs.”</span></i></p>
<p><span>Twenty minutes of downtime, ended only when a human picked a node to kill. (</span><a href="https://groups.google.com/g/codership-team/c/KJI1JyE9tuI"><span>codership-team</span></a><span>)</span></p>
<p><span>Or the flow-control stall where writes time out and </span><span>wsrep_flow_control_sent</span><span> still reads 0, so there’s nothing to alert on and nothing to write in the post-mortem. (</span><a href="https://groups.google.com/g/codership-team/c/znt-3N1RAyQ"><span>codership-team</span></a><span>)</span></p>
<p><span>You have your own version of these. You didn’t need mine.</span></p>
<p><span>And here’s the thing about all of them: the root cause is usually not Galera. It’s flashcache, or a shadowed </span><span>wsrep_provider_options</span><span> line, or DNS resolving to the wrong interface, or a one-way firewall path. One team diagnosed a Kubernetes crash loop by reading the operator’s source code.</span></p>
<p><span>So your MTTR isn’t dominated by repair. It’s dominated by diagnosis. The cluster is not hard to fix once you know what’s wrong, but it’s hard to know what’s wrong at 3am with writes stalled and clean logs.</span></p>
<p><span>That’s what changed on 30 September 2026.</span></p>
<h2><span>What the EOL actually takes away</span></h2>
<p><span>MariaDB plc acquired Codership in May 2025 and set the end of support for all current MySQL Galera Cluster versions at 30 September 2026. No development, no maintenance, no binary releases after that.</span></p>
<p><span>Your cluster will behave on 1 October exactly as it did the day before. Nothing new breaks. What disappears is the two things that used to bound those incidents: someone to escalate to, and a release that eventually contains the fix.</span></p>
<p><span>MariaDB Enterprise’s recommended way out is migrating to MariaDB Galera Cluster. That’s a different database: distinct system table structure, a data dictionary that differs fundamentally from MySQL’s, and user privileges that MariaDB’s own migration notes say you have to systematically recreate. You built this HA layer on open source MySQL because you wanted control. Changing database vendor to keep a support contract is the opposite of control.</span></p>
<p><span>Two paths that don’t ask you to:</span></p>
<h2><span>Keep the cluster, buy back the escalation path</span></h2>
<p><span>If diagnosis is the expensive part, what shortens it is a senior engineer who has seen your failure mode before. Not a new binary.</span></p>
<p><span>Percona MySQL Support covers your existing MySQL Galera Cluster as it is. No migration, no re-platforming, no privilege rebuild. Senior MySQL engineers 24x7x365 on follow-the-sun, consultative support as well as incident triage (wsrep tuning, DDL strategy, write distribution, quorum design), and contractual SLAs down to 15-minute initial response for Severity 1 incidents on Premium tier. Details in the</span><a href="https://learn.percona.com/hubfs/Datasheet/New-Services-Offerings/Percona-Support-for-MySQL-datasheet.pdf"> <span>support datasheet</span></a><span>.</span></p>
<h2><span>Or move to PXC, where the cluster layer still gets fixed</span></h2>
<p><span>Percona XtraDB Cluster is Percona Server for MySQL plus the Galera library – our own fork, in a</span><a href="https://github.com/percona/galera"> <span>public repo</span></a><span>, compatible with MySQL Community Edition.</span></p>
<p><span>The difference from the MariaDB path is mechanical: your schema, system tables, and user accounts carry over as they are. Same wsrep behavior, same ecosystem, same tools, same connectors. The</span><a href="https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/"> <span>ten-step walkthrough</span></a><span> is public and free to follow on your own.</span></p>
<p><span>The reason to pick PXC isn’t that it’s another Galera build. It’s that the failure modes at the top of this post are what we ship fixes for: IST and SST behavior, node eviction, flow control, DDL under concurrent writes. Public release notes, Jira IDs attached, every quarter:</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-54529" src="https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-300x148.png" alt="" width="300" height="148" srcset="https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-300x148.png 300w, https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-1024x504.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-768x378.png 768w, https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-1536x756.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/galera-eol-release-table-2048x1008.png 2048w" sizes="auto, (max-width: 300px) 100vw, 300px"></p>
<p><span>That cadence goes back to 2012, across 5.5, 5.6, 5.7, 8.0 and now 8.4, with the 8.0 line still shipping alongside. In the last 90 days, nearly 4,000 PXC host instances reported telemetry outside Kubernetes, and the PXC Operator runs across thousands more Kubernetes deployments. The engineers you’d escalate to are in this cluster layer every week.- See the recent work on</span><a href="https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/"> <span>gcache inspection</span></a><span> and</span><a href="https://www.percona.com/blog/deploying-cross-site-replication-in-percona-operator-for-mysql-pxc/"> <span>cross-site replication in the Operator</span></a><span>.</span></p>
<p><span>If you’d rather not migrate alone, our migration service includes a test environment on the target cluster with a wsrep behavior comparison against your current one: you watch it handle the same failures before you commit, plus a documented rollback plan, scheduled cutover with off-hours support, and a health audit a few weeks after go-live.</span></p>
<h2><span>What’s not an option</span></h2>
<p><span>Running unmaintained. Not because of the date since your cluster won’t notice the date. Because the next time all the queries pile up and the logs say nothing, there’s no ticket to open and no release to wait for.</span></p>
<p><span>Support buys time. Migration ends the exposure. Doing both, in that order, is a perfectly good plan. Either way your clustering layer stays open source, stays on MySQL, and stays supported.</span></p>
<p><a href="https://www.percona.com/mysql/support/mysql-galera/"><span>Talk to us</span></a><span> about which one fits.</span></p>
<p>The post <a href="https://www.percona.com/blog/your-galera-clusters-hardest-problem-was-never-the-replication-library/">Your Galera cluster’s hardest problem was never the replication library</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Thu, 08 Oct 2026 16:07:52 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>MySQL</category>
    <category>Percona Announcements</category>
    <category>XtraDB Cluster (PXC)</category>
  </item>

  <item>
    <title>Making Sense of MySQL Telemetry Metrics</title>
    <guid isPermaLink="false">9d926ac03ca810fa61af4cd77beb8613</guid>
    <link>https://blogs.oracle.com/mysql/making-sense-of-mysql-telemetry-metrics</link>
    <description>In the first post in this series, we configured MySQL Telemetry to export OpenTelemetry metrics to Prometheus. Once the metrics arrive, the more important question is how a DBA should read them. This post is not a catalogue of every metric or meter. Its purpose is to build a practical way of thinking: start with […]</description>
    <pubDate>Wed, 07 Oct 2026 03:04:33 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>Monitoring</category>
    <category>MySQL</category>
    <category>MySQL Enterprise</category>
  </item>

  <item>
    <title>Why MySQL Bug Fixes Can Differ Across LTS Releases</title>
    <guid isPermaLink="false">d6c08698c8939b8bce61bb94e585e703</guid>
    <link>https://blogs.oracle.com/mysql/why-mysql-bug-fixes-can-differ-across-lts-releases</link>
    <description>At a recent discussion with MySQL ACEs and Rockstars, we were asked a fair question: If a bug is fixed in one supported MySQL LTS series, why isn’t it always fixed in another? A quick refresher: LTS means Long-Term Support. Within an LTS series, MySQL aims to keep the feature set and data format stable […]</description>
    <pubDate>Tue, 06 Oct 2026 19:54:58 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>DBTrail: Analytical Reports on Your MySQL Data, Without Running Them on MySQL – Part 1</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54477</guid>
    <link>https://www.percona.com/blog/dbtrail-analytical-reports-on-your-mysql-data-without-running-them-on-mysql-part-1/</link>
    <description>MySQL is very good at serving application workloads, but things get more complicated when somebody decides to run a large report on the same server.
A scan can push hot pages out of the buffer pool. A long-running read can hold back purge. A GROUP BY over millions of rows can start writing temporary tables to disk.
The traditional answer is a reporting replica. But that means running another MySQL server, and at the end of the day it is still a row-oriented database.
Another option is moving the data into an analytical database. That can work very well, but now you have a data pipeline to build, operate, monitor, and eventually debug.
We have been looking at what MySQL users can do with DuckDB, and DBTrail is an interesting approach.
DBTrail is open source under Apache 2.0. It is being built by Daniel Guzman-Burgos, who previously worked at Percona as a MySQL Technical Lead.
The basic idea is simple.
DBTrail connects to MySQL similarly to a replica. It makes an initial copy of the tables using mydumper and stores the data as Parquet files, either locally or in S3.
After that, it reads the MySQL binary log and periodically applies changes to the copy.
You query the resulting data with DuckDB.
Nothing is installed inside MySQL, there is no MySQL plugin, agent, or trigger.
The setup would look like this:

Let’s do some performance testing.
The setup
We used three AWS machines in the same subnet in us-west-2a.




Role
Machine
Software




Source
r7i.4xlarge
Percona Server for MySQL 8.4.11-11


DBTrail and DuckDB
r7i.4xlarge
DBTrail 0.99.0, DuckDB 1.5.6


Load generator
c7i.2xlarge
sysbench 1.0.20, sysbench-tpcc




Machines in more detail





Source and DBTrail machines
Load machine




CPU
Intel Xeon Platinum 8488C, 8 cores, 16 threads
Same CPU, 4 cores, 8 threads


Memory
128 GB, 123.8 GB usable
16 GB


Disk
EBS gp3, 400 GB, 16,000 IOPS, 1,000 MB/s provisioned
Not used by the test


Filesystem
ext4, relatime,discard,commit=30



I/O scheduler
none, 128 KB read-ahead



OS
Ubuntu 24.04.5, kernel 7.0.0-1013-aws
Same


Kernel settings
swappiness 60, dirty ratio 20/10, THP madvise
Same


Docker
29.1.3





The MySQL source
Percona Server runs in Docker using host networking.
We used full durability and a buffer pool large enough to keep the whole data set in memory:&amp;lt;code&amp;gt;innodb_buffer_pool_size        = 96G
innodb_redo_log_capacity       = 32G
innodb_flush_log_at_trx_commit = 1
sync_binlog                    = 1
innodb_flush_method            = O_DIRECT
innodb_io_capacity             = 8000
innodb_io_capacity_max         = 16000
binlog_format                  = ROW
binlog_row_image               = FULL
gtid_mode                      = ON&amp;lt;/code&amp;gt;The workload is sysbench-tpcc with 200 warehouses, which produces 102 million rows and about 19 GB of data when the run started.
For the initial workload we held the workload at 300 transactions per second.
One TPC-C transaction in this test is roughly 28 SQL statements, so that means about:

8,500 QPS
5,200 row changes per second

It is important to put that load into context—on the same server flat out with 64 threads and nothing else attached, it reached 4,049 transactions per second, or about 115,000 QPS.
So the 300 TPS test uses only about 7% of the available capacity, and we chose a relatively light load intentionally. We wanted to see what analytical queries cost even when the source has plenty of headroom.

DBTrail
DBTrail installs with one command that starts its Docker Compose stack:&amp;lt;code&amp;gt;curl -fsSL https://raw.githubusercontent.com/dbtrail/dbtrail/v0.99.0/install.sh \
  | DBTRAIL_REF=v0.99.0 sh&amp;lt;/code&amp;gt;The stack contains two containers: DBTrail itself and a MySQL 8.4.9 instance DBTrail uses as an index of row changes.
This internal MySQL needs attention, as the default installation leaves its buffer pool at MySQL’s 128 MB default. For this workload, that is nowhere near enough.
For the main tests we configured it like this:&amp;lt;code&amp;gt;innodb_buffer_pool_size        = 48G
innodb_redo_log_capacity       = 16G
innodb_log_buffer_size         = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method            = O_DIRECT
innodb_io_capacity             = 8000
innodb_io_capacity_max         = 16000
innodb_page_cleaners           = 16
innodb_flush_neighbors         = 0
skip-log-bin&amp;lt;/code&amp;gt;DuckDB ran on the same machine with its defaults: 16 threads and a 99 GB memory limit.

Does DBTrail slow MySQL down?
Short answer: at this workload, almost not at all.
The load ran continuously. Each row below represents one phase of the same test.




Phase
TPS
QPS
Median 95th percentile
Worst second




Nothing attached, 10 min
299.9
8,554
41.9 ms
46.6 ms


DBTrail reading binlog, 10 min
300.5
8,535
41.9 ms
47.5 ms


Initial copy of all tables, 4m 40s
298.1
8,460
41.1 ms
118.9 ms


Copy updated every 5 min, 75 min
300.2
8,539
41.1 ms
46.6 ms





There is one spike here that stands out.
When the initial copy started, at approximately second 1,500 of the workload, the 95th percentile latency jumped to 119 ms and throughput dropped to 271 transactions for that second and it happened once.
The initial copy takes a global read lock briefly when it starts, and the timing matches that operation, after that, DBTrail read 102 million rows in less than five minutes without any measurable slowdown in the application workload.
CPU usage confirms the same story: the source normally sat around 18% CPU, but during the initial table copy it increased to about 35%.
When we later ran the five analytical reports directly against MySQL, CPU was around 24%.


There is also a noticeable increase in source writes around minute 12. That happened three minutes before DBTrail connected, so it was unrelated to DBTrail.
The DBTrail machine itself writes roughly as much as the source because it keeps an index of every captured row change.
Query times on the TPC-C data
DBTrail creates a views.sql file containing a DuckDB view for each source table.
From there you can query the data normally:&amp;lt;code&amp;gt;$ duckdb -init views.sql

D SELECT
      i.i_id,
      i.i_name,
      sum(ol.ol_quantity) AS units,
      sum(ol.ol_amount) AS revenue
  FROM tpcc.order_line1 ol
  JOIN tpcc.item1 i ON i.i_id = ol.ol_i_id
  GROUP BY i.i_id, i.i_name
  ORDER BY revenue DESC
  LIMIT 10;&amp;lt;/code&amp;gt;We ran the same five reports against MySQL and against DuckDB while the transactional workload continued running, during the workload the data had grown from 102 million to 113 million rows.
MySQL had essentially everything cached in memory and the DBTrail copy was also in its normal operating state: a base Parquet file plus the accumulated change files that DuckDB needs to combine when executing the query.
The results:




Report
MySQL
DuckDB on DBTrail copy




Revenue per warehouse
9.7 s
0.75 s


Orders and revenue per district/month
46.5 s
2.8 s


Ten best-selling items
5m 32s
1.7 s


Customers by state and credit
3.6 s
0.18 s


Warehouses low on stock
3.0 s
0.23 s


Total
6m 35s
5.7 s




This is where the difference becomes interesting: 6m 35s on MySQL and less than 6s on DuckDB.

Running TPC-H: all 22 queries
TPC-H is a more standard analytical workload; for this test, we loaded TPC-H at scale factor 10.
The dataset contained:

86.6 million rows
18.2 GB of InnoDB data
Normal indexes on join keys, order dates, and ship dates

The initial DBTrail copy took 6 minutes 26 seconds and produced 2.8 GB of Parquet files.
We ran the same SQL against both systems and compared results:







MySQL
DuckDB warm




All 22 queries
5m 24s
10.3 s




MySQL wins Q19 because an index can go directly to the relevant rows, Q17 is effectively a tie and for the rest, DuckDB is substantially faster.
If you are interested in these performance numbers, stay tuned for Part 2, where we will look into how often DBTrail refreshes data to stay up to date.
The post DBTrail: Analytical Reports on Your MySQL Data, Without Running Them on MySQL – Part 1 appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>MySQL is very good at serving application workloads, but things get more complicated when somebody decides to run a large report on the same server.</p>
<p>A scan can push hot pages out of the buffer pool. A long-running read can hold back purge. A <code>GROUP BY</code> over millions of rows can start writing temporary tables to disk.</p>
<p>The traditional answer is a reporting replica. But that means running another MySQL server, and at the end of the day it is still a row-oriented database.</p>
<p>Another option is moving the data into an analytical database. That can work very well, but now you have a data pipeline to build, operate, monitor, and eventually debug.</p>
<p>We have been looking at what MySQL users can do with DuckDB, and DBTrail is an interesting approach.</p>
<p>DBTrail is open source under Apache 2.0. It is being built by Daniel Guzman-Burgos, who previously worked at Percona as a MySQL Technical Lead.</p>
<p><strong>The basic idea is simple.</strong></p>
<p>DBTrail connects to MySQL similarly to a replica. It makes an initial copy of the tables using <code>mydumper</code> and stores the data as Parquet files, either locally or in S3.</p>
<p>After that, it reads the MySQL binary log and periodically applies changes to the copy.</p>
<p>You query the resulting data with DuckDB.</p>
<p><strong>Nothing is installed inside MySQL, there is no MySQL plugin, agent, or trigger.</strong></p>
<p>The setup would look like this:</p>
<p><strong><a href="https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-scaled.webp"><img loading="lazy" decoding="async" class="aligncenter size-large wp-image-54479" src="https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-1024x576.webp" alt="" width="1024" height="576" srcset="https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-1024x576.webp 1024w, https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-300x169.webp 300w, https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-768x432.webp 768w, https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-1536x864.webp 1536w, https://www.percona.com/wp-content/uploads/2026/10/dbtrail_mysql_analytics_flow_web_hires-2048x1153.webp 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></strong></p>
<p>Let’s do some performance testing.</p>
<h2>The setup</h2>
<p>We used three AWS machines in the same subnet in <code>us-west-2a</code>.</p>
<div>
<table>
<thead>
<tr>
<th>Role</th>
<th>Machine</th>
<th>Software</th>
</tr>
</thead>
<tbody>
<tr>
<td>Source</td>
<td>r7i.4xlarge</td>
<td>Percona Server for MySQL 8.4.11-11</td>
</tr>
<tr>
<td>DBTrail and DuckDB</td>
<td>r7i.4xlarge</td>
<td>DBTrail 0.99.0, DuckDB 1.5.6</td>
</tr>
<tr>
<td>Load generator</td>
<td>c7i.2xlarge</td>
<td>sysbench 1.0.20, sysbench-tpcc</td>
</tr>
</tbody>
</table>
</div>
<h3>Machines in more detail</h3>
<div>
<table>
<thead>
<tr>
<th></th>
<th>Source and DBTrail machines</th>
<th>Load machine</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CPU</strong></td>
<td>Intel Xeon Platinum 8488C, 8 cores, 16 threads</td>
<td>Same CPU, 4 cores, 8 threads</td>
</tr>
<tr>
<td><strong>Memory</strong></td>
<td>128 GB, 123.8 GB usable</td>
<td>16 GB</td>
</tr>
<tr>
<td><strong>Disk</strong></td>
<td>EBS gp3, 400 GB, 16,000 IOPS, 1,000 MB/s provisioned</td>
<td>Not used by the test</td>
</tr>
<tr>
<td><strong>Filesystem</strong></td>
<td>ext4, <code>relatime,discard,commit=30</code></td>
<td></td>
</tr>
<tr>
<td><strong>I/O scheduler</strong></td>
<td>none, 128 KB read-ahead</td>
<td></td>
</tr>
<tr>
<td><strong>OS</strong></td>
<td>Ubuntu 24.04.5, kernel 7.0.0-1013-aws</td>
<td>Same</td>
</tr>
<tr>
<td><strong>Kernel settings</strong></td>
<td>swappiness 60, dirty ratio 20/10, THP <code>madvise</code></td>
<td>Same</td>
</tr>
<tr>
<td><strong>Docker</strong></td>
<td>29.1.3</td>
<td></td>
</tr>
</tbody>
</table>
</div>
<h2>The MySQL source</h2>
<p>Percona Server runs in Docker using host networking.</p>
<p>We used full durability and a buffer pool large enough to keep the whole data set in memory:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;code&gt;innodb_buffer_pool_size        = 96G
innodb_redo_log_capacity       = 32G
innodb_flush_log_at_trx_commit = 1
sync_binlog                    = 1
innodb_flush_method            = O_DIRECT
innodb_io_capacity             = 8000
innodb_io_capacity_max         = 16000
binlog_format                  = ROW
binlog_row_image               = FULL
gtid_mode                      = ON&lt;/code&gt;</pre><p>The workload is sysbench-tpcc with 200 warehouses, which produces 102 million rows and about 19 GB of data when the run started.</p>
<p>For the initial workload we held the workload at <strong>300 transactions per second</strong>.</p>
<p>One TPC-C transaction in this test is roughly 28 SQL statements, so that means about:</p>
<ul>
<li><strong>8,500 QPS</strong></li>
<li><strong>5,200 row changes per second</strong></li>
</ul>
<p>It is important to put that load into context—on the same server flat out with 64 threads and nothing else attached, it reached <strong>4,049 transactions per second</strong>, or about <strong>115,000 QPS</strong>.</p>
<p>So the 300 TPS test uses only about <strong>7% of the available capacity</strong>, and we chose a relatively light load intentionally. We wanted to see what analytical queries cost even when the source has plenty of headroom.</p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/08-load-in-context.svg"><img decoding="async" class="aligncenter size-full wp-image-54481" src="https://www.percona.com/wp-content/uploads/2026/10/08-load-in-context.svg" alt=""></a></p>
<h2>DBTrail</h2>
<p>DBTrail installs with one command that starts its Docker Compose stack:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;code&gt;curl -fsSL https://raw.githubusercontent.com/dbtrail/dbtrail/v0.99.0/install.sh \
  | DBTRAIL_REF=v0.99.0 sh&lt;/code&gt;</pre><p>The stack contains two containers: DBTrail itself and a MySQL 8.4.9 instance DBTrail uses as an index of row changes.</p>
<p>This internal MySQL needs attention, as the default installation leaves its buffer pool at MySQL’s <strong>128 MB default</strong>. For this workload, <strong>that is nowhere near enough.</strong></p>
<p>For the main tests we configured it like this:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;code&gt;innodb_buffer_pool_size        = 48G
innodb_redo_log_capacity       = 16G
innodb_log_buffer_size         = 256M
innodb_flush_log_at_trx_commit = 2
innodb_flush_method            = O_DIRECT
innodb_io_capacity             = 8000
innodb_io_capacity_max         = 16000
innodb_page_cleaners           = 16
innodb_flush_neighbors         = 0
skip-log-bin&lt;/code&gt;</pre><p>DuckDB ran on the same machine with its defaults: 16 threads and a 99 GB memory limit.</p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-scaled.png"><img loading="lazy" decoding="async" class="aligncenter size-large wp-image-54482" src="https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-1024x640.png" alt="" width="1024" height="640" srcset="https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-1024x640.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-300x188.png 300w, https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-768x480.png 768w, https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-1536x960.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/tpcc-overview-2048x1280.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></p>
<h2>Does DBTrail slow MySQL down?</h2>
<p><strong>Short answer: at this workload, almost not at all.</strong></p>
<p>The load ran continuously. Each row below represents one phase of the same test.</p>
<div>
<table>
<thead>
<tr>
<th>Phase</th>
<th>TPS</th>
<th>QPS</th>
<th>Median 95th percentile</th>
<th>Worst second</th>
</tr>
</thead>
<tbody>
<tr>
<td>Nothing attached, 10 min</td>
<td>299.9</td>
<td>8,554</td>
<td>41.9 ms</td>
<td>46.6 ms</td>
</tr>
<tr>
<td>DBTrail reading binlog, 10 min</td>
<td>300.5</td>
<td>8,535</td>
<td>41.9 ms</td>
<td>47.5 ms</td>
</tr>
<tr>
<td><strong>Initial copy of all tables, 4m 40s</strong></td>
<td>298.1</td>
<td>8,460</td>
<td>41.1 ms</td>
<td><strong>118.9 ms</strong></td>
</tr>
<tr>
<td><strong>Copy updated every 5 min, 75 min</strong></td>
<td>300.2</td>
<td>8,539</td>
<td>41.1 ms</td>
<td>46.6 ms</td>
</tr>
</tbody>
</table>
</div>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/01-source-cost.svg"><img decoding="async" class="aligncenter size-full wp-image-54483" src="https://www.percona.com/wp-content/uploads/2026/10/01-source-cost.svg" alt=""></a></p>
<p>There is one spike here that stands out.</p>
<p>When the initial copy started, at approximately second 1,500 of the workload, the 95th percentile latency jumped to <span><strong>119 ms</strong></span> and throughput dropped to <span><strong>271 transactions</strong></span> for that second and it happened once.</p>
<p>The initial copy takes a global read lock briefly when it starts, and the timing matches that operation, after that, DBTrail read <span><strong>102 million rows in less than five minutes without any measurable slowdown in the application workload.</strong></span></p>
<p>CPU usage confirms the same story: the source normally sat around <strong>18% CPU</strong>, but during the initial table copy it increased to about <strong>35%</strong>.</p>
<p>When we later ran the five analytical reports directly against MySQL, CPU was around 24%.</p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/09-cpu.svg"><img decoding="async" class="aligncenter size-full wp-image-54484" src="https://www.percona.com/wp-content/uploads/2026/10/09-cpu.svg" alt=""></a></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/10-disk.svg"><img decoding="async" class="aligncenter size-full wp-image-54485" src="https://www.percona.com/wp-content/uploads/2026/10/10-disk.svg" alt=""></a></p>
<p>There is also a noticeable increase in source writes around minute 12. That happened three minutes before DBTrail connected, so it was unrelated to DBTrail.</p>
<p>The DBTrail machine itself writes roughly as much as the source because it keeps an index of every captured row change.</p>
<h2>Query times on the TPC-C data</h2>
<p>DBTrail creates a <code>views.sql</code> file containing a DuckDB view for each source table.</p>
<p>From there you can query the data normally:</p><pre class="urvanov-syntax-highlighter-plain-tag">&lt;code&gt;$ duckdb -init views.sql

D SELECT
      i.i_id,
      i.i_name,
      sum(ol.ol_quantity) AS units,
      sum(ol.ol_amount) AS revenue
  FROM tpcc.order_line1 ol
  JOIN tpcc.item1 i ON i.i_id = ol.ol_i_id
  GROUP BY i.i_id, i.i_name
  ORDER BY revenue DESC
  LIMIT 10;&lt;/code&gt;</pre><p>We ran the same five reports against MySQL and against DuckDB while the transactional workload continued running, during the workload the data had grown from 102 million to 113 million rows.</p>
<p>MySQL had essentially everything cached in memory and the DBTrail copy was also in its normal operating state: a base Parquet file plus the accumulated change files that DuckDB needs to combine when executing the query.</p>
<p>The results:</p>
<div>
<table>
<thead>
<tr>
<th>Report</th>
<th>MySQL</th>
<th>DuckDB on DBTrail copy</th>
</tr>
</thead>
<tbody>
<tr>
<td>Revenue per warehouse</td>
<td>9.7 s</td>
<td>0.75 s</td>
</tr>
<tr>
<td>Orders and revenue per district/month</td>
<td>46.5 s</td>
<td>2.8 s</td>
</tr>
<tr>
<td>Ten best-selling items</td>
<td>5m 32s</td>
<td>1.7 s</td>
</tr>
<tr>
<td>Customers by state and credit</td>
<td>3.6 s</td>
<td>0.18 s</td>
</tr>
<tr>
<td>Warehouses low on stock</td>
<td>3.0 s</td>
<td>0.23 s</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>6m 35s</strong></td>
<td><strong>5.7 s</strong></td>
</tr>
</tbody>
</table>
</div>
<p><strong>This is where the difference becomes interesting: 6m 35s on MySQL and less than 6s on DuckDB.</strong></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/duckdb-report.png"><img loading="lazy" decoding="async" class="aligncenter size-large wp-image-54486" src="https://www.percona.com/wp-content/uploads/2026/10/duckdb-report-1024x464.png" alt="" width="1024" height="464" srcset="https://www.percona.com/wp-content/uploads/2026/10/duckdb-report-1024x464.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/duckdb-report-300x136.png 300w, https://www.percona.com/wp-content/uploads/2026/10/duckdb-report-768x348.png 768w, https://www.percona.com/wp-content/uploads/2026/10/duckdb-report-1536x696.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/duckdb-report.png 1902w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></p>
<h2>Running TPC-H: all 22 queries</h2>
<p>TPC-H is a more standard analytical workload; for this test, we loaded TPC-H at scale factor 10.</p>
<p>The dataset contained:</p>
<ul>
<li><strong>86.6 million rows</strong></li>
<li><strong>18.2 GB of InnoDB data</strong></li>
<li>Normal indexes on join keys, order dates, and ship dates</li>
</ul>
<p>The initial DBTrail copy took <strong>6 minutes 26 seconds</strong> and produced <strong>2.8 GB of Parquet files</strong>.</p>
<p>We ran the same SQL against both systems and compared results:</p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries.png"><img loading="lazy" decoding="async" class="aligncenter size-large wp-image-54487" src="https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries-1024x891.png" alt="" width="1024" height="891" srcset="https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries-1024x891.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries-300x261.png 300w, https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries-768x668.png 768w, https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries-1536x1336.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/12-tpch-queries.png 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total.png"><img loading="lazy" decoding="async" class="aligncenter size-large wp-image-54488" src="https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total-1024x300.png" alt="" width="1024" height="300" srcset="https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total-1024x300.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total-300x88.png 300w, https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total-768x225.png 768w, https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total-1536x449.png 1536w, https://www.percona.com/wp-content/uploads/2026/10/13-tpch-total.png 1600w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></a></p>
<div>
<table>
<thead>
<tr>
<th></th>
<th>MySQL</th>
<th>DuckDB warm</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>All 22 queries</strong></td>
<td><strong>5m 24s</strong></td>
<td><strong>10.3 s</strong></td>
</tr>
</tbody>
</table>
</div>
<p>MySQL wins <strong>Q19</strong> because an index can go directly to the relevant rows, <strong>Q17 is effectively a tie</strong> and for the rest, <strong>DuckDB is substantially faster.</strong></p>
<p>If you are interested in these performance numbers, stay tuned for Part 2, where we will look into how often DBTrail refreshes data to stay up to date.</p>
<p>The post <a href="https://www.percona.com/blog/dbtrail-analytical-reports-on-your-mysql-data-without-running-them-on-mysql-part-1/">DBTrail: Analytical Reports on Your MySQL Data, Without Running Them on MySQL – Part 1</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Tue, 06 Oct 2026 16:28:16 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Benchmarks</category>
    <category>MySQL</category>
  </item>

  <item>
    <title>Village News: MySQL News + Events (6 October 2026)</title>
    <guid isPermaLink="false">6ac432dc38515d0001bcda84</guid>
    <link>https://villagesql.com/blog/village-news-mysql-news-events-6-october-2026/</link>
    <description>Welcome back to Village News, our curated roundup of MySQL and database news.
If you want to get these updates, just subscribe to the blog.
Enjoy!
MySQL News
Note: Aggregated MySQL news can be found at Planet for MySQL Community and Planet MySQL (Oracle curated)
Thread Pool in Percona Server and MySQL (Part 1)
Percona Blog
TL;DR - MySQL 26.7.0 and 9.7.2 bring a thread pool to Community Server for the first time.
TidesDB now available for MySQL v9, v26
Alex Gaetano Padula, TidesDB Blog
TL;DR - TideSQL-MySQL 2.0.0 loads the TidesDB engine as a plugin into stock MySQL 9.7.0 and 26.7.0, so TidesDB tables can sit alongside InnoDB tables in the same server. For now it installs with a script from the repository.
How easy is it to use the VillageSQL MCP extension?
Ronald Bradford
TL;DR - Ronald built and installed vsql-mcp on a throwaway server in about four minutes and answered a business question through a SELECT-only MCP user. His attempts to get past its guardrails were blocked.
Database News
Supabase is acquiring Turso
Paul Copplestone, Supabase Blog
TL;DR - Supabase is buying Turso, the team that rewrote SQLite in Rust, to build database infrastructure for AI agents. Turso founders Glauber Costa and Pekka Enberg join Supabase's leadership.
Amazon Aurora's analytics is DuckDB: a reproducible side-by-side with pg_duckdb
Franck Pachot
TL;DR - Franck puts Aurora PostgreSQL's aurora_analytics and pg_duckdb side by side. The EXPLAIN plans match, down to DuckDB's internal compression operators and constants, which shows that Aurora's S3 Parquet/Iceberg analytics runs on embedded DuckDB.
MariaDB Foundation Adds PostgreSQL to Its Engine‑Agnostic Testing Framework (TAF).
Jonathan Miller, MariaDB Foundation
TL;DR - TAF gains a PostgreSQL plugin and beta HammerDB TPROC-C/TPROC-H and Sysbench profiles, so MariaDB, MySQL and Postgres can be benchmarked under identical, reproducible conditions.
Scale without limits: Multigres, OrioleDB, and dbarena
Supabase Blog
TL;DR - At Supabase Select, the company launched three things. Multigres (pooling and multi-node failover for Postgres) is in private alpha. The OrioleDB storage engine is in public beta. dbarena, an open benchmark site comparing managed Postgres providers, is live.
When AI Finds the Bugs We Missed: A Very Busy Year for MariaDB Security
Frédéric Descamps, MariaDB Foundation
TL;DR - AI-assisted research drove 173 security reports over two quarters, which led to 27 published advisories and delayed some MariaDB releases. Update to the latest maintenance releases.
Benchmark Analysis with HammerDB TPROC-C(TPC-C) on TideSQL v5.1.0, InnoDB in MariaDB v11.4.13
Alex Gaetano Padula, TidesDB Blog
TL;DR - On a 3,000-warehouse TPROC-C run, TideSQL and InnoDB peaked at similar NOPM, but InnoDB had half the p99 latency. TideSQL held throughput at 128 VUs, where InnoDB dropped 34%. The author notes that one InnoDB setting was left untuned.
Influence Is Not Ownership: Anna Widenius and Kaj Arnö on Governance, Open Source, and the Future of MariaDB
Roberto V. Zicari, ODBMS Industry Watch
TL;DR - The MariaDB Foundation's leaders explain its newly formalised governance. Authority comes from contribution rather than employer, and there is a defined path from contributor to committer, reviewer and maintainer.
Designing Neki for performance
Dirkjan Bussink, PlanetScale
TL;DR - PlanetScale's sharded-Postgres router decodes lazily. Point lookups pass through as raw wire-protocol bytes, and only sort keys or row boundaries get decoded when a query needs them.
Working with foreign key constraints in Aurora DSQL
Rekha Reddy Anupati and Arnab Chowdhury, AWS Database Blog
TL;DR - Aurora DSQL foreign keys are checked against the transaction snapshot, and conflicting concurrent key changes fail at commit with SQLSTATE 40001. Cascades count toward the 3,000-row transaction limit, and existing tables can use NOT VALID plus async validation.
Upcoming Database Events
High Performance Transaction Systems (HPTS)
October 4-7, 2026
Asilomar Conference Grounds
Pacific Grove, CA
Open Source Summit Europe
October 7-9, 2026
Prague, Czechia
Community Over Code 2026
October 11-14, 2026
Hilton Glasgow
Glasgow, UK
TiDB SCaiLE 2026
October 15, 2026
Computer History Museum
Mountain View, CA
All Things Open 2026
October 19-20, 2026
Raleigh Convention Center
Raleigh, NC
PGConf.EU
October 20–23, 2026 (PostgreSQL Europe)
Valencia, Spain
P99 CONF 2026
October 21-22, 2026
Online
MySQL Public Discussion #6
October 22, 2026, 17:00–18:00 UTC
Online
Oracle AI World
October 25–28, 2026
Las Vegas, NV
PG Down Under 2026
October 30, 2026
Surry Hills
Sydney, Australia
MySQL Contributor Summit
November 4-5, 2026
Virtual
KubeCon + CloudNativeCon North America (VillageSQL is a sponsor)
November 9-12, 2026
Salt Lake City, Utah
PASS Data Community Summit
November 9-11, 2026
Hyatt Regency Seattle
Seattle, WA
PGConf.Asia 2026
November 17-18, 2026
Hong Kong
PGConf.PL 2026
November 24, 2026
ARCHE Dwór Uphagena
Gdańsk, Poland
AWS re:Invent 2026
November 30 – December 4, 2026
Las Vegas, NV
Open Source Summit Japan
December 7-9, 2026
Tokyo, Japan
CIDR 2027
January 24-27, 2027
Mövenpick Hotel Amsterdam City Centre
Amsterdam, The Netherlands
FOSDEM 2027
January 30-31, 2027
ULB Solbosch Campus
Brussels, Belgium
CERN PGDay 2027
February 12, 2027
CERN Council Chamber
Geneva, Switzerland
PGConf India 2027
March 2-5, 2027
Sheraton Grand Hotel at Brigade Gateway
Bengaluru, India
KubeCon + CloudNativeCon Europe 2027
March 15-18, 2027
Barcelona, Spain
Nordic PGDay 2027
March 16, 2027
Courtyard Kungsholmen
Stockholm, Sweden
SCaLE 24x
April 1-4, 2027
Pasadena Convention Center
Pasadena, CA
PgDay Boston 2027
April 10, 2027
Tufts University Joyce Cummings Center
Boston, MA
PGConf.dev 2027
May 11-14, 2027
Plaza Centre-Ville
Montréal, QC, Canada</description>
    <content:encoded><![CDATA[<img src="https://storage.ghost.io/c/db/c7/dbc78ca4-dcfe-468b-b2fa-030396d2a98e/content/images/2026/10/village-news-banner.jpg" alt="Village News: MySQL News + Events (6 October 2026)"><p>Welcome back to Village News, our curated roundup of MySQL and database news.</p>
<p>If you want to get these updates, just <a href="https://villagesql.com/blog/blog#/portal/signup">subscribe</a> to the blog.</p>
<p>Enjoy!</p>
<h2>MySQL News</h2>
<p><em>Note: Aggregated MySQL news can be found at <a href="https://planet.oursqlcommunity.org/?source=villagesql">Planet for MySQL Community</a> and <a href="https://planet.mysql.com/">Planet MySQL</a> (Oracle curated)</em></p>
<p><strong><a href="https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-1/">Thread Pool in Percona Server and MySQL (Part 1)</a></strong><br>
<em>Percona Blog</em></p>
<p><strong>TL;DR -</strong> MySQL 26.7.0 and 9.7.2 bring a thread pool to Community Server for the first time.</p>
<p><strong><a href="https://tidesdb.com/articles/tidesdb-now-available-for-mysql/">TidesDB now available for MySQL v9, v26</a></strong><br>
<em>Alex Gaetano Padula, TidesDB Blog</em></p>
<p><strong>TL;DR -</strong> TideSQL-MySQL 2.0.0 loads the TidesDB engine as a plugin into stock MySQL 9.7.0 and 26.7.0, so TidesDB tables can sit alongside InnoDB tables in the same server. For now it installs with a script from the repository.</p>
<p><strong><a href="https://ronaldbradford.com/blog/2026-10-02-how-easy-is-it-to-use-villagesql-mcp-extension/">How easy is it to use the VillageSQL MCP extension?</a></strong><br>
<em>Ronald Bradford</em></p>
<p><strong>TL;DR -</strong> Ronald built and installed vsql-mcp on a throwaway server in about four minutes and answered a business question through a SELECT-only MCP user. His attempts to get past its guardrails were blocked.</p>
<h2>Database News</h2>
<p><strong><a href="https://supabase.com/blog/supabase-is-acquiring-turso">Supabase is acquiring Turso</a></strong><br>
<em>Paul Copplestone, Supabase Blog</em></p>
<p><strong>TL;DR -</strong> Supabase is buying Turso, the team that rewrote SQLite in Rust, to build database infrastructure for AI agents. Turso founders Glauber Costa and Pekka Enberg join Supabase's leadership.</p>
<p><strong><a href="https://dev.to/franckpachot/amazon-auroras-analytics-is-duckdb-a-reproducible-side-by-side-with-pgduckdb-5343">Amazon Aurora's analytics is DuckDB: a reproducible side-by-side with pg_duckdb</a></strong><br>
<em>Franck Pachot</em></p>
<p><strong>TL;DR -</strong> Franck puts Aurora PostgreSQL's <code>aurora_analytics</code> and pg_duckdb side by side. The EXPLAIN plans match, down to DuckDB's internal compression operators and constants, which shows that Aurora's S3 Parquet/Iceberg analytics runs on embedded DuckDB.</p>
<p><strong><a href="https://mariadb.org/mariadb-foundation-adds-postgresql-to-its-engine-agnostic-testing-framework-taf/">MariaDB Foundation Adds PostgreSQL to Its Engine‑Agnostic Testing Framework (TAF).</a></strong><br>
<em>Jonathan Miller, MariaDB Foundation</em></p>
<p><strong>TL;DR -</strong> TAF gains a PostgreSQL plugin and beta HammerDB TPROC-C/TPROC-H and Sysbench profiles, so MariaDB, MySQL and Postgres can be benchmarked under identical, reproducible conditions.</p>
<p><strong><a href="https://supabase.com/blog/select-2026-scale-without-limits">Scale without limits: Multigres, OrioleDB, and dbarena</a></strong><br>
<em>Supabase Blog</em></p>
<p><strong>TL;DR -</strong> At Supabase Select, the company launched three things. Multigres (pooling and multi-node failover for Postgres) is in private alpha. The OrioleDB storage engine is in public beta. dbarena, an open benchmark site comparing managed Postgres providers, is live.</p>
<p><strong><a href="https://mariadb.org/when-ai-finds-the-bugs-we-missed-a-very-busy-year-for-mariadb-security/">When AI Finds the Bugs We Missed: A Very Busy Year for MariaDB Security</a></strong><br>
<em>Frédéric Descamps, MariaDB Foundation</em></p>
<p><strong>TL;DR -</strong> AI-assisted research drove 173 security reports over two quarters, which led to 27 published advisories and delayed some MariaDB releases. Update to the latest maintenance releases.</p>
<p><strong><a href="https://tidesdb.com/articles/benchmark-analysis-hammerdb-tpc-c-tidesql-v5-1-0-innodb-mariadb-v11-4-13/">Benchmark Analysis with HammerDB TPROC-C(TPC-C) on TideSQL v5.1.0, InnoDB in MariaDB v11.4.13</a></strong><br>
<em>Alex Gaetano Padula, TidesDB Blog</em></p>
<p><strong>TL;DR -</strong> On a 3,000-warehouse TPROC-C run, TideSQL and InnoDB peaked at similar NOPM, but InnoDB had half the p99 latency. TideSQL held throughput at 128 VUs, where InnoDB dropped 34%. The author notes that one InnoDB setting was left untuned.</p>
<p><strong><a href="https://www.odbms.org/blog/2026/10/influence-is-not-ownership-anna-widenius-and-kaj-arno-on-governance-open-source-and-the-future-of-mariadb/">Influence Is Not Ownership: Anna Widenius and Kaj Arnö on Governance, Open Source, and the Future of MariaDB</a></strong><br>
<em>Roberto V. Zicari, ODBMS Industry Watch</em></p>
<p><strong>TL;DR -</strong> The MariaDB Foundation's leaders explain its newly formalised governance. Authority comes from contribution rather than employer, and there is a defined path from contributor to committer, reviewer and maintainer.</p>
<p><strong><a href="https://planetscale.com/blog/designing-neki-for-performance">Designing Neki for performance</a></strong><br>
<em>Dirkjan Bussink, PlanetScale</em></p>
<p><strong>TL;DR -</strong> PlanetScale's sharded-Postgres router decodes lazily. Point lookups pass through as raw wire-protocol bytes, and only sort keys or row boundaries get decoded when a query needs them.</p>
<p><strong><a href="https://aws.amazon.com/blogs/database/working-with-foreign-key-constraints-in-aurora-dsql/">Working with foreign key constraints in Aurora DSQL</a></strong><br>
<em>Rekha Reddy Anupati and Arnab Chowdhury, AWS Database Blog</em></p>
<p><strong>TL;DR -</strong> Aurora DSQL foreign keys are checked against the transaction snapshot, and conflicting concurrent key changes fail at commit with SQLSTATE 40001. Cascades count toward the 3,000-row transaction limit, and existing tables can use NOT VALID plus async validation.</p>
<h2>Upcoming Database Events</h2>
<p><strong><a href="https://hpts.ws/">High Performance Transaction Systems (HPTS)</a></strong><br>
October 4-7, 2026<br>
Asilomar Conference Grounds<br>
Pacific Grove, CA</p>
<p><strong><a href="https://events.linuxfoundation.org/open-source-summit-europe/">Open Source Summit Europe</a></strong><br>
October 7-9, 2026<br>
Prague, Czechia</p>
<p><strong><a href="https://communityovercode.org/">Community Over Code 2026</a></strong><br>
October 11-14, 2026<br>
Hilton Glasgow<br>
Glasgow, UK</p>
<p><strong><a href="https://www.pingcap.com/tidb-scaile-summit/">TiDB SCaiLE 2026</a></strong><br>
October 15, 2026<br>
Computer History Museum<br>
Mountain View, CA</p>
<p><strong><a href="https://2026.allthingsopen.org/">All Things Open 2026</a></strong><br>
October 19-20, 2026<br>
Raleigh Convention Center<br>
Raleigh, NC</p>
<p><strong><a href="http://pgconf.eu/">PGConf.EU</a></strong><br>
October 20–23, 2026 (PostgreSQL Europe)<br>
Valencia, Spain</p>
<p><strong><a href="https://www.p99conf.io/">P99 CONF 2026</a></strong><br>
October 21-22, 2026<br>
Online</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Public Discussion #6</a></strong><br>
October 22, 2026, 17:00–18:00 UTC<br>
Online</p>
<p><strong><a href="https://www.oracle.com/ai-world/">Oracle AI World</a></strong><br>
October 25–28, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://conf.pgdu.org/conferences/2026/">PG Down Under 2026</a></strong><br>
October 30, 2026<br>
Surry Hills<br>
Sydney, Australia</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Contributor Summit</a></strong><br>
November 4-5, 2026<br>
Virtual</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-north-america/">KubeCon + CloudNativeCon North America</a></strong> (<strong>VillageSQL is a sponsor</strong>)<br>
November 9-12, 2026<br>
Salt Lake City, Utah</p>
<p><strong><a href="https://passdatacommunitysummit.com/">PASS Data Community Summit</a></strong><br>
November 9-11, 2026<br>
Hyatt Regency Seattle<br>
Seattle, WA</p>
<p><strong><a href="https://www.pgconf.asia/EN/2026/">PGConf.Asia 2026</a></strong><br>
November 17-18, 2026<br>
Hong Kong</p>
<p><strong><a href="https://2026.pgconf.pl/">PGConf.PL 2026</a></strong><br>
November 24, 2026<br>
ARCHE Dwór Uphagena<br>
Gdańsk, Poland</p>
<p><strong><a href="https://aws.amazon.com/events/reinvent">AWS re:Invent 2026</a></strong><br>
November 30 – December 4, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://events.linuxfoundation.org/open-source-summit-japan/">Open Source Summit Japan</a></strong><br>
December 7-9, 2026<br>
Tokyo, Japan</p>
<p><strong><a href="https://www.cidrdb.org/cidr2027/">CIDR 2027</a></strong><br>
January 24-27, 2027<br>
Mövenpick Hotel Amsterdam City Centre<br>
Amsterdam, The Netherlands</p>
<p><strong><a href="https://fosdem.org/2027/">FOSDEM 2027</a></strong><br>
January 30-31, 2027<br>
ULB Solbosch Campus<br>
Brussels, Belgium</p>
<p><strong><a href="https://indico.cern.ch/event/1720940/">CERN PGDay 2027</a></strong><br>
February 12, 2027<br>
CERN Council Chamber<br>
Geneva, Switzerland</p>
<p><strong><a href="https://pgconf.in/">PGConf India 2027</a></strong><br>
March 2-5, 2027<br>
Sheraton Grand Hotel at Brigade Gateway<br>
Bengaluru, India</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/">KubeCon + CloudNativeCon Europe 2027</a></strong><br>
March 15-18, 2027<br>
Barcelona, Spain</p>
<p><strong><a href="https://2027.nordicpgday.org/">Nordic PGDay 2027</a></strong><br>
March 16, 2027<br>
Courtyard Kungsholmen<br>
Stockholm, Sweden</p>
<p><strong><a href="https://www.socallinuxexpo.org/scale/24x">SCaLE 24x</a></strong><br>
April 1-4, 2027<br>
Pasadena Convention Center<br>
Pasadena, CA</p>
<p><strong><a href="https://2027.pgdayboston.org/">PgDay Boston 2027</a></strong><br>
April 10, 2027<br>
Tufts University Joyce Cummings Center<br>
Boston, MA</p>
<p><strong><a href="https://2027.pgconf.dev/">PGConf.dev 2027</a></strong><br>
May 11-14, 2027<br>
Plaza Centre-Ville<br>
Montréal, QC, Canada</p>]]></content:encoded>
    <pubDate>Tue, 06 Oct 2026 13:00:04 +0000</pubDate>
    <dc:creator>VillageSQL</dc:creator>
  </item>

  <item>
    <title>Movie Finder: semantic search over 1M films, inside MySQL</title>
    <guid isPermaLink="false">https://askdba.net/?p=2991</guid>
    <link>https://askdba.net/movie-finder-semantic-search-over-1m-films-inside-mysql/</link>
    <description>
Type “a heist that goes wrong in a snowy town” and get back real films, with posters, in a few milliseconds. The vector search behind it runs inside MySQL, with no separate vector database.



Movie Finder is the new demo app that ships with MyVector. It searches up to 1,035,695 TMDB movies by meaning, not keywords. One docker compose up gives you the whole thing: MySQL 9.7 with the MyVector component, a loader, and a small web app.



Try it live: https://demo.myvector.online/







We built it to answer the questions people ask us most. Does vector search in MySQL hold up at a million rows? Can I mix it with ordinary WHERE filters? What happens when I insert a new row? The demo shows the answers on the page, with the SQL that ran under every result.



Live: What you can do on the page



Every feature on the page maps to a MyVector capability, and every result panel shows its SQL.




Describe a movie in your own words and get the nearest films. Ask for “a boy wizard at a school of magic” and Harry Potter comes back first.



Filter by genre, year, rating and language. These are plain SQL predicates combined with the vector search, and the page tells you which filtered-search path ran and why.



Rank by similarity alone, or with a small boost for films many people have rated, so the famous match beats an obscure one at almost the same distance. Both are ordinary ORDER BY expressions.



More like this: a film’s stored vector becomes the next query, in pure SQL.



HNSW vs exact: run both side by side and compare speed and recall.



Add a movie: a plain INSERT, searchable within about a second, with no index rebuild. (Not on live demo) 



Under the hood: live index details from myvector_index_status, a bar showing where each query’s time went, and a speed-vs-recall chart that sweeps ef_search from 10 to 640 against an exact scan.




How it works







The app embeds only your query text; every movie’s vector already sits in MySQL, so a search is one SQL round trip. The movie vectors come with the dataset, made by nomic-embed-text-v1.5 from each film’s title, tagline, and overview. The app uses the same model locally on the CPU, so there is no API key, and nothing leaves your machine.



New rows need no rebuild. Adding a movie is a plain INSERT; MyVector’s binlog listener picks it up and adds it to the HNSW index, usually within a second.



The SQL



The whole index is declared in a column comment. This is the movies table’s vector column:



embedding VARBINARY(3080) COMMENT
  'MYVECTOR COLUMN type=HNSW,dim=768,size=...,M=16,ef=100,dist=Cosine,online=Y,idcol=id,threads=N'



The loader inserts the rows, then builds the HNSW index with one call:



CALL mysql.myvector_index_build('movies.movies.embedding', 'id');



A search turns the query vector into a nearest-first list of ids with myvector_ann_set(), then joins back to the table. JSON_TABLE keeps the order:



SELECT m.title, myvector_distance(m.embedding, @q, 'Cosine') AS distance
FROM (SELECT myvector_ann_set('movies.movies.embedding', 'id', @q,
                              'nn=10,ef_search=100') AS js) src,
     JSON_TABLE(src.js, '$[*]' COLUMNS (rank_no FOR ORDINALITY, id INT PATH '$')) nn
JOIN movies.movies m ON m.id = nn.id
ORDER BY nn.rank_no;



“More like this” needs no embedding at all. It reads a film’s stored vector into the query variable:



SELECT embedding INTO @q FROM movies.movies WHERE id = @movie_id;



Filters pick one of two paths. The app counts each filter on its own index and takes the smallest count as an upper bound:




50,000 matches or fewer: it passes the matching keys as the fifth argument of myvector_ann_set, so HNSW searches only among them.



More than that: it calls MYVECTOR_ANN_FILTERED, which takes the nearest candidates and keeps those that pass the filter.




Run it yourself



To run your own copy, from a clone of the repository:



cd examples/movie-finder
MOVIES=100k docker compose up      # then open http://localhost:8080



The first start downloads the TMDB data, about 7 GB, once. MOVIES picks how many films to load, most-voted first: 10k, 100k (the default) or full. Measured on a 16-core Arm (Neoverse-N1) Linux host with MySQL 9.7.2:



Step10k100kfull (1,035,695)Insert7 s41 s407 sBuild the HNSW index (16 threads)29 s33 s545 sHNSW search3–5 ms4–7 ms5–20 msExact search (scans every row)30 ms280 ms3 s warmRecall@10, HNSW vs exact100%100%100%



At a million movies, HNSW answers in 5–20 ms where a full scan takes 3 seconds, with the same top 10. Recall is for the unfiltered test query; a broad genre filter (Drama) at full size gave 80% at ef_search 100, and the page lets you raise ef_search to trade time for recall. For the full profile, give Docker about 12 GB of memory and set MYSQL_BUFFER_POOL=6G.



Running it on a remote server? The page and MySQL listen on localhost only. Forward the port instead of opening it: ssh -N -L 8080:127.0.0.1:8080 &amp;lt;server&amp;gt;.



Version note: the demo uses features newer than the v1.26.9 images: filtered search, MYVECTOR_ANN_FILTERED, per-query ef_search, and online updates that survive a restart. Until the next release ships, the README shows how to build a local image from main.



Data: the movie metadata and posters come from TMDB via a Hugging Face mirror. Your machine downloads it; it is not in the repository or any image. This is a non-commercial demo, not endorsed by TMDB.



Try it and tell us



Try the live demo first. To run your own, start with MOVIES=10k for a quick first look, then go to full to watch a million rows answer in milliseconds. The demo guide has screenshots and the other demos; the Movie Finder README has the full details.



If a query surprises you, or you want a feature the page doesn’t show yet, open an issue on GitHub. A star helps other MySQL users find the project.



</description>
    <content:encoded><![CDATA[<p><img src="https://askdba.net/wp-content/uploads/2026/10/myvector_movies-Large.jpeg" width="1280" height="720" alt=""></p>
<p class="wp-block-paragraph">Type “a heist that goes wrong in a snowy town” and get back real films, with posters, in a few milliseconds. The vector search behind it runs inside MySQL, with no separate vector database.</p>



<p class="wp-block-paragraph">Movie Finder is the new demo app that ships with MyVector. It searches up to 1,035,695 TMDB movies by meaning, not keywords. One <code>docker compose up</code> gives you the whole thing: MySQL 9.7 with the MyVector component, a loader, and a small web app.</p>



<p class="wp-block-paragraph"><strong>Try it live:</strong> <a href="https://demo.myvector.online/">https://demo.myvector.online/</a></p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="432" src="https://askdba.net/wp-content/uploads/2026/10/image-1-1024x432.png" alt="" class="wp-image-2994" srcset="https://askdba.net/wp-content/uploads/2026/10/image-1-1024x432.png 1024w, https://askdba.net/wp-content/uploads/2026/10/image-1-300x127.png 300w, https://askdba.net/wp-content/uploads/2026/10/image-1-768x324.png 768w, https://askdba.net/wp-content/uploads/2026/10/image-1-624x263.png 624w, https://askdba.net/wp-content/uploads/2026/10/image-1.png 1161w" sizes="(max-width: 1024px) 100vw, 1024px"></figure>



<p class="wp-block-paragraph">We built it to answer the questions people ask us most. Does vector search in MySQL hold up at a million rows? Can I mix it with ordinary <code>WHERE</code> filters? What happens when I insert a new row? The demo shows the answers on the page, with the SQL that ran under every result.</p>



<h2 class="wp-block-heading">Live: What you can do on the page</h2>



<p class="wp-block-paragraph">Every feature on the page maps to a MyVector capability, and every result panel shows its SQL.</p>



<ul class="wp-block-list">
<li><strong>Describe a movie</strong> in your own words and get the nearest films. Ask for “a boy wizard at a school of magic” and Harry Potter comes back first.</li>



<li><strong>Filter</strong> by genre, year, rating and language. These are plain SQL predicates combined with the vector search, and the page tells you which filtered-search path ran and why.</li>



<li><strong>Rank</strong> by similarity alone, or with a small boost for films many people have rated, so the famous match beats an obscure one at almost the same distance. Both are ordinary <code>ORDER BY</code> expressions.</li>



<li><strong>More like this</strong>: a film’s stored vector becomes the next query, in pure SQL.</li>



<li><strong>HNSW vs exact</strong>: run both side by side and compare speed and recall.</li>



<li><s><strong>Add a movie</strong>: a plain <code>INSERT</code>, searchable within about a second, with no index rebuild.</s> (Not on live demo) </li>



<li><strong>Under the hood</strong>: live index details from <code>myvector_index_status</code>, a bar showing where each query’s time went, and a speed-vs-recall chart that sweeps <code>ef_search</code> from 10 to 640 against an exact scan.</li>
</ul>



<h2 class="wp-block-heading">How it works</h2>



<figure class="wp-block-image size-full"><img decoding="async" width="670" height="342" src="https://askdba.net/wp-content/uploads/2026/10/image.png" alt="" class="wp-image-2992" srcset="https://askdba.net/wp-content/uploads/2026/10/image.png 670w, https://askdba.net/wp-content/uploads/2026/10/image-300x153.png 300w, https://askdba.net/wp-content/uploads/2026/10/image-624x319.png 624w" sizes="(max-width: 670px) 100vw, 670px"></figure>



<p class="wp-block-paragraph">The app embeds only your query text; every movie’s vector already sits in MySQL, so a search is one SQL round trip. The movie vectors come with the dataset, made by <a href="https://huggingface.co/nomic-ai/nomic-embed-text-v1.5" target="_blank" rel="noopener">nomic-embed-text-v1.5</a> from each film’s title, tagline, and overview. The app uses the same model locally on the CPU, so there is no API key, and nothing leaves your machine.</p>



<p class="wp-block-paragraph">New rows need no rebuild. Adding a movie is a plain <code>INSERT</code>; MyVector’s binlog listener picks it up and adds it to the HNSW index, usually within a second.</p>



<h2 class="wp-block-heading">The SQL</h2>



<p class="wp-block-paragraph">The whole index is declared in a column comment. This is the <code>movies</code> table’s vector column:</p>



<pre class="wp-block-code"><code>embedding VARBINARY(3080) COMMENT
  'MYVECTOR COLUMN type=HNSW,dim=768,size=...,M=16,ef=100,dist=Cosine,online=Y,idcol=id,threads=N'</code></pre>



<p class="wp-block-paragraph">The loader inserts the rows, then builds the HNSW index with one call:</p>



<pre class="wp-block-code"><code>CALL mysql.myvector_index_build('movies.movies.embedding', 'id');</code></pre>



<p class="wp-block-paragraph">A search turns the query vector into a nearest-first list of ids with <code>myvector_ann_set()</code>, then joins back to the table. <code>JSON_TABLE</code> keeps the order:</p>



<pre class="wp-block-code"><code>SELECT m.title, myvector_distance(m.embedding, @q, 'Cosine') AS distance
FROM (SELECT myvector_ann_set('movies.movies.embedding', 'id', @q,
                              'nn=10,ef_search=100') AS js) src,
     JSON_TABLE(src.js, '$[*]' COLUMNS (rank_no FOR ORDINALITY, id INT PATH '$')) nn
JOIN movies.movies m ON m.id = nn.id
ORDER BY nn.rank_no;</code></pre>



<p class="wp-block-paragraph">“More like this” needs no embedding at all. It reads a film’s stored vector into the query variable:</p>



<pre class="wp-block-code"><code>SELECT embedding INTO @q FROM movies.movies WHERE id = @movie_id;</code></pre>



<p class="wp-block-paragraph">Filters pick one of two paths. The app counts each filter on its own index and takes the smallest count as an upper bound:</p>



<ul class="wp-block-list">
<li><strong>50,000 matches or fewer</strong>: it passes the matching keys as the fifth argument of <code>myvector_ann_set</code>, so HNSW searches only among them.</li>



<li><strong>More than that</strong>: it calls <code>MYVECTOR_ANN_FILTERED</code>, which takes the nearest candidates and keeps those that pass the filter.</li>
</ul>



<h2 class="wp-block-heading">Run it yourself</h2>



<p class="wp-block-paragraph">To run your own copy, from a clone of the repository:</p>



<pre class="wp-block-code"><code>cd examples/movie-finder
MOVIES=100k docker compose up      # then open http://localhost:8080</code></pre>



<p class="wp-block-paragraph">The first start downloads the TMDB data, about 7 GB, once. <code>MOVIES</code> picks how many films to load, most-voted first: <code>10k</code>, <code>100k</code> (the default) or <code>full</code>. Measured on a 16-core Arm (Neoverse-N1) Linux host with MySQL 9.7.2:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><th>Step</th><th>10k</th><th>100k</th><th>full (1,035,695)</th></tr><tr><td>Insert</td><td>7 s</td><td>41 s</td><td>407 s</td></tr><tr><td>Build the HNSW index (16 threads)</td><td>29 s</td><td>33 s</td><td>545 s</td></tr><tr><td>HNSW search</td><td>3–5 ms</td><td>4–7 ms</td><td>5–20 ms</td></tr><tr><td>Exact search (scans every row)</td><td>30 ms</td><td>280 ms</td><td>3 s warm</td></tr><tr><td>Recall@10, HNSW vs exact</td><td>100%</td><td>100%</td><td>100%</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">At a million movies, HNSW answers in 5–20 ms where a full scan takes 3 seconds, with the same top 10. Recall is for the unfiltered test query; a broad genre filter (Drama) at full size gave 80% at <code>ef_search</code> 100, and the page lets you raise <code>ef_search</code> to trade time for recall. For the full profile, give Docker about 12 GB of memory and set <code>MYSQL_BUFFER_POOL=6G</code>.</p>



<p class="wp-block-paragraph"><strong>Running it on a remote server?</strong> The page and MySQL listen on localhost only. Forward the port instead of opening it: <code>ssh -N -L 8080:127.0.0.1:8080 &lt;server&gt;</code>.</p>



<p class="wp-block-paragraph"><strong>Version note:</strong> the demo uses features newer than the v1.26.9 images: filtered search, <code>MYVECTOR_ANN_FILTERED</code>, per-query <code>ef_search</code>, and online updates that survive a restart. Until the next release ships, the <a target="_blank" rel="noopener" href="https://github.com/askdba/myvector/tree/main/examples/movie-finder">README</a> shows how to build a local image from <code>main</code>.</p>



<p class="wp-block-paragraph"><strong>Data:</strong> the movie metadata and posters come from <a target="_blank" rel="noopener" href="https://www.themoviedb.org/">TMDB</a> via a <a target="_blank" rel="noopener" href="https://huggingface.co/datasets/remsky/Embeddings__Ultimate_1Million_Movies_Dataset">Hugging Face mirror</a>. Your machine downloads it; it is not in the repository or any image. This is a non-commercial demo, not endorsed by TMDB.</p>



<h2 class="wp-block-heading">Try it and tell us</h2>



<p class="wp-block-paragraph">Try the <a href="https://demo.myvector.online/" target="_blank" rel="noopener">live demo</a> first. To run your own, start with <code>MOVIES=10k</code> for a quick first look, then go to <code>full</code> to watch a million rows answer in milliseconds. The <a href="https://myvector.online/DEMO/" target="_blank" rel="noopener">demo guide</a> has screenshots and the other demos; the <a href="https://github.com/askdba/myvector/tree/main/examples/movie-finder" target="_blank" rel="noopener">Movie Finder README</a> has the full details.</p>



<p class="wp-block-paragraph">If a query surprises you, or you want a feature the page doesn’t show yet, open an issue on <a target="_blank" rel="noopener" href="https://github.com/askdba/myvector">GitHub</a>. A star helps other MySQL users find the project.</p>



<p class="wp-block-paragraph"></p>]]></content:encoded>
    <pubDate>Mon, 05 Oct 2026 20:30:03 +0000</pubDate>
    <dc:creator>Alkin Tezuysal</dc:creator>
    <category>mysql</category>
    <category>Technical</category>
    <category>ai</category>
    <category>llm</category>
    <category>myvector</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 3</title>
    <guid isPermaLink="false">http://www.tusacentral.com/joomla/index.php/mysql-blogs/271-pgo-or-not-pgo-this-is-the-dilemma-step-3</guid>
    <link>http://www.tusacentral.com/joomla/index.php/mysql-blogs/271-pgo-or-not-pgo-this-is-the-dilemma-step-3</link>
    <description>Step 3: Why my sysbench-trained build loses and how to do it right.
Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

Three compounding reasons:

Uncovered code gets pessimized. Sysbench-tpcc touches a narrow slice of mysqld. Every function with zero counts is treated as cold: GCC optimizes it for size, skips inlining, and shoves it into cold sections. But at runtime I still execute plenty of code my training never touched, such as purge, flushing, stats recalculation, error paths, different optimizer plans, connection churn. All of that is now running de-optimized code, and I pay icache penalties every time hot code calls into &quot;cold&quot; regions. This is why GCC added -fprofile-partial-training, without it, a narrow profile actively hurts everything outside it.
High-concurrency instrumented runs produce corrupted or skewed profiles. GCC's profile counters are non-atomic by default. With 128–1024 threads hammering the same counters I get lost updates and internally inconsistent counts (I actually had &quot;profile count data file corrupted/inconsistent&quot; warnings at the -fprofile-use compile). I need to use -fprofile-update=atomic, which almost nobody sets and was at the beginning not aware of. In short my profile was garbage and a garbage profile is worse than no profile, consistent with my PGO build being slower than plain -O3 at 128 threads.
Instrumentation distorts what &quot;hot&quot; means under contention. The instrumented binary is 2–10x slower, which shifts where threads pile up. Spin loops in mutexes and rw-locks record enormous counts, so the compiler lavishes optimization on waiting code instead of useful work. While at high thread counts my real bottleneck is lock contention and memory latency things branch layout can't fix. 

Do I have a way to merge the different profiles like MTR + sysbench?The answer is yes. For GCC it's simple to do so, because the runtime automatically merges profile data across multiple training runs against the same instrumented binary. I don't need a separate merge step like Clang does.
What it does is that each time an instrumented binary exits, it writes its counters into files. If those files already exist (from a previous run), GCC's runtime adds the new counts to the existing ones rather than overwriting them. So if I run MTR first, then run sysbench against the same instrumented build with the same FPROFILE_DIR, the second run's counts accumulate on top of the first. The final profile reflects both workloads combined. 
Combining MTR with a moderate-thread sysbench run for PGO training is helpful because the two workloads cover different, complementary dimensions of mysqld's behavior: 

MTR sweeps broad functional breadth parser, optimizer, DDL, replication, error paths. However it runs almost entirely single-connection, so it never exercises the branches that only exist under real concurrency, like the contended slow-path of a latch, MVCC visibility checks against in-flight writers, lock-wait queuing, or redo-log group-commit batching;
A moderate-concurrency sysbench run (something like 4–64 threads, enough to create actual simultaneous access without descending into the timing-distortion and counter-corruption problems), fills exactly that gap by giving those concurrency-only branches nonzero execution counts, which keeps the compiler from treating them as cold, since cold code paths get optimized for size instead of speed.

So the merged profile ends up with both the wide code coverage MTR provides and the concurrent-path coverage MTR structurally can't, at the cost of only a modest, second-order improvement over MTR alone since PGO's overall gains are already small and this specific slice of the binary is a narrow fraction of total execution.
 
However even where it does help, I am stacking a small effect on top of a small effect. I have already found PGO vs non-PGO gives me under 5%. The incremental gain from better-covering a narrow slice of concurrency-only code within that is a second-order refinement. It is plausibly in the sub-1% range, quite  smaller than the run-to-run noise I had seen just from benchmark variance.
Or at least that is what I now think, let me validate it.Commands to build the code:
cmake ../mysql-9.7.2 \
-DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-instrument \
-DCMAKE_BUILD_TYPE=Release \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_PACKAGE_FLAGS=OFF \
-DCOMPILATION_COMMENT_SERVER=&quot;Marco compile 9.7.2-PGO instrument&quot; \
-DCOMPILATION_COMMENT=&quot;Marco compile 9.7.2 PGO instrument&quot; \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_FLAGS=&quot;-fuse-ld=lld&quot; \
-DCMAKE_CXX_FLAGS=&quot;-fuse-ld=lld&quot; \
-DFPROFILE_GENERATE=ON \
-DWITH_LTO=OFF \
-DFPROFILE_DIR=/opt/mysql_source/profile

Run the mtr:
perl mysql-test-run.pl --force --max-test-fail=0 --parallel=8 --suite=main,innodb,innodb_undo,binlog,rpl,perfschema,sys_vars
 
Then run the sysbench-tpcc test with 64 threads
sysbench /opt/tools/sysbench-tpcc/tpcc.lua --mysql-host=127.0.0.1 --mysql-port=3307  --mysql-user=app_test --mysql-password=test --mysql-db=tpcc --db-driver=mysql --tables=10 --scale=100 --rand-type=uniform --report-interval=1  --histogram --report_csv=yes  --stats_format=csv --db-ps-mode=disable --trx_level=RR --enable_purge=yes --time=600 --threads=64 --mysql-ssl=PREFERRED --mysql-ignore-errors=none  --reconnect=0  run
And got the profile as
[root@sm-blade03 profile]# llvm-profdata-20 show -detailed-summary /opt/mysql_source/profile/mysql.profdata
/opt/mysql_source/profile/mysql.profdata
Instrumentation level: IR entry_first = 0 instrument_loop_entries = 0
Total functions: 52403
Maximum function count: 289910820864
Maximum internal block count: 16881288068
Total number of blocks: 619050
Total count: 2473216025729
Detailed summary:
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 1% of the total counts.
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 10% of the total counts.
2 blocks (0.00%) with count &amp;gt;= 289910820864 account for 20% of the total counts.
11 blocks (0.00%) with count &amp;gt;= 16777125216 account for 30% of the total counts.
32 blocks (0.01%) with count &amp;gt;= 7806039615 account for 40% of the total counts.
77 blocks (0.01%) with count &amp;gt;= 3839409465 account for 50% of the total counts.
160 blocks (0.03%) with count &amp;gt;= 2273821592 account for 60% of the total counts.
312 blocks (0.05%) with count &amp;gt;= 1111890303 account for 70% of the total counts.
719 blocks (0.12%) with count &amp;gt;= 365989756 account for 80% of the total counts.
2165 blocks (0.35%) with count &amp;gt;= 85858392 account for 90% of the total counts.
4893 blocks (0.79%) with count &amp;gt;= 24359463 account for 95% of the total counts.
16760 blocks (2.71%) with count &amp;gt;= 2841200 account for 99% of the total counts.
45006 blocks (7.27%) with count &amp;gt;= 148881 account for 99.9% of the total counts.
96619 blocks (15.61%) with count &amp;gt;= 8640 account for 99.99% of the total counts.
157023 blocks (25.37%) with count &amp;gt;= 960 account for 99.999% of the total counts.
220001 blocks (35.54%) with count &amp;gt;= 91 account for 99.9999% of the total counts 

Checking on how counts concentrate: just 2 blocks account for 20% of all executed instructions across the entire training run (almost certainly a tight InnoDB buffer-pool/redo-log/lock_manager loop  counts in the hundreds of billions), while 90% of total execution volume is concentrated in only 2,165 blocks (0.35% of all covered blocks). 
That's the &quot;hot core&quot; PGO is designed to find and optimize aggressively. Meanwhile the long tail,  the other 96%+ of blocks, still has nonzero counts (this is a sparse profile, so anything appearing here was actually executed at least once).
Meaning a huge amount of MTR's functional-path breadth got captured even though it's numerically dwarfed by sysbench's tight hot loops.
This is the ideal shape for a merged profile: a small, extremely hot core (from sustained sysbench load) sitting on top of broad, low-frequency-but-nonzero coverage (from MTR's functional sweep). 
If MTR had contributed nothing, I would have a much flatter, narrower distribution with far fewer total functions covered. If sysbench had swamped everything with no MTR contribution, I would have a similar shape but with a much smaller &quot;Total functions&quot; number, since sysbench-tpcc only touches a fraction of mysqld's total surface.
Command to build the final optimized binaries:
llvm-profdata-20 merge -sparse /opt/mysql_source/profile/*.profraw -o /opt/mysql_source/profile/mysql.profdata

cmake ../mysql-9.7.2 \
 -DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-optimized-MTR-sysbench \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLED_LOCAL_INFILE=1 \
  -DWITH_FEDERATED_STORAGE_ENGINE=1 \
  -DWITH_ARCHIVE_STORAGE_ENGINE=1 \
  -DWITH_PACKAGE_FLAGS=OFF \
  -DCOMPILATION_COMMENT_SERVER=&quot;Marco compile 9.7.2-PGO optimized MTR+sysbench&quot; \
  -DCOMPILATION_COMMENT=&quot;Marco compile 9.7.2 PGO optimized MTR+sysbench&quot; \
  -DCMAKE_C_COMPILER=clang-20 \
  -DCMAKE_CXX_COMPILER=clang++-20 \
  -DFPROFILE_USE=ON \
  -DFPROFILE_DIR=/opt/mysql_source/profile/mysql.profdata \
  -DWITH_SSL=system -DWITH_ZLIB=system -DWITH_LZ4=system -DWITH_ICU=system \
  -DWITH_NUMA=ON -DWITH_LTO=ON -DWITH_LD=lld -DWITH_SYSTEMD=1 \
  -DWITH_UNIT_TESTS=OFF -DWITH_ROUTER=OFF -DMYSQL_MAINTAINER_MODE=OFF

Re-running the test I got this:

 
  
 
 
 
 
 
 
 
 
 
 
 
As expected the benefit I got was minimal, something was there but that disappeared while the concurrency increased.
 
Conclusions

Non-PGO vs PGO comparison: we had a 12% increase with low concurrency. But when simulating a more realistic load with higher contention the win becomes smaller and smaller, under 2%.
Training-workload choice mattered a lot: a sysbench-tpcc-trained PGO build ended up slower than a non-PGO.
After switching to a merged MTR + moderate-concurrency-sysbench training profile and re-running, the gain was still minimal, and it shrank further as concurrency increased.

 
Why the sysbench-only build lost
Sysbench-tpcc only exercises a narrow slice of mysqld, so everything outside that slice (purge, flushing, stats, error paths, alternate optimizer plans) gets pessimized as &quot;cold&quot; code.
At 128–1024 threads, GCC's non-atomic profile counters get corrupted under contention unless I explicitly set -fprofile-update=atomic,a garbage profile is worse than no profile at all.
Instrumentation overhead (2–10x slowdown) distorts what looks &quot;hot&quot; under load mutex/rwlock spin loops dominate the counts, so the compiler optimizes waiting code instead of real work, while the actual bottleneck (lock contention, memory latency) is something branch layout can't fix anyway.
Bottom line: PGO for MySQL/Percona Server binaries does work in the sense that the mechanism is sound whole-binary function reordering for something the size of mysqld is a legitimate, often the single biggest, win in PGO generally. 
But empirically here the payoff was consistently small (sub-5%, trending toward sub-1% for the concurrency-specific refinement), fragile to training-workload choice, fragile to build-flag correctness (atomic counters, -fprofile-partial-training), and it erodes further as thread count rises,  which is exactly what happens in production and what we care about most.
So my practical answer: it's not a clear &quot;yes, always compile with PGO.&quot; It's more a “maybe, and only if you get every detail right&quot;. Correct broad-coverage training data (MTR, ideally merged with moderate-concurrency sysbench), atomic profile counters, and realistic expectations that the win is marginal and shrinks under heavy concurrency. 
Get any of those wrong and you can end up worse than a plain build. Given the size of the benefit versus the number of ways to mess up the training methodology, PGO reads more like a niche optimization for a well-controlled build pipeline than a default you'd flip on broadly.
I would be more than happy to prove wrong and I am eager to get other people's feedback, so please test, test, test and let me know. 
 
Happy MySQL to everyone
Go to:
What is PGO
How PGO it works</description>
    <content:encoded><![CDATA[<h2><span>Step 3: Why my sysbench-trained build loses and how to do it right.</span><span><br></span></h2>
<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/269-pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></li>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></li>
<li aria-level="1"><span>Why my sysbench-trained build loses and how to do it right.</span></li>
</ol>
<h3><span>Three compounding reasons:</span></h3>
<ol>
<li aria-level="1"><i><span>Uncovered code gets pessimized.</span></i> <span><br></span><span>Sysbench-tpcc touches a narrow slice of mysqld. </span><span><br></span><span>Every function with zero counts is treated as cold: GCC optimizes it for size, skips inlining, and shoves it into cold sections. </span><span><br></span><span>But at runtime I still execute plenty of code my training never touched, such as purge, flushing, stats recalculation, error paths, different optimizer plans, connection churn. </span><span><br></span><span>All of that is now running de-optimized code, and I pay icache penalties every time hot code calls into "cold" regions. </span><span><br></span><span>This is why GCC added </span><span>-fprofile-partial-training</span><span>, without it, a narrow profile actively hurts everything outside it.</span></li>
<li aria-level="1"><i><span>High-concurrency instrumented runs produce corrupted or skewed profiles.</span></i> <span><br></span><span>GCC's profile counters are non-atomic by default. </span><span><br></span><span>With 128–1024 threads hammering the same counters I get lost updates and internally inconsistent counts (I actually had "profile count data file corrupted/inconsistent" warnings at the </span><span>-fprofile-use</span><span> compile). </span><span><br></span><span>I need to use </span><span>-fprofile-update=atomic</span><span>, which almost nobody sets and was at the beginning not aware of. </span><span><br></span><span>In short my profile was garbage and a garbage profile is worse than no profile, consistent with my PGO build being </span><i><span>slower</span></i><span> than plain </span><span>-O3</span><span> at 128 threads.</span></li>
<li aria-level="1"><i><span>Instrumentation distorts what "hot" means under contention.</span></i> <span><br></span><span>The instrumented binary is 2–10x slower, which shifts where threads pile up. Spin loops in mutexes and rw-locks record enormous counts, so the compiler lavishes optimization on </span><i><span>waiting</span></i><span> code instead of useful work. While at high thread counts my real bottleneck is lock contention and memory latency things branch layout can't fix. </span></li>
</ol>
<p><i><span>Do I have a way to merge the different profiles like MTR + sysbench?</span></i><span><br></span><span>The answer is yes. For GCC it's simple to do so, because the runtime automatically merges profile data across multiple training runs against the same instrumented binary. I don't need a separate merge step like Clang does.</span></p>
<p><span>What it does is that each time an instrumented binary exits, it writes its counters into files. If those files already exist (from a previous run), GCC's runtime </span><i><span>adds</span></i><span> the new counts to the existing ones rather than overwriting them. </span><span><br></span><span>So if I run MTR first, then run sysbench against the same instrumented build with the same </span><span>FPROFILE_DIR</span><span>, the second run's counts accumulate on top of the first. The final profile reflects both workloads combined. </span></p>
<p><span>Combining MTR with a moderate-thread sysbench run for PGO training is helpful because the two workloads cover different, complementary dimensions of mysqld's behavior: </span></p>
<ul>
<li aria-level="1"><span>MTR sweeps broad functional breadth parser, optimizer, DDL, replication, error paths. However it runs almost entirely single-connection, so it never exercises the branches that only exist under real concurrency, like the contended slow-path of a latch, MVCC visibility checks against in-flight writers, lock-wait queuing, or redo-log group-commit batching;</span></li>
<li aria-level="1"><span>A moderate-concurrency sysbench run (something like 4–64 threads, enough to create actual simultaneous access without descending into the timing-distortion and counter-corruption problems), fills exactly that gap by giving those concurrency-only branches nonzero execution counts, which keeps the compiler from treating them as cold, since cold code paths get optimized for size instead of speed.</span></li>
</ul>
<p><span>So the merged profile ends up with both the wide code coverage MTR provides and the concurrent-path coverage MTR structurally can't, at the cost of only a modest, second-order improvement over MTR alone since PGO's overall gains are already small and this specific slice of the binary is a narrow fraction of total execution.</span></p>
<p> </p>
<p><span>However even where it does help, I am stacking a small effect on top of a small effect. I have already found PGO vs non-PGO gives me under 5%. </span><span><br></span><span>The incremental gain from better-covering a narrow slice of concurrency-only code within that is a second-order refinement. </span><span><br></span><span>It is plausibly in the sub-1% range, quite  smaller than the run-to-run noise I had seen just from benchmark variance.</span></p>
<p><span>Or at least that is what I now think, let me validate it.</span><span><br></span><span><br></span><span>Commands to build the code:</span></p>
<pre class="lang:sh decode:true">cmake ../mysql-9.7.2 \
-DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-instrument \
-DCMAKE_BUILD_TYPE=Release \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_PACKAGE_FLAGS=OFF \
-DCOMPILATION_COMMENT_SERVER="Marco compile 9.7.2-PGO instrument" \
-DCOMPILATION_COMMENT="Marco compile 9.7.2 PGO instrument" \
-DCMAKE_C_COMPILER=clang-20 \
-DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_C_FLAGS="-fuse-ld=lld" \
-DCMAKE_CXX_FLAGS="-fuse-ld=lld" \
-DFPROFILE_GENERATE=ON \
-DWITH_LTO=OFF \
-DFPROFILE_DIR=/opt/mysql_source/profile
</pre>
<p><span>Run the mtr:</span><span><br></span></p>
<pre class="lang:default decode:true">perl mysql-test-run.pl --force --max-test-fail=0 --parallel=8 --suite=main,innodb,innodb_undo,binlog,rpl,perfschema,sys_vars</pre>
<p> </p>
<p><span>Then run the sysbench-tpcc test with 64 threads</span></p>
<pre class="lang:default decode:true">sysbench /opt/tools/sysbench-tpcc/tpcc.lua --mysql-host=127.0.0.1 --mysql-port=3307  --mysql-user=app_test --mysql-password=test --mysql-db=tpcc --db-driver=mysql --tables=10 --scale=100 --rand-type=uniform --report-interval=1  --histogram --report_csv=yes  --stats_format=csv --db-ps-mode=disable --trx_level=RR --enable_purge=yes --time=600 --threads=64 --mysql-ssl=PREFERRED --mysql-ignore-errors=none  --reconnect=0  run</pre>
<p><span>And got the profile as</span></p>
<pre class="lang:default decode:true">[root@sm-blade03 profile]# llvm-profdata-20 show -detailed-summary /opt/mysql_source/profile/mysql.profdata
/opt/mysql_source/profile/mysql.profdata
Instrumentation level: IR entry_first = 0 instrument_loop_entries = 0
Total functions: 52403
Maximum function count: 289910820864
Maximum internal block count: 16881288068
Total number of blocks: 619050
Total count: 2473216025729
Detailed summary:
2 blocks (0.00%) with count &gt;= 289910820864 account for 1% of the total counts.
2 blocks (0.00%) with count &gt;= 289910820864 account for 10% of the total counts.
2 blocks (0.00%) with count &gt;= 289910820864 account for 20% of the total counts.
11 blocks (0.00%) with count &gt;= 16777125216 account for 30% of the total counts.
32 blocks (0.01%) with count &gt;= 7806039615 account for 40% of the total counts.
77 blocks (0.01%) with count &gt;= 3839409465 account for 50% of the total counts.
160 blocks (0.03%) with count &gt;= 2273821592 account for 60% of the total counts.
312 blocks (0.05%) with count &gt;= 1111890303 account for 70% of the total counts.
719 blocks (0.12%) with count &gt;= 365989756 account for 80% of the total counts.
2165 blocks (0.35%) with count &gt;= 85858392 account for 90% of the total counts.
4893 blocks (0.79%) with count &gt;= 24359463 account for 95% of the total counts.
16760 blocks (2.71%) with count &gt;= 2841200 account for 99% of the total counts.
45006 blocks (7.27%) with count &gt;= 148881 account for 99.9% of the total counts.
96619 blocks (15.61%) with count &gt;= 8640 account for 99.99% of the total counts.
157023 blocks (25.37%) with count &gt;= 960 account for 99.999% of the total counts.
220001 blocks (35.54%) with count &gt;= 91 account for 99.9999% of the total counts 
</pre>
<p><span>Checking on how counts concentrate: just 2 blocks account for 20% of </span><i><span>all</span></i><span> executed instructions across the entire training run (almost certainly a tight InnoDB buffer-pool/redo-log/lock_manager loop  counts in the hundreds of billions), while 90% of total execution volume is concentrated in only 2,165 blocks (0.35% of all covered blocks). </span></p>
<p><span>That's the "hot core" PGO is designed to find and optimize aggressively. Meanwhile the long tail,  the other 96%+ of blocks, still has nonzero counts (this is a sparse profile, so anything appearing here was actually executed at least once).</span></p>
<p><span>Meaning a huge amount of MTR's functional-path breadth got captured even though it's numerically dwarfed by sysbench's tight hot loops.</span></p>
<p><span>This is the ideal shape for a merged profile: a small, extremely hot core (from sustained sysbench load) sitting on top of broad, low-frequency-but-nonzero coverage (from MTR's functional sweep). </span></p>
<p><span>If MTR had contributed nothing, I would have a much flatter, narrower distribution with far fewer total functions covered.</span><span><br></span><span> If sysbench had swamped everything with no MTR contribution, I would have a similar shape but with a much smaller "Total functions" number, since sysbench-tpcc only touches a fraction of mysqld's total surface.</span></p>
<p><span>Command to build the final optimized binaries:</span></p>
<pre class="lang:sh decode:true">llvm-profdata-20 merge -sparse /opt/mysql_source/profile/*.profraw -o /opt/mysql_source/profile/mysql.profdata

cmake ../mysql-9.7.2 \
 -DCMAKE_INSTALL_PREFIX=/opt/mysql_templates/mysql-9.7.2-PGO-optimized-MTR-sysbench \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLED_LOCAL_INFILE=1 \
  -DWITH_FEDERATED_STORAGE_ENGINE=1 \
  -DWITH_ARCHIVE_STORAGE_ENGINE=1 \
  -DWITH_PACKAGE_FLAGS=OFF \
  -DCOMPILATION_COMMENT_SERVER="Marco compile 9.7.2-PGO optimized MTR+sysbench" \
  -DCOMPILATION_COMMENT="Marco compile 9.7.2 PGO optimized MTR+sysbench" \
  -DCMAKE_C_COMPILER=clang-20 \
  -DCMAKE_CXX_COMPILER=clang++-20 \
  -DFPROFILE_USE=ON \
  -DFPROFILE_DIR=/opt/mysql_source/profile/mysql.profdata \
  -DWITH_SSL=system -DWITH_ZLIB=system -DWITH_LZ4=system -DWITH_ICU=system \
  -DWITH_NUMA=ON -DWITH_LTO=ON -DWITH_LD=lld -DWITH_SYSTEMD=1 \
  -DWITH_UNIT_TESTS=OFF -DWITH_ROUTER=OFF -DMYSQL_MAINTAINER_MODE=OFF
</pre>
<p><span>Re-running the test I got this:</span></p>
<p><a href="http://www.tusacentral.com/joomla/images/stories/pgo/pgo_not_pgo_mtr_vs_mtr_sysbench.png"><img src="http://www.tusacentral.com/joomla/images/stories/pgo/pgo_not_pgo_mtr_vs_mtr_sysbench.png" alt="" width="700" height="307" loading="lazy" data-path="local-images:/stories/pgo/pgo_not_pgo_mtr_vs_mtr_sysbench.png"></a></p>
<p> </p>
<p>  </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p><span>As expected the benefit I got was minimal, something was there but that disappeared while the concurrency increased.</span></p>
<p> </p>
<h2><span>Conclusions</span></h2>
<ul>
<li aria-level="1"><span>Non-PGO vs PGO comparison: we had a 12% increase with low concurrency. But when simulating a more realistic load with higher contention the win becomes smaller and smaller, under 2%.</span></li>
<li aria-level="1"><span>Training-workload choice mattered a lot: a sysbench-tpcc-trained PGO build ended up </span><i><span>slower</span></i><span> than a non-PGO.</span></li>
<li aria-level="1"><span>After switching to a merged MTR + moderate-concurrency-sysbench training profile and re-running, the gain was still minimal, and it shrank further as concurrency increased.</span></li>
</ul>
<p> </p>
<h3><span>Why the sysbench-only build lost</span></h3>
<p><span>Sysbench-tpcc only exercises a narrow slice of mysqld, so everything outside that slice (purge, flushing, stats, error paths, alternate optimizer plans) gets pessimized as "cold" code.</span></p>
<p><span>At 128–1024 threads, GCC's non-atomic profile counters get corrupted under contention unless I explicitly set </span><span>-fprofile-update=atomic,</span><span>a garbage profile is worse than no profile at all.</span></p>
<p><span>Instrumentation overhead (2–10x slowdown) distorts what looks "hot" under load mutex/rwlock spin loops dominate the counts, so the compiler optimizes waiting code instead of real work, while the actual bottleneck (lock contention, memory latency) is something branch layout can't fix anyway.</span></p>
<p><b>Bottom line:</b><span> PGO for MySQL/Percona Server binaries does work in the sense that the mechanism is sound whole-binary function reordering for something the size of mysqld is a legitimate, often the single biggest, win in PGO generally. </span></p>
<p><span>But empirically here the payoff was consistently small (sub-5%, trending toward sub-1% for the concurrency-specific refinement), fragile to training-workload choice, fragile to build-flag correctness (atomic counters, </span><span>-fprofile-partial-training</span><span>), and it erodes further as thread count rises,  which is exactly what happens in production and what we care about most.</span></p>
<p><span>So my practical answer: </span><b>it's not a clear "yes, always compile with PGO."</b><span> It's more a “</span><b>maybe, and only if you get every detail right</b><span>". Correct broad-coverage training data (MTR, ideally merged with moderate-concurrency sysbench), atomic profile counters, and realistic expectations that the win is marginal and shrinks under heavy concurrency. </span></p>
<p><span>Get any of those wrong and you can end up </span><i><span>worse</span></i><span> than a plain build. </span><span><br></span><span>Given the size of the benefit versus the number of ways to mess up the training methodology, PGO reads more like a niche optimization for a well-controlled build pipeline than a default you'd flip on broadly.</span></p>
<p><span>I would be more than happy to prove wrong and I am eager to get other people's feedback, so please test, test, test and let me know. </span></p>
<p> </p>
<p><span>Happy MySQL to everyone</span></p>
<p>Go to:</p>
<p><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/269-pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></p>
<p><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></p>]]></content:encoded>
    <pubDate>Mon, 05 Oct 2026 16:15:24 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>MySQL</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 2</title>
    <guid isPermaLink="false">http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2</guid>
    <link>http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2</link>
    <description>Step 2: How it works
Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

How PGO works: PGO is a two-pass build. First pass compiles with instrumentation (-fprofile-generate): every basic block and branch gets a counter. You run a training workload, counters are dumped to profraw files. Second pass recompiles using those counts to drive inlining decisions, branch layout (hot path falls through, cold path jumps away), hot/cold function splitting, code ordering for icache/iTLB locality, loop unrolling, and indirect-call promotion. Crucially, PGO is not &quot;make the trained workload fast&quot; it's &quot;tell the compiler which code is hot and which is cold, and let it reshape the whole binary accordingly.&quot;
 
But how does it work?
Phase 1: what actually gets recorded. The instrumented binary has counters injected at compile time one per edge in the control-flow graph, not just per function. So for every branch, every loop back-edge, every call site, there's a counter that increments each time execution takes that path. This is finer-grained than &quot;function X was called N times&quot; it's &quot;when we reached this branch, we went left 950,000 times and right 50 times.&quot; That per-edge granularity is what lets the second phase make surgical decisions rather than just &quot;function X is hot, function Y is cold.&quot;
Phase 2: what the compiler does with those counts. Several distinct transformations, all driven by the same counter data:
Inlining. Normally the compiler inlines based on static heuristics:

function size call-site count
estimated cost/benefit. 

With profile data it can override those heuristics: a call site executed millions of times gets inlined even if it looks &quot;too expensive&quot; by static cost rules, because the runtime benefit clearly outweighs the code-size cost. 
A call site that's technically inlinable but sits in dead-cold code gets left as a real call inlining it would only bloat the binary for no benefit.
Branch layout. Every &quot;if&quot; in our code compiles down to a branch instruction with two possible outcomes: 

continue straight to the next instruction
jump somewhere else. 

Continuing straight is basically free; the CPU is already fetching instructions in order, so there's no extra cost. Jumping is not free: the CPU has to guess in advance which way a branch will go so it can keep fetching ahead of time, and if it guesses wrong, it has to throw away the work it already queued up and start over from the right place.
That is the &quot;pipeline bubble,&quot; a small stall. So the &quot;straight through&quot; path is cheap and the &quot;jump elsewhere&quot; path carries a penalty when the guess is wrong. 
The compiler arranges the hot path as the fall-through and pushes the cold path out of line; literally relocated to a separate location in the binary, often into a .text.unlikely section. So an if (unlikely_error_condition) { ...rare handling... } block doesn't sit inline interrupting the hot path anymore; it is moved somewhere else entirely, and the hot path becomes a straight run of instructions with no diversion.
To be clear, reordering our if/else in the source code usually doesn't change how the compiler lays out the machine code. Optimizing compilers decide branch layout themselves based on either profile data (PGO) or static heuristics, not on which branch we happened to write first in the source. So swapping the order of our if blocks by hand generally has little to no effect on the compiled result.
Hot/cold function splitting. This is the same idea applied within a single function. A function might have a hot core loop and a rarely-hit error-handling tail. The compiler physically splits the function into two pieces: 

the hot part stays in .text.hot
the cold part moves to .text.unlikely. 

The function still works identically (a jump connects them when needed), but now the hot part is smaller and denser, so more of it fits in an instruction-cache line, and cold code that's almost never touched isn't wasting icache space sitting next to it.
Whole-binary function reordering.This is where &quot;reshape the whole binary&quot; becomes literal. At link time (especially with LTO, which MySQL's PGO build enables), functions get physically reordered in the final executable so that functions which call each other frequently, or execute in sequence during a hot workload, are placed near each other in memory. This maximizes instruction-cache and iTLB locality; the CPU's fetch unit is pulling in a tight cluster of hot functions instead of jumping all over a 100+MB binary. For something the size of mysqld, this is often the single biggest win, because normal builds place functions in whatever order the source files happen to be compiled, which has no relationship to runtime call patterns.
Indirect-call promotion. If profile data shows a virtual call or function-pointer call resolves to the same target the overwhelming majority of the time (common in C++ with vtables, e.g. a storage-engine interface with basically only InnoDB registered), the compiler inserts a guarded direct call: &quot;if target == this specific address, call it directly and skip the indirect jump; otherwise fall back to the indirect call.&quot; Direct calls are cheaper and more predictable for the branch predictor than an indirect jump through a table.
Register allocation and code density trade-offs. Hot code gets compiled favoring speed, more aggressive unrolling, more registers dedicated to hot-path values. Cold code, especially with -fprofile-partial-training and cold-path treatment, gets compiled favoring size, fewer registers, less unrolling because it barely executes. So runtime cost there is irrelevant but its footprint in the binary is not free (it still occupies disk/page-cache space and can evict hot lines from cache if placed carelessly, which is exactly why it gets segregated into .text.unlikely rather than just left unoptimized in place).
Switch/jump-table lowering. A switch statement with many cases can be compiled as a jump table (fast, O(1), but requires a full table load and indirect jump) or as a cascade of compares (slower per-case but better branch prediction if one case dominates). Profile data tells the compiler which case actually dominates in practice and picks accordingly.
Given the above,  &quot;reshape the whole binary&quot; is not metaphorical. The compiler is redrawing the physical layout of machine code in the executable: which instructions are adjacent to which, which code sections are hot and packed tightly versus cold and shoved to the side, which calls are direct versus indirect, and where the CPU's fetch/prediction effort gets spent. None of this requires the values processed during training to resemble production traffic. It only requires the shape of control flow. Which branches, functions, and paths are frequently exercised to resemble production traffic. That's the core reason MTR's broad-but-different-data coverage transfers well: it walks nearly every code path in mysqld even though the actual queries and data are nothing like a TPCC workload, and control-flow shape is exactly what PGO optimizes for.
 
Why my sysbench-trained build loses and how to do it right.</description>
    <content:encoded><![CDATA[<h2><span>Step 2: How it works</span></h2>
<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/269-pgo-or-not-pgo-this-is-the-dilemma-step-1"><span>What is PGO</span></a></li>
<li aria-level="1"><span>How PGO it works</span></li>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/271-pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></li>
</ol>
<p><b>How PGO works:</b><span> PGO is a two-pass build. First pass compiles with instrumentation (</span><i><span>-fprofile-generate</span></i><span>): every basic block and branch gets a counter. </span><span><br></span><span>You run a training workload, counters are dumped to </span><i><span>profraw </span></i><span>files. </span><span><br></span><span>Second pass recompiles using those counts to drive inlining decisions, branch layout (hot path falls through, cold path jumps away), hot/cold function splitting, code ordering for icache/iTLB locality, loop unrolling, and indirect-call promotion. </span><span><br></span><span>Crucially, PGO is not "make the trained workload fast" it's "tell the compiler which code is hot and which is cold, and let it reshape the whole binary accordingly."</span></p>
<p> </p>
<h3><span>But how does it work?</span></h3>
<p><b>Phase 1: what actually gets recorded.</b><span> The instrumented binary has counters injected at compile time one per edge in the control-flow graph, not just per function. </span><span><br></span><span>So for every branch, every loop back-edge, every call site, there's a counter that increments each time execution takes that path. </span><span><br></span><span>This is finer-grained than "function X was called N times" it's "when we reached this branch, we went left 950,000 times and right 50 times." </span><span><br></span><span>That per-edge granularity is what lets the second phase make surgical decisions rather than just "function X is hot, function Y is cold."</span></p>
<p><b>Phase 2: what the compiler does with those counts.</b><span> Several distinct transformations, all driven by the same counter data:</span></p>
<p><i><span>Inlining.</span></i> <span><br></span><span>Normally the compiler inlines based on static heuristics:</span></p>
<ul>
<li aria-level="1"><span>function size call-site count</span></li>
<li aria-level="1"><span>estimated cost/benefit. </span></li>
</ul>
<p><span>With profile data it can override those heuristics: a call site executed millions of times gets inlined even if it looks "too expensive" by static cost rules, because the runtime benefit clearly outweighs the code-size cost. </span></p>
<p><span>A call site that's technically inlinable but sits in dead-cold code gets left as a real call inlining it would only bloat the binary for no benefit.</span></p>
<p><i><span>Branch layout.</span></i> <span><br></span><span>Every "if" in our code compiles down to a branch instruction with two possible outcomes: </span></p>
<ul>
<li aria-level="1"><span>continue straight to the next instruction</span></li>
<li aria-level="1"><span>jump somewhere else. </span></li>
</ul>
<p><span>Continuing straight is basically free; the CPU is already fetching instructions in order, so there's no extra cost. </span><span><br></span><span>Jumping is not free: the CPU has to guess in advance which way a branch will go so it can keep fetching ahead of time, and if it guesses wrong, it has to throw away the work it already queued up and start over from the right place.</span></p>
<p><span>That is the "pipeline bubble," a small stall. So the "straight through" path is cheap and the "jump elsewhere" path carries a penalty when the guess is wrong. </span></p>
<p><span>The compiler arranges the hot path as the fall-through and pushes the cold path out of line; literally relocated to a separate location in the binary, often into a </span><span>.text.unlikely</span><span> section. </span><span><br></span><span>So an </span><span>if (unlikely_error_condition) { ...rare handling... }</span><span> block doesn't sit inline interrupting the hot path anymore; it is moved somewhere else entirely, and the hot path becomes a straight run of instructions with no diversion.</span></p>
<p><i><span>To be clear, reordering our if/else in the source code usually doesn't change how the compiler lays out the machine code. Optimizing compilers decide branch layout themselves based on either profile data (PGO) or static heuristics, not on which branch we happened to write first in the source. So swapping the order of our if blocks by hand generally has little to no effect on the compiled result.</span></i></p>
<p><i><span>Hot/cold function splitting.</span></i> <span><br></span><span>This is the same idea applied within a single function. </span><span><br></span><span>A function might have a hot core loop and a rarely-hit error-handling tail. </span><span><br></span><span>The compiler physically splits the function into two pieces: </span></p>
<ul>
<li aria-level="1"><span>the hot part stays in </span><span>.text.hot</span></li>
<li aria-level="1"><span>the cold part moves to </span><span>.text.unlikely</span><span>. </span></li>
</ul>
<p><span>The function still works identically (a jump connects them when needed), but now the hot part is smaller and denser, so more of it fits in an instruction-cache line, and cold code that's almost never touched isn't wasting icache space sitting next to it.</span></p>
<p><i><span>Whole-binary function reordering.</span></i><span><br></span><span>This is where "reshape the whole binary" becomes literal. </span><span><br></span><span>At link time (especially with LTO, which MySQL's PGO build enables), functions get physically reordered in the final executable so that functions which call each other frequently, or execute in sequence during a hot workload, are placed near each other in memory. </span><span><br></span><span>This maximizes instruction-cache and iTLB locality; the CPU's fetch unit is pulling in a tight cluster of hot functions instead of jumping all over a 100+MB binary. </span><span><br></span><span>For something the size of </span><span>mysqld</span><span>, </span><b>this is often the single biggest win</b><span>, because normal builds place functions in </span><b>whatever order the source files happen to be compiled,</b> <b>which has no relationship to runtime call patterns</b><span>.</span></p>
<p><i><span>Indirect-call promotion.</span></i> <span><br></span><span>If profile data shows a virtual call or function-pointer call resolves to the same target the overwhelming majority of the time (common in C++ with vtables, e.g. a storage-engine interface with basically only InnoDB registered), the compiler inserts a guarded direct call: "if target == this specific address, call it directly and skip the indirect jump; otherwise fall back to the indirect call." </span><span><br></span><span>Direct calls are cheaper and more predictable for the branch predictor than an indirect jump through a table.</span></p>
<p><i><span>Register allocation and code density trade-offs.</span></i> <span><br></span><span>Hot code gets compiled favoring speed, more aggressive unrolling, more registers dedicated to hot-path values. </span><span><br></span><span>Cold code, especially with </span><span>-fprofile-partial-training</span><span> and cold-path treatment, gets compiled favoring size, fewer registers, less unrolling because it barely executes. So runtime cost there is irrelevant but its footprint in the binary is not free (it still occupies disk/page-cache space and can evict hot lines from cache if placed carelessly, which is exactly why it gets segregated into </span><span>.text.unlikely</span><span> rather than just left unoptimized in place).</span></p>
<p><i><span>Switch/jump-table lowering.</span></i> <span><br></span><span>A switch statement with many cases can be compiled as a jump table (fast, O(1), but requires a full table load and indirect jump) or as a cascade of compares (slower per-case but better branch prediction if one case dominates). </span><span><br></span><span>Profile data tells the compiler which case actually dominates in practice and picks accordingly.</span></p>
<p><b>Given the above</b><span>,  "reshape the whole binary" is not metaphorical. The compiler is redrawing the physical layout of machine code in the executable: which instructions are adjacent to which, which code sections are hot and packed tightly versus cold and shoved to the side, which calls are direct versus indirect, and where the CPU's fetch/prediction effort gets spent. </span><span><br></span><span>None of this requires the </span><i><span>values</span></i><span> processed during training to resemble production traffic. It only requires the </span><i><span>shape</span></i><span> of control flow. </span><span><br></span><span>Which branches, functions, and paths are frequently exercised to resemble production traffic. </span><span><br></span><span>That's the core reason MTR's broad-but-different-data coverage transfers well: it walks nearly every code path in mysqld even though the actual queries and data are nothing like a TPCC workload, and control-flow shape is exactly what PGO optimizes for.</span></p>
<p> </p>
<p><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/271-pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></p>]]></content:encoded>
    <pubDate>Mon, 05 Oct 2026 16:13:01 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>MySQL</category>
  </item>

  <item>
    <title>PGO or not PGO this is the dilemma. Step 1</title>
    <guid isPermaLink="false">http://www.tusacentral.com/joomla/index.php/mysql-blogs/269-pgo-or-not-pgo-this-is-the-dilemma-step-1</guid>
    <link>http://www.tusacentral.com/joomla/index.php/mysql-blogs/269-pgo-or-not-pgo-this-is-the-dilemma-step-1</link>
    <description>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:

What is PGO
How PGO it works
Why my sysbench-trained build loses and how to do it right.

Step 1: What is PGO
PGO (Profile-Guided Optimization) is a two-pass compilation technique. 
First, we build the program with instrumentation that adds counters to every branch, loop, and call site. Next, we run it against a representative training workload so these counters can record which code paths are actually hot versus cold. We then recompile the program using that data to drive the compiler's decisions.
With this profiling data, the compiler inlines hot call sites more aggressively and lays out hot paths as straight-line, fall-through code while pushing cold paths out of the way. Finally, it physically reorders functions in the binary so frequently interacting hot code sits close together for better instruction-cache locality, and converts frequently resolved indirect calls into direct ones.The result isn't &quot;make the training run fast&quot; but rather &quot;reshape the entire binary's layout around real execution patterns,&quot; which is why the quality and representativeness of the training workload matters so much to whether PGO actually helps. 
It is nice to be wrong
Not so far ago I was wondering if having Percona Server compiled with PGO default is a good idea or not. Then I started to do some tests and I end up with this:
If I compare non PGO with PGO release I can see an optimization, minimal below 5% but is there.
If I compile MySQL using PGO and use as sample the recording of a specific test say sysbench-tpcc my compile will always be slower than my compile without PGO, no mater how many threads I use during recording, I tried from 128 to 1024.

 
 
 
 
 
 
 
 
 
 
 
 
 
Given my understanding of PGO was that it should be the other way around I was a bit disoriented. So I decided to read a bit and get a better understanding of what PGO really means/does and if it makes sense or not.
 
How PGO it works</description>
    <content:encoded><![CDATA[<p><span>Given the topic complexity and the length of this article I have split it in 3 three different blog-post:</span></p>
<ol>
<li aria-level="1"><span>What is PGO</span></li>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></li>
<li aria-level="1"><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/271-pgo-or-not-pgo-this-is-the-dilemma-step-3"><span>Why my sysbench-trained build loses and how to do it right.</span></a></li>
</ol>
<h2><span>Step 1: What is PGO</span></h2>
<p><span>PGO (Profile-Guided Optimization) is a two-pass compilation technique. </span></p>
<p><span>First, we build the program with instrumentation that adds counters to every branch, loop, and call site. </span><span><br></span><span>Next, we run it against a representative training workload so these counters can record which code paths are actually hot versus cold. </span><span><br></span><span>We then recompile the program using that data to drive the compiler's decisions.</span></p>
<p><span>With this profiling data, the compiler inlines hot call sites more aggressively and lays out hot paths as straight-line, fall-through code while pushing cold paths out of the way. </span><span><br></span><span>Finally, it physically reorders functions in the binary so frequently interacting hot code sits close together for better instruction-cache locality, and converts frequently resolved indirect calls into direct ones.</span><span><br></span><span><br></span><span>The result isn't "make the training run fast" but rather "reshape the entire binary's layout around real execution patterns," which is why the quality and representativeness of the training workload matters so much to whether PGO actually helps. </span></p>
<h2><span>It is nice to be wrong</span></h2>
<p><span>Not so far ago I was wondering if having Percona Server compiled with PGO default is a good idea or not. Then I started to do some tests and I end up with this:</span></p>
<p><i><span>If I compare non PGO with PGO release I can see an optimization, minimal below 5% but is there.</span></i></p>
<p><i><span>If I compile MySQL using PGO and use as sample the recording of a specific test say sysbench-tpcc my compile will always be slower than my compile without PGO, no mater how many threads I use during recording, I tried from 128 to 1024.</span></i></p>
<p><a href="http://www.tusacentral.com/joomla/images/stories/pgo/Percona_server_no_pg_vs_ps_marco_sysbench_pgo.png"><img src="http://www.tusacentral.com/joomla/images/stories/pgo/Percona_server_no_pg_vs_ps_marco_sysbench_pgo.png" alt="Percona server no pg vs ps marco sysbench pgo" width="700" height="307"></a></p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p><span>Given my understanding of PGO was that it should be the other way around I was a bit disoriented. So I decided to read a bit and get a better understanding of what PGO really means/does and if it makes sense or not.</span></p>
<p> </p>
<p><a href="http://www.tusacentral.com/joomla/index.php/mysql-blogs/270-pgo-or-not-pgo-this-is-the-dilemma-step-2"><span>How PGO it works</span></a></p>]]></content:encoded>
    <pubDate>Mon, 05 Oct 2026 16:07:41 +0000</pubDate>
    <dc:creator>Marco Tusa</dc:creator>
    <category>MySQL</category>
  </item>

  <item>
    <title>Thread Pool in Percona Server and MySQL (Part 1)</title>
    <guid isPermaLink="false">https://www.percona.com/?p=54362</guid>
    <link>https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-1/</link>
    <description>Thread Pool in Percona Server and MySQL (Part 1)
A new version of Community MySQL Server July release contains two versions 26.7.0 and 9.7.2. It is an important milestone because it finally brings a Thread Pooling feature to the community. The official MySQL Server Thread Pool plugin existed for a long time, but was available exclusively for MySQL Enterprise Edition.
Before MySQL 26.7.0 / 9.7.2 the most popular free thread pooling enabled alternatives were:

Percona Server for MySQL, which shipped Thread Pool functionality as a plugin
MariaDB with the Thread Pooling built into the server
Third-party plugins such as xiezhenye/mysql-plugin-threadpool available on GitHub

Now we will be able to see what benefits the thread pooling mechanism brings to MySQL Server and compare it with the implementation of the thread pool in Percona Server for MySQL.
Part 1 will explain the basics of thread pooling and analyze the efficiency of its implementation in Percona Server for MySQL. The performance comparison between Percona Server and MySQL Server is done in Part 2.
What is Thread Pooling and why is it needed?
Thread Pooling is a technique that reuses a fixed number of pre-created threads to handle multiple client connections and execute statements inside a database.
By default, MySQL creates a dedicated thread whenever a client connects, runs the queries, and then destroys the thread when finished. When connection counts grow very large, this default method causes performance to drop significantly because creating, destroying, and constantly switching between thousands of threads wastes system resources and causes contention.
This blog post from Vadim Tkachenko explains the working of thread pool, which gives an easy to understand similarity with the car traffic:
SimCity outages, traffic control and Thread Pool for MySQL
More information about it can be found in Percona Documentation.
WARNING: the post is several pages long because it looks into different aspects of thread pooling and compares the effect of various options. If you do not want to dive into the details, feel free to read the SPOILER below and proceed to the Conclusion section.
SPOILER: Thread Pool is not a universal database booster, which always brings performance up. Like every advanced tool it should be used with full understanding of the goals and objectives. Otherwise the results might be far from perfect.
How it works

Thread Groups: The pool divides threads into distinct groups that are assigned to a specific CPU core. For this reason the number of groups often matches the number of CPU cores.
Round-Robin Assignment: As client connections come in, they are distributed evenly across these groups.
Listening and Queuing: Inside each group, a listener thread monitors incoming queries and places them into either a high-priority queue (such as queries within an active transaction) or a normal/low-priority queue.
Execution and Reuse: Worker threads pick up queries from the queues (handling high-priority items first), execute them, and then wait to handle the next request instead of shutting down.

See also:

Core concepts and usage examples
Priority connection scheduling

Thread Pool configuration:
In this research we will be sweeping across the range of various parameters of the Thread Pool and see how the performance of the server is affected.
The base line will be the server configuration when the thread pool is not enabled. This is done by setting thread_handling=one-thread-per-connection. When the pooling is enabled (thread_handling=pool-of-threads) the benchmark will set different values for the following variables that control the thread pool behavior:



Name
Sweep range
Description


thread_pool_size
10 / 20 / 40 / 80 / 120 / 160
Number of thread groups in the pool.
NOTE: thread_pool_size=5 was also tested, but the performance was unsatisfactory, for that reason it is not included in the report.


thread_pool_max_threads /
thread_pool_max_active_query_threads
12000
Maximum number of threads in the pool.


thread_pool_oversubscribe /
thread_pool_query_threads_per_group
2 / 3 / 4
Number of threads that can be active at the same time within the same group.



 
Other Configuration and Methodology
The configuration was as follows:



Benchmark
Sysbench OLTP Read-Write


CPU
Intel Xeon Gold 6230 (2×20 cores, HT = 80 logical CPUs)


RAM
187 GiB DDR4


Storage
NVMe SSD (2.9 TB) INTEL SSDPE2KE032T8


OS
Ubuntu 24.04, kernel 6.8.0-60-generic


DB Engines
Percona Server for MySQL 9.7.1-1 (release build)
MySQL Server 26.7.0 (release build)



Additional dimensions for the benchmark:



Database Sizes (Row Number)
24Gb (100M rows)


Number of tables in DB Schema
20 (this number is constant for all runs)
Database Schema definition can be downloaded from here: 
https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql


Number of concurrent threads
40 / 80 / 120 / 160 / 320 / 640 / 1280 / 2560 / 5120


Buffer to Data Ratio
1:12 (I/O bound), 1:2 (Partially buffered), 1:1 (Fully buffered)



Execution of the benchmarks was done as follows:



Ramp-up
48G – 600 sec (10 min)
The Ramp-up times were established experimentally depending on the Data Size until the point when increasing them further did not bring significant changes.


Measurement window
900 sec (15 min)
Ideally it should be as long as possible, but measurements should take reasonable time. Hence, we used the experience of previous benchmarks and established that this window is adequate for the purpose.


Number of runs
1 
Could be more runs, but testing took a long time.



Important Database Configuration options (the actual config files with specific settings for each run can be downloaded from the interactive graphs):



InnoDB – Buffer pool Tier


innodb_buffer_pool_size
2G/12G/32G


innodb_buffer_pool_load_at_startup
OFF


innodb_buffer_pool_dump_at_shutdown
OFF


Threading


thread_stack
512K


thread_cache_size
256


back_log
4096


InnoDB I/O


innodb_io_capacity
10000


innodb_io_capacity_max
20000


innodb_read_io_threads
16


innodb_write_io_threads
16


innodb_use_native_aio
ON


InnoDB Log / Durability


innodb_log_buffer_size
256M


innodb_flush_log_at_trx_commit
1 # full ACID


innodb_doublewrite
ON


InnoDB – Concurrency &amp;amp; OLTP Tuning


innodb_stats_on_metadata
OFF


innodb_open_files
65536


innodb_lock_wait_timeout
50


innodb_rollback_on_timeout
ON


Per-Session Buffers


sort_buffer_size    
4M


join_buffer_size    
4M


read_buffer_size    
2M


read_rnd_buffer_size
4M


tmp_table_size      
256M


max_heap_table_size
256M


Binary Log


disable_log_bin
ON # Disabled binlog


Other InnoDB settings


innodb_redo_log_capacity    
4G


innodb_change_buffering     
none


innodb_flush_method         
O_DIRECT


innodb_buffer_pool_instances
Calculated as
(innodb_buffer_pool_size G / 5)
But must be in range [1..8]


Misc server settings


collation_server
utf8mb4_unicode_ci


bulk_insert_buffer_size
256M


myisam_sort_buffer_size 
128M


key_buffer_size         
64M # MyISAM only, keep small for OLTP



 
Results
The scope of results is broad and therefore the results presentation will be divided into the sections.
Percona Server 9.7.1-1 with and without thread pool:
The most impressive effect of the thread pool can be illustrated on the following graph where the level of oversubscription is 4 (os4 in the legend) and the number of thread groups is 10 (TP 10 in the legend).
 Graph 1.0 – best effect of thread pooling [ INTERACTIVE GRAPH ][ TABLE ]
The green line (thread_pool_size=10/thread_pool_oversubscribe=4) is closely followed by the blue line, which corresponds to the number of groups 20 and the oversubscription 2.
As the graph shows – the effect of the thread pool on the performance in the low thread count (40/80) is relatively small. A notable effect starts to show at 120 threads when non-pooled configuration TPS plunges down. It is good to keep in mind that the thread pooling mechanism is not a universal performance booster. Though, it helps to avoid system thrashing when the number of threads is much larger than CPU cores can process.
With 2560 connections the best configuration with thread pool shows excellent efficiency of 2304 TPS against 129 TPS without the thread pool, which is 17.9 times faster. Some thread-pooled configurations are more efficient than the other ones. The combination thread_pool_size=20 with thread_pool_oversubscribe=4 fails to prevent sharp TPS drop at 120 client connections, though its shape improves as the number of connections increases.The drop from the maximum of 2485 TPS at 640 connections to 2304 TPS at 2560 connections is visible (7.5%), but not critical.
Interesting that the optimal number of groups is smaller than the number of physical CPU cores (40).
Another important observation is that in this test increase of the number of thread groups causes the TPS performance to drop: the curve with thread_pool_size=20 is below thread_pool_size=10 and thread_pool_size=40/80/120/160 are even lower.
 Graph 1.1 – thread pool size variations [ INTERACTIVE GRAPH ][ TABLE ]
The next graph shows how TPS changes when we consider the optimal value for thread_pool_size=10, but change thread_pool_oversubscribe.
 Graph 2.0 – oversubscription variations  [ INTERACTIVE GRAPH ][ TABLE ]
With the best configuration (thread_pool_oversubscribe=4) having 2304 TPS and the least performing pooling configuration (thread_pool_oversubscribe=2) with 2043 TPS the speed difference is 11%. So, a non-optimal oversubscription value can incur a significant penalty.
The above two cases correspond to the I/O bound scenario when the data size (24G) is much larger than the server buffer size (2G).
Let us see how the buffer pool efficiency changes as innodb_buffer_pool_size increased to 12G, which allows buffering approximately half of the data set.
 Graph 3.0 – partially buffered data set (12G buffer)  [ INTERACTIVE GRAPH ][ TABLE ]
This time thread_pool_size=20 is the optimal value for the data size (24G) and the allocated server buffer size (12G). The non-optimal oversubscription values have a similar effect (look at INTERACTIVE GRAPH) on the performance as in Graph 2.0.
When the server configuration moves to the fully buffered data set the distribution of performance is changed:
 Graph 4.0 – fully buffered data set (32G buffer)  [ INTERACTIVE GRAPH ][ TABLE ]
The configuration without the thread pool wins in all number of client connections in this test. The largest deviation from the base line is observed around 80 to 640 connections, but then the performance converses to around 16K TPS for all configurations. The configuration with  thread_pool_size=10 is now the slowest one and larger thread pool sizes perform better.
Varying oversubscription did not have a notable effect with fully buffered data.
NOTE: It would be interesting to see if thread pool wins with 5K or 10K connections. We might do another research devoted to extremely high thread count, but this post describes the efficiency of the thread pool across the low (40) to the high (2560) number of client connection threads.
Despite not having superior performance with regards to TPS with the fully buffered data set, the thread pool is still beneficial with regards to predictability of the server behavior. What I mean by this is most of the clients (95%) connected to the server have their transactions executed quickly, but a minority (5%) have to wait much longer. The response latency for the unlucky 5% might be out of the acceptable range. The long waiting clients do not care about other clients whose data was quickly processed. Therefore, the raw TPS does not hold much value for them.
 Graph 5.0 – p95 latency  [ INTERACTIVE GRAPH ][ TABLE ]
Unlike TPS when the higher value is better, the p95 Latency should be kept as low as possible.
Up to 1280 client connections the p95 latency was grouping around roughly the same numbers. However, for 2560 connections it changed dramatically. The configuration with disabled thread pooling suddenly demonstrated much longer p95 than the pooled counterparts.
The 5% of clients will have to wait longer than 787 ms if the thread pool is not used. The pooled configurations keep a much tighter range of 235-297 ms. The system performs as well as its slowest/weakest link and if a client application performs several transactions it is more likely to end up waiting significantly longer. For some outliers the latency reached 2000 ms. 
 
Conclusion
Thread pooling does not bring any gains and even causes a small slow down on a low number of server connections.
The most visible effect of thread pooling can be seen in the scenario when I/O is one of the factors that limit performance.
Careful tuning of thread pool options is required to get the best performance. Even a seemingly small variation from optimal parameter values can cause large performance drops.
However, when properly configured, it can show spectacular results with almost 18 times TPS boost compared to the same server running without thread pool.
For system stability we should not only consider the raw TPS numbers, but also the latency related to processing of some transactions. Even if no TPS gain is obtained, the thread pool allows to minimize the waiting time for slowest transactions.
The post Thread Pool in Percona Server and MySQL (Part 1) appeared first on Percona.</description>
    <content:encoded><![CDATA[<h2><span>Thread Pool in Percona Server and MySQL (Part 1)</span></h2>
<p><span>A new version of Community MySQL Server July release contains two versions 26.7.0 and 9.7.2. It is an important milestone because it finally brings a Thread Pooling feature to the community. The official MySQL Server Thread Pool plugin existed for a long time, but was available exclusively for MySQL Enterprise Edition.</span></p>
<p><span>Before MySQL 26.7.0 / 9.7.2 the most popular free thread pooling enabled alternatives were:</span></p>
<ul>
<li aria-level="1"><span>Percona Server for MySQL, which shipped Thread Pool functionality as a plugin</span></li>
<li aria-level="1"><span>MariaDB with the Thread Pooling built into the server</span></li>
<li aria-level="1"><span>Third-party plugins such as xiezhenye/mysql-plugin-threadpool available on GitHub</span></li>
</ul>
<p><span>Now we will be able to see what benefits the thread pooling mechanism brings to MySQL Server and compare it with the implementation of the thread pool in Percona Server for MySQL.</span></p>
<p><span>Part 1 will explain the basics of thread pooling and analyze the efficiency of its implementation in Percona Server for MySQL. The performance comparison between Percona Server and MySQL Server is done in Part 2.</span></p>
<h2><span>What is Thread Pooling and why is it needed?</span></h2>
<p><span>Thread Pooling is a technique that reuses a fixed number of pre-created threads to handle multiple client connections and execute statements inside a database.</span></p>
<p><span>By default, MySQL creates a dedicated thread whenever a client connects, runs the queries, and then destroys the thread when finished. When connection counts grow very large, this default method causes performance to drop significantly because creating, destroying, and constantly switching between thousands of threads wastes system resources and causes contention.</span></p>
<p><span>This blog post from Vadim Tkachenko explains the working of thread pool, which gives an easy to understand similarity with the car traffic:</span></p>
<p><a href="https://www.percona.com/blog/simcity-outages-traffic-control-and-thread-pool-for-mysql/" target="_blank" rel="noopener"><span>SimCity outages, traffic control and Thread Pool for MySQL</span></a></p>
<p><span>More information about it can be found in </span><a href="https://docs.percona.com/percona-server/9.7/threadpool.html" target="_blank" rel="noopener"><span>Percona Documentation</span></a><span>.</span></p>
<p><span><b>WARNING:</b> the post is several pages long because it looks into different aspects of thread pooling and compares the effect of various options. If you do not want to dive into the details, feel free to read the <b>SPOILER</b> below and proceed to the Conclusion section.</span></p>
<p><span><b>SPOILER</b>: Thread Pool is not a universal database booster, which always brings performance up. Like every advanced tool it should be used with full understanding of the goals and objectives. Otherwise the results might be far from perfect.</span></p>
<h3><span>How it works</span></h3>
<ol>
<li aria-level="1"><b>Thread Groups:</b><span> The pool divides threads into distinct groups that are assigned to a specific CPU core. For this reason the number of groups often matches the number of CPU cores.</span></li>
<li aria-level="1"><b>Round-Robin Assignment:</b><span> As client connections come in, they are distributed evenly across these groups.</span></li>
<li aria-level="1"><b>Listening and Queuing:</b><span> Inside each group, a listener thread monitors incoming queries and places them into either a high-priority queue (such as queries within an active transaction) or a normal/low-priority queue.</span></li>
<li aria-level="1"><b>Execution and Reuse:</b><span> Worker threads pick up queries from the queues (handling high-priority items first), execute them, and then wait to handle the next request instead of shutting down.</span></li>
</ol>
<p><span>See also:</span></p>
<ul>
<li aria-level="1"><a href="https://docs.percona.com/percona-server/9.7/threadpool.html#core-concepts-and-usage-examples" target="_blank" rel="noopener"><span>Core concepts and usage examples</span></a></li>
<li aria-level="1"><a href="https://docs.percona.com/percona-server/9.7/threadpool.html#priority-connection-scheduling" target="_blank" rel="noopener"><span>Priority connection scheduling</span></a></li>
</ul>
<h2><span>Thread Pool configuration:</span></h2>
<p><span>In this research we will be sweeping across the range of various parameters of the Thread Pool and see how the performance of the server is affected.</span></p>
<p><span>The base line will be the server configuration when the thread pool is not enabled. This is done by setting <strong>thread_handling=one-thread-per-connection</strong>. When the pooling is enabled (<strong>thread_handling=pool-of-threads</strong>) the benchmark will set different values for the following variables that control the thread pool behavior:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr bgcolor="#DDDDDD">
<td><span><b>Name</b></span></td>
<td width="200"><span><b>Sweep range</b></span></td>
<td><span><b>Description</b></span></td>
</tr>
<tr>
<td><span><b>thread_pool_size</b></span></td>
<td><span>10 / 20 / 40 / 80 / 120 / 160</span></td>
<td><span>Number of thread groups in the pool.</span><br>
<span>NOTE: <b>thread_pool_size=5</b> was also tested, but the performance was unsatisfactory, for that reason it is not included in the report.</span></td>
</tr>
<tr>
<td><span><b>thread_pool_max_threads /</b><b><br>
</b><b>thread_pool_max_active_query_threads</b></span></td>
<td><span>12000</span></td>
<td><span>Maximum number of threads in the pool.</span></td>
</tr>
<tr>
<td><span><b>thread_pool_oversubscribe /</b><b><br>
</b><b>thread_pool_query_threads_per_group</b></span></td>
<td><span>2 / 3 / 4</span></td>
<td><span>Number of threads that can be active at the same time within the same group.</span></td>
</tr>
</tbody>
</table>
<p> </p>
<h2><span>Other Configuration and Methodology</span></h2>
<p><span>The configuration was as follows:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Benchmark</span></td>
<td><span>Sysbench OLTP Read-Write</span></td>
</tr>
<tr>
<td><span>CPU</span></td>
<td><span>Intel Xeon Gold 6230 (2×20 cores, HT = 80 logical CPUs)</span></td>
</tr>
<tr>
<td><span>RAM</span></td>
<td><span>187 GiB DDR4</span></td>
</tr>
<tr>
<td><span>Storage</span></td>
<td><span>NVMe SSD (2.9 TB) INTEL SSDPE2KE032T8</span></td>
</tr>
<tr>
<td><span>OS</span></td>
<td><span>Ubuntu 24.04, kernel 6.8.0-60-generic</span></td>
</tr>
<tr>
<td><span>DB Engines</span></td>
<td><span>Percona Server for MySQL 9.7.1-1 (release build)</span>
<p><span>MySQL Server 26.7.0 (release build)</span></p></td>
</tr>
</tbody>
</table>
<p><span>Additional dimensions for the benchmark:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Database Sizes (Row Number)</span></td>
<td><span>24Gb (100M rows)</span></td>
</tr>
<tr>
<td><span>Number of tables in DB Schema</span></td>
<td><span>20 (this number is constant for all runs)</span>
<p><span>Database Schema definition can be downloaded from here: </span></p>
<p><a href="https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql" target="_blank" rel="noopener"><span>https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql</span></a></p></td>
</tr>
<tr>
<td><span>Number of concurrent threads</span></td>
<td><span>40 / 80 / 120 / 160 / 320 / 640 / 1280 / 2560 / 5120</span></td>
</tr>
<tr>
<td><span>Buffer to Data Ratio</span></td>
<td><span>1:12 (I/O bound), 1:2 (Partially buffered), 1:1 (Fully buffered)</span></td>
</tr>
</tbody>
</table>
<p><span>Execution of the benchmarks was done as follows:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Ramp-up</span></td>
<td><span><strong>48G – 600 sec (10 min)</strong></span>
<p>The Ramp-up times were established experimentally depending on the Data Size until the point when increasing them further did not bring significant changes.</p></td>
</tr>
<tr>
<td>Measurement window</td>
<td><span><strong>900 sec (15 min)</strong></span>
<p>Ideally it should be as long as possible, but measurements should take reasonable time. Hence, we used the experience of previous benchmarks and established that this window is adequate for the purpose.</p></td>
</tr>
<tr>
<td>Number of runs</td>
<td><span><strong>1 </strong></span>
<p>Could be more runs, but testing took a long time.</p></td>
</tr>
</tbody>
</table>
<p><span>Important Database Configuration options (the actual config files with specific settings for each run can be downloaded from the interactive graphs):</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>InnoDB – Buffer pool Tier</b></span></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_size</span></td>
<td><b>2G/12G/32G</b></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_load_at_startup</span></td>
<td><span>OFF</span></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_dump_at_shutdown</span></td>
<td><span>OFF</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>Threading</b></span></td>
</tr>
<tr>
<td><span>thread_stack</span></td>
<td><span>512K</span></td>
</tr>
<tr>
<td><span>thread_cache_size</span></td>
<td><span>256</span></td>
</tr>
<tr>
<td><span>back_log</span></td>
<td><span>4096</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>InnoDB I/O</b></span></td>
</tr>
<tr>
<td><span>innodb_io_capacity</span></td>
<td><span>10000</span></td>
</tr>
<tr>
<td><span>innodb_io_capacity_max</span></td>
<td><span>20000</span></td>
</tr>
<tr>
<td><span>innodb_read_io_threads</span></td>
<td><span>16</span></td>
</tr>
<tr>
<td><span>innodb_write_io_threads</span></td>
<td><span>16</span></td>
</tr>
<tr>
<td><span>innodb_use_native_aio</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>InnoDB Log / Durability</b></span></td>
</tr>
<tr>
<td><span>innodb_log_buffer_size</span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>innodb_flush_log_at_trx_commit</span></td>
<td><span>1 # full ACID</span></td>
</tr>
<tr>
<td><span>innodb_doublewrite</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>InnoDB – Concurrency &amp; OLTP Tuning</b></span></td>
</tr>
<tr>
<td><span>innodb_stats_on_metadata</span></td>
<td><span>OFF</span></td>
</tr>
<tr>
<td><span>innodb_open_files</span></td>
<td><span>65536</span></td>
</tr>
<tr>
<td><span>innodb_lock_wait_timeout</span></td>
<td><span>50</span></td>
</tr>
<tr>
<td><span>innodb_rollback_on_timeout</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>Per-Session Buffers</b></span></td>
</tr>
<tr>
<td><span>sort_buffer_size    </span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>join_buffer_size    </span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>read_buffer_size    </span></td>
<td><span>2M</span></td>
</tr>
<tr>
<td><span>read_rnd_buffer_size</span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>tmp_table_size      </span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>max_heap_table_size</span></td>
<td><span>256M</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>Binary Log</b></span></td>
</tr>
<tr>
<td><span>disable_log_bin</span></td>
<td><span>ON # Disabled binlog</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>Other InnoDB settings</b></span></td>
</tr>
<tr>
<td><span>innodb_redo_log_capacity    </span></td>
<td><span>4G</span></td>
</tr>
<tr>
<td><span>innodb_change_buffering     </span></td>
<td><span>none</span></td>
</tr>
<tr>
<td><span>innodb_flush_method         </span></td>
<td><span>O_DIRECT</span></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_instances</span></td>
<td><b>Calculated as</b><b><br>
</b><b>(innodb_buffer_pool_size G / 5)</b><b><br>
</b><b>But must be in range [1..8]</b></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><span><b>Misc server settings</b></span></td>
</tr>
<tr>
<td><span>collation_server</span></td>
<td><span>utf8mb4_unicode_ci</span></td>
</tr>
<tr>
<td><span>bulk_insert_buffer_size</span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>myisam_sort_buffer_size </span></td>
<td><span>128M</span></td>
</tr>
<tr>
<td><span>key_buffer_size         </span></td>
<td><span>64M # MyISAM only, keep small for OLTP</span></td>
</tr>
</tbody>
</table>
<p> </p>
<h2><span>Results</span></h2>
<p><span>The scope of results is broad and therefore the results presentation will be divided into the sections.</span></p>
<h4><span>Percona Server 9.7.1-1 with and without thread pool:</span></h4>
<p><span>The most impressive effect of the thread pool can be illustrated on the following graph where the level of oversubscription is 4 (os4 in the legend) and the number of thread groups is 10 (TP 10 in the legend).</span></p>
<p><span><b> Graph 1.0 – best effect of thread pooling </b>[ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10%2C20&amp;os=2%2C4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10%2C20&amp;os=2%2C4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54382" src="https://www.percona.com/wp-content/uploads/2026/10/001.png" alt="Graph 1.1 - thread pool size variations" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/001.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/001-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/001-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/001-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>The green line (<b>thread_pool_size=10/thread_pool_oversubscribe=4</b>) is closely followed by the blue line, which corresponds to the number of groups 20 and the oversubscription 2.</span></p>
<p><span>As the graph shows – the effect of the thread pool on the performance in the low thread count (40/80) is relatively small. A notable effect starts to show at 120 threads when non-pooled configuration TPS plunges down. It is good to keep in mind that the thread pooling mechanism is not a universal performance booster. Though, it helps to avoid system thrashing when the number of threads is much larger than CPU cores can process.</span></p>
<p><span>With 2560 connections the best configuration with thread pool shows excellent efficiency of 2304 TPS against 129 TPS without the thread pool, which is 17.9 times faster. Some thread-pooled configurations are more efficient than the other ones. The combination <b>thread_pool_size=20</b> with <b>thread_pool_oversubscribe=4</b> fails to prevent sharp TPS drop at 120 client connections, though its shape improves as the number of connections increases.The drop from the maximum of 2485 TPS at 640 connections to 2304 TPS at 2560 connections is visible (7.5%), but not critical.</span></p>
<p><span>Interesting that the optimal number of groups is smaller than the number of physical CPU cores (40).</span></p>
<p><span>Another important observation is that in this test increase of the number of thread groups causes the TPS performance to drop: the curve with <b>thread_pool_size=20</b> is below <b>thread_pool_size=10</b> and <b>thread_pool_size=40/80/120/160</b> are even lower.</span></p>
<p><span><b> Graph 1.1 – thread pool size variations </b>[ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10%2C20%2C40%2C80%2C120%2C160&amp;os=4"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10%2C20%2C40%2C80%2C120%2C160&amp;os=4"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54383" src="https://www.percona.com/wp-content/uploads/2026/10/001-1.png" alt="Graph 1.1 - thread pool size variations" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/001-1.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/001-1-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/001-1-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/001-1-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>The next graph shows how TPS changes when we consider the optimal value for <b>thread_pool_size=10</b>, but change <b>thread_pool_oversubscribe</b>.</span></p>
<p><span><b> Graph 2.0 – oversubscription variations </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10&amp;os=all" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=2&amp;tp=off%2C10&amp;os=all" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone wp-image-54387" src="https://www.percona.com/wp-content/uploads/2026/10/002.png" alt="Graph 2.0 - oversubscription variations" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/002.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/002-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/002-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/002-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>With the best configuration (<b>thread_pool_oversubscribe=4</b>) having 2304 TPS and the least performing pooling configuration (<b>thread_pool_oversubscribe=2</b>) with 2043 TPS the speed difference is 11%. So, a non-optimal oversubscription value can incur a significant penalty.</span></p>
<p><span>The above two cases correspond to the I/O bound scenario when the data size (24G) is much larger than the server buffer size (2G).</span></p>
<p><span>Let us see how the buffer pool efficiency changes as <b>innodb_buffer_pool_size</b> increased to 12G, which allows buffering approximately half of the data set.</span></p>
<p><span><b> Graph 3.0 – partially buffered data set (12G buffer) </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=12&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=12&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54389" src="https://www.percona.com/wp-content/uploads/2026/10/003.png" alt="Graph 3.0 - partially buffered data set (12G buffer)" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/003.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/003-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/003-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/003-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>This time <b>thread_pool_size=20</b> is the optimal value for the data size (24G) and the allocated server buffer size (12G). The non-optimal oversubscription values have a similar effect (look at <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=12&amp;tp=20&amp;os=all" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a>) on the performance as in Graph 2.0.</span></p>
<p><span>When the server configuration moves to the fully buffered data set the distribution of performance is changed:</span></p>
<p><span><b> Graph 4.0 – fully buffered data set (32G buffer) </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=32&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=tps&amp;server=Percona-Server+9.7.1-1&amp;mem=32&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54390" src="https://www.percona.com/wp-content/uploads/2026/10/004.png" alt="Graph 4.0 - fully buffered data set (32G buffer)" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/004.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/004-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/004-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/004-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>The configuration without the thread pool wins in all number of client connections in this test. The largest deviation from the base line is observed around 80 to 640 connections, but then the performance converses to around 16K TPS for all configurations. The configuration with  <b>thread_pool_size=10</b> is now the slowest one and larger thread pool sizes perform better.</span></p>
<p><span>Varying oversubscription did not have a notable effect with fully buffered data.</span></p>
<p><span><strong>NOTE:</strong> It would be interesting to see if thread pool wins with 5K or 10K connections. We might do another research devoted to extremely high thread count, but this post describes the efficiency of the thread pool across the low (40) to the high (2560) number of client connection threads.</span></p>
<p><span>Despite not having superior performance with regards to TPS with the fully buffered data set, the thread pool is still beneficial with regards to predictability of the server behavior. What I mean by this is most of the clients (95%) connected to the server have their transactions executed quickly, but a minority (5%) have to wait much longer. The response latency for the unlucky 5% might be out of the acceptable range. The long waiting clients do not care about other clients whose data was quickly processed. Therefore, the raw TPS does not hold much value for them.</span></p>
<p><span> <b>Graph 5.0 – p95 latency </b> [ <a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=graph&amp;metric=lat95&amp;server=Percona-Server+9.7.1-1&amp;mem=32&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/thread-pool-benchmark/benchmark_report.html?display=table&amp;metric=lat95&amp;server=Percona-Server+9.7.1-1&amp;mem=32&amp;tp=all&amp;os=4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></span><img loading="lazy" decoding="async" class="alignnone size-full wp-image-54393" src="https://www.percona.com/wp-content/uploads/2026/10/005.png" alt="Graph 5.0 - p95 latency" width="1235" height="780" srcset="https://www.percona.com/wp-content/uploads/2026/10/005.png 1235w, https://www.percona.com/wp-content/uploads/2026/10/005-300x189.png 300w, https://www.percona.com/wp-content/uploads/2026/10/005-1024x647.png 1024w, https://www.percona.com/wp-content/uploads/2026/10/005-768x485.png 768w" sizes="auto, (max-width: 1235px) 100vw, 1235px"></p>
<p><span>Unlike TPS when the higher value is better, the p95 Latency should be kept as low as possible.</span></p>
<p><span>Up to 1280 client connections the p95 latency was grouping around roughly the same numbers. However, for 2560 connections it changed dramatically. The configuration with disabled thread pooling suddenly demonstrated much longer p95 than the pooled counterparts.</span></p>
<p><span>The 5% of clients will have to wait longer than 787 ms if the thread pool is not used. The pooled configurations keep a much tighter range of 235-297 ms. The system performs as well as its slowest/weakest link and if a client application performs several transactions it is more likely to end up waiting significantly longer. For some outliers the latency reached 2000 ms. </span></p>
<p> </p>
<h2><span>Conclusion</span></h2>
<p><span>Thread pooling does not bring any gains and even causes a small slow down on a low number of server connections.</span></p>
<p><span>The most visible effect of thread pooling can be seen in the scenario when I/O is one of the factors that limit performance.</span></p>
<p><span>Careful tuning of thread pool options is required to get the best performance. Even a seemingly small variation from optimal parameter values can cause large performance drops.</span></p>
<p><span>However, when properly configured, it can show spectacular results with almost 18 times TPS boost compared to the same server running without thread pool.</span></p>
<p><span>For system stability we should not only consider the raw TPS numbers, but also the latency related to processing of some transactions. Even if no TPS gain is obtained, the thread pool allows to minimize the waiting time for slowest transactions.</span></p>
<p>The post <a href="https://www.percona.com/blog/thread-pool-in-percona-server-and-mysql-part-1/">Thread Pool in Percona Server and MySQL (Part 1)</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 05 Oct 2026 10:48:57 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Benchmarks</category>
    <category>MySQL</category>
    <category>benchmark</category>
    <category>Percona Server for MySQL</category>
  </item>

  <item>
    <title>How Nutanix Database Service Handles Oracle Database Patching: An Out-of-Place, Automated, and Reversible Workflow</title>
    <guid isPermaLink="false">https://www.nutanix.com/blog/how-nutanix-database-service-handles-oracle-database-patching</guid>
    <link>https://www.nutanix.com/blog/how-nutanix-database-service-handles-oracle-database-patching</link>
    <description>Oracle patching carries operational risk. The traditional approach, manual in-place updates, leaves limited margin for error. One misstep in an Oracle software home can mean hours of unplanned downtime and a difficult root-cause conversation afterward.</description>
    <pubDate>Mon, 05 Oct 2026 10:18:00 +0000</pubDate>
    <dc:creator>Nutanix</dc:creator>
    <category>MOF - Interest</category>
    <category>Nutanix Database Service (NDB)</category>
    <category>Security</category>
    <category>Database-as-a-Service</category>
    <category>Brand</category>
  </item>

  <item>
    <title>MySQL Community Meetups Fall 2026</title>
    <guid isPermaLink="false">7ae026344ba0c21c34ecb629d6d3a430</guid>
    <link>https://blogs.oracle.com/mysql/mysql-community-meetups-fall-2026</link>
    <description>In our recent Where can you find MySQL from August through October 2026?, we shared a broad view of MySQL conferences, community activities, and user group meetups. Since then, community gatherings in Denver, Taipei, and Nashville have brought MySQL users, developers, and contributors together for technical discussion and local connection. We are continuing that momentum […]</description>
    <pubDate>Fri, 02 Oct 2026 10:22:00 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
  </item>

  <item>
    <title>Facebook Status Box using Node.js and MySQL</title>
    <guid isPermaLink="false">http://codeforgeek.com/?p=94</guid>
    <link>https://codeforgeek.com/facebook-status-box-using-node-js-expressjs-mysql/</link>
    <description>A status with angle brackets appears as text in the feed rather than turning into page markup. I appreciate that line breaks also stay visible, so a longer update reads more like what was typed. A submitted status gives you a concrete path into the feed. See how the app saves that update so the […]</description>
    <pubDate>Fri, 02 Oct 2026 00:21:29 +0000</pubDate>
    <dc:creator>Shahid shaikh</dc:creator>
    <category>Database</category>
    <category>Express tutorials</category>
    <category>Mysql</category>
    <category>Node Tutorials</category>
    <category>Tutorials</category>
  </item>

  <item>
    <title>Village News: MySQL News + Events (1 October 2026)</title>
    <guid isPermaLink="false">6abd536e7495d70001d83857</guid>
    <link>https://villagesql.com/blog/village-news-mysql-news-events-1-october-2026/</link>
    <description>Welcome back to Village News, our curated roundup of MySQL and database news.
If you want to get these updates, just subscribe to the blog.
Enjoy!
MySQL News
Note: Aggregated MySQL news can be found at Planet for MySQL Community and Planet MySQL (Oracle curated)
MySQL 26.10.0 Early Access Is Here
Oracle MySQL Group
TL;DR - A preview build of the next Innovation release is on MySQL Labs. The headline change is MySQL REST Service as a server component, which serves HTTPS endpoints without a separate MySQL Router. It is not for production.
Newsletter #1: What Happened in September 2026
Maria Nesterova, OurSQL Foundation
TL;DR - The Foundation ran its first booth at Percona Live Amsterdam, launched the OurSQL Universe map of the MySQL ecosystem, and scheduled webinars on row-level recovery (15 Oct) and ProxySQL (3 Nov).
Introducing the MySQL Analytical Replica (Parquet + DuckDB)
dbtrail Blog
TL;DR - An Apache 2.0 tool that follows the binlog, keeps a copy of your MySQL tables as Parquet files in local storage or S3, and lets you query them with DuckDB, so reporting queries stay off production.
Resolving query plan regressions after a MySQL engine upgrade
Chandana DA and Sravan Kumar Gogana, AWS Database Blog
TL;DR - A three-step workflow for RDS and Aurora MySQL upgrades: refresh statistics, compare EXPLAIN ANALYZE plans, then fix each regression with optimizer_switch flags, index hints or composite indexes.
MyVector v1.26.9: Keeping Up With MySQL Innovation
Alkin Tezuysal
TL;DR - The MyVector vector search plugin adds support for MySQL 26.7, the first Innovation release under Oracle's new calendar-based versioning. It also puts the Component architecture from v1.26.5 through heavier testing.
Open FIPS Build, Vectors, and Binlog Server: Marco Tusa at Percona Live
Percona Community
TL;DR - Notes from Marco Tusa's Percona Live Amsterdam keynote. FIPS support moves from the Pro Build to the standard build, vector search lands in Percona Server for MySQL, and an early binlog server is shown.
Amazon RDS for MySQL announces Extended Support minor versions 5.7.44-rds.20260902 and 8.0.46-rds.20260908
AWS What's New
TL;DR - New CVE and bug-fix builds for RDS customers still on MySQL 5.7 and 8.0 under paid Extended Support.
Join MySQL Public Discussion #6 and the November Contributor Summit
Oracle MySQL Group
TL;DR - Public Discussion #6 is on 22 October. The next MySQL Contributor Summit runs online on 4–5 November, continuing Oracle's series of open community sessions.
Access MySQL over MCP with vsql-mcp
VillageSQL
TL;DR - A Rust VillageSQL Server extension that runs a Model Context Protocol (MCP) server inside the database. It offers six tools for schema browsing, read-only queries, query plans and optional writes, with bearer-token auth, table allowlists and row limits.
Hardening EmergencyReparentShard in v25
Vitess
TL;DR - Vitess v25 speeds up emergency failover by filtering relay-log apply by received GTIDs. It also orders candidates consistently by GTID dominance and adds explicit split-brain recovery options.
How Atomic DDL Works in MySQL 8.0
Libing Song
TL;DR - An internals walkthrough of how MySQL 8.0's transactional data dictionary replaced .frm files. It shows how this closed the crash windows that used to leave server metadata out of step with InnoDB's own metadata and data.
PeakAIO contributes support of 8k API nodes to RonDB 26.02.11
Mikael Ronstrom
TL;DR - RonDB, the NDB Cluster fork, goes from 2,039 to 8,191 API nodes in 26.02.11. The change was contributed by PeakAIO, which uses RonDB as the metadata server for its pNFS Lattice file system.
Database News
MySQL BR Conf 2026 Slides: Managed ProxySQL in Your Own AWS Account
ProxySQL
TL;DR - Slides from René Cannaò and Alkin Tezuysal's São Paulo session on ProxySQL.Cloud: bring-your-own-cloud architecture, configuration and day-to-day operations. A recap of the conference is also up.
dbdeployer v2.4.2: Faster MariaDB Sandboxes, Better Cluster Checks, and Five Months of Progress
ProxySQL Blog
TL;DR - v2.4.2 removes a readiness wait that cuts MariaDB sandbox deployment from about eight minutes to 20 seconds and fixes Galera/PXC checks. Recent releases also added MariaDB downloads and PostgreSQL replication checks.
Lakebase Search: State-of-the-art full text and vector search for Postgres
Zhou Sun, Jinjing Zhou, Pranav Aurora et al., Databricks
TL;DR - Two Lakebase Postgres extensions are now GA on AWS and Azure. lakebase_vector does ANN search with RaBitQ-compressed IVF indexes, and lakebase_text replaces GIN with a BM25 index. Both can be fused in one SQL query for hybrid search.
MariaDB Foundation's Test Automation Framework (TAF) 4.0 Release
Jonathan Miller, MariaDB Foundation
TL;DR - The Foundation's performance test framework adds PostgreSQL alongside MariaDB and MySQL. It also generates database and test-case configuration at runtime and changes recovery mode to remove data skew.
For MariaDB, LLMs are the new search engines to win over
Richard Speed, The Register
TL;DR - MariaDB Foundation chairman Kaj Arnö says MariaDB's vector search is ready, but AI coding assistants rarely recommend it. More documentation and real-world use are needed to change that.
TideSQL 5 and MariaDB: the Tides Are Moving Fast
Frédéric Descamps, MariaDB Foundation
TL;DR - The TidesDB-backed storage engine reaches 5.0 with MVCC, secondary indexes, foreign keys, XA, partitioning and online DDL. It now installs from RPM/DEB packages via MariaDB Foundry, with no source build.
Online enabling checksums in PostgreSQL 19
Franck Pachot
TL;DR - PostgreSQL 19's pg_enable_data_checksums() turns on data checksums in the background without downtime. It needs superuser and two background workers, and a crash mid-run means starting over.
Anatomy of a (Postgres) search engine
Eric Ridge, PlanetScale
TL;DR - A walkthrough of inverted indexes: term dictionaries, postings lists and segment merging. It covers how PlanetScale's TIN extension handles MVCC visibility and mixes full-text constraints with ordinary SQL in Postgres. A follow-up, TIN Postgres search is faster, better, and cheaper, reruns a popular Postgres search demo on TIN.
Upcoming Database Events
Postgres Summit US (formerly PGConf.NYC)
September 30 – October 2, 2026 (PgUS)
New York City, NY
High Performance Transaction Systems (HPTS)
October 4-7, 2026
Asilomar Conference Grounds
Pacific Grove, CA
Open Source Summit Europe
October 7-9, 2026
Prague, Czechia
TiDB SCaiLE 2026
October 15, 2026
Computer History Museum
Mountain View, CA
All Things Open 2026
October 19-20, 2026
Raleigh Convention Center
Raleigh, NC
PGConf.EU
October 20–23, 2026 (PostgreSQL Europe)
Valencia, Spain
MySQL Public Discussion #6
October 22, 2026, 17:00–18:00 UTC
Online
Oracle AI World
October 25–28, 2026
Las Vegas, NV
MySQL Contributor Summit
November 4-5, 2026
Online
KubeCon + CloudNativeCon North America (VillageSQL is a sponsor)
November 9-12, 2026
Salt Lake City, Utah
AWS re:Invent 2026
November 30 – December 4, 2026
Las Vegas, NV
Open Source Summit Japan
December 7-9, 2026
Tokyo, Japan
CIDR 2027
January 24-27, 2027
Mövenpick Hotel Amsterdam City Centre
Amsterdam, The Netherlands
FOSDEM 2027
January 30-31, 2027
ULB Solbosch Campus
Brussels, Belgium
PGConf India 2027
March 2-5, 2027
Sheraton Grand Hotel at Brigade Gateway
Bengaluru, India
KubeCon + CloudNativeCon Europe 2027
March 15-18, 2027
Barcelona, Spain
Nordic PGDay 2027
March 16, 2027
Courtyard Kungsholmen
Stockholm, Sweden
SCaLE 24x
April 1-4, 2027
Pasadena Convention Center
Pasadena, CA
PGConf.dev 2027
May 11-14, 2027
Plaza Centre-Ville
Montréal, QC, Canada</description>
    <content:encoded><![CDATA[<img src="https://storage.ghost.io/c/db/c7/dbc78ca4-dcfe-468b-b2fa-030396d2a98e/content/images/2026/09/village-news-banner-1.jpg" alt="Village News: MySQL News + Events (1 October 2026)"><p>Welcome back to Village News, our curated roundup of MySQL and database news.</p>
<p>If you want to get these updates, just <a href="https://villagesql.com/blog/blog#/portal/signup">subscribe</a> to the blog.</p>
<p>Enjoy!</p>
<h2>MySQL News</h2>
<p><em>Note: Aggregated MySQL news can be found at <a href="https://planet.oursqlcommunity.org/?source=villagesql">Planet for MySQL Community</a> and <a href="https://planet.mysql.com/">Planet MySQL</a> (Oracle curated)</em></p>
<p><strong><a href="https://blogs.oracle.com/mysql/mysql-26-10-0-early-access-is-here">MySQL 26.10.0 Early Access Is Here</a></strong><br>
<em>Oracle MySQL Group</em></p>
<p><strong>TL;DR -</strong> A preview build of the next Innovation release is on MySQL Labs. The headline change is MySQL REST Service as a server component, which serves HTTPS endpoints without a separate MySQL Router. It is not for production.</p>
<p><strong><a href="https://oursqlfoundation.org/blog/newsletter-01-september-2026/">Newsletter #1: What Happened in September 2026</a></strong><br>
<em>Maria Nesterova, OurSQL Foundation</em></p>
<p><strong>TL;DR -</strong> The Foundation ran its first booth at Percona Live Amsterdam, launched the OurSQL Universe map of the MySQL ecosystem, and scheduled webinars on row-level recovery (15 Oct) and ProxySQL (3 Nov).</p>
<p><strong><a href="https://blog.dbtrail.com/introducing-mysql-analytical-replica/">Introducing the MySQL Analytical Replica (Parquet + DuckDB)</a></strong><br>
<em>dbtrail Blog</em></p>
<p><strong>TL;DR -</strong> An Apache 2.0 tool that follows the binlog, keeps a copy of your MySQL tables as Parquet files in local storage or S3, and lets you query them with DuckDB, so reporting queries stay off production.</p>
<p><strong><a href="https://aws.amazon.com/blogs/database/resolving-query-plan-regressions-after-a-mysql-engine-upgrade/">Resolving query plan regressions after a MySQL engine upgrade</a></strong><br>
<em>Chandana DA and Sravan Kumar Gogana, AWS Database Blog</em></p>
<p><strong>TL;DR -</strong> A three-step workflow for RDS and Aurora MySQL upgrades: refresh statistics, compare EXPLAIN ANALYZE plans, then fix each regression with optimizer_switch flags, index hints or composite indexes.</p>
<p><strong><a href="https://askdba.net/myvector-v1-26-9-keeping-up-with-mysql-innovation/">MyVector v1.26.9: Keeping Up With MySQL Innovation</a></strong><br>
<em>Alkin Tezuysal</em></p>
<p><strong>TL;DR -</strong> The MyVector vector search plugin adds support for MySQL 26.7, the first Innovation release under Oracle's new calendar-based versioning. It also puts the Component architecture from v1.26.5 through heavier testing.</p>
<p><strong><a href="https://percona.community/blog/2026/09/25/open-mysql-fips-build-percona-live/">Open FIPS Build, Vectors, and Binlog Server: Marco Tusa at Percona Live</a></strong><br>
<em>Percona Community</em></p>
<p><strong>TL;DR -</strong> Notes from Marco Tusa's Percona Live Amsterdam keynote. FIPS support moves from the Pro Build to the standard build, vector search lands in Percona Server for MySQL, and an early binlog server is shown.</p>
<p><strong><a href="https://aws.amazon.com/about-aws/whats-new/2026/09/amazon-rds-mysql-extended-support-minor-5744-8046-rds/">Amazon RDS for MySQL announces Extended Support minor versions 5.7.44-rds.20260902 and 8.0.46-rds.20260908</a></strong><br>
<em>AWS What's New</em></p>
<p><strong>TL;DR -</strong> New CVE and bug-fix builds for RDS customers still on MySQL 5.7 and 8.0 under paid Extended Support.</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">Join MySQL Public Discussion #6 and the November Contributor Summit</a></strong><br>
<em>Oracle MySQL Group</em></p>
<p><strong>TL;DR -</strong> Public Discussion #6 is on 22 October. The next MySQL Contributor Summit runs online on 4–5 November, continuing Oracle's series of open community sessions.</p>
<p><strong><a href="https://villagesql.com/blog/mcp/">Access MySQL over MCP with vsql-mcp</a></strong><br>
<em>VillageSQL</em></p>
<p><strong>TL;DR -</strong> A Rust VillageSQL Server extension that runs a Model Context Protocol (MCP) server inside the database. It offers six tools for schema browsing, read-only queries, query plans and optional writes, with bearer-token auth, table allowlists and row limits.</p>
<p><strong><a href="https://vitess.io/blog/2026-09-23-hardening-emergency-reparent-shard/">Hardening EmergencyReparentShard in v25</a></strong><br>
<em>Vitess</em></p>
<p><strong>TL;DR -</strong> Vitess v25 speeds up emergency failover by filtering relay-log apply by received GTIDs. It also orders candidates consistently by GTID dominance and adds explicit split-brain recovery options.</p>
<p><strong><a href="https://songlibing.github.io/posts/mysql-atomic-ddl-en/">How Atomic DDL Works in MySQL 8.0</a></strong><br>
<em>Libing Song</em></p>
<p><strong>TL;DR -</strong> An internals walkthrough of how MySQL 8.0's transactional data dictionary replaced .frm files. It shows how this closed the crash windows that used to leave server metadata out of step with InnoDB's own metadata and data.</p>
<p><strong><a href="https://mikaelronstrom.blogspot.com/2026/09/peakaio-contributes-support-of-8k-api.html">PeakAIO contributes support of 8k API nodes to RonDB 26.02.11</a></strong><br>
<em>Mikael Ronstrom</em></p>
<p><strong>TL;DR -</strong> RonDB, the NDB Cluster fork, goes from 2,039 to 8,191 API nodes in 26.02.11. The change was contributed by PeakAIO, which uses RonDB as the metadata server for its pNFS Lattice file system.</p>
<h2>Database News</h2>
<p><strong><a href="https://proxysql.com/blog/mysql-br-conf-2026-managed-proxysql-slides/">MySQL BR Conf 2026 Slides: Managed ProxySQL in Your Own AWS Account</a></strong><br>
<em>ProxySQL</em></p>
<p><strong>TL;DR -</strong> Slides from René Cannaò and Alkin Tezuysal's São Paulo session on ProxySQL.Cloud: bring-your-own-cloud architecture, configuration and day-to-day operations. A <a href="https://proxysql.com/blog/mysql-br-conf-2026-recap/">recap of the conference</a> is also up.</p>
<p><strong><a href="https://proxysql.com/blog/dbdeployer-2-4-2/">dbdeployer v2.4.2: Faster MariaDB Sandboxes, Better Cluster Checks, and Five Months of Progress</a></strong><br>
<em>ProxySQL Blog</em></p>
<p><strong>TL;DR -</strong> v2.4.2 removes a readiness wait that cuts MariaDB sandbox deployment from about eight minutes to 20 seconds and fixes Galera/PXC checks. Recent releases also added MariaDB downloads and PostgreSQL replication checks.</p>
<p><strong><a href="https://www.databricks.com/blog/lakebase-search-state-art-full-text-and-vector-search-postgres">Lakebase Search: State-of-the-art full text and vector search for Postgres</a></strong><br>
<em>Zhou Sun, Jinjing Zhou, Pranav Aurora et al., Databricks</em></p>
<p><strong>TL;DR -</strong> Two Lakebase Postgres extensions are now GA on AWS and Azure. lakebase_vector does ANN search with RaBitQ-compressed IVF indexes, and lakebase_text replaces GIN with a BM25 index. Both can be fused in one SQL query for hybrid search.</p>
<p><strong><a href="https://mariadb.org/mariadb-foundations-test-automation-framework-taf-4-0-release/">MariaDB Foundation's Test Automation Framework (TAF) 4.0 Release</a></strong><br>
<em>Jonathan Miller, MariaDB Foundation</em></p>
<p><strong>TL;DR -</strong> The Foundation's performance test framework adds PostgreSQL alongside MariaDB and MySQL. It also generates database and test-case configuration at runtime and changes recovery mode to remove data skew.</p>
<p><strong><a href="https://www.theregister.com/databases/2026/09/24/for-mariadb-llms-are-the-new-search-engines-to-win-over/5298104">For MariaDB, LLMs are the new search engines to win over</a></strong><br>
<em>Richard Speed, The Register</em></p>
<p><strong>TL;DR -</strong> MariaDB Foundation chairman Kaj Arnö says MariaDB's vector search is ready, but AI coding assistants rarely recommend it. More documentation and real-world use are needed to change that.</p>
<p><strong><a href="https://mariadb.org/tidesql-5-and-mariadb-the-tides-are-moving-fast/">TideSQL 5 and MariaDB: the Tides Are Moving Fast</a></strong><br>
<em>Frédéric Descamps, MariaDB Foundation</em></p>
<p><strong>TL;DR -</strong> The TidesDB-backed storage engine reaches 5.0 with MVCC, secondary indexes, foreign keys, XA, partitioning and online DDL. It now installs from RPM/DEB packages via MariaDB Foundry, with no source build.</p>
<p><strong><a href="https://dev.to/franckpachot/online-enabling-checksums-in-postgresql-19-2agd">Online enabling checksums in PostgreSQL 19</a></strong><br>
<em>Franck Pachot</em></p>
<p><strong>TL;DR -</strong> PostgreSQL 19's <code>pg_enable_data_checksums()</code> turns on data checksums in the background without downtime. It needs superuser and two background workers, and a crash mid-run means starting over.</p>
<p><strong><a href="https://planetscale.com/blog/anatomy-of-a-postgres-search-engine">Anatomy of a (Postgres) search engine</a></strong><br>
<em>Eric Ridge, PlanetScale</em></p>
<p><strong>TL;DR -</strong> A walkthrough of inverted indexes: term dictionaries, postings lists and segment merging. It covers how PlanetScale's TIN extension handles MVCC visibility and mixes full-text constraints with ordinary SQL in Postgres. A follow-up, <a href="https://planetscale.com/blog/searching-hn-with-tin">TIN Postgres search is faster, better, and cheaper</a>, reruns a popular Postgres search demo on TIN.</p>
<h2>Upcoming Database Events</h2>
<p><strong><a href="https://2026.postgressummit.us/">Postgres Summit US (formerly PGConf.NYC)</a></strong><br>
September 30 – October 2, 2026 (PgUS)<br>
New York City, NY</p>
<p><strong><a href="https://hpts.ws/">High Performance Transaction Systems (HPTS)</a></strong><br>
October 4-7, 2026<br>
Asilomar Conference Grounds<br>
Pacific Grove, CA</p>
<p><strong><a href="https://events.linuxfoundation.org/open-source-summit-europe/">Open Source Summit Europe</a></strong><br>
October 7-9, 2026<br>
Prague, Czechia</p>
<p><strong><a href="https://www.pingcap.com/tidb-scaile-summit/">TiDB SCaiLE 2026</a></strong><br>
October 15, 2026<br>
Computer History Museum<br>
Mountain View, CA</p>
<p><strong><a href="https://2026.allthingsopen.org/">All Things Open 2026</a></strong><br>
October 19-20, 2026<br>
Raleigh Convention Center<br>
Raleigh, NC</p>
<p><strong><a href="http://pgconf.eu/">PGConf.EU</a></strong><br>
October 20–23, 2026 (PostgreSQL Europe)<br>
Valencia, Spain</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Public Discussion #6</a></strong><br>
October 22, 2026, 17:00–18:00 UTC<br>
Online</p>
<p><strong><a href="https://www.oracle.com/ai-world/">Oracle AI World</a></strong><br>
October 25–28, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://blogs.oracle.com/mysql/join-mysql-public-discussion-6-and-the-november-contributor-summit">MySQL Contributor Summit</a></strong><br>
November 4-5, 2026<br>
Online</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-north-america/">KubeCon + CloudNativeCon North America</a></strong> (<strong>VillageSQL is a sponsor</strong>)<br>
November 9-12, 2026<br>
Salt Lake City, Utah</p>
<p><strong><a href="https://aws.amazon.com/events/reinvent">AWS re:Invent 2026</a></strong><br>
November 30 – December 4, 2026<br>
Las Vegas, NV</p>
<p><strong><a href="https://events.linuxfoundation.org/open-source-summit-japan/">Open Source Summit Japan</a></strong><br>
December 7-9, 2026<br>
Tokyo, Japan</p>
<p><strong><a href="https://www.cidrdb.org/cidr2027/">CIDR 2027</a></strong><br>
January 24-27, 2027<br>
Mövenpick Hotel Amsterdam City Centre<br>
Amsterdam, The Netherlands</p>
<p><strong><a href="https://fosdem.org/2027/">FOSDEM 2027</a></strong><br>
January 30-31, 2027<br>
ULB Solbosch Campus<br>
Brussels, Belgium</p>
<p><strong><a href="https://pgconf.in/">PGConf India 2027</a></strong><br>
March 2-5, 2027<br>
Sheraton Grand Hotel at Brigade Gateway<br>
Bengaluru, India</p>
<p><strong><a href="https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/">KubeCon + CloudNativeCon Europe 2027</a></strong><br>
March 15-18, 2027<br>
Barcelona, Spain</p>
<p><strong><a href="https://2027.nordicpgday.org/">Nordic PGDay 2027</a></strong><br>
March 16, 2027<br>
Courtyard Kungsholmen<br>
Stockholm, Sweden</p>
<p><strong><a href="https://www.socallinuxexpo.org/scale/24x">SCaLE 24x</a></strong><br>
April 1-4, 2027<br>
Pasadena Convention Center<br>
Pasadena, CA</p>
<p><strong><a href="https://2027.pgconf.dev/">PGConf.dev 2027</a></strong><br>
May 11-14, 2027<br>
Plaza Centre-Ville<br>
Montréal, QC, Canada</p>]]></content:encoded>
    <pubDate>Thu, 01 Oct 2026 13:00:31 +0000</pubDate>
    <dc:creator>VillageSQL</dc:creator>
  </item>

  <item>
    <title>MySQL HeatWave Core Capabilities (September 2026)</title>
    <guid isPermaLink="false">https://dasini.net/blog/?p=8978</guid>
    <link>https://dasini.net/blog/2026/09/30/mysql-heatwave-core-capabilities-september-2026/</link>
    <description>OCI Open Source Service Update – Sept. 2026
Explore how MySQL HeatWave on Oracle Cloud Infrastructure helps optimize, protect, and scale MySQL applications.
Covers Autopilot Indexing, high availability with automatic failover, managed read replicas, enterprise security, and fast DB system cloning for development, testing, and regional recovery.
The post MySQL HeatWave Core Capabilities (September 2026) first appeared on Data Daz (dasini.net) - Data Systems, AI, and Real-World Insights.</description>
    <content:encoded><![CDATA[<p>OCI Open Source Service Update – Sept. 2026<br>
Explore how MySQL HeatWave on Oracle Cloud Infrastructure helps optimize, protect, and scale MySQL applications.<br>
Covers Autopilot Indexing, high availability with automatic failover, managed read replicas, enterprise security, and fast DB system cloning for development, testing, and regional recovery.</p>
The post <a href="https://dasini.net/blog/2026/09/30/mysql-heatwave-core-capabilities-september-2026/">MySQL HeatWave Core Capabilities (September 2026)</a> first appeared on <a href="https://dasini.net/blog">Data Daz (dasini.net) - Data Systems, AI, and Real-World Insights</a>.]]></content:encoded>
    <pubDate>Wed, 30 Sep 2026 14:28:19 +0000</pubDate>
    <dc:creator>Olivier Dasini</dc:creator>
    <category>HeatWave</category>
    <category>MDS</category>
    <category>MySQL</category>
    <category>Cloud</category>
    <category>Heatwave</category>
    <category>MySQL Enterprise</category>
  </item>

  <item>
    <title>MySQL BR Conf 2026 Slides: Managed ProxySQL in Your Own AWS Account</title>
    <guid isPermaLink="false">https://proxysql.com/blog/mysql-br-conf-2026-managed-proxysql-slides/</guid>
    <link>https://proxysql.com/blog/mysql-br-conf-2026-managed-proxysql-slides/</link>
    <description>The final slides from René Cannaò and Alkin Tezuysal's session on ProxySQL.Cloud, BYOC architecture, configuration, and day-to-day operations.</description>
    <pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate>
    <dc:creator>ProxySQL</dc:creator>
  </item>

  <item>
    <title>ProxySQL at MySQL BR Conf 2026: Together in São Paulo</title>
    <guid isPermaLink="false">https://proxysql.com/blog/mysql-br-conf-2026-recap/</guid>
    <link>https://proxysql.com/blog/mysql-br-conf-2026-recap/</link>
    <description>René Cannaò and Alkin Tezuysal presented managed ProxySQL in your own AWS account at MySQL BR Conf 2026 in São Paulo.</description>
    <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
    <dc:creator>ProxySQL</dc:creator>
  </item>

  <item>
    <title>When pt-online-schema-change “where” Meets Galera: Understanding Chunk Auto-Resize and Flow Control</title>
    <guid isPermaLink="false">https://www.percona.com/?p=53966</guid>
    <link>https://www.percona.com/blog/when-pt-online-schema-change-where-meets-galera-understanding-chunk-auto-resize-and-flow-control/</link>
    <description>Percona Toolkit’s pt-online-schema-change (pt-osc) has long been the preferred solution for performing online schema changes with minimal downtime. Its chunk-based copy algorithm is designed to adapt dynamically to the workload, making it suitable for very large tables in production environments.
However, under specific conditions, one of its optimization mechanisms can become counterproductive.
During a customer engagement involving Percona XtraDB Cluster (PXC), we investigated a case where running pt-online-schema-change with a selective –where clause caused severe Galera Flow Control and general overhead, effectively stalling the cluster for an extended period. The issue was not caused by the schema change itself, but by the interaction between:

adaptive chunk auto-resizing
highly selective copying of the newest rows
clustered replication in Galera
InnoDB checkpointing

This article explains why it happens and proposes a set of practical mitigations that eliminate the root cause.
 
The Scenario
Consider a large table containing hundreds of millions of rows.
Instead of rebuilding the entire table, we only want to copy a subset of rows and apply a schema change like adding a new index.
The command looks similar to:pt-online-schema-change \
--alter &quot;ADD INDEX idx_status(status)&quot; \
--where &quot;created_at &amp;amp;gt;= NOW() - INTERVAL 30 DAY&quot; \
D=test,t=events \
--execute --forceThis is a common approach when:

historical data can be ignored
only active records require modification
performing staged migrations
reducing migration time

At first glance this seems perfectly reasonable.
Unfortunately, it exposes an interesting corner case in pt-online-schema-change.
 
How Chunk Auto-Resize Works
During the copy phase, pt-osc continuously adjusts the chunk size in order to keep each chunk copy close to the configured execution time (–chunk-time), 0.5 seconds by default.
Simplified, the algorithm works like this:

if chunks execute too quickly, increase the next chunk size
if chunks become slower, decrease the size
repeat until reaching a stable value

For ordinary table scans this works remarkably well because row density is relatively uniform.
The problem appears when most scanned rows are discarded by –where.
 
The Hidden Effect of a Selective WHERE Clause
Imagine a table like this with 1 billion rows:



Rows
Match WHERE


990 million
No


10 million
Yes



Suppose the matching rows are located at the end of the clustered index — and that was exactly the case here. Keep in mind that chunk selection always relies on the Primary Key boundaries starting from minimum value, but in this case those boundaries were combined (AND) with the condition passed via –where. Since the created_at values increased in step with the Primary Key, all the matching rows ended up clustered at the end of the index.
For a long time pt-osc scans chunks where almost every row is rejected. The copy operation becomes extremely fast.
The adaptive algorithm interprets this as: “Chunks are too small.” It therefore keeps increasing the chunk size.
Eventually, when the scan reaches the first matching rows, the chunk size has already grown dramatically. Instead of copying a few thousand rows, pt-osc may suddenly copy hundreds of thousands—or even a million—of rows in a single transaction.
Nothing is technically wrong. But Galera now has a completely different problem.
 
Why Galera Suffers
Every copied chunk is replicated as a write set.
Very large transactions produce:

large write sets
long certification phases
increased apply queues
longer replication latency
Flow Control activation

Once Flow Control starts, cluster throughput decreases dramatically.
Application commits begin waiting.
In extreme situations the cluster appears almost frozen until the oversized transaction has been fully applied by every node.
Ironically, the adaptive algorithm—designed to optimize throughput—ends up creating exactly the workload pattern that Galera dislikes the most.

Why InnoDB Suffers too
As soon as the first large chunks began processing, the following messages appeared in the PXC writer node’s error log:[Warning] [MY-014084] [InnoDB] Threads are unable to reserve space in redo log which can't be reclaimed 
due to the 'log_checkpointer' consumer still lagging behind at LSN = 852886039088. 
Consider increasing innodb_redo_log_capacity.This indicates that InnoDB received a very large transaction, and the writing threads could not find enough free space in the redo logs because the log_checkpointer consumer was falling behind. In practice, this forces the server to trigger checkpoints more frequently and on a larger scale, which increases overall overhead.
After Galera Replication propagated the write set to the other nodes, the same type of message was observed on them as well, during the apply phase of the replicated transaction.

Symptoms
Typical symptoms include:

Flow Control percentage rapidly increasing
Growing wsrep_local_recv_queue
High replication lag between nodes
Application commits waiting
CPU utilization increasing
Large spikes in transaction apply time
More frequent InnoDB checkpoints on all the nodes

The schema change itself may still complete successfully, but the impact on production traffic can be severe. In the worst scenario this can lead to a complete stall of the cluster due to excessive flow control. This actually happened in production, and the cluster had to be restarted after remaining locked for dozens of minutes.
 
The Proposed Fixes
Here are a few options, starting with the simplest and most effective one: disable adaptive chunk resizing whenever a selective –where clause is used.
Instead of continuously increasing chunk sizes, keep them fixed throughout the operation.
Simply include the following options in the command:--chunk-time = 0
--max-flow-ctl = 0If –chunk-time option is set to zero, the chunk size doesn’t auto-adjust, so query times will vary, but query chunk sizes will not. Another way to do the same thing is to specify a value for –chunk-size explicitly, instead of leaving it at the default, and omit the option –chunk-time.
The adaptive algorithm is bypassed. Chunk execution becomes predictable. Transaction size remains bounded. Galera never receives unexpectedly massive write sets. The overall schema change may take slightly longer, but cluster stability improves dramatically. For production environments this is usually the preferable trade-off.
 
Another parameter you should consider is –max-flow-ctl.
With –max-flow-ctl=PCT, pt-osc periodically checks wsrep_flow_control_paused (the percentage of time the node has spent paused for flow control) and automatically pauses row copying whenever it exceeds the threshold, resuming once the cluster drops back below it.
Setting –-max-flow-ctl to zero (or very close to it, e.g. 1) makes pt-online-schema-change stop copying rows at the very first sign that any node has entered flow control, rather than tolerating some pause time before backing off — which is the safest possible stance for a production cluster, especially one that’s already showing signs of stress (like the checkpointer-lag warnings, where large transactions were pushing InnoDB’s redo log reclamation to its limit): any additional write pressure from the migration’s chunked copy could tip an already struggling node into a much bigger stall, so a near-zero threshold trades migration speed for safety, letting the tool fully defer to the cluster’s real-time capacity and only advance when there’s genuinely no contention, which is generally the right call for critical, latency-sensitive production workloads where a slower-but-safe schema change beats a fast one that risks a cluster-wide write freeze.
A related lever worth mentioning is –chunk-index. By default, pt-online-schema-change chunks the table using the primary key (or the most appropriate unique index it can find), which is precisely why, in our case, chunk boundaries tracked the same monotonically increasing column as created_at and produced the long stretch of near-empty chunks described above. If the table has a secondary index whose ordering is not correlated with created_at, forcing the tool to chunk on that index instead (–chunk-index=idx_name) can spread matching and non-matching rows more evenly across chunks, so the adaptive algorithm never sees the extended low-density run that drives chunk size to unsafe levels in the first place. That said, this is not a substitute for –chunk-time=0 and –max-flow-ctl: pt-osc’s own documentation warns that a poorly chosen chunking index can hurt performance, since it forces query plans, and few tables have a convenient index that is both selective enough to chunk on and uncorrelated with the –where condition. In practice, we recommend treating –chunk-index as a secondary, table-specific optimization, with fixed chunk sizes and flow-control awareness remaining the primary safeguard.
 
A third option is to avoid the –where clause altogether and split the task into two phases instead. First, run pt-archiver solely to delete the rows you no longer need; then run pt-online-schema-change to alter the table. This two-step procedure avoids any risk stemming from the non-uniform distribution of the matching rows.
 
Conclusion
pt-online-schema-change remains one of the best tools available for online schema migrations.
Nevertheless, when using –where to copy only a small subset of rows on Percona XtraDB Cluster, adaptive chunk resizing can unintentionally generate oversized transactions that trigger excessive Galera Flow Control and temporarily stall the cluster.
Keeping chunk sizes fixed eliminates this feedback loop, producing smoother replication behaviour and reducing the operational risk of online schema changes. Other fixes are also available.
Sometimes, predictability is a better optimization than aggressiveness.
The post When pt-online-schema-change “where” Meets Galera: Understanding Chunk Auto-Resize and Flow Control appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>Percona Toolkit’s pt-online-schema-change (pt-osc) has long been the preferred solution for performing online schema changes with minimal downtime. Its chunk-based copy algorithm is designed to adapt dynamically to the workload, making it suitable for very large tables in production environments.</p>
<p>However, under specific conditions, one of its optimization mechanisms can become counterproductive.</p>
<p>During a customer engagement involving <b>Percona XtraDB Cluster (PXC)</b>, we investigated a case where running pt-online-schema-change with a selective –where clause caused severe <b>Galera Flow Control </b>and general overhead, effectively stalling the cluster for an extended period. The issue was not caused by the schema change itself, but by the interaction between:</p>
<ul>
<li aria-level="1">adaptive chunk auto-resizing</li>
<li aria-level="1">highly selective copying of the newest rows</li>
<li aria-level="1">clustered replication in Galera</li>
<li aria-level="1">InnoDB checkpointing</li>
</ul>
<p>This article explains why it happens and proposes a set of practical mitigations that eliminate the root cause.</p>
<p> </p>
<h2><b>The Scenario</b></h2>
<p>Consider a large table containing hundreds of millions of rows.</p>
<p>Instead of rebuilding the entire table, we only want to copy a subset of rows and apply a schema change like adding a new index.</p>
<p>The command looks similar to:</p><pre class="urvanov-syntax-highlighter-plain-tag">pt-online-schema-change \
--alter "ADD INDEX idx_status(status)" \
--where "created_at &amp;gt;= NOW() - INTERVAL 30 DAY" \
D=test,t=events \
--execute --force</pre><p>This is a common approach when:</p>
<ul>
<li aria-level="1">historical data can be ignored</li>
<li aria-level="1">only active records require modification</li>
<li aria-level="1">performing staged migrations</li>
<li aria-level="1">reducing migration time</li>
</ul>
<p>At first glance this seems perfectly reasonable.</p>
<p>Unfortunately, it exposes an interesting corner case in pt-online-schema-change.</p>
<p> </p>
<h2><b>How Chunk Auto-Resize Works</b></h2>
<p>During the copy phase, pt-osc continuously adjusts the chunk size in order to keep each chunk copy close to the configured execution time (–chunk-time), 0.5 seconds by default.</p>
<p>Simplified, the algorithm works like this:</p>
<ul>
<li aria-level="1">if chunks execute too quickly, increase the next chunk size</li>
<li aria-level="1">if chunks become slower, decrease the size</li>
<li aria-level="1">repeat until reaching a stable value</li>
</ul>
<p>For ordinary table scans this works remarkably well because row density is relatively uniform.</p>
<p>The problem appears when most scanned rows are discarded by –where.</p>
<p> </p>
<h2><b>The Hidden Effect of a Selective WHERE Clause</b></h2>
<p>Imagine a table like this with 1 billion rows:</p>
<table>
<tbody>
<tr>
<td><b>Rows</b></td>
<td><b>Match WHERE</b></td>
</tr>
<tr>
<td>990 million</td>
<td>No</td>
</tr>
<tr>
<td>10 million</td>
<td>Yes</td>
</tr>
</tbody>
</table>
<p>Suppose the matching rows are located at the end of the clustered index — and that was exactly the case here. Keep in mind that chunk selection always relies on the Primary Key boundaries starting from minimum value, but in this case those boundaries were combined (AND) with the condition passed via –where. Since the created_at values increased in step with the Primary Key, all the matching rows ended up clustered at the end of the index.</p>
<p>For a long time pt-osc scans chunks where almost every row is rejected. The copy operation becomes extremely fast.</p>
<p>The adaptive algorithm interprets this as: <strong>“Chunks are too small.”</strong> It therefore keeps increasing the chunk size.</p>
<p>Eventually, when the scan reaches the first matching rows, the chunk size has already grown dramatically. Instead of copying a few thousand rows, pt-osc may suddenly copy hundreds of thousands—or even a million—of rows in a single transaction.</p>
<p>Nothing is technically wrong. But Galera now has a completely different problem.</p>
<p> </p>
<h2><b>Why Galera Suffers</b></h2>
<p>Every copied chunk is replicated as a write set.</p>
<p>Very large transactions produce:</p>
<ul>
<li aria-level="1">large write sets</li>
<li aria-level="1">long certification phases</li>
<li aria-level="1">increased apply queues</li>
<li aria-level="1">longer replication latency</li>
<li aria-level="1">Flow Control activation</li>
</ul>
<p>Once Flow Control starts, cluster throughput decreases dramatically.</p>
<p>Application commits begin waiting.</p>
<p>In extreme situations the cluster appears almost frozen until the oversized transaction has been fully applied by every node.</p>
<p>Ironically, the adaptive algorithm—designed to optimize throughput—ends up creating exactly the workload pattern that Galera dislikes the most.</p>
<h2></h2>
<h2><b>Why InnoDB Suffers too</b></h2>
<p>As soon as the first large chunks began processing, the following messages appeared in the PXC writer node’s error log:</p><pre class="urvanov-syntax-highlighter-plain-tag">[Warning] [MY-014084] [InnoDB] Threads are unable to reserve space in redo log which can't be reclaimed 
due to the 'log_checkpointer' consumer still lagging behind at LSN = 852886039088. 
Consider increasing innodb_redo_log_capacity.</pre><p>This indicates that InnoDB received a very large transaction, and the writing threads could not find enough free space in the redo logs because the log_checkpointer consumer was falling behind. In practice, this forces the server to trigger checkpoints more frequently and on a larger scale, which increases overall overhead.</p>
<p>After Galera Replication propagated the write set to the other nodes, the same type of message was observed on them as well, during the apply phase of the replicated transaction.</p>
<h2></h2>
<h2><b>Symptoms</b></h2>
<p>Typical symptoms include:</p>
<ul>
<li aria-level="1">Flow Control percentage rapidly increasing</li>
<li aria-level="1">Growing wsrep_local_recv_queue</li>
<li aria-level="1">High replication lag between nodes</li>
<li aria-level="1">Application commits waiting</li>
<li aria-level="1">CPU utilization increasing</li>
<li aria-level="1">Large spikes in transaction apply time</li>
<li aria-level="1">More frequent InnoDB checkpoints on all the nodes</li>
</ul>
<p>The schema change itself may still complete successfully, but the impact on production traffic can be severe. In the worst scenario this can lead to a complete stall of the cluster due to excessive flow control. This actually happened in production, and the cluster had to be restarted after remaining locked for dozens of minutes.</p>
<p> </p>
<h2><b>The Proposed Fixes</b></h2>
<p>Here are a few options, starting with the simplest and most effective one: <b>disable adaptive chunk resizing whenever a selective </b><b>–where</b><b> clause is used.</b></p>
<p>Instead of continuously increasing chunk sizes, keep them fixed throughout the operation.</p>
<p>Simply include the following options in the command:</p><pre class="urvanov-syntax-highlighter-plain-tag">--chunk-time = 0
--max-flow-ctl = 0</pre><p>If <strong>–chunk-time</strong> option is set to zero, the chunk size doesn’t auto-adjust, so query times will vary, but query chunk sizes will not. Another way to do the same thing is to specify a value for<a href="https://docs.percona.com/percona-toolkit/pt-online-schema-change.html#cmdoption-pt-online-schema-change-chunk-size"> –chunk-size</a> explicitly, instead of leaving it at the default, and omit the option<a href="https://docs.percona.com/percona-toolkit/pt-online-schema-change.html#cmdoption-pt-online-schema-change-chunk-time"> –chunk-time</a>.</p>
<p>The adaptive algorithm is bypassed. Chunk execution becomes predictable. Transaction size remains bounded. Galera never receives unexpectedly massive write sets. The overall schema change may take slightly longer, but cluster stability improves dramatically. For production environments this is usually the preferable trade-off.</p>
<p> </p>
<p>Another parameter you should consider is <strong>–max-flow-ctl</strong>.</p>
<p>With <strong>–max-flow-ctl=PCT</strong>, pt-osc periodically checks wsrep_flow_control_paused (the percentage of time the node has spent paused for flow control) and automatically pauses row copying whenever it exceeds the threshold, resuming once the cluster drops back below it.</p>
<p>Setting –<strong>-max-flow-ctl</strong> to zero (or very close to it, e.g. 1) makes pt-online-schema-change stop copying rows at the very first sign that any node has entered flow control, rather than tolerating some pause time before backing off — which is the safest possible stance for a production cluster, especially one that’s already showing signs of stress (like the checkpointer-lag warnings, where large transactions were pushing InnoDB’s redo log reclamation to its limit): any additional write pressure from the migration’s chunked copy could tip an already struggling node into a much bigger stall, so a near-zero threshold trades migration speed for safety, letting the tool fully defer to the cluster’s real-time capacity and only advance when there’s genuinely no contention, which is generally the right call for critical, latency-sensitive production workloads where a slower-but-safe schema change beats a fast one that risks a cluster-wide write freeze.</p>
<p>A related lever worth mentioning is <strong>–chunk-index</strong>. By default, pt-online-schema-change chunks the table using the primary key (or the most appropriate unique index it can find), which is precisely why, in our case, chunk boundaries tracked the same monotonically increasing column as created_at and produced the long stretch of near-empty chunks described above. If the table has a secondary index whose ordering is not correlated with created_at, forcing the tool to chunk on that index instead (–chunk-index=idx_name) can spread matching and non-matching rows more evenly across chunks, so the adaptive algorithm never sees the extended low-density run that drives chunk size to unsafe levels in the first place. That said, this is not a substitute for <strong>–chunk-time=0</strong> and <strong>–max-flow-ctl</strong>: pt-osc’s own documentation warns that a poorly chosen chunking index can hurt performance, since it forces query plans, and few tables have a convenient index that is both selective enough to chunk on and uncorrelated with the –where condition. In practice, we recommend treating <strong>–chunk-index</strong> as a secondary, table-specific optimization, with fixed chunk sizes and flow-control awareness remaining the primary safeguard.</p>
<p> </p>
<p>A third option is to avoid the –where clause altogether and split the task into two phases instead. First, run <b>pt-archiver</b> solely to delete the rows you no longer need; then run pt-online-schema-change to alter the table. This two-step procedure avoids any risk stemming from the non-uniform distribution of the matching rows.</p>
<p> </p>
<h2><b>Conclusion</b></h2>
<p><strong>pt-online-schema-change</strong> remains one of the best tools available for online schema migrations.</p>
<p>Nevertheless, when using <strong>–where</strong> to copy only a small subset of rows on <b>Percona XtraDB Cluster</b>, adaptive chunk resizing can unintentionally generate oversized transactions that trigger excessive Galera Flow Control and temporarily stall the cluster.</p>
<p>Keeping chunk sizes fixed eliminates this feedback loop, producing smoother replication behaviour and reducing the operational risk of online schema changes. Other fixes are also available.</p>
<p>Sometimes, predictability is a better optimization than aggressiveness.</p>
<p>The post <a href="https://www.percona.com/blog/when-pt-online-schema-change-where-meets-galera-understanding-chunk-auto-resize-and-flow-control/">When pt-online-schema-change “where” Meets Galera: Understanding Chunk Auto-Resize and Flow Control</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 28 Sep 2026 13:56:53 +0000</pubDate>
    <dc:creator>Corrado Pandiani</dc:creator>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MySQL</category>
    <category>Open Source</category>
    <category>Percona Software</category>
    <category>XtraDB Cluster (PXC)</category>
    <category>pt-onine-schema-change</category>
    <category>pxc</category>
    <category>toolkit</category>
  </item>

  <item>
    <title>MyVector v1.26.9: Keeping Up With MySQL Innovation</title>
    <guid isPermaLink="false">https://askdba.net/?p=2986</guid>
    <link>https://askdba.net/myvector-v1-26-9-keeping-up-with-mysql-innovation/</link>
    <description>




MySQL 26.7 Innovation Support Lands



September 22, 2026 · ⁠GitHub Release



MySQL just changed the rules.



With MySQL 26.7, Oracle has moved to a new calendar-based versioning model for Innovation releases. For MyVector, that means one thing:



We need to keep up.



MyVector v1.26.9 adds MySQL 26.7 Innovation support, but the more interesting story is what happened beneath the surface.



This release puts the Component architecture introduced in v1.26.5 through a much more serious test.



From architecture to reliability



When we introduced the MySQL Component architecture in v1.26.5, the goal was straightforward: build MyVector in the direction MySQL itself is taking.



Now we are testing what happens when things don’t go perfectly.



v1.26.9 adds proper Component lifecycle testing, including installation, uninstallation, failed removal, and deinitialization rollback.



Because installing something is easy.



Installing, removing, failing, restarting, and recovering correctly is the real test.



HNSW gets harder to kill



There is also some important HNSW work in this release.



A number of failure paths around HNSW index creation and persistence have been fixed, including a case where an index build could crash mysqld.



That obviously isn’t acceptable.



We also fixed configuration handling so type=hnsw is correctly honored, and persistence failures are now surfaced instead of disappearing silently.



If MyVector cannot save an index, you should know about it.



Does it survive a restart?



This became an explicit test in v1.26.9.



It is one thing to create an index and run a vector search.



It is another thing to restart MySQL and have that index come back.



The new lifecycle tests verify that persisted indexes are actually reloaded from disk.



That is an important distinction as MyVector moves from an interesting MySQL extension toward something people can consider for real workloads.



The release process got better too



v1.26.9 also expands the testing and release pipeline around:




MySQL 26.7 compatibility



Component lifecycle



HNSW



Persistent indexes



Online updates



Stress testing



Stanford dataset smoke tests



Benchmark failure detection



Docker builds




What’s next?



The original MyVector idea hasn’t changed:



Why move your data to another database just because your application needs vector search?



The answer is becoming more interesting as MySQL evolves.



With MyVector, the goal is to bring:



SQL + transactional data + vector search + full-text search + AI workloads



into the same database environment.



MySQL 26.7 brings a new Innovation release.



MyVector 1.26.9 makes sure we’re ready to follow it.



v1.26.5 was about introducing the Component architecture.



v1.26.9 is about proving it.



Get MyVector



⁠MyVector v1.26.9



⁠GitHub Repository



⁠Documentation



MyVector is open source. Feedback, issues, benchmarks, and contributions are welcome.</description>
    <content:encoded><![CDATA[<p><img src="https://askdba.net/wp-content/uploads/2026/09/myvector269.png" width="1672" height="941" alt=""></p>
<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">MySQL 26.7 Innovation Support Lands</h3>



<p class="wp-block-paragraph"><br>September 22, 2026 · ⁠<a href="https://github.com/askdba/myvector/releases/tag/v1.26.9">GitHub Release</a></p>



<p class="wp-block-paragraph">MySQL just changed the rules.</p>



<p class="wp-block-paragraph">With MySQL 26.7, Oracle has moved to a new calendar-based versioning model for Innovation releases. For MyVector, that means one thing:</p>



<p class="wp-block-paragraph"><strong>We need to keep up.</strong></p>



<p class="wp-block-paragraph">MyVector v1.26.9 adds MySQL 26.7 Innovation support, but the more interesting story is what happened beneath the surface.</p>



<p class="wp-block-paragraph">This release puts the Component architecture introduced in v1.26.5 through a much more serious test.</p>



<h2 class="wp-block-heading"><strong>From architecture to reliability</strong></h2>



<p class="wp-block-paragraph">When we introduced the MySQL Component architecture in v1.26.5, the goal was straightforward: build MyVector in the direction MySQL itself is taking.</p>



<p class="wp-block-paragraph">Now we are testing what happens when things don’t go perfectly.</p>



<p class="wp-block-paragraph">v1.26.9 adds proper Component lifecycle testing, including installation, uninstallation, failed removal, and deinitialization rollback.</p>



<p class="wp-block-paragraph">Because installing something is easy.</p>



<p class="wp-block-paragraph"><strong>Installing, removing, failing, restarting, and recovering correctly is the real test.</strong></p>



<h2 class="wp-block-heading"><strong>HNSW gets harder to kill</strong></h2>



<p class="wp-block-paragraph">There is also some important HNSW work in this release.</p>



<p class="wp-block-paragraph">A number of failure paths around HNSW index creation and persistence have been fixed, including a case where an index build could crash <code>mysqld</code>.</p>



<p class="wp-block-paragraph">That obviously isn’t acceptable.</p>



<p class="wp-block-paragraph">We also fixed configuration handling so <code>type=hnsw</code> is correctly honored, and persistence failures are now surfaced instead of disappearing silently.</p>



<p class="wp-block-paragraph">If MyVector cannot save an index, you should know about it.</p>



<h2 class="wp-block-heading"><strong>Does it survive a restart?</strong></h2>



<p class="wp-block-paragraph">This became an explicit test in v1.26.9.</p>



<p class="wp-block-paragraph">It is one thing to create an index and run a vector search.</p>



<p class="wp-block-paragraph">It is another thing to restart MySQL and have that index come back.</p>



<p class="wp-block-paragraph">The new lifecycle tests verify that persisted indexes are actually reloaded from disk.</p>



<p class="wp-block-paragraph">That is an important distinction as MyVector moves from an interesting MySQL extension toward something people can consider for real workloads.</p>



<h2 class="wp-block-heading"><strong>The release process got better too</strong></h2>



<p class="wp-block-paragraph">v1.26.9 also expands the testing and release pipeline around:</p>



<ul class="wp-block-list">
<li>MySQL 26.7 compatibility</li>



<li>Component lifecycle</li>



<li>HNSW</li>



<li>Persistent indexes</li>



<li>Online updates</li>



<li>Stress testing</li>



<li>Stanford dataset smoke tests</li>



<li>Benchmark failure detection</li>



<li>Docker builds</li>
</ul>



<p class="wp-block-paragraph"><strong>What’s next?</strong></p>



<p class="wp-block-paragraph">The original MyVector idea hasn’t changed:</p>



<p class="wp-block-paragraph"><strong>Why move your data to another database just because your application needs vector search?</strong></p>



<p class="wp-block-paragraph">The answer is becoming more interesting as MySQL evolves.</p>



<p class="wp-block-paragraph">With MyVector, the goal is to bring:</p>



<p class="wp-block-paragraph"><strong>SQL + transactional data + vector search + full-text search + AI workloads</strong></p>



<p class="wp-block-paragraph">into the same database environment.</p>



<p class="wp-block-paragraph">MySQL 26.7 brings a new Innovation release.</p>



<p class="wp-block-paragraph">MyVector 1.26.9 makes sure we’re ready to follow it.</p>



<p class="wp-block-paragraph"><strong>v1.26.5 was about introducing the Component architecture.</strong></p>



<p class="wp-block-paragraph"><strong>v1.26.9 is about proving it.</strong></p>



<h3 class="wp-block-heading"><strong>Get MyVector</strong></h3>



<p class="wp-block-paragraph">⁠<a href="https://github.com/askdba/myvector/releases/tag/v1.26.9">MyVector v1.26.9</a></p>



<p class="wp-block-paragraph">⁠<a href="https://github.com/askdba/myvector">GitHub Repository</a></p>



<p class="wp-block-paragraph">⁠<a href="https://myvector.online/">Documentation</a></p>



<p class="wp-block-paragraph">MyVector is open source. Feedback, issues, benchmarks, and contributions are welcome.</p>]]></content:encoded>
    <pubDate>Fri, 25 Sep 2026 13:36:50 +0000</pubDate>
    <dc:creator>Alkin Tezuysal</dc:creator>
    <category>mysql</category>
    <category>Technical</category>
    <category>ai/ml</category>
    <category>artificial-intelligence</category>
    <category>hnsw</category>
    <category>llm</category>
    <category>vector</category>
  </item>

  <item>
    <title>Open FIPS Build, Vectors, and Binlog Server: Marco Tusa at Percona Live</title>
    <guid isPermaLink="false">https://percona.community/blog/2026/09/25/open-mysql-fips-build-percona-live/</guid>
    <link>https://percona.community/blog/2026/09/25/open-mysql-fips-build-percona-live/</link>
    <description>This post collects the main things Marco Tusa covered in his keynote at Percona Live 2026 in Amsterdam: where Percona stands on MySQL, which features landed in the server, and what holds the MySQL community together.
Watch it here: “I Was Going to Show You a Roadmap”, about 18 minutes.
Start with the community, because the rest of it follows from there. In 2011, after the Sun and Oracle acquisitions, O’Reilly stopped the MySQL Conference, and Percona kept a conference going when nobody knew what came next. The community scattered again more recently, and Peter Zaitsev and others regrouped it in the OurSQL Foundation. MySQL, MariaDB, and Percona ship competing solutions. The people running and building them are one community, with one shared interest: this environment stays usable and open.
Oppose the decision, not the people
MariaDB moved later Galera development inside an enterprise circle. The MariaDB Foundation pushed back. MariaDB keeps its own version, and others can still improve it. Percona keeps shipping Percona XtraDB Cluster (PXC). PXC has been in production since about 2012, and it was not built as a reply to that dispute. The argument is with the decision, not with the people who made it.
The same rule applies inside Percona. A FIPS implementation in the Pro Build program was available to customers and not to everyone else. That broke the mandate to stay open. The code is open now: it is the default, and everyone can get it.
What is already there
This is work in progress, not a list of future dates.

A binlog server, shown in Yura Sorokin’s talk “Beyond the Dumb Pipe.” It is early: a bicycle that should become a full server.
Vector search inside Percona Server for MySQL, with no separate system beside it.
Monitoring and observability an agent can use to see a query more clearly.
MyRocks and PXC, still in active development. They are first-class parts of the server, not items added to fill a list.
MySQL Enterprise features carried from version to version. Each version needs merges and adjustments. That work stays in the freely available server.

The written vision is at percona.com/mysql/vision. The roadmap follows it, in public.
Answer the MariaDB questionnaire from Percona Live, and share the answer. MySQL Calculator is an open source project you can try through the operator. If something is wrong, open an issue.
More sessions from the event are in the Percona Live 2026 Amsterdam playlist. The same recordings are on the conference site, in Videos.</description>
    <content:encoded><![CDATA[<p>This post collects the main things Marco Tusa covered in his keynote at Percona Live 2026 in Amsterdam: where Percona stands on MySQL, which features landed in the server, and what holds the MySQL community together.</p>
<p>Watch it here: <a href="https://www.youtube.com/watch?v=cB6iDhdWW8o" target="_blank" rel="noopener noreferrer">“I Was Going to Show You a Roadmap”</a>, about 18 minutes.</p>
<p>Start with the community, because the rest of it follows from there. In 2011, after the Sun and Oracle acquisitions, O’Reilly stopped the MySQL Conference, and Percona kept a conference going when nobody knew what came next. The community scattered again more recently, and Peter Zaitsev and others regrouped it in the <a href="https://oursqlfoundation.org/" target="_blank" rel="noopener noreferrer">OurSQL Foundation</a>. MySQL, MariaDB, and Percona ship competing solutions. The people running and building them are one community, with one shared interest: this environment stays usable and open.</p>
<h2>Oppose the decision, not the people</h2>
<p>MariaDB moved later Galera development inside an enterprise circle. The MariaDB Foundation pushed back. MariaDB keeps its own version, and others can still improve it. Percona keeps shipping Percona XtraDB Cluster (PXC). PXC has been in production since about 2012, and it was not built as a reply to that dispute. The argument is with the decision, not with the people who made it.</p>
<p>The same rule applies inside Percona. A FIPS implementation in the Pro Build program was available to customers and not to everyone else. That broke the mandate to stay open. The code is open now: it is the default, and everyone can get it.</p>
<h2>What is already there</h2>
<p>This is work in progress, not a list of future dates.</p>
<ul>
<li>A binlog server, shown in Yura Sorokin’s talk “Beyond the Dumb Pipe.” It is early: a bicycle that should become a full server.</li>
<li>Vector search inside Percona Server for MySQL, with no separate system beside it.</li>
<li>Monitoring and observability an agent can use to see a query more clearly.</li>
<li>MyRocks and PXC, still in active development. They are first-class parts of the server, not items added to fill a list.</li>
<li>MySQL Enterprise features carried from version to version. Each version needs merges and adjustments. That work stays in the freely available server.</li>
</ul>
<p>The written vision is at <a href="https://www.percona.com/mysql/vision/" target="_blank" rel="noopener noreferrer">percona.com/mysql/vision</a>. The roadmap follows it, in public.</p>
<p>Answer the MariaDB questionnaire from Percona Live, and share the answer. MySQL Calculator is an open source project you can try through the operator. If something is wrong, open an issue.</p>
<p>More sessions from the event are in the <a href="https://www.youtube.com/playlist?list=PLOgsvlaOXz2c" target="_blank" rel="noopener noreferrer">Percona Live 2026 Amsterdam playlist</a>. The same recordings are on the conference site, in <a href="https://perconalive.com/2026-amsterdam/videos/" target="_blank" rel="noopener noreferrer">Videos</a>.</p>]]></content:encoded>
    <pubDate>Fri, 25 Sep 2026 12:00:00 +0000</pubDate>
    <dc:creator>Percona Community</dc:creator>
    <category>MySQL</category>
    <category>Percona Live</category>
    <category>Open Source</category>
    <category>Percona Server</category>
    <category>PXC</category>
  </item>

  <item>
    <title>MySQL 26.10.0 Early Access Is Here</title>
    <guid isPermaLink="false">67736b8de833261306fce23bbfa7b47a</guid>
    <link>https://blogs.oracle.com/mysql/mysql-26-10-0-early-access-is-here</link>
    <description>The MySQL 26.10.0 Early Access Release is now available, giving developers and database teams an early opportunity to preview, evaluate, and provide feedback on the next evolution of MySQL. Early Access releases are where curiosity becomes contribution. Whether you’re testing an application stack, validating upgrade plans, or simply eager to see what’s coming, this is […]</description>
    <pubDate>Fri, 25 Sep 2026 11:58:21 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>early access</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

</channel>
</rss>
