Skip to main content

Notes on the MySQL 9.7 LTS release

Tianzhou · May 12, 2026

MySQL 9.7 shipped on April 21, 2026. It's the first LTS since 8.4, and the first release after Oracle's February pledge to re-engage with the MySQL community. The release announcement covers everything that landed. Below are the ones that caught my attention after I went through the release notes.

The Hypergraph Optimizer

MySQL's classical optimizer is a left-deep, greedy join enumerator. It plans fast and produces good plans on OLTP. The catch is the plan shapes it can produce: every join extends a single left-deep chain, hash-vs-nested-loop is not a general cost-based choice, and interesting orders are not part of the search. Throw an analytic query with many joins at it and that gap shows up as orders-of-magnitude differences in execution time.

The Hypergraph Optimizer is the rewrite. It implements DPhyp from Moerkotte and Neumann's Dynamic Programming Strikes Back (SIGMOD 2008): a cost-based, hypergraph-aware enumerator that considers bushy plans, picks hash vs nested-loop per join based on cost, and propagates interesting orders. It first surfaced as a developer preview in 8.0.23 and spent the entire 9.x Innovation cycle behind Enterprise. 9.7 is where it crosses into Community.

-- Session-level
SET optimizer_switch = 'hypergraph_optimizer=on';

-- Statement-level
SELECT /*+ SET_VAR(optimizer_switch = 'hypergraph_optimizer=on') */
  o.id, c.name, SUM(li.price)
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN line_items li ON li.order_id = o.id
WHERE o.created_at >= '2026-01-01'
GROUP BY o.id, c.name;

The verdict: this is a real planner for deep joins, star schemas, and analytical filters, and you get it without paying for Enterprise or moving to HeatWave. That's the headline. Per-statement opt-in via SET_VAR is the pragmatic pattern. Flipping it on globally is the wrong move. The classical optimizer still wins on the OLTP fast path, and planning cost on simple queries is higher, so you'd be taxing every short query to help the long ones.

It is still experimental and off by default, and the numbers say why. On Oracle's published TPC-DS run, nearly half the queries improved by at least 25%, 19 improved by at least 50%, but 14 regressed by 50% or more. The wins outnumber the losses, but the losses are real and you will hit some of them. Refresh ANALYZE TABLE and your column histograms before flipping the switch, because the hypergraph planner is far more sensitive to statistics quality than the classical one. And keep SET_VAR ready in both directions, in case a specific query turns out to be one of the losers.

JSON Duality Views

JSON Duality Views let an application read and write a JSON document while the server stores the data as normalized relational rows. The 9.x Innovation track shipped them read-only in Community, read-write in Enterprise. 9.7 closes the gap: INSERT, UPDATE, and DELETE against a duality view are now supported in Community Server, with auto-increment propagation.

CREATE JSON DUALITY VIEW customer_orders AS
SELECT JSON_DUALITY_OBJECT{
  '_id': c.id,
  'name': c.name,
  'orders': [ SELECT JSON_DUALITY_OBJECT{
                '_id': o.id,
                'total': o.total_cents,
                'placed_at': o.created_at
              } FROM orders o WHERE o.customer_id = c.id ]
} FROM customers c;

-- Document-style write. Server decomposes it into INSERT/UPDATE on base tables
INSERT INTO customer_orders VALUES (JSON_OBJECT(
  'name', 'Alice',
  'orders', JSON_ARRAY(JSON_OBJECT('total', 4200, 'placed_at', NOW()))
));

What you get is document and relational access from the same schema. A service that prefers documents reads and writes the view; an analytics pipeline reads the base tables directly. No ORM sitting in the middle pretending the two worlds are the same. And the part I like best: a constraint violation on the base tables rolls back the document write. That's the point. You want those errors loud, not papered over by a JSON layer that swallows them.

Group Replication

Group Replication is already MySQL's clearest lead over Postgres: a multi-primary-capable, in-server HA topology with automatic failover, where Postgres still hands failover orchestration off to external tools. The catch was that running it well required Enterprise. The components that told you why the group made a given decision lived only there. So you had a built-in HA story, but when it misbehaved at 3am you were flying blind unless you'd bought the contract. 9.7 closes that gap, which is what turns a paywalled lead into a real one.

  • Replication Applier Metrics: per-worker lag and throughput in multi-threaded replication.
  • Group Replication Flow Control Statistics: how often, how long, and against which member flow control engaged.
  • Group Replication Resource Manager: automatic eviction and rejoin of unhealthy members.
  • Group Replication Primary Election: favors the most up-to-date eligible node during failover, not the lowest-UUID default.

Net effect: your HA topology no longer has to track your support contract. Turn on applier metrics first, because they're how you tell a single hot worker apart from broader replication pressure. Resource manager and primary election actually change failover behavior, so roll those out against a staging cluster before production. New failover logic is not something you want to meet for the first time during an incident.

OpenTelemetry Support

The Telemetry Component lands in Community. Previously Enterprise-only, it exports MySQL logs, metrics, and traces over OTLP. Configuration is component-based, installed and tuned like any other server component rather than a separate plugin.

The win here is simple: MySQL is now on the same OTLP pipeline as everything else, so MySQL spans land in the same dashboard as the application traces they're attached to. No more MySQL-shaped hole in your centralized monitoring that needs a custom exporter to fill. One thing to plan for on busy clusters: the collector and the network path between it and the server have to handle the extra OTLP traffic, and that's not free.

Dynamic Data Masking

Until 9.7, masking a sensitive column in MySQL meant one of three things: wrap the base table in a view, call an Enterprise masking function in every query that touched the column, or put a third-party gateway in front of the database. Anyone who has tried to hold one of these together knows the dance. None of them survived a SELECT *, and none of them survived a developer pointing their own client at the read replica.

MySQL Enterprise 9.7 makes Dynamic Data Masking (DDM) a first-class object in the server. A masking policy decides, per row at query time, whether to return the original value or a transformed one, based on the executing user or active role. You attach the policy to a column on the base table, and every read path goes through it: SELECT *, mysqldump, ad-hoc client sessions.

-- 1. Define a policy. The body is a CASE expression that returns either
--    the column value or a masked transformation of it.
CREATE MASKING POLICY mask_ssn AS (val VARCHAR(11))
RETURNS VARCHAR(11) ->
  CASE
    WHEN CURRENT_ROLE() IN ('compliance_auditor', 'pii_reader')
      THEN val
    ELSE mask_inner(val, 0, 4, 'X')
  END;

-- 2. Attach the policy to a column on the base table.
ALTER TABLE customers
  ALTER COLUMN ssn SET MASKING POLICY mask_ssn;

-- 3. Reads are filtered by the policy, transparently.
SELECT id, name, ssn FROM customers WHERE id = 42;
-- → 42 | Alice | XXXXX6789       (analyst role)
-- → 42 | Alice | 123-45-6789     (compliance_auditor role)

The policy body draws on Enterprise's pre-built masking library, which has shipped since 8.0: mask_inner, mask_outer, mask_pan, mask_ssn, gen_rnd_email, gen_rnd_us_phone. The functions aren't new. What's new in 9.7 is the policy object. Before, you could call these functions, but binding one to a column meant application code or a view. Now the server does the binding for you. The SQL surface looks a lot like Snowflake's column-level DDM, same CREATE MASKING POLICY ... RETURNS ... -> shape, same ALTER COLUMN ... SET MASKING POLICY attachment, and not at all like Oracle's procedural DBMS_REDACT.ADD_POLICY API, which is the older sibling in the same family of products.

The shift is from "every read path has to remember to call the function" to "the column is masked." A masked column is masked everywhere it appears, including inside WHERE and JOIN predicates, so the obvious side-channel (WHERE ssn LIKE '123-%') is closed. One thing DDM doesn't do: it controls which roles see which columns in cleartext, but it doesn't control which rows a role can read at all. If both axes matter, pair it with row-level filtering. DDM hides the value, not the row.

Re-earning the Trust

MySQL's community story drifted last year. Three-month stretches with no public commits. Layoffs inside the engineering team. On Hacker News, in conference hallways, in architecture reviews, the default answer became "use Postgres." Picking MySQL needed a defense.

That's changing. Oracle's February New Era of MySQL Community Engagement post made the promise; 9.7 keeps it. The optimizer rewrite ships in Community. Group Replication observability ships in Community. Dynamic Data Masking stays Enterprise, which is the right call: the people buying masking are the same people buying the audit posture and the support contract that come with it.

And to be fair to MySQL, on operational merits it stands up against Postgres better than the consensus suggests:

  • Storage engine. InnoDB is clustered-index and undo-log-based: old row versions live in the undo log, not the heap. Postgres's heap-plus-vacuum design is the most-cited architectural pain point operators raise about Postgres. InnoDB tends to outperform heap+vacuum on OLTP workloads.
  • Replication and HA. Group Replication ships in the server with automatic primary election and node eviction; MySQL Router and MySQL Shell finish the stack. Postgres has streaming replication in core but leaves failover orchestration to external tools.
  • Learning curve. A new engineer is productive with MySQL in a day. Postgres asks more up front (roles, schemas, search_path, vacuum, autovacuum, TOAST, wraparound) and the first day is steeper.

None of this means MySQL won the argument. It means the argument is worth having again, which a year ago it wasn't. For an LTS that has to carry teams for years, re-earning that default is the most important thing 9.7 ships.

References

Back to blog

Explore the standard for database governance