<?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>Mon, 27 Jul 2026 00:50:11 +0000</pubDate>
  <language>en</language>
  <description>Planet MySQL - https://planet.mysql.com</description>

  <item>
    <title>A first look at MySQL 26.7 Early Access</title>
    <guid isPermaLink="false">https://ronaldbradford.com/blog/2026-07-23-a-first-look-at-mysql-26-7-early-access/</guid>
    <link>https://ronaldbradford.com/blog/2026-07-23-a-first-look-at-mysql-26-7-early-access/</link>
    <description>MySQL has dropped its newest release , categorized as “Early Access” and available at https://labs.mysql.com/ .
While this post is not going to go into depth, I wanted to at least validate the management changes you verify between normal MySQL upgrades.</description>
    <pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate>
    <dc:creator>Ronald Bradford</dc:creator>
  </item>

  <item>
    <title>MySQL 9.7 Community Edition: Smarter Join Planning with the Hypergraph Optimizer</title>
    <guid isPermaLink="false">3c07c7e10710eb102cb67245697bedd4</guid>
    <link>https://blogs.oracle.com/mysql/smarter-join-planning-with-the-hypergraph-optimizer</link>
    <description>With the release of MySQL 9.7 Community Edition, the Hypergraph Optimizer is now available to everyone. This is a significant addition to MySQL and one that has generated a lot of excitement in the MySQL community. The promise is simple: better execution plans for complex queries, especially those with many joins. Like most new features, […]</description>
    <pubDate>Wed, 22 Jul 2026 06:00:00 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>hypergraph optimizer</category>
    <category>mysql</category>
    <category>MySQL 9.7 LTS</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>OCI Cache and MySQL HeatWave: Better Together for High-Performance Applications</title>
    <guid isPermaLink="false">https://dasini.net/blog/?p=8808</guid>
    <link>https://dasini.net/blog/2026/07/21/oci-cache-and-mysql-heatwave-better-together-for-high-performance-applications/</link>
    <description>Modern applications are expected to deliver instant responses while processing increasingly large volumes of data. Achieving this level of performance isn’t simply a matter of making the database faster.It requires placing the right workload on the right layer of the architecture. Some operations require ultra-fast repeated reads, others demand transactional consistency, while analytical queries benefit […]
The post OCI Cache and MySQL HeatWave: Better Together for High-Performance Applications first appeared on Data Daz (dasini.net) - Data Systems, AI, and Real-World Insights.</description>
    <content:encoded><![CDATA[<p>Modern applications are expected to deliver instant responses while processing increasingly large volumes of data. Achieving this level of performance isn’t simply a matter of making the database faster.It requires placing the right workload on the right layer of the architecture. Some operations require ultra-fast repeated reads, others demand transactional consistency, while analytical queries benefit […]</p>
The post <a href="https://dasini.net/blog/2026/07/21/oci-cache-and-mysql-heatwave-better-together-for-high-performance-applications/">OCI Cache and MySQL HeatWave: Better Together for High-Performance Applications</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>Tue, 21 Jul 2026 12:14:36 +0000</pubDate>
    <dc:creator>Olivier Dasini</dc:creator>
    <category>Coding</category>
    <category>HeatWave</category>
    <category>MDS</category>
    <category>MySQL</category>
    <category>NoSQL</category>
    <category>Cache</category>
    <category>Cloud</category>
    <category>Heatwave</category>
    <category>Redis</category>
    <category>Valkey</category>
  </item>

  <item>
    <title>MySQL on OKE: Database Operations as Kubernetes State</title>
    <guid isPermaLink="false">5ee398b6761171651191d792221c6817</guid>
    <link>https://blogs.oracle.com/mysql/mysql-on-oke-database-operations-as-kubernetes-state</link>
    <description>MySQL is one of the databases developers trust most when an application needs a proven, familiar, open source relational engine. Kubernetes has become the orchestration layer teams rely on to run and scale modern workloads. Put them together, and the question gets interesting: how do you run MySQL with the same declarative, repeatable operating model […]</description>
    <pubDate>Mon, 20 Jul 2026 14:26:46 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
  </item>

  <item>
    <title>From Tokyo to Seoul to Taipei: MySQL Community Conversations Across JAPAC</title>
    <guid isPermaLink="false">0f0d734f062ff3ec2884e7e3679e4ff2</guid>
    <link>https://blogs.oracle.com/mysql/from-tokyo-to-seoul-to-taipei-mysql-community-conversations-across-japac</link>
    <description>Over the past year, we have taken important steps to increase transparency and engagement across the MySQL ecosystem. Through public roadmap discussions, Early Access releases, publication of worklogs, bug transparency and backlog reduction, community public discussions, increased use of GitHub discussions, and contributor events, we have created more opportunities for the community to understand what […]</description>
    <pubDate>Fri, 17 Jul 2026 17:08:56 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>Optimizing Replication Lag for Large Transactions and DDL in MySQL</title>
    <guid isPermaLink="false">https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/</guid>
    <link>https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication-en/</link>
    <description>
  This article is also available in Chinese: 中文版. Browse all English articles.


Since MySQL 5.6, the MySQL replication team has been working to reduce replication lag. The first step was schema-level parallel application of the binlog, but schema-level parallelism only helps when writes are spread across many databases; in the common case, where most write traffic hits a single database, it provides almost no parallelism. MySQL 5.7 then introduced the Commit-Order parallel-replay strategy, which depends on how many transactions run concurrently on the primary: the replica can replay quickly only when the primary is highly concurrent. When concurrency on the primary is low, the replica still replays slowly and lag builds up. To fix that, MySQL 5.7 also introduced the Writeset (row-level) strategy, which lets the replica replay in parallel quickly no matter how concurrent the primary is.

We rolled out the writeset-based strategy across our fleet long ago, and it eliminated roughly 60% of our replication-lag problems. Another more than 30% comes from large transactions and DDL — the hardest replication-lag problem to solve in MySQL. Last year we built a mechanism in AliSQL called Binlog Realtime Replication (BRR) that solves it completely.

How Binlog Realtime Replication Works



The figure above shows why large transactions and DDL cause replication lag. Binlog replication works at the granularity of a transaction: a transaction’s events are written to the binlog file only after it commits, then shipped to the replica and executed there (a DDL can be treated as a single transaction). The change becomes visible to applications only once the replica finishes executing. If a transaction takes a long time on the primary, it takes just as long on the replica, and the lag equals the replica’s execution time. In practice the lag is often worse. First, a large transaction produces very large binlog events, which adds transmission delay. Second, while a large transaction — especially a DDL — is running, it can block the replay of other transactions, so relay log piles up; once the large transaction or DDL finishes replaying, that backlog also needs time to drain before the replica catches up.



The idea behind the optimization is simple: have the replica start executing the large transaction or DDL at the same time as the primary, and once the primary commits, tell the replica to commit too. With this mechanism, replication lag for large transactions and DDL stays under one second. The chart below compares the lag from a large transaction before and after the optimization: with realtime replication, large transactions no longer cause lag, and neither do DDLs.



The feature has been enabled by default in our RDS service since 2025. To date more than 3,000 instances have used it, running realtime replication about 300,000 times for large transactions and about 60,000 times for DDL.

Implementing Realtime Replication

The core idea of realtime replication fits in one sentence: as soon as the primary starts executing, it ships the binlog events (or DDL) to the replica, which executes them in lockstep; when the primary finally commits or rolls back, the replica does the same.

Realtime replication has two parts: realtime transmission and realtime application. Realtime transmission streams the binlog events a large transaction produces on the primary to the replica as they are generated; that part is covered in Binlog Transmission Optimization for Large MySQL Transactions. Realtime application replays those events on the replica as they arrive, using a dedicated group of replay threads, as shown below:



While a transaction runs on the primary, the binlog events it produces are first buffered in the Binlog Cache. If the transaction is large (the Binlog Cache exceeds a threshold), the primary’s Dump thread reads the Binlog Cache temporary file and sends the events straight to the replica. The replica writes them into a dedicated Brr Cache (not the relay log file), where a new group of Brr Worker threads applies them in real time.

For DDL, binlog events are produced later than for a large transaction — a DDL writes its Query_log_event into the Binlog Cache only during the commit phase. BRR therefore handles DDL specially: once the primary starts executing the DDL, it builds the Query_log_event directly and puts it in an in-memory buffer, ddl_query_buffer; the Dump thread reads events from this buffer and sends them to the replica, where a Brr Worker again executes the DDL in real time.

As a result, replica execution of DDL and large transactions shifts from run only after the primary finishes to run on the primary and replica in parallel, leaving only network transmission and commit as the residual lag — typically on the order of tens of milliseconds.

Below we look at how BRR is implemented, from both the primary and the replica side.

Overall BRR Architecture



Primary Side

When a large transaction or DDL needs realtime replication, a Brr_trx is created and registered with the Brr_trx_manager.

Brr_binlog_sender is an extension of the Dump thread; it reads events from a Brr_trx and pushes them to the replica. Originally the Dump thread did just one thing: read events from the binlog file and send them to the replica. BRR gives it one more job — poll each active Brr_trx, read binlog events from its Binlog Cache temporary file or from ddl_query_buffer, and send them to the replica.

Realtime transmission reuses the existing Dump channel. To tell BRR traffic apart from ordinary traffic, BRR borrows an idea from Semisync and attaches an extra BRR Header to each event; the header identifies whether an event is BRR or ordinary replication traffic. And to keep BRR events from choking the ordinary binlog-event channel, BRR applies flow control.



Replica Side

Using the BRR Header, the replica’s IO thread splits events into two kinds: BRR events go into the Brr_cache, while normal events take the original path into the relay log.

Brr_cache is the replica-side storage for a BRR transaction; each BRR transaction has one Brr_cache. When the IO thread receives a BRR event, it uses the brr_index in the header to locate the matching Brr_cache (if it’s the first event, it creates a new Brr_cache and wakes a Brr Worker), writes the event into the Brr_cache temporary file, and updates the readable position.

Brr_rpl_info manages these BRR transactions.

The BRR Worker threads are dedicated to applying BRR transactions. When idle, a Brr Worker picks a Brr_cache that hasn’t started being applied and makes itself its owner. From then on it is bound to that Brr_cache, looping to read and replay binlog events until it sees a Gtid_log_event (the primary has committed) or receives a BRR_ROLLBACK_EVENT (the primary rolled back).

The gtid_executed Snapshot

The uncommitted BRR transactions from the primary run in parallel on the replica alongside already-committed transactions. If a BRR transaction depends on an already-committed one, its binlog events must not start until that dependency has finished replaying on the replica; Otherwise you get escalating failures: a deadlock, then a broken replication channel, and in the worst case data inconsistency between primary and replica. Take this example:

1
2
INSERT INTO t1(pk, c2) VALUES(pk1, 1);
UPDATE t1 SET c2 = 2;  -- large transaction


The UPDATE is the large transaction, and it must not begin until the INSERT has finished replaying. If the UPDATE runs first, it fails when updating the pk1 row because that row doesn’t exist yet.

BRR uses a gtid_executed snapshot to enforce these ordering dependencies. When a DDL or large transaction starts on the primary, the primary’s current gtid_executed captures every preceding transaction it saw. Once the replica’s gtid_executed has caught up to that value (that is, is a superset of it), all the transactions this one depends on have been replayed on the replica, and it is safe to start applying it.

To do this, BRR adds a new event type, Brr_gtid_executed_log_event, whose body holds a gtid_executed set. At specific moments the primary takes a gtid_executed snapshot and writes it to the BRR channel; when a replica Brr Worker reads the snapshot, it waits for all the GTIDs in it to finish before continuing.

Realtime Replication of Large Transactions



Creating and Updating a Brr_trx

When a transaction runs on the primary, its binlog events go first into the Binlog Cache (an in-memory buffer backed by a temporary file). In MySQL, once the Binlog Cache fills its in-memory buffer, it spills to the temporary file.

BRR hooks in here: after each batch of events is written to the Binlog Cache, it checks the temporary file’s size. Once the file exceeds a certain size, BRR creates a Brr_trx, records the temporary file name and the current readable position, and registers it with Brr_trx_manager. From then on, every append to the Binlog Cache updates the Brr_trx’s end_position and wakes the Dump thread to send those events to the replica.

Transmitting Binlog Events

Before sending each batch of binlog events, the Dump thread emits a Brr_gtid_executed_log_event as that batch’s dependency snapshot, then sends the batch itself.

Committing the Transaction

For a large transaction, the binlog events sit in the Brr_cache temporary file — not yet relay log — until the Brr Worker reads the Gtid_log_event. When the primary finally commits, it sends the Gtid_log_event over the BRR channel, and the IO thread does two things:


  Renames the Brr_cache temporary file into a relay log file. Based on the GTID, the primary’s Dump thread then skips sending this transaction, so its events aren’t shipped again as an ordinary transaction.
  Notifies the Brr Worker to read the Gtid_log_event and Xid_log_event and complete the commit.


Rolling Back the Transaction

The rollback path is straightforward: when the primary rolls back, it sends a BRR_ROLLBACK_EVENT over the BRR channel; on receiving it, the replica’s IO thread sends a KILL_QUERY signal to the corresponding Brr Worker. The Brr Worker detects KILL_QUERY, rolls back the current transaction, cleans up, and moves on to the next Brr_cache.

Note that after being killed, a Brr Worker neither exits nor propagates the error to the SQL thread — unlike an ordinary Worker, which must halt all replication on an error. The reason: for an ordinary Worker the transaction has already committed on the primary, so if the replica gives up, the two diverge. A Brr Worker’s transaction, by contrast, runs concurrently with the primary, so a primary rollback is the normal path and the replica must roll back as well.

Realtime Application of DDL



Creating a Brr_trx

For large transactions, we decide whether a transaction is “large” by the total size of its binlog events in the Binlog Cache. DDL is trickier: some DDLs only touch metadata and finish almost instantly, while for DDLs that touch data the run time depends on how much data is involved and is hard to estimate accurately. So instead of predicting a DDL’s run time up front, we decide whether to realtime-replicate it by whether its execution exceeds a timeout.

Every DDL creates a Brr_trx, but that Brr_trx isn’t sent to the replica right away. A DDL’s Brr_trx has a threshold — 1000 ms by default — and only when the DDL’s run time exceeds it does the Dump thread start sending the Brr_trx. If a DDL finishes quickly, within one second, its Brr_trx is silently discarded and the DDL ships to the replica over the ordinary binlog channel, exactly as if BRR were off.

A DDL’s Brr_trx is created during the DDL’s Prepare phase — that is, after the DDL has acquired the MDL X lock — because only with the X lock does the DDL have permission to operate on the table. Any conflicting operations have either already committed or must wait until the DDL releases the X lock or finishes.

Two gtid_executed Snapshots

An Online DDL runs in three phases: Prepare, Execute, and Commit. After Prepare, the MDL X lock is downgraded to an S lock, so during Execut, DML and DDL can run in parallel. During Commit, the S lock is upgraded back to an X lock; regaining the X lock means all those parallel DMLs have already committed. The replica must honor the same rule: those committed DMLs have to finish replaying before the replica can enter the Commit phase.

So realtime replication of an Online DDL has two points on the replica that must be synchronized: one before entering Prepare, and one before entering Commit. Correspondingly, the primary takes two gtid_executed snapshots — one after the DDL enters Prepare, and one after it enters Commit.

Shipping Binlog Events Twice

In the large-transaction section we saw that a large transaction is transmitted to the replica via BRR, and the copy in the binlog file is not shipped again. DDL is different: it ships twice — once over BRR, and again as the binlog events in the binlog file.

A DDL’s Query_log_event is tiny, so shipping it twice costs almost nothing. Shipping it only once would force us into the large-transaction rename logic (renaming the Brr_cache temporary file into relay log), with all its edge cases. For DDL, simply shipping it twice and discarding the Brr_cache afterward is the simplest approach.

As for ordering, the Dump thread guarantees BRR events ship before ordinary events. That way the Brr Worker is sure to get the DDL first and start executing it; by the time the ordinary events reach the relay log, the Brr Worker is already applying the DDL.

When an ordinary Worker reads the DDL from the relay log, it checks whether the GTID is in owned_gtids. If it is (a Brr Worker is executing it), the ordinary Worker waits; once the Brr Worker commits, the GTID is added into gtid_executed. The ordinary Worker wakes and finds the GTID already in gtid_executed, so it skips the whole DDL.

If the Brr Worker rolled the DDL back, the GTID is removed from owned_gtids and never added to gtid_executed. The ordinary Worker then wakes and sees the transaction wasn’t executed. It runs the DDL normally — the fallback path, equivalent to running with BRR off.

Conclusion

AliSQL’s Binlog Realtime Replication tackles the thorniest lag in MySQL binlog replication — lag from large transactions and DDL — by executing on the primary and replica in parallel. On top of that, we’ve made optimizations for the writeset mechanism, for massively concurrent workloads, and for the medium-sized transactions that batch jobs produce. Together, these have eliminated 95% of the replication lag in our production environment.</description>
    <content:encoded><![CDATA[<blockquote class="prompt-tip">
  <p>This article is also available in Chinese: <a href="https://songlibing.github.io/posts/mysql-large-transaction-ddl-replication/">中文版</a>. Browse <a href="https://songlibing.github.io/english/">all English articles</a>.</p>
</blockquote>

<p>Since MySQL 5.6, the MySQL replication team has been working to reduce replication lag. The first step was schema-level parallel application of the binlog, but schema-level parallelism only helps when writes are spread across many databases; in the common case, where most write traffic hits a single database, it provides almost no parallelism. MySQL 5.7 then introduced the <code class="language-plaintext highlighter-rouge">Commit-Order</code> parallel-replay strategy, which depends on how many transactions run concurrently on the primary: the replica can replay quickly only when the primary is highly concurrent. When concurrency on the primary is low, the replica still replays slowly and lag builds up. To fix that, MySQL 5.7 also introduced the <code class="language-plaintext highlighter-rouge">Writeset</code> (row-level) strategy, which lets the replica replay in parallel quickly no matter how concurrent the primary is.</p>

<p>We rolled out the writeset-based strategy across our fleet long ago, and it eliminated roughly 60% of our replication-lag problems. Another more than 30% comes from large transactions and DDL — the hardest replication-lag problem to solve in MySQL. Last year we built a mechanism in AliSQL called <code class="language-plaintext highlighter-rouge">Binlog Realtime Replication (BRR)</code> that solves it completely.</p>

<h2>How Binlog Realtime Replication Works</h2>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-1-en.png" alt=""></p>

<p>The figure above shows why large transactions and DDL cause replication lag. Binlog replication works at the granularity of a transaction: a transaction’s events are written to the binlog file only after it commits, then shipped to the replica and executed there (a DDL can be treated as a single transaction). The change becomes visible to applications only once the replica finishes executing. If a transaction takes a long time on the primary, it takes just as long on the replica, and the lag equals the replica’s execution time. In practice the lag is often worse. First, a large transaction produces very large binlog events, which adds transmission delay. Second, while a large transaction — especially a DDL — is running, it can block the replay of other transactions, so relay log piles up; once the large transaction or DDL finishes replaying, that backlog also needs time to drain before the replica catches up.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-2-en.png" alt=""></p>

<p>The idea behind the optimization is simple: have the replica start executing the large transaction or DDL at the same time as the primary, and once the primary commits, tell the replica to commit too. With this mechanism, replication lag for large transactions and DDL stays under one second. The chart below compares the lag from a large transaction before and after the optimization: with realtime replication, large transactions no longer cause lag, and neither do DDLs.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-3-en.jpg" alt=""></p>

<p>The feature has been enabled by default in our RDS service since 2025. To date more than 3,000 instances have used it, running realtime replication about 300,000 times for large transactions and about 60,000 times for DDL.</p>

<h2>Implementing Realtime Replication</h2>

<p>The core idea of realtime replication fits in one sentence: <em>as soon as the primary starts executing, it ships the binlog events (or DDL) to the replica, which executes them in lockstep; when the primary finally commits or rolls back, the replica does the same.</em></p>

<p>Realtime replication has two parts: realtime transmission and realtime application. Realtime transmission streams the binlog events a large transaction produces on the primary to the replica as they are generated; that part is covered in <em><a href="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/">Binlog Transmission Optimization for Large MySQL Transactions</a></em>. Realtime application replays those events on the replica as they arrive, using a dedicated group of replay threads, as shown below:</p>

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

<p>While a transaction runs on the primary, the binlog events it produces are first buffered in the Binlog Cache. If the transaction is large (the Binlog Cache exceeds a threshold), the primary’s Dump thread reads the Binlog Cache temporary file and sends the events straight to the replica. The replica writes them into a dedicated <code class="language-plaintext highlighter-rouge">Brr Cache</code> (not the relay log file), where a new group of <code class="language-plaintext highlighter-rouge">Brr Worker</code> threads applies them in real time.</p>

<p>For DDL, binlog events are produced later than for a large transaction — a DDL writes its <code class="language-plaintext highlighter-rouge">Query_log_event</code> into the Binlog Cache only during the commit phase. BRR therefore handles DDL specially: once the primary starts executing the DDL, it builds the <code class="language-plaintext highlighter-rouge">Query_log_event</code> directly and puts it in an in-memory buffer, <code class="language-plaintext highlighter-rouge">ddl_query_buffer</code>; the Dump thread reads events from this buffer and sends them to the replica, where a Brr Worker again executes the DDL in real time.</p>

<p>As a result, replica execution of DDL and large transactions shifts from <em>run only after the primary finishes</em> to <em>run on the primary and replica in parallel</em>, leaving only network transmission and commit as the residual lag — typically on the order of tens of milliseconds.</p>

<p>Below we look at how BRR is implemented, from both the primary and the replica side.</p>

<h3>Overall BRR Architecture</h3>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-5-en.jpg" alt=""></p>

<h4>Primary Side</h4>

<p>When a large transaction or DDL needs realtime replication, a <code class="language-plaintext highlighter-rouge">Brr_trx</code> is created and registered with the <code class="language-plaintext highlighter-rouge">Brr_trx_manager</code>.</p>

<p><code class="language-plaintext highlighter-rouge">Brr_binlog_sender</code> is an extension of the Dump thread; it reads events from a <code class="language-plaintext highlighter-rouge">Brr_trx</code> and pushes them to the replica. Originally the Dump thread did just one thing: read events from the binlog file and send them to the replica. BRR gives it one more job — poll each active <code class="language-plaintext highlighter-rouge">Brr_trx</code>, read binlog events from its Binlog Cache temporary file or from <code class="language-plaintext highlighter-rouge">ddl_query_buffer</code>, and send them to the replica.</p>

<p>Realtime transmission reuses the existing Dump channel. To tell BRR traffic apart from ordinary traffic, BRR borrows an idea from Semisync and attaches an extra <code class="language-plaintext highlighter-rouge">BRR Header</code> to each event; the header identifies whether an event is BRR or ordinary replication traffic. And to keep BRR events from choking the ordinary binlog-event channel, BRR applies flow control.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-6-en.png" alt=""></p>

<h4>Replica Side</h4>

<p>Using the <code class="language-plaintext highlighter-rouge">BRR Header</code>, the replica’s IO thread splits events into two kinds: BRR events go into the <code class="language-plaintext highlighter-rouge">Brr_cache</code>, while normal events take the original path into the relay log.</p>

<p><code class="language-plaintext highlighter-rouge">Brr_cache</code> is the replica-side storage for a BRR transaction; each BRR transaction has one <code class="language-plaintext highlighter-rouge">Brr_cache</code>. When the IO thread receives a BRR event, it uses the <code class="language-plaintext highlighter-rouge">brr_index</code> in the header to locate the matching <code class="language-plaintext highlighter-rouge">Brr_cache</code> (if it’s the first event, it creates a new <code class="language-plaintext highlighter-rouge">Brr_cache</code> and wakes a Brr Worker), writes the event into the <code class="language-plaintext highlighter-rouge">Brr_cache</code> temporary file, and updates the readable position.</p>

<p><code class="language-plaintext highlighter-rouge">Brr_rpl_info</code> manages these BRR transactions.</p>

<p>The <code class="language-plaintext highlighter-rouge">BRR Worker</code> threads are dedicated to applying BRR transactions. When idle, a Brr Worker picks a <code class="language-plaintext highlighter-rouge">Brr_cache</code> that hasn’t started being applied and makes itself its owner. From then on it is bound to that <code class="language-plaintext highlighter-rouge">Brr_cache</code>, looping to read and replay binlog events until it sees a <code class="language-plaintext highlighter-rouge">Gtid_log_event</code> (the primary has committed) or receives a <code class="language-plaintext highlighter-rouge">BRR_ROLLBACK_EVENT</code> (the primary rolled back).</p>

<h3>The gtid_executed Snapshot</h3>

<p>The <code class="language-plaintext highlighter-rouge">uncommitted</code> BRR transactions from the primary run in parallel on the replica alongside <code class="language-plaintext highlighter-rouge">already-committed</code> transactions. If a BRR transaction depends on an already-committed one, its binlog events must not start until that dependency has finished replaying on the replica; Otherwise you get escalating failures: a deadlock, then a broken replication channel, and in the worst case data inconsistency between primary and replica. Take this example:</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="k">INSERT</span> <span class="k">INTO</span> <span class="n">t1</span><span class="p">(</span><span class="n">pk</span><span class="p">,</span> <span class="n">c2</span><span class="p">)</span> <span class="k">VALUES</span><span class="p">(</span><span class="n">pk1</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
<span class="k">UPDATE</span> <span class="n">t1</span> <span class="k">SET</span> <span class="n">c2</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  <span class="c1">-- large transaction</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">UPDATE</code> is the large transaction, and it must not begin until the <code class="language-plaintext highlighter-rouge">INSERT</code> has finished replaying. If the <code class="language-plaintext highlighter-rouge">UPDATE</code> runs first, it fails when updating the <code class="language-plaintext highlighter-rouge">pk1</code> row because that row doesn’t exist yet.</p>

<p>BRR uses a <code class="language-plaintext highlighter-rouge">gtid_executed snapshot</code> to enforce these ordering dependencies. When a DDL or large transaction starts on the primary, the primary’s current <code class="language-plaintext highlighter-rouge">gtid_executed</code> captures every preceding transaction it saw. Once the replica’s <code class="language-plaintext highlighter-rouge">gtid_executed</code> has caught up to that value (that is, is a superset of it), all the transactions this one depends on have been replayed on the replica, and it is safe to start applying it.</p>

<p>To do this, BRR adds a new event type, <code class="language-plaintext highlighter-rouge">Brr_gtid_executed_log_event</code>, whose body holds a <code class="language-plaintext highlighter-rouge">gtid_executed</code> set. At specific moments the primary takes a gtid_executed snapshot and writes it to the BRR channel; when a replica Brr Worker reads the snapshot, it waits for all the GTIDs in it to finish before continuing.</p>

<h3>Realtime Replication of Large Transactions</h3>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-7-en.png" alt=""></p>

<h4>Creating and Updating a Brr_trx</h4>

<p>When a transaction runs on the primary, its binlog events go first into the Binlog Cache (an in-memory buffer backed by a temporary file). In MySQL, once the Binlog Cache fills its in-memory buffer, it spills to the temporary file.</p>

<p>BRR hooks in here: after each batch of events is written to the Binlog Cache, it checks the temporary file’s size. Once the file exceeds a certain size, BRR creates a <code class="language-plaintext highlighter-rouge">Brr_trx</code>, records the temporary file name and the current readable position, and registers it with <code class="language-plaintext highlighter-rouge">Brr_trx_manager</code>. From then on, every append to the Binlog Cache updates the <code class="language-plaintext highlighter-rouge">Brr_trx</code>’s <code class="language-plaintext highlighter-rouge">end_position</code> and wakes the Dump thread to send those events to the replica.</p>

<h4>Transmitting Binlog Events</h4>

<p>Before sending each batch of binlog events, the Dump thread emits a <code class="language-plaintext highlighter-rouge">Brr_gtid_executed_log_event</code> as that batch’s dependency snapshot, then sends the batch itself.</p>

<h4>Committing the Transaction</h4>

<p>For a large transaction, the binlog events sit in the <code class="language-plaintext highlighter-rouge">Brr_cache</code> temporary file — not yet relay log — until the Brr Worker reads the <code class="language-plaintext highlighter-rouge">Gtid_log_event</code>. When the primary finally commits, it sends the <code class="language-plaintext highlighter-rouge">Gtid_log_event</code> over the BRR channel, and the IO thread does two things:</p>

<ol>
  <li>Renames the <code class="language-plaintext highlighter-rouge">Brr_cache</code> temporary file into a relay log file. Based on the GTID, the primary’s Dump thread then skips sending this transaction, so its events aren’t shipped again as an ordinary transaction.</li>
  <li>Notifies the Brr Worker to read the <code class="language-plaintext highlighter-rouge">Gtid_log_event</code> and <code class="language-plaintext highlighter-rouge">Xid_log_event</code> and complete the commit.</li>
</ol>

<h4>Rolling Back the Transaction</h4>

<p>The rollback path is straightforward: when the primary rolls back, it sends a <code class="language-plaintext highlighter-rouge">BRR_ROLLBACK_EVENT</code> over the BRR channel; on receiving it, the replica’s IO thread sends a <code class="language-plaintext highlighter-rouge">KILL_QUERY</code> signal to the corresponding Brr Worker. The Brr Worker detects <code class="language-plaintext highlighter-rouge">KILL_QUERY</code>, rolls back the current transaction, cleans up, and moves on to the next <code class="language-plaintext highlighter-rouge">Brr_cache</code>.</p>

<p>Note that after being killed, a Brr Worker neither exits nor propagates the error to the SQL thread — unlike an ordinary Worker, which must halt all replication on an error. The reason: for an ordinary Worker the transaction has already committed on the primary, so if the replica gives up, the two diverge. A Brr Worker’s transaction, by contrast, runs concurrently with the primary, so a primary rollback is the normal path and the replica must roll back as well.</p>

<h3>Realtime Application of DDL</h3>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-repl-8-en.png" alt=""></p>

<h4>Creating a Brr_trx</h4>

<p>For large transactions, we decide whether a transaction is “large” by the total size of its binlog events in the Binlog Cache. DDL is trickier: some DDLs only touch metadata and finish almost instantly, while for DDLs that touch data the run time depends on how much data is involved and is hard to estimate accurately. So instead of predicting a DDL’s run time up front, we decide whether to realtime-replicate it <em>by whether its execution exceeds a timeout.</em></p>

<p>Every DDL creates a <code class="language-plaintext highlighter-rouge">Brr_trx</code>, but that <code class="language-plaintext highlighter-rouge">Brr_trx</code> isn’t sent to the replica right away. A DDL’s <code class="language-plaintext highlighter-rouge">Brr_trx</code> has a threshold — 1000 ms by default — and only when the DDL’s run time exceeds it does the Dump thread start sending the <code class="language-plaintext highlighter-rouge">Brr_trx</code>. If a DDL finishes quickly, within one second, its <code class="language-plaintext highlighter-rouge">Brr_trx</code> is silently discarded and the DDL ships to the replica over the ordinary binlog channel, exactly as if BRR were off.</p>

<p>A DDL’s <code class="language-plaintext highlighter-rouge">Brr_trx</code> is created during the DDL’s Prepare phase — that is, <em>after the DDL has acquired the MDL X lock</em> — because only with the X lock does the DDL have permission to operate on the table. Any conflicting operations have either already committed or must wait until the DDL releases the X lock or finishes.</p>

<h3>Two gtid_executed Snapshots</h3>

<p>An Online DDL runs in three phases: <code class="language-plaintext highlighter-rouge">Prepare</code>, <code class="language-plaintext highlighter-rouge">Execute</code>, and <code class="language-plaintext highlighter-rouge">Commit</code>. After Prepare, the MDL <code class="language-plaintext highlighter-rouge">X lock</code> is downgraded to an <code class="language-plaintext highlighter-rouge">S lock</code>, so during Execut, DML and DDL can run in parallel. During Commit, the <code class="language-plaintext highlighter-rouge">S lock</code> is upgraded back to an <code class="language-plaintext highlighter-rouge">X lock</code>; regaining the X lock means all those parallel DMLs have already committed. The replica must honor the same rule: those committed DMLs have to finish replaying before the replica can enter the Commit phase.</p>

<p>So realtime replication of an Online DDL has two points on the replica that must be synchronized: one before entering Prepare, and one before entering Commit. Correspondingly, the primary takes two <code class="language-plaintext highlighter-rouge">gtid_executed</code> snapshots — one after the DDL enters Prepare, and one after it enters Commit.</p>

<h3>Shipping Binlog Events Twice</h3>

<p>In the large-transaction section we saw that a large transaction is transmitted to the replica via BRR, and the copy in the binlog file is not shipped again. DDL is different: it ships twice — <em>once over BRR, and again as the binlog events in the binlog file.</em></p>

<p>A DDL’s <code class="language-plaintext highlighter-rouge">Query_log_event</code> is tiny, so shipping it twice costs almost nothing. Shipping it only once would force us into the large-transaction rename logic (renaming the <code class="language-plaintext highlighter-rouge">Brr_cache</code> temporary file into relay log), with all its edge cases. For DDL, simply shipping it twice and discarding the <code class="language-plaintext highlighter-rouge">Brr_cache</code> afterward is the simplest approach.</p>

<p>As for ordering, the Dump thread guarantees BRR events ship before ordinary events. That way the Brr Worker is sure to get the DDL first and start executing it; by the time the ordinary events reach the relay log, the Brr Worker is already applying the DDL.</p>

<p>When an ordinary Worker reads the DDL from the relay log, it checks whether the GTID is in <code class="language-plaintext highlighter-rouge">owned_gtids</code>. If it is (a Brr Worker is executing it), the ordinary Worker waits; once the Brr Worker commits, the GTID is added into <code class="language-plaintext highlighter-rouge">gtid_executed</code>. The ordinary Worker wakes and finds the GTID already in <code class="language-plaintext highlighter-rouge">gtid_executed</code>, so it skips the whole DDL.</p>

<p>If the Brr Worker rolled the DDL back, the GTID is removed from <code class="language-plaintext highlighter-rouge">owned_gtids</code> and never added to <code class="language-plaintext highlighter-rouge">gtid_executed</code>. The ordinary Worker then wakes and sees the transaction wasn’t executed. It runs the DDL normally — <em>the fallback path, equivalent to running with BRR off.</em></p>

<h2>Conclusion</h2>

<p>AliSQL’s Binlog Realtime Replication tackles the thorniest lag in MySQL binlog replication — lag from large transactions and DDL — by executing on the primary and replica in parallel. On top of that, we’ve made optimizations for the writeset mechanism, for massively concurrent workloads, and for the medium-sized transactions that batch jobs produce. Together, these have eliminated 95% of the replication lag in our production environment.</p>]]></content:encoded>
    <pubDate>Fri, 17 Jul 2026 09:30:00 +0000</pubDate>
    <dc:creator>Libing Song</dc:creator>
    <category>MySQL</category>
    <category>Replication</category>
    <category>DDL</category>
    <category>Large Transaction</category>
    <category>Replication Lag</category>
  </item>

  <item>
    <title>MySQL Major Version Upgrade Checklist – how to</title>
    <guid isPermaLink="false">https://kedar.nitty-witty.com/blog/?p=3608</guid>
    <link>https://kedar.nitty-witty.com/blog/mysql-major-version-upgrade-checklist-how-to?utm_source=rss&amp;amp;utm_medium=rss&amp;amp;utm_campaign=mysql-major-version-upgrade-checklist-how-to</link>
    <description>This article provides MySQL Major Version Upgrade Checklist along with video, one may follow to ease the upgarde task.
The post MySQL Major Version Upgrade Checklist – how to first appeared on Change Is Inevitable.</description>
    <content:encoded><![CDATA[<p>This article provides MySQL Major Version Upgrade Checklist along with video, one may follow to ease the upgarde task.</p>
The post <a href="https://kedar.nitty-witty.com/blog/mysql-major-version-upgrade-checklist-how-to">MySQL Major Version Upgrade Checklist – how to</a> first appeared on <a href="https://kedar.nitty-witty.com/blog">Change Is Inevitable</a>.]]></content:encoded>
    <pubDate>Thu, 16 Jul 2026 12:00:00 +0000</pubDate>
    <dc:creator>Kedar Vaijanapurkar</dc:creator>
    <category>MySQL</category>
    <category>MySQL Upgrade</category>
    <category>Download MySQL Checklist</category>
    <category>MySQL Checklist</category>
    <category>MySQL Major Version Upgrade</category>
    <category>MySQL upgrade</category>
    <category>Upgrade Checklist.</category>
  </item>

  <item>
    <title>Missed the May 2026 MySQL Contributor Summit? Watch Every Session On Demand</title>
    <guid isPermaLink="false">433689ff565fb6ee8601d653cdc73ce2</guid>
    <link>https://blogs.oracle.com/mysql/missed-the-may-2026-mysql-contributor-summit-watch-every-session-on-demand</link>
    <description>The inaugural MySQL Contributor Summit, held in May 2026, brought together Oracle engineers, customers, partners, and members of the open source community for a full day of technical collaboration focused on the future of MySQL. The Summit featured more than 20 sessions covering topics including AI integration, performance, observability, replication, developer experience, extensibility, and community […]</description>
    <pubDate>Thu, 16 Jul 2026 06:00:00 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>MySQL Contributor Summit</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>Binlog Transmission Optimization for Large MySQL Transactions</title>
    <guid isPermaLink="false">https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/</guid>
    <link>https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission-en/</link>
    <description>
  This article is also available in Chinese: 中文版. Browse all English articles.


Large transactions are a notorious problem in MySQL: they cause not only replication lag but also stability problems. A previous article, MySQL Large Transaction Commit Optimization, covered the problems a large transaction causes at commit time and the optimizations we made in AliSQL. This article looks at the problems a large transaction causes during semi-synchronous replication, and how AliSQL solves them.

In MySQL Large Transaction Commit Optimization we noted that writing the binlog when a large transaction commits can produce strange slow queries like these:




  An INSERT that normally runs in an instant took 1.3s, yet the slow-query log shows no long lock wait.
  Every statement in a multi-statement transaction had already finished, yet the COMMIT alone took 1.3s.


Besides writing the binlog at commit, transmitting a large transaction’s binlog during semi-synchronous replication produces the same symptom. Below is a simulated test: we used sysbench oltp_write_only to simulate a normal write workload, then in the background, a transaction that generated 2 GB of binlog events (with the large-transaction commit optimization already applied). When the large transaction commits, writes drop to zero and don’t recover until semisync times out.



Root Cause



The figure above shows the commit flow of a transaction under semi-synchronous replication:


  On commit, the transaction runs two-phase commit, starting with Prepare.
  It then writes its binlog events to the binlog file.
  After writing the binlog, it waits for its binlog events to be sent to the replica (after_sync mode).
  The binlog Dump thread then sends the transaction’s binlog events to the replica.
  The replica’s IO thread receives these events and writes them into the relay log file.
  Once it has the complete transaction, the IO thread sends the primary an acknowledgment saying it has all of the transaction’s binlog events. The ack is expressed as a binlog file name and offset. In the figure, Trx_n’s binlog end offset is 530, so the replica’s IO thread sends master-bin.000001:530 to the primary, meaning every transaction before master-bin.000001:530 has been received.
  On the primary, the Semisync Ack Receiver thread receives the ack and, based on the offset, wakes the corresponding transaction.
  Once woken, the transaction finishes committing and returns OK to the user.


There is only one Dump thread between the primary and the replica, and it transmits binlog events in the order they were written to the binlog. The replica’s IO thread likewise writes received events into the relay log in that same order before acknowledging the primary. So a later transaction can’t be sent until the earlier one has finished. If the current transaction has a huge number of binlog events, sending them takes a very long time, and a later transaction — however small — has to wait. That wait includes not just its own transmission time but the large transaction’s ahead of it. Hence the slow-log symptom: a small transaction suddenly becomes very slow.

To cope with this, MySQL provides the rpl_semi_sync_master_timeout parameter, which sets how long a transaction waits for an ack; once the wait exceeds rpl_semi_sync_master_timeout, replication automatically falls back to asynchronous. We can set this to a small value to avoid the severe case where a large transaction makes the whole instance unwritable.

An RPO = 0 Design Based on Semi-Synchronous Replication

Because a transaction under semi-synchronous replication can’t commit until its binlog has been replicated to a replica, it’s natural to think of using semisync to build an RPO = 0 (zero data loss) consistency solution.



This architecture needs two replicas, and semisync guarantees that a transaction commits only after it receives an ack from at least one of them.


  If the primary crashes, the data has been replicated to at least one replica.
  If one replica becomes unavailable, cluster availability is unaffected.


To guarantee RPO = 0, semisync must never fall back to async. MySQL semisync has two points where it can degrade to async:


  After a crash and restart, transactions already written to the binlog are committed automatically, even though they may not yet have been replicated to a replica.
  Once the wait reaches rpl_semi_sync_master_timeout, it degrades to async.


The former can’t be controlled from outside — it requires changing MySQL’s code. The latter requires setting rpl_semi_sync_master_timeout to a very large value so semisync never degrades. Large transactions are clearly the thorniest issue in an RPO = 0 design: the moment one appears, it makes the whole cluster unwritable, so the design must take countermeasures. A DBA with strong influence over the application can arrange for it to avoid large transactions; but at a large company, with sprawling and complex applications, eliminating them entirely is hard, and an RDS provider has no control over its users at all. In practice, availability usually matters far more than consistency, so many designs adopt a temporary-degradation strategy, falling back to async whenever a large transaction appears.

Realtime Transmission of Large Transactions

In AliSQL we designed a realtime-transmission mechanism to solve the problems large-transaction transmission causes; with it, there is no need to degrade semisync to async.



The realtime large-transaction transmission mechanism reads a transaction’s binlog events out of the Binlog Cache temporary file and sends them to the replica while the transaction is still doing DML. The key steps:


  During DML execution, once the binlog events of a transaction has produced exceed a certain amount, the transaction is registered in the large-transaction list and handled as a large transaction.
  Based on that list, the binlog Dump thread reads the large transaction’s binlog temporary file and sends its contents to the replica. The large transaction’s binlog events and the events from the binlog file are sent interleaved, with flow control on the large transaction: events from the binlog file take priority, so the transaction currently committing is unaffected.
  The large transaction’s binlog events carry a special marker and extra information. When the replica’s IO thread receives them, it stores them in a temporary file called the Relay Log Cache.
  At commit, once the Dump thread has sent all the binlog events, it sends a Gtid_event to the replica.
  On receiving the Gtid_event, the replica knows it has all of the transaction’s binlog events, and it turns the Relay Log Cache into a Relay Log file.
  When several large transactions run at once, the mechanism can transmit them all in real time simultaneously.


From these steps we can see: a large transaction’s binlog events are sent to the replica bit by bit as they are produced, so at commit only the Gtid_event needs to be sent. The amount of data sent at commit is therefore tiny, and it no longer blocks other transactions’ binlog-event transmission. It also removes the sudden burst of network traffic, reducing congestion.

Relay Log Cache

The realtime-transmission mechanism follows directly from the large-transaction commit optimization and reuses parts of its implementation. A transaction’s binlog events are produced and accumulate during DML execution; once they exceed binlog_cache_size, they are written to a temporary file, and at commit they are written to the binlog file all at once. In MySQL Large Transaction Commit Optimization, a large transaction’s temporary file is automatically turned into a new binlog file, which eliminates the problems that large-transaction commit causes.

Realtime large-transaction transmission reuses this logic, reserving some space at the head of the Relay Log Cache. When the Relay Log Cache is turned into a Relay Log file, that head space is filled with the special binlog events a relay log needs, such as the Format_description_event.



Handling Failures

A large transaction runs for a long time, so any failure along the way has to be handled.


  If the large transaction rolls back on the primary, the binlog Dump thread sends a rollback to the replica; on receiving it, the IO thread destroys the corresponding Relay Log Cache.
  If the IO thread’s connection to the primary drops, or a STOP SLAVE is issued, the IO thread destroys all Relay Log Caches. After reconnecting, it restarts realtime replication of the large transaction.


Results

We used sysbench oltp_write_only to simulate a normal write workload, then committed, in the background, a transaction that generated 2 GB of binlog events. The results:



With realtime replication, the application’s writes run smoothly, with no more drops to zero.

Conclusion

In MySQL’s semi-synchronous replication architecture, large transactions are a classic problem. To keep them from destabilizing the instance, people have had to work hard to eliminate large transactions from their applications, or simply let replication degrade to async. Realtime large-transaction transmission moves the transmission of a large transaction’s binlog events from the commit phase up to the execution phase, sending each event to the replica as soon as it is produced. This avoids blocking other transactions’ binlog-event transmission for a long time at commit, and avoids network congestion. When a large transaction comes along, semisync no longer needs to degrade to async — clearing away a thorny obstacle on the path to a semisync-based RPO = 0 design.</description>
    <content:encoded><![CDATA[<blockquote class="prompt-tip">
  <p>This article is also available in Chinese: <a href="https://songlibing.github.io/posts/mysql-large-transaction-binlog-transmission/">中文版</a>. Browse <a href="https://songlibing.github.io/english/">all English articles</a>.</p>
</blockquote>

<p>Large transactions are a notorious problem in MySQL: they cause not only replication lag but also stability problems. A previous article, <em><a href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en">MySQL Large Transaction Commit Optimization</a></em>, covered the problems a large transaction causes at commit time and the optimizations we made in AliSQL. This article looks at the problems a large transaction causes during semi-synchronous replication, and how AliSQL solves them.</p>

<p>In <em><a href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en">MySQL Large Transaction Commit Optimization</a></em> we noted that writing the binlog when a large transaction commits can produce strange slow queries like these:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-1.webp" alt=""></p>

<ul>
  <li>An <code class="language-plaintext highlighter-rouge">INSERT</code> that normally runs in an instant took <code class="language-plaintext highlighter-rouge">1.3s</code>, yet the slow-query log shows no long lock wait.</li>
  <li>Every statement in a multi-statement transaction had already finished, yet the <code class="language-plaintext highlighter-rouge">COMMIT</code> alone took <code class="language-plaintext highlighter-rouge">1.3s</code>.</li>
</ul>

<p>Besides writing the binlog at commit, transmitting a large transaction’s binlog during semi-synchronous replication produces the same symptom. Below is a simulated test: we used sysbench <code class="language-plaintext highlighter-rouge">oltp_write_only</code> to simulate a normal write workload, then in the background, a transaction that generated 2 GB of binlog events (with the large-transaction commit optimization already applied). When the large transaction commits, writes drop to zero and don’t recover until semisync times out.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-2-en.png" alt=""></p>

<h2>Root Cause</h2>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-3-en.png" alt=""></p>

<p>The figure above shows the commit flow of a transaction under semi-synchronous replication:</p>

<ul>
  <li>On commit, the transaction runs two-phase commit, starting with Prepare.</li>
  <li>It then writes its binlog events to the binlog file.</li>
  <li>After writing the binlog, it waits for its binlog events to be sent to the replica (<code class="language-plaintext highlighter-rouge">after_sync</code> mode).</li>
  <li>The binlog Dump thread then sends the transaction’s binlog events to the replica.</li>
  <li>The replica’s IO thread receives these events and writes them into the relay log file.</li>
  <li>Once it has the complete transaction, the IO thread sends the primary an acknowledgment saying it has all of the transaction’s binlog events. The ack is expressed as a binlog file name and offset. In the figure, Trx_n’s binlog end offset is <code class="language-plaintext highlighter-rouge">530</code>, so the replica’s IO thread sends <code class="language-plaintext highlighter-rouge">master-bin.000001:530</code> to the primary, meaning every transaction before <code class="language-plaintext highlighter-rouge">master-bin.000001:530</code> has been received.</li>
  <li>On the primary, the Semisync Ack Receiver thread receives the ack and, based on the offset, wakes the corresponding transaction.</li>
  <li>Once woken, the transaction finishes committing and returns OK to the user.</li>
</ul>

<p>There is only one Dump thread between the primary and the replica, and it transmits binlog events in the order they were written to the binlog. The replica’s IO thread likewise writes received events into the relay log in that same order before acknowledging the primary. So a later transaction can’t be sent until the earlier one has finished. If the current transaction has a huge number of binlog events, sending them takes a very long time, and a later transaction — however small — has to wait. That wait includes not just its own transmission time but the large transaction’s ahead of it. Hence the slow-log symptom: a small transaction suddenly becomes very slow.</p>

<p>To cope with this, MySQL provides the <code class="language-plaintext highlighter-rouge">rpl_semi_sync_master_timeout</code> parameter, which sets how long a transaction waits for an ack; once the wait exceeds <code class="language-plaintext highlighter-rouge">rpl_semi_sync_master_timeout</code>, replication automatically falls back to asynchronous. We can set this to a small value to avoid the severe case where a large transaction makes the whole instance unwritable.</p>

<h2>An RPO = 0 Design Based on Semi-Synchronous Replication</h2>

<p>Because a transaction under semi-synchronous replication can’t commit until its binlog has been replicated to a replica, it’s natural to think of using semisync to build an RPO = 0 (zero data loss) consistency solution.</p>

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

<p>This architecture needs two replicas, and semisync guarantees that a transaction commits only after it receives an ack from at least one of them.</p>

<ul>
  <li>If the primary crashes, the data has been replicated to at least one replica.</li>
  <li>If one replica becomes unavailable, cluster availability is unaffected.</li>
</ul>

<p>To guarantee RPO = 0, semisync must never fall back to async. MySQL semisync has two points where it can degrade to async:</p>

<ul>
  <li>After a crash and restart, transactions already written to the binlog are committed automatically, even though they may not yet have been replicated to a replica.</li>
  <li>Once the wait reaches <code class="language-plaintext highlighter-rouge">rpl_semi_sync_master_timeout</code>, it degrades to async.</li>
</ul>

<p>The former can’t be controlled from outside — it requires changing MySQL’s code. The latter requires setting <code class="language-plaintext highlighter-rouge">rpl_semi_sync_master_timeout</code> to a very large value so semisync never degrades. Large transactions are clearly the thorniest issue in an RPO = 0 design: the moment one appears, it makes the whole cluster unwritable, so the design must take countermeasures. A DBA with strong influence over the application can arrange for it to avoid large transactions; but at a large company, with sprawling and complex applications, eliminating them entirely is hard, and an RDS provider has no control over its users at all. In practice, availability usually matters far more than consistency, so many designs adopt a temporary-degradation strategy, falling back to async whenever a large transaction appears.</p>

<h2>Realtime Transmission of Large Transactions</h2>

<p>In AliSQL we designed a realtime-transmission mechanism to solve the problems large-transaction transmission causes; with it, there is no need to degrade semisync to async.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-5-en.png" alt=""></p>

<p>The realtime large-transaction transmission mechanism reads a transaction’s binlog events out of the Binlog Cache temporary file and sends them to the replica while the transaction is still doing DML. The key steps:</p>

<ul>
  <li>During DML execution, once the binlog events of a transaction has produced exceed a certain amount, the transaction is registered in the large-transaction list and handled as a large transaction.</li>
  <li>Based on that list, the binlog Dump thread reads the large transaction’s binlog temporary file and sends its contents to the replica. The large transaction’s binlog events and the events from the binlog file are sent interleaved, with flow control on the large transaction: events from the binlog file take priority, so the transaction currently committing is unaffected.</li>
  <li>The large transaction’s binlog events carry a special marker and extra information. When the replica’s IO thread receives them, it stores them in a temporary file called the <code class="language-plaintext highlighter-rouge">Relay Log Cache</code>.</li>
  <li>At commit, once the Dump thread has sent all the binlog events, it sends a <code class="language-plaintext highlighter-rouge">Gtid_event</code> to the replica.</li>
  <li>On receiving the <code class="language-plaintext highlighter-rouge">Gtid_event</code>, the replica knows it has all of the transaction’s binlog events, and it turns the <code class="language-plaintext highlighter-rouge">Relay Log Cache</code> into a <code class="language-plaintext highlighter-rouge">Relay Log</code> file.</li>
  <li>When several large transactions run at once, the mechanism can transmit them all in real time simultaneously.</li>
</ul>

<p>From these steps we can see: <em>a large transaction’s binlog events are sent to the replica bit by bit as they are produced, so at commit only the <code class="language-plaintext highlighter-rouge">Gtid_event</code> needs to be sent.</em> The amount of data sent at commit is therefore tiny, and it no longer blocks other transactions’ binlog-event transmission. It also removes the sudden burst of network traffic, reducing congestion.</p>

<h3>Relay Log Cache</h3>

<p>The realtime-transmission mechanism follows directly from the large-transaction commit optimization and reuses parts of its implementation. A transaction’s binlog events are produced and accumulate during DML execution; once they exceed <code class="language-plaintext highlighter-rouge">binlog_cache_size</code>, they are written to a temporary file, and at commit they are written to the binlog file all at once. In <em>MySQL Large Transaction Commit Optimization</em>, a large transaction’s temporary file is automatically turned into a new binlog file, which eliminates the problems that large-transaction commit causes.</p>

<p>Realtime large-transaction transmission reuses this logic, reserving some space at the head of the Relay Log Cache. When the Relay Log Cache is turned into a Relay Log file, that head space is filled with the special binlog events a relay log needs, such as the <code class="language-plaintext highlighter-rouge">Format_description_event</code>.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-6-en.png" alt=""></p>

<h3>Handling Failures</h3>

<p>A large transaction runs for a long time, so any failure along the way has to be handled.</p>

<ul>
  <li>If the large transaction rolls back on the primary, the binlog Dump thread sends a <code class="language-plaintext highlighter-rouge">rollback</code> to the replica; on receiving it, the IO thread destroys the corresponding Relay Log Cache.</li>
  <li>If the IO thread’s connection to the primary drops, or a <code class="language-plaintext highlighter-rouge">STOP SLAVE</code> is issued, the IO thread destroys all Relay Log Caches. After reconnecting, it restarts realtime replication of the large transaction.</li>
</ul>

<h2>Results</h2>

<p>We used sysbench <code class="language-plaintext highlighter-rouge">oltp_write_only</code> to simulate a normal write workload, then committed, in the background, a transaction that generated 2 GB of binlog events. The results:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-binlog-7-en.png" alt=""></p>

<p>With realtime replication, the application’s writes run smoothly, with no more drops to zero.</p>

<h2>Conclusion</h2>

<p>In MySQL’s semi-synchronous replication architecture, large transactions are a classic problem. To keep them from destabilizing the instance, people have had to work hard to eliminate large transactions from their applications, or simply let replication degrade to async. <em>Realtime large-transaction transmission</em> moves the transmission of a large transaction’s binlog events from the commit phase up to the execution phase, sending each event to the replica as soon as it is produced. This avoids blocking other transactions’ binlog-event transmission for a long time at commit, and avoids network congestion. When a large transaction comes along, semisync no longer needs to degrade to async — clearing away a thorny obstacle on the path to a semisync-based RPO = 0 design.</p>]]></content:encoded>
    <pubDate>Thu, 16 Jul 2026 02:00:00 +0000</pubDate>
    <dc:creator>Libing Song</dc:creator>
    <category>MySQL</category>
    <category>Replication</category>
    <category>Binlog</category>
    <category>Large Transaction</category>
    <category>Semisync</category>
  </item>

  <item>
    <title>Inside MySQL 9.7 LTS Features</title>
    <guid isPermaLink="false">https://www.percona.com/?p=50225</guid>
    <link>https://www.percona.com/blog/inside-mysql-9-7-lts-features/</link>
    <description>MySQL 9.7, a Long-Term Support (LTS) release, incorporates a variety of potential features spanning across multiple technical domains. This article covers some of the primary features introduced and evaluates their practical utility within the MySQL database environment.
Following the End-of-Life (EOL) status of MySQL 8.0, this subsequent LTS release is designed to provide enhanced stability alongside significant architectural innovations.
Let’s discuss each of these features below with some examples and usage.
Flow-control monitoring in Group Replication
Flow control monitoring has been improved and provides more granularity by introducing the additional status variables listed below.

Gr_flow_control_throttle_count : It denotes the number of transactions that have been throttled.
Gr_flow_control_throttle_time_sum :It denotes the time in microseconds that transactions have been throttled.
Gr_flow_control_throttle_active_count :It denotes the number of transactions currently being throttled.
Gr_flow_control_throttle_last_throttle_timestamp : It denotes the most recent date and time that a transaction was throttled.

To use these status variables, we must install the “Group Replication Flow Control Statistics”  component.
mysql&amp;gt; Install component 'file://component_group_replication_flow_control_stats';After the component is installed, the statistics will be visible.mysql&amp;gt; SELECT * FROM performance_schema.global_status WHERE VARIABLE_NAME LIKE 'Gr_flow_control%';
+--------------------------------------------------+----------------+
| VARIABLE_NAME                                    | VARIABLE_VALUE |
+--------------------------------------------------+----------------+
| Gr_flow_control_throttle_active_count            | 0              |
| Gr_flow_control_throttle_count                   | 0              |
| Gr_flow_control_throttle_last_throttle_timestamp |                |
| Gr_flow_control_throttle_time_sum                | 0              |
+--------------------------------------------------+----------------+
Multi-threaded applier extended statistics
We now have additional verbosity for the Applier threads for both Asynchronous and Group Replication topologies. This means we can get more details of the transactions or potential misbehaviours during the transactions applier stage. This feature is particularly useful for troubleshooting performance bottlenecks in multi-threaded replication environments, where understanding the specific cause of lag can be challenging.
This requires installing the “Replication Applier Metrics” component.mysql&amp;gt; Install component 'file://component_replication_applier_metrics';Upon successful installation of the requisite component, the performance schema tables facilitate tracking of transaction details and various performance metrics during the replication applier phase. For instance, monitoring the table “replication_applier_metrics” enables observing channel-specific operations.mysql&amp;gt; SELECT * FROM performance_schema.replication_applier_metrics where CHANNEL_NAME='group_replication_applier'\G;
*************************** 1. row ***************************
                                CHANNEL_NAME: group_replication_applier
                  TOTAL_ACTIVE_TIME_DURATION: 0
                          LAST_APPLIER_START: 0000-00-00 00:00:00
                TRANSACTIONS_COMMITTED_COUNT: 0
                  TRANSACTIONS_ONGOING_COUNT: 0
                  TRANSACTIONS_PENDING_COUNT: 0
       TRANSACTIONS_COMMITTED_SIZE_BYTES_SUM: 0
    TRANSACTIONS_ONGOING_FULL_SIZE_BYTES_SUM: 0
TRANSACTIONS_ONGOING_PROGRESS_SIZE_BYTES_SUM: 0
         TRANSACTIONS_PENDING_SIZE_BYTES_SUM: NULL
                      EVENTS_COMMITTED_COUNT: 0
            WAITS_FOR_WORK_FROM_SOURCE_COUNT: 0
         WAITS_FOR_WORK_FROM_SOURCE_SUM_TIME: 0
            WAITS_FOR_AVAILABLE_WORKER_COUNT: 0
         WAITS_FOR_AVAILABLE_WORKER_SUM_TIME: 0
      WAITS_COMMIT_SCHEDULE_DEPENDENCY_COUNT: 0
   WAITS_COMMIT_SCHEDULE_DEPENDENCY_SUM_TIME: 0
         WAITS_FOR_WORKER_QUEUE_MEMORY_COUNT: 0
      WAITS_FOR_WORKER_QUEUE_MEMORY_SUM_TIME: 0
              WAITS_WORKER_QUEUES_FULL_COUNT: 0
           WAITS_WORKER_QUEUES_FULL_SUM_TIME: 0
             WAITS_DUE_TO_COMMIT_ORDER_COUNT: 0
          WAITS_DUE_TO_COMMIT_ORDER_SUM_TIME: 0
        TIME_TO_READ_FROM_RELAY_LOG_SUM_TIME: 0In addition to aggregate metrics, MySQL 9.7 provides a way to inspect the progress of individual worker threads via monitoring stats in the “replication_applier_progress_by_worker” table. This level of detail helps administrators identify if a single transaction is monopolising a specific worker, causing overall replication delay.mysql&amp;gt; SELECT * FROM performance_schema.replication_applier_progress_by_worker\G;
*************************** 1. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 0
                             THREAD_ID: 62
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 2. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 1
                             THREAD_ID: 63
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 3. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 2
                             THREAD_ID: 64
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 4. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 3
                             THREAD_ID: 65
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
Automatic eviction &amp;amp; rejoin
The Group Replication resource manager now provides auto-eviction functionality, which we can configure using the available options. This basically ensures that the unhealthy node is removed from the Group to maintain the cluster’s high availability and overall performance.
This requires installing the “group replication resource manager” component.mysql&amp;gt; INSTALL COMPONENT 'file://component_group_replication_resource_manager';Once the component is available,  we can use various options to decide the node expulsion policy.
1) Applier channel
We can set the applier channel replication lag threshold values using the configuration parameter below.mysql&amp;gt; set global group_replication_resource_manager.applier_channel_lag = &amp;lt;value&amp;gt;;If lag exceeds  “applier_channel_lag” threshold 10 times or more in a row, this server is expelled from the group. The status variable below is used for tracking the lag exceed rate.mysql&amp;gt; show global status like 'Gr_resource_manager_applier_channel_lag';
+-----------------------------------------+-------+
| Variable_name                           | Value |
+-----------------------------------------+-------+
| Gr_resource_manager_applier_channel_lag | 0     |
+-----------------------------------------+-------+
2) Recovery Channel
Similarly, we can define a threshold for the group member recovery process to attempt to rejoin the cluster. mysql&amp;gt; set global group_replication_resource_manager.recovery_channel_lag = &amp;lt;value&amp;gt;;If the secondary’s recovery lag exceeds “recovery_channel_lag”, 10 times or more in succession, the server is expelled from the group. mysql show global status like 'Gr_resource_manager_recovery_channel_lag';
+------------------------------------------+-------+
| Variable_name                            | Value |
+------------------------------------------+-------+
| Gr_resource_manager_recovery_channel_lag | 0     |
+------------------------------------------+-------+3) Memory/Resource Usage
We can also define an expelled condition based on the group member’s memory or resource usage %.mysql&amp;gt; set global group_replication_resource_manager.memory_used_limit = 10;If the memory usage exceeds memory_used_limit % by 10 or more consecutive times, the node will be expelled from the group.mysql&amp;gt; show global status like 'Gr_resource_manager_memory_used%';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| Gr_resource_manager_memory_used | 78    |
+---------------------------------+-------+
1 row in set (0.002 sec)In addition to the discussed options above, we can also track various server status variables to monitor group replication and the resource manager component.mysql&amp;gt; select * from performance_schema.global_status where variable_name in ('Gr_resource_manager_applier_channel_threshold_hits','Gr_resource_manager_applier_channel_eviction_timestamp','Gr_resource_manager_recovery_channel_threshold_hits','Gr_resource_manager_recovery_channel_eviction_timestamp','Gr_resource_manager_memory_threshold_hits','Gr_resource_manager_memory_eviction_timestamp');
+---------------------------------------------------------+----------------+
| VARIABLE_NAME                                           | VARIABLE_VALUE |
+---------------------------------------------------------+----------------+
| Gr_resource_manager_applier_channel_eviction_timestamp  |                |
| Gr_resource_manager_applier_channel_threshold_hits      | 0              |
| Gr_resource_manager_memory_eviction_timestamp           |                |
| Gr_resource_manager_memory_threshold_hits               | 6703           |
| Gr_resource_manager_recovery_channel_eviction_timestamp |                |
| Gr_resource_manager_recovery_channel_threshold_hits     | 0              |
+---------------------------------------------------------+----------------+
6 rows in set (0.003 sec)The expelled node can attempt to automatically rejoin based on the value of the group_replication_autorejoin_tries variable.mysql&amp;gt; show variables like '%group_replication_autorejoin_tries%';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| group_replication_autorejoin_tries | 3     |
+------------------------------------+-------+
1 row in set (0.006 sec)If the node cannot join, it will perform the behaviour specified in the group_replication_exit_state_action variable.mysql&amp;gt; show variables like '%group_replication_exit_state_action%';
+-------------------------------------+--------------+
| Variable_name                       | Value        |
+-------------------------------------+--------------+
| group_replication_exit_state_action | OFFLINE_MODE |
+-------------------------------------+--------------+
1 row in set (0.005 sec)After a server is evicted from the group (for whatever reason), it gets a grace period (group_replication_resource_manager) when it rejoins. During this period, the Resource Manager won’t immediately kick it out again, even if it’s still lagging or breaching the defined threshold as discussed above.mysql&amp;gt; show variables like '%group_replication_resource_manager.quarantine_time%';
+----------------------------------------------------+-------+
| Variable_name                                      | Value |
+----------------------------------------------------+-------+
| group_replication_resource_manager.quarantine_time | 3600  |
+----------------------------------------------------+-------+
Up-to-date aware Primary election
The Primary election process is more mature and cohesive. The Group Replication Manager now uses the most up-to-date status as a criterion for selecting the new primary.
Here is how the Group Replication Manager performs the most up-to-date primary selection prior to MySQL v9.7.

The lowest MySQL version is checked for each member.
If more than one member is running the lowest MySQL Server version, each member’s weight is determined by the “group_replication_member_weight” system variable.
If there is more than one member running the lowest MySQL Server version, and also more than one of those members has the highest member weight, the third factor considered is the lexicographical order of the generated server UUIDs “server_uuid” of each group member. The member with the lowest server UUID is chosen as the new primary.


In MySQL version 9.7, “group_replication_elect_prefers_most_updated” was introduced, so the failover will be determined by how many transactions are in the secondary backlog. Basically the secondary with the least backlog will be selected as Primary.
Now, it will consider the “most up-to-date” node first, then “weight” and then “UUID”. 
To use “group_replication_elect_prefers_most_updated”, we need to install the “Group Replication Primary Election” component listed below on each Group Member.mysql&amp;gt; Install component 'file://component_group_replication_elect_prefers_most_updated';By default, the most up-to-date group member selection is enabled. We need to make sure it’s enabled on all Group Members. mysql&amp;gt; select @@group_replication_elect_prefers_most_updated.enabled;
+--------------------------------------------------------+
| @@group_replication_elect_prefers_most_updated.enabled |
+--------------------------------------------------------+
|                                                      1 |
+--------------------------------------------------------+
1 row in set (0.007 sec)In the event that a new primary is elected via the most up-to-date selection mechanism, this metric represents the transaction processing differential between the newly designated primary and the secondary node with the highest level of synchronisation.mysql&amp;gt; show status like 'Gr_latest_primary_election_by_most_uptodate_members_trx_delta';
+---------------------------------------------------------------+-------+
| Variable_name                                                 | Value |
+---------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_members_trx_delta | 0     |
+---------------------------------------------------------------+-------+Also, we can track the timestamp of the most recent primary election on the most up-to-date node.mysql&amp;gt; show status like 'Gr_latest_primary_election_by_most_uptodate_member_timestamp';
+--------------------------------------------------------------+-------+
| Variable_name                                                | Value |
+--------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_member_timestamp |       |
+--------------------------------------------------------------+-------+
1 row in set (0.005 sec)The database logs also tell exactly what criteria the primary member selected during failover.2026-06-14T10:04:02.243809Z 0 [System] [MY-015575] [Repl] Plugin group_replication reported: 'Member with uuid 00021702-2222-2222-2222-222222222222 was elected primary since it was the most up-to-date member with 2755 transactions more than second most up-to-date member 00021703-3333-3333-3333-333333333333. In case of a tie member weight and then uuid lexical order was used over the most updated members.'
MySQL JSON duality views
With the introduction of JSON duality views, we can leverage a single unified JSON document for both relational and hierarchical JSON data. This provides a common, structured JSON format for the application, allowing it to perform both read and write operations.
Let’s see a quick scenario below on how it works.
Below are two relational tables from which we obtain aggregated information in JSON format. mysql&amp;gt; CREATE TABLE products (
  product_id INT PRIMARY KEY,
  product_type VARCHAR(100)
);

mysql&amp;gt; CREATE TABLE products_details (
  product_detail_id INT PRIMARY KEY,
  product_id INT,
  name VARCHAR(100),
  active varchar(10)
);mysql&amp;gt; INSERT INTO products (product_id,product_type) VALUES (1,'IT'), (2,'TEL');
mysql&amp;gt; INSERT INTO products_details (product_detail_id,product_id,name,active) VALUES (1,1,'Laptop','Yes'), (2,2,'Mobile','Yes');Here is the exact Json View which fetch the columns from the relation table based on the join condition. Each of those relational table columns is mapped with a JSON data structure (_id,v_product_type,v_product_type ), and the complete details of the product details table are fetched into the (product) array.mysql&amp;gt; CREATE JSON RELATIONAL DUALITY VIEW view_product AS
SELECT JSON_DUALITY_OBJECT( WITH(INSERT,UPDATE,DELETE)
    '_id': product_id,
    'v_product_type': product_type,
    'product': (
        SELECT JSON_ARRAYAGG(
            JSON_DUALITY_OBJECT(WITH(INSERT,UPDATE,DELETE)
                'v_product_detail_id': product_detail_id,
                'v_name': name,
                'v_active': active
                
            )
        )
        FROM products_details
        WHERE products_details.product_id = products.product_id
    )
)
FROM products;mysql&amp;gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {&quot;_id&quot;: 1, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Laptop&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 1}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;313642c2aa24f0571264332afa140715&quot;}, &quot;v_product_type&quot;: &quot;IT&quot;}  |
| {&quot;_id&quot;: 2, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Mobile&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 2}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;3d229ada02ac660f9f6cac994b44831a&quot;}, &quot;v_product_type&quot;: &quot;TEL&quot;} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.002 sec)Once the duality view is created, we can perform both read/write operations.
Reading the duality viewmysql&amp;gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {&quot;_id&quot;: 1, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Laptop&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 1}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;313642c2aa24f0571264332afa140715&quot;}, &quot;v_product_type&quot;: &quot;IT&quot;}  |
| {&quot;_id&quot;: 2, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Mobile&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 2}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;3d229ada02ac660f9f6cac994b44831a&quot;}, &quot;v_product_type&quot;: &quot;TEL&quot;} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+Writing the underlying table in the duality viewmysql&amp;gt; UPDATE view_product
SET data = JSON_SET(
    data,
    '$.product[0].v_name',
    'Notepad'
)
WHERE JSON_EXTRACT(data, '$._id') = 1;mysql&amp;gt; select * from products_details;
+-------------------+------------+---------+--------+
| product_detail_id | product_id | name    | active |
+-------------------+------------+---------+--------+
|                 1 |          1 | Notepad | Yes    |
|                 2 |          2 | Mobile  | Yes    |
+-------------------+------------+---------+--------+After performing the above write operations, we can see that the view now shows the updated data.mysql &amp;gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {&quot;_id&quot;: 1, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Notepad&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 1}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;72c4368420cdc698842d0ab4bd9315ab&quot;}, &quot;v_product_type&quot;: &quot;IT&quot;} |
| {&quot;_id&quot;: 2, &quot;product&quot;: [{&quot;v_name&quot;: &quot;Mobile&quot;, &quot;v_active&quot;: &quot;Yes&quot;, &quot;v_product_detail_id&quot;: 2}], &quot;_metadata&quot;: {&quot;etag&quot;: &quot;3d229ada02ac660f9f6cac994b44831a&quot;}, &quot;v_product_type&quot;: &quot;TEL&quot;} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Hypergraph Optimizer
With the Hypergraph Optimiser, we now have more advanced optimisation for complex queries and a broader set of Join plans than the older traditional method, missing earlier. By using “Join hypergraph”, the optimiser now has better reach to all tables in the join condition.
Hypergraph Optimiser is OFFmysql&amp;gt; SET optimizer_switch='hypergraph_optimizer=off';mysql&amp;gt; SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;Output:| 498870 | 119 |
| 498729 | 119 |
| 497668 | 119 |
| 498076 | 119 |
+--------+-----+
100 rows in set (4.000 sec)Explain output:-&amp;gt; Limit: 100 row(s)
    -&amp;gt; Sort: cnt DESC, limit input to 100 row(s) per chunk
        -&amp;gt; Stream results  (cost=1.22e+6 rows=175136)
            -&amp;gt; Group aggregate: count(0)  (cost=1.22e+6 rows=175136)
                -&amp;gt; Nested loop inner join  (cost=1.1e+6 rows=493200)
                    -&amp;gt; Nested loop inner join  (cost=601547 rows=493200)
                        -&amp;gt; Filter: (t1.k between 200000 and 500000)  (cost=99122 rows=493200)
                            -&amp;gt; Covering index range scan on t1 using k_1 over (200000 &amp;lt;= k &amp;lt;= 500000)  (cost=99122 rows=493200)
                        -&amp;gt; Single-row covering index lookup on t2 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)
                    -&amp;gt; Single-row covering index lookup on t3 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)Hypergraph Optimiser is ONmysql&amp;gt; SET optimizer_switch='hypergraph_optimizer=on';mysql&amp;gt; SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;Output:| 499721 | 119 |
| 499052 | 119 |
| 498870 | 119 |
| 498384 | 119 |
+--------+-----+
100 rows in set (0.498 sec)Explain output:-&amp;gt; Sort: cnt DESC, limit input to 100 row(s) per chunk  (cost=1.96e+6..1.96e+6 rows=100)
    -&amp;gt; Table scan on &amp;lt;temporary&amp;gt;  (cost=1.87e+6..1.9e+6 rows=175136)
        -&amp;gt; Aggregate using temporary table  (cost=1.87e+6..1.87e+6 rows=175136)
            -&amp;gt; Inner hash join (t2.id = t3.id)  (cost=990754..1.44e+6 rows=493200)
                -&amp;gt; Covering index scan on t3 using k_1  (cost=0.312..308240 rows=986400)
                -&amp;gt; Hash
                    -&amp;gt; Inner hash join (t1.id = t2.id)  (cost=370988..824021 rows=493200)
                        -&amp;gt; Covering index scan on t2 using k_1  (cost=0.312..308240 rows=986400)
                        -&amp;gt; Hash
                            -&amp;gt; Filter: (t1.k between 200000 and 500000)  (cost=0.416..205287 rows=493200)
                                -&amp;gt; Covering index range scan on t1 using k_1 over (200000 &amp;lt;= k &amp;lt;= 500000)  (cost=0.359..176877 rows=493200)We can see that with “hypergraph_optimizer=enabled”, the query execution time is almost 8x faster.
The performance difference might not be noticeable with a few joins or a smaller table’s data set, but with more complex joins, it can yield better performance. In the above example, we can see that when “hypergraph_optimizer=enabled”, the optimiser replaces “Nested loop inner join” with “Inner hash join”, which is generally better for large datasets. 
Higher version source allowed
Now, it’s possible that a lower version replica can connect to a higher version source when the major versions differ. That means we don’t have to rely on all replicas being upgraded in one go; we can just upgrade the source, verify it, and later perform rolling upgrades on lower-version replicas as per our own timelines and convenience.
Of course, we have to be cautious not to run any such feature or change on the source that doesn’t support lower-version replicas.
Please note – This won’t be applicable to previous releases, say (8.4, 8.0), as they didn’t restrict such replication connectivity. It would be useful for 9.7 or the next major release.
To enable this functionality, we need to ensure the following variable is enabled on the Replica. By default its enabled on 9.7mysql&amp;gt; show variables like 'replica_allow_higher_version_source';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| replica_allow_higher_version_source | ON    |
+-------------------------------------+-------+
1 row in set (0.008 sec)

Summary
The above discussion highlights key advancements in MySQL 9.7 LTS, ranging from some innovative or operational improvements to developer-centric features such as “JSON Duality” Views. Also, the “Hypergraph Optimiser” is now available for community release, which was previously exclusive to MySQL Heatwave/Enterprise.  As a Long-Term Support (LTS) release, MySQL 9.7 is structured to provide a stable and consistent environment, prioritising architectural reliability over frequent experimental changes.
One more important mention here: It’s suggested to use MySQL 9.7.1, or the next sub-releases, as 9.7.0 has some higer severity CVE’s. If you are using Percona Server for MySQL (PS), we skipped 9.7.0 and are shipping the fixed 9.7.1 version directly.
Still, it’s highly recommended to test any new component or changes in your lower/staging environment before deploying in production to better assess the overall impact on existing workload, queries, and database behaviour.
 
The post Inside MySQL 9.7 LTS Features appeared first on Percona.</description>
    <content:encoded><![CDATA[<p><span>MySQL 9.7, a Long-Term Support (LTS) release, incorporates a variety of potential features spanning across multiple technical domains. This article covers some of the primary features introduced and evaluates their practical utility within the MySQL database environment.</span></p>
<p><span>Following the End-of-Life (EOL) status of MySQL 8.0, this subsequent LTS release is designed to provide enhanced stability alongside significant architectural innovations.</span></p>
<p><span>Let’s discuss each of these features below with some examples and usage.</span></p>
<h2><span>Flow-control monitoring in Group Replication</span></h2>
<p><span>Flow control monitoring has been improved and provides more granularity by introducing the additional status variables listed below.</span></p>
<ul>
<li aria-level="1"><span>Gr_flow_control_throttle_count : It denotes the number of transactions that have been throttled.</span></li>
<li aria-level="1"><span>Gr_flow_control_throttle_time_sum :It denotes the time in microseconds that transactions have been throttled.</span></li>
<li aria-level="1"><span>Gr_flow_control_throttle_active_count :It denotes the number of transactions currently being throttled.</span></li>
<li aria-level="1"><span>Gr_flow_control_throttle_last_throttle_timestamp : It denotes the most recent date and time that a transaction was throttled.</span></li>
</ul>
<p><span>To use these status variables, we must install the “</span><b>Group Replication Flow Control Statistics”  </b><span>component.<br>
</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; Install component 'file://component_group_replication_flow_control_stats';</pre><p><span>After the component is installed, the statistics will be visible.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT * FROM performance_schema.global_status WHERE VARIABLE_NAME LIKE 'Gr_flow_control%';
+--------------------------------------------------+----------------+
| VARIABLE_NAME                                    | VARIABLE_VALUE |
+--------------------------------------------------+----------------+
| Gr_flow_control_throttle_active_count            | 0              |
| Gr_flow_control_throttle_count                   | 0              |
| Gr_flow_control_throttle_last_throttle_timestamp |                |
| Gr_flow_control_throttle_time_sum                | 0              |
+--------------------------------------------------+----------------+</pre><p></p>
<h2><span>Multi-threaded applier extended statistics</span></h2>
<p><span>We now have additional verbosity for the Applier threads for both Asynchronous and Group Replication topologies. This means we can get more details of the transactions or potential misbehaviours during the transactions applier stage. This feature is particularly useful for troubleshooting performance bottlenecks in multi-threaded replication environments, where understanding the specific cause of lag can be challenging.</span></p>
<p><span>This requires installing the “</span><b>Replication Applier Metrics” </b><span>component.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; Install component 'file://component_replication_applier_metrics';</pre><p><span>Upon successful installation of the requisite component, the performance schema tables facilitate tracking of transaction details and various performance metrics during the replication applier phase. For instance, monitoring the table “</span><b>replication_applier_metrics”</b><span> enables observing channel-specific operations.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT * FROM performance_schema.replication_applier_metrics where CHANNEL_NAME='group_replication_applier'\G;
*************************** 1. row ***************************
                                CHANNEL_NAME: group_replication_applier
                  TOTAL_ACTIVE_TIME_DURATION: 0
                          LAST_APPLIER_START: 0000-00-00 00:00:00
                TRANSACTIONS_COMMITTED_COUNT: 0
                  TRANSACTIONS_ONGOING_COUNT: 0
                  TRANSACTIONS_PENDING_COUNT: 0
       TRANSACTIONS_COMMITTED_SIZE_BYTES_SUM: 0
    TRANSACTIONS_ONGOING_FULL_SIZE_BYTES_SUM: 0
TRANSACTIONS_ONGOING_PROGRESS_SIZE_BYTES_SUM: 0
         TRANSACTIONS_PENDING_SIZE_BYTES_SUM: NULL
                      EVENTS_COMMITTED_COUNT: 0
            WAITS_FOR_WORK_FROM_SOURCE_COUNT: 0
         WAITS_FOR_WORK_FROM_SOURCE_SUM_TIME: 0
            WAITS_FOR_AVAILABLE_WORKER_COUNT: 0
         WAITS_FOR_AVAILABLE_WORKER_SUM_TIME: 0
      WAITS_COMMIT_SCHEDULE_DEPENDENCY_COUNT: 0
   WAITS_COMMIT_SCHEDULE_DEPENDENCY_SUM_TIME: 0
         WAITS_FOR_WORKER_QUEUE_MEMORY_COUNT: 0
      WAITS_FOR_WORKER_QUEUE_MEMORY_SUM_TIME: 0
              WAITS_WORKER_QUEUES_FULL_COUNT: 0
           WAITS_WORKER_QUEUES_FULL_SUM_TIME: 0
             WAITS_DUE_TO_COMMIT_ORDER_COUNT: 0
          WAITS_DUE_TO_COMMIT_ORDER_SUM_TIME: 0
        TIME_TO_READ_FROM_RELAY_LOG_SUM_TIME: 0</pre><p><span>In addition to aggregate metrics, MySQL 9.7 provides a way to inspect the progress of individual worker threads via monitoring stats in the </span><b>“replication_applier_progress_by_worker”</b><span> table. This level of detail helps administrators identify if a single transaction is monopolising a specific worker, causing overall replication delay.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT * FROM performance_schema.replication_applier_progress_by_worker\G;
*************************** 1. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 0
                             THREAD_ID: 62
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 2. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 1
                             THREAD_ID: 63
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 3. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 2
                             THREAD_ID: 64
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0
*************************** 4. row ***************************
                          CHANNEL_NAME: group_replication_applier
                             WORKER_ID: 3
                             THREAD_ID: 65
              ONGOING_TRANSACTION_TYPE: UNASSIGNED
   ONGOING_TRANSACTION_FULL_SIZE_BYTES: 0
ONGOING_TRANSACTION_APPLIED_SIZE_BYTES: 0</pre><p></p>
<h2><span>Automatic eviction &amp; rejoin</span></h2>
<p><span>The Group Replication resource manager now provides auto-eviction functionality, which we can configure using the available options. This basically ensures that the unhealthy node is removed from the Group to maintain the cluster’s high availability and overall performance.</span></p>
<p><span>This requires installing the “</span><b>group replication resource manager”</b><span> component.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; INSTALL COMPONENT 'file://component_group_replication_resource_manager';</pre><p><span>Once the component is available,  we can use various options to decide the node expulsion policy.</span></p>
<p><b>1) Applier channel</b></p>
<p><span>We can set the applier channel replication lag threshold values using the configuration parameter below.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; set global group_replication_resource_manager.applier_channel_lag = &lt;value&gt;;</pre><p><span>If lag exceeds  “</span><b>applier_channel_lag”</b><span> threshold 10 times or more in a row, this server is expelled from the group. The status variable below is used for tracking the lag exceed rate.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show global status like 'Gr_resource_manager_applier_channel_lag';
+-----------------------------------------+-------+
| Variable_name                           | Value |
+-----------------------------------------+-------+
| Gr_resource_manager_applier_channel_lag | 0     |
+-----------------------------------------+-------+</pre><p><span><br>
</span><b>2)</b> <b>Recovery Channel</b><b></b></p>
<p><span>Similarly, we can define a threshold for the group member recovery process to attempt to rejoin the cluster. </span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; set global group_replication_resource_manager.recovery_channel_lag = &lt;value&gt;;</pre><p><span>If the secondary’s recovery lag exceeds “</span><strong>recovery_channel_lag”</strong><span>, 10 times or more in succession, the server is expelled from the group. </span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql show global status like 'Gr_resource_manager_recovery_channel_lag';
+------------------------------------------+-------+
| Variable_name                            | Value |
+------------------------------------------+-------+
| Gr_resource_manager_recovery_channel_lag | 0     |
+------------------------------------------+-------+</pre><p><b>3) Memory/Resource Usage</b></p>
<p><span>We can also define an expelled condition based on the group member’s memory or resource usage %.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; set global group_replication_resource_manager.memory_used_limit = 10;</pre><p><span>If the memory usage exceeds </span><strong>memory_used_limit</strong><span> % by 10 or more consecutive times, the node will be expelled from the group.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show global status like 'Gr_resource_manager_memory_used%';
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| Gr_resource_manager_memory_used | 78    |
+---------------------------------+-------+
1 row in set (0.002 sec)</pre><p><span>In addition to the discussed options above, we can also track various <a href="https://dev.mysql.com/doc/refman/9.7/en/group-replication-resource-manager-component.html">server status variables</a> to monitor group replication and the resource manager component.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; select * from performance_schema.global_status where variable_name in ('Gr_resource_manager_applier_channel_threshold_hits','Gr_resource_manager_applier_channel_eviction_timestamp','Gr_resource_manager_recovery_channel_threshold_hits','Gr_resource_manager_recovery_channel_eviction_timestamp','Gr_resource_manager_memory_threshold_hits','Gr_resource_manager_memory_eviction_timestamp');
+---------------------------------------------------------+----------------+
| VARIABLE_NAME                                           | VARIABLE_VALUE |
+---------------------------------------------------------+----------------+
| Gr_resource_manager_applier_channel_eviction_timestamp  |                |
| Gr_resource_manager_applier_channel_threshold_hits      | 0              |
| Gr_resource_manager_memory_eviction_timestamp           |                |
| Gr_resource_manager_memory_threshold_hits               | 6703           |
| Gr_resource_manager_recovery_channel_eviction_timestamp |                |
| Gr_resource_manager_recovery_channel_threshold_hits     | 0              |
+---------------------------------------------------------+----------------+
6 rows in set (0.003 sec)</pre><p><span>The expelled node can attempt to automatically rejoin based on the value of the </span><b>group_replication_autorejoin_tries</b><span> variable</span><b>.</b></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show variables like '%group_replication_autorejoin_tries%';
+------------------------------------+-------+
| Variable_name                      | Value |
+------------------------------------+-------+
| group_replication_autorejoin_tries | 3     |
+------------------------------------+-------+
1 row in set (0.006 sec)</pre><p><span>If the node cannot join, it will perform the behaviour specified in the </span><b>group_replication_exit_state_action </b><span>variable.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show variables like '%group_replication_exit_state_action%';
+-------------------------------------+--------------+
| Variable_name                       | Value        |
+-------------------------------------+--------------+
| group_replication_exit_state_action | OFFLINE_MODE |
+-------------------------------------+--------------+
1 row in set (0.005 sec)</pre><p><span>After a server is evicted from the group (for whatever reason), it gets a </span><b>grace period</b><span> (</span><b>group_replication_resource_manager</b><span>) when it rejoins. During this period, the Resource Manager won’t immediately kick it out again, even if it’s still lagging or breaching the defined threshold as discussed above.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show variables like '%group_replication_resource_manager.quarantine_time%';
+----------------------------------------------------+-------+
| Variable_name                                      | Value |
+----------------------------------------------------+-------+
| group_replication_resource_manager.quarantine_time | 3600  |
+----------------------------------------------------+-------+</pre><p></p>
<h2><span>Up-to-date aware Primary election</span></h2>
<p><span>The Primary election process is more mature and cohesive. The Group Replication Manager now uses the most up-to-date status as a criterion for selecting the new primary.</span></p>
<p><span>Here is how the Group Replication Manager performs the most up-to-date primary selection prior to MySQL v9.7.</span></p>
<ol>
<li aria-level="1"><span>The lowest MySQL version is checked for each member.</span></li>
<li aria-level="1"><span>If more than one member is running the lowest MySQL Server version, each member’s weight is determined by the “</span>group_replication_member_weight”<span> system variable.</span></li>
<li aria-level="1"><span>If there is more than one member running the lowest MySQL Server version, and also more than one of those members has the highest member weight, the third factor considered is the lexicographical order of the generated server UUIDs “</span>server_uuid”<span> of each group member. The member with the lowest server UUID is chosen as the new primary.<br>
</span></li>
</ol>
<p><span>In MySQL version 9.7, “</span><b>group_replication_elect_prefers_most_updated”</b><span> was introduced, so the failover will be determined by </span><b>how many transactions are in the secondary backlog</b><span>. Basically the secondary with the least backlog will be selected as Primary.</span></p>
<p><span>Now, it will consider the</span><b> “most up-to-date” </b><span>node first,</span> <span>then “</span><b>weight”</b><span> and then “</span><b>UUID”</b><span>. </span></p>
<p><span>To use “</span><b>group_replication_elect_prefers_most_updated”</b><span>, we need to install the “</span><b>Group Replication Primary Election</b><span>” component listed below on each Group Member.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; Install component 'file://component_group_replication_elect_prefers_most_updated';</pre><p><span>By default, the most up-to-date group member selection is enabled. We need to make sure it’s enabled on all Group Members. </span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; select @@group_replication_elect_prefers_most_updated.enabled;
+--------------------------------------------------------+
| @@group_replication_elect_prefers_most_updated.enabled |
+--------------------------------------------------------+
|                                                      1 |
+--------------------------------------------------------+
1 row in set (0.007 sec)</pre><p><span>In the event that a new primary is elected via the most up-to-date selection mechanism, this metric represents the transaction processing differential between the newly designated primary and the secondary node with the highest level of synchronisation.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show status like 'Gr_latest_primary_election_by_most_uptodate_members_trx_delta';
+---------------------------------------------------------------+-------+
| Variable_name                                                 | Value |
+---------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_members_trx_delta | 0     |
+---------------------------------------------------------------+-------+</pre><p><span>Also, we can track the timestamp of the most recent primary election on the most up-to-date node.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show status like 'Gr_latest_primary_election_by_most_uptodate_member_timestamp';
+--------------------------------------------------------------+-------+
| Variable_name                                                | Value |
+--------------------------------------------------------------+-------+
| Gr_latest_primary_election_by_most_uptodate_member_timestamp |       |
+--------------------------------------------------------------+-------+
1 row in set (0.005 sec)</pre><p><span>The database logs also tell exactly what criteria the primary member selected during failover.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">2026-06-14T10:04:02.243809Z 0 [System] [MY-015575] [Repl] Plugin group_replication reported: 'Member with uuid 00021702-2222-2222-2222-222222222222 was elected primary since it was the most up-to-date member with 2755 transactions more than second most up-to-date member 00021703-3333-3333-3333-333333333333. In case of a tie member weight and then uuid lexical order was used over the most updated members.'</pre><p></p>
<h2><span>MySQL JSON duality views</span></h2>
<p><span>With the introduction of JSON duality views, we can leverage a single unified JSON document for both relational and hierarchical JSON data. This provides a common, structured JSON format for the application, allowing it to perform both read and write operations.</span></p>
<p><span>Let’s see a quick scenario below on how it works.</span></p>
<p><span>Below are two relational tables from which we obtain aggregated information in JSON format. </span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; CREATE TABLE products (
  product_id INT PRIMARY KEY,
  product_type VARCHAR(100)
);

mysql&gt; CREATE TABLE products_details (
  product_detail_id INT PRIMARY KEY,
  product_id INT,
  name VARCHAR(100),
  active varchar(10)
);</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; INSERT INTO products (product_id,product_type) VALUES (1,'IT'), (2,'TEL');
mysql&gt; INSERT INTO products_details (product_detail_id,product_id,name,active) VALUES (1,1,'Laptop','Yes'), (2,2,'Mobile','Yes');</pre><p><span>Here is the exact Json View which fetch the columns from the relation table based on the join condition. Each of those relational table columns is mapped with a JSON data structure (</span><b>_id</b><span>,</span><b>v_product_type</b><span>,</span><b>v_product_type</b><span> ), and the complete details of the</span><b> product details </b><span>table are fetched into the (</span><b>product</b><span>) array.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; CREATE JSON RELATIONAL DUALITY VIEW view_product AS
SELECT JSON_DUALITY_OBJECT( WITH(INSERT,UPDATE,DELETE)
    '_id': product_id,
    'v_product_type': product_type,
    'product': (
        SELECT JSON_ARRAYAGG(
            JSON_DUALITY_OBJECT(WITH(INSERT,UPDATE,DELETE)
                'v_product_detail_id': product_detail_id,
                'v_name': name,
                'v_active': active
                
            )
        )
        FROM products_details
        WHERE products_details.product_id = products.product_id
    )
)
FROM products;</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Laptop", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "313642c2aa24f0571264332afa140715"}, "v_product_type": "IT"}  |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 rows in set (0.002 sec)</pre><p><span>Once the duality view is created, we can perform both read/write operations.</span></p>
<p><strong>Reading the duality view</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Laptop", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "313642c2aa24f0571264332afa140715"}, "v_product_type": "IT"}  |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p><strong>Writing the underlying table in the duality view</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; UPDATE view_product
SET data = JSON_SET(
    data,
    '$.product[0].v_name',
    'Notepad'
)
WHERE JSON_EXTRACT(data, '$._id') = 1;</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; select * from products_details;
+-------------------+------------+---------+--------+
| product_detail_id | product_id | name    | active |
+-------------------+------------+---------+--------+
|                 1 |          1 | Notepad | Yes    |
|                 2 |          2 | Mobile  | Yes    |
+-------------------+------------+---------+--------+</pre><p><span>After performing the above write operations, we can see that the view now shows the updated data.</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql &gt; select * from view_product;
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| data                                                                                                                                                                           |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| {"_id": 1, "product": [{"v_name": "Notepad", "v_active": "Yes", "v_product_detail_id": 1}], "_metadata": {"etag": "72c4368420cdc698842d0ab4bd9315ab"}, "v_product_type": "IT"} |
| {"_id": 2, "product": [{"v_name": "Mobile", "v_active": "Yes", "v_product_detail_id": 2}], "_metadata": {"etag": "3d229ada02ac660f9f6cac994b44831a"}, "v_product_type": "TEL"} |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+</pre><p></p>
<h2><span>Hypergraph Optimizer</span></h2>
<p><span>With the Hypergraph Optimiser, we now have more </span><span>advanced optimisation for complex queries and a broader set of Join plans than the older traditional method, missing earlier. By using “</span><b>Join hypergraph</b><span>”, the optimiser now has better reach to all tables in the join condition.</span></p>
<p><b>Hypergraph Optimiser is OFF</b></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SET optimizer_switch='hypergraph_optimizer=off';</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;</pre><p><span><strong>Output</strong>:</span></p><pre class="urvanov-syntax-highlighter-plain-tag">| 498870 | 119 |
| 498729 | 119 |
| 497668 | 119 |
| 498076 | 119 |
+--------+-----+
100 rows in set (4.000 sec)</pre><p><strong>Explain output:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">-&gt; Limit: 100 row(s)
    -&gt; Sort: cnt DESC, limit input to 100 row(s) per chunk
        -&gt; Stream results  (cost=1.22e+6 rows=175136)
            -&gt; Group aggregate: count(0)  (cost=1.22e+6 rows=175136)
                -&gt; Nested loop inner join  (cost=1.1e+6 rows=493200)
                    -&gt; Nested loop inner join  (cost=601547 rows=493200)
                        -&gt; Filter: (t1.k between 200000 and 500000)  (cost=99122 rows=493200)
                            -&gt; Covering index range scan on t1 using k_1 over (200000 &lt;= k &lt;= 500000)  (cost=99122 rows=493200)
                        -&gt; Single-row covering index lookup on t2 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)
                    -&gt; Single-row covering index lookup on t3 using PRIMARY (id = t1.id)  (cost=0.919 rows=1)</pre><p><b>Hypergraph Optimiser is ON</b></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SET optimizer_switch='hypergraph_optimizer=on';</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT t1.k, COUNT(*) AS cnt
FROM sbtest1 t1
JOIN sbtest2 t2 ON t1.id = t2.id
JOIN sbtest3 t3 ON t1.id = t3.id
WHERE t1.k BETWEEN 200000 AND 500000
GROUP BY t1.k
ORDER BY cnt DESC
LIMIT 100;</pre><p><b>Output:</b></p><pre class="urvanov-syntax-highlighter-plain-tag">| 499721 | 119 |
| 499052 | 119 |
| 498870 | 119 |
| 498384 | 119 |
+--------+-----+
100 rows in set (0.498 sec)</pre><p><strong>Explain output:</strong></p><pre class="urvanov-syntax-highlighter-plain-tag">-&gt; Sort: cnt DESC, limit input to 100 row(s) per chunk  (cost=1.96e+6..1.96e+6 rows=100)
    -&gt; Table scan on &lt;temporary&gt;  (cost=1.87e+6..1.9e+6 rows=175136)
        -&gt; Aggregate using temporary table  (cost=1.87e+6..1.87e+6 rows=175136)
            -&gt; Inner hash join (t2.id = t3.id)  (cost=990754..1.44e+6 rows=493200)
                -&gt; Covering index scan on t3 using k_1  (cost=0.312..308240 rows=986400)
                -&gt; Hash
                    -&gt; Inner hash join (t1.id = t2.id)  (cost=370988..824021 rows=493200)
                        -&gt; Covering index scan on t2 using k_1  (cost=0.312..308240 rows=986400)
                        -&gt; Hash
                            -&gt; Filter: (t1.k between 200000 and 500000)  (cost=0.416..205287 rows=493200)
                                -&gt; Covering index range scan on t1 using k_1 over (200000 &lt;= k &lt;= 500000)  (cost=0.359..176877 rows=493200)</pre><p><span>We can see that with “</span><b>hypergraph_optimizer=enabled”, </b><span>the query execution time is almost 8x faster.</span></p>
<p><span>The performance difference might not be noticeable with a few joins or a smaller table’s data set, but with more complex joins, it can yield better performance. In the above example, we can see that when</span><b> “hypergraph_optimizer=enabled”</b><span>, the optimiser replaces “</span><b>Nested loop inner join</b><span>” with “</span><b>Inner</b> <b>hash join</b><span>”, which is generally better for large datasets. </span></p>
<h2><span>Higher version source allowed</span></h2>
<p><span>Now, it’s possible that a lower version replica can connect to a higher version source when the major versions differ. That means we don’t have to rely on all replicas being upgraded in one go; we can just upgrade the source, verify it, and later perform rolling upgrades on lower-version replicas as per our own timelines and convenience.</span></p>
<p><span>Of course, we have to be cautious not to run any such feature or change on the source that doesn’t support lower-version replicas.</span></p>
<p><b>Please note –</b><span> This won’t be applicable to previous releases, say (8.4, 8.0), as they didn’t restrict such replication connectivity. It would be useful for 9.7 or the next major release.</span></p>
<p><span>To enable this functionality, we need to ensure the following variable is enabled on the Replica. By default its enabled on 9.7</span></p><pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; show variables like 'replica_allow_higher_version_source';
+-------------------------------------+-------+
| Variable_name                       | Value |
+-------------------------------------+-------+
| replica_allow_higher_version_source | ON    |
+-------------------------------------+-------+
1 row in set (0.008 sec)</pre><p></p>
<h2></h2>
<h2><span>Summary</span></h2>
<p><span>The above discussion highlights key advancements in MySQL 9.7 LTS, ranging from some innovative or operational improvements to developer-centric features such as “JSON Duality” Views. Also, the “Hypergraph Optimiser” is now available for community release, which was previously exclusive to MySQL Heatwave/Enterprise.  As a Long-Term Support (LTS) release, MySQL 9.7 is structured to provide a stable and consistent environment, prioritising architectural reliability over frequent experimental changes.</span></p>
<p><b>One more important mention here</b><span>: It’s suggested to use MySQL 9.7.1, or the next sub-releases, as 9.7.0 has some</span><a href="https://www.oracle.com/security-alerts/cspujun2026verbose.html"><span> higer severity CVE’s</span></a><span>. If you are using </span><b>Percona Server for MySQL (PS), </b><span>we</span> <a href="https://www.percona.com/blog/percona-server-mysql-8-4-9-9-7-0-skipped/"><b>skipped 9.7.0</b></a> <span>and are shipping the fixed 9.7.1 version directly</span><b>.</b></p>
<p><b>Still, it’s highly recommended</b><span> to test any new component or changes in your lower/staging environment before deploying in production to better assess the overall impact on existing workload, queries, and database behaviour.</span></p>
<p><span> </span></p>
<p>The post <a href="https://www.percona.com/blog/inside-mysql-9-7-lts-features/">Inside MySQL 9.7 LTS Features</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Wed, 15 Jul 2026 05:00:23 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Featured</category>
    <category>Insight for DBAs</category>
    <category>Insight for Developers</category>
    <category>MariaDB</category>
    <category>MySQL</category>
    <category>Open Source</category>
    <category>Storage Engine</category>
    <category>Uncategorized</category>
    <category>asynchronous replication</category>
    <category>MySQL 9.7</category>
    <category>MySQL 9.7 Features</category>
    <category>MySQL 9.7 LTS</category>
    <category>MySQL features</category>
    <category>MySQL Group Replication</category>
    <category>Percona Server for MySQL</category>
  </item>

  <item>
    <title>Commit Optimization for Large MySQL Transactions</title>
    <guid isPermaLink="false">https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/</guid>
    <link>https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/</link>
    <description>
  This article is also available in Chinese: 中文版. Browse all English articles.


If you use and operate MySQL, you’ve surely run into a strange slow query like this:




  An INSERT that’s normally instant took 1.3s, and the slow-query log shows no long lock wait.
  Every statement in a multi-statement transaction had already finished, yet the COMMIT alone took 1.3s.


When this happens, the most likely cause is a large transaction committing. Below is a simulated test: we used sysbench to simulate a normal workload, then ran a large UPDATE in the background every 5 seconds. You can see the large UPDATE severely hurts performance.



Root Cause



The figure above shows the execution of two transactions:


  A transaction runs in two phases: an execution phase and a commit phase.
  During execution, when a statement updates data it generates binlog events. These are stored in the Binlog Cache, which has two parts: an in-memory buffer and a temporary file. When the buffer fills up, the events are written to the temporary file.
  At commit, all the binlog events in the Binlog Cache are copied into the binlog file.
  Writing binlog events to the binlog file must be serialized — one transaction can’t do it until the previous one has finished. So while Trx_n is writing to the binlog file, Trx_m has to wait.
  In the figure, Trx_n is a large transaction that produced a lot of binlog events. The time to copy binlog events into the binlog file is linear in the size of the events the transaction produced — the more events, the longer the copy takes.
  Trx_m is a small transaction. Even though its execution phase finished quickly, at commit it runs into the large transaction Trx_n committing, so it must wait for Trx_n to finish copying its binlog events before it can proceed. Trx_m spends most of its commit phase waiting for Trx_n to write the binlog file — and that’s why the small transaction becomes slow.


How Serious the Problem Is

As our simulated test shows, committing a large transaction has a major impact on workload stability. In real-world scenarios it can be far worse, and it’s common.


  A GB-scale transaction can make the instance unwritable for a long time. Since storage IO bandwidth is fixed, the time to write a large transaction’s binlog depends on the transaction’s size. The largest transaction we’ve seen in production produced 104 GB of binlog events.
  A GB-scale transaction can push IO throughput up and slow it down, or even saturate IO, which also slows queries.
  A few-hundred-MB transaction won’t cause a long outage, but it can still add hundreds of milliseconds to application DML. For latency-sensitive workloads, even that may be unacceptable.
  On top of this, all of the above can raise the number of active connections. If those active connections aren’t cleared in time, CPU spikes, and it can turn into a vicious cycle — eventually an avalanche and a much bigger problem.


Optimizing How Large Transactions Write the Binlog

In AliSQL we optimized how a large transaction writes the binlog, completely eliminating the stability impact of large-transaction commit. RDS 5.7 and RDS 8.0 both enable this optimization by default. Last year we contributed it to MariaDB, and the feature shipped in MariaDB 11.71.

The Approach

Here is the implementation in MariaDB 11.7. MySQL and MariaDB have diverged quite a bit in code, but the underlying logic — and therefore the approach — is the same.



The idea is simple and clean: since the Binlog Cache has already written the binlog events to a file, we just rename that file directly into a binlog file. This avoids copying the binlog events, so there is no extra IO. And a rename takes constant time regardless of the Binlog Cache’s size, which fully solves the large-transaction problem. Let’s look at the implementation.

The #binlog_cache_files Directory

The Binlog Cache’s file is a system temporary file, which can’t be renamed into a regular file directly. So we create a directory, #binlog_cache_files, in the binlog directory; the file the Binlog Cache creates then becomes a regular file in this directory instead of a system temp file.

1
2
$ls var/mysqld.1/data/#binlog_cache_files
ML_140413554102520


Reserving Head Space

The Binlog Cache file contains only the transaction’s binlog events. To turn it into a binlog file, we need to reserve some space for the binlog header events, such as the Format_description_event.



The reserved space is 4 KB-aligned, so at least 4 KB is reserved, which is enough in most cases. But in some situations the Gtid_list_log_event (similar to MySQL’s Previous_gtids_event, recording the GTID set generated before this binlog) can be very large. To keep the feature usable in that case, when generating a new binlog file we adjust the reserved space based on how much the header events actually occupy; the Binlog Cache file’s reserved space is then adjusted when the next transaction begins. The binlog header events usually take less than 4 KB, so after writing them some space may be left over. How do we handle the leftover? Thanks to MariaDB’s mechanism of padding a Gtid_log_event with trailing zeros, the leftover space is absorbed into the corresponding Gtid_log_event. After the Binlog Cache file is turned into a binlog file, its structure looks like this:



The Rename Process



The rename works roughly as follows:


  Persist the Binlog Cache file. At this point the rename hasn’t started, so it doesn’t block other transactions from committing.
  Perform a rotate: close the current binlog file and create a new one.
  Copy the new file’s header into the head of the Binlog Cache.
  Generate the Gtid_log_event.
  Delete the newly generated binlog file, and rename the Binlog Cache file into the new binlog file.


Results

Again we used sysbench to simulate a workload, then ran a large UPDATE in the background every 5 seconds, each producing 512 MB of binlog events. The results:



With the large-transaction commit optimization, sysbench’s TPS is fairly steady, with no violent swings. There’s still a small dip every 5 seconds, but that comes from the large UPDATE itself using some CPU, not from transaction commit.

We also simulated the DML latency caused by transactions of different sizes. The results:




  Without the optimization, once a large transaction exceeds 64 MB, sysbench’s max latency starts to climb noticeably, and rises rapidly as the transaction grows.
  With the optimization on, sysbench’s max latency stays stable no matter how large the transaction, holding at normal workload levels. At 1024 GB, one extra binlog rotate adds a slight bump in latency.


Conclusion

In MySQL’s binlog replication architecture, large transactions are a classic trigger for trouble, causing stability and replication-lag problems. By renaming the Binlog Cache’s temporary file directly into a binlog file, we avoid copying binlog events and eliminate the extra IO, keeping large-transaction commit fast and stable — and fully resolving the various stability problems that large-transaction commit causes.


  
    
      MariaDB 11.7 — Binlog Commit Optimization for Large Transactions ↩︎
    
  
</description>
    <content:encoded><![CDATA[<blockquote class="prompt-tip">
  <p>This article is also available in Chinese: <a href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization/">中文版</a>. Browse <a href="https://songlibing.github.io/english/">all English articles</a>.</p>
</blockquote>

<p>If you use and operate MySQL, you’ve surely run into a strange slow query like this:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-1.webp" alt=""></p>

<ul>
  <li>An <code class="language-plaintext highlighter-rouge">INSERT</code> that’s normally instant took <code class="language-plaintext highlighter-rouge">1.3s</code>, and the slow-query log shows no long lock wait.</li>
  <li>Every statement in a multi-statement transaction had already finished, yet the <code class="language-plaintext highlighter-rouge">COMMIT</code> alone took <code class="language-plaintext highlighter-rouge">1.3s</code>.</li>
</ul>

<p>When this happens, the most likely cause is a large transaction committing. Below is a simulated test: we used sysbench to simulate a normal workload, then ran a large UPDATE in the background every 5 seconds. You can see the large UPDATE severely hurts performance.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-2-en.png" alt=""></p>

<h2>Root Cause</h2>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-3.webp" alt=""></p>

<p>The figure above shows the execution of two transactions:</p>

<ul>
  <li>A transaction runs in two phases: an execution phase and a commit phase.</li>
  <li>During execution, when a statement updates data it generates binlog events. These are stored in the Binlog Cache, which has two parts: an in-memory buffer and a temporary file. When the buffer fills up, the events are written to the temporary file.</li>
  <li>At commit, all the binlog events in the Binlog Cache are copied into the binlog file.</li>
  <li><strong>Writing binlog events to the binlog file must be serialized — one transaction can’t do it until the previous one has finished.</strong> So while <code class="language-plaintext highlighter-rouge">Trx_n</code> is writing to the binlog file, <code class="language-plaintext highlighter-rouge">Trx_m</code> has to wait.</li>
  <li>In the figure, <code class="language-plaintext highlighter-rouge">Trx_n</code> is a large transaction that produced a lot of binlog events. <em>The time to copy binlog events into the binlog file is linear in the size of the events the transaction produced — the more events, the longer the copy takes.</em></li>
  <li><code class="language-plaintext highlighter-rouge">Trx_m</code> is a small transaction. Even though its execution phase finished quickly, at commit it runs into the large transaction <code class="language-plaintext highlighter-rouge">Trx_n</code> committing, so it must wait for <code class="language-plaintext highlighter-rouge">Trx_n</code> to finish copying its binlog events before it can proceed. <code class="language-plaintext highlighter-rouge">Trx_m</code> spends most of its commit phase waiting for <code class="language-plaintext highlighter-rouge">Trx_n</code> to write the binlog file — and that’s why the small transaction becomes slow.</li>
</ul>

<h2>How Serious the Problem Is</h2>

<p>As our simulated test shows, committing a large transaction has a major impact on workload stability. In real-world scenarios it can be far worse, and it’s common.</p>

<ul>
  <li>A GB-scale transaction can make the instance unwritable for a long time. Since storage IO bandwidth is fixed, the time to write a large transaction’s binlog depends on the transaction’s size. The largest transaction we’ve seen in production produced <code class="language-plaintext highlighter-rouge">104 GB</code> of binlog events.</li>
  <li>A GB-scale transaction can push IO throughput up and slow it down, or even saturate IO, which also slows queries.</li>
  <li>A few-hundred-MB transaction won’t cause a long outage, but it can still add hundreds of milliseconds to application DML. For latency-sensitive workloads, even that may be unacceptable.</li>
  <li>On top of this, all of the above can raise the number of active connections. If those active connections aren’t cleared in time, CPU spikes, and it can turn into a vicious cycle — eventually an avalanche and a much bigger problem.</li>
</ul>

<h2>Optimizing How Large Transactions Write the Binlog</h2>

<p>In AliSQL we optimized how a large transaction writes the binlog, completely eliminating the stability impact of large-transaction commit. RDS 5.7 and RDS 8.0 both enable this optimization by default. Last year we contributed it to MariaDB, and the feature shipped in MariaDB 11.7<sup><a href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</p>

<h3>The Approach</h3>

<p>Here is the implementation in MariaDB 11.7. MySQL and MariaDB have diverged quite a bit in code, but the underlying logic — and therefore the approach — is the same.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-4.webp" alt=""></p>

<p>The idea is simple and clean: since the Binlog Cache has already written the binlog events to a file, we just <em>rename that file directly into a binlog file.</em> This avoids copying the binlog events, so there is no extra IO. And a rename takes constant time regardless of the Binlog Cache’s size, which fully solves the large-transaction problem. Let’s look at the implementation.</p>

<h3>The #binlog_cache_files Directory</h3>

<p>The Binlog Cache’s file is a system temporary file, which can’t be renamed into a regular file directly. So we create a directory, <code class="language-plaintext highlighter-rouge">#binlog_cache_files</code>, in the binlog directory; the file the Binlog Cache creates then becomes a regular file in this directory instead of a system temp file.</p>

<div class="language-shell 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="nv">$ls</span> var/mysqld.1/data/#binlog_cache_files
ML_140413554102520
</pre></td></tr></tbody></table></code></pre></div></div>

<h3>Reserving Head Space</h3>

<p>The Binlog Cache file contains only the transaction’s binlog events. To turn it into a binlog file, we need to reserve some space for the binlog header events, such as the <code class="language-plaintext highlighter-rouge">Format_description_event</code>.</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-5.webp" alt=""></p>

<p>The reserved space is 4 KB-aligned, so at least 4 KB is reserved, which is enough in most cases. But in some situations the <code class="language-plaintext highlighter-rouge">Gtid_list_log_event</code> (similar to MySQL’s <code class="language-plaintext highlighter-rouge">Previous_gtids_event</code>, recording the GTID set generated before this binlog) can be very large. To keep the feature usable in that case, when generating a new binlog file we adjust the reserved space based on how much the header events actually occupy; the Binlog Cache file’s reserved space is then adjusted when the next transaction begins. The binlog header events usually take less than 4 KB, so after writing them some space may be left over. How do we handle the leftover? Thanks to MariaDB’s mechanism of padding a <code class="language-plaintext highlighter-rouge">Gtid_log_event</code> with trailing zeros, the leftover space is absorbed into the corresponding <code class="language-plaintext highlighter-rouge">Gtid_log_event</code>. After the Binlog Cache file is turned into a binlog file, its structure looks like this:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-6.webp" alt=""></p>

<h3>The Rename Process</h3>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-7.webp" alt=""></p>

<p>The rename works roughly as follows:</p>

<ul>
  <li>Persist the Binlog Cache file. At this point the rename hasn’t started, so it doesn’t block other transactions from committing.</li>
  <li>Perform a rotate: close the current binlog file and create a new one.</li>
  <li>Copy the new file’s header into the head of the Binlog Cache.</li>
  <li>Generate the <code class="language-plaintext highlighter-rouge">Gtid_log_event</code>.</li>
  <li>Delete the newly generated binlog file, and rename the Binlog Cache file into the new binlog file.</li>
</ul>

<h2>Results</h2>

<p>Again we used sysbench to simulate a workload, then ran a large UPDATE in the background every 5 seconds, each producing 512 MB of binlog events. The results:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-8.webp" alt=""></p>

<p>With the large-transaction commit optimization, sysbench’s TPS is fairly steady, with no violent swings. There’s still a small dip every 5 seconds, but that comes from the large UPDATE itself using some CPU, not from transaction commit.</p>

<p>We also simulated the DML latency caused by transactions of different sizes. The results:</p>

<p><img src="https://songlibing.github.io/assets/img/bigtxn-commit-9.webp" alt=""></p>

<ul>
  <li>Without the optimization, once a large transaction exceeds 64 MB, sysbench’s max latency starts to climb noticeably, and rises rapidly as the transaction grows.</li>
  <li>With the optimization on, sysbench’s max latency stays stable no matter how large the transaction, holding at normal workload levels. At 1024 GB, one extra binlog rotate adds a slight bump in latency.</li>
</ul>

<h2>Conclusion</h2>

<p>In MySQL’s binlog replication architecture, large transactions are a classic trigger for trouble, causing stability and replication-lag problems. By renaming the Binlog Cache’s temporary file directly into a binlog file, we avoid copying binlog events and eliminate the extra IO, keeping large-transaction commit fast and stable — and fully resolving the various stability problems that large-transaction commit causes.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li>
      <p>MariaDB 11.7 — <a href="https://mariadb.com/resources/blog/binlog-commit-optimization-for-large-transaction/">Binlog Commit Optimization for Large Transactions</a> <a href="https://songlibing.github.io/posts/mysql-large-transaction-commit-optimization-en/#fnref:1" class="reversefootnote" role="doc-backlink">↩︎</a></p>
    </li>
  </ol>
</div>]]></content:encoded>
    <pubDate>Wed, 15 Jul 2026 02:00:00 +0000</pubDate>
    <dc:creator>Libing Song</dc:creator>
    <category>MySQL</category>
    <category>Binlog</category>
    <category>Large Transaction</category>
    <category>Commit Optimization</category>
  </item>

  <item>
    <title>MyDumper Locking Mechanisms Revisited: Introducing SAFE_NO_LOCK</title>
    <guid isPermaLink="false">https://www.percona.com/?p=49959</guid>
    <link>https://www.percona.com/blog/mydumper-locking-mechanisms-revisited-introducing-safe_no_lock/</link>
    <description>About a year ago, we discussed how MyDumper refactored its locking mechanisms to move away from old, rigid flags and transitioned towards more flexible, streamlined execution. Since then, the MyDumper community hasn’t stood still.
In recent releases, the locking architecture was further standardized under a single overarching option: --sync-thread-lock-mode. Along with this modernization came a powerful new safety feature designed to give you lock-free thread synchronization without risking silent inconsistency: SAFE_NO_LOCK (merged in PR #2031).
Let’s explore the new thread-synchronization landscape and break down when you should use each mode.
What is --sync-thread-lock-mode?
Previously, flags like -k, --no-locks or --lock-all-tables dictated how MyDumper behaved. These have now been deprecated in favor of --sync-thread-lock-mode, which accepts five core values: AUTO, FTWRL, LOCK_ALL, GTID, NO_LOCK, and the newly added SAFE_NO_LOCK.
As a multi-threaded tool, MyDumper’s main challenge is ensuring that every single worker thread establishes its database snapshot at the exact same point in time. The sync mode you choose completely alters how MyDumper orchestrates this point-in-time synchronization.
Understanding SAFE_NO_LOCK
MyDumper fires off START TRANSACTION WITH CONSISTENT SNAPSHOT across its threads. It captures the binary log position at the very beginning of the process and compares it after the worker threads have attempted to synchronize.
When using NO_LOCK, if the threads don’t actually hit the same point in time—meaning they fail to synchronize—MyDumper simply logs a warning and continues backing up. This results in an inconsistent backup, which is a massive gamble for production systems.
SAFE_NO_LOCK adds a strict transactional safety net. If MyDumper detects any differences or drift in the binlog position among the threads during the synchronization phase, it immediately stops the backup. This prevents you from generating a corrupted, out-of-sync backup that will fail or cause data anomalies during a later restore.
Choosing the Right Mode
Depending on your architecture, uptime requirements, and database vendor, here is the breakdown of when to use each mode:
AUTO (The Default)
What it does: MyDumper automatically evaluates the database vendor, version, and capabilities to choose the safest, least-intrusive method.
When to use it: The vast majority of standard backups. It removes the guesswork and adapts dynamically if your database infrastructure upgrades.
FTWRL (Flush Tables With Read Lock)
What it does: It is the traditional method. It issues a global read lock via FLUSH TABLES WITH READ LOCK on the main connection, forces all threads to establish their consistent snapshot at that exact freeze frame, and then releases the lock.
When to use it:

When you have non-transactional tables (like MyISAM or ARCHIVE) that must be consistently backed up alongside InnoDB tables.
When your database lacks advanced snapshot-tracking capabilities (older MySQL versions).

Downside: It blocks writes across the entire instance during synchronization, which can cause a queue cascade on a busy production server.
GTID
Leverages a specific server variable in Percona Server called binlog_snapshot_gtid_executed to instantly verify if all threads are watching the exact same transaction state.
When to use it: If you are running Percona Server with GTID enabled and want a lightning-fast, lockless synchronization method that is guaranteed to be transactionally accurate.
SAFE_NO_LOCK
What it does: Uses transaction isolation to sync threads without global locks, but immediately aborts the backup if binlog positions diverge during initialization.
When to use it:

On highly sensitive production systems, where global write locks are absolutely forbidden due to strict SLAs.
When you are entirely utilizing transactional engines (InnoDB).
When you want a lock-free backup but require absolute certainty that your backup is 100% consistent.

Downside: In high-throughput write environments, threads may fail to align within the retry window, causing the backup job to abort. (Though an abort is always preferable to an inconsistent backup!).
NO_LOCK
What it does: Attempts lockless synchronization but logs a warning and proceeds even if consistency fails.
When to use it: Rarely, if ever, in the production primary server. It is acceptable for staging environments, development seeding, or scratch pads where data accuracy and point-in-time consistency are entirely secondary to getting a quick data dump without locking the server.
LOCK_ALL
What it does: Explicitly issues a LOCK TABLE command for every single table being exported.
When to use it: Primarily a fallback mode. Use this only when FLUSH TABLES WITH READ LOCK is completely unavailable due to restricted cloud permissions (certain restricted PaaS environments) or specific database limitations.
Conclusion
The addition of --sync-thread-lock-mode=SAFE_NO_LOCK bridges a long-standing gap in logical MySQL backups: achieving a completely lockless synchronization state without flying blind. By implementing a strict fail-fast policy, MyDumper ensures that database administrators never have to sacrifice backup integrity for system availability.
The post MyDumper Locking Mechanisms Revisited: Introducing SAFE_NO_LOCK appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>About a year ago, we discussed how <a href="https://www.percona.com/blog/mydumper-refactors-locking-mechanisms/">MyDumper refactored its locking mechanisms</a> to move away from old, rigid flags and transitioned towards more flexible, streamlined execution. Since then, the MyDumper community hasn’t stood still.</p>
<p>In recent releases, the locking architecture was further standardized under a single overarching option: <code>--sync-thread-lock-mode</code>. Along with this modernization came a powerful new safety feature designed to give you lock-free thread synchronization without risking silent inconsistency: SAFE_NO_LOCK (merged in PR <a href="https://github.com/mydumper/mydumper/pull/2031">#2031</a>).</p>
<p>Let’s explore the new thread-synchronization landscape and break down when you should use each mode.</p>
<h2>What is <code>--sync-thread-lock-mode</code>?</h2>
<p>Previously, flags like <code>-k</code>, <code>--no-locks</code> or <code>--lock-all-tables</code> dictated how MyDumper behaved. These have now been deprecated in favor of <code>--sync-thread-lock-mode</code>, which accepts five core values: AUTO, FTWRL, LOCK_ALL, GTID, NO_LOCK, and the newly added SAFE_NO_LOCK.</p>
<p>As a multi-threaded tool, MyDumper’s main challenge is ensuring that every single worker thread establishes its database snapshot at the exact same point in time. The sync mode you choose completely alters how MyDumper orchestrates this point-in-time synchronization.</p>
<h2>Understanding SAFE_NO_LOCK</h2>
<p>MyDumper fires off START TRANSACTION WITH CONSISTENT SNAPSHOT across its threads. It captures the binary log position at the very beginning of the process and compares it after the worker threads have attempted to synchronize.</p>
<p><span>When using NO_LOCK, if the threads don’t actually hit the same point in time—meaning they fail to synchronize—MyDumper simply logs a warning and continues backing up. This results in an inconsistent backup, which is a massive gamble for production systems.</span></p>
<p>SAFE_NO_LOCK adds a strict transactional safety net. If MyDumper detects any differences or drift in the binlog position among the threads during the synchronization phase, it immediately stops the backup. This prevents you from generating a corrupted, out-of-sync backup that will fail or cause data anomalies during a later restore.</p>
<h2>Choosing the Right Mode</h2>
<p>Depending on your architecture, uptime requirements, and database vendor, here is the breakdown of when to use each mode:</p>
<h3>AUTO (The Default)</h3>
<p>What it does: MyDumper <strong>automatically evaluates</strong> the database vendor, version, and capabilities to choose the safest, <strong>least-intrusive method</strong>.</p>
<p>When to use it: The vast majority of standard backups. It <strong>removes the guesswork</strong> and adapts dynamically if your database infrastructure upgrades.</p>
<h3>FTWRL (Flush Tables With Read Lock)</h3>
<p>What it does: It is the traditional method. It issues a <strong>global read lock</strong> via FLUSH TABLES WITH READ LOCK on the main connection, forces all threads to establish their consistent snapshot at that exact freeze frame, and then releases the lock.</p>
<p>When to use it:</p>
<ul>
<li>When you have non-transactional tables (like MyISAM or ARCHIVE) that must be consistently backed up alongside InnoDB tables.</li>
<li>When your database lacks advanced snapshot-tracking capabilities (older MySQL versions).</li>
</ul>
<p><span>Downside: It </span><b>blocks writes</b><span> across the entire instance during synchronization, which can cause a queue cascade </span><b>on a busy production server</b><span>.</span></p>
<h3>GTID</h3>
<p><span>Leverages a specific server variable in Percona Server called binlog_snapshot_gtid_executed to instantly verify if all threads are watching the exact same transaction state.</span></p>
<p>When to use it: If you are running <strong>Percona Server with GTID enabled</strong> and want a lightning-fast, lockless synchronization method that is guaranteed to be transactionally accurate.</p>
<h3>SAFE_NO_LOCK</h3>
<p>What it does: Uses transaction isolation to sync threads without global locks, but immediately aborts the backup if binlog positions diverge during initialization.</p>
<p>When to use it:</p>
<ul>
<li aria-level="1"><span>On highly sensitive production systems, where </span><b>global write locks are absolutely forbidden</b><span> due to strict SLAs.</span></li>
<li>When you are <strong>entirely utilizing transactional engines</strong> (InnoDB).</li>
<li>When you want a <strong>lock-free backup</strong> but require absolute certainty that your backup is <strong>100% consistent</strong>.</li>
</ul>
<p><span>Downside: In high-throughput write environments, threads may fail to align within the retry window, causing the backup job to abort. (Though an abort is always preferable to an inconsistent backup!).</span></p>
<h3>NO_LOCK</h3>
<p>What it does: Attempts lockless synchronization but logs a warning and proceeds even if consistency fails.</p>
<p><span>When to use it: Rarely, if ever, in the production primary server. It is </span><b>acceptable for staging environments</b><span>, development seeding, or scratch pads where data accuracy and point-in-time consistency are entirely secondary to getting a quick data dump without locking the server.</span></p>
<h3>LOCK_ALL</h3>
<p>What it does: Explicitly issues a <strong>LOCK TABLE</strong> command for every single table being exported.</p>
<p>When to use it: Primarily a fallback mode. Use this only when FLUSH TABLES WITH READ LOCK is completely unavailable due to restricted cloud permissions (certain restricted PaaS environments) or specific database limitations.</p>
<h2>Conclusion</h2>
<p>The addition of <code>--sync-thread-lock-mode=SAFE_NO_LOCK</code> bridges a long-standing gap in logical MySQL backups: achieving a completely lockless synchronization state without flying blind. By implementing a strict fail-fast policy, MyDumper ensures that database administrators never have to sacrifice backup integrity for system availability.</p>
<p>The post <a href="https://www.percona.com/blog/mydumper-locking-mechanisms-revisited-introducing-safe_no_lock/">MyDumper Locking Mechanisms Revisited: Introducing SAFE_NO_LOCK</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 13 Jul 2026 12:24:41 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>Insight for DBAs</category>
    <category>MySQL</category>
  </item>

  <item>
    <title>Dynamic Data Masking (DDM) with MySQL Enterprise Edition 9.7: Reduce your sensitive data exposure.</title>
    <guid isPermaLink="false">a09c38d0ec6d0e01fda2ff6d7c7135dd</guid>
    <link>https://blogs.oracle.com/mysql/dynamic-data-masking-ddm-with-mysql-enterprise-edition-9-7-reduce-your-sensitive-data-exposure</link>
    <description>With the new LTS (Long Term Support) release of MySQL 9.7.0 https://dev.mysql.com/doc/relnotes/mysql/9.7/en/ , Dynamic Data Masking (DDM) is one of the new features introduced as part of Enterprise Edition. The recent blog by Mike Frank, MySQL Product Management Director, details why DDM is important in every industry where PII (Personal Identifiable Information) data is stored […]</description>
    <pubDate>Mon, 13 Jul 2026 11:48:33 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Basics/How-To</category>
    <category>MySQL Enterprise</category>
    <category>Technical Solutions</category>
    <category>mysql</category>
    <category>Security</category>
  </item>

  <item>
    <title>InnoDB Flushing is simple – explained</title>
    <guid isPermaLink="false">https://kedar.nitty-witty.com/blog/?p=3598</guid>
    <link>https://kedar.nitty-witty.com/blog/innodb-flushing-is-simple-explained?utm_source=rss&amp;amp;utm_medium=rss&amp;amp;utm_campaign=innodb-flushing-is-simple-explained</link>
    <description>As a junior once I asked a seasoned MySQL DBA (Abuelo) “How do you stay so calm in critical situations?”Abuelo DBA then uttered golden words: “Son, I keep my dirty…
The post InnoDB Flushing is simple – explained first appeared on Change Is Inevitable.</description>
    <content:encoded><![CDATA[<p>As a junior once I asked a seasoned MySQL DBA (Abuelo) “How do you stay so calm in critical situations?”Abuelo DBA then uttered golden words: “Son, I keep my dirty…</p>
The post <a href="https://kedar.nitty-witty.com/blog/innodb-flushing-is-simple-explained">InnoDB Flushing is simple – explained</a> first appeared on <a href="https://kedar.nitty-witty.com/blog">Change Is Inevitable</a>.]]></content:encoded>
    <pubDate>Mon, 13 Jul 2026 05:15:00 +0000</pubDate>
    <dc:creator>Kedar Vaijanapurkar</dc:creator>
    <category>MySQL</category>
    <category>adaptive flushing</category>
    <category>buffer pool</category>
    <category>checkpoint age</category>
    <category>dirty pages</category>
    <category>innodb</category>
    <category>InnoDB flushing</category>
    <category>MySQL internals</category>
    <category>MySQL performance tuning</category>
    <category>redo log</category>
    <category>sync flush</category>
  </item>

  <item>
    <title>Running DuckDB as a MySQL 9.7 storage engine</title>
    <guid isPermaLink="false">https://www.percona.com/?p=50215</guid>
    <link>https://www.percona.com/blog/running-duckdb-as-a-mysql-9-7-storage-engine/</link>
    <description>ducksdb-mysql-engine is an experimental build of MySQL 9.7 where a table you mark ENGINE=DuckDB answers analytical queries from DuckDB instead of InnoDB. Same server, same connection, no second copy of the data. On TPC-H at scale factor 10, InnoDB times out on 6 of the 22 queries and burns 1317 seconds on the 16 it finishes. The DuckDB tables run all 22 in about 15 seconds.
It’s an experiment, not production software. It patches mysqld and has rough edges, which we list at the end. Source is on GitHub under GPLv2: https://github.com/Percona-Lab/ducksdb-mysql-engine.
Why we made it
MySQL is great for transactions and slow at analytics. A wide GROUP BY over a few hundred million rows, or a six-way join, takes minutes on InnoDB. The usual fix is to copy the data into a column store and keep it in sync, so now you’re running two systems and the pipeline between them.
We wanted the table itself to be the column store, with the heavy queries offloaded for you. Mark it ENGINE=DuckDB, query it the way you always have, and DuckDB does the analytical work.
What it actually is
DuckDB is an in-process columnar query engine, basically SQLite for OLAP. It stores data by column and it’s built for scans and aggregations, which is exactly what a row store is bad at.
We’re not the first to put it behind a relational table. Alibaba’s AliSQL has had a built-in DuckDB engine for a while. MariaDB shipped MariaDB DuckDB recently. AliSQL got there first; MariaDB and we landed on the same idea independently, around the same time, them on MariaDB and us on stock MySQL 9.7. Their engine is the closest comparison to ours, so it’s in the benchmarks below.
How it hooks into MySQL
MySQL doesn’t have a select_handler, the API MariaDB uses to grab a whole SELECT and run it inside an engine. We added our own: a handlerton::pushdown_select hook.

The pushdown path. Either the whole query renders to DuckDB SQL and runs columnar, or it declines and the normal row path handles it.
The engine is compiled into mysqld, and each schema is one DuckDB file under the datadir. Three patches do the integration, and all three are generic, so they’ll fire for any engine that exposes the hook:

The hook runs at the end of JOIN::optimize(). If every base table in the block is one engine that has the hook, that engine looks at the optimized JOIN, and if it can translate the whole query it sets JOIN::override_executor_func (which the executor already checks in sql_union.cc). The query gets regenerated as DuckDB SQL, prepared once, run, and the aggregated result is staged into a temp table. EXPLAIN is left alone.
A server-side LOAD DATA INFILE goes into a DuckDB COPY instead of crawling through write_row row by row. At 600M rows that’s a 20-minute load instead of 80.
For single-engine statements we clear OPTIMIZER_SWITCH_SEMIJOIN in prepare, so IN, EXISTS, NOT IN and NOT EXISTS stay as subqueries the builder can render instead of getting rewritten into semijoin nests it can’t recognize.

The builder only renders a node when the output is provably identical to what MySQL would return. If it can’t, it declines and MySQL runs the query unchanged. Literals are bound as parameters. Collation, NULL ordering and decimal scale are matched on purpose, and an unmapped collation or a REAL literal is enough to make it back off. With that in place, all 22 TPC-H queries push down and match InnoDB row for row.
 
Getting started
The fastest way in is the image:  docker run -d --name mysql-duckdb -p 3306:3306 \ 
   -e MYSQL_ROOT_PASSWORD=secret \ 
   -v mysql-duckdb-data:/var/lib/mysql \ 
   perconalab/ducksdb-mysql-engine:9.7-duckdb-v0.2.2Make a table, put a few rows in, run an aggregate:CREATE DATABASE shop; USE shop;
CREATE TABLE sales (id INT PRIMARY KEY, region INT, amount DECIMAL(12,2)) ENGINE=DuckDB;
INSERT INTO sales VALUES (1,1,100),(2,1,200),(3,2,50);SELECT region, SUM(amount) FROM sales GROUP BY region;
-- region | SUM(amount)
-- 1 | 300.00
-- 2 | 50.00 
Nothing about that query is special, and that is the point. To check it actually went to DuckDB rather than down the row path, watch the Ducksdb_pushdown_count status variable:
SELECT region, SUM(amount) FROM sales GROUP BY region; -- offloaded
SHOW STATUS LIKE 'Ducksdb_pushdown_count'; -- counter goes +1

SELECT * FROM sales WHERE id = 3; -- point lookup
SHOW STATUS LIKE 'Ducksdb_pushdown_count'; -- counter unchanged 
The single-row lookup stays on the row path deliberately. For one row an index seek beats spinning up a DuckDB result, so there is no reason to offload it. OLTP keeps its path, analytics get the column store, and you do not pick by hand.
If you would rather build it, you need the MySQL 9.7 tree under vendor/mysql-server/ and a DuckDB prefix, then:ln -s ../../engine vendor/mysql-server/storage/duckdb
scripts/build-server.sh # applies the 3 patches, builds mysqld + clients 
Does it actually go fast?
All 22 TPC-H queries, were executed in a Docker on one laptop (20 cores, 62 GiB RAM), the same data loaded into four engines: InnoDB, our MySQL+DuckDB, MariaDB+DuckDB, and standalone DuckDB as the reference. Warm wall-clock, minimum over a few runs, in seconds.
SF10, around 60 million lineitem rows
A handful of rows here; the whole table is in docs/tpch_engine_comparison.md:



Query
InnoDB
MySQL+DuckDB
MariaDB +DuckDB
Native DuckDB


Q1
&amp;gt;180
1.77
0.84
0.77


Q5
127.6
0.53
0.46
0.71


Q7
145.0
0.45
0.38
0.67


Q9
&amp;gt;180
1.52
2.30
1.78


Q18
101.7
1.35
1.39
1.31


Q19
120.4
0.15
0.67
0.83


All 22 
Finished 16/22
15.1s
13.3s
16.3



 

SF10, all 22 queries, log scale (lower is better). Hatched InnoDB bars did not finish inside 180 s. The three DuckDB engines sit in a tight band near the floor.
InnoDB is somewhere between 100 and 340 times slower per query, and on 6 of the 22 it never finished inside the 180-second cap (the correlated subqueries and the heaviest scans). It burned 1317 seconds on just the 16 it did finish. The three DuckDB engines get through all 22 in about 15 seconds, and ours lands right between MariaDB and plain DuckDB. The gap is so big there is not much else to say about it.
SF100, around 600 million lineitem rows
At this size InnoDB is out of the running (a copy of the data alone is about 100 GB and queries run for hours), so it is the three DuckDB engines only, run one at a time:



Query
MySQL+DuckDB
MariaDB+DuckDB
Native DuckDB


Q1
15.25
6.50
5.50


Q9
20.97
115.29
19.54


Q10
10.64
ERR
8.14


Q13
19.75
ERR
13.27


Q18
14.88
29.62
11.08


Q19
2.91
7.98
6.61


Correct
22/22
20/22
22/22



 

SF100, three DuckDB engines, log scale (lower is better). Q15 for our engine is shown at its matched-memory time (~4 s); the capped run measured 1309 s, explained below. MariaDB errored on Q10 and Q13.
At 600 million rows ours is still correct on all 22 and stays close to plain DuckDB. MariaDB’s engine drops two queries (Q10, and Q13 on its column-list syntax) and is a lot slower on the big joins – Q9 took 115 seconds against our 21 and native’s 20.
One honest word on Q15 at SF100, because in the full table it shows an ugly number for our engine. It is not a real loss. We capped DuckDB’s memory so it spills to disk instead of getting OOM-killed inside mysqld, and under that cap Q15’s CTE spills a lot. Give it the memory MariaDB had and it runs in about 4 seconds, like native. The answer was always right; only the clock was bad.
And one number we did not expect: loading those 600 million rows took about 20 minutes with our engine (the COPY shortcut) versus about 80 minutes with MariaDB, which loads row by row on a single core. Roughly four times faster to get the data in.
Try it, then tell us
If any of this sounds useful, pull the image and throw your own queries at it:docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret \ 
   perconalab/ducksdb-mysql-engine:9.7-duckdb-v0.2.2
The source is on GitHub (GPLv2), patches and benchmark harness included: https://github.com/Percona-Lab/ducksdb-mysql-engine. The full per-query benchmark and how we measured it live in docs/tpch_engine_comparison.md.
 
The post Running DuckDB as a MySQL 9.7 storage engine appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>ducksdb-mysql-engine is an experimental build of MySQL 9.7 where a table you mark ENGINE=DuckDB answers analytical queries from DuckDB instead of InnoDB. Same server, same connection, no second copy of the data. On TPC-H at scale factor 10, InnoDB times out on 6 of the 22 queries and burns 1317 seconds on the 16 it finishes. The DuckDB tables run all 22 in about 15 seconds.</p>
<p>It’s an experiment, not production software. It patches mysqld and has rough edges, which we list at the end. Source is on GitHub under GPLv2: <a href="https://github.com/Percona-Lab/ducksdb-mysql-engine.git"><u>https://github.com/Percona-Lab/ducksdb-mysql-engine</u></a>.</p>
<h2><b>Why we made it</b></h2>
<p>MySQL is great for transactions and slow at analytics. A wide GROUP BY over a few hundred million rows, or a six-way join, takes minutes on InnoDB. The usual fix is to copy the data into a column store and keep it in sync, so now you’re running two systems and the pipeline between them.</p>
<p>We wanted the table itself to be the column store, with the heavy queries offloaded for you. Mark it ENGINE=DuckDB, query it the way you always have, and DuckDB does the analytical work.</p>
<h2><b>What it actually is</b></h2>
<p><a href="https://duckdb.org/"><u>DuckDB</u></a> is an in-process columnar query engine, basically SQLite for OLAP. It stores data by column and it’s built for scans and aggregations, which is exactly what a row store is bad at.</p>
<p>We’re not the first to put it behind a relational table. Alibaba’s<a href="https://github.com/alibaba/AliSQL"> <u>AliSQL</u></a> has had a built-in DuckDB engine for a while. MariaDB shipped<a href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/"> <u>MariaDB DuckDB</u></a> recently. AliSQL got there first; MariaDB and we landed on the same idea independently, around the same time, them on MariaDB and us on stock MySQL 9.7. Their engine is the closest comparison to ours, so it’s in the benchmarks below.</p>
<h2><b>How it hooks into MySQL</b></h2>
<p>MySQL doesn’t have a select_handler, the API MariaDB uses to grab a whole SELECT and run it inside an engine. We added our own: a handlerton::pushdown_select hook.</p>
<p><img fetchpriority="high" decoding="async" class="aligncenter wp-image-50220 size-full" src="https://www.percona.com/wp-content/uploads/2026/07/architecture.png" alt="" width="761" height="883" srcset="https://www.percona.com/wp-content/uploads/2026/07/architecture.png 761w, https://www.percona.com/wp-content/uploads/2026/07/architecture-259x300.png 259w" sizes="(max-width: 761px) 100vw, 761px"></p>
<p><i>The pushdown path. Either the whole query renders to DuckDB SQL and runs columnar, or it declines and the normal row path handles it.</i></p>
<p>The engine is compiled into mysqld, and each schema is one DuckDB file under the datadir. Three patches do the integration, and all three are generic, so they’ll fire for any engine that exposes the hook:</p>
<ul>
<li>The hook runs at the end of JOIN::optimize(). If every base table in the block is one engine that has the hook, that engine looks at the optimized JOIN, and if it can translate the whole query it sets JOIN::override_executor_func (which the executor already checks in sql_union.cc). The query gets regenerated as DuckDB SQL, prepared once, run, and the aggregated result is staged into a temp table. EXPLAIN is left alone.</li>
<li>A server-side LOAD DATA INFILE goes into a DuckDB COPY instead of crawling through write_row row by row. At 600M rows that’s a 20-minute load instead of 80.</li>
<li>For single-engine statements we clear OPTIMIZER_SWITCH_SEMIJOIN in prepare, so IN, EXISTS, NOT IN and NOT EXISTS stay as subqueries the builder can render instead of getting rewritten into semijoin nests it can’t recognize.</li>
</ul>
<p>The builder only renders a node when the output is provably identical to what MySQL would return. If it can’t, it declines and MySQL runs the query unchanged. Literals are bound as parameters. Collation, NULL ordering and decimal scale are matched on purpose, and an unmapped collation or a REAL literal is enough to make it back off. With that in place, all 22 TPC-H queries push down and match InnoDB row for row.</p>
<p> </p>
<h2><b>Getting started</b></h2>
<p>The fastest way in is the image:</p><pre class="urvanov-syntax-highlighter-plain-tag">  docker run -d --name mysql-duckdb -p 3306:3306 \ 
   -e MYSQL_ROOT_PASSWORD=secret \ 
   -v mysql-duckdb-data:/var/lib/mysql \ 
   perconalab/ducksdb-mysql-engine:9.7-duckdb-v0.2.2</pre><p>Make a table, put a few rows in, run an aggregate:</p><pre class="urvanov-syntax-highlighter-plain-tag">CREATE DATABASE shop; USE shop;
CREATE TABLE sales (id INT PRIMARY KEY, region INT, amount DECIMAL(12,2)) ENGINE=DuckDB;
INSERT INTO sales VALUES (1,1,100),(2,1,200),(3,2,50);</pre><p></p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT region, SUM(amount) FROM sales GROUP BY region;
-- region | SUM(amount)
-- 1 | 300.00
-- 2 | 50.00</pre><p> </p>
<p>Nothing about that query is special, and that is the point. To check it actually went to DuckDB rather than down the row path, watch the Ducksdb_pushdown_count status variable:</p>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">SELECT region, SUM(amount) FROM sales GROUP BY region; -- offloaded
SHOW STATUS LIKE 'Ducksdb_pushdown_count'; -- counter goes +1

SELECT * FROM sales WHERE id = 3; -- point lookup
SHOW STATUS LIKE 'Ducksdb_pushdown_count'; -- counter unchanged</pre><p> </p>
<p>The single-row lookup stays on the row path deliberately. For one row an index seek beats spinning up a DuckDB result, so there is no reason to offload it. OLTP keeps its path, analytics get the column store, and you do not pick by hand.</p>
<p>If you would rather build it, you need the MySQL 9.7 tree under vendor/mysql-server/ and a DuckDB prefix, then:</p><pre class="urvanov-syntax-highlighter-plain-tag">ln -s ../../engine vendor/mysql-server/storage/duckdb
scripts/build-server.sh # applies the 3 patches, builds mysqld + clients</pre><p> </p>
<h2><b>Does it actually go fast?</b></h2>
<p>All 22 TPC-H queries, were executed in a Docker on one laptop (20 cores, 62 GiB RAM), the same data loaded into four engines: InnoDB, our MySQL+DuckDB, MariaDB+DuckDB, and standalone DuckDB as the reference. Warm wall-clock, minimum over a few runs, in seconds.</p>
<h3><b>SF10, around 60 million lineitem rows</b></h3>
<p>A handful of rows here; the whole table is in docs/tpch_engine_comparison.md:</p>
<table width="643" cellspacing="0" cellpadding="7">
<tbody>
<tr>
<td width="58">Query</td>
<td width="125">InnoDB</td>
<td width="118">MySQL+DuckDB</td>
<td width="144">MariaDB +DuckDB</td>
<td width="125">Native DuckDB</td>
</tr>
<tr>
<td width="58">Q1</td>
<td width="125">&gt;180</td>
<td width="118">1.77</td>
<td width="144">0.84</td>
<td width="125">0.77</td>
</tr>
<tr>
<td width="58">Q5</td>
<td width="125">127.6</td>
<td width="118">0.53</td>
<td width="144">0.46</td>
<td width="125">0.71</td>
</tr>
<tr>
<td width="58">Q7</td>
<td width="125">145.0</td>
<td width="118">0.45</td>
<td width="144">0.38</td>
<td width="125">0.67</td>
</tr>
<tr>
<td width="58">Q9</td>
<td width="125">&gt;180</td>
<td width="118">1.52</td>
<td width="144">2.30</td>
<td width="125">1.78</td>
</tr>
<tr>
<td width="58">Q18</td>
<td width="125">101.7</td>
<td width="118">1.35</td>
<td width="144">1.39</td>
<td width="125">1.31</td>
</tr>
<tr>
<td width="58">Q19</td>
<td width="125">120.4</td>
<td width="118">0.15</td>
<td width="144">0.67</td>
<td width="125">0.83</td>
</tr>
<tr>
<td width="58">All 22 </td>
<td width="125">Finished 16/22</td>
<td width="118">15.1s</td>
<td width="144">13.3s</td>
<td width="125">16.3</td>
</tr>
</tbody>
</table>
<p> </p>
<p align="center"><img decoding="async" class="aligncenter wp-image-50219 size-full" src="https://www.percona.com/wp-content/uploads/2026/07/sf10.png" alt="" width="2233" height="888" srcset="https://www.percona.com/wp-content/uploads/2026/07/sf10.png 2233w, https://www.percona.com/wp-content/uploads/2026/07/sf10-300x119.png 300w, https://www.percona.com/wp-content/uploads/2026/07/sf10-1024x407.png 1024w, https://www.percona.com/wp-content/uploads/2026/07/sf10-768x305.png 768w, https://www.percona.com/wp-content/uploads/2026/07/sf10-1536x611.png 1536w, https://www.percona.com/wp-content/uploads/2026/07/sf10-2048x814.png 2048w" sizes="(max-width: 2233px) 100vw, 2233px"></p>
<p align="center"><i>SF10, all 22 queries, log scale (lower is better). Hatched InnoDB bars did not finish inside 180 s. The three DuckDB engines sit in a tight band near the floor.</i></p>
<p>InnoDB is somewhere between 100 and 340 times slower per query, and on 6 of the 22 it never finished inside the 180-second cap (the correlated subqueries and the heaviest scans). It burned 1317 seconds on just the 16 it did finish. The three DuckDB engines get through all 22 in about 15 seconds, and ours lands right between MariaDB and plain DuckDB. The gap is so big there is not much else to say about it.</p>
<h3><b>SF100, around 600 million lineitem rows</b></h3>
<p>At this size InnoDB is out of the running (a copy of the data alone is about 100 GB and queries run for hours), so it is the three DuckDB engines only, run one at a time:</p>
<table width="640" cellspacing="0" cellpadding="7">
<tbody>
<tr>
<td width="142">Query</td>
<td width="146">MySQL+DuckDB</td>
<td width="146">MariaDB+DuckDB</td>
<td width="148">Native DuckDB</td>
</tr>
<tr>
<td width="142"><i>Q1</i></td>
<td width="146"><i>15.25</i></td>
<td width="146"><i>6.50</i></td>
<td width="148"><i>5.50</i></td>
</tr>
<tr>
<td width="142"><i>Q9</i></td>
<td width="146"><i>20.97</i></td>
<td width="146"><i>115.29</i></td>
<td width="148"><i>19.54</i></td>
</tr>
<tr>
<td width="142"><i>Q10</i></td>
<td width="146"><i>10.64</i></td>
<td width="146"><i>ERR</i></td>
<td width="148"><i>8.14</i></td>
</tr>
<tr>
<td width="142"><i>Q13</i></td>
<td width="146"><i>19.75</i></td>
<td width="146"><i>ERR</i></td>
<td width="148"><i>13.27</i></td>
</tr>
<tr>
<td width="142"><i>Q18</i></td>
<td width="146"><i>14.88</i></td>
<td width="146"><i>29.62</i></td>
<td width="148"><i>11.08</i></td>
</tr>
<tr>
<td width="142"><i>Q19</i></td>
<td width="146"><i>2.91</i></td>
<td width="146"><i>7.98</i></td>
<td width="148"><i>6.61</i></td>
</tr>
<tr>
<td width="142"><i>Correct</i></td>
<td width="146"><i>22/22</i></td>
<td width="146"><i>20/22</i></td>
<td width="148"><i>22/22</i></td>
</tr>
</tbody>
</table>
<p> </p>
<p><img decoding="async" class="aligncenter wp-image-50218 size-full" src="https://www.percona.com/wp-content/uploads/2026/07/sf100.png" alt="" width="2233" height="888" srcset="https://www.percona.com/wp-content/uploads/2026/07/sf100.png 2233w, https://www.percona.com/wp-content/uploads/2026/07/sf100-300x119.png 300w, https://www.percona.com/wp-content/uploads/2026/07/sf100-1024x407.png 1024w, https://www.percona.com/wp-content/uploads/2026/07/sf100-768x305.png 768w, https://www.percona.com/wp-content/uploads/2026/07/sf100-1536x611.png 1536w, https://www.percona.com/wp-content/uploads/2026/07/sf100-2048x814.png 2048w" sizes="(max-width: 2233px) 100vw, 2233px"></p>
<p align="center"><i>SF100, three DuckDB engines, log scale (lower is better). Q15 for our engine is shown at its matched-memory time (~4 s); the capped run measured 1309 s, explained below. MariaDB errored on Q10 and Q13.</i></p>
<p>At 600 million rows ours is still correct on all 22 and stays close to plain DuckDB. MariaDB’s engine drops two queries (Q10, and Q13 on its column-list syntax) and is a lot slower on the big joins – Q9 took 115 seconds against our 21 and native’s 20.</p>
<p>One honest word on Q15 at SF100, because in the full table it shows an ugly number for our engine. It is not a real loss. We capped DuckDB’s memory so it spills to disk instead of getting OOM-killed inside mysqld, and under that cap Q15’s CTE spills a lot. Give it the memory MariaDB had and it runs in about 4 seconds, like native. The answer was always right; only the clock was bad.</p>
<p>And one number we did not expect: loading those 600 million rows took about 20 minutes with our engine (the COPY shortcut) versus about 80 minutes with MariaDB, which loads row by row on a single core. Roughly four times faster to get the data in.</p>
<h2><b>Try it, then tell us</b></h2>
<p>If any of this sounds useful, pull the image and throw your own queries at it:</p><pre class="urvanov-syntax-highlighter-plain-tag">docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret \ 
   perconalab/ducksdb-mysql-engine:9.7-duckdb-v0.2.2</pre><p></p>
<p>The source is on GitHub (GPLv2), patches and benchmark harness included:<a href="https://github.com/Percona-Lab/ducksdb-mysql-engine.git"> <u>https://github.com/Percona-Lab/ducksdb-mysql-engine</u></a>. The full per-query benchmark and how we measured it live in docs/tpch_engine_comparison.md.</p>
<p> </p>
<p>The post <a href="https://www.percona.com/blog/running-duckdb-as-a-mysql-9-7-storage-engine/">Running DuckDB as a MySQL 9.7 storage engine</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Fri, 10 Jul 2026 13:38:16 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>MariaDB</category>
    <category>MySQL</category>
    <category>Open Source</category>
    <category>Storage Engine</category>
    <category>Percona</category>
  </item>

  <item>
    <title>MySQL 9.x: Moving Away From SHA1 and MD5</title>
    <guid isPermaLink="false">8c2b7d947df5329bfb49c4dc4e4ec2e6</guid>
    <link>https://blogs.oracle.com/mysql/mysql-9-x-moving-away-from-sha1-and-md5</link>
    <description>TL;DR If you use MD5(), SHA1(), or SHA() in MySQL today, start planning the move to SHA2(). Beginning with MySQL 9.6, MD5(), SHA1(), and SHA() are no longer native built-in SQL functions in the server binary. They are available through the Legacy Hashing Component: That component should be treated as a stopgap solution. It gives […]</description>
    <pubDate>Thu, 09 Jul 2026 13:20:51 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>generated columns</category>
    <category>MD5</category>
    <category>mysql</category>
    <category>MySQL 9.7 LTS</category>
    <category>Security</category>
    <category>SHA1</category>
    <category>SHA2</category>
    <category>upgrades</category>
  </item>

  <item>
    <title>Cross-site Disaster Recovery with Percona Operator for MySQL</title>
    <guid isPermaLink="false">https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/</guid>
    <link>https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/</link>
    <description>A MySQL InnoDB Cluster provides high availability for a single database cluster using Group Replication. This works well for node failures inside the cluster, but disaster recovery usually requires another cluster in a separate location: another Kubernetes cluster, region, data center, or cloud.
This replica cluster needs to stay in sync with the primary, remain protected from accidental writes, and be ready to take over when you need to move traffic, either as a planned operation or during an outage.
InnoDB ClusterSet addresses this by linking multiple MySQL clusters into a single disaster-recovery topology. One cluster handles writes, while the others stay synchronized as read-only replicas.
Starting from v1.2.0, the Percona Operator for MySQL adds a new custom resource, PerconaServerMySQLClusterSet, which allows managing InnoDB ClusterSets. Creating the ClusterSet, adding replicas, switching the primary, and performing a forced failover are all handled declaratively by updating the Kubernetes spec and letting the operator reconcile the desired state.
This post explains how ClusterSet works, how to set it up with the Percona Operator, and how planned switchovers and emergency failovers work in practice.
Understanding InnoDB ClusterSet
Any disaster recovery design usually comes down to two important numbers:

Recovery Point Objective, or RPO, is how much data you can afford to lose. For example, an RPO of five seconds means the business can tolerate losing up to five seconds of writes.
Recovery Time Objective, or RTO, is how long the system can be unavailable before service must be restored.

The way you design and operate a ClusterSet directly affects both. To understand why, it helps to first look at the architecture.
An InnoDB ClusterSet is built from two or more InnoDB Clusters. Each InnoDB Cluster is a Group Replication group. In other words, it is the same kind of highly available MySQL cluster that the Percona Operator for MySQL can already deploy and manage.
A ClusterSet adds another layer on top of those clusters. One cluster is the primary cluster and accepts writes, while the others are replica clusters and remain read-only. The primary sends its changes to each replica using asynchronous replication over a dedicated replication channel.
This gives us two layers of replication, each solving a different problem.
Inside each cluster, Group Replication protects against the loss of individual MySQL nodes. Members are expected to be closer together, usually within the same region or availability zone group. Writes are coordinated by the group, which helps keep the local cluster consistent and highly available.
Between clusters, asynchronous replication protects against the loss of an entire site. Replica clusters can be located in another region, another Kubernetes cluster, or another cloud provider. Because this replication is asynchronous, long-distance network latency does not slow down writes on the primary cluster.
But the tradeoff here is that a replica cluster may be slightly behind the primary. The amount of lag depends on write volume, network latency, and the health of the replication channel. If the primary site is lost, any writes that had not yet reached the replica are lost. That lag is the practical data-loss window during an emergency failover. Any transactions that had not replicated before failover could be lost.
Before building a ClusterSet with the operator, there are a few important requirements to keep in mind:

Every cluster in the ClusterSet must use the Group Replication topology. The operator also supports asynchronous replication with Orchestrator for standalone clusters, but that topology cannot be part of an InnoDB ClusterSet.
You need MySQL 8.0.27 or later
Clusters are linked by network address, not by Kubernetes references. A replica cluster only needs to be reachable and managed by an operator. It does not need to live in the same Kubernetes cluster as the primary.

With the model in place, let’s build a simple cross-site disaster recovery setup.


Setting up ClusterSet
We’ll create the simplest useful ClusterSet: two Group Replication clusters named dc1 and dc2.
In this example:
dc1 is the primary cluster.
dc2 is the read-only replica cluster.
In a real deployment, these would usually run in separate Kubernetes clusters, regions, or cloud environments. The steps are mostly the same. The main requirement is that the endpoints listed in the ClusterSet spec must be routable between sites.
Creating a primary cluster
The primary cluster dc1 is a regular Group Replication cluster. There is nothing ClusterSet-specific about it at this stage.

yaml
Copy
Copied!



apiVersion: ps.percona.com/v1
kind: PerconaServerMySQL
metadata:
 name: dc1
spec:
 mysql:
 clusterType: group-replication
 # ... the rest of a normal cluster spec


You can find a complete YAML here. Apply it and wait for it to come up the way you normally would, just as you would for any normal Percona Operator-managed MySQL cluster.
Creating the replica cluster
The replica cluster dc2 is also a Group Replication cluster, but with one important difference:

yaml
Copy
Copied!



apiVersion: ps.percona.com/v1
kind: PerconaServerMySQL
metadata:
 name: dc2
spec:
 mysql:
 clusterType: group-replication
 bootstrap:
 mode: manual  # 


Normally, when the operator creates a Group Replication cluster, the first MySQL pod bootstraps the group as soon as it starts. Subsequent pods then join that group.
For a ClusterSet replica, that is not what we want. We do not want dc2 to form an independent empty cluster. Instead, we want it to receive data from the primary cluster and then join the ClusterSet as a replica.
With bootstrap.mode: manual, the first pod starts but does not bootstrap its own Group Replication group. It waits until the ClusterSet process adopts it, clones data from the primary, and then forms the replica cluster. During this stage, the first dc2 pod may remain in a NotReady state until it is a part of the ClusterSet.
Sharing cluster credentials
The operator automatically creates a clusterset MySQL user in every cluster and stores its password in the cluster secret.
The operator uses this user to orchestrate ClusterSet operations, so the password must be the same across all clusters in the ClusterSet. When your clusters are deployed separately, copy the clusterset value from the primary cluster secret into the replica cluster secret before linking them.
For example, if dc1 is the primary, copy the clusterset password from the dc1 secret into the corresponding secret for dc2.
Linking the clusters
Once both clusters are applied, create a PerconaServerMySQLClusterSet custom resource.

yaml
Copy
Copied!



apiVersion: ps.percona.com/v1
kind: PerconaServerMySQLClusterSet
metadata:
 name: my-cluster-set
 finalizers:
 - percona.com/clusterset-dissolve
spec:
 primaryCluster: dc1
 credentialsSecret:
 name: dc1-secrets
 key: clusterset
 sslMode: AUTO
 createReplicaClusterOptions:
 recoveryMethod: clone
 clusters:
 - innodbClusterName: dc1
 endpoints:
 - host: dc1-mysql-primary.default.svc.cluster.local
 - innodbClusterName: dc2
 endpoints:
 - host: dc2-mysql-0.dc2-mysql.default.svc.cluster.local
 mysqlshellRunner:
 image: perconalab/percona-server-mysql-operator:main-psmysql8.4


The most important fields are:

primaryCluster defines which cluster currently accepts writes. The value must match one of the entries under clusters.
clusters lists every member of the ClusterSet and the endpoint the operator should use to reach it. These endpoints are plain network addresses, which is what allows members to run in different Kubernetes clusters or regions.
credentialsSecret points to the secret that contains the clusterset user password.
recoveryMethod: clone tells the replica cluster to take a full copy of the primary data when it joins the ClusterSet. The alternative is an incremental recovery method, which uses existing binary logs instead of cloning the full dataset.
mysqlshellRunner defines the helper pod image used by the operator to run MySQL Shell operations.

After you apply this resource, the operator starts a MySQL Shell runner pod and creates the ClusterSet on dc1. It then joins dc2, which clones the data, starts replication, and brings up the remaining pods in the replica cluster.
At this point, dc1 serves reads and writes, while dc2 acts as a live read-only copy.

Seeding large replica clusters
In this example, the replica cluster is created with recoveryMethod: clone, so MySQL Shell provisions the first replica member by copying a physical snapshot from an existing ClusterSet member. That is convenient for medium/small datasets, but it can be fragile across WAN links or very large databases.
A full clone can take hours, consume significant bandwidth, add load to the donor, run into network interruptions, and become expensive to retry if the operation fails partway through. It can also not be the best fit when the primary is busy or when cross-region egress cost is a concern.
The operator makes it possible to seed the replica cluster from an existing backup of the primary cluster instead. Create a PerconaServerMySQLBackup on the primary, restore that backup into the replica cluster with PerconaServerMySQLRestore, and then add the replica to the ClusterSet using recoveryMethod: incremental. You can find the exact restore procedure in the documentation.
At that point, the replica already has the primary’s data and GTID history, so ClusterSet only needs to catch it up from the primary’s binary logs instead of transferring the full dataset again.
Verifying it worked
The simplest way to confirm that the ClusterSet is working is to write data to the primary cluster and read it from the replica.
For example:

Connect to dc1.
Create a test table or insert a row.
Connect to dc2.
Confirm that the same data appears there.

If the row appears on dc2, the asynchronous replication channel is running and the replica cluster is receiving changes from the primary.
Planned Switchover
A planned switchover is used when both clusters are healthy and you intentionally want to move writes from one site to another. This is useful for regional maintenance, Kubernetes cluster upgrades, cloud migrations, or controlled DR testing.
To move the primary role from dc1 to dc2, update the primaryCluster field:

shell
Copy
Copied!



kubectl patch ps-clusterset my-cluster-set --type=merge \
 -p '{&quot;spec&quot;:{&quot;primaryCluster&quot;:&quot;dc2&quot;}}'


The operator notices that the desired primary cluster no longer matches the current primary. It then uses MySQL Shell to perform a clean switchover.
Because both clusters are available, the operator can make sure the replica has caught up before changing roles. After the switchover completes, dc2 becomes the writable primary and dc1 becomes a read-only replica.
Emergency Failover
An emergency failover can be used when the primary cluster is unreachable and a clean handover is no longer possible.
This is the disaster recovery case: the Kubernetes cluster, region, or network path to the primary may be down, and you need to promote a surviving replica so the application can resume writes.
To fail over to dc2, update primaryCluster and explicitly set the forced failover flag:

shell
Copy
Copied!



kubectl patch ps-clusterset my-cluster-set --type=merge \
 -p '{&quot;spec&quot;:{&quot;primaryCluster&quot;:&quot;dc2&quot;,&quot;unsafeFlags&quot;:{&quot;forcedFailover&quot;:true}}}'


The operator only follows this path when it can confirm that the current primary cluster is unreachable. It then promotes dc2, allowing it to accept writes.
The explicit flag is important because failover can cause data loss. Replication between clusters is asynchronous, so any writes that reached the old primary but had not yet replicated to dc2 are not present on the new primary. Once dc2 is promoted, those missing writes become unrecoverable through normal ClusterSet recovery.
The risk of data loss is why the field is named unsafeFlags.forcedFailover.
Another important point is that when the old primary comes back, it does not automatically resume as primary. After a forced failover, the recovered cluster must be explicitly reintroduced into the ClusterSet as a replica.
Adding and removing clusters
Adding or removing clusters follows the same declarative pattern: update the custom resource spec and let the operator reconcile the difference.
To add another replica cluster, add a new entry under clusters:

yaml
Copy
Copied!



apiVersion: ps.percona.com/v1
kind: PerconaServerMySQLClusterSet
metadata:
 name: my-cluster-set
spec:
 # .. existing spec
 clusters:
 # .. existing clusters
 - innodbClusterName: dc3
 endpoints:
 - host: dc3-mysql-primary.default.svc.cluster.local


The operator joins the new cluster in the same way it joined dc2: it clones data from the primary, configures replication, and brings the cluster into the ClusterSet as a read-only replica.
To remove a cluster, delete its entry from the clusters list. You can update your manifest and reapply it, or use a JSON patch:

shell
Copy
Copied!



kubectl patch ps-clusterset my-cluster-set --type=json \
 -p '[{&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/spec/clusters/1&quot;}]'


If the cluster is healthy, the operator detaches it cleanly and it becomes a normal standalone cluster again.
If the cluster being removed is unreachable, you can force its removal:

shell
Copy
Copied!



kubectl patch ps-clusterset my-cluster-set --type=json -p '[
 {&quot;op&quot;:&quot;remove&quot;,&quot;path&quot;:&quot;/spec/clusters/1&quot;},
 {&quot;op&quot;:&quot;add&quot;,&quot;path&quot;:&quot;/spec/unsafeFlags/forcedClusterRemoval&quot;,&quot;value&quot;:true}
]'


Like forced failover, forced removal is gated behind an unsafe flag because the operator should not make this decision silently. Removing an unreachable cluster from a ClusterSet is an operational decision with consequences, and it should be made explicitly.
Wrapping up
The Percona Operator for MySQL allows extending Group Replication beyond a single site by managing InnoDB ClusterSet through a custom resource PerconaServerMySQLClusterSet. A primary cluster handles writes, replica clusters stay synchronized, and the operator manages switchovers, failovers, and membership changes declaratively.
For planned maintenance, switchover moves the primary role safely with no data loss. For outages, forced failover promotes a surviving replica, with the expected risk of losing any writes that had not yet replicated. That replication lag is the practical RPO, so it should be monitored and tested as part of the DR plan.
With the Percona Operator for MySQL, disaster recovery becomes repeatable, Kubernetes-native, and easier to operate across regions or clusters.
Further reading

InnoDB ClusterSet docs
Cross-site replication in Percona Operator for MySQL
</description>
    <content:encoded><![CDATA[<p>A MySQL InnoDB Cluster provides high availability for a single database cluster using Group Replication. This works well for node failures inside the cluster, but disaster recovery usually requires another cluster in a separate location: another Kubernetes cluster, region, data center, or cloud.</p>
<p>This replica cluster needs to stay in sync with the primary, remain protected from accidental writes, and be ready to take over when you need to move traffic, either as a planned operation or during an outage.</p>
<p><a href="https://dev.mysql.com/doc/mysql-shell/8.0/en/innodb-clusterset.html" target="_blank" rel="noopener noreferrer">InnoDB ClusterSet</a> addresses this by linking multiple MySQL clusters into a single disaster-recovery topology. One cluster handles writes, while the others stay synchronized as read-only replicas.</p>
<p>Starting from v1.2.0, the Percona Operator for MySQL adds a new custom resource, <code>PerconaServerMySQLClusterSet</code>, which allows managing InnoDB ClusterSets. Creating the ClusterSet, adding replicas, switching the primary, and performing a forced failover are all handled declaratively by updating the Kubernetes spec and letting the operator reconcile the desired state.</p>
<p>This post explains how ClusterSet works, how to set it up with the Percona Operator, and how planned switchovers and emergency failovers work in practice.</p>
<h2>Understanding InnoDB ClusterSet</h2>
<p>Any disaster recovery design usually comes down to two important numbers:</p>
<ul>
<li><strong>Recovery Point Objective</strong>, or RPO, is how much data you can afford to lose. For example, an RPO of five seconds means the business can tolerate losing up to five seconds of writes.</li>
<li><strong>Recovery Time Objective</strong>, or RTO, is how long the system can be unavailable before service must be restored.</li>
</ul>
<p>The way you design and operate a ClusterSet directly affects both. To understand why, it helps to first look at the architecture.</p>
<p>An InnoDB ClusterSet is built from two or more InnoDB Clusters. Each InnoDB Cluster is a Group Replication group. In other words, it is the same kind of highly available MySQL cluster that the <a href="https://docs.percona.com/percona-operator-for-mysql/latest/index.html" target="_blank" rel="noopener noreferrer">Percona Operator for MySQL</a> can already deploy and manage.</p>
<p>A ClusterSet adds another layer on top of those clusters. One cluster is the primary cluster and accepts writes, while the others are replica clusters and remain read-only. The primary sends its changes to each replica using asynchronous replication over a dedicated replication channel.</p>
<p>This gives us two layers of replication, each solving a different problem.</p>
<p>Inside each cluster, Group Replication protects against the loss of individual MySQL nodes. Members are expected to be closer together, usually within the same region or availability zone group. Writes are coordinated by the group, which helps keep the local cluster consistent and highly available.</p>
<p>Between clusters, asynchronous replication protects against the loss of an entire site. Replica clusters can be located in another region, another Kubernetes cluster, or another cloud provider. Because this replication is asynchronous, long-distance network latency does not slow down writes on the primary cluster.</p>
<p>But the tradeoff here is that a replica cluster may be slightly behind the primary. The amount of lag depends on write volume, network latency, and the health of the replication channel. If the primary site is lost, any writes that had not yet reached the replica are lost. That lag is the practical data-loss window during an emergency failover. Any transactions that had not replicated before failover could be lost.</p>
<p>Before building a ClusterSet with the operator, there are a few important requirements to keep in mind:</p>
<ul>
<li>Every cluster in the ClusterSet must use the Group Replication topology. The operator also supports asynchronous replication with Orchestrator for standalone clusters, but that topology cannot be part of an InnoDB ClusterSet.</li>
<li>You need MySQL 8.0.27 or later</li>
<li>Clusters are linked by network address, not by Kubernetes references. A replica cluster only needs to be reachable and managed by an operator. It does not need to live in the same Kubernetes cluster as the primary.</li>
</ul>
<p>With the model in place, let’s build a simple cross-site disaster recovery setup.</p>
<p>
<figure><img src="https://percona.community/blog/2026/06/ps-clusterset-architecture.svg" alt="Figure 1. InnoDB ClusterSet overview"></figure></p>
<h2>Setting up ClusterSet</h2>
<p>We’ll create the simplest useful ClusterSet: two Group Replication clusters named <code>dc1</code> and <code>dc2</code>.</p>
<p>In this example:
<code>dc1</code> is the primary cluster.
<code>dc2</code> is the read-only replica cluster.</p>
<p>In a real deployment, these would usually run in separate Kubernetes clusters, regions, or cloud environments. The steps are mostly the same. The main requirement is that the endpoints listed in the ClusterSet spec must be routable between sites.</p>
<h3>Creating a primary cluster</h3>
<p>The primary cluster <code>dc1</code> is a regular Group Replication cluster. There is nothing ClusterSet-specific about it at this stage.</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-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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">ps.percona.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PerconaServerMySQL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</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">dc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</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</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clusterType</span><span class="p">:</span><span class="w"> </span><span class="l">group-replication</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># ... the rest of a normal cluster spec</span></span></span></code></pre></div>
</div>
</div>
<p>You can find a complete YAML <a href="https://github.com/percona/percona-server-mysql-operator/blob/main/deploy/cr.yaml" target="_blank" rel="noopener noreferrer">here</a>. Apply it and wait for it to come up the way you normally would, just as you would for any normal Percona Operator-managed MySQL cluster.</p>
<h3>Creating the replica cluster</h3>
<p>The replica cluster <code>dc2</code> is also a Group Replication cluster, but with one important difference:</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-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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">ps.percona.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PerconaServerMySQL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</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">dc2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</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</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clusterType</span><span class="p">:</span><span class="w"> </span><span class="l">group-replication</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">mode</span><span class="p">:</span><span class="w"> </span><span class="l">manual </span><span class="w"> </span><span class="c"># </span></span></span></code></pre></div>
</div>
</div>
<p>Normally, when the operator creates a Group Replication cluster, the first MySQL pod bootstraps the group as soon as it starts. Subsequent pods then join that group.</p>
<p>For a ClusterSet replica, that is not what we want. We do not want <code>dc2</code> to form an independent empty cluster. Instead, we want it to receive data from the primary cluster and then join the ClusterSet as a replica.</p>
<p>With <code>bootstrap.mode: manual</code>, the first pod starts but does not bootstrap its own Group Replication group. It waits until the ClusterSet process adopts it, clones data from the primary, and then forms the replica cluster. During this stage, the first <code>dc2</code> pod may remain in a <code>NotReady</code> state until it is a part of the ClusterSet.</p>
<h3>Sharing cluster credentials</h3>
<p>The operator automatically creates a <code>clusterset</code> MySQL user in every cluster and stores its password in the cluster secret.</p>
<p>The operator uses this user to orchestrate ClusterSet operations, so the password must be the same across all clusters in the ClusterSet. When your clusters are deployed separately, copy the <code>clusterset</code> value from the primary cluster secret into the replica cluster secret before linking them.</p>
<p>For example, if <code>dc1</code> is the primary, copy the <code>clusterset</code> password from the <code>dc1</code> secret into the corresponding secret for <code>dc2</code>.</p>
<h3>Linking the clusters</h3>
<p>Once both clusters are applied, create a <code>PerconaServerMySQLClusterSet</code> custom resource.</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">ps.percona.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PerconaServerMySQLClusterSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</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">my-cluster-set</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">finalizers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">percona.com/clusterset-dissolve</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">primaryCluster</span><span class="p">:</span><span class="w"> </span><span class="l">dc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">credentialsSecret</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">dc1-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">clusterset</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">sslMode</span><span class="p">:</span><span class="w"> </span><span class="l">AUTO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">createReplicaClusterOptions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">recoveryMethod</span><span class="p">:</span><span class="w"> </span><span class="l">clone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clusters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">innodbClusterName</span><span class="p">:</span><span class="w"> </span><span class="l">dc1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">dc1-mysql-primary.default.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">innodbClusterName</span><span class="p">:</span><span class="w"> </span><span class="l">dc2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">dc2-mysql-0.dc2-mysql.default.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">mysqlshellRunner</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">perconalab/percona-server-mysql-operator:main-psmysql8.4</span></span></span></code></pre></div>
</div>
</div>
<p>The most important fields are:</p>
<ul>
<li><code>primaryCluster</code> defines which cluster currently accepts writes. The value must match one of the entries under clusters.</li>
<li><code>clusters</code> lists every member of the ClusterSet and the endpoint the operator should use to reach it. These endpoints are plain network addresses, which is what allows members to run in different Kubernetes clusters or regions.</li>
<li><code>credentialsSecret</code> points to the secret that contains the clusterset user password.</li>
<li><code>recoveryMethod: clone</code> tells the replica cluster to take a full copy of the primary data when it joins the ClusterSet. The alternative is an incremental recovery method, which uses existing binary logs instead of cloning the full dataset.</li>
<li><code>mysqlshellRunner</code> defines the helper pod image used by the operator to run MySQL Shell operations.</li>
</ul>
<p>After you apply this resource, the operator starts a MySQL Shell runner pod and creates the ClusterSet on <code>dc1</code>. It then joins <code>dc2</code>, which clones the data, starts replication, and brings up the remaining pods in the replica cluster.</p>
<p>At this point, <code>dc1</code> serves reads and writes, while <code>dc2</code> acts as a live read-only copy.</p>
<blockquote>
<p><strong>Seeding large replica clusters</strong></p>
<p>In this example, the replica cluster is created with <code>recoveryMethod: clone</code>, so MySQL Shell provisions the first replica member by copying a physical snapshot from an existing ClusterSet member. That is convenient for medium/small datasets, but it can be fragile across WAN links or very large databases.</p>
<p>A full clone can take hours, consume significant bandwidth, add load to the donor, run into network interruptions, and become expensive to retry if the operation fails partway through. It can also not be the best fit when the primary is busy or when cross-region egress cost is a concern.</p>
<p>The operator makes it possible to seed the replica cluster from an existing backup of the primary cluster instead. Create a <code>PerconaServerMySQLBackup</code> on the primary, restore that backup into the replica cluster with <code>PerconaServerMySQLRestore</code>, and then add the replica to the ClusterSet using <code>recoveryMethod: incremental</code>. You can find the exact restore procedure in the <a href="https://docs.percona.com/percona-operator-for-mysql/latest/backups-restore-to-new-cluster.html" target="_blank" rel="noopener noreferrer">documentation</a>.</p>
<p>At that point, the replica already has the primary’s data and GTID history, so ClusterSet only needs to catch it up from the primary’s binary logs instead of transferring the full dataset again.</p></blockquote>
<h3>Verifying it worked</h3>
<p>The simplest way to confirm that the ClusterSet is working is to write data to the primary cluster and read it from the replica.</p>
<p>For example:</p>
<ul>
<li>Connect to <code>dc1</code>.</li>
<li>Create a test table or insert a row.</li>
<li>Connect to <code>dc2</code>.</li>
<li>Confirm that the same data appears there.</li>
</ul>
<p>If the row appears on <code>dc2</code>, the asynchronous replication channel is running and the replica cluster is receiving changes from the primary.</p>
<h3>Planned Switchover</h3>
<p>A planned switchover is used when both clusters are healthy and you intentionally want to move writes from one site to another. This is useful for regional maintenance, Kubernetes cluster upgrades, cloud migrations, or controlled DR testing.</p>
<p>To move the primary role from <code>dc1</code> to <code>dc2</code>, update the primaryCluster field:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">shell</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-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ps-clusterset my-cluster-set --type<span class="o">=</span>merge <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -p <span class="s1">'{"spec":{"primaryCluster":"dc2"}}'</span></span></span></code></pre></div>
</div>
</div>
<p>The operator notices that the desired primary cluster no longer matches the current primary. It then uses MySQL Shell to perform a clean switchover.</p>
<p>Because both clusters are available, the operator can make sure the replica has caught up before changing roles. After the switchover completes, <code>dc2</code> becomes the writable primary and <code>dc1</code> becomes a read-only replica.</p>
<h3>Emergency Failover</h3>
<p>An emergency failover can be used when the primary cluster is unreachable and a clean handover is no longer possible.</p>
<p>This is the disaster recovery case: the Kubernetes cluster, region, or network path to the primary may be down, and you need to promote a surviving replica so the application can resume writes.</p>
<p>To fail over to <code>dc2</code>, update primaryCluster and explicitly set the forced failover flag:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">shell</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-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ps-clusterset my-cluster-set --type<span class="o">=</span>merge <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -p <span class="s1">'{"spec":{"primaryCluster":"dc2","unsafeFlags":{"forcedFailover":true}}}'</span></span></span></code></pre></div>
</div>
</div>
<p>The operator only follows this path when it can confirm that the current primary cluster is unreachable. It then promotes <code>dc2</code>, allowing it to accept writes.</p>
<p>The explicit flag is important because failover can cause data loss. Replication between clusters is asynchronous, so any writes that reached the old primary but had not yet replicated to <code>dc2</code> are not present on the new primary. Once <code>dc2</code> is promoted, those missing writes become unrecoverable through normal ClusterSet recovery.</p>
<p>The risk of data loss is why the field is named <code>unsafeFlags.forcedFailover</code>.</p>
<p>Another important point is that when the old primary comes back, it does not automatically resume as primary. After a forced failover, the recovered cluster must be explicitly reintroduced into the ClusterSet as a replica.</p>
<h3>Adding and removing clusters</h3>
<p>Adding or removing clusters follows the same declarative pattern: update the custom resource spec and let the operator reconcile the difference.</p>
<p>To add another replica cluster, add a new entry under clusters:</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-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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">ps.percona.com/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PerconaServerMySQLClusterSet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</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">my-cluster-set</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># .. existing spec</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">clusters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># .. existing clusters</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">innodbClusterName</span><span class="p">:</span><span class="w"> </span><span class="l">dc3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">endpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">dc3-mysql-primary.default.svc.cluster.local</span></span></span></code></pre></div>
</div>
</div>
<p>The operator joins the new cluster in the same way it joined <code>dc2</code>: it clones data from the primary, configures replication, and brings the cluster into the ClusterSet as a read-only replica.</p>
<p>To remove a cluster, delete its entry from the clusters list. You can update your manifest and reapply it, or use a JSON patch:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">shell</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-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ps-clusterset my-cluster-set --type<span class="o">=</span>json <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -p <span class="s1">'[{"op":"remove","path":"/spec/clusters/1"}]'</span></span></span></code></pre></div>
</div>
</div>
<p>If the cluster is healthy, the operator detaches it cleanly and it becomes a normal standalone cluster again.</p>
<p>If the cluster being removed is unreachable, you can force its removal:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">shell</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-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ps-clusterset my-cluster-set --type<span class="o">=</span>json -p <span class="s1">'[
</span></span></span><span class="line"><span class="cl"><span class="s1"> {"op":"remove","path":"/spec/clusters/1"},
</span></span></span><span class="line"><span class="cl"><span class="s1"> {"op":"add","path":"/spec/unsafeFlags/forcedClusterRemoval","value":true}
</span></span></span><span class="line"><span class="cl"><span class="s1">]'</span></span></span></code></pre></div>
</div>
</div>
<p>Like forced failover, forced removal is gated behind an unsafe flag because the operator should not make this decision silently. Removing an unreachable cluster from a ClusterSet is an operational decision with consequences, and it should be made explicitly.</p>
<h3>Wrapping up</h3>
<p>The Percona Operator for MySQL allows extending Group Replication beyond a single site by managing InnoDB ClusterSet through a custom resource <code>PerconaServerMySQLClusterSet</code>. A primary cluster handles writes, replica clusters stay synchronized, and the operator manages switchovers, failovers, and membership changes declaratively.</p>
<p>For planned maintenance, switchover moves the primary role safely with no data loss. For outages, forced failover promotes a surviving replica, with the expected risk of losing any writes that had not yet replicated. That replication lag is the practical RPO, so it should be monitored and tested as part of the DR plan.</p>
<p>With the Percona Operator for MySQL, disaster recovery becomes repeatable, Kubernetes-native, and easier to operate across regions or clusters.</p>
<h3>Further reading</h3>
<ul>
<li><a href="https://dev.mysql.com/doc/mysql-shell/8.0/en/innodb-clusterset.html" target="_blank" rel="noopener noreferrer">InnoDB ClusterSet docs</a></li>
<li><a href="https://docs.percona.com/percona-operator-for-mysql/latest/replication.html" target="_blank" rel="noopener noreferrer">Cross-site replication in Percona Operator for MySQL</a></li>
</ul>]]></content:encoded>
    <pubDate>Mon, 06 Jul 2026 10:00:00 +0000</pubDate>
    <dc:creator>Percona Community</dc:creator>
    <category>Percona</category>
    <category>Kubernetes</category>
    <category>Cloud</category>
    <category>Community</category>
    <category>Open Source</category>
    <category>MySQL</category>
  </item>

  <item>
    <title>MySQL Community Server 26.7 Early Access Release</title>
    <guid isPermaLink="false">16e5ffbccc916b1c9e2b4dc61875438c</guid>
    <link>https://blogs.oracle.com/mysql/mysql-community-server-26-7-early-access-release</link>
    <description>MySQL 26.7 is the initial MySQL Innovation release following the MySQL 9.7 LTS release and uses the new yy.mmCalVer versioning model for quarterly Innovation releases. This Early Access release provides a preview of selected functionality planned for the MySQL Community Server package and gives users an opportunity to evaluate upcoming changes before general availability. Download MySQL […]</description>
    <pubDate>Fri, 03 Jul 2026 16:58:15 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>News</category>
    <category>early access</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

  <item>
    <title>Still on MySQL 5.7 or 8.0? Those high-severity CVE fixes are covered</title>
    <guid isPermaLink="false">https://www.percona.com/?p=50165</guid>
    <link>https://www.percona.com/blog/mysql-8-0-eol-support-cve-fixes-covered/</link>
    <description>Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. If you’re running Percona Server for MySQL 5.7 or 8.0 under Extended Lifecycle Support (ELS), the program we previously called Post EOL Support, you don’t have to do anything to qualify for them. We’ve already applied the fixes and re-released the affected ELS builds.
This is the point of ELS. When a major version reaches End of Life (EOL), the community stops shipping patches, but the databases running on it don’t stop mattering. ELS keeps critical bug and security fixes coming for versions that are past their EOL date, so you can stay on 5.7 or 8.0 on your own timeline instead of a deadline someone else set.
What we did
These CVE fixes landed upstream outside the normal cadence. Under ELS, customers are entitled to security fixes for the versions they run, so we pulled the patches into the 5.7 and 8.0 builds and re-released them. ELS customers will get access to the updated builds from the usual private repository in the next couple of weeks.
Why this matters if you’re still on 5.7 or 8.0
Percona Server for MySQL 5.7 reached EOL in October 2023. Percona Server for MySQL 8.0 reached EOL in April 2026. Plenty of production systems are still on both, and not every migration can happen on the upstream’s schedule. Running an unpatched database past EOL is where the real risk sits: no security fixes, no bug fixes, and no support when something breaks at 2:00 a.m.
ELS closes that gap. You keep getting the critical fixes, including out-of-schedule security patches like these, while you plan an upgrade on terms that work for your team.
Where to go from here
If you’re on 5.7 or 8.0 and don’t have ELS in place, now is a good time to look at it. The fixes we just shipped are exactly what the program is for. See the details for your version: Extended Lifecycle Support for MySQL 8.0 or Extended Lifecycle Support for MySQL 5.7. Or reach out via percona.com or the Percona Community Forum to discuss coverage for your environment.
 

Written by @Dennis Kittrell – Reviewed by @Matthew Boehm &amp;amp; @Varun Nagaraju 

The post Still on MySQL 5.7 or 8.0? Those high-severity CVE fixes are covered appeared first on Percona.</description>
    <content:encoded><![CDATA[<p>Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. If you’re running Percona Server for MySQL 5.7 or 8.0 under Extended Lifecycle Support (ELS), the program we previously called Post EOL Support, you don’t have to do anything to qualify for them. We’ve already applied the fixes and re-released the affected ELS builds.</p>
<p>This is the point of ELS. When a major version reaches End of Life (EOL), the community stops shipping patches, but the databases running on it don’t stop mattering. ELS keeps critical bug and security fixes coming for versions that are past their EOL date, so you can stay on 5.7 or 8.0 on your own timeline instead of a deadline someone else set.</p>
<h2>What we did</h2>
<p>These CVE fixes landed upstream outside the normal cadence. Under ELS, customers are entitled to security fixes for the versions they run, so we pulled the patches into the 5.7 and 8.0 builds and re-released them. ELS customers will get access to the updated builds from the usual private repository in the next couple of weeks.</p>
<h2>Why this matters if you’re still on 5.7 or 8.0</h2>
<p>Percona Server for MySQL 5.7 reached EOL in October 2023. Percona Server for MySQL 8.0 reached EOL in April 2026. Plenty of production systems are still on both, and not every migration can happen on the upstream’s schedule. Running an unpatched database past EOL is where the real risk sits: no security fixes, no bug fixes, and no support when something breaks at 2:00 a.m.</p>
<p>ELS closes that gap. You keep getting the critical fixes, including out-of-schedule security patches like these, while you plan an upgrade on terms that work for your team.</p>
<h2>Where to go from here</h2>
<p>If you’re on 5.7 or 8.0 and don’t have ELS in place, now is a good time to look at it. The fixes we just shipped are exactly what the program is for. See the details for your version: <a href="https://www.percona.com/mysql-8-0-eol-support/">Extended Lifecycle Support for MySQL 8.0</a> or <a href="https://www.percona.com/post-mysql-5-7-eol-support/">Extended Lifecycle Support for MySQL 5.7</a>. Or reach out via <a href="http://percona.com/">percona.com</a> or the Percona Community Forum to discuss coverage for your environment.</p>
<p> </p>
<hr>
<p><span class="notion-enable-hover" data-token-index="0">Written by </span><span class="notion-text-mention-token notion-enable-hover notion-focusable-token" tabindex="0" contenteditable="false" data-token-index="1">@Dennis Kittrell</span><span class="notion-enable-hover" data-token-index="2"> – Reviewed by </span><span class="notion-text-mention-token notion-enable-hover notion-focusable-token" tabindex="0" contenteditable="false" data-token-index="3">@Matthew Boehm</span><span class="notion-enable-hover" data-token-index="4"> &amp; </span><span class="notion-text-mention-token notion-enable-hover notion-focusable-token" tabindex="0" contenteditable="false" data-token-index="5">@Varun Nagaraju</span> <!-- notionvc: 48fbd903-e255-42ad-8db1-f691698fae89 --></p>
<p><!-- notionvc: f563df38-9d48-4c17-a7b8-cf1211d095a0 --></p>
<p>The post <a href="https://www.percona.com/blog/mysql-8-0-eol-support-cve-fixes-covered/">Still on MySQL 5.7 or 8.0? Those high-severity CVE fixes are covered</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Thu, 02 Jul 2026 08:01:50 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>MySQL</category>
    <category>Percona Software</category>
    <category>Security</category>
    <category>CVE</category>
    <category>Extended Lifecycle Support</category>
    <category>MySQL 5.7</category>
    <category>MySQL 8.0 EOL</category>
    <category>Percona Server for MySQL</category>
    <category>security</category>
  </item>

  <item>
    <title>MySQL &amp; MySQL HeatWave Report – June 2026</title>
    <guid isPermaLink="false">https://dasini.net/blog/?p=8792</guid>
    <link>https://dasini.net/blog/2026/07/01/mysql-mysql-heatwave-report-june-2026/</link>
    <description>Keeping up with the MySQL ecosystem is becoming increasingly challenging. Every release introduces new features, performance improvements, security enhancements, and cloud capabilities. While the official documentation is comprehensive, it is not always easy to quickly identify what really matters.
To help with that, I've published a new edition of my MySQL &amp;amp; MySQL HeatWave Report, covering the most important announcements around MySQL 9.7 LTS and MySQL HeatWave 9.7.
Slides: https://speakerdeck.com/freshdaz/mysql-and-mysql-heatwave-report-june-2026
The post MySQL &amp;amp; MySQL HeatWave Report – June 2026 first appeared on Data Daz (dasini.net) - Data Systems, AI, and Real-World Insights.</description>
    <content:encoded><![CDATA[<p>Keeping up with the MySQL ecosystem is becoming increasingly challenging. Every release introduces new features, performance improvements, security enhancements, and cloud capabilities. While the official documentation is comprehensive, it is not always easy to quickly identify what really matters.</p>
<p>To help with that, I've published a new edition of my MySQL &amp; MySQL HeatWave Report, covering the most important announcements around MySQL 9.7 LTS and MySQL HeatWave 9.7.<br>
Slides: https://speakerdeck.com/freshdaz/mysql-and-mysql-heatwave-report-june-2026</p>
The post <a href="https://dasini.net/blog/2026/07/01/mysql-mysql-heatwave-report-june-2026/">MySQL &amp; MySQL HeatWave Report – June 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, 01 Jul 2026 13:05:59 +0000</pubDate>
    <dc:creator>Olivier Dasini</dc:creator>
    <category>AI</category>
    <category>Cloud</category>
    <category>HeatWave</category>
    <category>High Availability</category>
    <category>MDS</category>
    <category>Monitoring</category>
    <category>MySQL</category>
    <category>Security</category>
    <category>MySQL 9.7</category>
    <category>MySQL Report</category>
  </item>

  <item>
    <title>Skipping Percona Server for MySQL 8.4.9 and 9.7.0</title>
    <guid isPermaLink="false">https://www.percona.com/?p=50084</guid>
    <link>https://www.percona.com/blog/percona-server-mysql-8-4-9-9-7-0-skipped/</link>
    <description>Update, July 1, 2026: Percona Server for MySQL 8.4.10-10 is now available. It carries the content originally planned for 8.4.9 plus the upstream security fixes. See the 8.4.10-10 release notes. 9.7.1 is still on the way; we’ll link its release notes here when it ships.

Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. We’ve pulled those fixes into our next builds and are skipping the two versions we had already queued: Percona Server for MySQL 8.4.9 and 9.7.0.
These fixes arrived through Oracle’s new monthly Critical Security Patch Updates (CSPUs), which Oracle announced begin May 28, 2026. CSPUs ship targeted high-severity fixes between Oracle’s quarterly Critical Patch Updates. For MySQL, these updates are issued as needed rather than on a fixed monthly schedule, so out-of-schedule security fixes like these may become more common.
We’ve handled a skip like this before. When MySQL Community Server 8.4.2 followed 8.4.1 by only a few weeks, we skipped 8.4.1 and shipped its contents in 8.4.2-2. This is the same approach.
What’s happening
The code for 8.4.9 and 9.7.0 was already ready for packaging when the CVE fixes landed. Rather than ship those builds and follow immediately with a security patch, we applied the fixes, re-tested, and re-tagged. Percona Server for MySQL 8.4.10 and 9.7.1 will carry everything 8.4.9 and 9.7.0 would have contained, plus the upstream high-severity CVE fixes.
These fixes come from Oracle’s June 2026 Critical Security Patch Update; the specific CVE identifiers will be listed in the 8.4.10 and 9.7.1 release notes. No action is required on your part. The fixes reach you in 8.4.10 and 9.7.1, expected within days. If your security policy requires faster remediation, contact Percona Support to discuss interim options.
8.4.9 and 9.7.0 will not appear in the package repositories. A normal upgrade moves you straight to 8.4.10 or 9.7.1, which carry the skipped versions’ content.
Who this affects
If you were waiting specifically for 8.4.9 or 9.7.0, those versions won’t be published. Point your upgrade at the next releases instead, which include the same content and the CVE fixes. The delay is a few days, not weeks. If you weren’t tracking a specific version number, nothing changes for you.
What to do
Nothing urgent. Upgrade to the next Percona Server for MySQL releases as you normally would once they’re published. We’ll announce them through release notes and the Percona Blog. For questions about timing or the security content, reach out to Percona Support or post in the Percona Community Forum.
What to expect going forward
Oracle’s monthly CSPUs mean out-of-schedule fixes will happen more often. Our approach stays consistent: we evaluate every upstream release, and when high-severity fixes land between our scheduled releases, we fold them into the next release rather than shipping a separate build for each one. Your LTS support commitments don’t change. We’re watching how often Oracle uses the monthly cadence and will adjust release planning if the volume warrants it.

The post Skipping Percona Server for MySQL 8.4.9 and 9.7.0 appeared first on Percona.</description>
    <content:encoded><![CDATA[<p class="font-claude-response-body break-words whitespace-normal"><strong>Update, July 1, 2026:</strong> <em>Percona Server for MySQL 8.4.10-10 is now available. It carries the content originally planned for 8.4.9 plus the upstream security fixes. See the <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://docs.percona.com/percona-server/8.4/release-notes/8.4.10-10.html">8.4.10-10 release notes</a>. 9.7.1 is still on the way; we’ll link its release notes here when it ships.</em></p>
<hr>
<p>Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. We’ve pulled those fixes into our next builds and are skipping the two versions we had already queued: Percona Server for MySQL 8.4.9 and 9.7.0.</p>
<p>These fixes arrived through Oracle’s new monthly Critical Security Patch Updates (CSPUs), which <a href="https://blogs.oracle.com/security/update-monthly-critical-security-patch-updates-cspus-begin-may-28-2026" target="_blank" rel="noopener">Oracle announced begin May 28, 2026</a>. CSPUs ship targeted high-severity fixes between Oracle’s quarterly Critical Patch Updates. For MySQL, these updates are issued as needed rather than on a fixed monthly schedule, so out-of-schedule security fixes like these may become more common.</p>
<p>We’ve handled a skip like this before. When MySQL Community Server 8.4.2 followed 8.4.1 by only a few weeks, we skipped 8.4.1 and shipped its contents in 8.4.2-2. This is the same approach.</p>
<h2>What’s happening</h2>
<p>The code for 8.4.9 and 9.7.0 was already ready for packaging when the CVE fixes landed. Rather than ship those builds and follow immediately with a security patch, we applied the fixes, re-tested, and re-tagged. Percona Server for MySQL 8.4.10 and 9.7.1 will carry everything 8.4.9 and 9.7.0 would have contained, plus the upstream high-severity CVE fixes.</p>
<p>These fixes come from Oracle’s <a href="https://www.oracle.com/security-alerts/cspujun2026.html" target="_blank" rel="noopener">June 2026 Critical Security Patch Update</a>; the specific CVE identifiers will be listed in the 8.4.10 and 9.7.1 release notes. No action is required on your part. The fixes reach you in 8.4.10 and 9.7.1, expected within days. If your security policy requires faster remediation, contact Percona Support to discuss interim options.</p>
<p>8.4.9 and 9.7.0 will not appear in the package repositories. A normal upgrade moves you straight to 8.4.10 or 9.7.1, which carry the skipped versions’ content.</p>
<h2>Who this affects</h2>
<p>If you were waiting specifically for 8.4.9 or 9.7.0, those versions won’t be published. Point your upgrade at the next releases instead, which include the same content and the CVE fixes. The delay is a few days, not weeks. If you weren’t tracking a specific version number, nothing changes for you.</p>
<h2>What to do</h2>
<p>Nothing urgent. Upgrade to the next Percona Server for MySQL releases as you normally would once they’re published. We’ll announce them through release notes and the Percona Blog. For questions about timing or the security content, reach out to Percona Support or post in the Percona Community Forum.</p>
<h2>What to expect going forward</h2>
<p>Oracle’s monthly CSPUs mean out-of-schedule fixes will happen more often. Our approach stays consistent: we evaluate every upstream release, and when high-severity fixes land between our scheduled releases, we fold them into the next release rather than shipping a separate build for each one. Your LTS support commitments don’t change. We’re watching how often Oracle uses the monthly cadence and will adjust release planning if the volume warrants it.</p>
<p><!-- notionvc: df2d4121-e0ee-4824-bc7f-3c4c0773142e --></p>
<p>The post <a href="https://www.percona.com/blog/percona-server-mysql-8-4-9-9-7-0-skipped/">Skipping Percona Server for MySQL 8.4.9 and 9.7.0</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>]]></content:encoded>
    <pubDate>Mon, 29 Jun 2026 15:19:11 +0000</pubDate>
    <dc:creator>MySQL Performance Blog</dc:creator>
    <category>MySQL</category>
    <category>Security</category>
    <category>Percona Server for MySQL</category>
    <category>Release Notes</category>
    <category>security</category>
  </item>

  <item>
    <title>Continuing the Conversation: MySQL Community Engagement Across JAPAC </title>
    <guid isPermaLink="false">a95449b6f6eeb0fc0d15f1e07db6ae7e</guid>
    <link>https://blogs.oracle.com/mysql/continuing-the-conversation-mysql-community-engagement-across-japac</link>
    <description>One of the key themes of the MySQL Community over the past year has been increasing transparency, participation, and collaboration.  Through Public Discussions, Design Proposals, the MySQL Developer Guide, GitHub collaboration, and the MySQL Contributor Summit, we have been working to create more opportunities for the community to engage with the future direction of MySQL.  […]</description>
    <pubDate>Mon, 29 Jun 2026 02:57:35 +0000</pubDate>
    <dc:creator>Oracle MySQL Group</dc:creator>
    <category>MySQL</category>
    <category>MySQL Community</category>
    <category>mysql</category>
    <category>mysqlcommunity</category>
  </item>

</channel>
</rss>
