<?xml version="1.0" encoding="UTF-8"?>
<feed
  xmlns="http://www.w3.org/2005/Atom"
  xmlns:thr="http://purl.org/syndication/thread/1.0"
  xml:lang="en-US"
  xml:base="https://mariadb.org/wp-atom.php">

  <title>MariaDB.org-Planet-Feed</title>
  <link type="application/atom+xml" href="https://mariadb.org/planet-atom" rel="self" />
  <link rel="alternate" href="https://mariadb.org/planet/" />
  <updated>2026-09-15T04:53:00+03:00</updated>
  <id>https://mariadb.org/planet-atom</id>

        <entry>
      <title>Data Quality SLOs: 5 Proven Indicators, Error Budgets and Burn-Rate Alerts on dbt and Airflow</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/data-quality-slos/" />
      <id>https://minervadb.com/data-quality-slos/</id>
      <updated>2026-09-15T04:53:00+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>Every data platform we take over has data quality checks. Hundreds of them, usually: not_null tests on every column, row-count assertions, a freshness dashboard nobody looks at, and a Slack channel where the failures scroll [...]</p>
<p><a href="https://minervadb.com/data-quality-slos/">Data Quality SLOs: 5 Proven Indicators, Error Budgets and Burn-Rate Alerts on dbt and Airflow</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Every data platform we take over has data quality checks. Hundreds of them, usually: <code>not_null</code> tests on every column, row-count assertions, a freshness dashboard nobody looks at, and a Slack channel where the failures scroll past unread because there are forty a day and none of them says whether anyone should care. The checks exist; the discipline that turns them into a promise does not.</p>
<p>Data quality SLOs are that discipline, borrowed directly from the way SRE teams run services: a small number of measured indicators per dataset, an explicit target for each, an error budget that says how much failure is tolerable before work stops, and alerts that fire on the rate at which the budget is burning rather than on every individual miss.</p>
<p>This post sets out the data quality SLOs we implement for customers running dbt and Airflow on PostgreSQL, ClickHouse, Snowflake, BigQuery or Databricks. It takes one dataset, an order-line fact table, and follows it through the five indicators we measure, the SQL that produces each, the SLO table that stores the targets and their owners, the burn-rate alerts that replace per-test noise, and the incident and review loop that keeps the targets honest. All the code runs on PostgreSQL 16 or later and dbt Core 1.8 or later; the ClickHouse variants are noted where they differ. Targets and figures are illustrative unless a measurement source is named.</p>
<p><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/data-quality-slos-one-dataset-five-indicators.png" alt="Data quality SLOs for one dataset: consumers and consequences, five indicators with targets, observation log, error budget and burn-rate alerting" width="1100" height="420" class="aligncenter size-full wp-image-93257"></p>
<h2>An SLO is a promise to a consumer, so start with the consumer<a class="anchor-link" id="an-slo-is-a-promise-to-a-consumer-so-start-with-the-consumer"></a></h2>
<p>The first mistake in data quality SLOs is writing them for the pipeline rather than for the people who depend on it. A freshness target of &ldquo;loaded by 06:00&rdquo; means nothing until someone says what breaks at 06:01. So each dataset gets a named consumer and a stated consequence: finance closes on <code>fct_order_line</code> at 07:00 on the third working day; the pricing service reads it every fifteen minutes; the board pack is built from it on the first Monday.</p>
<p>Those three consumers want different things, and the SLO records the strictest requirement each dimension actually has, with the consumer and the consequence written beside it. That is what makes the target defensible when the on-call engineer is woken at 03:00.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Data quality SLOs live in a table, per dataset and indicator, with owner and consequence
CREATE TABLE dq.slo (
    dataset            TEXT        NOT NULL,          -- 'marts.fct_order_line'
    indicator          TEXT        NOT NULL
                       CONSTRAINT dq_slo_indicator_chk
                       CHECK (indicator IN ('freshness','completeness','validity','consistency','lineage')),
    target             NUMERIC(6,4) NOT NULL,         -- 0.9950 = 99.5 % of evaluation windows meet the SLI
    window_days        SMALLINT    NOT NULL DEFAULT 28,
    sli_threshold      TEXT        NOT NULL,          -- '45 minutes', '0.999', '0.0010'
    consumer           TEXT        NOT NULL,          -- 'finance close', 'pricing service'
    consequence        TEXT        NOT NULL,          -- what breaks when the SLI misses
    owner_email        TEXT        NOT NULL,
    set_at             TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT dq_slo_pk PRIMARY KEY (dataset, indicator)
);

INSERT INTO dq.slo VALUES
  ('marts.fct_order_line','freshness',   0.9950, 28, '45 minutes', 'pricing service',
   'pricing falls back to yesterday''s margins; measurable revenue impact per hour', 'data-platform-lead@example.com', now()),
  ('marts.fct_order_line','completeness',0.9990, 28, '0.999',      'finance close',
   'close reconciliation fails; controllers work manually', 'fpa-lead@example.com', now()),
  ('marts.fct_order_line','validity',    0.9990, 28, '0.0010',     'all',
   'downstream models produce nulls in margin', 'data-platform-lead@example.com', now()),
  ('marts.fct_order_line','consistency', 0.9900, 28, '0.0010',     'finance close',
   'ledger variance above tolerance blocks sign-off', 'fpa-lead@example.com', now()),
  ('marts.fct_order_line','lineage',     0.9990, 28, 'all upstream succeeded', 'all',
   'silently stale or partial upstream; every consumer affected', 'data-platform-lead@example.com', now());</pre>
<p>Five rows of data quality SLOs for the most important table in the warehouse, and that is deliberate. Data quality SLOs are meant to be few: the twenty or thirty datasets that consumers actually depend on, five indicators each, and nothing else promised. The other four hundred models keep their dbt tests, but a failing test on an intermediate model is a build failure for the engineer, not a page, and not a breach of anything.</p>
<h2>Five data quality SLOs indicators, and the query behind each<a class="anchor-link" id="five-data-quality-slos-indicators-and-the-query-behind-each"></a></h2>
<p>An SLI behind data quality SLOs is a measurement, taken on a schedule, that yields a pass or fail against a threshold. Data quality SLOs collapse the hundreds of possible checks on a dataset into five that between them cover what a consumer can be harmed by: the data is late, incomplete, malformed, inconsistent with another source of truth, or built on something that did not succeed. Each is computed by the platform from its own tables and written to one evaluation log, so the SLO attainment and the error budget are queries over that log rather than a dashboard someone assembles.</p>
<p><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/data-quality-slos-five-indicators.png" alt="Data quality SLOs five indicators: freshness, completeness, validity, consistency and lineage integrity with what each catches" width="1100" height="400" class="aligncenter size-full wp-image-93258"></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- One evaluation log for every SLI observation
CREATE TABLE dq.sli_observation (
    dataset            TEXT        NOT NULL,
    indicator          TEXT        NOT NULL,
    observed_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
    value              NUMERIC,                       -- minutes of lag, ratio, count
    passed             BOOLEAN     NOT NULL,
    detail             JSONB,                         -- what was compared, for the incident
    CONSTRAINT dq_sli_observation_pk PRIMARY KEY (dataset, indicator, observed_at)
);

-- 1. Freshness: age of the newest business event versus its own expected cadence
INSERT INTO dq.sli_observation (dataset, indicator, value, passed, detail)
SELECT 'marts.fct_order_line', 'freshness',
       EXTRACT(EPOCH FROM (now() - MAX(order_ts))) / 60                       AS lag_minutes,
       EXTRACT(EPOCH FROM (now() - MAX(order_ts))) / 60 = 0.999                                  AS passed,
       jsonb_build_object('mart_rows', m.n, 'source_rows', s.n, 'window', 'yesterday')
FROM (SELECT count(*) AS n FROM marts.fct_order_line
      WHERE order_ts &gt;= CURRENT_DATE - 1 AND order_ts = CURRENT_DATE - 1 AND event_ts &lt; CURRENT_DATE) AS s;

-- 3. Validity: share of rows violating the dataset's own invariants
INSERT INTO dq.sli_observation (dataset, indicator, value, passed, detail)
SELECT 'marts.fct_order_line', 'validity',
       count(*) FILTER (WHERE net_amount IS NULL OR landed_cost &lt; 0 OR (customer_id IS NULL AND channel  'guest'))::NUMERIC
         / NULLIF(count(*), 0)                                                 AS invalid_ratio,
       count(*) FILTER (WHERE net_amount IS NULL OR landed_cost &lt; 0 OR (customer_id IS NULL AND channel  'guest'))::NUMERIC
         / NULLIF(count(*), 0) = CURRENT_DATE - 1;</pre>
<p>In data quality SLOs, freshness is measured against the business timestamp, not the load timestamp; a pipeline that runs on time and loads nothing new is late, and only the business timestamp shows it. Completeness compares against the source&rsquo;s own count for the same window, which is the only comparison that catches a CDC connector silently dropping a partition. Validity checks the dataset&rsquo;s invariants as a ratio rather than a boolean, so a single bad row in ten million does not fail the SLI while a thousand do.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- 4. Consistency: reconciliation to an external truth, here the finance ledger for the last closed month
INSERT INTO dq.sli_observation (dataset, indicator, value, passed, detail)
SELECT 'marts.fct_order_line', 'consistency',
       ABS(m.net - g.net) / NULLIF(g.net, 0)                                   AS variance_ratio,
       ABS(m.net - g.net) / NULLIF(g.net, 0) = date_trunc('month', CURRENT_DATE - INTERVAL '1 month')
        AND order_ts &lt;  date_trunc('month', CURRENT_DATE)) AS m,
     (SELECT SUM(amount) AS net FROM finance.gl_revenue_by_period
      WHERE period = date_trunc('month', CURRENT_DATE - INTERVAL '1 month')
        AND account_class = 'NET_REVENUE') AS g;

-- 5. Lineage integrity: every upstream model in this dataset's DAG succeeded in the run that produced it
-- (dq.run_result is loaded from dbt's run_results.json after every run)
INSERT INTO dq.sli_observation (dataset, indicator, value, passed, detail)
SELECT 'marts.fct_order_line', 'lineage',
       count(*) FILTER (WHERE status  'success')                            AS failed_upstream,
       count(*) FILTER (WHERE status  'success') = 0                        AS passed,
       jsonb_build_object('run_id', MAX(run_id),
                          'failed', jsonb_agg(node_name) FILTER (WHERE status  'success'))
FROM dq.run_result
WHERE run_id = (SELECT MAX(run_id) FROM dq.run_result WHERE node_name = 'marts.fct_order_line')
  AND node_name IN (SELECT upstream FROM dq.lineage WHERE downstream = 'marts.fct_order_line');</pre>
<p>Lineage integrity is the data quality SLOs indicator most teams do not have and most incidents trace back to. A mart can build successfully from a staging model that failed silently and was skipped, or from a source that loaded yesterday&rsquo;s file twice. Loading dbt&rsquo;s <code>run_results.json</code> and <code>manifest.json</code> into two small tables after every run makes &ldquo;did everything this table depends on actually succeed this time&rdquo; a query, and it is the query that runs first when a consumer reports a wrong number.</p>
<h2>Data quality SLOs attainment and error budget are queries, not opinions<a class="anchor-link" id="data-quality-slos-attainment-and-error-budget-are-queries-not-opinions"></a></h2>
<p>With targets in one table and observations in another, data quality SLOs become arithmetic. Attainment over the window is the share of observations that passed. The error budget is the shortfall the target permits, and the budget remaining is how much of it has been spent. A freshness SLO of 99.5 percent over 28 days with an observation every fifteen minutes allows about thirteen failed observations, roughly three and a quarter hours of lateness, in the window. Once that is spent, the dataset is in breach and the policy in the next section applies.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Attainment and error budget remaining, per dataset and indicator, over each SLO's own window
SELECT
    s.dataset,
    s.indicator,
    s.target,
    count(o.*)                                                    AS observations,
    count(o.*) FILTER (WHERE o.passed)                            AS passed,
    round(count(o.*) FILTER (WHERE o.passed)::NUMERIC / NULLIF(count(o.*), 0), 5) AS attainment,
    round((1 - s.target) * count(o.*))                            AS budget_total_obs,
    round((1 - s.target) * count(o.*)) - count(o.*) FILTER (WHERE NOT o.passed) AS budget_remaining_obs,
    s.owner_email
FROM dq.slo AS s
LEFT JOIN dq.sli_observation AS o
       ON o.dataset = s.dataset AND o.indicator = s.indicator
      AND o.observed_at &gt;= now() - make_interval(days =&gt; s.window_days)
GROUP BY s.dataset, s.indicator, s.target, s.owner_email
ORDER BY budget_remaining_obs ASC;</pre>
<p>That view is the weekly data quality SLOs review. A dataset with budget to spare can take a risky migration this sprint; one with none cannot, and the change freeze is a number rather than an argument. On ClickHouse the same two tables and the same query work unchanged apart from <code>make_interval</code>, which becomes <code>INTERVAL s.window_days DAY</code>.</p>
<h2>Alert on burn rate, not on every miss<a class="anchor-link" id="alert-on-burn-rate-not-on-every-miss"></a></h2>
<p>The reason forty data quality Slack messages a day go unread is that each one is a single failed check with no sense of proportion. Data quality SLOs replace that with the SRE multi-window burn-rate alert: page when the budget is being consumed fast enough that it will be exhausted long before the window ends, and warn when it is being consumed steadily faster than the target allows. Two windows per alert, a long one for significance and a short one to confirm the problem is still happening, keep a transient blip from paging anyone and a slow bleed from being ignored.</p>
<p><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/data-quality-slos-burn-rate-alerting.png" alt="Data quality SLOs burn-rate alerting: multi-window page and warn thresholds versus per-check alerting" width="1100" height="400" class="aligncenter size-full wp-image-93259"></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Multi-window burn-rate alert for the freshness SLO (illustrative: page at 14.4x over 1h and 5m; warn at 6x over 6h and 30m)
WITH s AS (
    SELECT dataset, indicator, target, window_days FROM dq.slo
    WHERE dataset = 'marts.fct_order_line' AND indicator = 'freshness'
),
rates AS (
    SELECT
        (SELECT (1 - target) FROM s)                                                   AS allowed_fail_rate,
        AVG((NOT passed)::int) FILTER (WHERE observed_at &gt;= now() - INTERVAL '1 hour')   AS fail_1h,
        AVG((NOT passed)::int) FILTER (WHERE observed_at &gt;= now() - INTERVAL '5 minutes') AS fail_5m,
        AVG((NOT passed)::int) FILTER (WHERE observed_at &gt;= now() - INTERVAL '6 hours')  AS fail_6h,
        AVG((NOT passed)::int) FILTER (WHERE observed_at &gt;= now() - INTERVAL '30 minutes') AS fail_30m
    FROM dq.sli_observation
    WHERE dataset = 'marts.fct_order_line' AND indicator = 'freshness'
      AND observed_at &gt;= now() - INTERVAL '6 hours'
)
SELECT
    CASE
        WHEN fail_1h / allowed_fail_rate &gt;= 14.4 AND fail_5m  / allowed_fail_rate &gt;= 14.4 THEN 'page'
        WHEN fail_6h / allowed_fail_rate &gt;= 6.0  AND fail_30m / allowed_fail_rate &gt;= 6.0  THEN 'warn'
        ELSE 'ok'
    END AS level,
    round(fail_1h / allowed_fail_rate, 1) AS burn_1h,
    round(fail_6h / allowed_fail_rate, 1) AS burn_6h
FROM rates;</pre>
<p>For data quality SLOs, a burn rate of 14.4 over an hour means the dataset is failing at a pace that would exhaust a 28-day budget in about two days; that is worth waking someone for. A burn rate of six over six hours exhausts it in under five days and is worth a ticket in the morning. The constants are the ones <a href="https://sre.google/workbook/alerting-on-slos/" target="_blank" rel="noopener">Google&rsquo;s SRE workbook</a> popularised and they are a starting point, not a law; the right ones for a dataset come from looking at its own history of misses and asking which of them the consumer would have wanted to know about at 03:00.</p>
<h2>Wiring data quality SLOs into dbt and Airflow<a class="anchor-link" id="wiring-data-quality-slos-into-dbt-and-airflow"></a></h2>
<p>Data quality SLOs need no new platform. The five data quality SLOs queries are dbt models or singular tests that write to <code>dq.sli_observation</code>; the attainment view is a dbt model; the burn-rate query is an Airflow task that runs every five minutes and routes on its result. The one addition that matters is loading dbt&rsquo;s run artefacts into the warehouse after every run, because that is what makes the lineage indicator and the incident investigation possible.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python"># Airflow 2.9+: burn-rate check every five minutes, routed by level; run artefacts loaded after every dbt run
from datetime import timedelta
from airflow.decorators import dag, task
from airflow.providers.postgres.hooks.postgres import PostgresHook

@dag(schedule=timedelta(minutes=5), catchup=False, max_active_runs=1, tags=["dq", "slo"])
def dq_burn_rate():

    @task
    def evaluate() -&gt; list[dict]:
        hook = PostgresHook(postgres_conn_id="warehouse")
        rows = hook.get_records(open("/opt/airflow/sql/dq_burn_rate_all.sql").read())
        return [dict(dataset=r[0], indicator=r[1], level=r[2], burn_1h=r[3], owner=r[4]) for r in rows]

    @task
    def route(results: list[dict]) -&gt; None:
        for r in results:
            if r["level"] == "page":
                page_oncall(service="data-platform", summary=f"{r['dataset']} {r['indicator']} burn {r['burn_1h']}x", owner=r["owner"])
            elif r["level"] == "warn":
                open_ticket(queue="data-quality", summary=f"{r['dataset']} {r['indicator']} burning {r['burn_1h']}x", owner=r["owner"])

    route(evaluate())

dq_burn_rate()</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># After every dbt run: load run_results.json and manifest.json so lineage integrity is a query
dbt run --select +marts.fct_order_line
python load_dbt_artifacts.py --target-path target/ --schema dq   # writes dq.run_result and dq.lineage</pre>
<p>The pager and ticket functions are whatever the estate already uses for data quality SLOs; the point is that the page carries the dataset, the indicator, the burn rate and the owner from the SLO table, so the person woken knows in the first line whether the pricing service is about to fall back to stale margins or a controller will have a bad morning.</p>
<h2>The incident and the review are part of the SLO<a class="anchor-link" id="the-incident-and-the-review-are-part-of-the-slo"></a></h2>
<p>A page on data quality SLOs is an incident and is run as one: acknowledged, mitigated, resolved, reviewed. The review has one question the pipeline-check world never asks, which is whether the SLO was right. If the freshness page fired and nobody downstream noticed the lateness, the target is stricter than the consumer needs and it should loosen.</p>
<p>If a consumer reported a wrong number and no indicator fired, an indicator is missing or a threshold is too loose, and the review adds or tightens it. Quarterly, the owner of every one of the data quality SLOs confirms the consumer and consequence still hold, and datasets nobody depends on any longer lose their SLOs rather than accumulating.</p>
<p><img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/data-quality-slos-review-loop.png" alt="Data quality SLOs operating loop: observe, evaluate, respond, review, recertify, with the error budget policy" width="1100" height="380" class="aligncenter size-full wp-image-93260"></p>
<div>
<table>
<thead>
<tr>
<th>Review finding</th>
<th>What it says about the SLO</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Page fired, no consumer impact</td>
<td>Target stricter than the consequence justifies</td>
<td>Loosen target or threshold; record the consumer&rsquo;s real tolerance</td>
</tr>
<tr>
<td>Consumer reported a defect, nothing fired</td>
<td>Missing indicator or threshold too loose</td>
<td>Add the indicator that would have caught it; backfill observations to confirm</td>
</tr>
<tr>
<td>Budget exhausted three months running</td>
<td>Pipeline cannot meet the promise as built</td>
<td>Reliability work takes priority over features until budget is positive</td>
</tr>
<tr>
<td>Budget never touched</td>
<td>Either excellent, or the SLI is not measuring what breaks</td>
<td>Check the SLI against a known past incident; tighten if it would have missed it</td>
</tr>
<tr>
<td>Owner cannot name the consumer</td>
<td>SLO has outlived its purpose</td>
<td>Retire it; keep the dbt tests</td>
</tr>
</tbody>
</table>
</div>
<h2>Where data quality SLOs go wrong<a class="anchor-link" id="where-data-quality-slos-go-wrong"></a></h2>
<p>Three data quality SLOs failure modes recur. Over-instrumentation: a team promises SLOs on every model, the budget arithmetic becomes noise, and the pager fires for datasets nobody reads. Twenty to thirty datasets with data quality SLOs is the range we see work. Measuring the pipeline instead of the data: a freshness SLI on &ldquo;last job run time&rdquo; passes while the job loads nothing, which is why every SLI here reads the business data or an external truth. And SLOs without consequences: a target with no named consumer and no stated impact will be argued down the first time it pages, so the consequence column is not optional.</p>
<h2>Working with MinervaDB on data quality SLOs<a class="anchor-link" id="working-with-minervadb-on-data-quality-slos"></a></h2>
<p>Data quality SLOs are the operating discipline inside our <a href="https://minervadb.com/data-governance-consulting/">data governance consulting</a> practice and the Data SRE layer of our <a href="https://minervadb.com/data-engineering/">data engineering</a> work: a typical engagement selects the datasets that matter with their consumers, writes the five indicators and targets for each, wires the observation log and burn-rate alerts into the existing dbt and Airflow estate, and runs the first quarterly review with the owners. The same data quality SLOs evaluation-log pattern underpins the reconciliation tests in our <a href="https://minervadb.com/metrics-layer-dbt/">metrics layer</a> post and the freshness SLIs in our <a href="https://minervadb.com/feature-store-architecture/">feature store architecture</a>.</p>
<p>Under managed operations the burn-rate pager routes to our 24&times;7 teams under the standard S1 to S4 commitments, with an exhausted error budget on a finance-close dataset treated as an S2 and a lineage-integrity failure on a production model as an S1. As always: test every query, threshold and constant here against your own datasets and history before applying them to production, and keep the observation log itself under a restore posture; it is the evidence behind every promise.</p>

<p><a href="https://minervadb.com/data-quality-slos/">Data Quality SLOs: 5 Proven Indicators, Error Budgets and Burn-Rate Alerts on dbt and Airflow</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona and HexaCluster: Faster, Safer Oracle Migration</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-and-hexacluster-faster-safer-oracle-migration/" />
      <id>https://www.percona.com/blog/percona-and-hexacluster-faster-safer-oracle-migration/</id>
      <updated>2026-09-14T22:19:16+03:00</updated>
      <author><name>Percona Team</name></author>
      <summary type="html"><![CDATA[<p>Percona and HexaCluster have partnered to remove the hardest part of an open source database migration: getting off Oracle, SQL Server, DB2 or Sybase ASE with confidence, on a predictable timeline, without a multi-year consulting program. Percona brings open source expertise, its own distributions, operators and enterprise support. HexaCluster brings the assessment and migration engineering … Continued<br />
The post Percona and HexaCluster: Faster, Safer Oracle Migration appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-and-hexacluster-faster-safer-oracle-migration/">Percona and HexaCluster: Faster, Safer Oracle Migration</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://www.percona.com/"><span>Percona</span></a><span> and HexaCluster have partnered to remove the hardest part of an open source database migration: getting off Oracle, SQL Server, DB2 or Sybase ASE with confidence, on a predictable timeline, without a multi-year consulting program. Percona brings open source expertise, its own distributions, operators and enterprise support. HexaCluster brings the assessment and migration engineering that gets workloads onto those supported databases.</span></p>
<p><span>In this Q&amp;A, Avi Vallarapu, CEO of </span><a href="https://hexacluster.ai/"><span>HexaCluster</span></a><span>, explains what the partnership changes for organizations planning an Oracle migration, why a database migration assessment is the single most valuable step, and how a joint engagement runs from first assessment through long-term support.</span></p>
<p>&nbsp;</p>
<h2><b>Quick answer: what is the Percona and HexaCluster partnership?</b><a class="anchor-link" id="quick-answer-what-is-the-percona-and-hexacluster-partnership"></a></h2>
<p><span>Percona and HexaCluster is a joint database migration partnership that moves organizations off Oracle, SQL Server, DB2 and Sybase ASE onto Percona-supported open source databases, primarily PostgreSQL. HexaCluster runs the assessment and migration engineering, using its own DMAT and HexaRocket tooling. Percona runs the resulting database long term through its distributions, operators and support.</span></p>
<ul>
<li><b>Problem it solves:</b><span> disconnected migration tools and skipped assessments, which is where most Oracle migrations fail</span></li>
<li><b>What HexaCluster does:</b><span> database migration assessment, automatic schema conversion, data migration, and live replication, with validation &amp; rollback</span></li>
<li><b>What Percona does:</b><span> the open source database destination, plus operators, distributions and expert support</span></li>
<li><b>Typical use case:</b><span> Oracle, SQL Server, DB2 or Sybase ASE exits, data center exits, and hybrid cloud migrations</span></li>
</ul>
<p><span>Short version: HexaCluster gets you to open source. Percona keeps you running on it. Together, that&rsquo;s one path from a proprietary database to a fully supported open source one.</span></p>
<p>&nbsp;</p>
<h2><b>Q: What does HexaCluster do differently?</b><a class="anchor-link" id="q-what-does-hexacluster-do-differently"></a></h2>
<p><b>Avi:</b><span> HexaCluster comes from the same place Percona does. We&rsquo;re open source contributors first. Our team has contributed to more than 50 popular PostgreSQL extensions, and over 90 percent of what we build is open source.</span></p>
<p><span>The best known example is Ora2Pg, the most widely used open source Oracle-to-PostgreSQL migration tool, created and maintained by our team. Google Cloud, Microsoft Azure and AWS have all pointed customers to it for complex schema conversions. We&rsquo;ve also built extensions that emulate Oracle and SQL Server behavior inside PostgreSQL, making application code portable instead of rewritten.</span></p>
<p><span>That work taught us where migrations actually break, which is where HexaRocket came from. Almost every existing migration tool is partial: some handle schema conversion, some handle data migration and change data capture, very few do both, and almost none give a credible rollback. Customers end up stitching disconnected tools together, and the seams between them are exactly where migrations fail.</span></p>
<p><span>HexaRocket runs the whole path in one platform: schema migration with validation, data migration with validation, change data capture for live replication, and reverse replication so you can roll back if something goes wrong. Rollback is what lets a CIO approve a cutover date.</span></p>
<p>&nbsp;</p>
<h2><b>Q: Why does the partnership with Percona matter?</b><a class="anchor-link" id="q-why-does-the-partnership-with-percona-matter"></a></h2>
<p><b>Avi:</b><span> Because neither company covers the whole journey alone. Percona is the master of open source on the services and product side: Percona Distribution for PostgreSQL, Percona MySQL, Percona Software for MongoDB, the Percona Operators, and enterprise support. That&rsquo;s hard to replicate.</span></p>
<p><span>What Percona hasn&rsquo;t done is move workloads off Oracle, SQL Server, DB2 or Sybase in the first place. That&rsquo;s the gap HexaCluster bridges.</span></p>
<p><span>Put simply: HexaCluster gets you to open source, Percona keeps you running on it. That&rsquo;s the whole journey covered by two open source companies, not a general-purpose systems integrator learning your database on your budget. It works in reverse for us too: when customers ask what they&rsquo;re landing on, pointing at Percona&rsquo;s distribution, with built-in transparent data encryption, makes that conversation simpler.</span></p>
<p>&nbsp;</p>
<h2><b>Q: How did this relationship start?</b><a class="anchor-link" id="q-how-did-this-relationship-start"></a></h2>
<p><b>Avi:</b><span> It&rsquo;s not new. I joined Percona in 2018 to start and lead the PostgreSQL practice there. The relationship continued through everything I&rsquo;ve built since, first through MigOps and now through HexaCluster. This partnership formalizes a working relationship that&rsquo;s existed for years.</span></p>
<p>&nbsp;</p>
<h2><b>Q: What&rsquo;s actually driving companies to do this right now?</b><a class="anchor-link" id="q-whats-actually-driving-companies-to-do-this-right-now"></a></h2>
<p><b>Avi:</b><span> Three things, stacking on top of each other.</span></p>
<p><b>Cost optimization.</b><span> Commercial database licensing is one of the largest and least defensible line items in most infrastructure budgets. When finance looks for structural savings, this is where they land.</span></p>
<p><b>Scale in the AI era.</b><span> Organizations are scaling data volumes faster than planned. Per-core commercial licensing turns growth into a penalty; open source turns it into a hardware conversation instead of a procurement one.</span></p>
<p><b>Features and performance.</b><span> PostgreSQL isn&rsquo;t the pragmatic compromise anymore. It&rsquo;s the target: extensions, JSON, logical replication, partitioning, and now transparent data encryption through Percona&rsquo;s work on the Distribution. The question has shifted from whether to adopt PostgreSQL to how fast the rest of the estate can follow.</span></p>
<p>&nbsp;</p>
<h2><b>Q: Where do companies most often go wrong in migrations?</b><a class="anchor-link" id="q-where-do-companies-most-often-go-wrong-in-migrations"></a></h2>
<p><b>Avi:</b><span> They skip the assessment.</span></p>
<p><span>Migrations fail long before cutover, at the point someone estimates the effort without knowing what&rsquo;s actually inside the databases. Some carry genuine complexity: heavy PL/SQL, embedded business logic, dependencies nobody has read in a decade. Others are almost trivial. Without an assessment, you can&rsquo;t tell those apart, so both estimates are wrong.</span></p>
<p><span>Two mistakes compound that one: choosing a partner who treats your migration as a learning exercise, and choosing disconnected tools that leave the integration risk with you. Assessment first. Everything else gets easier after that.</span></p>
<p>&nbsp;</p>
<h2><b>Q: What does a Percona and HexaCluster engagement look like for a customer?</b><a class="anchor-link" id="q-what-does-a-percona-and-hexacluster-engagement-look-like-for-a-customer"></a></h2>
<p><b>Avi:</b><span> It&rsquo;s one team across the full lifecycle. The engagement starts the moment someone first thinks about migrating: assessment, then roadmap and sequencing, then schema and data migration with validation at every stage, then cutover with rollback available, then production, then support for as long as the database lives.</span></p>
<p><span>The customer isn&rsquo;t managing a handoff between an assessment vendor, a tooling vendor, a migration integrator and a support provider. That&rsquo;s where cost, delay and blame usually live. The result is cost-effective and easier, because the people who build the tools are the same people who support the databases afterward.</span></p>
<p>&nbsp;</p>
<h2><b>Q: Can you make that concrete? What has this looked like in practice?</b><a class="anchor-link" id="q-can-you-make-that-concrete-what-has-this-looked-like-in-practice"></a></h2>
<p><b>Avi:</b><span> Two examples.</span></p>
<p><span>A leading Middle East bank had a very large Oracle footprint. We assessed 120 Oracle databases in 30 minutes, then handed them a complete migration roadmap within two days. Individual databases ranged from 30 minutes to four days of work to them out of Oracle each, roughly 40 person-days across the whole estate. We also ran a proof of concept in two days and showed a working migration, not just a demo.</span></p>
<p><span>A global enterprise moved 800 TB of databases from Oracle Standard Edition to Amazon Aurora PostgreSQL, covering more than 6,000 customers, as part of an acquisition strategy, completed over two years with validation and direct customer coordination throughout.</span></p>
<p><span>Both examples share the same pattern: unknowns get resolved at the start, not discovered in the middle.</span></p>
<p>&nbsp;</p>
<h2><b>Q: So what does the customer actually get out of this partnership?</b><a class="anchor-link" id="q-so-what-does-the-customer-actually-get-out-of-this-partnership"></a></h2>
<p><b>Avi:</b><span> The most valuable thing is that the customer doesn&rsquo;t have to invent migration from scratch. It&rsquo;s a solved problem they can buy: no unknowns, because the assessment tells you what&rsquo;s in the estate before you commit to a plan; validation at every stage, so schema and data are verified, not assumed; rollback through reverse replication, which is what makes teams willing to schedule a cutover; and committed timelines, which is often the real blocker for leadership.</span></p>
<p>&nbsp;</p>
<h2><b>Q: If I&rsquo;m a CIO reading this, what should I be asking myself?</b><a class="anchor-link" id="q-if-im-a-cio-reading-this-what-should-i-be-asking-myself"></a></h2>
<p><b>Avi:</b><span> Three questions, in this order.</span></p>
<p><span>Did I pick the right database to migrate first? Starting with the hardest one is the most common self-inflicted wound. It burns budget and demoralizes leadership before any value is delivered.</span></p>
<p><span>Have we actually assessed our databases, so we know which ones are low-hanging fruit and which need real engineering? Without that, you&rsquo;re guessing.</span></p>
<p><span>Did we pick the right tooling: a platform genuinely built for end-to-end migration, not a set of disconnected tools stitched together? Get those three right, and the migration is largely an execution problem.</span></p>
<p>&nbsp;</p>
<h2><b>Q: What are the biggest migration goals you&rsquo;re seeing?</b><a class="anchor-link" id="q-what-are-the-biggest-migration-goals-youre-seeing"></a></h2>
<p><b>Avi:</b><span> Four exits dominate the conversations we&rsquo;re in: Oracle, SQL Server, DB2 and Sybase ASE. Sybase ASE deserves a special mention, since customers there are being pushed toward SAP or SQL Server, trading one proprietary database for another. Through this partnership, PostgreSQL becomes a realistic, cheaper target instead.</span></p>
<p><span>Many of these tie into data center exits and hybrid cloud programs, where a single platform pays off most. We cover homogeneous migrations (PostgreSQL to PostgreSQL, MySQL to MySQL, any platform to any platform), heterogeneous migrations (Oracle, SQL Server, DB2 and Sybase ASE to PostgreSQL), and same-engine platform moves, like Azure Managed SQL to SQL Server on VMs. A data center exit is rarely one type of migration. It&rsquo;s usually all three at once, under a deadline set by a lease or a contract.</span></p>
<p>&nbsp;</p>
<h2><b>Q: What are the next steps for someone reading this?</b><a class="anchor-link" id="q-what-are-the-next-steps-for-someone-reading-this"></a></h2>
<p><b>Avi:</b><span> Start with the assessment. It&rsquo;s fast, it&rsquo;s concrete, and it&rsquo;s the step that de-risks everything after it.</span></p>
<p><span>We invite organizations to run a DMAT database migration assessment across their estate and get back a detailed roadmap: which databases to move first, what effort each one takes, and what the cost savings look like. From there, Percona and HexaCluster can take it through to production and support it long term.</span></p>
<p><span>You don&rsquo;t need to commit to a migration to find out what one would cost you. That&rsquo;s the point of assessing first.</span></p>
<p><span>To start an assessment or discuss an Oracle, SQL Server, DB2 or Sybase ASE exit,</span><a href="https://www.percona.com/about/contact"> <span>contact your Percona representative</span></a><span> or reach out to HexaCluster directly.</span></p>
<p>The post <a href="https://www.percona.com/blog/percona-and-hexacluster-faster-safer-oracle-migration/">Percona and HexaCluster: Faster, Safer Oracle Migration</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-and-hexacluster-faster-safer-oracle-migration/">Percona and HexaCluster: Faster, Safer Oracle Migration</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/" />
      <id>https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/</id>
      <updated>2026-09-14T16:35:33+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that Amazon Web Services (AWS) has renewed its Diamond sponsorship for a fourth consecutive year. …<br />
Continue reading \"AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year\"<br />
AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/">AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that <a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/">Amazon Web Services (AWS</a>) has renewed its <a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/">Diamond sponsorship</a> for a fourth consecutive year. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year&rdquo;</span></a></p>
<p><a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/">AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/aws-renews-diamond-sponsorship-of-mariadb-foundation-for-a-fourth-consecutive-year/">AWS renews Diamond sponsorship of MariaDB Foundation for a fourth consecutive year</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Prove the Backup: A Percona Distribution for PostgreSQL Restore Drill on Windows</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/09/14/percona-postgresql-restore-drill-on-windows/" />
      <id>https://percona.community/blog/2026/09/14/percona-postgresql-restore-drill-on-windows/</id>
      <updated>2026-09-14T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>A backup is only useful if it can be restored. This lab seeds a source database, takes a logical archive, deliberately deletes the source database, restores into a clean target, and checks the recovered state. Percona Distribution for PostgreSQL does not provide a native Windows package. Windows is only the Docker Desktop host; PostgreSQL and its utilities run in Linux containers.</p>
<p><a href="https://percona.community/blog/2026/09/14/percona-postgresql-restore-drill-on-windows/">Prove the Backup: A Percona Distribution for PostgreSQL Restore Drill on Windows</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A backup is only useful if it can be restored. This lab seeds a source database, takes a logical archive, deliberately deletes the source database, restores into a clean target, and checks the recovered state. Percona Distribution for PostgreSQL does not provide a native Windows package. Windows is only the Docker Desktop host; PostgreSQL and its utilities run in Linux containers.</p>
<p>The current Percona 17 Docker documentation uses <code>percona/percona-distribution-postgresql:17.11</code>, requires <code>POSTGRES_PASSWORD</code>, and exposes PostgreSQL on container port 5432. Docker Desktop is the recommended way to obtain Docker Engine, the CLI, and Compose on Windows. <a href="https://docs.percona.com/postgresql/17/docker.html" target="_blank" rel="noopener noreferrer">Percona Docker guide</a> &middot; <a href="https://docs.docker.com/compose/install/" target="_blank" rel="noopener noreferrer">Docker Compose installation</a></p>
<h2>1. Create the Compose project<a class="anchor-link" id="1-create-the-compose-project"></a></h2>
<p>Start Docker Desktop, open PowerShell, and make a uniquely named lab directory. The timestamp gives Compose a new project name, so an old named volume cannot silently turn a rerun into a test of stale data.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="s2">"Stop"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">version</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Docker Engine is unavailable."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">version</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Docker Compose is unavailable."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$lab</span> <span class="p">=</span> <span class="s2">"percona-restore-lab-{0}"</span> <span class="o">-f</span> <span class="p">(</span><span class="nb">Get-Date</span> <span class="n">-Format</span> <span class="s2">"yyyyMMdd-HHmmss"</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">-Path</span> <span class="nv">$lab</span> <span class="p">|</span> <span class="nb">Out-Null</span>
</span></span><span class="line"><span class="cl"><span class="nb">Set-Location</span> <span class="nv">$lab</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">-Path</span> <span class="n">backups</span> <span class="p">|</span> <span class="nb">Out-Null</span></span></span></code></pre>
</div>
</div>
</div>
<p>Save this as <code>compose.yaml</code>. Separate named volumes prevent the recovery target from sharing source data; the archive will be copied through the Windows filesystem.</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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-distribution-postgresql:17.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">drill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">lab-only-change-me</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">drill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"127.0.0.1:55432:5432"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">source-data:/data/db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"CMD-SHELL"</span><span class="p">,</span><span class="w"> </span><span class="s2">"pg_isready -q -U $${POSTGRES_USER} -d $${POSTGRES_DB}"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-distribution-postgresql:17.11</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">drill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">lab-only-change-me</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">restore_drill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"127.0.0.1:55433:5432"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">target-data:/data/db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"CMD-SHELL"</span><span class="p">,</span><span class="w"> </span><span class="s2">"pg_isready -q -U $${POSTGRES_USER} -d $${POSTGRES_DB}"</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">source-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="l">target-data:</span></span></span></code></pre>
</div>
</div>
</div>
<p>This password is disposable; use proper secret handling outside a lab. Percona&rsquo;s v17 Dockerfile sets <code>/data/db</code> as <code>PGDATA</code>. Compose does not otherwise equate &ldquo;running&rdquo; with &ldquo;ready,&rdquo; so health checks matter. <a href="https://github.com/percona/percona-docker/blob/main/percona-distribution-postgresql-17/Dockerfile" target="_blank" rel="noopener noreferrer">Percona v17 Dockerfile</a> &middot; <a href="https://docs.docker.com/compose/how-tos/startup-order/" target="_blank" rel="noopener noreferrer">Docker startup-order guidance</a></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">config</span> <span class="p">-</span><span class="n">-quiet</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Compose validation failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">pull</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Image pull failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">up</span> <span class="n">-d</span> <span class="p">-</span><span class="n">-wait</span> <span class="p">-</span><span class="n">-wait-timeout</span> <span class="mf">60</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Source failed to become healthy."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$serverVersion</span> <span class="p">=</span> <span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">drill</span> <span class="n">-Atc</span> <span class="s2">"SELECT version();"</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Version query failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">$serverVersion</span></span></span></code></pre>
</div>
</div>
</div>
<p>Record the actual version string. Do not infer it from the tag alone.</p>
<h2>2. Seed known data and prove the baseline<a class="anchor-link" id="2-seed-known-data-and-prove-the-baseline"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="sh">@'
</span></span></span><span class="line"><span class="cl"><span class="sh">CREATE TABLE orders (
</span></span></span><span class="line"><span class="cl"><span class="sh"> id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
</span></span></span><span class="line"><span class="cl"><span class="sh"> customer text NOT NULL,
</span></span></span><span class="line"><span class="cl"><span class="sh"> amount numeric(10,2) NOT NULL CHECK (amount &gt;= 0)
</span></span></span><span class="line"><span class="cl"><span class="sh">);
</span></span></span><span class="line"><span class="cl"><span class="sh">INSERT INTO orders (customer, amount)
</span></span></span><span class="line"><span class="cl"><span class="sh">VALUES ('Ada', 10.25), ('Grace', 20.00), ('Linus', 30.00);
</span></span></span><span class="line"><span class="cl"><span class="sh">'@</span> <span class="p">|</span> <span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">drill</span> <span class="n">-v</span> <span class="n">ON_ERROR_STOP</span><span class="p">=</span><span class="mf">1</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Seed step failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$baseline</span> <span class="p">=</span> <span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">drill</span> <span class="n">-Atc</span> <span class="s2">"SELECT count(*) || '|' || sum(amount) FROM orders;"</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span> <span class="o">-or</span> <span class="nv">$baseline</span><span class="p">.</span><span class="py">Trim</span><span class="p">()</span> <span class="o">-ne</span> <span class="s2">"3|60.25"</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">throw</span> <span class="s2">"Baseline check failed: </span><span class="nv">$baseline</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Pass:</strong> the command returns <code>3|60.25</code>. Anything else is a stop condition.</p>
<h2>3. Create and export the archive<a class="anchor-link" id="3-create-and-export-the-archive"></a></h2>
<p><code>pg_dump -Fc</code> creates a custom-format archive for <code>pg_restore</code>; PostgreSQL describes this format as flexible and compressed by default. Write it inside the Linux container, inspect its table of contents, and copy it to Windows:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">pg_dump</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">drill</span> <span class="n">-Fc</span> <span class="o">-f</span> <span class="p">/</span><span class="n">tmp</span><span class="p">/</span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"pg_dump failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">pg_restore</span> <span class="p">-</span><span class="n">-list</span> <span class="p">/</span><span class="n">tmp</span><span class="p">/</span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Archive inspection failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="nb">cp </span><span class="n">source</span><span class="err">:</span><span class="p">/</span><span class="n">tmp</span><span class="p">/</span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span> <span class="p">.</span><span class="n">backups</span><span class="p"></span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Archive export failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$backup</span> <span class="p">=</span> <span class="nb">Get-Item</span> <span class="p">.</span><span class="n">backups</span><span class="p"></span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span> <span class="n">-ErrorAction</span> <span class="n">Stop</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$backup</span><span class="p">.</span><span class="py">Length</span> <span class="o">-le</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Backup file is empty."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">$backup</span> <span class="p">|</span> <span class="nb">Select-Object</span> <span class="n">FullName</span><span class="p">,</span> <span class="n">Length</span><span class="p">,</span> <span class="n">LastWriteTime</span>
</span></span><span class="line"><span class="cl"><span class="nb">Get-FileHash</span> <span class="p">.</span><span class="n">backups</span><span class="p"></span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span> <span class="n">-Algorithm</span> <span class="n">SHA256</span></span></span></code></pre>
</div>
</div>
</div>
<p>Copying avoids routing a binary archive through PowerShell&rsquo;s output pipeline. <code>docker compose cp</code> transfers files between a service container and the local filesystem. The hash identifies this particular file; it is not a universal expected value. Two clean image-side validation runs each produced a 2,360-byte archive but different SHA-256 hashes because the archives contain run-specific metadata. Record the hash so the same file can be checked after another transfer. <a href="https://www.postgresql.org/docs/17/app-pgdump.html" target="_blank" rel="noopener noreferrer">PostgreSQL <code>pg_dump</code></a> &middot; <a href="https://docs.docker.com/reference/cli/docker/compose/cp/" target="_blank" rel="noopener noreferrer">Docker Compose <code>cp</code></a></p>
<h2>4. Inject failure, then recover<a class="anchor-link" id="4-inject-failure-then-recover"></a></h2>
<p>This is the destructive step. Confirm that the current timestamped lab directory is disposable before continuing.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">postgres</span> <span class="n">-v</span> <span class="n">ON_ERROR_STOP</span><span class="p">=</span><span class="mf">1</span> <span class="n">-c</span> <span class="s2">"DROP DATABASE drill;"</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Failure injection command failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Windows PowerShell 5.1 treats redirected native stderr as an error record.</span>
</span></span><span class="line"><span class="cl"><span class="c"># This one command is expected to fail; capture it, then restore strict handling.</span>
</span></span><span class="line"><span class="cl"><span class="nv">$previousErrorActionPreference</span> <span class="p">=</span> <span class="nv">$ErrorActionPreference</span>
</span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="s2">"Continue"</span>
</span></span><span class="line"><span class="cl"> <span class="nv">$missingDb</span> <span class="p">=</span> <span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">source</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">drill</span> <span class="n">-c</span> <span class="s2">"SELECT 1;"</span> <span class="mf">2</span><span class="p">&gt;&amp;</span><span class="mf">1</span> <span class="p">|</span> <span class="nb">Out-String</span>
</span></span><span class="line"><span class="cl"> <span class="nv">$missingDbExit</span> <span class="p">=</span> <span class="nv">$LASTEXITCODE</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="nv">$previousErrorActionPreference</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nv">$missingDb</span><span class="p">.</span><span class="py">Trim</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$missingDbExit</span> <span class="o">-eq</span> <span class="mf">0</span> <span class="o">-or</span> <span class="nv">$missingDb</span> <span class="o">-notmatch</span> <span class="s1">'databases+"drill"s+doess+nots+exist'</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">throw</span> <span class="s2">"Unexpected failure probe result: </span><span class="nv">$missingDb</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">stop</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Could not stop the failed source."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">up</span> <span class="n">-d</span> <span class="p">-</span><span class="n">-wait</span> <span class="p">-</span><span class="n">-wait-timeout</span> <span class="mf">60</span> <span class="n">target</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Target failed to become healthy."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="nb">cp </span><span class="p">.</span><span class="n">backups</span><span class="p"></span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span> <span class="n">target</span><span class="err">:</span><span class="p">/</span><span class="n">tmp</span><span class="p">/</span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Archive import failed."</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">target</span> <span class="n">pg_restore</span> <span class="n">-U</span> <span class="n">drill</span> <span class="p">-</span><span class="n">-dbname</span><span class="p">=</span><span class="n">restore_drill</span> <span class="p">-</span><span class="n">-clean</span> <span class="p">-</span><span class="n">-if-exists</span> <span class="p">-</span><span class="n">-single-transaction</span> <span class="p">/</span><span class="n">tmp</span><span class="p">/</span><span class="n">drill</span><span class="p">.</span><span class="py">dump</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span><span class="p">)</span> <span class="p">{</span> <span class="k">throw</span> <span class="s2">"Restore failed."</span> <span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Failure check passes</strong> only when the post-drop connection fails specifically because <code>drill</code> does not exist. The regular expression tolerates whitespace because Windows PowerShell 5.1 can wrap native error text across lines. Stopping the now-unhealthy source keeps it out of the target&rsquo;s readiness check. The target has a deliberately different database name, so the restore omits <code>--create</code> and loads directly into <code>restore_drill</code>. <code>--clean --if-exists</code> makes replacement explicit without missing-object noise. For this small lab, <code>--single-transaction</code> makes the restore all-or-nothing and implies exit-on-error. <a href="https://www.postgresql.org/docs/17/app-pgrestore.html" target="_blank" rel="noopener noreferrer">PostgreSQL <code>pg_restore</code></a></p>
<h2>5. Verify the recovered state<a class="anchor-link" id="5-verify-the-recovered-state"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">powershell</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$verifySql</span> <span class="p">=</span> <span class="sh">@'
</span></span></span><span class="line"><span class="cl"><span class="sh">SELECT concat_ws('|',
</span></span></span><span class="line"><span class="cl"><span class="sh"> (SELECT string_agg(id::text || ':' || customer || ':' || amount::text, ',' ORDER BY id) FROM orders),
</span></span></span><span class="line"><span class="cl"><span class="sh"> (SELECT count(*) FROM pg_constraint WHERE conrelid = 'public.orders'::regclass),
</span></span></span><span class="line"><span class="cl"><span class="sh"> (SELECT is_identity FROM information_schema.columns
</span></span></span><span class="line"><span class="cl"><span class="sh"> WHERE table_schema = 'public' AND table_name = 'orders' AND column_name = 'id'),
</span></span></span><span class="line"><span class="cl"><span class="sh"> (SELECT last_value || ':' || is_called FROM public.orders_id_seq)
</span></span></span><span class="line"><span class="cl"><span class="sh">);
</span></span></span><span class="line"><span class="cl"><span class="sh">'@</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$restored</span> <span class="p">=</span> <span class="nv">$verifySql</span> <span class="p">|</span> <span class="n">docker</span> <span class="n">compose</span> <span class="n">exec</span> <span class="n">-T</span> <span class="n">target</span> <span class="n">psql</span> <span class="n">-U</span> <span class="n">drill</span> <span class="n">-d</span> <span class="n">restore_drill</span> <span class="n">-At</span> <span class="n">-v</span> <span class="n">ON_ERROR_STOP</span><span class="p">=</span><span class="mf">1</span>
</span></span><span class="line"><span class="cl"><span class="nv">$expected</span> <span class="p">=</span> <span class="s2">"1:Ada:10.25,2:Grace:20.00,3:Linus:30.00|2|YES|3:true"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$LASTEXITCODE</span> <span class="o">-ne</span> <span class="mf">0</span> <span class="o">-or</span> <span class="nv">$restored</span><span class="p">.</span><span class="py">Trim</span><span class="p">()</span> <span class="o">-ne</span> <span class="nv">$expected</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="k">throw</span> <span class="s2">"Restore verification failed: </span><span class="nv">$restored</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="s2">"PASS: rows, constraints, identity definition, and sequence state recovered."</span></span></span></code></pre>
</div>
</div>
</div>
<p>The drill passes only if readiness succeeds, the baseline matches, the archive is nonempty and parseable, the expected missing-database error is observed, <code>pg_restore</code> exits zero, and the final row, constraint, identity, and sequence signature matches exactly.</p>
<h2>When a check fails<a class="anchor-link" id="when-a-check-fails"></a></h2>
<p>Treat a thrown error as evidence, not an invitation to skip ahead. If the source never becomes healthy, run <code>docker compose ps</code> and <code>docker compose logs --no-color source</code>; initialization and permission errors normally appear there. A &ldquo;port is already allocated&rdquo; error concerns the host side of 55432 or 55433, not container port 5432. If <code>pg_restore --list</code> fails, discard the archive and create a new one rather than attempting a hopeful restore.</p>
<p>Ownership errors usually mean that source and target roles differ. This lab intentionally creates the <code>drill</code> superuser in both containers, avoiding that variable; a production restore needs an explicit role and privilege plan. A final signature mismatch is also a failed restore even when <code>pg_restore</code> returned zero. Preserve the logs and archive, identify the difference, fix the procedure, and repeat from a new timestamped project. Do not label the backup usable until every check passes.</p>
<h2>PowerShell and Docker Desktop gotchas<a class="anchor-link" id="powershell-and-docker-desktop-gotchas"></a></h2>
<ul>
<li>Use <code>docker compose</code>, not the legacy <code>docker-compose</code> executable.</li>
<li>Run from the directory containing <code>compose.yaml</code>. Keep Windows paths such as <code>.backups</code> separate from container paths such as <code>/tmp</code>.</li>
<li>Keep <code>$LASTEXITCODE</code> checks immediately after native commands. <code>-T</code> disables pseudo-TTY allocation for noninteractive execution.</li>
<li>Avoid PowerShell backtick line continuations: an invisible trailing space can break them.</li>
<li>If host port 55432 or 55433 is occupied, change only the left side of that mapping. The container port remains 5432.</li>
<li><code>docker compose down</code> preserves named volumes. For this lab only, <code>docker compose down -v</code> deletes both disposable data volumes; never apply <code>-v</code> casually elsewhere. <a href="https://docs.docker.com/reference/cli/docker/compose/down/" target="_blank" rel="noopener noreferrer">Docker Compose <code>down</code></a></li>
</ul>
<h2>What this drill does not prove<a class="anchor-link" id="what-this-drill-does-not-prove"></a></h2>
<p>This is a logical, single-database restore&mdash;not physical backup, point-in-time recovery, replication, failover, or a performance test. <code>pg_dump</code> does not capture cluster-wide roles and tablespaces; PostgreSQL directs those cases to <code>pg_dumpall</code>. A backup kept on the same laptop is not disaster-resilient. The lab also does not exercise application reconnects, large datasets, extensions, encryption, or remote object storage. Restore only archives from trusted sources: PostgreSQL warns that restoring can execute code chosen by source superusers. <a href="https://www.postgresql.org/docs/17/app-pgdump.html" target="_blank" rel="noopener noreferrer">PostgreSQL <code>pg_dump</code> limitations and warning</a></p>
<h2>Validation scope<a class="anchor-link" id="validation-scope"></a></h2>
<p>On September 5, 2026, this workflow completed one clean, AI-assisted automated run on the author&rsquo;s Windows 11 Pro 25H2 laptop (build 26200.9168), using Docker Desktop 4.89.0, Docker Engine 29.7.2, Compose 5.5.0, and Windows PowerShell 5.1.26100.9168. The run used a fresh Compose project and separate new source and target data volumes. Linux amd64 containers from <code>percona/percona-distribution-postgresql:17.11</code> reported PostgreSQL 17.11 &mdash; Percona Server for PostgreSQL 17.11.1.</p>
<p>Plain <code>docker compose pull</code> succeeded. Both services became healthy within the article&rsquo;s 60-second timeout. The source returned the <code>3|60.25</code> baseline, and <code>pg_restore --list</code> parsed the exported 2,360-byte archive. Both <code>docker compose cp</code> transfers passed through the Windows filesystem. The Windows archive and imported target copy had the same SHA-256: <code>bb521cadcca1547c39d3d3a161a65228300a68f0905badf09b79011236c5311d</code>.</p>
<p>After the source database was dropped, its connection probe returned exit 2 with the expected missing-database error. The source was stopped, <code>pg_restore</code> into the clean target exited zero, and the recovered signature matched exactly: <code>1:Ada:10.25,2:Grace:20.00,3:Linus:30.00|2|YES|3:true</code>.</p>
<p>Execution used a PowerShell evidence wrapper around the article&rsquo;s Compose layout and database commands, adding explicit project scoping, container ownership checks, command-output capture, and the imported-archive hash comparison. The expected-error probe was corrected for Windows PowerShell 5.1: temporary <code>Continue</code> handling preserves the native error and exit code, and a whitespace-tolerant pattern accepts line-wrapped error text. The saved transcript, command outputs, status record, and backup support these results. This was automated execution on the author&rsquo;s Windows laptop; no manual command-by-command execution is claimed.</p>
<h2>AI-assistance disclosure<a class="anchor-link" id="ai-assistance-disclosure"></a></h2>
<p>AI assistance was used to structure this lab, cross-check commands against official documentation, edit the prose, and prepare and execute the automated PowerShell validation on the author&rsquo;s Windows laptop. The author remains responsible for reviewing the evidence, correcting the article, and approving it for publication.</p>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>

<p><a href="https://percona.community/blog/2026/09/14/percona-postgresql-restore-drill-on-windows/">Prove the Backup: A Percona Distribution for PostgreSQL Restore Drill on Windows</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona releases Galera Cluster Version 9.7</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/percona/percona-xtradb-galera-cluster-9-7-released/" />
      <id>https://www.fromdual.com/blog/percona/percona-xtradb-galera-cluster-9-7-released/</id>
      <updated>2026-09-14T09:33:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Finally, some good news again!<br />
What has happened so far<br />
After MariaDB Corp. acquired Codership OY—and with it, Galera Cluster for MySQL—in May 2025 [ 1 ], then announced the discontinuation of support for Galera Cluster for MySQL 8.4 at the end of September 2026 [ 2 ], and subsequently stated that it would no longer support Galera Cluster (for MariaDB!) in the Community Edition [ 3 ], there was a loud outcry from the community. MariaDB Corp. then made a partial retreat (“We’ve thoroughly considered your feedback and decided that now is not the time for a major change,”). A clear commitment to continued support for Galera Cluster in the Community Edition sounds different.<br />
Shortly thereafter, the MariaDB Foundation announced that it did not intend to fork Galera Cluster (“Thus the Foundation does not see a need for forking Galera and does not support or encourage such action.”) [ 4 ].<br />
So, until Friday (2026-09-10), the future of Galera Cluster for MySQL was very uncertain!<br />
Percona’s announcement yesterday<br />
But fortunately, there’s still Percona with its Percona XtraDB Cluster (PXC), which is an improved branch/fork of both MySQL and Galera Cluster.<br />
And Percona finally announced the release of Percona XtraDB Cluster 9.7 on Friday (September 10, 2026) [ 5 ]. For us, this was the long-awaited and hoped-for signal from Percona!<br />
It would be nice if Percona could provide a clearer statement regarding Galera Cluster so that we can better plan for it in terms of timing…<br />
Installation<br />
Of course, we couldn’t resist and immediately downloaded the latest release [ 6 ] and set up a Percona XtraDB test cluster.<br />
Due to our standard Galera installation process, we had to make a few adjustments here and there to ensure that the Percona XtraDB Cluster ran smoothly.<br />
We’ve described the details here: Migration from MySQL Galera Cluster 8.4 to Percona XtraDB Cluster 9.7<br />
Further tests will follow in the coming days and weeks…<br />
Now would actually be a good time for commercially oriented Galera Cluster users to sponsor Percona…? For a vibrant and healthy MySQL open-source ecosystem: The Future of the MySQL Ecosystem.<br />
Sources</p>
<p>Percona XtraDB Cluster 9.7 Documentation</p>
<p>This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/percona/percona-xtradb-galera-cluster-9-7-released/">Percona releases Galera Cluster Version 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Finally, some good news again!</p>
<h2>What has happened so far<a class="anchor-link" id="what-has-happened-so-far"></a></h2>
<p>After MariaDB Corp. acquired Codership OY&mdash;and with it, Galera Cluster for MySQL&mdash;in May 2025 [&nbsp;<a href="https://www.infoworld.com/article/3999634/mariadbs-acquisition-of-codership-why-enterprises-should-care.html" target="_blank" title="MariaDB&rsquo;s acquisition of Codership: Why enterprises should care">1</a>&nbsp;], then announced the discontinuation of support for Galera Cluster for MySQL 8.4 at the end of September 2026 [&nbsp;<a href="https://www.theregister.com/databases/2026/07/30/mariadb-again-faces-questions-over-galeras-open-source-future/5280975" target="_blank" title="MariaDB again faces questions over Galera's open source future">2</a>&nbsp;], and subsequently stated that it would no longer support Galera Cluster (for MariaDB!) in the Community Edition [&nbsp;<a href="https://www.theregister.com/software/2026/03/09/mariadb-backs-down-on-galera-removal-after-community-outcry/5224684" target="_blank" title="MariaDB backs down on Galera removal after community outcry">3</a>&nbsp;], there was a loud outcry from the community. MariaDB Corp. then made a partial retreat (&ldquo;<em>We&rsquo;ve thoroughly considered your feedback and decided that <strong>now is not the time</strong> for a major change</em>,&rdquo;). A clear commitment to continued support for Galera Cluster in the Community Edition sounds different.</p>
<p>Shortly thereafter, the MariaDB Foundation announced that it did not intend to fork Galera Cluster (&ldquo;<em>Thus the Foundation <strong>does not see a need for forking Galera</strong> and does not support or encourage such action.</em>&rdquo;) [&nbsp;<a href="https://mariadb.org/galera-continuity-and-responsibility-how-the-foundation-and-mariadb-plc-move-forward/" target="_blank" title="Galera, continuity, and responsibility: how the Foundation and MariaDB plc move forward">4</a>&nbsp;].</p>
<p>So, until Friday (2026-09-10), the future of Galera Cluster for MySQL was very uncertain!</p>
<h2>Percona&rsquo;s announcement yesterday<a class="anchor-link" id="perconas-announcement-yesterday"></a></h2>
<p>But fortunately, there&rsquo;s still Percona with its <a href="https://www.percona.com/resource/percona-xtradb-cluster/" target="_blank">Percona XtraDB Cluster (PXC)</a>, which is an improved branch/fork of both MySQL and Galera Cluster.</p>
<p>And Percona finally announced the release of Percona XtraDB Cluster 9.7 on Friday (September 10, 2026) [&nbsp;<a href="https://docs.percona.com/new/2026/09/10/percona-xtradb-cluster-971-1-has-been-released/" target="_blank" title="Percona XtraDB Cluster 9.7.1-1 has been released">5</a>&nbsp;]. For us, this was the long-awaited and hoped-for signal from Percona!</p>
<p>It would be nice if Percona could provide a clearer statement regarding Galera Cluster so that we can better plan for it in terms of timing&hellip;</p>
<h2>Installation<a class="anchor-link" id="installation"></a></h2>
<p>Of course, we couldn&rsquo;t resist and immediately downloaded the latest release [&nbsp;<a href="https://www.percona.com/downloads/" target="_blank" title="Percona Download">6</a>&nbsp;] and set up a Percona XtraDB test cluster.</p>
<p>Due to our standard Galera installation process, we had to make a few adjustments here and there to ensure that the Percona XtraDB Cluster ran smoothly.</p>
<p>We&rsquo;ve described the details here: <a href="https://www.fromdual.com/blog/mysql-mariadb-migration/#migration-from-mysql-galera-cluster-84-to-percona-xtradb-cluster-97">Migration from MySQL Galera Cluster 8.4 to Percona XtraDB Cluster 9.7</a></p>
<p>Further tests will follow in the coming days and weeks&hellip;</p>
<p>Now would actually be a good time for commercially oriented Galera Cluster users to sponsor Percona&hellip;? For a vibrant and healthy MySQL open-source ecosystem: <a href="https://oursqlfoundation.org/" target="_blank">The Future of the MySQL Ecosystem</a>.</p>
<h2>Sources<a class="anchor-link" id="sources"></a></h2>
<ul>
<li><a href="https://docs.percona.com/percona-xtradb-cluster/9.7/" target="_blank">Percona XtraDB Cluster 9.7 Documentation</a></li>
</ul>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/percona/percona-xtradb-galera-cluster-9-7-released/">Percona releases Galera Cluster Version 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Optimising Multi-Range Queries with Logical Charts</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/optimising-multi-range-queries-with-logical-charts/" />
      <id>https://vettabase.com/optimising-multi-range-queries-with-logical-charts/</id>
      <updated>2026-09-14T09:12:22+03:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>Regular database indexes cannot be used for queries that contain conditions based on two or more ranges. But there are unconventional ways to solve this problem and make queries faster. In this post we’ll explore a conceptually simple way: logical charts. The Problem Suppose we need to find the customers in a certain age band who made a certain number of orders in the last 12 months. We have both these values in the same table, and we have an index covering both columns and nothing else. Do you expect the following query to use the index? Depending on which database you use, there are a few options: Why? A B-Tree index optimises queries because it’s an ordered data structure. But the order of values doesn’t help making a search on multiple ranges. For more information about what a B-Tree can do, see Query Optimisation: Using indexes for WHERE with multiple conditions. I call queries like this Double-Range Queries. A Solution: GIS Data Think of the data in the only way that feels natural for a human: a point chart where the axes represent the date of birth and the number of orders in the last year. The following image, […]</p>
<p><a href="https://vettabase.com/optimising-multi-range-queries-with-logical-charts/">Optimising Multi-Range Queries with Logical Charts</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">Regular database indexes cannot be used for queries that contain conditions based on two or more ranges. But there are unconventional ways to solve this problem and make queries faster. In this post we&rsquo;ll explore a conceptually simple way: logical charts.</p>
<h2 class="wp-block-heading">The Problem<a class="anchor-link" id="the-problem"></a></h2>
<p class="wp-block-paragraph">Suppose we need to find the customers in a certain age band who made a certain number of orders in the last 12 months. We have both these values in the same table, and we have an index covering both columns and nothing else. Do you expect the following query to use the index?</p>
<pre class="wp-block-code"><code>SELECT id, email
    FROM customer
    WHERE
        (date_of_birth BETWEEN DATE '1994-01-01' AND DATE '2004-01-01')
        AND (last_year_orders BETWEEN 5 AND 10)
;</code></pre>
<p class="wp-block-paragraph">Depending on which database you use, there are a few options:</p>
<ul class="wp-block-list">
<li>The index isn&rsquo;t used at all.</li>
<li>The index is used for the first column stored in the index, but to read the second the database makes additional reads from the data. Depending on your data distribution, this is probably slower than a full table scan (which is usually slow, unless the table is very small).
<ul class="wp-block-list">
<li>If the first column in the index has lower selectivity than the second, the query execution will be even slower than it should be.</li>
</ul>
</li>
<li>The whole index is scanned. This is probably as slow as a full table scan, or slightly faster.</li>
</ul>
<p class="wp-block-paragraph">Why? A B-Tree index optimises queries because it&rsquo;s an ordered data structure. But the order of values doesn&rsquo;t help making a search on multiple ranges. For more information about what a B-Tree can do, see <a href="https://vettabase.com/query-optimisation-using-indexes-for-where-with-multiple-conditions/" data-type="post" data-id="360103">Query Optimisation: Using indexes for WHERE with multiple conditions</a>.</p>
<p class="wp-block-paragraph">I call queries like this <strong>Double-Range Queries</strong>.</p>
<h2 class="wp-block-heading">A Solution: GIS Data<a class="anchor-link" id="a-solution-gis-data"></a></h2>
<p class="wp-block-paragraph">Think of the data in the only way that feels natural for a human: a point chart where the axes represent the date of birth and the number of orders in the last year. The following image, created by Nano Banana 2, represents this way of visualising the data:</p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://vettabase.com/wp-content/uploads/2026/08/2d_data_representation-1024x559.jpeg" alt="" class="wp-image-awgt_b14846fa1ba0"></figure>
<p class="wp-block-paragraph">Each point represents a customer, and therefore a point we need to select. Conceptually, to select the right points we only need to draw a rectangle on the chart:</p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://vettabase.com/wp-content/uploads/2026/08/2d_data_and_query_representation-1024x559.jpeg" alt="" class="wp-image-awgt_b14846fa1ba0"></figure>
<p class="wp-block-paragraph">Now the concept should be clear, but we need to write the SQL. Let&rsquo;s see the table definition, and then comment it:</p>
<pre class="wp-block-code"><code>CREATE TABLE customer (
    id INT AUTO_INCREMENT PRIMARY KEY,
    date_of_birth DATE NOT NULL,
    last_year_orders INT NOT NULL,
    -- ...probably more columns here...
    dob_orders_chart POINT GENERATED ALWAYS AS (
        POINT(TO_DAYS(date_of_birth), last_year_orders)
    ) STORED NOT NULL,
    SPATIAL INDEX sp_idx_dob_orders (dob_orders_chart)
) ENGINE=InnoDB;</code></pre>
<p class="wp-block-paragraph">Things to note in the above snippet:</p>
<ul class="wp-block-list">
<li>We added a column called <code>dob_orders_chart</code>.</li>
<li>The new column could have been of <code>GEOMETRY</code> type. That would work. But all the values we&rsquo;re going to have are points, so we choose the more efficient <code>POINT</code> type instead.</li>
<li><code>dob_orders_chart</code> is a <em>generated column</em>. This means that when a row is inserted, or one of (<code>date_of_birth</code>, <code>last_year_orders</code>) is updated, a new value for <code>dob_orders_chart</code> is calculated automatically. Users cannot change these values manually.</li>
<li>While <code>last_year_orders</code> is an <code>INTEGER</code>, <code>date_of_birth</code> is a <code>DATE</code> and its raw values cannot be used as coordinates for GIS data. The problem is easy to solve: <code>TO_DAYS(date_of_birth)</code> returns an <code>INTEGER</code>, so it&rsquo;s a valid coordinate. More specifically, <code><a href="https://mariadb.com/docs/server/reference/sql-functions/date-time-functions/to_days" rel="noopener">TO_DAYS()</a></code> returns the number of days passed from the beginning of the <a href="https://en.wikipedia.org/wiki/Gregorian_calendar" rel="noopener">Gregorian calendar</a> (Friday, 15 October 1582).</li>
<li>We index this column with a <code>SPATIAL INDEX</code>, which uses an <a href="https://en.wikipedia.org/wiki/R-tree" rel="noopener">R-Tree</a> data structure. An R-Tree index partitions a space in nested areas with boundaries that depend on data distribution.</li>
</ul>
<p class="wp-block-paragraph">Now let&rsquo;s see the query that will find the desired data from the above table:</p>
<pre class="wp-block-code"><code>SELECT id
    FROM customer
    WHERE MBRWITHIN(
        dob_orders_chart,
        ST_GEOMFROMTEXT(
            CONCAT(
                'POLYGON((',
                    TO_DAYS('1994-01-01'), ' 5, ',
                    TO_DAYS('2004-01-01'), ' 5, ',
                    TO_DAYS('2004-01-01'), ' 10, ',
                    TO_DAYS('1994-01-01'), ' 10, ',
                    TO_DAYS('1994-01-01'), ' 5',
                '))'
            )
        )
    )
;</code></pre>
<p class="wp-block-paragraph">Things to note about the above query:</p>
<ul class="wp-block-list">
<li><code><a href="https://mariadb.com/docs/server/reference/sql-statements/geometry-constructors/mbr-minimum-bounding-rectangle/mbrwithin" rel="noopener">MBRWITHIN()</a></code> can often be described as an approximate function, because MBR stands for Minimum Bounding Rectangle. For example, if you use it with a circle, it will return all points that are contained in the smallest square that can contain the circle &ndash; not just the circle itself. But in our case it&rsquo;s used with a rectangle that is aligned with the space axis, so this function will return precise results.</li>
<li><code><a href="https://mariadb.com/docs/server/reference/sql-statements/geometry-constructors/wkt/st_geomfromtext" rel="noopener">ST_GEOMFROMTEXT()</a></code> accepts a text representation of a geometric shape and its coordinates. To build this representation, we can use <code><a href="https://vettabase.com/how-to-compose-strings-in-mariadb/#CONCAT">CONCAT()</a></code>.</li>
<li>We have 5 coordinates, and that is intended. The last coordinate matches the first, and it&rsquo;s there to close the shape.</li>
</ul>
<p class="wp-block-paragraph">To concatenate strings, we can also use <code><a href="https://vettabase.com/how-to-compose-strings-in-mariadb/#SFORMAT">SFORMAT()</a></code>:</p>
<pre class="wp-block-code"><code>ST_PolyFromText(
    SFORMAT(
        'POLYGON(({0} {1}, {2} {1}, {2} {3}, {0} {3}, {0} {1}))',
        TO_DAYS('1994-01-01'), 
        5,
        TO_DAYS('2004-01-01'), 
        10
    )
)</code></pre>
<p class="wp-block-paragraph">These syntaxes are equivalent. Just use the one that you and your team find easier to understand. For more details, see <a href="https://vettabase.com/how-to-compose-strings-in-mariadb/" data-type="post" data-id="267070">How to compose strings in MariaDB</a>.</p>
<h2 class="wp-block-heading">Multi-Dimensional Charts<a class="anchor-link" id="multi-dimensional-charts"></a></h2>
<p class="wp-block-paragraph">Most databases, including MariaDB and MySQL, can&rsquo;t deal with multi-dimensional geospatial data.</p>
<h3 class="wp-block-heading">Up to 4 Dimensions on PostgreSQL<a class="anchor-link" id="up-to-4-dimensions-on-postgresql"></a></h3>
<p class="wp-block-paragraph">PostgreSQL users are luckier here: PostGIS supports multi-dimensional data, and can include up to 4 dimensions in a GiST index, thanks to the idx_spatial_nd operator class. The syntax to use for creating such an index is the following:</p>
<pre class="wp-block-code"><code>CREATE INDEX idx_spatial_nd 
    ON customer_3d 
    USING GIST (geom gist_geometry_ops_nd)
;</code></pre>
<p class="wp-block-paragraph">PostGIS doesn&rsquo;t require us to specify the list of columns to index.</p>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>Note that the number of dimensions affects the performance of a GiST index.</p>
</div>
</fieldset>
</div>
<h3 class="wp-block-heading">More Than 3 Dimensions on Other Databases<a class="anchor-link" id="more-than-3-dimensions-on-other-databases"></a></h3>
<p class="wp-block-paragraph">If we have more than two range conditions and we use a database that limits spatial indexes to 2 dimensions (or doesn&rsquo;t support GIS at all), we need to find an alternative solution.</p>
<p class="wp-block-paragraph">In several cases, you can come up with a math expression that turns the columns you need to search into a single score of type <code>INTEGER</code> or <code>FLOAT</code>. However, this is beyond the scope of this article. If you want me to cover this topic in a future article, leave a comment here.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">We&rsquo;ve discussed the type of queries I call Double-Range queries and why they can&rsquo;t make an optimal use of a B-Tree index. We&rsquo;ve seen a solution that works well in most cases. Our example even covers the case when one of the ranges is a date. I have briefly mentioned which solutions exist for queries with more than two ranges &ndash; this might be a topic for a future article.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/optimising-multi-range-queries-with-logical-charts/">Optimising Multi-Range Queries with Logical Charts</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Perfect Serve: What Tennis Taught Me About Reading PMM Like a Coach Instead of Guessing</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/09/10/the-perfect-serve/" />
      <id>https://percona.community/blog/2026/09/10/the-perfect-serve/</id>
      <updated>2026-09-10T12:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When I started learning tennis as an adult, I realized that what made tennis difficult was not learning the tennis strokes, instead it was learning to realize the value of the feedback loop: racket vibrations, tennis ball sounds, and the way my balance shifted before I even realized it. None of this information is obvious on the court. However, without it I would continue to make the same significant error, and be frustrated that “trying harder” doesn’t seem to aid.</p>
<p><a href="https://percona.community/blog/2026/09/10/the-perfect-serve/">The Perfect Serve: What Tennis Taught Me About Reading PMM Like a Coach Instead of Guessing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When I started learning tennis as an adult, I realized that what made tennis difficult was not learning the tennis strokes, instead it was learning to realize the value of the feedback loop: racket vibrations, tennis ball sounds, and the way my balance shifted before I even realized it. None of this information is obvious on the court. However, without it I would continue to make the same significant error, and be frustrated that &ldquo;trying harder&rdquo; doesn&rsquo;t seem to aid.</p>
<p>A similar realization came to me a few months ago, although this time was pretty different. Think of Percona Monitoring and Management (PMM) as the tennis racket and Percona Server for MySQL 8.0 as the tennis court. The measured result was a 95.0% reduction in average query time, from 54.17 ms to 2.71 ms. The only thing that changed was my perspective, not the setup.</p>
<h2>The Setup<a class="anchor-link" id="the-setup"></a></h2>
<p>Very basic setup. One Percona Server for MySQL 8.0 primary, two EC2 read replicas, and PMM 2.x running in a Docker container, with QAN through performance_schema. Nothing special. The symptom was also standard: an internal analytics service joined three tables (orders, order_items, and users) and polled the primary every few minutes for recent order activity. None of the tables were big.</p>
<p>The screenshots and measurements in this walkthrough come from a lab environment created to reproduce the query pattern, not from a production incident.</p>
<p>In the baseline window, the query ran at 0.16 QPS and averaged 54.17 ms. It also lined up closely with CPU spikes and occasional replica-lag warnings.</p>
<p>My &ldquo;power play&rdquo; instinct kicked in, and I thought about increasing the resources for the primary, or moving the analytic workload totally over to a replica. Both ideas had potential, but at the end of the day, they would just hide the problem. None of the ideas actually would have helped me understand what the problem was.</p>
<h2>Reading the Court: QAN Sorted by Load, Not Raw Query Time<a class="anchor-link" id="reading-the-court-qan-sorted-by-load-not-raw-query-time"></a></h2>
<p>The first real change in my mindset came from how I was sorting Query Analytics. The usual way to do this is to look for the query with the maximum or mean query time, and sort the results that way.</p>
<p>For this reason, PMM&rsquo;s QAN dashboard has a Load column. Load is the average number of simultaneous executions of a query. Instead of looking at how long a query takes when executed, a better way to look at this is how much of the server&rsquo;s attention is this query consuming, on average, right now. When sorting by Load, the three-table join jumped to the top. An average execution time of 54.17 ms does not look problematic until you realize the query is executed multiple times a minute, over and over again, continuously throughout the day.</p>
<p>That&rsquo;s the first lesson: If a query never stops running, a query that is &ldquo;fast enough&rdquo; can still be your biggest rival.</p>
<p><figure><img decoding="async" width="2506" height="918" src="https://percona.community/blog/2026/09/baseline_qan_hu_ac4be93c5ac16a26.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p><em>Figure 1: PMM Query Analytics showing the three-table join at the top of the workload, with an average query time of 54.17 ms before optimization.</em></p>
<h2>Film Review Before Stepping on the Court: EXPLAIN<a class="anchor-link" id="film-review-before-stepping-on-the-court-explain"></a></h2>
<p>Once QAN pointed at the query, the next step was to observe EXPLAIN, as a coach would observe game film: review it before making any changes in production.</p>
<p>The query, in simplified form, looked like this:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">created_at</span><span class="p">,</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">total_amount</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">email</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">oi</span><span class="p">.</span><span class="n">product_id</span><span class="p">,</span><span class="w"> </span><span class="n">oi</span><span class="p">.</span><span class="n">quantity</span><span class="p">,</span><span class="w"> </span><span class="n">oi</span><span class="p">.</span><span class="n">price</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">order_items</span><span class="w"> </span><span class="n">oi</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">oi</span><span class="p">.</span><span class="n">order_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">JOIN</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">user_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="mi">15</span><span class="w"> </span><span class="k">MINUTE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">created_at</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>EXPLAIN confirmed this was a perfect example for a full table scan on orders (type: ALL). MySQL had to read every row to filter for the time window, then it used a nested loop to join order_items and users, which exacerbated the original issue.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">orders:
</span></span><span class="line"><span class="cl">type: ALL
</span></span><span class="line"><span class="cl">key: NULL
</span></span><span class="line"><span class="cl">rows: 199875
</span></span><span class="line"><span class="cl">Extra: Using where; Using filesort
</span></span><span class="line"><span class="cl">users:
</span></span><span class="line"><span class="cl">type: eq_ref
</span></span><span class="line"><span class="cl">key: PRIMARY
</span></span><span class="line"><span class="cl">order_items:
</span></span><span class="line"><span class="cl">type: ref
</span></span><span class="line"><span class="cl">key: idx_order_items_order_id</span></span></code></pre>
</div>
</div>
</div>
<p>Thanks to PMM&rsquo;s rows examined to rows sent ratio, the query&rsquo;s inefficiency actually showed itself where it could not in raw EXPLAIN output. The query would even return a reasonably sized and useful result. The issue was that the ratio of rows examined and rows sent to the client was totally lopsided. Each execution examined about 201,370 rows while returning 819 rows, or 245.87 rows examined for every row sent. This would not trigger any system alerts, but it showed that the database was doing SO much work for a trivial result. I&rsquo;m calling this &ldquo;invisible&rdquo; inefficiency, and the more the query becomes used, the more detrimental it becomes.</p>
<p><figure><img decoding="async" width="2548" height="1334" src="https://percona.community/blog/2026/09/baseline_examined_hu_a5d12ec61d5a6df9.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p><em>Figure 2: Before optimization, each execution examined about 201,370 rows while returning 819 rows, or 245.87 rows examined for every row sent. PMM also recorded a full scan and no index used on every execution.</em></p>
<h2>The Fix: Small, Targeted Adjustments<a class="anchor-link" id="the-fix-small-targeted-adjustments"></a></h2>
<p>This was actually the best example of tennis in the real world for me. The tendency in this sort of query optimization is to swing harder (a bigger instance, more read replicas, etc.), but this was more like lightly changing the grip by a few grams.</p>
<p>Two index changes, one query change, and that was it.</p>
<p>A composite index on orders that matches the access pattern:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_orders_created_at_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="n">created_at</span><span class="p">,</span><span class="w"> </span><span class="n">id</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>The composite index on (created_at, id) allowed MySQL to use a range scan on the created_at predicate instead of scanning the entire orders table. In EXPLAIN, the orders access changed from type: ALL with roughly 199,875 rows estimated to type: range using idx_orders_created_at_id. The id column does not make this access covering, because the query still needs total_amount and user_id from the orders row.</p>
<p>A covering index on order_items:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_order_items_covering</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ON</span><span class="w"> </span><span class="n">order_items</span><span class="w"> </span><span class="p">(</span><span class="n">order_id</span><span class="p">,</span><span class="w"> </span><span class="n">product_id</span><span class="p">,</span><span class="w"> </span><span class="n">quantity</span><span class="p">,</span><span class="w"> </span><span class="n">price</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>The covering index on (order_id, product_id, quantity, price) allowed MySQL to satisfy the join and requested item columns from the index itself. EXPLAIN reported Using index.</p>
<p>The last change was to split the users lookup from the main join. The first query returned user_id, and a second query used WHERE id IN (&hellip;) to fetch the email addresses from users.</p>
<p>None of these improvements required any downtime, schema redesigns, or having to talk about sizing the instances. Each improvement was the kind of adjustment where it looked like, on its own, nothing particularly important was actually done.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">orders:
</span></span><span class="line"><span class="cl">type: range
</span></span><span class="line"><span class="cl">key: idx_orders_created_at_id
</span></span><span class="line"><span class="cl">rows: 40
</span></span><span class="line"><span class="cl">Extra: Using index condition; Backward index scan
</span></span><span class="line"><span class="cl">users:
</span></span><span class="line"><span class="cl">type: eq_ref
</span></span><span class="line"><span class="cl">key: PRIMARY
</span></span><span class="line"><span class="cl">order_items:
</span></span><span class="line"><span class="cl">type: ref
</span></span><span class="line"><span class="cl">key: idx_order_items_covering
</span></span><span class="line"><span class="cl">Extra: Using index</span></span></code></pre>
</div>
</div>
</div>
<h2>The Results<a class="anchor-link" id="the-results"></a></h2>
<p>The same normalized query remained at the top of the selected QAN workload after optimization, now running at 0.99 QPS and averaging 2.71 ms per execution.</p>
<p><figure><img decoding="async" width="2040" height="822" src="https://percona.community/blog/2026/09/optimised_qan_hu_ef292a84f6950e70.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p><em>Figure 3: The same normalized query after optimization, now averaging 2.71 ms per execution in the post-index sampling window.</em></p>
<p><figure><img decoding="async" width="2034" height="620" src="https://percona.community/blog/2026/09/optimised_metrics_hu_990c9680b2429206.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p><em>Figure 4: After the changes, average query time fell to 2.71 ms and rows examined dropped to about 1,460 per execution. The rows-examined-to-rows-sent ratio fell to 1.67, and executions used range access instead of a full scan.</em></p>
<p>Regarding the query, QAN indicated that average query time decreased from 54.17 ms to 2.71 ms, yielding a 95.0% reduction and making the query about 20 times faster. Rows examined per query fell from 201.37k to 1.46k, a 99.27% reduction, or about 138 times fewer rows examined. The ratio of rows examined to rows sent fell from 245.87 to 1.67, a 99.32% reduction. Full scan and no index used disappeared from the optimized sampling window, while Select Range reached 1.00 per query.</p>
<p>Improvements were the result of a number of incremental changes rather than a single large scale change. Composite indexing of orders and covering indexing of order_items helped rationalize the query to a considerable extent. Together, the changes rationalized the query and had an impact that would otherwise have been easy to chase with additional hardware.</p>
<h2>Shifting Your Mindset<a class="anchor-link" id="shifting-your-mindset"></a></h2>
<p>Beyond the specific indexes, the key point of this case study is how the investigation was framed at the outset.</p>
<p>QAN was sorted by Load. This let me identify the query that consumed the most attention from the primary. Unlike the query that had the most problematic single execution time.</p>
<p>Taking time to analyze EXPLAIN and the rows-examined-to-rows-sent ratio before implementing the fix let me focus on the cause of the problem. A full scan on orders. Instead of a symptom, which was a CPU spike with a concurrent lag on the replica.</p>
<p>By implementing two specific indexes and a small query rewrite, I ended up with measurable, attributable, and repeatable results. And if something was negatively impacted after the implementation of the fix, I would be able to identify which specific change caused the problem.</p>
<p>The engineer that looks as if they are moving the least in a tennis match, because they have already anticipated the next three shots, is the one doing the most invisible work, and that is exactly what this kind of production database work looks like from the outside.</p>
<p>PMM didn&rsquo;t perform the optimization, but it made the invisible work just visible enough to act on the changes, and the rest was patience, and a willingness to implement incremental changes rather than larger changes.</p>
<h2>About the author<a class="anchor-link" id="about-the-author"></a></h2>
<p>Shivank Pandey is one of two engineers managing PMM at his company. He works with Percona Server for MySQL 8.0 in a primary-replica setup on EC2 and writes about database observability and query optimization.</p>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>

<p><a href="https://percona.community/blog/2026/09/10/the-perfect-serve/">The Perfect Serve: What Tennis Taught Me About Reading PMM Like a Coach Instead of Guessing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Enabling TLS in PXC without Downtime</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/enabling-tls-in-pxc-without-downtime/" />
      <id>https://www.percona.com/blog/enabling-tls-in-pxc-without-downtime/</id>
      <updated>2026-09-09T23:32:15+03:00</updated>
      <author><name>Juan Arruti</name></author>
      <summary type="html"><![CDATA[<p>Starting with Percona XtraDB Cluster (PXC) 8.0, replication traffic encryption is enabled by default. That said, it’s common to find clusters running without TLS that suddenly need it: a new compliance requirement, an audit finding, a network segment that is no longer considered trusted. PXC has a variable for exactly that case, pxc-encrypt-cluster-traffic, which handles … Continued<br />
The post Enabling TLS in PXC without Downtime appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/enabling-tls-in-pxc-without-downtime/">Enabling TLS in PXC without Downtime</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>Starting with Percona XtraDB Cluster (PXC) 8.0, replication traffic encryption is enabled by default. That said, it&rsquo;s common to find clusters running without TLS that suddenly need it: a new compliance requirement, an audit finding, a network segment that is no longer considered trusted. </span></p>
<p><span>PXC has a variable for exactly that case, pxc-encrypt-cluster-traffic, which handles SSL encryption for inter-node traffic, including State Snapshot Transfer (SST), Incremental State Transfer (IST), and the group communication the nodes use for replication.</span></p>
<p><span>The variable is not dynamic, and turning it on normally costs a full cluster restart since the node that encrypts traffic listens on ssl:// while its peers are still on tcp://. If a node joins the cluster with TLS enabled while remaining nodes don&rsquo;t, the restarting node fails to reach out to the other peers with the following error:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-09-02T02:16:02.359992Z 0 [Note] [MY-000000] [Galera] Failed to establish connection: wrong version number</pre>
<p><span>This is a common OpenSSL error seen when a client sends encrypted TLS traffic to a server that expects unencrypted plaintext.</span></p>
<p><span>Starting with PXC 8.0.28 and in all PXC 8.4 versions, there is a way to work around the full cluster restart, which relies on the Galera socket.dynamic option. In this post I&rsquo;ll go through how this option works and how it can be used to enable TLS without downtime.</span></p>
<h3><span>The socket.dynamic Option</span><a class="anchor-link" id="the-socket-dynamic-option"></a></h3>
<p><span>This provider option lets a node establish communication with other peers using both encrypted and plaintext connections. The instance will initially open an encrypted connection, and if it fails since the other nodes are not using TLS, it will retry to establish an unencrypted connection.</span></p>
<p><span>The Joiner error log will show the following sequence:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-09-02T02:16:43.321209Z 0 [Note] [MY-000000] [Galera] Failed to establish connection: wrong version number
2026-09-02T02:16:43.321521Z 0 [Note] [MY-000000] [Galera] (565f0a92-8283, 'tcp://0.0.0.0:4567') connection established to 1c72fb64-ac57 tcp://172.18.0.3:4567
2026-09-02T02:16:43.321566Z 0 [Note] [MY-000000] [Galera] (565f0a92-8283, 'tcp://0.0.0.0:4567') connection established to 2001e9dd-a09a tcp://172.18.0.4:4567</pre>
<p><span>The first line is the same error we saw previously: our node initiated a TLS handshake, but the peer responded with something that was not TLS. Without socket.dynamic, the node would stop at that point and never join, but with it, it retries (without encryption) and is able to establish a connection.</span></p>
<p><span>This Galera option can&rsquo;t be changed at runtime, so you need to restart the node to enable it.</span></p>
<h3><span>The Procedure</span><a class="anchor-link" id="the-procedure"></a></h3>
<p><span>In this procedure, we consider a typical PXC architecture with 3 nodes. Enabling encryption requires two rolling restarts: the first turns TLS on while still allowing plaintext connections, and the second removes that option and forces only TLS connections.</span></p>
<ol>
<li><span> Distribute the same certificate files across all nodes:</span></li>
</ol>
<p><span>Whether the certificates are auto-generated by MySQL or issued by us, every member of the cluster has to use the same key and certificate files. It&rsquo;s recommended to keep the certificates outside the data directory for security and configuration clarity, and to keep them clear of SST since a joiner wipes its data directory before a full state transfer, and only files matching the [sst] cpat pattern survive. That default covers *.pem, so the MySQL-generated names are safe, but a .crt or .key left in the data directory is deleted.</span></p>
<ol start="2">
<li><span> Update the my.cnf on every node:</span></li>
</ol>
<p><span>&ndash; pxc-encrypt-cluster-traffic=ON, this switch adds TLS for group communication, IST, and SST.</span></p>
<p><span>&ndash; socket.dynamic=YES option inside the wsrep_provider_options variable, so each node can speak both protocols while the rollout is in progress.</span></p>
<p><span>&ndash; The ssl-* variables, in case we&rsquo;re setting the certificates outside the default path, which is the data directory.</span></p>
<p><span>&ndash; An [sst] section with the encrypt=4 option and the ssl-* variables. Since pxc-encrypt-cluster-traffic enforces SST channel to encrypt=4, this is a safety measure for those that did not yet performed the change, so donor and joiner can communicate over TLS while the change has not been applied in all nodes. Even though nothing has been restarted, the SST script re-reads my.cnf on every transfer, so as soon as the file is in place the donors encrypt SST whether or not they have been restarted.</span></p>
<p><span>The my.cnf file shows the following values:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">[mysqld]
pxc-encrypt-cluster-traffic=ON
wsrep_provider_options="socket.dynamic=YES"
ssl-ca=/etc/mysql/certs/ca.pem
ssl-cert=/etc/mysql/certs/server-cert.pem
ssl-key=/etc/mysql/certs/server-key.pem

[sst]
encrypt=4
ssl-ca=/etc/mysql/certs/ca.pem
ssl-cert=/etc/mysql/certs/server-cert.pem
ssl-key=/etc/mysql/certs/server-key.pem</pre>

<ol start="3">
<li><span> Restart node #1 and node #2, one at a time:</span></li>
</ol>
<p><span>Wait for each node to rejoin and report Synced before moving to the next. A restarted node will try TLS first and then fall back.</span></p>
<p><span>You can verify the change was correctly applied by running the following query. Since the SSL settings are in the wsrep_provider_options variable, which is a long string of key/value pairs, we pull out only the ones we are interested in:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; SELECT
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; REGEXP_SUBSTR(@@wsrep_provider_options, 'socket\.dynamic = [^;]+')&nbsp; AS socket_dynamic,
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; REGEXP_SUBSTR(@@wsrep_provider_options, 'socket\.ssl = [^;]+')&nbsp; &nbsp; &nbsp; AS socket_ssl,
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; REGEXP_SUBSTR(@@wsrep_provider_options, 'socket\.ssl_ca = [^;]+') &nbsp; AS socket_ssl_ca,
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; REGEXP_SUBSTR(@@wsrep_provider_options, 'socket\.ssl_cert = [^;]+') AS socket_ssl_cert,
&nbsp;&nbsp;&nbsp;&nbsp;-&gt; REGEXP_SUBSTR(@@wsrep_provider_options, 'socket\.ssl_key = [^;]+')&nbsp; AS socket_ssl_keyG
*************************** 1. row ***************************
&nbsp;socket_dynamic: socket.dynamic = YES
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;socket_ssl: socket.ssl = YES
&nbsp;&nbsp;socket_ssl_ca: socket.ssl_ca = /etc/mysql/certs/ca.pem
socket_ssl_cert: socket.ssl_cert = /etc/mysql/certs/server-cert.pem
&nbsp;socket_ssl_key: socket.ssl_key = /etc/mysql/certs/server-key.pem
1 row in set (0.00 sec)</pre>
<p><span>The provider option socket.ssl and ssl certificates are passed down from MySQL to the provider because pxc-encrypt-cluster-traffic is on.</span></p>
<p><span>That confirms the settings reached the provider. That said, the replication traffic is not encrypted yet.</span></p>
<p><span>We can check this by using tcpdump on node #1 to listen on the replication port (4567) packets coming from node #2:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ tcpdump -i any -nn -A -s0 "tcp port 4567 and host node2" -c 2000 | grep -a table_test</pre>
<p><span>On node #2 we create the table_test that will be replicated to the other nodes:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; create table test.table_test (c1 integer primary key);
Query OK, 0 rows affected (0.10 sec)</pre>
<p><span>Since in PXC DDLs are replicated as a statement, tcpdump on node #1 shows the create table command in plaintext:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">...x..x.j.....y............................ ..E......std............test.create table test.table_test (c1 integer primary key).......</pre>

<ol start="4">
<li><span> Restart the third node with socket.dynamic=NO:</span></li>
</ol>
<p><span>Node #3 does not need the socket.dynamic option since the other two nodes are already capable of talking either TLS or plaintext.</span></p>
<p><span>We modify the my.cnf option file as below:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">[mysqld]
wsrep_provider_options="socket.dynamic=NO"</pre>
<p><span>And restart the node. This node will start and request only TLS from the other nodes, which they can respond to, since they already have all the changes in place.</span></p>
<p><span>The node will show reaching out to cluster peers over ssl://</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-09-02T02:19:03.640009Z 0 [Note] [MY-000000] [Galera] (aac049fc-a401, 'ssl://0.0.0.0:4567') connection established to 8d5f826f-8dd1 ssl://172.18.0.3:4567
2026-09-02T02:19:03.640410Z 0 [Note] [MY-000000] [Galera] (aac049fc-a401, 'ssl://0.0.0.0:4567') connection established to 565f0a92-8283 ssl://172.18.0.2:4567</pre>

<ol start="5">
<li><span> Restart node #1 and node #2, turning off the socket.dynamic option:</span></li>
</ol>
<p><span>In order to disallow the non-encrypted option and to establish a TLS connection with the other nodes, nodes #1 and #2 should be restarted.</span></p>
<p><span>Similar to the previous step, when restarting, the nodes will establish communication on TLS.</span></p>
<p><span>Node #1</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-09-02T02:20:12.565801Z 0 [Note] [MY-000000] [Galera] (d3d58399-9ea2, 'ssl://0.0.0.0:4567') connection established to aac049fc-a401 ssl://172.18.0.4:4567
2026-09-02T02:20:12.565855Z 0 [Note] [MY-000000] [Galera] (d3d58399-9ea2, 'ssl://0.0.0.0:4567') connection established to 8d5f826f-8dd1 ssl://172.18.0.3:4567</pre>
<p><span>Node #2</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-09-02T02:21:20.911318Z 0 [Note] [MY-000000] [Galera] (fc923848-a6d1, 'ssl://0.0.0.0:4567') connection established to d3d58399-9ea2 ssl://172.18.0.2:4567
2026-09-02T02:21:20.911339Z 0 [Note] [MY-000000] [Galera] (fc923848-a6d1, 'ssl://0.0.0.0:4567') connection established to aac049fc-a401 ssl://172.18.0.4:4567</pre>
<p><span>To confirm the replication traffic is encrypted, we can perform the same check as in step 3, by running tcpdump on node #1 and listening on the replication port (4567) for packets coming from node #2:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ tcpdump -i any -nn -A -s0 "tcp port 4567 and host node2" -c 2000 | grep -a table_test</pre>
<p><span>On node #2 we drop the table_test previously created:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; drop table test.table_test;
Query OK, 0 rows affected (0.04 sec)</pre>
<p><span>tcpdump does not show a matching pattern since the replication traffic is now encrypted.</span></p>
<h3><span>Disabling TLS</span><a class="anchor-link" id="disabling-tls"></a></h3>
<p><span>We can use the same mechanism to disable TLS for whatever reason. As with enabling it, this takes two rolling restarts. Starting from a cluster where every node has pxc-encrypt-cluster-traffic=ON we perform the following steps:</span></p>
<p><span>&ndash; Restart nodes #1 and #2 with socket.dynamic=YES, keeping pxc-encrypt-cluster-traffic=ON, so they accept plaintext connections again.&nbsp;</span></p>
<p><span>&ndash; Restart node #3 with pxc-encrypt-cluster-traffic=OFF, it will come up in plaintext only, and the other two nodes will accept it because they are back to accepting both protocols.</span></p>
<p><span>&ndash; Restart nodes #1 and #2 with pxc-encrypt-cluster-traffic=OFF and without socket.dynamic, which leaves the whole cluster in plaintext.</span></p>
<p><span>&ndash; Finally, remove from the sst section the encrypt and SSL related variables.</span></p>
<h3><span>Conclusion</span><a class="anchor-link" id="conclusion"></a></h3>
<p><span>Enabling TLS for inter-node traffic can be done without stopping the whole cluster. The socket.dynamic option lets a node accept both encrypted and plaintext connections during the transition, so the change can be rolled out one node at a time. Please note we&rsquo;ll need at least two rolling restarts to achieve that the cluster fully communicates via TLS.</span></p>
<p>The post <a href="https://www.percona.com/blog/enabling-tls-in-pxc-without-downtime/">Enabling TLS in PXC without Downtime</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/enabling-tls-in-pxc-without-downtime/">Enabling TLS in PXC without Downtime</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Operator for PostgreSQL 3.1.0: Transparent Data Encryption, Logical Replicas, and Persistent Logging</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-operator-for-postgresql-3-1-0-transparent-data-encryption-logical-replicas-persistent-logging/" />
      <id>https://www.percona.com/blog/percona-operator-for-postgresql-3-1-0-transparent-data-encryption-logical-replicas-persistent-logging/</id>
      <updated>2026-09-09T16:08:31+03:00</updated>
      <author><name>Slava Sarzhan</name></author>
      <summary type="html"><![CDATA[<p>Percona Operator for PostgreSQL 3.1.0 takes on three things that decide whether a PostgreSQL platform passes review: is the data encrypted at rest, can it serve reads without straining the primary, and are the logs there when you need them. This release answers all three inside the custom resource, so none of them is a … Continued<br />
The post Percona Operator for PostgreSQL 3.1.0: Transparent Data Encryption, Logical Replicas, and Persistent Logging appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-operator-for-postgresql-3-1-0-transparent-data-encryption-logical-replicas-persistent-logging/">Percona Operator for PostgreSQL 3.1.0: Transparent Data Encryption, Logical Replicas, and Persistent Logging</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" class="alignnone wp-image-53160 size-full" src="https://www.percona.com/wp-content/uploads/2026/09/Cover-1000-x-420-1.png" alt="" width="1001" height="420"></p>
<p><b>Percona Operator for PostgreSQL 3.1.0</b><span> takes on three things that decide whether a PostgreSQL platform passes review: is the data encrypted at rest, can it serve reads without straining the primary, and are the logs there when you need them. This release answers all three inside the custom resource, so none of them is a bolt-on you maintain yourself.</span></p>
<p><span>The three headline features are </span><b>transparent data encryption with </b><b>pg_tde</b><span>, </span><b>logical replicas</b><span>, and </span><b>persistent logging</b><span>. </span><span>pg_tde</span><span> encrypts your data on disk, including the write-ahead log. Logical replicas add a read-only copy inside the cluster for reporting and analytics. Persistent logging keeps PostgreSQL and pgBackRest logs across Pod restarts.</span></p>
<p><span>The operator is open source and runs on any CNCF-certified Kubernetes distribution. This release also widens where it runs, adding official Rancher Kubernetes Engine (RKE2) support and full ARM64 images. Much of what shipped here comes from requests on </span><a href="https://forums.percona.com/"><span>forums.percona.com</span></a><span> and the public issue tracker.</span></p>
<p><span>In this post, you&rsquo;ll learn about:</span></p>
<ul>
<li><span>Transparent data encryption with pg_tde</span></li>
<li><span>Logical replicas for read-only workloads</span></li>
<li><span>Persistent logging for PostgreSQL and pgBackRest</span></li>
<li><span>Other improvements worth knowing about<br>
</span></li>
</ul>
<p>&nbsp;</p>
<h2><b>Transparent data encryption with pg_tde</b><a class="anchor-link" id="transparent-data-encryption-with-pg_tde"></a></h2>
<p><span>Encryption at rest is usually the line item that blocks a database from going into a regulated environment. Storage-level encryption from the cloud provider covers the disk, but it does not protect a copied volume, a leaked backup, or a stray WAL segment, and auditors increasingly want encryption the database itself controls. This release adds transparent data encryption through </span><span>pg_tde</span><span>, Percona&rsquo;s open source TDE extension for PostgreSQL.</span></p>
<p>&nbsp;</p>
<h3><b>Why it matters</b><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span>With </span><span>pg_tde</span><span>, the data in tables, indexes, temporary tables, and the write-ahead log stays encrypted on disk, and PostgreSQL decrypts it only in memory for a session that holds the key. That closes the gaps storage encryption leaves open: a snapshot of the volume, a backup shipped to object storage, or a WAL file replicated off-node is ciphertext without the key. Because the key lives in an external provider rather than next to the data, you separate who runs the database from who controls the keys. In practice this is what lets a team bring PostgreSQL on Kubernetes into scope for a standard like PCI DSS or HIPAA without moving the workload off the platform: the encryption the auditor asks about lives in the database, and key custody lives in Vault under a different team&rsquo;s control.</span></p>
<p>&nbsp;</p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span>The operator wires </span><span>pg_tde</span><span> to HashiCorp Vault as the key provider. You enable the extension in the custom resource and point it at your Vault instance and a token Secret. The operator handles loading the extension and configuring the key set on the PostgreSQL instances. WAL encryption is a separate switch, so you can encrypt table data and the write-ahead log together. </span><span>pg_tde</span><span> uses a two-tier key model: a principal key in Vault wraps the internal data keys, so rotating the principal key is a key-management operation in Vault rather than a full re-encryption of the database.</span></p>
<h3><b><br>
Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>

<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: pgv2.percona.com/v2
kind: PerconaPGCluster
metadata:
  name: cluster1
spec:
  extensions:
    pg_tde:
      enabled: true
      walEncryption: true
      vault:
        host: https://vault-service:8200
        mountPath: tde
        tokenSecret:
          name: pg-tde-vault-secret
          key: token
        caSecret:
          name: pg-tde-vault-secret
          key: ca.crt</pre>
<p>&nbsp;</p>
<p><em><span>enabled: true</span></em><span> loads </span><em><span>pg_tde</span></em><span> and turns on encryption for the cluster, and </span><em><span>walEncryption: true</span></em><span> extends it to the write-ahead log. The </span><span>vault</span><span> block points at your key provider: </span><em><span>host</span></em><span> and </span><em><span>mountPath</span></em><span> locate the secrets engine, </span><em><span>tokenSecret</span></em><span> holds the Vault token, and </span><em><span>caSecret</span></em><span> carries the CA certificate so the operator trusts the Vault endpoint. Keep the Vault token and CA in Kubernetes Secrets, not in the manifest.</span></p>
<p>&nbsp;</p>
<blockquote>
<p><b>Note:</b><span> pg_tde in 3.1.0 is available for PostgreSQL 17 and 18. Encryption applies to data written after you enable it, so plan enablement as part of provisioning a cluster rather than as a switch on a full production database.</span></p>
</blockquote>
<h2><span><br>
<b>Logical replicas for read-only workloads</b></span><a class="anchor-link" id="logical-replicas-for-read-only-workloads"></a></h2>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-53161 size-medium_large" src="https://www.percona.com/wp-content/uploads/2026/09/logical-replica-topology-768x404.png" alt="" width="768" height="404"></p>
<p>&nbsp;</p>
<p><span>A PostgreSQL cluster under the operator is a primary with streaming physical replicas that Patroni manages for high availability. Those replicas exist to take over on failover, not to be a stable place to point a reporting tool, because their role can change at any time. Teams that want a durable read endpoint for analytics have had to run a second cluster or wire up replication by hand. This release adds a logical replica you declare inside the same cluster.</span></p>
<p><span>&nbsp;</span></p>
<h3><b>Why it matters</b><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span>Reporting and analytics queries have a different shape than transactional traffic: they scan more, run longer, and arrive in bursts when a dashboard refreshes or a nightly job starts. Pointed at the primary, they compete with the writes that keep the application responsive. A logical replica gives that traffic its own copy and its own compute, so a heavy analytics query slows down a chart, not a checkout. Because the endpoint is stable, you set the reporting tool&rsquo;s connection string once and leave it.</span></p>
<h3><b><br>
How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span>A logical replica is a read-only copy with its own volume and its own Service, seeded from a pgBackRest backup and kept current through logical replication. Patroni does not manage it, so it never gets promoted and its endpoint stays stable: a reporting query or a dashboard can point at it and stay pointed at it. You can target specific databases or replicate all of them, and size the replica independently of the primary.</span></p>
<h3><span><br>
<b>Wiring it up</b></span><a class="anchor-link" id="wiring-it-up"></a></h3>

<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  logicalReplicas:
  - name: analytics
    databases: []  # empty = all non-template databases except "postgres"
    bootstrapMethod: pgbackrest
    dataVolumeClaimSpec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
    resources:
      limits:
        cpu: 2.0
        memory: 4Gi
    expose:
      type: LoadBalancer</pre>
<p><span><br>
<em>name</em></span><span> becomes the replica&rsquo;s identity and the basis for its Service. </span><span>databases</span><span> selects what to replicate, where an empty list means every non-template <em>database</em> except </span><em><span>postgres</span></em><span>. </span><em>bootstrapMethod: pgbackrest</em><span> seeds the replica from a backup rather than from the live primary, which keeps the initial sync off the primary&rsquo;s back. </span><em><span>dataVolumeClaimSpec</span></em><span> and </span><span>resources</span><span> size it for the read workload, and </span><span>expose</span><span> publishes the read endpoint.</span></p>
<p>&nbsp;</p>
<blockquote>
<p><b>Note:</b><span> Logical replicas are a tech preview in 3.1.0 and require PostgreSQL 17 or later. Logical replication does not copy schema changes automatically, so treat DDL on the primary as something you coordinate with the replica.</span></p>
</blockquote>
<p><span>&nbsp;</span></p>
<h2><b>Persistent logging for PostgreSQL and pgBackRest</b><a class="anchor-link" id="persistent-logging-for-postgresql-and-pgbackrest"></a></h2>
<p><span>Logs matter most right after something goes wrong, which is exactly when a Pod is most likely to have restarted and taken its logs with it. When PostgreSQL logs only to a container&rsquo;s stdout, a crash-loop or a reschedule erases the evidence you need to explain it. The classic case is a crash-looping instance: by the time you exec into a fresh Pod, the stdout from the crash is gone, but an on-disk log still holds the panic and the queries around it. This release keeps PostgreSQL and pgBackRest logs on the instance data volume, so they survive Pod restarts.</span></p>
<p><span>&nbsp;</span></p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span>The operator runs a Fluent Bit log collector as a sidecar that reads the on-disk logs and emits them as structured JSON lines. Because the logs live on the data volume, they persist across restarts and rescheduling. From there you can forward them off-cluster: the collector can ship to S3 or over OpenTelemetry to whatever aggregation stack you already run, configured through the custom resource. The same on-disk logs that help you debug an incident become the audit trail your security team retains, without a second logging agent to install.</span></p>
<h3><span><br>
<b>Wiring it up</b><br>
</span><a class="anchor-link" id="wiring-it-up"></a></h3>

<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  logcollector:
    enabled: true
    image: docker.io/perconalab/fluentbit:main-logcollector
#    configuration: |
#      pipeline:
#        filters:
#          - name: record_modifier
#            match: "*"
#            record:
#              - cluster_name cluster1</pre>
<p>&nbsp;</p>
<p><em><span>enabled: true</span></em><span> turns on the Fluent Bit collector, and </span><em><span>image</span></em><span> pins the collector build. The commented </span><em><span>configuration</span></em><span> block is a Fluent Bit pipeline you can supply to filter, enrich, or route logs: the example tags every record with a </span><em><span>cluster_name</span></em><span>, and the same mechanism adds an output to forward logs to S3 or an OpenTelemetry endpoint. You can also tune log rotation so retention matches your operational windows and compliance rules.</span></p>
<p>&nbsp;</p>
<blockquote>
<p><b>Note:</b><span> Persistent logs consume space on the instance data volume. Set a rotation policy that fits the volume so logs do not compete with the database for storage.</span></p>
</blockquote>
<p><span>&nbsp;</span></p>
<h2><b>Other improvements</b><a class="anchor-link" id="other-improvements"></a></h2>
<p><span>Beyond the three headline features, 3.1.0 ships a set of enhancements that smooth day-two operations:</span></p>
<ul>
<li><b>Community PostgreSQL images and custom registries</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-1056"><span>K8SPG-1056</span></a><span>): run Percona Distribution for PostgreSQL, community PostgreSQL, or your own images by setting </span><span>spec.image</span><span>, </span><span>proxy.pgBouncer.image</span><span>, and </span><span>spec.backups.pgbackrest.image</span><span>.</span></li>
<li><b>Auto-growable pgBackRest disks</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-691"><span>K8SPG-691</span></a><span>): let backup repository volumes grow to a limit instead of filling up and failing a backup.</span></li>
<li><b>Pause and resume pgBouncer</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-1115"><span>K8SPG-1115</span></a><span>): set </span><span>proxy.pgBouncer.paused</span><span> to hold client traffic without dropping application connections.</span></li>
<li><b>mTLS for pgBouncer</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-952"><span>K8SPG-952</span></a><span>): extend the pgBouncer trust bundle with your external CA through </span><span>proxy.pgBouncer.additionalTrustedCAs</span><span> while the operator keeps rotating cluster TLS.</span></li>
<li><b>cert-manager Issuer and TLS policy</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-951"><span>K8SPG-951</span></a><span>, </span><a href="https://perconadev.atlassian.net/browse/K8SPG-1045"><span>K8SPG-1045</span></a><span>): point the operator at your own Issuer, and use </span><span>certManagementPolicy</span><span> to decide who owns certificate lifecycle.</span></li>
<li><b>Extra volume mounts</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-440"><span>K8SPG-440</span></a><span>): mount extra ConfigMap, Secret, PVC, or emptyDir volumes into the PostgreSQL container through </span><span>instances.extraVolumes</span><span>.</span></li>
<li><b>pg_cron</b><b> and </b><b>set_user</b><b> are now built-in</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-1040"><span>K8SPG-1040</span></a><span>): enable them through </span><span>extensions</span><span> without supplying a custom build.</span></li>
<li><b>PostgreSQL 19 tech preview</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-1051"><span>K8SPG-1051</span></a><span>) and </span><b>full ARM64 support</b><span> (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-881"><span>K8SPG-881</span></a><span>): evaluate the next major early, and run the operator natively on ARM.</span></li>
</ul>
<p><span>One deprecation to plan for: this release removes PMM2 support (</span><a href="https://perconadev.atlassian.net/browse/K8SPG-944"><span>K8SPG-944</span></a><span>), so move monitoring to </span><a href="https://docs.percona.com/percona-monitoring-and-management/"><span>PMM3</span></a><span>. The </span><span>extensions.builtin</span><span> field is deprecated in favor of </span><span>extensions..enabled</span><span>; migrate before 3.4.0.</span></p>
<p>&nbsp;</p>
<h2><b>Conclusion</b><a class="anchor-link" id="conclusion"></a></h2>
<p><span>Percona Operator for PostgreSQL 3.1.0 tightens the parts of a PostgreSQL platform that reviews and on-call rotations care about most: </span><span>pg_tde</span><span> encrypts data at rest under keys you control, logical replicas give analytics a stable read endpoint without a second cluster, and persistent logging keeps the evidence when a Pod restarts. With RKE2 and full ARM64 support, more of that runs on the platforms teams actually use. If there is a workflow you still script around the operator, tell us on the forum, since that is where releases like this one come from.</span></p>
<h2><b><br>
Try Percona Operator for PostgreSQL 3.1.0</b><a class="anchor-link" id="try-percona-operator-for-postgresql-3-1-0"></a></h2>
<ul>
<li><b>Release notes</b><span>: </span><a href="https://docs.percona.com/percona-operator-for-postgresql/ReleaseNotes/Kubernetes-Operator-for-PostgreSQL-RN3.1.0.html"><span>Percona Operator for PostgreSQL 3.1.0 Release Notes</span></a></li>
<li><b>Documentation</b><span>: </span><a href="https://docs.percona.com/percona-operator-for-postgresql/latest/"><span>Percona Operator for PostgreSQL docs</span></a></li>
<li><b>GitHub</b><span>: </span><a href="https://github.com/percona/percona-postgresql-operator"><span>percona/percona-postgresql-operator</span></a></li>
<li><b>Community Forum</b><span>: </span><a href="https://forums.percona.com/"><span>forums.percona.com</span></a><span>: share your feedback, ask questions, or report issues</span></li>
</ul>
<p>&nbsp;</p>
<p><span>&nbsp;</span></p>
<p>The post <a href="https://www.percona.com/blog/percona-operator-for-postgresql-3-1-0-transparent-data-encryption-logical-replicas-persistent-logging/">Percona Operator for PostgreSQL 3.1.0: Transparent Data Encryption, Logical Replicas, and Persistent Logging</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-operator-for-postgresql-3-1-0-transparent-data-encryption-logical-replicas-persistent-logging/">Percona Operator for PostgreSQL 3.1.0: Transparent Data Encryption, Logical Replicas, and Persistent Logging</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>SQL Server 2025 Expensive Queries: 7 Proven Ways to Find and Fix Them by Latency and Resource Cost</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/sql-server-expensive-queries/" />
      <id>https://minervadb.com/sql-server-expensive-queries/</id>
      <updated>2026-09-09T15:20:44+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>\"Expensive queries\" is a phrase that hides two different problems. One is response time: a statement the application waits too long for. The other is resource efficiency: a statement that burns CPU, logical reads and [...]</p>
<p><a href="https://minervadb.com/sql-server-expensive-queries/">SQL Server 2025 Expensive Queries: 7 Proven Ways to Find and Fix Them by Latency and Resource Cost</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>&ldquo;Expensive queries&rdquo; is a phrase that hides two different problems. One is response time: a statement the application waits too long for. The other is resource efficiency: a statement that burns CPU, logical reads and tempdb out of proportion to the rows it returns, whether or not anyone is waiting on it. On SQL Server 2025 the second kind is the one that quietly sets your core count, and the first kind is the one that generates the ticket. They need different evidence, and often different fixes, and the mistake I see most is tuning one while measuring the other.</p>
<p>This post is the method I use for expensive queries on SQL Server 2025 (17.x, CU8 at the time of writing, database compatibility level 170) to find them both ways, read what the plan is really doing, and fix them with the index or the rewrite the evidence points at. Where SQL Server 2022 behaves differently I say so inline. Every query here runs against Query Store or the DMVs and is safe to run on a production instance; the sample outputs are illustrative and labelled as such, because the numbers that matter are yours.</p>
<h2>Expensive queries are two problems, and the axis you pick decides the fix<a class="anchor-link" id="expensive-queries-are-two-problems-and-the-axis-you-pick-decides-the-fix"></a></h2>
<p>For expensive queries, response time is what the user experiences: duration, including every wait the session accumulated while blocked, while waiting for a memory grant, or while the client drained a large result set. Resource efficiency is what the engine spent: CPU time, logical reads, tempdb pages and worker threads, per execution and per row returned. A statement can be slow and cheap, when it spends its life in LCK_M_X or ASYNC_NETWORK_IO. A statement can be fast and ruinously expensive, when a two-millisecond lookup runs five thousand times a second with two hundred logical reads per call.</p>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5NjAgNTIwIiB3aWR0aD0iOTYwIiBoZWlnaHQ9IjUyMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InQ5NDIwOCBkOTQyMDgiPjx0aXRsZSBpZD0idDk0MjA4Ij5FeHBlbnNpdmUgcXVlcmllcyBjbGFzc2lmaWVkIGJ5IHJlc3BvbnNlIHRpbWUgYW5kIHJlc291cmNlIGVmZmljaWVuY3k8L3RpdGxlPjxkZXNjIGlkPSJkOTQyMDgiPkEgdHdvLWJ5LXR3bzogcmVzcG9uc2UgdGltZSBvbiB0aGUgdmVydGljYWwgYXhpcywgcmVzb3VyY2UgY29zdCBwZXIgcm93IG9uIHRoZSBob3Jpem9udGFsLCB3aXRoIHRoZSBldmlkZW5jZSBzb3VyY2UgYW5kIGZpeCBmYW1pbHkgZm9yIGVhY2ggcXVhZHJhbnQuPC9kZXNjPjxkZWZzPjxtYXJrZXIgaWQ9ImFyciIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4LjUiIHJlZlk9IjUiIG1hcmtlcldpZHRoPSI3IiBtYXJrZXJIZWlnaHQ9IjciIG9yaWVudD0iYXV0by1zdGFydC1yZXZlcnNlIiBtYXJrZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxsaW5lIHgxPSIxLjUiIHkxPSIxLjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMzZDRmNjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PGxpbmUgeDE9IjEuNSIgeTE9IjguNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L21hcmtlcj48bWFya2VyIGlkPSJhcnJiIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjguNSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGxpbmUgeDE9IjEuNSIgeTE9IjEuNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bGluZSB4MT0iMS41IiB5MT0iOC41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz48cmVjdCB3aWR0aD0iOTYwIiBoZWlnaHQ9IjUyMCIgZmlsbD0iI2ZmZiIvPjx0ZXh0IHg9IjQ4MC4wIiB5PSIyOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzBhMWEyZiI+RXhwZW5zaXZlIHF1ZXJpZXMgYXJlIHR3byBkaWZmZXJlbnQgcHJvYmxlbXM6IHJlc3BvbnNlIHRpbWUgYW5kIHJlc291cmNlIGVmZmljaWVuY3k8L3RleHQ+PGxpbmUgeDE9IjEyMCIgeTE9IjQ3MCIgeDI9Ijg2MCIgeTI9IjQ3MCIgc3Ryb2tlPSIjM2Q0ZjYyIiBzdHJva2Utd2lkdGg9IjEuNSIgbWFya2VyLWVuZD0idXJsKCNhcnIpIi8+PGxpbmUgeDE9IjEyMCIgeTE9IjQ3MCIgeDI9IjEyMCIgeTI9IjcwIiBzdHJva2U9IiMzZDRmNjIiIHN0cm9rZS13aWR0aD0iMS41IiBtYXJrZXItZW5kPSJ1cmwoI2FycikiLz48dGV4dCB4PSI0OTAuMCIgeT0iNDk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMzZDRmNjIiPlJlc291cmNlIGNvc3QgcGVyIHJvdyByZXR1cm5lZCAoQ1BVIG1zLCBsb2dpY2FsIHJlYWRzLCB0ZW1wZGIpIOKAlCBoaWdoZXIgaXMgd29yc2U8L3RleHQ+PHRleHQgeD0iOTgiIHk9IjI3MC4wIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMzZDRmNjIiIHRyYW5zZm9ybT0icm90YXRlKC05MCA5OCAyNzAuMCkiPlJlc3BvbnNlIHRpbWUgcDk1IChkdXJhdGlvbiwgd2FpdHMpIOKAlCBoaWdoZXIgaXMgd29yc2U8L3RleHQ+PGxpbmUgeDE9IjQ5MC4wIiB5MT0iNDcwIiB4Mj0iNDkwLjAiIHkyPSI3MCIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2UtZGFzaGFycmF5PSI0IDQiLz48bGluZSB4MT0iMTIwIiB5MT0iMjcwLjAiIHgyPSI4NjAiIHkyPSIyNzAuMCIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2UtZGFzaGFycmF5PSI0IDQiLz48cmVjdCB4PSIxNDAiIHk9IjkwIiB3aWR0aD0iMzIwIiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNlZmY0ZmYiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMzAwLjAiIHk9IjEyMy40NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPlNsb3cgYnV0IGNoZWFwPC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSIxMzguMzI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj53YWl0cyBkb21pbmF0ZTogYmxvY2tpbmcsIEFTWU5DX05FVFdPUktfSU8sPC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSIxNTMuMTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5SRVNPVVJDRV9TRU1BUEhPUkUsIGxvZyBmbHVzaDwvdGV4dD48dGV4dCB4PSIzMDAuMCIgeT0iMTY4LjAyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+PC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSIxODIuODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5ldmlkZW5jZTogcXVlcnlfc3RvcmVfd2FpdF9zdGF0cyw8L3RleHQ+PHRleHQgeD0iMzAwLjAiIHk9IjE5Ny43MjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPnN5cy5kbV9vc193YWl0aW5nX3Rhc2tzPC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSIyMTIuNTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5maXg6IGNvbmN1cnJlbmN5LCBpc29sYXRpb24sIGFwcCBmZXRjaCBwYXR0ZXJuPC90ZXh0PjxyZWN0IHg9IjUyMC4wIiB5PSI5MCIgd2lkdGg9IjMyMCIgaGVpZ2h0PSIxNTAiIHJ4PSIyIiBmaWxsPSIjMGExYTJmIiBzdHJva2U9IiMwYTFhMmYiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjY4MC4wIiB5PSIxMjMuNDc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjZmZmIj5TbG93IGFuZCBleHBlbnNpdmU8L3RleHQ+PHRleHQgeD0iNjgwLjAiIHk9IjEzOC4zMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiNmZmYiPnNjYW5zLCBzcGlsbHMsIGxvb2t1cHMgb24gaG90IHBhdGhzPC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIxNTMuMTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj50aGUgY2xhc3NpYyB0dW5pbmcgdGFyZ2V0PC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIxNjguMDI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj48L3RleHQ+PHRleHQgeD0iNjgwLjAiIHk9IjE4Mi44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiNmZmYiPmV2aWRlbmNlOiBydW50aW1lX3N0YXRzIGR1cmF0aW9uICsgY3B1PC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIxOTcuNzI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj4rIGxvZ2ljYWxfaW9fcmVhZHMgcGVyIGV4ZWN1dGlvbjwvdGV4dD48dGV4dCB4PSI2ODAuMCIgeT0iMjEyLjU3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iI2ZmZiI+Zml4OiBwbGFuIHNoYXBlLCB0aGVuIHRoZSBpbmRleDwvdGV4dD48cmVjdCB4PSIxNDAiIHk9IjI5MC4wIiB3aWR0aD0iMzIwIiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMzAwLjAiIHk9IjMyMy40NzQ5OTk5OTk5OTk5NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+RmFzdCBhbmQgY2hlYXA8L3RleHQ+PHRleHQgeD0iMzAwLjAiIHk9IjMzOC4zMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmxlYXZlIGFsb25lPC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSIzNTMuMTc0OTk5OTk5OTk5OTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPjwvdGV4dD48dGV4dCB4PSIzMDAuMCIgeT0iMzY4LjAyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+YnV0IHdhdGNoIGV4ZWN1dGlvbnMgcGVyIHNlY29uZDo8L3RleHQ+PHRleHQgeD0iMzAwLjAiIHk9IjM4Mi44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmEgMiBtcyBxdWVyeSBhdCA1LDAwMC9zIGlzIHRoZTwvdGV4dD48dGV4dCB4PSIzMDAuMCIgeT0iMzk3LjcyNDk5OTk5OTk5OTk3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5iaWdnZXN0IENQVSBjb25zdW1lciBvbiB0aGUgYm94PC90ZXh0Pjx0ZXh0IHg9IjMwMC4wIiB5PSI0MTIuNTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5ldmlkZW5jZTogY291bnRfZXhlY3V0aW9ucyB4IGF2ZyBjcHU8L3RleHQ+PHJlY3QgeD0iNTIwLjAiIHk9IjI5MC4wIiB3aWR0aD0iMzIwIiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNlZmY0ZmYiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iNjgwLjAiIHk9IjMyMy40NzQ5OTk5OTk5OTk5NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+RmFzdCBidXQgZXhwZW5zaXZlPC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIzMzguMzI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj50aGUgcXVpZXQgYnVkZ2V0IGtpbGxlcjwvdGV4dD48dGV4dCB4PSI2ODAuMCIgeT0iMzUzLjE3NDk5OTk5OTk5OTk1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5oaWdoIGxvZ2ljYWwgcmVhZHMgcGVyIHJvdywgbG93IGR1cmF0aW9uPC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIzNjguMDI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj48L3RleHQ+PHRleHQgeD0iNjgwLjAiIHk9IjM4Mi44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmV2aWRlbmNlOiB0b3RhbCBjcHUgYW5kIHRvdGFsIHJlYWRzPC90ZXh0Pjx0ZXh0IHg9IjY4MC4wIiB5PSIzOTcuNzI0OTk5OTk5OTk5OTciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPnJhbmtlZCBvdmVyIGFuIGludGVydmFsLCBub3QgcGVyIGNhbGw8L3RleHQ+PHRleHQgeD0iNjgwLjAiIHk9IjQxMi41NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmZpeDogY292ZXJpbmcgaW5kZXgsIHByZWRpY2F0ZSByZXdyaXRlPC90ZXh0Pjwvc3ZnPg==" alt="Expensive queries in SQL Server classified into four quadrants by response time and resource cost per row, with the evidence source and fix family for each quadrant" width="960" height="520"><br><em>Figure 1. Expensive queries on two axes. The top-right quadrant gets all the attention; the bottom-right quadrant is where the licence cores go.</em></p>
<p>The reason the distinction matters for expensive queries in practice is that the fixes do not overlap much. Waits are fixed with concurrency design, isolation level, batch sizing and the application&rsquo;s fetch pattern. Resource cost is fixed with plan shape: predicates, statistics, indexes and, occasionally, a hint. If you rank by duration and the top entry is a query blocked behind a long transaction, adding an index to it changes nothing except the maintenance cost of every insert on that table.</p>
<h2>Query Store is the evidence chain, and on SQL Server 2025 it is on everywhere<a class="anchor-link" id="query-store-is-the-evidence-chain-and-on-sql-server-2025-it-is-on-everywhere"></a></h2>
<p>Query Store has been the right place to find expensive queries since SQL Server 2016, but two things changed in 2025 that make it the only place I look first. It is enabled by default for new databases, and it now runs on readable secondaries by default, so an Always On read-scale workload is no longer invisible. The runtime statistics are aggregated per plan per interval, which is exactly the granularity that separates response time from resource cost: duration, CPU, logical reads, rowcount, tempdb and DOP all sit in the same row.</p>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDQzMCIgd2lkdGg9IjEwMDAiIGhlaWdodD0iNDMwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idDQ0OTExIGQ0NDkxMSI+PHRpdGxlIGlkPSJ0NDQ5MTEiPlF1ZXJ5IFN0b3JlIHZpZXdzIGNoYWluZWQgaW50byBmb3VyIHJhbmtpbmdzIG9mIGV4cGVuc2l2ZSBxdWVyaWVzPC90aXRsZT48ZGVzYyBpZD0iZDQ0OTExIj5Gb3VyIFF1ZXJ5IFN0b3JlIHZpZXdzIGpvaW5lZCBsZWZ0IHRvIHJpZ2h0LCB0aGVuIGZvdXIgcmFua2luZyBmb3JtdWxhczogcmVzcG9uc2UgdGltZSwgQ1BVLCBJL08gZWZmaWNpZW5jeSBhbmQgdmFyaWFuY2UsIGVhY2ggcmVzb2x2aW5nIHRvIGEgcXVlcnlfaWQgYW5kIHBsYW5faWQuPC9kZXNjPjxkZWZzPjxtYXJrZXIgaWQ9ImFyciIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4LjUiIHJlZlk9IjUiIG1hcmtlcldpZHRoPSI3IiBtYXJrZXJIZWlnaHQ9IjciIG9yaWVudD0iYXV0by1zdGFydC1yZXZlcnNlIiBtYXJrZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxsaW5lIHgxPSIxLjUiIHkxPSIxLjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMzZDRmNjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PGxpbmUgeDE9IjEuNSIgeTE9IjguNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L21hcmtlcj48bWFya2VyIGlkPSJhcnJiIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjguNSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGxpbmUgeDE9IjEuNSIgeTE9IjEuNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bGluZSB4MT0iMS41IiB5MT0iOC41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvbWFya2VyPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwMCIgaGVpZ2h0PSI0MzAiIGZpbGw9IiNmZmYiLz48dGV4dCB4PSI1MDAuMCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwYTFhMmYiPlF1ZXJ5IFN0b3JlIGFzIHRoZSBldmlkZW5jZSBjaGFpbiBmb3IgZXhwZW5zaXZlIHF1ZXJpZXMgb24gU1FMIFNlcnZlciAyMDI1PC90ZXh0PjxyZWN0IHg9IjIwIiB5PSI3MCIgd2lkdGg9IjIyNCIgaGVpZ2h0PSIxMjAiIHJ4PSIyIiBmaWxsPSIjZmZmIiBzdHJva2U9IiNjOWQzZTAiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjEzMi4wIiB5PSIxMDMuMzI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5zeXMucXVlcnlfc3RvcmVfcXVlcnk8L3RleHQ+PHRleHQgeD0iMTMyLjAiIHk9IjExOC4xNzUwMDAwMDAwMDAwMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+KyBxdWVyeV90ZXh0PC90ZXh0Pjx0ZXh0IHg9IjEzMi4wIiB5PSIxMzMuMDI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5xdWVyeV9pZCwgcXVlcnlfaGFzaCw8L3RleHQ+PHRleHQgeD0iMTMyLjAiIHk9IjE0Ny44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPnBhcmFtZXRlcml6YXRpb24sPC90ZXh0Pjx0ZXh0IHg9IjEzMi4wIiB5PSIxNjIuNzI1MDAwMDAwMDAwMDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmxhc3RfY29tcGlsZV9iYXRjaDwvdGV4dD48bGluZSB4MT0iMjQ0IiB5MT0iMTMwIiB4Mj0iMjY2IiB5Mj0iMTMwIiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMS40IiBtYXJrZXItZW5kPSJ1cmwoI2FycmIpIi8+PHJlY3QgeD0iMjY2IiB5PSI3MCIgd2lkdGg9IjIyNCIgaGVpZ2h0PSIxMjAiIHJ4PSIyIiBmaWxsPSIjZmZmIiBzdHJva2U9IiNjOWQzZTAiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjM3OC4wIiB5PSIxMDMuMzI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5zeXMucXVlcnlfc3RvcmVfcGxhbjwvdGV4dD48dGV4dCB4PSIzNzguMCIgeT0iMTE4LjE3NTAwMDAwMDAwMDAxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj48L3RleHQ+PHRleHQgeD0iMzc4LjAiIHk9IjEzMy4wMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPnBsYW5faWQsIGlzX2ZvcmNlZF9wbGFuLDwvdGV4dD48dGV4dCB4PSIzNzguMCIgeT0iMTQ3Ljg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+Y29tcGF0aWJpbGl0eV9sZXZlbCw8L3RleHQ+PHRleHQgeD0iMzc4LjAiIHk9IjE2Mi43MjUwMDAwMDAwMDAwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+cXVlcnlfcGxhbiBYTUw8L3RleHQ+PGxpbmUgeDE9IjQ5MCIgeTE9IjEzMCIgeDI9IjUxMiIgeTI9IjEzMCIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEuNCIgbWFya2VyLWVuZD0idXJsKCNhcnJiKSIvPjxyZWN0IHg9IjUxMiIgeT0iNzAiIHdpZHRoPSIyMjQiIGhlaWdodD0iMTIwIiByeD0iMiIgZmlsbD0iIzBhMWEyZiIgc3Ryb2tlPSIjMGExYTJmIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI2MjQuMCIgeT0iMTAzLjMyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2ZmZiI+cXVlcnlfc3RvcmVfcnVudGltZV9zdGF0czwvdGV4dD48dGV4dCB4PSI2MjQuMCIgeT0iMTE4LjE3NTAwMDAwMDAwMDAxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj5wZXIgaW50ZXJ2YWw8L3RleHQ+PHRleHQgeD0iNjI0LjAiIHk9IjEzMy4wMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiNmZmYiPmF2Zy9tYXgvc3RkZXYgZHVyYXRpb24sPC90ZXh0Pjx0ZXh0IHg9IjYyNC4wIiB5PSIxNDcuODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj5jcHVfdGltZSwgbG9naWNhbF9pb19yZWFkcyw8L3RleHQ+PHRleHQgeD0iNjI0LjAiIHk9IjE2Mi43MjUwMDAwMDAwMDAwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iI2ZmZiI+cm93Y291bnQsIHRlbXBkYiwgRE9QPC90ZXh0PjxsaW5lIHgxPSI3MzYiIHkxPSIxMzAiIHgyPSI3NTgiIHkyPSIxMzAiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyYikiLz48cmVjdCB4PSI3NTgiIHk9IjcwIiB3aWR0aD0iMjI0IiBoZWlnaHQ9IjEyMCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iODcwLjAiIHk9IjEwMy4zMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPnF1ZXJ5X3N0b3JlX3dhaXRfc3RhdHM8L3RleHQ+PHRleHQgeD0iODcwLjAiIHk9IjExOC4xNzUwMDAwMDAwMDAwMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+cGVyIHBsYW4gYW5kIGludGVydmFsPC90ZXh0Pjx0ZXh0IHg9Ijg3MC4wIiB5PSIxMzMuMDI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj53YWl0X2NhdGVnb3J5OjwvdGV4dD48dGV4dCB4PSI4NzAuMCIgeT0iMTQ3Ljg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+Q1BVLCBMb2NrLCBMYXRjaCw8L3RleHQ+PHRleHQgeD0iODcwLjAiIHk9IjE2Mi43MjUwMDAwMDAwMDAwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+QnVmZmVyIElPLCBOZXR3b3JrIElPLCBNZW1vcnk8L3RleHQ+PHRleHQgeD0iNTAwLjAiIHk9IjIyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzBhMWEyZiI+UmFuayBvbiB0aGUgcXVlc3Rpb24geW91IGFyZSBhY3R1YWxseSBhbnN3ZXJpbmc8L3RleHQ+PHJlY3QgeD0iMjAiIHk9IjI0NSIgd2lkdGg9IjIyNCIgaGVpZ2h0PSI4MCIgcng9IjIiIGZpbGw9IiNlZmY0ZmYiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMTMyLjAiIHk9IjI3My4xNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPkJ5IHJlc3BvbnNlIHRpbWU8L3RleHQ+PHRleHQgeD0iMTMyLjAiIHk9IjI4OC4wMjUwMDAwMDAwMDAwMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+YXZnX2R1cmF0aW9uIHggZXhlY3V0aW9uczwvdGV4dD48dGV4dCB4PSIxMzIuMCIgeT0iMzAyLjg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+b3IgbWF4X2R1cmF0aW9uIGZvciB0aGUgcDk5IGNvbXBsYWludDwvdGV4dD48cmVjdCB4PSIyNjYiIHk9IjI0NSIgd2lkdGg9IjIyNCIgaGVpZ2h0PSI4MCIgcng9IjIiIGZpbGw9IiNlZmY0ZmYiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMzc4LjAiIHk9IjI3My4xNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPkJ5IENQVTwvdGV4dD48dGV4dCB4PSIzNzguMCIgeT0iMjg4LjAyNTAwMDAwMDAwMDAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5hdmdfY3B1X3RpbWUgeCBleGVjdXRpb25zPC90ZXh0Pjx0ZXh0IHg9IjM3OC4wIiB5PSIzMDIuODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj50aGUgbGljZW5jZS1jb3JlIHF1ZXN0aW9uPC90ZXh0PjxyZWN0IHg9IjUxMiIgeT0iMjQ1IiB3aWR0aD0iMjI0IiBoZWlnaHQ9IjgwIiByeD0iMiIgZmlsbD0iI2VmZjRmZiIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI2MjQuMCIgeT0iMjczLjE3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+QnkgSS9PIGVmZmljaWVuY3k8L3RleHQ+PHRleHQgeD0iNjI0LjAiIHk9IjI4OC4wMjUwMDAwMDAwMDAwMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+bG9naWNhbF9pb19yZWFkcyAvIHJvd2NvdW50PC90ZXh0Pjx0ZXh0IHg9IjYyNC4wIiB5PSIzMDIuODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5yZWFkcyBwZXIgcm93IHJldHVybmVkPC90ZXh0PjxyZWN0IHg9Ijc1OCIgeT0iMjQ1IiB3aWR0aD0iMjI0IiBoZWlnaHQ9IjgwIiByeD0iMiIgZmlsbD0iI2VmZjRmZiIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI4NzAuMCIgeT0iMjczLjE3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+QnkgdmFyaWFuY2U8L3RleHQ+PHRleHQgeD0iODcwLjAiIHk9IjI4OC4wMjUwMDAwMDAwMDAwMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+c3RkZXZfZHVyYXRpb24gLyBhdmdfZHVyYXRpb248L3RleHQ+PHRleHQgeD0iODcwLjAiIHk9IjMwMi44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPnBhcmFtZXRlciBzZW5zaXRpdml0eSwgcGxhbiBmbGlwczwvdGV4dD48cmVjdCB4PSIyMDAiIHk9IjM1NSIgd2lkdGg9IjYwMCIgaGVpZ2h0PSI1NiIgcng9IjIiIGZpbGw9IiNmNGY1ZjciIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iNTAwLjAiIHk9IjM3OC41OTk5OTk5OTk5OTk5NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+RXZlcnkgcmFuayBwcm9kdWNlcyBhIHF1ZXJ5X2lkIGFuZCBhIHBsYW5faWQ6IHRoYXQgcGFpciBpcyB3aGF0IHlvdSB0dW5lLCBmb3JjZSwgaGludCBvciBpbmRleCBhZ2FpbnN0LjwvdGV4dD48dGV4dCB4PSI1MDAuMCIgeT0iMzkzLjQ1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5QZXJzaXN0ZWQgYWNyb3NzIHJlc3RhcnRzLCBhbmQgb24gcmVhZGFibGUgc2Vjb25kYXJpZXMgYnkgZGVmYXVsdCBzaW5jZSBTUUwgU2VydmVyIDIwMjUuPC90ZXh0PjxsaW5lIHgxPSIxMzIiIHkxPSIzMjUiIHgyPSIxMzIiIHkyPSIzNTMiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyKSIvPjxsaW5lIHgxPSIzNzgiIHkxPSIzMjUiIHgyPSIzNzgiIHkyPSIzNTMiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyKSIvPjxsaW5lIHgxPSI2MjQiIHkxPSIzMjUiIHgyPSI2MjQiIHkyPSIzNTMiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyKSIvPjxsaW5lIHgxPSI4NzAiIHkxPSIzMjUiIHgyPSI4NzAiIHkyPSIzNTMiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyKSIvPjwvc3ZnPg==" alt="Query Store views for expensive queries on SQL Server 2025: query, plan, runtime stats and wait stats chained together, then four rankings by response time, CPU, I/O efficiency and variance" width="1000" height="430"><br><em>Figure 2. The evidence chain for expensive queries. Every ranking resolves to a query_id and a plan_id, and those two numbers are what you act on.</em></p>
<p>Before ranking expensive queries, confirm Query Store is capturing what you need. QUERY_CAPTURE_MODE of AUTO skips trivial and infrequent statements, which is fine for finding expensive queries and wrong for auditing everything. Check the operation mode, the interval length and how much of the allocated space is used, because a Query Store that has flipped to READ_ONLY under space pressure is silently stale.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Query Store health before you trust its rankings (SQL Server 2022 and 2025)">SELECT   actual_state_desc,
         desired_state_desc,
         readonly_reason,
         current_storage_size_mb,
         max_storage_size_mb,
         interval_length_minutes,
         query_capture_mode_desc,
         stale_query_threshold_days,
         wait_stats_capture_mode_desc
FROM     sys.database_query_store_options;

-- If actual_state_desc is READ_ONLY with readonly_reason 65536, storage is full:
-- ALTER DATABASE CURRENT SET QUERY_STORE (MAX_STORAGE_SIZE_MB = 2048);   -- online, no restart</pre>
<p>Then rank the expensive queries. The query below is the one I keep in a snippet. It aggregates the last N hours of runtime intervals per query and plan and computes the four numbers the quadrant diagram asks for: total duration weighted by executions, total CPU, logical reads per row returned, and the coefficient of variation of duration. Sort by whichever question you are answering. The ORDER BY is the whole point of the query, so I leave all four options in and comment out three.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Expensive queries from Query Store, rankable by response time, CPU, I/O efficiency or variance">DECLARE @hours INT = 24;

WITH rs AS (
    SELECT   rs.plan_id,
             SUM(rs.count_executions)                                          AS executions,
             SUM(rs.count_executions * rs.avg_duration)        / 1000.0        AS total_duration_ms,
             SUM(rs.count_executions * rs.avg_cpu_time)        / 1000.0        AS total_cpu_ms,
             SUM(rs.count_executions * rs.avg_logical_io_reads)                AS total_logical_reads,
             SUM(rs.count_executions * rs.avg_rowcount)                        AS total_rows,
             SUM(rs.count_executions * rs.avg_tempdb_space_used)               AS total_tempdb_pages,
             MAX(rs.max_duration)                              / 1000.0        AS max_duration_ms,
             MAX(rs.stdev_duration)                            / 1000.0        AS max_stdev_duration_ms,
             MAX(rs.max_dop)                                                   AS max_dop
    FROM     sys.query_store_runtime_stats          AS rs
    JOIN     sys.query_store_runtime_stats_interval AS i
          ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id
    WHERE    i.start_time &gt;= DATEADD(HOUR, -@hours, SYSUTCDATETIME())
    GROUP BY rs.plan_id
)
SELECT   TOP (25)
         q.query_id,
         p.plan_id,
         p.is_forced_plan,
         p.compatibility_level,
         rs.executions,
         CAST(rs.total_duration_ms / NULLIF(rs.executions, 0) AS DECIMAL(18, 2))   AS avg_duration_ms,
         CAST(rs.max_duration_ms AS DECIMAL(18, 2))                                  AS max_duration_ms,
         CAST(rs.total_cpu_ms AS DECIMAL(18, 2))                                     AS total_cpu_ms,
         CAST(rs.total_logical_reads / NULLIF(rs.total_rows, 0) AS DECIMAL(18, 1))  AS reads_per_row,
         CAST(rs.max_stdev_duration_ms / NULLIF(rs.total_duration_ms / NULLIF(rs.executions, 0), 0) AS DECIMAL(9, 3)) AS duration_cv,
         rs.total_tempdb_pages,
         rs.max_dop,
         LEFT(qt.query_sql_text, 160)                                                AS query_text
FROM     rs
JOIN     sys.query_store_plan       AS p  ON p.plan_id  = rs.plan_id
JOIN     sys.query_store_query      AS q  ON q.query_id = p.query_id
JOIN     sys.query_store_query_text AS qt ON qt.query_text_id = q.query_text_id
ORDER BY rs.total_duration_ms DESC;       -- response time, weighted by how often it runs
-- ORDER BY rs.total_cpu_ms DESC;         -- resource cost: the core-count question
-- ORDER BY reads_per_row DESC;           -- efficiency: reads per row returned
-- ORDER BY duration_cv DESC;             -- variance: parameter sensitivity and plan flips</pre>
<p>Two of those columns do more work for expensive queries than the rest. reads_per_row is the efficiency ratio: a query returning ten rows at 40,000 logical reads per execution is doing 4,000 reads for each row it hands back, and no amount of hardware makes that reasonable. duration_cv, the standard deviation of duration divided by the mean, is how you find parameter-sensitive expensive queries before a user does; a value above 1 on a frequently executed statement almost always means more than one plan shape is being used for the same query_id.</p>
<p>The output below is illustrative, not from a customer system, and shows the shape I am looking for: one query high on every axis, one high on CPU only, and one whose variance gives it away.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="text" data-enlighter-title="Illustrative output (not measured on a real instance) ordered by total duration">query_id plan_id executions avg_duration_ms max_duration_ms total_cpu_ms reads_per_row duration_cv max_dop
-------- ------- ---------- --------------- --------------- ------------ ------------- ----------- -------
   40217    9912      18240          412.8         8391.2     6204812.1        3892.4       1.812       8
   11203    2210    1928400            1.9           24.7     3517110.6         206.0       0.220       1
   40391   10044       2210         1290.5         2011.0       81023.9          12.3       0.140       4
...</pre>
<p>Among those three expensive queries, query_id 40217 is the classic top-right quadrant: slow, expensive and unstable. query_id 11203 is the bottom-right one, two milliseconds a call and the second-largest CPU consumer on the instance because it runs 1.9 million times a day with 206 reads per row. Sorting by average duration would never have shown it. query_id 40391 is slow and steady with a modest read ratio, which usually means it is waiting rather than working, and the wait stats view settles that.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Wait categories per plan: is the expensive query working or waiting?">SELECT   ws.plan_id,
         ws.wait_category_desc,
         SUM(ws.total_query_wait_time_ms)                    AS total_wait_ms,
         MAX(ws.max_query_wait_time_ms)                      AS max_wait_ms
FROM     sys.query_store_wait_stats               AS ws
JOIN     sys.query_store_runtime_stats_interval   AS i
      ON i.runtime_stats_interval_id = ws.runtime_stats_interval_id
WHERE    ws.plan_id IN (9912, 2210, 10044)
AND      i.start_time &gt;= DATEADD(HOUR, -24, SYSUTCDATETIME())
GROUP BY ws.plan_id, ws.wait_category_desc
ORDER BY ws.plan_id, total_wait_ms DESC;</pre>
<p>If an expensive query&rsquo;s duration is mostly Lock, Network IO or Memory waits, it belongs in the slow-but-cheap quadrant and the index conversation is over before it starts. On SQL Server 2025 there is a wait category worth new attention: the LCK_M_*_XACT waits that appear once optimized locking is enabled, where sessions wait on the transaction resource rather than on individual rows. Baseline lock waits before turning that feature on, or you will not be able to tell whether it helped.</p>
<h2>Expensive queries from the plan cache DMVs: still useful for one thing<a class="anchor-link" id="expensive-queries-from-the-plan-cache-dmvs-still-useful-for-one-thing"></a></h2>
<p>sys.dm_exec_query_stats still has a place, mainly on instances where Query Store is off or on ad hoc workloads where it captures little. Its numbers reset on plan eviction and restart, it has no interval history, and on a busy instance it undercounts anything that recompiles often. I use it for one thing Query Store does not do well: finding expensive queries by the memory grant they requested, which is the fastest route to the RESOURCE_SEMAPHORE waits that stall everything else.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Grant-hungry statements from the plan cache: the RESOURCE_SEMAPHORE suspects">SELECT   TOP (20)
         qs.execution_count,
         qs.max_grant_kb,
         qs.max_used_grant_kb,
         qs.max_ideal_grant_kb,
         qs.max_spills,
         qs.total_worker_time / 1000 / NULLIF(qs.execution_count, 0)  AS avg_cpu_ms,
         qs.total_logical_reads / NULLIF(qs.execution_count, 0)       AS avg_logical_reads,
         qs.query_hash,
         SUBSTRING(st.text, (qs.statement_start_offset / 2) + 1,
                   ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
                     ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS statement_text
FROM     sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE    qs.max_grant_kb &gt; 65536
ORDER BY qs.max_grant_kb DESC;</pre>
<p>A max_used_grant_kb far below max_grant_kb is an over-estimate that starves other sessions; a max_spills above zero with used close to granted is an under-estimate that spilled. Both come back to cardinality, which is where reading the plans of expensive queries starts.</p>
<h2>Reading the plan of an expensive query without flattering it<a class="anchor-link" id="reading-the-plan-of-an-expensive-query-without-flattering-it"></a></h2>
<p>The actual execution plan is where a ranking of expensive queries becomes a diagnosis. Estimated plans are useful for review; they are useless for troubleshooting, because the whole problem is usually that the estimate was wrong. Query Store keeps the plan XML per plan_id, and on SQL Server 2019 and later the LAST_QUERY_PLAN_STATS database-scoped configuration keeps the last actual plan with runtime counters for cached statements, which is the cheapest way to get an actual plan for something you cannot re-run at will.</p>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5NjAgNDA1IiB3aWR0aD0iOTYwIiBoZWlnaHQ9IjQwNSIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InQxODUzNyBkMTg1MzciPjx0aXRsZSBpZD0idDE4NTM3Ij5GaXZlIGV4cGVuc2l2ZSBwbGFuIG9wZXJhdG9yIHBhdHRlcm5zIGluIFNRTCBTZXJ2ZXIgYW5kIHRoZSBldmlkZW5jZSBmb3IgZWFjaDwvdGl0bGU+PGRlc2MgaWQ9ImQxODUzNyI+Rml2ZSBjb2x1bW5zOiBrZXkgbG9va3VwLCBub24tU0FSR2FibGUgc2Nhbiwgc3BpbGwsIHNwb29sIGFuZCBlc3RpbWF0ZSBnYXAsIGVhY2ggd2l0aCBob3cgaXQgc2hvd3MgaW4gdGhlIHBsYW4gYW5kIHRoZSBkaXJlY3Rpb24gb2YgdGhlIGZpeC48L2Rlc2M+PGRlZnM+PG1hcmtlciBpZD0iYXJyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjguNSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGxpbmUgeDE9IjEuNSIgeTE9IjEuNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bGluZSB4MT0iMS41IiB5MT0iOC41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjM2Q0ZjYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9ImFycmIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOC41IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48bGluZSB4MT0iMS41IiB5MT0iMS41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxsaW5lIHgxPSIxLjUiIHkxPSI4LjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9tYXJrZXI+PC9kZWZzPjxyZWN0IHdpZHRoPSI5NjAiIGhlaWdodD0iNDA1IiBmaWxsPSIjZmZmIi8+PHRleHQgeD0iNDgwLjAiIHk9IjI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMGExYTJmIj5XaGF0IGFuIGV4cGVuc2l2ZSBwbGFuIGxvb2tzIGxpa2U6IGZpdmUgb3BlcmF0b3IgcGF0dGVybnMgYW5kIHRoZSBudW1iZXIgdGhhdCBleHBvc2VzIGVhY2g8L3RleHQ+PHJlY3QgeD0iMjAiIHk9IjYwIiB3aWR0aD0iMTc2IiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMTA4LjAiIHk9Ijk1LjM2MjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPkluZGV4IFNlZWsgKyBLZXkgTG9va3VwPC90ZXh0Pjx0ZXh0IHg9IjEwOC4wIiB5PSIxMDkuNTM3NDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+TmVzdGVkIExvb3BzIGpvaW5pbmcgc2VlayB0bzwvdGV4dD48dGV4dCB4PSIxMDguMCIgeT0iMTIzLjcxMjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmNsdXN0ZXJlZCBpbmRleCwgb25jZSBwZXIgcm93PC90ZXh0Pjx0ZXh0IHg9IjEwOC4wIiB5PSIxMzcuODg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5leHBvc2VkIGJ5OiBsb29rdXAgZXhlY3V0aW9uczwvdGV4dD48dGV4dCB4PSIxMDguMCIgeT0iMTUyLjA2MjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+bmVhciBzZWVrIHJvd3M7IGxvZ2ljYWwgcmVhZHM8L3RleHQ+PHRleHQgeD0iMTA4LjAiIHk9IjE2Ni4yMzc0OTk5OTk5OTk5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5zZXZlcmFsIHRpbWVzIHRoZSByb3cgY291bnQ8L3RleHQ+PHRleHQgeD0iMTA4LjAiIHk9IjE4MC40MTI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmZpeDogSU5DTFVERSBsb29rdXAgY29sdW1uczwvdGV4dD48cmVjdCB4PSIyMDYiIHk9IjYwIiB3aWR0aD0iMTc2IiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iMjk0LjAiIHk9Ijk1LjM2MjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPkNsdXN0ZXJlZCBJbmRleCBTY2FuPC90ZXh0Pjx0ZXh0IHg9IjI5NC4wIiB5PSIxMDkuNTM3NDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+b24gYSBmaWx0ZXJlZCBxdWVyeTwvdGV4dD48dGV4dCB4PSIyOTQuMCIgeT0iMTIzLjcxMjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPnByZWRpY2F0ZSBub3QgU0FSR2FibGU8L3RleHQ+PHRleHQgeD0iMjk0LjAiIHk9IjEzNy44ODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmV4cG9zZWQgYnk6IHJvd3MgcmVhZCBmYXIgb3ZlcjwvdGV4dD48dGV4dCB4PSIyOTQuMCIgeT0iMTUyLjA2MjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+cm93cyBvdXRwdXQ7IGltcGxpY2l0LWNvbnZlcnQ8L3RleHQ+PHRleHQgeD0iMjk0LjAiIHk9IjE2Ni4yMzc0OTk5OTk5OTk5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj53YXJuaW5nIG9uIHRoZSBzY2FuPC90ZXh0Pjx0ZXh0IHg9IjI5NC4wIiB5PSIxODAuNDEyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5maXg6IHJld3JpdGUgcHJlZGljYXRlLCBpbmRleDwvdGV4dD48cmVjdCB4PSIzOTIiIHk9IjYwIiB3aWR0aD0iMTc2IiBoZWlnaHQ9IjE1MCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iNDgwLjAiIHk9Ijk1LjM2MjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPlNvcnQgLyBIYXNoIHNwaWxsPC90ZXh0Pjx0ZXh0IHg9IjQ4MC4wIiB5PSIxMDkuNTM3NDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+bWVtb3J5IGdyYW50IHRvbyBzbWFsbDwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMTIzLjcxMjQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmZvciBhY3R1YWwgcm93czwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMTM3Ljg4NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+ZXhwb3NlZCBieTogU3BpbGxUb1RlbXBEYjwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMTUyLjA2MjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+d2FybmluZywgdGVtcGRiX3NwYWNlX3VzZWQ8L3RleHQ+PHRleHQgeD0iNDgwLjAiIHk9IjE2Ni4yMzc0OTk5OTk5OTk5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5pbiBydW50aW1lX3N0YXRzPC90ZXh0Pjx0ZXh0IHg9IjQ4MC4wIiB5PSIxODAuNDEyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5maXg6IGZpeCBlc3RpbWF0ZXMsIG9yIHRoZSBzb3J0PC90ZXh0PjxyZWN0IHg9IjU3OCIgeT0iNjAiIHdpZHRoPSIxNzYiIGhlaWdodD0iMTUwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI2NjYuMCIgeT0iOTUuMzYyNDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+VGFibGUgU3Bvb2wgLyBFYWdlciBTcG9vbDwvdGV4dD48dGV4dCB4PSI2NjYuMCIgeT0iMTA5LjUzNzQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPm9wdGltaXplciBtYXRlcmlhbGlzaW5nIGE8L3RleHQ+PHRleHQgeD0iNjY2LjAiIHk9IjEyMy43MTI0OTk5OTk5OTk5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5zdWJ0cmVlLCBvZnRlbiBmb3IgSGFsbG93ZWVuPC90ZXh0Pjx0ZXh0IHg9IjY2Ni4wIiB5PSIxMzcuODg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5leHBvc2VkIGJ5OiBzcG9vbCByZWJpbmRzIGFuZDwvdGV4dD48dGV4dCB4PSI2NjYuMCIgeT0iMTUyLjA2MjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+cmV3aW5kcywgaHVnZSB3b3JrdGFibGUgcmVhZHM8L3RleHQ+PHRleHQgeD0iNjY2LjAiIHk9IjE2Ni4yMzc0OTk5OTk5OTk5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5pbiBTVEFUSVNUSUNTIElPPC90ZXh0Pjx0ZXh0IHg9IjY2Ni4wIiB5PSIxODAuNDEyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMC41IiBmaWxsPSIjMWEyMzMyIj5maXg6IHJld3JpdGUgdGhlIHVwZGF0ZSBwYXR0ZXJuPC90ZXh0PjxyZWN0IHg9Ijc2NCIgeT0iNjAiIHdpZHRoPSIxNzYiIGhlaWdodD0iMTUwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI4NTIuMCIgeT0iOTUuMzYyNDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+RXN0aW1hdGUgdnMgYWN0dWFsIGdhcDwvdGV4dD48dGV4dCB4PSI4NTIuMCIgeT0iMTA5LjUzNzQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmVzdGltYXRlZCByb3dzIDEsIGFjdHVhbCAxLjJNPC90ZXh0Pjx0ZXh0IHg9Ijg1Mi4wIiB5PSIxMjMuNzEyNDk5OTk5OTk5OTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+c3RhbGUgc3RhdHMsIHNrZXcsIG9yIGE8L3RleHQ+PHRleHQgeD0iODUyLjAiIHk9IjEzNy44ODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPnBhcmFtZXRlci1zbmlmZmVkIHBsYW48L3RleHQ+PHRleHQgeD0iODUyLjAiIHk9IjE1Mi4wNjI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPmV4cG9zZWQgYnk6IHN0ZGV2X2R1cmF0aW9uLDwvdGV4dD48dGV4dCB4PSI4NTIuMCIgeT0iMTY2LjIzNzQ5OTk5OTk5OTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwLjUiIGZpbGw9IiMxYTIzMzIiPm11bHRpcGxlIHBsYW5zIHBlciBxdWVyeV9pZDwvdGV4dD48dGV4dCB4PSI4NTIuMCIgeT0iMTgwLjQxMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAuNSIgZmlsbD0iIzFhMjMzMiI+Zml4OiBzdGF0cywgT1BQTywgb3IgYSBoaW50PC90ZXh0PjxyZWN0IHg9IjEyMCIgeT0iMjM1IiB3aWR0aD0iNzIwIiBoZWlnaHQ9IjYwIiByeD0iMiIgZmlsbD0iIzBlMjQ0MCIgc3Ryb2tlPSIjMGUyNDQwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI0ODAuMCIgeT0iMjYwLjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiNmZmYiPlJlYWQgYWN0dWFsIHBsYW5zLCBuZXZlciBlc3RpbWF0ZWQgb25lcywgZm9yIHRyb3VibGVzaG9vdGluZzwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMjc1LjQ1MDAwMDAwMDAwMDA1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj5TRVQgU1RBVElTVElDUyBYTUwgT04sIFF1ZXJ5IFN0b3JlIHBsYW4gWE1MLCBvciB0aGUgbGFzdF9xdWVyeV9wbGFuX3N0YXRzIERNRiB3aGVuIHRoZSBjb25maWd1cmF0aW9uIGlzIGVuYWJsZWQ8L3RleHQ+PHJlY3QgeD0iMTIwIiB5PSIzMTUiIHdpZHRoPSI3MjAiIGhlaWdodD0iNzAiIHJ4PSIyIiBmaWxsPSIjZjRmNWY3IiBzdHJva2U9IiNjOWQzZTAiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjQ4MC4wIiB5PSIzMzguMTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5Sb3dzIFJlYWQgdnMgQWN0dWFsIE51bWJlciBvZiBSb3dzIGlzIHRoZSBzaW5nbGUgbW9zdCB1c2VmdWwgcGFpciBpbiB0aGUgcGxhbjwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMzUzLjAyNTAwMDAwMDAwMDAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5Sb3dzIFJlYWQgY291bnRzIHdoYXQgdGhlIG9wZXJhdG9yIHRvdWNoZWQ7IEFjdHVhbCBOdW1iZXIgb2YgUm93cyBjb3VudHMgd2hhdCBzdXJ2aXZlZCB0aGUgcHJlZGljYXRlLjwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMzY3Ljg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+VGhlIHJhdGlvIGJldHdlZW4gdGhlbSBpcyB0aGUgZWZmaWNpZW5jeSBvZiB0aGF0IG9wZXJhdG9yLjwvdGV4dD48L3N2Zz4=" alt="Five expensive plan operator patterns in SQL Server: key lookup, non-SARGable clustered index scan, sort or hash spill, table spool and estimate versus actual gap, with the evidence and fix for each" width="960" height="405"><br><em>Figure 3. Five plan shapes that account for most expensive queries, and the number in the plan that exposes each.</em></p>
<p>Rather than opening every one of the expensive queries in a plan viewer, I pull the warnings and the operator inventory out of the XML for the top plan_ids in one pass. The query below flags the four things I want to know before I look at anything else: is there a key lookup, a spill, an implicit conversion, or a scan on a table the predicate should have seeked.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Plan triage from Query Store XML: lookups, spills, implicit conversions and scans for a set of plan_ids">;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan')
SELECT   p.plan_id,
         p.query_id,
         x.value('count(//IndexScan[@Lookup="true"])', 'INT')                       AS key_lookups,
         x.value('count(//RelOp[@PhysicalOp="Clustered Index Scan" or @PhysicalOp="Index Scan" or @PhysicalOp="Table Scan"])', 'INT')
                                                                                   AS scans,
         x.value('count(//Warnings/SpillToTempDb)', 'INT')                        AS spills,
         x.value('count(//Warnings/PlanAffectingConvert[@ConvertIssue="Seek Plan"])', 'INT')
                                                                                   AS implicit_conversions,
         x.value('count(//RelOp[@PhysicalOp="Table Spool" or @PhysicalOp="Index Spool"])', 'INT')
                                                                                   AS spools,
         x.value('(//StmtSimple/@StatementOptmEarlyAbortReason)[1]', 'NVARCHAR(50)') AS optimizer_abort_reason,
         x.value('(//QueryPlan/@CachedPlanSize)[1]', 'INT')                       AS cached_plan_kb
FROM     sys.query_store_plan AS p
CROSS APPLY (SELECT TRY_CONVERT(XML, p.query_plan)) AS c(x)
WHERE    p.plan_id IN (9912, 2210, 10044)
ORDER BY p.plan_id;</pre>
<p>For the expensive queries that survive triage, the operator-level view is where the efficiency ratio lives. Two properties on every operator in an actual plan tell you what it cost: Number of Rows Read, which is how many rows the operator touched, and Actual Number of Rows, which is how many came out. A seek that reads 900,000 rows to output 40 is a seek in name only; it is a range scan with a residual predicate, and the residual is nearly always a non-SARGable expression or a column that should have been in the key. STATISTICS IO gives the same information per table when you can run the statement yourself.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Measuring one expensive query directly: reads and CPU per table, with the actual plan">SET STATISTICS IO, TIME ON;
SET STATISTICS XML ON;

DECLARE @customer_id INT = 48213, @from DATE = '2026-08-01';

SELECT   o.order_id, o.order_date, o.status, ol.sku, ol.qty, ol.line_total
FROM     dbo.orders      AS o
JOIN     dbo.order_lines AS ol ON ol.order_id = o.order_id
WHERE    o.customer_id = @customer_id
AND      CONVERT(DATE, o.order_date) &gt;= @from        -- non-SARGable: function on the column
ORDER BY o.order_date DESC;

SET STATISTICS XML OFF;
SET STATISTICS IO, TIME OFF;

-- Illustrative STATISTICS IO line (not a measured result):
-- Table 'orders'. Scan count 1, logical reads 184112, ... lob logical reads 0
-- Table 'order_lines'. Scan count 61, logical reads 244, ...</pre>
<p>That CONVERT on order_date is the single most common reason an otherwise well-indexed statement becomes one of the expensive queries on an instance. The optimizer cannot seek on a function of the column, so it scans the whole customer&rsquo;s range, or the whole table, and filters afterwards. The rewrite is a half-open range on the raw column, and the reads collapse to whatever the index range actually covers. The same demotion happens with implicit conversions between NVARCHAR parameters and VARCHAR columns, with ISNULL(col, x) = y, with LIKE &lsquo;%&rsquo; + @p, and with arithmetic on the column side of a comparison.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="The SARGable rewrite: same rows, a seekable predicate">WHERE    o.customer_id = @customer_id
AND      o.order_date  &gt;= CAST(@from AS DATETIME2(0))   -- compare the raw column, match its type
ORDER BY o.order_date DESC;</pre>
<h2>What SQL Server 2025 changed for expensive queries, and where 2022 differs<a class="anchor-link" id="what-sql-server-2025-changed-for-expensive-queries-and-where-2022-differs"></a></h2>
<p>Intelligent Query Processing is the umbrella for the optimizer features that adjust plans from runtime feedback, and SQL Server 2025 extended it in ways that change how expensive queries behave after an upgrade. I pin each item to the version and the compatibility level it needs, because a database restored onto a 2025 instance at compatibility level 150 gets almost none of this.</p>
<table>
<caption>SQL Server 2025 optimizer behaviour that affects expensive queries, against SQL Server 2022</caption>
<thead>
<tr>
<th>Feature</th>
<th>SQL Server 2022</th>
<th>SQL Server 2025</th>
<th>What to check</th>
</tr>
</thead>
<tbody>
<tr>
<td>Parameter Sensitive Plan optimization</td>
<td>New in 2022, compat 160; up to three plan variants per parameterised predicate</td>
<td>Same mechanism; variant queries appear as their own query_ids linked to the parent in sys.query_store_query_variant</td>
<td>Join runtime stats through sys.query_store_query_variant; high duration_cv on a query with a single plan means PSP did not engage</td>
</tr>
<tr>
<td>Optional Parameter Plan Optimization (OPPO)</td>
<td>Not available</td>
<td>Multiple plans per statement chosen on which optional parameters are NULL; the answer to the &ldquo;WHERE (@a IS NULL OR col = @a)&rdquo; pattern</td>
<td>Compat 170; on by default via the OPTIONAL_PARAMETER_OPTIMIZATION database-scoped configuration; DISABLE_OPTIONAL_PARAMETER_OPTIMIZATION hint per query</td>
</tr>
<tr>
<td>Cardinality estimation feedback</td>
<td>Correlation, join containment and row goal scenarios; compat 160, Query Store required</td>
<td>Extended to expressions; CE_FEEDBACK database-scoped configuration and the DISABLE_CE_FEEDBACK hint</td>
<td>Build number: a CE feedback defect in an early CU caused plan cache growth and CPU; confirm the fix is in your CU before relying on it</td>
</tr>
<tr>
<td>DOP feedback</td>
<td>Opt-in via DOP_FEEDBACK</td>
<td>On by default; parallel expensive queries that gained nothing from parallelism get their DOP reduced over successive executions</td>
<td>max_dop in runtime_stats dropping across intervals for the same plan_id is feedback working, not a regression</td>
</tr>
<tr>
<td>Query Store on readable secondaries</td>
<td>Preview, off by default</td>
<td>On by default, with persisted statistics on secondaries</td>
<td>Rank expensive queries on the reporting replica too; the secondary no longer loses its stats on failover</td>
</tr>
<tr>
<td>Query Store hint ABORT_QUERY_EXECUTION</td>
<td>Not available</td>
<td>Block a query shape at compile time without touching application code; raises error 8778 to the caller</td>
<td>A circuit breaker for the runaway report at 02:00, reversible with sp_query_store_clear_hints</td>
</tr>
<tr>
<td>Optimized locking</td>
<td>Not available on-premises</td>
<td>Opt-in per database; requires ADR, needs RCSI for lock-after-qualification; new LCK_M_*_XACT waits</td>
<td>Changes the wait profile of slow-but-cheap expensive queries; review write-ordering assumptions before enabling</td>
</tr>
</tbody>
</table>
<p>The compatibility-level point deserves its own sentence. Microsoft&rsquo;s rule is that cardinality estimator changes only activate at the default compatibility level of the version that introduced them, so a database left at 150 after a 2025 upgrade keeps its old plans and gains none of the feedback mechanisms above, which is sometimes exactly what you want for the first weeks and is never what you want permanently. Microsoft&rsquo;s <a href="https://learn.microsoft.com/en-us/sql/sql-server/what-s-new-in-sql-server-2025" target="_blank" rel="noopener">What&rsquo;s new in SQL Server 2025</a> page is the reference for the table, and the <a href="https://learn.microsoft.com/en-us/sql/relational-databases/performance/intelligent-query-processing-details" target="_blank" rel="noopener">Intelligent Query Processing details</a> page carries the per-feature compatibility requirements.</p>
<h2>Indexing expensive queries: the decision, then the proof<a class="anchor-link" id="indexing-expensive-queries-the-decision-then-the-proof"></a></h2>
<p>Only after the predicate is SARGable and the statistics are current does an index change for expensive queries make sense; an index built around a bad predicate fossilises the bad predicate. The design sequence I follow is short and I follow it in order every time, because the order is what decides whether the index seeks or scans.</p>
<p><img decoding="async" loading="lazy" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5NjAgNDQwIiB3aWR0aD0iOTYwIiBoZWlnaHQ9IjQ0MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InQ3Mjg0OSBkNzI4NDkiPjx0aXRsZSBpZD0idDcyODQ5Ij5JbmRleCBkZXNpZ24gZGVjaXNpb24gc2VxdWVuY2UgZm9yIGFuIGV4cGVuc2l2ZSBxdWVyeSBpbiBTUUwgU2VydmVyPC90aXRsZT48ZGVzYyBpZD0iZDcyODQ5Ij5Gb3VyIGtleS1kZXNpZ24gc3RlcHMgbGVmdCB0byByaWdodCwgZm91ciBjb3N0IGNoZWNrcyBiZWxvdyB0aGVtLCBhbmQgYSBwcm9vZiBzdGVwIGJlZm9yZSB0aGUgaW5kZXggcmVhY2hlcyBwcm9kdWN0aW9uLjwvZGVzYz48ZGVmcz48bWFya2VyIGlkPSJhcnIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOC41IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48bGluZSB4MT0iMS41IiB5MT0iMS41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjM2Q0ZjYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxsaW5lIHgxPSIxLjUiIHkxPSI4LjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMzZDRmNjIiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9tYXJrZXI+PG1hcmtlciBpZD0iYXJyYiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI4LjUiIHJlZlk9IjUiIG1hcmtlcldpZHRoPSI3IiBtYXJrZXJIZWlnaHQ9IjciIG9yaWVudD0iYXV0by1zdGFydC1yZXZlcnNlIiBtYXJrZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxsaW5lIHgxPSIxLjUiIHkxPSIxLjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PGxpbmUgeDE9IjEuNSIgeTE9IjguNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48L21hcmtlcj48L2RlZnM+PHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI0NDAiIGZpbGw9IiNmZmYiLz48dGV4dCB4PSI0ODAuMCIgeT0iMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMwYTFhMmYiPkluZGV4aW5nIGFuIGV4cGVuc2l2ZSBxdWVyeTogZGVjaWRlIGZyb20gdGhlIHByZWRpY2F0ZSBhbmQgdGhlIHNlbGVjdCBsaXN0LCBpbiB0aGlzIG9yZGVyPC90ZXh0PjxyZWN0IHg9IjIwIiB5PSI2MCIgd2lkdGg9IjIxOCIgaGVpZ2h0PSIxMDAiIHJ4PSIyIiBmaWxsPSIjZWZmNGZmIiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjEyOS4wIiB5PSI5MC43NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+MS4gRXF1YWxpdHkgcHJlZGljYXRlczwvdGV4dD48dGV4dCB4PSIxMjkuMCIgeT0iMTA1LjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmJlY29tZSB0aGUgbGVhZGluZyBrZXkgY29sdW1ucyw8L3RleHQ+PHRleHQgeD0iMTI5LjAiIHk9IjEyMC40NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+bW9zdCBzZWxlY3RpdmUgZmlyc3Qgd2hlbjwvdGV4dD48dGV4dCB4PSIxMjkuMCIgeT0iMTM1LjMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmFsbCBhcmUgZXF1YWxpdHk8L3RleHQ+PGxpbmUgeDE9IjIzOCIgeTE9IjExMCIgeDI9IjI2MCIgeTI9IjExMCIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEuNCIgbWFya2VyLWVuZD0idXJsKCNhcnJiKSIvPjxyZWN0IHg9IjI2MCIgeT0iNjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSIzNjkuMCIgeT0iOTAuNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPjIuIE9uZSByYW5nZSBwcmVkaWNhdGU8L3RleHQ+PHRleHQgeD0iMzY5LjAiIHk9IjEwNS42IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5nb2VzIGxhc3QgaW4gdGhlIGtleTs8L3RleHQ+PHRleHQgeD0iMzY5LjAiIHk9IjEyMC40NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+YSBzZWNvbmQgcmFuZ2UgY29sdW1uPC90ZXh0Pjx0ZXh0IHg9IjM2OS4wIiB5PSIxMzUuMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+Y2Fubm90IHNlZWs8L3RleHQ+PGxpbmUgeDE9IjQ3OCIgeTE9IjExMCIgeDI9IjUwMCIgeTI9IjExMCIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEuNCIgbWFya2VyLWVuZD0idXJsKCNhcnJiKSIvPjxyZWN0IHg9IjUwMCIgeT0iNjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2VmZjRmZiIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI2MDkuMCIgeT0iOTAuNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPjMuIE9SREVSIEJZIC8gR1JPVVAgQlk8L3RleHQ+PHRleHQgeD0iNjA5LjAiIHk9IjEwNS42IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5jb2x1bW5zIG5leHQgaW4gdGhlIGtleTwvdGV4dD48dGV4dCB4PSI2MDkuMCIgeT0iMTIwLjQ1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5pZiB0aGV5IGZvbGxvdyB0aGUgcmFuZ2UsPC90ZXh0Pjx0ZXh0IHg9IjYwOS4wIiB5PSIxMzUuMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+ZWxzZSBhY2NlcHQgdGhlIHNvcnQ8L3RleHQ+PGxpbmUgeDE9IjcxOCIgeTE9IjExMCIgeDI9Ijc0MCIgeTI9IjExMCIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEuNCIgbWFya2VyLWVuZD0idXJsKCNhcnJiKSIvPjxyZWN0IHg9Ijc0MCIgeT0iNjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI4NDkuMCIgeT0iOTAuNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPjQuIFNlbGVjdC1saXN0IGNvbHVtbnM8L3RleHQ+PHRleHQgeD0iODQ5LjAiIHk9IjEwNS42IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5nbyBpbiBJTkNMVURFLCBuZXZlciB0aGUga2V5OzwvdGV4dD48dGV4dCB4PSI4NDkuMCIgeT0iMTIwLjQ1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5jb3ZlcmluZyByZW1vdmVzIHRoZSBsb29rdXA8L3RleHQ+PHRleHQgeD0iODQ5LjAiIHk9IjEzNS4zIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5hbmQgd2lkZW5zIHRoZSBsZWFmIHBhZ2U8L3RleHQ+PHRleHQgeD0iNDgwLjAiIHk9IjIwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzBhMWEyZiI+VGhlbiBjaGVjayB0aGUgY29zdCBzaWRlIGJlZm9yZSBDUkVBVEUgSU5ERVg8L3RleHQ+PHJlY3QgeD0iMjAiIHk9IjIyMCIgd2lkdGg9IjIxOCIgaGVpZ2h0PSIxMDAiIHJ4PSIyIiBmaWxsPSIjZmZmIiBzdHJva2U9IiNjOWQzZTAiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjEyOS4wIiB5PSIyNTAuNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPldyaXRlIGFtcGxpZmljYXRpb248L3RleHQ+PHRleHQgeD0iMTI5LjAiIHk9IjI2NS42IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5ldmVyeSBJTlNFUlQvVVBEQVRFL0RFTEVURTwvdGV4dD48dGV4dCB4PSIxMjkuMCIgeT0iMjgwLjQ1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5tYWludGFpbnMgaXQ7IGNoZWNrPC90ZXh0Pjx0ZXh0IHg9IjEyOS4wIiB5PSIyOTUuMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+dXNlcl91cGRhdGVzIHZzIHVzZXJfc2Vla3M8L3RleHQ+PHJlY3QgeD0iMjYwIiB5PSIyMjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSIzNjkuMCIgeT0iMjUwLjc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5FeGlzdGluZyBvdmVybGFwPC90ZXh0Pjx0ZXh0IHg9IjM2OS4wIiB5PSIyNjUuNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+c3lzLmRtX2RiX2luZGV4X3VzYWdlX3N0YXRzPC90ZXh0Pjx0ZXh0IHg9IjM2OS4wIiB5PSIyODAuNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmFuZCBtaXNzaW5nX2luZGV4IERNVnM8L3RleHQ+PHRleHQgeD0iMzY5LjAiIHk9IjI5NS4zIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5iZWZvcmUgYWRkaW5nIGEgbmVhci1kdXBsaWNhdGU8L3RleHQ+PHJlY3QgeD0iNTAwIiB5PSIyMjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI2MDkuMCIgeT0iMjUwLjc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5GaWx0ZXJlZCBpbmRleD88L3RleHQ+PHRleHQgeD0iNjA5LjAiIHk9IjI2NS42IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5XSEVSRSBvbiBhIGxvdy1jYXJkaW5hbGl0eTwvdGV4dD48dGV4dCB4PSI2MDkuMCIgeT0iMjgwLjQ1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5zdGF0dXMgY29sdW1uIHNocmlua3MgdGhlPC90ZXh0Pjx0ZXh0IHg9IjYwOS4wIiB5PSIyOTUuMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+aW5kZXggdG8gdGhlIHJvd3MgdGhhdCBtYXR0ZXI8L3RleHQ+PHJlY3QgeD0iNzQwIiB5PSIyMjAiIHdpZHRoPSIyMTgiIGhlaWdodD0iMTAwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI4NDkuMCIgeT0iMjUwLjc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5Db2x1bW5zdG9yZSBpbnN0ZWFkPzwvdGV4dD48dGV4dCB4PSI4NDkuMCIgeT0iMjY1LjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxYTIzMzIiPmFnZ3JlZ2F0aW9uIG92ZXIgbWlsbGlvbnMgb2Y8L3RleHQ+PHRleHQgeD0iODQ5LjAiIHk9IjI4MC40NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+cm93czogbm9uY2x1c3RlcmVkIGNvbHVtbnN0b3JlPC90ZXh0Pjx0ZXh0IHg9Ijg0OS4wIiB5PSIyOTUuMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+YmVhdHMgYW55IHJvd3N0b3JlIGtleTwvdGV4dD48cmVjdCB4PSIxMjAiIHk9IjM1MCIgd2lkdGg9IjcyMCIgaGVpZ2h0PSI3MCIgcng9IjIiIGZpbGw9IiMwZTI0NDAiIHN0cm9rZT0iIzBlMjQ0MCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iNDgwLjAiIHk9IjM3My4xNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiNmZmYiPlByb3ZlIGl0IGJlZm9yZSBpdCBzaGlwczwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iMzg4LjAyNTAwMDAwMDAwMDAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjZmZmIj5SdW4gdGhlIHF1ZXJ5IHdpdGggdGhlIGluZGV4IGluIGEgY29weSBvZiB0aGUgZGF0YWJhc2UsIGNvbXBhcmUgbG9naWNhbCByZWFkcyBhbmQgQ1BVIGZyb20gU1RBVElTVElDUyBJTy9USU1FLDwvdGV4dD48dGV4dCB4PSI0ODAuMCIgeT0iNDAyLjg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iI2ZmZiI+dGhlbiBDUkVBVEUgSU5ERVggLi4uIFdJVEggKE9OTElORSA9IE9OKSBpbiBwcm9kdWN0aW9uIGFuZCByZS1yZWFkIFF1ZXJ5IFN0b3JlIGFmdGVyIGEgZnVsbCBidXNpbmVzcyBjeWNsZS48L3RleHQ+PC9zdmc+" alt="Index design decision sequence for expensive queries in SQL Server: equality columns first, one range column, order columns, INCLUDE for the select list, then write amplification, overlap, filtered and columnstore checks, then proof before production" width="960" height="440"><br><em>Figure 4. Indexing expensive queries: key design from the predicate, cost checks from the DMVs, proof before CREATE INDEX reaches production.</em></p>
<p>The missing index DMVs are a starting point for expensive queries and nothing more. They propose one index per query shape, never consolidate, over-include, and ignore the write side entirely. I read them together with the usage stats, so every proposal is weighed against what the table already carries and how hard it is written.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Missing index proposals weighed against existing index usage on the same table">SELECT   OBJECT_SCHEMA_NAME(mid.object_id) + '.' + OBJECT_NAME(mid.object_id)   AS table_name,
         mid.equality_columns,
         mid.inequality_columns,
         mid.included_columns,
         migs.user_seeks + migs.user_scans                                          AS would_have_used,
         CAST(migs.avg_total_user_cost * migs.avg_user_impact / 100.0
              * (migs.user_seeks + migs.user_scans) AS DECIMAL(18, 1))              AS improvement_measure,
         (SELECT COUNT(*) FROM sys.indexes i WHERE i.object_id = mid.object_id AND i.index_id &gt; 0) AS existing_indexes,
         (SELECT SUM(us.user_updates) FROM sys.dm_db_index_usage_stats us
           WHERE us.object_id = mid.object_id AND us.database_id = DB_ID())          AS index_writes_since_restart
FROM     sys.dm_db_missing_index_details AS mid
JOIN     sys.dm_db_missing_index_groups  AS mig  ON mig.index_handle = mid.index_handle
JOIN     sys.dm_db_missing_index_group_stats AS migs ON migs.group_handle = mig.index_group_handle
WHERE    mid.database_id = DB_ID()
ORDER BY improvement_measure DESC;</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Indexes that cost writes and return nothing: candidates to drop before adding more">SELECT   OBJECT_SCHEMA_NAME(i.object_id) + '.' + OBJECT_NAME(i.object_id)   AS table_name,
         i.name                                                              AS index_name,
         i.type_desc,
         i.is_unique,
         i.has_filter,
         us.user_seeks, us.user_scans, us.user_lookups, us.user_updates,
         us.last_user_seek, us.last_user_scan,
         ps.used_page_count * 8 / 1024                                       AS size_mb
FROM     sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS us
       ON us.object_id = i.object_id AND us.index_id = i.index_id AND us.database_id = DB_ID()
JOIN     sys.dm_db_partition_stats AS ps
      ON ps.object_id = i.object_id AND ps.index_id = i.index_id
WHERE    i.index_id &gt; 1                 -- nonclustered only
AND      i.is_primary_key = 0
AND      i.is_unique_constraint = 0
AND      OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1
ORDER BY ISNULL(us.user_seeks, 0) + ISNULL(us.user_scans, 0) + ISNULL(us.user_lookups, 0) ASC,
         us.user_updates DESC;</pre>
<p>Index usage stats reset on restart, so read them against the instance uptime from sys.dm_os_sys_info, and never drop an index on the strength of a counter that has only seen a fortnight; the quarter-end report that uses it is real. When I do add an index for one of the expensive queries, the DDL follows the design sequence exactly, and it goes in online and resumable so the operation can be paused if it starts hurting.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Covering index for the orders query: equality, then range, then INCLUDE; online and resumable on SQL Server 2022 and 2025 Enterprise">CREATE NONCLUSTERED INDEX ix_orders_customer_date
    ON dbo.orders (customer_id, order_date DESC)
    INCLUDE (status)
    WITH (ONLINE = ON, RESUMABLE = ON, MAX_DURATION = 60 MINUTES,
          SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE);

-- Pause and resume without losing progress if the window closes:
-- ALTER INDEX ix_orders_customer_date ON dbo.orders PAUSE;
-- ALTER INDEX ix_orders_customer_date ON dbo.orders RESUME;

-- Filtered variant when only open orders are ever queried this way:
CREATE NONCLUSTERED INDEX ix_orders_customer_date_open
    ON dbo.orders (customer_id, order_date DESC)
    INCLUDE (status)
    WHERE status IN ('NEW', 'PICKING', 'PACKED')
    WITH (ONLINE = ON, DATA_COMPRESSION = PAGE);</pre>
<p>Three notes on that DDL. The descending key on order_date lets the ORDER BY come straight off the index without a sort, which matters more than it looks on a statement that runs thousands of times an hour. </p>
<p>The filtered variant is smaller and cheaper to maintain, but it only serves queries whose predicate the optimizer can prove is contained in the filter, and a parameterised status predicate usually cannot be proved, so I check the plan rather than assume. And if the expensive query is an aggregation over millions of rows rather than a lookup of dozens, no rowstore key fixes it; a nonclustered columnstore index on the same table, with batch mode, is the right tool and does not interfere with the OLTP path.</p>
<h2>The fix ladder: reversible first, permanent second<a class="anchor-link" id="the-fix-ladder-reversible-first-permanent-second"></a></h2>
<p><img decoding="async" loading="lazy" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5NjAgMzMwIiB3aWR0aD0iOTYwIiBoZWlnaHQ9IjMzMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InQ5ODU3MSBkOTg1NzEiPjx0aXRsZSBpZD0idDk4NTcxIj5FeHBlbnNpdmUgcXVlcnkgdHJvdWJsZXNob290aW5nIGxvb3Agd2l0aCByb2xsYmFjazwvdGl0bGU+PGRlc2MgaWQ9ImQ5ODU3MSI+Rml2ZSBzdGVwcyBpbiBhIGxvb3A6IHJhbmssIHJlYWQsIGNoYW5nZSBvbmUgdGhpbmcsIHZlcmlmeSwga2VlcCBvciByb2xsIGJhY2ssIHdpdGggdGhlIHJldmVyc2libGUgbWVjaGFuaXNtcyBuYW1lZC48L2Rlc2M+PGRlZnM+PG1hcmtlciBpZD0iYXJyIiB2aWV3Qm94PSIwIDAgMTAgMTAiIHJlZlg9IjguNSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjciIG1hcmtlckhlaWdodD0iNyIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiIG1hcmtlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGxpbmUgeDE9IjEuNSIgeTE9IjEuNSIgeDI9IjguNSIgeTI9IjUiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bGluZSB4MT0iMS41IiB5MT0iOC41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjM2Q0ZjYyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9ImFycmIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOC41IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iNyIgbWFya2VySGVpZ2h0PSI3IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSIgbWFya2VyVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48bGluZSB4MT0iMS41IiB5MT0iMS41IiB4Mj0iOC41IiB5Mj0iNSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjxsaW5lIHgxPSIxLjUiIHkxPSI4LjUiIHgyPSI4LjUiIHkyPSI1IiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9tYXJrZXI+PC9kZWZzPjxyZWN0IHdpZHRoPSI5NjAiIGhlaWdodD0iMzMwIiBmaWxsPSIjZmZmIi8+PHRleHQgeD0iNDgwLjAiIHk9IjI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMGExYTJmIj5UaGUgbG9vcDogcmFuaywgcmVhZCwgY2hhbmdlIG9uZSB0aGluZywgdmVyaWZ5IGFnYWluc3QgdGhlIHNhbWUgUXVlcnkgU3RvcmUgaW50ZXJ2YWw8L3RleHQ+PHJlY3QgeD0iMzAiIHk9IjgwIiB3aWR0aD0iMTY0IiBoZWlnaHQ9IjkwIiByeD0iMiIgZmlsbD0iIzBhMWEyZiIgc3Ryb2tlPSIjMGExYTJmIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSIxMTIuMCIgeT0iMTEzLjE3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2ZmZiI+UmFuazwvdGV4dD48dGV4dCB4PSIxMTIuMCIgeT0iMTI4LjAyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iI2ZmZiI+UXVlcnkgU3RvcmUgcnVudGltZV9zdGF0czwvdGV4dD48dGV4dCB4PSIxMTIuMCIgeT0iMTQyLjg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iI2ZmZiI+YnkgdGhlIHF1ZXN0aW9uIGFza2VkPC90ZXh0PjxsaW5lIHgxPSIxOTQiIHkxPSIxMjUiIHgyPSIyMTYiIHkyPSIxMjUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyYikiLz48cmVjdCB4PSIyMTYiIHk9IjgwIiB3aWR0aD0iMTY0IiBoZWlnaHQ9IjkwIiByeD0iMiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjYzlkM2UwIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSIyOTguMCIgeT0iMTEzLjE3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iIzFhMjMzMiI+UmVhZDwvdGV4dD48dGV4dCB4PSIyOTguMCIgeT0iMTI4LjAyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+YWN0dWFsIHBsYW4sIHdhaXQgY2F0ZWdvcnksPC90ZXh0Pjx0ZXh0IHg9IjI5OC4wIiB5PSIxNDIuODc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5yb3dzIHJlYWQgdnMgcm93cyBvdXRwdXQ8L3RleHQ+PGxpbmUgeDE9IjM4MCIgeTE9IjEyNSIgeDI9IjQwMiIgeTI9IjEyNSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjEuNCIgbWFya2VyLWVuZD0idXJsKCNhcnJiKSIvPjxyZWN0IHg9IjQwMiIgeT0iODAiIHdpZHRoPSIxNjQiIGhlaWdodD0iOTAiIHJ4PSIyIiBmaWxsPSIjZmZmIiBzdHJva2U9IiNjOWQzZTAiIHN0cm9rZS13aWR0aD0iMSIvPjx0ZXh0IHg9IjQ4NC4wIiB5PSIxMTMuMTc1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iNzAwIiBmaWxsPSIjMWEyMzMyIj5DaGFuZ2Ugb25lIHRoaW5nPC90ZXh0Pjx0ZXh0IHg9IjQ4NC4wIiB5PSIxMjguMDI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWEyMzMyIj5wcmVkaWNhdGUsIHN0YXRpc3RpY3MsIGluZGV4LDwvdGV4dD48dGV4dCB4PSI0ODQuMCIgeT0iMTQyLjg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+aGludCBvciBmb3JjZWQgcGxhbjwvdGV4dD48bGluZSB4MT0iNTY2IiB5MT0iMTI1IiB4Mj0iNTg4IiB5Mj0iMTI1IiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS13aWR0aD0iMS40IiBtYXJrZXItZW5kPSJ1cmwoI2FycmIpIi8+PHJlY3QgeD0iNTg4IiB5PSI4MCIgd2lkdGg9IjE2NCIgaGVpZ2h0PSI5MCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIxIi8+PHRleHQgeD0iNjcwLjAiIHk9IjExMy4xNzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSI3MDAiIGZpbGw9IiMxYTIzMzIiPlZlcmlmeTwvdGV4dD48dGV4dCB4PSI2NzAuMCIgeT0iMTI4LjAyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+c2FtZSBxdWVyeV9pZCwgbmV4dCBpbnRlcnZhbDwvdGV4dD48dGV4dCB4PSI2NzAuMCIgeT0iMTQyLjg3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFhMjMzMiI+ZHVyYXRpb24sIGNwdSwgcmVhZHMsIHN0ZGV2PC90ZXh0PjxsaW5lIHgxPSI3NTIiIHkxPSIxMjUiIHgyPSI3NzQiIHkyPSIxMjUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIxLjQiIG1hcmtlci1lbmQ9InVybCgjYXJyYikiLz48cmVjdCB4PSI3NzQiIHk9IjgwIiB3aWR0aD0iMTY0IiBoZWlnaHQ9IjkwIiByeD0iMiIgZmlsbD0iIzBhMWEyZiIgc3Ryb2tlPSIjMGExYTJmIiBzdHJva2Utd2lkdGg9IjEiLz48dGV4dCB4PSI4NTYuMCIgeT0iMTEzLjE3NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IlNhcmFsYSwgQXJpYWwsIEhlbHZldGljYSwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2ZmZiI+S2VlcCBvciByb2xsIGJhY2s8L3RleHQ+PHRleHQgeD0iODU2LjAiIHk9IjEyOC4wMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiNmZmYiPnVuZm9yY2VfcGxhbiwgRFJPUCBJTkRFWCw8L3RleHQ+PHRleHQgeD0iODU2LjAiIHk9IjE0Mi44NzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiNmZmYiPm9yIGNsZWFyIHRoZSBoaW50PC90ZXh0PjxsaW5lIHgxPSI4NTIiIHkxPSIxNzAiIHgyPSI4NTIiIHkyPSIyMjAiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIvPjxsaW5lIHgxPSI4NTIiIHkxPSIyMjAiIHgyPSIxMTIiIHkyPSIyMjAiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIvPjxsaW5lIHgxPSIxMTIiIHkxPSIyMjAiIHgyPSIxMTIiIHkyPSIxNzIiIHN0cm9rZT0iIzNkNGY2MiIgc3Ryb2tlLXdpZHRoPSIxLjQiIHN0cm9rZS1kYXNoYXJyYXk9IjUgNCIgbWFya2VyLWVuZD0idXJsKCNhcnIpIi8+PHRleHQgeD0iNDgwIiB5PSIyNDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtc3R5bGU9Iml0YWxpYyIgZmlsbD0iIzNkNGY2MiI+dGhlIHNlY29uZC13b3JzdCBxdWVyeSBpcyBub3cgdGhlIHdvcnN0OyB0aGUgdmVyaWZpY2F0aW9uIG51bWJlcnMgYmVjb21lIHRoZSBuZXh0IGJhc2VsaW5lPC90ZXh0Pjx0ZXh0IHg9IjQ4MCIgeT0iMjgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iU2FyYWxhLCBBcmlhbCwgSGVsdmV0aWNhLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjM2Q0ZjYyIj5ldmVyeSBjaGFuZ2UgaGVyZSBpcyByZXZlcnNpYmxlIHdpdGhvdXQgYW4gb3V0YWdlOiB0aGF0IGlzIHRoZSByZWFzb24gdG8gcHJlZmVyIFF1ZXJ5IFN0b3JlIGhpbnRzIGFuZCBmb3JjZWQgcGxhbnMgb3ZlciBjb2RlIGNoYW5nZXMgYXMgdGhlIGZpcnN0IHN0ZXA8L3RleHQ+PHRleHQgeD0iNDgwIiB5PSIzMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJTYXJhbGEsIEFyaWFsLCBIZWx2ZXRpY2EsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtc3R5bGU9Iml0YWxpYyIgZmlsbD0iIzNkNGY2MiI+KGNvZGUgY2hhbmdlcyBhcmUgc3RpbGwgdGhlIHJpZ2h0IGxvbmctdGVybSBmaXg7IHRoZXkganVzdCBhcmUgbm90IHRoZSBmaXJzdCBtb3ZlIGF0IDAyOjAwKTwvdGV4dD48L3N2Zz4=" alt="Expensive query troubleshooting loop for SQL Server: rank in Query Store, read the actual plan, change one thing, verify against the next interval, keep or roll back with unforce plan, drop index or clear hint" width="960" height="330"><br><em>Figure 5. The loop for expensive queries: one change per iteration, verified against the same query_id in the next Query Store interval, with the rollback named before the change is made.</em></p>
<p>What makes SQL Server unusual among the engines I work on is how much of the fix ladder for expensive queries is reversible without a deployment. Query Store hints attach OPTION clauses to a query_id without touching application code; plan forcing pins a known-good plan_id while the real fix is engineered; and on 2025 ABORT_QUERY_EXECUTION stops a query shape outright. All three are undone with one procedure call. I use them as the first move and the index or rewrite as the second, because at 02:00 the reversible fix is the safe one, and because a forced plan buys the time to do the permanent fix properly.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Reversible fixes for an expensive query: hint, force, or block, and how each is undone">-- 1. A hint without a code change: cap the memory grant and pin the estimator behaviour
EXEC sys.sp_query_store_set_hints
     @query_id = 40217,
     @query_hints = N'OPTION (MAX_GRANT_PERCENT = 10, USE HINT(''DISABLE_CE_FEEDBACK''))';

-- Verify it took: hint_id and the query_hints text
SELECT query_id, query_hint_text, source_desc FROM sys.query_store_query_hints WHERE query_id = 40217;

-- Undo
EXEC sys.sp_query_store_clear_hints @query_id = 40217;

-- 2. Force the plan that behaved, while the permanent fix is built
EXEC sys.sp_query_store_force_plan @query_id = 40217, @plan_id = 9877;

-- Verify: is_forced_plan = 1 and no force_failure_count growth
SELECT plan_id, is_forced_plan, force_failure_count, last_force_failure_reason_desc
FROM   sys.query_store_plan WHERE query_id = 40217;

-- Undo
EXEC sys.sp_query_store_unforce_plan @query_id = 40217, @plan_id = 9877;

-- 3. SQL Server 2025 only: stop a runaway query shape at compile time (circuit breaker, not a fix)
EXEC sys.sp_query_store_set_hints
     @query_id = 40217,
     @query_hints = N'OPTION (USE HINT(''ABORT_QUERY_EXECUTION''))';
-- Undo with sp_query_store_clear_hints as above</pre>
<p>Two cautions from experience with expensive queries and forced plans. A forced plan that references an index you later drop fails silently to force and the query falls back to whatever the optimizer picks; force_failure_count is the only place that shows it, so I alert on it. And hints that disable feedback mechanisms should be dated and reviewed, because the whole reason the mechanism existed was to fix the class of problem the hint is masking.</p>
<p>Verification is the same ranking of expensive queries that found the problem, filtered to the query_id, one interval later. I want to see avg_duration_ms and reads_per_row move in the right direction and duration_cv fall, on the same executions count; if executions also collapsed, someone changed the application and I have not measured anything. I keep the before and after rows in the ticket, because the next time someone asks whether the index was worth its write cost, those two rows are the answer.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Before and after for one query_id, interval by interval">SELECT   i.start_time,
         rs.plan_id,
         rs.count_executions,
         CAST(rs.avg_duration / 1000.0 AS DECIMAL(18, 2))                                    AS avg_duration_ms,
         CAST(rs.avg_cpu_time / 1000.0 AS DECIMAL(18, 2))                                    AS avg_cpu_ms,
         CAST(rs.avg_logical_io_reads / NULLIF(rs.avg_rowcount, 0) AS DECIMAL(18, 1))        AS reads_per_row,
         CAST(rs.stdev_duration / NULLIF(rs.avg_duration, 0) AS DECIMAL(9, 3))               AS duration_cv,
         rs.max_dop
FROM     sys.query_store_runtime_stats          AS rs
JOIN     sys.query_store_runtime_stats_interval AS i ON i.runtime_stats_interval_id = rs.runtime_stats_interval_id
JOIN     sys.query_store_plan                   AS p ON p.plan_id = rs.plan_id
WHERE    p.query_id = 40217
AND      i.start_time &gt;= DATEADD(DAY, -3, SYSUTCDATETIME())
ORDER BY i.start_time, rs.plan_id;</pre>
<h2>Expensive queries: fixes that did not help, and the ones I do not do any more<a class="anchor-link" id="expensive-queries-fixes-that-did-not-help-and-the-ones-i-do-not-do-any-more"></a></h2>
<p>Adding MAXDOP hints to expensive queries that were slow because of a spill did nothing except make them serial and slower. Rebuilding indexes to fix an expensive query has, in my experience, fixed the query exactly as often as the rebuild happened to refresh statistics that were the actual problem, which is why I now update statistics with FULLSCAN first and rebuild only for fragmentation that the plan shows is hurting. </p>
<p>Turning off parameter sniffing instance-wide traded one set of expensive queries for another and made every plan mediocre; PSP optimization and OPPO exist precisely so that trade is no longer necessary on 2022 and 2025. And clearing the plan cache &ldquo;to see if it helps&rdquo; is a way of losing the evidence you needed while creating a compilation storm.</p>
<h2>Where MinervaDB fits<a class="anchor-link" id="where-minervadb-fits"></a></h2>
<p>MinervaDB delivers <a href="https://minervadb.com/sql-server-support/">SQL Server consulting, 24&times;7 consultative support and remote DBA services</a> for on-premises, Always On and Azure SQL estates. Work on expensive queries is a standing part of it: Query Store based performance health checks, 2019 and 2022 to 2025 upgrade assessments that stage the compatibility level jump behind Query Store and review optimized locking before it is enabled, and index estate reviews that remove the dead weight before adding anything. We work alongside your DBAs and your Microsoft support agreement rather than in place of either.</p>
<p>Test every query, hint, forced plan and index in this post on a non-production copy of your database with a representative workload before you apply it to production, keep backups and a tested restore path current so that every change has a rollback, and maintain your disaster recovery posture throughout. The method for expensive queries holds across workloads. Which of these fixes pays on yours is something only your Query Store can tell you.</p>

<p><a href="https://minervadb.com/sql-server-expensive-queries/">SQL Server 2025 Expensive Queries: 7 Proven Ways to Find and Fix Them by Latency and Resource Cost</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>A Fail-Fast PostgreSQL Migration Preflight for CI</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/09/08/postgresql-migration-preflight-ci/" />
      <id>https://percona.community/blog/2026/09/08/postgresql-migration-preflight-ci/</id>
      <updated>2026-09-08T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>A database migration can be syntactically correct and still be a poor release candidate. A regular index build can block writes, an ALTER TABLE may wait behind a conflicting lock or hold a strong lock longer than expected, and a large data modification can turn a routine deployment into an incident.</p>
<p><a href="https://percona.community/blog/2026/09/08/postgresql-migration-preflight-ci/">A Fail-Fast PostgreSQL Migration Preflight for CI</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A database migration can be syntactically correct and still be a poor release candidate. A regular index build can block writes, an <code>ALTER TABLE</code> may wait behind a conflicting lock or hold a strong lock longer than expected, and a large data modification can turn a routine deployment into an incident.</p>
<p>Static analysis cannot determine whether a migration is safe in production. It does not know table size, traffic, open transactions, data distribution, or how the migration runner executes the file. But it can catch a smaller class of mistakes where the SQL itself provides strong evidence of operational risk.</p>
<p>I built <a href="https://github.com/yZangEren/postgres-migration-preflight" target="_blank" rel="noopener noreferrer">postgres-migration-preflight</a> as an experiment in that narrower problem. It parses PostgreSQL SQL into an abstract syntax tree (AST), evaluates statements in source order, and exits non-zero when configured blocking rules fire. The examples and documentation references below use PostgreSQL 18.</p>
<p>The goal is not to certify a migration as production-safe. It is to make obvious hazards difficult to merge unnoticed.</p>
<h2>Focus on risks visible from SQL<a class="anchor-link" id="focus-on-risks-visible-from-sql"></a></h2>
<p>The first version deliberately limits itself to rules that can be explained from the migration text:</p>
<ul>
<li>regular <code>CREATE INDEX</code> and <code>DROP INDEX</code> operations;</li>
<li>selected lock-sensitive <code>ALTER TABLE</code> operations;</li>
<li>destructive DDL such as <code>DROP TABLE</code> and <code>TRUNCATE</code>;</li>
<li><code>UPDATE</code> or <code>DELETE</code> statements without a <code>WHERE</code> clause;</li>
<li>DDL without a preceding non-zero session-level <code>lock_timeout</code>;</li>
<li>SQL that the parser cannot understand.</li>
</ul>
<p>These rules are heuristics, not proofs. A normal <code>CREATE INDEX</code> allows reads but blocks inserts, updates, and deletes while the index is built. <code>CREATE INDEX CONCURRENTLY</code> avoids blocking those writes, but PostgreSQL performs two table scans, waits for relevant transactions, does more total work, and can leave an invalid index after certain failures. It also cannot run inside a transaction block. A migration framework may create such a transaction even when the SQL file contains no <code>BEGIN</code>, so the finding asks the reviewer to check both the index operation and the runner.</p>
<p>A normal <code>DROP INDEX</code> acquires an <code>ACCESS EXCLUSIVE</code> lock on the table. Its concurrent form reduces that interference but cannot run in a transaction block, cannot use <code>CASCADE</code>, and cannot remove an index that supports a <code>UNIQUE</code> or <code>PRIMARY KEY</code> constraint.</p>
<p><code>ALTER TABLE</code> is a family of operations rather than one risk category. A column type change normally rewrites the table, but PostgreSQL can avoid the rewrite for some binary-compatible conversions. <code>SET NOT NULL</code> ordinarily scans existing rows, but can skip the scan when a valid <code>CHECK</code> constraint proves that no <code>NULL</code> can exist. A foreign key added as <code>NOT VALID</code> can enforce new writes before a separate <code>VALIDATE CONSTRAINT</code> checks existing rows with a less disruptive lock.</p>
<p>The current lab still blocks every column type change conservatively because it has no catalog context. That severity is a policy choice for review, not proof that a particular conversion will rewrite a table.</p>
<h2>Preserve order and fail closed<a class="anchor-link" id="preserve-order-and-fail-closed"></a></h2>
<p>Some protections matter only when they appear before the statement they are meant to protect:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">TYPE</span><span class="w"> </span><span class="nb">varchar</span><span class="p">(</span><span class="mi">320</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">lock_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'2s'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>The timeout does not protect the preceding <code>ALTER TABLE</code>. The analyzer walks statements in source order and tracks the session-level <code>lock_timeout</code>. A non-zero value counts only for subsequent DDL, and setting it back to <code>0</code> disables the protection again.</p>
<p>This is a review signal, not a claim that a timeout makes DDL safe. <code>lock_timeout</code> limits only time spent waiting to acquire a lock. After the lock is acquired, an operation can continue much longer. An equal or shorter <code>statement_timeout</code> can also fire first.</p>
<p>The analyzer uses <code>pgsql-ast-parser</code> instead of regular expressions so supported rules operate on statement structure. It can distinguish an <code>UPDATE</code> with a predicate, a concurrent index declaration, and individual <code>ALTER TABLE</code> actions. An AST still does not provide catalog state or workload context. If the parser cannot understand valid PostgreSQL syntax, the tool emits <code>SQL_PARSE_ERROR</code> at blocker severity and exits non-zero. Parser failures are coverage gaps requiring review, not evidence that PostgreSQL would reject the migration.</p>
<h2>Compare passing and blocked migrations<a class="anchor-link" id="compare-passing-and-blocked-migrations"></a></h2>
<p>A deliberately small passing example is:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">lock_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'2s'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">statement_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'15min'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">CONCURRENTLY</span><span class="w"> </span><span class="n">idx_users_last_seen_at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">last_seen_at</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">archived</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">42</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>The checker parses four statements and reports no findings. That result does not make the migration automatically safe: the caller still has to ensure the concurrent index statement is not executed inside a transaction block.</p>
<p>The blocked example contains stronger static signals:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="n">idx_users_email</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">users</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ALTER</span><span class="w"> </span><span class="k">COLUMN</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">TYPE</span><span class="w"> </span><span class="nb">varchar</span><span class="p">(</span><span class="mi">320</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">DELETE</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">audit_events</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>The checker reports five findings: two critical, one high, and two medium. The critical findings cover the column type change and unbounded delete. The regular index build is high severity, while the medium findings identify DDL without a preceding non-zero <code>lock_timeout</code>. The process exits with code <code>1</code>, so CI can stop before deployment.</p>
<p>The examples test rule behavior, not the hardest PostgreSQL migration problems. A <code>WHERE</code> clause can still affect most rows of a large table, and a binary-compatible type change can be much cheaper than another statement with similar syntax. Production-scale changes still require workload-aware review.</p>
<h2>Give CI a simple contract<a class="anchor-link" id="give-ci-a-simple-contract"></a></h2>
<p>The CLI exits with <code>0</code> when no blocking finding exists, <code>1</code> when the migration is blocked, and <code>2</code> for invalid command usage. A minimal workflow is:</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-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install migration preflight</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm ci</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Test the analyzer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">npm test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check the pending migration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">node src/check-migration.mjs path/to/migration.sql</span></span></span></code></pre>
</div>
</div>
</div>
<p>JSON output can feed CI annotations or review tooling. The current test suite covers the passing case, the risky case, ordered timeout handling, and parser failure. I would introduce a gate gradually: start with rules supported by strong SQL evidence, measure false positives, and only then make broader organization-specific policies blocking.</p>
<p>The checker reports findings rather than rewriting migrations. Adding <code>CONCURRENTLY</code> can change transaction requirements and recovery steps, while splitting constraint creation and validation changes deployment sequencing. Those decisions still belong to the developer and reviewer.</p>
<h2>Keep the boundary explicit<a class="anchor-link" id="keep-the-boundary-explicit"></a></h2>
<p>SQL text cannot reveal relation size, data distribution, long-running transactions, current lock holders, replica lag, WAL capacity, application traffic, or a migration framework&rsquo;s transaction policy. PostgreSQL versions also differ in DDL behavior and available optimizations.</p>
<p>A passing preflight should therefore be followed by production-sized rehearsal and runtime verification for consequential migrations. That may include checking expected scans or rewrites, monitoring locks and replica lag, planning rollback procedures, and verifying constraints and indexes afterward. Following a failed concurrent index build, catalog state such as <code>pg_index.indisvalid</code> also needs inspection.</p>
<p>The value of a static gate is not that it understands production. Its value is that some mistakes do not require production knowledge to recognize. Catching those mistakes in CI leaves reviewers more time for the harder questions that static analysis cannot answer.</p>
<p>That is the boundary I want <code>postgres-migration-preflight</code> to maintain: a small, explainable first line of defense for PostgreSQL migrations, not a production-readiness oracle.</p>
<h2>References<a class="anchor-link" id="references"></a></h2>
<ul>
<li><a href="https://www.postgresql.org/docs/18/indexes-intro.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: Introduction to indexes</a></li>
<li><a href="https://www.postgresql.org/docs/18/sql-createindex.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: CREATE INDEX</a></li>
<li><a href="https://www.postgresql.org/docs/18/sql-dropindex.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: DROP INDEX</a></li>
<li><a href="https://www.postgresql.org/docs/18/sql-altertable.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: ALTER TABLE</a></li>
<li><a href="https://www.postgresql.org/docs/18/runtime-config-client.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: Client connection defaults, including <code>lock_timeout</code></a></li>
<li><a href="https://www.postgresql.org/docs/18/catalog-pg-index.html" target="_blank" rel="noopener noreferrer">PostgreSQL 18: <code>pg_index</code> catalog</a></li>
</ul>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>

<p><a href="https://percona.community/blog/2026/09/08/postgresql-migration-preflight-ci/">A Fail-Fast PostgreSQL Migration Preflight for CI</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Enterprise Server Maintenance Releases</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-enterprise-server-maintenance-releases/" />
      <id>https://mariadb.com/resources/blog/mariadb-enterprise-server-maintenance-releases/</id>
      <updated>2026-09-07T17:50:25+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>New security maintenance releases for MariaDB Enterprise Server: 11.8.9-6, 11.4.13-10, and 10.6.28-24 are now available. Download Now Notable Release Updates […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-maintenance-releases/">MariaDB Enterprise Server Maintenance Releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>New security maintenance releases for MariaDB Enterprise Server: 11.8.9-6, 11.4.13-10, and 10.6.28-24 are now available. Download Now MariaDB Enterprise Server is an enhanced, hardened and secured version of MariaDB Community Server that delivers enterprise reliability, stability and long-term support as well as greater operational efficiency when&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-maintenance-releases/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-maintenance-releases/">MariaDB Enterprise Server Maintenance Releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>SQL Server 2025 Performance, Scalability and High Availability: What Changed On-Premises and in the Cloud</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/sql-server-2025-performance-scalability-ha/" />
      <id>https://minervadb.com/sql-server-2025-performance-scalability-ha/</id>
      <updated>2026-09-07T11:13:51+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>SQL Server 2025 went generally available on 18 November 2025 and, as of this writing, sits at Cumulative Update 8 (build 17.0.4075.5, 13 August 2026). Strip away the AI features that took most of the [...]</p>
<p><a href="https://minervadb.com/sql-server-2025-performance-scalability-ha/">SQL Server 2025 Performance, Scalability and High Availability: What Changed On-Premises and in the Cloud</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/sql-server-2025-performance-scalability-high-availability.png" alt="SQL Server 2025 performance, scalability and high availability on-premises and in the cloud" width="1200" height="630" class="aligncenter size-full wp-image-93027"></p>
<p>SQL Server 2025 went generally available on 18 November 2025 and, as of this writing, sits at Cumulative Update 8 (build 17.0.4075.5, 13 August 2026). Strip away the AI features that took most of the launch coverage and there are four changes in this release that alter how a production SQL Server behaves under load and during a failover.</p>
<p>The first is optimized locking, which replaces per-row locks held to commit with a single transaction-ID lock. The second is tempdb space governance and ADR in tempdb, which turn the most common cause of a 3 a.m. outage into a per-workload error. The third is a set of Always On availability group knobs (a configurable group commit time, endpoint flow control, immediate failover on persistent health issues, full and differential backups on secondaries). The fourth is a new Standard edition ceiling of 32 cores and 256 GB of buffer pool with Resource Governor included, which is the largest scalability change for mid-market estates since 2016 SP1.</p>
<p>This post works through each of them for SQL Server 2025 performance, scalability and high availability, with the T-SQL to enable and verify them, and then lays out what is and is not available when the same engine runs in Azure SQL Managed Instance, Azure SQL Database, and Amazon RDS.</p>
<p>Everything here is version-pinned to 17.x and was checked against the current Microsoft Learn documentation. Where a feature is preview-only behind <code>PREVIEW_FEATURES</code>, I say so, because those are not production features however good the demo looks.</p>
<h2>Which SQL Server 2025 build you should be on<a class="anchor-link" id="which-sql-server-2025-build-you-should-be-on"></a></h2>
<p>The release cadence has settled into a monthly CU. The builds that matter for a production plan:</p>
<div>
<table>
<thead>
<tr>
<th>Build</th>
<th>Version</th>
<th>Date</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody>
<tr>
<td>RTM (GA)</td>
<td>17.0.1000.x</td>
<td>18 Nov 2025</td>
<td>Lifecycle start; mainstream support ends 7 Jan 2031, extended 7 Jan 2036</td>
</tr>
<tr>
<td>CU3</td>
<td>17.0.4025.3</td>
<td>12 Mar 2026</td>
<td>Microsoft Entra authentication for Change Event Streaming on Arc-enabled and Azure VM instances</td>
</tr>
<tr>
<td>CU6 + GDR</td>
<td>17.0.4060.2</td>
<td>14 Jul 2026</td>
<td>Security-only branch for estates that cannot take CUs</td>
</tr>
<tr>
<td>CU8</td>
<td>17.0.4075.5</td>
<td>13 Aug 2026</td>
<td>Current CU at time of writing; the baseline I recommend for a new deployment</td>
</tr>
</tbody>
</table>
</div>
<p>Two edition facts change the sizing conversation before any feature does. Standard edition now runs on the lesser of 4 sockets or 32 cores and addresses 256 GB of buffer pool, up from 24 cores and 128 GB in 2022, and it gets Resource Governor for the first time. Web edition is discontinued, and Express grows to a 50 GB database limit with the Advanced Services features folded in. A great many Enterprise licences in the field exist only because a 24-core or 128 GB ceiling was in the way; on SQL Server 2025 that argument needs re-examining with the actual workload numbers.</p>
<h2>Performance: optimized locking is the feature to plan around<a class="anchor-link" id="performance-optimized-locking-is-the-feature-to-plan-around"></a></h2>
<p>Optimized locking arrived in Azure SQL Database in 2023 and is on by default there. SQL Server 2025 brings it on-premises as an opt-in per database, and it is the single largest change to the engine&rsquo;s concurrency behaviour since row versioning. It has two parts. Transaction ID (TID) locking: every row already carries the transaction ID of the last writer, so instead of holding an X lock on every modified row until commit, the engine takes an X lock on the transaction ID itself and releases the row and page locks as soon as each row is written; anyone who needs to wait for that row takes an S lock on the TID.</p>
<p>Lock after qualification (LAQ): with read committed snapshot isolation on, an UPDATE or DELETE evaluates its predicate against the latest committed row version without taking a U lock first, and only locks rows that actually qualify.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/sql-server-2025-optimized-locking-lock-lifetime.png" alt="SQL Server 2025 optimized locking: lock lifetime for a 1,000-row UPDATE with and without TID locking and lock after qualification" width="1800" height="800" class="aligncenter size-full wp-image-93028"><figcaption>With SQL Server 2025 optimized locking the lock count for a large UPDATE goes from proportional to rows touched to constant. That is why lock memory and escalation both fall away.</figcaption></figure>
<p>Enabling it on SQL Server 2025 is three statements, in this order, because optimized locking requires accelerated database recovery (the persistent version store is what makes releasing row locks early safe) and LAQ only works under RCSI:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- SQL Server 2025: enable optimized locking on one database, in dependency order.
-- ADR is a prerequisite; RCSI is required for the LAQ half of the feature.
-- ALTER DATABASE ... SET READ_COMMITTED_SNAPSHOT needs exclusive access: schedule it.
ALTER DATABASE [ops] SET ACCELERATED_DATABASE_RECOVERY = ON;
ALTER DATABASE [ops] SET READ_COMMITTED_SNAPSHOT = ON WITH ROLLBACK IMMEDIATE;
ALTER DATABASE [ops] SET OPTIMIZED_LOCKING = ON;

-- Verify all three; every column must be 1
SELECT name,
       is_accelerated_database_recovery_on,
       is_read_committed_snapshot_on,
       is_optimized_locking_on
FROM   sys.databases
WHERE  name = N'ops';

-- Rollback path: reverse order. Optimized locking must be off before ADR can be turned off.
-- ALTER DATABASE [ops] SET OPTIMIZED_LOCKING = OFF;
-- ALTER DATABASE [ops] SET ACCELERATED_DATABASE_RECOVERY = OFF;</pre>
<p>What you see afterwards in <code>sys.dm_tran_locks</code> is a single <code>XACT</code> resource per writing transaction instead of a list of <code>KEY</code> and <code>PAGE</code> entries. Blocking shows up as the three new wait types, and the waiting session&rsquo;s <code>wait_resource</code> reads <code>XACT:</code> followed by the transaction ID:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- SQL Server 2025: who is holding the TID lock, and who is waiting on it
SELECT l.request_session_id  AS spid,
       l.resource_type,          -- XACT for TID locks
       l.request_mode,           -- X = writer, S = waiter
       l.request_status,
       l.resource_description,
       r.wait_type,              -- LCK_M_S_XACT_MODIFY / LCK_M_S_XACT_READ / LCK_M_S_XACT
       r.wait_time
FROM   sys.dm_tran_locks AS l
LEFT JOIN sys.dm_exec_requests AS r
       ON r.session_id = l.request_session_id
WHERE  l.resource_type = N'XACT'
ORDER  BY l.request_status, l.request_session_id;

-- Is LAQ being skipped on your workload? locking_stats fires every few minutes per database
CREATE EVENT SESSION [laq_watch] ON SERVER
ADD EVENT sqlserver.locking_stats,
ADD EVENT sqlserver.lock_after_qual_stmt_abort
ADD TARGET package0.event_file (SET filename = N'laq_watch')
WITH (STARTUP_STATE = ON);
ALTER EVENT SESSION [laq_watch] ON SERVER STATE = START;</pre>
<p>Now the part that has to be in the change ticket. LAQ changes the answer a query can return under concurrent writes, because it evaluates the predicate against the last committed version rather than blocking on the uncommitted one. Microsoft&rsquo;s own example is the honest one: transaction one runs <code>UPDATE t SET b = 2 WHERE a = 1</code> and has not committed; transaction two runs <code>UPDATE t SET b = 3 WHERE b = 2</code>. Without LAQ, two blocks, then sees <code>b = 2</code> and updates it. With LAQ, two reads the committed <code>b = 1</code>, the predicate fails, and it updates nothing.</p>
<p>Neither result is wrong under READ COMMITTED, but any application that silently depended on the blocking order will behave differently. If a workload relies on that order, it needs REPEATABLE READ or SERIALIZABLE on those statements, not a global rollback of the feature. LAQ also steps aside on its own for statements with <code>UPDLOCK</code>, <code>XLOCK</code>, <code>HOLDLOCK</code> or <code>READCOMMITTEDLOCK</code> hints, on tables with a columnstore index, for MERGE, and for statements with variable assignment or an OUTPUT clause; the <code>locking_stats</code> event above is how you find out how often that is happening.</p>
<p>My deployment stance: enable on one busy OLTP database at a time, after ADR has been on for at least a full business cycle so the persistent version store size is known, with the deadlock and blocking monitors already comparing against a pre-change baseline. Expect the first thing to disappear to be lock escalation on wide UPDATEs, and the wait type that appears in its place to be <code>LCK_M_S_XACT_MODIFY</code> where <code>LCK_M_U</code> used to be.</p>
<h3>The rest of the query-processing list<a class="anchor-link" id="the-rest-of-the-query-processing-list"></a></h3>
<p>Optional parameter plan optimization extends the parameter-sensitive plan machinery from 2022 to the <code>WHERE (@p IS NULL OR col = @p)</code> pattern that every reporting stored procedure has; it compiles separate plans for the NULL and non-NULL cases instead of one plan that is wrong for one of them. Degree of parallelism feedback, which was off by default in 2022, is now on: Query Store watches repeated executions and lowers MAXDOP for queries whose parallelism is not paying for itself.</p>
<p>Cardinality estimation feedback now covers expressions, not just join and containment assumptions. All three are compatibility level 170 behaviours with Query Store on, which is the pattern since 2022: the intelligent query processing features are Query Store features, so a database with Query Store off gets none of them.</p>
<p>The <code>ABORT_QUERY_EXECUTION</code> hint is small and useful. Attached through Query Store hints, it stops a known-bad query shape from executing at all until someone fixes it, which is the right answer to the runaway report that a business user re-runs every ten minutes:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Find the query_id from Query Store, then pin the hint to it. Nobody can run this shape now.
EXEC sys.sp_query_store_set_hints
     @query_id      = 4711,
     @query_hints   = N'OPTION (ABORT_QUERY_EXECUTION)';

-- Verify, and remove when the query is rewritten
SELECT query_id, query_hint_text, source_desc
FROM   sys.query_store_query_hints;

EXEC sys.sp_query_store_clear_hints @query_id = 4711;</pre>
<h3>tempdb: governance and ADR<a class="anchor-link" id="tempdb-governance-and-adr"></a></h3>
<p>Two tempdb changes in SQL Server 2025, and the first is the one I would put on every SQL Server 2025 instance on day one. Resource Governor can now cap the tempdb data space a workload group may use, and a session that crosses the cap gets error 1138 instead of filling the drive for everyone. The trap is that a percentage limit is only in force when the tempdb data files are either all capped with autogrow, or all uncapped with autogrow off; the default install (unlimited MAXSIZE, autogrow on) silently satisfies neither, and you get warning 10989 rather than a working limit. Use the MB form unless you have re-laid tempdb.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- SQL Server 2025 tempdb governance: cap ad hoc reporting at 64 GB of tempdb data; leave the OLTP group unlimited
CREATE WORKLOAD GROUP [wg_reporting]
    WITH (GROUP_MAX_TEMPDB_DATA_MB = 65536,
          MAX_DOP = 8)
    USING [default];
ALTER RESOURCE GOVERNOR RECONFIGURE;

-- What each group is using right now, and how often the cap has fired
SELECT g.name,
       g.tempdb_data_space_kb / 1024                AS tempdb_mb_now,
       g.peak_tempdb_data_space_kb / 1024           AS tempdb_mb_peak,
       g.total_tempdb_data_limit_violation_count    AS cap_hits
FROM   sys.dm_resource_governor_workload_groups AS g
ORDER  BY g.peak_tempdb_data_space_kb DESC;

-- Error the offending session sees:
-- Msg 1138, Level 17: Could not allocate a new page for database 'tempdb'
-- because that would exceed the limit set for workload group 'wg_reporting'.</pre>
<p>What is counted: temp tables, table variables, table-valued parameters, cursors, and every spill (sorts, hash, spools). What is not: the version store, including the ADR persistent version store, and the tempdb log. Global temp tables are charged to whichever group inserted the first row, which is a fairness problem you should know about before you cap a shared ETL group. Standard edition has Resource Governor now, so this applies to the mid-market estate too.</p>
<p>The second change is accelerated database recovery inside tempdb. Long rollbacks of temp-table-heavy batches used to hold the engine hostage exactly the way long rollbacks in user databases did before 2019; with ADR in tempdb they become instant. On Linux, tempdb can additionally sit on <code>tmpfs</code>, which is worth measuring on a memory-rich host before assuming NVMe is fast enough.</p>
<h3>Backups: ZSTD and immutable targets<a class="anchor-link" id="backups-zstd-and-immutable-targets"></a></h3>
<p>SQL Server 2025 backup compression gains a ZSTD option alongside the MS_XPRESS default and QAT. Microsoft&rsquo;s claim is faster and smaller than MS_XPRESS; measure your own backup and restore windows before switching a fleet, because ZSTD trades CPU for ratio and a CPU-bound instance will notice:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">BACKUP DATABASE [ops]
TO URL = N'https://${STORAGE_ACCOUNT}.blob.core.windows.net/backups/ops_full.bak'
WITH COMPRESSION (ALGORITHM = ZSTD),
     CHECKSUM,
     STATS = 5;

-- Compression ratio per backup, to justify the change with a number
SELECT TOP (20) b.database_name, b.backup_start_date,
       b.backup_size / 1048576.0            AS size_mb,
       b.compressed_backup_size / 1048576.0 AS compressed_mb,
       b.backup_size * 1.0 / NULLIF(b.compressed_backup_size, 0) AS ratio
FROM   msdb.dbo.backupset AS b
WHERE  b.type = 'D'
ORDER  BY b.backup_start_date DESC;</pre>
<p>Backup to URL now works against immutable blob storage, which is the ransomware answer that used to require a third-party product, and on an availability group secondary it can now be a full or differential backup rather than copy-only, which changes how the backup preference on an AG should be set. More on that below.</p>
<h2>Scalability: the Standard edition ceiling and read scale-out<a class="anchor-link" id="scalability-the-standard-edition-ceiling-and-read-scale-out"></a></h2>
<p>The scalability story in SQL Server 2025 is less about a single engine change than about where the walls moved. A 32-core, 256 GB Standard edition instance is a serious OLTP server. Combined with Resource Governor to protect it from itself, tempdb governance, and optimized locking to keep lock memory flat, the workload that needed Enterprise for headroom reasons in 2022 frequently does not in SQL Server 2025.</p>
<p>The features that remain Enterprise-only and still matter for scale are the ones that always did: online index operations at full scope, partitioned table parallelism at its best, unlimited memory, and the full Always On availability group feature set (Standard is limited to basic availability groups with one database per group and no readable secondary).</p>
<p>For read scale-out on Enterprise, two small changes make readable secondaries first-class rather than a compromise. Query Store is now on by default for readable secondaries, so the plan regressions on the reporting replica are visible and fixable with the same hints mechanism as the primary. And SQL Server 2025 creates persisted statistics on readable secondaries; before this, statistics the secondary needed but the primary never built lived in tempdb on the secondary and evaporated on restart, which is why reporting on a secondary was slow every Monday morning after patching.</p>
<p>Columnstore gets ordered nonclustered columnstore indexes (the ordered clustered form arrived in 2022), online build for them, and a batch-mode path for a set of built-in functions and <code>DATETRUNC</code>. For an HTAP-style operational reporting table that is the combination that lets a single instance carry both the OLTP write path and the aggregate reads without a separate analytics copy:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- SQL Server 2025 ordered NCCI, built online, on a hot OLTP table used for operational reporting.
-- ORDER controls rowgroup elimination on the reporting predicate; keep it to one or two columns.
CREATE NONCLUSTERED COLUMNSTORE INDEX ncci_order_line_reporting
    ON sales.order_line (order_date, region_id, sku_id, quantity, net_amount)
    ORDER (order_date)
    WITH (ONLINE = ON, MAXDOP = 4, COMPRESSION_DELAY = 10 MINUTES);

-- Rowgroup elimination evidence after a day of load
SELECT rg.state_desc, COUNT(*) AS rowgroups,
       MIN(rg.total_rows) AS min_rows, MAX(rg.total_rows) AS max_rows
FROM   sys.dm_db_column_store_row_group_physical_stats AS rg
WHERE  rg.object_id = OBJECT_ID(N'sales.order_line')
GROUP  BY rg.state_desc;</pre>
<h2>High availability: what changed in Always On<a class="anchor-link" id="high-availability-what-changed-in-always-on"></a></h2>
<p>None of the availability group changes in SQL Server 2025 is a headline feature and together they are the most useful HA release since 2016. Each one is a knob for a specific failure we have all watched happen.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/sql-server-2025-availability-group-new-knobs.png" alt="SQL Server 2025 availability group topology showing where commit time, flow control, restart threshold, backups on secondary and TDS 8 apply" width="1800" height="860" class="aligncenter size-full wp-image-93029"><figcaption>Nothing here is new topology. Every box carries a SQL Server 2025 change that shortens a specific part of the failover or offloads a specific job from the primary.</figcaption></figure>
<p>Availability group commit time is the one to test carefully, and the first thing to know is which way it works. The primary already batches commits for up to 10 milliseconds before shipping the log block to secondaries; that grouping is what keeps a busy AG from saturating the network with tiny sends. SQL Server 2025 exposes the window as an instance-level <code>sp_configure</code> option (default 0, meaning the built-in 10 ms), and the use case is lowering it: a latency-sensitive workload with modest transaction volume can shave up to 10 ms off synchronous commit at the cost of more, smaller sends.</p>
<p>On a high-volume system leave it alone; the batching is doing you a favour. Measure with <code>HADR_SYNC_COMMIT</code> waits per transaction before and after, at the same time of day:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- SQL Server 2025 AG commit time: baseline first
SELECT wait_type, waiting_tasks_count, wait_time_ms,
       wait_time_ms * 1.0 / NULLIF(waiting_tasks_count, 0) AS avg_ms
FROM   sys.dm_os_wait_stats
WHERE  wait_type IN (N'HADR_SYNC_COMMIT', N'WRITELOG');

-- Then shorten the group commit window on the primary instance and re-measure
-- (advanced option; 0 = engine default of 10 ms; no restart required)
EXEC sys.sp_configure N'show advanced options', 1;  RECONFIGURE;
EXEC sys.sp_configure N'availability group commit time', 2;  RECONFIGURE;

-- Flow control between HADR endpoints, also instance-wide; the primary uses it to
-- detect a secondary falling behind. Check the option's current name and default
-- with sp_configure before scripting it: it is new in 17.x.
EXEC sys.sp_configure N'ucs_flow_control';</pre>
<p>SQL Server 2025&rsquo;s fast failover for persistent health issues closes a gap that has existed since 2012. When the AG resource&rsquo;s health check fails, WSFC&rsquo;s default is to try restarting the resource in place before failing it over, which on a node with a genuine problem adds a minute or more of unavailability while a restart that cannot succeed is attempted. Setting the restart threshold to zero on the AG resource tells the cluster to fail over immediately. This is set through the cluster, and the release notes document it alongside better health-check timeout diagnostics so that you can tell a genuine hang from a synchronisation stall:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># On any WSFC node: fail over the AG resource immediately on a persistent health failure
# instead of attempting an in-place restart first (default RestartThreshold is 3 within RestartPeriod)
Get-ClusterResource -Name 'ag_ops' | Set-ClusterParameter -Name RestartThreshold -Value 0

# Verify
Get-ClusterResource -Name 'ag_ops' | Get-ClusterParameter -Name RestartThreshold, RestartPeriod, HealthCheckTimeout</pre>
<p>SQL Server 2025 backups on secondaries are no longer restricted to copy-only. That sentence changes the design of every Enterprise AG&rsquo;s backup plan: the full and differential chain can now run entirely on a secondary, with the log backups wherever the backup preference sends them, and the primary&rsquo;s I/O is left for the workload. Set <code>AUTOMATED_BACKUP_PREFERENCE = SECONDARY_ONLY</code> and make sure the backup job checks <code>sys.fn_hadr_backup_is_preferred_replica</code> before running, as it always should have.</p>
<p>The remaining items are operational quality of life: a listener IP can be removed without dropping the listener, read-only and read-write routing can be set to NONE to pull traffic back to the primary during maintenance, distributed availability groups now work between two contained availability groups (which makes the contained AG, introduced in 2022, usable for DR rather than only for HA), the asynchronous commit path in distributed AGs is less prone to saturating the link, and every replication and HA channel, including WSFC, AG endpoints, FCI, log shipping and linked servers, can run TDS 8.0 over TLS 1.3.</p>
<p>Databases that fail to read their persisted AG configuration during a network interruption now move to RESOLVING rather than staying in an ambiguous state, which is a small change that removes a confusing manual step from more than one runbook we maintain.</p>
<h2>On-premises versus cloud: where each feature actually runs<a class="anchor-link" id="on-premises-versus-cloud-where-each-feature-actually-runs"></a></h2>
<p>The same 17.x engine appears in four shapes, and the feature list is not the same in each. The table is what I hand to clients deciding where a SQL Server 2025 workload should live.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/sql-server-2025-deployment-shapes-on-premises-cloud.png" alt="SQL Server 2025 deployment shapes: self-managed, Azure SQL Managed Instance, Azure SQL Database and Amazon RDS, and which control surfaces each exposes" width="1800" height="600" class="aligncenter size-full wp-image-93031"><figcaption>Pick the shape by which control surface your failure modes need, not by feature list.</figcaption></figure>
<div>
<table>
<thead>
<tr>
<th>Capability</th>
<th>SQL Server 2025 self-managed (on-prem, Azure VM, EC2, Arc-enabled)</th>
<th>Azure SQL Managed Instance</th>
<th>Azure SQL Database</th>
<th>Amazon RDS for SQL Server</th>
</tr>
</thead>
<tbody>
<tr>
<td>Engine version and patching</td>
<td>17.x, you apply CUs; CU8 current</td>
<td>Always-up-to-date policy already carried most SQL Server 2025 features before GA; SQL Server 2025 update policy pins the surface</td>
<td>Continuously updated; ahead of the boxed product</td>
<td>SQL Server 2025 supported since 21 Jul 2026 (Enterprise, Standard, Developer); CU applied on the AWS schedule</td>
</tr>
<tr>
<td>Optimized locking</td>
<td>Opt-in per database; ADR then RCSI then enable</td>
<td>Always on (update policy 2025 or always-up-to-date)</td>
<td>Always on since 2023</td>
<td>Opt-in per database, same T-SQL; verify against the RDS feature list for your engine version</td>
</tr>
<tr>
<td>tempdb space governance</td>
<td>Resource Governor, Standard and Enterprise</td>
<td>Available; MI exposes Resource Governor</td>
<td>Not applicable; per-database resource limits instead</td>
<td>Resource Governor is available on RDS; workload-group DDL runs as the master user</td>
</tr>
<tr>
<td>Always On AG</td>
<td>Full control: commit time, flow control, restart threshold, backups on secondary, contained and distributed AGs</td>
<td>Managed failover groups and built-in HA; AG internals not exposed</td>
<td>Built-in HA, geo-replication, failover groups</td>
<td>Multi-AZ uses AGs (Enterprise) or DBM (Standard) managed by AWS; readable secondaries on Enterprise; AG knobs not exposed</td>
</tr>
<tr>
<td>Backups</td>
<td>ZSTD, immutable blob, full/diff on secondaries</td>
<td>Automated; compression and targets managed</td>
<td>Automated</td>
<td>Automated snapshots plus native backup to S3 via <code>rds_backup_database</code>; compression option set by RDS</td>
</tr>
<tr>
<td>Standard edition 32-core / 256 GB</td>
<td>Yes, plus Resource Governor</td>
<td>Not applicable (vCore tiers)</td>
<td>Not applicable</td>
<td>Yes; instance class sets the ceiling below that anyway on most classes</td>
</tr>
<tr>
<td>Change Event Streaming, Fabric mirroring</td>
<td>Preview; targets are Azure Event Hubs / Fabric; Entra auth from CU3 on Arc or Azure VM</td>
<td>Fabric mirroring supported</td>
<td>Fabric mirroring supported</td>
<td>Not applicable; CDC to Kinesis/MSK via DMS or Debezium instead</td>
</tr>
<tr>
<td>Vector search, DiskANN index</td>
<td>Vector type GA; vector index and VECTOR_SEARCH are preview</td>
<td>Vector type available</td>
<td>Vector type available; index status follows the service</td>
<td>Vector type available; treat the index as preview until AWS documents otherwise</td>
</tr>
</tbody>
</table>
</div>
<p>The pattern is the one you would expect. Self-managed SQL Server 2025 gives you every knob in this post and makes you responsible for using them. Managed Instance gives you the engine-level features (optimized locking, the query processing improvements, columnstore changes) with the HA internals taken away and replaced with failover groups; if your reason for wanting SQL Server 2025 is the AG knobs, MI is not where you get them. Azure SQL Database had most of the performance features first and has never exposed an AG at all. RDS is a self-managed engine with a managed control plane: the database-scoped features work, the instance-level and cluster-level ones are AWS&rsquo;s to configure, and the Azure-targeted integrations do not apply.</p>
<h2>Where I would deploy SQL Server 2025, and where I would wait<a class="anchor-link" id="where-i-would-deploy-sql-server-2025-and-where-i-would-wait"></a></h2>
<p>As of September 2026, with CU8 out: for a new self-managed deployment, SQL Server 2025 on CU8 is the version, and the migration from 2019 in particular should not wait, given that 2019 has been on extended support only since February 2025. For an in-place upgrade of a stable 2022 estate, the case is strongest where the workload suffers from lock escalation or tempdb exhaustion, or where a Standard edition instance is capped at 24 cores or 128 GB; if none of those is true, 2022 is supported to 2033 and there is no urgency.</p>
<p>Turn on optimized locking one database at a time with the wait-stat baseline described above, put tempdb governance on every instance, and leave the preview features (vector indexes, Change Event Streaming, the fuzzy-matching functions, optimized <code>sp_executesql</code>) off in production until they lose the <code>PREVIEW_FEATURES</code> gate. In Azure, Managed Instance on the SQL Server 2025 update policy is the shape that keeps your on-premises and cloud feature surfaces aligned, which matters more than any single feature if you run both.</p>
<p>Test all of this against your own workload in a staging environment with production data volumes and production concurrency before it goes anywhere near production, keep a verified backup and a rehearsed restore in place before enabling any database-scoped option, and treat the LAQ semantic change as an application test item, not a database one. If you want a second pair of eyes on a SQL Server 2025 upgrade, an AG design, or a Standard-versus-Enterprise decision with the new ceilings, that is the work the <a href="https://minervadb.com/sql-server-support/">MinervaDB SQL Server support</a> team does every week, on-premises and in Azure and AWS.</p>
<h2>References<a class="anchor-link" id="references"></a></h2>
<p><a href="https://learn.microsoft.com/en-us/sql/sql-server/what-s-new-in-sql-server-2025" rel="noopener" target="_blank">What&rsquo;s new in SQL Server 2025</a> &middot; <a href="https://learn.microsoft.com/en-us/sql/sql-server/sql-server-2025-release-notes" rel="noopener" target="_blank">SQL Server 2025 release notes</a> &middot; <a href="https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2025/build-versions" rel="noopener" target="_blank">SQL Server 2025 build versions</a> &middot; <a href="https://learn.microsoft.com/en-us/lifecycle/products/sql-server-2025" rel="noopener" target="_blank">SQL Server 2025 lifecycle</a> &middot; <a href="https://learn.microsoft.com/en-us/sql/relational-databases/performance/optimized-locking" rel="noopener" target="_blank">Optimized locking</a> &middot; <a href="https://learn.microsoft.com/en-us/sql/relational-databases/resource-governor/tempdb-space-resource-governance" rel="noopener" target="_blank">tempdb space resource governance</a> &middot; <a href="https://learn.microsoft.com/en-us/sql/sql-server/editions-and-components-of-sql-server-2025" rel="noopener" target="_blank">Editions and supported features of SQL Server 2025</a> &middot; <a href="https://techcommunity.microsoft.com/blog/sqlserver/sql-server-2025-is-now-generally-available/4470570" rel="noopener" target="_blank">SQL Server 2025 is now generally available</a> &middot; <a href="https://aws.amazon.com/about-aws/whats-new/2026/07/rds-sqlserver-supports-sqlserver-2025/" rel="noopener" target="_blank">Amazon RDS for SQL Server now supports SQL Server 2025</a> &middot; <a href="https://learn.microsoft.com/en-us/azure/azure-sql/managed-instance/update-policy" rel="noopener" target="_blank">Azure SQL Managed Instance update policy</a></p>

<p><a href="https://minervadb.com/sql-server-2025-performance-scalability-ha/">SQL Server 2025 Performance, Scalability and High Availability: What Changed On-Premises and in the Cloud</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ProxySQL HA with BGP ECMP Anycast</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/09/07/proxysql-ha-with-bgp-ecmp-anycast/" />
      <id>https://percona.community/blog/2026/09/07/proxysql-ha-with-bgp-ecmp-anycast/</id>
      <updated>2026-09-07T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When setting up a new database for an application, high availability (HA) is one of the main priorities. Let’s assume for this example that you chose to use a Percona XtraDB (PXC) cluster to host your database. But how does the application know which PXC node is healthy and can receive application traffic? Introducing a cluster of ProxySQLs can solve this problem, as ProxySQL will healthcheck the database nodes and route the application traffic to the healthy nodes. However, now the HA problem comes up again: how does the application know which ProxySQL host is healthy?</p>
<p><a href="https://percona.community/blog/2026/09/07/proxysql-ha-with-bgp-ecmp-anycast/">ProxySQL HA with BGP ECMP Anycast</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When setting up a new database for an application, high availability (HA) is one of the main priorities. Let&rsquo;s assume for this example that you chose to use a Percona XtraDB (PXC) cluster to host your database.<br>
But how does the application know which PXC node is healthy and can receive application traffic? Introducing a cluster of ProxySQLs can solve this problem, as ProxySQL will healthcheck the database nodes and route the application traffic to the healthy nodes.<br>
However, now the HA problem comes up again: how does the application know which ProxySQL host is healthy?</p>
<p>Putting &ldquo;one more component&rdquo; in front of your servers to make them highly available just shifts the problem up by one layer.<br>
From making the database HA, to making ProxySQL HA, to needing to make HAProxy HA and so on&hellip;</p>
<p>At some point, you may land on the common HA strategy, keepalived, but BGP ECMP is a powerful alternative worth considering.</p>
<h2>What is BGP, what is ECMP?<a class="anchor-link" id="what-is-bgp-what-is-ecmp"></a></h2>
<p>As a brief summary, Border Gateway Protocol (BGP) is a routing protocol which operates over TCP. Equal-Cost Multi-Path (ECMP) defines that we want the traffic to be loadbalanced equally across the given routes.<br>
Routers acting as BGP speakers are configured to accept routes for defined IP ranges and Autonomous System (AS) numbers, storing information about the networks that the router can reach in a Routing Information Base (RIB) table.<br>
To benefit from Anycast, we will assign both/all ProxySQL nodes the same virtual IP address. BGP is then used to let the router know multiple routes to reach that IP. Configuring ECMP will cause the router to balance the traffic over these routes.</p>
<p>User defined health checks are executed by a BGP-speaking daemon, such as ExaBGP, running on the host to ensure that the router has the correct information about the status of a route.<br>
As soon as a health check fails, ExaBGP will trigger a BGP update to withdraw the route. The router will remove the entry from its RIB table and stop forwarding packets to that host.<br>
The traffic will be redistributed to the remaining healthy ProxySQL nodes.</p>
<p>For a more in depth guide about BGP please refer to <a href="https://www.ciscopress.com/articles/article.asp?p=2738462&amp;seqNum=2" target="_blank" rel="noopener noreferrer">Cisco press</a>.</p>
<h2>How ECMP based on BGP works<a class="anchor-link" id="how-ecmp-based-on-bgp-works"></a></h2>
<p>For our setup we can&rsquo;t have the applications connect to the normal IPs of the ProxySQL nodes, as these are assigned to one node and fixed.<br>
Instead we need a single separate anycast IP (let&rsquo;s take 10.5.200.1/32) that we can assign to both ProxySQL nodes. This IP will be assigned to the loopback interface and has to be of a separate network, not overlapping with the IP-Range the normal ProxySQL node IPs are from.</p>
<p>When a packet with the destination set to the ProxySQL anycast IP (10.5.200.1) arrives at the router, the router checks its internal routing table to determine the next hop for the packet.<br>
The router will see the two ProxySQL nodes in the cluster as potential next hop (as they both have the same anycast IP) and will pick one of the ProxySQL nodes, based on ECMP, to forward the packet to.<br>
In ECMP the next hop is dynamically decided based on a 5-tuple hash from the packet header fields:</p>
<p><code>{ source IP address | destination IP address | protocol | source port | destination port }</code></p>
<p>Because the IP address and ports are in the hash, this ensures that the packets belonging to the same TCP stream are kept on the same path, to prevent packets of the same TCP connection ending up on multiple ProxySQL nodes.</p>
<p>You can visualise the setup like this:</p>
<p><figure><img decoding="async" width="1383" height="1024" src="https://percona.community/blog/2026/09/proxysql-ha-with-bgp-ecmp-anycast-diagram_hu_fb1e65d520c6a31d.webp" alt="Diagram" loading="lazy"></figure>
</p>
<p>By doing so, we have achieved high availability by leveraging BGP ECMP to loadbalance traffic in an active/active configuration across the ProxySQL nodes.<br>
Additionally, the application config can be simplified, as only the single anycast IP (or DNS record for that IP) needs to be used for the ProxySQL cluster, and the logic of routing will be handled by the router.<br>
Failures of a ProxySQL host are automatically handled by ExaBGP; a BGP update is sent to the router, and the route to the failed ProxySQL is withdrawn. The router redirects traffic to the remaining healthy ProxySQL, with no manual interventions required.<br>
Extra infrastructure components (such as internal loadbalancers) can be avoided, eliminating additional network hops and improving network latency.</p>
<h2>Alternative strategies for using BGP with databases<a class="anchor-link" id="alternative-strategies-for-using-bgp-with-databases"></a></h2>
<p>Of course, a ProxySQL cluster is not the only way to leverage BGP ECMP in order to achieve high availability for your databases. Some other strategies could be to use it for read-replica routing, local traffic routing, or for routing towards loadbalancers (e.g. haproxy).</p>
<h3>Routing towards the database loadbalancer<a class="anchor-link" id="routing-towards-the-database-loadbalancer"></a></h3>
<p>Using BGP ECMP does not mean that you have to forego a database loadbalancer. You can configure your database servers to sit behind a database loadbalancer,<br>
such as HAproxy or ProxySQL, and implement BGP ECMP in order to route traffic towards the loadbalancers and operate them highly available as true active/active pair.<br>
If one of the loadbalancer instances dies, then BGP automatically takes care of routing the traffic to the remaining healthy peers for you.</p>
<h3>Read replica routing<a class="anchor-link" id="read-replica-routing"></a></h3>
<p>You can use BGP ECMP to distribute MySQL-Connections over multiple read replicas without using any Loadbalancer / ProxySQL at all. This saves you the additional latency and network hops of using a loadbalancer/ProxySQL.<br>
If you need to ensure that you do not read from a replica which is lagging behind or has stopped replicating, you can implement this logic in the BGP health checks.</p>
<h3>BGP Local Preference<a class="anchor-link" id="bgp-local-preference"></a></h3>
<p>If you have a multi-datacenter setup, you can choose to use local preference to keep traffic localised within the same datacenter. For example if you have an application server and a proxysql host in one datacenter (datacenter A), and an application server and proxysql host in a second datacenter (datacenter B),<br>
you can tell the router to send traffic from the application to the proxysql within the same datacenter. The advantage of this is that it keeps network latency low, and avoids cross-site transit. Configuring this in BGP means that the application does not need to be aware of which datacenter it is running in. The BGP router handles localised routing for you.<br>
If the local route would disappear, then the BGP router would automatically divert traffic from the application in datacenter A to the proxysql in the datacenter B.</p>
<p><figure><img decoding="async" width="1619" height="971" src="https://percona.community/blog/2026/09/proxysql-ha-with-bgp-ecmp-anycast-lpref_hu_6bf20dfe86744131.webp" alt="Diagram BGP local preference" loading="lazy"></figure>
</p>
<h2>Advantages of BGP<a class="anchor-link" id="advantages-of-bgp"></a></h2>
<ul>
<li>One advantage of BGP over keepalived is that your anycast-nodes don&rsquo;t need to be in the same subnet (especially useful for multi-datacenter setups). Keepalived instead requires the nodes to be part of the same Layer 2 Network.</li>
<li>BGP ECMP supports active/active, unlike keepalived which only supports active/passive architectures.</li>
<li>In BGP, the router has the overview of which node is healthy or unhealthy. In keepalived this knowledge resides in the keepalived process running on the node itself.<br>
As long as the nodes running keepalived can see each other, keepalived thinks everything is fine, but the nodes might have lost connection to the router.<br>
Whereas with BGP, health checks ensure that BGP is aware of the state of the route. In case there would be a network problem that would make the node unreachable, the route would disappear from the router.</li>
<li>You can horizontally scale the nodes with BGP.</li>
<li>BGP Local preference allows you to automatically route traffic within a datacenter, without the application needing to configure logic like &ldquo;use ProxySQL-A when running in datacenter-A, otherwise ProxySQL-B.&rdquo;</li>
<li>You can set BGP to eliminate additional hops of infrastructure components, for example connecting to a pool of read replicas, without needing to connect over a loadbalancer.</li>
</ul>
<h2>Caveats with BGP<a class="anchor-link" id="caveats-with-bgp"></a></h2>
<p>BGP ECMP is not connection-state aware, so if instances disappear/die or new instances join and the RIB table is rebuilt, the hashing algorithm will most likely forward packets for existing connections to a different instance than before. As that instance will not be aware of this TCP-Connection, it will send an RST-packet and the application will have to re-open its database connection.</p>
<p>In the next post, we will explain the technical details of setting up BGP ECMP for our ProxySQL cluster using OPNsense as Router.</p>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>

<p><a href="https://percona.community/blog/2026/09/07/proxysql-ha-with-bgp-ecmp-anycast/">ProxySQL HA with BGP ECMP Anycast</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Oracle Exadata to PostgreSQL 18 Migration: An Airline Operations Case Study from the MinervaDB Data Migration Team</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/oracle-exadata-to-postgresql-18-migration-airline/" />
      <id>https://minervadb.com/oracle-exadata-to-postgresql-18-migration-airline/</id>
      <updated>2026-09-07T09:44:58+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>This is the MinervaDB Data Migration Team\'s write-up of an Oracle Exadata to PostgreSQL 18 migration for an airline operations platform: crew scheduling, flight status, aircraft rotation and the reporting that hangs off them. The [...]</p>
<p><a href="https://minervadb.com/oracle-exadata-to-postgresql-18-migration-airline/">Oracle Exadata to PostgreSQL 18 Migration: An Airline Operations Case Study from the MinervaDB Data Migration Team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/oracle-exadata-to-postgresql-18-migration-airline-case-study.png" alt="Oracle Exadata to PostgreSQL 18 migration for airline operations: MinervaDB Data Migration Team case study" width="1200" height="630" class="aligncenter size-full wp-image-93019"></p>
<p>This is the MinervaDB Data Migration Team&rsquo;s write-up of an Oracle Exadata to PostgreSQL 18 migration for an airline operations platform: crew scheduling, flight status, aircraft rotation and the reporting that hangs off them. The system left an Exadata X9M-2 quarter rack running Oracle 19c RAC and landed on community PostgreSQL 18 on bare metal in the airline&rsquo;s own data centres, under Patroni, with pgBackRest for backup and point-in-time recovery. The client is anonymised and every figure below is rounded, but the method, the SQL, the PL/SQL mapping and the sizing arithmetic are exactly what we used.</p>
<p>The short version of the outcome: the workload met the p95 latency and peak-throughput targets we set from the Oracle AWR baselines, on three commodity servers, without a compatibility layer, and with the PL/SQL estate rewritten rather than emulated. The longer version, which is the useful one, is below. It is organised the way the engagement was: assessment, target selection, capacity planning and sizing, schema mapping, SQL mapping, PL/SQL to PL/pgSQL, data movement and cutover, and what we would do differently.</p>
<h2>Starting state: the Exadata estate and the forcing constraint<a class="anchor-link" id="starting-state-the-exadata-estate-and-the-forcing-constraint"></a></h2>
<p>The source was an Exadata X9M-2 quarter rack: two database servers, each with two 32-core Intel Xeon Platinum 8358 sockets and 1 TB of memory, and three High Capacity storage servers, each with sixteen-core Xeon storage CPUs, PMem in front of NVMe flash in front of 18 TB disks, all on the RoCE fabric. Oracle 19c ran as a two-node RAC with three pluggable databases, of which one, the operations PDB, was the migration scope. Allocated size was around 12 TB with roughly 9 TB of live data, the rest being free space inside tablespaces, undo and a large temp.</p>
<p>The workload profile that sized the PostgreSQL 18 target came from AWR, not from interviews. Peak periods (early morning rotation building and the evening crew-legality run) showed roughly 9,000 executions per second, of which about 70 percent were single-row lookups and short-range scans by flight, tail number or crew ID, the remainder being the legality and pairing logic that runs inside PL/SQL. </p>
<p>Around 2,400 concurrent sessions, most of them idle in application pools. Redo generation peaked near 40 MB/s. Physical reads were low because the buffer cache absorbed almost everything; the storage cells were doing very little smart scan for this PDB, which turned out to be the most important sizing fact of the whole engagement and I will come back to it.</p>
<figure>
<img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/exadata-x9m-2-source-topology-oracle-19c-rac.png" alt="Source Oracle Exadata X9M-2 quarter rack topology: two 19c RAC database servers and three storage servers, with the operations PDB in scope" width="1800" height="720" class="aligncenter size-full wp-image-93020"><figcaption>The important number in this picture is not the hardware, it is how little of the storage tier the operations PDB was actually using.</figcaption></figure>
<p>The forcing constraint was commercial and contractual rather than technical: the Exadata support renewal and the Oracle licence position were coming up together, and the airline&rsquo;s platform group had already standardised on PostgreSQL for new services. The technical question we were hired to answer was whether the operations platform, with its PL/SQL and its RAC dependency, could move without the performance regression everyone assumed came with leaving Exadata.</p>
<h2>Assessment: inventory first, opinions later<a class="anchor-link" id="assessment-inventory-first-opinions-later"></a></h2>
<p>Every Oracle Exadata to PostgreSQL 18 migration we run starts from the Oracle catalog, because interviews undercount PL/SQL by a factor of two or three. The inventory queries are short:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Object inventory for the schemas in scope
SELECT object_type,
       COUNT(*) AS objects
FROM   dba_objects
WHERE  owner IN ('OPS', 'CREW', 'FLT', 'RPT')
GROUP  BY object_type
ORDER  BY objects DESC;

-- PL/SQL volume by unit
SELECT owner, name, type,
       COUNT(*) AS loc
FROM   dba_source
WHERE  owner IN ('OPS', 'CREW', 'FLT', 'RPT')
GROUP  BY owner, name, type
ORDER  BY loc DESC;

-- Feature dependencies that have no direct community-PostgreSQL equivalent
SELECT owner, name, type, text
FROM   dba_source
WHERE  owner IN ('OPS', 'CREW', 'FLT', 'RPT')
AND    REGEXP_LIKE(UPPER(text),
       'PRAGMA AUTONOMOUS_TRANSACTION|DBMS_AQ|DBMS_SCHEDULER|UTL_FILE|DBMS_LOB|BULK COLLECT|FORALL|CONNECT BY|DBMS_SQL|UTL_HTTP|DBMS_RLS|FLASHBACK');</pre>
<p>The rounded inventory for the operations PDB, and the conversion route we assigned to each class, is the matrix that the effort estimate came from:</p>
<div>
<table>
<thead>
<tr>
<th>Object class</th>
<th>Count (rounded)</th>
<th>Route</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tables</td>
<td>~640</td>
<td>Automatic (ora2pg)</td>
<td>38 range/interval-partitioned; 12 with LOBs</td>
</tr>
<tr>
<td>Indexes</td>
<td>~1,900</td>
<td>Automatic, then pruned</td>
<td>~400 were redundant prefixes; skip scan (PostgreSQL 18) let us drop more</td>
</tr>
<tr>
<td>Sequences</td>
<td>~210</td>
<td>Automatic</td>
<td>CACHE semantics differ; identity columns where the table allowed</td>
</tr>
<tr>
<td>Views</td>
<td>~380</td>
<td>Assisted</td>
<td>(+) joins, DECODE, NVL, CONNECT BY inside views</td>
</tr>
<tr>
<td>Materialised views</td>
<td>22</td>
<td>Manual</td>
<td>6 were ON COMMIT fast refresh; redesigned</td>
</tr>
<tr>
<td>Packages</td>
<td>110 (~90k lines)</td>
<td>Manual rewrite</td>
<td>Legality, pairing, rotation engines; 14 with autonomous transactions</td>
</tr>
<tr>
<td>Standalone procedures/functions</td>
<td>~260</td>
<td>Assisted</td>
<td>Mostly wrappers; ora2pg output usable after review</td>
</tr>
<tr>
<td>Triggers</td>
<td>~150</td>
<td>Assisted</td>
<td>Audit triggers reviewed for the PostgreSQL 18 AFTER-trigger role change</td>
</tr>
<tr>
<td>Types (object/collection)</td>
<td>~40</td>
<td>Manual</td>
<td>Composite types plus arrays; methods became functions</td>
</tr>
<tr>
<td>Scheduler jobs</td>
<td>~70</td>
<td>Manual</td>
<td>pg_cron plus an application scheduler for chains</td>
</tr>
<tr>
<td>AQ queues</td>
<td>4</td>
<td>Manual</td>
<td>Two moved to Kafka (already in the estate), two to pgmq</td>
</tr>
<tr>
<td>VPD policies</td>
<td>9</td>
<td>Manual</td>
<td>Row-level security</td>
</tr>
<tr>
<td>DB links</td>
<td>3</td>
<td>Manual</td>
<td>oracle_fdw during coexistence, retired after</td>
</tr>
</tbody>
</table>
</div>
<p>Two findings from this phase changed the plan. First, the RAC dependency was an availability requirement, not a scale requirement; the second instance was there to survive a node failure, and inter-instance traffic showed it was carrying a fraction of the load. That maps to a Patroni-managed replica set, not to a distributed PostgreSQL. Second, the six ON COMMIT fast-refresh materialised views were load-bearing for the reporting schema and PostgreSQL 18 has no incremental refresh in core; they needed a design decision, not a conversion.</p>
<p>ora2pg 25.0 did the mechanical part of the assessment. Its migration-cost report is a useful floor for the estimate, not the estimate:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># ora2pg 25.0, assessment mode against the operations PDB
ora2pg -c ora2pg.conf -t SHOW_REPORT --estimate_cost --dump_as_html 
       -O "SCHEMA=OPS,CREW,FLT,RPT" &gt; ops_pdb_assessment.html

# Object-level detail for the matrix
ora2pg -c ora2pg.conf -t SHOW_TABLE   -O "SCHEMA=OPS"
ora2pg -c ora2pg.conf -t SHOW_COLUMN  -O "SCHEMA=OPS"</pre>
<h2>Why community PostgreSQL 18, and what we rejected<a class="anchor-link" id="why-community-postgresql-18-and-what-we-rejected"></a></h2>
<p>We considered four targets and wrote down why three lost, because this section is what the client&rsquo;s architecture board actually wanted to read.</p>
<p>EDB Postgres Advanced Server with its Oracle-compatible PL/SQL dialect would have cut the package rewrite substantially. We rejected it because the airline&rsquo;s stated goal was to remove vendor lock-in, and EPAS trades Oracle lock-in for EDB lock-in. That is a legitimate trade when the rewrite budget is the binding constraint; here it was not, and 90,000 lines of PL/SQL is a rewrite a team of four can do in a quarter with the right tooling. We said so in writing and the board agreed.</p>
<p>PostgreSQL 17 was the safer-looking landing zone when we started scoping in early 2026, with 18 only a few minors old. We chose 18 for three reasons specific to a migration, not because it was newer. The <code>pg_upgrade</code> statistics carry-over matters for the next major upgrade rather than this cutover, but it means the client is not planning a stats-cold upgrade during stabilisation. PostgreSQL 18&rsquo;s asynchronous I/O and the default <code>effective_io_concurrency</code> of 16 change how a multi-terabyte table scans on NVMe, which affected sizing. And 18&rsquo;s end of life is November 2030, four years from cutover. PostgreSQL 19 was still in beta throughout the project and is not a production target until it has a couple of minors behind it.</p>
<p>A managed cloud PostgreSQL 18 was rejected on data residency and on the airline&rsquo;s requirement to run the operations platform in its own two data centres. Distributed PostgreSQL (Citus, or a YugabyteDB-style fork) was rejected because the workload is not sharded by nature and, as the AWR numbers showed, does not need to be; a single well-sized primary with synchronous standbys covers it with headroom.</p>
<h2>Capacity planning and sizing the PostgreSQL 18 stack<a class="anchor-link" id="capacity-planning-and-sizing-the-postgresql-18-stack"></a></h2>
<p>The mistake we see most often on an Exadata exit is sizing the PostgreSQL 18 hardware from Exadata&rsquo;s CPU utilisation. Exadata CPU utilisation on the database servers understates the work because smart scan, storage indexes and flash cache absorb I/O and filtering on the cells. On a PostgreSQL 18 target every one of those bytes comes back to the database host. So we size from logical work, not from host utilisation. The inputs, all from AWR and ASH over a four-week window covering month-end and a schedule change:</p>
<div>
<table>
<thead>
<tr>
<th>AWR / ASH input</th>
<th>Rounded value (peak hour)</th>
<th>What it sizes on PostgreSQL 18</th>
</tr>
</thead>
<tbody>
<tr>
<td>DB CPU per second</td>
<td>~22 CPU-seconds/s across both instances</td>
<td>Core count, after correcting for Oracle-side overheads that do not exist in PostgreSQL (RAC cache fusion, ASM) and PostgreSQL-side work that does not exist in Oracle (vacuum, checkpoints, per-connection processes)</td>
</tr>
<tr>
<td>Logical reads per second</td>
<td>~1.4 million</td>
<td><code>shared_buffers</code> and total RAM: the working set must fit in memory or PostgreSQL 18 will do the physical reads Exadata&rsquo;s cells were hiding</td>
</tr>
<tr>
<td>Physical reads per second (host)</td>
<td>~3,000</td>
<td>Deceptively low: cell flash cache served most of them. We measured the true cold working set on a restored copy instead</td>
</tr>
<tr>
<td>Redo bytes per second</td>
<td>~40 MB/s peak</td>
<td>WAL volume, WAL device throughput, archive bandwidth, pgBackRest repo and the synchronous replica network</td>
</tr>
<tr>
<td>Executions per second</td>
<td>~9,000</td>
<td>Transaction rate; with PostgreSQL&rsquo;s per-connection process model this drives PgBouncer pool sizing</td>
</tr>
<tr>
<td>Concurrent sessions / active sessions</td>
<td>~2,400 / ~60 active</td>
<td>PgBouncer client connections vs server pool size; <code>max_connections</code> is sized from the 60, not the 2,400</td>
</tr>
<tr>
<td>Segment sizes (DBA_SEGMENTS)</td>
<td>~9 TB live, ~2.2 TB of that index</td>
<td>Data volume after type mapping, with a bloat allowance and index rebuild space</td>
</tr>
<tr>
<td>Top SQL by elapsed and by executions</td>
<td>Top 50 captured</td>
<td>The regression test set for post-migration plan comparison</td>
</tr>
</tbody>
</table>
</div>
<h3>CPU<a class="anchor-link" id="cpu"></a></h3>
<p>Twenty-two Oracle CPU-seconds per second is about 22 fully busy cores at peak. We remove the RAC and ASM overhead (measured from the instance-level wait profile at around 10 percent), then add PostgreSQL&rsquo;s own background work. Autovacuum on a 9 TB estate with a 40 MB/s write rate is real CPU, checkpointing is real CPU, and PostgreSQL&rsquo;s connection processes carry per-process overhead that Oracle&rsquo;s shared server does not.</p>
<p>Our working rule from previous exits is 1.3 to 1.5 times the corrected Oracle CPU figure for headroom on a single primary, so 28 to 30 cores at peak. We specified 64 cores per node (two 32-core sockets) to leave room for reporting queries that were about to lose smart scan, and because NVMe throughput on a modern two-socket box is wasted with fewer cores driving it.</p>
<h3>Memory<a class="anchor-link" id="memory"></a></h3>
<p>The working set was measured, not assumed. We restored the PDB to a scratch server, ran the captured top-50 SQL plus the batch jobs against a cold cache with <code>pg_buffercache</code> and <code>pg_stat_io</code> sampling, and watched the buffer pool stabilise at roughly 380 GB of distinct blocks touched in a peak hour. That set the memory floor: 1 TB per node, with <code>shared_buffers</code> at 256 GB and the remainder left to the OS page cache, <code>work_mem</code> allocations and maintenance operations. We do not go past a quarter of RAM for <code>shared_buffers</code> on a mixed workload without a measured reason, and here the double-buffering cost of going higher was not worth the marginal hit-rate gain we saw in testing.</p>
<h3>Storage<a class="anchor-link" id="storage"></a></h3>
<p>Nine terabytes of Oracle data does not become nine terabytes of PostgreSQL 18 data. NUMBER-to-numeric mapping, the absence of Oracle&rsquo;s row-level compression on a few large tables, and PostgreSQL&rsquo;s per-tuple header made the converted data about 10 percent larger; the index estate shrank because we dropped redundant indexes. We plan for the converted size times a 1.3 bloat and maintenance factor, plus WAL retention for the synchronous replica and PITR, plus space to rebuild the largest table&rsquo;s indexes concurrently.</p>
<p>The specification per node was six 7.68 TB NVMe drives in RAID 10 (about 23 TB usable), a separate pair of 1.92 TB NVMe for WAL, and the pgBackRest repository on object storage at the DR site. The WAL device was sized for sustained 40 MB/s writes with fsync latency under a millisecond, which any current enterprise NVMe delivers; we measured it with <code>pg_test_fsync</code> before accepting the hardware.</p>
<h3>Connections<a class="anchor-link" id="connections"></a></h3>
<p>Twenty-four hundred sessions cannot become 2,400 PostgreSQL 18 backends. PgBouncer in transaction pooling mode fronts the primary with a server pool sized from active sessions, not connected sessions: 60 active at peak became a default pool of 96 with a reserve, and <code>max_connections</code> on the server was set to 300 to leave room for replication, monitoring and administrative sessions. The application team had to remove session-state assumptions (temporary tables and <code>SET</code> commands that assumed a sticky session) that transaction pooling breaks, and PgBouncer 1.24 and later enable prepared statements by default, which the JDBC driver configuration had to account for.</p>
<figure>
<img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/postgresql-18-target-topology-patroni-pgbackrest.png" alt="Target PostgreSQL 18 topology: PgBouncer pair, three-node Patroni cluster with etcd, synchronous replica, DR standby cluster and pgBackRest repositories" width="1800" height="940" class="aligncenter size-full wp-image-93021"><figcaption>Three PostgreSQL 18 nodes in the primary site replace two RAC nodes and three storage cells; the DR site runs as a Patroni standby cluster.</figcaption></figure>
<h3>The postgresql.conf that came out of the sizing<a class="anchor-link" id="the-postgresql-conf-that-came-out-of-the-sizing"></a></h3>
<p>Only the parameters we changed from the PostgreSQL 18 defaults, with the reasoning and the restart requirement, because that is the format the client&rsquo;s change board needed:</p>
<div>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>PostgreSQL 18 default</th>
<th>Set to</th>
<th>Unit</th>
<th>Reload / restart</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shared_buffers</code></td>
<td>128</td>
<td>262144 (256 GB)</td>
<td>MB</td>
<td>restart</td>
<td>Measured 380 GB peak-hour working set; rest to page cache</td>
</tr>
<tr>
<td><code>huge_pages</code></td>
<td>try</td>
<td>on</td>
<td>enum</td>
<td>restart</td>
<td>Fail loudly if the kernel reservation is missing</td>
</tr>
<tr>
<td><code>effective_cache_size</code></td>
<td>4 GB</td>
<td>720 GB</td>
<td>MB</td>
<td>reload</td>
<td>Planner hint: shared_buffers plus page cache</td>
</tr>
<tr>
<td><code>work_mem</code></td>
<td>4</td>
<td>64</td>
<td>MB</td>
<td>reload</td>
<td>Set per role: reporting role gets 512 MB via <code>ALTER ROLE</code></td>
</tr>
<tr>
<td><code>maintenance_work_mem</code></td>
<td>64</td>
<td>4096</td>
<td>MB</td>
<td>reload</td>
<td>PostgreSQL 17+ TidStore actually uses it; fewer index passes per vacuum</td>
</tr>
<tr>
<td><code>autovacuum_work_mem</code></td>
<td>-1</td>
<td>2048</td>
<td>MB</td>
<td>reload</td>
<td>Six workers times 2 GB, bounded</td>
</tr>
<tr>
<td><code>autovacuum_max_workers</code></td>
<td>3</td>
<td>6</td>
<td>workers</td>
<td>restart</td>
<td>640 tables, 38 partitioned; 3 is not enough</td>
</tr>
<tr>
<td><code>autovacuum_worker_slots</code></td>
<td>16</td>
<td>16</td>
<td>slots</td>
<td>restart</td>
<td>Left at default so workers can be raised at runtime later</td>
</tr>
<tr>
<td><code>autovacuum_vacuum_scale_factor</code></td>
<td>0.2</td>
<td>0.02</td>
<td>ratio</td>
<td>reload</td>
<td>Per-table overrides on the hot tables; 20% of a 900 GB table is not a threshold</td>
</tr>
<tr>
<td><code>autovacuum_vacuum_max_threshold</code></td>
<td>100000000</td>
<td>5000000</td>
<td>tuples</td>
<td>reload</td>
<td>PostgreSQL 18: hard cap on dead tuples before a vacuum triggers</td>
</tr>
<tr>
<td><code>max_wal_size</code></td>
<td>1 GB</td>
<td>64 GB</td>
<td>MB</td>
<td>reload</td>
<td>40 MB/s peak redo; keep checkpoints on the schedule, not on the size limit</td>
</tr>
<tr>
<td><code>checkpoint_timeout</code></td>
<td>5 min</td>
<td>15 min</td>
<td>s</td>
<td>reload</td>
<td>Recovery time budget agreed with the client</td>
</tr>
<tr>
<td><code>checkpoint_completion_target</code></td>
<td>0.9</td>
<td>0.9</td>
<td>ratio</td>
<td>reload</td>
<td>Default kept</td>
</tr>
<tr>
<td><code>wal_compression</code></td>
<td>off</td>
<td>lz4</td>
<td>enum</td>
<td>reload</td>
<td>Cuts WAL volume for full-page writes; CPU is cheap here</td>
</tr>
<tr>
<td><code>wal_buffers</code></td>
<td>-1</td>
<td>256</td>
<td>MB</td>
<td>restart</td>
<td>pg_stat_io showed wal_buffers_full events at the default</td>
</tr>
<tr>
<td><code>synchronous_commit</code></td>
<td>on</td>
<td>on</td>
<td>enum</td>
<td>reload</td>
<td>With <code>synchronous_standby_names</code> managed by Patroni (synchronous_mode)</td>
</tr>
<tr>
<td><code>io_method</code></td>
<td>worker</td>
<td>worker</td>
<td>enum</td>
<td>restart</td>
<td>io_uring not in the distro build; sync is the documented rollback</td>
</tr>
<tr>
<td><code>io_workers</code></td>
<td>3</td>
<td>12</td>
<td>workers</td>
<td>reload</td>
<td>Default is low for NVMe; justified from pg_stat_io read counts under the batch test</td>
</tr>
<tr>
<td><code>effective_io_concurrency</code></td>
<td>16</td>
<td>64</td>
<td>requests</td>
<td>reload</td>
<td>NVMe RAID10 sustains far deeper queues than the default</td>
</tr>
<tr>
<td><code>maintenance_io_concurrency</code></td>
<td>16</td>
<td>64</td>
<td>requests</td>
<td>reload</td>
<td>Same reasoning, vacuum and index builds</td>
</tr>
<tr>
<td><code>random_page_cost</code></td>
<td>4.0</td>
<td>1.1</td>
<td>cost</td>
<td>reload</td>
<td>NVMe; the default assumes spinning disk and pushes the planner to sequential scans</td>
</tr>
<tr>
<td><code>max_connections</code></td>
<td>100</td>
<td>300</td>
<td>conns</td>
<td>restart</td>
<td>PgBouncer pool plus replication, monitoring, admin</td>
</tr>
<tr>
<td><code>max_parallel_workers_per_gather</code></td>
<td>2</td>
<td>6</td>
<td>workers</td>
<td>reload</td>
<td>Reporting queries lost smart scan; parallelism gives some of it back</td>
</tr>
<tr>
<td><code>max_worker_processes</code></td>
<td>8</td>
<td>48</td>
<td>processes</td>
<td>restart</td>
<td>Parallel workers, io workers, pg_cron, logical replication</td>
</tr>
<tr>
<td><code>max_locks_per_transaction</code></td>
<td>64</td>
<td>512</td>
<td>locks</td>
<td>restart</td>
<td>38 partitioned tables; planning across many partitions exhausts 64 fast</td>
</tr>
<tr>
<td><code>track_io_timing</code></td>
<td>off</td>
<td>on</td>
<td>bool</td>
<td>reload</td>
<td>Needed for the AWR-vs-pg_stat_statements comparison to mean anything</td>
</tr>
<tr>
<td><code>log_min_duration_statement</code></td>
<td>-1</td>
<td>500</td>
<td>ms</td>
<td>reload</td>
<td>Slow-query capture during stabilisation, raised to 2000 after</td>
</tr>
<tr>
<td><code>shared_preload_libraries</code></td>
<td>&rdquo;</td>
<td>pg_stat_statements, pg_cron, auto_explain</td>
<td>list</td>
<td>restart</td>
<td>auto_explain at 2 s with buffers during stabilisation only</td>
</tr>
</tbody>
</table>
</div>
<p>Two of those need a warning. <code>random_page_cost</code> at 1.1 is right for local NVMe and wrong for anything network-attached; it is the first thing we check when a client copies a config between environments. And <code>io_workers</code> at 12 was arrived at by measuring, starting from the default of 3, doubling, and watching <code>pg_stat_io</code> and the I/O wait events; it is not a formula, and on a smaller box 6 was enough.</p>
<h2>Schema mapping: Oracle to PostgreSQL 18<a class="anchor-link" id="schema-mapping-oracle-to-postgresql-18"></a></h2>
<p>The type mapping is the part of an Oracle Exadata to PostgreSQL 18 migration that ora2pg gets mostly right and that we still review column by column on the hot tables, because the defaults are conservative in a way that costs performance.</p>
<div>
<table>
<thead>
<tr>
<th>Oracle</th>
<th>ora2pg default</th>
<th>What we used</th>
<th>Why</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NUMBER</code> (no precision)</td>
<td><code>numeric</code></td>
<td><code>numeric</code>, but <code>bigint</code> on keys and counters</td>
<td><code>numeric</code> is variable-width and slow in joins; every ID column that held only integers became <code>bigint</code>, proven by <code>MAX(ABS(col - TRUNC(col)))</code> = 0 on the source</td>
</tr>
<tr>
<td><code>NUMBER(p,0)</code>, p &le; 9 / &le; 18</td>
<td><code>integer</code> / <code>bigint</code></td>
<td>same</td>
<td>ora2pg does this correctly</td>
</tr>
<tr>
<td><code>NUMBER(p,s)</code></td>
<td><code>numeric(p,s)</code></td>
<td>same</td>
<td>Money and fuel quantities stay exact</td>
</tr>
<tr>
<td><code>VARCHAR2(n)</code></td>
<td><code>varchar(n)</code></td>
<td><code>varchar(n)</code>, <code>text</code> where n was a guess</td>
<td>Length checks kept where the application relied on them</td>
</tr>
<tr>
<td><code>CHAR(n)</code></td>
<td><code>char(n)</code></td>
<td><code>varchar(n)</code> or <code>text</code></td>
<td><code>char</code> padding semantics differ and cause equality surprises</td>
</tr>
<tr>
<td><code>DATE</code></td>
<td><code>timestamp(0)</code></td>
<td><code>timestamp(0)</code>, <code>timestamptz</code> for departure/arrival times</td>
<td>Oracle DATE has a time component; airline times are inherently zoned</td>
</tr>
<tr>
<td><code>TIMESTAMP WITH TIME ZONE</code></td>
<td><code>timestamptz</code></td>
<td>same</td>
<td></td>
</tr>
<tr>
<td><code>CLOB</code> / <code>BLOB</code></td>
<td><code>text</code> / <code>bytea</code></td>
<td>same; TOAST with <code>lz4</code></td>
<td>No large-object API; nothing exceeded the 1 GB field limit</td>
</tr>
<tr>
<td><code>RAW(16)</code> GUIDs</td>
<td><code>bytea</code></td>
<td><code>uuid</code></td>
<td>Native type, 16 bytes, indexable; new rows use <code>uuidv7()</code> (PostgreSQL 18) for insert locality</td>
</tr>
<tr>
<td><code>ROWID</code> logic</td>
<td>n/a</td>
<td>Surrogate <code>bigint</code> keys, <code>ctid</code> never</td>
<td><code>ctid</code> changes on update</td>
</tr>
<tr>
<td>Sequences with <code>CACHE 20</code></td>
<td><code>CACHE 20</code></td>
<td><code>CACHE 1</code> or identity columns</td>
<td>PostgreSQL 18 caches per session, not per instance; gaps behaved differently</td>
</tr>
<tr>
<td>Virtual columns</td>
<td>generated column</td>
<td><code>GENERATED ALWAYS AS (...) STORED</code> explicitly</td>
<td>PostgreSQL 18 defaults to virtual; we wanted indexable stored columns and said so in the DDL</td>
</tr>
</tbody>
</table>
</div>
<p>The crew assignment table is a good example of where PostgreSQL 18 gave us something Oracle did not have in the schema at all. Crew legality rules forbid overlapping assignments for the same crew member. On Oracle this was enforced by a trigger and a package. On PostgreSQL 18 it is a temporal primary key:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- PostgreSQL 18: temporal primary key replaces the Oracle trigger + package pair
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE crew.assignment (
    assignment_id   bigint GENERATED ALWAYS AS IDENTITY,
    crew_member_id  bigint      NOT NULL,
    pairing_id      bigint      NOT NULL,
    duty_period     tstzrange   NOT NULL,
    base_iata       char(3)     NOT NULL,
    duty_hours      numeric(5,2)
        GENERATED ALWAYS AS (
            EXTRACT(EPOCH FROM (upper(duty_period) - lower(duty_period))) / 3600.0
        ) STORED,
    created_at      timestamptz NOT NULL DEFAULT now(),
    row_uid         uuid        NOT NULL DEFAULT uuidv7(),
    CONSTRAINT pk_assignment
        PRIMARY KEY (crew_member_id, duty_period WITHOUT OVERLAPS),
    CONSTRAINT fk_assignment_crew
        FOREIGN KEY (crew_member_id) REFERENCES crew.member (crew_member_id),
    CONSTRAINT ck_assignment_period
        CHECK (NOT isempty(duty_period))
);

-- range-partitioned tables (flight_leg, by operating_date) keep their monthly layout;
-- assignment stays unpartitioned so the temporal primary key can be a single GiST index
CREATE INDEX ix_assignment_pairing
    ON crew.assignment (pairing_id, crew_member_id);</pre>
<p>The table is deliberately not partitioned. A unique or primary key on a partitioned table has to include the partition key, and a temporal key over a range column cannot do that cleanly, so the 38 partitioned tables in the estate are the big append-only ones (<code>flight_leg</code> by <code>operating_date</code>, the movement log, the audit tables) and the assignment table, at a few hundred million rows, stays whole. The <code>WITHOUT OVERLAPS</code> constraint removed a 300-line trigger and package pair, and a set of race conditions that had been patched around for years, because a constraint is checked under the same lock the row insert takes and the trigger was not.</p>
<p>Virtual Private Database policies became row-level security. This is the pattern for the nine policies, shown for the base-scoped one:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Oracle: DBMS_RLS.ADD_POLICY(... policy_function =&gt; 'sec.base_predicate' ...)
-- PostgreSQL 18: the predicate becomes a policy on the table

ALTER TABLE ops.flight_leg ENABLE ROW LEVEL SECURITY;
ALTER TABLE ops.flight_leg FORCE ROW LEVEL SECURITY;   -- applies to the table owner too

CREATE POLICY p_flight_leg_base ON ops.flight_leg
    FOR ALL
    TO ops_app, ops_reporting
    USING (
        origin_base = current_setting('app.base_iata', true)
        OR pg_has_role(current_user, 'ops_network_control', 'member')
    );

-- the application sets the context once per transaction through PgBouncer:
-- SET LOCAL app.base_iata = 'BLR';</pre>
<p>The <code>SET LOCAL</code> is deliberate. Under transaction pooling a plain <code>SET</code> leaks to whichever client next gets the server connection; <code>SET LOCAL</code> dies with the transaction, which is the property VPD&rsquo;s session context gave you for free and PgBouncer takes away.</p>
<h3>Materialised views<a class="anchor-link" id="materialised-views"></a></h3>
<p>The six ON COMMIT fast-refresh materialised views had no equivalent and we did not pretend otherwise. Three were replaced by summary tables maintained by statement-level triggers on the base tables (the same mechanism Oracle used underneath, made explicit). Two were replaced by ordinary views once we confirmed PostgreSQL 18 ran the underlying aggregate fast enough with parallel query and a covering index, which Oracle&rsquo;s design predated. One, the crew-hours ledger, kept its trigger-maintained summary plus a nightly <code>REFRESH MATERIALIZED VIEW CONCURRENTLY</code> reconciliation so drift could be detected and corrected rather than assumed away.</p>
<h3>The empty-string audit<a class="anchor-link" id="the-empty-string-audit"></a></h3>
<p>Oracle treats <code>''</code> as NULL; PostgreSQL 18 does not. Every migration has this, and it is a silent-corruption class rather than an error class, so we treat it as a test-suite item. The audit query against the Oracle source found the columns that could ever carry the ambiguity:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- On Oracle: which VARCHAR2 columns are nullable and are compared to '' or wrapped in NVL in code
SELECT DISTINCT c.table_name, c.column_name
FROM   dba_tab_columns c
JOIN   dba_source s
       ON s.owner = c.owner
       AND REGEXP_LIKE(UPPER(s.text), '(NVLs*(s*' || c.column_name || '|' || c.column_name || 's*(=||!=)s*'''')')
WHERE  c.owner = 'OPS'
AND    c.data_type = 'VARCHAR2'
AND    c.nullable = 'Y';</pre>
<p>Every column that came back got a <code>CHECK (col  '')</code> constraint on the PostgreSQL 18 side and a load-time transform that turned empty strings into NULL, so the converted code could keep its <code>IS NULL</code> semantics without a per-predicate rewrite.</p>
<h2>SQL mapping: the patterns that actually appeared<a class="anchor-link" id="sql-mapping-the-patterns-that-actually-appeared"></a></h2>
<p>ora2pg converts the syntactic Oracle-isms. What it cannot do is tell you which converted statement will plan badly. We took the AWR top 50 by elapsed time and by executions, converted them, and ran each under <code>EXPLAIN (ANALYZE, BUFFERS)</code> (buffers are on by default in PostgreSQL 18&rsquo;s <code>EXPLAIN ANALYZE</code>) against the restored data. The recurring patterns:</p>
<p>Old-style outer joins were the most common conversion, and ora2pg handles them. <code>CONNECT BY</code> appeared in the pairing engine for building multi-leg duty chains and became a recursive CTE; the performance was comparable because both are effectively iterative. <code>ROWNUM</code> pagination became <code>FETCH FIRST n ROWS ONLY</code>, but the fifteen queries that used <code>ROWNUM</code> as a row-number-in-order-of-arrival had to be checked one by one because that behaviour is undefined without <code>ORDER BY</code> in both engines and Oracle happened to be consistent about it.</p>
<p>The interesting case was the flight-status lookup, the single most executed statement in the system. On Oracle it used a composite index on <code>(operating_date, carrier_code, flight_number, leg_sequence)</code> and was usually called with all four columns. A second, heavily used variant called it without <code>carrier_code</code> because the caller had the flight number from a codeshare feed. Oracle satisfied that with an index skip scan. PostgreSQL before 18 would not, and the standard fix was a second index. PostgreSQL 18 added skip scan for multicolumn B-tree indexes, and the plan shows it:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF)
SELECT leg_id, status_code, eta_utc
FROM   ops.flight_leg
WHERE  operating_date = DATE '2026-09-07'
AND    flight_number  = 1207
AND    leg_sequence   = 1;

-- Index Only Scan using ix_flight_leg_lookup on flight_leg
--   Index Cond: ((operating_date = '2026-09-07'::date) AND (flight_number = 1207) AND (leg_sequence = 1))
--   Index Searches: 6          &lt;-- one search per distinct carrier_code on that date: the skip scan
--   Heap Fetches: 0
--   Buffers: shared hit=31
-- (abridged; the Index Searches line is new EXPLAIN output in PostgreSQL 18)</pre>
<p>Six index searches instead of a second 40 GB index. The condition for skip scan to pay off is a low-cardinality leading column being skipped, which <code>carrier_code</code> is (the airline plus a handful of codeshare partners). We checked the <code>Index Searches</code> count under load rather than trusting the plan shape; when the skipped column has thousands of distinct values, skip scan is worse than a dedicated index and the planner usually, but not always, knows it.</p>
<p>Hints were the other PostgreSQL 18 conversation. The PL/SQL estate carried around 80 <code>/*+ ... */</code> hints, most of them stale. Community PostgreSQL 18 has no hints and we did not install <code>pg_hint_plan</code>; we fixed the underlying statistics problems instead, which in four cases meant <code>CREATE STATISTICS</code> on correlated columns (base and carrier, date and season) and in one case a partial index. PostgreSQL 19&rsquo;s in-core plan advice will change this conversation for future migrations; it was not an option for this one.</p>
<h2>PL/SQL to PL/pgSQL: how 110 packages became schemas and functions<a class="anchor-link" id="pl-sql-to-pl-pgsql-how-110-packages-became-schemas-and-functions"></a></h2>
<p>Packages do not exist in community PostgreSQL 18. The mechanical mapping is one schema per package, one function or procedure per package member, and a decision about package state. Ninety of the 110 packages were stateless once we looked, which meant the mapping was mechanical. The other 20 held session state in package variables (the current legality rule set, cached lookup tables, a &ldquo;current run ID&rdquo; for the rotation engine). Those became either <code>SET LOCAL</code> custom GUCs read with <code>current_setting()</code> for scalar state, or a session-scoped unlogged table keyed by <code>pg_backend_pid()</code> for anything larger. The GUC route is the one that survives transaction pooling correctly, so it was the default.</p>
<p>Here is a cut-down member of the legality package as it ran on Oracle and as it runs on PostgreSQL 18. It computes the cumulative duty hours for a crew member inside a rolling window, using <code>BULK COLLECT</code> because that was the idiom:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Oracle PL/SQL (abridged), CREW.LEGALITY package
FUNCTION duty_hours_in_window (
    p_crew_member_id IN NUMBER,
    p_window_end     IN TIMESTAMP WITH TIME ZONE,
    p_window_days    IN NUMBER DEFAULT 7
) RETURN NUMBER IS
    TYPE t_hours IS TABLE OF NUMBER;
    l_hours  t_hours;
    l_total  NUMBER := 0;
BEGIN
    SELECT duty_hours
    BULK COLLECT INTO l_hours
    FROM   crew.assignment
    WHERE  crew_member_id = p_crew_member_id
    AND    duty_start &gt;= p_window_end - p_window_days
    AND    duty_start &lt;  p_window_end;

    FOR i IN 1 .. l_hours.COUNT LOOP
        l_total := l_total + l_hours(i);
    END LOOP;
    RETURN l_total;
EXCEPTION
    WHEN NO_DATA_FOUND THEN RETURN 0;
    WHEN OTHERS THEN
        crew.errlog.log_error('duty_hours_in_window', SQLERRM);   -- autonomous transaction inside
        RAISE;
END duty_hours_in_window;</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- PostgreSQL 18 PL/pgSQL, schema crew_legality (one schema per former package)
CREATE OR REPLACE FUNCTION crew_legality.duty_hours_in_window (
    p_crew_member_id bigint,
    p_window_end     timestamptz,
    p_window_days    integer DEFAULT 7
) RETURNS numeric
LANGUAGE plpgsql
STABLE
PARALLEL SAFE
AS $$
DECLARE
    l_total numeric := 0;
BEGIN
    -- BULK COLLECT + loop collapses to a set-based aggregate; the planner does the loop
    SELECT COALESCE(SUM(a.duty_hours), 0)
    INTO   l_total
    FROM   crew.assignment AS a
    WHERE  a.crew_member_id = p_crew_member_id
    AND    a.duty_period &amp;&amp; tstzrange(p_window_end - make_interval(days =&gt; p_window_days),
                                      p_window_end, '[)');
    RETURN l_total;
EXCEPTION
    WHEN OTHERS THEN
        -- no autonomous transactions, and a STABLE function cannot write anyway:
        -- the NOTICE goes to the server log (shipped centrally); the durable errlog row
        -- is written by the calling procedure using the savepoint pattern below
        RAISE NOTICE 'crew_legality.duty_hours_in_window failed: % (%)', SQLERRM, SQLSTATE;
        RAISE;
END;
$$;</pre>
<p>Three things in that conversion are the whole story of the PL/SQL work. The <code>BULK COLLECT</code> and loop became a single aggregate, which is what almost every <code>BULK COLLECT</code> in the estate wanted to be; the Oracle idiom exists to avoid context switches between the SQL and PL/SQL engines, and PL/pgSQL does not have that boundary in the same way. The <code>NO_DATA_FOUND</code> handler disappeared because <code>SELECT ... INTO</code> in PL/pgSQL leaves the variable NULL rather than raising when no row is found (the opposite of Oracle; this is the second most common behavioural trap after the empty-string one, and we grep for every <code>SELECT INTO</code> and decide whether it needs <code>STRICT</code>). And the autonomous-transaction error log is gone.</p>
<p>Autonomous transactions were the one Oracle feature with no clean answer, and we had 14 packages using them, all for the same purpose: write an error or audit row that survives the rollback of the enclosing transaction. We rejected <code>dblink</code> loopback connections for this, because under a 9,000-transaction-per-second workload a loopback connection per error is a connection storm waiting for a bad day. The pattern we used instead: the error row is inserted in the enclosing transaction, and the enclosing transaction is not rolled back on a business exception; the exception is caught at the procedure boundary, the work is undone with a savepoint, and the log row is kept.</p>
<p>For the two cases where the transaction had to abort entirely, the application logs, not the database.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- PostgreSQL 18 savepoint pattern replacing PRAGMA AUTONOMOUS_TRANSACTION for audit-on-failure
CREATE OR REPLACE PROCEDURE crew_ops.apply_pairing (p_pairing_id bigint)
LANGUAGE plpgsql
AS $$
BEGIN
    BEGIN
        -- the unit of work that may fail
        PERFORM crew_ops.assign_pairing_members(p_pairing_id);
        PERFORM crew_legality.validate_pairing(p_pairing_id);
    EXCEPTION
        WHEN check_violation OR exclusion_violation THEN
            -- inner block acts as a savepoint: its writes are rolled back, the outer transaction survives
            INSERT INTO crew_ops.errlog (unit_name, sqlstate, message, ref_id, logged_at)
            VALUES ('crew_ops.apply_pairing', SQLSTATE, SQLERRM, p_pairing_id, clock_timestamp());
            RETURN;   -- caller commits: the audit row persists, the pairing does not
    END;
END;
$$;</pre>
<p>The remaining constructs mapped as follows. <code>DBMS_SCHEDULER</code> jobs became <code>pg_cron</code> entries for anything that is a single SQL statement or procedure call, and moved to the airline&rsquo;s existing application scheduler for the twelve job chains with dependencies, because <code>pg_cron</code> does not do chains and pretending otherwise produces fragile shell scripts. <code>UTL_FILE</code> writes (crew reports to a shared filesystem) moved to the application tier with <code>COPY ... TO STDOUT</code> feeding it; the database no longer touches the filesystem.</p>
<p><code>DBMS_LOB</code> calls became ordinary <code>text</code> and <code>bytea</code> operations. <code>REF CURSOR</code> outputs became <code>refcursor</code> or, more often, set-returning functions, which the JDBC layer handles better. <code>DBMS_AQ</code> is covered above. Object types with methods became composite types plus functions taking the composite as the first argument, which PostgreSQL&rsquo;s function-call syntax lets you write as <code>value.method()</code> anyway.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Partition maintenance that used to be a DBMS_SCHEDULER job, now pg_cron on PostgreSQL 18
SELECT cron.schedule(
    'flight_leg_partitions',
    '0 2 1 * *',                       -- 02:00 on the 1st, server time zone UTC
    $$ CALL ops_maint.create_month_partition('ops.flight_leg', date_trunc('month', now() + interval '2 months')) $$
);

-- Monthly reconciliation of the crew-hours ledger against its summary
SELECT cron.schedule(
    'crew_hours_ledger_refresh',
    '30 3 * * *',
    $$ REFRESH MATERIALIZED VIEW CONCURRENTLY rpt.crew_hours_ledger $$
);</pre>
<p>Every converted unit was tested on PostgreSQL 18 against the Oracle original with a differential harness: the same inputs, both engines, results compared, and every mismatch classified as a defect in the conversion or a defect in the original that Oracle had been hiding. The second category was not empty.</p>
<h2>Data movement and cutover<a class="anchor-link" id="data-movement-and-cutover"></a></h2>
<p>The nine terabytes moved twice: once in bulk for the parallel-run environment, and once more through change data capture for the cutover. Bulk load used ora2pg&rsquo;s parallel COPY export straight into PostgreSQL 18 with indexes and foreign keys dropped, then rebuilt in parallel afterwards; on the target hardware the load ran at a rate bounded by the Oracle-side extract, not by PostgreSQL 18.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># ora2pg 25.0 bulk data export, direct to PostgreSQL 18, 16 parallel table jobs, 8 parallel per-partition jobs
ora2pg -c ora2pg.conf -t COPY 
       -O "SCHEMA=OPS" 
       -O "PG_DSN=dbi:Pg:dbname=ops;host=pg-node1;port=5432" 
       -O "PG_USER=${PG_MIG_USER}" -O "PG_PWD=${PG_MIG_PASSWORD}" 
       -O "JOBS=8" -O "ORACLE_COPIES=8" -O "PARALLEL_TABLES=16" 
       -O "DROP_INDEXES=1" -O "DROP_FKEY=1" -O "TRUNCATE_TABLE=1" 
       -O "DATA_LIMIT=20000" -O "BLOB_LIMIT=500" 
       -O "EMPTY_LOB_NULL=1" -O "REPLACE_ZERO_DATE=-INFINITY"

# then rebuild, in parallel, with the maintenance memory sized above
psql -h pg-node1 -d ops -c "SET maintenance_work_mem = '8GB'; SET max_parallel_maintenance_workers = 8;" 
     -f ops_indexes.sql</pre>
<p>Validation after every load into PostgreSQL 18 was row counts per table plus a content checksum on the 40 critical tables, computed the same way on both sides so the numbers are comparable:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Oracle side
SELECT COUNT(*) AS rows_,
       SUM(ORA_HASH(leg_id || '|' || flight_number || '|' || TO_CHAR(std_utc, 'YYYYMMDDHH24MISS') || '|' || status_code)) AS chk
FROM   ops.flight_leg
WHERE  operating_date &gt;= DATE '2026-01-01';

-- PostgreSQL 18 side: same columns, same formatting, a 64-bit hash folded to match ranges
SELECT COUNT(*) AS rows_,
       SUM(hashtextextended(leg_id::text || '|' || flight_number::text || '|' ||
                            to_char(std_utc AT TIME ZONE 'UTC', 'YYYYMMDDHH24MISS') || '|' || status_code, 0)) AS chk
FROM   ops.flight_leg
WHERE  operating_date &gt;= DATE '2026-01-01';</pre>
<p>The hash functions differ, so the checksum is compared per row on a sample and by count and aggregate on the whole; what matters is that the row-shape string is identical on both sides, including the timestamp formatting, because that is where the DATE-to-timestamptz mapping shows up if it is wrong.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/exadata-to-postgresql-18-cutover-flow.png" alt="Oracle Exadata to PostgreSQL 18 cutover flow: bulk load, CDC catch-up, parallel run, go/no-go gate, write freeze, switch, reverse CDC rollback path" width="1800" height="660" class="aligncenter size-full wp-image-93022"><figcaption>The rollback path is the part of the PostgreSQL 18 cutover plan that makes the rest of it possible to approve.</figcaption></figure>
<p>The change-data-capture bridge was Debezium&rsquo;s Oracle connector reading LogMiner, started from the SCN recorded at the bulk export, into Kafka (already in the estate) and then into PostgreSQL 18 through a sink with the type mapping applied in the sink. Three weeks of parallel run gave us three weeks of reconciliation reports and three weeks of the converted PL/SQL running against real change volume.</p>
<p>The cutover itself was the boring part: application pools drained, writes frozen on Oracle, CDC lag watched to zero, sequences on PostgreSQL 18 advanced past the Oracle high-water marks, connection strings switched at PgBouncer, and reverse CDC from PostgreSQL 18 to Oracle running so that a rollback in the first week would have been a connection-string change and not a data-loss event.</p>
<p>It was never needed. It was drilled twice.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Sequence advance at cutover: PostgreSQL 18 value must exceed every Oracle value ever issued
-- (Oracle CACHE means the last issued value can exceed LAST_NUMBER; use the data, not the catalog)
SELECT setval('ops.flight_leg_leg_id_seq',
              (SELECT COALESCE(MAX(leg_id), 0) + 1000 FROM ops.flight_leg),
              false);

-- Verification after, generated per owned sequence from pg_depend and run as one script:
-- every row must return ok = true before the connection strings are switched
SELECT 'ops.flight_leg_leg_id_seq' AS sequence_name,
       (SELECT last_value FROM ops.flight_leg_leg_id_seq)           AS seq_value,
       (SELECT MAX(leg_id) FROM ops.flight_leg)                     AS table_max,
       (SELECT last_value FROM ops.flight_leg_leg_id_seq)
         &gt; (SELECT MAX(leg_id) FROM ops.flight_leg)                 AS ok;</pre>
<h2>Performance and scalability on the PostgreSQL 18 stack: what we measured<a class="anchor-link" id="performance-and-scalability-on-the-postgresql-18-stack-what-we-measured"></a></h2>
<p>We do not publish the client&rsquo;s numbers, so this section describes what was measured and how, and states the outcome against the targets rather than as absolute figures. The targets were set from Oracle: for each of the top 50 statements, the AWR p95 elapsed time in the peak hour, and for the system, the peak execution rate with 25 percent headroom. The PostgreSQL 18 side of the comparison came from <code>pg_stat_statements</code> with <code>track_io_timing</code> on, sampled at the same hours over the parallel run, and from the load test that replayed captured traffic at 1.25 times peak rate.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- PostgreSQL 18 post-cutover top SQL, comparable to the AWR "SQL ordered by Elapsed Time" section (PG 17+ column names)
SELECT queryid,
       calls,
       ROUND(total_exec_time::numeric / 1000, 1)               AS total_s,
       ROUND(mean_exec_time::numeric, 3)                       AS mean_ms,
       ROUND((shared_blk_read_time + shared_blk_write_time)::numeric, 1) AS io_ms,
       shared_blks_hit,
       shared_blks_read,
       ROUND(100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct,
       LEFT(query, 80)                                          AS query
FROM   pg_stat_statements
WHERE  dbid = (SELECT oid FROM pg_database WHERE datname = 'ops')
ORDER  BY total_exec_time DESC
LIMIT  50;

-- Where the time goes at the I/O layer (PostgreSQL 18: WAL I/O included here, per backend type)
SELECT backend_type, object, context,
       reads, read_time, writes, write_time, fsyncs, fsync_time
FROM   pg_stat_io
WHERE  reads &gt; 0 OR writes &gt; 0
ORDER  BY read_time + write_time DESC;</pre>
<p>The outcome against the targets: all 50 statements met their p95 target at 1.25 times peak; the legality batch, which had been the item everyone expected to regress without Exadata, ran inside its window with margin once the <code>BULK COLLECT</code> loops were set-based; and the three reporting queries that had leaned on smart scan were the only ones that needed design work, which parallel query and one covering index resolved. The cache-hit ratio on <code>pg_stat_io</code> at peak sat where the working-set measurement predicted it would, which is the check that the sizing method was sound rather than lucky.</p>
<p>On scalability, the design has two levers left unpulled. Read scaling goes to the standbys through Patroni&rsquo;s replica routing, and the reporting workload already runs there. Write scaling on a single primary is bounded by the 64 cores and the WAL device; at 1.25 times peak the primary was under half utilised on CPU and the WAL device was under a quarter of its measured throughput.</p>
<p>The airline&rsquo;s growth plan does not reach the point where a single primary becomes the constraint inside the PostgreSQL 18 support window, and we wrote the trigger conditions (sustained CPU over 60 percent at peak, WAL write latency over 2 ms) into the handover so the conversation about partitioning across nodes starts from a measurement, not a feeling.</p>
<h2>What we would do differently<a class="anchor-link" id="what-we-would-do-differently"></a></h2>
<p>Three things. We would run the working-set measurement before agreeing the hardware specification with procurement rather than in parallel with it; it confirmed the 1 TB nodes but it could have told us 768 GB was enough and saved money. We would convert the six materialised views before the PL/SQL rather than after, because the reporting team&rsquo;s acceptance testing was gated on them and it became the critical path. And we would have put the AFTER-trigger role change in PostgreSQL 18 on the assessment checklist from the start; two audit triggers written for Oracle assumed the committing role, PostgreSQL 18 runs them as the role that queued the event, and we found it in test rather than in review.</p>
<p>One thing we would not change: rewriting the PL/SQL instead of emulating it. It cost a quarter of a four-person team and it produced code the airline&rsquo;s own engineers can read, which is the point of leaving a proprietary platform.</p>
<h2>Reproducibility and caveats<a class="anchor-link" id="reproducibility-and-caveats"></a></h2>
<p>Versions, so the claims above can be checked: Oracle Database 19c on Exadata X9M-2; PostgreSQL 18.6 (the current minor at time of writing, released 13 August 2026); ora2pg 25.0; Patroni 4.1.x with etcd 3.5; pgBackRest 2.59; PgBouncer 1.25.2; pg_cron 1.6 or later; Debezium 3.x Oracle connector with LogMiner. Every sizing figure is rounded and the client is anonymised; the arithmetic applies to your estate only after you have replaced our AWR numbers with yours.</p>
<p>Test every conversion pattern here against your own PL/SQL and your own data in a staging environment that mirrors production topology, keep a verified backup and a rehearsed restore in place before any cutover, and keep the reverse CDC path warm until sign-off; a migration without a rollback path is a bet, not a plan.</p>
<p>If you are planning an Exadata exit and want the assessment matrix, the sizing method, or the PL/SQL conversion patterns applied to your estate, that is the work the <a href="https://minervadb.com/postgresql-consulting/">MinervaDB PostgreSQL consulting</a> team does, on-premises and in the cloud, with the same rule we apply to ourselves: the numbers come from the catalog and the AWR, never from the sales deck.</p>
<h2>References<a class="anchor-link" id="references"></a></h2>
<p><a href="https://www.postgresql.org/docs/18/release-18.html" rel="noopener" target="_blank">PostgreSQL 18 release notes</a> &middot; <a href="https://www.postgresql.org/docs/release/" rel="noopener" target="_blank">PostgreSQL release history</a> &middot; <a href="https://www.postgresql.org/docs/18/indexes-multicolumn.html" rel="noopener" target="_blank">Multicolumn indexes and skip scan</a> &middot; <a href="https://www.postgresql.org/docs/18/ddl-constraints.html" rel="noopener" target="_blank">Constraints, including WITHOUT OVERLAPS</a> &middot; <a href="https://www.postgresql.org/docs/18/runtime-config-resource.html" rel="noopener" target="_blank">Resource consumption parameters (io_method, io_workers)</a> &middot; <a href="https://www.postgresql.org/docs/18/monitoring-stats.html" rel="noopener" target="_blank">pg_stat_io and cumulative statistics</a> &middot; <a href="https://www.postgresql.org/docs/18/ddl-rowsecurity.html" rel="noopener" target="_blank">Row security policies</a> &middot; <a href="https://ora2pg.darold.net/" rel="noopener" target="_blank">ora2pg</a> &middot; <a href="https://patroni.readthedocs.io/" rel="noopener" target="_blank">Patroni</a> &middot; <a href="https://pgbackrest.org/" rel="noopener" target="_blank">pgBackRest</a> &middot; <a href="https://www.pgbouncer.org/" rel="noopener" target="_blank">PgBouncer</a> &middot; <a href="https://github.com/citusdata/pg_cron" rel="noopener" target="_blank">pg_cron</a> &middot; <a href="https://debezium.io/documentation/reference/stable/connectors/oracle.html" rel="noopener" target="_blank">Debezium Oracle connector</a> &middot; <a href="https://blogs.oracle.com/exadata/exadata-x9m" rel="noopener" target="_blank">Oracle Exadata X9M</a> &middot; <a href="https://www.oracle.com/a/ocom/docs/engineered-systems/exadata/exadata-x9m-2-ds.pdf" rel="noopener" target="_blank">Exadata X9M-2 data sheet</a></p>

<p><a href="https://minervadb.com/oracle-exadata-to-postgresql-18-migration-airline/">Oracle Exadata to PostgreSQL 18 Migration: An Airline Operations Case Study from the MinervaDB Data Migration Team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MongoDB 8.3 High Availability and Scalability: What Changed On-Premises and in Atlas</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mongodb-8-3-high-availability-and-scalability/" />
      <id>https://minervadb.com/mongodb-8-3-high-availability-and-scalability/</id>
      <updated>2026-09-07T08:22:35+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MongoDB 8.3 shipped on 4 May 2026, and for anyone who runs sharded clusters it is the first minor release where the self-managed builds get the same sharding lifecycle tooling that Atlas has been driving [...]</p>
<p><a href="https://minervadb.com/mongodb-8-3-high-availability-and-scalability/">MongoDB 8.3 High Availability and Scalability: What Changed On-Premises and in Atlas</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MongoDB 8.3 shipped on 4 May 2026, and for anyone who runs sharded clusters it is the first minor release where the self-managed builds get the same sharding lifecycle tooling that Atlas has been driving behind the scenes since 8.0. Three changes matter operationally: the <code>removeShard</code> command is deprecated in favour of a four-step draining workflow you can pause and inspect, a replica set can now be turned directly into a sharded cluster with an embedded config shard in one rolling restart, and a set of overload controls (targeted mirrored reads in 8.2, connection establishment rate limiting, overload-aware retry targeting in MongoDB 8.3) closes the gap that used to open up in the minutes after an election.</p>
<p>This post walks through each of those for MongoDB 8.3 high availability and scalability work, on-premises and in Atlas, and ends with where I would and would not deploy MongoDB 8.3 today.</p>
<p>One thing to get out of the way first, because it changes the whole decision: MongoDB 8.3 is a <em>minor</em> release, not the next long-term line. That has consequences on-premises that Atlas customers never see.</p>
<p><img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/mongodb-8-3-high-availability-scalability.png" alt="MongoDB 8.3 high availability and scalability on-premises and in Atlas" width="1200" height="630" class="aligncenter size-full wp-image-93008"></p>
<h2>Which MongoDB 8.x are you actually running?<a class="anchor-link" id="which-mongodb-8-x-are-you-actually-running"></a></h2>
<p>MongoDB&rsquo;s cadence since 8.0 has two tracks. Major releases (7.0, 8.0) arrive roughly every two years, run on Atlas and on-premises, and carry a five-year lifecycle. Minor releases (8.1, 8.2, 8.3) arrive quarterly-ish and until 8.2 were Atlas-only. Starting with 8.2 the minor track is downloadable for Community and Enterprise Advanced, which is how MongoDB 8.3 ends up on an on-prem server in the first place.</p>
<p>The support policy for minors is the part people miss: once a new minor ships, the previous one stops receiving patches. 8.2 reached end of life on 31 July 2026, ten months after it was released. If you deployed 8.2 on-premises last autumn you are already off support unless you have moved to MongoDB 8.3.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/mongodb-8-3-release-tracks.png" alt="MongoDB 8.3 release tracks: the 8.0 major line versus the 8.1, 8.2 and 8.3 minor line, with support windows" width="1800" height="600" class="aligncenter size-full wp-image-93009"><figcaption>The 8.0 line is the only one with a multi-year patch horizon. MongoDB 8.3 is current, but its patch window ends the day 8.4 (or 9.0) ships.</figcaption></figure>
<p>Three more rules from the versioning policy that shape a self-managed upgrade plan. Minor-to-minor upgrades are strictly sequential, so an 8.1 cluster goes through 8.2 before it can reach MongoDB 8.3, and each hop is a binary upgrade plus an FCV bump. Going from a minor back to a major is a downgrade, and binary downgrades are not supported on Community Edition at all. And two features are explicitly unsupported on the minor track: Atlas Live Migration and <code>mongosync</code>. If you have a cluster-to-cluster sync running for DR, or you are planning a migration into Atlas next year, a MongoDB 8.3 source takes those tools off the table.</p>
<p>The 8.0 line, by contrast, is the one that receives backports. The connection rate limiter that I cover below is documented as available in 8.0.12, and the initial sync index memory controls in 8.0.13. Percona Server for MongoDB, which is what most of our self-managed customers run, is built on the 7.0 and 8.0 lines only; the 8.0.29-13 build from 20 August 2026 carries the same CVE fixes as upstream 8.0.29. There is no Percona build of MongoDB 8.3.</p>
<h2>MongoDB 8.3 scalability: what changed in the sharded cluster lifecycle<a class="anchor-link" id="mongodb-8-3-scalability-what-changed-in-the-sharded-cluster-lifecycle"></a></h2>
<h3>Config shards, and the three-shard rule<a class="anchor-link" id="config-shards-and-the-three-shard-rule"></a></h3>
<p>Since 8.0 a sharded cluster no longer needs a dedicated config server replica set. One shard can carry the cluster metadata alongside application data; MongoDB calls this a config shard or embedded config server. On a small cluster that removes three nodes of infrastructure, which is the difference between nine <code>mongod</code> processes and six for a two-shard deployment.</p>
<p>The documentation&rsquo;s guidance is blunt, and I agree with it from operating both shapes: use a config shard at three shards or fewer, and move to a dedicated CSRS beyond that, or earlier if the workload is latency-sensitive enough that you do not want metadata reads and refreshes competing with user I/O on the same WiredTiger cache. Queryable Encryption collections and on-prem queryable backups also require a dedicated config server.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/mongodb-8-3-config-shard-vs-dedicated-csrs.png" alt="MongoDB 8.3 sharded cluster topology: embedded config shard versus dedicated config server replica set" width="1800" height="760" class="aligncenter size-full wp-image-93010"><figcaption>The embedded shape saves three nodes; the dedicated shape isolates metadata I/O. The transition between them is online in both directions.</figcaption></figure>
<p>In Atlas the decision is made for you. Atlas-Managed Config Servers is on by default for every 8.0+ sharded cluster: at five shards or fewer Atlas runs an embedded config server, and when you add a sixth shard it transitions to a dedicated config server automatically, draining user data off <code>config-0</code> with chunk migrations and <code>moveCollection</code>, waiting out <code>orphanCleanupDelaySecs</code>, and then adding a replacement shard to keep your shard count.</p>
<p>That transition is online but it is not free: receiving shards see elevated CPU, memory and I/O for the duration, you cannot change the cluster tier while it runs, and Atlas tells you not to cancel it. On a multi-terabyte cluster it can run for days. Clusters using Atlas Search, unsharded time series collections or Queryable Encryption are pinned to whichever config type they started with.</p>
<h3>MongoDB 8.3: replica set to sharded cluster in one rolling restart<a class="anchor-link" id="mongodb-8-3-replica-set-to-sharded-cluster-in-one-rolling-restart"></a></h3>
<p>This is the change I am most pleased about for self-managed estates. Before MongoDB 8.3, converting a replica set into a sharded cluster with a config shard meant first turning it into a dedicated config server and then transitioning, which was awkward because a dedicated CSRS is not supposed to hold user data. MongoDB 8.3 adds <code>--replicaSetConfigShardMaintenanceMode</code>, which relaxes the startup checks so the members can be restarted as <code>--configsvr</code> while still carrying application data.</p>
<p>The sequence, on a three-member replica set <code>rs0</code>, secondaries first:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># Step 1: rolling restart, one member at a time, in maintenance mode
mongod --config /etc/mongod.conf --configsvr --replicaSetConfigShardMaintenanceMode
# wait for rs.status() to show the member back in SECONDARY before the next one</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// Step 2: on the primary, flag the replica set as a config server
var conf = rs.conf();
conf.configsvr = true;
conf.version += 1;
rs.reconfig(conf);

// confirm every member has applied it before going further
db.aggregate([
  { $documents: rs.status().members },
  { $group: { _id: null, allConfigSvr: { $min: { $eq: ["$configsvr", true] } } } }
]);
// expected: { _id: null, allConfigSvr: true }</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># Step 3: second rolling restart WITHOUT the maintenance flag
mongod --config /etc/mongod.conf --configsvr

# Step 4: start a router pointing at the set
mongos --config /etc/mongos.conf   # sharding.configDB: rs0/host1:27017,host2:27017,host3:27017</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// Step 5: through mongos, make rs0 both config server and first shard
db.adminCommand({ transitionFromDedicatedConfigServer: 1 });
// { ok: 1 }

sh.isConfigShardEnabled();          // enabled: true
db.adminCommand({ listShards: 1 }); // shard _id "config" is rs0</pre>
<p>Two things to write into the runbook before you run this. First, the application connection string changes from the replica set seed list to the <code>mongos</code> address, so plan the cutover with the application team rather than discovering it during the change window. Second, and this is new in MongoDB 8.3: a replica set that has been a sharded cluster cannot be converted back to a plain replica set. The shard identity document survives, and clearing it is a support-assisted procedure. Treat the conversion as one-way and rehearse it on a restored backup first.</p>
<h3>Draining a shard without <code>removeShard</code><a class="anchor-link" id="draining-a-shard-without-removeshard"></a></h3>
<p><code>removeShard</code> has been the same opaque call since 2.x: issue it, poll it, and hope the balancer keeps moving. MongoDB 8.3 deprecates it and splits the operation into four commands, all run through <code>mongos</code> with the <code>clusterManager</code> role.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// 1. begin draining; the balancer does the actual chunk moves, so it must be enabled
db.adminCommand({ balancerStatus: 1 });          // mode: "full"
db.adminCommand({ startShardDraining: "shard04" });

// 2. poll; this is the part removeShard never gave you cleanly
db.adminCommand({ shardDrainingStatus: "shard04" });
/* abridged from the MongoDB 8.3 reference:
{
  shard: "shard04",
  remainingCriticalSectionChunks: 0,
  totalChunksToDrain: 15,
  chunksLeftToDrain: 8,
  databases: [ { name: "orders", isPrimary: true, collections: [ ... ] } ],
  ok: 1
} */

// 3. anything the balancer cannot move has to be moved by hand:
//    databases that have shard04 as primary shard ...
db.adminCommand({ movePrimary: "orders", to: "shard03" });
//    ... and unsharded collections that live on shard04
db.adminCommand({ moveCollection: "orders.fx_rates", toShard: "shard03" });

// 4. commit only when chunksLeftToDrain is 0 and databases is empty
db.adminCommand({ commitShardRemoval: "shard04" });

// change of plan? draining is reversible until you commit
db.adminCommand({ stopShardDraining: "shard04" });</pre>
<p>The operational gain is not the syntax; it is that draining is now an inspectable, stoppable state. On a cluster where the balancer window is restricted to nights, you can start the drain on Monday, watch <code>chunksLeftToDrain</code> fall over the week, and commit on Friday without ever holding the old command open. Two constraints carry over from <code>removeShard</code> and are worth restating: you cannot take a cluster backup while a drain is in progress, and open change stream cursors may close and not resume across it. None of these commands exist on Atlas, where shard removal is a cluster-configuration change that Atlas executes for you.</p>
<h3>Moving data without changing the shard key<a class="anchor-link" id="moving-data-without-changing-the-shard-key"></a></h3>
<p>The rest of the data-movement toolbox arrived in 8.0 and is unchanged in MongoDB 8.3, but it is what makes the draining workflow usable, so it belongs here. <code>moveCollection</code> relocates an unsharded collection to a named shard. <code>unshardCollection</code> collapses a sharded collection onto one shard. And <code>reshardCollection</code> with <code>forceRedistribution: true</code> rebalances a collection across the current shard set on its existing key, which is how you spread a collection onto a newly added shard without waiting for the balancer&rsquo;s chunk-by-chunk pace.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// spread orders.events across all shards on the same key after adding shard05
db.adminCommand({
  reshardCollection: "orders.events",
  key: { tenant_id: "hashed" },
  forceRedistribution: true
});

// progress: remainingOperationTimeEstimatedSecs is -1 until the clone phase has run long enough to extrapolate
db.getSiblingDB("admin").aggregate([
  { $currentOp: { allUsers: true, localOps: false } },
  { $match: { type: "op", "originatingCommand.reshardCollection": "orders.events" } },
  { $project: { shard: 1, desc: 1, totalOperationTimeElapsedSecs: 1, remainingOperationTimeEstimatedSecs: 1 } }
]);</pre>
<p>All three run on the resharding machinery, so all three inherit its preconditions, and these are the numbers I check before approving one on a production cluster: each participating shard needs free storage of at least twice the collection size plus indexes divided by the shard count, I/O utilisation under 50 percent, CPU under 80 percent, and <code>writeConcernMajorityJournalDefault</code> set to <code>true</code>. The operation has a floor of about five minutes even for a tiny collection, and it ends with a critical section where writes to the collection are blocked for roughly two seconds. Applications with aggressive client-side timeouts notice that. Index builds started during the operation can fail silently, so freeze DDL on the namespace for the duration.</p>
<h3>The orphan cleanup change that kills your reporting queries<a class="anchor-link" id="the-orphan-cleanup-change-that-kills-your-reporting-queries"></a></h3>
<p>This one is easy to miss in the 8.2 notes and it is the change most likely to show up as a mystery ticket after an upgrade. After a chunk migration the donor shard has to delete the range it gave away. Two defaults changed in 8.2. <code>orphanCleanupDelaySecs</code> went from 900 to 3600 seconds, so orphans now sit on the donor for an hour before deletion. And <code>terminateSecondaryReadsOnOrphanCleanup</code> was introduced with a default of <code>true</code>: when the range deletion finally runs, any long-running read on a secondary that could still be reading the orphaned range is terminated first.</p>
<p>The intent is correctness, because a secondary read that spans a range deletion could return partial results. The effect is that hour-long analytics queries pinned to secondaries with <code>readPreference: secondary</code> die at unpredictable times after every migration. The counter to watch is <code>serverStatus().metrics.operation.killedDueToRangeDeletion</code>; if it climbs, either move those queries to a dedicated analytics node with a balancer window that avoids them, or raise <code>orphanCleanupDelaySecs</code> beyond the longest legitimate query. Note that a new value only applies to range deletions created after the change; existing ones keep the old delay until you step the primary down.</p>
<div>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Pre-8.2</th>
<th>8.2 / 8.3 default</th>
<th>Unit</th>
<th>Change requires</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>orphanCleanupDelaySecs</code></td>
<td>900</td>
<td>3600</td>
<td>seconds</td>
<td>runtime <code>setParameter</code>; applies to new range deletions only</td>
</tr>
<tr>
<td><code>terminateSecondaryReadsOnOrphanCleanup</code></td>
<td>n/a</td>
<td>true</td>
<td>boolean</td>
<td>runtime <code>setParameter</code></td>
</tr>
<tr>
<td><code>mirrorReads.targetedMirroring.tag</code></td>
<td>n/a</td>
<td>{} (off)</td>
<td>document</td>
<td>runtime <code>setParameter</code>, primary only</td>
</tr>
<tr>
<td><code>ingressConnectionEstablishmentRateLimiterEnabled</code></td>
<td>n/a (8.0.12+)</td>
<td>false</td>
<td>boolean</td>
<td>runtime <code>setParameter</code></td>
</tr>
<tr>
<td><code>initialSyncIndexBuildMemoryPercentage</code></td>
<td>n/a (8.0.13+)</td>
<td>10.0</td>
<td>percent of RAM</td>
<td>runtime <code>setParameter</code></td>
</tr>
<tr>
<td><code>overloadAwareServerSelectionEnabled</code></td>
<td>n/a</td>
<td>false (8.3)</td>
<td>boolean</td>
<td>runtime <code>setParameter</code></td>
</tr>
</tbody>
</table>
</div>
<h2>MongoDB 8.3 high availability: closing the post-election gap<a class="anchor-link" id="mongodb-8-3-high-availability-closing-the-post-election-gap"></a></h2>
<p>Elections in MongoDB have been fast for years; a healthy replica set picks a new primary in well under ten seconds. The availability loss that actually hurts is what happens in the two or three minutes afterwards: the new primary has a cold cache, every application pool reconnects at once, and the retry logic in drivers and in <code>mongos</code> hammers whichever node answered first. The changes in MongoDB 8.2 and MongoDB 8.3 target exactly that window.</p>
<figure>
<img decoding="async" loading="lazy" src="https://minervadb.com/wp-content/uploads/2026/09/mongodb-8-3-failover-controls.png" alt="MongoDB 8.3 high availability controls and where they act during a failover" width="1800" height="660" class="aligncenter size-full wp-image-93011"><figcaption>None of the MongoDB 8.3 controls shorten the election. They shorten what comes after it.</figcaption></figure>
<h3>Targeted mirrored reads (8.2)<a class="anchor-link" id="targeted-mirrored-reads-8-2"></a></h3>
<p>Mirrored reads have been on by default since 4.4: the primary forwards one percent of eligible reads (<code>find</code>, <code>count</code>, <code>distinct</code>, and the filter portion of <code>update</code> and <code>findAndModify</code>) to electable secondaries, fire-and-forget, so their caches are not stone cold when one of them wins an election. The limitation was that it sprayed evenly across every electable member and could not reach hidden nodes. 8.2 adds <code>targetedMirroring</code>, which mirrors to members matching a replica set tag, at its own sampling rate, and hidden members are allowed.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// tag the member you intend to fail over to (a DR-site member, or the one with priority 2)
var cfg = rs.conf();
cfg.members[2].tags = { warm: "standby" };
cfg.version += 1;
rs.reconfig(cfg);

// on the primary: keep the 1% general mirroring, and additionally mirror 25% of reads to the tagged member
db.adminCommand({
  setParameter: 1,
  mirrorReads: {
    samplingRate: 0.01,
    maxTimeMS: 1000,
    targetedMirroring: { tag: { warm: "standby" }, samplingRate: 0.25, maxTimeMS: 1000 }
  }
});

// confirm it is doing something
db.serverStatus({ mirroredReads: 1 }).mirroredReads;</pre>
<p>A few cautions from running this. The setting lives on the primary only, so after a failover the new primary has whatever <code>mirrorReads</code> value it was started with; put it in <code>mongod.conf</code> under <code>setParameter</code> on every member, not just the current primary. Only one tag can be supplied, and every member carrying that tag is targeted. Mirrored reads consume connections from a pool capped by <code>mirrorReadsMaxConnPoolSize</code> (default 4, new in 8.2), so a high targeted sampling rate on a busy primary will start dropping mirrors rather than slowing the primary, which is the right failure mode but means the warm-up is best-effort.</p>
<p>And it warms the WiredTiger cache, not the filesystem cache or the plan cache in any guaranteed way; on a member with much less RAM than the primary it cannot do much.</p>
<h3>Connection establishment rate limiting (8.2, in 8.0.12)<a class="anchor-link" id="connection-establishment-rate-limiting-8-2-in-8-0-12"></a></h3>
<p>Of everything in this post, this is the change I would enable first on a production replica set, and the fact that it is backported to 8.0.12 means you do not need the minor track to get it. When a primary steps down, every application server&rsquo;s pool detects the topology change and reconnects. A fleet of 400 application pods with 50-connection pools is 20,000 TLS handshakes and SCRAM exchanges landing on the new primary inside a few seconds, and on a mid-sized node that alone can push CPU to saturation and delay the very operations the pools are reconnecting to run.</p>
<p>Before 8.2 the only knobs were <code>maxIncomingConnections</code>, which is a hard cap, and driver-side jitter, which you do not control across every team.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># mongod.conf: admit 500 new connections/sec, absorb a 4-second burst above that,
# queue up to 2000 more, reject the rest with a retryable error
setParameter:
  ingressConnectionEstablishmentRateLimiterEnabled: true
  ingressConnectionEstablishmentRatePerSec: 500
  ingressConnectionEstablishmentBurstCapacitySecs: 4
  ingressConnectionEstablishmentMaxQueueDepth: 2000</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// what to graph after enabling it
var s = db.serverStatus();
printjson({
  queued:   s.connections.queuedForEstablishment,
  rejected: s.connections.establishmentRateLimit.rejected,
  exempted: s.connections.establishmentRateLimit.exempted,
  avgQueuedMicros: s.queues.ingressSessionEstablishment.averageTimeQueuedMicros,
  tlsHandshakeMicros: s.metrics.network.averageTimeToCompletedTLSHandshakeMicros,
  authMicros:         s.metrics.network.averageTimeToCompletedAuthMicros
});</pre>
<p>The numbers above are a starting shape, not a recommendation; size the rate from your own measured reconnect behaviour. The way to get that number is to look at <code>averageTimeToCompletedTLSHandshakeMicros</code> and <code>averageTimeToCompletedAuthMicros</code> during a planned stepdown in staging with the limiter off, then set the rate so the queue drains in a few seconds rather than letting the handshakes starve the oplog appliers. The default <code>MaxQueueDepth</code> of 0 rejects anything that would queue, which is almost never what you want once the limiter is on; pick a real depth. And test the driver behaviour: a rejected connection surfaces as a network error the driver retries with backoff, which is fine for modern drivers and not fine for anything hand-rolled.</p>
<h3>Overload-aware server selection (8.3)<a class="anchor-link" id="overload-aware-server-selection-8-3"></a></h3>
<p>MongoDB 8.3 adds <code>overloadAwareServerSelectionEnabled</code>, off by default. It changes how a <code>mongos</code> or a <code>mongod</code> acting as an internal client picks a target when an operation fails with an error labelled <code>SystemOverloadedError</code>: instead of retrying against the same member it prefers one that has not recently reported overload. Alongside it, MongoDB 8.3 gives the internal retry path a token bucket (<code>shardRetryTokenBucketCapacity</code>, <code>shardRetryTokenReturnRate</code>) and explicit backoff controls (<code>defaultClientRetryAttempts</code>, default 3, plus base and max backoff in milliseconds).</p>
<p>Together these are the sharded-cluster equivalent of the connection limiter: they stop a mongos fleet from converting one slow shard secondary into a cluster-wide retry storm. I have not yet run this one under a production-shaped load test, so I am reporting what it does rather than how much it helps; it is on my list for the next MongoDB 8.3 lab pass.</p>
<h3>Rebuilding a member faster<a class="anchor-link" id="rebuilding-a-member-faster"></a></h3>
<p>Initial sync is the other place where HA quietly degrades: while a replaced member is syncing, a three-member set is running with a majority of two out of two live members, and a second failure means read-only. 8.2 lets index builds during initial sync use a percentage of RAM (<code>initialSyncIndexBuildMemoryPercentage</code>, default 10, bounded by <code>initialSyncIndexBuildMemoryMinMB</code> 200 and <code>MaxMB</code> 16384) instead of the old fixed budget. On a 256 GB node that is 16 GB for index builds instead of a few hundred megabytes, and it is backported to 8.0.13 and 7.0.26. Raise it during a rebuild and drop it back afterwards; 40 percent on a member that is otherwise idle during sync is a reasonable ceiling.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="javascript">// on the syncing member, before it reaches the index build phase
db.adminCommand({ setParameter: 1, initialSyncIndexBuildMemoryPercentage: 40 });

// catch-up visibility, new in MongoDB 8.3
db.serverStatus().metrics.repl.network.oplogFetcherLagSeconds;
db.serverStatus().metrics.repl.network.oplogGetMoresProcessed;</pre>
<h3>What &ldquo;majority&rdquo; means since 8.0<a class="anchor-link" id="what-majority-means-since-8-0"></a></h3>
<p>One 8.0 change still surprises teams moving from 7.0 and it matters for how you read HA metrics. <code>w: "majority"</code> now acknowledges when a majority of members have <em>written</em> the oplog entry, not when they have <em>applied</em> it. Writes get faster; a read from a secondary immediately after an acknowledged write, without causal consistency, can still miss it. <code>rs.status()</code> exposes the distinction per member as <code>optimeWritten</code> alongside <code>optime</code>, and if you alert on replication lag you should decide which of the two you mean. For application correctness, sessions with causal consistency or <code>readConcern: "majority"</code> behave exactly as before.</p>
<h2>MongoDB 8.3 high availability and scalability on-premises versus Atlas<a class="anchor-link" id="mongodb-8-3-high-availability-and-scalability-on-premises-versus-atlas"></a></h2>
<p>The same server code runs in both places, but the operational surface for MongoDB 8.3 high availability and scalability is different enough that the runbooks do not transfer. The table is how I frame it for customers deciding where a new sharded workload should live.</p>
<div>
<table>
<thead>
<tr>
<th>Concern</th>
<th>Self-managed (Community / EA / Percona)</th>
<th>Atlas</th>
</tr>
</thead>
<tbody>
<tr>
<td>Version track</td>
<td>8.0.x for a multi-year patch horizon; MongoDB 8.3 only if you need its features and accept sequential minor upgrades and no <code>mongosync</code></td>
<td>Pin &ldquo;major version&rdquo; (8.0) or opt into auto-upgrade to latest; once auto-upgraded onto a minor you cannot return to the major until the next major ships</td>
</tr>
<tr>
<td>Config server type</td>
<td>Your call; embedded up to 3 shards, dedicated beyond, with <code>transitionTo/FromDedicatedConfigServer</code> and the MongoDB 8.3 direct conversion</td>
<td>Atlas-managed; embedded to 5 shards, automatic online transition beyond, blocked for Search/QE/unsharded time series</td>
</tr>
<tr>
<td>Adding or removing shards</td>
<td><code>addShard</code>; MongoDB 8.3 draining workflow; you own balancer windows and <code>movePrimary</code></td>
<td>Change shard count in the cluster config; draining commands are not exposed</td>
</tr>
<tr>
<td>Vertical scaling</td>
<td>Rolling hardware or VM changes, your rolling-restart runbook</td>
<td>Tier change with rolling restart; reactive and predictive compute auto-scaling, storage auto-scaling</td>
</tr>
<tr>
<td>Post-failover overload</td>
<td>Configure the 8.2/8.3 parameters above yourself, per member, in <code>mongod.conf</code></td>
<td>Server parameters largely not user-settable; rely on Atlas tier headroom and driver settings</td>
</tr>
<tr>
<td>Cross-region HA</td>
<td>Replica set members across sites with priorities and tags; zone sharding by hand</td>
<td>Multi-region and multi-cloud replica sets, Global Clusters (always dedicated config servers)</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>Percona Operator for MongoDB or the MongoDB Enterprise operator; you own storage classes and PDBs</td>
<td>Atlas Kubernetes Operator manages Atlas resources, not pods</td>
</tr>
</tbody>
</table>
</div>
<p>The honest summary is that Atlas removes the sharding lifecycle work, which is where most self-managed sharded clusters go wrong, and in exchange takes away the server-parameter surface that lets you tune the post-election window. If your failure mode is &ldquo;we mis-sized a shard migration&rdquo;, Atlas is the safer place. If your failure mode is &ldquo;a reconnect storm took the primary to 100 percent CPU&rdquo;, self-managed 8.0.12+ with the connection limiter is the more controllable one. For most of the estates we run, a self-managed 8.0.x line on Percona builds with the backported controls enabled is where the risk is lowest today.</p>
<h2>Where I would deploy MongoDB 8.3, and where I would not<a class="anchor-link" id="where-i-would-deploy-mongodb-8-3-and-where-i-would-not"></a></h2>
<p>As of September 2026: on Atlas, take MongoDB 8.3 on non-production and on production clusters that do not depend on Live Migration, and keep production sharded clusters pinned to the 8.0 major until you have read the no-revert rule twice. On-premises, stay on the 8.0 line for anything that has to be supportable in 2028, and reach for MongoDB 8.3 only when a feature forces it: the direct replica set to config shard conversion, the shard draining workflow, or the search, vector search and Queryable Encryption capabilities that are the stated reason the minor track exists on-prem at all.</p>
<p>If you do go to MongoDB 8.3 self-managed, budget for a binary upgrade every quarter, because the patch window closes the day the next minor ships, and check the Linux kernel: 8.2 carried a TCMalloc incompatibility with kernels 6.19 through 7.0.13 that crashed the process, and the fix was to move the kernel, not the database.</p>
<p>Whichever version you land on, MongoDB 8.3 high availability and scalability still come down to operations. The four things that decide availability in most MongoDB incidents I get called into are not version features. They are an oplog window shorter than the longest maintenance operation, a balancer running during peak hours, secondaries with less RAM than the primary they were expected to replace, and nobody having rehearsed the failover. MongoDB 8.3 gives you better tools for the aftermath of an election; it does not replace the drill.</p>
<p>Everything above should be tested against your own workload in a staging environment that mirrors production topology before any of it is applied to production, with a verified backup and a rehearsed restore in place. If you want a second pair of eyes on a sharded cluster design, an 8.0 to MongoDB 8.3 upgrade plan, or a failover drill, that is the kind of work the <a href="https://minervadb.com/mongodb-consulting/">MinervaDB MongoDB consulting</a> team does every week, on-premises and in Atlas.</p>
<h2>References<a class="anchor-link" id="references"></a></h2>
<p><a href="https://www.mongodb.com/docs/manual/release-notes/8.3/" rel="noopener" target="_blank">Release Notes for MongoDB 8.3</a> &middot; <a href="https://www.mongodb.com/docs/manual/release-notes/8.2/" rel="noopener" target="_blank">Release Notes for MongoDB 8.2</a> &middot; <a href="https://www.mongodb.com/docs/manual/reference/versioning/" rel="noopener" target="_blank">MongoDB Versioning</a> &middot; <a href="https://www.mongodb.com/docs/manual/core/config-shard/" rel="noopener" target="_blank">Config Shard</a> &middot; <a href="https://www.mongodb.com/docs/manual/tutorial/convert-replica-set-to-embedded-config-server/" rel="noopener" target="_blank">Convert Replica Set to an Embedded Config Shard</a> &middot; <a href="https://www.mongodb.com/docs/manual/reference/command/startShardDraining/" rel="noopener" target="_blank">startShardDraining</a> &middot; <a href="https://www.mongodb.com/docs/manual/reference/command/moveCollection/" rel="noopener" target="_blank">moveCollection</a> &middot; <a href="https://www.mongodb.com/docs/manual/core/sharding-reshard-a-collection/" rel="noopener" target="_blank">Reshard a Collection</a> &middot; <a href="https://www.mongodb.com/docs/manual/reference/parameters/" rel="noopener" target="_blank">Server Parameters</a> &middot; <a href="https://www.mongodb.com/docs/atlas/transition-to-dedicated-config-servers/" rel="noopener" target="_blank">Atlas: Transition to Dedicated Config Servers</a> &middot; <a href="https://www.mongodb.com/docs/atlas/architecture/current/scalability/" rel="noopener" target="_blank">Atlas Architecture Center: Scalability</a> &middot; <a href="https://www.mongodb.com/legal/support-policy/lifecycles" rel="noopener" target="_blank">MongoDB Software Lifecycle Schedules</a> &middot; <a href="https://docs.percona.com/new/2026/08/20/percona-server-for-mongodb-7040-22-and-8029-13-have-been-released/" rel="noopener" target="_blank">Percona Server for MongoDB 8.0.29-13</a></p>

<p><a href="https://minervadb.com/mongodb-8-3-high-availability-and-scalability/">MongoDB 8.3 High Availability and Scalability: What Changed On-Premises and in Atlas</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Rethinking PAM</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/rethinking-pam/" />
      <id>https://mariadb.org/rethinking-pam/</id>
      <updated>2026-09-07T06:59:17+03:00</updated>
      <author><name>Daniel Black</name></author>
      <summary type="html"><![CDATA[<p>PAM, Pluggable Authentication Modules, originated in Solaris and became common place in all Linux, BSDs, and AIX, HP-UX and macOS. MariaDB has had PAM support as an option since MariaDB 5.2.10 becoming stable in 10.0.11. …<br />
Continue reading \"Rethinking PAM\"<br />
Rethinking PAM appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/rethinking-pam/">Rethinking PAM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>PAM, Pluggable Authentication Modules, originated in Solaris and became common place in all Linux, BSDs, and AIX, HP-UX and macOS. MariaDB has had PAM support as an option since MariaDB <a href="https://mariadb.org/rethinking-pam/" data-type='"post"' data-id='"721"'>5.2.10</a> becoming stable in 10.0.11. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/rethinking-pam/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;Rethinking PAM&rdquo;</span></a></p>
<p><a href="https://mariadb.org/rethinking-pam/">Rethinking PAM</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/rethinking-pam/">Rethinking PAM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>From a Chocolate Wrapper to Concurrent InnoDB Page Splits</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/" />
      <id>https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/</id>
      <updated>2026-09-05T20:24:39+03:00</updated>
      <author><name>Roman Nozdrin</name></author>
      <summary type="html"><![CDATA[<p>This work started from a conversation with Monty Widenius at Meet Magento Czech in August. We discussed possible ways to improve the scalability of the InnoDB B+Tree, especially its insert path and page splits. …<br />
Continue reading \"From a Chocolate Wrapper to Concurrent InnoDB Page Splits\"<br />
From a Chocolate Wrapper to Concurrent InnoDB Page Splits appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/">From a Chocolate Wrapper to Concurrent InnoDB Page Splits</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This work started from a conversation with Monty Widenius at Meet Magento Czech in August. We discussed possible ways to improve the scalability of the InnoDB B+Tree, especially its insert path and page splits. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;From a Chocolate Wrapper to Concurrent InnoDB Page Splits&rdquo;</span></a></p>
<p><a href="https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/">From a Chocolate Wrapper to Concurrent InnoDB Page Splits</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/from-a-chocolate-wrapper-to-concurrent-innodb-page-splits/">From a Chocolate Wrapper to Concurrent InnoDB Page Splits</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Polyglot Persistence: The Proven Case for 5 Data Models</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/polyglot-persistence/" />
      <id>https://minervadb.com/polyglot-persistence/</id>
      <updated>2026-09-05T06:07:42+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>Your transactional database is not failing you. It is doing precisely what it was engineered to do: take a write, make it durable, isolate it from every other write, and never lie about it. The [...]</p>
<p><a href="https://minervadb.com/polyglot-persistence/">Polyglot Persistence: The Proven Case for 5 Data Models</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Your transactional database is not failing you. It is doing precisely what it was engineered to do: take a write, make it durable, isolate it from every other write, and never lie about it. The problem is that a consumer-facing business no longer has one data consumption model. It has at least five, and each of them wants a storage layout and a consistency contract that contradicts the others. That contradiction is the whole argument for polyglot persistence, and this post makes it with measurements rather than slogans.</p>
<p>We will show, on a real PostgreSQL 16 and a real ClickHouse 26 build, why the same table and the same question cost 1.79 GB of I/O in one engine and 22 MiB in the other, why sixty-four writers queue behind a single row no matter how many cores you buy, why eventually consistent platforms exist at all, and why a vector index is a different kind of object from anything in the relational world. Then we lay out the polyglot persistence decision frame a CTO or an investor should apply before believing any &ldquo;one database for everything&rdquo; story, including ours.</p>
<h2>What polyglot persistence actually claims<a class="anchor-link" id="what-polyglot-persistence-actually-claims"></a></h2>
<p>Martin Fowler gave the pattern its name in 2011: <a href="https://martinfowler.com/bliki/PolyglotPersistence.html" target="_blank" rel="noopener">polyglot persistence</a> means using different data storage technologies for different data, chosen by how the application reads and writes that data. The idea of polyglot persistence predates the term. Every large internet company arrived at polyglot persistence independently, usually after an outage, and usually after trying very hard not to.</p>
<p>The version of polyglot persistence that matters to a board is narrower. A business that serves consumers on the internet runs four or five workload classes with incompatible physics: transactions that must be exactly right, interactive traffic that must be fast everywhere, analytics that must scan everything, retrieval that must be semantically close, and the elastic cloud substrate underneath all of it. No single engine is optimal for more than one or two of those. Each additional engine is a real cost, so the decision is about which contradictions you can afford to paper over and which you cannot.</p>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5NjAiIGhlaWdodD0iNTgwIiB2aWV3Qm94PSIwIDAgOTYwIDU4MCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KPHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI1ODAiIGZpbGw9IiNmZmZmZmYiLz4KPHRleHQgeD0iNDgwIiB5PSIzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPk9uZSBidXNpbmVzcywgZml2ZSBkYXRhIG1vZGVsczogdGhlIHBvbHlnbG90IHBlcnNpc3RlbmNlIGxhbmRzY2FwZTwvdGV4dD4KPHRleHQgeD0iNDgwIiB5PSI1OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzZiNzI4MCI+UGxhY2VtZW50IGlzIHF1YWxpdGF0aXZlLiBFYWNoIGVuZ2luZSBpcyBlbmdpbmVlcmVkIGZvciBpdHMgcmVnaW9uIG9mIHRoZSBwbGFuZTsgbm9uZSBjb3ZlcnMgYWxsIG9mIGl0LjwvdGV4dD4KPCEtLSBheGVzIC0tPgo8bGluZSB4MT0iOTAiIHkxPSI1MjAiIHgyPSI5MTAiIHkyPSI1MjAiIHN0cm9rZT0iIzBhMTQyNCIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxsaW5lIHgxPSI5MCIgeTE9IjUyMCIgeDI9IjkwIiB5Mj0iODAiIHN0cm9rZT0iIzBhMTQyNCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9IjUwMCIgeT0iNTUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjM2QzZDQ0Ij5SZWFkIHNoYXBlOiBwb2ludCBsb29rdXBzIGJ5IGtleSBvbiB0aGUgbGVmdCwgd2lkZSBzY2FucyBhbmQgYWdncmVnYXRlcyBvdmVyIGJpbGxpb25zIG9mIHJvd3Mgb24gdGhlIHJpZ2h0PC90ZXh0Pgo8dGV4dCB4PSI0MCIgeT0iMzAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjM2QzZDQ0IiB0cmFuc2Zvcm09InJvdGF0ZSgtOTAgNDAgMzAwKSI+V3JpdGUgY29udHJhY3Q6IHN0cmljdCBBQ0lEIGF0IHRoZSBib3R0b20sIGV2ZW50dWFsIC8gYXBwZW5kLW9ubHkgLyBhcHByb3hpbWF0ZSBhdCB0aGUgdG9wPC90ZXh0Pgo8IS0tIEV2ZW50dWFsbHkgY29uc2lzdGVudCAtLT4KPGc+CjxyZWN0IHg9IjExMCIgeT0iMTIwIiB3aWR0aD0iMjkwIiBoZWlnaHQ9IjExMiIgcng9IjgiIGZpbGw9IiNmZmY0ZTUiIHN0cm9rZT0iI2UwOGEwMCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9IjI1NSIgeT0iMTQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+RXZlbnR1YWxseSBjb25zaXN0ZW50IHN0b3JlczwvdGV4dD4KPHRleHQgeD0iMjU1IiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPkNhc3NhbmRyYSA1LngsIER5bmFtb0RCLCBSZWRpcyAvIFZhbGtleTwvdGV4dD4KPHRleHQgeD0iMjU1IiB5PSIxOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlR1bmFibGUgcXVvcnVtLCBwYXJ0aXRpb24ta2V5ZWQsIGxhc3Qtd3JpdGUtd2luczwvdGV4dD4KPHRleHQgeD0iMjU1IiB5PSIyMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlNlc3Npb25zLCBmZWVkcywgY2FydHMsIHByb2ZpbGVzPC90ZXh0Pgo8L2c+CjwhLS0gQ29sdW1uYXIgT0xBUCAtLT4KPGc+CjxyZWN0IHg9IjYyMCIgeT0iMTAwIiB3aWR0aD0iMjkwIiBoZWlnaHQ9IjExMiIgcng9IjgiIGZpbGw9IiNlNmY3ZWUiIHN0cm9rZT0iIzBmOGE0YSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9Ijc2NSIgeT0iMTI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+Q29sdW1uYXIgT0xBUDwvdGV4dD4KPHRleHQgeD0iNzY1IiB5PSIxNTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPkNsaWNrSG91c2UgMjYueCBMVFMsIEJpZ1F1ZXJ5LCBTbm93Zmxha2U8L3RleHQ+Cjx0ZXh0IHg9Ijc2NSIgeT0iMTcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5Db2x1bW4gZ3JhbnVsZXMsIHZlY3RvcmlzZWQgZXhlY3V0aW9uPC90ZXh0Pgo8dGV4dCB4PSI3NjUiIHk9IjE5MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+RGFzaGJvYXJkcywgZnVubmVscywgb2JzZXJ2YWJpbGl0eTwvdGV4dD4KPC9nPgo8IS0tIFZlY3RvciAvIFJBRyAtLT4KPGc+CjxyZWN0IHg9IjM2NSIgeT0iMjU1IiB3aWR0aD0iMjkwIiBoZWlnaHQ9IjExMiIgcng9IjgiIGZpbGw9IiNmM2U4ZmYiIHN0cm9rZT0iIzdiM2ZlNCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9IjUxMCIgeT0iMjgzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+VmVjdG9yIC8gUkFHPC90ZXh0Pgo8dGV4dCB4PSI1MTAiIHk9IjMwNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+TWlsdnVzIDIuNiwgcGd2ZWN0b3IgMC44LCBPcGVuU2VhcmNoPC90ZXh0Pgo8dGV4dCB4PSI1MTAiIHk9IjMyNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+SE5TVyAvIElWRiBhcHByb3hpbWF0ZSBzZWFyY2gsIHJlY2FsbCB2cyBsYXRlbmN5PC90ZXh0Pgo8dGV4dCB4PSI1MTAiIHk9IjM0NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+U2VtYW50aWMgc2VhcmNoLCByZXRyaWV2YWwgZm9yIExMTXM8L3RleHQ+CjwvZz4KPCEtLSBPTFRQIC0tPgo8Zz4KPHJlY3QgeD0iMTEwIiB5PSIzODUiIHdpZHRoPSIyOTAiIGhlaWdodD0iMTEyIiByeD0iOCIgZmlsbD0iI2U4ZjBmZSIgc3Ryb2tlPSIjMGY2MmZlIiBzdHJva2Utd2lkdGg9IjIiLz4KPHRleHQgeD0iMjU1IiB5PSI0MTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTUiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMGExNDI0Ij5PTFRQIChyb3cgc3RvcmUpPC90ZXh0Pgo8dGV4dCB4PSIyNTUiIHk9IjQzNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+UG9zdGdyZVNRTCAxNissIE15U1FMIDguNCssIFNRTCBTZXJ2ZXI8L3RleHQ+Cjx0ZXh0IHg9IjI1NSIgeT0iNDU3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5XQUwsIE1WQ0MsIHJvdyBsb2Nrcywgc2VyaWFsaXNhYmxlIHRyYW5zYWN0aW9uczwvdGV4dD4KPHRleHQgeD0iMjU1IiB5PSI0NzciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlNvdXJjZSBvZiB0cnV0aDogb3JkZXJzLCBiYWxhbmNlcywgaW52ZW50b3J5PC90ZXh0Pgo8L2c+CjwhLS0gQ2xvdWQtbmF0aXZlIGRpc3RyaWJ1dGVkIFNRTCAtLT4KPGc+CjxyZWN0IHg9IjYyMCIgeT0iMzg1IiB3aWR0aD0iMjkwIiBoZWlnaHQ9IjExMiIgcng9IjgiIGZpbGw9IiNmNGY1ZjciIHN0cm9rZT0iIzZiNzI4MCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9Ijc2NSIgeT0iNDEzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+Q2xvdWQtbmF0aXZlIGRpc3RyaWJ1dGVkIFNRTDwvdGV4dD4KPHRleHQgeD0iNzY1IiB5PSI0MzciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlNwYW5uZXIsIEF1cm9yYSwgQWxsb3lEQiwgQ29ja3JvYWNoREI8L3RleHQ+Cjx0ZXh0IHg9Ijc2NSIgeT0iNDU3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5EaXNhZ2dyZWdhdGVkIHN0b3JhZ2UsIGNvbnNlbnN1cyB3cml0ZXM8L3RleHQ+Cjx0ZXh0IHg9Ijc2NSIgeT0iNDc3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5HbG9iYWwgT0xUUCB3aXRoIGVsYXN0aWMgY2FwYWNpdHk8L3RleHQ+CjwvZz4KPC9zdmc+Cg==" alt="Polyglot persistence landscape mapping OLTP row stores, eventually consistent key-value stores, columnar OLAP, vector databases and cloud-native distributed SQL against read shape and write contract" width="960" height="580"><br><em>Figure 1. The polyglot persistence landscape: five workload classes, five storage contracts.</em></p>
<h2>The OLTP engine is keeping a promise, not hitting a wall<a class="anchor-link" id="the-oltp-engine-is-keeping-a-promise-not-hitting-a-wall"></a></h2>
<p>A transaction-processing database sells one thing: ACID. Atomicity, consistency, isolation and durability are not marketing features; they are mechanisms with costs that show up in specific catalog views. Durability is a synchronous write to the write-ahead log before a commit is acknowledged. Isolation is a row-level lock, or in MVCC engines a tuple version chain plus a lock on the row being updated. Atomicity is the ability to roll every one of those back. In <a href="https://www.postgresql.org/docs/16/mvcc.html" target="_blank" rel="noopener">PostgreSQL&rsquo;s MVCC implementation</a>, two sessions that update the same row serialise on that row by design, because that is the only way to keep the second update from clobbering the first.</p>
<p>The cleanest way to see the cost is to measure it. The runs below are on a two-vCPU sandbox with PostgreSQL 16.13, <code>synchronous_commit = off</code> to remove disk latency from the picture, and pgbench driving a single-statement transaction for ten seconds. This is a demonstration of a mechanism, not a benchmark; the absolute numbers are meaningless outside this box, and the shape of the curve is the point.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Two workloads: one hot row versus writes spread over 1,000 rows">CREATE TABLE inventory (
    sku_id   INT PRIMARY KEY,
    on_hand  INT NOT NULL
);
INSERT INTO inventory
SELECT g, 1000000000
FROM generate_series(1, 1000) g;

-- hot.sql: every client decrements the same SKU
UPDATE inventory SET on_hand = on_hand - 1 WHERE sku_id = 1;

-- spread.sql: clients decrement a random SKU
set sku random(1, 1000)
UPDATE inventory SET on_hand = on_hand - 1 WHERE sku_id = :sku;</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-title="pgbench, 10-second runs, output trimmed to tps and latency">$ for c in 1 8 32 64; do pgbench -n -f hot.sql -c $c -j 2 -T 10; done
clients=1    latency average = 0.065 ms   tps = 15298
clients=8    latency average = 0.260 ms   tps = 30790
clients=32   latency average = 2.020 ms   tps = 15838
clients=64   latency average = 5.871 ms   tps = 10900

$ for c in 1 8 32 64; do pgbench -n -f spread.sql -c $c -j 2 -T 10; done
clients=1    latency average = 0.066 ms   tps = 15238
clients=8    latency average = 0.190 ms   tps = 42089
clients=32   latency average = 0.932 ms   tps = 34339
clients=64   latency average = 2.080 ms   tps = 30774</pre>
<p>Both workloads saturate two cores by eight clients, so the interesting comparison is what happens after that. The spread workload holds three quarters of its peak at sixty-four clients. The hot-row workload loses two thirds of its peak and its latency grows ninety-fold. A snapshot of <code>pg_stat_activity</code> during the sixty-four-client hot-row run shows where the time went.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Where 64 sessions are waiting, mid-run">SELECT wait_event_type,
       wait_event,
       COUNT(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND query LIKE 'UPDATE inventory%'
GROUP BY 1, 2
ORDER BY 3 DESC;

 wait_event_type |  wait_event   | count
-----------------+---------------+-------
 Lock            | tuple         |    38
 LWLock          | BufferContent |     7
 LWLock          | LockManager   |     6
 Lock            | transactionid |     6
                 |               |     5
 Client          | ClientRead    |     2</pre>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5NjAiIGhlaWdodD0iMzMwIiB2aWV3Qm94PSIwIDAgOTYwIDMzMCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KPHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSIzMzAiIGZpbGw9IiNmZmZmZmYiLz4KPHRleHQgeD0iNDgwIiB5PSIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPldoeSB0aGUgaG90IHJvdyBzZXJpYWxpc2VzOiB3aGF0IEFDSUQgY29zdHMgdW5kZXIgNjQgY29uY3VycmVudCB3cml0ZXJzPC90ZXh0Pgo8dGV4dCB4PSI0ODAiIHk9IjU0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNmI3MjgwIj5wZ19zdGF0X2FjdGl2aXR5IHNuYXBzaG90IHRha2VuIG1pZC1ydW4gaW4gdGhlIGxhYiBhYm92ZSAoUG9zdGdyZVNRTCAxNi4xMywgNjQgY2xpZW50cywgb25lIHNrdV9pZCk8L3RleHQ+CjxnIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMwYTE0MjQiPgo8dGV4dCB4PSI2MCIgeT0iMTAwIj5Mb2NrOiB0dXBsZTwvdGV4dD48cmVjdCB4PSIyMzAiIHk9Ijg2IiB3aWR0aD0iNTcwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjMGY2MmZlIi8+PHRleHQgeD0iODEwIiB5PSIxMDEiIGZvbnQtc2l6ZT0iMTIiPjM4IGJhY2tlbmRzPC90ZXh0Pgo8dGV4dCB4PSI2MCIgeT0iMTM1Ij5MV0xvY2s6IEJ1ZmZlckNvbnRlbnQ8L3RleHQ+PHJlY3QgeD0iMjMwIiB5PSIxMjEiIHdpZHRoPSIxMDUiIGhlaWdodD0iMjAiIGZpbGw9IiMwZjYyZmUiIG9wYWNpdHk9IjAuNyIvPjx0ZXh0IHg9IjM0NSIgeT0iMTM2IiBmb250LXNpemU9IjEyIj43PC90ZXh0Pgo8dGV4dCB4PSI2MCIgeT0iMTcwIj5MV0xvY2s6IExvY2tNYW5hZ2VyPC90ZXh0PjxyZWN0IHg9IjIzMCIgeT0iMTU2IiB3aWR0aD0iOTAiIGhlaWdodD0iMjAiIGZpbGw9IiMwZjYyZmUiIG9wYWNpdHk9IjAuNyIvPjx0ZXh0IHg9IjMzMCIgeT0iMTcxIiBmb250LXNpemU9IjEyIj42PC90ZXh0Pgo8dGV4dCB4PSI2MCIgeT0iMjA1Ij5Mb2NrOiB0cmFuc2FjdGlvbmlkPC90ZXh0PjxyZWN0IHg9IjIzMCIgeT0iMTkxIiB3aWR0aD0iOTAiIGhlaWdodD0iMjAiIGZpbGw9IiMwZjYyZmUiIG9wYWNpdHk9IjAuNyIvPjx0ZXh0IHg9IjMzMCIgeT0iMjA2IiBmb250LXNpemU9IjEyIj42PC90ZXh0Pgo8dGV4dCB4PSI2MCIgeT0iMjQwIj5SdW5uaW5nIChubyB3YWl0KTwvdGV4dD48cmVjdCB4PSIyMzAiIHk9IjIyNiIgd2lkdGg9Ijc1IiBoZWlnaHQ9IjIwIiBmaWxsPSIjMGY4YTRhIi8+PHRleHQgeD0iMzE1IiB5PSIyNDEiIGZvbnQtc2l6ZT0iMTIiPjU8L3RleHQ+Cjx0ZXh0IHg9IjYwIiB5PSIyNzUiPkNsaWVudDogQ2xpZW50UmVhZDwvdGV4dD48cmVjdCB4PSIyMzAiIHk9IjI2MSIgd2lkdGg9IjMwIiBoZWlnaHQ9IjIwIiBmaWxsPSIjOWNhM2FmIi8+PHRleHQgeD0iMjcwIiB5PSIyNzYiIGZvbnQtc2l6ZT0iMTIiPjI8L3RleHQ+CjwvZz4KPHRleHQgeD0iNDgwIiB5PSIzMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPkZpZnR5IG9mIHNpeHR5LWZvdXIgc2Vzc2lvbnMgYXJlIHF1ZXVlZCBiZWhpbmQgdGhlIHJvdyBsb2NrLiBUaGUgZW5naW5lIGlzIGVuZm9yY2luZyBleGFjdGx5IHRoZSBpc29sYXRpb24gaXQgcHJvbWlzZWQ7IGNvbmN1cnJlbmN5IGNhbm5vdCBmaXggYSBzZXJpYWwgY29udHJhY3QuPC90ZXh0Pgo8L3N2Zz4K" alt="Bar chart of PostgreSQL wait events under 64 concurrent writers on one row, dominated by Lock:tuple" width="960" height="330"><br><em>Figure 2. Wait events under hot-row contention, the first measurement behind polyglot persistence.</em></p>
<p>Fifty of sixty-four sessions are queued on the tuple lock or the transaction ID of the session holding it. Nothing here is a bug, a missing index or a tuning gap. The engine is serialising updates to one row because you asked it to guarantee that no decrement is lost. Sharding does not change the arithmetic for a single hot key; it only spreads the keys that are not hot. This is the first fundamental limit that forces polyglot persistence: an ACID row store&rsquo;s throughput on a contended key is bounded by the serial critical section, and that is exactly what a consumer flash sale, a viral post&rsquo;s like counter or a global leaderboard produces.</p>
<h2>What the consumer internet changed<a class="anchor-link" id="what-the-consumer-internet-changed"></a></h2>
<p>Enterprise applications of the previous era had a bounded number of users, a business-hours load curve and a tolerance for a few hundred milliseconds. Consumer applications have none of those properties. The traffic is spiky and global, the read-to-write ratio is often a thousand to one, and the product team measures latency at the 99th percentile in a region the database was never deployed in.</p>
<p>Amazon documented the consequences in the <a href="https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf" target="_blank" rel="noopener">Dynamo paper</a>. When a network partition happens, and it will, a system can keep accepting writes or it can keep every replica in agreement, but not both. A shopping cart that refuses to accept an item because a replica is unreachable costs more than a cart that briefly shows a stale item. Cassandra, DynamoDB, Riak and the key-value tier of most large platforms descend from that decision, and polyglot persistence at internet scale starts with it. The write contract becomes &ldquo;this will converge&rdquo;, enforced by quorum arithmetic and a conflict rule, rather than &ldquo;this is true now&rdquo;, enforced by a lock.</p>
<p>The trade is explicit and tunable. In Cassandra, a read at <code>LOCAL_QUORUM</code> against a replication factor of three touches two of three replicas in the local datacenter and returns the newest timestamped value it sees; the <a href="https://cassandra.apache.org/doc/latest/cassandra/architecture/dynamo.html" target="_blank" rel="noopener">consistency documentation</a> spells out the read-plus-write-greater-than-replication-factor rule that makes that read see the latest acknowledged write. What you give up is any notion of a multi-row transaction, a join or an ad hoc query, which is why the data model is designed backwards from the queries.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Query-first modelling in CQL: the table is the query (Cassandra 5.x)">CREATE KEYSPACE consumer
WITH replication = {
    'class'      : 'NetworkTopologyStrategy',
    'us-east-1'  : 3,
    'ap-south-1' : 3
};

-- One partition per user, newest activity first, bounded by TTL.
CREATE TABLE consumer.activity_feed_by_user (
    user_id      UUID,
    event_ts     TIMEUUID,
    event_type   TEXT,
    payload      TEXT,
    PRIMARY KEY ((user_id), event_ts)
)
WITH CLUSTERING ORDER BY (event_ts DESC)
 AND default_time_to_live = 2592000
 AND compaction = {
    'class'                : 'TimeWindowCompactionStrategy',
    'compaction_window_unit': 'DAYS',
    'compaction_window_size': 1
 };

-- The application reads at LOCAL_QUORUM and writes at LOCAL_QUORUM:
-- 2 + 2 &gt; 3, so a read observes the latest acknowledged write in-region.
CONSISTENCY LOCAL_QUORUM;
SELECT event_ts, event_type, payload
FROM consumer.activity_feed_by_user
WHERE user_id = 7a3f1c2e-4d5b-4e6f-8a9b-0c1d2e3f4a5b
LIMIT 50;</pre>
<p>Notice what is missing. There is no foreign key to a users table, no join to the events catalog, no way to ask &ldquo;which users had the most events this week&rdquo; without a full cluster scan or a second table maintained by the application. That is the mirror image of the OLTP limit. An eventually consistent platform buys availability and horizontal write scale by refusing every feature that would require global coordination. Polyglot persistence is what happens when you stop pretending one of these contracts can substitute for the other.</p>
<h2>Analytics is columnar because the questions are columnar<a class="anchor-link" id="analytics-is-columnar-because-the-questions-are-columnar"></a></h2>
<p>An analytical question is a function of a few columns over a very large number of rows. A row store answers it by reading every byte of every row, because the tuple is the unit of storage. A column store keeps each column in its own file, so a scan opens only the columns the query names, and compresses each column with a codec suited to that column&rsquo;s distribution. This is the second fundamental limit behind polyglot persistence, and it is easy to measure.</p>
<p>The lab table is a 5-million-row, 24-column <code>orders</code> table with realistic width: two free-text addresses, a UUID session ID, a referrer URL, optional notes. It was generated in PostgreSQL 16.13, exported to CSV, and loaded unchanged into ClickHouse 26.7 through chdb, so both engines hold identical data. The question is the one every product dashboard asks first: revenue and order count by region and channel for the last ninety days.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="PostgreSQL 16.13: the same aggregate, warm cache, EXPLAIN (ANALYZE, BUFFERS)">EXPLAIN (ANALYZE, BUFFERS, COSTS OFF, TIMING OFF)
SELECT region,
       channel,
       COUNT(*)    AS orders,
       SUM(amount) AS revenue
FROM orders
WHERE order_ts &gt;= now() - interval '90 days'
GROUP BY region, channel
ORDER BY revenue DESC;

 Sort (actual rows=20 loops=1)
   Sort Key: (sum(amount)) DESC
   Buffers: shared hit=123593 read=105740
   I/O Timings: shared read=219.788
   -&gt;  Finalize GroupAggregate (actual rows=20 loops=1)
         -&gt;  Gather Merge (actual rows=60 loops=1)
               Workers Planned: 2
               Workers Launched: 2
               ...
               -&gt;  Partial HashAggregate (actual rows=20 loops=3)
                     -&gt;  Parallel Seq Scan on orders (actual rows=408674 loops=3)
                           Filter: (order_ts &gt;= (now() - '90 days'::interval))
                           Rows Removed by Filter: 1257993
                           Buffers: shared hit=123574 read=105740
 Execution Time: 674.381 ms

-- pg_class.relpages for orders: 229314  (229314 x 8 KB = 1.79 GB heap)</pre>
<p>The planner did nothing wrong. There is an index on <code>order_ts</code>, but a quarter of the table matches, so a parallel sequential scan is the right plan, and it is the right plan in every row store. The cost is structural: to sum one <code>NUMERIC</code> column across 1.2 million qualifying rows, the executor pulled all 229,314 heap pages through shared buffers, 105,740 of them from the operating system, because <code>ship_address</code>, <code>session_id</code> and <code>notes</code> live in the same 8 KB pages as <code>amount</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="ClickHouse 26.7 (chdb): identical data, explicit MergeTree declaration">CREATE TABLE lab.orders
(
    order_id        UInt64,
    customer_id     UInt64,
    order_ts        DateTime64(6, 'UTC'),
    region          LowCardinality(String),
    channel         LowCardinality(String),
    status          LowCardinality(String),
    currency        FixedString(3),
    amount          Decimal(12, 2),
    -- ... 16 further columns identical to the PostgreSQL table
    carrier         LowCardinality(String),
    updated_at      DateTime64(6, 'UTC')
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(order_ts)
ORDER BY (region, order_ts, order_id)
SETTINGS index_granularity = 8192;</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="ClickHouse 26.7: EXPLAIN indexes = 1 for the same aggregate">EXPLAIN indexes = 1
SELECT region,
       channel,
       count()     AS orders,
       sum(amount) AS revenue
FROM lab.orders
WHERE order_ts &gt;= now() - INTERVAL 90 DAY
GROUP BY region, channel
ORDER BY revenue DESC;

Aggregating
   Keys: region, channel
   Aggregates: count(), sum(amount)
   ReadFromMergeTree (lab.orders)
      Parts: 4 | Granules: 166
      Output: region, channel, amount
      Prewhere filter column: order_ts &gt;= '2026-06-07 04:05:17'
      Indexes:
         Partition   Parts: 4/4    Granules: 173/173
         Min-Max     Parts: 4/13   Granules: 173/652
         PrimaryKey  Parts: 4/4    Granules: 166/173

-- 20 rows returned in 0.032 s on the same 2-vCPU sandbox</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="system.parts_columns: why the column store reads so little">SELECT column,
       formatReadableSize(sum(column_data_compressed_bytes))   AS compressed,
       formatReadableSize(sum(column_data_uncompressed_bytes)) AS uncompressed
FROM system.parts_columns
WHERE database = 'lab' AND table = 'orders' AND active
  AND column IN ('region','channel','amount','order_ts','ship_address','session_id')
GROUP BY column
ORDER BY sum(column_data_compressed_bytes) DESC;

   &#9484;&#9472;column&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;compressed&#9472;&#9516;&#9472;uncompressed&#9472;&#9488;
1. &#9474; ship_address &#9474; 185.17 MiB &#9474; 290.35 MiB   &#9474;
2. &#9474; session_id   &#9474; 76.61 MiB  &#9474; 76.29 MiB    &#9474;
3. &#9474; amount       &#9474; 19.25 MiB  &#9474; 38.15 MiB    &#9474;
4. &#9474; channel      &#9474; 2.73 MiB   &#9474; 4.78 MiB     &#9474;
5. &#9474; order_ts     &#9474; 190.29 KiB &#9474; 38.15 MiB    &#9474;
6. &#9474; region       &#9474; 26.85 KiB  &#9474; 4.78 MiB     &#9474;
   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</pre>
<p><img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5NjAiIGhlaWdodD0iNDcwIiB2aWV3Qm94PSIwIDAgOTYwIDQ3MCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KPHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI0NzAiIGZpbGw9IiNmZmZmZmYiLz4KPHRleHQgeD0iNDgwIiB5PSIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIyMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPlNhbWUgNSBNLXJvdyB0YWJsZSwgc2FtZSA0LWNvbHVtbiBhZ2dyZWdhdGU6IHJvdyBzdG9yZSB2cyBjb2x1bW4gc3RvcmU8L3RleHQ+Cjx0ZXh0IHg9IjQ4MCIgeT0iNTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM2YjcyODAiPk1lYXN1cmVkIGluIHRoZSBsYWIgYWJvdmU6IFBvc3RncmVTUUwgMTYuMTMgaGVhcCBwYWdlcyB2cyBDbGlja0hvdXNlIDI2LjcgTWVyZ2VUcmVlIGdyYW51bGVzPC90ZXh0Pgo8IS0tIFJvdyBzdG9yZSAtLT4KPHRleHQgeD0iMjQwIiB5PSI5MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNSIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPlBvc3RncmVTUUwgaGVhcCAoOCBLQiBwYWdlcyk8L3RleHQ+CjxnIGlkPSJyb3dzIj4KPHJlY3QgeD0iNjAiIHk9IjEwNSIgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyNiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMGY2MmZlIi8+CjxyZWN0IHg9IjYwIiB5PSIxMzUiIHdpZHRoPSIzNjAiIGhlaWdodD0iMjYiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzBmNjJmZSIvPgo8cmVjdCB4PSI2MCIgeT0iMTY1IiB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI2IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMwZjYyZmUiLz4KPHJlY3QgeD0iNjAiIHk9IjE5NSIgd2lkdGg9IjM2MCIgaGVpZ2h0PSIyNiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMGY2MmZlIi8+CjxyZWN0IHg9IjYwIiB5PSIyMjUiIHdpZHRoPSIzNjAiIGhlaWdodD0iMjYiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzBmNjJmZSIvPgo8cmVjdCB4PSI2MCIgeT0iMjU1IiB3aWR0aD0iMzYwIiBoZWlnaHQ9IjI2IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMwZjYyZmUiLz4KPC9nPgo8IS0tIGhpZ2hsaWdodCBjb2x1bW5zIHdpdGhpbiBlYWNoIHJvdzogb3JkZXJfdHMsIHJlZ2lvbiwgY2hhbm5lbCwgYW1vdW50ID0gNCBzbWFsbCBjZWxscyBhbW9uZyAyNCAtLT4KPGcgZmlsbD0iIzBmNjJmZSIgb3BhY2l0eT0iMC44NSI+CjxyZWN0IHg9IjkyIiB5PSIxMDciIHdpZHRoPSIxNCIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjEwOCIgeT0iMTA3IiB3aWR0aD0iMTIiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxMjIiIHk9IjEwNyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTUwIiB5PSIxMDciIHdpZHRoPSIxNCIgaGVpZ2h0PSIyMiIvPgo8cmVjdCB4PSI5MiIgeT0iMTM3IiB3aWR0aD0iMTQiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxMDgiIHk9IjEzNyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTIyIiB5PSIxMzciIHdpZHRoPSIxMiIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjE1MCIgeT0iMTM3IiB3aWR0aD0iMTQiIGhlaWdodD0iMjIiLz4KPHJlY3QgeD0iOTIiIHk9IjE2NyIgd2lkdGg9IjE0IiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTA4IiB5PSIxNjciIHdpZHRoPSIxMiIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjEyMiIgeT0iMTY3IiB3aWR0aD0iMTIiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxNTAiIHk9IjE2NyIgd2lkdGg9IjE0IiBoZWlnaHQ9IjIyIi8+CjxyZWN0IHg9IjkyIiB5PSIxOTciIHdpZHRoPSIxNCIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjEwOCIgeT0iMTk3IiB3aWR0aD0iMTIiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxMjIiIHk9IjE5NyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTUwIiB5PSIxOTciIHdpZHRoPSIxNCIgaGVpZ2h0PSIyMiIvPgo8cmVjdCB4PSI5MiIgeT0iMjI3IiB3aWR0aD0iMTQiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxMDgiIHk9IjIyNyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTIyIiB5PSIyMjciIHdpZHRoPSIxMiIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjE1MCIgeT0iMjI3IiB3aWR0aD0iMTQiIGhlaWdodD0iMjIiLz4KPHJlY3QgeD0iOTIiIHk9IjI1NyIgd2lkdGg9IjE0IiBoZWlnaHQ9IjIyIi8+PHJlY3QgeD0iMTA4IiB5PSIyNTciIHdpZHRoPSIxMiIgaGVpZ2h0PSIyMiIvPjxyZWN0IHg9IjEyMiIgeT0iMjU3IiB3aWR0aD0iMTIiIGhlaWdodD0iMjIiLz48cmVjdCB4PSIxNTAiIHk9IjI1NyIgd2lkdGg9IjE0IiBoZWlnaHQ9IjIyIi8+CjwvZz4KPHRleHQgeD0iMjQwIiB5PSIzMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPkV2ZXJ5IHR1cGxlIGNhcnJpZXMgYWxsIDI0IGNvbHVtbnMgKGFkZHJlc3NlcywgVVVJRHMsIG5vdGVzKTwvdGV4dD4KPHRleHQgeD0iMjQwIiB5PSIzMTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlNjYW4gdG91Y2hlZCAyMjksMzE0IHBhZ2VzID0gMS43OSBHQiB0byB1c2UgNCBjb2x1bW5zPC90ZXh0Pgo8dGV4dCB4PSIyNDAiIHk9IjMzNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+QnVmZmVyczogc2hhcmVkIGhpdD0xMjMsNTkzIHJlYWQ9MTA1LDc0MCDCtyA2NzQgbXMgKDIgd29ya2Vycyk8L3RleHQ+CjwhLS0gQ29sdW1uIHN0b3JlIC0tPgo8dGV4dCB4PSI3MjAiIHk9IjkwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE1IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+Q2xpY2tIb3VzZSBNZXJnZVRyZWUgKGNvbHVtbiBmaWxlcywgODE5Mi1yb3cgZ3JhbnVsZXMpPC90ZXh0Pgo8Zz4KPHJlY3QgeD0iNTQwIiB5PSIxMDUiIHdpZHRoPSIzNCIgaGVpZ2h0PSIxNzYiIGZpbGw9IiMwZjhhNGEiIG9wYWNpdHk9IjAuOSIvPjx0ZXh0IHg9IjU1NyIgeT0iMjk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMGExNDI0Ij5vcmRlcl90czwvdGV4dD4KPHJlY3QgeD0iNTgwIiB5PSIxMDUiIHdpZHRoPSIzNCIgaGVpZ2h0PSIxNzYiIGZpbGw9IiMwZjhhNGEiIG9wYWNpdHk9IjAuOSIvPjx0ZXh0IHg9IjU5NyIgeT0iMjk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMGExNDI0Ij5yZWdpb248L3RleHQ+CjxyZWN0IHg9IjYyMCIgeT0iMTA1IiB3aWR0aD0iMzQiIGhlaWdodD0iMTc2IiBmaWxsPSIjMGY4YTRhIiBvcGFjaXR5PSIwLjkiLz48dGV4dCB4PSI2MzciIHk9IjI5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzBhMTQyNCI+Y2hhbm5lbDwvdGV4dD4KPHJlY3QgeD0iNjYwIiB5PSIxMDUiIHdpZHRoPSIzNCIgaGVpZ2h0PSIxNzYiIGZpbGw9IiMwZjhhNGEiIG9wYWNpdHk9IjAuOSIvPjx0ZXh0IHg9IjY3NyIgeT0iMjk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMGExNDI0Ij5hbW91bnQ8L3RleHQ+CjxyZWN0IHg9IjcxMCIgeT0iMTA1IiB3aWR0aD0iMzQiIGhlaWdodD0iMTc2IiBmaWxsPSIjZmZmIiBzdHJva2U9IiM5Y2EzYWYiLz48cmVjdCB4PSI3NTAiIHk9IjEwNSIgd2lkdGg9IjM0IiBoZWlnaHQ9IjE3NiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjOWNhM2FmIi8+PHJlY3QgeD0iNzkwIiB5PSIxMDUiIHdpZHRoPSIzNCIgaGVpZ2h0PSIxNzYiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzljYTNhZiIvPjxyZWN0IHg9IjgzMCIgeT0iMTA1IiB3aWR0aD0iMzQiIGhlaWdodD0iMTc2IiBmaWxsPSIjZmZmIiBzdHJva2U9IiM5Y2EzYWYiLz4KPHRleHQgeD0iNzg3IiB5PSIyOTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2YjcyODAiPjIwIG90aGVyIGNvbHVtbnMsIG5ldmVyIG9wZW5lZDwvdGV4dD4KPC9nPgo8dGV4dCB4PSI3MjAiIHk9IjMyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+UGFydGl0aW9uICsgbWluLW1heCArIHByaW1hcnkta2V5IHBydW5pbmc6IDE2NiBvZiA2NTIgZ3JhbnVsZXM8L3RleHQ+Cjx0ZXh0IHg9IjcyMCIgeT0iMzQwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5Gb3VyIGNvbHVtbnMgY29tcHJlc3MgdG8gMjIgTWlCIGluIHRvdGFsIChyZWdpb246IDI3IEtpQiwgb3JkZXJfdHM6IDE5MCBLaUIpPC90ZXh0Pgo8dGV4dCB4PSI3MjAiIHk9IjM1OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+U2FtZSBhbnN3ZXIgaW4gMzIgbXMgb24gdGhlIHNhbWUgMi12Q1BVIHNhbmRib3g8L3RleHQ+CjxyZWN0IHg9IjYwIiB5PSIzODUiIHdpZHRoPSI4NDAiIGhlaWdodD0iNjAiIHJ4PSI2IiBmaWxsPSIjZjRmNWY3IiBzdHJva2U9IiNkY2RlZTQiLz4KPHRleHQgeD0iNDgwIiB5PSI0MTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMwYTE0MjQiPk5laXRoZXIgZW5naW5lIGlzIHdyb25nLiBUaGUgcm93IHN0b3JlIGtlZXBzIGEgdHVwbGUgY29udGlndW91cyBzbyBhIHRyYW5zYWN0aW9uIGNhbiBsb2NrLCB1cGRhdGUgYW5kIGxvZyBpdCBhdG9taWNhbGx5LjwvdGV4dD4KPHRleHQgeD0iNDgwIiB5PSI0MzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMwYTE0MjQiPlRoZSBjb2x1bW4gc3RvcmUga2VlcHMgYSBjb2x1bW4gY29udGlndW91cyBzbyBhIHNjYW4gcmVhZHMgb25seSB3aGF0IHRoZSBhZ2dyZWdhdGUgbmVlZHMuIFRoYXQgaXMgdGhlIHdob2xlIGRpZmZlcmVuY2UuPC90ZXh0Pgo8L3N2Zz4K" alt="Row store versus column store I/O for the same 5 million row aggregate: PostgreSQL scans 229,314 heap pages while ClickHouse MergeTree opens four column files and 166 of 652 granules" width="960" height="470"><br><em>Figure 3. Row store versus column store for one aggregate, the second measurement behind polyglot persistence.</em></p>
<p>The four columns the query needs compress to about 22 MiB for the entire table, and the partition, min-max and primary-key indexes cut that to 166 of 652 granules before a byte of <code>amount</code> is decoded. The twenty columns the query does not need, including the 185 MiB of addresses, are never opened. Thirty milliseconds versus seven hundred is not ClickHouse being clever; it is the <a href="https://clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree" target="_blank" rel="noopener">MergeTree storage layout</a> being shaped like the question. The same layout is why ClickHouse is a poor system of record: a single-row <code>UPDATE</code> is a mutation that rewrites parts, and there is no row lock to serialise two of them.</p>
<h2>Cloud-native data platforms changed the unit of scale<a class="anchor-link" id="cloud-native-data-platforms-changed-the-unit-of-scale"></a></h2>
<p>The third shift in polyglot persistence is not a data model but an operating model. Aurora, AlloyDB, Spanner, Snowflake, BigQuery and ClickHouse Cloud separate compute from storage, put the storage on a replicated log or an object store, and let capacity change in minutes. For a founder the argument is time to market: a team of four can stand up a multi-AZ PostgreSQL-compatible cluster with automated failover before lunch. For a CFO the argument is that capacity becomes an operating expense that tracks demand.</p>
<p>The trade-offs are just as concrete, and a vendor-neutral practice has to name them. Managed services diverge from the open-source engine they are compatible with: extension allow-lists, superuser removal, version lag behind community releases, and storage layers whose performance characteristics (Aurora&rsquo;s quorum writes, Spanner&rsquo;s TrueTime commit wait) differ from the engine&rsquo;s documentation.</p>
<p>Egress and cross-region replication are priced per byte, which matters precisely when the polyglot persistence topology is moving change streams between stores. And the exit cost is asymmetric: getting a terabyte in is a weekend, getting it out with zero downtime is a project. None of this argues against cloud platforms. It argues for choosing them per workload, which is polyglot persistence applied to the operating model, with the same rigour as the engine itself.</p>
<h2>Vector platforms and RAG are a different kind of object<a class="anchor-link" id="vector-platforms-and-rag-are-a-different-kind-of-object"></a></h2>
<p>A relational index answers &ldquo;is this key present&rdquo; exactly. A B-tree, a hash, a bitmap: the answer is deterministic and complete. A vector index answers &ldquo;which stored vectors are nearest to this one&rdquo; approximately, because exact nearest-neighbour search in a thousand dimensions is a full scan. HNSW and IVF indexes trade recall for latency with explicit knobs, and a retrieval-augmented generation pipeline lives or dies on that trade plus the metadata filters applied before or after the approximate search.</p>
<p>That makes vector search a distinct workload class in any polyglot persistence design rather than a feature bolted onto an existing engine, even when it ships inside one. <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener">pgvector</a> 0.8 on PostgreSQL 16+ is an excellent choice while the corpus fits in memory, the filter predicates are selective and the embedding refresh rate is modest, because it keeps the vectors transactionally next to the rows they describe. A dedicated platform such as Milvus 2.6 earns its place when index build and query serving need to scale independently, when collections reach hundreds of millions of vectors, or when the workload needs GPU indexes, tiered storage and multi-tenant isolation that a general-purpose engine will not prioritise.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="pgvector 0.8 on PostgreSQL 16+: HNSW with a partial index for the hot tenant">CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE support_chunks (
    chunk_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id   INT          NOT NULL,
    ticket_id   BIGINT       NOT NULL,
    chunk_text  TEXT         NOT NULL,
    embedding   VECTOR(1024) NOT NULL,
    updated_at  TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Recall/latency trade-off is explicit: m and ef_construction at build time,
-- hnsw.ef_search at query time. Test on your corpus before fixing these.
CREATE INDEX idx_support_chunks_embedding_hnsw
    ON support_chunks
 USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 128);

SET hnsw.ef_search = 80;
SELECT chunk_id,
       ticket_id,
       1 - (embedding  $1::vector) AS cosine_similarity
FROM support_chunks
WHERE tenant_id = $2
ORDER BY embedding  $1::vector
LIMIT 8;</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-title="RAG retrieval loop: the vector store is a derived index, never the source of truth">import psycopg
from openai import OpenAI   # any embedding provider; keep it in-VPC for regulated data

EMBED_MODEL = "text-embedding-3-large"
client = OpenAI()

def embed(text: str) -&gt; list[float]:
    return client.embeddings.create(model=EMBED_MODEL, input=text).data[0].embedding

def retrieve(conn: psycopg.Connection, tenant_id: int, question: str, k: int = 8):
    qvec = embed(question)
    with conn.cursor() as cur:
        cur.execute("SET LOCAL hnsw.ef_search = 80")
        cur.execute(
            """
            SELECT chunk_id, ticket_id, chunk_text,
                   1 - (embedding  %s::vector) AS score
            FROM support_chunks
            WHERE tenant_id = %s
            ORDER BY embedding  %s::vector
            LIMIT %s
            """,
            (qvec, tenant_id, qvec, k),
        )
        return cur.fetchall()

# The ticket text itself is owned by the OLTP schema; this table is rebuilt
# from it when the embedding model changes. Treat it like a materialised view.</pre>
<p>Two operational facts follow for any polyglot persistence estate. First, an embedding model change invalidates every vector, so the vector store must be rebuildable from the system of record; it is a derived index, not a database of record. Second, recall is a measured quantity. A RAG pipeline that has never had its retrieval recall measured against a labelled set is not in production, whatever the dashboard says.</p>
<h2>The polyglot persistence decision frame<a class="anchor-link" id="the-polyglot-persistence-decision-frame"></a></h2>
<p>Executives do not need to memorise storage internals. They need a frame that turns &ldquo;which database&rdquo; into a question about the workload, so that the answer can be checked against measurement. These are the dimensions that decide it in practice.</p>
<table>
<caption>Polyglot persistence decision matrix by workload profile</caption>
<thead>
<tr>
<th>Workload dimension</th>
<th>ACID row store</th>
<th>Eventually consistent KV / wide-column</th>
<th>Columnar OLAP</th>
<th>Vector platform</th>
</tr>
</thead>
<tbody>
<tr>
<td>Correctness contract</td>
<td>Exact, now, multi-row</td>
<td>Converges; single-partition atomicity</td>
<td>Exact over a snapshot; eventual after merge</td>
<td>Approximate by design (recall &lt; 100%)</td>
</tr>
<tr>
<td>Hot-key write scaling</td>
<td>Serial on the key (measured above)</td>
<td>Linear across partitions; hot partition still hurts</td>
<td>Append-only batches; updates are mutations</td>
<td>Batch upserts; index rebuild cost dominates</td>
</tr>
<tr>
<td>Wide scan / aggregate</td>
<td>Reads whole tuples (1.79 GB above)</td>
<td>Unsupported without a second table</td>
<td>Reads named columns only (22 MiB above)</td>
<td>Not a query shape it serves</td>
</tr>
<tr>
<td>Global low-latency reads</td>
<td>Read replicas with lag; or distributed SQL</td>
<td>Native multi-DC with LOCAL_QUORUM</td>
<td>Replicated per region for dashboards</td>
<td>Replicated collections; rebuild per region</td>
</tr>
<tr>
<td>Ad hoc queries and joins</td>
<td>Full SQL, cost-based optimiser</td>
<td>Query-first schema; no joins</td>
<td>Full analytical SQL; joins need care</td>
<td>Similarity plus metadata filters</td>
</tr>
<tr>
<td>Typical system-of-record role</td>
<td>Yes</td>
<td>Only for data that is naturally per-key</td>
<td>No; derived copy</td>
<td>No; rebuildable index</td>
</tr>
</tbody>
</table>
<p>Read the rows, not the columns. If a workload needs two cells that live in different columns, that is a second engine, and the honest polyglot persistence question is how the data gets from the first to the second and how stale it is allowed to be on arrival.</p>
<h2>What a polyglot persistence topology looks like when it works<a class="anchor-link" id="what-a-polyglot-persistence-topology-looks-like-when-it-works"></a></h2>
<p><img decoding="async" loading="lazy" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5NjAiIGhlaWdodD0iNTYwIiB2aWV3Qm94PSIwIDAgOTYwIDU2MCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KPHJlY3Qgd2lkdGg9Ijk2MCIgaGVpZ2h0PSI1NjAiIGZpbGw9IiNmZmZmZmYiLz4KPGRlZnM+PG1hcmtlciBpZD0iYSIgbWFya2VyV2lkdGg9IjEwIiBtYXJrZXJIZWlnaHQ9IjgiIHJlZlg9IjkiIHJlZlk9IjQiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAsMCBMMTAsNCBMMCw4IHoiIGZpbGw9IiMzZDNkNDQiLz48L21hcmtlcj48L2RlZnM+Cjx0ZXh0IHg9IjQ4MCIgeT0iMzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMGExNDI0Ij5SZWZlcmVuY2UgcG9seWdsb3QgcGVyc2lzdGVuY2UgdG9wb2xvZ3kgZm9yIGEgY29uc3VtZXItZmFjaW5nIHBsYXRmb3JtPC90ZXh0Pgo8dGV4dCB4PSI0ODAiIHk9IjU0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNmI3MjgwIj5UaGUgc3lzdGVtIG9mIHJlY29yZCBzdGF5cyBBQ0lELiBFdmVyeSBvdGhlciBzdG9yZSBpcyBhIGRlcml2ZWQsIHB1cnBvc2Utc2hhcGVkIGNvcHkgZmVkIGJ5IGNoYW5nZSBkYXRhIGNhcHR1cmUuPC90ZXh0Pgo8IS0tIGFwcCB0aWVyIC0tPgo8cmVjdCB4PSIzNjAiIHk9IjgwIiB3aWR0aD0iMjQwIiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0iIzBhMWEyZiIvPgo8dGV4dCB4PSI0ODAiIHk9IjExMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmYiPk1vYmlsZSAvIHdlYiBBUEkgdGllciAoc3RhdGVsZXNzKTwvdGV4dD4KPCEtLSBPTFRQIC0tPgo8cmVjdCB4PSIzNDUiIHk9IjE3NSIgd2lkdGg9IjI3MCIgaGVpZ2h0PSI4MCIgcng9IjgiIGZpbGw9IiNlOGYwZmUiIHN0cm9rZT0iIzBmNjJmZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9IjQ4MCIgeT0iMjAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+U3lzdGVtIG9mIHJlY29yZDogUG9zdGdyZVNRTCAxNis8L3RleHQ+Cjx0ZXh0IHg9IjQ4MCIgeT0iMjIzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5QYXRyb25pIEhBLCBQZ0JvdW5jZXIsIHN5bmNocm9ub3VzIHJlcGxpY2E8L3RleHQ+Cjx0ZXh0IHg9IjQ4MCIgeT0iMjQxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5vcmRlcnMsIHBheW1lbnRzLCBpbnZlbnRvcnksIGxlZGdlcnM8L3RleHQ+CjwhLS0gY2FjaGUgLyBLViAtLT4KPHJlY3QgeD0iMjUiIHk9IjE3NSIgd2lkdGg9IjI2MCIgaGVpZ2h0PSI4MCIgcng9IjgiIGZpbGw9IiNmZmY0ZTUiIHN0cm9rZT0iI2UwOGEwMCIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjx0ZXh0IHg9IjE1NSIgeT0iMjAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBhMTQyNCI+SG90IHBhdGg6IFZhbGtleSA4IC8gQ2Fzc2FuZHJhIDU8L3RleHQ+Cjx0ZXh0IHg9IjE1NSIgeT0iMjIzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5zZXNzaW9ucywgY2FydHMsIGZlZWRzLCBwcm9maWxlczwvdGV4dD4KPHRleHQgeD0iMTU1IiB5PSIyNDEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPkxPQ0FMX1FVT1JVTSwgVFRMLWJvdW5kZWQgc3RhbGVuZXNzPC90ZXh0Pgo8IS0tIENEQyBidXMgLS0+CjxyZWN0IHg9IjMwMCIgeT0iMzE1IiB3aWR0aD0iMzYwIiBoZWlnaHQ9IjUwIiByeD0iOCIgZmlsbD0iI2Y0ZjVmNyIgc3Ryb2tlPSIjNmI3MjgwIiBzdHJva2Utd2lkdGg9IjIiLz4KPHRleHQgeD0iNDgwIiB5PSIzMzciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMGExNDI0Ij5EZWJleml1bSAocGdvdXRwdXQpIOKGkiBLYWZrYSA0Lng8L3RleHQ+Cjx0ZXh0IHg9IjQ4MCIgeT0iMzU1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5vbmUgdG9waWMgcGVyIHRhYmxlLCBrZXllZCBieSBwcmltYXJ5IGtleSwgc2NoZW1hIHJlZ2lzdHJ5PC90ZXh0Pgo8IS0tIE9MQVAgLS0+CjxyZWN0IHg9IjMwIiB5PSI0MzAiIHdpZHRoPSIyODAiIGhlaWdodD0iOTAiIHJ4PSI4IiBmaWxsPSIjZTZmN2VlIiBzdHJva2U9IiMwZjhhNGEiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSIxNzAiIHk9IjQ1OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPkFuYWx5dGljczogQ2xpY2tIb3VzZSAyNi54IExUUzwvdGV4dD4KPHRleHQgeD0iMTcwIiB5PSI0NzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPlJlcGxhY2luZ01lcmdlVHJlZSBwZXIgQ0RDIHRvcGljPC90ZXh0Pgo8dGV4dCB4PSIxNzAiIHk9IjQ5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+bWF0ZXJpYWxpemVkIHZpZXdzLCBkYXNoYm9hcmRzLCBwcm9kdWN0IGFuYWx5dGljczwvdGV4dD4KPCEtLSBWZWN0b3IgLS0+CjxyZWN0IHg9IjM0MCIgeT0iNDMwIiB3aWR0aD0iMjgwIiBoZWlnaHQ9IjkwIiByeD0iOCIgZmlsbD0iI2YzZThmZiIgc3Ryb2tlPSIjN2IzZmU0IiBzdHJva2Utd2lkdGg9IjIiLz4KPHRleHQgeD0iNDgwIiB5PSI0NTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMGExNDI0Ij5SZXRyaWV2YWw6IE1pbHZ1cyAyLjYgb3IgcGd2ZWN0b3IgMC44PC90ZXh0Pgo8dGV4dCB4PSI0ODAiIHk9IjQ3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+ZW1iZWRkaW5ncyByZWJ1aWx0IGZyb20gcHJvZHVjdCAvIHRpY2tldCByb3dzPC90ZXh0Pgo8dGV4dCB4PSI0ODAiIHk9IjQ5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+SE5TVyBpbmRleCwgbWV0YWRhdGEgZmlsdGVycywgUkFHIGZvciBzdXBwb3J0PC90ZXh0Pgo8IS0tIExha2Vob3VzZSAtLT4KPHJlY3QgeD0iNjUwIiB5PSI0MzAiIHdpZHRoPSIyODAiIGhlaWdodD0iOTAiIHJ4PSI4IiBmaWxsPSIjZjRmNWY3IiBzdHJva2U9IiM2YjcyODAiIHN0cm9rZS13aWR0aD0iMiIvPgo8dGV4dCB4PSI3OTAiIHk9IjQ1OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwYTE0MjQiPkxha2Vob3VzZTogSWNlYmVyZyBvbiBTMzwvdGV4dD4KPHRleHQgeD0iNzkwIiB5PSI0NzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPmxvbmcgcmV0ZW50aW9uLCBNTCBmZWF0dXJlcywgZmluYW5jZSBjbG9zZTwvdGV4dD4KPHRleHQgeD0iNzkwIiB5PSI0OTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzZDNkNDQiPnF1ZXJpZWQgYnkgVHJpbm8gLyBTbm93Zmxha2UgLyBDbGlja0hvdXNlPC90ZXh0Pgo8IS0tIGFycm93cyAtLT4KPGcgc3Ryb2tlPSIjM2QzZDQ0IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiIG1hcmtlci1lbmQ9InVybCgjYSkiPgo8bGluZSB4MT0iNDgwIiB5MT0iMTMwIiB4Mj0iNDgwIiB5Mj0iMTczIi8+CjxsaW5lIHgxPSIzNjAiIHkxPSIxMDUiIHgyPSIxNTUiIHkyPSIxNzMiLz4KPGxpbmUgeDE9IjQ4MCIgeTE9IjI1NSIgeDI9IjQ4MCIgeTI9IjMxMyIvPgo8bGluZSB4MT0iNDAwIiB5MT0iMzY1IiB4Mj0iMjAwIiB5Mj0iNDI4Ii8+CjxsaW5lIHgxPSI0ODAiIHkxPSIzNjUiIHgyPSI0ODAiIHkyPSI0MjgiLz4KPGxpbmUgeDE9IjU2MCIgeTE9IjM2NSIgeDI9Ijc2MCIgeTI9IjQyOCIvPgo8bGluZSB4MT0iNjE1IiB5MT0iMjE1IiB4Mj0iNjg4IiB5Mj0iMjE1Ii8+CjwvZz4KPHJlY3QgeD0iNjkwIiB5PSIxODAiIHdpZHRoPSIyNTUiIGhlaWdodD0iNzAiIHJ4PSI4IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMwZjYyZmUiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNCIvPgo8dGV4dCB4PSI4MTciIHk9IjIwNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzBhMTQyNCI+RXNjYXBlIGhhdGNoIGZvciBnbG9iYWwgT0xUUDo8L3RleHQ+Cjx0ZXh0IHg9IjgxNyIgeT0iMjIzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjM2QzZDQ0Ij5TcGFubmVyIC8gQ29ja3JvYWNoREIgLyBBdXJvcmEgR2xvYmFsPC90ZXh0Pgo8dGV4dCB4PSI4MTciIHk9IjI0MSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzNkM2Q0NCI+b25seSB3aGVuIG9uZSByZWdpb24gY2Fubm90IGhvbGQgdGhlIGxlZGdlcjwvdGV4dD4KPHRleHQgeD0iMjAwIiB5PSIzMDAiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiM2YjcyODAiPndyaXRlLXRocm91Z2ggLyByZWFkLWFzaWRlPC90ZXh0Pgo8dGV4dCB4PSI0OTAiIHk9IjI5MCIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzZiNzI4MCI+bG9naWNhbCBkZWNvZGluZyBzbG90PC90ZXh0Pgo8L3N2Zz4K" alt="Reference polyglot persistence topology: PostgreSQL system of record feeding Valkey and Cassandra on the hot path, and Debezium plus Kafka change data capture into ClickHouse, a vector store and an Iceberg lakehouse" width="960" height="560"><br><em>Figure 4. Reference polyglot persistence topology with one system of record and derived stores.</em></p>
<p>The polyglot persistence pattern that survives contact with production has one rule: exactly one store owns each fact, and every other store holds a derived, purpose-shaped copy that can be rebuilt from the owner. The system of record stays an ACID engine, usually PostgreSQL or MySQL with proper HA, because ledgers, orders and inventory are the facts a regulator will ask about. Change data capture through logical decoding and Kafka fans those facts out. The analytics store receives them into a <code>ReplacingMergeTree</code> keyed by the primary key so that CDC updates collapse on merge. The vector store receives the subset that needs embedding. The lakehouse receives everything for retention and model training.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Landing a Debezium CDC topic in ClickHouse 26.x: derived copy, rebuildable, explicit engine">CREATE TABLE analytics.orders_cdc
(
    order_id     UInt64,
    customer_id  UInt64,
    order_ts     DateTime64(6, 'UTC'),
    region       LowCardinality(String),
    channel      LowCardinality(String),
    status       LowCardinality(String),
    amount       Decimal(12, 2),
    _version     UInt64,          -- Debezium source.lsn or ts_ms
    _deleted     UInt8            -- 1 when op = 'd'
)
ENGINE = ReplicatedReplacingMergeTree(
    '/clickhouse/tables/{shard}/analytics/orders_cdc',
    '{replica}',
    _version,
    _deleted
)
PARTITION BY toYYYYMM(order_ts)
ORDER BY (region, order_ts, order_id)
SETTINGS index_granularity = 8192;

-- Dashboards read with FINAL or through a materialised view that
-- pre-aggregates; the raw table is never the source of truth.
SELECT region,
       channel,
       sum(amount) AS revenue
FROM analytics.orders_cdc FINAL
WHERE order_ts &gt;= now() - INTERVAL 90 DAY
  AND _deleted = 0
GROUP BY region, channel
ORDER BY revenue DESC;</pre>
<p>The cost of this architecture is real and should be stated to a board plainly. Every arrow is a replication lag to monitor, a schema contract to version, a backfill procedure to rehearse and an on-call surface to staff. Polyglot persistence multiplies the number of things that can be wrong at three in the morning, which is why the number of stores must be the smallest that the workload contradictions force, and never one more.</p>
<h2>Questions a CIO, a founder or an investor should ask about polyglot persistence<a class="anchor-link" id="questions-a-cio-a-founder-or-an-investor-should-ask-about-polyglot-persistence"></a></h2>
<p>The technical argument for polyglot persistence above reduces to a handful of questions that a non-specialist can put to any engineering team or any vendor, and that a diligence process should insist on getting answered with evidence.</p>
<p>Which store owns each fact, and can every other store be rebuilt from it without downtime? What is the measured replication lag between the system of record and the analytics store at peak, and who is paged when it exceeds the agreed staleness? What is the hot-key throughput ceiling of the transactional engine on its current hardware, measured the way we measured it above, and how far is peak traffic from that ceiling? What does the cloud bill look like per workload class, including egress between stores, and what is the exit plan for each managed service? What is the recall of the retrieval pipeline against a labelled set, and when was it last measured?</p>
<p>A team that can answer those with catalog views, <code>system.*</code> tables and dated measurements is running polyglot persistence deliberately. A team that answers with a vendor&rsquo;s architecture slide is running it by accident, and the accident is usually discovered during a growth spike, an audit or an acquisition.</p>
<h2>Why a polyglot persistence partner has to be vendor-neutral<a class="anchor-link" id="why-a-polyglot-persistence-partner-has-to-be-vendor-neutral"></a></h2>
<p>Every engine in a polyglot persistence estate is sold by a company whose revenue depends on you choosing it for as much of your estate as possible. The transactional vendor will add columnar indexes and vector types and tell you the second and third engines are unnecessary. The columnar vendor will add row-level updates and tell you it can be the system of record. The cloud vendor will bundle all of them and tell you the topology question is solved. Each claim is partly true, and each is a conflict of interest, because the vendor is paid for the engine and not for the outcome.</p>
<p><a href="https://minervadb.com/">MinervaDB</a> is paid for the outcome. We are data platform practitioners rather than a product company: our engineers have run <a href="https://minervadb.com/postgresql-consulting/">PostgreSQL</a>, MySQL, SQL Server, MongoDB, Cassandra, <a href="https://minervadb.com/redis-support/">Redis and Valkey</a>, <a href="https://minervadb.com/clickhouse-consulting/">ClickHouse</a>, Milvus and the managed cloud editions of all of them in production, across more than 900 enterprises, and we have no licence to sell.</p>
<p>That independence is what lets us tell a client that pgvector is enough for their corpus, that their Cassandra cluster should be a PostgreSQL partition, or that the columnar migration they were sold will not fix a hot-row problem. The measurements in this post are the kind of evidence we bring to every architecture review, and the polyglot persistence decision frame above is the one we apply.</p>
<p>If you are designing, funding or acquiring a business that runs on a consumer-facing data platform, the polyglot persistence conversation is the one to have before the growth spike rather than after it. <a href="https://minervadb.com/contact-minervadb-book-an-appointment/">Talk to MinervaDB</a> about an independent architecture review of your data platform, from the system of record to the retrieval layer. As always: test every change on your own workload before it reaches production, and keep a rehearsed disaster-recovery posture for every store in the topology, derived copies included.</p>
<h2>Frequently asked questions about polyglot persistence<a class="anchor-link" id="frequently-asked-questions-about-polyglot-persistence"></a></h2>
<p><strong>Is polyglot persistence just over-engineering for a startup?</strong> Usually, at first. A single well-run PostgreSQL with a cache in front of it carries most products to meaningful scale. Polyglot persistence becomes necessary when a specific measurement, such as the hot-row ceiling or the analytics scan cost shown above, contradicts a specific product requirement. Add the second store when the measurement says so, not when a slide does.</p>
<p><strong>Can a multi-model database replace polyglot persistence?</strong> A multi-model engine reduces the operational surface of polyglot persistence, which is valuable, but it does not change the physics. A row store with a columnar index still reads tuples for transactional work and still serialises on a hot key; a columnar engine with row updates still performs mutations. Evaluate multi-model features by measuring the specific workload against a dedicated engine and pricing the difference.</p>
<p><strong>Does the cloud make the polyglot persistence decision for me?</strong> No. Cloud platforms make each engine easier to provision and scale, and they make moving data between engines more expensive. The workload contradictions are the same on-premises and in the cloud; what changes is the operating model and the cost structure of the arrows between stores.</p>
<p><strong>Where does the vector database sit in a polyglot persistence design?</strong> As a derived, rebuildable index over facts owned by the system of record. Embedding model changes and recall regressions are routine, so the pipeline that rebuilds the vector store must be as tested as the backup that restores the ledger.</p>
<p><em>Lab notes for the polyglot persistence measurements: PostgreSQL 16.13 (Ubuntu build), shared_buffers 1 GB, synchronous_commit off, two vCPUs, 7 GB RAM; ClickHouse 26.7.2 embedded via chdb on the same host; 5,000,000 rows generated with generate_series and exported unchanged between engines; pgbench 10-second runs; all outputs are verbatim with trims marked. Figures are mechanism demonstrations from a sandbox and are not vendor benchmarks. Reproduce on your own hardware before drawing capacity conclusions.</em></p>

<p><a href="https://minervadb.com/polyglot-persistence/">Polyglot Persistence: The Proven Case for 5 Data Models</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Launching MariaDB’s Database Survey 2026</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/launching-mariadbs-database-survey-2026/" />
      <id>https://mariadb.org/launching-mariadbs-database-survey-2026/</id>
      <updated>2026-09-04T10:03:41+03:00</updated>
      <author><name>Robert Silén</name></author>
      <summary type="html"><![CDATA[<p>We want to know how you’re really using MariaDB — and databases in general. Not the marketing version or the one-off case study. The real story: what you’re building, how you are using it, what’s working, and what’s frustrating. …<br />
Continue reading \"Launching MariaDB’s Database Survey 2026\"<br />
Launching MariaDB’s Database Survey 2026 appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/launching-mariadbs-database-survey-2026/">Launching MariaDB’s Database Survey 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We want to know how you&rsquo;re really using MariaDB &mdash; and databases in general. Not the marketing version or the one-off case study. The real story: what you&rsquo;re building, how you are using it, what&rsquo;s working, and what&rsquo;s frustrating. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/launching-mariadbs-database-survey-2026/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;Launching MariaDB&rsquo;s Database Survey 2026&rdquo;</span></a></p>
<p><a href="https://mariadb.org/launching-mariadbs-database-survey-2026/">Launching MariaDB&rsquo;s Database Survey 2026</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/launching-mariadbs-database-survey-2026/">Launching MariaDB’s Database Survey 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Plugins Beyond C++: What the Community Told Us</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/" />
      <id>https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/</id>
      <updated>2026-09-04T09:56:31+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>A few weeks ago, we asked a simple question:<br />
Which language would you use to write MariaDB plugins?<br />
The question related to MDEV-40189 (Support plugins written in various languages), an idea we introduced earlier this summer to lower the barrier to MariaDB plugin development by supporting languages other than C and C++. …<br />
Continue reading \"MariaDB Plugins Beyond C++: What the Community Told Us\"<br />
MariaDB Plugins Beyond C++: What the Community Told Us appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/">MariaDB Plugins Beyond C++: What the Community Told Us</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A few weeks ago, we asked a simple question:<br>
Which language would you use to write MariaDB plugins?<br>
The question related to <a href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/">MDEV-40189</a> (Support plugins written in various languages), an idea we introduced earlier this summer to lower the barrier to MariaDB plugin development by supporting languages other than C and C++. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;MariaDB Plugins Beyond C++: What the Community Told Us&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/">MariaDB Plugins Beyond C++: What the Community Told Us</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-plugins-beyond-c-what-the-community-told-us/">MariaDB Plugins Beyond C++: What the Community Told Us</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>BYOC Database Security: A Measurable Standard in 7 Domains</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/byoc-database-security-standard/" />
      <id>https://minervadb.com/byoc-database-security-standard/</id>
      <updated>2026-09-03T13:30:42+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>BYOC database security conversations usually stall on the same question: is this deployment secure? Asked like that, the question has no answer. A ClickHouse cluster or a PostgreSQL fleet running in your own AWS account [...]</p>
<p><a href="https://minervadb.com/byoc-database-security-standard/">BYOC Database Security: A Measurable Standard in 7 Domains</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><strong>BYOC database security</strong> conversations usually stall on the same question: is this deployment secure? Asked like that, the question has no answer. A ClickHouse cluster or a PostgreSQL fleet running in your own AWS account under a vendor&rsquo;s control plane is neither secure nor insecure in the abstract. It is conformant, or not, to a standard you wrote down. If the standard exists and every control in it is measurable, then a sentence like &ldquo;analytics-ch-prod is 77.4% conformant to MDB-SEC-2026-09-BYOC v1.0, non-conformant on one gate&rdquo; is a fact you can defend to an auditor, a CISO and the vendor. Without the standard, &ldquo;secure&rdquo; is an opinion.</p>
<p>This post shows how to write that BYOC database security standard so it can be scored, how to collect evidence from the database and the cloud account rather than from a questionnaire, and how to turn the evidence into a percentage and a verdict that behave sensibly. Everything below is runnable. The evaluator output shown later is a real run against a hand-built evidence file, not a mock-up.</p>
<h2>What BYOC actually splits, and why BYOC database security has to be scored differently<a class="anchor-link" id="what-byoc-actually-splits-and-why-byoc-database-security-has-to-be-scored-differently"></a></h2>
<p>Bring Your Own Cloud (BYOC) is a specific deployment model, and the security argument hinges on its shape. The vendor&rsquo;s control plane, meaning the orchestrator, the tenant console, billing and support tooling, runs in the vendor&rsquo;s account. The data plane, meaning the database nodes, the block storage, the object storage holding data and backups, the network boundary and the KMS keys, runs in your account. ClickHouse describes its BYOC data plane as running <a href="https://clickhouse.com/docs/cloud/reference/byoc/overview" target="_blank" rel="noopener">entirely in your cloud account</a> with the control plane in ClickHouse&rsquo;s VPC; Redpanda&rsquo;s <a href="https://docs.redpanda.com/cloud-data-platform/get-started/byoc-arch/" target="_blank" rel="noopener">BYOC architecture</a> follows the same split, and so do most of the other vendors offering the model. </p>
<p>The bridge between the two halves is a cross-account IAM role the control-plane agent assumes, plus whatever break-glass path the vendor&rsquo;s operators use when something needs hands.</p>
<p>That split is what makes BYOC database security scoreable at all. In a fully managed service you can only attest: read the SOC 2 report, accept the shared-responsibility matrix, move on. In BYOC, every object in the data plane is queryable through your own cloud APIs and your own database catalog. You can measure it, and if you can measure it you can score it. The control plane stays opaque, and the standard has to be honest about that boundary rather than pretending a SOC 2 PDF is the same kind of evidence as a security-group rule you read from the API.</p>
<figure><img decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MjAiIGhlaWdodD0iNTcwIiB2aWV3Qm94PSIwIDAgOTIwIDU3MCIgZm9udC1mYW1pbHk9Ik1lbmxvLCBDb25zb2xhcywgJ0RlamFWdSBTYW5zIE1vbm8nLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZjIzMjgiPgogIDxkZWZzPgogICAgPHBhdHRlcm4gaWQ9ImhhdGNoIiB3aWR0aD0iOCIgaGVpZ2h0PSI4IiBwYXR0ZXJuVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBwYXR0ZXJuVHJhbnNmb3JtPSJyb3RhdGUoNDUpIj4KICAgICAgPGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iMCIgeTI9IjgiIHN0cm9rZT0iI2M5YzJiNiIgc3Ryb2tlLXdpZHRoPSIxIi8+CiAgICA8L3BhdHRlcm4+CiAgICA8bWFya2VyIGlkPSJhcnIiIHZpZXdCb3g9IjAgMCAxMCAxMCIgcmVmWD0iOSIgcmVmWT0iNSIgbWFya2VyV2lkdGg9IjgiIG1hcmtlckhlaWdodD0iOCIgb3JpZW50PSJhdXRvLXN0YXJ0LXJldmVyc2UiPgogICAgICA8cGF0aCBkPSJNMCwwIEwxMCw1IEwwLDEwIHoiIGZpbGw9IiMxZjIzMjgiLz4KICAgIDwvbWFya2VyPgogIDwvZGVmcz4KICA8cmVjdCB3aWR0aD0iOTIwIiBoZWlnaHQ9IjU3MCIgZmlsbD0iI2ZkZmNmYSIvPgoKICA8IS0tIHZlbmRvciBhY2NvdW50IC0tPgogIDxyZWN0IHg9IjMwIiB5PSI1MCIgd2lkdGg9IjMzMCIgaGVpZ2h0PSIyMDAiIGZpbGw9InVybCgjaGF0Y2gpIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBzdHJva2UtZGFzaGFycmF5PSI2IDQiLz4KICA8cmVjdCB4PSIzMCIgeT0iNTAiIHdpZHRoPSIzMzAiIGhlaWdodD0iMjAwIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuNzIiLz4KICA8dGV4dCB4PSI0NCIgeT0iNzIiIGZvbnQtd2VpZ2h0PSJib2xkIj5WRU5ET1IgQUNDT1VOVCAgwrcgIGNvbnRyb2wgcGxhbmU8L3RleHQ+CiAgPHRleHQgeD0iNDQiIHk9Ijg4IiBmaWxsPSIjNTc2MDZhIj55b3UgbmV2ZXIgZ2V0IGNyZWRlbnRpYWxzIGhlcmU8L3RleHQ+CgogIDxyZWN0IHg9IjUwIiB5PSIxMDQiIHdpZHRoPSIxNDAiIGhlaWdodD0iNDQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIxMjAiIHk9IjEyMyIgdGV4dC1hbmNob3I9Im1pZGRsZSI+b3JjaGVzdHJhdG9yIC88L3RleHQ+CiAgPHRleHQgeD0iMTIwIiB5PSIxMzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnNjaGVkdWxlcjwvdGV4dD4KCiAgPHJlY3QgeD0iMjA1IiB5PSIxMDQiIHdpZHRoPSIxNDAiIGhlaWdodD0iNDQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIyNzUiIHk9IjEyMyIgdGV4dC1hbmNob3I9Im1pZGRsZSI+dGVuYW50IGNvbnNvbGU8L3RleHQ+CiAgPHRleHQgeD0iMjc1IiB5PSIxMzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmJpbGxpbmcgwrcgc3VwcG9ydDwvdGV4dD4KCiAgPHJlY3QgeD0iNTAiIHk9IjE3MCIgd2lkdGg9IjI5NSIgaGVpZ2h0PSI0NCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjE5NyIgeT0iMTg5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5vcGVyYXRvciBicmVhay1nbGFzcyBwYXRoPC90ZXh0PgogIDx0ZXh0IHg9IjE5NyIgeT0iMjA0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5DUEwtMDEgIMK3ICB0aWNrZXRlZCwgVFRMLWJvdW5kPC90ZXh0PgoKICA8IS0tIGN1c3RvbWVyIGFjY291bnQgLS0+CiAgPHJlY3QgeD0iNDIwIiB5PSI1MCIgd2lkdGg9IjQ3MCIgaGVpZ2h0PSI0NzAiIGZpbGw9IiNmM2Y2ZjQiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjYiLz4KICA8dGV4dCB4PSI0MzQiIHk9IjcyIiBmb250LXdlaWdodD0iYm9sZCI+WU9VUiBDTE9VRCBBQ0NPVU5UICDCtyAgZGF0YSBwbGFuZTwvdGV4dD4KICA8dGV4dCB4PSI0MzQiIHk9Ijg4IiBmaWxsPSIjNTc2MDZhIj5ldmVyeXRoaW5nIGJlbG93IGlzIHNjb3JlYWJsZSBieSB5b3U8L3RleHQ+CgogIDwhLS0gVlBDIC0tPgogIDxyZWN0IHg9IjQ0MCIgeT0iMTA0IiB3aWR0aD0iNDMwIiBoZWlnaHQ9IjMwMCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgc3Ryb2tlLWRhc2hhcnJheT0iMyAzIi8+CiAgPHRleHQgeD0iODYwIiB5PSIxMjIiIHRleHQtYW5jaG9yPSJlbmQiPlZQQyAgMTAuNDAuMC4wLzE2PC90ZXh0PgoKICA8cmVjdCB4PSI0NTUiIHk9IjE0MCIgd2lkdGg9IjE2NSIgaGVpZ2h0PSI1NiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjUzNyIgeT0iMTYwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5jb250cm9sLXBsYW5lIGFnZW50PC90ZXh0PgogIDx0ZXh0IHg9IjUzNyIgeT0iMTc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5JQU0gcm9sZSAg4oaSICBDUEwtMDI8L3RleHQ+CiAgPHRleHQgeD0iNTM3IiB5PSIxOTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPkV4dGVybmFsSWQgIOKGkiAgSUFNLTAzPC90ZXh0PgoKICA8cmVjdCB4PSI2MzUiIHk9IjE0MCIgd2lkdGg9IjIzMCIgaGVpZ2h0PSI1NiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9Ijc1MCIgeT0iMTYwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5kYXRhYmFzZSBub2RlcyAoazhzIC8gVk1zKTwvdGV4dD4KICA8dGV4dCB4PSI3NTAiIHk9IjE3NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+VExTIGxpc3RlbmVycyAg4oaSICBFTkMtMDIsIENGRy0wMTwvdGV4dD4KICA8dGV4dCB4PSI3NTAiIHk9IjE5MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+YXV0aCAvIHJvbGVzICAg4oaSICBJQU0tMDEsIElBTS0wMjwvdGV4dD4KCiAgPHJlY3QgeD0iNDYwIiB5PSIyMzAiIHdpZHRoPSIxODAiIGhlaWdodD0iNTAiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSI1NTAiIHk9IjI1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+c2VjdXJpdHkgZ3JvdXBzIC8gTkFDTDwvdGV4dD4KICA8dGV4dCB4PSI1NTAiIHk9IjI2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+TkVULTAxICDCtyAgTkVULTAzIGVncmVzczwvdGV4dD4KCiAgPHJlY3QgeD0iNjU1IiB5PSIyMzAiIHdpZHRoPSIyMTIiIGhlaWdodD0iNTAiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSI3NjEiIHk9IjI1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UHJpdmF0ZUxpbmsgLyBQU0MgZW5kcG9pbnQ8L3RleHQ+CiAgPHRleHQgeD0iNzYxIiB5PSIyNjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPk5FVC0wMjwvdGV4dD4KCiAgPHJlY3QgeD0iNDYwIiB5PSIzMTAiIHdpZHRoPSIxODAiIGhlaWdodD0iNTAiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSI1NTAiIHk9IjMzMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+YmxvY2sgc3RvcmFnZSAoQ01LKTwvdGV4dD4KICA8dGV4dCB4PSI1NTAiIHk9IjM0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+RU5DLTAxPC90ZXh0PgoKICA8cmVjdCB4PSI2NzAiIHk9IjMxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSI1MCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9Ijc2MCIgeT0iMzMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5hdWRpdCBsb2cg4oaSIHlvdXIgU0lFTTwvdGV4dD4KICA8dGV4dCB4PSI3NjAiIHk9IjM0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+TE9HLTAxICDCtyAgTE9HLTAyPC90ZXh0PgoKICA8IS0tIG91dHNpZGUgVlBDIGJ1dCBpbnNpZGUgYWNjb3VudCAtLT4KICA8cmVjdCB4PSI0MzUiIHk9IjQyNCIgd2lkdGg9IjIyNSIgaGVpZ2h0PSI3MCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjU0NyIgeT0iNDQ2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5vYmplY3Qgc3RvcmFnZTogZGF0YSArIGJhY2t1cHM8L3RleHQ+CiAgPHRleHQgeD0iNTQ3IiB5PSI0NjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPmJ1Y2tldCBwb2xpY3k6IHZlbmRvciBjYW5ub3QgZGVsZXRlPC90ZXh0PgogIDx0ZXh0IHg9IjU0NyIgeT0iNDc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5CS1AtMDEgIMK3ICBFTkMtMDM8L3RleHQ+CgogIDxyZWN0IHg9IjY3MCIgeT0iNDI0IiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjcwIiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iNzcwIiB5PSI0NDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPktNUyBrZXlzICh5b3Vycyk8L3RleHQ+CiAgPHRleHQgeD0iNzcwIiB5PSI0NjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPmtleSBwb2xpY3kgPSBraWxsIHN3aXRjaDwvdGV4dD4KICA8dGV4dCB4PSI3NzAiIHk9IjQ3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+RU5DLTAxICDCtyAgRU5DLTAzPC90ZXh0PgoKICA8IS0tIGFycm93cyAtLT4KICA8bGluZSB4MT0iMzQ1IiB5MT0iMTI2IiB4Mj0iNDU1IiB5Mj0iMTYwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycikiLz4KICA8dGV4dCB4PSIzMDAiIHk9IjQyIiBmaWxsPSIjNTc2MDZhIj5hZ2VudCBhc3N1bWVzIHJvbGU6IHN0czpBc3N1bWVSb2xlICsgRXh0ZXJuYWxJZCDihpg8L3RleHQ+CiAgPGxpbmUgeDE9IjMzMCIgeTE9IjIxNCIgeDI9IjMzMCIgeTI9IjMxOCIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDxsaW5lIHgxPSIzMzAiIHkxPSIzMTgiIHgyPSI2NDAiIHkyPSIzMTgiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1kYXNoYXJyYXk9IjIgMyIvPgogIDxsaW5lIHgxPSI2NDAiIHkxPSIzMTgiIHgyPSI3MDAiIHkyPSIxOTYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIHN0cm9rZS1kYXNoYXJyYXk9IjIgMyIgbWFya2VyLWVuZD0idXJsKCNhcnIpIi8+CiAgPHRleHQgeD0iNDQiIHk9IjI3MiIgZmlsbD0iIzU3NjA2YSI+b3BlcmF0b3Igc3NoIC8ga3ViZWN0bCByZWFjaGVzIG5vZGVzPC90ZXh0Pjx0ZXh0IHg9IjQ0IiB5PSIyODUiIGZpbGw9IiM1NzYwNmEiPm9ubHkgdGhyb3VnaCB0aGUgdGlja2V0ZWQgcGF0aCDihpI8L3RleHQ+CgogIDwhLS0gY2xpZW50cyAtLT4KICA8cmVjdCB4PSIzMCIgeT0iMzMwIiB3aWR0aD0iMTQwIiBoZWlnaHQ9IjQ0IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMTAwIiB5PSIzNDkiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnlvdXIgYXBwbGljYXRpb25zPC90ZXh0PgogIDx0ZXh0IHg9IjEwMCIgeT0iMzY0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5wcml2YXRlIHN1Ym5ldHM8L3RleHQ+CiAgPGxpbmUgeDE9IjE3MCIgeTE9IjM1MiIgeDI9IjY2NSIgeTI9IjI1OCIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhcnIpIi8+CgogIDwhLS0gbGVnZW5kIC8gY2FwdGlvbiBzdHJpcCAtLT4KICA8bGluZSB4MT0iMzAiIHkxPSI1MzAiIHgyPSI4OTAiIHkyPSI1MzAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIwLjgiLz4KICA8dGV4dCB4PSIzMCIgeT0iNTQ1IiBmaWxsPSIjNTc2MDZhIj5GSUcuIDEgICBCWU9DIHJlc3BvbnNpYmlsaXR5IHNwbGl0LCB3aXRoIE1EQi1TRUMtMjAyNi0wOS1CWU9DIGNvbnRyb2wgSURzIHBsYWNlZCBvbiB0aGUgb2JqZWN0IGVhY2ggb25lIG1lYXN1cmVzLjwvdGV4dD48dGV4dCB4PSIzMCIgeT0iNTU4IiBmaWxsPSIjNTc2MDZhIj5IYXRjaGVkIGFyZWEgaXMgb3BhcXVlIHRvIHlvdTogYXR0ZXN0IGl0LCBkbyBub3QgcHJldGVuZCB0byBtZWFzdXJlIGl0LjwvdGV4dD4KPC9zdmc+Cg==" alt="BYOC database security responsibility split between vendor control plane and customer data plane with control IDs" width="920"><figcaption>Fig. 1 &mdash; The BYOC database security responsibility split, with the control IDs from the standard below placed on the object each one measures. The hatched region is the part you attest rather than measure.</figcaption></figure>
<h2>Writing a BYOC database security standard that produces a number<a class="anchor-link" id="writing-a-byoc-database-security-standard-that-produces-a-number"></a></h2>
<p>Most BYOC database security policies fail the measurability test for a mundane reason: their controls are sentences, not predicates. &ldquo;Access to production databases must be appropriately restricted&rdquo; cannot be scored. A control becomes scoreable when it has four properties. It is atomic, testing exactly one thing. It binds to exactly one evidence key, a value some collector can produce without a human interpreting it. It carries an expectation expressed as an operator and a value. And it carries a weight, because a wildcard IAM policy on the control-plane agent and a SOC 2 report that is thirteen months old are not the same size of problem.</p>
<p>The BYOC database security standard below has seven domains and eighteen controls. It is deliberately short. A standard with two hundred controls, half of which nobody can collect evidence for, produces a number nobody trusts. Eighteen controls with real collectors produce a number you can put in a board pack. Add controls only when a collector exists for them.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># MDB-SEC-2026-09-BYOC  --  BYOC database security standard, v1.0
# Every control is atomic, has exactly one evidence key, and carries a weight.
# gate: true means a FAIL caps the whole target at "non-conformant" regardless of score.
standard: MDB-SEC-2026-09-BYOC
version: "1.0"
weights: {critical: 5, high: 3, medium: 1}

domains:
  - id: IAM
    name: Identity and access
    controls:
      - id: IAM-01
        title: No shared superuser / default admin account enabled for application use
        severity: critical
        gate: true
        evidence: db.superuser_logins_last_30d
        expect: {op: eq, value: 0}
      - id: IAM-02
        title: Database authentication is federated (IAM / OIDC / cert), not password-only
        severity: high
        evidence: db.password_only_roles
        expect: {op: eq, value: 0}
      - id: IAM-03
        title: Vendor cross-account role uses an ExternalId and is scoped to the data-plane account only
        severity: critical
        gate: true
        evidence: cloud.vendor_role_external_id_enforced
        expect: {op: eq, value: true}

  - id: NET
    name: Network boundary
    controls:
      - id: NET-01
        title: Database endpoints have no 0.0.0.0/0 ingress on any port
        severity: critical
        gate: true
        evidence: cloud.public_ingress_rules
        expect: {op: eq, value: 0}
      - id: NET-02
        title: Client access only through PrivateLink / Private Service Connect / peering
        severity: high
        evidence: cloud.private_connectivity_only
        expect: {op: eq, value: true}
      - id: NET-03
        title: Egress from the data plane is allow-listed (vendor control-plane CIDRs + object storage only)
        severity: high
        evidence: cloud.egress_allowlisted
        expect: {op: eq, value: true}

  - id: ENC
    name: Encryption
    controls:
      - id: ENC-01
        title: Storage volumes and object-storage buckets encrypted with a customer-managed key
        severity: critical
        gate: true
        evidence: cloud.cmk_encryption
        expect: {op: eq, value: true}
      - id: ENC-02
        title: TLS 1.2+ enforced on every client-facing listener
        severity: high
        evidence: db.tls_min_version
        expect: {op: gte, value: 1.2}
      - id: ENC-03
        title: Backups encrypted with the same or a dedicated CMK, never vendor-default keys
        severity: high
        evidence: cloud.backup_cmk
        expect: {op: eq, value: true}

  - id: CPL
    name: Control-plane access
    controls:
      - id: CPL-01
        title: Vendor operator access to the data plane is break-glass, ticketed, and time-boxed
        severity: high
        evidence: vendor.breakglass_ttl_hours
        expect: {op: lte, value: 8}
      - id: CPL-02
        title: Control-plane agent runs with least-privilege IAM (no iam:*, no kms:ScheduleKeyDeletion)
        severity: critical
        gate: true
        evidence: cloud.agent_policy_wildcards
        expect: {op: eq, value: 0}
      - id: CPL-03
        title: Vendor SOC 2 Type II report reviewed within the last 12 months
        severity: medium
        evidence: vendor.soc2_age_days
        expect: {op: lte, value: 365}

  - id: LOG
    name: Audit and logging
    controls:
      - id: LOG-01
        title: Database audit log (DDL, grants, auth failures) shipped to a SIEM the vendor cannot write to
        severity: high
        evidence: db.audit_log_shipped
        expect: {op: eq, value: true}
      - id: LOG-02
        title: Cloud API audit trail (CloudTrail / Audit Logs) enabled for the data-plane account
        severity: high
        evidence: cloud.api_audit_enabled
        expect: {op: eq, value: true}

  - id: BKP
    name: Backup and recovery
    controls:
      - id: BKP-01
        title: Backups stored in a bucket the vendor control plane cannot delete
        severity: critical
        gate: true
        evidence: cloud.backup_bucket_vendor_delete_denied
        expect: {op: eq, value: true}
      - id: BKP-02
        title: Restore drill executed and evidenced within the last 90 days
        severity: high
        evidence: ops.last_restore_drill_days
        expect: {op: lte, value: 90}

  - id: CFG
    name: Configuration hardening
    controls:
      - id: CFG-01
        title: No database-level access from the public internet (listen / pg_hba / ClickHouse listen_host)
        severity: high
        evidence: db.listens_public
        expect: {op: eq, value: false}
      - id: CFG-02
        title: Version within vendor support window and no unpatched CVE older than 30 days
        severity: medium
        evidence: db.oldest_unpatched_cve_days
        expect: {op: lte, value: 30}</pre>
<p>Three design choices in that file matter more than the specific controls, and they are what make BYOC database security measurable rather than merely documented. First, the <code>gate: true</code> flag. Six controls are gates; a failure on any of them makes the target non-conformant regardless of the percentage. Without gates, weighted scoring produces the pathology where a deployment with an internet-exposed listener still reads as &ldquo;81%, mostly fine&rdquo;. </p>
<p>Second, every control&rsquo;s <code>evidence</code> key is a dotted path into a JSON document, so the standard never references a database engine, a cloud provider or a vendor by name. The same file scores a ClickHouse BYOC cluster on AWS and a PostgreSQL BYOC deployment on GCP; only the collectors differ. Third, the file has an ID and a version in the MinervaDB document convention, <code>MDB-SEC-2026-09-BYOC v1.0</code>, because a score is meaningless unless the reader knows which standard it was scored against. Changing a weight changes every historical number, so weights change through a versioned pull request, never in place.</p>
<h2>Collecting BYOC database security evidence from the database, not from a questionnaire<a class="anchor-link" id="collecting-byoc-database-security-evidence-from-the-database-not-from-a-questionnaire"></a></h2>
<p>The evidence for the db.* keys in a BYOC database security assessment comes from the database&rsquo;s own catalog. The point of writing BYOC database security collectors as SQL is that the SQL is the audit trail: anyone can rerun the query and get the same answer. Two collectors follow, one for ClickHouse and one for PostgreSQL, each producing the raw values the standard expects. Version pinning matters here; several of these system tables changed shape recently.</p>
<h3>ClickHouse BYOC database security collector (24.8 LTS and later)<a class="anchor-link" id="clickhouse-byoc-database-security-collector-24-8-lts-and-later"></a></h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- BYOC database security: ClickHouse collector for db.* keys.  Tested syntax on ClickHouse 24.8 LTS and later.
-- Run as a user holding SHOW USERS, SHOW GRANTS and access to system.query_log.

-- IAM-01  logins as the built-in `default` user in the last 30 days
--         (query_log is on by default; session_log is not, so we count from query_log)
SELECT count() AS superuser_logins_last_30d
FROM system.query_log
WHERE user = 'default'
  AND type = 'QueryStart'
  AND event_date &gt;= today() - INTERVAL 30 DAY;

-- IAM-02  roles that can still log in with a password only
--         auth_type became an Array when multiple auth methods per user
--         landed (24.9+); on older builds drop the hasAny() and compare directly
SELECT count() AS password_only_roles
FROM system.users
WHERE hasAny(auth_type,
             ['plaintext_password', 'sha256_password',
              'double_sha1_password', 'bcrypt_password'])
  AND NOT hasAny(auth_type, ['ldap', 'kerberos', 'ssl_certificate', 'jwt', 'http']);

-- CFG-01  is the server listening on anything but private / loopback interfaces?
--         (system.server_settings, 23.x+)
SELECT name, value, changed
FROM system.server_settings
WHERE name IN ('listen_host', 'tcp_port', 'http_port',
               'tcp_port_secure', 'https_port');

-- ENC-02  plaintext listeners left open count as a FAIL for TLS enforcement
SELECT countIf(name IN ('tcp_port', 'http_port') AND value != '') AS plaintext_listeners
FROM system.server_settings;

-- CPL-01 / audit trail  grants issued outside the change window
SELECT event_time, user, query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query_kind IN ('Grant', 'Revoke', 'Create', 'Drop', 'Alter')
  AND event_date &gt;= today() - INTERVAL 7 DAY
ORDER BY event_time DESC
LIMIT 50;</pre>
<p>Two notes on these queries. <code>system.query_log</code> is the right place to count logins by the built-in <code>default</code> user because it is on by default and <code>system.session_log</code> usually is not; if you enable session_log you get a cleaner signal, including failed logins. And the <code>auth_type</code> column on <code>system.users</code> became an array when multiple authentication methods per user landed, so the <code>hasAny()</code> form is the one that survives upgrades. On BYOC deployments you typically cannot edit <code>config.xml</code> directly, but you can read <code>system.server_settings</code>, and that is enough to score the listener posture.</p>
<h3>PostgreSQL BYOC database security collector (16 and later)<a class="anchor-link" id="postgresql-byoc-database-security-collector-16-and-later"></a></h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- BYOC database security: PostgreSQL collector for db.* keys.  Tested syntax on PostgreSQL 16 and 17.
-- Run as a role holding pg_read_all_settings and pg_read_all_stats.

-- IAM-01  login-capable superusers other than the bootstrap role
SELECT count(*) AS extra_login_superusers
FROM pg_roles
WHERE rolsuper
  AND rolcanlogin
  AND rolname  'postgres';

-- IAM-02  pg_hba entries that authenticate with a password or nothing at all
--         (federated methods: cert, gss, sspi, ldap, radius, oauth in 18+)
SELECT count(*) AS password_only_rules
FROM pg_hba_file_rules
WHERE auth_method IN ('trust', 'password', 'md5', 'scram-sha-256')
  AND error IS NULL;

-- CFG-01 / ENC-02  listener and TLS posture in one pass
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN ('listen_addresses', 'ssl', 'ssl_min_protocol_version',
               'log_connections', 'password_encryption');

-- LOG-01  is pgaudit loaded and shipping DDL / role changes?
SELECT name, setting
FROM pg_settings
WHERE name IN ('shared_preload_libraries', 'pgaudit.log', 'log_destination');</pre>
<p><code>pg_hba_file_rules</code> is the collector that most often surprises people. It shows the rules PostgreSQL actually loaded, including ones with parse errors, so filter on <code>error IS NULL</code> or you will count rules that are not in effect. On PostgreSQL 18 the <code>oauth</code> method joins the federated list. A superuser that can log in and is not the bootstrap role is the PostgreSQL analogue of the ClickHouse <code>default</code> user problem: it is where application credentials end up when nobody is looking.</p>
<h3>Cloud account BYOC database security collector (AWS shown, read-only)<a class="anchor-link" id="cloud-account-byoc-database-security-collector-aws-shown-read-only"></a></h3>
<p>The cloud.* keys are where BYOC database security differs from every other model, because the objects that matter live in your account. The collector below reads security groups, the vendor&rsquo;s cross-account role trust policy, the agent role&rsquo;s attached policies, EBS key management and CloudTrail state. It runs under a read-only audit role and writes nothing. The GCP and Azure equivalents are structurally identical: replace the IAM trust-policy check with a Workload Identity binding or a service-principal check, and the EBS/KMS check with CMEK on persistent disks or managed disks.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/env python3
"""
collect_aws.py -- BYOC database security: produce the cloud.* evidence keys for one BYOC data-plane account.

Run with a READ-ONLY role in the data-plane account:
    AWS_PROFILE=byoc-audit python3 collect_aws.py 
        --vendor-role ClickHouseBYOCControlPlane 
        --tag Key=byoc-target,Value=analytics-ch-prod &gt; evidence/cloud.json

The script only reads. It writes nothing to AWS.
"""
import argparse
import json

import boto3

WILDCARD_DENYLIST = {"iam:*", "kms:*", "kms:ScheduleKeyDeletion", "s3:*", "ec2:*", "*"}


def public_ingress_rules(ec2, tag_key, tag_val) -&gt; int:
    """NET-01: security-group ingress rules open to the world on data-plane ENIs."""
    sgs = ec2.describe_security_groups(
        Filters=[{"Name": f"tag:{tag_key}", "Values": [tag_val]}])["SecurityGroups"]
    hits = 0
    for sg in sgs:
        for rule in sg["IpPermissions"]:
            hits += sum(1 for r in rule.get("IpRanges", []) if r["CidrIp"] == "0.0.0.0/0")
            hits += sum(1 for r in rule.get("Ipv6Ranges", []) if r["CidrIpv6"] == "::/0")
    return hits


def vendor_role_external_id_enforced(iam, role_name) -&gt; bool:
    """IAM-03: the trust policy must carry a StringEquals on sts:ExternalId."""
    doc = iam.get_role(RoleName=role_name)["Role"]["AssumeRolePolicyDocument"]
    for stmt in doc["Statement"]:
        cond = stmt.get("Condition", {}).get("StringEquals", {})
        if "sts:ExternalId" not in cond:
            return False
    return True


def agent_policy_wildcards(iam, role_name) -&gt; int:
    """CPL-02: count denylisted wildcard actions across inline + attached policies."""
    docs = []
    for name in iam.list_role_policies(RoleName=role_name)["PolicyNames"]:
        docs.append(iam.get_role_policy(RoleName=role_name, PolicyName=name)["PolicyDocument"])
    for att in iam.list_attached_role_policies(RoleName=role_name)["AttachedPolicies"]:
        ver = iam.get_policy(PolicyArn=att["PolicyArn"])["Policy"]["DefaultVersionId"]
        docs.append(iam.get_policy_version(PolicyArn=att["PolicyArn"], VersionId=ver)["PolicyVersion"]["Document"])
    hits = 0
    for doc in docs:
        for stmt in doc["Statement"]:
            if stmt.get("Effect") != "Allow":
                continue
            actions = stmt["Action"] if isinstance(stmt["Action"], list) else [stmt["Action"]]
            hits += sum(1 for a in actions if a in WILDCARD_DENYLIST)
    return hits


def cmk_encryption(ec2, kms, tag_key, tag_val) -&gt; bool:
    """ENC-01: every tagged EBS volume encrypted with a key whose KeyManager is CUSTOMER."""
    vols = ec2.describe_volumes(Filters=[{"Name": f"tag:{tag_key}", "Values": [tag_val]}])["Volumes"]
    if not vols:
        return False
    for v in vols:
        if not v["Encrypted"]:
            return False
        if kms.describe_key(KeyId=v["KmsKeyId"])["KeyMetadata"]["KeyManager"] != "CUSTOMER":
            return False
    return True


def api_audit_enabled(ct) -&gt; bool:
    """LOG-02: at least one multi-region trail that is logging and log-file-validated."""
    for t in ct.describe_trails()["trailList"]:
        if t.get("IsMultiRegionTrail") and t.get("LogFileValidationEnabled"):
            if ct.get_trail_status(Name=t["TrailARN"])["IsLogging"]:
                return True
    return False


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--vendor-role", required=True)
    ap.add_argument("--tag", required=True, help="Key=,Value= identifying data-plane resources")
    a = ap.parse_args()
    tag_key, tag_val = [kv.split("=", 1)[1] for kv in a.tag.split(",")]

    ec2, iam, kms, ct = (boto3.client(s) for s in ("ec2", "iam", "kms", "cloudtrail"))
    print(json.dumps({"cloud": {
        "public_ingress_rules": public_ingress_rules(ec2, tag_key, tag_val),
        "vendor_role_external_id_enforced": vendor_role_external_id_enforced(iam, a.vendor_role),
        "agent_policy_wildcards": agent_policy_wildcards(iam, a.vendor_role),
        "cmk_encryption": cmk_encryption(ec2, kms, tag_key, tag_val),
        "api_audit_enabled": api_audit_enabled(ct),
    }}, indent=2))</pre>
<p>The IAM-03 check deserves emphasis. A vendor cross-account role whose trust policy lacks a <code>sts:ExternalId</code> condition is the textbook confused-deputy setup: any tenant of that vendor who learns your role ARN can ask the vendor&rsquo;s control plane to act on it. Every serious BYOC vendor issues an ExternalId during onboarding. The control exists because onboarding scripts get copied between accounts, and the condition gets dropped.</p>
<figure><img decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MjAiIGhlaWdodD0iNDAwIiB2aWV3Qm94PSIwIDAgOTIwIDQwMCIgZm9udC1mYW1pbHk9Ik1lbmxvLCBDb25zb2xhcywgJ0RlamFWdSBTYW5zIE1vbm8nLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZjIzMjgiPgogIDxkZWZzPgogICAgPG1hcmtlciBpZD0iYXJyMiIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI5IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+CiAgICAgIDxwYXRoIGQ9Ik0wLDAgTDEwLDUgTDAsMTAgeiIgZmlsbD0iIzFmMjMyOCIvPgogICAgPC9tYXJrZXI+CiAgPC9kZWZzPgogIDxyZWN0IHdpZHRoPSI5MjAiIGhlaWdodD0iNDAwIiBmaWxsPSIjZmRmY2ZhIi8+CgogIDwhLS0gY29sbGVjdG9ycyBjb2x1bW4gLS0+CiAgPHRleHQgeD0iMzAiIHk9IjQwIiBmb250LXdlaWdodD0iYm9sZCI+MSAgY29sbGVjdDwvdGV4dD4KICA8cmVjdCB4PSIzMCIgeT0iNTIiIHdpZHRoPSIxOTAiIGhlaWdodD0iNDIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIxMjUiIHk9IjcwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5TUUwgY29sbGVjdG9yPC90ZXh0PgogIDx0ZXh0IHg9IjEyNSIgeT0iODUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPnN5c3RlbS4qIC8gcGdfY2F0YWxvZzwvdGV4dD4KCiAgPHJlY3QgeD0iMzAiIHk9IjEwOCIgd2lkdGg9IjE5MCIgaGVpZ2h0PSI0MiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjEyNSIgeT0iMTI2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5jbG91ZCBBUEkgY29sbGVjdG9yPC90ZXh0PgogIDx0ZXh0IHg9IjEyNSIgeT0iMTQxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5ib3RvMyAvIGdjbG91ZCAvIGF6PC90ZXh0PgoKICA8cmVjdCB4PSIzMCIgeT0iMTY0IiB3aWR0aD0iMTkwIiBoZWlnaHQ9IjQyIiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMTI1IiB5PSIxODIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnZlbmRvciBhdHRlc3RhdGlvbjwvdGV4dD4KICA8dGV4dCB4PSIxMjUiIHk9IjE5NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+U09DIDIsIGJyZWFrLWdsYXNzIGxvZzwvdGV4dD4KCiAgPHJlY3QgeD0iMzAiIHk9IjIyMCIgd2lkdGg9IjE5MCIgaGVpZ2h0PSI0MiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjEyNSIgeT0iMjM4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5vcHMgcmVnaXN0ZXI8L3RleHQ+CiAgPHRleHQgeD0iMTI1IiB5PSIyNTMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPnJlc3RvcmUgZHJpbGxzLCBDVkUgYWdlPC90ZXh0PgoKICA8IS0tIGV2aWRlbmNlIGZpbGUgLS0+CiAgPHRleHQgeD0iMzAwIiB5PSI0MCIgZm9udC13ZWlnaHQ9ImJvbGQiPjIgIG5vcm1hbGlzZTwvdGV4dD4KICA8cGF0aCBkPSJNMjk1IDk2IGgxODUgdjEyMCBoLTE4NSB6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHBhdGggZD0iTTI5NSA5NiBoMTY1IGwyMCAyMCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjM4NyIgeT0iMTI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5ldmlkZW5jZS5qc29uPC90ZXh0PgogIDx0ZXh0IHg9IjM4NyIgeT0iMTQ2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5vbmUgZmlsZSBwZXIgdGFyZ2V0PC90ZXh0PgogIDx0ZXh0IHg9IjM4NyIgeT0iMTYyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5mbGF0IGtleXM6IGRiLiogY2xvdWQuKjwvdGV4dD4KICA8dGV4dCB4PSIzODciIHk9IjE3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+dmVuZG9yLiogb3BzLio8L3RleHQ+CiAgPHRleHQgeD0iMzg3IiB5PSIyMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPm1pc3Npbmcga2V5ID0gVU5LTk9XTjwvdGV4dD4KCiAgPGxpbmUgeDE9IjIyMCIgeTE9IjczIiB4Mj0iMzAwIiB5Mj0iMTIwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjIpIi8+CiAgPGxpbmUgeDE9IjIyMCIgeTE9IjEyOSIgeDI9IjMwMCIgeTI9IjE0NSIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhcnIyKSIvPgogIDxsaW5lIHgxPSIyMjAiIHkxPSIxODUiIHgyPSIzMDAiIHkyPSIxNzAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMikiLz4KICA8bGluZSB4MT0iMjIwIiB5MT0iMjQxIiB4Mj0iMzAwIiB5Mj0iMTk1IiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjIpIi8+CgogIDwhLS0gc3RhbmRhcmQgLS0+CiAgPHBhdGggZD0iTTI5NSAyNTAgaDE4NSB2NzAgaC0xODUgeiIgZmlsbD0iI2ZiZjZlYSIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDxwYXRoIGQ9Ik0yOTUgMjUwIGgxNjUgbDIwIDIwIiBmaWxsPSJub25lIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMzg3IiB5PSIyODAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPnN0YW5kYXJkLnlhbWw8L3RleHQ+CiAgPHRleHQgeD0iMzg3IiB5PSIyOTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPmNvbnRyb2xzIMK3IHdlaWdodHMgwrcgZ2F0ZXM8L3RleHQ+CiAgPHRleHQgeD0iMzg3IiB5PSIzMTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPnZlcnNpb25lZCwgY29kZS1yZXZpZXdlZDwvdGV4dD4KCiAgPCEtLSBldmFsdWF0b3IgLS0+CiAgPHRleHQgeD0iNTQwIiB5PSI0MCIgZm9udC13ZWlnaHQ9ImJvbGQiPjMgIGV2YWx1YXRlPC90ZXh0PgogIDxyZWN0IHg9IjU0MCIgeT0iMTIwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjEzMCIgcng9IjIiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjYiLz4KICA8dGV4dCB4PSI2MjUiIHk9IjE1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+YnlvY19zY29yZS5weTwvdGV4dD4KICA8dGV4dCB4PSI2MjUiIHk9IjE3NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+ZXhwZWN0KG9wLCB2YWx1ZSk8L3RleHQ+CiAgPHRleHQgeD0iNjI1IiB5PSIxOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPs6jIHdlaWdodChQQVNTKTwvdGV4dD4KICA8dGV4dCB4PSI2MjUiIHk9IjIwOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAPC90ZXh0PgogIDx0ZXh0IHg9IjYyNSIgeT0iMjI0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj7OoyB3ZWlnaHQoYXBwbGljYWJsZSk8L3RleHQ+CgogIDxsaW5lIHgxPSI0NzAiIHkxPSIxNTYiIHgyPSI1NDAiIHkyPSIxNzAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMikiLz4KICA8bGluZSB4MT0iNDcwIiB5MT0iMjg1IiB4Mj0iNTQwIiB5Mj0iMjE1IiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjIpIi8+CgogIDwhLS0gb3V0cHV0cyAtLT4KICA8dGV4dCB4PSI3NjAiIHk9IjQwIiBmb250LXdlaWdodD0iYm9sZCI+NCAgYWN0PC90ZXh0PgogIDxyZWN0IHg9Ijc2MCIgeT0iODAiIHdpZHRoPSIxMzAiIGhlaWdodD0iNDQiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSI4MjUiIHk9Ijk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5yZXBvcnQuanNvbjwvdGV4dD4KICA8dGV4dCB4PSI4MjUiIHk9IjExMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+c2NvcmUgKyB2ZXJkaWN0PC90ZXh0PgoKICA8cmVjdCB4PSI3NjAiIHk9IjE1MCIgd2lkdGg9IjEzMCIgaGVpZ2h0PSI0NCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjgyNSIgeT0iMTY4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj50aW1lIHNlcmllczwvdGV4dD4KICA8dGV4dCB4PSI4MjUiIHk9IjE4MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzU3NjA2YSI+YWxlcnQgb24gzpQgJmx0OyAwPC90ZXh0PgoKICA8cmVjdCB4PSI3NDUiIHk9IjIyMCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSI0NCIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9IjgyMCIgeT0iMjM4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj50aWNrZXRzPC90ZXh0PgogIDx0ZXh0IHg9IjgyMCIgeT0iMjUzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5vbmUgcGVyIEZBSUwgLyBVTktOT1dOPC90ZXh0PgoKICA8bGluZSB4MT0iNzEwIiB5MT0iMTYwIiB4Mj0iNzYwIiB5Mj0iMTA1IiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjIpIi8+CiAgPGxpbmUgeDE9IjcxMCIgeTE9IjE4MCIgeDI9Ijc2MCIgeTI9IjE3MiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhcnIyKSIvPgogIDxsaW5lIHgxPSI3MTAiIHkxPSIyMDUiIHgyPSI3NDUiIHkyPSIyNDAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMikiLz4KCiAgPCEtLSBzY2hlZHVsZSBub3RlIC0tPgogIDx0ZXh0IHg9IjU0MCIgeT0iMzAwIiBmaWxsPSIjNTc2MDZhIj5ydW5zIGZyb20gQ0kgb24gYSBzY2hlZHVsZSAobmlnaHRseSkgYW5kIG9uIGV2ZXJ5PC90ZXh0PgogIDx0ZXh0IHg9IjU0MCIgeT0iMzE2IiBmaWxsPSIjNTc2MDZhIj5jaGFuZ2UgdG8gc3RhbmRhcmQueWFtbCBvciB0aGUgdmVuZG9yJ3MgYWdlbnQgdmVyc2lvbjwvdGV4dD4KCiAgPGxpbmUgeDE9IjMwIiB5MT0iMzYwIiB4Mj0iODkwIiB5Mj0iMzYwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMC44Ii8+CiAgPHRleHQgeD0iMzAiIHk9IjM3OCIgZmlsbD0iIzU3NjA2YSI+RklHLiAyICAgRXZpZGVuY2UgcGlwZWxpbmUuIENvbGxlY3RvcnMgbmV2ZXIgc2NvcmUgYW5kIHRoZSBldmFsdWF0b3IgbmV2ZXIgY29sbGVjdHMuPC90ZXh0Pjx0ZXh0IHg9IjMwIiB5PSIzOTEiIGZpbGw9IiM1NzYwNmEiPlRoZSBzdGFuZGFyZCBpcyBhIHJldmlld2VkIGZpbGUgdW5kZXIgdmVyc2lvbiBjb250cm9sLCBub3QgYSBzcHJlYWRzaGVldC48L3RleHQ+Cjwvc3ZnPgo=" alt="BYOC database security evidence pipeline from collectors through normalised evidence and versioned standard to scoring" width="920"><figcaption>Fig. 2 &mdash; The BYOC database security evidence pipeline. Collectors emit raw values; the standard is a reviewed file; the evaluator joins the two. Keeping those three roles separate is what lets the same score be reproduced by someone who was not in the room.</figcaption></figure>
<h2>The BYOC database security scoring engine<a class="anchor-link" id="the-byoc-database-security-scoring-engine"></a></h2>
<p>The evaluator that turns evidence into a BYOC database security score is short on purpose. Its rules are the interesting part, and each one is a decision about what the number should mean. The score is the weighted share of applicable controls that pass.</p>
<p>A control whose evidence key is absent is UNKNOWN and scores as a fail; unproven is treated as unsafe, which is the only rule that stops &ldquo;we did not run the collector&rdquo; from inflating the number. A control the evidence marks as not applicable leaves the denominator entirely. And any failing gate control caps the verdict at NON-CONFORMANT while still reporting the score, so the percentage remains useful for tracking progress even when the verdict is red.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/env python3
"""
byoc_score.py -- BYOC database security: score one target against a control standard.

    python3 byoc_score.py byoc_security_standard.yaml evidence/analytics-ch-prod.json

Rules the number is built on (keep these stable; changing them changes history):
  * score = sum(weight of PASS) / sum(weight of applicable controls)
  * a control with missing evidence is UNKNOWN and scores as FAIL -- unproven is unsafe
  * a control marked not_applicable in evidence is removed from the denominator
  * any gate control that fails caps the verdict at NON-CONFORMANT, whatever the score
"""
import json
import operator
import sys
from collections import defaultdict

import yaml

OPS = {"eq": operator.eq, "ne": operator.ne, "gte": operator.ge, "lte": operator.le}


def lookup(evidence: dict, dotted: str):
    """evidence['db']['tls_min_version'] for 'db.tls_min_version'; None if absent."""
    node = evidence
    for part in dotted.split("."):
        if not isinstance(node, dict) or part not in node:
            return None
        node = node[part]
    return node


def evaluate(standard: dict, evidence: dict) -&gt; dict:
    weights = standard["weights"]
    results, by_domain = [], defaultdict(lambda: {"earned": 0, "possible": 0})
    earned = possible = 0
    gate_failures = []

    for domain in standard["domains"]:
        for ctl in domain["controls"]:
            w = weights[ctl["severity"]]
            value = lookup(evidence, ctl["evidence"])
            if value == "not_applicable":
                status = "N/A"
            elif value is None:
                status = "UNKNOWN"
            else:
                exp = ctl["expect"]
                status = "PASS" if OPS[exp["op"]](value, exp["value"]) else "FAIL"

            if status != "N/A":
                possible += w
                by_domain[domain["id"]]["possible"] += w
                if status == "PASS":
                    earned += w
                    by_domain[domain["id"]]["earned"] += w
                elif ctl.get("gate"):
                    gate_failures.append(ctl["id"])

            results.append({"id": ctl["id"], "domain": domain["id"], "severity": ctl["severity"],
                            "weight": w, "status": status, "observed": value,
                            "expect": ctl["expect"], "title": ctl["title"]})

    score = round(100 * earned / possible, 1) if possible else 0.0
    verdict = "NON-CONFORMANT" if gate_failures else ("CONFORMANT" if score &gt;= 90 else "PARTIAL")
    return {"standard": f'{standard["standard"]} v{standard["version"]}',
            "target": evidence.get("target", "?"),
            "score": score, "verdict": verdict, "gate_failures": gate_failures,
            "domains": {d: round(100 * v["earned"] / v["possible"], 1) if v["possible"] else None
                        for d, v in by_domain.items()},
            "controls": results}


def print_report(rep: dict) -&gt; None:
    print(f'{rep["target"]}  vs  {rep["standard"]}')
    print(f'score {rep["score"]}%  verdict {rep["verdict"]}'
          + (f'  (gates failed: {", ".join(rep["gate_failures"])})' if rep["gate_failures"] else ""))
    print("-" * 78)
    for c in rep["controls"]:
        flag = " GATE" if c["status"] == "FAIL" and c["id"] in rep["gate_failures"] else ""
        print(f'{c["id"]:7} {c["status"]:8} w={c["weight"]}  observed={c["observed"]!r:6}%')


if __name__ == "__main__":
    with open(sys.argv[1]) as f:
        std = yaml.safe_load(f)
    with open(sys.argv[2]) as f:
        ev = json.load(f)
    report = evaluate(std, ev)
    print_report(report)
    with open(sys.argv[2].replace(".json", ".report.json"), "w") as f:
        json.dump(report, f, indent=2)</pre>
<p>Here is a hand-built BYOC database security evidence file for a ClickHouse BYOC cluster. The values are illustrative; the file shape is exactly what the collectors above produce once merged. Note the empty ops block: nobody recorded a restore drill, and that absence is going to be scored.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="json">{
  "target": "analytics-ch-prod (ClickHouse BYOC, AWS eu-central-1)",
  "collected_at": "2026-09-03T10:14:00Z",
  "db": {
    "superuser_logins_last_30d": 0,
    "password_only_roles": 2,
    "tls_min_version": 1.2,
    "audit_log_shipped": true,
    "listens_public": false,
    "oldest_unpatched_cve_days": 12
  },
  "cloud": {
    "vendor_role_external_id_enforced": true,
    "public_ingress_rules": 0,
    "private_connectivity_only": true,
    "egress_allowlisted": false,
    "cmk_encryption": true,
    "backup_cmk": true,
    "agent_policy_wildcards": 1,
    "api_audit_enabled": true,
    "backup_bucket_vendor_delete_denied": true
  },
  "vendor": {
    "breakglass_ttl_hours": 4,
    "soc2_age_days": 210
  },
  "ops": {}
}</pre>
<p>And the run, unedited:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ python3 byoc_score.py byoc_security_standard.yaml evidence/analytics-ch-prod.json
analytics-ch-prod (ClickHouse BYOC, AWS eu-central-1)  vs  MDB-SEC-2026-09-BYOC v1.0
score 77.4%  verdict NON-CONFORMANT  (gates failed: CPL-02)
------------------------------------------------------------------------------
IAM-01  PASS     w=5  observed=0            No shared superuser / default admin account 
IAM-02  FAIL     w=3  observed=2            Database authentication is federated (IAM / 
IAM-03  PASS     w=5  observed=True         Vendor cross-account role uses an ExternalId
NET-01  PASS     w=5  observed=0            Database endpoints have no 0.0.0.0/0 ingress
NET-02  PASS     w=3  observed=True         Client access only through PrivateLink / Pri
NET-03  FAIL     w=3  observed=False        Egress from the data plane is allow-listed (
ENC-01  PASS     w=5  observed=True         Storage volumes and object-storage buckets e
ENC-02  PASS     w=3  observed=1.2          TLS 1.2+ enforced on every client-facing lis
ENC-03  PASS     w=3  observed=True         Backups encrypted with the same or a dedicat
CPL-01  PASS     w=3  observed=4            Vendor operator access to the data plane is 
CPL-02  FAIL     w=5  observed=1            Control-plane agent runs with least-privileg GATE
CPL-03  PASS     w=1  observed=210          Vendor SOC 2 Type II report reviewed within 
LOG-01  PASS     w=3  observed=True         Database audit log (DDL, grants, auth failur
LOG-02  PASS     w=3  observed=True         Cloud API audit trail (CloudTrail / Audit Lo
BKP-01  PASS     w=5  observed=True         Backups stored in a bucket the vendor contro
BKP-02  UNKNOWN  w=3  observed=None         Restore drill executed and evidenced within 
CFG-01  PASS     w=3  observed=False        No database-level access from the public int
CFG-02  PASS     w=1  observed=12           Version within vendor support window and no 
------------------------------------------------------------------------------
IAM     76.9%
NET     72.7%
ENC    100.0%
CPL     44.4%
LOG    100.0%
BKP     62.5%
CFG    100.0%</pre>
<p>Read the output the way the CISO will. The headline is not 77.4%; it is NON-CONFORMANT with CPL-02 named as the reason, meaning the vendor&rsquo;s control-plane agent holds a wildcard action in your account. That single line is the conversation to have with the vendor this week. </p>
<p>The 77.4% is the second sentence, and it decomposes: two password-only ClickHouse roles, egress from the data plane not allow-listed, and a restore drill nobody can evidence. The domain breakdown shows CPL at 44.4% and BKP at 62.5%, which is where the next quarter&rsquo;s work goes. Encryption, logging and configuration hardening are at 100% and can be left alone.</p>
<figure><img decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MjAiIGhlaWdodD0iNDgwIiB2aWV3Qm94PSIwIDAgOTIwIDQ4MCIgZm9udC1mYW1pbHk9Ik1lbmxvLCBDb25zb2xhcywgJ0RlamFWdSBTYW5zIE1vbm8nLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZjIzMjgiPgogIDxkZWZzPgogICAgPG1hcmtlciBpZD0iYXJyMyIgdmlld0JveD0iMCAwIDEwIDEwIiByZWZYPSI5IiByZWZZPSI1IiBtYXJrZXJXaWR0aD0iOCIgbWFya2VySGVpZ2h0PSI4IiBvcmllbnQ9ImF1dG8tc3RhcnQtcmV2ZXJzZSI+CiAgICAgIDxwYXRoIGQ9Ik0wLDAgTDEwLDUgTDAsMTAgeiIgZmlsbD0iIzFmMjMyOCIvPgogICAgPC9tYXJrZXI+CiAgPC9kZWZzPgogIDxyZWN0IHdpZHRoPSI5MjAiIGhlaWdodD0iNDgwIiBmaWxsPSIjZmRmY2ZhIi8+CgogIDwhLS0gcGVyLWNvbnRyb2wgZGVjaXNpb24gLS0+CiAgPHRleHQgeD0iMzAiIHk9IjQwIiBmb250LXdlaWdodD0iYm9sZCI+cGVyIGNvbnRyb2w8L3RleHQ+CiAgPHJlY3QgeD0iMzAiIHk9IjU2IiB3aWR0aD0iMTgwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMTIwIiB5PSI4MCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+bG9va3VwKGV2aWRlbmNlIGtleSk8L3RleHQ+CgogIDxwYXRoIGQ9Ik0xMjAgMTMwIGw3MCAyOCBsLTcwIDI4IGwtNzAgLTI4IHoiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIxMjAiIHk9IjE2MiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+cHJlc2VudD88L3RleHQ+CiAgPGxpbmUgeDE9IjEyMCIgeTE9Ijk2IiB4Mj0iMTIwIiB5Mj0iMTMwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjMpIi8+CgogIDxyZWN0IHg9IjI0MCIgeT0iMTM4IiB3aWR0aD0iMTc1IiBoZWlnaHQ9IjQwIiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMzI3IiB5PSIxNTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlVOS05PV048L3RleHQ+CiAgPHRleHQgeD0iMzI3IiB5PSIxNzEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiM1NzYwNmEiPndlaWdodCBjb3VudGVkLCAwIGVhcm5lZDwvdGV4dD4KICA8bGluZSB4MT0iMTkwIiB5MT0iMTU4IiB4Mj0iMjQwIiB5Mj0iMTU4IiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjMpIi8+CiAgPHRleHQgeD0iMjAwIiB5PSIxNTIiIGZpbGw9IiM1NzYwNmEiPm5vPC90ZXh0PgoKICA8cGF0aCBkPSJNMTIwIDIyMCBsNzAgMjggbC03MCAyOCBsLTcwIC0yOCB6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMTIwIiB5PSIyNTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPm5vdF9hcHBsaWNhYmxlPzwvdGV4dD4KICA8bGluZSB4MT0iMTIwIiB5MT0iMTg2IiB4Mj0iMTIwIiB5Mj0iMjIwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjMpIi8+CiAgPHRleHQgeD0iMTI4IiB5PSIyMDUiIGZpbGw9IiM1NzYwNmEiPnllczwvdGV4dD4KCiAgPHJlY3QgeD0iMjQwIiB5PSIyMjgiIHdpZHRoPSIxNzUiIGhlaWdodD0iNDAiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIzMjciIHk9IjI0NiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Ti9BPC90ZXh0PgogIDx0ZXh0IHg9IjMyNyIgeT0iMjYxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5sZWF2ZXMgdGhlIGRlbm9taW5hdG9yPC90ZXh0PgogIDxsaW5lIHgxPSIxOTAiIHkxPSIyNDgiIHgyPSIyNDAiIHkyPSIyNDgiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMykiLz4KICA8dGV4dCB4PSIyMDAiIHk9IjI0MiIgZmlsbD0iIzU3NjA2YSI+eWVzPC90ZXh0PgoKICA8cGF0aCBkPSJNMTIwIDMwNiBsOTUgMzIgbC05NSAzMiBsLTk1IC0zMiB6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMTIwIiB5PSIzNDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPm9wKG9ic2VydmVkLGV4cGVjdCk8L3RleHQ+CiAgPGxpbmUgeDE9IjEyMCIgeTE9IjI3NiIgeDI9IjEyMCIgeTI9IjMxMCIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhcnIzKSIvPgogIDx0ZXh0IHg9IjEyOCIgeT0iMjk1IiBmaWxsPSIjNTc2MDZhIj5ubzwvdGV4dD4KCiAgPHJlY3QgeD0iMjQwIiB5PSIzMDAiIHdpZHRoPSIxMjAiIGhlaWdodD0iMzYiIGZpbGw9IiNlZWY1ZWUiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSIzMDAiIHk9IjMyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UEFTUyAgK3dlaWdodDwvdGV4dD4KICA8bGluZSB4MT0iMjE1IiB5MT0iMzM4IiB4Mj0iMjQwIiB5Mj0iMzE4IiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjMpIi8+CgogIDxyZWN0IHg9IjI0MCIgeT0iMzUwIiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjM2IiBmaWxsPSIjZjdlZWVlIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iMzAwIiB5PSIzNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkZBSUwgICswPC90ZXh0PgogIDxsaW5lIHgxPSIyMTUiIHkxPSIzMzgiIHgyPSIyNDAiIHkyPSIzNjgiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMykiLz4KCiAgPCEtLSBhZ2dyZWdhdGUgLS0+CiAgPHRleHQgeD0iNDUwIiB5PSI0MCIgZm9udC13ZWlnaHQ9ImJvbGQiPnBlciB0YXJnZXQ8L3RleHQ+CiAgPHJlY3QgeD0iNDUwIiB5PSI1NiIgd2lkdGg9IjQ0MCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiNmZmYiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiLz4KICA8dGV4dCB4PSI0NzAiIHk9IjgyIj5zY29yZSAgPSAgzqMgd2VpZ2h0KFBBU1MpIC8gzqMgd2VpZ2h0KFBBU1Mg4oiqIEZBSUwg4oiqIFVOS05PV04pPC90ZXh0PgogIDx0ZXh0IHg9IjQ3MCIgeT0iMTA2IiBmaWxsPSIjNTc2MDZhIj53ZWlnaHRzICAgY3JpdGljYWw9NSAgIGhpZ2g9MyAgIG1lZGl1bT0xPC90ZXh0PgogIDx0ZXh0IHg9IjQ3MCIgeT0iMTI2IiBmaWxsPSIjNTc2MDZhIj53b3JrZWQgcnVuOiAgZWFybmVkIDY1IC8gcG9zc2libGUgODQgIOKGkiAgNzcuNCU8L3RleHQ+CiAgPHRleHQgeD0iNDcwIiB5PSIxNDQiIGZpbGw9IiM1NzYwNmEiPihJQU0tMDIgMyArIE5FVC0wMyAzICsgQ1BMLTAyIDUgKyBCS1AtMDIgMyA9IDE5IG5vdCBlYXJuZWQpPC90ZXh0PgoKICA8cGF0aCBkPSJNNjcwIDIwMCBsOTAgMzAgbC05MCAzMCBsLTkwIC0zMCB6IiBmaWxsPSIjZmZmIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iNjcwIiB5PSIyMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPmFueSBnYXRlIEZBSUw/PC90ZXh0PgogIDxsaW5lIHgxPSI2NzAiIHkxPSIxNTYiIHgyPSI2NzAiIHkyPSIyMDAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMykiLz4KCiAgPHJlY3QgeD0iNDUwIiB5PSIyOTAiIHdpZHRoPSIxNzAiIGhlaWdodD0iNTIiIGZpbGw9IiNmN2VlZWUiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjYiLz4KICA8dGV4dCB4PSI1MzUiIHk9IjMxMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC13ZWlnaHQ9ImJvbGQiPk5PTi1DT05GT1JNQU5UPC90ZXh0PgogIDx0ZXh0IHg9IjUzNSIgeT0iMzI5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjNTc2MDZhIj5zY29yZSBzdGlsbCByZXBvcnRlZDwvdGV4dD4KICA8bGluZSB4MT0iNjAwIiB5MT0iMjQwIiB4Mj0iNTM1IiB5Mj0iMjkwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIiBtYXJrZXItZW5kPSJ1cmwoI2FycjMpIi8+CiAgPHRleHQgeD0iNTQ1IiB5PSIyNjIiIGZpbGw9IiM1NzYwNmEiPnllczwvdGV4dD4KCiAgPHBhdGggZD0iTTc2MCAzMDAgbDkwIDMwIGwtOTAgMzAgbC05MCAtMzAgeiIgZmlsbD0iI2ZmZiIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9Ijc2MCIgeT0iMzM0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5zY29yZSDiiaUgOTA/PC90ZXh0PgogIDxsaW5lIHgxPSI3MDAiIHkxPSIyNTAiIHgyPSI3NjAiIHkyPSIzMDAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMykiLz4KICA8dGV4dCB4PSI3MzUiIHk9IjI3MiIgZmlsbD0iIzU3NjA2YSI+bm88L3RleHQ+CgogIDxyZWN0IHg9IjY0MCIgeT0iMzkwIiB3aWR0aD0iMTEwIiBoZWlnaHQ9IjM2IiBmaWxsPSIjZWVmNWVlIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMS4yIi8+CiAgPHRleHQgeD0iNjk1IiB5PSI0MTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkNPTkZPUk1BTlQ8L3RleHQ+CiAgPGxpbmUgeDE9IjcxNSIgeTE9IjM0NSIgeDI9IjY5NSIgeTI9IjM5MCIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIgbWFya2VyLWVuZD0idXJsKCNhcnIzKSIvPgoKICA8cmVjdCB4PSI3OTAiIHk9IjM5MCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIzNiIgZmlsbD0iI2ZiZjZlYSIgc3Ryb2tlPSIjMWYyMzI4IiBzdHJva2Utd2lkdGg9IjEuMiIvPgogIDx0ZXh0IHg9Ijg0MCIgeT0iNDEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5QQVJUSUFMPC90ZXh0PgogIDxsaW5lIHgxPSI4MDUiIHkxPSIzNDUiIHgyPSI4MzUiIHkyPSIzOTAiIHN0cm9rZT0iIzFmMjMyOCIgc3Ryb2tlLXdpZHRoPSIxLjIiIG1hcmtlci1lbmQ9InVybCgjYXJyMykiLz4KCiAgPGxpbmUgeDE9IjMwIiB5MT0iNDQwIiB4Mj0iODkwIiB5Mj0iNDQwIiBzdHJva2U9IiMxZjIzMjgiIHN0cm9rZS13aWR0aD0iMC44Ii8+CiAgPHRleHQgeD0iMzAiIHk9IjQ1NiIgZmlsbD0iIzU3NjA2YSI+RklHLiAzICAgSG93IHRoZSBwZXJjZW50YWdlIGFuZCB0aGUgdmVyZGljdCBhcmUgZGVyaXZlZC48L3RleHQ+PHRleHQgeD0iMzAiIHk9IjQ2OSIgZmlsbD0iIzU3NjA2YSI+VGhlIGdhdGUgY2hlY2sgZXhpc3RzIHNvIHRoYXQgNzclIHdpdGggYSB3aWxkY2FyZCBJQU0gcG9saWN5IGNhbiBuZXZlciBiZSByZWFkIGFzICJtb3N0bHkgZmluZSIuPC90ZXh0Pgo8L3N2Zz4K" alt="How a BYOC database security score and verdict are derived from per-control results with gate checks" width="920"><figcaption>Fig. 3 &mdash; Per-control and per-target BYOC database security decision logic. The worked figures are from the run above: 65 weight earned out of 84 applicable.</figcaption></figure>
<h2>What a BYOC database security percentage means, and what it does not<a class="anchor-link" id="what-a-byoc-database-security-percentage-means-and-what-it-does-not"></a></h2>
<p>A BYOC database security conformance percentage is a statement about a standard, not about risk. Two things follow for anyone reporting BYOC database security upward. First, the number is only as good as the weights, and weights are a policy decision, so publish them and version them; a team that quietly changes critical from 5 to 3 to hit a target has broken the metric, not improved security. Second, the number can be gamed by removing controls, which is why the standard&rsquo;s control count and version travel with every score. &ldquo;92% against v1.0&rdquo; and &ldquo;92% against v1.3&rdquo; are different claims, and the report.json carries both.</p>
<p>There is also a boundary the standard has to admit. The vendor.* keys are attestations: a SOC 2 report&rsquo;s age, a break-glass TTL the vendor documented. You are recording that the vendor said something, not measuring it. That is why those controls carry lower weights and no gates. If a vendor offers an API for its break-glass audit log, and some do, promote that control from attestation to measurement and raise its weight in the next version of the standard. The direction of travel for BYOC database security is precisely that: moving controls from the hatched region of Fig. 1 into the measurable one.</p>
<h2>Running BYOC database security scoring continuously<a class="anchor-link" id="running-byoc-database-security-scoring-continuously"></a></h2>
<p>A BYOC database security score taken once during onboarding is a photograph. The value of a measurable standard comes from running it on a schedule and treating a drop as an incident. The pipeline in Fig. 2 runs nightly from CI and on two additional triggers: any change to the standard file, and any change in the vendor&rsquo;s agent version, because agent upgrades are when IAM policies quietly grow. Store each report.json with its timestamp, plot the score per target, and alert when it decreases. A decrease means something in your account changed, or the vendor changed something in your account, and in a BYOC deployment those are the two things you most want to know about.</p>
<p>Open one BYOC database security ticket per FAIL and per UNKNOWN, tagged with the control ID, and close them by re-running the evaluator rather than by hand. UNKNOWN BYOC database security tickets are usually the cheapest to close and the most revealing: they are the controls nobody has ever actually checked.</p>
<h2>Where to start with BYOC database security scoring<a class="anchor-link" id="where-to-start-with-byoc-database-security-scoring"></a></h2>
<p>If you run one BYOC database today, you can have a first BYOC database security score by the end of the week. Take the standard above as v0.1, delete the controls you cannot yet collect evidence for, run the SQL and cloud collectors by hand into a single evidence file, and score it. Then add BYOC database security collectors back one at a time. The first number will be low and the first verdict will probably be NON-CONFORMANT; that is the standard doing its job. A deployment you thought was fine and a deployment you can prove is 77.4% conformant with one named gate failure are different things, and only one of them survives contact with an auditor.</p>
<div>Test every BYOC database security collector against a staging deployment before pointing it at production, run the cloud collector under a read-only role, and keep the standard file under the same review discipline as application code. Nothing in this post changes a running database; the collectors read, the evaluator computes. Maintain a tested DR posture independently of any BYOC database security score.</div>
<p>MinervaDB handles BYOC database security reviews for teams running PostgreSQL, ClickHouse, MySQL, MongoDB and Kafka across all three clouds under vendor-neutral <a href="https://minervadb.com/minervadb-consultative-support-2/">24&times;7 consultative support</a> and <a href="https://minervadb.com/remote-dba-services/">remote DBA</a> engagements, and building a scoreable BYOC database security standard for a specific vendor and estate is a common first deliverable. If you want the collectors extended to your platform, or a review of a standard you have already written, <a href="https://minervadb.com/contact-minervadb-book-an-appointment/">book a working session</a>.</p>

<p><a href="https://minervadb.com/byoc-database-security-standard/">BYOC Database Security: A Measurable Standard in 7 Domains</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Connector/C Compatibility: An Update on CONC-821</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/" />
      <id>https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/</id>
      <updated>2026-09-03T12:02:56+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>As a DBA, there are some surprises I enjoy. Discovering a useful feature is one of them. Discovering that an application behaves differently after a maintenance update is considerably further down the list. …<br />
Continue reading \"MariaDB Connector/C Compatibility: An Update on CONC-821\"<br />
MariaDB Connector/C Compatibility: An Update on CONC-821 appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/">MariaDB Connector/C Compatibility: An Update on CONC-821</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>As a DBA, there are some surprises I enjoy. Discovering a useful feature is one of them. Discovering that an application behaves differently after a maintenance update is considerably further down the list. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;MariaDB Connector/C Compatibility: An Update on CONC-821&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/">MariaDB Connector/C Compatibility: An Update on CONC-821</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-connector-c-compatibility-an-update-on-conc-821/">MariaDB Connector/C Compatibility: An Update on CONC-821</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL 26.7 and 9.7 LTS: 7 Powerful Features for Performance, Scalability and High Availability</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mysql-26-7-performance-scalability-high-availability/" />
      <id>https://minervadb.com/mysql-26-7-performance-scalability-high-availability/</id>
      <updated>2026-09-02T19:07:03+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MySQL 26.7 is the first release Oracle has shipped under calendar versioning, and it landed on 28 July 2026 together with MySQL 9.7.2 LTS and MySQL 8.4.11 LTS. Most of the coverage so far has [...]</p>
<p><a href="https://minervadb.com/mysql-26-7-performance-scalability-high-availability/">MySQL 26.7 and 9.7 LTS: 7 Powerful Features for Performance, Scalability and High Availability</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MySQL 26.7 is the first release Oracle has shipped under calendar versioning, and it landed on 28 July 2026 together with MySQL 9.7.2 LTS and MySQL 8.4.11 LTS. Most of the coverage so far has stopped at the version-number change. That is the least interesting thing about it. What matters for anyone running MySQL in production is that MySQL 26.7 moves the Thread Pool plugin into Community Edition, introduces a second replication applier with up to 1,024 worker threads per channel, flips the default Group Replication communication stack, and adds post-quantum key exchange to TLS.</p>
<p>Combined with what 9.7 LTS already carried across from Enterprise (the hypergraph optimizer, OpenTelemetry, the Group Replication components), the Community versus Enterprise line has moved further in two releases than in the previous ten years.</p>
<p>This post walks through the features that actually change performance, scalability and high availability outcomes, with the configuration and the measurement queries you need to verify each one on your own hardware. It also lists what is still Enterprise-only, because that list is now short enough to reason about honestly. Everything here is version-pinned against the release notes; where Oracle has published performance figures, they are labelled as Oracle&rsquo;s, not ours.</p>
<h2>MySQL 26.7, 9.7 LTS and 8.4 LTS: which one goes to production<a class="anchor-link" id="mysql-26-7-9-7-lts-and-8-4-lts-which-one-goes-to-production"></a></h2>
<p>Three supported lines shipped in the July 2026 drop, and they are not interchangeable. MySQL 9.7 was the last release to use sequential version numbers; everything after it is YY.M.P, so 26.7.0 is simply the July 2026 Innovation release. Innovation releases are supported only until the next one appears. They have no Extended Support window. MySQL 26.7 is therefore the right choice for a fleet that upgrades quarterly and wants the Change Stream Applier or the Community thread pool today, and the wrong choice for anything that needs to sit still for three years.</p>
<p>The ladder below is the one Oracle supports. In-place upgrades between LTS lines are adjacent-only, so an 8.0 estate cannot jump to 9.7; it goes through 8.4 first. MySQL 8.0 entered Sustaining Support on 21 April 2026, which means no new patches and no new security fixes for on-premises installations (the HeatWave extension to April 2027 does not apply outside OCI).</p>
<figure><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/mysql-26-7-release-ladder-8-0-8-4-9-7-lts.png" alt="MySQL 26.7 Innovation, 9.7 LTS, 8.4 LTS and 8.0 release ladder with support dates and the adjacent-LTS upgrade path" width="960" height="300" class="size-full" loading="lazy"></figure>
<div>
<table>
<thead>
<tr>
<th>Release</th>
<th>Track</th>
<th>GA</th>
<th>Support horizon</th>
<th>Carries from MySQL 26.7?</th>
</tr>
</thead>
<tbody>
<tr>
<td>MySQL 26.7.0</td>
<td>Innovation</td>
<td>2026-07-28</td>
<td>Until the next Innovation release</td>
<td>Everything in this post</td>
</tr>
<tr>
<td>MySQL 9.7.2</td>
<td>LTS</td>
<td>2026-07-28 (9.7.0 GA 2026-04-21)</td>
<td>Premier 2034-04, Extended 2037-04</td>
<td>Hypergraph, OTel, GR components, duality-view DML, Dynamic Data Masking (EE); thread pool remains EE; no CSA, no PQC</td>
</tr>
<tr>
<td>MySQL 8.4.11</td>
<td>LTS</td>
<td>2026-07-28 (8.4.0 GA 2024-04-10)</td>
<td>Premier 2029-04, Extended 2032-04</td>
<td>Bug fixes only</td>
</tr>
<tr>
<td>MySQL 8.0.46</td>
<td>Sustaining</td>
<td>2026-04-07</td>
<td>No new fixes since 2026-04-21</td>
<td>Nothing</td>
</tr>
</tbody>
</table>
</div>
<p>One practical consequence of calendar versioning that Ronald Bradford flagged in his early-access write-up: any tooling that sorts version strings lexically now believes 26.7 is older than 9.7. Check your inventory scripts, your package pinning and your monitoring dashboards before the first MySQL 26.7 host reports in. Oracle added <code>MYSQL_PREVIOUS_LTS_VERSION</code> and <code>MYSQL_PREVIOUS_LTS_VERSION_ID</code> to <code>mysql_version.h</code> for exactly this reason.</p>
<h2>Thread Pool in MySQL 26.7 Community: connection scalability without the Enterprise licence<a class="anchor-link" id="thread-pool-in-mysql-26-7-community-connection-scalability-without-the-enterprise-licence"></a></h2>
<p>For fifteen years the Thread Pool plugin was the single most-cited reason to buy MySQL Enterprise Edition. MySQL 26.7.0 ships it in Community. Percona Server has had its own thread pool implementation since 5.5, so the capability is not new to the ecosystem, but this is the first time Oracle&rsquo;s implementation, with its priority queues, stall detection and the <code>TP_CONNECTION_ADMIN</code> escape hatch, is available without a subscription. On MySQL 9.7 LTS it is still Enterprise-only; the Community transfer is a 26.7 change.</p>
<p>The mechanism is worth understanding before you turn it on, because it changes the shape of your latency curve rather than just shifting it. The default one-thread-per-connection model creates a kernel thread for every session. At two or three thousand active connections the cost is not memory, it is the OS scheduler and the InnoDB mutexes that all those runnable threads contend on. The thread pool replaces that with <code>thread_pool_size</code> thread groups (default 16), each owning a listener thread, a high-priority queue, a low-priority queue and a small set of worker threads. Connections are assigned to groups round-robin.</p>
<p>The pool tries to keep exactly one thread executing per group and only spawns another when a statement exceeds <code>thread_pool_stall_limit</code> (default 60 ms).</p>
<figure><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/mysql-26-7-thread-pool-architecture.png" alt="MySQL 26.7 Thread Pool architecture: thread groups, listener thread, high and low priority queues, worker threads and InnoDB" width="960" height="420" class="size-full" loading="lazy"></figure>
<p>Loading it on MySQL 26.7 is a startup option, not a dynamic change, so it needs a restart and a maintenance window:</p>
<pre><code># /etc/my.cnf  (MySQL 26.7.0 Community; restart required)
[mysqld]
plugin-load-add               = thread_pool.so
thread_handling               = pool-of-threads
thread_pool_size              = 16        # start at physical cores, not vCPUs
thread_pool_stall_limit       = 60        # ms; raise for OLAP-style statements
thread_pool_max_unused_threads = 32       # 26.7 / 9.7.2 default (was 2)
thread_pool_prio_kickup_timer = 1000      # ms before a low-priority stmt is promoted
thread_pool_max_transactions_limit = 0    # 0 = no cap; set only with TP_CONNECTION_ADMIN granted</code></pre>
<p>Two of those defaults changed in this release cycle. <code>thread_pool_max_unused_threads</code> moved from 2 to 32 in both 26.7.0 and 9.7.2, which reduces thread churn on bursty workloads at the cost of a few idle stacks. Verify the plugin loaded and confirm all four components are active (the plugin registers itself plus three Performance Schema tables):</p>
<pre><code>SELECT plugin_name,
       plugin_status,
       plugin_type
  FROM information_schema.plugins
 WHERE plugin_name LIKE 'thread_pool%'
    OR plugin_name LIKE 'TP_%';</code></pre>
<p>Then measure instead of guessing. The statistics table is the one to watch; the ratio of queued to executed statements per group tells you whether <code>thread_pool_size</code> is too low, and a rising stall count with acceptable throughput tells you the stall limit is too aggressive for your statement mix:</p>
<pre><code>SELECT tp_group_id,
       connections_started,
       queries_executed,
       queries_queued,
       stalled_queries_executed,
       threads_started,
       prio_kickups
  FROM performance_schema.tp_thread_group_stats
 ORDER BY tp_group_id;</code></pre>
<p>The honest framing for anyone planning to enable this on MySQL 26.7: the thread pool improves p99 under connection storms and protects InnoDB from thundering-herd contention. It does not raise peak throughput on a workload that was already CPU-bound at moderate concurrency, and on some short-transaction OLTP profiles it costs a few percent of throughput at low connection counts. Run a sysbench sweep from 64 to 4,096 connections with and without it, on your schema, before committing. We covered the plugin&rsquo;s Community arrival in more detail in <a href="https://minervadb.com/mysql-thread-pool-community-26-7/">MySQL Thread Pool comes to Community in 26.7</a>.</p>
<h2>Change Stream Applier: the MySQL 26.7 replication applier built for backlog<a class="anchor-link" id="change-stream-applier-the-mysql-26-7-replication-applier-built-for-backlog"></a></h2>
<p>This is the largest piece of new replication engineering since the multi-threaded applier gained writeset dependency tracking in 8.0. The Change Stream Applier (CSA) is an opt-in, per-channel alternative to the classic MTA, selected with <code>APPLIER_VERSION = 2</code>. It supports between 1 and 1,024 worker threads per channel, compared to the MTA&rsquo;s coordinator-bound model, and, more importantly, it separates apply progression from commit progression.</p>
<p>Why that separation matters: in the MTA, a worker that finishes applying a transaction parks until every earlier transaction has committed when <code>replica_preserve_commit_order</code> is ON. Under a write-heavy backlog with long-tail transactions, workers spend more time parked than applying. In the CSA design, later independent transactions keep moving through the scheduler while earlier ones wait on their dependencies; commit order is still preserved, but the avoidable parking is gone. Relay-log reads are parallel and events are released after use, which is what makes the new per-channel memory budget possible.</p>
<figure><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/mysql-26-7-change-stream-applier-vs-mta.png" alt="MySQL 26.7 Change Stream Applier pipeline compared with the multi-threaded applier: provider, dependency adaptation, scheduler, thread pool and commit" width="960" height="440" class="size-full" loading="lazy"></figure>
<p>Enabling it on a MySQL 26.7 replica is a channel-level change and requires the channel to be stopped:</p>
<pre><code>-- MySQL 26.7.0 replica; run per channel, outside peak, with a rollback plan.
STOP REPLICA FOR CHANNEL 'ch_primary';

CHANGE REPLICATION SOURCE TO
    APPLIER_VERSION             = 2,
    APPLIER_WORKER_COUNT        = 64,
    APPLIER_EVENT_MEMORY_LIMIT  = 1073741824,   -- 1 GiB per-channel event cache
    REQUIRE_ROW_FORMAT          = 1,
    GTID_ONLY                   = 1
FOR CHANNEL 'ch_primary';

START REPLICA FOR CHANNEL 'ch_primary';

-- Verify the channel is running with the new applier.
SELECT channel_name,
       applier_version,
       applier_worker_count,
       applier_event_memory_limit
  FROM performance_schema.replication_applier_configuration;</code></pre>
<p>Rollback is symmetric: stop the channel, set <code>APPLIER_VERSION = 1</code>, start it. The existing <code>performance_schema.replication_applier_status_by_worker</code> and <code>replication_applier_status_by_coordinator</code> views keep working, so your lag dashboards do not need to change. <code>APPLIER_EVENT_MEMORY_LIMIT</code> replaces the global <code>replica_pending_jobs_size_max</code> for CSA channels and is silently raised to <code>replica_max_allowed_packet</code> if you set it lower.</p>
<p>The prerequisites are strict and they are where most first attempts fail. The source must run with <code>gtid_mode = ON</code>, the channel must be <code>GTID_ONLY = 1</code> and <code>REQUIRE_ROW_FORMAT = 1</code>, and statement or mixed binlog formats are rejected outright. File-and-position replication, <code>SOURCE_DELAY</code>, <code>sql_replica_skip_counter</code>, <code>IGNORE_SERVER_IDS</code> and most <code>START REPLICA ... UNTIL</code> forms are unsupported. If you run a delayed replica for operator-error recovery, that channel stays on the MTA.</p>
<p>Oracle&rsquo;s engineering post reports sysbench <code>oltp_write_only</code> results with 64 workers where the CSA reached roughly 10.5k TPS against 7.2k for the MTA on the default durability profile with ten updates per transaction, and a wider gap with relaxed durability. Those are Oracle-reported numbers on Oracle&rsquo;s hardware.</p>
<p>The pattern they show is plausible given the design (the gain grows with transaction size and with backlog depth), but the only number that matters for you is your own replica&rsquo;s <code>Seconds_Behind_Source</code> recovery time after an induced backlog, measured with <code>APPLIER_VERSION = 1</code> and <code>2</code> on the same channel. A clean way to induce one is to <code>STOP REPLICA SQL_THREAD</code> for a fixed interval under production-shaped load, restart it, and record time-to-zero-lag from <code>replication_applier_status_by_worker</code>. Run it at least three times per configuration and report the median.</p>
<h2>Group Replication in MySQL 26.7: the MYSQL communication stack becomes the default<a class="anchor-link" id="group-replication-in-mysql-26-7-the-mysql-communication-stack-becomes-the-default"></a></h2>
<p>MySQL 26.7 changes the default of <code>group_replication_communication_stack</code> from <code>XCOM</code> to <code>MYSQL</code>, deprecates the variable itself along with <code>group_replication_ip_allowlist</code>, and emits deprecation warnings on the XCom-specific options. The MYSQL stack has existed since 8.0.27; what changed is that it is now the path Oracle is committing to, and new clusters bootstrapped on 26.7 will use it unless told otherwise.</p>
<p>Under the MYSQL stack, group communication rides the server&rsquo;s own listener. <code>group_replication_local_address</code> must be one of the IP:port pairs the server is already bound to, the allowlist is ignored, and access control becomes ordinary MySQL authentication: the replication user needs <code>GROUP_REPLICATION_STREAM</code> and <code>CONNECTION_ADMIN</code> on top of <code>REPLICATION SLAVE</code> and <code>BACKUP_ADMIN</code>. TLS for group traffic is taken from the distributed-recovery settings, and <code>require_secure_transport</code> must agree with <code>group_replication_ssl_mode</code> on every member. Network namespaces work, which they never did under XCom.</p>
<figure><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/09/mysql-26-7-group-replication-mysql-vs-xcom-stack.png" alt="MySQL 26.7 Group Replication XCOM communication stack versus the default MYSQL communication stack with ports and privileges" width="960" height="430" class="size-full" loading="lazy"></figure>
<p>The operational point is that this is a topology change, not a parameter change. A cluster cannot run mixed stacks; members on the wrong one simply fail to join, without a helpful error. Moving an existing XCom cluster to the MYSQL stack requires stopping Group Replication on every member and re-bootstrapping, so it is a planned outage or a ClusterSet failover, not something to attempt with <code>SET PERSIST</code> on a live primary. The procedure below is the one we would put in a runbook, with its gates:</p>
<pre><code>-- Phase 0: pre-flight on EVERY member (MySQL 26.7.0). Record the output.
SELECT @@group_replication_communication_stack,
       @@group_replication_local_address,
       @@bind_address,
       @@require_secure_transport,
       @@group_replication_ssl_mode;

SELECT member_host, member_port, member_state, member_role, member_version
  FROM performance_schema.replication_group_members;

-- Phase 1: grant the MYSQL-stack privileges (all members, binlog off).
SET SQL_LOG_BIN = 0;
GRANT GROUP_REPLICATION_STREAM ON *.* TO '${GR_USER}'@'%';
GRANT CONNECTION_ADMIN         ON *.* TO '${GR_USER}'@'%';
SET SQL_LOG_BIN = 1;

-- Phase 2: stop the group, secondaries first, primary last.
STOP GROUP_REPLICATION;

-- Phase 3: switch stack and re-point the local address to a bound port.
SET PERSIST group_replication_communication_stack = 'MYSQL';
SET PERSIST group_replication_local_address       = '10.0.1.11:3306';
SET PERSIST group_replication_group_seeds         =
    '10.0.1.11:3306,10.0.1.12:3306,10.0.1.13:3306';

-- Phase 4: bootstrap ONCE, on the former primary only. CONFIRMATION GATE:
-- a second bootstrap creates a split brain. Verify no member is ONLINE first.
SET GLOBAL group_replication_bootstrap_group = ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group = OFF;

-- Phase 5: join the remaining members, then validate.
START GROUP_REPLICATION;
SELECT member_host, member_state, member_role
  FROM performance_schema.replication_group_members;</code></pre>
<p>Firewall rules, load-balancer health checks and MySQL Router bootstrap configuration all encode the port model, so a stack migration on MySQL 26.7 touches more than the database hosts. If you are bootstrapping a new cluster, let the new default stand and skip the migration entirely. If you run an existing XCom cluster and are not moving to 26.7 this quarter, there is no urgency: the deprecation is a warning in 26.7, and 9.7 LTS keeps the XCom default. Plan the switch into the next LTS upgrade rather than as a standalone change.</p>
<p>Two smaller HA items in the same release: the Clone plugin now understands calendar versions and permits cloning from an LTS to the next LTS (but not backwards, and not across non-sequential LTS lines), and the 9.7 LTS transfer already put the Group Replication flow-control statistics, resource manager and primary-election components into Community. For a deeper treatment of write throughput on InnoDB Cluster, see <a href="https://minervadb.com/innodb-cluster-write-performance/">our InnoDB Cluster write-performance analysis</a>.</p>
<h2>Optimizer and InnoDB features MySQL 26.7 inherits from 9.7 LTS<a class="anchor-link" id="optimizer-and-innodb-features-mysql-26-7-inherits-from-9-7-lts"></a></h2>
<p>Everything in 9.7.0 is in MySQL 26.7, and a few of those features change day-to-day performance work more than anything new to 26.7.</p>
<h3>Hypergraph optimizer, now free, still off by default<a class="anchor-link" id="hypergraph-optimizer-now-free-still-off-by-default"></a></h3>
<p>The hypergraph join optimizer moved from Enterprise to Community in 9.7.0. It is disabled by default and enabled with the <code>optimizer_switch</code> flag, either globally or per statement:</p>
<pre><code>-- Per-statement trial: no global blast radius.
SELECT /*+ SET_VAR(optimizer_switch = 'hypergraph_optimizer=on') */
       o.customer_id,
       SUM(oi.quantity * oi.unit_price) AS revenue
  FROM orders      AS o
  JOIN order_items AS oi ON oi.order_id = o.order_id
  JOIN products    AS p  ON p.product_id = oi.product_id
 WHERE o.created_at &gt;= '2026-08-01'
 GROUP BY o.customer_id
 ORDER BY revenue DESC
 LIMIT 50;

-- Compare plans. Hypergraph only emits EXPLAIN FORMAT=TREE.
EXPLAIN FORMAT=TREE
SELECT /*+ SET_VAR(optimizer_switch = 'hypergraph_optimizer=on') */ ...;</code></pre>
<p>The adoption blocker is the EXPLAIN restriction: hypergraph does not produce <code>TRADITIONAL</code> or <code>JSON</code> output, only <code>TREE</code>. Any plan-diffing tooling built around JSON EXPLAIN stops working the moment you enable it. Note also that MySQL 9.5 already changed the default <code>explain_format</code> to <code>TREE</code> and <code>explain_json_format_version</code> to 2, so scripts that parsed the old tabular output broke one release earlier. Both are dynamic and can be reverted with <code>SET GLOBAL explain_format = 'TRADITIONAL'</code>. Measure before and after with the digest statistics rather than with anecdotes:</p>
<pre><code>SELECT digest,
       LEFT(digest_text, 80)                          AS stmt,
       count_star                                     AS execs,
       ROUND(sum_timer_wait / count_star / 1e9, 3)    AS avg_ms,
       ROUND(quantile_99 / 1e9, 3)                    AS p99_ms,
       sum_rows_examined / count_star                 AS rows_examined_avg
  FROM performance_schema.events_statements_summary_by_digest
 WHERE schema_name = 'app'
 ORDER BY sum_timer_wait DESC
 LIMIT 20;</code></pre>
<h3>Foreign keys moved to the SQL layer (9.6)<a class="anchor-link" id="foreign-keys-moved-to-the-sql-layer-9-6"></a></h3>
<p>MySQL 9.6 moved foreign-key enforcement and cascade handling out of InnoDB and into the SQL layer, and MySQL 26.7 inherits that. The stated reason is completeness of the binary log: cascaded deletes and updates now appear as row events. That is good news for CDC pipelines (Debezium and friends finally see cascades) and a surprise for capacity planning, because binlog volume and replication row traffic go up on schemas with deep cascade chains. <code>innodb_native_foreign_keys</code> reverts to the old behaviour. Track the delta with <code>SHOW BINARY LOGS</code> growth per hour before and after the upgrade.</p>
<h3>Defaults that changed underneath you<a class="anchor-link" id="defaults-that-changed-underneath-you"></a></h3>
<p>Several InnoDB and replication defaults shifted between 9.3 and 9.5, and every one of them is live on a fresh MySQL 26.7 install. The ones that alter observable behaviour:</p>
<div>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Old default</th>
<th>New default</th>
<th>Since</th>
<th>Restart?</th>
<th>Why you care</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>innodb_change_buffer_max_size</code></td>
<td>25</td>
<td>5 (%)</td>
<td>9.4</td>
<td>No</td>
<td>Less buffer pool spent on change buffering; secondary-index-heavy inserts may regress on slow storage</td>
</tr>
<tr>
<td><code>innodb_change_buffering</code></td>
<td>none</td>
<td>all</td>
<td>9.5</td>
<td>No</td>
<td>Reverses the 8.4 default; check <code>Innodb_ibuf_*</code> status counters</td>
</tr>
<tr>
<td><code>back_log</code></td>
<td>151</td>
<td>10000</td>
<td>9.4</td>
<td>Yes</td>
<td>Connection storms queue instead of failing; pairs well with the thread pool</td>
</tr>
<tr>
<td><code>binlog_transaction_dependency_history_size</code></td>
<td>25,000</td>
<td>1,000,000</td>
<td>9.5</td>
<td>No</td>
<td>Better writeset parallelism for MTA and CSA; more memory on the source</td>
</tr>
<tr>
<td><code>caching_sha2_password_digest_rounds</code></td>
<td>5,000</td>
<td>10,000</td>
<td>9.5</td>
<td>No</td>
<td>Doubles CPU per new authentication; matters on high connection churn</td>
</tr>
<tr>
<td><code>gtid_mode</code> / <code>enforce_gtid_consistency</code></td>
<td>OFF</td>
<td>ON</td>
<td>9.5</td>
<td>Yes</td>
<td>GTID-inconsistent statements start failing; required by the CSA anyway</td>
</tr>
<tr>
<td><code>SOURCE_SSL</code>, <code>group_replication_ssl_mode</code></td>
<td>0 / DISABLED</td>
<td>1 / REQUIRED</td>
<td>9.5</td>
<td>Channel restart</td>
<td>Plaintext replication breaks on upgrade</td>
</tr>
<tr>
<td><code>thread_pool_max_unused_threads</code></td>
<td>2</td>
<td>32</td>
<td>26.7 / 9.7.2</td>
<td>No</td>
<td>Fewer thread create/destroy cycles under bursty load</td>
</tr>
</tbody>
</table>
</div>
<h3>Container-aware sizing<a class="anchor-link" id="container-aware-sizing"></a></h3>
<p>Since 9.3 the server reads cgroup limits rather than host totals when auto-sizing <code>innodb_buffer_pool_size</code>, <code>innodb_buffer_pool_instances</code>, <code>innodb_page_cleaners</code>, <code>innodb_purge_threads</code>, the read and parallel-read thread counts and <code>temptable_max_ram</code>. MySQL 26.7 adds the 9.7 refinement for <code>cpuset-cpus</code>. If you run MySQL on Kubernetes, this is the release where a pod with a 16 GiB limit stops trying to allocate a buffer pool sized for the 256 GiB node underneath it. Confirm what the server actually chose after startup with <code>SELECT @@innodb_buffer_pool_size, @@innodb_buffer_pool_instances, @@server_memory;</code> and compare against the pod spec.</p>
<p>The 9.6 undo-truncation work, which in 26.7 persists progress in the tablespace header instead of separate log files, is the other InnoDB item worth noting: long undo truncations now survive a crash cleanly.</p>
<h2>Post-quantum TLS in MySQL 26.7<a class="anchor-link" id="post-quantum-tls-in-mysql-26-7"></a></h2>
<p>When built against OpenSSL 3.5 or later, MySQL 26.7 negotiates post-quantum key-exchange groups on TLS 1.3 connections. Fourteen new variables control it across five channels: client, admin, asynchronous replication, Group Replication and X Plugin. Each channel has a <code>*_tls_kex</code> list, a <code>*_force_pqc</code> boolean and a <code>*_use_pqc_sign</code> boolean, all defaulting to OFF, so nothing changes until you opt in. The negotiated result is visible per session in <code>Tls_key_exchange_algorithm</code> and <code>Tls_sign_algorithm</code>.</p>
<pre><code>-- Opt replication traffic into a hybrid PQC group first; clients later.
SET PERSIST replication_tls_kex   = 'X25519MLKEM768:X25519';
SET PERSIST replication_force_pqc = ON;

-- Verify on an established replication session.
SHOW SESSION STATUS LIKE 'Tls_key_exchange_algorithm';</code></pre>
<p>The performance angle is real but small: ML-KEM handshakes carry a larger key share, so the cost lands on connection establishment, not on the data path. For a pooled application it is noise. For a workload that opens thousands of short-lived TLS connections per second it is measurable, and it stacks with the doubled <code>caching_sha2_password_digest_rounds</code>. Watch <code>Connections</code> per second against CPU before turning on <code>force_pqc</code> for the client channel. The build dependency matters too: distro packages built against OpenSSL 3.0 silently lack the capability, so check <code>SHOW VARIABLES LIKE 'tls_version'</code> alongside <code>@@version_compile_os</code> and the linked OpenSSL version reported at startup.</p>
<h2>What is still Enterprise-only in MySQL 26.7 and 9.7<a class="anchor-link" id="what-is-still-enterprise-only-in-mysql-26-7-and-9-7"></a></h2>
<p>After the 9.7.0 transfer (hypergraph, telemetry, the three Group Replication components, replication applier metrics, JSON duality-view DML) and the 26.7 thread-pool transfer, the Enterprise value proposition has moved from performance and HA to security, backup and AI. That is worth stating plainly because it changes the renewal conversation. This is the remaining list as of the July 2026 releases:</p>
<div>
<table>
<thead>
<tr>
<th>Enterprise capability</th>
<th>State in 9.7.2 / 26.7.0</th>
<th>Community route</th>
</tr>
</thead>
<tbody>
<tr>
<td>MySQL Enterprise Backup</td>
<td>Hot InnoDB backup, incremental, encryption, PITR</td>
<td>Percona XtraBackup; GA for 8.0 and 8.4, still release-candidate for 9.7 as of August 2026. A 9.7 or 26.7 estate has no GA physical backup outside MEB today</td>
</tr>
<tr>
<td>Dynamic Data Masking</td>
<td>New in 9.7: <code>CREATE MASKING POLICY</code>, server-side enforcement, role-based unmasking</td>
<td>None in-server; views or proxy rewrites. If masking is a compliance mandate, EE is defensible</td>
</tr>
<tr>
<td>Enterprise Audit</td>
<td>Modular components since 9.6; 26.7 adds <code>audit_log.file_count</code></td>
<td>Percona Server audit component (JSONL default); filter syntax differs, so migration is rule-rewriting work</td>
</tr>
<tr>
<td>Enterprise Firewall</td>
<td>Component (plugin deprecated 9.4)</td>
<td>ProxySQL query rules at the proxy tier</td>
</tr>
<tr>
<td>Keyring for KMIP, OCI Vault, AWS, HashiCorp</td>
<td>Components; all keyring plugins deprecated since 9.4</td>
<td><code>component_keyring_file</code> is Community; Percona ships open KMIP and Vault components. InnoDB encryption itself was never Enterprise-only</td>
</tr>
<tr>
<td>Enterprise Authentication</td>
<td>LDAP, PAM, Kerberos, WebAuthn</td>
<td>Percona <code>auth_pam</code> for PAM and LDAP-via-PAM; no Community Kerberos</td>
</tr>
<tr>
<td>Thread Pool</td>
<td>EE on 9.7 LTS; <strong>Community on 26.7</strong></td>
<td>Percona Server thread pool on any version</td>
</tr>
<tr>
<td>MySQL AI</td>
<td>On-premises EE option: in-database vector store and search, LLMs, AutoML. Verify GA and pricing with Oracle before planning on it</td>
<td>Community can store <code>VECTOR</code> columns but has no vector index or distance function; MariaDB 11.8+ or pgvector if you need on-prem similarity search without EE</td>
</tr>
<tr>
<td><code>mysqldm</code> diagnostic monitor</td>
<td>EE since 9.5</td>
<td>pt-stalk, PMM</td>
</tr>
</tbody>
</table>
</div>
<p>MySQL Enterprise Monitor is absent from that table because it reached end of life on 1 January 2025. Estates still running it should be on a Percona Monitoring and Management migration path regardless of edition. The renewal audit we run before offering any opinion is short: <code>SHOW PLUGINS</code>, the <code>information_schema.plugins</code> rows with a non-null library, the <code>audit%</code> and <code>keyring%</code> variables, and whether MEB appears in the backup jobs. In most Enterprise estates the answer is MEB plus Audit, and both have substitutes; on 26.7 specifically, the thread pool is no longer a reason to renew.</p>
<h2>Upgrade checklist before the first MySQL 26.7 host<a class="anchor-link" id="upgrade-checklist-before-the-first-mysql-26-7-host"></a></h2>
<p>Whether the target is 9.7 LTS or MySQL 26.7, the pre-flight is the same, because the landmines arrived in 9.3, 9.5 and 9.6 and 26.7 inherits all of them. Run these on every source and replica and keep the output with the change record:</p>
<pre><code>-- 1. Replication defaults that flip on upgrade (9.5+).
SELECT @@gtid_mode,
       @@enforce_gtid_consistency,
       @@binlog_format;

SELECT channel_name,
       ssl_allowed,
       ssl_verify_server_cert
  FROM performance_schema.replication_connection_configuration;

-- 2. Variables removed in 9.3 that abort startup if left in my.cnf.
SELECT variable_name
  FROM performance_schema.global_variables
 WHERE variable_name IN ('innodb_undo_tablespaces',
                         'innodb_log_file_size',
                         'innodb_log_files_in_group',
                         'replica_parallel_type');

-- 3. Authentication plugins that no longer load by default (8.4+).
SELECT user, host, plugin
  FROM mysql.user
 WHERE plugin IN ('mysql_native_password', 'sha256_password');

-- 4. Functions moved out of core in 9.6 (MD5, SHA1 -&gt; classic_hashing component).
SELECT table_schema, table_name, column_name, generation_expression
  FROM information_schema.columns
 WHERE generation_expression REGEXP 'MD5\(|SHA1\(';</code></pre>
<pre><code># 5. Oracle's upgrade checker (MySQL Shell). 26.7 adds live progress reporting.
mysqlsh -- util check-for-server-upgrade 
    ${MYSQL_USER}@${MYSQL_HOST}:3306 
    --target-version=26.7.0 
    --output-format=JSON 
    --config-path=/etc/my.cnf &gt; /var/tmp/upgrade-check-$(hostname).json</code></pre>
<p>Then capture the baseline you will compare against after the upgrade: the top-50 digests by total wait from <code>events_statements_summary_by_digest</code>, replication lag distribution over a representative week, binlog growth per hour, and connection and CPU counters. Without the baseline, the post-upgrade argument about whether the hypergraph optimizer or the change-buffer default helped or hurt is opinion. Test on a clone with production-shaped load, keep a downgrade path inside the LTS series, and treat the Innovation track as something you can roll forward from but not back.</p>
<h2>Where MySQL 26.7 fits in a production estate<a class="anchor-link" id="where-mysql-26-7-fits-in-a-production-estate"></a></h2>
<p>MySQL 26.7 is the most substantial Innovation release since the track was introduced, and for once the interesting parts are not previews. The Community thread pool solves a concurrency problem that used to cost an Enterprise subscription or a switch to Percona Server. The Change Stream Applier is a genuinely new replication architecture, and any estate that has fought replica lag with <code>replica_parallel_workers</code> tuning should benchmark it. The MYSQL communication stack default and post-quantum TLS are both changes you can defer, but not ignore.</p>
<p>The deployment answer for most fleets is still 9.7 LTS for anything that has to stay put, with MySQL 26.7 on replicas and on quarterly-refreshed tiers where the CSA and the thread pool pay for the operational cadence. Whichever way you go, the version ladder is fixed: 8.0 to 8.4 to 9.7, adjacent only, and 8.0 is already out of patches.</p>
<p>If you want a second pair of eyes on an upgrade plan, a Change Stream Applier benchmark or a Group Replication stack migration, our <a href="https://minervadb.com/mysql-consulting/">MySQL consulting</a> and <a href="https://minervadb.com/expert-mysql-upgrades-and-migration-services/">MySQL upgrade and migration</a> teams do this work every week across Community, Enterprise and Percona builds. As always: test every change here on a non-production copy first, and keep the DR posture verified before the upgrade window, not after.</p>
<h3>References<a class="anchor-link" id="references"></a></h3>
<p><a href="https://dev.mysql.com/doc/relnotes/mysql/26.7/en/news-26-7-0.html" target="_blank" rel="noopener">MySQL 26.7.0 release notes</a> &middot; <a href="https://dev.mysql.com/doc/relnotes/mysql/9.7/en/news-9-7-2.html" target="_blank" rel="noopener">MySQL 9.7.2 release notes</a> &middot; <a href="https://blogs.oracle.com/mysql/mysql-july-2026-ga-releases-now-available" target="_blank" rel="noopener">Oracle: MySQL July 2026 GA releases</a> &middot; <a href="https://blogs.oracle.com/mysql/introducing-the-change-stream-applier-csa-a-new-mysql-replication-applier-in-labs" target="_blank" rel="noopener">Oracle: Introducing the Change Stream Applier</a> &middot; <a href="https://dev.mysql.com/doc/refman/26.7/en/thread-pool-operation.html" target="_blank" rel="noopener">Thread pool operation, MySQL 26.7 reference manual</a> &middot; <a href="https://dev.mysql.com/doc/refman/26.7/en/group-replication-connection-security.html" target="_blank" rel="noopener">Group Replication communication stacks</a> &middot; <a href="https://www.mysql.com/support/eol-notice.html" target="_blank" rel="noopener">MySQL end-of-life notice</a> &middot; <a href="https://ronaldbradford.com/blog/2026-07-23-a-first-look-at-mysql-26-7-early-access/" target="_blank" rel="noopener">Ronald Bradford: a first look at MySQL 26.7 Early Access</a></p>

<p><a href="https://minervadb.com/mysql-26-7-performance-scalability-high-availability/">MySQL 26.7 and 9.7 LTS: 7 Powerful Features for Performance, Scalability and High Availability</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB R2DBC Connector 1.4.2 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-2-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-2-now-available/</id>
      <updated>2026-09-02T18:03:33+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/R2DBC 1.4.2 GA release. Download Now Release Notes MariaDB […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-2-now-available/">MariaDB R2DBC Connector 1.4.2 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/R2DBC 1.4.2 GA release. Download Now MariaDB Connector/R2DBC 1.4.2 is a Stable (GA) release. Notable items in this release include: See the Connector/R2DBC 1.4.2 release notes page for details and visit mariadb.com/downloads/connectors/connectors-data-access/r2dbc-connector/</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-2-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-2-now-available/">MariaDB R2DBC Connector 1.4.2 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup/" />
      <id>https://www.percona.com/blog/mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup/</id>
      <updated>2026-09-02T11:38:40+03:00</updated>
      <author><name>Zelmar Michelini</name></author>
      <summary type="html"><![CDATA[<p>Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup One day, my friend Martín told me about a problem he and his team were dealing with.  Every time they needed to run a pre-production test, they had to restore a copy of the production database into their test cluster. That process alone takes about … Continued<br />
The post Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup/">Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1><b>Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup</b><a class="anchor-link" id="mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup"></a></h1>
<p><span>One day, my friend Mart&iacute;n told me about a problem he and his team were dealing with.&nbsp;</span></p>
<p><span>Every time they needed to run a pre-production test, they had to restore a copy of the production database into their test cluster. That process alone takes about three hours because of the size of the database.</span></p>
<p><span>If something went wrong during the test and they needed to run it again, they had no choice but to sit through the entire restore cycle again.</span></p>
<p><span>Three hours. Twice (or more) just to run the pre-production test.</span></p>
<p><span>That&rsquo;s what motivated me to build</span><a href="https://github.com/zelmario/mongorewind"> <span>mongorewind</span></a><span>, a terminal UI tool that watches your MongoDB cluster for changes and lets you instantly undo all of them, inserts, updates, replaces, and deletes. In reverse order, without touching your backup.</span></p>
<h2><b>How It Works</b><a class="anchor-link" id="how-it-works"></a></h2>
<p><b>mongorewind</b><span> opens a cluster-wide </span><a href="https://www.mongodb.com/docs/manual/changestreams/"><span>change stream</span></a><span> and records every data-modifying operation to a local log file. When you press </span><b>R</b><span> to rewind, it applies the inverse of each recorded operation in reverse chronological order:</span></p>
<table>
<tbody>
<tr>
<td><b>Recorded operation</b></td>
<td><b>Rewind action</b></td>
</tr>
<tr>
<td><span>insert</span></td>
<td><span>deleteOne</span></td>
</tr>
<tr>
<td><span>update</span><span> / </span><span>replace</span></td>
<td><span>replaceOne</span><span> with pre-image (upsert)</span></td>
</tr>
<tr>
<td><span>delete</span></td>
<td><span>replaceOne</span><span> with pre-image (upsert)</span></td>
</tr>
</tbody>
</table>
<p><span>To undo updates and deletes correctly, mongorewind needs to know what the document looked like </span><i><span>before</span></i><span> the change.&nbsp;</span></p>
<p><span>It captures this automatically using MongoDB&rsquo;s </span><a href="https://www.mongodb.com/docs/manual/reference/method/db.collection.watch/#change-streams-with-document-pre--and-post-images"><span>changeStreamPreAndPostImages</span></a><span> feature, which it enables on every collection it finds &mdash; and on any new collection the moment it is created.</span></p>
<p>&nbsp;</p>
<h2><b>Requirements</b><a class="anchor-link" id="requirements"></a></h2>
<ul>
<li><b>Go 1.24+ and MongoDB 6.0+</b><span> running as a replica set or sharded cluster (change streams are not available on standalone instances, can be a 1 node replica set)</span></li>
</ul>
<h2><b>Installation</b><a class="anchor-link" id="installation"></a></h2>

<pre class="urvanov-syntax-highlighter-plain-tag">git clone https://github.com/zelmario/mongorewind.git
cd mongorewind
go build -o mongorewind .</pre>
<p>&nbsp;</p>
<p><span>Or install directly:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">go install github.com/zelmario/mongorewind@latest</pre>
<p>&nbsp;</p>
<h2><b>Running It</b><a class="anchor-link" id="running-it"></a></h2>
<p><span>Start mongorewind pointing at your cluster before running any tests:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mongorewind --uri "mongodb://host1:27017,host2:27017,host3:27017/?replicaSet=rs0"</pre>
<p><span>While mongorewind is running, you will see a terminal dashboard showing the operations it has recorded:</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-52694" src="https://www.percona.com/wp-content/uploads/2026/08/mongorewind1.png" alt="" width="817" height="368"></p>
<p><span>The status indicator shows </span><span>&#9679; watching</span><span> (green) while the change stream is active. When you press </span><b>R</b><span>, it switches to</span> <span>&#9675;idle</span><span> (yellow) while the rewind is in progress.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-52695" src="https://www.percona.com/wp-content/uploads/2026/08/mongorewind2.png" alt="" width="809" height="370"></p>
<p>&nbsp;</p>
<p><span>Once the test run is complete and something went wrong, you can press </span><b>R</b><span> and mongorewind undoes every change it recorded &mdash; bringing the data back to exactly the state it was in before the test started. No restore, no waiting.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-52696" src="https://www.percona.com/wp-content/uploads/2026/08/mongorewind3.png" alt="" width="778" height="221"></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2><b>Using It in CI Pipelines</b><a class="anchor-link" id="using-it-in-ci-pipelines"></a></h2>
<p><span>If you are running automated tests in a CI environment, mongorewind also supports a non-interactive mode. You can start the watcher in a terminal or the background and trigger rewinds from your scripts:</span></p>
<p><span>bash</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag"># Start the watcher in the background

mongorewind --uri "mongodb://..." &amp;

# Run your test suite

run_tests




# Rewind all changes and run again

mongorewind --rewind

run_tests</pre>
<p>&nbsp;</p>
<p><span>mongorewind &ndash;rewind</span><span> exits with code </span><span>0</span><span> on success and </span><span>1</span><span> on error, so it integrates naturally into any CI pipeline.</span></p>
<p><span>If you use a custom log path, pass the same </span><span>&ndash;log</span><span> value to both commands so they share the same socket:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mongorewind --log /tmp/mytest.log --uri "mongodb://..." &amp;

mongorewind --log /tmp/mytest.log --rewind</pre>
<p>&nbsp;</p>
<h2><b>A Few Things to Keep in Mind</b><a class="anchor-link" id="a-few-things-to-keep-in-mind"></a></h2>
<p><b>Replica set required.</b> <a href="https://www.mongodb.com/docs/manual/changeStreams/"><span>Change streams</span></a><span> need a replica set or sharded cluster. If you are working locally, you can start a single-node replica set with:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mongod --replSet rs0</pre>
<p>&nbsp;</p>
<p><span>And then run </span><span>rs.initiate()</span><span> in the mongo shell.</span></p>
<p><b>The log file is scoped to a session.</b><span> The file is truncated on startup and after a successful rewind, so each test session starts clean. System databases (</span><span>admin</span><span>, </span><span>local</span><span>, </span><span>config</span><span>) and </span><span>system.*</span><span> collections are ignored automatically.</span></p>
<p><b>Pre-images are enabled automatically.</b><span> mongorewind polls every 2 seconds to catch newly created collections and enables </span><span>changeStreamPreAndPostImages</span><span> on them. You don&rsquo;t need to configure anything manually.</span></p>
<h2><b>Going Back to Mart&iacute;n&rsquo;s Problem</b><a class="anchor-link" id="going-back-to-martins-problem"></a></h2>
<p><span>With </span><b>mongorewind</b><span>, Mart&iacute;n&rsquo;s team does the restoration once. After that, every time a test fails and they need to start over, they just run </span><span>mongorewind &ndash;rewind</span><span>. The database goes back to its original state in seconds, and the test runs again. Three hours become just a few seconds.</span></p>
<p>&nbsp;</p>
<p>Contributions are welcome! Since I&rsquo;m not a developer, your feedback is valuable. If you are a developer and notice any mistakes or want to enhance the script, please feel free to contribute!</p>
<p>The post <a href="https://www.percona.com/blog/mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup/">Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/mongorewind-rewind-your-mongodb-test-data-without-restoring-a-backup/">Mongorewind: Rewind Your MongoDB Test Data Without Restoring a Backup</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>On how MySQL implements JSON ARRAY indexing</title>
      <link rel="alternate" type="text/html" href="https://petrunia.net/2026/09/02/on-how-mysql-implements-json-array-indexing/" />
      <id>https://petrunia.net/2026/09/02/on-how-mysql-implements-json-array-indexing/</id>
      <updated>2026-09-02T11:04:07+03:00</updated>
      <author><name>spetrunia2</name></author>
      <summary type="html"><![CDATA[<p>Me and Yuchen Pei have been studying how JSON ARRAY indexing is done in MySQL. The details are posted here: MDEV-40822. Yuchen has discovered cases where using ARRAY index for reads causes different query results from the same query not using the index. Looks like a bug (or maybe two). Sometimes one has to choose […]</p>
<p><a href="https://petrunia.net/2026/09/02/on-how-mysql-implements-json-array-indexing/">On how MySQL implements JSON ARRAY indexing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">Me and <a href="https://github.com/mariadb-YuchenPei">Yuchen Pei</a> have been studying how JSON ARRAY indexing is done in MySQL. The details are posted here: <a href="https://jira.mariadb.org/browse/MDEV-40822">MDEV-40822</a>. </p>
<p class="wp-block-paragraph">Yuchen has discovered cases where using ARRAY index for reads <a href="https://jira.mariadb.org/browse/MDEV-40822#5.2Typeerasure%3Afalsepositivesandfalsenegatives">causes different query results</a> from the same query not using the index. Looks like a bug (or maybe two). Sometimes one has to choose between being compatible with MySQL and producing correct query results.</p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://petrunia.net/2026/09/02/on-how-mysql-implements-json-array-indexing/">On how MySQL implements JSON ARRAY indexing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>OpenID Connect Authentication for MySQL, Now Fully Open Source</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/" />
      <id>https://www.percona.com/blog/oidc-authentication-for-percona-mysql/</id>
      <updated>2026-09-02T08:56:26+03:00</updated>
      <author><name>Michał Jankowski</name></author>
      <summary type="html"><![CDATA[<p>Percona Server for MySQL now ships with a fully open source OpenID Connect (OIDC) authentication plugin, available starting with Percona Server for MySQL 8.4.11-11 and 9.7.2-2 (not yet released as of this writing). It allows a MySQL account to authenticate against any standards-compliant Identity Provider (IdP) instead of relying on a locally stored password, closing … Continued<br />
The post OpenID Connect Authentication for MySQL, Now Fully Open Source appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/">OpenID Connect Authentication for MySQL, Now Fully Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Percona Server for MySQL now ships with a fully open source OpenID Connect (OIDC) authentication plugin, available starting with <strong>Percona Server for MySQL 8.4.11-11</strong> and <strong>9.7.2-2</strong> (not yet released as of this writing). It allows a MySQL account to authenticate against any standards-compliant Identity Provider (IdP) instead of relying on a locally stored password, closing the gap with MySQL Enterprise Edition, which has offered OIDC authentication since MySQL 9.1 and, in several respects, going beyond it.</p>
<p>Oracle offers the same category of functionality, but its server-side plugin is part of the paid MySQL Enterprise Edition. Percona&rsquo;s implementation is open source and adds three capabilities the Enterprise plugin does not provide: automatic signing-key synchronization from a JWKS endpoint, IdP group-to-role mapping, and proxy-user support. This article explains how the plugin works and why those differences matter in practice.</p>
<h2>What OpenID Connect Brings to MySQL Authentication<a class="anchor-link" id="what-openid-connect-brings-to-mysql-authentication"></a></h2>
<p>OpenID Connect is an identity layer built on top of the OAuth 2.0 authorization framework <a href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/#oidc">[5]</a>. Whereas OAuth 2.0 governs delegated access to resources, OIDC adds a standardized way for a client to establish who a user is. After a user signs in to an Identity Provider, the IdP issues a signed JSON Web Token (JWT), called an ID token, that carries the user&rsquo;s identity and attributes in a verifiable, tamper-evident form.</p>
<p>Using that model for MySQL authentication brings several practical advantages over password-based accounts:</p>
<ul>
<li><strong>Alignment with single sign-on.</strong> Users authenticate once with their IdP and can reuse that session context across OIDC-aware applications, including databases. User lifecycle and password management remain centralized.</li>
<li><strong>No long-lived secrets on the wire.</strong> ID tokens are short-lived and cryptographically signed, so there is no static password to steal, rotate, or accidentally commit to a configuration file.</li>
<li><strong>Support for hybrid deployments.</strong> Organizations that run MySQL on-premises while hosting applications in the cloud can still authenticate through the same identity plane on both sides.</li>
<li><strong>Broad interoperability.</strong> Because OpenID Connect is a widely adopted standard, the plugin can work with any compliant provider, including Keycloak, Okta, Microsoft Entra ID, and Google Identity.</li>
</ul>
<p>None of that is unique to Percona; Oracle makes a similar value proposition for the Enterprise plugin. The real difference lies in how much operational burden the plugin removes from the administrator, which becomes clear in the next sections.</p>
<h2>How OpenID Connect Authentication Works<a class="anchor-link" id="how-openid-connect-authentication-works"></a></h2>
<p><img decoding="async" class="alignnone size-medium wp-image-52740" src="https://www.percona.com/wp-content/uploads/2026/09/percona_oidc_flow-300x146.png" alt="" width="100%"></p>
<p>Once the plugin and its configuration are in place, the authentication path is the same regardless of which IdP issued the token:</p>
<ol>
<li><strong>The user authenticates to the IdP</strong> and receives a signed ID token.</li>
<li><strong>The token is written to a local file</strong> that only the client operating system account can read.</li>
<li><strong>The MySQL client</strong> uses an option that causes the client-side OIDC plugin to load and read the token from the file. <strong>The token is sent to the server</strong> as part of the authentication handshake.</li>
<li><strong>The server validates the secure channel</strong> and decodes the token. It then <strong>verifies the token signature</strong> using the selected IdP&rsquo;s public key, <strong>checks the expiration time</strong>, and <strong>validates the configured claims</strong>.</li>
<li><strong>The server resolves the final identity</strong> as either a personal account or a group-based proxy target. The plugin may also return roles mapped from the user&rsquo;s group membership.</li>
</ol>
<h2>Configuring Trusted Providers and Letting the Plugin Manage the Keys<a class="anchor-link" id="configuring-trusted-providers-and-letting-the-plugin-manage-the-keys"></a></h2>
<p>Identity Providers rotate their signing keys periodically as a basic security measure. If a key is ever compromised, limiting its lifetime reduces the potential impact, and regular rotation also lowers the long-term value of any one key as a target. In practice, rotation is gradual: a new key is published and accepted before it starts signing tokens, and an old key remains valid for a period after it stops signing so that tokens already in flight can still be verified.</p>
<p>Public keys are exposed through the standard JWKS (JSON Web Key Set) endpoint, which applications can use to verify tokens issued by the IdP <a href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/#jwks">[6]</a>.</p>
<p>The Percona OpenID Connect authentication plugin can download public keys from a configured JWKS endpoint when the plugin is loaded, typically during installation and server startup, and store them in a cache. It also provides a User Defined Function (UDF) that can refresh the cache on demand or periodically through the Event Scheduler.</p>
<p>By contrast, Oracle&rsquo;s counterpart plugin requires signing keys to be configured statically through the <strong>authentication_openid_connect_configuration</strong> server variable, supplied either as an inline JSON string or as a path to a JSON file. There is no retrieval or refresh from the JWKS endpoint, so keeping keys current after each rotation remains a manual task for the administrator. In the window just after a rotation, tokens signed with the previous key are still valid but cannot be verified until the configuration is updated. Percona&rsquo;s plugin supports static key configuration as well, but that mode is better suited to testing or temporary setups than to production.</p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<p>Using the feature requires two simple steps. First, JWKS endpoint URL must be set in the plugin&rsquo;s configuration. For example, the below configuration defines IdP named as example-keycloak (pay attention to jwks-url element):</p>
<pre class="urvanov-syntax-highlighter-plain-tag">{
  "example-keycloak": {
  "issuer-name": "https://keycloak.example.com/realms/master",
  "jwks-url": "https://keycloak.example.com/realms/master/protocol/openid-connect/certs",
  "audiences": [ "mysql-oidc" ]
  }
}</pre>
<p>The second step is ensuring the MySQL event scheduler is running and creating an event updating the keys. For example, to enable updating the keys for example-keycloak every hour run from MySQL client:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">CREATE EVENT update_oidc_keys
  ON SCHEDULE EVERY 1 HOUR
  DO SELECT update_jwks("example-keycloak");</pre>

<h2>Benefits of Using IdP Groups<a class="anchor-link" id="benefits-of-using-idp-groups"></a></h2>
<p>This is where Percona&rsquo;s plugin diverges most clearly from the Enterprise implementation.</p>
<p>Groups are managed by the corporate Identity Provider and group membership may be carried by ID tokens. OIDC does not define a standard claim for that, but most IdP implementations allow adding a group claim to the tokens. The Percona&rsquo;s plugin allows the administrator to configure the group claim name so that it matches the token format used by the chosen IdP.</p>
<p>There are two practical ways to take advantage of this feature:&nbsp; group-to-role mapping and proxy users.</p>
<h2>Group-to-Role Mapping<a class="anchor-link" id="group-to-role-mapping"></a></h2>
<p>Membership in a group can automatically translate into MySQL roles and therefore privileges across multiple MySQL servers at the same time. On a single server, the flow looks like this:</p>
<ol>
<li>The administrator <strong>creates roles and grants them privileges</strong>.</li>
<li>The administrator defines the <strong>IdP group-to-MySQL role mapping</strong> in the plugin configuration file.</li>
<li>When the user connects, the plugin returns the roles that match the user&rsquo;s groups, and the server <strong>automatically grants those roles to the user</strong>.</li>
<li>The user can activate any granted role and <strong>exercise the privileges assigned to it</strong>.</li>
</ol>
<p>Please note, that group-to-role mapping still requires an account created for each user, but automates managing user privileges.</p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<p>To create roles and grant them some privileges one may run:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">CREATE ROLE accounting;
GRANT ALL PRIVILEGES ON accounting_database.* TO accounting;
CREATE ROLE sales;
GRANT ALL PRIVILEGES ON sales_database.* TO sales;</pre>
<p>Then, to to define the mapping add to IDP configuration:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">"group-claim": "groups",
"group-role": [
  { "/accounting": "accounting" },
  { "/marketing": "marketing" }
]</pre>
<p>Any user connecting with an ID token containing claim <strong>&ldquo;groups&rdquo;:[&ldquo;/accounting&rdquo;]</strong> will be granted with role accounting and effectively obtain access to <strong>accounting_database</strong> and so on.</p>
<h2>Proxy Users<a class="anchor-link" id="proxy-users"></a></h2>
<p>The proxy capability in MySQL allows an authentication plugin to request that the connecting external user be logged in as a different MySQL user. In this model, the external identity is the <em>proxy user</em> and the mapped MySQL account is the <em>proxied user</em>. The purpose is to <strong>let multiple users share accounts with the same privilege set, avoiding the need to create a separate personal database account for every individual</strong>.</p>
<p>This feature must be supported by the authentication plugin, whose job is to choose the proxied user according to the specifics of the authentication method. In the Percona OIDC plugin, that selection is based on the group claim in the token and works as follows:</p>
<ol>
<li>The administrator <strong>creates a proxy user</strong> identified by the OIDC plugin. This can be either a single anonymous account (&rdquo;@&rdquo;) without a specific group name, referred to as anonymous proxying, or multiple group-related accounts, referred to as named group proxying.</li>
<li>The administrator <strong>creates proxied users for each group</strong>. These accounts should not use a login plugin, so nobody can connect to them directly. The username must match the group name.</li>
<li>The administrator <strong>grants the PROXY privilege for each proxy user</strong> on all related proxied users.</li>
<li>When a user connects, in the <strong>anonymous proxying</strong> case the plugin returns the user&rsquo;s <strong>first group as the proxied username</strong>. In the <strong>named group proxying</strong> case, the plugin <strong>checks whether the user belongs to the group and returns that group</strong> as the proxied username.</li>
<li>The server verifies that the requested proxied account exists and that the proxy user has the required PROXY privilege on it. If both checks succeed, <strong>the session runs with the proxied account&rsquo;s privileges</strong>.</li>
</ol>
<h2>Other Features<a class="anchor-link" id="other-features"></a></h2>
<p>Supported signing algorithms include RSASSA-PKCS1-v1_5, RSASSA-PSS, and ECDSA with SHA-256, SHA-384, and SHA-512 hashing functions.</p>
<p>The Percona approach uses the client-side OpenID Connect plugin from upstream MySQL, which ensures compatibility with the standard Oracle client.</p>
<p>Both client-side and server-side OpenID Connect plugins ensure that the token is sent via a secure channel. Accepted protocols are TCP protected by TLS, Unix sockets, and shared memory.</p>
<h2>What OpenID Connect Authentication Does Not Do<a class="anchor-link" id="what-openid-connect-authentication-does-not-do"></a></h2>
<p>There are some limits worth knowing.</p>
<p>The first comes from MySQL&rsquo;s authentication design: any authentication plugin is used at connection time only. In the case of OIDC, the token is validated when the user connects, and a session that stays open may outlive the ID token that opened it. There is no out-of-the-box mechanism to force re-authentication after some time (except for idle connection timeout).</p>
<p>A similar situation applies to group-role mapping. The roles tied to the user&rsquo;s groups in the ID token are granted or revoked at connection time. As a result, if a user is added to or removed from an IdP group, they must reconnect to Percona Server for the change to be reflected in their granted roles.</p>
<p>The proxying mechanism uses group membership claim instead of the token&rsquo;s subject, so any token signed by a configured IdP that carries the required group is accepted. Group membership is your trust boundary in those modes, so treat it that way.</p>
<p>The current proxying implementation assumes the proxied user&rsquo;s name matches the group name. This can be a problem when a group name isn&rsquo;t a valid MySQL username (for example, it&rsquo;s too long or contains disallowed characters), or when multiple groups need to map to a single account. We plan to add group-to-proxied-account mapping in future releases to address this.</p>
<p>The client-side plugin doesn&rsquo;t verify the ID token (for example, check whether it has expired) before connecting, and the server doesn&rsquo;t report the reason for access being denied (for security reasons). A good practice is to obtain a fresh token before connecting.</p>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>Functionally, Percona&rsquo;s OpenID Connect plugin covers the same core ground as the counterpart in MySQL Enterprise Edition: signed ID tokens, claim validation, subject matching, and secure-transport enforcement.</p>
<p>It goes further in several important areas:</p>
<ul>
<li>It is open source.</li>
<li>Keys can stay current automatically through JWKS synchronization.</li>
<li>Group-to-role mapping allows IdP group membership to drive MySQL role grants for the lifetime of the session.</li>
<li>Proxy-user support allows many IdP identities to share a smaller set of MySQL accounts.</li>
</ul>
<p>Our OIDC implementation is suitable for real-world identity operations at scale. It can automatically map identities and groups managed by an IdP to database users and roles, and synchronize cryptographic keys.</p>
<h2>References<a class="anchor-link" id="references"></a></h2>
<ol>
<li><a href="https://docs.percona.com/percona-server/8.4/openid-connect-authentication.html">Percona Server for MySQL documentation: OpenID Connect authentication</a>.</li>
<li><a href="https://docs.percona.com/percona-server/8.4/quickstart-openid-connect.html">Percona Server for MySQL documentation: Get started with OpenID Connect authentication</a>.</li>
<li><a href="https://dev.mysql.com/doc/refman/9.7/en/openid-pluggable-authentication.html">MySQL 9.7 Reference Manual: OpenID Connect Pluggable Authentication</a>.</li>
<li><a href="https://dev.mysql.com/doc/refman/9.7/en/proxy-users.html">MySQL 9.7 Reference Manual: Proxy Users</a>.</li>
<li><a href="https://openid.net/foundation/how-connect-works/">OpenID Foundation: How OpenID Connect Works</a></li>
<li><a href="https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets">auth0 Docs: JSON Web Key Sets</a>.</li>
</ol>
<hr>
<p><em>Written by Michal Jankowski. Reviewed by Dennis Kittrell and Oleksiy Lukin.<br>
Percona&reg; is a registered trademark of Percona LLC. MySQL&reg; is a registered trademark of Oracle Corporation.<br>
</em></p>
<p>The post <a href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/">OpenID Connect Authentication for MySQL, Now Fully Open Source</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/oidc-authentication-for-percona-mysql/">OpenID Connect Authentication for MySQL, Now Fully Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB/MySQL Environment MyEnv 3.0.1 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/" />
      <id>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/</id>
      <updated>2026-09-02T07:51:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.1 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment MyEnv.<br />
The new MyEnv can be downloaded here. How to install MyEnv is described in the MyEnv Installation Guide.<br />
In the inconceivable case that you find a bug in the MyEnv please report it to our public repository on Codeberg.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Upgrade from 2.x to 3.0<br />
Please check the MyEnv Installation Guide: Upgrading MyEnv.<br />
In MyEnv v3.0.1 tpl/aliases.conf.template has changed. Thus you should replace the MyEnv aliases.conf as follows:<br />
$ cp /etc/myenv/aliases.conf /etc/myenv/aliases.conf.$(date \'+%Y-%m-%d\')<br />
$ cp myenv/tpl/aliases.conf.template /etc/myenv/aliases.conf<br />
Changes in MyEnv 3.0.1<br />
MyEnv</p>
<p>Bug in setMyEnv.php fixed, socket was not set correctly.<br />
Some more aliases added for log tracking.<br />
Prompt port was always delayed by one switch, fixed.<br />
Missing port in PS1 prompt fixed again (issues/12).<br />
up/down for PostgreSQL should work correctly now.<br />
PostgreSQL should be evaluated correctly now in up.<br />
up should show PostgreSQL status correctly now (issues/4).<br />
Environment variables refactored and PostgreSQL specific variables added (issues/3).<br />
Discrepancies between my.cnf and myenv.conf leads now to abort if relevant configuration variables are affected (issues/5).<br />
Database seems OK after start which is wrong, timing issue (issues/2).<br />
hideschema works better now (issues/13).</p>
<p>MyEnv Installer</p>
<p>Wrong default for socket fixed in installer.<br />
Nasty error message when installing PostgreSQL suppressed.<br />
basedir filter implemented.<br />
Instance type mariadb was added to MyEnv installer (issues/10).<br />
MySQL 26.7 is recognized correctly now (issues/1).</p>
<p>MyEnv Utilities</p>
<p>insert_test.sh made more PostgreSQL friendly.<br />
test table commands made more comfortable for PostgreSQL.</p>
<p>PostgreSQL</p>
<p>See MyEnv, MyEnv Installer and MyEnv Utilities.</p>
<p>General</p>
<p>Cosmetic fixes.<br />
Some bugs and feature requests moved from TODO to …</p>
<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.1 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment <a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment">MyEnv</a>.</p>
<p>The new MyEnv can be downloaded <a href="https://support.fromdual.com/admin/public/download.php?operation=select&amp;product_id=5" target="_blank" title="FromDual download">here</a>. How to install MyEnv is described in the <a href="https://support.fromdual.com/documentation/myenv/myenv.html#installation-guide" target="_blank">MyEnv Installation Guide</a>.</p>
<p>In the inconceivable case that you find a bug in the MyEnv please report it to our public repository on <a href="https://codeberg.org/FromDual/MyEnv/issues" target="_blank" title="FromDual / MyEnv">Codeberg</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20myenv">send them to us</a>.</p>
<h2>Upgrade from 2.x to 3.0<a class="anchor-link" id="upgrade-from-2-x-to-3-0"></a></h2>
<p>Please check the MyEnv Installation Guide: <a href="https://support.fromdual.com/documentation/myenv/myenv.html#upgrade" target="_blank">Upgrading MyEnv</a>.</p>
<p>In MyEnv v3.0.1 <code>tpl/aliases.conf.template</code> has changed. Thus you should replace the MyEnv <code>aliases.conf</code> as follows:</p>
<pre><code>$ cp /etc/myenv/aliases.conf /etc/myenv/aliases.conf.$(date '+%Y-%m-%d')
$ cp myenv/tpl/aliases.conf.template /etc/myenv/aliases.conf
</code></pre>
<h2>Changes in MyEnv 3.0.1<a class="anchor-link" id="changes-in-myenv-3-0-1"></a></h2>
<h3>MyEnv<a class="anchor-link" id="myenv"></a></h3>
<ul>
<li>Bug in <code>setMyEnv.php</code> fixed, socket was not set correctly.</li>
<li>Some more aliases added for log tracking.</li>
<li>Prompt port was always delayed by one switch, fixed.</li>
<li>Missing port in <code>PS1</code> prompt fixed again (<a href="https://codeberg.org/FromDual/MyEnv/issues/12" target="_blank">issues/12</a>).</li>
<li><code>up</code>/<code>down</code> for PostgreSQL should work correctly now.</li>
<li>PostgreSQL should be evaluated correctly now in <code>up</code>.</li>
<li><code>up</code> should show PostgreSQL status correctly now (<a href="https://codeberg.org/FromDual/MyEnv/issues/4" target="_blank">issues/4</a>).</li>
<li>Environment variables refactored and PostgreSQL specific variables added (<a href="https://codeberg.org/FromDual/MyEnv/issues/3" target="_blank">issues/3</a>).</li>
<li>Discrepancies between <code>my.cnf</code> and <code>myenv.conf</code> leads now to abort if relevant configuration variables are affected (<a href="https://codeberg.org/FromDual/MyEnv/issues/5" target="_blank">issues/5</a>).</li>
<li>Database seems OK after start which is wrong, timing issue (<a href="https://codeberg.org/FromDual/MyEnv/issues/2" target="_blank">issues/2</a>).</li>
<li><code>hideschema</code> works better now (<a href="https://codeberg.org/FromDual/MyEnv/issues/13" target="_blank">issues/13</a>).</li>
</ul>
<h3>MyEnv Installer<a class="anchor-link" id="myenv-installer"></a></h3>
<ul>
<li>Wrong default for socket fixed in installer.</li>
<li>Nasty error message when installing PostgreSQL suppressed.</li>
<li><code>basedir</code> filter implemented.</li>
<li>Instance <code>type</code> <code>mariadb</code> was added to MyEnv installer (<a href="https://codeberg.org/FromDual/MyEnv/issues/10" target="_blank">issues/10</a>).</li>
<li>MySQL 26.7 is recognized correctly now (<a href="https://codeberg.org/FromDual/MyEnv/issues/1" target="_blank">issues/1</a>).</li>
</ul>
<h3>MyEnv Utilities<a class="anchor-link" id="myenv-utilities"></a></h3>
<ul>
<li><code>insert_test.sh</code> made more PostgreSQL friendly.</li>
<li><code>test</code> table commands made more comfortable for PostgreSQL.</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li>See <a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/#myenv">MyEnv</a>, <a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/#myenv-installer">MyEnv Installer</a> and <a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/#myenv-utilities">MyEnv Utilities</a>.</li>
</ul>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Cosmetic fixes.</li>
<li>Some bugs and feature requests moved from <code>TODO</code> to &hellip;</li>
</ul>

<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.1-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 12.3 InnoDB-Accelerated Binary Logging: Delivering 1.50x Higher Throughput</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-12-3-innodb-accelerated-binary-logging-delivering-1-50x-higher-throughput/" />
      <id>https://mariadb.com/resources/blog/mariadb-12-3-innodb-accelerated-binary-logging-delivering-1-50x-higher-throughput/</id>
      <updated>2026-09-01T18:12:29+03:00</updated>
      <author><name>Rahul Raj</name></author>
      <summary type="html"><![CDATA[<p>What Is the MariaDB Binary Log Bottleneck and How Does InnoDB-Based Logging Solve It? In modern high-performance database infrastructure, the […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-innodb-accelerated-binary-logging-delivering-1-50x-higher-throughput/">MariaDB 12.3 InnoDB-Accelerated Binary Logging: Delivering 1.50x Higher Throughput</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In modern high-performance database infrastructure, the binary log (Binlog) is both a lifesaver and a well-known architectural bottleneck. While critical for replication, point-in-time recovery, and data auditing, writing sequentially to transactional logs and simultaneously managing the global MariaDB Binlog introduces massive locking and synchronization overhead. With the release of MariaDB&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-innodb-accelerated-binary-logging-delivering-1-50x-higher-throughput/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-innodb-accelerated-binary-logging-delivering-1-50x-higher-throughput/">MariaDB 12.3 InnoDB-Accelerated Binary Logging: Delivering 1.50x Higher Throughput</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Newsletter – September 2026</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-newsletter-september-2026/" />
      <id>https://mariadb.org/mariadb-foundation-newsletter-september-2026/</id>
      <updated>2026-09-01T11:30:29+03:00</updated>
      <author><name>Simona Aleksandrova</name></author>
      <summary type="html"><![CDATA[<p>August belonged to transparency: MariaDB published its governance framework, and the first contribution statistics report in over a year put numbers on who actually builds the server. …<br />
Continue reading \"MariaDB Foundation Newsletter – September 2026\"<br />
MariaDB Foundation Newsletter – September 2026 appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-foundation-newsletter-september-2026/">MariaDB Foundation Newsletter – September 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>August belonged to transparency: MariaDB published its governance framework, and the first contribution statistics report in over a year put numbers on who actually builds the server. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/mariadb-foundation-newsletter-september-2026/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;MariaDB Foundation Newsletter &ndash; September 2026&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-foundation-newsletter-september-2026/">MariaDB Foundation Newsletter &ndash; September 2026</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-foundation-newsletter-september-2026/">MariaDB Foundation Newsletter – September 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Rotating Expiring X.509 Certificates in Percona Server for MongoDB with Minimal Service Interruption</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/rotating-expiring-x-509-certificates-in-percona-server-for-mongodb-with-minimal-service-interruption/" />
      <id>https://www.percona.com/blog/rotating-expiring-x-509-certificates-in-percona-server-for-mongodb-with-minimal-service-interruption/</id>
      <updated>2026-08-31T11:48:11+03:00</updated>
      <author><name>Ivan Groenewold</name></author>
      <summary type="html"><![CDATA[<p>Expired TLS certificates can prevent new client connections and, when X.509 is used for Percona Server for MongoDB internal authentication, also prevent members of a replica set or sharded cluster from authenticating to one another. In this post we will discuss performing a same-CA renewal: replacement certificates for server, member, and client leaf are issued … Continued<br />
The post Rotating Expiring X.509 Certificates in Percona Server for MongoDB with Minimal Service Interruption appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/rotating-expiring-x-509-certificates-in-percona-server-for-mongodb-with-minimal-service-interruption/">Rotating Expiring X.509 Certificates in Percona Server for MongoDB with Minimal Service Interruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Expired TLS certificates can prevent new client connections and, when X.509 is used for <a href="https://www.percona.com/mongodb/software/">Percona Server for MongoDB</a> internal authentication, also prevent members of a replica set or sharded cluster from authenticating to one another.</p>
<p>In this post we will discuss performing a <b>same-CA renewal</b>: replacement certificates for server, member, and client leaf are issued by the existing trusted CA, and the X.509 attributes used for cluster membership do not change. In this scenario, the rotateCertificates command reloads TLS material for new connections without restarting mongod or mongos.</p>
<p><b>Important:</b> Do not apply this hot-reload procedure when replacing the issuing CA, changing a certificate subject DN, or changing cluster-membership attributes. Those are not ordinary renewals.</p>
<h3><b>What rotates, and what does not</b><a class="anchor-link" id="what-rotates-and-what-does-not"></a></h3>
<p>Percona Server for MongoDB can reload the files configured through the following TLS options:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">net:
 &nbsp;tls:
 &nbsp;&nbsp;&nbsp;mode: requireTLS
 &nbsp;&nbsp;&nbsp;certificateKeyFile: /etc/mongod/tls/server.pem
 &nbsp;&nbsp;&nbsp;CAFile: /etc/mongod/tls/ca.pem
 &nbsp;&nbsp;&nbsp;clusterFile: /etc/mongod/tls/cluster.pem</pre>
<p>The certificateKeyFile contains the certificate and private key presented to normal clients. The clusterFile holds the certificate and key that a mongod or mongos process presents when connecting to other cluster members. If clusterFile is not configured, certificateKeyFile is also used for member authentication.</p>
<p>The rotateCertificates command affects <b>new TLS connections</b>. It does not terminate established client sessions or force a replica-set election.</p>
<h3><b>Before the maintenance window</b><a class="anchor-link" id="before-the-maintenance-window"></a></h3>
<p>Begin this process well in advance of the certificate expiry, and avoid performing your first attempt in a production environment.</p>
<ol>
<li>Inventory every process and client certificates. Include all mongod members, all mongos routers, application drivers, mongosh hosts, backup jobs, monitoring, and automation tools.</li>
<li>Confirm this is a same-CA renewal. The issuer chain trusted by every participant stays the same, and the O, OU, and DC attributes used for default internal X.509 membership matching remain unchanged.</li>
<li>Create a new PEM file for every server and client that needs rotation. A PEM file referenced by certificateKeyFile or clusterFile must include both the certificate and its matching private key. The file must strictly contain the key first, followed by the certificate, including their encapsulation boundaries.</li>
<li>Verify the new certificate details and validate the cert against the CA before copying to the production TLS directory</li>
</ol>
<p><b>Stage a renewed server or member certificate</b></p>
<p>There are a few limitations for rotating certificates online:</p>
<ul>
<li>Each new certificate must have the same filename and same filepath as the certificate it is replacing.</li>
<li>If the TLS Certificate is password-protected, its password must be the same as the old certificate it is replacing.</li>
</ul>
<p>If CAFile, a CRL, or another configured TLS input is being renewed as part of the same operation, replace it before invoking the reload command. The command reloads the configured TLS inputs as a set; a missing or invalid input causes the reload to fail.</p>
<p>Luckily, incorrect certificate files will cause the rotation to fail, but will not invalidate the existing configuration or have any other side effects.</p>
<h3><b>Reload one process</b><a class="anchor-link" id="reload-one-process"></a></h3>
<p>Connect directly to the specific mongod or mongos with an administrative user and execute the following command:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">db.getSiblingDB("admin").runCommand({rotateCertificates: 1, message: "Renewed TLS certificate"})'</pre>
<p>Immediately validate a <b>new</b> TLS connection to that process with a renewed client certificate. Also inspect the log for the successful certificate-rotation message and any TLS errors. Check our <a href="https://docs.percona.com/percona-server-for-mongodb/8.3/index.html">documentation</a> for guidelines to perform the procedure on a replica set or sharded cluster.</p>
<h3><b>Final validation and cleanup</b><a class="anchor-link" id="final-validation-and-cleanup"></a></h3>
<p>After completing the procedure, it is a good idea to reconfirm the expiry date and SANs of the certificate presented by every mongod and mongos. Retain the old certificates only for the approved overlap period, then remove or revoke them. Don&rsquo;t forget to record the new expiry dates and create alerts with enough lead time before the expiration date of the new certificates.</p>
<h3><b>When the CA or member identity changes</b><a class="anchor-link" id="when-the-ca-or-member-identity-changes"></a></h3>
<p>A different procedure is required when any of the following changes:</p>
<ul>
<li>The issuing CA or trusted CA chain.</li>
<li>The subject DN used by a MONGODB-X509 client user.</li>
<li>The O, OU, or DC values used for default intra-cluster X.509 membership matching.</li>
<li>net.tls.clusterAuthX509.attributes or net.tls.clusterAuthX509.extensionValue.</li>
</ul>
<p>This is a topic for another time.</p>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/rotating-expiring-x-509-certificates-in-percona-server-for-mongodb-with-minimal-service-interruption/">Rotating Expiring X.509 Certificates in Percona Server for MongoDB with Minimal Service Interruption</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/rotating-expiring-x-509-certificates-in-percona-server-for-mongodb-with-minimal-service-interruption/">Rotating Expiring X.509 Certificates in Percona Server for MongoDB with Minimal Service Interruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB at Percona Live Amsterdam 2026: Ecosystem, Plugins, Security, and Community</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-at-percona-live-amsterdam-2026-ecosystem-plugins-security-and-community/" />
      <id>https://mariadb.org/mariadb-at-percona-live-amsterdam-2026-ecosystem-plugins-security-and-community/</id>
      <updated>2026-08-31T10:21:33+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is heading to Percona Live Amsterdam 2026! Join us for sessions on server plugins, protocol security and ecosystem building, hear independent MariaDB experiences, meet Percona\'s new General Manager for MariaDB, and visit the MariaDB booth for demos and discussions with our engineers.<br />
MariaDB at Percona Live Amsterdam 2026: Ecosystem, Plugins, Security, and Community appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-at-percona-live-amsterdam-2026-ecosystem-plugins-security-and-community/">MariaDB at Percona Live Amsterdam 2026: Ecosystem, Plugins, Security, and Community</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is heading to Percona Live Amsterdam 2026! Join us for sessions on server plugins, protocol security and ecosystem building, hear independent MariaDB experiences, meet Percona&rsquo;s new General Manager for MariaDB, and visit the MariaDB booth for demos and discussions with our engineers.</p>
<p><a href="https://mariadb.org/mariadb-at-percona-live-amsterdam-2026-ecosystem-plugins-security-and-community/">MariaDB at Percona Live Amsterdam 2026: Ecosystem, Plugins, Security, and Community</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-at-percona-live-amsterdam-2026-ecosystem-plugins-security-and-community/">MariaDB at Percona Live Amsterdam 2026: Ecosystem, Plugins, Security, and Community</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Building Useful Community Software with MongoDB Using AI Agents</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/08/31/community-leaderboard/" />
      <id>https://percona.community/blog/2026/08/31/community-leaderboard/</id>
      <updated>2026-08-31T08:16:04+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When activity spreads across GitHub, the forum, and content, real contribution is easy to lose in the noise. A community program only works when people trust the numbers behind the thank-you, so the leaderboard has to be accurate and explainable. For our Community Leaderboard we built scheduled indexers for those sources, Percona Server for MongoDB (PSMDB) as a flexible store for messy multi-source data, private dashboards to verify staff vs community and identity maps, and a daily JSON feed that powers a static Hugo widget. AI agents took care of much of the boilerplate: API clients, first-pass field mappings, early widget prototypes. That let us spend the week on rules, trust, and the public experience. The same pattern should transfer if you run a community or like database-shaped side projects.</p>
<p><a href="https://percona.community/blog/2026/08/31/community-leaderboard/">Building Useful Community Software with MongoDB Using AI Agents</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When activity spreads across GitHub, the forum, and content, real contribution is easy to lose in the noise. A community program only works when people trust the numbers behind the thank-you, so the leaderboard has to be accurate and explainable. For our <a href="https://percona.community/ascent/leaderboard/" target="_blank" rel="noopener noreferrer">Community Leaderboard</a> we built scheduled indexers for those sources, <a href="https://docs.percona.com/percona-server-for-mongodb/8.0/index.html" target="_blank" rel="noopener noreferrer">Percona Server for MongoDB</a> (PSMDB) as a flexible store for messy multi-source data, private dashboards to verify staff vs community and identity maps, and a daily JSON feed that powers a static Hugo widget. <strong>AI agents</strong> took care of much of the boilerplate: API clients, first-pass field mappings, early widget prototypes. That let us spend the week on rules, trust, and the public experience. The same pattern should transfer if you run a community or like database-shaped side projects.</p>
<p>In July our community lead <strong>Laura Czajkowski</strong> published <a href="https://percona.community/blog/2026/07/16/introducing-mountaineers/">Introducing Mountaineers: A Way to Say Thank You</a>. That program is why we built the board. Laura wanted contribution to stop disappearing into noise: the bug filed late at night, the forum thread that sat unanswered, the PR, the hour telling engineering what&rsquo;s broken. Mountaineers tracks that energy and gives something back: recognition, access, swag. Points, Basecamp, and the reward ladder are in her post and on the <a href="https://percona.community/ascent/mountaineers/" target="_blank" rel="noopener noreferrer">Mountaineers</a> page. The leaderboard is the public face; my job was to make the numbers trustworthy enough for that program to work.</p>
<p><figure><img decoding="async" width="2816" height="2816" src="https://percona.community/blog/2026/08/leaderboard-widget_hu_4400601fb99204a.webp" alt="Community Leaderboard widget for 2026, period picker open" loading="lazy"></figure>
</p>
<h2>Why the board exists<a class="anchor-link" id="why-the-board-exists"></a></h2>
<p>Laura&rsquo;s &ldquo;what counts&rdquo; list is what the board has to measure. It isn&rsquo;t only code. GitHub issues, PRs, and merges count. Forum topics, replies, and accepted solutions count. Blog posts and tutorials count, including work through the <a href="https://percona.community/blog/2026/05/22/write-for-percona-community/">Community Writers Program</a>. Direct feedback to engineering counts too. Show up in more than one place in the same month and the climb goes faster. The public <a href="https://percona.community/ascent/leaderboard/" target="_blank" rel="noopener noreferrer">leaderboard</a> is how that becomes visible outside the team.</p>
<p>A board only helps if the community trusts it. That&rsquo;s where the engineering problem starts. The store holds <strong>600+ Percona GitHub repositories</strong>; in <strong>2026</strong> staff alone were active in <strong>100+</strong> of them (<strong>~150</strong> employee contributors). Staff and community work in the same channels &ndash; GitHub and the forum &ndash; so without filters you cannot tell whose work you&rsquo;re looking at. Separating community PRs from staff PRs, and spotting forum questions from community members that still have no answer, isn&rsquo;t something you do by scrolling notifications. You need a place where that signal is collected, filtered, and easy to inspect.</p>
<p>That&rsquo;s what the internal dashboards are for. They aren&rsquo;t the public site. They&rsquo;re how the community team finds the work worth recognizing and the threads that still need a human.</p>
<p>In <strong>2026</strong> so far community activity is already substantial. On GitHub: <strong>~750 pull requests and issues</strong> from <strong>270+ contributors</strong>, including <strong>~80 merged PRs</strong>. On the forum: <strong>~290 active community users</strong>, <strong>~400 active topics</strong>, and <strong>~900 community posts</strong>. The public board for that year already has <strong>498</strong> people on it (<strong>257</strong> GitHub, <strong>240</strong> forum, <strong>5</strong> content). Staff also post heavily in the same places, which is why the dashboards filter staff vs community before anything is scored for Mountaineers. Without that step, employee noise would bury the people the program exists to thank.</p>
<p>To turn that activity into a comparable ranking, we use a simple, fixed set of weights:</p>
<table>
<thead>
<tr>
<th>GitHub</th>
<th>Content</th>
<th>Forum</th>
</tr>
</thead>
<tbody>
<tr>
<td>Issue submitted &middot; <strong>10</strong></td>
<td>Community blog post &middot; <strong>100</strong></td>
<td>Topic created &middot; <strong>5</strong></td>
</tr>
<tr>
<td>PR created &middot; <strong>25</strong></td>
<td>YouTube video &middot; <strong>75</strong></td>
<td>Reply &middot; <strong>2</strong></td>
</tr>
<tr>
<td>PR merged &middot; <strong>75</strong></td>
<td>External article &middot; <strong>50</strong></td>
<td>Solution provided &middot; <strong>50</strong></td>
</tr>
</tbody>
</table>
<p>Content is still thin on the board. We only recently launched the <a href="https://percona.community/blog/2026/05/22/write-for-percona-community/">Community Writers Program</a>, so most climbing today is GitHub and forum. That will change as more posts land.</p>
<p><figure><img decoding="async" width="2502" height="1578" src="https://percona.community/blog/2026/08/leaderboard-dashboard-leaderboard_hu_5a56a6b201043b66.webp" alt="Internal leaderboard: Show Community, Global tab, 498 contributors for 2026" loading="lazy"></figure>
</p>
<p>Those numbers and weights are the <em>why</em>. The rest of the post is the <em>how</em>: private dashboards, a public JSON feed, PSMDB, and a recipe you can reuse.</p>
<h2>Private dashboards, public site<a class="anchor-link" id="private-dashboards-public-site"></a></h2>
<p>The architecture is a bit unusual on purpose. The dashboards live in our private corporate cloud. They talk to APIs, write to PSMDB, and show rich views for staff. The <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">community website</a> is a static <strong>Hugo</strong> site on <strong>GitHub Pages</strong>. It has no backend and no path into that cloud.</p>
<p>So we needed a bridge that doesn&rsquo;t couple the two. Once a day the leaderboard job scores the periods we care about (month, quarter, year), writes community-only JSON, and publishes it to a public GitHub repository: <a href="https://github.com/percona/community-leaderboard/tree/widget" target="_blank" rel="noopener noreferrer">percona/community-leaderboard</a>. The Hugo page embeds a JS widget that fetches those files from raw GitHub URLs. No VPN. No private API. If the internal app is offline for maintenance, the last published feed still works.</p>
<p>We call that lifehack <strong>GitHub as a database</strong>. It&rsquo;s a boring, cacheable public artifact. An object store would work the same way. For a static site, a daily JSON dump is often simpler and safer than exposing your analytics database.</p>
<p>The application breaks into a few components. Data sources are polled by indexer jobs and land in PSMDB. The same database feeds internal dashboards (explore, filter staff vs community) and a leaderboard component that builds period reports and publishes JSON to GitHub. The community site only talks to that public feed.</p>
<pre class="mermaid">
flowchart TB
subgraph sources["Data sources"]
GH["GitHub"]
Forum["Forum"]
Content["Blog / content"]
end
subgraph private["Private app &middot; corporate cloud"]
Idx["Indexer jobs"]
Mongo[("PSMDB")]
Dash["Dashboards"]
LB["Leaderboard component"]
end
subgraph public["Public"]
Feed["JSON on GitHub"]
Site["Community site &middot; Hugo widget"]
end
GH --&gt; Idx
Forum --&gt; Idx
Content --&gt; Idx
Idx --&gt;|"index &middot; upsert"| Mongo
Mongo --&gt;|"read &middot; filter &middot; charts"| Dash
Mongo --&gt;|"score &middot; export"| LB
LB --&gt;|"publish rankings"| Feed
Feed --&gt;|"fetch JSON"| Site
style Mongo fill:#e8f5e9
style Feed fill:#fff8e1
style Site fill:#e1f5ff
style private fill:#f9f7ff
</pre>
<p>Indexers and the leaderboard component run on a schedule (cron). The diagram above is the data path; the schedule is just when each box wakes up.</p>
<p>On the site, the widget is more than a dumped table. You can switch periods and categories (global, GitHub, forum), open a person and see how they scored, and browse leaders without leaving Hugo. The layout had to feel like part of Community Ascent, not like an admin export pasted into a page. There is also a <a href="https://percona.community/ascent/summit/" target="_blank" rel="noopener noreferrer">Global Summit</a> view (top 10 on a mountain) for the people who climb furthest.</p>
<p><figure><img decoding="async" width="2688" height="1672" src="https://percona.community/blog/2026/08/leaderboard-widget-popup-details_hu_6479b1ad7447ef4d.webp" alt="Contributor detail popup: points by source, PRs, and forum topics" loading="lazy"></figure>
</p>
<p><figure><img decoding="async" width="2082" height="1484" src="https://percona.community/blog/2026/08/leaderboard-widget-global-summit_hu_f59c7d16527a7479.webp" alt="Global Summit: top 10 worldwide on the mountain" loading="lazy"></figure>
</p>
<h2>Public feed: JSON the widget reads<a class="anchor-link" id="public-feed-json-the-widget-reads"></a></h2>
<p>The Hugo widget does not guess rankings. It loads plain JSON from the public repo. On first paint it fetches <code>meta.json</code> for available periods and categories, then <code>{category}/{period}.json</code> for the table (for example <code>global/2026.json</code>). When you open someone in the modal, it loads <code>users/{period}/{user_key}.json</code> for the breakdown.</p>
<p>The layout is simple:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">meta.json
</span></span><span class="line"><span class="cl">global/{period}.json
</span></span><span class="line"><span class="cl">github/{period}.json
</span></span><span class="line"><span class="cl">forum/{period}.json
</span></span><span class="line"><span class="cl">users/{period}/{user_key}.json</span></span></code></pre>
</div>
</div>
</div>
<p><code>meta.json</code> is the index. The widget uses it to build the period picker:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">json</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"generated_at"</span><span class="p">:</span> <span class="s2">"2026-08-31T04:11:39+00:00"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"default_period"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"periods"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="nt">"key"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span> <span class="nt">"label"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span> <span class="nt">"type"</span><span class="p">:</span> <span class="s2">"year"</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="nt">"key"</span><span class="p">:</span> <span class="s2">"2026-Q3"</span><span class="p">,</span> <span class="nt">"label"</span><span class="p">:</span> <span class="s2">"Q3 2026"</span><span class="p">,</span> <span class="nt">"type"</span><span class="p">:</span> <span class="s2">"quarter"</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span> <span class="nt">"key"</span><span class="p">:</span> <span class="s2">"2026-08"</span><span class="p">,</span> <span class="nt">"label"</span><span class="p">:</span> <span class="s2">"August 2026"</span><span class="p">,</span> <span class="nt">"type"</span><span class="p">:</span> <span class="s2">"month"</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"categories"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"global"</span><span class="p">,</span> <span class="s2">"github"</span><span class="p">,</span> <span class="s2">"content"</span><span class="p">,</span> <span class="s2">"forum"</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p>Each ranking file is one period and one category. The table reads <code>top30</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">json</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"period"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"period_type"</span><span class="p">:</span> <span class="s2">"year"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"period_label"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"category"</span><span class="p">:</span> <span class="s2">"global"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"top30"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"rank"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"user_key"</span><span class="p">:</span> <span class="s2">"gh-example"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"display_name"</span><span class="p">:</span> <span class="s2">"Alex Contributor"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"avatar_url"</span><span class="p">:</span> <span class="s2">"https://avatars.githubusercontent.com/u/12345?v=4"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"github_login"</span><span class="p">:</span> <span class="s2">"alex-contrib"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"forum_username"</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"points_total"</span><span class="p">:</span> <span class="mi">595</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"points_github"</span><span class="p">:</span> <span class="mi">500</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"points_forum"</span><span class="p">:</span> <span class="mi">95</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"issues_created"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"prs_created"</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"prs_merged"</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"topics_created"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"replies"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"solutions"</span><span class="p">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p>The per-user file adds contribution lists for the detail view:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">json</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"user_key"</span><span class="p">:</span> <span class="s2">"gh-example"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"display_name"</span><span class="p">:</span> <span class="s2">"Alex Contributor"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"period"</span><span class="p">:</span> <span class="s2">"2026"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"points_total"</span><span class="p">:</span> <span class="mi">595</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"github_prs"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"title"</span><span class="p">:</span> <span class="s2">"Fix replication lag in operator"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"url"</span><span class="p">:</span> <span class="s2">"https://github.com/percona/example/pull/42"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"repo"</span><span class="p">:</span> <span class="s2">"percona/example"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2026-07-10"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"merged"</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"forum_replies"</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"title"</span><span class="p">:</span> <span class="s2">"Slow queries after upgrade"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"url"</span><span class="p">:</span> <span class="s2">"https://forums.percona.com/t/example/12345"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2026-06-15"</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p>That is all the static site needs. Live files are in <a href="https://github.com/percona/community-leaderboard/tree/widget" target="_blank" rel="noopener noreferrer">percona/community-leaderboard</a>; the widget base URL is set in the Hugo layout (<code>window.LB_BASE</code>).</p>
<h2>What runs every day<a class="anchor-link" id="what-runs-every-day"></a></h2>
<p>Scheduled indexer jobs poll the APIs we care about, one family of sources each, and upsert documents into PSMDB. Another scheduled job builds the rankings and pushes the public feed. The web UI is how we see the store: date ranges, charts, activity tables, and a leaderboard toggle for community, staff, or all. Staff numbers matter internally. Who answered on the forum this week? Which PRs came from outside? Which community questions are still waiting? The public Mountaineers board stays community-only. Same data, different filter.</p>
<p>One person is often three strings in the data. GitHub login, forum username, and a blog byline under a real name rarely match. Without linking, the same Mountaineer shows up as three rows and their points never add up. <strong>Identity mapping</strong> is how we stitch that together. You can link accounts by hand in the dashboard. When a display name or handle lines up across sources, the UI also proposes a merge for approval. That is semi-automatic, not a silent auto-join. Only confirmed maps feed the public ranking.</p>
<p>We develop against <a href="https://docs.percona.com/percona-server-for-mongodb/8.0/index.html" target="_blank" rel="noopener noreferrer">Percona Server for MongoDB</a> in Docker locally. Same engine shape in production. I run <code>docker compose up</code>, connect from the app, and start indexing. For browsing collections I use <strong>MongoDB Compass</strong> on <code>localhost:27018</code> (or whatever port you map). It is the fastest way to check that indexers wrote what you expect before you trust a public export.</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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">psmdb</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-server-mongodb:8.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./data:/data/db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MONGO_INITDB_ROOT_USERNAME</span><span class="p">:</span><span class="w"> </span><span class="l">root</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MONGO_INITDB_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">changeme</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">MONGO_INITDB_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"27018:27017"</span></span></span></code></pre>
</div>
</div>
</div>
<p>Pick the image tag for your CPU (<code>8.0-arm64</code> on Apple Silicon, <code>8.0</code> / amd64 elsewhere). No cloud dependency just to try an idea.</p>
<h2>MongoDB as the use case<a class="anchor-link" id="mongodb-as-the-use-case"></a></h2>
<p>If you like databases, this is the interesting middle of the story. We run <a href="https://docs.percona.com/percona-server-for-mongodb/8.0/index.html" target="_blank" rel="noopener noreferrer">Percona Server for MongoDB</a> (PSMDB), but the fit is the document model. Community activity arrives as heterogeneous JSON. Issues, pull requests, forum posts, user profiles, and blog metadata don&rsquo;t share one neat relational schema. Nested objects are normal, and APIs grow new fields over time. GitHub can suddenly add <code>reactions</code>; the forum can expose a new <code>trust_level</code> or group list. In MongoDB that usually means new keys on the document, not a migration that blocks indexing.</p>
<p>PSMDB fits that. We keep sources in separate collections and store documents close to what the API returned, then add what reporting needs: stable ids and comparable dates for range queries. Upserts by source id make re-runs safe. Staff signals such as forum groups or trust level can stay on the user document while rules evolve. Indexes on the date fields keep month and quarter scans practical. Open one document and you can explain why an event counted or not.</p>
<p>PSMDB is the system of record for events and profiles. Rankings are derived. The public site never connects to it. That split is useful beyond our case: a rich private store for organizers, a dumb public snapshot for the website.</p>
<p>We looked at paid leaderboard products first. They were rigid on sources, weak on staff versus community, or awkward with a static site. Building around PSMDB let us encode our rules instead of bending the program to a SaaS schema.</p>
<p>A few mistakes we deliberately avoided: exposing a public API that talks to the private database; scoring staff and community in one undifferentiated stream for the public board; asking Hugo to compute rankings at build time. The site only fetches JSON. Everything heavy stays behind the corporate network.</p>
<h2>Building it without hundreds of hours<a class="anchor-link" id="building-it-without-hundreds-of-hours"></a></h2>
<p>I could have written this stack myself. I&rsquo;ve been in web development for many years and I&rsquo;ve built similar pipelines before: cron jobs, API clients, admin UIs, JSON exports, frontend widgets. None of it is magic. I know how long the boring parts take when you do them by hand: wiring indexers, shaping documents, iterating on the Hugo widget layout, debugging JavaScript, mapping fields from noisy API payloads. That is often weeks of calendar time that never shows up in a program announcement.</p>
<p>In about a week we had a working path from APIs to PSMDB to rankings to a public widget. An AI assistant did a lot of that scaffolding for me: boilerplate API clients for GitHub and the forum, first-pass mapping of those payloads into MongoDB collections, and early prototypes of the Preact widget (layout, period picker, loading states). I still owned the architecture and the rules; the agent compressed construction I would otherwise have typed line by line. That difference is real, and I notice it because I&rsquo;m not new to this work. Judgment stayed with the community team and with Laura&rsquo;s brief: what counts, who is staff, how identities merge, what the public is allowed to see, and how the board should feel on the Ascent pages.</p>
<p>I used a similar approach for <a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">semantic search on this site</a> with Postgres and pgvector. Different store, same idea: know what you want, let the assistant handle the repetitive build.</p>
<h2>If you want something like this<a class="anchor-link" id="if-you-want-something-like-this"></a></h2>
<p>You don&rsquo;t need our private cloud. You need the same separation of concerns.</p>
<p>Ask for scheduled indexer jobs that poll your APIs, upsert into <a href="https://docs.percona.com/percona-server-for-mongodb/8.0/index.html" target="_blank" rel="noopener noreferrer">Percona Server for MongoDB</a> in Docker, keep documents close to each API&rsquo;s shape, and add stable ids and dates for range queries.</p>
<p>Ask for a private UI over that database with tables, charts, date filters, and an explicit community / staff / all view, behind basic auth or SSO, so you can verify fairness before anything is public. Use it to find outside PRs and unanswered community questions, not only to draw a ranking.</p>
<p>Ask for a scoring job that builds period rankings, publishes community-only JSON to a public feed (GitHub raw files or object storage), and a small JS widget for your site that only reads that feed.</p>
<p>Collect, understand, publish, render. Honest collection, careful filters, a simple public publish path.</p>
<p>If you try it and get stuck, write to me. I&rsquo;m happy to talk through what worked and what we threw away. To see the result in production, open the <a href="https://percona.community/ascent/leaderboard/" target="_blank" rel="noopener noreferrer">Community Leaderboard</a>. To join the program behind it, start with Laura&rsquo;s <a href="https://percona.community/blog/2026/07/16/introducing-mountaineers/">Mountaineers announcement</a> or the <a href="https://percona.community/ascent/mountaineers/" target="_blank" rel="noopener noreferrer">program page</a>.</p>

<p><a href="https://percona.community/blog/2026/08/31/community-leaderboard/">Building Useful Community Software with MongoDB Using AI Agents</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Benchmarking vector indexes</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/benchmarking-vector-indexes/" />
      <id>https://www.percona.com/blog/benchmarking-vector-indexes/</id>
      <updated>2026-08-27T13:35:32+03:00</updated>
      <author><name>Evgeniy Patlan</name></author>
      <summary type="html"><![CDATA[<p>Nearly every database has vector search now, and every one of them has a blog post with a big number in it. Almost none of those numbers can be checked, because the thing that makes them meaningful is usually missing. We built a vector-bench to stop guessing. You name the engines you want, build them … Continued<br />
The post Benchmarking vector indexes appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/benchmarking-vector-indexes/">Benchmarking vector indexes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>Nearly every database has vector search now, and every one of them has a blog post with a big number in it. Almost none of those numbers can be checked, because the thing that makes them meaningful is usually missing.</span></p>
<p><span>We built a vector-bench to stop guessing. You name the engines you want, build them from pinned versions, put each one in the same container on the same cores with the same data, run the same measurements against all of them, and write a report. This post is about how it measures.</span></p>
<p><span>If you work with databases but haven&rsquo;t touched vectors yet, the first half is the part you need.</span></p>
<p><b>What&rsquo;s being indexed</b></p>
<p><span>An embedding is a fixed-length array of floats that comes out of a model. The useful property is that semantically similar inputs land close together when you measure the distance between them.</span></p>
<p><span>Two distance measures cover almost everything. </span><b>L2</b><span> is an ordinary straight-line distance, the Pythagorean one, extended to however many dimensions you have. </span><b>Cosine Similarity </b><span>&nbsp;measures the angle between two vectors and ignores their length. Which one applies is decided by the model that produced the embeddings. It isn&rsquo;t a choice you get to make at query time, and getting it wrong is a good way to produce nonsense.</span></p>
<p><span>So the query you want is &ldquo;the 10 rows whose vectors are nearest this one&rdquo;:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">SELECT id FROM documents ORDER BY distance(embedding, ?) LIMIT 10;</pre>
<p><span>That 10 is </span><b>k</b><span>.</span></p>
<p><span>Now the problem. Answering that exactly means computing the distance from your query vector to every single row, then sorting. No B-tree or hash index helps, because neither one can order a million points by proximity in 1536 dimensions. Exact vector search is a full table scan with a lot of arithmetic bolted on.</span></p>
<p><span>A vector index gives up exactness to avoid that. It looks at a few thousand promising candidates instead of every row and returns the best it found. That&rsquo;s the </span><b>approximate nearest neighbour</b><span> search, or ANN. It&rsquo;s usually right.</span></p>
<p><span>&ldquo;Usually&rdquo; is doing a lot of work in that sentence, and pinning it down is most of what this benchmark does.</span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-52524 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/recall.png" alt="" width="740" height="914"></p>
<p><span>To score that you need to know the right answer in the first place. That&rsquo;s the </span><b>ground truth</b><span>: the true nearest neighbours for every query, computed once by brute force with no index involved. The public ANN datasets ship theirs alongside the vectors, and without it you couldn&rsquo;t score an approximate index at all.</span></p>
<p><span>This is the number that makes everything else meaningful, and it&rsquo;s the one most vector search claims leave out. That omission is the reason this project exists.</span></p>
<h2><b>The two kinds of vector index</b><a class="anchor-link" id="the-two-kinds-of-vector-index"></a></h2>
<p><span>Almost every database that has added vector search picked one of two designs. They attack the same problem from opposite ends, and which one you have decides what you&rsquo;re allowed to tune.</span></p>
<h3><b>HNSW</b><a class="anchor-link" id="hnsw"></a></h3>
<p><b>HNSW</b><span> stands for Hierarchical Navigable Small World, which is a mouthful for something fairly intuitive. If you&rsquo;ve ever implemented a skip list, you already have the shape of it.</span></p>
<p><span>It&rsquo;s a graph of vectors built in layers. Every vector is a node, linked to some number of its nearest neighbours. The top layer has few nodes and its links jump long distances across the data. Each layer below has more nodes and shorter links. A search starts at the top and keeps hopping to whichever neighbour is closer to the query. When nothing is closer, it drops a layer and carries on, until it runs out of layers.</span></p>
<p><img decoding="async" class="alignnone size-full wp-image-52523 aligncenter" src="https://www.percona.com/wp-content/uploads/2026/08/hnsw.svg" alt=""></p>
<p><span>Two settings matter:</span></p>
<ul>
<li><b>M</b><span> is how many links each node keeps. It&rsquo;s fixed when the index is built. Higher M means a better-connected graph and better recall, at the cost of a slower </span> <span>build and a bigger index.</span></li>
<li><b>ef_search</b><span> is how many candidates the search keeps track of while it walks. It&rsquo;s a session variable, so </span> <span>you can change it per query. Turn it up and the search visits more </span> <span>nodes, gets better recall, and runs slower.</span></li>
</ul>
<p><span>There&rsquo;s </span><b>ef_construction</b><span> too, the same idea applied while the index is being built. Not every engine lets you set it, which turns out to matter when you try to compare them fairly.</span></p>
<h3><b>IVF</b><a class="anchor-link" id="ivf"></a></h3>
<p><b>IVF</b><span> stands for Inverted File. It partitions the data instead of linking it, not unlike list partitioning on a table.</span></p>
<p><span>At build time it groups the vectors into </span><span>nlist</span><span> clusters, each with a representative vector at its centre. At query time it compares the query against those representatives, picks the closest </span><span>nprobe</span><span> clusters, and searches only inside them. It builds much faster than HNSW and uses less memory, but usually gives worse recall at the same speed. It misses when the true neighbour happens to sit just outside the clusters it looked in.</span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-52521 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/ivf.svg" alt="" width="462" height="444"></p>
<p><span>We only test engines running HNSW, which is what most databases shipped. Putting an IVF engine on the same chart would mostly measure the gap between two algorithms rather than how well anybody implemented one, so IVF-only engines get their own bucket.</span></p>
<h2><b>Why one number is never enough</b><a class="anchor-link" id="why-one-number-is-never-enough"></a></h2>
<p><span>Recall isn&rsquo;t a property of an engine. It&rsquo;s a setting, and </span><span>ef_search</span><span> is the dial.</span></p>
<p><span>Here&rsquo;s one HNSW index on one machine, same data, same queries. The only difference is that on the first row the search tracks 10 candidate nodes as it walks the graph, and on the second it tracks 800:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">ef_search=10 3,678 queries/sec recall 0.9593
ef_search=800 409 queries/sec recall 0.9987</pre>
<p><span>Keeping 800 candidates instead of 10 finds a better answer and takes nine times as long. Both rows are honest measurements of the same index on the same hardware.</span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-52516 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/tradeoff.png" alt="" width="1746" height="712"></p>
<p><span>Which is why &ldquo;our database does 3,678 vector queries a second&rdquo; tells you nothing. You don&rsquo;t know how often it was handing back the wrong rows, and the person quoting it may not know either. The reverse is just as empty: recall with no throughput next to it is free, because recall 1.0 is always available if you turn the index off and scan the table.</span></p>
<p><span>Every measurement here is a pair. If you take one thing from this post, take that.</span></p>
<h2><b>What the harness puts on each engine</b><a class="anchor-link" id="what-the-harness-puts-on-each-engine"></a></h2>
<p><span>One table per engine. An id, an integer </span><span>tag</span><span> column used only by the filtered tests, the vector, and an HNSW index on it at a configured M.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE t1 (
id INTEGER PRIMARY KEY,
tag INTEGER NOT NULL,
v VECTOR(1536)
);</pre>
<p>&nbsp;</p>
<p><span>Then two queries, plain top-k and the same search restricted to a subset of rows:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">SELECT id FROM t1 ORDER BY distance(v, ?) LIMIT 10;
SELECT id FROM t1 WHERE tag &lt; ? ORDER BY distance(v, ?) LIMIT 10;</pre>
<p>&nbsp;</p>
<p><span>tag</span><span> holds values 0 to 99 spread evenly, so </span><span>tag &lt; 10</span><span> passes about 10% of rows and </span><span>tag &lt; 1</span><span> about 1%. That&rsquo;s how we control selectivity.</span></p>
<p><span>Every engine writes all of this differently. Some declare the index inside CREATE TABLE, others want a separate CREATE INDEX, and the distance functions have different names everywhere. Translating that is the driver&rsquo;s job, and the drivers are the only engine-specific code in the whole harness.</span></p>
<p><span>Every engine also has at least one setup detail that will quietly wreck your numbers. PostgreSQL, for instance, stores oversized values out of line in what it calls TOAST, and a 1536-dimension vector counts as oversized. Unless the column is set to </span><span>STORAGE PLAIN</span><span>, every single distance comparison pays for an extra fetch. It&rsquo;s one line of DDL. Miss it and you publish PostgreSQL looking slow for a reason that has nothing to do with its vector search, and you&rsquo;d never know from the results.</span></p>
<h2><b>What we measure</b><a class="anchor-link" id="what-we-measure"></a></h2>
<p><b>Recall against throughput.</b><span> Iterate </span><span>ef_search</span><span> against a fixed index, record recall and QPS at each point, repeat at a few values of M. k=10 throughout. The query vectors come from the dataset&rsquo;s own held-out query set, never from the rows we loaded, because searching for a vector that&rsquo;s already in the index is a much easier problem and would flatter everybody equally.</span></p>
<p><span>The two settings behave completely differently, and it shapes how long a run takes. </span><span>ef_search</span><span> is a session variable, so iterating it reuses the index that&rsquo;s already built and each extra point costs almost nothing. M is baked into the index, so every value of M means dropping the table and loading the entire dataset again. On a million 1536-dimension vectors that&rsquo;s hours per value. Hence many </span><span>ef_search</span><span> points and very few M values.</span></p>
<p><b>Build cost.</b><span> Wall time, rows per second, index size on disk, peak memory.</span></p>
<p><span>This is the easiest place in the whole benchmark to publish a misleading number, because engines don&rsquo;t build the index the same way. </span><span>Engines can build indexes either incrementally, bulk, or both. What does that mean?&nbsp;</span></p>
<p><b>Incremental.</b><span> The graph is updated on every INSERT. Loading is slow, but when the last row lands the index is finished and the table is ready to query.</span></p>
<p><b>Bulk.</b><span> All the rows load first, then the whole graph gets built in one pass. Much faster in total, but the table can&rsquo;t answer a vector query until the build finishes.</span></p>
<p><span>Those are two different operations. One engine in our set does both, and its bulk path loaded 18 times more rows per second than its own incremental path. Same engine, same data, same machine, 18x apart.</span></p>
<p><span>So a bulk number from one engine put next to an incremental number from another doesn&rsquo;t compare engines at all. It compares two ways of building an index, and the ratio looks impressive enough that people quote it anyway. We measure both paths on any engine that has both, and the report says which is which.</span></p>
<p><span>Peak memory comes from the server&rsquo;s container, with the database as the only thing running in it. The harness runs in a separate container and reaches the server over a private network.</span></p>
<p><span>That separation matters more than it sounds. The client holds the entire dataset in memory, several GB of Python arrays. If it shared a container with the database, the container&rsquo;s memory accounting would count those arrays as database memory, and every memory figure we published would be inflated by whatever the client happened to be holding.</span></p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-52518 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/harness.png" alt="" width="1246" height="439"></p>
<p><b>Concurrency.</b><span> QPS and latency percentiles from 1 to 32 clients. Engines cache their graphs in quite different ways and none of that shows up until clients start competing for the same cache. We report how much of the ideal speedup each engine actually got alongside raw QPS, because an engine that stops gaining throughput at 2 clients while its p99 gets 15 times worse is doing something very different from one that keeps scaling, and a throughput column on its own hides that completely.</span></p>
<p><b>Filtered search</b><span>, at several selectivities down to 1% of rows passing. This is the case that&rsquo;s supposed to justify keeping vectors in your database instead of a dedicated store, so it deserves more attention than it usually gets.</span></p>
<p><span>Filtering changes what &ldquo;correct&rdquo; means. The true top 10 among rows where </span><span>tag &lt; 10</span><span> is not the true top 10 overall, so for every selectivity we recompute ground truth by brute force over only the rows that pass. Score filtered results against the unfiltered ground truth that shipped with the dataset and every engine gets a recall near zero. We know, because we did exactly that for a while.</span></p>
<p><span>Some queries come back with fewer than 10 rows. In one run, 81 out of 200 did. This is not the data running out. At 10% selectivity about 99,000 rows pass the filter, so there are always at least 10 to find. The cause is the order of the operations. HNSW searches by distance first, then applies the WHERE clause. It gathers a few thousand candidates, the filter throws most of them away, and sometimes fewer than 10 are left. (If a filter really did match fewer than 10 rows, the ground truth shrinks too, and the engine still scores 1.0.) Recall already handles this. A row the engine did not return counts as a miss, so six correct rows score 0.6. We report the count because two different problems score the same. &ldquo;10 rows, four of them wrong&rdquo; and &ldquo;six rows, all correct&rdquo; are both 0.6. The first needs a wider search. The second needs iterative scanning. The count tells you which one you have. It also means the throughput is flattered, since six rows is less work than ten.</span></p>
<p><b>Churn.</b><span> Recall and throughput before and after deleting and reinserting part of the corpus, since deletions leave graph edges pointing at rows that are gone. Whether rebuilding the index recovers what&rsquo;s lost, we don&rsquo;t know yet. It&rsquo;s the obvious next thing to test and we haven&rsquo;t done it.</span></p>
<h2><b>Keeping the comparison fair</b><a class="anchor-link" id="keeping-the-comparison-fair"></a></h2>
<p><span>Everything runs twice.</span></p>
<p><span>The </span><b>normalized</b><span> pass gives every engine identical CPU, memory and cache budgets, so a difference in the results belongs to the implementation rather than to who was handed more RAM. The </span><b>tuned</b><span> pass lets each engine use the settings its own documentation recommends. Tuned is more realistic and less controlled, which is exactly why it doesn&rsquo;t replace the first one. A result that survives both passes is about the engine. One that flips between them is interesting for a completely different reason.</span></p>
<p><span>Cores are pinned explicitly. One logical CPU per physical core, because SMT siblings share execution units and two threads on one core don&rsquo;t behave like two cores. Never a mix of P-cores and E-cores on hybrid chips either, since migration between core types adds more variance than several of the effects we&rsquo;re trying to measure. Durability is relaxed the same way everywhere, or we&rsquo;d be comparing default fsync policies and calling it vector search.</span></p>
<p><span>Some differences can&rsquo;t be equalised at all, so we write them down instead of pretending. A knob only one engine exposes goes unused in the normalized pass, because using it would hand that engine a tuning axis nobody else has. An engine that insists on a particular isolation level gets it set for everyone. And defaults that are obviously placeholders get sized from a shared budget &mdash; one family of engines still ships a 16 MiB graph cache, which is nothing, and judging an engine on a value its own vendor expects you to change measures absolutely nothing. All of these land in a &ldquo;known asymmetries&rdquo; section above the results.</span></p>
<p><span>One hardware note that catches people out. Several of these implementations ship hand-written AVX-512 code for the distance maths, where a single instruction does the arithmetic for 16 floats at once. The same index on a CPU without AVX-512 is effectively a different benchmark, and the slowdown isn&rsquo;t the same for every engine, so you can&rsquo;t even scale the numbers to compensate. The CPU model and its feature flags go into every run&rsquo;s manifest for that reason, along with engine versions and commits, image IDs, and the resource limits as they are actually resolved rather than as we requested them. No manifest, no report.</span></p>
<h2><b>Reading the results</b><a class="anchor-link" id="reading-the-results"></a></h2>
<p><span>Read the validity section before you look at a single chart. Our reports go environment, then validity, then known asymmetries, then results, in that order on purpose. A failed phase, an engine returning short result sets, a CPU missing the instruction set the engines wanted &mdash; all of it lands in front of you before you&rsquo;ve formed an opinion.</span></p>
<p><span>The thing to watch for is the silent full scan.</span></p>
<p><span>Any of these engines will quietly stop using the vector index and scan the table instead. A scan returns exact results, slowly, so in the output it looks like high recall and low throughput. That&rsquo;s indistinguishable from a conservatively tuned index unless you go and read the query plan.</span></p>
<p><span>It happens for thoroughly boring reasons. One engine&rsquo;s optimizer costs the vector index against a table scan and takes the scan once the LIMIT is above roughly a quarter of the table, and we still haven&rsquo;t found a setting that moves it. Another falls back with no error and no warning when the query asks for a different distance than the index was built for &mdash; build the index for cosine, write the query with the L2 operator, and you get a sequential scan and a sort, with nothing anywhere to tell you.</span></p>
<p><span>So every driver runs EXPLAIN for each configuration and checks the index name appears in the plan.</span></p>
<p><span>WARNING: vector index NOT used (k=10, filtered=True). Plan: &hellip;Seq Scan&hellip;</span></p>
<p><span>Anything that is scanned goes into validity. This is far and away the easiest way to produce impressive vector benchmark numbers by accident, and if a benchmark doesn&rsquo;t mention checking for it, we&rsquo;d want to know why before believing anything in it.</span></p>
<p><span>For recall against throughput, the useful presentation is a curve rather than a number. Iterate </span><span>ef_search</span><span>, plot recall against QPS, keep the best points: for each level of accuracy, the highest throughput anything reached at it. One engine beats another only where its curve sits above the other&rsquo;s at the same recall. If the curves cross, then the answer genuinely depends on how accurate you need to be, and saying so is a result rather than a dodge.</span></p>
<p><span>Curves do invite comparing shapes instead of heights at one point, so there are bar charts as well, QPS at recall floors of 0.90, 0.95 and 0.99. Pick the accuracy you&rsquo;d actually accept and read across.</span></p>
<h2><b>Things that went wrong while we built this</b><a class="anchor-link" id="things-that-went-wrong-while-we-built-this"></a></h2>
<p><span>Worth listing, partly because they&rsquo;re the reason to trust anything else here, and partly because anyone building something similar will walk into them.</span></p>
<p><span>Our first ingest numbers were garbage. The load path was doing one INSERT per network round trip with autocommit on, and we measured 88 rows a second. Batching 500 rows per transaction took the same engine to 373. Publishing the first number would have been benchmarking our own client and calling it a database.</span></p>
<p><span>Filtered search and churn were scored against full-corpus ground truth even on runs that used a subset of rows. Every engine looked bad and the bug was entirely ours. Ground truth is now keyed on dataset, k, row count and selectivity.</span></p>
<p><span>Both resource passes shared one results directory, and the ANN runner skips configurations that already have results. So the tuned pass quietly skipped everything the normalized pass had computed, and our tuned numbers were mostly normalized numbers wearing a different label. That one took an embarrassingly long time to notice.</span></p>
<p><span>Readiness probes lie. One engine&rsquo;s standard &ldquo;are you accepting connections&rdquo; check returns success before the database it&rsquo;s supposed to create actually exists. The probe passed, the first query failed, and we spent a while convinced it was an engine problem.</span></p>
<p><span>The most recent one, on a 1536-dimension corpus. The ANN runner holds the whole dataset in memory twice, once in the parent process and again in a forked worker, and the copies aren&rsquo;t shared. That&rsquo;s roughly 12 GB for a million embeddings, on top of whatever the server is using, in a container we&rsquo;d sized for the server alone. The kernel killed the worker. The runner doesn&rsquo;t check worker exit codes, so it logged &ldquo;Terminating 1 workers&rdquo;, exited successfully and wrote no results &mdash; which looks exactly like a run that had nothing left to do. Three hours to fail, and it failed silently.</span></p>
<h2><b>Adding a database</b><a class="anchor-link" id="adding-a-database"></a></h2>
<p><span>This is the part we cared most about getting right, because the whole point was to avoid rebuilding the apparatus every time somebody ships vector search. Each engine needs:</span></p>
<ul>
<li><span>a Dockerfile producing a runtime image and a test image from a pinned version</span></li>
<li><span>a config declaring ports, credentials, and which server settings map onto the normalized CPU and memory budget</span></li>
<li><span>a module for the recall and throughput side</span></li>
<li><span>a driver: create index, load, query, filtered query, index size, and the EXPLAIN check</span></li>
</ul>
<h2><b>What&rsquo;s next</b><a class="anchor-link" id="whats-next"></a></h2>
<p><span>Results, published with the manifests and the raw per-configuration records, so you can check them instead of taking our word for it.</span></p>
<p><span>Everything is at</span> <a href="https://github.com/Percona-Lab/vector-bench"><span>https://github.com/Percona-Lab/vector-bench</span></a><span> harness, drivers, Dockerfiles, docs. If we&rsquo;re measuring something wrong, or being unfair to an engine you know better than we do, tell us.</span></p>
<p>The post <a href="https://www.percona.com/blog/benchmarking-vector-indexes/">Benchmarking vector indexes</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/benchmarking-vector-indexes/">Benchmarking vector indexes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Performance Progression of Percona Server for MySQL 8.4</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/performance-progression-of-percona-server-for-mysql-8-4/" />
      <id>https://www.percona.com/blog/performance-progression-of-percona-server-for-mysql-8-4/</id>
      <updated>2026-08-27T13:07:20+03:00</updated>
      <author><name>Bogdan Degtyariov</name></author>
      <summary type="html"><![CDATA[<p>1. Purpose and scope This performance investigation aims to look into the read/write performance of Percona Server for MySQL 8.4 and how it changed between versions released in 2026: 8.4.8-8 released on 12 March 2026 8.4.10-10 released on 30 June 2026 8.4.11-11 released on 20 August 2026 We want to see if there are improvements … Continued<br />
The post Performance Progression of Percona Server for MySQL 8.4 appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/performance-progression-of-percona-server-for-mysql-8-4/">Performance Progression of Percona Server for MySQL 8.4</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2><span>1. Purpose and scope</span><a class="anchor-link" id="1-purpose-and-scope"></a></h2>
<p><span>This performance investigation aims to look into the read/write performance of Percona Server for MySQL 8.4 and how it changed between versions released in 2026:</span></p>
<ul>
<li><span>8.4.8-8 released on 12 March 2026</span></li>
<li><span>8.4.10-10 released on 30 June 2026</span></li>
<li><span>8.4.11-11 released on 20 August 2026</span></li>
</ul>
<p><span>We want to see if there are improvements in scalability and performance in OLTP read/write operations, where the improvements are most noticeable and how they were achieved. For some readers this material might help with making the decision whether upgrading to a newer version is worth the effort.</span></p>
<p><span>An important note is that the new features or security patches will not be taken into consideration.</span></p>
<p><span>Measuring Latency (Percentiles) and Resource Utilization (CPU, RAM, I/O) is not in the scope of this post.</span></p>
<p>&nbsp;</p>
<h2><span>2. Configuration and Methodology</span><a class="anchor-link" id="2-configuration-and-methodology"></a></h2>
<p><span>The configuration was as follows:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Benchmark</span></td>
<td><span>Sysbench OLTP Read-Write</span></td>
</tr>
<tr>
<td><span>CPU</span></td>
<td><span>Intel Xeon Gold 6230 (2&times;20 cores, HT = 80 logical CPUs)</span></td>
</tr>
<tr>
<td><span>RAM</span></td>
<td><span>187 GiB DDR4</span></td>
</tr>
<tr>
<td><span>Storage</span></td>
<td><span>NVMe SSD (2.9 TB) INTEL SSDPE2KE032T8</span></td>
</tr>
<tr>
<td><span>OS</span></td>
<td><span>Ubuntu 24.04, kernel 6.8.0-60-generic</span></td>
</tr>
<tr>
<td><span>DB Engines</span></td>
<td><span>Percona Server for MySQL 8.4.8-8 (release build)</span><span><br>
</span><span>Percona Server for MySQL 8.4.10-10 (release build)</span><span>Percona Server for MySQL 8.4.11-11 (release build)</span></td>
</tr>
</tbody>
</table>
<p><span>The benchmarks were done across the following dimensions:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Database Sizes (Row Number)</span></td>
<td><span>24Gb (100M rows) / 48Gb (200M rows) / 96Gb (400M rows)</span></td>
</tr>
<tr>
<td><span>Number of tables in DB Schema</span></td>
<td><span>20 (this number is constant for all runs)</span>
<p><span>Database Schema definition can be downloaded from here:&nbsp;</span></p>
<p><a href="https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql" target="_blank" rel="noopener"><span>https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql</span></a></p>
</td>
</tr>
<tr>
<td><span>Number of concurrent threads</span></td>
<td><span>1 / 4 / 16 / 32 / 64 / 128 / 256 / 512</span></td>
</tr>
<tr>
<td><span>Buffer to Data Ratio</span></td>
<td><span>1:12 (I/O bound), 1:2 (Partially buffered), 1:1 (Fully buffered)</span></td>
</tr>
</tbody>
</table>
<p><span>One of the points in benchmarking was to create combinations of similar Buffer to Data Ratios, but with the different Database Sizes. This gives us the following possible combinations of </span><span>innodb_buffer_pool_size</span><span> and Database Size:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>1:12 (I/O bound)</span></td>
<td><span>innodb_buffer_pool_size = 2G, Data Size = </span><span>24Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 4G, Data Size = </span><span>48Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 8G, Data Size = </span><span>96Gb</span></td>
</tr>
<tr>
<td><span>1:2 (Partially buffered)</span></td>
<td><span>innodb_buffer_pool_size = 12G, Data Size = </span><span>24Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 24G, Data Size = </span><span>48Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 48G, Data Size = </span><span>96Gb</span></td>
</tr>
<tr>
<td><span>1:1 (Fully buffered)</span></td>
<td><span>innodb_buffer_pool_size = 32G, Data Size = </span><span>24Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 64G, Data Size = </span><span>48Gb</span><span><br>
</span><span>innodb_buffer_pool_size = 128G, Data Size = </span><span>96Gb</span></td>
</tr>
</tbody>
</table>
<p><span>We should be able to see how efficiently the server manages an increasingly larger number of rows while keeping the Buffer to Data Ratio the same.</span></p>
<p><span>Execution of the benchmarks was done as follows:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td><span>Ramp-up</span></td>
<td><b>24G &ndash; 600 sec (10 min)</b><span> &ndash; could be shorter</span><span><br>
</span><b>48G &ndash; 600 sec (10 min)</b><b>96G &ndash; 900 sec (15 min)</b><span><br>
</span><span><br>
</span><span>The Ramp-up times were established experimentally depending on the Data Size until the point when increasing them further did not bring significant changes.</span></td>
</tr>
<tr>
<td><span>Measurement window</span></td>
<td><b>900 sec (15 min)</b><span><br>
</span><span><br>
</span><span>Ideally it should be as long as possible, but measurements should take reasonable time. Hence, we used the experience of previous benchmarks and established that this window is adequate for the purpose.</span></td>
</tr>
<tr>
<td><span>Number of runs</span></td>
<td><b>3</b>
<p><span>For each combination there are multiple runs.</span><span><br>
</span><span>The interactive graph can show data for individual runs as well as averaged value.</span></p>
</td>
</tr>
</tbody>
</table>
<p><span>Important Database Configuration options (the actual config files with specific settings for each run can be downloaded from the interactive graphs):</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>InnoDB &ndash; Buffer pool Tier</b></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_size</span></td>
<td><b>2G/4G/8G/12G/24G/32G/48G/64G/128G</b></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_load_at_startup</span></td>
<td><span>OFF</span></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_dump_at_shutdown</span></td>
<td><span>OFF</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Thread Pool</b></td>
</tr>
<tr>
<td><span>thread_handling</span></td>
<td><span>pool-of-threads</span></td>
</tr>
<tr>
<td><span>thread_pool_size</span></td>
<td><span>80 # match physical core count</span></td>
</tr>
<tr>
<td><span>thread_pool_max_threads</span></td>
<td><span>2000</span></td>
</tr>
<tr>
<td><span>thread_pool_oversubscribe</span></td>
<td><span>3</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Threading</b></td>
</tr>
<tr>
<td><span>thread_stack</span></td>
<td><span>512K</span></td>
</tr>
<tr>
<td><span>thread_cache_size</span></td>
<td><span>256</span></td>
</tr>
<tr>
<td><span>back_log</span></td>
<td><span>4096</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>InnoDB I/O</b></td>
</tr>
<tr>
<td><span>innodb_io_capacity</span></td>
<td><span>10000</span></td>
</tr>
<tr>
<td><span>innodb_io_capacity_max</span></td>
<td><span>20000</span></td>
</tr>
<tr>
<td><span>innodb_read_io_threads</span></td>
<td><span>16</span></td>
</tr>
<tr>
<td><span>innodb_write_io_threads</span></td>
<td><span>16</span></td>
</tr>
<tr>
<td><span>innodb_use_native_aio</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>InnoDB Log / Durability</b></td>
</tr>
<tr>
<td><span>innodb_log_buffer_size</span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>innodb_flush_log_at_trx_commit</span></td>
<td><span>1 # full ACID</span></td>
</tr>
<tr>
<td><span>innodb_doublewrite</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>InnoDB &ndash; Concurrency &amp; OLTP Tuning</b></td>
</tr>
<tr>
<td><span>innodb_stats_on_metadata</span></td>
<td><span>OFF</span></td>
</tr>
<tr>
<td><span>innodb_open_files</span></td>
<td><span>65536</span></td>
</tr>
<tr>
<td><span>innodb_lock_wait_timeout</span></td>
<td><span>50</span></td>
</tr>
<tr>
<td><span>innodb_rollback_on_timeout</span></td>
<td><span>ON</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Per-Session Buffers</b></td>
</tr>
<tr>
<td><span>sort_buffer_size&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>join_buffer_size&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>read_buffer_size&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>2M</span></td>
</tr>
<tr>
<td><span>read_rnd_buffer_size</span></td>
<td><span>4M</span></td>
</tr>
<tr>
<td><span>tmp_table_size&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>max_heap_table_size</span></td>
<td><span>256M</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Binary Log</b></td>
</tr>
<tr>
<td><span>disable_log_bin</span></td>
<td><span>ON # Disabled binlog</span></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Other InnoDB settings</b></td>
</tr>
<tr>
<td><span>innodb_redo_log_capacity&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>4G</span></td>
</tr>
<tr>
<td><span>innodb_change_buffering&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>none</span></td>
</tr>
<tr>
<td><span>innodb_flush_method&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>O_DIRECT</span></td>
</tr>
<tr>
<td><span>innodb_buffer_pool_instances</span></td>
<td><b>Calculated as</b><b><br>
</b><b>(innodb_buffer_pool_size G / 5)</b><b><br>
</b><b>But must be in range [1..8]</b></td>
</tr>
<tr bgcolor="#DDDDDD">
<td colspan="2"><b>Misc server settings</b></td>
</tr>
<tr>
<td><span>collation_server</span></td>
<td><span>utf8mb4_unicode_ci</span></td>
</tr>
<tr>
<td><span>bulk_insert_buffer_size</span></td>
<td><span>256M</span></td>
</tr>
<tr>
<td><span>myisam_sort_buffer_size&nbsp;</span></td>
<td><span>128M</span></td>
</tr>
<tr>
<td><span>key_buffer_size&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></td>
<td><span>64M # MyISAM only, keep small for OLTP</span></td>
</tr>
</tbody>
</table>
<p><span>In the high concurrency scenario when all CPU cores are working under maximum load the performance fluctuations might appear out of the ability of a specific CPU crystal to work at a specific sustainable maximum frequency. Intel Xeon Gold 6230 processors installed in the test servers have a base frequency of 2100 MHz and maximum turbo frequency of 3900 MHz. However, such turbo frequency can only be achieved for a short period of time on an isolated core. The load and the heat production of the physical core neighbours limit the frequency of the whole CPU. Some CPU&rsquo;s were able to hold 2530 MHz on all cores for 20+ hours of intense load, others could only reach 2420 MHz. For consistency of the tests the turbo frequency was capped to 2400 MHz from the beginning on all servers. It helped to eliminate the struggle between turbo mode trying to increase the frequency beyond sustainable levels and the CPU thermal protection bringing the clock down. More stable hardware performance reduced the measurement fluctuations during the benchmark runs regardless if they were done on the same or a different physical server.</span></p>
<p>&nbsp;</p>
<h2><span>3. Results</span><a class="anchor-link" id="3-results"></a></h2>
<p><span>First, let&rsquo;s check the I/O bound scenario where the InnoDB Buffer to Data Size is the smallest (1:12).</span></p>
<p><span>The graph shows the configurations with </span><span>innodb_buffer_pool_size</span><span>=8G and Data Size 96G (or 20M rows per table, 400M rows in total):</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=8" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-52471" src="https://www.percona.com/wp-content/uploads/2026/08/graph-8g.png" alt="" width="1013" height="621"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=8" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=8" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>The first thing that catches the eye is the hugely superior performance of the version 8.4.11-11 over 8.4.10-10 and 8.4.8-8 in the high thread numbers. In the situations when the number of physical cores (80) is smaller than the number of threads (128+) the versions 8.4.10-10 and 8.4.8-8 have a steep performance degradation. However, the TPS for 8.4.11-11 keeps growing. This is due to the optimization done to InnoDB LRU pages flushing algorithm. The optimization specifically targeted the scenario when the data size is larger than the available server buffers and the server has many concurrent connections doing random read-write operations. The optimizations in 8.4.11-11 deserve a separate explanation and they will be published in another blog post.</span><span><br>
</span></p>
<p><span>The less noticeable, but important difference can be spotted between the TPS for 8.4.8-8 and 8.4.10-10.</span></p>
<p><span>The version 8.4.10-10 shows better performance (especially at the saturation point with 64 threads), which should mostly be attributed to the introduction of Performance Guided Optimization (PGO).&nbsp;</span></p>
<p><span>More information on PGO can be found here:</span></p>
<p><a href="https://docs.percona.com/percona-server/8.4/pgo.html"><span>https://docs.percona.com/percona-server/8.4/pgo.html</span></a></p>
<p><span>With the smaller data and buffer sizes the performance difference gives an almost identical picture:</span></p>
<table border="1" width="100%" cellpadding="5">
<tbody>
<tr>
<td width="50%"><span>4G buffer, 48G data </span><span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=4" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=4" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=4" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="alignnone wp-image-52475 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/graph-4g.png" alt="" width="1007" height="623"></a></td>
<td width="50%"><span>2G buffer, 24G data </span><span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=2" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=2" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=2" target="_blank" rel="noopener"><img loading="lazy" decoding="async" class="alignnone wp-image-52474 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/graph-2g.png" alt="" width="1013" height="623"></a></td>
</tr>
</tbody>
</table>
<p><span>Now let&rsquo;s review what happens with the ratio 1:2.</span><span><br>
</span><span>This time the buffer pool size also plays a more significant role and the performance difference is not characterized by the Buffer / Data size ratio.</span></p>
<p><span>With </span><span>innodb_buffer_pool_size</span><span>=12G and 24G data size the performance gap between 8.4.11-11 and older versions is still huge as can be seen on the graph:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=12" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52481" src="https://www.percona.com/wp-content/uploads/2026/08/graph-12g.png" alt="" width="1012" height="624"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=12" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=12" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>However, setting </span><span>innodb_buffer_pool_size</span><span>=24G and 48G data size reduces the gap. The superiority of 8.4.11-11 is still visible:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=12" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52482" src="https://www.percona.com/wp-content/uploads/2026/08/graph-24g.png" alt="" width="1012" height="622"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=24" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=24" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>Moving to </span><span>innodb_buffer_pool_size</span><span>=48G and 96G data size shrinks the gap even more:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=48" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52485" src="https://www.percona.com/wp-content/uploads/2026/08/graph-48g.png" alt="" width="1014" height="626"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=48" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=48" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>In this post we are not going to talk about mechanisms behind shrinking performance gaps in 1:2 Buffer / Data size ratio.</span></p>
<p><span>Holding the entire data set in memory is not the most common thing for the database server, but in some cases it happens. Therefore, we are covering such situations as well.</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=32" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52487" src="https://www.percona.com/wp-content/uploads/2026/08/graph-32g.png" alt="" width="1010" height="625"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=32" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=32" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>As the above graph shows, 8.4.10-10 is slightly ahead of 8.4.11-11, but the gap is very small.</span></p>
<p><span>This behavior is consistent with other data sizes for fully buffered data:</span></p>
<p><span>innodb_buffer_pool_size=64G and 48G Data Size:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=64" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52488" src="https://www.percona.com/wp-content/uploads/2026/08/graph-64g.png" alt="" width="1010" height="620"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=64" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=64" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>innodb_buffer_pool_size=128G and 96G Data Size:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=128" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52490" src="https://www.percona.com/wp-content/uploads/2026/08/graph-128g.png" alt="" width="1014" height="623"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=graph&amp;mem=128" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona.html?display=table&amp;mem=128" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>Again, we will not go into details about why this happens. Though it is worth noting that both 8.4.10-10 and 8.4.11-11 do better than 8.4.8-8 in all runs and configurations.</span></p>
<p><span>The table interpretation of the results is available as well.</span></p>
<p>&nbsp;</p>
<h2><span>4. Comparing with Upstream MySQL 8.4.11.</span><a class="anchor-link" id="4-comparing-with-upstream-mysql-8-4-11"></a></h2>
<p><span>The performance improvements in Percona Server for MySQL 8.4.11-11 are not a part of the Upstream MySQL 8.4.11. The patch was specifically designed to address the issue of Percona Server being slower than MySQL in I/O bound scenarios.</span></p>
<p><span>Also, the patch eliminated the abrupt performance degradation in the higher thread count after reaching the saturation point at 64 threads:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=graph&amp;mem=12" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52500" src="https://www.percona.com/wp-content/uploads/2026/08/graph-m-12g.png" alt="" width="1015" height="624"></a></p>
<p><span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=graph&amp;mem=12" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=table&amp;mem=12" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<p><span>As the graph shows &ndash; Percona Server 8.4.8-8 / 8.4.10-10 was slower than MySQL in lower thread count. Although it was still faster in 128+ threads, the Percona Server was still subject to a substantial slow-down. That is where Percona Server 8.4.11-11 really shines.</span></p>
<p><span>However, with the fully buffered data MySQL goes faster than any Percona Server:</span></p>
<p><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=graph&amp;mem=64" target="_blank" rel="noopener"><img decoding="async" loading="lazy" class="alignnone size-full wp-image-52501" src="https://www.percona.com/wp-content/uploads/2026/08/graph-m-64g.png" alt="" width="1011" height="624"></a><br>
<span>[ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=graph&amp;mem=64" target="_blank" rel="noopener"><span>INTERACTIVE GRAPH</span></a><span> ][ </span><a href="https://percona-lab-results.github.io/ps-mysql-versions-perf/sysbench_percona_mysql.html?display=table&amp;mem=64" target="_blank" rel="noopener"><span>TABLE</span></a><span> ]</span></p>
<h2><span>5. Summary</span><a class="anchor-link" id="5-summary"></a></h2>
<p><span>The Performance of the Percona Server 8.4 for MySQL is progressing well from older to newer version offering significant performance improvements especially in the version 8.4.11-11. This version shows very significant improvements in performance on the data sets that require I/O. Also, it outperformed the upstream MySQL 8.4.11.</span></p>
<p><span>With fully buffered data sets the version 8.4.10-10 is slightly better than 8.4.11-11. MySQL Server in this case shows the fastest performance.</span></p>
<p><span>The PGO had a positive impact demonstrating the version 8.4.10-10 being faster in all tests on all configurations than 8.4.8-8.</span></p>
<p><span>The performance depends not only on the ratio between the buffer and the data size, but also on the buffer size.</span></p>
<p>The post <a href="https://www.percona.com/blog/performance-progression-of-percona-server-for-mysql-8-4/">Performance Progression of Percona Server for MySQL 8.4</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/performance-progression-of-percona-server-for-mysql-8-4/">Performance Progression of Percona Server for MySQL 8.4</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>DuckDB Speed on MySQL with dbtrail, Without a New Storage Engine</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/08/26/duckdb-speed-on-mysql-without-a-new-storage-engine/" />
      <id>https://percona.community/blog/2026/08/26/duckdb-speed-on-mysql-without-a-new-storage-engine/</id>
      <updated>2026-08-26T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Two recent posts on the Percona blog caught my attention. In Replicating from InnoDB into a DuckDB storage engine and The DuckDB MySQL engine at 500 GB, Evgeniy Patlan wired DuckDB into mysqld as a storage engine, pointed row-based replication at it, and measured it at TPC-H scale factor 500. The numbers are striking: loads 25 times faster than InnoDB, one fifth of the disk, and the full 22-query TPC-H suite done in 186 seconds where InnoDB needed about 28 hours and never finished four of the queries.</p>
<p><a href="https://percona.community/blog/2026/08/26/duckdb-speed-on-mysql-without-a-new-storage-engine/">DuckDB Speed on MySQL with dbtrail, Without a New Storage Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Two recent posts on the Percona blog caught my attention. In <a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/" target="_blank" rel="noopener noreferrer">Replicating from InnoDB into a DuckDB storage engine</a> and <a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/" target="_blank" rel="noopener noreferrer">The DuckDB MySQL engine at 500 GB</a>, Evgeniy Patlan wired DuckDB into mysqld as a storage engine, pointed row-based replication at it, and measured it at TPC-H scale factor 500. The numbers are striking: loads 25 times faster than InnoDB, one fifth of the disk, and the full 22-query TPC-H suite done in 186 seconds where InnoDB needed about 28 hours and never finished four of the queries.</p>
<p>The posts are also honest about the cost. The work is experimental. The worst bug found was silent data loss on every replicated transaction: the two-phase commit code for mixed-engine transactions dropped prepared DuckDB transactions before they committed, so replication reported success while no rows persisted. The test harness caught it and it was fixed. The row-by-row applier also cannot keep up with bulk writes on the primary.</p>
<p>I want to add a second road to the same place, one that changes nothing inside mysqld. Disclosure first: I build <a href="https://github.com/dbtrail/dbtrail" target="_blank" rel="noopener noreferrer">dbtrail</a>, the Apache 2.0 open source tool used below, so read this as one more point in the design space, from someone who is not neutral. Everything here ran on stock Percona Server 8.0 from Docker Hub and stock DuckDB from Homebrew, and the commands at the end reproduce all of it.</p>
<h2>Your binlog is already a columnar feed<a class="anchor-link" id="your-binlog-is-already-a-columnar-feed"></a></h2>
<p>The storage engine approach puts the column store inside the server, so the server must cooperate: a patched build, a new engine in the commit path, replica tables created by hand with <code>ENGINE=DuckDB</code>. The other road starts from what MySQL already provides. With <code>binlog_format=ROW</code> and <code>binlog_row_image=FULL</code>, the binary log is a complete, ordered feed of every row change, with full before and after images. Any process can consume that feed as a replication client, the way a replica does, and install nothing on the server.</p>
<p>dbtrail is that process. It does two things with the feed:</p>
<ol>
<li>It keeps a short, searchable window of recent events in a plain MySQL table, for investigation and recovery.</li>
<li>It moves closed hours out to Parquet files, the open columnar format every analytical engine can read. DuckDB reads it natively.</li>
</ol>
<p>The pipeline is <code>mysqld -&gt; binlog -&gt; Parquet -&gt; DuckDB</code>. No patched server, no plugin, nothing new in the commit path. And because the feed is history rather than current state, dbtrail can answer a question no replica can: what did this row look like before, and how do I undo what happened to it?</p>
<h2>The demo<a class="anchor-link" id="the-demo"></a></h2>
<p>One container for the source, stock image, with the settings any change-capture consumer needs (plus one line to keep the demo&rsquo;s own index out of the binlog, since source and index share one server here):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d --name demo -p 13310:3306 -e <span class="nv">MYSQL_ROOT_PASSWORD</span><span class="o">=</span>demo <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> percona/percona-server:8.0 <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --gtid-mode<span class="o">=</span>ON --enforce-gtid-consistency<span class="o">=</span>ON <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --binlog-rows-query-log-events<span class="o">=</span>ON --binlog-ignore-db<span class="o">=</span>bintrail_index</span></span></code></pre>
</div>
</div>
</div>
<p><code>binlog_format=ROW</code> and <code>binlog_row_image=FULL</code> are already the 8.0 defaults. The schema is a small shop: <code>customers</code>, <code>orders</code>, <code>order_items</code>. Three short commands point dbtrail at it (<code>init</code>, <code>snapshot</code>, <code>stream</code>; they are in the last section), and from there the work happens in dbtrail&rsquo;s web console. Then I replayed a five-hour synthetic workload: 3,265,000 row events in all. 2.7 million INSERTs, 510 thousand UPDATEs, 55 thousand DELETEs. (A tip for demo builders: <code>SET TIMESTAMP = </code> in the writing session backdates the binlog event timestamps, so a fast replay spreads across past hours and you can watch retention behave as it would in real life.)</p>
<p>The console&rsquo;s Status view answers the first question an operator should ask of any change-capture pipeline: did we miss anything?</p>
<p><figure><img decoding="async" width="1440" height="820" src="https://percona.community/blog/2026/08/dbtrail-console-status_hu_dc6797ddbe100111.webp" alt="dbtrail console Status view: a green no-gaps verdict, 3,265,000 events captured, and an archive tier of 7 Parquet files totaling 26 MB" loading="lazy"></figure>
</p>
<p>The green banner is a verdict, not a guess: dbtrail tracks continuity across the captured range, and it says plainly that this is not a liveness check. On my MacBook the capture stream held about 18,000 events per second while it tailed the binlog. That answers the applier problem from the first post: dbtrail does not push rows through the storage engine API one at a time, it batches them into an index, so bulk writes on the primary are absorbed rather than queued.</p>
<h2>From hot partitions to Parquet<a class="anchor-link" id="from-hot-partitions-to-parquet"></a></h2>
<p>The index table is range-partitioned by hour. dbtrail archives each closed hour to zstd-compressed Parquet, checks the result, and only then drops the partition. Retention on the expensive tier becomes hours, not months. The demo moved all 3.26 million events out in 14 seconds, and the Status view above already showed the result: 7 archive files, 26 MB. Here is that number next to what the same events cost in InnoDB:</p>
<p><figure><img decoding="async" width="2160" height="780" src="https://percona.community/blog/2026/08/duckdb-mysql-storage_hu_f49bae55fc9733cb.webp" alt="Bar chart: the same 3,265,000 events take 2,822 MB as an InnoDB table and 26 MB as zstd Parquet" loading="lazy"></figure>
</p>
<p>Do not take that 100x as a general truth. Synthetic demo data repeats itself and compresses far too well, and the InnoDB figure includes the primary key and secondary indexes that make the hot tier searchable. On production data expect about one order of magnitude. The direction matches what Evgeniy measured at 500 GB, where the DuckDB engine held TPC-H in 26 percent of the raw CSV size and InnoDB needed 135 percent. Column formats fit this data. Row formats do not.</p>
<p>One <code>mydumper</code> pass adds the state side: a baseline snapshot of the tables themselves, also stored as Parquet. For the demo shop that was 2.6 million rows in 8 MB, done in under four seconds. dbtrail uses baselines to rebuild full tables and single rows at a point in time; here they also give DuckDB something current to query.</p>
<h2>Stock DuckDB, no plugins<a class="anchor-link" id="stock-duckdb-no-plugins"></a></h2>
<p>The archive is just files, so the query engine does not have to be dbtrail. One command, <code>bintrail views</code> (bintrail is the name of dbtrail&rsquo;s CLI binary), writes a single SQL file of DuckDB view definitions over the layout it knows about: an <code>events</code> view across every archived partition, and one <code>state__</code></p>
<table> view per table of the newest baseline:
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">bintrail views --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --baseline-dir /data/baselines --out views.sql
</span></span><span class="line"><span class="cl">duckdb
</span></span><span class="line"><span class="cl">D .read views.sql</span></span></code></pre>
</div>
</div>
</div>
<p>That is the whole integration. dbtrail never opens DuckDB and never runs what it prints; the file is plain SQL you can read before you use it. From there, use any DuckDB you like: the CLI, a notebook, a BI tool&rsquo;s connector. Same laptop, same data, same queries on both engines:</p>
<p><figure><img decoding="async" width="2160" height="1456" src="https://percona.community/blog/2026/08/duckdb-mysql-query-latency_hu_65d4d0d857c5dd52.webp" alt="Dot plot on a log scale, two groups: three queries over the event history and two over current state; MySQL InnoDB takes 0.4 to 4.6 seconds, stock DuckDB over Parquet takes 0.04 to 0.16 seconds, 11 to 112 times faster" loading="lazy"></figure>
</p>
<p>Now the fine print, because a benchmark without it is just marketing. The chart has two groups. The first group queries the event history: those scans hit a 2.8 GB index with the container&rsquo;s stock 128 MB buffer pool, so InnoDB paid for disk reads, and a bigger pool narrows that gap. The second group runs over current state and is the fair comparison: I grew the pool to 4 GB, both tables sat fully in cache, and the 11x and 20x that remain come from the design of the engine, not from the disk. Both engines returned the same results, down to the last decimal. That is a free integrity check: two independent systems read the same history and agreed.</p>
<p>These are laptop numbers, and I did not run TPC-H. For the ceiling of what columnar execution does at 500 GB, read Evgeniy&rsquo;s second post. My point is the floor: 2.8 GB of freshly rotated history sat on my laptop as 26 MB of open files, and a stock engine answers questions over them in tens of milliseconds. Nobody patched anything to get here.</p>
<h2>The part a replica cannot do<a class="anchor-link" id="the-part-a-replica-cannot-do"></a></h2>
<p>A DuckDB replica holds current state. dbtrail holds what happened. This is the same data the analytics just scanned, now in the console&rsquo;s Events view, filtered to one order:</p>
<p><figure><img decoding="async" width="1493" height="812" src="https://percona.community/blog/2026/08/dbtrail-console-events-diff_hu_4818001f8a06f677.webp" alt="dbtrail console Events view: the three events of order 997000, INSERT then UPDATE then DELETE, with the UPDATE expanded to show full before and after images and an Undo this change button" loading="lazy"></figure>
</p>
<p>Three events tell the row&rsquo;s whole story: created, cancelled, deleted, each with its GTID and the connection that did it. The expanded UPDATE shows the full before and after images dbtrail keeps for every change.</p>
<p>Here is the detail that surprises people: when I took that screenshot, the MySQL side of the index held zero rows. Rotation had dropped every partition. The console read the answer from the Parquet tier in under 200 milliseconds. The Undo button works from there too:</p>
<p><figure><img decoding="async" width="1493" height="812" src="https://percona.community/blog/2026/08/dbtrail-console-restore-undo_hu_a6b3c278c6e1ea4a.webp" alt="dbtrail console Restore view: one click on Undo this change produced reversal.sql, a reviewed-before-applied script that reverses exactly one UPDATE, with Copy and Download buttons" loading="lazy"></figure>
</p>
<p>One click wrote <code>reversal.sql</code>: a script that puts the row back exactly as it was, tagged with the GTID it reverses. Nothing runs on its own; you read the script, then you apply it. The same Restore view takes a whole table and a time window: the demo&rsquo;s 5,000 deleted orders came back as 5,000 INSERT statements, generated in 0.2 seconds, from files, while the database that held that history no longer existed.</p>
<p>A column store fed by your binlog does not have to be a replica. It can be a time machine.</p>
<h2>Trade-offs, honestly<a class="anchor-link" id="trade-offs-honestly"></a></h2>
<p>Neither road wins outright.</p>
<p><strong>Where the storage engine is better: its DuckDB copy is always current, and it speaks MySQL protocol.</strong> A DuckDB engine on a replica runs seconds behind the primary, and existing BI tools connect to it unchanged. dbtrail&rsquo;s capture also runs seconds behind, and its console and CLI query that fresh index directly. What waits is the copy DuckDB reads: the events view covers the hours already rotated to Parquet, and the state views show the latest baseline, so that side is as current as your rotation and baseline schedule. If you need a BI tool on MySQL protocol reading a complete, current copy, Evgeniy&rsquo;s architecture, or a product like HeatWave, aims at exactly that.</p>
<p><strong>Where staying outside the server is better: risk and history.</strong> The hardest place to change a database is the commit path, and a storage engine lives there. The 2PC bug shows the kind of failure that layer produces, and it took a dedicated test harness to find it, because it was silent. A replication client cannot corrupt a commit. Its failure modes are lag and gaps, and dbtrail reports gaps as a first-class verdict rather than hoping. History is the other half: audit, point-in-time rebuilds, and row-level undo all come from the same files the analytics read.</p>
<p><strong>Shared constraints.</strong> Both roads need <code>ROW</code> format with <code>FULL</code> row images. Both need primary keys to apply or reverse UPDATEs and DELETEs. Both leave the source of truth in InnoDB, untouched. On maturity: the engine posts describe an experiment and say so; dbtrail is released and versioned (v0.65.0 as I write this) but young, and you should question my numbers the same way I question everyone else&rsquo;s.</p>
<p>Both posts and this one agree on the base facts: row stores are the wrong shape for analytical scans, DuckDB fits MySQL-shaped data well, and the open question is where the column store should live. Evgeniy shows what you gain when the server cooperates. dbtrail shows what you get when you leave the server alone.</p>
<h2>Reproduce it<a class="anchor-link" id="reproduce-it"></a></h2>
<p>dbtrail binaries are on the <a href="https://github.com/dbtrail/dbtrail/releases" target="_blank" rel="noopener noreferrer">releases page</a>; DuckDB comes from your package manager. The web console ships as its own binary, <code>bintrail-console</code>, and can also run capture and console together as one daemon (<code>bintrail-console watch</code>).</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. A source with binlogs (ROW + FULL are 8.0 defaults)</span>
</span></span><span class="line"><span class="cl">docker run -d --name demo -p 13310:3306 -e <span class="nv">MYSQL_ROOT_PASSWORD</span><span class="o">=</span>demo <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> percona/percona-server:8.0 --gtid-mode<span class="o">=</span>ON --enforce-gtid-consistency<span class="o">=</span>ON <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --binlog-rows-query-log-events<span class="o">=</span>ON --binlog-ignore-db<span class="o">=</span>bintrail_index
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 2. Create your schema, then point dbtrail at it</span>
</span></span><span class="line"><span class="cl">bintrail init --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl">bintrail snapshot --source-dsn <span class="s2">"</span><span class="nv">$SRC</span><span class="s2">"</span> --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --schemas shop
</span></span><span class="line"><span class="cl">bintrail stream --source-dsn <span class="s2">"</span><span class="nv">$SRC</span><span class="s2">"</span> --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --server-id <span class="m">4444</span> --schemas shop <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">bintrail-console serve --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --baseline-dir /data/baselines <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Run your workload, then tier the history out and take a baseline</span>
</span></span><span class="line"><span class="cl">bintrail rotate --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --retain 7d --archive-dir /data/archives
</span></span><span class="line"><span class="cl">bintrail dump --source-dsn <span class="s2">"</span><span class="nv">$SRC</span><span class="s2">"</span> --output-dir /data/dump --schemas shop
</span></span><span class="line"><span class="cl">bintrail baseline --input /data/dump --output /data/baselines
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 4. Hand the whole thing to DuckDB</span>
</span></span><span class="line"><span class="cl">bintrail views --index-dsn <span class="s2">"</span><span class="nv">$IDX</span><span class="s2">"</span> --baseline-dir /data/baselines --out views.sql
</span></span><span class="line"><span class="cl">duckdb -c <span class="s2">".read views.sql"</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -c <span class="s2">"SELECT table_name, event_type, COUNT(*) FROM events GROUP BY 1,2;"</span></span></span></code></pre>
</div>
</div>
</div>
<p>If you try dbtrail, tell me where it fails as much as where it works well. Issues and pull requests are open at <a href="https://github.com/dbtrail/dbtrail" target="_blank" rel="noopener noreferrer">github.com/dbtrail/dbtrail</a>, and I am around in the Percona Community Slack.</p>
</table>
<p></p>

<p><a href="https://percona.community/blog/2026/08/26/duckdb-speed-on-mysql-without-a-new-storage-engine/">DuckDB Speed on MySQL with dbtrail, Without a New Storage Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Postgres + ClickHouse Architectural Patterns</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/postgres-clickhouse-architectural-patterns/" />
      <id>https://severalnines.com/blog/postgres-clickhouse-architectural-patterns/</id>
      <updated>2026-08-26T08:08:43+03:00</updated>
      <author><name>Agus Syafaat</name></author>
      <summary type="html"><![CDATA[<p>The role of databases has shifted significantly as modern applications must deliver real-time analytics, dashboards, and machine learning alongside low-latency transaction processing. Handling these diverse demands with a single relational database has become unsustainable under growing data volumes. Consequently, organizations are adopting specialized database architectures where multiple engines work together based on their strengths, allowing […]<br />
The post Postgres + ClickHouse Architectural Patterns appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/postgres-clickhouse-architectural-patterns/">Postgres + ClickHouse Architectural Patterns</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The role of databases has shifted significantly as modern applications must deliver real-time analytics, dashboards, and machine learning alongside low-latency transaction processing. Handling these diverse demands with a single relational database has become unsustainable under growing data volumes. Consequently, organizations are adopting specialized database architectures where multiple engines work together based on their strengths, allowing transactional and analytical workloads to coexist without competing for system resources.</p>
<p>This shift highlights the combination of <a href="https://severalnines.com/clustercontrol/databases/postgresql">PostgreSQL</a> and <a href="https://severalnines.com/clustercontrol/databases/clickhouse">ClickHouse</a> as a compelling solution for modern data platforms, with Postgres serving as the authoritative transactional system, and ClickHouse operating as a high-performance analytical platform for processing massive datasets in real time. Together, they function as complementary components connected through continuous Change Data Capture (CDC) synchronization. Before we get into the common architectural patterns, let&rsquo;s briefly look at why Postgres + ClickHouse.</p>
<h2 class="wp-block-heading">Why Postgres + ClickHouse?<a class="anchor-link" id="why-postgres-clickhouse"></a></h2>
<p>PostgreSQL excels at Online Transaction Processing (OLTP). Utilizing mature ACID compliance, MVCC, and advanced indexing, it serves as the operational system of record for managing concurrent transactions like accounts, finance, and inventory.</p>
<p>In contrast, analytical workloads like BI dashboards and fraud detection require scanning millions or billions of historical records. Running these massive sequential scans continuously on a production OLTP system increases CPU, memory, and latency, ultimately degrading application performance.</p>
<p>ClickHouse solves this issue as a column-oriented Online Analytical Processing (OLAP) database designed for rapid queries over massive datasets. Instead of replacing PostgreSQL, ClickHouse complements it by offloading complex analytical processing.<br>Consequently, architectural focus has shifted from choosing between the two platforms to determine how they can work together effectively. This reflects a trend toward polyglot persistence, where specialized databases collaborate to handle distinct transactional and analytical workloads.</p>
<h2 class="wp-block-heading">Pattern 1: Postgres to ClickHouse Real-Time Analytics<a class="anchor-link" id="pattern-1-postgres-to-clickhouse-real-time-analytics"></a></h2>
<p>The widely adopted PostgreSQL and ClickHouse architecture uses PostgreSQL as the transactional source of truth while continuously replicating data to ClickHouse for analytical processing. This separates workloads without complex ETL pipelines, allowing applications and BI tools to query ClickHouse directly. Because ClickHouse is optimized for large scans and aggregations, dashboards load significantly faster while keeping production PostgreSQL tables responsive and isolated from heavy reporting risks.</p>
<p>This approach is ideal for real-time systems like SaaS, fintech, and IoT platforms, where fast dashboard updates are a key part of the product experience.</p>
<h3 class="wp-block-heading">Architecture Overview<a class="anchor-link" id="architecture-overview"></a></h3>
<p>In a typical PostgreSQL and ClickHouse deployment, applications run transactional operations on PostgreSQL to ensure data consistency. Committed transactions are then replicated to ClickHouse via a Change Data Capture pipeline, enabling analytical queries to execute independently from the OLTP workload.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="753" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/1-753x1024.jpeg" alt="" class="wp-image-44506"></figure>
<p>By separating the transaction plane from the analytics plane, organizations allow each database to focus on the workload for which it was designed, improving scalability, reducing resource contention, and simplifying performance tuning.</p>
<h3 class="wp-block-heading">Keeping Analytics in Sync with Change Data Capture (CDC)<a class="anchor-link" id="keeping-analytics-in-sync-with-change-data-capture-cdc"></a></h3>
<p>This architecture uses Change Data Capture (CDC) to continuously synchronize transactional changes from PostgreSQL to ClickHouse. By capturing inserts, updates, and deletes directly from PostgreSQL&rsquo;s Write-Ahead Log (WAL) via logical decoding and replication, CDC avoids periodic ETL jobs, minimizing latency for near real-time operational dashboards and analytics.</p>
<p>Enabling this requires configuring the correct PostgreSQL WAL level and exposing individual tables via publications to stream changes.</p>
<pre class="wp-block-code"><code>ALTER SYSTEM SET wal_level = logical;

CREATE PUBLICATION app_events_pub
FOR TABLE
    orders,
    order_events,
    account_events;</code></pre>
<p>CDC services (such as ClickHouse, ClickPipes, or PeerDB) continuously replicate PostgreSQL changes to ClickHouse, reducing data delays and operational overhead compared to traditional batch ETL.</p>
<p>However, monitoring operational factors like replication slots, WAL retention, schema changes, connectivity, backfills, and replication lag is essential, making latency a critical component of real-time dashboard SLOs.</p>
<h3 class="wp-block-heading">Operational Readiness Checklist<a class="anchor-link" id="operational-readiness-checklist"></a></h3>
<p>Pre-production deployment of PostgreSQL alongside ClickHouse requires robust operations: identifying authoritative source tables, validating CDC pipelines, and setting replication lag alerts before dashboards rely on the data.</p>
<p>Schema design must transform normalized PostgreSQL tables into denormalized ClickHouse models, defining clear update/delete semantics for append-optimized engines like <code>ReplacingMergeTree</code>. Independently test backfills and run regular reconciliation jobs to detect sync issues early.</p>
<p>Finally, set clear data freshness objectives and monitor metrics like ingestion throughput, WAL growth, slot utilization, and latency, treating CDC as a production service to ensure a reliable platform.</p>
<h2 class="wp-block-heading">Pattern 2: Hot / Cold Time-Series Data<a class="anchor-link" id="pattern-2-hot-cold-time-series-data"></a></h2>
<p>In this architecture, PostgreSQL stores only recent operational data for transactional processing, while ClickHouse holds the complete historical record. A continuous CDC pipeline synchronizes changes into ClickHouse, enabling PostgreSQL to safely expire older data after verified replication.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="906" src="https://severalnines.com/wp-content/uploads/2026/08/2-1024x906.jpeg" alt="" class="wp-image-44507"></figure>
<h3 class="wp-block-heading">Time-Series Data Lifecycle<a class="anchor-link" id="time-series-data-lifecycle"></a></h3>
<p>Combining PostgreSQL and ClickHouse separates operational data from long-term analytical data. Organizations can store only day-to-day operational records in PostgreSQL, while continuously archiving historical data in ClickHouse via a Change Data Capture (CDC) pipeline.</p>
<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="476" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/3-476x1024.jpeg" alt="" class="wp-image-44508"></figure>
<p>In a typical deployment, PostgreSQL serves as the system of record for transactional workloads, storing recent data, e.g., the last 30 days, to support low-latency OLTP operations. Simultaneously, committed transactions are streamed to ClickHouse via logical replication and a CDC connector such as Debezium, PeerDB, or native decoding. ClickHouse maintains a complete historical archive optimized for analytical queries without affecting operational database performance.</p>
<h2 class="wp-block-heading">Pattern 3: Federated Query with pg_clickhouse<a class="anchor-link" id="pattern-3-federated-query-with-pg_clickhouse"></a></h2>
<p>Instead of forcing applications and BI tools to communicate directly with ClickHouse, PostgreSQL remains the primary SQL endpoint. The <code>pg_clickhouse</code> extension transparently pushes supported analytical queries to ClickHouse while preserving PostgreSQL compatibility.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="281" src="https://severalnines.com/wp-content/uploads/2026/08/4-1024x281.jpeg" alt="" class="wp-image-44509"></figure>
<h3 class="wp-block-heading">Federated Query Execution Flow<a class="anchor-link" id="federated-query-execution-flow"></a></h3>
<p>Upon reaching PostgreSQL, a query is processed by the parser and query planner. The <code>pg_clickhouse</code> extension then automatically determines if it is a transactional (OLTP) or analytical (OLAP) workload. This routing happens before execution and is completely transparent to the application.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="772" src="https://severalnines.com/wp-content/uploads/2026/08/5-1024x772.jpeg" alt="" class="wp-image-44510"></figure>
<p><strong>OLTP queries</strong>, such as inserting orders or retrieving records by primary key, execute locally within PostgreSQL to maintain ACID guarantees, MVCC concurrency control, and low-latency transaction processing.</p>
<p><strong>Analytical queries</strong>, like large aggregations or historical reporting scanning millions of rows, are forwarded to ClickHouse, leveraging its columnar storage engine, vectorized execution, and compression for significantly faster performance.</p>
<h2 class="wp-block-heading">Pattern 4: Embedded Analytics in SaaS Applications<a class="anchor-link" id="pattern-4-embedded-analytics-in-saas-applications"></a></h2>
<p>Customer-facing dashboards require analytics to become part of the production application itself. Every customer request may trigger analytical queries while transactional operations continue independently. This makes CDC freshness, query latency, and workload isolation part of the application&rsquo;s reliability requirements.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="772" src="https://severalnines.com/wp-content/uploads/2026/08/6-1024x772.jpeg" alt="" class="wp-image-44511"></figure>
<h3 class="wp-block-heading">Embedded Analytics Operational Components<a class="anchor-link" id="embedded-analytics-operational-components"></a></h3>
<p>While PostgreSQL and ClickHouse serve different workloads, the success of a hybrid analytics platform depends on the operational components that ensure data consistency, low latency, and service reliability. The CDC pipeline is only one part of the architecture and operators must also continuously monitor the health of the entire data flow.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="646" src="https://severalnines.com/wp-content/uploads/2026/08/7-1024x646.jpeg" alt="" class="wp-image-44512"></figure>
<p>End-to-end observability is critical for production deployments. Monitoring should include PostgreSQL replication health, CDC connector status, ClickHouse ingestion throughput, query performance, storage utilization, and dashboard latency. A centralized monitoring platform enables operators to correlate issues across the entire pipeline, reducing mean time to detection (MTTD) and mean time to recovery (MTTR).</p>
<h2 class="wp-block-heading">Pattern 5: Hybrid<a class="anchor-link" id="pattern-5-hybrid"></a></h2>
<p>Many production environments combine three specialized database platforms. PostgreSQL serves as the transactional system of record for business-critical operations like user management, orders, accounts, and billing. Its ACID compliance, MVCC model, and mature ecosystem suit Online Transaction Processing (OLTP) workloads requiring strong consistency.</p>
<p>Meanwhile, TimescaleDB handles operational time-series workloads like application metrics, IoT readings, and telemetry. Hypertables, native compression, continuous aggregates, and automated retention enable efficient storage and querying while maintaining full PostgreSQL compatibility.</p>
<p>For analytical processing, ClickHouse offers a column-oriented database optimized for Online Analytical Processing (OLAP). It runs complex queries across billions of rows for dashboards, BI, and trend analysis. Isolating analytical workloads prevents reports and insights from impacting operational application performance.</p>
<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="826" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/8-826x1024.jpeg" alt="" class="wp-image-44513"></figure>

<h3 class="wp-block-heading">Postgres vs. TimescaleDB vs. ClickHouse Decision Tree<a class="anchor-link" id="postgres-vs-timescaledb-vs-clickhouse-decision-tree"></a></h3>
<p>To optimize performance, scalability, and operational efficiency, a simple decision process helps determine whether PostgreSQL, <a href="https://severalnines.com/clustercontrol/databases/timescaledb">TimescaleDB</a>, or ClickHouse is the best fit for a particular use case.</p>
<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="510" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/9-510x1024.jpeg" alt="" class="wp-image-44514"></figure>
<h2 class="wp-block-heading">Operating Postgres + ClickHouse in Production<a class="anchor-link" id="operating-postgres-clickhouse-in-production"></a></h2>
<p>Previously, we explored how PostgreSQL and ClickHouse complement each other through real-time operational analytics, hot/cold storage, and federated queries using <code>pg_clickhouse</code>. However, successfully operating them in production requires understanding Change Data Capture (CDC) behavior under failure conditions, monitoring health, preparing runbooks, and establishing clear ownership across all layers. I&rsquo;ll look at the operational perspective, examining how to keep the architecture healthy, diagnose common problems, and run a reliable production platform.</p>
<h3 class="wp-block-heading">Native CDC for Operators<a class="anchor-link" id="native-cdc-for-operators"></a></h3>
<p>Change Data Capture (CDC) is the foundation of PostgreSQL and ClickHouse synchronization, ensuring ClickHouse operates as a near-real-time analytical platform rather than an outdated copy. For operators, understanding its internal mechanics is essential.</p>
<p>In PostgreSQL, CDC utilizes the Write-Ahead Log (WAL), logical decoding, and logical replication instead of scheduled ETL jobs. PostgreSQL continuously records committed transactions in the WAL, which logical replication decodes into inserts, updates, and deletes. The process starts with an initial snapshot of existing data before transitioning into continuous streaming, keeping ClickHouse perfectly synchronized.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="772" src="https://severalnines.com/wp-content/uploads/2026/08/10-1024x772.jpeg" alt="" class="wp-image-44515"></figure>
<p>While modern services like ClickPipes and PeerDB simplify deployment over traditional Kafka- or Debezium-based architectures, native CDC still requires operational responsibility. Complexity merely shifts to monitoring, validation, and operational governance.</p>
<p>Crucially, operators must monitor logical replication slots. If a CDC consumer stops, PostgreSQL retains unconsumed WAL files, causing uncontrolled disk growth that can exhaust storage and disrupt the primary database.</p>
<p>A useful operational query for monitoring replication slots is shown below:</p>
<pre class="wp-block-code"><code>SELECT
    slot_name,
    plugin,
    active,
    restart_lsn,
    confirmed_flush_lsn
FROM pg_replication_slots;</code></pre>
<p>This information allows operators to verify whether replication slots remain active and downstream consumers acknowledge changes.</p>
<p><strong>Distinguishing between the initial snapshot and continuous replication is critical.</strong> Initial snapshots transfer gigabytes or terabytes of data, where replication lag is expected and should not trigger alerts. Afterward, continuous replication must meet defined freshness objectives, limiting delay to seconds or minutes.</p>
<p>ClickHouse managed CDC services require direct PostgreSQL connectivity and do not support proxy layers like PgBouncer, Amazon RDS Proxy, or Supabase Pooler. This must be considered during network, firewall, and infrastructure deployment.</p>
<h3 class="wp-block-heading">Operational Failure Modes<a class="anchor-link" id="operational-failure-modes"></a></h3>
<p>The primary challenge in PostgreSQL and ClickHouse architectures lies in system interactions, making an effective troubleshooting strategy vital when incidents span multiple layers.</p>
<p>The most frequent operational issue is CDC (Change Data Capture) lag, which leads to stale dashboards and visible delays for users even if PostgreSQL remains healthy. Operators must monitor a range of metrics, including replication lag, slot status, WAL generation rates, ClickHouse throughput, and end-to-end freshness, rather than just basic database health.</p>
<p>Schema evolution also presents difficulties, as application releases regularly modify PostgreSQL tables. Because ClickHouse schemas are denormalized for analysis, schema updates require precise transformations to avoid pipeline interruptions or data gaps.</p>
<p>Furthermore, updates and deletes require deliberate management. While PostgreSQL modifies rows directly, ClickHouse is built for append-heavy workloads. Managing updates demands strategies like ReplacingMergeTree, version columns, or deduplication, while deletes rely on tombstones, soft-delete flags, or scheduled merges.</p>
<p>Query routing errors frequently impact performance; analytical queries on PostgreSQL exhaust resources, whereas point lookups on ClickHouse introduce latency. Federated query setups add further complexity, as performance hinges on whether execution is successfully pushed to ClickHouse or falls back to PostgreSQL.</p>
<p>Finally, security and governance grow more complex because user accounts, roles, authentication, and auditing differ between the platforms. Replicated analytical data often requires distinct access controls, encryption, and logging compared to the source transactional system.</p>
<h2 class="wp-block-heading">Hybrid Operations with ClusterControl<a class="anchor-link" id="hybrid-operations-with-clustercontrol"></a></h2>
<p>As organizations adopt specialized databases like PostgreSQL, ClickHouse, TimescaleDB, Redis, Valkey, MongoDB, MySQL, and MariaDB, operational complexity grows. Rather than managing each technology independently, platform teams require unified tooling to support these heterogeneous environments.</p>
<p><a href="https://severalnines.com/clustercontrol/">ClusterControl</a> fits this strategy by delivering unified lifecycle management, including deployment, monitoring, backup, recovery, and automation, for multiple open-source databases across on-premises and cloud environments. As PostgreSQL and ClickHouse architectures grow more common, its operational management extends well beyond simple server provisioning.</p>
<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="393" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/11-393x1024.jpeg" alt="" class="wp-image-44516"></figure>
<p>PostgreSQL management involves deployment automation, high availability, backups, PITR, replication monitoring, and upgrade planning. ClickHouse adds backup strategies, merge monitoring, storage capacity planning, and query optimization. The connecting CDC pipeline also requires production-level monitoring, health checks, and incident response.</p>
<p>Support teams must prepare operational runbooks before production deployment. These should document reference architectures, failure modes, CDC troubleshooting, reconciliation workflows, schema migrations, and team escalation paths.</p>
<h2 class="wp-block-heading">Recommended Reference Architectures<a class="anchor-link" id="recommended-reference-architectures"></a></h2>
<p>Although PostgreSQL and ClickHouse can be combined in numerous ways, several architectural patterns have consistently emerged across production deployments.</p>
<h3 class="wp-block-heading">Architecture A: SaaS Operational Analytics<a class="anchor-link" id="architecture-a-saas-operational-analytics"></a></h3>
<p>This architecture positions PostgreSQL as the transactional system of record while ClickHouse powers customer-facing dashboards through near-real-time CDC replication. Optionally, <code>pg_clickhouse</code> provides SQL compatibility for existing applications. This pattern is particularly suitable for SaaS platforms, product analytics, billing systems, and customer usage reporting.</p>
<figure class="wp-block-image aligncenter size-large is-resized"><img loading="lazy" decoding="async" width="486" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/12-486x1024.jpeg" alt="" class="wp-image-44517"></figure>
<h3 class="wp-block-heading">Architecture B: Hot / Cold Time-Series Data<a class="anchor-link" id="architecture-b-hot-cold-time-series-data"></a></h3>
<p>Recent operational data remains inside PostgreSQL while historical records migrate into ClickHouse after successful replication. Data expiration is governed by replication watermarks rather than fixed retention schedules, ensuring historical data remains protected.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="772" src="https://severalnines.com/wp-content/uploads/2026/08/13-1024x772.jpeg" alt="" class="wp-image-44518"></figure>
<h3 class="wp-block-heading">Architecture C: Federated Analytics<a class="anchor-link" id="architecture-c-federated-analytics"></a></h3>
<p>Applications continue connecting to PostgreSQL while pg_clickhouse pushes analytical execution into ClickHouse. This minimizes migration effort while improving analytical performance.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="753" height="1024" src="https://severalnines.com/wp-content/uploads/2026/08/14-753x1024.jpeg" alt="" class="wp-image-44520"></figure>
<h3 class="wp-block-heading">Architecture D: Hybrid Cloud Analytics<a class="anchor-link" id="architecture-d-hybrid-cloud-analytics"></a></h3>
<p>Transactional PostgreSQL clusters remain within customer-controlled environments while ClickHouse operates as a managed analytical platform in the cloud. Secure connectivity is established through VPNs, private networking, or dedicated links.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="772" src="https://severalnines.com/wp-content/uploads/2026/08/15-1024x772.jpeg" alt="" class="wp-image-44521"></figure>
<h2 class="wp-block-heading">Production Readiness Checklist<a class="anchor-link" id="production-readiness-checklist"></a></h2>
<p>Before deploying PostgreSQL and ClickHouse into production, organizations should validate both technical implementation and operational preparedness. Successful production environments depend as much on operational discipline as on architectural design.</p>
<h3 class="wp-block-heading">Before Deployment<a class="anchor-link" id="before-deployment"></a></h3>
<ul class="wp-block-list">
<li>Identify authoritative source-of-truth tables.</li>
<li>Define which datasets require CDC.</li>
<li>Enable logical replication.</li>
<li>Design ClickHouse analytical schemas.</li>
<li>Define update and delete handling.</li>
<li>Establish naming conventions.</li>
<li>Validate firewall and network connectivity.</li>
<li>Review proxy limitations.</li>
<li>Plan initial snapshots and historical backfills.</li>
</ul>
<h3 class="wp-block-heading">During Rollout<a class="anchor-link" id="during-rollout"></a></h3>
<ul class="wp-block-list">
<li>Execute the initial snapshot.</li>
<li>Enable continuous CDC.</li>
<li>Compare row counts and business metrics.</li>
<li>Benchmark representative analytical queries.</li>
<li>Test failover scenarios.</li>
<li>Validate schema migrations.</li>
<li>Verify dashboard freshness.</li>
<li>Ensure retention policies do not remove data prematurely.</li>
</ul>
<h3 class="wp-block-heading">After Go-Live<a class="anchor-link" id="after-go-live"></a></h3>
<ul class="wp-block-list">
<li>Monitor replication lag continuously.</li>
<li>Observe WAL growth.</li>
<li>Monitor ClickHouse insert throughput.</li>
<li>Watch merge activity.</li>
<li>Reconcile PostgreSQL and ClickHouse data regularly.</li>
<li>Review schema drift after every application release.</li>
<li>Maintain operational documentation and escalation procedures.</li>
</ul>
<h2 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>Modern data platforms use specialized architectures where each database handles specific workloads. PostgreSQL serves as a reliable transactional system of record, while ClickHouse enables high-performance real-time analytics without impacting transactional performance.<br>Operating this architecture requires managing Change Data Capture, replication health, schema evolution, and data reconciliation. In hybrid database environments, platforms like </p>
<p>ClusterControl provide valuable unified management across multiple database technologies.<br>Ultimately, the future involves combining both technologies into a cohesive platform. Organizations that invest in both the architecture and its supporting operational processes will ensure long-term scalability and production reliability.</p>
<h2 class="wp-block-heading">Install ClusterControl and try Postgres and ClickHouse&nbsp;free for 30 days<a class="anchor-link" id="install-clustercontrol-and-try-postgres-and-clickhouse-free-for-30-days"></a></h2>
<h3 class="wp-block-heading">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc
sudo ./install-cc     # omit sudo if you run as root</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<p>The post <a href="https://severalnines.com/blog/postgres-clickhouse-architectural-patterns/">Postgres + ClickHouse Architectural Patterns</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/postgres-clickhouse-architectural-patterns/">Postgres + ClickHouse Architectural Patterns</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Server 12.3, 11.8, 11.4 and 10.11 – Q3 2026 Maintenance Releases, and Goodbye 10.6</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/" />
      <id>https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/</id>
      <updated>2026-08-25T12:14:09+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Server maintenance releases are here!<br />
On August 24, we released updates for our four currently maintained Long Term Support series:<br />
As usual, these maintenance releases include bug fixes, stability improvements, and ongoing work across MariaDB Server. …<br />
Continue reading \"MariaDB Server 12.3, 11.8, 11.4 and 10.11 – Q3 2026 Maintenance Releases, and Goodbye 10.6\"<br />
MariaDB Server 12.3, 11.8, 11.4 and 10.11 – Q3 2026 Maintenance Releases, and Goodbye 10.6 appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/">MariaDB Server 12.3, 11.8, 11.4 and 10.11 – Q3 2026 Maintenance Releases, and Goodbye 10.6</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Server maintenance releases are here!<br>
On August 24, we released updates for our four currently maintained Long Term Support series:<br>
As usual, these maintenance releases include bug fixes, stability improvements, and ongoing work across MariaDB Server. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;MariaDB Server 12.3, 11.8, 11.4 and 10.11 &ndash; Q3 2026 Maintenance Releases, and Goodbye 10.6&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/">MariaDB Server 12.3, 11.8, 11.4 and 10.11 &ndash; Q3 2026 Maintenance Releases, and Goodbye 10.6</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-and-10-11-q3-2026-maintenance-releases-and-goodbye-10-6/">MariaDB Server 12.3, 11.8, 11.4 and 10.11 – Q3 2026 Maintenance Releases, and Goodbye 10.6</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Contribution Statistics, January-June 2026</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/" />
      <id>https://mariadb.org/mariadb-contribution-statistics-january-june-2026/</id>
      <updated>2026-08-25T08:28:42+03:00</updated>
      <author><name>Georgi Kodinov</name></author>
      <summary type="html"><![CDATA[<p>After a long break, I will be taking over reporting on the contributions. FYI, the last one was done in Jan 2025.<br />
Just like last time, I’m going to start with a breakdown of all the organizations who have contributed to MariaDB Server during the above period. …<br />
Continue reading \"MariaDB Contribution Statistics, January-June 2026\"<br />
MariaDB Contribution Statistics, January-June 2026 appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/">MariaDB Contribution Statistics, January-June 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>After a long break, I will be taking over reporting on the contributions. FYI, the last one was done in <a href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/%5C%22/mariadb-contribution-statistics-january-2025/%5C%22">Jan 2025</a>.<br>
Just like last time, I&rsquo;m going to start with a breakdown of all the organizations who have contributed to MariaDB Server during the above period. &hellip; </p>
<p class='"link-more"'><a href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/" class='"more-link"'>Continue reading<span class='"screen-reader-text"'> &ldquo;MariaDB Contribution Statistics, January-June 2026&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/">MariaDB Contribution Statistics, January-June 2026</a> appeared first on <a href="https://mariadb.org/">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-contribution-statistics-january-june-2026/">MariaDB Contribution Statistics, January-June 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Community Server Q3 2026 maintenance releases</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-community-server-q3-2026-maintenance-releases/" />
      <id>https://mariadb.com/resources/blog/mariadb-community-server-q3-2026-maintenance-releases/</id>
      <updated>2026-08-24T23:40:06+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Community Server 12.3.3, 11.8.9, 11.4.13, and 10.11.19 maintenance releases. See […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-q3-2026-maintenance-releases/">MariaDB Community Server Q3 2026 maintenance releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Community Server 12.3.3, 11.8.9, 11.4.13, and 10.11.19 maintenance releases. See the release notes and changelogs for additional details on each release and visit mariadb.com/downloads to download.</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-q3-2026-maintenance-releases/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-q3-2026-maintenance-releases/">MariaDB Community Server Q3 2026 maintenance releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Oracle Exadata Cost Optimization: Migrating to an Open Source Data Infrastructure Stack Without Compromising Performance, Scalability, Availability, or Reliability</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/oracle-exadata-cost-optimization-open-source-stack/" />
      <id>https://minervadb.com/oracle-exadata-cost-optimization-open-source-stack/</id>
      <updated>2026-08-24T20:37:28+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MinervaDB whitepaper on Oracle Exadata cost optimization: measure Exadata license spend, optimize in place, then migrate to PostgreSQL 18, ClickHouse 26.3 LTS, Kafka/Debezium and Valkey with code, diagrams, TCO model and a rollback-safe cutover plan. [...]</p>
<p><a href="https://minervadb.com/oracle-exadata-cost-optimization-open-source-stack/">Oracle Exadata Cost Optimization: Migrating to an Open Source Data Infrastructure Stack Without Compromising Performance, Scalability, Availability, or Reliability</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><strong>Document ID:</strong> MDB-WP-2026-08-EXADATA-OSS &nbsp;|&nbsp; <strong>Version:</strong> 1.0 &nbsp;|&nbsp; <strong>Published:</strong> 24 August 2026 &nbsp;|&nbsp; <strong>Prepared by:</strong> MinervaDB Inc. Database Architecture Practice &nbsp;|&nbsp; <strong>Classification:</strong> Public whitepaper</p>
<p>Oracle Exadata cost optimization has two honest paths. The first is to keep Exadata and cut what you pay for it: prune unused options, enable fewer cores, and negotiate before the 19c Extended Support uplift lands. The second, and the one this MinervaDB whitepaper engineers in detail, is a staged data transformation onto a fully open source data infrastructure stack &mdash; PostgreSQL 18 for OLTP, ClickHouse 26.3 LTS for analytics, Apache Kafka 4.3 with Debezium 3.6 for change data capture, and Valkey 9.1 for caching &mdash; that removes the recurring license and support line entirely while holding performance, scalability, availability, and reliability at Exadata-class SLOs.</p>
<p>The finding, stated up front: on a representative Exadata X11M quarter-rack estate, <strong>the annual software support bill alone (licenses at 22% of net) exceeds the five-year hardware plus subscription-support cost of the replacement open source stack</strong>, and the performance characteristics that justify Exadata &mdash; Smart Scan, Storage Indexes, RAC, Hybrid Columnar Compression &mdash; each have a measured open source equivalent when the workload is routed to the right engine rather than forced through one. The rest of this paper shows the arithmetic, the architecture, the code, and the failure modes.</p>
<h2>1. Where Oracle Exadata spend actually goes<a class="anchor-link" id="1-where-oracle-exadata-spend-actually-goes"></a></h2>
<p>Most Exadata cost optimization conversations start with the rack. That is the wrong place to look. The rack is a one-time capital line; the recurring line is software licensing and its 22% annual support, and on Exadata that line is amplified by three mechanisms: every enabled database-server core is licensed at the Oracle core factor, the storage tier carries its own per-disk software license, and the features that make Exadata fast (RAC, Partitioning, Advanced Compression, In-Memory) are separately licensed options rather than part of Enterprise Edition.</p>
<p>The list prices below are taken from Oracle&rsquo;s published <a href="https://www.oracle.com/a/ocom/docs/corporate/pricing/technology-price-list-070617.pdf" target="_blank" rel="noopener">Technology Global Price List</a> and <a href="https://www.oracle.com/a/ocom/docs/corporate/pricing/exadata-pricelist-070598.pdf" target="_blank" rel="noopener">Engineered Systems Price List</a> (June/August 2026 editions). Real contracts carry discounts, frequently in the 40&ndash;70% range; the ratios between line items are what matter for the model, and the support percentage applies to net, not list.</p>
<table>
<thead>
<tr>
<th>Line item (per processor license unless noted)</th>
<th>List price (USD)</th>
<th>Annual support (22%)</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Oracle Database Enterprise Edition</td>
<td>$47,500</td>
<td>$10,450</td>
<td>Mandatory on Exadata</td>
</tr>
<tr>
<td>Real Application Clusters</td>
<td>$23,000</td>
<td>$5,060</td>
<td>Instance-failure RTO and consolidation</td>
</tr>
<tr>
<td>Partitioning</td>
<td>$11,500</td>
<td>$2,530</td>
<td>Almost universally in use on Exadata estates</td>
</tr>
<tr>
<td>Advanced Compression</td>
<td>$11,500</td>
<td>$2,530</td>
<td>HCC itself is Exadata-included; OLTP compression is not</td>
</tr>
<tr>
<td>Diagnostics Pack + Tuning Pack</td>
<td>$12,500</td>
<td>$2,750</td>
<td>Required to legally query AWR/ASH</td>
</tr>
<tr>
<td>Active Data Guard</td>
<td>$11,500</td>
<td>$2,530</td>
<td>Readable standby</td>
</tr>
<tr>
<td>Database In-Memory</td>
<td>$23,000</td>
<td>$5,060</td>
<td>Optional; common on HTAP estates</td>
</tr>
<tr>
<td>Exadata Storage Server Software (per disk drive)</td>
<td>$10,000</td>
<td>$2,200</td>
<td>Storage tier, independent of core licensing</td>
</tr>
<tr>
<td>Exadata Database Machine X11M quarter rack (hardware)</td>
<td>$314,681</td>
<td>~$62,936 (systems + OS)</td>
<td>2 database servers, 3 storage servers</td>
</tr>
</tbody>
</table>
<p>Apply the arithmetic to a quarter rack. Each X11M database server ships with two 96-core AMD EPYC processors; with capacity-on-demand a typical estate enables 64 cores per server, so 128 enabled cores &times; Oracle&rsquo;s 0.5 core factor for AMD EPYC = 64 processor licenses (verify against the current Oracle Processor Core Factor Table at engagement time). </p>
<p>A &ldquo;standard Exadata option set&rdquo; of EE + RAC + Partitioning + Advanced Compression + Diagnostics/Tuning is $106,000 per processor at list, so 64 processors is <strong>$6.78M list</strong> and <strong>$1.49M per year in support</strong>. Add 36 storage-server disk licenses ($360,000 list, $79,200 per year support) and hardware support, and the recurring line on an illustrative quarter rack lands around <strong>$1.63M per year at list, roughly $0.65&ndash;0.98M per year after a typical 40&ndash;60% discount</strong>. Over five years that recurring line, not the rack, is the number to optimize.</p>
<p>Two further pressures make 2026&ndash;2027 the decision window. Oracle Database 21c reaches end of support on 31 July 2027 with no Extended Support, and 19c Premier Support ends 31 December 2029 with Extended Support carrying a +10% year-one and +20% subsequent-year uplift on the support fee (see <a href="https://endoflife.date/oracle-database" target="_blank" rel="noopener">the Oracle Database lifecycle tracker</a>; re-verify dates against MOS note 742060.1 before quoting them in a contract). Estates that stay on Exadata will pay more to stand still.</p>
<h2>2. Oracle Exadata cost optimization in place (what to do first)<a class="anchor-link" id="2-oracle-exadata-cost-optimization-in-place-what-to-do-first"></a></h2>
<p>MinervaDB is vendor-neutral, and that cuts both ways: not every Exadata estate should leave. Before any migration business case, run the four measures below. They are reversible, they require no application change, and on several engagements they have cut the recurring line by 25&ndash;40% (illustrative range; your figure comes from your own <code>DBA_FEATURE_USAGE_STATISTICS</code>).</p>
<h3>2.1 Measure option usage before you pay for it<a class="anchor-link" id="2-1-measure-option-usage-before-you-pay-for-it"></a></h3>
<p>Licensing follows usage, and Oracle&rsquo;s own catalog tells you what is used. This is the first query on every MinervaDB Oracle takeover, and it is the evidence for pruning options at renewal. Note the licensing gate: querying <code>DBA_HIST_*</code> or <code>V$ACTIVE_SESSION_HISTORY</code> on an estate without Diagnostics Pack is itself a license event, so check <code>CONTROL_MANAGEMENT_PACK_ACCESS</code> first.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Oracle: feature usage baseline (run as a user with SELECT on DBA_ views)">-- 1. Licensing gate: what packs is this instance allowed to use?
SHOW PARAMETER control_management_pack_access;

-- 2. Which separately licensed options are actually in use?
SELECT
    u.name                                   AS feature_name,
    u.detected_usages,
    u.currently_used,
    TO_CHAR(u.first_usage_date, 'YYYY-MM-DD') AS first_used,
    TO_CHAR(u.last_usage_date,  'YYYY-MM-DD') AS last_used
FROM dba_feature_usage_statistics u
WHERE u.name IN (
        'Real Application Clusters (RAC)',
        'Partitioning (user)',
        'Advanced Compression',
        'HeapCompression',
        'Hybrid Columnar Compression',
        'In-Memory Column Store',
        'Active Data Guard - Real-Time Query on Physical Standby',
        'Oracle Multitenant',
        'Automatic Workload Repository',
        'SQL Tuning Advisor',
        'Exadata'
      )
  AND u.dbid = (SELECT dbid FROM v$database)
ORDER BY u.currently_used DESC, u.detected_usages DESC;

-- 3. Enabled cores that drive the processor-license count
SELECT
    cpu_count_current,
    cpu_core_count_current,
    cpu_socket_count_current
FROM v$license;</pre>
<p>An option showing <code>currently_used = FALSE</code> and <code>last_used</code> older than the current support term is a renewal negotiation item. In-Memory and Active Data Guard are the most common finds; Tuning Pack is the second.</p>
<h3>2.2 Reduce enabled cores with capacity-on-demand<a class="anchor-link" id="2-2-reduce-enabled-cores-with-capacity-on-demand"></a></h3>
<p>Exadata licenses follow enabled cores, not installed cores. Right-size from <code>DBA_HIST_SYSMETRIC_SUMMARY</code> (<code>Host CPU Utilization (%)</code>, p95 over 90 days, only if Diagnostics Pack is licensed &mdash; otherwise STATSPACK or OS-level <code>sar</code> data) and reduce enabled cores through OEDA/<code>dbmcli</code> during a maintenance window. Every two cores removed on AMD EPYC saves one processor license of support every year. Blast radius: CPU headroom during peak; rollback: re-enable cores (a reboot of the database server, not a license event, but confirm with your Oracle account team in writing before changing core counts).</p>
<h3>2.3 Offload analytics before you offload anything else<a class="anchor-link" id="2-3-offload-analytics-before-you-offload-anything-else"></a></h3>
<p>The most expensive SQL on most Exadata estates is reporting: long-running aggregations that make Exadata&rsquo;s Smart Scan look heroic precisely because they are being run on a row-store OLTP engine. Moving those queries to ClickHouse via CDC (Section 5.2) shrinks the Exadata CPU footprint, which in turn reduces enabled cores, which reduces support. This is also the first, lowest-risk phase of the full migration, so nothing done here is wasted if the estate later leaves Exadata entirely.</p>
<h3>2.4 Time the negotiation to the lifecycle<a class="anchor-link" id="2-4-time-the-negotiation-to-the-lifecycle"></a></h3>
<p>Do not renew a multi-year support term that runs across the 19c Extended Support boundary without pricing the uplift in. A credible, funded open source migration plan is the most effective negotiating asset an Oracle customer has; the sections below are that plan.</p>
<h2>3. Target open source data infrastructure stack<a class="anchor-link" id="3-target-open-source-data-infrastructure-stack"></a></h2>
<p>Exadata is one engine asked to do three jobs: transactional OLTP, analytical reporting, and hot-key lookups that in practice live in the buffer cache and result cache. The open source design principle is the opposite: route each workload class to the engine built for it, connect them with change data capture, and operate all of it with the same observability and SRE discipline. Every component below is 100% open source, community-supported, and runs on commodity x86/ARM hardware or any cloud.</p>
<figure>
<img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDY2MCIgd2lkdGg9IjEwMCUiIHJvbGU9ImltZyIgYXJpYS1sYWJlbD0iVGFyZ2V0IG9wZW4gc291cmNlIGRhdGEgaW5mcmFzdHJ1Y3R1cmUgc3RhY2sgcmVwbGFjaW5nIE9yYWNsZSBFeGFkYXRhOiBQb3N0Z3JlU1FMIDE4IHdpdGggUGF0cm9uaSwgUGdCb3VuY2VyIGFuZCBwZ0JhY2tSZXN0IGZvciBPTFRQOyBEZWJleml1bSBhbmQgS2Fma2EgZm9yIENEQzsgQ2xpY2tIb3VzZSAyNi4zIExUUyBmb3IgYW5hbHl0aWNzOyBWYWxrZXkgOS4xIGZvciBjYWNoaW5nOyBQcm9tZXRoZXVzIGFuZCBHcmFmYW5hIGZvciBvYnNlcnZhYmlsaXR5Ij4KPGRlZnM+CjxzdHlsZT4KLmJveHtmaWxsOiNmZmZmZmY7c3Ryb2tlOiMxZjNhNWY7c3Ryb2tlLXdpZHRoOjI7cng6OH0KLnpvbmV7ZmlsbDojZjNmN2ZiO3N0cm9rZTojOWRiM2M5O3N0cm9rZS13aWR0aDoxLjU7c3Ryb2tlLWRhc2hhcnJheTo2IDQ7cng6MTJ9Ci50e2ZvbnQtZmFtaWx5OkludGVyLEFyaWFsLEhlbHZldGljYSxzYW5zLXNlcmlmO2ZvbnQtc2l6ZToxMy41cHg7ZmlsbDojMWYzYTVmfQouaHtmb250LWZhbWlseTpJbnRlcixBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtmb250LXNpemU6MTZweDtmb250LXdlaWdodDo3MDA7ZmlsbDojMWYzYTVmfQouc3tmb250LWZhbWlseTpJbnRlcixBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtmb250LXNpemU6MTEuNXB4O2ZpbGw6IzRhNWE2YX0KLnp7Zm9udC1mYW1pbHk6SW50ZXIsQXJpYWwsSGVsdmV0aWNhLHNhbnMtc2VyaWY7Zm9udC1zaXplOjEzcHg7Zm9udC13ZWlnaHQ6NzAwO2ZpbGw6IzZiN2Y5NDtsZXR0ZXItc3BhY2luZzouMDZlbX0KLmxue3N0cm9rZTojMWYzYTVmO3N0cm9rZS13aWR0aDoyO2ZpbGw6bm9uZTttYXJrZXItZW5kOnVybCgjYXJyKX0KLmNkY3tzdHJva2U6I2MwMzkyYjtzdHJva2Utd2lkdGg6Mi41O2ZpbGw6bm9uZTttYXJrZXItZW5kOnVybCgjYXJycik7c3Ryb2tlLWRhc2hhcnJheTo4IDV9Cjwvc3R5bGU+CjxtYXJrZXIgaWQ9ImFyciIgbWFya2VyV2lkdGg9IjEwIiBtYXJrZXJIZWlnaHQ9IjEwIiByZWZYPSI5IiByZWZZPSI1IiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wLDAgTDEwLDUgTDAsMTAgeiIgZmlsbD0iIzFmM2E1ZiIvPjwvbWFya2VyPgo8bWFya2VyIGlkPSJhcnJyIiBtYXJrZXJXaWR0aD0iMTAiIG1hcmtlckhlaWdodD0iMTAiIHJlZlg9IjkiIHJlZlk9IjUiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAsMCBMMTAsNSBMMCwxMCB6IiBmaWxsPSIjYzAzOTJiIi8+PC9tYXJrZXI+CjwvZGVmcz4KCjx0ZXh0IHg9IjYwMCIgeT0iMzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJoIj5PcGVuIHNvdXJjZSBkYXRhIGluZnJhc3RydWN0dXJlIHN0YWNrIOKAlCBPcmFjbGUgRXhhZGF0YSByZXBsYWNlbWVudCByZWZlcmVuY2UgYXJjaGl0ZWN0dXJlIChNaW5lcnZhREIsIEF1ZyAyMDI2KTwvdGV4dD4KCjwhLS0gQXBwbGljYXRpb25zIC0tPgo8cmVjdCB4PSI0MCIgeT0iNjAiIHdpZHRoPSIxMTIwIiBoZWlnaHQ9IjcwIiBjbGFzcz0iem9uZSIvPgo8dGV4dCB4PSI2MCIgeT0iODIiIGNsYXNzPSJ6Ij5BUFBMSUNBVElPTiBUSUVSPC90ZXh0Pgo8cmVjdCB4PSI4MCIgeT0iOTAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMzAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIxODAiIHk9IjExMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPk9MVFAgc2VydmljZXMgKEpEQkMvcHN5Y29wZyk8L3RleHQ+CjxyZWN0IHg9IjMzMCIgeT0iOTAiIHdpZHRoPSIyMjAiIGhlaWdodD0iMzAiIGNsYXNzPSJib3giLz48dGV4dCB4PSI0NDAiIHk9IjExMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlJlcG9ydGluZyAvIEJJIC8gZGFzaGJvYXJkczwvdGV4dD4KPHJlY3QgeD0iNjAwIiB5PSI5MCIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIzMCIgY2xhc3M9ImJveCIvPjx0ZXh0IHg9IjcwMCIgeT0iMTEwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idCI+U2Vzc2lvbiAvIGhvdC1rZXkgbG9va3VwczwvdGV4dD4KPHJlY3QgeD0iODUwIiB5PSI5MCIgd2lkdGg9IjI3MCIgaGVpZ2h0PSIzMCIgY2xhc3M9ImJveCIvPjx0ZXh0IHg9Ijk4NSIgeT0iMTEwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idCI+QmF0Y2gsIE1MIGZlYXR1cmUgcGlwZWxpbmVzPC90ZXh0PgoKPCEtLSBPTFRQIHpvbmUgLS0+CjxyZWN0IHg9IjQwIiB5PSIxNjAiIHdpZHRoPSI0MDAiIGhlaWdodD0iMzMwIiBjbGFzcz0iem9uZSIvPgo8dGV4dCB4PSI2MCIgeT0iMTgyIiBjbGFzcz0ieiI+T0xUUCDigJQgUE9TVEdSRVNRTCAxODwvdGV4dD4KPHJlY3QgeD0iNzAiIHk9IjIwMCIgd2lkdGg9IjM0MCIgaGVpZ2h0PSI0MCIgY2xhc3M9ImJveCIvPgo8dGV4dCB4PSIyNDAiIHk9IjIxOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlBnQm91bmNlciAxLjI1IMOXMiAodHJhbnNhY3Rpb24gcG9vbGluZyk8L3RleHQ+Cjx0ZXh0IHg9IjI0MCIgeT0iMjMzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+SEFQcm94eSAvIGtlZXBhbGl2ZWQgVklQLCBQYXRyb25pIFJFU1QgaGVhbHRoIGNoZWNrczwvdGV4dD4KPHJlY3QgeD0iNzAiIHk9IjI2MCIgd2lkdGg9IjEwNSIgaGVpZ2h0PSI3MCIgY2xhc3M9ImJveCIvPjx0ZXh0IHg9IjEyMiIgeT0iMjg1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idCI+UEcgMTg8L3RleHQ+PHRleHQgeD0iMTIyIiB5PSIzMDMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5wcmltYXJ5PC90ZXh0Pjx0ZXh0IHg9IjEyMiIgeT0iMzE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+QVotYTwvdGV4dD4KPHJlY3QgeD0iMTg3IiB5PSIyNjAiIHdpZHRoPSIxMDUiIGhlaWdodD0iNzAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIyMzkiIHk9IjI4NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlBHIDE4PC90ZXh0Pjx0ZXh0IHg9IjIzOSIgeT0iMzAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+c3luYyBzdGFuZGJ5PC90ZXh0Pjx0ZXh0IHg9IjIzOSIgeT0iMzE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+QVotYjwvdGV4dD4KPHJlY3QgeD0iMzA0IiB5PSIyNjAiIHdpZHRoPSIxMDUiIGhlaWdodD0iNzAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIzNTYiIHk9IjI4NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlBHIDE4PC90ZXh0Pjx0ZXh0IHg9IjM1NiIgeT0iMzAzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+YXN5bmMgc3RhbmRieTwvdGV4dD48dGV4dCB4PSIzNTYiIHk9IjMxOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPkFaLWM8L3RleHQ+CjxyZWN0IHg9IjcwIiB5PSIzNTAiIHdpZHRoPSIxNjAiIGhlaWdodD0iNDAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIxNTAiIHk9IjM2OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlBhdHJvbmkgNC4xPC90ZXh0Pjx0ZXh0IHg9IjE1MCIgeT0iMzgzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+ZXRjZCAzLjYgw5czIERDUzwvdGV4dD4KPHJlY3QgeD0iMjUwIiB5PSIzNTAiIHdpZHRoPSIxNjAiIGhlaWdodD0iNDAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIzMzAiIHk9IjM2OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPnBnQmFja1Jlc3QgMi41OTwvdGV4dD48dGV4dCB4PSIzMzAiIHk9IjM4MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPmZ1bGwvZGlmZi9pbmNyICsgV0FMIOKGkiBTMzwvdGV4dD4KPHJlY3QgeD0iNzAiIHk9IjQxMCIgd2lkdGg9IjM0MCIgaGVpZ2h0PSI2MCIgY2xhc3M9ImJveCIvPgo8dGV4dCB4PSIyNDAiIHk9IjQzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPkV4dGVuc2lvbnM6IHBnX3BhcnRtYW4sIHBnX3N0YXRfc3RhdGVtZW50cyw8L3RleHQ+Cjx0ZXh0IHg9IjI0MCIgeT0iNDUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idCI+b3JhZmNlLCBwZ3ZlY3RvciwgcGdfY3JvbiwgcGdfaGludF9wbGFuIChnb3Zlcm5lZCk8L3RleHQ+Cgo8IS0tIENEQyB6b25lIC0tPgo8cmVjdCB4PSI0NzAiIHk9IjE2MCIgd2lkdGg9IjI2MCIgaGVpZ2h0PSIzMzAiIGNsYXNzPSJ6b25lIi8+Cjx0ZXh0IHg9IjQ5MCIgeT0iMTgyIiBjbGFzcz0ieiI+Q0RDIEJBQ0tCT05FPC90ZXh0Pgo8cmVjdCB4PSI0OTUiIHk9IjIwMCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSI2MCIgY2xhc3M9ImJveCIvPgo8dGV4dCB4PSI2MDAiIHk9IjIyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPkRlYmV6aXVtIDMuNjwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIyNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5LYWZrYSBDb25uZWN0IMOXMiAocGdvdXRwdXQgLzwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIyNTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5PcmFjbGUgTG9nTWluZXIgZHVyaW5nIGN1dG92ZXIpPC90ZXh0Pgo8cmVjdCB4PSI0OTUiIHk9IjI4NSIgd2lkdGg9IjIxMCIgaGVpZ2h0PSI4MCIgY2xhc3M9ImJveCIvPgo8dGV4dCB4PSI2MDAiIHk9IjMwOCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPkFwYWNoZSBLYWZrYSA0LjMgKEtSYWZ0KTwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIzMjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj4zIGJyb2tlcnMsIFJGPTMsIG1pbi5pbnN5bmM9MjwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSIzNDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj50aWVyZWQgc3RvcmFnZSDihpIgUzM8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iMzU3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+U2NoZW1hIFJlZ2lzdHJ5IChBcGljdXJpbyk8L3RleHQ+CjxyZWN0IHg9IjQ5NSIgeT0iMzkwIiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjgwIiBjbGFzcz0iYm94Ii8+Cjx0ZXh0IHg9IjYwMCIgeT0iNDEyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idCI+U3RyZWFtIGNvbnN1bWVyczwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSI0MzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5DbGlja0hvdXNlIEthZmthIGVuZ2luZTwvdGV4dD4KPHRleHQgeD0iNjAwIiB5PSI0NDYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5WYWxrZXkgY2FjaGUgaW52YWxpZGF0b3I8L3RleHQ+Cjx0ZXh0IHg9IjYwMCIgeT0iNDYxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+UmV2ZXJzZSBDREMg4oaSIE9yYWNsZSAocm9sbGJhY2spPC90ZXh0PgoKPCEtLSBBbmFseXRpY3Mgem9uZSAtLT4KPHJlY3QgeD0iNzYwIiB5PSIxNjAiIHdpZHRoPSI0MDAiIGhlaWdodD0iMzMwIiBjbGFzcz0iem9uZSIvPgo8dGV4dCB4PSI3ODAiIHk9IjE4MiIgY2xhc3M9InoiPkFOQUxZVElDUyDigJQgQ0xJQ0tIT1VTRSAyNi4zIExUUzwvdGV4dD4KPHJlY3QgeD0iNzkwIiB5PSIyMDAiIHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiIGNsYXNzPSJib3giLz48dGV4dCB4PSI4NzAiIHk9IjIyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlNoYXJkIDE8L3RleHQ+PHRleHQgeD0iODcwIiB5PSIyNDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJzIj5yZXBsaWNhIEEgwrcgcmVwbGljYSBCPC90ZXh0Pjx0ZXh0IHg9Ijg3MCIgeT0iMjU0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+UmVwbGljYXRlZE1lcmdlVHJlZTwvdGV4dD4KPHJlY3QgeD0iOTcwIiB5PSIyMDAiIHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiIGNsYXNzPSJib3giLz48dGV4dCB4PSIxMDUwIiB5PSIyMjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0Ij5TaGFyZCAyPC90ZXh0Pjx0ZXh0IHg9IjEwNTAiIHk9IjI0MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPnJlcGxpY2EgQSDCtyByZXBsaWNhIEI8L3RleHQ+PHRleHQgeD0iMTA1MCIgeT0iMjU0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+UmVwbGljYXRlZE1lcmdlVHJlZTwvdGV4dD4KPHJlY3QgeD0iNzkwIiB5PSIyODAiIHdpZHRoPSIzNDAiIGhlaWdodD0iNDAiIGNsYXNzPSJib3giLz48dGV4dCB4PSI5NjAiIHk9IjI5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPkNsaWNrSG91c2UgS2VlcGVyIMOXMyAoZGVkaWNhdGVkLCBOVk1lKTwvdGV4dD48dGV4dCB4PSI5NjAiIHk9IjMxMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPnJlcGxpY2F0aW9uIGxvZywgRERMIHF1ZXVlLCBwYXJ0IGNvb3JkaW5hdGlvbjwvdGV4dD4KPHJlY3QgeD0iNzkwIiB5PSIzNDAiIHdpZHRoPSIzNDAiIGhlaWdodD0iNjAiIGNsYXNzPSJib3giLz4KPHRleHQgeD0iOTYwIiB5PSIzNjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0Ij5EaXN0cmlidXRlZCB0YWJsZXMgwrcgcHJvamVjdGlvbnMgwrcgbWF0ZXJpYWxpemVkIHZpZXdzPC90ZXh0Pgo8dGV4dCB4PSI5NjAiIHk9IjM4MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPlNtYXJ0IFNjYW4gZXF1aXZhbGVudDogY29sdW1uYXIgcHJ1bmluZywgc2tpcCBpbmRleGVzLCBQUkVXSEVSRTwvdGV4dD4KPHJlY3QgeD0iNzkwIiB5PSI0MjAiIHdpZHRoPSIzNDAiIGhlaWdodD0iNTAiIGNsYXNzPSJib3giLz4KPHRleHQgeD0iOTYwIiB5PSI0NDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0Ij5UaWVyZWQgc3RvcmFnZTogaG90IE5WTWUg4oaSIFMzIHZpYSBUVEwgVE8gVk9MVU1FPC90ZXh0Pgo8dGV4dCB4PSI5NjAiIHk9IjQ1NyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPmNsaWNraG91c2UtYmFja3VwIOKGkiBTMywgcXVhcnRlcmx5IHJlc3RvcmUgZHJpbGxzPC90ZXh0PgoKPCEtLSBDYWNoZSArIE9ic2VydmFiaWxpdHkgLS0+CjxyZWN0IHg9IjQwIiB5PSI1MjAiIHdpZHRoPSI1NDAiIGhlaWdodD0iMTEwIiBjbGFzcz0iem9uZSIvPgo8dGV4dCB4PSI2MCIgeT0iNTQyIiBjbGFzcz0ieiI+Q0FDSEUg4oCUIFZBTEtFWSA5LjE8L3RleHQ+CjxyZWN0IHg9IjcwIiB5PSI1NTUiIHdpZHRoPSI0OTAiIGhlaWdodD0iNjAiIGNsYXNzPSJib3giLz4KPHRleHQgeD0iMzE1IiB5PSI1NzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0Ij5WYWxrZXkgQ2x1c3RlciA5LjE6IDMgcHJpbWFyaWVzICsgMyByZXBsaWNhcyAoQlNELTMpPC90ZXh0Pgo8dGV4dCB4PSIzMTUiIHk9IjU5OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InMiPlJlcGxhY2VzIE9yYWNsZSByZXN1bHQgY2FjaGUgLyBLRUVQIHBvb2wgZm9yIGhvdCBrZXlzOyB3cml0ZS10aHJvdWdoIGZyb20gQ0RDIHN0cmVhbTwvdGV4dD4KCjxyZWN0IHg9IjYxMCIgeT0iNTIwIiB3aWR0aD0iNTUwIiBoZWlnaHQ9IjExMCIgY2xhc3M9InpvbmUiLz4KPHRleHQgeD0iNjMwIiB5PSI1NDIiIGNsYXNzPSJ6Ij5PQlNFUlZBQklMSVRZICZhbXA7IERBVEEgU1JFPC90ZXh0Pgo8cmVjdCB4PSI2NDAiIHk9IjU1NSIgd2lkdGg9IjUwMCIgaGVpZ2h0PSI2MCIgY2xhc3M9ImJveCIvPgo8dGV4dCB4PSI4OTAiIHk9IjU3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InQiPlByb21ldGhldXMgwrcgR3JhZmFuYSDCtyBBbGVydG1hbmFnZXIgwrcgcG9zdGdyZXNfZXhwb3J0ZXIgwrcgQ0ggL21ldHJpY3M8L3RleHQ+Cjx0ZXh0IHg9Ijg5MCIgeT0iNTk4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyI+U0xPczogcDk5IGxhdGVuY3ksIHJlcGxpY2F0aW9uIGxhZywgQ0RDIGVuZC10by1lbmQgbGFnLCBlcnJvciBidWRnZXRzLCBydW5ib29rIGF1dG9tYXRpb248L3RleHQ+Cgo8IS0tIGFycm93cyAtLT4KPHBhdGggZD0iTTE4MCwxMjAgTDIwMCwyMDAiIGNsYXNzPSJsbiIvPgo8cGF0aCBkPSJNNDQwLDEyMCBMNDQwLDE0MCBMNzQ1LDE0MCBMNzQ1LDIzMCBMNzkwLDIzMCIgY2xhc3M9ImxuIi8+CjxwYXRoIGQ9Ik03MDAsMTIwIEw3MDAsMTQwIEw0NTUsMTQwIEw0NTUsNTA1IEw0MDAsNTU1IiBjbGFzcz0ibG4iLz4KPHBhdGggZD0iTTk4NSwxMjAgTDk4NSwxNDAgTDExNDUsMTQwIEwxMTQ1LDIzMCBMMTEzMCwyMzAiIGNsYXNzPSJsbiIvPgo8cGF0aCBkPSJNMjQwLDI0MCBMMjAwLDI2MCIgY2xhc3M9ImxuIi8+CjxwYXRoIGQ9Ik00MTAsMjkwIEw0OTUsMjMwIiBjbGFzcz0iY2RjIi8+CjxwYXRoIGQ9Ik02MDAsMjYwIEw2MDAsMjg1IiBjbGFzcz0iY2RjIi8+CjxwYXRoIGQ9Ik02MDAsMzY1IEw2MDAsMzkwIiBjbGFzcz0iY2RjIi8+CjxwYXRoIGQ9Ik03MDUsNDIwIEw3OTAsMjQwIiBjbGFzcz0iY2RjIi8+CjxwYXRoIGQ9Ik02MDAsNDcwIEw2MDAsNTA1IEw1MDAsNTA1IEw1MDAsNTU1IiBjbGFzcz0iY2RjIi8+Cjx0ZXh0IHg9IjYwMCIgeT0iNjQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0icyIgZmlsbD0iI2MwMzkyYiI+4oCUIOKAlCBDREMgLyBldmVudCBmbG93IChEZWJleml1bSDihpIgS2Fma2Eg4oaSIGNvbnN1bWVycyk8L3RleHQ+Cjwvc3ZnPg==" alt="Oracle Exadata cost optimization: open source data infrastructure stack reference architecture (PostgreSQL 18, ClickHouse 26.3 LTS, Kafka, Debezium, Valkey)" width="1200" height="660"><figcaption>Figure 1. Target open source data infrastructure stack. Versions are the production-recommended lines as of August 2026: PostgreSQL 18.6, ClickHouse 26.3 LTS, Kafka 4.3.1, Debezium 3.6, Valkey 9.1.1, Patroni 4.1.3, pgBackRest 2.59.0, PgBouncer 1.25.2.</figcaption></figure>
<h3>3.1 Workload-to-engine mapping<a class="anchor-link" id="3-1-workload-to-engine-mapping"></a></h3>
<table>
<thead>
<tr>
<th>Exadata capability</th>
<th>What it actually does for the workload</th>
<th>Open source equivalent</th>
<th>Evidence to compare</th>
</tr>
</thead>
<tbody>
<tr>
<td>Smart Scan / Storage Indexes</td>
<td>Pushes predicate filtering and column projection to storage so large scans read less I/O</td>
<td>ClickHouse MergeTree: columnar storage, primary-key index granules, skip indexes, <code>PREWHERE</code>; PostgreSQL 18 asynchronous I/O (<code>io_method = io_uring</code>) and parallel sequential scans for the OLTP-side reporting that remains</td>
<td><code>cell physical IO bytes saved by storage index</code> vs <code>system.query_log.read_bytes</code> / <code>read_rows</code></td>
</tr>
<tr>
<td>Hybrid Columnar Compression</td>
<td>10&ndash;15&times; compression on cold, read-mostly data</td>
<td>ClickHouse codecs (<code>ZSTD</code>, <code>Delta</code>, <code>DoubleDelta</code>, <code>Gorilla</code>, <code>T64</code>) routinely reach comparable ratios on time-series and fact data; PostgreSQL TOAST <code>lz4</code> for large values</td>
<td><code>DBA_TABLES.COMPRESS_FOR</code> and segment sizes vs <code>system.parts.data_compressed_bytes / data_uncompressed_bytes</code></td>
</tr>
<tr>
<td>RAC</td>
<td>Instance-failure RTO in seconds; horizontal read scaling within one shared-storage database</td>
<td>Patroni quorum failover (RTO measured in tens of seconds, Section 7); read scaling via hot standbys behind PgBouncer; ClickHouse replicas for analytic reads</td>
<td><code>GV$INSTANCE</code> failover drill timings vs Patroni switchover/failover drill timings</td>
</tr>
<tr>
<td>In-Memory Column Store</td>
<td>Vectorised aggregation on hot tables</td>
<td>ClickHouse is vectorised end to end; Valkey serves the hot-key subset</td>
<td>AWR SQL ordered by elapsed time vs <code>system.query_log</code> p99 by <code>normalized_query_hash</code></td>
</tr>
<tr>
<td>Data Guard / Active Data Guard</td>
<td>Physical standby, readable</td>
<td>PostgreSQL streaming replication (sync + async) with hot standby; pgBackRest PITR; ClickHouse cross-DC replicas</td>
<td><code>V$DATAGUARD_STATS</code> apply lag vs <code>pg_stat_replication.replay_lag</code></td>
</tr>
<tr>
<td>Result cache / KEEP pool</td>
<td>Sub-millisecond repeated reads</td>
<td>Valkey 9.1 cluster, write-through from the CDC stream, hash-field TTLs</td>
<td>AWR &ldquo;Result Cache&rdquo; section vs <code>INFO commandstats</code>, keyspace hit ratio</td>
</tr>
</tbody>
</table>
<p>The mapping is honest about one thing: PostgreSQL alone does not replace Exadata for an estate that mixes heavy analytics with OLTP. The analytics tier is what makes the performance claim hold, and it is why the architecture is a stack rather than a database swap.</p>
<h2>4. Options considered and rejected<a class="anchor-link" id="4-options-considered-and-rejected"></a></h2>
<p>A migration recommendation is only credible if the alternatives were evaluated on their merits. These were, and each is a legitimate choice for a different constraint set.</p>
<p><strong>Oracle Autonomous Database / Exadata Database Service on OCI or Database@Azure/AWS/Google.</strong> Genuine strengths: RAC and Data Guard semantics preserved, patching automated, and the @-cloud variants burn down hyperscaler commitments. Rejected for the cost objective because the license line moves rather than disappears (BYOL or License Included, ECPU-metered), Autonomous removes SYSDBA, RMAN, and OS access, and the exit path from Autonomous is logical-only (Data Pump or GoldenGate). For an estate whose strategic direction is off-Oracle, every Oracle cloud service deepens the moat.</p>
<p><strong>EDB Postgres Advanced Server (EPAS) with Oracle-compatibility mode.</strong> Genuine strengths: packages, PL/SQL dialect, OCI-compatible connector &mdash; the shortest path when the PL/SQL estate is large and the rewrite budget is the binding constraint. Rejected as the default because it exchanges Oracle lock-in for EDB lock-in and reintroduces a per-core subscription. MinervaDB supports EPAS estates without ideology; we recommend it only against a named PL/SQL-volume requirement and we say so in writing.</p>
<p><strong>PostgreSQL-only with Citus columnar or TimescaleDB for analytics.</strong> Genuine strengths: one engine to operate, one skill set, transactional consistency across OLTP and reporting. Rejected for estates with true Exadata-class scan volumes because row-store parallelism and columnar access methods in PostgreSQL do not match a purpose-built vectorised MergeTree engine on the billion-row aggregations that justified Exadata in the first place. It remains the right answer for smaller estates whose &ldquo;analytics&rdquo; is a few hundred GB of reporting.</p>
<p><strong>Managed cloud DBaaS (Amazon RDS/Aurora, Azure Database for PostgreSQL, Cloud SQL/AlloyDB) as the landing zone.</strong> Genuine strengths: operational automation, PostgreSQL 18 availability within days of community GA. Not rejected &mdash; the stack in Figure 1 runs on any of them &mdash; but the whitepaper models self-managed on commodity hardware or IaaS because that is the configuration with no vendor line at all. </p>
<p>The trade-offs are documented in MinervaDB&rsquo;s <a href="https://minervadb.com/postgresql-cloud-aws-aurora-gcp-azure/">PostgreSQL cloud guide for AWS, GCP, and Azure</a>.</p>
<p><strong>Staying on Exadata with the Section 2 measures.</strong> The right answer for estates with deep RAC-dependent ISV applications under change freeze, or where PL/SQL volume exceeds roughly 500k lines with no rewrite budget. Those estates should optimize in place and revisit at the 19c Extended Support boundary.</p>
<h2>5. Performance parity engineering<a class="anchor-link" id="5-performance-parity-engineering"></a></h2>
<p>&ldquo;Without compromising on performance&rdquo; is a measurable claim or it is marketing, and it is the test every Oracle Exadata cost optimization plan must pass. The measurement frame we use on every Exadata exit is the same: capture the top-N SQL by elapsed time and I/O from AWR (or STATSPACK on unlicensed estates) before migration, map each statement to its target engine, and compare against <code>pg_stat_statements</code> and <code>system.query_log</code> after migration under production-shaped load. Parity is declared per statement class, never in aggregate.</p>
<h3>5.1 OLTP on PostgreSQL 18: partitioning, plans, and I/O<a class="anchor-link" id="5-1-oltp-on-postgresql-18-partitioning-plans-and-i-o"></a></h3>
<p>Oracle Partitioning is a paid option; PostgreSQL declarative partitioning is not, and since PostgreSQL 17 it supports partition-wise joins and aggregates, identity columns and exclusion constraints on partitioned tables, and partition pruning at execution time. The pattern below converts a typical Oracle range-partitioned order table, retains the Oracle-side <code>NUMBER</code> precision decisions deliberately, and adds <code>pg_partman</code> for the maintenance that Oracle interval partitioning did implicitly.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="PostgreSQL 18: partitioned OLTP table converted from an Oracle range-partitioned table">CREATE TABLE sales.orders (
    order_id          BIGINT GENERATED ALWAYS AS IDENTITY,
    customer_id       BIGINT        NOT NULL,
    order_ts          TIMESTAMP(0)  NOT NULL,   -- Oracle DATE carries time: map to timestamp(0), never date
    status            VARCHAR(16)   NOT NULL,
    amount            NUMERIC(14,2) NOT NULL,   -- NUMBER(14,2) &rarr; numeric(14,2); bare NUMBER hot columns &rarr; bigint where domain allows
    region_code       CHAR(3)       NOT NULL,
    CONSTRAINT pk_orders PRIMARY KEY (order_id, order_ts)
) PARTITION BY RANGE (order_ts);

-- pg_partman manages monthly partitions and pre-creates 3 months ahead
SELECT partman.create_parent(
    p_parent_table =&gt; 'sales.orders',
    p_control      =&gt; 'order_ts',
    p_interval     =&gt; '1 month',
    p_premake      =&gt; 3
);

CREATE INDEX ix_orders_customer_ts
    ON sales.orders (customer_id, order_ts DESC);

-- Empty string &ne; NULL in PostgreSQL. Oracle code that relied on '' IS NULL must be
-- audited; this CHECK makes the migration assumption explicit and testable.
ALTER TABLE sales.orders
    ADD CONSTRAINT ck_orders_status_not_blank CHECK (status  '');</pre>
<p>Plan reasoning, not just DDL: the hot OLTP query &ldquo;recent orders for a customer&rdquo; must prune to one or two partitions and walk <code>ix_orders_customer_ts</code>. Verify it with <code>EXPLAIN (ANALYZE, BUFFERS)</code> and look for <em>Partitions removed</em> or a plan that lists only the matching child indexes; a plan that appends every partition is a missing pruning predicate on <code>order_ts</code>, the most common week-one regression on Oracle migrations.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="PostgreSQL 18: verify partition pruning and index usage">EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT order_id, order_ts, status, amount
FROM sales.orders
WHERE customer_id = 8812931
  AND order_ts &gt;= now() - INTERVAL '45 days'
ORDER BY order_ts DESC
LIMIT 50;

-- Expected shape (trimmed):
-- Limit  (actual time=0.041..0.118 rows=50 loops=1)
--   -&gt;  Merge Append
--         -&gt;  Index Scan Backward using orders_p2026_08_customer_id_order_ts_idx on orders_p2026_08
--               Index Cond: ((customer_id = 8812931) AND (order_ts &gt;= ...))
--         -&gt;  Index Scan Backward using orders_p2026_07_customer_id_order_ts_idx on orders_p2026_07
--   Buffers: shared hit=14
-- Two partitions touched, 14 buffer hits, no heap fetches beyond the index &mdash; this is the target shape.</pre>
<p>PostgreSQL 18 changes the I/O story that Exadata customers care about most. The new asynchronous I/O subsystem (<code>io_method = worker</code> by default, <code>io_uring</code> on Linux where enabled at build time) lets sequential scans, bitmap heap scans, and VACUUM issue reads ahead of consumption, and the release notes document the supporting changes: skip-scan on B-tree indexes, parallel GIN builds, and <code>pg_stat_io</code> byte-level accounting (<a href="https://www.postgresql.org/docs/release/18.0/" target="_blank" rel="noopener">PostgreSQL 18 release notes</a>). </p>
<p>The configuration deltas from default that we apply on an Exadata-replacement OLTP node are listed with their reload/restart requirement, per house convention. Proposed values are for a 64 vCPU / 512 GB / NVMe node.</p>
<table>
<colgroup>
<col>
<col>
<col>
<col></colgroup>
<thead>
<tr>
<th>Parameter</th>
<th>Default &rarr; proposed (unit)</th>
<th>Applies via</th>
<th>Justifying metric</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>shared_buffers</code></td>
<td>128MB &rarr; <strong>128GB</strong> (bytes)</td>
<td>restart</td>
<td><code>pg_stat_io</code> hit ratio; <code>pg_buffercache</code> usage counts</td>
</tr>
<tr>
<td><code>effective_cache_size</code></td>
<td>4GB &rarr; <strong>384GB</strong> (bytes)</td>
<td>reload</td>
<td>OS page cache size; planner cost accuracy</td>
</tr>
<tr>
<td><code>io_method</code></td>
<td>worker &rarr; <strong>io_uring</strong> (enum)</td>
<td>restart</td>
<td><code>pg_stat_io</code> read latency under seq-scan load (PG 18+, Linux build with liburing)</td>
</tr>
<tr>
<td><code>io_workers</code></td>
<td>3 &rarr; <strong>8</strong> (count)</td>
<td>restart</td>
<td>Only when <code>io_method = worker</code>; <code>pg_stat_io</code> backend type io worker</td>
</tr>
<tr>
<td><code>max_parallel_workers_per_gather</code></td>
<td>2 &rarr; <strong>8</strong> (count)</td>
<td>reload</td>
<td>EXPLAIN ANALYZE &ldquo;Workers Launched&rdquo; on reporting queries that stay on PG</td>
</tr>
<tr>
<td><code>wal_compression</code></td>
<td>off &rarr; <strong>zstd</strong> (enum)</td>
<td>reload</td>
<td><code>pg_stat_wal.wal_bytes</code>; replication bandwidth</td>
</tr>
<tr>
<td><code>synchronous_commit</code></td>
<td>on &rarr; <strong>on</strong>, with a sync standby in <code>synchronous_standby_names</code> (enum)</td>
<td>reload</td>
<td>RPO = 0 requirement; <code>pg_stat_replication.sync_state</code></td>
</tr>
<tr>
<td><code>autovacuum_vacuum_cost_limit</code></td>
<td>-1 (200) &rarr; <strong>2000</strong> (cost units)</td>
<td>reload</td>
<td><code>pg_stat_user_tables.n_dead_tup</code> trend; bloat under Oracle-style update-heavy load</td>
</tr>
<tr>
<td><code>track_io_timing</code></td>
<td>off &rarr; <strong>on</strong> (bool)</td>
<td>reload</td>
<td>Required for <code>pg_stat_statements</code> I/O time columns</td>
</tr>
</tbody>
</table>
<p>Test every value in staging under replayed production load before applying to production, and keep a robust DR posture (verified pgBackRest restore) before any restart-class change.</p>
<h3>5.2 Analytics on ClickHouse: the Smart Scan replacement<a class="anchor-link" id="5-2-analytics-on-clickhouse-the-smart-scan-replacement"></a></h3>
<p>The reporting queries that made Exadata Smart Scan indispensable have a consistent shape: filter by time and a low-cardinality dimension, aggregate measures, group by a handful of columns. On a columnar MergeTree table with the right <code>ORDER BY</code>, ClickHouse reads only the granules and columns the query touches, which is the same I/O-avoidance idea Smart Scan implements in storage cells, applied at the storage format instead of the storage hardware. Engine declarations carry full parameter lists &mdash; house rule, and the difference between a table that replicates and one that silently does not.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="ClickHouse 26.3 LTS: replicated fact table fed by CDC from PostgreSQL">CREATE TABLE sales.orders_local ON CLUSTER 'analytics'
(
    order_id      UInt64,
    customer_id   UInt64,
    order_ts      DateTime('UTC') CODEC(DoubleDelta, ZSTD(3)),
    status        LowCardinality(String),
    amount        Decimal(14, 2)  CODEC(T64, ZSTD(3)),
    region_code   LowCardinality(FixedString(3)),
    _version      UInt64,                       -- Debezium source.lsn / ts_ms, drives ReplacingMergeTree dedup
    _deleted      UInt8 DEFAULT 0,
    INDEX ix_customer customer_id TYPE bloom_filter(0.01) GRANULARITY 4
)
ENGINE = ReplicatedReplacingMergeTree(
    '/clickhouse/tables/{shard}/sales/orders_local',
    '{replica}',
    _version
)
PARTITION BY toYYYYMM(order_ts)
ORDER BY (region_code, status, order_ts, order_id)
TTL order_ts + INTERVAL 18 MONTH TO VOLUME 'cold_s3'
SETTINGS
    index_granularity            = 8192,
    storage_policy               = 'tiered',
    min_bytes_for_wide_part      = 10485760,
    ttl_only_drop_parts          = 1;

CREATE TABLE sales.orders ON CLUSTER 'analytics'
AS sales.orders_local
ENGINE = Distributed('analytics', 'sales', 'orders_local', cityHash64(customer_id));

-- Kafka engine consumer for the Debezium topic (JSON unwrapped by the ExtractNewRecordState SMT)
CREATE TABLE sales.orders_kafka ON CLUSTER 'analytics'
(
    order_id UInt64, customer_id UInt64, order_ts DateTime('UTC'),
    status String, amount Decimal(14,2), region_code String,
    __lsn UInt64, __deleted String
)
ENGINE = Kafka
SETTINGS
    kafka_broker_list        = 'kafka-1:9092,kafka-2:9092,kafka-3:9092',
    kafka_topic_list         = 'pg.sales.orders',
    kafka_group_name         = 'clickhouse-sales-orders',
    kafka_format             = 'JSONEachRow',
    kafka_num_consumers      = 4,
    kafka_max_block_size     = 65536,
    kafka_handle_error_mode  = 'stream';

CREATE MATERIALIZED VIEW sales.orders_mv ON CLUSTER 'analytics'
TO sales.orders_local AS
SELECT
    order_id, customer_id, order_ts, status, amount,
    toFixedString(region_code, 3) AS region_code,
    __lsn                          AS _version,
    if(__deleted = 'true', 1, 0)   AS _deleted
FROM sales.orders_kafka;</pre>
<p>Plan reasoning for the reporting query: with <code>ORDER BY (region_code, status, order_ts, order_id)</code>, a query filtering on region and a month range touches only the matching partition and the primary-key granules for that region prefix. <code>EXPLAIN indexes = 1</code> shows the pruning; <code>system.query_log</code> shows the outcome. On ClickHouse 26.3 LTS the evidence pair is <code>EXPLAIN PIPELINE</code> plus <code>system.trace_log</code>; 26.7 adds <code>EXPLAIN ANALYZE</code>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="ClickHouse: verify granule pruning, then measure from system.query_log">EXPLAIN indexes = 1
SELECT
    region_code,
    status,
    count()      AS orders,
    sum(amount)  AS revenue
FROM sales.orders FINAL
WHERE region_code = 'APJ'
  AND order_ts &gt;= toDateTime('2026-07-01 00:00:00', 'UTC')
  AND order_ts = now() - INTERVAL 1 DAY
  AND has(tables, 'sales.orders_local')
GROUP BY normalized_query_hash
ORDER BY p99_ms DESC
LIMIT 20;</pre>
<p>Two ClickHouse 26.3 LTS specifics matter for an Exadata replacement. First, <code>async_insert</code> became enabled by default in 26.3; on a CDC-fed estate pin <code>async_insert = 0</code> in the ingestion profile before cutover and re-enable deliberately under Keeper observation, because the change alters part-count and Keeper load characteristics. Second, use <code>FINAL</code> or <code>argMax</code> patterns on ReplacingMergeTree only where the reporting SLA tolerates it; for dashboards, a scheduled <code>OPTIMIZE ... FINAL</code> on closed partitions or a projection is cheaper. The ClickHouse engineering for this tier is delivered by our sister company, <a href="https://chistadata.com/" target="_blank" rel="noopener">ChistaDATA</a>, and scoped through MinervaDB&rsquo;s <a href="https://minervadb.com/clickhouse-consulting/">ClickHouse consulting practice</a>.</p>
<h3>5.3 Hot keys on Valkey 9.1<a class="anchor-link" id="5-3-hot-keys-on-valkey-9-1"></a></h3>
<p>Oracle&rsquo;s result cache and KEEP buffer pool absorb the repeated point reads (session state, entitlement lookups, reference data) that would otherwise be latency outliers. Valkey 9.1 &mdash; BSD-3-licensed, Linux Foundation governed, with hash-field expiration and multi-database cluster mode since 9.0 (<a href="https://valkey.io/blog/introducing-valkey-9/" target="_blank" rel="noopener">Valkey 9 release blog</a>) &mdash; takes that role with write-through invalidation from the CDC stream, so the cache never serves a value newer data has superseded.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-title="Valkey 9.1: CDC-driven write-through cache for customer entitlements">import json, os
from confluent_kafka import Consumer
from valkey.cluster import ValkeyCluster

vk = ValkeyCluster(
    host=os.environ["VALKEY_HOST"], port=6379,
    password=os.environ["VALKEY_PASSWORD"], ssl=True,
)

consumer = Consumer({
    "bootstrap.servers": os.environ["KAFKA_BOOTSTRAP"],
    "group.id": "valkey-entitlement-cache",
    "auto.offset.reset": "earliest",
    "enable.auto.commit": False,
})
consumer.subscribe(["pg.sales.customer_entitlements"])

while True:
    msg = consumer.poll(1.0)
    if msg is None or msg.error():
        continue
    row = json.loads(msg.value())
    key = f"ent:{row['customer_id']}"
    if row.get("__deleted") == "true":
        vk.delete(key)
    else:
        # Hash with per-field TTL (Valkey 9.0+): plan fields expire independently
        vk.hset(key, mapping={"tier": row["tier"], "limits": row["limits_json"]})
        vk.hexpire(key, 86400, "limits")
    consumer.commit(msg)</pre>
<h2>6. Scalability without RAC<a class="anchor-link" id="6-scalability-without-rac"></a></h2>
<p>RAC scales one database across nodes over shared storage. The open source stack scales each tier by the mechanism that suits its access pattern, and the honest statement is that write scaling for a single PostgreSQL primary is vertical (PostgreSQL 18 on a 2-socket 192-core node comfortably exceeds the enabled-core footprint most Exadata quarter racks license) while read scaling and analytic scaling are horizontal.</p>
<p>Read scaling for Oracle Exadata cost optimization targets: hot standbys behind PgBouncer with <code>hot_standby_feedback = on</code> and a read-routing pool; replication lag from <code>pg_stat_replication</code> is the SLO. Analytic scaling: add ClickHouse shards, rebalance with <code>Distributed</code> table weights, and keep <code>system.parts</code> per-partition counts inside the merge budget; parallel replicas (<code>enable_parallel_replicas</code>) fan a single heavy query across replicas of one shard. </p>
<p>Write scaling beyond a single primary, if the workload truly demands it after measurement (<code>pg_stat_database.xact_commit</code> rate against saturation evidence in <code>pg_stat_activity</code> wait events), is Citus sharding on a tenant or hash key &mdash; a step we scope only after vertical headroom is measured, because it changes the data model. </p>
<p>Connection scaling is solved at the pooler: Oracle&rsquo;s dedicated-server assumptions meet PostgreSQL&rsquo;s process-per-connection model, and PgBouncer sizing from measured concurrency is part of every migration design, not an afterthought.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini" data-enlighter-title="PgBouncer 1.25: transaction pooling sized from measured concurrency">[databases]
sales_rw = host=pg-primary.internal port=5432 dbname=sales pool_size=64
sales_ro = host=pg-replicas.internal port=5432 dbname=sales pool_size=128

[pgbouncer]
listen_addr             = 0.0.0.0
listen_port             = 6432
auth_type               = scram-sha-256
auth_file               = /etc/pgbouncer/userlist.txt
pool_mode               = transaction
max_client_conn         = 10000
default_pool_size       = 64
reserve_pool_size       = 16
server_idle_timeout     = 300
max_prepared_statements = 200      ; 1.24+ enables prepared statements in transaction mode
server_tls_sslmode      = verify-full
server_tls_ca_file      = /etc/ssl/certs/internal-ca.pem
stats_period            = 60</pre>
<h2>7. Availability and reliability engineering<a class="anchor-link" id="7-availability-and-reliability-engineering"></a></h2>
<p>Availability numbers are engineered, not quoted, and Oracle Exadata cost optimization is meaningless if the replacement stack cannot match the protection tier. Exadata estates typically run RAC for instance failure plus Data Guard for site failure; the open source stack reaches the same protection tiers with Patroni for automated failover, synchronous replication for RPO = 0, pgBackRest for point-in-time recovery, and ReplicatedMergeTree with a dedicated Keeper ensemble for the analytics tier. The arithmetic below is what we put in front of a customer before claiming any nines.</p>
<table>
<thead>
<tr>
<th>Failure mode</th>
<th>Detection</th>
<th>Recovery action</th>
<th>Illustrative RTO budget</th>
<th>RPO</th>
<th>Evidence source</th>
</tr>
</thead>
<tbody>
<tr>
<td>PostgreSQL primary crash</td>
<td>Patroni <code>ttl</code> 30 s / <code>loop_wait</code> 10 s</td>
<td>Quorum failover to sync standby; PgBouncer re-resolves via Patroni REST</td>
<td>&le; 45 s (drill-measured, quarterly)</td>
<td>0 (sync standby)</td>
<td><code>patronictl history</code>, <code>pg_stat_replication</code></td>
</tr>
<tr>
<td>AZ loss</td>
<td>etcd quorum + Patroni</td>
<td>Failover to surviving AZ; async standby promoted to sync</td>
<td>&le; 60 s</td>
<td>0</td>
<td>Drill log; <code>pg_last_wal_receive_lsn()</code> on survivors</td>
</tr>
<tr>
<td>Logical corruption / bad deploy</td>
<td>Application / monitoring</td>
<td>pgBackRest PITR to a timestamp; ClickHouse partition restore</td>
<td>Minutes to hours by data size (measured monthly)</td>
<td>Seconds (WAL archive interval)</td>
<td><code>pgbackrest info</code>, restore drill timings</td>
</tr>
<tr>
<td>ClickHouse replica loss</td>
<td>Keeper session expiry</td>
<td>Queries route to surviving replica; replacement replica re-fetches parts</td>
<td>0 for reads; rebuild in background</td>
<td>0</td>
<td><code>system.replicas</code>, <code>system.replication_queue</code></td>
</tr>
<tr>
<td>Kafka broker loss</td>
<td>KRaft controller</td>
<td>ISR shrinks; producers with <code>acks=all</code> continue on min.insync=2</td>
<td>0</td>
<td>0</td>
<td><code>UnderReplicatedPartitions</code> metric</td>
</tr>
<tr>
<td>Region loss</td>
<td>Manual / runbook gate</td>
<td>Promote DR PostgreSQL; ClickHouse cross-region replicas serve reads</td>
<td>&le; 15 min (runbook-drilled)</td>
<td>&le; async lag (measured)</td>
<td>Quarterly DR drill report</td>
</tr>
</tbody>
</table>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-title="Patroni 4.1: three-node PostgreSQL 18 cluster with quorum failover and one synchronous standby">scope: sales-pg18
namespace: /minervadb/
name: pg-az-a

restapi:
  listen: 0.0.0.0:8008
  connect_address: pg-az-a.internal:8008

etcd3:
  hosts: etcd-1.internal:2379,etcd-2.internal:2379,etcd-3.internal:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576        # bytes; async standby is never promoted beyond this
    synchronous_mode: quorum                # Patroni 4.x quorum-based synchronous replication
    synchronous_node_count: 1
    failsafe_mode: true                     # keep primary up if DCS is unreachable but members are
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        max_wal_senders: 16
        max_replication_slots: 16
        hot_standby: "on"
        hot_standby_feedback: "on"
        wal_keep_size: 8GB
        archive_mode: "on"
        archive_command: "pgbackrest --stanza=sales archive-push %p"
        restore_command: "pgbackrest --stanza=sales archive-get %f %p"

postgresql:
  listen: 0.0.0.0:5432
  connect_address: pg-az-a.internal:5432
  data_dir: /pgdata/18/main
  bin_dir: /usr/lib/postgresql/18/bin
  authentication:
    replication:
      username: replicator
      password: ${PG_REPL_PASSWORD}
    superuser:
      username: postgres
      password: ${PG_SUPER_PASSWORD}
  create_replica_methods:
    - pgbackrest
    - basebackup
  pgbackrest:
    command: /usr/bin/pgbackrest --stanza=sales --delta restore
    keep_data: true
    no_params: true

tags:
  nofailover: false
  noloadbalance: false
  sync_priority: 100        # prefer this node as the synchronous standby when it is a replica</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="ini" data-enlighter-title="pgBackRest 2.59: encrypted, S3-backed, with retention that supports 35-day PITR">[global]
repo1-type=s3
repo1-s3-bucket=${PGBACKREST_BUCKET}
repo1-s3-endpoint=s3.ap-south-1.amazonaws.com
repo1-s3-region=ap-south-1
repo1-path=/pgbackrest
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=${PGBACKREST_CIPHER_PASS}
repo1-retention-full=4
repo1-retention-diff=14
repo1-retention-archive-type=full
repo1-bundle=y
repo1-block=y
process-max=8
compress-type=zst
compress-level=3
archive-async=y
spool-path=/var/spool/pgbackrest
log-level-console=info

[sales]
pg1-path=/pgdata/18/main
pg1-port=5432
pg1-user=postgres
# 2.59.0 restricts root execution to `restore` by default &mdash; run backups as the postgres user.</pre>
<p>Reliability discipline is where Exadata-class expectations are actually met: monthly <code>pgbackrest restore --type=time</code> validation on an isolated host, quarterly Patroni switchover drills with timings recorded, quarterly ClickHouse partition-restore drills via clickhouse-backup, and a written escalation matrix. MinervaDB&rsquo;s <a href="https://minervadb.com/24-7-emergency-dba-coverage/">24&times;7 emergency DBA coverage</a> operates on S1 15-minute response, and the drills are what make that response meaningful.</p>
<h2>8. The migration program: assess, convert, replicate, cut over<a class="anchor-link" id="8-the-migration-program-assess-convert-replicate-cut-over"></a></h2>
<figure>
<img loading="lazy" decoding="async" src="image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDMwMCIgd2lkdGg9IjEwMCUiIHJvbGU9ImltZyIgYXJpYS1sYWJlbD0iU2V2ZW4tcGhhc2UgT3JhY2xlIEV4YWRhdGEgdG8gb3BlbiBzb3VyY2UgbWlncmF0aW9uIHRpbWVsaW5lOiBhc3Nlc3MsIG9mZmxvYWQgYW5hbHl0aWNzLCBjb252ZXJ0IHNjaGVtYSBhbmQgUEwvU1FMLCBidWxrIGxvYWQsIENEQyBwYXJhbGxlbCBydW4sIGN1dCBvdmVyLCBzdGFiaWxpemUgYW5kIGRlY29tbWlzc2lvbiI+CjxkZWZzPjxzdHlsZT4KLnBoe2ZpbGw6I2ZmZmZmZjtzdHJva2U6IzFmM2E1ZjtzdHJva2Utd2lkdGg6MjtyeDoxMH0KLnBoMntmaWxsOiNlYWYxZjg7c3Ryb2tlOiMxZjNhNWY7c3Ryb2tlLXdpZHRoOjI7cng6MTB9Ci50dHtmb250LWZhbWlseTpJbnRlcixBcmlhbCxIZWx2ZXRpY2Esc2Fucy1zZXJpZjtmb250LXNpemU6MTRweDtmb250LXdlaWdodDo3MDA7ZmlsbDojMWYzYTVmfQoudHh7Zm9udC1mYW1pbHk6SW50ZXIsQXJpYWwsSGVsdmV0aWNhLHNhbnMtc2VyaWY7Zm9udC1zaXplOjExcHg7ZmlsbDojNGE1YTZhfQoud2t7Zm9udC1mYW1pbHk6SW50ZXIsQXJpYWwsSGVsdmV0aWNhLHNhbnMtc2VyaWY7Zm9udC1zaXplOjEycHg7Zm9udC13ZWlnaHQ6NzAwO2ZpbGw6I2MwMzkyYn0KLmFye3N0cm9rZTojMWYzYTVmO3N0cm9rZS13aWR0aDoyO2ZpbGw6bm9uZTttYXJrZXItZW5kOnVybCgjYTIpfQo8L3N0eWxlPjxtYXJrZXIgaWQ9ImEyIiBtYXJrZXJXaWR0aD0iMTAiIG1hcmtlckhlaWdodD0iMTAiIHJlZlg9IjkiIHJlZlk9IjUiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAsMCBMMTAsNSBMMCwxMCB6IiBmaWxsPSIjMWYzYTVmIi8+PC9tYXJrZXI+PC9kZWZzPgo8dGV4dCB4PSI2MDAiIHk9IjI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHQiPkV4YWRhdGEgZXhpdCBwcm9ncmFtIOKAlCBwaGFzZXMsIGdhdGVzIGFuZCByb2xsYmFjayBwb3N0dXJlIChpbGx1c3RyYXRpdmUgZHVyYXRpb25zIGZvciBhIDIwIFRCIGVzdGF0ZSk8L3RleHQ+CjxnPgo8cmVjdCB4PSIyMCIgeT0iNjAiIHdpZHRoPSIxNTAiIGhlaWdodD0iMTIwIiBjbGFzcz0icGgiLz48dGV4dCB4PSI5NSIgeT0iODUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0dCI+MS4gQXNzZXNzPC90ZXh0Pjx0ZXh0IHg9Ijk1IiB5PSIxMDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0eCI+REJBX09CSkVDVFMgLyBEQkFfU09VUkNFPC90ZXh0Pjx0ZXh0IHg9Ijk1IiB5PSIxMjEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0eCI+Y29tcGxleGl0eSBtYXRyaXg8L3RleHQ+PHRleHQgeD0iOTUiIHk9IjEzNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5BV1IvU1RBVFNQQUNLIGJhc2VsaW5lPC90ZXh0Pjx0ZXh0IHg9Ijk1IiB5PSIxNjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ3ayI+V2Vla3MgMeKAkzQ8L3RleHQ+CjxyZWN0IHg9IjE4NSIgeT0iNjAiIHdpZHRoPSIxNTAiIGhlaWdodD0iMTIwIiBjbGFzcz0icGgyIi8+PHRleHQgeD0iMjYwIiB5PSI4NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR0Ij4yLiBPZmZsb2FkIGFuYWx5dGljczwvdGV4dD48dGV4dCB4PSIyNjAiIHk9IjEwNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5EZWJleml1bSBPcmFjbGUg4oaSIEthZmthPC90ZXh0Pjx0ZXh0IHg9IjI2MCIgeT0iMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPuKGkiBDbGlja0hvdXNlPC90ZXh0Pjx0ZXh0IHg9IjI2MCIgeT0iMTM3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPkV4YWRhdGEgY29yZXMgc2hyaW5rPC90ZXh0Pjx0ZXh0IHg9IjI2MCIgeT0iMTY1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0id2siPldlZWtzIDTigJMxMjwvdGV4dD4KPHJlY3QgeD0iMzUwIiB5PSI2MCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIxMjAiIGNsYXNzPSJwaCIvPjx0ZXh0IHg9IjQyNSIgeT0iODUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0dCI+My4gQ29udmVydDwvdGV4dD48dGV4dCB4PSI0MjUiIHk9IjEwNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5vcmEycGcgc2NoZW1hICsgUEwvU1FMPC90ZXh0Pjx0ZXh0IHg9IjQyNSIgeT0iMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPm9yYWZjZSwgdGVzdCBzdWl0ZTwvdGV4dD48dGV4dCB4PSI0MjUiIHk9IjEzNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij4nJyB2cyBOVUxMIGF1ZGl0PC90ZXh0Pjx0ZXh0IHg9IjQyNSIgeT0iMTY1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0id2siPldlZWtzIDjigJMyMDwvdGV4dD4KPHJlY3QgeD0iNTE1IiB5PSI2MCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIxMjAiIGNsYXNzPSJwaDIiLz48dGV4dCB4PSI1OTAiIHk9Ijg1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHQiPjQuIEJ1bGsgbG9hZDwvdGV4dD48dGV4dCB4PSI1OTAiIHk9IjEwNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5QYXJhbGxlbCBDT1BZIHBpcGVsaW5lPC90ZXh0Pjx0ZXh0IHg9IjU5MCIgeT0iMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPmluZGV4ZXMvRktzIGFmdGVyIGxvYWQ8L3RleHQ+PHRleHQgeD0iNTkwIiB5PSIxMzciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0eCI+cm93LWNvdW50ICsgaGFzaCBjaGVja3M8L3RleHQ+PHRleHQgeD0iNTkwIiB5PSIxNjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ3ayI+V2Vla3MgMTjigJMyMjwvdGV4dD4KPHJlY3QgeD0iNjgwIiB5PSI2MCIgd2lkdGg9IjE1MCIgaGVpZ2h0PSIxMjAiIGNsYXNzPSJwaCIvPjx0ZXh0IHg9Ijc1NSIgeT0iODUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0dCI+NS4gQ0RDIHBhcmFsbGVsIHJ1bjwvdGV4dD48dGV4dCB4PSI3NTUiIHk9IjEwNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5PcmFjbGUg4oaSIFBHIHZpYSBMb2dNaW5lcjwvdGV4dD48dGV4dCB4PSI3NTUiIHk9IjEyMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5yZWNvbmNpbGlhdGlvbiByZXBvcnRzPC90ZXh0Pjx0ZXh0IHg9Ijc1NSIgeT0iMTM3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPmxvYWQgcmVwbGF5IHZzIEFXUiB0b3AtTjwvdGV4dD48dGV4dCB4PSI3NTUiIHk9IjE2NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9IndrIj5XZWVrcyAyMuKAkzMwPC90ZXh0Pgo8cmVjdCB4PSI4NDUiIHk9IjYwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjEyMCIgY2xhc3M9InBoMiIvPjx0ZXh0IHg9IjkyMCIgeT0iODUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0dCI+Ni4gQ3V0IG92ZXI8L3RleHQ+PHRleHQgeD0iOTIwIiB5PSIxMDUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ0eCI+c3RvcCB3cml0ZXMsIGxhZyDihpIgMDwvdGV4dD48dGV4dCB4PSI5MjAiIHk9IjEyMSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5zd2l0Y2ggY29ubmVjdGlvbiBzdHJpbmdzPC90ZXh0Pjx0ZXh0IHg9IjkyMCIgeT0iMTM3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPnJldmVyc2UgQ0RDIFBHIOKGkiBPcmFjbGU8L3RleHQ+PHRleHQgeD0iOTIwIiB5PSIxNjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGNsYXNzPSJ3ayI+V2VlayAzMCAod2luZG93KTwvdGV4dD4KPHJlY3QgeD0iMTAxMCIgeT0iNjAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMTIwIiBjbGFzcz0icGgiLz48dGV4dCB4PSIxMDk1IiB5PSI4NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR0Ij43LiBTdGFiaWxpemU8L3RleHQ+PHRleHQgeD0iMTA5NSIgeT0iMTA1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPnBnX3N0YXRfc3RhdGVtZW50cyB2cyBBV1I8L3RleHQ+PHRleHQgeD0iMTA5NSIgeT0iMTIxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0idHgiPmF1dG92YWN1dW0gdHVuaW5nPC90ZXh0Pjx0ZXh0IHg9IjEwOTUiIHk9IjEzNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgY2xhc3M9InR4Ij5kZWNvbW1pc3Npb24gYXQgc2lnbi1vZmY8L3RleHQ+PHRleHQgeD0iMTA5NSIgeT0iMTY1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBjbGFzcz0id2siPldlZWtzIDMw4oCTNDI8L3RleHQ+CjwvZz4KPHBhdGggZD0iTTE3MCwxMjAgTDE4NSwxMjAiIGNsYXNzPSJhciIvPjxwYXRoIGQ9Ik0zMzUsMTIwIEwzNTAsMTIwIiBjbGFzcz0iYXIiLz48cGF0aCBkPSJNNTAwLDEyMCBMNTE1LDEyMCIgY2xhc3M9ImFyIi8+PHBhdGggZD0iTTY2NSwxMjAgTDY4MCwxMjAiIGNsYXNzPSJhciIvPjxwYXRoIGQ9Ik04MzAsMTIwIEw4NDUsMTIwIiBjbGFzcz0iYXIiLz48cGF0aCBkPSJNOTk1LDEyMCBMMTAxMCwxMjAiIGNsYXNzPSJhciIvPgo8cmVjdCB4PSIyMCIgeT0iMjAwIiB3aWR0aD0iMTE2MCIgaGVpZ2h0PSI5NSIgZmlsbD0iI2ZmZjZmNSIgc3Ryb2tlPSIjYzAzOTJiIiBzdHJva2Utd2lkdGg9IjEuNSIgcng9IjEwIi8+Cjx0ZXh0IHg9IjQwIiB5PSIyMjIiIGNsYXNzPSJ0dCIgZmlsbD0iI2MwMzkyYiI+Um9sbGJhY2sgcG9zdHVyZSBhdCBldmVyeSBnYXRlPC90ZXh0Pgo8dGV4dCB4PSI0MCIgeT0iMjQyIiBjbGFzcz0idHgiPlBoYXNlcyAx4oCTNTogT3JhY2xlIHJlbWFpbnMgc3lzdGVtIG9mIHJlY29yZDsgbm90aGluZyBpcyBkZXN0cnVjdGl2ZS4gUGhhc2UgNjogcmV2ZXJzZSBDREMgKFBvc3RncmVTUUwg4oaSIE9yYWNsZSkgc3RheXMgd2FybSB1bnRpbCBzaWduLW9mZiw8L3RleHQ+Cjx0ZXh0IHg9IjQwIiB5PSIyNTgiIGNsYXNzPSJ0eCI+c28gcm9sbGJhY2sgPSBzd2l0Y2ggY29ubmVjdGlvbiBzdHJpbmdzIGJhY2sgYW5kIGRyYWluIHJldmVyc2UgbGFnIHRvIHplcm8uIFBoYXNlIDc6IEV4YWRhdGEgbGljZW5jZXMgYXJlIG5vdCB0ZXJtaW5hdGVkIGFuZCB0aGUgcmFjayBpcyBub3Q8L3RleHQ+Cjx0ZXh0IHg9IjQwIiB5PSIyNzQiIGNsYXNzPSJ0eCI+ZGVjb21taXNzaW9uZWQgdW50aWwgdGhlIHJlY29uY2lsaWF0aW9uIGFuZCBwZXJmb3JtYW5jZSBzaWduLW9mZiBnYXRlIGlzIHBhc3NlZCBpbiB3cml0aW5nLiBObyBEUk9QL1RSVU5DQVRFIHdpdGhvdXQgYW4gZXhwbGljaXQgY29uZmlybWF0aW9uIGdhdGUuPC90ZXh0Pgo8L3N2Zz4=" alt="Oracle Exadata exit program: seven migration phases with gates and rollback posture" width="1200" height="660"><figcaption>Figure 2. Program phases with gates and rollback posture. Durations are illustrative and scale with PL/SQL volume, not with data size.</figcaption></figure>
<h3>8.1 Assessment: measure the estate, never guess it<a class="anchor-link" id="8-1-assessment-measure-the-estate-never-guess-it"></a></h3>
<p>Oracle Exadata cost optimization by migration starts here: effort is measured from the Oracle catalog. The output is an object-count matrix by complexity class and conversion route (automatic / assisted / manual rewrite), and it is the single artifact that turns a whitepaper into a funded program.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Oracle: migration assessment inventory">-- Object inventory by type
SELECT owner, object_type, COUNT(*) AS objects
FROM dba_objects
WHERE owner IN (${APP_SCHEMAS})
GROUP BY owner, object_type
ORDER BY objects DESC;

-- PL/SQL volume by unit (lines of code drive conversion effort, not table count)
SELECT owner, name, type, COUNT(*) AS loc
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
GROUP BY owner, name, type
ORDER BY loc DESC;

-- Constructs with no direct community-PostgreSQL equivalent (each is a line item)
SELECT owner, name, type, COUNT(*) AS hits, 'AUTONOMOUS_TRANSACTION' AS construct
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
  AND UPPER(text) LIKE '%PRAGMA AUTONOMOUS_TRANSACTION%'
GROUP BY owner, name, type
UNION ALL
SELECT owner, name, type, COUNT(*), 'BULK_COLLECT'
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
  AND UPPER(text) LIKE '%BULK COLLECT%'
GROUP BY owner, name, type
UNION ALL
SELECT owner, name, type, COUNT(*), 'CONNECT_BY'
FROM dba_source
WHERE owner IN (${APP_SCHEMAS})
  AND UPPER(text) LIKE '%CONNECT BY%'
GROUP BY owner, name, type
ORDER BY hits DESC;

-- Feature dependencies: DB links, AQ, VPD, MV fast refresh
SELECT 'DB_LINK' AS dependency, COUNT(*) AS n FROM dba_db_links
UNION ALL SELECT 'AQ_QUEUE',   COUNT(*) FROM dba_queues WHERE owner IN (${APP_SCHEMAS})
UNION ALL SELECT 'VPD_POLICY', COUNT(*) FROM dba_policies WHERE object_owner IN (${APP_SCHEMAS})
UNION ALL SELECT 'MV_FAST_REFRESH', COUNT(*) FROM dba_mviews WHERE owner IN (${APP_SCHEMAS}) AND refresh_method = 'FAST';</pre>
<h3>8.2 Conversion with ora2pg and orafce<a class="anchor-link" id="8-2-conversion-with-ora2pg-and-orafce"></a></h3>
<p><a href="https://ora2pg.darold.net/" target="_blank" rel="noopener">Ora2Pg</a> produces the assessment report (with its own cost-unit estimate per object) and performs schema, data, and a first-pass PL/SQL conversion; <a href="https://github.com/orafce/orafce" target="_blank" rel="noopener">orafce</a> supplies the Oracle-compatible functions (<code>NVL</code>, <code>DECODE</code>, <code>ADD_MONTHS</code>, <code>DBMS_OUTPUT</code>, and others) that keep converted code readable. Packages become schemas plus functions; package state becomes session GUCs or a state table; autonomous transactions become <code>dblink</code> or a background-worker pattern, chosen per call site.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini" data-enlighter-title="ora2pg.conf: assessment and schema export (excerpt)">ORACLE_DSN        dbi:Oracle:host=exa-scan.internal;sid=SALESPDB;port=1521
ORACLE_USER       ${ORA_MIG_USER}
ORACLE_PWD        ${ORA_MIG_PASSWORD}
SCHEMA            SALES
PG_VERSION        18
TYPE              TABLE,VIEW,SEQUENCE,TRIGGER,FUNCTION,PROCEDURE,PACKAGE,TYPE,PARTITION,MVIEW
EXPORT_SCHEMA     1
DATA_TYPE         DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW:bytea,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:numeric,INTEGER:numeric,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
PG_NUMERIC_TYPE   1
PG_INTEGER_TYPE   1
DEFAULT_NUMERIC   bigint
USE_ORAFCE        1
PLSQL_PGSQL       1
NULL_EQUAL_EMPTY  0        ; force the '' vs NULL audit instead of masking it
ESTIMATE_COST     1
COST_UNIT_VALUE   5
PARALLEL_TABLES   8
JOBS              8
ORACLE_COPIES     8
DATA_LIMIT        20000
FILE_PER_TABLE    1
OUTPUT_DIR        /migration/sales</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="shell" data-enlighter-title="Run the assessment, then export schema and code">ora2pg -c /migration/ora2pg.conf -t SHOW_REPORT --estimate_cost --dump_as_html &gt; /migration/sales/assessment.html
ora2pg -c /migration/ora2pg.conf -t TABLE     -o schema_tables.sql
ora2pg -c /migration/ora2pg.conf -t PACKAGE   -o packages.sql
ora2pg -c /migration/ora2pg.conf -t FUNCTION  -o functions.sql
# Data: COPY pipeline, 8 parallel jobs, indexes and FKs applied after load
ora2pg -c /migration/ora2pg.conf -t COPY -j 8 -J 8</pre>
<h3>8.3 CDC with Debezium: parallel run and rehearsable cutover<a class="anchor-link" id="8-3-cdc-with-debezium-parallel-run-and-rehearsable-cutover"></a></h3>
<p>The cutover is where an Oracle Exadata cost optimization program is won or lost, so it must be rehearsable and reversible. Debezium 3.6&rsquo;s Oracle connector (<a href="https://debezium.io/documentation/reference/stable/connectors/oracle.html" target="_blank" rel="noopener">documentation</a>) captures from LogMiner without a GoldenGate license, or from <a href="https://github.com/bersler/OpenLogReplicator" target="_blank" rel="noopener">OpenLogReplicator</a> for lower source overhead on high-redo estates; XStream requires a GoldenGate license and is not used. </p>
<p>The stream lands in Kafka 4.3 (KRaft-only since 4.0) and is applied to PostgreSQL by the JDBC sink connector during the parallel run. After cutover, the same topology is reversed &mdash; Debezium&rsquo;s PostgreSQL connector on <code>pgoutput</code> feeding a JDBC sink into Oracle &mdash; and kept warm as the rollback path until sign-off.</p>
<figure>
<p>Phase 5 &mdash; parallel run (Oracle is system of record)<br>
Oracle 19c on Exadatawrites &middot; supplemental logging ON<br>
Debezium 3.6Oracle connector (LogMiner)<br>
Kafka 4.3ora.sales.* topics, RF=3<br>
JDBC sinkupsert, pk-based, idempotent<br>
PostgreSQL 18read-only validation &middot; reconciliation<br>
Reconcilecounts &middot; hashes</p>
<p>Phase 6 &mdash; after cutover (PostgreSQL is system of record)<br>
PostgreSQL 18writes &middot; wal_level = logical<br>
Debezium 3.6PostgreSQL connector (pgoutput)<br>
Kafka 4.3pg.sales.* topics<br>
JDBC sink&rarr; Oracle (reverse)<br>
Oracle (warm)rollback target<br>
ClickHouse 26.3analytics consumer</p>
<p>&mdash; &mdash; reverse CDC: rollback = re-point applications to Oracle, drain lag to zero<figcaption>Figure 3. CDC topology before and after cutover. The reverse path is the rollback plan and stays live until written sign-off.</figcaption></p></figure>
<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-title="Debezium 3.6 Oracle connector (LogMiner) &mdash; Kafka Connect configuration">{
  "name": "ora-sales-source",
  "config": {
    "connector.class": "io.debezium.connector.oracle.OracleConnector",
    "tasks.max": "1",
    "database.hostname": "exa-scan.internal",
    "database.port": "1521",
    "database.user": "${ORA_CDC_USER}",
    "database.password": "${ORA_CDC_PASSWORD}",
    "database.dbname": "SALESCDB",
    "database.pdb.name": "SALESPDB",
    "topic.prefix": "ora",
    "table.include.list": "SALES.ORDERS,SALES.ORDER_ITEMS,SALES.CUSTOMER_ENTITLEMENTS",
    "database.connection.adapter": "logminer",
    "log.mining.strategy": "online_catalog",
    "log.mining.batch.size.default": "20000",
    "log.mining.transaction.retention.ms": "3600000",
    "lob.enabled": "false",
    "decimal.handling.mode": "precise",
    "time.precision.mode": "adaptive",
    "snapshot.mode": "initial",
    "schema.history.internal.kafka.bootstrap.servers": "kafka-1:9092,kafka-2:9092,kafka-3:9092",
    "schema.history.internal.kafka.topic": "schema-history.ora.sales",
    "heartbeat.interval.ms": "10000",
    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.delete.handling.mode": "rewrite",
    "transforms.unwrap.add.fields": "op,source.scn,source.ts_ms",
    "key.converter": "io.apicurio.registry.utils.converter.AvroConverter",
    "value.converter": "io.apicurio.registry.utils.converter.AvroConverter",
    "key.converter.apicurio.registry.url": "http://apicurio.internal:8080/apis/registry/v2",
    "value.converter.apicurio.registry.url": "http://apicurio.internal:8080/apis/registry/v2"
  }
}</pre>
<p>Source-side prerequisites are the part Oracle DBAs must own: <code>ARCHIVELOG</code> mode, minimal supplemental logging at database level plus <code>ALL COLUMNS</code> on captured tables, and a CDC user granted the LogMiner privileges the Debezium documentation lists. LogMiner load on the source is measurable in <code>V$SESSION</code> and, on licensed estates, in ASH; on a high-redo estate that overhead is the reason to evaluate OpenLogReplicator.</p>
<h3>8.4 Cutover runbook (excerpt) with verification and rollback<a class="anchor-link" id="8-4-cutover-runbook-excerpt-with-verification-and-rollback"></a></h3>
<p>The full cutover runbook is a versioned MinervaDB deliverable (MDB-RUN-*) with purpose, scope, prerequisites, roles, stepwise commands, expected output, verification after each phase, rollback, and an escalation matrix. The core sequence, with its verification queries, is reproduced here because it is the part readers ask for.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Cutover gates: verify lag is zero before and reconcile after">-- GATE 1 (Oracle side, writes already stopped at the application tier): confirm no in-flight transactions
SELECT COUNT(*) AS active_txns
FROM v$transaction;
-- expected: 0

-- GATE 2 (Kafka Connect): confirm the source connector has no lag (consumer group for the sink)
-- kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --describe --group connect-pg-sales-sink
-- expected: LAG column = 0 on every partition

-- GATE 3 (PostgreSQL side): per-table reconciliation against Oracle counts captured at GATE 1
SELECT
    'orders'                         AS table_name,
    COUNT(*)                         AS row_count,
    md5(string_agg(order_id::text || ':' || amount::text, ',' ORDER BY order_id)) AS content_hash
FROM sales.orders
WHERE order_ts &gt;= DATE '2026-01-01';   -- hot-window hash; full-table hashes run in the parallel-run reports

-- GATE 4: promote PostgreSQL to system of record (reverse CDC connector started, applications re-pointed)
-- Verification after cutover:
SELECT slot_name, active, confirmed_flush_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS reverse_cdc_lag
FROM pg_replication_slots
WHERE slot_name = 'debezium_reverse';
-- expected: active = t, lag in low MB and falling

-- ROLLBACK (any time before sign-off): stop application writes to PostgreSQL, wait for reverse_cdc_lag = 0 bytes,
-- re-point connection strings to the Oracle service, confirm V$TRANSACTION shows application activity, resume.
-- No object is dropped, truncated, or detached at any point in this runbook without a written confirmation gate.</pre>
<h2>9. Five-year TCO model (illustrative)<a class="anchor-link" id="9-five-year-tco-model-illustrative"></a></h2>
<p>Every figure in this table is illustrative and built from the list prices in Section 1 with a stated discount assumption, so that a reader can substitute their own contract values. It deliberately excludes people cost on both sides &mdash; an Exadata estate and an open source stack both need a database engineering function, and the honest difference is skills mix, not headcount. It also excludes the one-time migration program, which is estate-specific and comes out of the Section 8.1 assessment.</p>
<table>
<thead>
<tr>
<th>Cost line (5 years, USD, illustrative)</th>
<th>Exadata X11M quarter rack, 64 processor licenses, 50% discount</th>
<th>Open source stack (self-managed, commodity/IaaS)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hardware / infrastructure</td>
<td>$157k rack (net) + $315k hardware support</td>
<td>3&times; PostgreSQL nodes (64 vCPU/512 GB/NVMe), 4&times; ClickHouse nodes, 3&times; Keeper, 3&times; Kafka, 6&times; Valkey, 3&times; etcd, 2&times; PgBouncer/Connect: &asymp; $450k&ndash;$700k purchased, or equivalent reserved IaaS</td>
</tr>
<tr>
<td>Database software licenses (net, one-time)</td>
<td>$3.39M (EE + RAC + Partitioning + Adv. Compression + Diag/Tuning) + $180k storage software</td>
<td>$0 &mdash; PostgreSQL License, Apache 2.0 (ClickHouse, Kafka, Debezium), BSD-3 (Valkey), MIT (Patroni, pgBackRest)</td>
</tr>
<tr>
<td>Annual software support (22% of net), &times;5</td>
<td>$3.93M (rising +10%/+20% under 19c Extended Support from 2030)</td>
<td>$0 vendor line; optional subscription support from an independent provider such as MinervaDB, priced per engagement</td>
</tr>
<tr>
<td>Object storage for backups and cold tiers</td>
<td>ZDLRA or third-party; estate-specific</td>
<td>&asymp; $60k&ndash;$120k (S3-class, 5 years, 100&ndash;200 TB)</td>
</tr>
<tr>
<td><strong>Five-year total (excluding people and migration)</strong></td>
<td><strong>&asymp; $7.9M+</strong></td>
<td><strong>&asymp; $0.5M&ndash;$0.8M infrastructure + support subscription</strong></td>
</tr>
</tbody>
</table>
<p>The order-of-magnitude gap survives any reasonable discount assumption because the open source column has no line that scales with cores. </p>
<p>What the gap buys is not free: it is spent on the migration program, on the skills to operate three engines instead of one, and on the Data SRE discipline that keeps the SLOs honest. For most estates in the 10&ndash;100 TB range, the support line alone repays that investment inside the first support renewal cycle &mdash; but that claim is only true for your estate once the Section 8.1 assessment and a load-replay test have been run, which is why MinervaDB does not publish a customer number without them.</p>
<h2>10. Risks, honest edges, and where this does not apply<a class="anchor-link" id="10-risks-honest-edges-and-where-this-does-not-apply"></a></h2>
<p><strong>PL/SQL volume is the schedule driver.</strong> Data size determines the bulk-load window; procedural code determines the program length. Estates above roughly 500k lines of PL/SQL with packages, autonomous transactions, and REF CURSOR-heavy APIs should evaluate EPAS with eyes open, or phase the exit application by application.</p>
<p><strong>Semantics that silently change.</strong> Empty string versus NULL, <code>DATE</code> with time component, <code>NUMBER</code> without precision, sequence <code>CACHE</code> behaviour, and case-sensitive identifiers are the correctness classes that survive a passing conversion and fail in production. Each has a mandatory test in the MinervaDB migration test suite; none is optional.</p>
<p><strong>Optimizer differences are the week-two incident source.</strong> There are no hints in community PostgreSQL (<code>pg_hint_plan</code> only under governance), cardinality estimation differs, and Oracle&rsquo;s adaptive plans have no equivalent. Capture AWR top SQL before migration, compare against <code>pg_stat_statements</code> after, and budget tuning time in the stabilization phase.</p>
<p><strong>ISV applications.</strong> Packaged applications certified only on Oracle (many ERP, core banking, and telecom billing platforms) cannot be migrated by the database team; the decision belongs to the application roadmap. The Section 2 measures still apply.</p>
<p><strong>Materialized-view fast refresh ON COMMIT, Advanced Queuing, and fine-grained VPD</strong> map to application-level patterns (incremental views, pgmq or Kafka, row-level security) rather than one-to-one features; each is a design decision, not a conversion.</p>
<p><strong>What we did not test for this paper.</strong> No benchmark numbers are published here because a benchmark without your workload shape, hardware, and configuration deltas is noise. Where a MinervaDB engagement includes a load-replay comparison, the methodology (hardware, versions, config deltas, dataset, run count, median and spread) is published before the results, per house rule.</p>
<p><strong>Version boundaries.</strong> Claims above are pinned to PostgreSQL 18 (asynchronous I/O, skip scan), ClickHouse 26.3 LTS (<code>async_insert</code> default change; 25.8 LTS leaves support on 29 August 2026), Kafka 4.x (KRaft-only), Debezium 3.6, Valkey 9.x, and Oracle 19c/26ai lifecycle dates as of August 2026. Re-verify before relying on any of them in a contract.</p>
<h2>11. FAQ: Oracle Exadata cost optimization and open source migration<a class="anchor-link" id="11-faq-oracle-exadata-cost-optimization-and-open-source-migration"></a></h2>
<h3>Is Oracle Exadata cost optimization possible without leaving Oracle?<a class="anchor-link" id="is-oracle-exadata-cost-optimization-possible-without-leaving-oracle"></a></h3>
<p>Yes. Measure option usage in <code>DBA_FEATURE_USAGE_STATISTICS</code>, reduce enabled cores with capacity-on-demand, offload analytics to ClickHouse via CDC, and negotiate ahead of the 19c Extended Support uplift. Those four measures are reversible and typically remove 25&ndash;40% of the recurring line (illustrative; your figure comes from your own catalog).</p>
<h3>Can PostgreSQL really match Exadata performance?<a class="anchor-link" id="can-postgresql-really-match-exadata-performance"></a></h3>
<p>For OLTP, PostgreSQL 18 on a modern two-socket NVMe node matches or exceeds the enabled-core footprint most quarter-rack estates license, and the plan-level evidence is <code>EXPLAIN (ANALYZE, BUFFERS)</code> against the same statements. For Exadata-class analytics, PostgreSQL alone is not the answer; ClickHouse is, and that is why the target is a stack. Parity is declared per statement class from <code>pg_stat_statements</code> and <code>system.query_log</code>, never in aggregate.</p>
<h3>How do we replace RAC?<a class="anchor-link" id="how-do-we-replace-rac"></a></h3>
<p>RAC solves instance-failure RTO and read scaling. Patroni quorum failover with a synchronous standby delivers drill-measured RTO in the tens of seconds at RPO = 0; hot standbys behind PgBouncer deliver read scaling. Genuine write-anywhere requirements are rare and are evaluated separately, with the conflict analysis done before any active-active design is proposed.</p>
<h3>Do we need Oracle GoldenGate for the migration?<a class="anchor-link" id="do-we-need-oracle-goldengate-for-the-migration"></a></h3>
<p>No. Debezium&rsquo;s Oracle connector captures from LogMiner without a GoldenGate license; OpenLogReplicator is the lower-overhead alternative on high-redo estates. GoldenGate remains a valid choice where the customer already licenses it.</p>
<h3>What is the rollback plan if cutover fails?<a class="anchor-link" id="what-is-the-rollback-plan-if-cutover-fails"></a></h3>
<p>Reverse CDC from PostgreSQL to Oracle runs from the moment of cutover until written sign-off. Rollback is: stop application writes, drain reverse lag to zero (measured in <code>pg_replication_slots</code>), re-point connection strings to Oracle. Nothing is dropped, truncated, or decommissioned before sign-off.</p>
<h3>How long does an Exadata to open source migration take?<a class="anchor-link" id="how-long-does-an-exadata-to-open-source-migration-take"></a></h3>
<p>Illustratively 30&ndash;42 weeks for a 20 TB estate with moderate PL/SQL, per Figure 2. The schedule scales with procedural code volume rather than data size; the assessment in Section 8.1 replaces that illustration with a measured estimate.</p>
<h3>Who supports the open source stack in production?<a class="anchor-link" id="who-supports-the-open-source-stack-in-production"></a></h3>
<p>Community PostgreSQL, ClickHouse, Kafka, Debezium, and Valkey are supported by their projects; enterprise-grade 24&times;7 support with response SLAs comes from an independent provider. MinervaDB operates on S1 15 minutes / S2 12 hours / S3 24 hours / S4 48 hours, with ClickHouse engineering delivered through ChistaDATA.</p>
<h2>Next steps<a class="anchor-link" id="next-steps"></a></h2>
<p>An Oracle Exadata cost optimization program starts with evidence, not a proposal. MinervaDB&rsquo;s Exadata assessment delivers, within four weeks, the feature-usage baseline and enabled-core analysis for in-place savings, the object and PL/SQL complexity matrix, the AWR or STATSPACK top-SQL baseline mapped to target engines, and a versioned migration plan with rollback gates &mdash; the MDB-MIG deliverable that turns this whitepaper into your program. Talk to the <a href="https://minervadb.com/data-modernization/">MinervaDB data modernization practice</a>, review our <a href="https://minervadb.com/postgresql-consulting/">PostgreSQL consulting</a> and <a href="https://minervadb.com/postgresql-remote-dba/">PostgreSQL remote DBA</a> services, or <a href="https://minervadb.com/contact-minervadb-book-an-appointment/">book an architecture consultation</a>.</p>
<p><em>Standing caveat for all guidance in this document: test every change in a staging environment that mirrors production before applying it, and maintain a verified disaster-recovery posture (tested restores, not just backups) throughout any migration.</em></p>
<h3>References<a class="anchor-link" id="references"></a></h3>
<ul>
<li>Oracle Corporation, <a href="https://www.oracle.com/a/ocom/docs/corporate/pricing/technology-price-list-070617.pdf" target="_blank" rel="noopener">Oracle Technology Global Price List</a> and <a href="https://www.oracle.com/a/ocom/docs/corporate/pricing/exadata-pricelist-070598.pdf" target="_blank" rel="noopener">Oracle Engineered Systems Price List</a>; <a href="https://blogs.oracle.com/exadata/exadata-x11m" target="_blank" rel="noopener">Exadata X11M announcement</a>; <a href="https://docs.oracle.com/en/engineered-systems/exadata-database-machine/books.html" target="_blank" rel="noopener">Exadata documentation</a>.</li>
<li>PostgreSQL Global Development Group, <a href="https://www.postgresql.org/about/news/postgresql-18-released-3142/" target="_blank" rel="noopener">PostgreSQL 18 released</a>; <a href="https://www.postgresql.org/docs/release/18.0/" target="_blank" rel="noopener">release notes</a>; <a href="https://www.postgresql.org/docs/18/logical-replication.html" target="_blank" rel="noopener">logical replication</a>; <a href="https://www.postgresql.org/docs/18/pgstatstatements.html" target="_blank" rel="noopener">pg_stat_statements</a>; <a href="https://www.postgresql.org/support/versioning/" target="_blank" rel="noopener">versioning policy</a>.</li>
<li>ClickHouse, <a href="https://clickhouse.com/docs/faq/operations/production" target="_blank" rel="noopener">which version to use in production</a>; <a href="https://clickhouse.com/docs/engines/table-engines/mergetree-family/replication" target="_blank" rel="noopener">Replicated table engines</a>; <a href="https://clickhouse.com/docs/engines/table-engines/integrations/kafka" target="_blank" rel="noopener">Kafka table engine</a>; <a href="https://clickhouse.com/docs/resources/changelogs/oss/2026" target="_blank" rel="noopener">2026 changelog</a>.</li>
<li>Debezium, <a href="https://debezium.io/releases/" target="_blank" rel="noopener">releases</a> and <a href="https://debezium.io/documentation/reference/stable/connectors/oracle.html" target="_blank" rel="noopener">Oracle connector</a>; Apache Kafka, <a href="https://kafka.apache.org/blog/2026/06/25/apache-kafka-4.3.1-release-announcement/" target="_blank" rel="noopener">4.3.1 release</a>.</li>
<li><a href="https://patroni.readthedocs.io/en/latest/" target="_blank" rel="noopener">Patroni</a>, <a href="https://pgbackrest.org/" target="_blank" rel="noopener">pgBackRest</a>, <a href="https://www.pgbouncer.org/" target="_blank" rel="noopener">PgBouncer</a>, <a href="https://ora2pg.darold.net/" target="_blank" rel="noopener">Ora2Pg</a>, <a href="https://github.com/orafce/orafce" target="_blank" rel="noopener">orafce</a>, <a href="https://github.com/bersler/OpenLogReplicator" target="_blank" rel="noopener">OpenLogReplicator</a>, <a href="https://valkey.io/blog/introducing-valkey-9/" target="_blank" rel="noopener">Valkey 9</a>.</li>
<li>Oracle Database lifecycle dates: <a href="https://endoflife.date/oracle-database" target="_blank" rel="noopener">endoflife.date</a> (re-verify against MOS 742060.1).</li>
<li>Related MinervaDB reading: <a href="https://minervadb.com/rollback-strategy-postgresql-rollback-migration/">PostgreSQL migration rollback strategy</a>; <a href="https://minervadb.com/postgresql-cloud-aws-aurora-gcp-azure/">PostgreSQL on AWS, GCP, and Azure</a>.</li>
</ul>
<p><strong>Revision history:</strong> v1.0, 24 August 2026 &mdash; initial public release. Prepared by MinervaDB Database Architecture Practice; reviewed by MinervaDB PostgreSQL and Oracle practice leads; approved by Shiv Iyer, Founder &amp; CEO, MinervaDB Inc.</p>

<p><a href="https://minervadb.com/oracle-exadata-cost-optimization-open-source-stack/">Oracle Exadata Cost Optimization: Migrating to an Open Source Data Infrastructure Stack Without Compromising Performance, Scalability, Availability, or Reliability</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>BigQuery SOX Compliance Checklist: 12 Proven Controls</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/bigquery-sox-compliance-checklist/" />
      <id>https://minervadb.com/bigquery-sox-compliance-checklist/</id>
      <updated>2026-08-22T18:41:33+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>A production-tested BigQuery SOX compliance checklist: IAM, CMEK, audit-log retention, change management, and the SQL evidence queries auditors accept. [...]</p>
<p><a href="https://minervadb.com/bigquery-sox-compliance-checklist/">BigQuery SOX Compliance Checklist: 12 Proven Controls</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<figure><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/bigquery-sox-compliance-checklist.png" alt="BigQuery SOX Compliance Checklist &mdash; 12 proven controls for IAM, CMEK, audit logs and change management" width="1200" height="630"></figure>
<p>A <strong>BigQuery SOX compliance checklist</strong> succeeds or fails on four IT general control (ITGC) domains: access control, change management, operations, and monitoring. Everything an external auditor will test against your BigQuery estate maps to one of those four, and every one of them can be evidenced directly from Google Cloud primitives &mdash; IAM policies, Cloud Audit Logs, <code>INFORMATION_SCHEMA</code>, and Cloud KMS. This checklist walks through the twelve controls we implement for financial-reporting datasets in BigQuery, with the exact SQL and configuration each control needs and the evidence query an auditor will accept.</p>
<p>Scope note before we start: SOX (Sarbanes&ndash;Oxley Act of 2002, Sections 302 and 404) does not certify databases &mdash; it certifies <em>internal control over financial reporting (ICFR)</em>. BigQuery lands in scope the moment a dataset feeds a number that appears in a financial statement: revenue marts, billing pipelines, order-to-cash aggregates, close automation. Google&rsquo;s own infrastructure controls are covered by its <a href="https://cloud.google.com/security/compliance/soc-2" target="_blank" rel="noopener">SOC 2 Type II reports</a> (issued quarterly, auditable via Compliance Reports Manager); everything above the infrastructure line &mdash; who can read the revenue table, who changed the transformation SQL, how long the access trail is retained &mdash; is yours, and that is what this checklist covers.</p>
<h2>BigQuery SOX Compliance Checklist: The Control Architecture<a class="anchor-link" id="bigquery-sox-compliance-checklist-the-control-architecture"></a></h2>
<p>The twelve controls form three layers: preventive controls on the data itself (IAM, policy tags, row-level security, CMEK, VPC Service Controls), detective controls on activity (audit log pipeline, access monitoring, job history review), and process controls around change (declarative infrastructure, SQL change management, retention and recovery, evidence generation). The diagram below shows how they fit together.</p>
<figure>
       BigQuery SOX Control Architecture (ITGC mapping) <!-- VPC-SC perimeter -->  VPC SERVICE CONTROLS PERIMETER &middot; ORG POLICIES (CONTROL 6) <!-- Preventive layer -->  Preventive &mdash; Access 2 &middot; IAM groups, no primitive roles 3 &middot; Policy tags (column-level) 4 &middot; Row access policies 5 &middot; CMEK (Cloud KMS, 90d rotation) roles/bigquery.dataViewer datacatalog.categoryFineGrainedReader cloudkms.cryptoKeyEncrypterDecrypter <!-- In-scope datasets -->  In-scope datasets (1) finance_prod.revenue_reporting labels: sox_scope=in_scope Financial-reporting marts, close snapshots, billing pipeline Time travel 7d + close snapshots (10) <!-- Change management -->  Process &mdash; Change mgmt (9) Terraform (datasets, IAM, CMEK) dbt / Dataform SQL in PR review CI deploys via pipeline identity: deploy-sa@finance-prod.iam Humans hold no dataEditor on prod (segregation of duties, control 2) <!-- Deploy arrow -->  reviewed DDL only <!-- IAM arrow -->  <!-- Audit logs out -->  audit trail <!-- Detective layer -->  Detective &mdash; Monitoring 7 &middot; Data Access logs (on by default)     &rarr; sink &rarr; audit-prod dataset     7-year retention, separate IAM 8 &middot; Access + write monitoring SQL INFORMATION_SCHEMA.JOBS (180d) BigQueryAuditMetadata Alert: any non-pipeline write to an in-scope table 11 &middot; Quarterly IAM diff + attestation      from SetIamPolicy log events <!-- Evidence pack -->  Evidence pack (12) IAM exports &middot; zero-row write report DDL<img decoding="async" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="&harr;" class="wp-smiley">PR reconciliation &middot; KMS history restore drills &middot; SOC 2 Type II  Google Cloud behavior as of August 2026 &middot; BigQuery + Cloud Logging + Cloud KMS + VPC-SC<figcaption>The twelve controls of the BigQuery SOX compliance checklist, mapped to ITGC domains.</figcaption></figure>
<h2>Layer 1 &mdash; Preventive Controls<a class="anchor-link" id="layer-1-preventive-controls"></a></h2>
<h3>1. Inventory and label in-scope datasets<a class="anchor-link" id="1-inventory-and-label-in-scope-datasets"></a></h3>
<p>You cannot control what you have not scoped. Every SOX engagement starts with a dataset inventory that separates financial-reporting datasets from everything else, because the controls below are expensive to apply estate-wide and auditors only test in-scope objects. Use dataset labels as the scoping mechanism &mdash; they are queryable, enforceable in policy, and visible in billing exports.</p>
<pre><code class="language-sql">-- Inventory candidate in-scope datasets and their labels
SELECT
  catalog_name              AS project_id,
  schema_name               AS dataset_id,
  option_value              AS labels
FROM `region-us`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS
WHERE option_name = 'labels';
</code></pre>
<pre><code class="language-bash"># Label a dataset as SOX in-scope (reversible, no downtime)
bq update --set_label sox_scope:in_scope 
          --set_label data_owner:finance_engineering 
          finance_prod:revenue_reporting
</code></pre>
<h3>2. Enforce least-privilege IAM &mdash; no primitive roles<a class="anchor-link" id="2-enforce-least-privilege-iam-no-primitive-roles"></a></h3>
<p>The single most common SOX finding we see in BigQuery estates is a primitive role (<code>roles/editor</code>, <code>roles/owner</code>) granted at project level, silently conferring write access to every financial table. The control: all access to in-scope datasets flows through Google Groups mapped to job functions, using predefined BigQuery roles (<code>roles/bigquery.dataViewer</code>, <code>roles/bigquery.dataEditor</code>, <code>roles/bigquery.jobUser</code>) or narrower custom roles &mdash; never primitive roles, never individual user grants.</p>
<pre><code class="language-bash"># Evidence query: find primitive-role grants on the project
gcloud projects get-iam-policy finance-prod 
  --flatten="bindings[].members" 
  --filter="bindings.role:(roles/owner OR roles/editor OR roles/viewer)" 
  --format="table(bindings.role, bindings.members)"
</code></pre>
<p>Expected output on a passing control is an empty table (or break-glass accounts only, documented and alerted). Anything else is a segregation-of-duties exception the auditor will sample. Pair this with quarterly access reviews: export the IAM policy per dataset, have the data owner attest, retain the attestation. Segregation of duties in BigQuery terms means the humans who write transformation SQL do not hold <code>dataEditor</code> on production financial datasets &mdash; deployment happens through a service account owned by the CI pipeline (control 9).</p>
<h3>3. Column-level security with policy tags<a class="anchor-link" id="3-column-level-security-with-policy-tags"></a></h3>
<p>SOX scoping frequently overlaps PII and payment data. BigQuery&rsquo;s <a href="https://docs.cloud.google.com/bigquery/docs/column-level-security-intro" target="_blank" rel="noopener">column-level access control</a> attaches policy tags from a Data Catalog taxonomy to individual columns; at query time, reading a tagged column requires the <strong>Fine-Grained Reader</strong> role (<code>datacatalog.categoryFineGrainedReader</code>) on that tag. One policy tag per column, taxonomy and table colocated in the same region, and dynamic data masking available on top &mdash; masked readers get nulls, hashes, or defaults instead of a permission error, which keeps dashboards alive while protecting the raw value.</p>
<pre><code class="language-sql">-- Verify which columns carry policy tags in an in-scope dataset
SELECT
  table_name,
  column_name,
  policy_tags
FROM `finance_prod.revenue_reporting`.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS
WHERE ARRAY_LENGTH(policy_tags.names) &gt; 0
ORDER BY table_name, column_name;
</code></pre>
<h3>4. Row-level security for entity and regional segregation<a class="anchor-link" id="4-row-level-security-for-entity-and-regional-segregation"></a></h3>
<p>Where a single revenue table serves multiple legal entities, <a href="https://docs.cloud.google.com/bigquery/docs/managing-row-level-security" target="_blank" rel="noopener">row-level access policies</a> enforce entity segregation inside the table rather than through fragile view sprawl:</p>
<pre><code class="language-sql">CREATE ROW ACCESS POLICY entity_emea_only
ON `finance_prod.revenue_reporting.fct_revenue`
GRANT TO ('group:finance-emea@example.com')
FILTER USING (legal_entity = 'EMEA');
</code></pre>
<p>Two operational caveats we state in every engagement: row access policies silently filter rows (users see a subset, not an error), so reconciliation jobs must run as an identity with full-table access; and policies do not apply to time-travel reads by users with <code>bigquery.rowAccessPolicies.overrideTimeTravelRestrictions</code>-adjacent bypass paths &mdash; audit who holds table-level admin rights.</p>
<h3>5. Customer-managed encryption keys (CMEK)<a class="anchor-link" id="5-customer-managed-encryption-keys-cmek"></a></h3>
<p>BigQuery encrypts everything at rest by default, but default encryption gives you no key custody evidence. <a href="https://docs.cloud.google.com/bigquery/docs/customer-managed-encryption" target="_blank" rel="noopener">CMEK</a> puts the key-encryption key in your Cloud KMS keyring: you control rotation, you control revocation, and disabling the key renders the dataset unreadable &mdash; a demonstrable termination control. Grant the BigQuery encryption service account (<code>bq-PROJECT_NUMBER@bigquery-encryption.iam.gserviceaccount.com</code>) the <code>roles/cloudkms.cryptoKeyEncrypterDecrypter</code> role, colocate the key with the dataset region (a US multi-region dataset needs a <code>us</code> keyring), and set rotation &le; 90 days for in-scope data.</p>
<pre><code class="language-bash"># Current vs proposed: rotation period on the SOX keyring
# parameter: rotation-period | current: none (manual) | proposed: 90d | applies: next rotation
gcloud kms keys update sox-bq-key 
  --keyring=sox-keyring --location=us 
  --rotation-period=90d 
  --next-rotation-time=2026-09-01T00:00:00Z
</code></pre>
<h3>6. Perimeter controls: VPC Service Controls and organization policies<a class="anchor-link" id="6-perimeter-controls-vpc-service-controls-and-organization-policies"></a></h3>
<p>IAM answers &ldquo;who may read&rdquo;; it does not answer &ldquo;where may the data go.&rdquo; A <a href="https://docs.cloud.google.com/vpc-service-controls/docs/overview" target="_blank" rel="noopener">VPC Service Controls perimeter</a> around the financial projects blocks exfiltration paths IAM cannot see &mdash; <code>bq mk --transfer_config</code> into an external project, result extraction to an out-of-perimeter bucket, cross-project table copies. Complement it with organization policies: domain-restricted sharing (<code>constraints/iam.allowedPolicyMemberDomains</code>) so no grant can name an identity outside your Workspace domain, and disable public dataset access. These two org policies alone close the &ldquo;analyst shares revenue table with personal Gmail&rdquo; finding that appears in a depressing share of first-year audits.</p>
<h2>Layer 2 &mdash; Detective Controls<a class="anchor-link" id="layer-2-detective-controls"></a></h2>
<h3>7. The audit log pipeline: export, retain, lock<a class="anchor-link" id="7-the-audit-log-pipeline-export-retain-lock"></a></h3>
<p>This is the control auditors spend the most time on, and the one with a hard deadline in it. Per <a href="https://docs.cloud.google.com/logging/docs/audit" target="_blank" rel="noopener">Cloud Audit Logs</a> behavior: Admin Activity, System Event, and Policy Denied logs are always on; Data Access logs are disabled by default across Google Cloud <em>except for BigQuery, where they are enabled by default</em> &mdash; every query, every table read, every export lands in <code>BigQueryAuditMetadata</code> (use it, not the legacy <code>AuditData</code> format &mdash; see the <a href="https://docs.cloud.google.com/bigquery/docs/reference/auditlogs" target="_blank" rel="noopener">BigQuery audit logs reference</a>).</p>
<p>The trap is retention. Per <a href="https://docs.cloud.google.com/logging/quotas" target="_blank" rel="noopener">Cloud Logging quotas</a>, the <code>_Required</code> bucket holds Admin Activity logs for a non-configurable 400 days, but Data Access logs land in <code>_Default</code> with <strong>30-day retention</strong>. SOX audit workpaper retention is seven years. Thirty days of query history does not survive an audit cycle, let alone seven years &mdash; so route the logs into BigQuery itself via a log sink, and lock the bucket:</p>
<pre><code class="language-bash"># 1. Sink BigQuery data-access logs into a dedicated audit dataset
gcloud logging sinks create sox-bq-audit-sink 
  bigquery.googleapis.com/projects/audit-prod/datasets/bq_audit_logs 
  --log-filter='resource.type="bigquery_dataset" OR
    protoPayload.metadata."@type"="type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata"' 
  --use-partitioned-tables

# 2. Grant the sink writer identity dataEditor on the audit dataset
#    (printed as writerIdentity by the command above)

# 3. Alternative/parallel: raise _Default retention (1&ndash;3650 days, configurable)
#    parameter: retention-days | current: 30 | proposed: 2555 (7y) | applies: immediately
gcloud logging buckets update _Default --location=global --retention-days=2555
</code></pre>
<p>The audit dataset lives in a separate project with its own IAM (the people being audited must not hold write access to their own trail &mdash; that is the whole point), CMEK-encrypted, with the sink&rsquo;s partitioned tables giving you cheap seven-year storage on long-term pricing.</p>
<figure>
       Audit-log evidence pipeline (control 7)  BigQuery jobs Every query, read, export Data Access logs: ON by default for BigQuery   Cloud Logging _Default bucket 30-day retention <img decoding="async" src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="&#9888;" class="wp-smiley"> (configurable 1&ndash;3650 days)  log sink  Audit dataset audit-prod.bq_audit_logs Partitioned &middot; CMEK &middot; 7-year retention &middot; separate IAM   Evidence Scheduled queries Quarterly reviews Write alerts SOX workpaper retention is seven years &mdash; the 30-day default is the single most common audit-log finding. Auditees hold no write access to the audit project.<figcaption>From default 30-day log retention to a seven-year, tamper-isolated audit trail in BigQuery.</figcaption></figure>
<h3>8. Access monitoring: the queries that answer &ldquo;who touched revenue&rdquo;<a class="anchor-link" id="8-access-monitoring-the-queries-that-answer-who-touched-revenue"></a></h3>
<p>With the sink in place, the auditor&rsquo;s favorite question becomes a query. Who read the revenue tables, when, from where, and did any service account behave anomalously:</p>
<pre><code class="language-sql">-- Who accessed in-scope tables in the last quarter (from the audit sink)
SELECT
  protopayload_auditlog.authenticationInfo.principalEmail AS principal,
  JSON_VALUE(protopayload_auditlog.metadataJson,
             '$.tableDataRead.reason')                    AS read_reason,
  resource.labels.dataset_id                              AS dataset_id,
  COUNT(*)                                                AS access_count,
  MIN(timestamp)                                          AS first_access,
  MAX(timestamp)                                          AS last_access
FROM `audit-prod.bq_audit_logs.cloudaudit_googleapis_com_data_access`
WHERE resource.labels.dataset_id = 'revenue_reporting'
  AND timestamp &gt;= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY principal, read_reason, dataset_id
ORDER BY access_count DESC;
</code></pre>
<p>For interactive investigation inside the retention window, <a href="https://docs.cloud.google.com/bigquery/docs/information-schema-jobs" target="_blank" rel="noopener"><code>INFORMATION_SCHEMA.JOBS</code></a> gives you 180 days of job history without any pipeline:</p>
<pre><code class="language-sql">-- Jobs that wrote to in-scope tables outside the deployment service account
SELECT
  user_email,
  job_id,
  statement_type,
  destination_table.dataset_id,
  destination_table.table_id,
  creation_time
FROM `region-us`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE destination_table.dataset_id = 'revenue_reporting'
  AND statement_type IN ('INSERT', 'UPDATE', 'DELETE', 'MERGE',
                         'CREATE_TABLE_AS_SELECT', 'TRUNCATE_TABLE')
  AND user_email != 'deploy-sa@finance-prod.iam.gserviceaccount.com'
  AND creation_time &gt;= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
ORDER BY creation_time DESC;
</code></pre>
<p>A passing control returns zero rows: nothing writes to financial tables except the pipeline identity. Wire the same predicate into a log-based alert so an out-of-band write pages someone the day it happens, not the quarter it is sampled.</p>
<h3>9. Change management: declarative infrastructure and SQL under version control<a class="anchor-link" id="9-change-management-declarative-infrastructure-and-sql-under-version-control"></a></h3>
<p>Section 404 auditors test change management harder than access control, because manual hotfixes to revenue logic are where restatements come from. The control has three parts. First, schema and infrastructure are declarative &mdash; Terraform owns datasets, IAM bindings, CMEK wiring &mdash; so every change is a reviewed pull request with an approver who is not the author:</p>
<pre><code class="language-hcl">resource "google_bigquery_dataset" "revenue_reporting" {
  dataset_id                      = "revenue_reporting"
  location                        = "US"
  default_partition_expiration_ms = null   # financial data: no silent expiry

  default_encryption_configuration {
    kms_key_name = google_kms_crypto_key.sox_bq_key.id
  }

  labels = {
    sox_scope  = "in_scope"
    data_owner = "finance_engineering"
  }
}
</code></pre>
<p>Second, transformation SQL (dbt, Dataform, or plain scheduled queries) lives in the same review gate &mdash; no console-edited scheduled queries on in-scope datasets. Third, the audit logs closes the loop: every <code>google.cloud.bigquery.v2.JobService.InsertJob</code> with a DDL statement type against an in-scope dataset should reconcile 1:1 with a merged pull request. That reconciliation, run quarterly, is the change-management evidence.</p>
<h3>10. Retention and recoverability: time travel is not a backup<a class="anchor-link" id="10-retention-and-recoverability-time-travel-is-not-a-backup"></a></h3>
<p>BigQuery&rsquo;s <a href="https://docs.cloud.google.com/bigquery/docs/time-travel" target="_blank" rel="noopener">time travel</a> window is 2&ndash;7 days (default 7), with a fixed 7-day fail-safe behind it. That is an operational undo, not a SOX retention control &mdash; fourteen days of recoverability does not support a seven-year evidence obligation. The control set: time travel pinned to 7 days on in-scope datasets (state it explicitly, don&rsquo;t inherit defaults), scheduled table snapshots at period close so every reported number has a frozen source, and no default table expiration on financial datasets (an expiry policy silently deleting revenue history is a control failure you find at the worst possible moment).</p>
<pre><code class="language-sql">-- Snapshot the revenue fact table at quarter close (zero-copy until divergence)
CREATE SNAPSHOT TABLE `finance_prod.period_close.fct_revenue_2026_q2`
CLONE `finance_prod.revenue_reporting.fct_revenue`
FOR SYSTEM_TIME AS OF TIMESTAMP '2026-07-01 00:00:00+00';
</code></pre>
<p>Standing caveat: rehearse the restore. A quarterly drill that recovers a snapshot into a scratch dataset and reconciles row counts against the close report is ten minutes of work and the difference between a backup strategy and a backup hope. Test every procedure here in a non-production project before applying it to production, and keep your DR posture current.</p>
<h2>Layer 3 &mdash; Process and Evidence<a class="anchor-link" id="layer-3-process-and-evidence"></a></h2>
<h3>11. Quarterly access review with attestation<a class="anchor-link" id="11-quarterly-access-review-with-attestation"></a></h3>
<p>Auditors sample quarters; the control must therefore fire quarterly without heroics. Export per-dataset IAM (dataset ACLs and project bindings), diff against the previous quarter, route additions to the data owner for attestation, and retain the signed attestation alongside the IAM export in the audit project. The whole loop is scriptable with <code>bq show --format=prettyjson</code> plus a scheduled query over the audit sink for <code>SetIamPolicy</code> events &mdash; the grant history is already in your logs:</p>
<pre><code class="language-sql">-- All IAM changes on in-scope datasets this quarter
SELECT
  timestamp,
  protopayload_auditlog.authenticationInfo.principalEmail AS changed_by,
  resource.labels.dataset_id,
  protopayload_auditlog.methodName
FROM `audit-prod.bq_audit_logs.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
  AND timestamp &gt;= TIMESTAMP '2026-04-01 00:00:00+00'
ORDER BY timestamp;
</code></pre>
<h3>12. Evidence pack: make the audit boring<a class="anchor-link" id="12-evidence-pack-make-the-audit-boring"></a></h3>
<p>The final control is meta: everything above produces artifacts on a schedule, into one place. Our standard evidence pack per quarter &mdash; IAM exports and attestations, the zero-row output of the unauthorized-write query, DDL-to-PR reconciliation, KMS rotation history, restore-drill log, and Google&rsquo;s SOC 2 Type II report pulled from Compliance Reports Manager for the infrastructure layer. When the evidence generates itself, the audit costs days instead of weeks.</p>
<h2>The BigQuery SOX compliance checklist at a glance<a class="anchor-link" id="the-bigquery-sox-compliance-checklist-at-a-glance"></a></h2>
<p>Use this table as the working artifact: walk it quarterly, and require that every row can produce its evidence on demand. In our experience the first pass of a BigQuery SOX compliance checklist fails on rows 2, 7, and 9 &mdash; primitive roles, 30-day log retention, and console-edited scheduled queries &mdash; so start there if audit season is close.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Control</th>
<th>ITGC domain</th>
<th>Primary evidence</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Dataset inventory &amp; SOX labels</td>
<td>Access / scoping</td>
<td><code>SCHEMATA_OPTIONS</code> label query</td>
</tr>
<tr>
<td>2</td>
<td>Least-privilege IAM, no primitive roles</td>
<td>Access</td>
<td>Empty primitive-role grant listing</td>
</tr>
<tr>
<td>3</td>
<td>Column-level security (policy tags)</td>
<td>Access</td>
<td><code>COLUMN_FIELD_PATHS</code> tag query</td>
</tr>
<tr>
<td>4</td>
<td>Row-level access policies</td>
<td>Access</td>
<td>Policy DDL + entity access test</td>
</tr>
<tr>
<td>5</td>
<td>CMEK with &le;90-day rotation</td>
<td>Access / ops</td>
<td>KMS rotation history</td>
</tr>
<tr>
<td>6</td>
<td>VPC-SC perimeter + org policies</td>
<td>Access</td>
<td>Perimeter config, denied-egress logs</td>
</tr>
<tr>
<td>7</td>
<td>Audit log sink, 7-year locked retention</td>
<td>Monitoring</td>
<td>Sink config + bucket retention</td>
</tr>
<tr>
<td>8</td>
<td>Access &amp; write monitoring queries</td>
<td>Monitoring</td>
<td>Zero-row unauthorized-write report</td>
</tr>
<tr>
<td>9</td>
<td>Terraform + SQL change control</td>
<td>Change mgmt</td>
<td>DDL-to-PR reconciliation</td>
</tr>
<tr>
<td>10</td>
<td>Time travel + close snapshots + restore drills</td>
<td>Operations</td>
<td>Snapshot DDL, drill log</td>
</tr>
<tr>
<td>11</td>
<td>Quarterly access review</td>
<td>Access</td>
<td>Signed attestations</td>
</tr>
<tr>
<td>12</td>
<td>Automated evidence pack</td>
<td>All</td>
<td>Quarterly evidence archive</td>
</tr>
</tbody>
</table>
<h2>Version boundaries and honest edges<a class="anchor-link" id="version-boundaries-and-honest-edges"></a></h2>
<p>Everything above reflects Google Cloud behavior as of August 2026: BigQuery Data Access logs on by default, <code>_Default</code> bucket at 30 days (configurable 1&ndash;3650), <code>_Required</code> fixed at 400 days, time travel 2&ndash;7 days with a 7-day fail-safe, <code>INFORMATION_SCHEMA.JOBS</code> at 180 days. Retention defaults and log formats have changed before; re-verify against the linked documentation before you certify a control on them.</p>
<p>This BigQuery SOX compliance checklist covers the BigQuery-native control surface &mdash; it does not cover upstream pipeline controls (your Kafka/Datastream/Fivetran layer needs its own change management), application-level controls in your ERP, or the entity-level controls your auditors test outside IT entirely. And SOX applicability is a determination for your auditors and counsel, not your database team: we are engineers, and this is engineering guidance, not legal advice.</p>
<h2>Where this fits in a broader governance program<a class="anchor-link" id="where-this-fits-in-a-broader-governance-program"></a></h2>
<p>SOX controls on BigQuery rarely stand alone &mdash; they usually arrive alongside a broader push to make analytics platforms auditable, the same discipline we describe in our <a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/">fractional CDO real-time analytics playbook</a> and apply to regulated retail estates in the <a href="https://minervadb.com/retail-data-analytics-modern-retail-stack/">modern retail data stack</a>. If you are staring down your first SOX cycle on BigQuery &mdash; or your auditors just handed you a findings list &mdash; MinervaDB&rsquo;s <a href="https://minervadb.com/fractional-chief-data-officer/">fractional Chief Data Officer and database governance practice</a> implements exactly this control set, evidence pipeline included. As always: test every control in a staging project before touching production, and keep a rehearsed DR posture behind every retention promise.</p>

<p><a href="https://minervadb.com/bigquery-sox-compliance-checklist/">BigQuery SOX Compliance Checklist: 12 Proven Controls</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL 26.7 Moves the Thread Pool Plugin to Community Edition</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mysql-thread-pool-community-26-7/" />
      <id>https://minervadb.com/mysql-thread-pool-community-26-7/</id>
      <updated>2026-08-22T08:43:16+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MySQL 26.7 moved the Thread Pool plugin to Community edition. As of the 26.7.0 Innovation release (GA 2026-07-31), the MySQL Thread Pool — the connection-handling model Oracle reserved for MySQL Enterprise Edition for roughly fifteen [...]</p>
<p><a href="https://minervadb.com/mysql-thread-pool-community-26-7/">MySQL 26.7 Moves the Thread Pool Plugin to Community Edition</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MySQL 26.7 moved the Thread Pool plugin to Community edition. As of the 26.7.0 Innovation release (GA 2026-07-31), the MySQL Thread Pool &mdash; the connection-handling model Oracle reserved for MySQL Enterprise Edition for roughly fifteen years &mdash; now ships in MySQL Community Server &mdash; no subscription, no commercial binary. </p>
<p>For any team running high-connection-count MySQL on Community builds, this is the most consequential edition change since the MySQL 9.7 LTS Community transfers three months earlier.</p>
<p>This MySQL Thread Pool release analysis covers what actually changed, how to turn the plugin on and verify it is working, how to tune and monitor it with the Performance Schema, who should adopt it now versus wait, and where it leaves the Community-versus-Enterprise decision. Every claim is version-pinned; configuration values shown are starting points to be measured against your workload, not universal settings. Standard caveat: test in staging and keep a rollback path before changing <code>thread_handling</code> on a production server.</p>
<h2>What MySQL 26.7 Changed, and Why It Matters<a class="anchor-link" id="what-mysql-26-7-changed-and-why-it-matters"></a></h2>
<p>MySQL 26.7.0 is the first generally available Innovation release after MySQL 9.7 LTS, and the first MySQL release to use the new <code>YY.M</code> calendar-versioning model (so &ldquo;26.7&rdquo; is the July 2026 release, not a 26th major version). Its headline edition change is short to state and large in consequence: the <strong>Thread Pool plugin is now included in MySQL Community Edition</strong>. It is the same plugin, with the same system variables and the same Performance Schema instrumentation that Enterprise customers have used for years. Oracle confirmed the change in the <a href="https://blogs.oracle.com/mysql/mysql-july-2026-ga-releases-now-available" target="_blank" rel="noopener">MySQL July 2026 GA announcement</a>.</p>
<p>This continues a trajectory that began at 9.7. That LTS moved eight capability groups across the Enterprise-to-Community line &mdash; the Hypergraph Optimizer, the OpenTelemetry telemetry component, three Group Replication components (flow control, resource manager, primary election), replication applier metrics, and JSON Duality View DML. MySQL 26.7 adds Thread Pool to that list.</p>
<p><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/mysql-enterprise-to-community-thread-pool-timeline.png" alt="Timeline of MySQL Enterprise to Community feature migration: 9.7 LTS moved eight capability groups and MySQL 26.7 moved the Thread Pool plugin to Community edition" width="960" class="wp-image-92914"></p>
<p>The practical significance is that the MySQL Thread Pool was, for fifteen years, the single most-cited Enterprise Edition performance feature. Its move to Community does not invent a capability the open-source world lacked &mdash; Percona Server has shipped its own thread pool since 5.5, and MariaDB has long had one &mdash; but it makes Oracle&rsquo;s canonical implementation free on stock MySQL. For the renewal conversation, the effect is decisive: the Enterprise value proposition has shifted away from performance and high availability toward security, backup, AI, and support. Thread Pool is no longer a reason to hold an Enterprise subscription.</p>
<h2>The Problem the MySQL Thread Pool Solves: Connection Scaling<a class="anchor-link" id="the-problem-the-mysql-thread-pool-solves-connection-scaling"></a></h2>
<p>To understand the MySQL Thread Pool, start with the default. MySQL&rsquo;s connection-handling model is thread-per-connection: every client connection is served by one dedicated operating-system thread for its entire lifetime. This is simple and low-latency at moderate concurrency, and it is the right default for most workloads. It degrades predictably when the number of concurrent connections climbs well past the number of CPU cores.</p>
<p><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/thread-per-connection-vs-mysql-thread-pool-26-7.png" alt="Diagram comparing MySQL thread-per-connection model with the thread pool model introduced to Community edition in MySQL 26.7, showing connections multiplexed onto bounded thread groups" width="960" class="wp-image-92913"></p>
<p>Three mechanisms drive the degradation, and all three are measurable rather than theoretical. Context-switching overhead rises as thousands of runnable threads compete for a handful of cores, so the OS scheduler spends an increasing share of cycles switching rather than executing.</p>
<p>CPU-cache efficiency falls because each thread carries its own stack, and thousands of stacks evict each other&rsquo;s working set from L2/L3. And transaction parallelism drives contention inside InnoDB &mdash; more concurrent transactions means more pressure on shared structures, and past a point additional concurrency reduces throughput instead of increasing it.</p>
<p>The thread pool replaces &ldquo;one thread per connection&rdquo; with a bounded set of thread groups. Connections are distributed across <code>thread_pool_size</code> groups (default derived from CPU count), and each group runs a small number of active threads that multiplex many connections. Because the number of actively executing threads is capped near the core count, thread-stack reuse keeps the CPU-cache footprint small and transaction parallelism stays bounded &mdash; which is precisely what protects InnoDB from the contention cliff.</p>
<p>The trade is latency fairness: a bounded pool can queue a statement briefly under load, so a latency-critical, moderate-concurrency workload can be worse off on the pool than on the default model.</p>
<h2>Enabling the MySQL Thread Pool in 26.7 Community<a class="anchor-link" id="enabling-the-mysql-thread-pool-in-26-7-community"></a></h2>
<p>Because the MySQL Thread Pool is now bundled with Community Edition, enabling it is the same declarative install used on Enterprise, with no commercial package to source first. Confirm the plugin library is present, install the plugin set, and switch the connection-handling model. The plugin is loaded at startup, so the durable configuration lives in <code>my.cnf</code>. The mechanics match the <a href="https://dev.mysql.com/doc/refman/8.0/en/thread-pool.html" target="_blank" rel="noopener">MySQL Thread Pool reference documentation</a>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- 1. Confirm you are on a release that ships Thread Pool in Community
SELECT VERSION();   -- expect 26.7.0 or later on a Community build

-- 2. Load the plugin set (INSTALL PLUGIN persists in the mysql.plugin table,
--    but thread_handling must still be set in my.cnf; see step 4)
INSTALL PLUGIN thread_pool SONAME 'thread_pool.so';

-- 3. Verify the plugin and its companion Performance Schema tables are ACTIVE
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE
  FROM information_schema.PLUGINS
 WHERE PLUGIN_NAME LIKE 'thread_pool%'
    OR PLUGIN_NAME LIKE 'tp_%';
</pre>
<p>The connection-handling model itself is not a runtime-settable variable &mdash; it is read at server start. Set it in the configuration file and restart during a maintenance window. State the blast radius before you do: switching <code>thread_handling</code> changes how <em>every</em> connection is scheduled, so validate on a replica first and have the one-line rollback ready.</p>
<pre><code class="language-ini"># my.cnf  &mdash; Thread Pool configuration (MySQL 26.7 Community)
# Change: thread_handling  one-connection-per-thread -&gt; pool-of-threads
# Requires a server restart. Rollback: comment these out and restart.
[mysqld]
thread_handling                  = pool-of-threads
thread_pool_size                 = 16      # start at number of physical cores
thread_pool_max_transactions_limit = 512   # ~ cores x 32; caps concurrent txns
thread_pool_stall_limit          = 6       # units of 10ms; 6 = 60ms stall check
thread_pool_algorithm            = 1       # 1 favors high-concurrency OLTP
thread_pool_query_threads_per_group = 2    # worker threads per group to start
</code></pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- 4. After restart, confirm the model actually changed
SHOW GLOBAL VARIABLES LIKE 'thread_handling';
-- Expected: thread_handling = pool-of-threads

SHOW GLOBAL VARIABLES LIKE 'thread_pool%';
</pre>
<h2>Tuning the MySQL Thread Pool: The Variables That Matter<a class="anchor-link" id="tuning-the-mysql-thread-pool-the-variables-that-matter"></a></h2>
<p>Four MySQL Thread Pool variables carry most of the tuning weight. Treat the values below as starting points anchored to hardware, then adjust against measured throughput and latency &mdash; never against a rule of thumb alone. The starting points below follow the <a href="https://dev.mysql.com/doc/refman/8.0/en/thread-pool-tuning.html" target="_blank" rel="noopener">MySQL Thread Pool tuning guidance</a>.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>What it controls</th>
<th>Starting point</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>thread_pool_size</code></td>
<td>Number of thread groups; the primary concurrency dial</td>
<td>Number of physical cores for InnoDB workloads (max 512); keep low (4&ndash;8) for MyISAM-heavy ones</td>
</tr>
<tr>
<td><code>thread_pool_max_transactions_limit</code></td>
<td>Ceiling on concurrently executing transactions across all groups</td>
<td>Physical cores &times; 32, then tune down if InnoDB contention persists</td>
</tr>
<tr>
<td><code>thread_pool_stall_limit</code></td>
<td>How long a group waits before treating a running statement as stalled and starting another worker (units of 10&nbsp;ms)</td>
<td>Default 6 (60&nbsp;ms); lower it if short queries are stuck behind long ones</td>
</tr>
<tr>
<td><code>thread_pool_algorithm</code></td>
<td>Scheduling algorithm</td>
<td>1 for high-concurrency OLTP; 0 is the conservative default</td>
</tr>
</tbody>
</table>
<p>The <code>thread_pool_stall_limit</code> value is the one most worth understanding. When every thread in a group is busy, the group will not start a new statement until the stall timer expires &mdash; this is deliberate, and it is what caps parallelism. Set it too high and a burst of long-running statements can starve short OLTP queries; set it too low and you erode the contention protection you turned the pool on for. This is a workload-shape decision, not a default.</p>
<p>One interaction is worth calling out explicitly, because it trips up first-time tuners. <code>thread_pool_size</code> and <code>thread_pool_max_transactions_limit</code> are not independent knobs. The first sets how many thread groups exist; the second caps how many transactions can execute across all of those groups at once. If you raise <code>thread_pool_size</code> to match a high core count but leave the transaction limit low, you have created groups that are structurally allowed to run work but are throttled from doing so, and throughput plateaus below what the hardware can deliver. Conversely, a generous transaction limit with too few groups concentrates connections and reintroduces the queuing you were trying to avoid.</p>
<p>Tune them as a pair: set <code>thread_pool_size</code> to the physical core count first, then raise or lower <code>thread_pool_max_transactions_limit</code> while watching the stall ratio and InnoDB row-lock waits, and stop at the point where additional concurrency stops improving committed transactions per second. That inflection point is specific to your schema and access pattern, which is exactly why the values in the table above are starting points and not answers.</p>
<h2>Monitoring the MySQL Thread Pool with the Performance Schema<a class="anchor-link" id="monitoring-the-mysql-thread-pool-with-the-performance-schema"></a></h2>
<p>The MySQL Thread Pool ships three Performance Schema tables, and they are the measurement source for every tuning decision. Recommendations that are not anchored to these tables are guesses.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Live per-group state: how many connections and threads each group holds,
-- and how deep the high/low priority queues are right now
SELECT TP_GROUP_ID,
       CONNECTION_COUNT,
       THREAD_COUNT,
       ACTIVE_THREAD_COUNT,
       QUEUED_QUERIES        -- backlog is the early-warning signal
  FROM performance_schema.tp_thread_group_state
 ORDER BY QUEUED_QUERIES DESC;
</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Stall ratio: the single most important thread pool health metric.
-- A rising ratio means statements are being treated as stalled and
-- spawning extra workers &mdash; i.e. the pool is fighting your workload.
SELECT SUM(STALLED_QUERIES_EXECUTED) / NULLIF(SUM(QUERIES_EXECUTED), 0)
         AS stalled_ratio,
       SUM(QUERIES_EXECUTED)  AS total_queries,
       SUM(STALLED_QUERIES_EXECUTED) AS stalled_queries
  FROM performance_schema.tp_thread_group_stats;
</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="json">/* Illustrative output shape &mdash; values will differ on your server
+---------------+---------------+-----------------+
| stalled_ratio | total_queries | stalled_queries |
+---------------+---------------+-----------------+
|        0.0123 |      48211900 |          593006 |
+---------------+---------------+-----------------+
*/</pre>
<p>These two tables are the backbone of MySQL Thread Pool monitoring. Read these together. A persistently high <code>QUEUED_QUERIES</code> in <code>tp_thread_group_state</code> combined with a climbing stalled ratio in <code>tp_thread_group_stats</code> tells you the pool is too small for the offered load &mdash; raise <code>thread_pool_size</code> or investigate the long statements holding groups busy. A low stalled ratio with acceptable latency means the pool is doing its job. Always baseline both before and after any variable change, and correlate with connection counts the way you would when sizing a client-side pool in our <a href="https://minervadb.com/mysql-connection-pooling-best-practices-at-scale/">MySQL connection pooling guide</a>.</p>
<h2>Who Should Adopt the MySQL Thread Pool in 26.7 &mdash; and Who Should Wait<a class="anchor-link" id="who-should-adopt-the-mysql-thread-pool-in-26-7-and-who-should-wait"></a></h2>
<p>The MySQL Thread Pool is a targeted fix, not a universal upgrade. The clearest MySQL Thread Pool candidates are OLTP servers with thousands of mostly-idle or bursty connections &mdash; the classic PHP/short-connection or microservice fan-out pattern &mdash; where connection count runs far ahead of core count and throughput visibly collapses past a concurrency threshold.</p>
<p>If you can reproduce a throughput curve that peaks and then falls as concurrency rises, the pool is likely to flatten that fall.</p>
<p>Workloads that should <em>not</em> rush to it: latency-sensitive, moderate-concurrency systems where connection count stays near or below core count. For those, thread-per-connection is already optimal, and the pool&rsquo;s queuing can only add latency. Analytics/OLAP servers running a small number of large parallel queries gain nothing from connection multiplexing. And if a well-behaved client-side connection pool (ProxySQL, a framework pool) already holds server connections to a sane number, the server-side thread pool addresses a problem you may not have.</p>
<p><strong>Our stance for 26.7 specifically.</strong> The feature is genuinely valuable and now free, but 26.7 is an <em>Innovation</em> release, not LTS &mdash; it is supported only until the next Innovation release supersedes it, whereas 9.7 LTS carries Premier support to 2034.</p>
<p>For production, the disciplined path is to prove Thread Pool on 26.7 in staging, confirm the throughput win against your own workload, and deploy it in production on the LTS line you actually run &mdash; 9.7 &mdash; if and when the plugin is available there, rather than putting a short-lived Innovation build under a production estate for a single feature. Treat 26.7 as the release that <em>proves</em> the capability is free, and your LTS as where it lands.</p>
<h2>What This Means for the Community-vs-Enterprise Decision<a class="anchor-link" id="what-this-means-for-the-community-vs-enterprise-decision"></a></h2>
<p>With the MySQL Thread Pool gone to Community, the honest Enterprise Edition value proposition after 26.7 is security, backup, AI, and support &mdash; not performance and not high availability, both of which are now fully open source. With the MySQL Thread Pool no longer a differentiator, before renewing an Enterprise subscription run the usage audit: enumerate which Enterprise features are actually in use.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- The Enterprise renewal audit: what are you actually paying for?
SHOW PLUGINS;

SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_LIBRARY
  FROM information_schema.PLUGINS
 WHERE PLUGIN_LIBRARY IS NOT NULL;

SHOW GLOBAL VARIABLES LIKE 'audit%';     -- Enterprise Audit in use?
SHOW GLOBAL VARIABLES LIKE 'keyring%';   -- which keyring / KMS integration?
</pre>
<p>Most Enterprise estates use MySQL Enterprise Backup and Enterprise Audit and little else, and both have credible open-source equivalents (Percona XtraBackup and Percona Audit Log respectively).</p>
<p>Even with the MySQL Thread Pool now free, what legitimately keeps a subscription in place after 26.7 is a narrower list: Enterprise Backup where XtraBackup for your version line is not yet GA, hard Enterprise Firewall or Data Masking compliance requirements with no in-server open-source equivalent, Kerberos/LDAP enterprise authentication, DISA STIG or CIS contractual certification, NDB Cluster, or on-premises vector search and in-database LLMs via MySQL AI.</p>
<p>If none of those apply, Thread Pool&rsquo;s move removes one of the last performance-shaped reasons to renew. For a deeper look at eliminating server-side contention once the pool is in place, our <a href="https://minervadb.com/innodb-performance-optimization-mysql-tuning/">InnoDB performance optimization guide</a> covers the InnoDB-layer tuning that pairs with it.</p>
<h2>MySQL Thread Pool FAQ<a class="anchor-link" id="mysql-thread-pool-faq"></a></h2>
<h3>Is MySQL 26.7 Thread Pool the same as the Percona or MariaDB thread pool?<a class="anchor-link" id="is-mysql-26-7-thread-pool-the-same-as-the-percona-or-mariadb-thread-pool"></a></h3>
<p>Not quite. The MySQL Thread Pool and its cousins solve the same problem with the same core idea &mdash; bounded thread groups multiplexing many connections &mdash; but they are independent implementations with different variables and internals. What 26.7 changes is that Oracle&rsquo;s canonical Thread Pool plugin, previously Enterprise-only, is now the free default option on stock MySQL Community, so you no longer need Percona Server or MariaDB to get an Oracle-lineage thread pool.</p>
<h3>Do I need to restart MySQL to enable the MySQL Thread Pool?<a class="anchor-link" id="do-i-need-to-restart-mysql-to-enable-the-mysql-thread-pool"></a></h3>
<p>Yes. <code>thread_handling</code> is read at server startup, so switching from <code>one-connection-per-thread</code> to <code>pool-of-threads</code> requires a restart. Plan it in a maintenance window, validate on a replica first, and keep the rollback (comment out the settings, restart) ready.</p>
<h3>Should I run Thread Pool in production on MySQL 26.7?<a class="anchor-link" id="should-i-run-thread-pool-in-production-on-mysql-26-7"></a></h3>
<p>The MySQL Thread Pool is worth staging, not rushing. Prove it on 26.7 in staging, but for production prefer the LTS line (9.7) that carries long support, since 26.7 is an Innovation release supported only until the next Innovation release. Adopt the pool where connection count greatly exceeds core count and throughput degrades under concurrency; skip it for latency-sensitive, low-concurrency, or OLAP workloads.</p>
<h3>Does moving Thread Pool to Community end the case for MySQL Enterprise Edition?<a class="anchor-link" id="does-moving-thread-pool-to-community-end-the-case-for-mysql-enterprise-edition"></a></h3>
<p>No, but it narrows it. After 26.7, Enterprise&rsquo;s real value is security (Firewall, Data Masking, enterprise authentication), backup (MEB where XtraBackup is not GA for your line), MySQL AI, and Oracle support/certification &mdash; not performance or HA. Run the renewal audit above before deciding.</p>
<h2>MySQL Thread Pool: Final Thoughts<a class="anchor-link" id="mysql-thread-pool-final-thoughts"></a></h2>
<p>MySQL 26.7 moving the Thread Pool plugin to Community edition is a small change to state and a meaningful one to act on for anyone tuning the MySQL Thread Pool. It gives every Community user Oracle&rsquo;s own connection-scaling model for free, it closes a fifteen-year Enterprise differentiator, and it reshapes the renewal math for anyone paying for Enterprise primarily for performance. The engineering discipline around it is unchanged: enable the MySQL Thread Pool only where the connection-to-core ratio justifies it, tune <code>thread_pool_size</code> and <code>thread_pool_stall_limit</code> against measured throughput, monitor the stall ratio in <code>tp_thread_group_stats</code>, and land it on your LTS line rather than a short-lived Innovation build.</p>
<p>Done that way, the MySQL Thread Pool becomes a measured throughput win rather than a configuration gamble, and the plugin&rsquo;s new Community status simply removes the licensing friction that used to sit in front of that decision.</p>
<p>If you are weighing a Thread Pool rollout, planning a 9.7 LTS upgrade, or reassessing an Enterprise renewal in light of the 9.7 and 26.7 Community transfers, MinervaDB&rsquo;s MySQL consulting and <a href="https://minervadb.com/emergency-database-support/">24&times;7 enterprise-class database support</a> teams do this work daily across 900+ enterprise customers, with staged, reversible changes and a rollback path stated before anything touches production. As always: test before applying to production, and maintain a robust, tested DR posture.</p>

<p><a href="https://minervadb.com/mysql-thread-pool-community-26-7/">MySQL 26.7 Moves the Thread Pool Plugin to Community Edition</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Hackorum Update: What&#8217;s New Since February</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/08/20/hackorum-update-whats-new-since-february/" />
      <id>https://percona.community/blog/2026/08/20/hackorum-update-whats-new-since-february/</id>
      <updated>2026-08-20T10:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Back in February, I wrote about Hackorum, a forum style web view of the pg-hackers mailing list. If you missed that post, you can read it here first. It turns the mailing list into something that reads and navigates a bit more like a modern forum, while the mailing list itself stays the source of truth.</p>
<p><a href="https://percona.community/blog/2026/08/20/hackorum-update-whats-new-since-february/">Hackorum Update: What&#8217;s New Since February</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Back in February, I wrote about <a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">Hackorum</a>, a forum style web view of the pg-hackers mailing list. If you missed that post, you can <a href="https://percona.community/blog/2026/02/02/hackorum-a-forum-style-view-of-pg-hackers/" target="_blank" rel="noopener noreferrer">read it here</a> first. It turns the mailing list into something that reads and navigates a bit more like a modern forum, while the mailing list itself stays the source of truth.</p>
<p><figure><img decoding="async" width="1800" height="873" src="https://percona.community/blog/2026/08/hackorum-update-topic-index_hu_8413875a6b00bf8f.webp" alt="Hackorum topic index showing pg-hackers threads with commitfest, patch and CI status icons" loading="lazy"></figure>
</p>
<p>A lot has happened since then. We also talked about the project at the PostgreSQL meetup in Berlin in March, <a href="https://www.postgresql.eu/events/pgconfde2026/schedule/session/7760-modernising-postgres-community-communication-with-hackorum/" target="_blank" rel="noopener noreferrer">at pgconf.de</a>, and in a lightning talk at pgconf.dev, and a good chunk of what was still on the roadmap back then is live today. We also posted <a href="https://www.youtube.com/watch?v=onQQJzQ8Qlw" target="_blank" rel="noopener noreferrer">a video introduction to Hackorum</a>, if you prefer to see it in action rather than read about it. This post is a follow up to walk through what changed, what is new, and what we are looking at next.</p>
<h2>A quick recap<a class="anchor-link" id="a-quick-recap"></a></h2>
<p>Hackorum is an Open Source project from the community to be used by the community and hosted on <a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">hackorum.dev</a>. The code is on <a href="https://github.com/hackorum-dev/hackorum" target="_blank" rel="noopener noreferrer">GitHub</a>. If you are new here, the short version that it syncs postgres mailing lists in the background, keeps read status, stars, tags and notes for you, and adds commitfest context, commit and patch history, and contributor info directly next to the discussion.</p>
<p>Everything below is new for users since the last post. I am leaving out internal and admin only changes, since those do not affect how you use the site day to day.</p>
<h2>Commits linked back to their discussion<a class="anchor-link" id="commits-linked-back-to-their-discussion"></a></h2>
<p><figure><img decoding="async" width="1600" height="604" src="https://percona.community/blog/2026/08/hackorum-update-commit-profile_hu_2001c1ff82193dc2.webp" alt="Contributor profile showing commit credits and how many patch threads landed" loading="lazy"></figure>
</p>
<p>Hackorum now links pushed commits back to the thread and patch discussion that produced them. Contributor profile pages have a new commit history tab, so you can see someone&rsquo;s message activity and their commits in one place. There is also a <a href="https://www.postgresql.org/message-id/CAAKRu_Z42AAq7N%3DusSS3UPMtXqbVvsQzktNmH1X20oypyqA_Xg%40mail.gmail.com" target="_blank" rel="noopener noreferrer">community proposal on pgsql-www</a> for public contributor profile pages, with the PostgreSQL Contributors Committee looking to recognize specific contributions beyond code, for example volunteering at a conference. If that lands, it would be a natural fit to surface on this profile too.</p>
<h2>Patch CI results, now public<a class="anchor-link" id="patch-ci-results-now-public"></a></h2>
<p><img decoding="async" src="https://percona.community/blog/2026/08/hackorum-update-ci-status.png" alt="CI status card on a thread, showing apply, build and test results"></p>
<p>Patches attached to a thread are automatically applied, rebased when master moves, and built and tested with the PostgreSQL test suite. Those results used to be an internal experiment, they are now public for everyone:</p>
<ul>
<li>CI status icons in the topic index, and full detail in tabs on the thread itself</li>
<li>A CI dashboard at <a href="https://hackorum.dev/ci" target="_blank" rel="noopener noreferrer">hackorum.dev/ci</a></li>
<li>A per topic CI history view</li>
<li>A stats overview page</li>
</ul>
<p><figure><img decoding="async" width="1600" height="1156" src="https://percona.community/blog/2026/08/hackorum-update-ci-history_hu_90e1159e5d48f16a.webp" alt="Patch CI history for a thread, showing every tracked patchset version and its result" loading="lazy"></figure>
</p>
<p>We reapply the latest version of each patch once a day against current master. If it stops applying, we do not give up right away, we keep retrying for 30 more days, so one bad day does not retire a patch. Only after 30 days of failing do we mark it retired, meaning the base is too old, and stop trying until a new version is posted.</p>
<p>Once something actually gets committed, there is nothing left to test against. There are two ways this shows up. If the committer changed the patch before committing it, the old version no longer applies and Hackorum shows it as no longer matching. If the committer applied it exactly as submitted, the diff against the committed version on GitHub is empty, so we know it went in as-is. Either way, we stop running CI on that thread until a new patch shows up. Some threads get committed in stages, with fixups or follow up patches, so if a new patchset lands afterward, we pick CI back up and start testing it.</p>
<p>One more detail worth calling out is that Hackorum highlights patches that are already committed but still have an open commitfest entry, so those are easy to spot and clean up. That is currently a big issue and might really help to get this easier updated in the future. A future integration with the commitfest might be thinkable.</p>
<h2>Support for more mailing lists<a class="anchor-link" id="support-for-more-mailing-lists"></a></h2>
<p><img decoding="async" src="https://percona.community/blog/2026/08/hackorum-update-mailinglist-badge.png" alt="Mailing list badges next to each thread, showing hackers, bugs and docs"></p>
<p>Hackorum used to be pg-hackers only. It now also ingests pgsql-bugs, pgsql-docs, pgsql-general and pgsql-patches, with a badge next to each thread so you can see at a glance which list a message came from. We also tried pgsql-committers, it is imported up to around May/June, but we paused it for now. It is mostly terse commit notifications, and without a way to hide a list by default yet, it added more noise than value. We are planning to add more lists, so let us know if you are missing one.</p>
<h2>Saved searches<a class="anchor-link" id="saved-searches"></a></h2>
<p><img decoding="async" src="https://percona.community/blog/2026/08/hackorum-update-saved-searches.png" alt="Saved searches in the sidebar, grouped into global and personal searches"></p>
<p>The advanced search from the last post is still there, but you no longer have to retype the same query every time. You can save a search, personally or shared with your team, and pin it to your sidebar. We also ship a few global saved searches out of the box.</p>
<h2>Ignore threads you do not care about<a class="anchor-link" id="ignore-threads-you-do-not-care-about"></a></h2>
<p>A small one, but a popular request was that you can now ignore a thread from the topic list with one click. Ignored threads disappear from your views and search results by default, and you can always list what you have ignored, or bring one back.</p>
<h2>Sending email from Hackorum<a class="anchor-link" id="sending-email-from-hackorum"></a></h2>
<p><figure><img decoding="async" width="1100" height="387" src="https://percona.community/blog/2026/08/hackorum-update-reply-composer_hu_c1e96db47ee957f6.webp" alt="Reply composer for sending a message to the mailing list from Hackorum" loading="lazy"></figure>
</p>
<p>This was the top item on the &ldquo;planned&rdquo; list in February, and it is here now. You can reply to a thread directly from Hackorum, including reply all, with a Thunderbird style selective quote so you only quote the part you are actually replying to. There is a drafts sidebar so you can keep several replies in progress, and a &ldquo;My emails&rdquo; page that shows everything you have sent through the site.</p>
<p>This is currently limited to @gmail accounts and being under active testing. If you like early access, just reach out so we can activate it for your user.</p>
<h2>Everyday improvements<a class="anchor-link" id="everyday-improvements"></a></h2>
<p>None of these are headline features on their own, but you will notice them:</p>
<ul>
<li>Long threads load in batches, so opening a huge discussion is noticeably faster</li>
<li>A &ldquo;jump to latest&rdquo; button, and a setting to jump straight to your first unread message when you open a thread</li>
<li>Read messages can collapse automatically if you prefer a shorter view</li>
<li>Patch diffs now show inline line stats and highlighting</li>
<li>Sessions now stay signed in for 30 days across devices</li>
<li>The light/dark theme toggle is available everywhere, even before you sign in</li>
</ul>
<h2>Mobile got a real pass<a class="anchor-link" id="mobile-got-a-real-pass"></a></h2>
<p><img decoding="async" src="https://percona.community/blog/2026/08/hackorum-update-mobile-view.png" alt="Hackorum topic list on mobile"></p>
<p>The mobile experience got a lot of attention: a proper burger menu, a quick shortcut to threads you starred, working swipe back and forward gestures, and layout fixes for tablets and foldables.</p>
<h2>What we are working on next<a class="anchor-link" id="what-we-are-working-on-next"></a></h2>
<p>We are looking into AI generated summaries for longer threads, so you can get up to speed on a big discussion without reading every message.</p>
<h2>Try it and share feedback<a class="anchor-link" id="try-it-and-share-feedback"></a></h2>
<p>If you want to take a look, just go to <a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">https://hackorum.dev/</a>.</p>
<p>The repository, including a simple dev setup, is here: <a href="https://github.com/hackorum-dev/hackorum" target="_blank" rel="noopener noreferrer">https://github.com/hackorum-dev/hackorum</a></p>
<p>Want to chat with us, join the hackorum channel on the <a href="https://discordapp.com/channels/1258108670710124574/1471524461374083186" target="_blank" rel="noopener noreferrer">PostgreSQL Hacking Discord</a>.</p>
<p>Is this useful? What is missing? What would you change? Bug reports, feature requests, and contributions are all welcome: <a href="https://github.com/hackorum-dev/hackorum/issues" target="_blank" rel="noopener noreferrer">https://github.com/hackorum-dev/hackorum/issues</a></p>
<p>Thanks for taking a look, and we appreciate any feedback.</p>

<p><a href="https://percona.community/blog/2026/08/20/hackorum-update-whats-new-since-february/">Hackorum Update: What&#8217;s New Since February</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The curios case of timezone inconsistencies between PgBouncer and Patroni Cluster</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/" />
      <id>https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/</id>
      <updated>2026-08-20T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>I would like to introduce here the curious case that I had on a Patroni Cluster in production, after a migration from Oracle, with timezone differences between what was written from an application log and what was set in PostgreSQL.</p>
<p><a href="https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/">The curios case of timezone inconsistencies between PgBouncer and Patroni Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I would like to introduce here the curious case that I had on a Patroni Cluster in production, after a migration from Oracle, with timezone differences between what was written from an application log and what was set in PostgreSQL.</p>
<p>First of all a quick description of our setup. We have a two nodes Patroni cluster on Open Stack virtual machines, PostgreSQL 18, with leader and a read only replica, so we write only on one node and use the second for data extractions. All applications use Kubernetes pods that are also setup in Open Stack and access PostgreSQL via PgBouncer connection pooler. We do have also some Java application making use of their own Hikari connection pooler. All on premises.</p>
<p>The application, being a legacy one, needed to use all the time data types in the timezone of the customer, so I had to setup the timezone of the PostgreSQL cluster different from the one of the VMs, that is in UTC. That was the first error as I set it up using a simple ALTER SYSTEM SET TIMEZONE followed by a SELECT pg_reload_conf(). This changes the timezone on the postgresql.auto.conf configuration file, which is totally good if you have a normal single node or replica PostgreSQL but not with a Patroni Cluster! I will return on this point in a moment.</p>
<p>In itself the migration was fine, but we had to tailor a little bit the resources assigned to the nodes as unfortunately we were not able to test, prior to the migration, some parts of the application. So we had to increase RAM on the VMs and modify shared_buffers parameter, doing a restart of the PostgreSQL cluster one node at a time.</p>
<p>Unfortunately the restart wiped out the timezone configuration that I did, as I should have added this through a patronictl edit-config command as I did for the shared_buffers. At least we recognized quickly the problem as the timezone of the whole Patroni Cluster reverted back to UTC. This time I changed it modifying the Patroni Cluster configuration, using the following patronictl command edit-config:</p>
<p><figure><img decoding="async" width="564" height="20" src="https://percona.community/blog/2026/08/pgbouncer-patroni-fig1-edit-config_hu_50afa8d9c5860e10.webp" alt="patronictl edit-config command" loading="lazy"></figure>
</p>
<p><em>Fig. 1 &ndash; patronictl edit-config command</em></p>
<p>And adding the correct timezone:</p>
<p><figure><img decoding="async" width="507" height="47" src="https://percona.community/blog/2026/08/pgbouncer-patroni-fig2-timezone-value_hu_7a688fd3a7312933.webp" alt="timezone Africa/Lagos in Patroni config" loading="lazy"></figure>
</p>
<p><figure><img decoding="async" width="595" height="400" src="https://percona.community/blog/2026/08/pgbouncer-patroni-fig3-apply-config_hu_665a6c1581ac5f4f.webp" alt="Apply Patroni config change" loading="lazy"></figure>
</p>
<p><em>Fig. 2 &ndash; Result of edit-config command</em></p>
<p>Confirmed issuing a show-config:</p>
<p><figure><img decoding="async" width="510" height="45" src="https://percona.community/blog/2026/08/pgbouncer-patroni-fig4-show-config_hu_66d45dfda5521c10.webp" alt="show-config timezone Africa/Lagos" loading="lazy"></figure>
</p>
<p><em>Fig. 3 &ndash; Result of show-config command</em></p>
<p>After that we resumed normal operations and all seemed fine, problem solved, no big issue or disruptions to our customer.</p>
<p>The day after I got contacted by one developer telling me that PostgreSQL in production is still in UTC. I checked it immediately and it was instead in WAT, confirmed issuing:</p>
<p><figure><img decoding="async" width="860" height="955" src="https://percona.community/blog/2026/08/pgbouncer-patroni-fig5-pgadmin-timezone_hu_3702cf3278763eca.webp" alt="pgAdmin show timezone Africa/Lagos" loading="lazy"></figure>
</p>
<p><em>Fig. 4 &ndash; Screenshot of show timezone in pgAdmin</em></p>
<p>Then the developers sent me the results of a query done on an application log table showing a clear inconsistency of timestamps, some were correct and some with UTC timezone! That is a very bad scenario, where you do not have a clear indication, all wrong or all correct, but mixed results.</p>
<p>I started suspecting that there was something wrong on the application side as all that I saw on the database was correct, except some of the values that were recorded. So my next move was to ask devops to restart one by one all the application pods (remember that we are using Kubernetes).</p>
<p>This action brought mixed results as at the beginning it seemed that problem was solved and no rows with timestamp with wrong timezone was inserted&hellip;but after some time the first rows with UTC started resurfacing again! That was weird so I started digging a little bit more and searched our setup and our structure, as I was sure that the PostgreSQL database in itself was not the culprit.</p>
<p>As you may have guessed at this point there was one Elephant in the room that I had not yet investigated (and no was not PostgreSQL Elephant, AKA Slonik): PgBouncer! In fact I had totally left out of the picture the connection pooler as I thought that it was installed in kubernetes as a sidecar of the application pods, so expecting that a restart of the pods would have affected also PgBouncer.</p>
<p>As that was the architectural schema that was decided some years ago for our applications. Turned out that this was not the case!</p>
<p>Let&rsquo;s make a quick excursus on why it is extremely important to have a connection pooler in PostgreSQL, the pool mode parameter of PgBouncer and the possible ways of installing and configuring it. These points are important to understand our case.</p>
<p>PostgreSQL connections are costly, mainly in terms of RAM, so much that there is a parameter max_connections to limit the maximum number of sessions, both active and idle. The default value is 100 which obviously is quite low, but rising it means that we need to ensure that we have enough RAM available, as the overhead for each connection is roughly 10 Mb. Bringing this parameter up for example to 500 means having already 5 Gb of RAM used just for idle connections, without even issuing any query!</p>
<p>Here is where connection poolers come to the rescue, as they are capable of recycling the connections, so that applications do not need to open a new connection each time they need to query the database. The pool maintains a fixed set of open connections, all requests borrow a connection, use it, and return it to the pool. The connection itself is never closed between requests, it stays open and ready so that the next request picks it up instantly.</p>
<p>PgBouncer is the de facto standard for connection pooling in PostgreSQL as it is lightweight, easy to configure and maintain. There is a very important setup choice to be made when installing PgBouncer: the pool mode, that decides the behaviour of the pooler. There are 3 possible choices: Session &ndash; Transaction &ndash; Statement.</p>
<p>Session Pooling: Assigns a server connection to a client as soon as it connects and holds it until the client disconnects. It acts almost like a direct connection to PostgreSQL, supporting all session features, but offers the least connection reuse. This is the most safe way to configure PgBouncer, and since we use some session features like Prepared Statements, this was our choice.</p>
<p>Transaction Pooling: Assigns a server connection only for the duration of a single BEGIN &hellip; COMMIT/ROLLBACK transaction. Once the transaction completes, the connection goes back to the pool for another client to use.</p>
<p>Statement Pooling: Assigns a server connection for a single SQL statement, returning it immediately after execution. It allows maximum reuse, but breaks multi-statement transactions (BEGIN &hellip; COMMIT) and session features.</p>
<p>One last word on what are the recommended ways to install PgBouncer: first of all it should be on a separate server respect the PostgreSQL cluster, then in a Kubernetes environment, such as ours, it can be installed as a sidecar to Kubernetes pods (which is the recommended way for most cases) or as a separate deployment in its own pod. Turns out that we had installed PgBouncer in this last way instead of having it in the same pod of the application.</p>
<p>Now that we have a complete picture, let&rsquo;s go back to our case. Since our PgBouncer was installed as a standalone pod and not in sidecar, this become my primary culprit, searching a little bit I found out that there is the possibility in PgBouncer that if a database is created or its timezone is altered via ALTER DATABASE &hellip; SET timezone, PgBouncer does not properly invalidate its internal startup-parameter cache.</p>
<p>In our case the timezone was changed through Patroni, not ALTER DATABASE, but PgBouncer still kept a stale cached TimeZone.</p>
<p>So when a connection is recycled:</p>
<ul>
<li>PostgreSQL natively sets the timezone to Africa/Lagos.</li>
<li>PgBouncer looks at its cached baseline state for that database/user profile from when it first booted up (which was reset to UTC when we restarted the Patroni nodes remember!).</li>
<li>PgBouncer then subtly injects a session-level override back to the client, effectively masking the database&rsquo;s actual default settings.</li>
</ul>
<p>It was exactly our case, as the restart of Patroni Cluster nodes returned all the system to UTC, then we modified it again to WAT, but we never restarted the PGBouncer pod, so we still had some sessions in the pool with UTC timezone. That explained also the mix between correct and wrong timezones in the timestamps, it depended from the session with which the row was inserted, if it was one of the old ones we had UTC.</p>
<p>The fix at this point was obvious: clear the cache of PgBouncer. This can be done with a simple restart of that pod or a reload of the configuration. In our case we restarted PgBouncer and surely this action solved all our timezone problems as from that point we had only timestamps with correct timezone, confirming the above scenario.</p>
<p>So this was the curious case of timezone inconsistencies between PostgreSQL and PgBouncer, in the end was easily solved and not disruptive, it was due mainly to a couple of errors and assumptions that I did and that were surely avoidable, that&rsquo;s why I wanted to bring attention to this subject: prevent others doing my errors and recognize faster the issue.</p>
<p>Summarizing all in a few key points of lessons learned:</p>
<ul>
<li>After changing timezone in Patroni, always restart or RECONNECT PgBouncer so the pool doesn&rsquo;t keep stale session settings.</li>
<li>Remember to always change timezone in a Patroni cluster using patronictl command edit-config and not ALTER SYSTEM SET TIMEZONE.</li>
<li>In a Kubernetes environment install PgBouncer as a sidecar to Kubernetes pods instead of a standalone pod.</li>
</ul>
<p>Hope it will help some of you!</p>
<p><em>This post is part of the <a href="https://percona.community/blog/write-for-percona-community/">Percona Community Writers Program</a>.</em></p>

<p><a href="https://percona.community/blog/2026/08/20/timezone-inconsistencies-pgbouncer-patroni/">The curios case of timezone inconsistencies between PgBouncer and Patroni Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Security Advisory: Privileged ClickHouse access through the Grafana data source in PMM</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/security-advisory-cve-affecting-pmm/" />
      <id>https://www.percona.com/blog/security-advisory-cve-affecting-pmm/</id>
      <updated>2026-08-19T13:07:00+03:00</updated>
      <author><name>Ben Judge</name></author>
      <summary type="html"><![CDATA[<p>Date of release: 19 August 2026 Severity: High Affected product: PMM Impacted versions: 3.9.0 and below Summary Percona has recently been made aware of a security vulnerability affecting PMM. We take the security of our products and the protection of our customers’ data with the utmost seriousness. This advisory describes the vulnerability, the immediate steps … Continued<br />
The post Security Advisory: Privileged ClickHouse access through the Grafana data source in PMM appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/security-advisory-cve-affecting-pmm/">Security Advisory: Privileged ClickHouse access through the Grafana data source in PMM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><b>Date of release:</b><span> 19 August 2026<br>
</span><b>Severity:</b><span> High<br>
</span><b>Affected product:</b><span> PMM<br>
</span><b>Impacted versions:</b><span> 3.9.0 and below</span></p>
<h2><b>Summary</b><a class="anchor-link" id="summary"></a></h2>
<p><span>Percona has recently been made aware of a security vulnerability affecting PMM. We take the security of our products and the protection of our customers&rsquo; data with the utmost seriousness.<br>
</span><span>This advisory describes the vulnerability, the immediate steps you can take to protect your deployment, and the permanent fix.</span></p>
<h2><b>Vulnerability details</b><a class="anchor-link" id="vulnerability-details"></a></h2>
<ul>
<li aria-level="1"><b>CVE identifier number:</b> <span>Pending, this advisory will be updated once assigned&nbsp;</span></li>
<li aria-level="1"><b>CVSS score:</b><span> 8.7 (High)</span></li>
</ul>
<p><span>PMM&rsquo;s Grafana instance allows signed-in users, including those with the Viewer role, to call raw data source APIs. If anonymous access has been explicitly enabled (it is off by default), unauthenticated users can also reach these APIs.&nbsp;</span></p>
<p><span>Through the Grafana ClickHouse data source, such a user can submit arbitrary SQL. The data source connects to ClickHouse as the default identity, which holds global DDL, DML, and SOURCES privileges and can make outbound HTTP requests.</span></p>
<p><span>Chained together, this allows an attacker to reach AWS IMDSv1, obtain a live EC2 role session, read a Terraform remote-state object from S3, and authenticate as the PMM/Grafana administrator.</span></p>
<h2><b>Impact</b><a class="anchor-link" id="impact"></a></h2>
<p><span>An unauthenticated attacker can cross the public Grafana boundary into internal databases, AWS instance metadata, S3 remote state, and the PMM administrator account. This yields renewable cloud credentials and a remote-state file that can contain many independently reusable secrets and private infrastructure details.&nbsp;</span></p>
<p><span>The severity of impact depends on deployment configuration. The full IMDS-to-credential chain requires anonymous access to be enabled (off by default) and the PMM Server to be running on AWS EC2 with IMDSv1.&nbsp;</span></p>
<p><span>All deployments are affected by the arbitrary SQL execution via the ClickHouse data source.</span></p>
<h2><b>Remediation</b><a class="anchor-link" id="remediation"></a></h2>
<p><span>This vulnerability is fixed in PMM 3.9.1, scheduled for release on August 19, 2026. Upgrade as soon as it is available.</span></p>
<h2><b>Mitigation</b><a class="anchor-link" id="mitigation"></a></h2>
<p><span>If you cannot upgrade immediately, run the script below to close the exploitation path. It creates a least-privilege ClickHouse user for Grafana and points the ClickHouse data source at it, replacing the default superuser and making the exploitation of the vulnerability impossible.&nbsp;</span></p>
<p>&nbsp;</p>
<p><span>Before running the script, back up your PMM Server and save the </span><span><code></code></span><span>pmm-data</span><span></span><span> volume.&nbsp;</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">#!/bin/bash
# Create a least-privilege ClickHouse identity for Grafana and point the
# ClickHouse datasource at it, replacing the default superuser.


set -euo pipefail

CONTAINER=${CONTAINER:-pmm-server}
PMM_HOST=${PMM_HOST:-localhost}
PMM_PORT=${PMM_PORT:-443}
GRAFANA_URL="https://${PMM_HOST}:${PMM_PORT}"
ADMIN_PASS=${ADMIN_PASS:-$(cat /root/pmm-admin-password)}
# Drop-ins are loaded from users.d (users_config defaults to users.xml -&gt;
# users.d), NOT default-users.d.
BOOTSTRAP_XML=/etc/clickhouse-server/users.d/zz-provision-bootstrap.xml

CH_PASS=$(openssl rand -hex 24)
CH_HASH=$(printf '%s' "$CH_PASS" | sha256sum | awk '{print $1}')
BOOT_PASS=$(openssl rand -hex 24)
BOOT_HASH=$(printf '%s' "$BOOT_PASS" | sha256sum | awk '{print $1}')

ch_wait () {
  local user=$1 pass=$2 i
  for i in $(seq 1 45); do
    if docker exec -i "$CONTAINER" clickhouse-client --host 127.0.0.1 
         --user "$user" --password "$pass" -q "SELECT 1" &gt;/dev/null 2&gt;&amp;1; then
      return 0
    fi
    sleep 2
  done
  echo "ERROR: clickhouse did not accept $user within 90s" &gt;&amp;2
  return 1
}

# PMM's ClickHouse default superuser has access_management disabled, so it
# cannot run CREATE USER / GRANT even with its known password. Install a
# short-lived admin to run the DDL instead. Drop-ins must live in users.d;
# and a plaintext <password> is rejected outright at startup because PMM
# ships allow_plaintext_password=0.
docker exec -u root "$CONTAINER" mkdir -p /etc/clickhouse-server/users.d
docker exec -u root -i "$CONTAINER" bash -c "cat &gt; $BOOTSTRAP_XML" &lt;<xmleof>
    <users>
        <provision_admin>
            <password_sha256_hex>$BOOT_HASH</password_sha256_hex>
            <networks><ip>127.0.0.1</ip><ip>::1</ip></networks>
            <profile>default</profile>
            <quota>default</quota>
            <access_management>1</access_management>
        </provision_admin>
    </users>

XMLEOF
docker exec -u root "$CONTAINER" chown pmm:root "$BOOTSTRAP_XML"
docker exec -u root "$CONTAINER" supervisorctl restart clickhouse
ch_wait provision_admin "$BOOT_PASS"

# grafana_ro holds SELECT and nothing else. Without the SOURCES family it
# cannot call url(), s3(), mongodb(), remote() or file(); readonly=1
# additionally prevents it overriding server settings such as
# max_http_get_redirects. ALTER runs unconditionally so that re-running
# this script rotates the password rather than failing.
docker exec -i "$CONTAINER" clickhouse-client --host 127.0.0.1 
  --user provision_admin --password "$BOOT_PASS" --multiquery &lt;<sqleof create settings profile if not exists grafana_ro_profile readonly allow_ddl="0," max_execution_time="60;" user grafana_ro identified with sha256_hash by alter revoke all on from grant select pmm. to default. system.tables system.columns system.databases system.one system.numbers sqleof docker exec root rm supervisorctl restart clickhouse ch_wait repoint the datasource. uid is assigned pmm so look it up. ds_uid="$(curl" jq .uid head then echo no grafana-clickhouse-datasource found>&amp;2
  exit 1
fi

# Transient files hold the CH password; keep them in a private dir and
# always remove them, even if a curl below fails.
umask 077
TMPD=$(mktemp -d)
trap 'rm -rf "$TMPD"' EXIT
curl -sk -u "admin:$ADMIN_PASS" 
  "$GRAFANA_URL/graph/api/datasources/uid/$DS_UID" &gt; "$TMPD/ds-ch.json"
jq --arg p "$CH_PASS" 
  '.jsonData.username = "grafana_ro" | .secureJsonData.password = $p' 
  "$TMPD/ds-ch.json" &gt; "$TMPD/ds-ch.new.json"
curl -sk -u "admin:$ADMIN_PASS" -X PUT -H 'Content-Type: application/json' 
  -d @"$TMPD/ds-ch.new.json" 
  "$GRAFANA_URL/graph/api/datasources/uid/$DS_UID" &gt;/dev/null

# Fail the build rather than come up believing this worked.
if docker exec -i "$CONTAINER" clickhouse-client --host 127.0.0.1 
     --user grafana_ro --password "$CH_PASS" 
     -q "SELECT count() FROM url('http://169.254.169.254/latest/user-data','LineAsString','line String')" 
     &gt;/dev/null 2&gt;&amp;1; then
  echo "FATAL: grafana_ro can still reach url()" &gt;&amp;2
  exit 1
fi
if docker exec -i "$CONTAINER" clickhouse-client --host 127.0.0.1 
     --user grafana_ro --password "$CH_PASS" 
     -q "CREATE TABLE default.zz_provision_check (x String) ENGINE=Memory" 
     &gt;/dev/null 2&gt;&amp;1; then
  echo "FATAL: grafana_ro can still run DDL" &gt;&amp;2
  exit 1
fi

unset CH_PASS BOOT_PASS ADMIN_PASS
echo "ClickHouse datasource now authenticates as grafana_ro."</sqleof></xmleof></password></pre>
<p><span>Run the script as follows:&nbsp;</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">CONTAINER=pmm-server PMM_HOST=localhost PMM_PORT=443 ADMIN_PASS=XXXXX bash ./pmm-ch-user.sh</pre>

<h2><b>Support &amp; additional resources</b><a class="anchor-link" id="support-additional-resources"></a></h2>
<p><span>If you require further clarification or assistance, we are available 24/7:</span></p>
<ul>
<li aria-level="1"><a href="https://my.percona.com/"><span>Technical support portal for customers</span></a></li>
<li aria-level="1"><a href="https://forums.percona.com/c/percona-monitoring-and-management-pmm"><span>Technical support for community</span></a></li>
</ul>
<h2><b>Contact</b><a class="anchor-link" id="contact"></a></h2>
<p><span>For questions about this advisory, upgrade planning, or to discuss options for unsupported major versions, open a case via the </span><a href="https://customers.percona.com/"><span>Percona Customer Portal</span></a><span> or contact your Percona Customer Success Manager.</span><span>&nbsp;</span></p>
<p><span>For other security-related questions, write to </span><span>security@percona.com</span><span>.</span></p>
<p>The post <a href="https://www.percona.com/blog/security-advisory-cve-affecting-pmm/">Security Advisory: Privileged ClickHouse access through the Grafana data source in PMM</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/security-advisory-cve-affecting-pmm/">Security Advisory: Privileged ClickHouse access through the Grafana data source in PMM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Stop guessing at gcache: inspect Galera/PXC write sets with gcache-inspector</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/" />
      <id>https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/</id>
      <updated>2026-08-19T08:23:50+03:00</updated>
      <author><name>Przemysław Malkowski</name></author>
      <summary type="html"><![CDATA[<p>The common practice is to size the Galera Cache based on write volume measured during peak load, but often it is more of a guesswork. The writeset cache capacity planning is crucial to shorten the maintenance time and avoid long state transfers while the cluster runs with reduced compute power. Now, if you could understand … Continued<br />
The post Stop guessing at gcache: inspect Galera/PXC write sets with gcache-inspector appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/">Stop guessing at gcache: inspect Galera/PXC write sets with gcache-inspector</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>The common practice is to size the Galera Cache based on write volume measured during peak load, but often it is more of a guesswork. The writeset cache capacity planning is crucial to shorten the maintenance time and avoid long state transfers while the cluster runs with reduced compute power. Now, if you could understand what&rsquo;s exactly inside the cache, wouldn&rsquo;t the planning be more aware as compared to only calculating the best size based on</span><span> <code>wsrep_received/replicated_bytes</code></span><span> variables?</span></p>
<p><span>Similarly, while dealing with various incidents occurring in Percona XtraDB Cluster or MariaDB Galera Cluster environments, how many times did you stumble upon the fact that the GCache file (galera.cache) is a black box and you can&rsquo;t inspect it in a meaningful way?&nbsp;</span></p>
<p><span>In some scenarios, having the opportunity to see what exactly ended up in the cache file(s) could help us understand the write workload impact or what happened with transactions.</span></p>
<p><span>Why would one need to dig into galera.cache files, though? Let&rsquo;s think about possible scenarios:</span></p>
<ul>
<li aria-level="1"><span>Debugging replication issues or conflicts (BF aborts, etc).</span></li>
<li aria-level="1"><span>Understanding recent workload patterns per table (especially when binary log is not enabled or lost).</span></li>
<li aria-level="1"><span>Understanding the IST capacity and why node joining falls back to SST.</span></li>
<li aria-level="1"><span>Forensic analysis after incidents.</span></li>
<li aria-level="1"><span>Why on-demand gcache.page.X files are created and what transactions are inside.</span></li>
<li aria-level="1"><span>What committed writesets are still in &ldquo;assigned / live&rdquo; vs &ldquo;released / reclaimable&rdquo; state.</span></li>
<li aria-level="1"><span>Observe / confirm the impact of </span><a href="https://dev.mysql.com/doc/refman/8.4/en/replication-options-binary-log.html#sysvar_binlog_row_image"><span><code>binlog_row_image</code></span></a><span> setting on the writesets size.</span></li>
</ul>
<p><span>To address those, I decided to experiment with a tool that would decode the Galera cache files. As a result of these experiments, I recently published </span><a href="https://github.com/PrzemekMalkowski/gcache-inspector"><span>gcache-inspector</span></a><span> &ndash; an open source project available on GitHub.&nbsp;</span></p>
<p><span>Before I introduce how the tool works, let&rsquo;s quickly review the write set caching process.</span></p>
<h2><span>What is Galera Cache?&nbsp;</span><a class="anchor-link" id="what-is-galera-cache"></a></h2>
<p><span>In short, it is a RingBuffer file storing Write-set Cache, which is also memory-mapped. Every replicated transaction is appended to it. Due to the fixed size, the oldest entries are overwritten to allow new writes. In special circumstances when the cache file is too small to fit a big transaction or old entries are not ready to be removed, additional on-demand cache files are created.</span></p>
<p><span>From the operational perspective, the most important role of the Galera cache is to provide quick incremental synchronization (IST) of (re-)joining cluster nodes. Having the cache of enough size, so that it can store enough time&rsquo;s worth of writes, determines the joining process &ndash; whether a restarted node will be able to join quickly via IST or whether it will have to pull a full backup (SST) from the donor.</span></p>
<p><span>The diagram below shows the typical transaction lifecycle, role, and structure of the Galera cache.</span></p>
<p><img fetchpriority="high" decoding="async" class=" wp-image-51961 aligncenter" src="https://www.percona.com/wp-content/uploads/2026/08/fig1.png" alt="" width="656" height="610" srcset="https://www.percona.com/wp-content/uploads/2026/08/fig1.png 1100w, https://www.percona.com/wp-content/uploads/2026/08/fig1-300x279.png 300w, https://www.percona.com/wp-content/uploads/2026/08/fig1-1024x951.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/fig1-768x714.png 768w" sizes="(max-width: 656px) 100vw, 656px"></p>
<p><span>The IST determination is a bit more complex than you&rsquo;d expect. The joiner estimates the donor&rsquo;s capabilities with some safety margin.</span></p>
<p><span>It is possible to verify the current potential donor Galera cache coverage from its </span><a href="https://docs.percona.com/percona-xtradb-cluster/8.4/wsrep-status-index.html#wsrep_local_cached_downto"><span><code>wsrep_local_cached_downto</code></span></a><span> status variable.&nbsp;</span><span>Moreover, the cache rotation can be put on hold to extend the donor&rsquo;s time window coverage via the </span><a href="https://docs.percona.com/percona-xtradb-cluster/8.4/wsrep-provider-index.html#gcachefreeze_purge_at_seqno"><span><code>gcache.freeze_purge_at_seqno</code></span></a><span> provider option.</span></p>
<p><img loading="lazy" decoding="async" class="wp-image-51960 aligncenter" src="https://www.percona.com/wp-content/uploads/2026/08/fig2.png" alt="" width="667" height="461" srcset="https://www.percona.com/wp-content/uploads/2026/08/fig2.png 1096w, https://www.percona.com/wp-content/uploads/2026/08/fig2-300x207.png 300w, https://www.percona.com/wp-content/uploads/2026/08/fig2-1024x708.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/fig2-768x531.png 768w" sizes="auto, (max-width: 667px) 100vw, 667px"></p>
<p><span>If the above diagram is difficult to digest, the following blog post should shed light on the process: </span><a href="https://www.percona.com/blog/understanding-ist-donor-selected/"><span>https://www.percona.com/blog/understanding-ist-donor-selected/</span></a></p>
<p>&nbsp;</p>
<p><span>Given all this complexity, you may sometimes just want to check and verify for yourself what on earth is in the Galera cache files, instead of guessing.&nbsp;</span></p>
<p><span>And historically, the cache files were just a mystery &ndash; no tools available to actually properly inspect them. This is why I decided to experiment with a utility that would fill that gap.</span></p>
<h2><span>The gcache-inspector</span><a class="anchor-link" id="the-gcache-inspector"></a></h2>
<p><span>The tool I ended up with can fully decode the Galera cache files. It makes quick general write patterns statistics, write set nature information, and can decode the actual Row-based events (binary log style).</span></p>
<p><span>Gcache-inspector works offline (the examined PXC node can be running or not). You may point it to a galera.cache or gcache.page.X file. Below is an example of the default report without additional options used.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ gcache-inspector --file node2/data/galera.cache 
=== gcache-inspector 0.2.5 &mdash; GCache Summary ===
File:    /data/sandboxes/pxc_msb_pxc8_4_10/node2/data/galera.cache
Size:    128.00 MB
Version: 2   UUID: 62f2ad43-8de5-11f1-9fb8-8bae2a687fc9
Seqno (retained):  2 &ndash; 4533  (4532 in cache)
Synced:  yes   Offset: 1704
Encrypted: no
Flavor:  PXC / MySQL 8.x

Write-sets found:  4498  4498 retained, 0 older/overwritten
Decodable seqnos:  3 &ndash; 4533  (4498 write-sets; pick one with --seqno)
Time range:        2026-08-01 22:12:58 &ndash; 2026-08-05 23:04:02 CEST  (span 96h51m4s, newest 12d ago)
DDL statements:    216
GTID events seen:  0
Rows changed:      513758  (95.20 MB)  [all write-sets]

Top 10 tables by row activity:
  table                                      insert   update   delete    ddl       size
  &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  sbtest.sbtest1                               5039     5574       15      2        2.9M
  sbtest.sbtest32                              5053       37       18      2        0.9M
  sbtest.sbtest8                               5050       30       23      2        0.9M
  sbtest.sbtest60                              5049       31       21      2        0.9M
  sbtest.sbtest100                             5045       36       18      2        0.9M
  sbtest.sbtest10                              5054       26       19      2        0.9M
  sbtest.sbtest85                              5055       25       17      2        0.9M
  sbtest.sbtest35                              5042       40       14      2        0.9M
  sbtest.sbtest87                              5045       37       14      2        0.9M
  sbtest.sbtest33                              5048       30       18      2        0.9M</pre>
<p><span>By using the </span><b><code>--detail</code></b><span> parameter, the tool will show per-individual sequence number details, i.e.:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ gcache-inspector --file node2/data/galera.cache --detail --no-summary --seqno 100-105

=== Write-sets ===
  seqno 100           453632 B 2026-08-01 22:14:39  RELEASED  sbtest.sbtest16[i:2281 u:0 d:0]
  seqno 101              256 B 2026-08-01 22:14:39  RELEASED  1 DDL: CREATE INDEX k_16 ON sbtest16(k); sbtest.sbtest16[i:0 u:0 d:0]
  seqno 102              432 B 2026-08-01 22:14:39  RELEASED  1 DDL: CREATE TABLE sbtest17(; sbtest.sbtest17[i:0 u:0 d:0]
  seqno 103           540664 B 2026-08-01 22:14:39  RELEASED  sbtest.sbtest17[i:2719 u:0 d:0]
  seqno 104           453632 B 2026-08-01 22:14:39  RELEASED  sbtest.sbtest17[i:2281 u:0 d:0]
  seqno 105              256 B 2026-08-01 22:14:39  RELEASED  1 DDL: CREATE INDEX k_17 ON sbtest17(k); sbtest.sbtest17[i:0 u:0 d:0]</pre>
<p><span>The above example shows that a transaction committed with the sequence number 100 has inserted </span><span>2281</span><span> rows into the table sbtest16 and did not update or delete any rows.</span></p>
<p><span>To see exactly what a given transaction was about, the </span><b><code>--decode-rows</code></b><span> option prints the whole event details. For example, it&rsquo;s possible to see what rows were changed under seqno 4252:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ gcache-inspector --file node2/data/galera.cache --decode-rows --no-summary --seqno 4252

-- seqno 4252 at 2026-08-01 22:15:32 (816 bytes) RELEASED
### DELETE FROM `sbtest`.`sbtest41`
### WHERE
###   @1= 1843
###   @2= 3562
###   @3= '55824051154-00248428540-43829027453-18090470997-77687189613-13487855838-34568671126-01577127301-81564593132-49010886470'
###   @4= '09475435259-72703365718-14065084029-80972334150-38881617733'
### INSERT INTO `sbtest`.`sbtest41`
### SET
###   @1= 1843
###   @2= 4879
###   @3= '37041074202-54426174421-76052854404-43175485519-62755971707-75981734496-81616509419-51624022546-52075561216-00090498892'
###   @4= '46023326729-33104312594-23620888475-28615232417-62781559343'</pre>

<h3><span>A DDL investigation example</span><a class="anchor-link" id="a-ddl-investigation-example"></a></h3>
<p><span>Handling DDLs in Galera replication may be quite confusing. Even if, for instance, an ALTER query fails on the writer, it still gets replicated, causing surprising errors on the peer members, similar to this:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">2026-08-05T21:04:02.137267Z 11 [ERROR] [MY-010584] [Repl] Replica SQL: Error 'Table 'sbtest.foo' doesn't exist' on query. Default database: 'sbtest'. Query: 'alter table foo engine=innodb', Error_code: MY-001146
2026-08-05T21:04:02.137334Z 11 [Warning] [MY-000000] [WSREP] Event 1 Query apply failed: 1, seqno 4533
2026-08-05T21:04:02.138503Z 0 [Note] [MY-000000] [Galera] Member 0(przemek-d1) initiates vote on 62f2ad43-8de5-11f1-9fb8-8bae2a687fc9:4533,aebcd4f61a8a51aa:  Table 'sbtest.foo' doesn't exist, Error_code: 1146;</pre>
<p><span>Although such an event normally produces a GRA file to let us investigate, like in this case: </span><span>GRA_11_</span><b>4533</b><span>_v2.log</span><span>, now we can also look into the cache file for the same (here the SKIPPED flag confirms it was not applied):</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ gcache-inspector --file node2/data/galera.cache --detail --no-summary --seqno 4533

=== Write-sets ===
  seqno 4533             256 B 2026-08-05 23:04:02  RELEASED|SKIPPED  1 DDL: alter table foo engine=innodb; sbtest.foo[i:0 u:0 d:0]</pre>

<h2><span>Encrypted Galera Cache</span><a class="anchor-link" id="encrypted-galera-cache"></a></h2>
<p><span>For strict security compliance cases, Percona XtraDB Cluster allows </span><a href="https://docs.percona.com/percona-xtradb-cluster/8.4/gcache-write-set-cache-encryption.html"><span>encrypting</span></a><span> the Gcache files. The tool allows inspection of encrypted files as well, if the encryption key or vault credentials are provided. But there is one caveat here. A regular, non-encrypted cache file will contain all replicated transactions immediately. Whilst the encrypted one will not show anything new until the </span><a href="https://docs.percona.com/percona-xtradb-cluster/8.4/gcache-write-set-cache-encryption.html#gcacheencryption_cache_size"><span>encryption in-memory cache</span></a><span> is filled or synced during shutdown. Therefore, new transactions are expected to appear in the encrypted cache file with a delay.</span></p>
<p><span>Note: the tool does not support encryption available in MariaDB Galera Cluster Enterprise Edition (no source code access).</span></p>
<p><span>An example output against an encrypted file:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">$ gcache-inspector --file node1/data/galera.cache --keyring-file /opt/mysql/pxc8.4.10/keyring/component_keyring_file
=== gcache-inspector 0.2.5 &mdash; GCache Summary ===
File:   node1/data/galera.cache
Size:    128.00 MB
Version: 2   UUID: 62f2ad43-8de5-11f1-9fb8-8bae2a687fc9
Seqno (retained):  4395 &ndash; 4533  (139 in cache)
Synced:  yes   Offset: 1776
Encrypted: yes &mdash; decrypted   (enc version 1)
Master key: GaleraKey-d6945297-8f7a-11f1-9533-7a5bf82f508c@62eff734-8de5-11f1-b956-7f3a785ad5e2-1
Key source:/opt/mysql/pxc8.4.10/keyring/component_keyring_file (GaleraKey-d6945297-8f7a-11f1-9533-7a5bf82f508c@62eff734-8de5-11f1-b956-7f3a785ad5e2-1)
Cipher:    AES-256-ctr-file, clear below 0x400, counter from 0x0 [CTR unwrap (zero IV), keyring bytes]
Freshness: on a live node the encrypted file lags the in-memory cache (write-back page cache; flushed on eviction/shutdown)
Flavor:  PXC / MySQL 8.x

Write-sets found:  127  127 retained, 0 older/overwritten
Decodable seqnos:  4395 &ndash; 4533  (127 write-sets; pick one with --seqno)
Time range:        2026-08-01 22:15:32 &ndash; 2026-08-05 23:04:02 CEST  (span 96h48m30s, newest 12d ago)
DDL statements:    1
GTID events seen:  0
Rows changed:      5782  (2.08 MB)  [all write-sets]

Top 10 tables by row activity:
  table                                      insert   update   delete    ddl       size
  &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  sbtest.sbtest1                                  2     5545        1      0        2.0M
  sbtest.sbtest58                                 5        1        2      0        0.0M
  sbtest.sbtest64                                 3        2        2      0        0.0M
  sbtest.sbtest52                                 4        1        2      0        0.0M
  sbtest.sbtest14                                 2        3        1      0        0.0M
  sbtest.sbtest26                                 2        2        2      0        0.0M
  sbtest.sbtest6                                  2        2        2      0        0.0M
  sbtest.sbtest98                                 3        1        2      0        0.0M
  sbtest.sbtest97                                 4        0        2      0        0.0M
  sbtest.sbtest68                                 1        4        0      0        0.0M</pre>

<h2><span>Summary</span><a class="anchor-link" id="summary"></a></h2>
<p><span>Although in most cases, problems with PXC/Galera replication can be successfully investigated based on error logs, binary logs, and GRA files, there may be more complex cases where you may want to look inside the Galera cache files. Or simply for experimenting or to allow better understanding of how it works. I hope </span><span>gcache-inspector</span><span> will help you do this. The tool is available as GPLv3, with Go source code and binary packages ready to play with on GitHub: </span><a href="https://github.com/PrzemekMalkowski/gcache-inspector"><span>https://github.com/PrzemekMalkowski/gcache-inspector</span></a><span>. Demo recording: </span><a href="https://asciinema.org/a/1263342"><span>https://asciinema.org/a/1263342</span></a></p>
<p><span>If, despite acquiring details, you face undersized gcache or other reasons causing nodes to keep falling back to SST, Percona&rsquo;s engineers can help you tackle those problems. Talk to us about a cluster health review </span><a href="https://www.percona.com/contact-us/"><span>https://www.percona.com/contact-us/</span></a></p>
<p><span>Additional references about Galera Cache can be found in the following blog posts by other Percona engineers:</span><br>
<a href="https://www.percona.com/blog/all-you-need-to-know-about-gcache-galera-cache/"><span>https://www.percona.com/blog/all-you-need-to-know-about-gcache-galera-cache/</span></a><br>
<a href="https://www.percona.com/blog/no-sst-node-rejoins/"><span>https://www.percona.com/blog/no-sst-node-rejoins/</span></a><br>
<a href="https://www.percona.com/blog/understanding-ist-donor-selected/"><span>https://www.percona.com/blog/understanding-ist-donor-selected/</span></a><br>
<a href="https://www.percona.com/blog/gcache-and-record-set-cache-encryption-in-percona-xtradb-cluster-part-one/"><span>https://www.percona.com/blog/gcache-and-record-set-cache-encryption-in-percona-xtradb-cluster-part-one/</span></a></p>
<p>&nbsp;</p>
<p><i><span>The article was written by a human</span></i></p>
<p>The post <a href="https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/">Stop guessing at gcache: inspect Galera/PXC write sets with gcache-inspector</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/stop-guessing-at-gcache-inspect-galera-pxc-write-sets-with-gcache-inspector/">Stop guessing at gcache: inspect Galera/PXC write sets with gcache-inspector</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>What a careful MySQL to MariaDB migration still misses</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/what-a-careful-mysql-to-mariadb-migration-still-misses/" />
      <id>https://mariadb.com/resources/blog/what-a-careful-mysql-to-mariadb-migration-still-misses/</id>
      <updated>2026-08-18T16:58:35+03:00</updated>
      <author><name>Michael Aglietti</name></author>
      <summary type="html"><![CDATA[<p>The MariaDB Migrator announcement post introduced the MySQL-to-MariaDB migration tool and walked through its four modes. This post is about […]</p>
<p><a href="https://mariadb.com/resources/blog/what-a-careful-mysql-to-mariadb-migration-still-misses/">What a careful MySQL to MariaDB migration still misses</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The MariaDB Migrator announcement post introduced the MySQL-to-MariaDB migration tool and walked through its four modes. This post is about what those modes are up against. The three stories below are hypothetical, but the failures are not. I found the same patterns recurring across migration write-ups and forum threads, then reproduced each one on a MySQL 8.0 server migrating to MariaDB 11.4.</p>
<p><a href="https://mariadb.com/resources/blog/what-a-careful-mysql-to-mariadb-migration-still-misses/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/what-a-careful-mysql-to-mariadb-migration-still-misses/">What a careful MySQL to MariaDB migration still misses</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 12.3 LTS Webinar: Performance, Scalability, High Availability &#038; the AI-Native Database</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mariadb-12-3-lts-webinar/" />
      <id>https://minervadb.com/mariadb-12-3-lts-webinar/</id>
      <updated>2026-08-18T10:45:31+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>Our MariaDB 12.3 LTS webinar is now available as a free PDF download. Over 60 minutes, Shiv Iyer — Founder &#038; CEO of MinervaDB and a database engineer who has spent three decades inside MySQL, [...]</p>
<p><a href="https://minervadb.com/mariadb-12-3-lts-webinar/">MariaDB 12.3 LTS Webinar: Performance, Scalability, High Availability &#038; the AI-Native Database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Our <strong>MariaDB 12.3 LTS webinar</strong> is now available as a free PDF download. Over 60 minutes, Shiv Iyer &mdash; Founder &amp; CEO of MinervaDB and a database engineer who has spent three decades inside MySQL, MariaDB, and PostgreSQL internals &mdash; walks through what the 12.3 LTS generation actually changes in production: the InnoDB-based binary log, parallel replication into Galera, MaxScale failover mechanics, and the mHNSW vector index that turns MariaDB into a credible RAG backend.</p>
<p>This is not a changelog read-aloud. The deck is built the way we run client engagements: every claim tied to a measurement source, every architecture drawn with its RPO/RTO consequences, and a clear statement of where each feature does <em>not</em> apply. Fill in the short form at the bottom of this page and the full deck lands in your inbox.</p>
<p>Here is what the MariaDB 12.3 LTS webinar covers, and why we think the timing matters.</p>
<h2>Why the 12.3 LTS generation matters right now<a class="anchor-link" id="why-the-12-3-lts-generation-matters-right-now"></a></h2>
<p>MariaDB 10.6 LTS reached end of life in July 2026. If you are still running it, you are now accumulating unpatched CVEs on your primary OLTP tier &mdash; that alone justifies an hour with the MariaDB 12.3 LTS webinar material. The practical question for most estates is no longer <em>whether</em> to move, but whether to land on 11.8 LTS or 12.3 LTS.</p>
<p>The support windows are not symmetrical, and this catches people out. MariaDB 12.3 LTS, released Q2 2026, is maintained through 2029 on a three-year window. 11.8 LTS runs to mid-2030 &mdash; a longer runway than the newer release. The webinar opens with this decision: which workloads justify the 12.3 feature set, and which are better served by parking on 11.8 until the next LTS cycle. The <a href="https://mariadb.org/" target="_blank" rel="noopener">MariaDB Foundation</a> publishes the current release calendar; the deck maps it onto upgrade sequencing for mixed estates.</p>
<p>MariaDB&rsquo;s release model has also changed shape: rolling GA releases punctuated by LTS milestones (the .3 releases). If your change-management process still assumes the old cadence, part one of the webinar is the correction.</p>
<h2>What the MariaDB 12.3 LTS webinar covers<a class="anchor-link" id="what-the-mariadb-12-3-lts-webinar-covers"></a></h2>
<p>We built the MariaDB 12.3 LTS webinar for architects and DBAs who own upgrade decisions, not for a general audience.</p>
<p>The MariaDB 12.3 LTS webinar runs five parts across 60 minutes:</p>
<ul>
<li><strong>The platform in 2026</strong> &mdash; release model evolution, LTS support windows, upgrade planning for estates coming off 10.6.</li>
<li><strong>Performance</strong> &mdash; the InnoDB-based binary log, optimizer refinements, and how to validate vendor benchmark claims against your own workload.</li>
<li><strong>Scalability</strong> &mdash; parallel replication modes, MaxScale routing, storage engine selection, and analytics offload.</li>
<li><strong>High availability</strong> &mdash; Galera Cluster in 12.3, the RPO/RTO decision matrix, MaxScale automated failover, backup and point-in-time recovery.</li>
<li><strong>AI and vector search</strong> &mdash; mHNSW indexes, hybrid search with Reciprocal Rank Fusion, and RAG architectures that keep embeddings transactionally consistent with source rows.</li>
</ul>
<h2>Performance: one engine, one recovery protocol, one flush discipline<a class="anchor-link" id="performance-one-engine-one-recovery-protocol-one-flush-discipline"></a></h2>
<p>The headline change in the 12.3 generation is the InnoDB-based binary log. MariaDB has collapsed the old dual-log architecture &mdash; binlog and InnoDB redo log, each with its own fsync discipline and a two-phase commit stitching them together &mdash; into a single recovery protocol owned by InnoDB.</p>
<p>MariaDB reports roughly 4&times; write throughput on heavy workloads with this change, with crash safety inherited from InnoDB&rsquo;s redo protocol. We treat that number the way we treat every vendor benchmark: as a hypothesis to test. The webinar dedicates a section to validation methodology &mdash; replay your own production statement digests on the candidate version, hold <code>sync_binlog</code> and <code>innodb_flush_log_at_trx_commit</code> constant across runs, measure P95/P99 latency rather than throughput alone, and run at production concurrency with the thread pool configured the way you actually deploy it.</p>
<p>The optimizer work in 12.3 is quieter but operationally useful: reverse-ordered scans, loose index scan with DESC keys, and improved virtual-column costing. Two new hints &mdash; <code>JOIN_FIXED_ORDER</code> and <code>MAX_EXECUTION_TIME</code> &mdash; give you per-statement plan control, and <code>ANALYZE FORMAT=JSON</code> now surfaces actual row counts against estimates, which is the fastest way to catch a misestimation before it becomes a 2 a.m. page. Our standing advice, repeated in the deck: capture your top-N statement digests and plans before the upgrade, replay them in staging, and diff the plans before cutover. If you want the monitoring side of that discipline, we published the <a href="https://minervadb.com/mariadb-performance-monitoring-metrics/">20 MariaDB metrics we track in every engagement</a>.</p>
<h2>Scalability: the levers most teams never pull<a class="anchor-link" id="scalability-the-levers-most-teams-never-pull"></a></h2>
<p>MariaDB GTID is not MySQL GTID &mdash; the domain-server-sequence format is incompatible, which matters for anyone running mixed fleets or mid-migration topologies. From that foundation, the webinar works through the two parallel-apply modes: conservative, which parallelizes within group commit boundaries, and optimistic, which applies speculatively and rolls back on conflict.</p>
<p>The change we consider most significant for DR design: since 12.1, parallel replication works into Galera nodes. Async replicas feeding a DR cluster were previously throttled to single-threaded apply, and lag on that link was a standing RPO risk. That constraint is gone, and the deck shows the resulting cluster-to-cluster DR topology.</p>
<p>Beyond replication, part three covers MaxScale &mdash; read/write split, causal reads, transaction replay, and an honest note on its Business Source License terms &mdash; plus storage engine selection: InnoDB for OLTP, MyRocks where write amplification and compression dominate, Aria for temp tables, S3 for cold archives, ColumnStore for analytics, and Spider for sharding. If you run MariaDB on Kubernetes or cloud VMs, our guide to <a href="https://minervadb.com/tuning-mariadb-for-cloud/">tuning MariaDB for cloud and containerized environments</a> pairs well with this section.</p>
<h2>High availability: pick your RPO before you pick your topology<a class="anchor-link" id="high-availability-pick-your-rpo-before-you-pick-your-topology"></a></h2>
<p>Part four opens with the decision matrix we use in architecture reviews: asynchronous replication gives you seconds of RPO and seconds-to-minutes of RTO; semi-sync closes RPO to approximately zero at a latency cost; Galera gives you zero RPO within the cluster and seconds of RTO, in exchange for three-node quorum, primary keys on every InnoDB table, and a network you can trust.</p>
<figure><img loading="lazy" decoding="async" class="wp-image-92869" src="https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-scaled.png" alt="MariaDB 12.3 LTS webinar high availability reference architecture diagram &mdash; three-node Galera cluster with MaxScale routing and async GTID replication to a DR site" width="1600" height="780" srcset="https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-scaled.png 2560w, https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-300x146.png 300w, https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-1024x499.png 1024w, https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-768x374.png 768w, https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-1536x749.png 1536w, https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-lts-galera-maxscale-dr-architecture-2048x998.png 2048w" sizes="auto, (max-width: 1600px) 100vw, 1600px"><figcaption>Reference HA topology from the MariaDB 12.3 LTS webinar: single-writer Galera cluster behind MaxScale, async GTID replication (parallel apply) to a DR-site Galera cluster.</figcaption></figure>
<p>One trade-off in 12.3 deserves more attention than it has received: the InnoDB-based binary log and Galera are mutually exclusive. wsrep cannot intercept the atomic commit path, so cluster nodes keep the legacy binlog &mdash; meaning the 4&times; write-path improvement and Galera&rsquo;s synchronous replication cannot be combined on the same node. The webinar treats this as a first-class design input, not a footnote. We covered the surrounding architecture decisions in <a href="https://minervadb.com/mariadb-high-availability/">MariaDB high availability in 12.3 LTS</a> and in our piece on <a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/">building fault-tolerant MariaDB infrastructure at internet scale</a>.</p>
<p>The section closes on operations: MaxScale automated failover via <code>mariadbmon</code>, switchover for planned maintenance, hot physical backups with <code>mariabackup</code> (full plus incremental, with the <code>--prepare</code> step), and point-in-time recovery by replaying the binlog to a target GTID. Two cautions from the deck worth repeating here. First: verify that your <code>mariabackup</code> version and any CDC tooling that reads binlog files directly are certified for the new binlog format before you enable it. Second: an untested backup is a hypothesis, not a recovery plan &mdash; measure your verified restore time, and rehearse failover quarterly.</p>
<h2>The AI-native database: mHNSW, hybrid search, and honest boundaries<a class="anchor-link" id="the-ai-native-database-mhnsw-hybrid-search-and-honest-boundaries"></a></h2>
<p>MariaDB now ships a native <code>VECTOR</code> column type with an mHNSW index &mdash; a modified HNSW graph &mdash; and distance functions for both Euclidean and cosine metrics. The 12.3 generation pushes distance computation down into the storage layer and adds extrapolation-based graph pruning; the practical tuning lever is <code>M</code>, the graph connectivity parameter, which trades recall against memory and latency. A retrieval query stays plain SQL:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="">SELECT doc_id,
       VEC_DISTANCE_COSINE(embedding, @query_vec) AS distance
FROM   knowledge_chunks
ORDER  BY distance
LIMIT  10;</pre>
<p>The webinar&rsquo;s RAG argument is transactional rather than fashionable: embeddings and their source rows commit in one transaction, which eliminates the synchronization drift you accept the moment you bolt a separate vector store onto your OLTP database. Hybrid search fuses keyword and vector results with Reciprocal Rank Fusion, and the deck walks the full architecture. For the index internals, see our earlier post on <a href="https://minervadb.com/understanding-vector-indexes-in-mariadb/">how vector indexes work in MariaDB</a> and the <a href="https://mariadb.com/kb/en/vector-overview/" target="_blank" rel="noopener">MariaDB Vector documentation</a>.</p>
<p>And the boundary, stated plainly because vendor decks rarely do: MariaDB Vector is credible up to tens of millions of vectors. At billions-scale, or where GPU-accelerated indexing dominates the workload, a dedicated vector platform is the right call. We say this as a vendor-neutral consultancy &mdash; right-size first.</p>
<h2>Two reference architectures you can lift directly<a class="anchor-link" id="two-reference-architectures-you-can-lift-directly"></a></h2>
<p>The MariaDB 12.3 LTS webinar deck closes its infrastructure arc with two worked topologies. The performance-first design: a 12.3 primary on the InnoDB binlog, two replicas on parallel apply, a delayed replica as a fat-finger safety net, and scheduled backups &mdash; seconds of RPO, sub-minute RTO. The availability-first design: a three-node Galera cluster on the legacy binlog with single-writer routing through MaxScale, feeding an async DR cluster that uses the 12.1+ parallel-apply capability. Both diagrams carry version labels and the reasoning for each choice.</p>
<h2>Download the MariaDB 12.3 LTS webinar deck (free)<a class="anchor-link" id="download-the-mariadb-12-3-lts-webinar-deck-free"></a></h2>
<figure><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/MariaDB12.3-Performance-scale-ha-AI-MinervaDB-pdf.jpg" alt="MariaDB 12.3 LTS webinar PDF cover &mdash; performance, scalability, high availability and AI" width="854" height="480"><figcaption>The MariaDB 12.3 LTS webinar deck: Performance, Scalability, High Availability &amp; the AI-Native Database.</figcaption></figure>
<p>Fill in the form below to get the MariaDB 12.3 LTS webinar PDF &mdash; we&rsquo;ll show the download link immediately and email you a copy. We&rsquo;ll occasionally send you technical material like this; no spam, and never a sales sequence you didn&rsquo;t ask for. The MariaDB 12.3 LTS webinar PDF is free, and the download link arrives instantly.</p>
<div class="wpforms-container wpforms-container-full">Please enable JavaScript in your browser to complete this form.
<div class="wpforms-field-container">
<div class="wpforms-field wpforms-field-text" data-field-type="text" data-field-id="1"><label class="wpforms-field-label" for="wpforms-92864-field_1">First Name <span class="wpforms-required-label">*</span></label></div>
<div class="wpforms-field wpforms-field-text" data-field-type="text" data-field-id="2"><label class="wpforms-field-label" for="wpforms-92864-field_2">Last Name <span class="wpforms-required-label">*</span></label></div>
<div class="wpforms-field wpforms-field-text" data-field-type="text" data-field-id="3">
			<label class="wpforms-field-label" for="wpforms-92864-field_3">First Last Name</label></div>
<div class="wpforms-field wpforms-field-email" data-field-type="email" data-field-id="5"><label class="wpforms-field-label" for="wpforms-92864-field_5">Email <span class="wpforms-required-label">*</span></label></div>
<div class="wpforms-field wpforms-field-url" data-field-type="url" data-field-id="4"><label class="wpforms-field-label" for="wpforms-92864-field_4">URL</label></div>
</div>
<p><!-- .wpforms-field-container --></p>
<div class="wpforms-submit-container"><button type="submit" name="wpforms[submit]" class="wpforms-submit" data-alt-text="Sending..." data-submit-text="Download the Webinar PDF" aria-live="assertive" value="wpforms-submit">Download the Webinar PDF</button><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/plugins/wpforms/assets/images/submit-spin.svg" class="wpforms-submit-spinner" width="26" height="26" alt="Loading"></div>
</div>
<p>  <!-- .wpforms-container --></p>
<h2>Planning a MariaDB 12.3 upgrade? Talk to us<a class="anchor-link" id="planning-a-mariadb-12-3-upgrade-talk-to-us"></a></h2>
<p>MinervaDB runs MariaDB in production for enterprises worldwide &mdash; consulting, 24&times;7 support, and remote DBA across the full lifecycle. If the MariaDB 12.3 LTS webinar leaves you with questions &mdash; a 10.6 exit plan, a Galera design, a MariaDB Vector evaluation &mdash; and you would benefit from engineers who do this every week, <a href="https://minervadb.com/contact/">contact us</a>. We&rsquo;ll tell you honestly if 11.8 is the better landing zone for your workload &mdash; and, as always, test everything in staging before it touches production, and keep your DR posture rehearsed.</p>

<p><a href="https://minervadb.com/mariadb-12-3-lts-webinar/">MariaDB 12.3 LTS Webinar: Performance, Scalability, High Availability &#038; the AI-Native Database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Seravo becomes a Silver Sponsor of MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/" />
      <id>https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/</id>
      <updated>2026-08-18T13:35:41+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is delighted to welcome Seravo as a Silver Sponsor, turning a long-standing relationship in the open-source ecosystem into formal support for the future of MariaDB. …<br />
Continue reading \"Seravo becomes a Silver Sponsor of MariaDB Foundation\"<br />
Seravo becomes a Silver Sponsor of MariaDB Foundation appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/">Seravo becomes a Silver Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is delighted to welcome <a href="https://seravo.com/en/">Seravo</a> as a Silver Sponsor, turning a long-standing relationship in the open-source ecosystem into formal support for the future of MariaDB. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Seravo becomes a Silver Sponsor of MariaDB Foundation&rdquo;</span></a></p>
<p><a href="https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/">Seravo becomes a Silver Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/seravo-becomes-a-silver-sponsor-of-mariadb-foundation/">Seravo becomes a Silver Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona University Comes to Uruguay</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-university-comes-to-uruguay/" />
      <id>https://www.percona.com/blog/percona-university-comes-to-uruguay/</id>
      <updated>2026-08-17T16:34:42+03:00</updated>
      <author><name>Agustín Gallego</name></author>
      <summary type="html"><![CDATA[<p>Percona University is coming to Montevideo. On September 23rd, 2026, we’re getting together for a full day of technical talks on open source software, and you are invited! If you work or study with open source software in Uruguay, this one is for you. It’s a whole day of learning, with the people who build … Continued<br />
The post Percona University Comes to Uruguay appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-university-comes-to-uruguay/">Percona University Comes to Uruguay</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>Percona University is coming to Montevideo. On </span><b>September 23rd, 2026</b><span>, we&rsquo;re getting together for a full day of technical talks on open source software, and you are invited!</span></p>
<p><span>If you work or study with open source software in Uruguay, this one is for you. It&rsquo;s a whole day of learning, with the people who build and run these systems every day. No sales pitch, just good technical content and a community that likes to share what it knows.</span></p>
<p><span>Let me tell you what to expect, and why it&rsquo;s worth a spot on your calendar.</span></p>
<h2><b>What is Percona University?</b><a class="anchor-link" id="what-is-percona-university"></a></h2>
<p><span>Percona has run these events for years across the world, including a few stops in South America already. This is the third time it will be held in Uruguay. The idea is simple: bring the open-source database community together to share knowledge for free.</span></p>
<p><span>The word &ldquo;University&rdquo; is the important part here. This is an educational event, and not a sales one. The talks are technical, and the goal is that you walk out having learned something you can use daily.</span></p>
<p><span>You don&rsquo;t need to be a Percona customer, and you definitely don&rsquo;t need to be an expert. You just need to be curious about open source databases and want to spend a day getting better at them. Bring your questions, bring a notebook and your laptop, and plan to stay for the conversations between sessions.</span></p>
<h2><b>Who is it for and why you should come</b><a class="anchor-link" id="who-is-it-for-and-why-you-should-come"></a></h2>
<p><span>Come if you work with PostgreSQL, MySQL, MongoDB, or anything in the open source data world. It&rsquo;s a great place for DBAs, backend developers, SREs, students, and people just getting started to come together and learn from each other. The talks cover real problems: performance tuning, upgrades, monitoring, the things we all fight with in production.</span></p>
<p><span>Networking with peers is what I like the most about events like this. You get to meet the people behind the tools, ask engineers the questions you&rsquo;ve been sitting on for months, in person, and actually talk it through. You meet other folks in Uruguay doing the same work you do, and those hallway conversations are worth as much as the talks themselves.</span></p>
<p><span>Open source is built by the community, and it&rsquo;s nice to see that community in person. We don&rsquo;t get a full day of this in Montevideo very often at all, so it&rsquo;s worth showing up!</span></p>
<h2><b>The details</b><a class="anchor-link" id="the-details"></a></h2>
<p><span>Here&rsquo;s what you need to know:</span></p>
<ul>
<li aria-level="1"><b>When:</b><span> September 23rd, 2026, from 09:00 to 18:00</span></li>
<li aria-level="1"><b>Where:</b><span> Regency Way Montevideo Hotel. Av Gral Rivera 3377</span></li>
<li aria-level="1"><b>Cost:</b><span> FREE!</span></li>
<li aria-level="1"><b>Language:</b><span> talks will be in English and Spanish, with slides you can review afterward</span></li>
</ul>
<h2><b>What&rsquo;s on the agenda</b><a class="anchor-link" id="whats-on-the-agenda"></a></h2>
<ul>
<li aria-level="1"><span>Opening Session, State of Open Source Database Ecosystem. </span><b>Peter Zaitsev</b></li>
<li aria-level="1"><span>Beyond &ldquo;Postgres is up&rdquo;: detecting failures and performance tuning with Coroot. </span><b>Nikolay Sivko / Agust&iacute;n Gallego</b></li>
<li aria-level="1"><span>bpftrace 301: tracing variables life cycle for advanced bug investigations. </span><b>Marcos Albe</b></li>
<li aria-level="1"><span>TiDB 101. </span><b>Fernando Ipar</b></li>
<li aria-level="1"><span>MySQL 8.4 Upgrade: Best Practices For Zero-Surprise Prod Migration. </span><b>Fernando Mattera</b></li>
<li aria-level="1"><span>From Reactive to Ready: How AI Changed My Work as a Service Delivery Manager. </span><b>Mariana Bonsignore</b></li>
<li aria-level="1"><span>5-minute lightning talks at the end&hellip; and there are still some available slots for talks, so submit yours!</span></li>
</ul>
<p><span>We&rsquo;re lining up talks from Percona engineers and guest speakers from the region. Expect a mix of topics, with plenty of practical hands-on content. Since it runs all day, there&rsquo;s room for deeper sessions than a normal meetup allows, plus breaks to grab a coffee and keep talking. The full agenda will live on the Eventbrite page (we&rsquo;ll keep adding talks as they are confirmed).</span></p>
<h2><b>How to register</b><a class="anchor-link" id="how-to-register"></a></h2>
<p><span>Registration is free on Eventbrite, and it&rsquo;s quick.</span></p>
<p><b>Register here:</b></p>
<p><a href="https://www.eventbrite.com/e/percona-university-montevideo-uruguay-tickets-1998244427875"><span>https://www.eventbrite.com/e/percona-university-montevideo-uruguay-tickets-1998244427875</span></a></p>
<p><span>Grab your spot early, since seats are limited.</span></p>
<p><span>In short, Percona University Montevideo is a full day to learn, meet the community, and get better at the open source databases we all rely on. It&rsquo;s happening on September 23rd, and everyone is welcome to attend.</span></p>
<p><span>Register, tell a coworker, tell a friend, and come say hi. We hope to see you there!</span></p>
<p>The post <a href="https://www.percona.com/blog/percona-university-comes-to-uruguay/">Percona University Comes to Uruguay</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-university-comes-to-uruguay/">Percona University Comes to Uruguay</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB High Availability: Maximum Availability Solutions in MariaDB 12.3 LTS</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mariadb-high-availability/" />
      <id>https://minervadb.com/mariadb-high-availability/</id>
      <updated>2026-08-17T11:03:37+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MariaDB high availability reached a new baseline with MariaDB Server 12.3 LTS, GA on 2026-05-29 as version 12.3.2. For the first time, a MariaDB LTS release ships parallel replication between Galera Clusters (MDEV-20065), closing the [...]</p>
<p><a href="https://minervadb.com/mariadb-high-availability/">MariaDB High Availability: Maximum Availability Solutions in MariaDB 12.3 LTS</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB high availability reached a new baseline with <a href="https://mariadb.org/mariadb-server-12-3-lts-released/" target="_blank" rel="noopener">MariaDB Server 12.3 LTS</a>, GA on 2026-05-29 as version 12.3.2. For the first time, a MariaDB LTS release ships parallel replication between Galera Clusters (MDEV-20065), closing the throughput gap that constrained multi-datacenter MariaDB high availability designs for years. This guide walks through the complete maximum availability stack on MariaDB 12.3 LTS &mdash; GTID-based asynchronous and semi-synchronous replication, Galera Cluster, cluster-to-cluster disaster recovery, and MaxScale automated failover &mdash; with tested configuration, the system tables and status variables that prove each layer is healthy, and the upgrade risks you must clear before production.</p>
<p>Every MariaDB high availability recommendation here is version-pinned to MariaDB Community Server 12.3 LTS (with 11.8 LTS noted where behavior differs) and anchored to a named metric or system table. Test everything in a staging environment that mirrors production before applying any change, and maintain a verified backup and disaster recovery posture throughout.</p>
<h2>What Maximum Availability Means in Measurable Terms<a class="anchor-link" id="what-maximum-availability-means-in-measurable-terms"></a></h2>
<p>Maximum availability is not a product SKU &mdash; it is an engineering outcome defined by two numbers: Recovery Point Objective (RPO), the data you can afford to lose, and Recovery Time Objective (RTO), the downtime you can afford to absorb. A MariaDB high availability architecture is only as good as the RPO/RTO it can demonstrate under a live failover drill.</p>
<p>MariaDB 12.3 LTS lets you engineer three distinct tiers:</p>
<ul>
<li><strong>RPO &asymp; seconds, RTO &asymp; seconds to minutes</strong> &mdash; GTID-based asynchronous replication with MaxScale automated failover.</li>
<li><strong>RPO &asymp; 0 (per committed transaction), RTO &asymp; seconds</strong> &mdash; semi-synchronous replication with automated failover.</li>
<li><strong>RPO = 0, RTO &asymp; seconds</strong> &mdash; Galera Cluster synchronous multi-primary replication, extended across datacenters with cluster-to-cluster asynchronous replication.</li>
</ul>
<p>Treat these tiers as measurable service levels, not marketing labels. An availability SLO of 99.99% allows roughly 52 minutes of downtime per year &mdash; a budget that a single unrehearsed failover can consume entirely. That is why every MariaDB high availability design decision below is paired with the status variable, system table, or log line that proves it works, and why the article closes with the drill program that converts configuration into demonstrated RPO/RTO.</p>
<p>The rest of this article builds each tier bottom-up, then combines them into the reference architecture we deploy for production MariaDB high availability engagements.</p>
<h2>MariaDB 12.3 LTS: The Availability-Relevant Release Surface<a class="anchor-link" id="mariadb-12-3-lts-the-availability-relevant-release-surface"></a></h2>
<p>The <a href="https://mariadb.com/docs/release-notes/community-server/12.3/mariadb-12.3-changes-and-improvements" target="_blank" rel="noopener">MariaDB 12.3 changes and improvements</a> notes contain a dense cluster of replication and Galera work relevant to MariaDB high availability. These are the changes that matter operationally:</p>
<table>
<thead>
<tr>
<th>Change</th>
<th>Tracking</th>
<th>Operational impact</th>
</tr>
</thead>
<tbody>
<tr>
<td>Asynchronous replication between two Galera Clusters can use parallel replication, controlled by slave_parallel_threads</td>
<td>MDEV-20065</td>
<td>Removes the single-threaded applier ceiling on cluster-to-cluster DR links</td>
</tr>
<tr>
<td>Write set apply retry via wsrep_applier_retry_count</td>
<td>MDEV-36077</td>
<td>Transient applier conflicts retry instead of forcing node aborts</td>
</tr>
<tr>
<td>Unnecessary foreign key checks avoided during Incremental State Transfer (IST)</td>
<td>MDEV-34822</td>
<td>Faster node rejoin after short outages</td>
</tr>
<tr>
<td>Binary logging performance improved by removing a synchronization requirement</td>
<td>MDEV-34705</td>
<td>Lower commit-path latency on binlog-enabled primaries</td>
</tr>
<tr>
<td>ROW events larger than max_packet_size can be fragmented</td>
<td>MDEV-32570</td>
<td>Large-row workloads stop breaking replication</td>
</tr>
<tr>
<td>Galera package dependency removed from server packages</td>
<td>MDEV-38744</td>
<td>Install the Galera provider package explicitly on cluster nodes</td>
</tr>
</tbody>
</table>
<p>Carry-over from 11.8 LTS that completes the picture: slave_abort_blocking_timeout (MDEV-34857) automatically aborts long-running transactions that block the replication applier, and asynchronous rollback during crash recovery lets a recovering server accept connections before large rollbacks finish &mdash; measure recovery as startup-to-accepting-connections, not total rollback completion.</p>
<h2>Tier 1: GTID Replication with Semi-Synchronous Commit<a class="anchor-link" id="tier-1-gtid-replication-with-semi-synchronous-commit"></a></h2>
<p>Standard replication remains the foundation of MariaDB high availability. On 12.3 LTS, always run it with GTID (MariaDB format: Domain-ServerID-Sequence) so failover targets can be repointed without file/position arithmetic.</p>
<h3>Primary and Replica Configuration<a class="anchor-link" id="primary-and-replica-configuration"></a></h3>
<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/my.cnf.d/replication.cnf -- MariaDB 12.3 LTS
# Restart required for server_id and log_bin changes
[mariadb]
server_id                        = 101          # unique per server
log_bin                          = mariadb-bin
binlog_format                    = ROW
log_slave_updates                = ON
gtid_domain_id                   = 1            # distinct per replication domain
gtid_strict_mode                 = ON

# Durability on the primary: RPO depends on these two
innodb_flush_log_at_trx_commit   = 1
sync_binlog                      = 1

# Parallel applier on replicas
slave_parallel_threads           = 8            # size from workload, see below
slave_parallel_mode              = optimistic</pre>
<p>Point the replica at the primary using GTID:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">CHANGE MASTER TO
    MASTER_HOST     = 'primary.db.internal',
    MASTER_USER     = '${REPL_USER}',
    MASTER_PASSWORD = '${REPL_PASSWORD}',
    MASTER_USE_GTID = slave_pos,
    MASTER_SSL      = 1;

START REPLICA;</pre>
<p>Asynchronous replication alone leaves a nonzero RPO: transactions committed on the primary but not yet shipped are lost on failover. <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/semisynchronous-replication" target="_blank" rel="noopener">Semi-synchronous replication</a>, built into MariaDB Server (no plugin installation required since 10.3), closes that gap by refusing to acknowledge a commit to the client until at least one replica has received the event:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- Primary (dynamic, no restart)
SET GLOBAL rpl_semi_sync_master_enabled    = ON;
SET GLOBAL rpl_semi_sync_master_timeout    = 2000;        -- ms; then falls back to async
SET GLOBAL rpl_semi_sync_master_wait_point = AFTER_SYNC;  -- lossless wait point

-- Each replica (dynamic, no restart)
SET GLOBAL rpl_semi_sync_slave_enabled = ON;</pre>
<p><img loading="lazy" decoding="async" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NjAiIGhlaWdodD0iMzAwIiB2aWV3Qm94PSIwIDAgODYwIDMwMCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj48ZGVmcz48bWFya2VyIGlkPSJhMyIgbWFya2VyV2lkdGg9IjEwIiBtYXJrZXJIZWlnaHQ9IjEwIiByZWZYPSI4IiByZWZZPSIzIiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wLDAgTDgsMyBMMCw2IHoiIGZpbGw9IiM0NzU1NjkiLz48L21hcmtlcj48L2RlZnM+PHJlY3Qgd2lkdGg9Ijg2MCIgaGVpZ2h0PSIzMDAiIGZpbGw9IiNmOGZhZmMiLz48dGV4dCB4PSI0MzAiIHk9IjMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE2IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBmMTcyYSI+U2VtaS1TeW5jaHJvbm91cyBDb21taXQgUGF0aCAtIE1hcmlhREIgMTIuMyBMVFMgKHJwbF9zZW1pX3N5bmNfbWFzdGVyX3dhaXRfcG9pbnQgPSBBRlRFUl9TWU5DKTwvdGV4dD48cmVjdCB4PSI2MCIgeT0iNzAiIHdpZHRoPSIyMjAiIGhlaWdodD0iNjAiIHJ4PSI4IiBmaWxsPSIjZTJlOGYwIiBzdHJva2U9IiM0NzU1NjkiLz48dGV4dCB4PSIxNzAiIHk9Ijk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBmMTcyYSI+QXBwbGljYXRpb248L3RleHQ+PHRleHQgeD0iMTcwIiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMzMzQxNTUiPkNPTU1JVDwvdGV4dD48cmVjdCB4PSIzMzAiIHk9IjcwIiB3aWR0aD0iMjIwIiBoZWlnaHQ9IjYwIiByeD0iOCIgZmlsbD0iI2RiZWFmZSIgc3Ryb2tlPSIjMWQ0ZWQ4Ii8+PHRleHQgeD0iNDQwIiB5PSI5NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxZTNhOGEiPlByaW1hcnkgMTIuMy4yPC90ZXh0Pjx0ZXh0IHg9IjQ0MCIgeT0iMTE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWUzYThhIj5iaW5sb2cgd3JpdGUgKyBzeW5jPC90ZXh0PjxyZWN0IHg9IjYwMCIgeT0iNzAiIHdpZHRoPSIyMjAiIGhlaWdodD0iNjAiIHJ4PSI4IiBmaWxsPSIjZGNmY2U3IiBzdHJva2U9IiMxNTgwM2QiLz48dGV4dCB4PSI3MTAiIHk9Ijk2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzE0NTMyZCI+UmVwbGljYSAxMi4zLjI8L3RleHQ+PHRleHQgeD0iNzEwIiB5PSIxMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxNjY1MzQiPnJwbF9zZW1pX3N5bmNfc2xhdmVfZW5hYmxlZCA9IE9OPC90ZXh0PjxsaW5lIHgxPSIyODAiIHkxPSIxMDAiIHgyPSIzMjYiIHkyPSIxMDAiIHN0cm9rZT0iIzQ3NTU2OSIgc3Ryb2tlLXdpZHRoPSIxLjgiIG1hcmtlci1lbmQ9InVybCgjYTMpIi8+PGxpbmUgeDE9IjU1MCIgeTE9Ijg4IiB4Mj0iNTk2IiB5Mj0iODgiIHN0cm9rZT0iIzQ3NTU2OSIgc3Ryb2tlLXdpZHRoPSIxLjgiIG1hcmtlci1lbmQ9InVybCgjYTMpIi8+PHRleHQgeD0iNTczIiB5PSI3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzMzNDE1NSI+ZXZlbnQgc2hpcDwvdGV4dD48bGluZSB4MT0iNTk2IiB5MT0iMTEyIiB4Mj0iNTUwIiB5Mj0iMTEyIiBzdHJva2U9IiMxNTgwM2QiIHN0cm9rZS13aWR0aD0iMS44IiBtYXJrZXItZW5kPSJ1cmwoI2EzKSIvPjx0ZXh0IHg9IjU3MyIgeT0iMTI4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMTQ1MzJkIj5BQ0s8L3RleHQ+PGxpbmUgeDE9IjMyNiIgeTE9IjEyMCIgeDI9IjI4MCIgeTI9IjEyMCIgc3Ryb2tlPSIjMTU4MDNkIiBzdHJva2Utd2lkdGg9IjEuOCIgbWFya2VyLWVuZD0idXJsKCNhMykiLz48dGV4dCB4PSIzMDMiIHk9IjEzNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzE0NTMyZCI+Y29tbWl0IE9LPC90ZXh0PjxyZWN0IHg9IjYwIiB5PSIxOTAiIHdpZHRoPSI3NjAiIGhlaWdodD0iNzYiIHJ4PSI4IiBmaWxsPSIjZmVmOWMzIiBzdHJva2U9IiNiNDUzMDkiLz48dGV4dCB4PSI0MzAiIHk9IjIxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM5MjQwMGUiPlJQTyBndWFyYW50ZWU6IGNvbW1pdCBhY2tub3dsZWRnZWQgb25seSBhZnRlciBvbmUgcmVwbGljYSBob2xkcyB0aGUgZXZlbnQ8L3RleHQ+PHRleHQgeD0iNDMwIiB5PSIyMzYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiM5MjQwMGUiPkZhbGxiYWNrIHRvIGFzeW5jIGFmdGVyIHJwbF9zZW1pX3N5bmNfbWFzdGVyX3RpbWVvdXQgKDIwMDAgbXMpIC0gYWxlcnQgb24gUnBsX3NlbWlfc3luY19tYXN0ZXJfc3RhdHVzID0gT0ZGPC90ZXh0Pjx0ZXh0IHg9IjQzMCIgeT0iMjU0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjOTI0MDBlIj5Nb25pdG9yOiBScGxfc2VtaV9zeW5jX21hc3Rlcl95ZXNfdHggdnMgUnBsX3NlbWlfc3luY19tYXN0ZXJfbm9fdHg8L3RleHQ+PC9zdmc+" alt="MariaDB high availability semi-synchronous replication commit path in MariaDB 12.3 LTS" width="860" height="300"></p>
<p><em>Figure 3: Semi-synchronous commit path in a MariaDB high availability replication pair &mdash; the RPO guarantee and its fallback behavior.</em></p>
<p>Verify the guarantee is actually active &mdash; a silent fallback to asynchronous mode is the classic semi-sync failure mode:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SHOW GLOBAL STATUS
WHERE Variable_name IN
    ('Rpl_semi_sync_master_status',
     'Rpl_semi_sync_master_yes_tx',
     'Rpl_semi_sync_master_no_tx');</pre>
<p>Alert when Rpl_semi_sync_master_status reads OFF or Rpl_semi_sync_master_no_tx grows: both mean commits are proceeding without the semi-sync guarantee. For replica lag, do not rely on Seconds_Behind_Master alone &mdash; track gtid_slave_pos against gtid_binlog_pos on the primary, and on 12.3 monitor per-worker state in information_schema.SLAVE_WORKER_THREADS when tuning slave_parallel_threads. For replica provisioning at scale, see our note on <a href="https://minervadb.com/transfer-backed-up-data-to-a-mariadb-replica/">transferring backups efficiently to a MariaDB replica</a>.</p>
<h3>Sizing the Parallel Applier<a class="anchor-link" id="sizing-the-parallel-applier"></a></h3>
<p>slave_parallel_threads is not a bigger-is-better knob. In optimistic mode the applier speculatively executes transactions in parallel and rolls back on conflict, so a write pattern with hot rows can spend more time retrying than applying. Size it empirically: start at 4&ndash;8 threads, replay production-shaped load, and compare the drain rate of gtid_slave_pos against the primary while watching the retry counters in SHOW GLOBAL STATUS LIKE &lsquo;Slave_retried_transactions&rsquo;.</p>
<p>On replicas dedicated to failover (rather than read scaling), keep innodb_flush_log_at_trx_commit = 1 and sync_binlog = 1 as well &mdash; a failover target with relaxed durability quietly converts your semi-sync RPO &asymp; 0 design back into a data-loss scenario the moment it is promoted.</p>
<h2>Tier 2: Galera Cluster &mdash; Synchronous Multi-Primary<a class="anchor-link" id="tier-2-galera-cluster-synchronous-multi-primary"></a></h2>
<p><a href="https://mariadb.com/kb/en/galera-cluster/" target="_blank" rel="noopener">MariaDB Galera Cluster</a> is the core of any zero-data-loss MariaDB high availability design: it delivers RPO = 0 inside a datacenter through certification-based synchronous replication: a transaction commits only after its write set has been replicated to and certified by every node in the cluster. A minimum of three nodes preserves quorum through any single node failure.</p>
<p><img loading="lazy" decoding="async" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NjAiIGhlaWdodD0iNDQwIiB2aWV3Qm94PSIwIDAgODYwIDQ0MCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KICA8ZGVmcz4KICAgIDxtYXJrZXIgaWQ9ImFyIiBtYXJrZXJXaWR0aD0iMTAiIG1hcmtlckhlaWdodD0iMTAiIHJlZlg9IjgiIHJlZlk9IjMiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTTAsMCBMOCwzIEwwLDYgeiIgZmlsbD0iIzQ3NTU2OSIvPjwvbWFya2VyPgogIDwvZGVmcz4KICA8cmVjdCB3aWR0aD0iODYwIiBoZWlnaHQ9IjQ0MCIgZmlsbD0iI2Y4ZmFmYyIvPgogIDx0ZXh0IHg9IjQzMCIgeT0iMzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTciIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMGYxNzJhIj5NYXJpYURCIDEyLjMgTFRTIEhpZ2ggQXZhaWxhYmlsaXR5IOKAlCBHYWxlcmEgQ2x1c3RlciB3aXRoIE1heFNjYWxlIDI1LjEwPC90ZXh0PgoKICA8cmVjdCB4PSIzMzAiIHk9IjUyIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjUyIiByeD0iOCIgZmlsbD0iI2UyZThmMCIgc3Ryb2tlPSIjNDc1NTY5Ii8+CiAgPHRleHQgeD0iNDMwIiB5PSI3NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMwZjE3MmEiPkFwcGxpY2F0aW9uIENsaWVudHM8L3RleHQ+CiAgPHRleHQgeD0iNDMwIiB5PSI5MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzMzNDE1NSI+c2luZ2xlIGVuZHBvaW50LCBwb3J0IDMzMDY8L3RleHQ+CgogIDxyZWN0IHg9IjI5MCIgeT0iMTQwIiB3aWR0aD0iMjgwIiBoZWlnaHQ9IjcwIiByeD0iOCIgZmlsbD0iI2RiZWFmZSIgc3Ryb2tlPSIjMWQ0ZWQ4Ii8+CiAgPHRleHQgeD0iNDMwIiB5PSIxNjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMWUzYThhIj5NYXhTY2FsZSAyNS4xMCAocmVhZHdyaXRlc3BsaXQpPC90ZXh0PgogIDx0ZXh0IHg9IjQzMCIgeT0iMTgyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWUzYThhIj5tYXJpYWRibW9uOiBhdXRvX2ZhaWxvdmVyPXRydWUsIGF1dG9fcmVqb2luPXRydWU8L3RleHQ+CiAgPHRleHQgeD0iNDMwIiB5PSIxOTgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZTNhOGEiPnRyYW5zYWN0aW9uX3JlcGxheT10cnVlIMK3IEJTTC1saWNlbnNlZDwvdGV4dD4KCiAgPGxpbmUgeDE9IjQzMCIgeTE9IjEwNCIgeDI9IjQzMCIgeTI9IjEzNiIgc3Ryb2tlPSIjNDc1NTY5IiBzdHJva2Utd2lkdGg9IjEuNiIgbWFya2VyLWVuZD0idXJsKCNhcikiLz4KCiAgPGc+CiAgICA8cmVjdCB4PSI4MCIgeT0iMjkwIiB3aWR0aD0iMjAwIiBoZWlnaHQ9Ijg2IiByeD0iOCIgZmlsbD0iI2RjZmNlNyIgc3Ryb2tlPSIjMTU4MDNkIi8+CiAgICA8dGV4dCB4PSIxODAiIHk9IjMxNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxNDUzMmQiPmRiLWRjMS1ub2RlMTwvdGV4dD4KICAgIDx0ZXh0IHg9IjE4MCIgeT0iMzMyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMTY2NTM0Ij5NYXJpYURCIDEyLjMuMiArIEdhbGVyYSA0PC90ZXh0PgogICAgPHRleHQgeD0iMTgwIiB5PSIzNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxNjY1MzQiPndzcmVwX2xvY2FsX3N0YXRlOiBTeW5jZWQ8L3RleHQ+CiAgICA8dGV4dCB4PSIxODAiIHk9IjM2NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzE2NjUzNCI+U1NUOiBtYXJpYWJhY2t1cDwvdGV4dD4KICA8L2c+CiAgPGc+CiAgICA8cmVjdCB4PSIzMzAiIHk9IjI5MCIgd2lkdGg9IjIwMCIgaGVpZ2h0PSI4NiIgcng9IjgiIGZpbGw9IiNkY2ZjZTciIHN0cm9rZT0iIzE1ODAzZCIvPgogICAgPHRleHQgeD0iNDMwIiB5PSIzMTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMTQ1MzJkIj5kYi1kYzEtbm9kZTI8L3RleHQ+CiAgICA8dGV4dCB4PSI0MzAiIHk9IjMzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzE2NjUzNCI+TWFyaWFEQiAxMi4zLjIgKyBHYWxlcmEgNDwvdGV4dD4KICAgIDx0ZXh0IHg9IjQzMCIgeT0iMzQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMTY2NTM0Ij53c3JlcF9sb2NhbF9zdGF0ZTogU3luY2VkPC90ZXh0PgogICAgPHRleHQgeD0iNDMwIiB5PSIzNjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxNjY1MzQiPmdjYWNoZS5zaXplID0gNEc8L3RleHQ+CiAgPC9nPgogIDxnPgogICAgPHJlY3QgeD0iNTgwIiB5PSIyOTAiIHdpZHRoPSIyMDAiIGhlaWdodD0iODYiIHJ4PSI4IiBmaWxsPSIjZGNmY2U3IiBzdHJva2U9IiMxNTgwM2QiLz4KICAgIDx0ZXh0IHg9IjY4MCIgeT0iMzE0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzE0NTMyZCI+ZGItZGMxLW5vZGUzPC90ZXh0PgogICAgPHRleHQgeD0iNjgwIiB5PSIzMzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxNjY1MzQiPk1hcmlhREIgMTIuMy4yICsgR2FsZXJhIDQ8L3RleHQ+CiAgICA8dGV4dCB4PSI2ODAiIHk9IjM0OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzE2NjUzNCI+d3NyZXBfbG9jYWxfc3RhdGU6IFN5bmNlZDwvdGV4dD4KICAgIDx0ZXh0IHg9IjY4MCIgeT0iMzY0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMTY2NTM0Ij5xdW9ydW0gbWVtYmVyPC90ZXh0PgogIDwvZz4KCiAgPGxpbmUgeDE9IjM4MCIgeTE9IjIxNCIgeDI9IjE5MCIgeTI9IjI4NiIgc3Ryb2tlPSIjNDc1NTY5IiBzdHJva2Utd2lkdGg9IjEuNiIgbWFya2VyLWVuZD0idXJsKCNhcikiLz4KICA8bGluZSB4MT0iNDMwIiB5MT0iMjE0IiB4Mj0iNDMwIiB5Mj0iMjg2IiBzdHJva2U9IiM0NzU1NjkiIHN0cm9rZS13aWR0aD0iMS42IiBtYXJrZXItZW5kPSJ1cmwoI2FyKSIvPgogIDxsaW5lIHgxPSI0ODAiIHkxPSIyMTQiIHgyPSI2NzAiIHkyPSIyODYiIHN0cm9rZT0iIzQ3NTU2OSIgc3Ryb2tlLXdpZHRoPSIxLjYiIG1hcmtlci1lbmQ9InVybCgjYXIpIi8+CgogIDxwYXRoIGQ9Ik0xODAgMzgwIFE0MzAgNDMyIDY4MCAzODAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzE1ODAzZCIgc3Ryb2tlLXdpZHRoPSIxLjgiIHN0cm9rZS1kYXNoYXJyYXk9IjYgNCIvPgogIDx0ZXh0IHg9IjQzMCIgeT0iNDIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEyIiBmaWxsPSIjMTQ1MzJkIj5zeW5jaHJvbm91cyB3cml0ZS1zZXQgcmVwbGljYXRpb24gKGNlcnRpZmljYXRpb24tYmFzZWQsIFJQTyA9IDApPC90ZXh0Pgo8L3N2Zz4K" alt="MariaDB high availability architecture: three-node Galera Cluster on MariaDB 12.3 LTS behind MaxScale 25.10 automated failover" width="860" height="440"></p>
<p><em>Figure 1: MariaDB high availability reference topology &mdash; three-node Galera Cluster behind MaxScale automated failover.</em></p>
<h3>Cluster Configuration on 12.3 LTS<a class="anchor-link" id="cluster-configuration-on-12-3-lts"></a></h3>
<p>Note MDEV-38744: from 12.3, server packages no longer pull the Galera provider &mdash; install the galera-4 package explicitly on every node.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/my.cnf.d/galera.cnf -- MariaDB 12.3 LTS, Galera 4
# Restart required for wsrep_provider changes
[mariadb]
wsrep_on                     = ON
wsrep_provider               = /usr/lib64/galera-4/libgalera_smm.so
wsrep_cluster_name           = prod_cluster_dc1
wsrep_cluster_address        = gcomm://10.0.1.11,10.0.1.12,10.0.1.13
wsrep_node_name              = db-dc1-node1
wsrep_node_address           = 10.0.1.11

# Galera requirements
binlog_format                = ROW
innodb_autoinc_lock_mode     = 2
log_slave_updates            = ON            # required for cluster-to-cluster replication

# Applier parallelism and 12.3 retry behavior
wsrep_slave_threads          = 8
wsrep_applier_retry_count    = 4             # new in 12.3, MDEV-36077

# State transfer
wsrep_sst_method             = mariabackup
wsrep_sst_auth               = ${SST_USER}:${SST_PASSWORD}

# Provider tuning: size gcache so short outages recover via IST, not SST
wsrep_provider_options       = "gcache.size=4G;gcs.fc_limit=256"</pre>
<p>Size gcache.size from your write volume: it must hold more write sets than accumulate during your longest tolerated node outage, because a node that finds its missing transactions in the donor gcache rejoins via Incremental State Transfer (IST) instead of a full State Snapshot Transfer (SST).</p>
<p>MariaDB 12.3 makes IST cheaper again by skipping unnecessary foreign key checks during the transfer (MDEV-34822).</p>
<h3>Proving Cluster Health<a class="anchor-link" id="proving-cluster-health"></a></h3>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">SHOW GLOBAL STATUS
WHERE Variable_name IN
    ('wsrep_cluster_status',          -- must be Primary
     'wsrep_cluster_size',            -- expected node count
     'wsrep_local_state_comment',     -- Synced on a healthy node
     'wsrep_flow_control_paused',     -- fraction of time paused; alert above 0.1
     'wsrep_local_recv_queue_avg',    -- sustained &gt; 0.5 means applier lag
     'wsrep_cert_deps_distance');     -- guides wsrep_slave_threads sizing</pre>
<p>wsrep_flow_control_paused is the number that tells you the cluster is throttling writes to protect its slowest node &mdash; the mechanism behind most &ldquo;Galera is slow&rdquo; incidents. We covered the full diagnostic sequence in <a href="https://minervadb.com/troubleshooting-galera-cluster-for-performance/">Troubleshooting Galera Cluster for Performance Issues</a>. Remember the standing Galera constraints: InnoDB tables with primary keys only, write-set certification conflicts surface as deadlock errors to the application, and large transactions are bounded by wsrep_max_ws_size.</p>
<p>Two design decisions determine whether Galera delivers its RPO = 0 promise in practice. First, quorum arithmetic: always deploy an odd number of nodes (or two nodes plus a Galera arbitrator) so a network partition leaves exactly one primary component; a 50/50 split freezes both halves, which protects consistency at the cost of availability.</p>
<p>Second, write-conflict discipline: Galera is multi-primary, but pointing all writes at a single node through your proxy layer eliminates certification conflicts for most OLTP workloads and makes wsrep_cert_deps_distance far more predictable. Reserve true multi-node writes for workloads you have tested for conflict rate &mdash; the counter to watch is wsrep_local_cert_failures.</p>
<h2>Multi-Datacenter Design: Parallel Galera-to-Galera Replication in 12.3<a class="anchor-link" id="multi-datacenter-design-parallel-galera-to-galera-replication-in-12-3"></a></h2>
<p>Stretching one Galera Cluster across WAN links penalizes every commit with inter-DC round trips. The production-grade pattern for MariaDB high availability across regions is one Galera Cluster per datacenter, connected by GTID-based asynchronous replication &mdash; and this is exactly where MariaDB 12.3 LTS delivers its headline improvement: the asynchronous link between two Galera Clusters can now use parallel replication (MDEV-20065).</p>
<p>Before 12.3, the cluster-to-cluster applier was effectively single-threaded, so a write-heavy primary cluster could permanently outrun its DR cluster. Now the replica-side applier fans out through slave_parallel_threads:</p>
<p><img loading="lazy" decoding="async" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI4NjAiIGhlaWdodD0iMzYwIiB2aWV3Qm94PSIwIDAgODYwIDM2MCIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIj4KICA8ZGVmcz4KICAgIDxtYXJrZXIgaWQ9ImFyMiIgbWFya2VyV2lkdGg9IjEwIiBtYXJrZXJIZWlnaHQ9IjEwIiByZWZYPSI4IiByZWZZPSIzIiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wLDAgTDgsMyBMMCw2IHoiIGZpbGw9IiNiNDUzMDkiLz48L21hcmtlcj4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9Ijg2MCIgaGVpZ2h0PSIzNjAiIGZpbGw9IiNmOGZhZmMiLz4KICA8dGV4dCB4PSI0MzAiIHk9IjMwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE3IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzBmMTcyYSI+TXVsdGktREMgTWF4aW11bSBBdmFpbGFiaWxpdHkg4oCUIFBhcmFsbGVsIEdhbGVyYS10by1HYWxlcmEgUmVwbGljYXRpb24gKE1hcmlhREIgMTIuMywgTURFVi0yMDA2NSk8L3RleHQ+CgogIDxyZWN0IHg9IjQwIiB5PSI2MCIgd2lkdGg9IjMzMCIgaGVpZ2h0PSIyMzAiIHJ4PSIxMCIgZmlsbD0iI2VmZjZmZiIgc3Ryb2tlPSIjMWQ0ZWQ4Ii8+CiAgPHRleHQgeD0iMjA1IiB5PSI4NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxZTNhOGEiPkRhdGFjZW50ZXIgMSDigJQgcHJpbWFyeSAoZ3RpZF9kb21haW5faWQgPSAxKTwvdGV4dD4KICA8cmVjdCB4PSI2NSIgeT0iMTA1IiB3aWR0aD0iMTMwIiBoZWlnaHQ9IjQ4IiByeD0iNiIgZmlsbD0iI2RjZmNlNyIgc3Ryb2tlPSIjMTU4MDNkIi8+CiAgPHRleHQgeD0iMTMwIiB5PSIxMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMTQ1MzJkIj5ub2RlMTwvdGV4dD4KICA8dGV4dCB4PSIxMzAiIHk9IjE0MSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzE2NjUzNCI+MTIuMy4yICsgR2FsZXJhIDQ8L3RleHQ+CiAgPHJlY3QgeD0iMjE1IiB5PSIxMDUiIHdpZHRoPSIxMzAiIGhlaWdodD0iNDgiIHJ4PSI2IiBmaWxsPSIjZGNmY2U3IiBzdHJva2U9IiMxNTgwM2QiLz4KICA8dGV4dCB4PSIyODAiIHk9IjEyNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxNDUzMmQiPm5vZGUyPC90ZXh0PgogIDx0ZXh0IHg9IjI4MCIgeT0iMTQxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMTY2NTM0Ij4xMi4zLjIgKyBHYWxlcmEgNDwvdGV4dD4KICA8cmVjdCB4PSIxNDAiIHk9IjE3NSIgd2lkdGg9IjEzMCIgaGVpZ2h0PSI0OCIgcng9IjYiIGZpbGw9IiNkY2ZjZTciIHN0cm9rZT0iIzE1ODAzZCIvPgogIDx0ZXh0IHg9IjIwNSIgeT0iMTk1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzE0NTMyZCI+bm9kZTM8L3RleHQ+CiAgPHRleHQgeD0iMjA1IiB5PSIyMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiMxNjY1MzQiPmJpbmxvZyBzb3VyY2U8L3RleHQ+CiAgPHRleHQgeD0iMjA1IiB5PSIyNTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZTNhOGEiPmxvZ19zbGF2ZV91cGRhdGVzID0gT04gb24gYWxsIG5vZGVzPC90ZXh0PgogIDx0ZXh0IHg9IjIwNSIgeT0iMjc0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmaWxsPSIjMWUzYThhIj5zeW5jaHJvbm91cyBpbnNpZGUgREMgKFJQTyA9IDApPC90ZXh0PgoKICA8cmVjdCB4PSI0OTAiIHk9IjYwIiB3aWR0aD0iMzMwIiBoZWlnaHQ9IjIzMCIgcng9IjEwIiBmaWxsPSIjZWZmNmZmIiBzdHJva2U9IiMxZDRlZDgiLz4KICA8dGV4dCB4PSI2NTUiIHk9Ijg2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzFlM2E4YSI+RGF0YWNlbnRlciAyIOKAlCBEUiAoZ3RpZF9kb21haW5faWQgPSAyKTwvdGV4dD4KICA8cmVjdCB4PSI1MTUiIHk9IjEwNSIgd2lkdGg9IjEzMCIgaGVpZ2h0PSI0OCIgcng9IjYiIGZpbGw9IiNkY2ZjZTciIHN0cm9rZT0iIzE1ODAzZCIvPgogIDx0ZXh0IHg9IjU4MCIgeT0iMTI1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjExIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzE0NTMyZCI+bm9kZTEgKHJlcGxpY2EpPC90ZXh0PgogIDx0ZXh0IHg9IjU4MCIgeT0iMTQxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMTY2NTM0Ij5wYXJhbGxlbCBhcHBsaWVyPC90ZXh0PgogIDxyZWN0IHg9IjY2NSIgeT0iMTA1IiB3aWR0aD0iMTMwIiBoZWlnaHQ9IjQ4IiByeD0iNiIgZmlsbD0iI2RjZmNlNyIgc3Ryb2tlPSIjMTU4MDNkIi8+CiAgPHRleHQgeD0iNzMwIiB5PSIxMjUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMTQ1MzJkIj5ub2RlMjwvdGV4dD4KICA8dGV4dCB4PSI3MzAiIHk9IjE0MSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzE2NjUzNCI+MTIuMy4yICsgR2FsZXJhIDQ8L3RleHQ+CiAgPHJlY3QgeD0iNTkwIiB5PSIxNzUiIHdpZHRoPSIxMzAiIGhlaWdodD0iNDgiIHJ4PSI2IiBmaWxsPSIjZGNmY2U3IiBzdHJva2U9IiMxNTgwM2QiLz4KICA8dGV4dCB4PSI2NTUiIHk9IjE5NSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxNDUzMmQiPm5vZGUzPC90ZXh0PgogIDx0ZXh0IHg9IjY1NSIgeT0iMjExIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEwIiBmaWxsPSIjMTY2NTM0Ij4xMi4zLjIgKyBHYWxlcmEgNDwvdGV4dD4KICA8dGV4dCB4PSI2NTUiIHk9IjI1NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzFlM2E4YSI+c2xhdmVfcGFyYWxsZWxfdGhyZWFkcyA9IDg8L3RleHQ+CiAgPHRleHQgeD0iNjU1IiB5PSIyNzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMxZTNhOGEiPnNsYXZlX3BhcmFsbGVsX21vZGUgPSBvcHRpbWlzdGljPC90ZXh0PgoKICA8bGluZSB4MT0iMzcwIiB5MT0iMTY1IiB4Mj0iNDg2IiB5Mj0iMTY1IiBzdHJva2U9IiNiNDUzMDkiIHN0cm9rZS13aWR0aD0iMi40IiBtYXJrZXItZW5kPSJ1cmwoI2FyMikiLz4KICA8dGV4dCB4PSI0MjgiIHk9IjE1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMiIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM5MjQwMGUiPmFzeW5jIEdUSUQgcmVwbGljYXRpb248L3RleHQ+CiAgPHRleHQgeD0iNDI4IiB5PSIxODQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiM5MjQwMGUiPk1BU1RFUl9VU0VfR1RJRCA9IHNsYXZlX3BvczwvdGV4dD4KICA8dGV4dCB4PSI0MjgiIHk9IjIwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzkyNDAwZSI+cGFyYWxsZWwgc2luY2UgMTIuMzwvdGV4dD4KCiAgPHRleHQgeD0iNDMwIiB5PSIzMzAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiMzMzQxNTUiPlZlcmlmaWNhdGlvbjogU0VMRUNUIEBAZ3RpZF9zbGF2ZV9wb3MsIEBAZ3RpZF9iaW5sb2dfcG9zOyDigJQgcG9zaXRpb25zIG11c3QgY29udmVyZ2UgdW5kZXIgc3VzdGFpbmVkIHdyaXRlIGxvYWQ8L3RleHQ+Cjwvc3ZnPgo=" alt="MariaDB high availability multi-datacenter topology: parallel Galera-to-Galera asynchronous replication in MariaDB 12.3 LTS" width="860" height="360"></p>
<p><em>Figure 2: Multi-datacenter MariaDB high availability &mdash; one Galera Cluster per DC linked by parallel asynchronous replication (new in 12.3).</em></p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql">-- On one node of the DR cluster (DC2)
-- Each cluster keeps a distinct gtid_domain_id (e.g., DC1 = 1, DC2 = 2)
SET GLOBAL slave_parallel_threads = 8;
SET GLOBAL slave_parallel_mode    = optimistic;

CHANGE MASTER TO
    MASTER_HOST     = 'dc1-vip.db.internal',
    MASTER_USER     = '${REPL_USER}',
    MASTER_PASSWORD = '${REPL_PASSWORD}',
    MASTER_USE_GTID = slave_pos,
    MASTER_SSL      = 1;

START REPLICA;

-- Verify parallel apply is active and lag is draining
SHOW REPLICA STATUSG
SELECT @@gtid_slave_pos, @@gtid_binlog_pos;</pre>
<p>Every node in each cluster must run log_slave_updates = ON so replicated transactions re-enter the local cluster&rsquo;s write-set replication, and the replication user should exist on all nodes of the source cluster so the link can be re-pointed after a node failure. Combine this with slave_abort_blocking_timeout (11.8+) on the DR side so a stray analytical query cannot stall the applier indefinitely.</p>
<p>If you do stretch a single Galera Cluster across sites instead &mdash; legitimate for metro-distance, low-latency links &mdash; declare WAN topology to the provider with gmcast.segment in wsrep_provider_options (a distinct segment ID per site), so Galera relays traffic once per segment instead of once per node and picks IST/SST donors within the local segment. Measure the commit-latency cost before choosing this path: every transaction pays the inter-site round trip at certification time, which is precisely what the cluster-per-DC design above avoids.</p>
<h2>Automated Failover with MariaDB MaxScale 25.10<a class="anchor-link" id="automated-failover-with-mariadb-maxscale-25-10"></a></h2>
<p>Replication and Galera provide redundancy; MariaDB high availability additionally requires something to detect failure and redirect traffic in seconds. MariaDB MaxScale 25.10 (current release 25.10.3, GA 2026-06-15) pairs a readwritesplit router with <a href="https://mariadb.com/docs/maxscale/mariadb-maxscale-tutorials/automatic-failover-with-mariadb-monitor" target="_blank" rel="noopener">automatic failover driven by the MariaDB Monitor</a>:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/maxscale.cnf -- MaxScale 25.10.3
[Replication-Monitor]
type                     = monitor
module                   = mariadbmon
servers                  = db1,db2,db3
user                     = ${MAXSCALE_USER}
password                 = ${MAXSCALE_PASSWORD}
replication_user         = ${REPL_USER}
replication_password     = ${REPL_PASSWORD}
monitor_interval         = 2s
auto_failover            = true
auto_rejoin              = true
failcount                = 3            # 3 x 2s = failure declared after ~6s

[RW-Split-Service]
type                     = service
router                   = readwritesplit
servers                  = db1,db2,db3
user                     = ${MAXSCALE_USER}
password                 = ${MAXSCALE_PASSWORD}
transaction_replay       = true         # replays in-flight transactions after failover</pre>
<p>With auto_failover enabled, MaxScale promotes the most up-to-date replica when the primary fails, repoints the remaining replicas, and auto_rejoin re-subordinates the old primary when it returns &mdash; no split brain, no manual CHANGE MASTER. Planned maintenance uses a zero-data-loss switchover instead:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell"># Verify topology state before acting
maxctrl list servers

# Planned promotion of db2 (waits for replicas to catch up)
maxctrl call command mariadbmon switchover Replication-Monitor db2

# Validate: db2 is Master, others are Slave, Running
maxctrl list servers</pre>
<p>Two disclosures belong in every MaxScale design. First, licensing: MaxScale is distributed under the Business Source License (BSL) &mdash; not OSI open source &mdash; and production use beyond the license grant requires a MariaDB subscription. Second, vendor neutrality: ProxySQL and HAProxy (with an external failover orchestrator) remain capable open-source alternatives; MaxScale earns its place when you need integrated automated failover, transaction replay, and Galera-aware routing in one component. Choose per requirement, not by default.</p>
<p>Do not let the proxy become the new single point of failure. Run at least two MaxScale instances &mdash; either behind keepalived with a virtual IP, or using MaxScale cooperative monitoring so only one instance performs failover actions at a time &mdash; and monitor them with the same seriousness as the database tier. The availability of a MariaDB high availability stack is the availability of its weakest routing component.</p>
<h2>Choosing Your MariaDB High Availability Topology<a class="anchor-link" id="choosing-your-mariadb-high-availability-topology"></a></h2>
<p>There is no single correct MariaDB high availability topology &mdash; there is a correct topology per RPO/RTO requirement, write pattern, and operational maturity. The matrix below summarizes the trade-offs on MariaDB 12.3 LTS:</p>
<table>
<thead>
<tr>
<th>Topology (MariaDB 12.3 LTS)</th>
<th>RPO</th>
<th>RTO</th>
<th>Write scaling</th>
<th>Operational complexity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Async GTID replication + MaxScale auto_failover</td>
<td>Seconds (replication lag)</td>
<td>~5&ndash;15 s</td>
<td>Single primary</td>
<td>Low</td>
</tr>
<tr>
<td>Semi-sync replication + MaxScale auto_failover</td>
<td>&asymp; 0 per acknowledged commit</td>
<td>~5&ndash;15 s</td>
<td>Single primary</td>
<td>Low&ndash;medium</td>
</tr>
<tr>
<td>Galera Cluster (3+ nodes, single DC)</td>
<td>0</td>
<td>Seconds (connection re-route)</td>
<td>Multi-primary (conflict-bound)</td>
<td>Medium</td>
</tr>
<tr>
<td>Galera per DC + parallel cluster-to-cluster replication</td>
<td>0 in-DC; seconds cross-DC</td>
<td>Seconds in-DC; minutes for DC failover</td>
<td>Multi-primary per DC</td>
<td>High</td>
</tr>
</tbody>
</table>
<p>RTO figures are illustrative planning values, not benchmarks &mdash; validate them with failover drills on your own workload and infrastructure.</p>
<h2>Upgrade Risks to Clear Before 12.3 LTS<a class="anchor-link" id="upgrade-risks-to-clear-before-12-3-lts"></a></h2>
<p>An upgrade executed carelessly is itself an availability incident, so treat the move to 12.3 LTS as part of the MariaDB high availability program. Three 12.3 changes have direct availability implications:</p>
<ul>
<li><strong>innodb_snapshot_isolation now defaults to ON.</strong> REPEATABLE READ behaves as true snapshot isolation, and applications written against the previous semantics can see new conflict errors under concurrency. Load-test transaction-heavy paths on a 12.3 clone while watching Innodb_row_lock_% status counters and application error rates by SQL digest. Rollback is dynamic, no restart: SET GLOBAL innodb_snapshot_isolation = OFF;</li>
<li><strong>Three new reserved words: CONVERSION, ST_COLLECT, TO_DATE.</strong> Pre-flight scan information_schema.COLUMNS, TABLES, and ROUTINES for these identifiers before upgrading; rename rather than quote.</li>
<li><strong>A known replication issue affects master_use_gtid settings during upgrade to 12.3.</strong> Capture SHOW REPLICA STATUS (Using_Gtid, Gtid_IO_Pos) and SELECT @@gtid_slave_pos, @@gtid_binlog_pos; on every replica before upgrading, upgrade replicas before the primary, and explicitly re-assert MASTER_USE_GTID after the upgrade instead of trusting persistence.</li>
</ul>
<p>Also plan the support horizon: under the <a href="https://mariadb.org/about/maintenance-policy/" target="_blank" rel="noopener">MariaDB maintenance policy</a>, Community LTS binaries are supported for three years from GA &mdash; 12.3 to roughly mid-2029, 11.8 to 2028-06-04 &mdash; with two further years of source-only fixes. Estates still on 10.6 passed community binary EOL on 2026-07-06 and need a dated migration plan.</p>
<h2>Backups Are Part of the MariaDB High Availability Design<a class="anchor-link" id="backups-are-part-of-the-mariadb-high-availability-design"></a></h2>
<p>Replication multiplies data; it does not protect it. A DROP TABLE, an application bug, or ransomware replicates to every node and every datacenter in milliseconds, which is why a MariaDB high availability architecture without tested backups is incomplete. Run mariabackup for physical backups from a designated Galera node or replica (the same tooling already serving as your SST method), keep binary logs for point-in-time recovery between backup sets, and store at least one copy outside the failure domain of both datacenters.</p>
<p>The metric that matters is not backup success rate &mdash; it is restore time, measured by actually restoring: schedule a quarterly restore drill and record the wall-clock duration against your RTO budget. Backup validation belongs in the same review cadence as failover drills, because the two protect against different failure classes.</p>
<h2>Prove It: Drills and Standing Telemetry<a class="anchor-link" id="prove-it-drills-and-standing-telemetry"></a></h2>
<p>A MariaDB high availability stack that has not survived a rehearsed failure is an assumption, not an architecture. Institutionalize quarterly MariaDB high availability drills: kill the primary and measure detection-to-promotion from MaxScale logs; kill a Galera node and measure IST rejoin duration from the error log and wsrep_local_state_comment transitions; fail an entire DC and measure cluster-to-cluster catch-up by watching gtid_slave_pos converge.</p>
<p>Alert continuously on wsrep_cluster_status, wsrep_flow_control_paused, Rpl_semi_sync_master_status, and GTID position drift &mdash; these four catch the large majority of MariaDB high availability regressions before customers do.</p>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>MariaDB 12.3 LTS is the strongest availability release in the MariaDB Server line to date: parallel cluster-to-cluster replication removes the last structural bottleneck in the dual-DC Galera reference architecture, applier retry and cheaper IST harden day-2 operations, and the surrounding LTS window gives you a platform stable through 2029. Engineered deliberately &mdash; GTID everywhere, semi-sync or Galera where RPO demands it, MaxScale or an open-source proxy layer for sub-15-second failover, and drills that prove the numbers &mdash; MariaDB high availability on 12.3 LTS supports RPO = 0 designs with single-digit-second in-DC recovery.</p>
<p>MinervaDB builds, audits, and operates these architectures for enterprises worldwide &mdash; from topology design and failover automation to 24&times;7 monitoring with the exact telemetry described above. If you are planning a 12.3 LTS upgrade or a high availability redesign, our <a href="https://minervadb.com/mariadb-remote-dba/">MariaDB Remote DBA and Support team</a> can help you get there with measured RPO/RTO outcomes. Contact us at contact@minervadb.com.</p>

<p><a href="https://minervadb.com/mariadb-high-availability/">MariaDB High Availability: Maximum Availability Solutions in MariaDB 12.3 LTS</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/" />
      <id>https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/</id>
      <updated>2026-08-16T16:00:46+03:00</updated>
      <author><name>Roman Nozdrin</name></author>
      <summary type="html"><![CDATA[<p>MariaDB already allows developers to add new Pluggable Data Types and scalar Plugin Functions. One missing piece has been Pluggable Aggregate Functions operating on PDTs. That matters for functionality such as HyperLogLog, where an extension needs to aggregate values into a custom statistical sketch while preserving its native SQL type. …<br />
Continue reading \"Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog\"<br />
Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/">Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB already allows developers to add new Pluggable Data Types and scalar Plugin Functions. One missing piece has been Pluggable Aggregate Functions operating on PDTs. That matters for functionality such as HyperLogLog, where an extension needs to aggregate values into a custom statistical sketch while preserving its native SQL type. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog&rdquo;</span></a></p>
<p><a href="https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/">Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/extending-mariadb-with-native-aggregate-plugins-laying-the-groundwork-for-hyperloglog/">Extending MariaDB with Native Aggregate Plugins: Laying the Groundwork for HyperLogLog</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Redis 8.10 Performance and Reliability: What Actually Changed</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/redis-8-10-performance-reliability/" />
      <id>https://minervadb.com/redis-8-10-performance-reliability/</id>
      <updated>2026-08-15T07:07:18+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>Redis 8.10 performance and reliability: the short version Redis 8.10 performance and reliability: the three changes that alter how you size and operate a Redis fleet. The headline for Redis 8.10 performance is that this [...]</p>
<p><a href="https://minervadb.com/redis-8-10-performance-reliability/">Redis 8.10 Performance and Reliability: What Actually Changed</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Redis 8.10 performance and reliability: the short version<a class="anchor-link" id="redis-8-10-performance-and-reliability-the-short-version"></a></h2>
<figure><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-performance-reliability.png" alt="Redis 8.10 performance and reliability diagram: compact hashes, MP-AOF BACKUP family and replication stream compression" width="1200" height="630" class="size-full wp-image-92842" srcset="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-performance-reliability.png 1200w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-performance-reliability-300x158.png 300w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-performance-reliability-1024x538.png 1024w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-performance-reliability-768x403.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px"><figcaption>Redis 8.10 performance and reliability: the three changes that alter how you size and operate a Redis fleet.</figcaption></figure>
<p>The headline for <strong>Redis 8.10 performance</strong> is that this is a memory-and-durability release wearing a performance badge. The two changes that will alter how you size and operate a Redis fleet are <strong>compact hashes</strong>, which store field names once across keys that share a schema, and the <strong><code>BACKUP</code> command family</strong>, which finally gives you node-side online backups built on multi-part AOF instead of a hand-rolled <code>BGSAVE</code>-and-copy script. Everything else &mdash; the new list, set and stream commands, the JSONPath expansion, the search timeout controls &mdash; is genuinely useful, but it will not change your capacity plan.</p>
<p>This post covers the three changes that matter operationally, demonstrates each with runnable commands, states who is affected, and closes with a dated, version-pinned upgrade stance. It is written for engineers running Redis as production infrastructure, not as a changelog retelling.</p>
<hr>
<h2>Step zero: establish exactly what you are running<a class="anchor-link" id="step-zero-establish-exactly-what-you-are-running"></a></h2>
<p>Before any of this applies, confirm the engine and the version. &ldquo;Redis&rdquo; in a real estate is frequently Redis OSS 7.2.4 under BSD, Redis 8.x under the tri-license, Valkey, or a managed service running one of them. Everything downstream &mdash; feature availability, licensing posture, upgrade path &mdash; depends on the answer.</p>
<pre class="EnlighterJSRAW">redis-cli INFO server | grep -E 'redis_version|redis_mode|os|arch_bits|io_threads_active'
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_policy|mem_allocator'
redis-cli CONFIG GET appendonly appendfsync repl-diskless-sync save</pre>
<p>Two facts to record alongside the version:</p>
<ul>
<li><strong>Licensing.</strong> Redis Open Source 8.0.0 and later ships under a tri-license &mdash; RSALv2, SSPLv1, or AGPLv3, user&rsquo;s choice &mdash; and that same tri-license covers the integral modules (RediSearch, RedisJSON, RedisTimeSeries, RedisBloom). Redis 7.2.x and earlier were BSD-3-Clause. This is an architecture input, not legal trivia; route formal compliance questions to counsel.</li>
<li><strong>Support horizon.</strong> As of August 2026, 8.10 is the current release. 8.0&rsquo;s security support runs to 01 Dec 2026, and 7.2/7.4 to 01 Dec 2029. If you are on 8.0, you have a deadline.</li>
</ul>
<hr>
<h2>Change 1: Compact hashes and the memory line item<a class="anchor-link" id="change-1-compact-hashes-and-the-memory-line-item"></a></h2>
<h3>What changed<a class="anchor-link" id="what-changed"></a></h3>
<p>Redis 8.10 introduces a new hash encoding that stores field names <strong>once per template</strong>, shared across every key that uses the same field layout. Redis has always had two hash encodings &mdash; <code>listpack</code> for small hashes and <code>hashtable</code> for large ones &mdash; and in both, every key carried its own copy of every field name. For the single most common Redis modelling pattern in the wild (one hash per entity, identical fields across millions of entities), that repetition was pure overhead.</p>
<p>Consider a million user records stored as <code>user:{id}</code> hashes with fields <code>name</code>, <code>email</code>, <code>age</code>, <code>country</code>, <code>plan</code>, <code>created_at</code>. Before 8.10, those six field-name strings were materialised a million times each. With compact hashes, they are materialised once into a template, and each key stores only its values plus a template reference.</p>
<figure><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-compact-hashes.png" alt="Redis 8.10 compact hashes: plain hash encoding versus a shared hash template" width="1200" height="560" class="size-full wp-image-92844" srcset="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-compact-hashes.png 1200w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-compact-hashes-300x140.png 300w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-compact-hashes-1024x478.png 1024w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-compact-hashes-768x358.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px"><figcaption>Redis 8.10 compact hashes: field names are stored once per template instead of once per key.</figcaption></figure>
<h3>Measuring it<a class="anchor-link" id="measuring-it"></a></h3>
<p>Three new counters expose the encoding directly, so you can verify the saving rather than assume it:</p>
<pre class="EnlighterJSRAW"># Distinct templates and how many keys are backed by them
redis-cli INFO stats | grep -E 'hash_templates|hash_template_keys'

# Memory consumed by the templates themselves
redis-cli INFO memory | grep used_memory_hash_templates

# Per-key verification
redis-cli OBJECT ENCODING user:1
redis-cli MEMORY USAGE user:1</pre>
<p>The honest way to size the benefit on <strong>your</strong> data is to load a representative slice into a scratch instance on both versions and compare <code>used_memory</code>. Do not extrapolate from a vendor headline:</p>
<pre class="EnlighterJSRAW"># Reproducible A/B: same dataset, two instances, two versions.
# Run on a scratch host. Never point this at production.

for PORT in 6388 6389; do
  redis-cli -p "$PORT" FLUSHALL
done

# Generate 1M schema-identical hashes and pipe them in
python3 - &lt;&lt;'PY' &gt; /tmp/users.redis
for i in range(1, 1_000_001):
    print(f"HSET user:{i} name u{i} email u{i}@example.com age {20 + i % 50} "
          f"country IN plan pro created_at 2026-08-15")
PY

redis-cli -p 6388 --pipe &lt; /tmp/users.redis   # 8.8 instance
redis-cli -p 6389 --pipe &lt; /tmp/users.redis   # 8.10 instance

for PORT in 6388 6389; do
  echo -n "port $PORT: "
  redis-cli -p "$PORT" INFO memory | grep -E '^used_memory:'
done</pre>
<p>Report the delta from your own run. That number is the one your capacity plan can spend.</p>
<h3>The bulk-load path: HIMPORT<a class="anchor-link" id="the-bulk-load-path-himport"></a></h3>
<p><code>HIMPORT</code> (new in 8.10) is a connection-scoped session that declares field names once and then sends values only. It reduces network bytes and per-command parsing work, and it lands the keys directly in the compact encoding. The fieldset is local to the connection and is discarded when the connection closes or on <code>RESET</code>.</p>
<pre class="EnlighterJSRAW"># Declare an ordered fieldset named "u" on this connection
HIMPORT PREPARE u name email age

# Send values only &mdash; field names are never repeated on the wire
HIMPORT SET user:1 u alice alice@example.com 30
HIMPORT SET user:2 u bob   bob@example.com   25

# Housekeeping
HIMPORT DISCARD u
HIMPORT DISCARDALL</pre>
<p>This is the right tool for migrations, warm-cache rebuilds, and ETL sinks. It is not a replacement for <code>HSET</code> in application code.</p>
<h3>Configuration: what to set, and when<a class="anchor-link" id="configuration-what-to-set-and-when"></a></h3>
<p>Three startup-relevant parameters govern how plain hashes are converted to templates during RDB load. Treat the defaults as correct until measurement says otherwise.</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Controls</th>
<th>Change requires</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>hash-rdb-load-min-template-entries</code></td>
<td>Minimum field count before a plain hash is converted to a template during load</td>
<td><code>CONFIG SET</code> (verify on your build)</td>
</tr>
<tr>
<td><code>hash-rdb-load-max-template-entries</code></td>
<td>Maximum field count eligible for load-time conversion</td>
<td><code>CONFIG SET</code> (verify on your build)</td>
</tr>
<tr>
<td><code>hash-rdb-load-template-disassembly-threshold</code></td>
<td>Minimum number of keys a converted template must end up backing to be kept</td>
<td><code>CONFIG SET</code> (verify on your build)</td>
</tr>
</tbody>
</table>
<pre class="EnlighterJSRAW">redis-cli CONFIG GET 'hash-rdb-load-*'</pre>
<h3>Who is affected, and where it does not help<a class="anchor-link" id="who-is-affected-and-where-it-does-not-help"></a></h3>
<p><strong>It helps most</strong> when you have many keys with an identical field layout &mdash; session stores, user/product/device catalogues, feature stores, entity caches.</p>
<p><strong>It helps least</strong> when hashes are heterogeneous (every key a different shape), when hashes are few and enormous, or when your memory is dominated by strings, sorted sets or streams rather than hashes. A wide-column model with per-key ad-hoc fields will produce template churn rather than template reuse &mdash; watch <code>hash_templates</code> climbing towards <code>hash_template_keys</code>, which is the signature of a schema too variable to benefit.</p>
<p>If your memory pressure is actually eviction policy or TTL hygiene rather than encoding, the fix is elsewhere. Our field guide on <a href="https://minervadb.com/mastering-redis-ttl/">mastering Redis TTL</a> covers the expiry-side of the same problem.</p>
<hr>
<h2>Change 2: The BACKUP command family &mdash; online backups without the shell scripts<a class="anchor-link" id="change-2-the-backup-command-family-online-backups-without-the-shell-scripts"></a></h2>
<h3>What changed<a class="anchor-link" id="what-changed"></a></h3>
<p>Until 8.10, &ldquo;backing up Redis&rdquo; meant one of a small set of unsatisfying options: trigger <code>BGSAVE</code> and copy <code>dump.rdb</code>, copy the AOF directory and hope you caught a consistent manifest, or take a filesystem/EBS snapshot and accept the fork-timing risk. Redis 8.10 ships <code>BACKUP</code>, a container command that produces a <strong>self-contained, restorable artifact set</strong> reusing the multi-part AOF (MP-AOF) format, without stopping writes and without you managing rewrites by hand.</p>
<p>A sealed backup is three artefacts:</p>
<ul>
<li><code>appendonly.aof.N.base.rdb</code> &mdash; the BASE point-in-time snapshot</li>
<li><code>appendonly.aof.N.incr.aof</code> &mdash; the INCR file holding writes accumulated after the snapshot</li>
<li><code>appendonly.aof.manifest</code> &mdash; a standalone manifest describing the set</li>
</ul>
<h3>The workflow<a class="anchor-link" id="the-workflow"></a></h3>
<figure><img loading="lazy" decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-backup-workflow.png" alt="Redis 8.10 BACKUP command family workflow: START, LIST, SEAL, copy, CLEANUP on MP-AOF" width="1200" height="560" class="size-full wp-image-92845" srcset="https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-backup-workflow.png 1200w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-backup-workflow-300x140.png 300w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-backup-workflow-1024x478.png 1024w, https://minervadb.com/wp-content/uploads/2026/08/redis-8-10-backup-workflow-768x358.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px"><figcaption>The Redis 8.10 BACKUP command family: START, LIST, SEAL, copy, CLEANUP &mdash; restore via preload-file.</figcaption></figure>
<pre class="EnlighterJSRAW"># 1. Open a backup window and produce a fresh BASE.
#    Works whether or not AOF persistence is enabled.
redis-cli BACKUP START

# 2. Ask which immutable files are pinned so far.
#    The data plane can start copying BASE while Redis keeps accumulating INCR.
redis-cli BACKUP LIST

# 3. Freeze the backup: hard-link the INCR, write the manifest.
#    After SEAL, BACKUP LIST also returns the INCR and manifest paths.
redis-cli BACKUP SEAL

# 4. Copy every path reported by BACKUP LIST to your backup target.
#    (rsync/aws s3 cp/azcopy &mdash; your data plane, not Redis's job.)

# 5. Release the pinned artefacts once the copy is verified.
redis-cli BACKUP CLEANUP</pre>
<p>Two commands for observability at any point in that sequence:</p>
<pre class="EnlighterJSRAW">redis-cli BACKUP STATUS   # inspect current backup state
redis-cli BACKUP ABORT    # cancel a backup that has not yet been sealed</pre>
<p>Relevant settings:</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Default</th>
<th>Meaning</th>
<th>Reload vs restart</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>backupdirname</code></td>
<td><code>backupdir</code></td>
<td>Backup directory, resolved under the server&rsquo;s <code>dir</code></td>
<td><strong>Startup-only</strong></td>
</tr>
<tr>
<td><code>backup-sealed-ttl</code></td>
<td><code>0</code> (disabled)</td>
<td>Seconds before sealed files are auto-cleaned</td>
<td>Runtime</td>
</tr>
</tbody>
</table>
<h3>Restore<a class="anchor-link" id="restore"></a></h3>
<p>Restore is a startup-only setting, <code>preload-file</code>, in the form <code><type>:<path></path></type></code>:</p>
<pre class="EnlighterJSRAW"># Restore from a sealed MP-AOF backup
preload-file aof:/var/backups/redis/appendonly.aof.manifest

# Or from a single RDB
preload-file rdb:/var/backups/redis/dump-2026-08-15.rdb</pre>
<p>When <code>preload-file</code> is set, Redis loads only that file or manifest and skips its normal <code>appenddirname</code> and <code>dump.rdb</code> loading. Once preload completes, it resumes the persistence mode you configured.</p>
<h3>Why this matters operationally<a class="anchor-link" id="why-this-matters-operationally"></a></h3>
<p>The genuinely important design detail is that <strong>creation is decoupled from finalisation</strong>. <code>BACKUP START</code> and <code>BACKUP SEAL</code> are separate calls, so a control plane can stagger <code>START</code> across cluster nodes and avoid every node forking at the same instant &mdash; the classic cause of a synchronised RSS spike and a latency cliff across a whole shard group. Each node produces its BASE independently; consistency is established at the seal boundary.</p>
<p><strong>Production safety caveat.</strong> A backup you have never restored is not a backup. Before this replaces your existing procedure, run a full restore into an isolated instance, validate key counts and a sample of application-level invariants, and time the restore so you have a real RTO number. Test in staging first; keep your existing backup path running in parallel for at least one full retention cycle.</p>
<p>A backup path is only half of a DR posture. Our <a href="https://minervadb.com/redis-performance-audit/">Redis performance audit</a> methodology treats restore drills as a first-class deliverable, not an afterthought.</p>
<hr>
<h2>Change 3: Replication and full-sync hardening<a class="anchor-link" id="change-3-replication-and-full-sync-hardening"></a></h2>
<p>The replication path has been getting steady attention across the 8.x line, and 8.10 continues it. Three items are worth your attention.</p>
<p><strong>Replication stream compression.</strong> Redis 8.10 adds compression of the replication stream between primaries and replicas, aimed squarely at bandwidth consumption. This matters most for cross-AZ and cross-region topologies, where replication egress is a line item on the cloud bill, and for write-heavy primaries where the output buffer is the constraint. One caveat shipped alongside it: a fix for memory reported for compressed replication clients being lower than actual consumption &mdash; evidence that this is a young code path. Watch <code>client_output_buffer</code> behaviour after upgrading.</p>
<p><strong>I/O thread busy-looping for replica clients</strong> is fixed. If you run with <code>io-threads</code> enabled and have replicas attached, this was burning CPU for nothing.</p>
<p><strong>Full sync under heavy write load</strong> is fixed. Full syncs that coincide with a write burst are exactly the condition under which a replica fails to catch up and re-triggers another full sync &mdash; the resync loop that turns a routine replica restart into an incident.</p>
<p>That builds on 8.8, where Redis eliminated RDB checksum computation on diskless transfers on the grounds that the replication link already provides integrity. Redis published a 12 GB full sync on a <code>c8g.2xlarge</code> dropping from 35 seconds to 11 seconds &mdash; a 68% reduction &mdash; as a result. Redis also reported pipelined <code>SET</code>/<code>HSET</code>/<code>ZADD</code> with an attached replica running 3% to 26% faster after reworking per-write bookkeeping in the replication feed path.</p>
<p>Verify the effect on your own topology rather than trusting the number:</p>
<pre class="EnlighterJSRAW"># Full-sync counters: sync_full should stay flat in steady state.
redis-cli INFO stats | grep -E 'sync_full|sync_partial_ok|sync_partial_err'

# Replica-side lag and link health
redis-cli INFO replication

# Backlog sizing &mdash; the single most common cause of avoidable full syncs
redis-cli CONFIG GET repl-backlog-size repl-backlog-ttl
redis-cli CONFIG GET client-output-buffer-limit</pre>
<p>If <code>sync_partial_err</code> is non-zero and <code>sync_full</code> is climbing, your backlog is too small for your write rate and no amount of release upgrading will fix it.</p>
<hr>
<h2>Redis 8.10 performance in context: the 8.4 &rarr; 8.10 curve<a class="anchor-link" id="redis-8-10-performance-in-context-the-8-4-%e2%86%92-8-10-curve"></a></h2>
<p>Redis 8.10 performance work does not stand alone: the 8.x line has been shipping measurable command-level gains every quarter. The figures below are <strong>Redis&rsquo;s own published measurements</strong>, not MinervaDB benchmarks. We reproduce them here with their methodology attached because a number without methodology is marketing.</p>
<p><strong>Redis 8.8 vs 8.6</strong> &mdash; tested on AWS <code>m7i.metal-24xl</code> (x86) and <code>m8g</code> (ARM Graviton4), identical builds, official OSS spec, multiple runs:</p>
<table>
<thead>
<tr>
<th>Operation</th>
<th>Reported gain</th>
<th>Mechanism</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>MGET</code> (pipelined, I/O threads)</td>
<td>up to 68%</td>
<td>Batched dict-bucket prefetch</td>
</tr>
<tr>
<td><code>MGET</code> (pipelined, single thread)</td>
<td>up to 50%</td>
<td>Memory prefetch framework</td>
</tr>
<tr>
<td><code>HGETALL</code> (1,000-field hashtable hashes)</td>
<td>up to 25%</td>
<td>Cross-command + dict-bucket prefetch</td>
</tr>
<tr>
<td><code>XREADGROUP</code> (<code>COUNT 100</code>)</td>
<td>up to 83%</td>
<td>Radix-tree O(1) append path, last-child-first descent</td>
</tr>
<tr>
<td><code>ZADD</code> / <code>ZINCRBY</code> / <code>ZRANGEBYSCORE</code></td>
<td>up to 74%</td>
<td>Widened Clinger fast path in float parsing</td>
</tr>
<tr>
<td><code>SCAN</code> family (<code>COUNT 500</code>, pipeline 10)</td>
<td>+38.0% x86, +39.7% ARM</td>
<td>Stack-allocated reply vector, zero heap allocations</td>
</tr>
<tr>
<td>Diskless full sync (12 GB)</td>
<td>35s &rarr; 11s</td>
<td>RDB checksum elimination on diskless transfer</td>
</tr>
</tbody>
</table>
<p><strong>Redis 8.6 vs 8.4</strong> &mdash; single core, <code>m8g.24xlarge</code> (Graviton4):</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Reported change</th>
</tr>
</thead>
<tbody>
<tr>
<td>Sorted-set command latency</td>
<td>up to 35% lower</td>
</tr>
<tr>
<td><code>GET</code> on short strings</td>
<td>up to 15% lower</td>
</tr>
<tr>
<td>List command latency</td>
<td>up to 11% lower</td>
</tr>
<tr>
<td>Hash command latency</td>
<td>up to 7% lower</td>
</tr>
<tr>
<td>Hash memory footprint</td>
<td>up to 16.7% smaller</td>
</tr>
<tr>
<td>Sorted-set memory footprint</td>
<td>up to 30.5% smaller</td>
</tr>
<tr>
<td><code>VADD</code> insertion / query</td>
<td>up to 43% / 58% faster (binary and 8-bit quantisation, x86-64)</td>
</tr>
</tbody>
</table>
<p>Redis also reported 3.5M ops/sec on a single node at pipeline depth 16 with 11 I/O threads on <code>m8g.24xlarge</code>, and framed the 8.6 line as 5&times;+ the caching throughput of Redis 7.2.</p>
<p><strong>How to read these numbers.</strong> Treat them as a ceiling on Redis 8.10 performance, not a forecast. They are single-command microbenchmarks at high pipeline depth on large instances. Real application workloads are dominated by round-trip time, key distribution, value size, and the presence of one pathological command in the mix. The 8.x gains are real and they are free on upgrade &mdash; but if your p99 is 8 ms because a <code>KEYS</code> call runs every 30 seconds, no release will help you. That class of problem is what our <a href="https://minervadb.com/redis-performance-troubleshooting/">Redis performance troubleshooting field guide</a> exists to localise, and the <a href="https://minervadb.com/troubleshooting-redis-performance-using-ebpf/">eBPF-based tracing approach</a> is how we get per-operation visibility when <code>SLOWLOG</code> is not enough.</p>
<hr>
<h2>Redis 8.10 reliability fixes that are upgrade drivers on their own<a class="anchor-link" id="redis-8-10-reliability-fixes-that-are-upgrade-drivers-on-their-own"></a></h2>
<p>Read these as security and correctness debt you are carrying if you stay put. Redis 8.10 fixes, among 30+ core items:</p>
<ul>
<li><strong>ACL permission bypass</strong> in <code>SORT</code>, <code>GEORADIUS</code>, <code>GEORADIUSBYMEMBER</code>, <code>XREAD</code> and <code>XREADGROUP</code>. If you rely on ACLs for multi-tenant isolation, this is a confidentiality issue, not a nuisance.</li>
<li><strong>Error-reply manipulation via injected <code>rn</code> sequences</strong> &mdash; a protocol-level response-splitting class of bug.</li>
<li><strong>AOF load failure</strong> when the AOF file had an RDB preamble and active defragmentation was enabled. This is a restore-time failure: the worst possible time to discover it.</li>
<li><strong>Division-by-zero</strong> when active-defragmentation thresholds are misconfigured.</li>
<li><strong>Clients left permanently blocked</strong> on <code>BLPOP</code>, <code>BLMOVE</code> or <code>BLMOVEM</code> after a <code>SORT ... STORE</code> replaced the target key.</li>
<li><strong><code>MEMORY USAGE</code> over-reporting</strong>, duplicate KeyMeta restoration during AOF rewrite, and RDB load robustness fixes for streams.</li>
</ul>
<p>Redis 8.8 separately addressed five CVEs (CVE-2026-23479, CVE-2026-25243, CVE-2026-23631, CVE-2026-25588, CVE-2026-25589) covering use-after-free and invalid-memory-access classes. If you are on 8.6 or earlier, you are exposed to all of them.</p>
<p>Verify your ACL surface after upgrading:</p>
<pre class="EnlighterJSRAW">redis-cli ACL LIST
redis-cli ACL GETUSER <username>
redis-cli ACL CAT keyspace</username></pre>
<hr>
<h2>Bounded replies: the quietest Redis 8.10 reliability win<a class="anchor-link" id="bounded-replies-the-quietest-redis-8-10-reliability-win"></a></h2>
<p>Unbounded replies are one of the more common ways a healthy Redis instance takes an application down &mdash; a consumer asks for &ldquo;everything since last offset&rdquo; after an outage and receives a multi-gigabyte reply that blows the client output buffer. Redis 8.10 adds explicit caps:</p>
<pre class="EnlighterJSRAW"># Cap cumulative entries AND cumulative reply size on stream reads
XREAD MAXCOUNT 5000 MAXSIZE 8388608 STREAMS events $
XREADGROUP GROUP g1 c1 MAXCOUNT 1000 MAXSIZE 1048576 STREAMS events &gt;</pre>
<p>Set these in your consumers now. They are the cheapest reliability change in the release.</p>
<p>Related controls in the same theme:</p>
<ul>
<li><code>SUNIONCARD</code> and <code>SDIFFCARD</code> return set-operation cardinalities <strong>without materialising the result set</strong> &mdash; the answer to &ldquo;how many, roughly&rdquo; that used to cost a full <code>SUNIONSTORE</code>.</li>
<li><code>slowlog-entry-max-argc</code> and <code>slowlog-entry-max-string-len</code> (8.8) cap what a slowlog entry retains, so a pathological command no longer bloats the slowlog itself.</li>
<li>Redis Search gains a third <code>search-on-timeout</code> mode, <code>RETURN_STRICT</code>, alongside <code>FAIL</code> and the default <code>RETURN</code> &mdash; partial results with a strict post-processing timeout, for query paths where an unbounded tail is worse than an incomplete answer.</li>
</ul>
<hr>
<h2>What Redis 8.10 does not change<a class="anchor-link" id="what-redis-8-10-does-not-change"></a></h2>
<p>Honest edges matter more than feature lists, and three limits bound every Redis 8.10 performance claim above:</p>
<ul>
<li><strong>Command execution is still effectively single-threaded.</strong> I/O threads parallelise socket work, not command execution. One <code>O(N)</code> command on a large collection still stalls every other client. Data-structure and key-design discipline is unchanged &mdash; see our notes on <a href="https://minervadb.com/optimizing-redis-for-mixed-read-write-workloads/">optimizing Redis for mixed read/write workloads</a>.</li>
<li><strong>Durability is still a configuration decision, not a default.</strong> <code>appendfsync everysec</code> still means you can lose roughly a second of writes. Redis&rsquo;s own guidance remains: run AOF <em>and</em> RDB if you want data-safety comparable to a relational engine. <code>BACKUP</code> is a backup mechanism, not a durability upgrade.</li>
<li><strong>Cluster semantics are unchanged.</strong> Cross-slot operations, resharding mechanics and hash-tag design are what they were.</li>
<li><strong>Compact hashes are an encoding, not a schema.</strong> Redis is still schemaless; the template is an internal optimisation you should measure, not a contract you can rely on.</li>
</ul>
<hr>
<h2>Redis 8.10 vs Valkey 9.1: how to read the fork<a class="anchor-link" id="redis-8-10-vs-valkey-9-1-how-to-read-the-fork"></a></h2>
<p>Both engines are shipping serious performance work, and both publish numbers on their own hardware with their own methodology. Valkey 9.1 (19 May 2026) reported 2.1M requests/sec on a single server with 512-byte payloads, 9 I/O threads and pipeline depth 10, a new I/O threading model worth up to 17% across workloads, <code>XRANGE</code>/<code>XREVRANGE</code> up to 30% faster, string memory down up to 20% for values under 128 bytes, and TLS certificate hot-reload for rotation without downtime.</p>
<p><strong>These figures are not comparable to Redis&rsquo;s.</strong> Different instance types, different payload sizes, different pipeline depths, different workload mixes. Anyone presenting a Redis-vs-Valkey throughput ratio derived from the two vendors&rsquo; blog posts is doing arithmetic on incompatible inputs.</p>
<p>What is comparable is the decision frame:</p>
<ul>
<li><strong>License.</strong> Valkey is BSD-3-Clause. Redis 8.x is tri-licensed (RSALv2 / SSPLv1 / AGPLv3). For some enterprises this decides the question before any benchmark runs.</li>
<li><strong>Feature surface.</strong> Redis 8.x ships Search, JSON, TimeSeries and Bloom as integral, same-versioned modules. Valkey addresses this through separate BSD modules (<code>valkey-search</code>, <code>valkey-json</code>, <code>valkey-bloom</code>). If you use in-engine search or vectors, this is the axis that matters.</li>
<li><strong>Portability.</strong> Nothing after 7.2.4 should be assumed portable between the two. Verify per feature, per engine, per version. Compact hashes, <code>HIMPORT</code> and the <code>BACKUP</code> family are Redis-only as of this writing.</li>
<li><strong>Managed-service reality.</strong> Your cloud provider&rsquo;s roadmap may decide this for you regardless of what you prefer.</li>
</ul>
<p>We consult on both and recommend against either when it is the wrong fit. Where vector search is the actual requirement at serious scale, compare honestly against pgvector and Milvus before committing to in-engine vectors at all.</p>
<hr>
<h2>Our upgrade stance (dated: 15 August 2026)<a class="anchor-link" id="our-upgrade-stance-dated-15-august-2026"></a></h2>
<p><strong>If you are on Redis 8.8:</strong> upgrade to 8.10 within your normal patch cadence. The ACL bypass fixes and the AOF-with-defrag load failure are the drivers; compact hashes are the bonus. Low risk &mdash; 8.8 to 8.10 is an incremental step on the same line.</p>
<p><strong>If you are on Redis 8.6 or 8.4:</strong> upgrade with priority. You are carrying five unpatched CVEs from the 8.8 cycle plus the 8.10 ACL fixes, and you are missing the entire 8.8 prefetch and replication-feed performance work.</p>
<p><strong>If you are on Redis 8.0:</strong> plan now. Security support ends 01 Dec 2026. Treat this as a scheduled project with a restore drill, not a rolling patch.</p>
<p><strong>If you are on Redis 7.2 or 7.4:</strong> the upgrade is a licensing decision as much as a technical one, because it moves you from BSD-3-Clause to the tri-license. Make that call deliberately, with counsel involved, and evaluate Valkey in the same exercise rather than defaulting.</p>
<p><strong>Wait, if:</strong> you depend on a module or client library that has not certified against 8.10; you run a managed service where the version is not yours to choose; or you cannot schedule a restore drill against the new <code>BACKUP</code> path within the change window. In the last case, upgrade anyway but do not cut over your backup procedure until the drill is done.</p>
<h3>Upgrade checklist<a class="anchor-link" id="upgrade-checklist"></a></h3>
<pre class="EnlighterJSRAW"># --- BEFORE ---
# 1. Capture the baseline. You cannot claim an improvement without one.
redis-cli INFO all &gt; /tmp/pre-upgrade-info.txt
redis-cli CONFIG GET '*'  &gt; /tmp/pre-upgrade-config.txt
redis-cli --latency-history -i 5   # run for one full traffic cycle

# 2. Verify a restore works on the CURRENT version before changing anything.
#    Restore into an isolated instance. Never into production.

# 3. Confirm replica health and backlog sizing.
redis-cli INFO replication
redis-cli INFO stats | grep -E 'sync_full|sync_partial_err'

# --- AFTER (per node, replicas first, then failover, then old primary) ---
redis-cli INFO server   | grep redis_version
redis-cli INFO stats    | grep -E 'hash_templates|hash_template_keys'
redis-cli INFO memory   | grep -E 'used_memory:|used_memory_hash_templates'
redis-cli INFO stats    | grep -E 'sync_full|sync_partial_err'
redis-cli ACL LIST

# 4. Compare against the baseline. Then, and only then, exercise BACKUP.
redis-cli BACKUP START &amp;&amp; redis-cli BACKUP STATUS</pre>
<p>Every step above is read-only or additive. Nothing here drops, truncates or flushes anything &mdash; and if you adapt these commands, keep it that way. The <code>FLUSHALL</code> in the compact-hash benchmark earlier is scoped to a scratch host on purpose; it has no business anywhere near a production endpoint.</p>
<hr>
<h2>Frequently asked questions<a class="anchor-link" id="frequently-asked-questions"></a></h2>
<p><strong>Does upgrading alone improve Redis 8.10 performance?</strong> Partly. Command-level gains from the 8.6 and 8.8 cycles are free on upgrade, and compact hashes reduce memory without application changes. But if your latency comes from an <code>O(N)</code> command, an oversized value, or an undersized replication backlog, no release fixes it &mdash; measure first.</p>
<p><strong>Is Redis 8.10 backward compatible with 8.8?</strong> Yes for the covered surface. The new commands (<code>HIMPORT</code>, <code>BACKUP</code>, <code>LMOVEM</code>, <code>BLMOVEM</code>, <code>SUNIONCARD</code>, <code>SDIFFCARD</code>) are additive, and compact hashes are an internal encoding change &mdash; application code does not change. Validate your client library&rsquo;s support for the new commands before you use them.</p>
<p><strong>Do compact hashes require me to change my application?</strong> No.&nbsp;The encoding is chosen internally. <code>HIMPORT</code> is an optional bulk-load path, useful for migrations and ETL sinks, not a replacement for <code>HSET</code>.</p>
<p><strong>Does the BACKUP command replace RDB and AOF?</strong> No.&nbsp;<code>BACKUP</code> produces a restorable artefact set from the MP-AOF format; it does not change your durability configuration. <code>appendfsync</code> and <code>save</code> still govern what you can lose.</p>
<p><strong>How much memory will compact hashes actually save me?</strong> It depends entirely on how many of your keys share a field layout. Measure it with the A/B procedure above on your own dataset. Anyone quoting you a percentage without seeing your keyspace is guessing.</p>
<p><strong>Is Redis 8.10 open source?</strong> Redis Open Source 8.x is tri-licensed: RSALv2, SSPLv1, or AGPLv3 at the user&rsquo;s choice. AGPLv3 is OSI-approved; the other two are source-available. We are not lawyers &mdash; take licence-compliance questions to counsel.</p>
<p><strong>Should I move to Valkey instead?</strong> It depends on your licence constraints, your dependence on in-engine Search/JSON/TimeSeries, and your managed-service provider&rsquo;s roadmap. Both are credible. Decide on those three axes, not on vendor throughput headlines.</p>
<hr>
<h2>Working with MinervaDB<a class="anchor-link" id="working-with-minervadb"></a></h2>
<p>MinervaDB provides vendor-neutral <a href="https://minervadb.com/redis-support/">Redis support and consulting</a> &mdash; 24&times;7 consultative support, remote DBA, performance engineering and HA/DR architecture &mdash; as part of our <a href="https://minervadb.com/enterprise-database-management/">full-stack enterprise database practice</a> across PostgreSQL, MySQL, MariaDB, SQL Server, MongoDB, ClickHouse, Cassandra, Redis, Valkey and cloud DBaaS. If you want Redis 8.10 performance and reliability validated on your own estate &mdash; the compact-hash saving measured on your keyspace and the <code>BACKUP</code> path validated with a real restore drill before you trust it, that is a scoped engagement we run regularly.</p>
<p>Further reading from our library: the <a href="https://minervadb.com/redis-troubleshooting-cheatsheet/">Redis troubleshooting cheatsheet</a> and the <a href="https://minervadb.com/advanced-redis-operations-cheatsheets/">advanced Redis operations cheatsheet</a>.</p>
<p><strong>Standing caveat:</strong> every configuration change and command in this post must be tested in a non-production environment before it reaches production, and no upgrade should proceed without a verified, drilled DR posture.</p>
<hr>
<h2>Sources<a class="anchor-link" id="sources"></a></h2>
<ul>
<li><a href="https://redis.io/docs/latest/develop/whats-new/8-10/" target="_blank" rel="noopener noreferrer">Redis 8.10 &mdash; What&rsquo;s new</a></li>
<li><a href="https://raw.githubusercontent.com/redis/redis/8.10/00-RELEASENOTES" target="_blank" rel="noopener noreferrer">Redis Open Source 8.10 release notes (00-RELEASENOTES)</a></li>
<li><a href="https://github.com/redis/redis/releases/tag/8.10.0" target="_blank" rel="noopener noreferrer">Redis 8.10.0 release on GitHub</a></li>
<li><a href="https://redis.io/docs/latest/commands/himport/" target="_blank" rel="noopener noreferrer">HIMPORT command reference</a></li>
<li><a href="https://redis.io/docs/latest/commands/backup/" target="_blank" rel="noopener noreferrer">BACKUP command reference</a></li>
<li><a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/" target="_blank" rel="noopener noreferrer">Redis persistence &mdash; online backups with the BACKUP command family</a></li>
<li><a href="https://redis.io/blog/redis-88-performance-improvements-faster-mget-mset-streams-and-more/" target="_blank" rel="noopener noreferrer">Redis 8.8 performance improvements</a></li>
<li><a href="https://redis.io/docs/latest/develop/whats-new/8-8/" target="_blank" rel="noopener noreferrer">Redis 8.8 &mdash; What&rsquo;s new</a></li>
<li><a href="https://redis.io/blog/announcing-redis-86-performance-improvements-streams/" target="_blank" rel="noopener noreferrer">Announcing Redis 8.6: performance improvements and streams enhancements</a></li>
<li><a href="https://redis.io/docs/latest/develop/whats-new/8-6/" target="_blank" rel="noopener noreferrer">Redis 8.6 &mdash; What&rsquo;s new</a></li>
<li><a href="https://redis.io/legal/licenses/" target="_blank" rel="noopener noreferrer">Redis licences (RSALv2 / SSPLv1 / AGPLv3)</a></li>
<li><a href="https://endoflife.date/redis" target="_blank" rel="noopener noreferrer">Redis release and end-of-life dates</a></li>
<li><a href="https://valkey.io/blog/valkey-9-1-delivers-improvements-in-security-performance-and-more/" target="_blank" rel="noopener noreferrer">Valkey 9.1 release announcement</a></li>
</ul>

<p><a href="https://minervadb.com/redis-8-10-performance-reliability/">Redis 8.10 Performance and Reliability: What Actually Changed</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/" />
      <id>https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/</id>
      <updated>2026-08-14T15:48:21+03:00</updated>
      <author><name>Jonathan Miller</name></author>
      <summary type="html"><![CDATA[<p>Overview<br />
While validating MariaDB RSS stability under stored procedure workloads, I ran into unexpected memory growth. The goal was straightforward: confirm that MariaDB was not leaking memory when running TPROC-C stored procedure workloads. …<br />
Continue reading \"MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration\"<br />
MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/">MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Overview<br>
While validating MariaDB RSS stability under stored procedure workloads, I ran into unexpected memory growth. The goal was straightforward: confirm that MariaDB was not leaking memory when running TPROC-C stored procedure workloads. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/">MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-foundation-advances-taf-with-hammerdb-6-0-and-xt_reservoir-integration/">MariaDB Foundation Advances TAF with HammerDB 6.0 and xt_reservoir Integration</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Community Server 10.6.28 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-community-server-10-6-28-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-community-server-10-6-28-now-available/</id>
      <updated>2026-08-13T20:30:08+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Community Server 10.6.28 maintenance release. This is the final […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-10-6-28-now-available/">MariaDB Community Server 10.6.28 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Community Server 10.6.28 maintenance release. This is the final release in the MariaDB 10.6 series. See the release notes and changelog for additional details on this release and visit mariadb.com/downloads to download.</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-10-6-28-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-community-server-10-6-28-now-available/">MariaDB Community Server 10.6.28 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Replicating from InnoDB into a DuckDB storage engine</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/" />
      <id>https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/</id>
      <updated>2026-08-13T17:23:27+03:00</updated>
      <author><name>Evgeniy Patlan</name></author>
      <summary type="html"><![CDATA[<p>Our first post showed MySQL 9.7 with one change: mark a table ENGINE=DuckDB and its analytical queries run in DuckDB instead of InnoDB. The question we kept getting after that was about replication. Can you keep a normal InnoDB primary for the writes, and run a replica where the big tables are ENGINE=DuckDB? Then the … Continued<br />
The post Replicating from InnoDB into a DuckDB storage engine appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/">Replicating from InnoDB into a DuckDB storage engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>Our first post showed MySQL 9.7 with one change: mark a table ENGINE=DuckDB and its analytical queries run in DuckDB instead of InnoDB. The question we kept getting after that was about replication. Can you keep a normal InnoDB primary for the writes, and run a replica where the big tables are ENGINE=DuckDB? Then the heavy reports run on a column store, and ordinary MySQL replication keeps it current. No export job. No second database to sync by hand.</span></p>
<p><span>So we tried it. The first run failed, and it failed in a way that is easy to miss: the replica took every transaction, reported success, and stored nothing. We tracked down why, fixed it, and the whole test suite passes now. This post is what we tested, how we checked it, the bug we found, and where it stands.</span></p>
<p><span>It&rsquo;s still an experiment, not production software. The code and the test harness are on GitHub under GPLv2: </span><a href="https://github.com/Percona-Lab/ducksdb-mysql-engine"><span>https://github.com/Percona-Lab/ducksdb-mysql-engine</span></a><span>.</span></p>
<h2><span>Why replicate into DuckDB</span><a class="anchor-link" id="why-replicate-into-duckdb"></a></h2>
<p><span>A DuckDB table on one server is already useful. The analytical queries get fast and the application does not change. But almost nobody runs their reports on the primary &ndash; they run them on a replica, so the big scans stay out of the way of the OLTP traffic.</span></p>
<p><span>So the shape of it is simple. The primary stays InnoDB and takes the writes. The replica has the same tables, only marked ENGINE=DuckDB. Row-based replication ships the changes across, the replica writes them into the column store, and the reports run there. You get an analytics replica out of the replication you already run.</span></p>
<p><span>Row events are engine-agnostic on purpose. The primary logs the row changes, not the SQL, and the replica applies them through the storage-engine API. On paper, then, the replica should not care that one side is InnoDB and the other DuckDB. We wanted to see the paper version hold up on a running server.</span></p>
<h2><span>The setup</span><a class="anchor-link" id="the-setup"></a></h2>
<p><span>Two containers from the same image, one primary and one replica. It&rsquo;s all in Docker, so it repeats cleanly.</span></p>
<ul>
<li aria-level="1"><span>Primary: InnoDB, binlog_format=ROW, GTID on.</span></li>
<li aria-level="1"><span>Replica: same server, GTID on, tables made with ENGINE=DuckDB.</span></li>
<li aria-level="1"><span>Replication uses SOURCE_AUTO_POSITION=1.</span></li>
</ul>
<p><span>One thing you have to get right before any data moves. Create the replica tables as ENGINE=DuckDB yourself. A CREATE TABLE &hellip; ENGINE=InnoDB on the primary goes into the binlog with the ENGINE word still in it, and the replica runs it exactly as written, so you would end up with an InnoDB table there, not a DuckDB one. There is no automatic mapping. Pre-create the DuckDB tables on the replica, and let the row changes flow into them.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">-- primary (InnoDB)
CREATE TABLE t1 (id BIGINT PRIMARY KEY, region INT, amount DECIMAL(12,2)) ENGINE=InnoDB;

-- replica (same columns, DuckDB)
CREATE TABLE t1 (id BIGINT PRIMARY KEY, region INT, amount DECIMAL(12,2)) ENGINE=DuckDB;</pre>
<p><span>The other rule is a primary key on the replica table. UPDATE and DELETE row events find the row by its old image, and the engine needs the key for that. INSERT works without one, but put a key on it anyway.</span></p>
<p><span>One script drives all of this: bench/tb/07-replication-spike.sh. It starts both containers, wires up replication, runs every scenario below, and prints PASS or FAIL for each.</span></p>
<h2><span>What we tested, and how</span><a class="anchor-link" id="what-we-tested-and-how"></a></h2>
<p><span>The part that matters is the checking. Row counts are not enough &ndash; the replica can hold the right number of rows and still have the wrong data in them. So after each step the script dumps the whole table on both sides, ordered by primary key, and compares an md5 of the two dumps. One byte off is a FAIL. And rather than sleep between steps, it waits on WAIT_FOR_EXECUTED_GTID_SET(), so the checks do not race the replica.</span></p>
<p><span>Here is what went through it.</span></p>
<p><span>Basic DML. Insert, update a row, delete a row, compared after each one.</span></p>
<p><span>All the column types, in a single wide table: signed and unsigned integers, DECIMAL, DOUBLE, DATE, DATETIME, TIMESTAMP, CHAR, VARCHAR, TEXT, BLOB, a few NULLs, and a unicode string. Insert it, update it, compare byte for byte. Blobs get their own note below.</span></p>
<p><span>DDL. ALTER TABLE ADD COLUMN, ALTER TABLE ADD INDEX, and DROP TABLE against a DuckDB replica table. These arrive as statements. We check that the column shows up, the index shows up, the old rows survive, and the drop removes the table.</span></p>
<p><span>Transactions. A transaction with two inserts and an update has to land on the replica as one unit. A transaction the primary rolls back has to leave nothing behind. We also open a transaction straight on the replica and both roll it back and commit it, to check the engine&rsquo;s own commit and rollback.</span></p>
<p><span>Bulk load. 5000 rows through LOAD DATA on the primary, has to arrive and match.</span></p>
<p><span>Durability. Two cases, and the second is the hard one.</span></p>
<ul>
<li aria-level="1"><span>Clean restart. Stop the replica properly, write on the primary while it is down, start it again, and see it pick up from its GTID position.</span></li>
</ul>
<ul>
<li aria-level="1"><span>Crash. Apply some rows, then SIGKILL the replica. No clean shutdown, no checkpoint. Bring it back, write more on the primary, and check one exact thing: every row present once. Nothing lost &ndash; DuckDB has to replay its write-ahead log when it opens the file &ndash; and nothing applied twice, which means the saved position has to line up with the data that actually reached disk.</span></li>
</ul>
<h2><span>The bug: multi-engine transactions lost data</span><a class="anchor-link" id="the-bug-multi-engine-transactions-lost-data"></a></h2>
<p><span>The first full run fell down on the wide-table test. Zero rows on the replica, and then everything after it failed too. The applier had stopped with HA_ERR_KEY_NOT_FOUND. It went to UPDATE a row that was not there, because the INSERT before it had returned success and written nothing.</span></p>
<p><span>When a scenario fails, the harness saves the applier error, both server logs, and both schemas. The replica log had the line that mattered:</span></p>
<p><span>[Warning] Combining the storage engines InnoDB and DuckDB is deprecated, but the</span><span><br>
</span><span>statement or transaction updates both the InnoDB table mysql.slave_worker_info and the</span><span><br>
</span><span>DuckDB table rpl.wide.</span></p>
<p><span>That line is the whole thing. A replica does not only write your data. In the same transaction it also writes its own position into InnoDB system tables &ndash; mysql.slave_worker_info, the relay-log info, gtid_executed. So every applied transaction touches two engines at once: InnoDB for the position, DuckDB for the data. Two engines means MySQL runs a real two-phase commit: prepare, then commit.</span></p>
<p><span>Our prepare was wrong. It took the open DuckDB transaction, moved it into a registry meant for external XA COMMIT, and cleared the per-connection state. Then commit looked at that state, found it empty, and committed nothing. The position went into InnoDB, the GTID advanced, the binlog moved on, and the DuckDB rows were thrown away. No error anywhere. The replica looked healthy while it dropped every write.</span></p>
<p><span>We cut it down to the smallest case, with no replication at all. One server, one transaction into a DuckDB table and an InnoDB table:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">BEGIN;
INSERT INTO duck VALUES (1,10),(2,20),(3,30); &nbsp; -- DuckDB
INSERT INTO inno VALUES (1,10),(2,20),(3,30); &nbsp; -- InnoDB
COMMIT;
-- duck: 0 rows &nbsp; inno: 3 rows</pre>
<p><span>InnoDB kept its three rows, DuckDB kept none, and COMMIT said it was fine. A DuckDB-only transaction was fine as well, because with one engine MySQL skips the prepare step. It only broke with a second engine in the transaction. And on a replica, that is every transaction.</span></p>
<h2><span>The fix</span><a class="anchor-link" id="the-fix"></a></h2>
<p><span>Small change, in the engine&rsquo;s transaction code. prepare now remembers which prepared transaction belongs to the connection, and commit finishes that one instead of an empty state. External XA is untouched. It went out as v0.2.3.</span></p>
<p><span>With that in place the reproducer keeps three rows in both tables, and the full run comes back clean, crash test included:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">[8]&nbsp; data integrity: all column types, NULL / unicode / negatives ....... PASS
[9]&nbsp; DDL replication (ALTER ADD COLUMN / ADD INDEX / DROP) .............. PASS
[10] transactions (atomic commit, rollback, engine commit/rollback) ..... PASS
[11] bulk LOAD DATA on master -&gt; replica ................................ PASS
[12] durability: graceful restart, then SIGKILL crash recovery .......... PASS

VERDICT: PASS=24&nbsp; FAIL=0</pre>
<p><span>The crash case is the important one. After a SIGKILL in the middle of applying, the replica came back with every committed row exactly once, matching the primary. Committed transactions survive the kill, and the position stays in step with them.</span></p>
<p><span>We left two tests behind so this cannot slip back in quietly: an MTR test, txn_mixed_engine, that runs a mixed DuckDB+InnoDB transaction on every build, and scripts/repro-2pc-dataloss.sh, which you can point at any published image to check it.</span></p>
<h2><span>What works, and what doesn&rsquo;t yet</span><a class="anchor-link" id="what-works-and-what-doesnt-yet"></a></h2>
<p><span>Where it stands on v0.2.3, for an InnoDB primary feeding a DuckDB replica:</span></p>
<table>
<thead>
<tr>
<th><span>Scenario</span></th>
<th><span>Result</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><span>INSERT / UPDATE / DELETE</span></td>
<td><span>works, content matches</span></td>
</tr>
<tr>
<td><span>All column types (numeric, temporal, string, BLOB, NULL, unicode)</span></td>
<td><span>works</span></td>
</tr>
<tr>
<td><span>ALTER ADD COLUMN / ADD INDEX, DROP TABLE</span></td>
<td><span>works</span></td>
</tr>
<tr>
<td><span>Transaction commit / rollback</span></td>
<td><span>works, atomic</span></td>
</tr>
<tr>
<td><span>Bulk LOAD DATA</span></td>
<td><span>works</span></td>
</tr>
<tr>
<td><span>Graceful restart, resume from GTID</span></td>
<td><span>works</span></td>
</tr>
<tr>
<td><span>SIGKILL crash, no loss / no duplicates</span></td>
<td><span>works</span></td>
</tr>
</tbody>
</table>
<p><span>The things to keep in mind:</span></p>
<ul>
<li aria-level="1"><span>Create the replica tables as ENGINE=DuckDB yourself. A replicated CREATE TABLE keeps the primary&rsquo;s engine, so it will not turn into DuckDB on its own.</span></li>
</ul>
<ul>
<li aria-level="1"><span>Replica tables need a primary key for UPDATE and DELETE.</span></li>
</ul>
<ul>
<li aria-level="1"><span>The applier goes row by row. That is fine for a normal OLTP change stream. It is not fine for keeping up with a primary that bulk-loads at full speed &ndash; the replica will fall behind.</span></li>
</ul>
<ul>
<li aria-level="1"><span>Committed transactions are crash-safe, with one small gap. The engine holds a prepared-but-not-committed transaction in memory only, so a crash in the short window between prepare and commit can lose that single transaction. The applier commits right away, so the window is small, but it is not zero.</span></li>
</ul>
<ul>
<li aria-level="1"><span>Blobs behave differently over replication than through a direct statement. A plain UPDATE of a BLOB or TEXT column has a known limit in the engine and does not apply. Over replication it does apply, because the row event carries a full before-and-after image instead of the shared buffer the direct path uses.</span></li>
</ul>
<p><span>And the obvious one. This is an experiment. It is a functional result from a test harness on small data, not an HA or failover benchmark. We did not test multi-source replication, filters, or a real write rate.</span></p>
<h2><span>Where it stands</span><a class="anchor-link" id="where-it-stands"></a></h2>
<p><span>An InnoDB primary feeding a DuckDB replica works on v0.2.3. Inserts, updates, deletes, every common type, schema changes, transactions, bulk load &ndash; they all replicate and match, and it comes back clean from both a graceful restart and a hard kill. The one real bug, silent data loss on every replicated transaction, is found, understood, fixed, and covered by tests.</span></p>
<p><span>It is not production-ready, and we do not treat it as such. But the idea holds up. Point normal MySQL replication at a DuckDB replica, and you get an analytics copy that keeps itself in sync.</span></p>
<p>The post <a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/">Replicating from InnoDB into a DuckDB storage engine</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/replicating-from-innodb-into-a-duckdb-storage-engine/">Replicating from InnoDB into a DuckDB storage engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/" />
      <id>https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/</id>
      <updated>2026-08-13T09:45:51+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>JSON support in MariaDB has improved significantly over the years.<br />
We have functions to create JSON documents, extract values, modify objects, inspect arrays, compare documents, and even transform JSON into relational rows using JSON_TABLE(). …<br />
Continue reading \"MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements\"<br />
MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/">MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>JSON support in MariaDB has improved significantly over the years.<br>
We have functions to create JSON documents, extract values, modify objects, inspect arrays, compare documents, and even transform JSON into relational rows using <a href="https://mariadb.com/docs/server/reference/sql-functions/special-functions/json-functions/json_table">JSON_TABLE()</a>. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/">MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-json-operators-and-json_table-improvements/">MariaDB 13.1 Feature in Focus: JSON Operators and JSON_TABLE Improvements</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona for MongoDB: RHEL 10, Its Derivatives, and Debian 13 – On Both x86_64 and ARM</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-for-mongodb-rhel-10-its-derivatives-and-debian-13-on-both-x86_64-and-arm/" />
      <id>https://www.percona.com/blog/percona-for-mongodb-rhel-10-its-derivatives-and-debian-13-on-both-x86_64-and-arm/</id>
      <updated>2026-08-11T12:15:41+03:00</updated>
      <author><name>Radoslaw Szulgo</name></author>
      <summary type="html"><![CDATA[<p>We’re happy to announce that Percona Server for MongoDB (PSMDB) 8.0.28-12 extends platform support to RHEL 10 and its derivatives (Oracle Linux 10, Rocky Linux 10, AlmaLinux 10, and other RHEL-compatible distributions) for both x86_64 and ARM (aarch64) architectures. This release also adds support for Debian 13 “Trixie” on x86_64 and ARM64. We’ll continue to … Continued<br />
The post Percona for MongoDB: RHEL 10, Its Derivatives, and Debian 13 – On Both x86_64 and ARM appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-for-mongodb-rhel-10-its-derivatives-and-debian-13-on-both-x86_64-and-arm/">Percona for MongoDB: RHEL 10, Its Derivatives, and Debian 13 – On Both x86_64 and ARM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>We&rsquo;re happy to announce that </span><b>Percona Server for MongoDB (PSMDB) 8.0.28-12</b><span> extends platform support to </span><b>RHEL 10 and its derivatives</b><span> (Oracle Linux 10, Rocky Linux 10, AlmaLinux 10, and other RHEL-compatible distributions) for both x86_64 and ARM (aarch64) architectures. This release also adds support for </span><b>Debian 13 &ldquo;Trixie&rdquo;</b><span> on x86_64 and ARM64. We&rsquo;ll continue to support that for 8.0, 8.3, and newer releases.</span></p>
<p><span>This is an important step for anyone planning infrastructure refreshes around the latest Linux releases, and it&rsquo;s especially notable for teams evaluating ARM to cut infrastructure spend without sacrificing performance.</span></p>
<h2><span>What&rsquo;s new in 8.0.28-12</span><a class="anchor-link" id="whats-new-in-8-0-28-12"></a></h2>
<p><span>Starting with this release, Percona Server for MongoDB packages are available for:</span></p>
<ul>
<li aria-level="1"><span>RHEL 10 and derivatives like Oracle Linux 10, Rocky Linux 10, AlmaLinux 10 on x86_64 and ARM (aarch64)</span></li>
<li aria-level="1"><span>Debian 13 &ldquo;Trixie&rdquo; on x86_64 and ARM64 (aarch64)</span></li>
</ul>
<p><span>Additionally, starting this release, we&rsquo;ve included Software Bills of Materials (SBOMs) and Vulnerability Exploitability Exchange (VEX) for every release. SBOMs improve software supply chain transparency by documenting the components and dependencies included in a build. They are generated automatically as part of the release pipeline in the industry-standard </span><a href="https://cyclonedx.org/specification/overview/"><span>CycloneDX</span></a><span> format. OpenVEX files are published on GitHub Pages and provide the exploitability status of known vulnerabilities. For comprehensive information, refer to our [documentation](../</span><a href="http://sbom.md/"><span>sbom.md</span></a><span>).</span></p>
<p><span>To learn more, see the full </span><a href="https://docs.percona.com/percona-server-for-mongodb/8.0/release_notes/8.0.28-12.html"><span>release notes of Percona Server for MongoDB 8.0.28-12</span></a><span>.</span></p>
<h2><span>Ahead of upstream on Debian 13</span><a class="anchor-link" id="ahead-of-upstream-on-debian-13"></a></h2>
<p><span>As of this writing (August 2026), </span><b>upstream MongoDB Community/Enterprise Server does not yet officially package or support Debian 13</b><span>. Trixie isn&rsquo;t in MongoDB&rsquo;s supported platforms list, and the documented community workaround is to install the Debian 12 &ldquo;Bookworm&rdquo; build on Trixie hosts, since a native Trixie server build hasn&rsquo;t landed yet. PSMDB closes that gap now, with native Debian 13 packages rather than a buggy Bookworm build running out-of-distro.</span></p>
<h2><span>Why ARM is worth a serious look for MongoDB workloads</span><a class="anchor-link" id="why-arm-is-worth-a-serious-look-for-mongodb-workloads"></a></h2>
<p><span>We have heard multiple times from you directly, via our forum, or on Reddit about the Interest in ARM for database workloads. Over the last few years, adoption has moved well past the experimental phase to resilient production readiness. Our adoption telemetry data show nearly 3x as many ARM instances over the last 12 months!</span></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm.png"><img loading="lazy" decoding="async" class="aligncenter wp-image-51629 size-2048x2048" src="https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-2048x673.png" alt="" width="2048" height="673" srcset="https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-2048x673.png 2048w, https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-300x99.png 300w, https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-1024x336.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-768x252.png 768w, https://www.percona.com/wp-content/uploads/2026/08/pmm-adoption-arm-1536x505.png 1536w" sizes="auto, (max-width: 2048px) 100vw, 2048px"></a></p>
<p><span>I can see a number of benefits and reasons why our community users and customers adopted ARM over AMD or Intel CPU architectures:</span></p>
<ul>
<li aria-level="1"><b>Lower infrastructure costs.</b><span> Cloud ARM instances, such as AWS Graviton, are commonly cited as running 20&ndash;40% cheaper than comparable x86 instances at similar or better performance (</span><a href="https://sanj.dev/post/arm-vs-x86-cloud-2025/"><span>sanj.dev, &ldquo;ARM vs x86 Cloud: Which Architecture is Cheaper in 2025?&rdquo;</span></a><span>), and benchmarking write-ups report up to 60% lower energy consumption for compute-intensive workloads on Graviton compared to x86 equivalents (</span><a href="https://www.nops.io/blog/are-you-missing-out-on-aws-graviton-cost-savings/"><span>nOps, &ldquo;Are you missing out on AWS Graviton Cost Savings?&rdquo;</span></a><span>).</span></li>
<li aria-level="1"><b>Competitive, and often better, throughput.</b> <a href="https://www.usage.ai/blogs/aws/reserved-instances/rds/postgresql/graviton-instances/"><span>RDS PostgreSQL Graviton&rdquo; benchmark analysis</span></a><span> shows Graviton4 delivering up to 40% better performance for OLTP-style workloads versus the previous Graviton3 generation. </span><a href="https://www.velodb.io/blog/apache-doris-achieves-70-better-price-performance"><span>Apache Doris Delivers 70% Better Value on AWS Graviton</span></a><span>. I can see MongoDB database achieving a similar level of performance-to-cost gain.&nbsp;</span></li>
<li aria-level="1"><b>Memory bandwidth is a real differentiator.</b><span> Graviton3&rsquo;s memory bandwidth (cited at roughly 115&ndash;120 GB/s) is reported to significantly outpace typical Intel Xeon configurations (roughly 60&ndash;70 GB/s) and AMD EPYC (roughly 80&ndash;90 GB/s) in independent comparisons (</span><a href="https://byteiota.com/arm-vs-x86-cloud-2025-performance-cost-benchmark/"><span>byteiota, &ldquo;ARM vs x86 Cloud: 2025 Performance &amp; Cost Benchmark&rdquo;</span></a><span>), which matters for memory-hungry workloads like MongoDB&rsquo;s WiredTiger cache and in-memory working sets.</span></li>
</ul>
<p><i><span>(Note: The above figures come from third-party blogs and vendor case studies rather than peer-reviewed benchmarks. Treat them as directional evidence that ARM is worth evaluating, not a guarantee of results for your specific workload. For the official recommendation based on your workload, reach out to Percona)</span></i></p>
<p><span>Netflix has publicly stated that it saves over $15 million annually after migrating video encoding workloads to Graviton, while also seeing faster processing times, and other large-scale AWS customers have reported double-digit percentage reductions in compute costs after moving meaningful portions of their backend fleets to ARM (</span><a href="https://byteiota.com/arm-vs-x86-cloud-2025-performance-cost-benchmark/"><span>byteiota</span></a><span>; </span><a href="https://sanj.dev/post/arm-vs-x86-cloud-2025/"><span>sanj.dev</span></a><span>).</span></p>
<h2><span>What to watch out for</span><a class="anchor-link" id="what-to-watch-out-for"></a></h2>
<p><span>One RHEL 10 detail to keep in mind when planning a migration: Red Hat raised the CPU baseline for x86_64 to the </span><b>x86-64-v3</b><span> microarchitecture level, meaning the processor needs to support instruction sets such as AVX2 (</span><a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/considerations_in_adopting_rhel_10/architectures"><span>Red Hat, RHEL 10 architecture documentation</span></a><span>; </span><a href="https://vinfrastructure.it/2025/05/red-hat-enterprise-linux-10-0/"><span>vInfrastructure Blog</span></a><span>). On the ARM side, RHEL 10 targets the ARMv8.0-A baseline (</span><a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/considerations_in_adopting_rhel_10/architectures"><span>Red Hat documentation</span></a><span>). This applies equally to Oracle Linux 10, Rocky Linux 10, and AlmaLinux 10, since they build from the same upstream sources. I highly recommend checking your current infrastructure before planning the move, especially for older bare-metal fleets.</span></p>
<p><span>In general, but especially on ARM, remember that performance is workload-dependent. Some code paths and workloads leaning on x64-specific instruction extensions may not see the same gains as throughput-oriented, multi-threaded workloads on ARM. Benchmarking your own query patterns and index-heavy operations before a full cutover is essential. General benchmarks are a good signal, not a guarantee.</span></p>
<h2><span>Percona can help you get there</span><a class="anchor-link" id="percona-can-help-you-get-there"></a></h2>
<p><span>Migrating a production MongoDB deployment to a different infrastructure is a project with real decision points. There are a number of questions to answer around: Hardware or instance selection, driver and tooling compatibility, benchmarking against your actual workload, and a rollback plan. The Percona Services team helps customers plan and execute exactly this kind of migration. We start from initial architecture assessment and proof-of-concept benchmarking and go through to production cutover and post-migration tuning.</span></p>
<p><span>If you&rsquo;re weighing a move to ARM, or just want to get onto RHEL 10 or Debian 13 without surprises, </span><a href="https://www.percona.com/about/contact"><span>reach out to Percona</span></a><span> to talk through your environment.</span></p>
<p>The post <a href="https://www.percona.com/blog/percona-for-mongodb-rhel-10-its-derivatives-and-debian-13-on-both-x86_64-and-arm/">Percona for MongoDB: RHEL 10, Its Derivatives, and Debian 13 &ndash; On Both x86_64 and ARM</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-for-mongodb-rhel-10-its-derivatives-and-debian-13-on-both-x86_64-and-arm/">Percona for MongoDB: RHEL 10, Its Derivatives, and Debian 13 – On Both x86_64 and ARM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Making the PostgreSQL Documentation Even Better</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/postgresql/improve-postgresql-documentation/" />
      <id>https://www.fromdual.com/blog/postgresql/improve-postgresql-documentation/</id>
      <updated>2026-08-11T06:40:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>While working on our project “PostgreSQL for Dolphins and Sea Lions,” I pored over the PostgreSQL documentation on replication.<br />
Since I’m not quite up to speed on this topic yet, I like to look up certain terms (parameters, functions, etc.) every now and then to see exactly what they mean or how they work (RTFM!). That’s exactly why links were originally invented—the very thing that first made Gopher and later the Internet/WWW (http) so popular.<br />
Unfortunately, however, these links are often missing from the documentation in question, which disrupts the flow of reading.<br />
Fortunately, though, PostgreSQL is an open-source project, and contributions are highly encouraged! So instead of just grumbling about the documentation, I could add the missing links myself. But how exactly do I go about doing that in an ecosystem that’s new to me and therefore still a bit unfamiliar? An article by Elizabeth Christensen from Crunchy Data titled Contributing to Postgres 101: A Beginner’s Experience helped me get started.<br />
Since I have absolutely no programming experience myself, I see improving the documentation as a great opportunity to actively contribute to the project and help out…<br />
Improving the PostgreSQL Documentation<br />
The PostgreSQL documentation is stored directly in the server repository. So, first, let’s download the Git repository from the PostgreSQL server:<br />
$ git clone http://git.postgresql.org/git/postgresql.git<br />
The next challenge is finding the right file:<br />
$ cd postgresql/doc/src/sgml<br />
The grep command, in all its forms, comes in handy here:<br />
$ grep -r \'Planning for High Availability\' *.sgml<br />
high-availability.sgml: Planning for High Availability<br />
The correct document appears to be high-availability.sgml. The PostgreSQL documentation itself is written in SGML, which is similar to HTML and not particularly difficult to learn.<br />
These SGML files can be easily read and edited using your editor of choice with appropriate code highlighting.<br />
Next, we need to check the individual keywords to see if they’ve already been correctly marked up and, if so, add links to them:</p>
<p>					Keyword<br />
					Markup<br />
					Links</p>
<p>					synchronous_standby_names<br />
					synchronous_standby_names</p>
<p>					archive_command<br />
					archive_command</p>
<p>					archive_library<br />
					archive_library</p>
<p>					synchronous_commit<br />
					synchronous_commit</p>
<p>					pg_receivewal<br />
					pg_receivewal</p>
<p>					pg_recvlogical<br />
					pg_recvlogical</p>
<p>					pg_backup_stop<br />
					pg_backup_stop()<br />
					pg_backup_stop()</p>
<p>					pg_backup_start<br />
					pg_backup_start()<br />
					pg_backup_start()</p>
<p>					pg_switch_wal<br />
					pg_switch_wal()<br />
					pg_switch_wal()</p>
<p>Note: Keep in mind that keywords are written with an “_” (underscore) and links with a “-” (hyphen).<br />
While building the documentation, it was also noticed that some link targets (id) hadn’t been set at all, so these had to be adjusted as well:</p>
<p>-<br />
+ </p>
<p> pg_backup_start<br />
Quality Assurance<br />
Once all changes have been made, it’s time for quality control. To do this, build the documentation locally:<br />
$ cd postgresql<br />
$ ./configure<br />
$ cd doc<br />
$ make<br />
Exact details on how this works are described here.<br />
If the build finds any errors, they will be displayed and the build will be aborted. If everything runs smoothly, you can now use your browser of choice to check whether everything actually works as intended:<br />
$ firefox src/sgml/html/warm-standby.html<br />
Something else I discovered later:</p>
<p>Building the documentation can take very long. But there is a method to just check the correct syntax of the documentation files, which only takes a few seconds: [ 5 ]</p>
<p>$ make check<br />
make -C ../src/backend generated-headers<br />
make[1]: Entering directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend\'<br />
make -C ../include/catalog generated-headers<br />
make[2]: Entering directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/include/catalog\'<br />
make[2]: Nothing to be done for \'generated-headers\'.<br />
make[2]: Leaving directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/include/catalog\'<br />
make -C nodes generated-header-symlinks<br />
make[2]: Entering directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/nodes\'<br />
make[2]: Nothing to be done for \'generated-header-symlinks\'.<br />
make[2]: Leaving directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/nodes\'<br />
make -C utils generated-header-symlinks<br />
make[2]: Entering directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils\'<br />
make -C adt jsonpath_gram.h<br />
make[3]: Entering directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils/adt\'<br />
make[3]: \'jsonpath_gram.h\' is up to date.<br />
make[3]: Leaving directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils/adt\'<br />
make[2]: Leaving directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils\'<br />
make[1]: Leaving directory \'/home/oli/fromdual/postgresql/docu/postgresql/src/backend\'<br />
rm -rf \'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install<br />
/usr/bin/mkdir -p \'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/log<br />
make -C \'..\' DESTDIR=\'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install install >\'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/log/install.log 2 >&#038;1<br />
make -j1 checkprep > >\'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/log/install.log 2 >&#038;1<br />
PATH=\"/home/oli/fromdual/postgresql/docu/postgresql/tmp_install/usr/local/pgsql/bin:/home/oli/fromdual/postgresql/docu/postgresql/doc:$PATH\" LD_LIBRARY_PATH=\"/home/oli/fromdual/postgresql/docu/postgresql/tmp_install/usr/local/pgsql/lib:$LD_LIBRARY_PATH\" INITDB_TEMPLATE=\'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/initdb-template initdb --auth trust --no-sync --no-instructions --lc-messages=C --no-clean \'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/initdb-template > >\'/home/oli/fromdual/postgresql/docu/postgresql\'/tmp_install/log/initdb-template.log 2 >&#038;1<br />
Submitting the Patch<br />
If everything works as intended and to your satisfaction, you can then proceed to create the patch and submit it:<br />
$ git commit -m \'some references on variables and functions added\'<br />
$ git format-patch -1 HEAD<br />
This creates a file containing the commit comment: 0001-some-references-on-variables-and-functions-added.patch.<br />
Apparently, in the PostgreSQL project, you don’t create a merge request to incorporate the patch back into the source code; instead, the patch must be sent to the appropriate mailing list and then merged into the main branch by a developer with merge/commit privileges. I’ve now agreed with “my” committer that we’ll hold the discussion about my patch on the pgsql-docs mailing list.<br />
Let’s see how things go from here and how far I get with my patch…<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/postgresql/improve-postgresql-documentation/">Making the PostgreSQL Documentation Even Better</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>While working on our project &ldquo;PostgreSQL for Dolphins and Sea Lions,&rdquo; I pored over the PostgreSQL documentation on <a href="https://www.postgresql.org/docs/current/warm-standby.html" target="_blank" title="Log-Shipping Standby Servers">replication</a>.</p>
<p>Since I&rsquo;m not quite up to speed on this topic yet, I like to look up certain terms (parameters, functions, etc.) every now and then to see exactly what they mean or how they work (<a href="https://en.wikipedia.org/wiki/RTFM" target="_blank">RTFM</a>!). That&rsquo;s exactly why <strong>links</strong> were originally invented&mdash;the very thing that first made <a href="https://en.wikipedia.org/wiki/Gopher_(protocol)" target="_blank">Gopher</a> and later the Internet/WWW (http) so popular.</p>
<p>Unfortunately, however, these links are often missing from the documentation in question, which disrupts the flow of reading.</p>
<p>Fortunately, though, PostgreSQL is an open-source project, and contributions are highly encouraged! So instead of just grumbling about the documentation, I could add the missing links myself. But how exactly do I go about doing that in an ecosystem that&rsquo;s new to me and therefore still a bit unfamiliar? An article by Elizabeth Christensen from Crunchy Data titled <a href="https://www.crunchydata.com/blog/contributing-to-postgres-101-a-beginners-experience" target="_blank">Contributing to Postgres 101: A Beginner&rsquo;s Experience</a> helped me get started.</p>
<p>Since I have absolutely no programming experience myself, I see improving the documentation as a great opportunity to actively contribute to the project and help out&hellip;</p>
<h2 id="improving-the-postgresql-documentation">Improving the PostgreSQL Documentation<a class="anchor-link" id="improving-the-postgresql-documentation"></a></h2>
<p>The PostgreSQL documentation is stored directly in the server repository. So, first, let&rsquo;s download the Git repository from the PostgreSQL server:</p>
<pre><code>$ git clone http://git.postgresql.org/git/postgresql.git
</code></pre>
<p>The next challenge is finding the right file:</p>
<pre><code>$ cd postgresql/doc/src/sgml
</code></pre>
<p>The <code>grep</code> command, in all its forms, comes in handy here:</p>
<pre><code>$ grep -r 'Planning for High Availability' *.sgml
high-availability.sgml: &lt;title&gt;Planning for High Availability&lt;/title&gt;
</code></pre>
<p>The correct document appears to be <code>high-availability.sgml</code>. The PostgreSQL documentation itself is written in <a href="https://en.wikipedia.org/wiki/Standard_Generalized_Markup_Language" target="_blank">SGML</a>, which is similar to HTML and not particularly difficult to learn.</p>
<p>These SGML files can be easily read and edited using your editor of choice with appropriate code highlighting.</p>
<p>Next, we need to check the individual keywords to see if they&rsquo;ve already been correctly marked up and, if so, add links to them:</p>
<table>
<thead>
<tr>
<th>Keyword</th>
<th>Markup</th>
<th>Links</th>
</tr>
</thead>
<tbody>
<tr>
<td>synchronous_standby_names</td>
<td><b>&lt;varname&gt;</b>synchronous_standby_names<b>&lt;/varname&gt;</b></td>
<td>&lt;xref linkend="<strong>guc-synchronous-standby-names</strong>"/&gt;</td>
</tr>
<tr>
<td>archive_command</td>
<td><b>&lt;varname&gt;</b>archive_command<b>&lt;/varname&gt;</b></td>
<td>&lt;xref linkend="<strong>guc-archive-command</strong>"/&gt;</td>
</tr>
<tr>
<td>archive_library</td>
<td><b>&lt;varname&gt;</b>archive_library<b>&lt;/varname&gt;</b></td>
<td>&lt;xref linkend="<strong>guc-archive-library</strong>"/&gt;</td>
</tr>
<tr>
<td>synchronous_commit</td>
<td><b>&lt;varname&gt;</b>synchronous_commit<b>&lt;/varname&gt;</b></td>
<td>&lt;xref linkend="<strong>guc-synchronous-commit</strong>"/&gt;</td>
</tr>
<tr>
<td>pg_receivewal</td>
<td><b>&lt;command&gt;</b>pg_receivewal<b>&lt;/command&gt;</b></td>
<td>&lt;xref linkend="<strong>app-pgreceivewal</strong>"/&gt;</td>
</tr>
<tr>
<td>pg_recvlogical</td>
<td><b>&lt;command&gt;</b>pg_recvlogical<b>&lt;/command&gt;</b></td>
<td>&lt;xref linkend="<strong>app-pgrecvlogical</strong>"/&gt;</td>
</tr>
<tr>
<td>pg_backup_stop</td>
<td><b>&lt;function&gt;</b>pg_backup_stop()<b>&lt;/function&gt;</b></td>
<td><b>&lt;link linkend=&ldquo;pg-backup-stop&rdquo;&gt;</b>&lt;function&gt;pg_backup_stop()&lt;/function&gt;<b>&lt;/link&gt;</b></td>
</tr>
<tr>
<td>pg_backup_start</td>
<td><b>&lt;function&gt;</b>pg_backup_start()<b>&lt;/function&gt;</b></td>
<td><b>&lt;link linkend=&ldquo;pg-backup-start&rdquo;&gt;</b>&lt;function&gt;pg_backup_start()&lt;/function&gt;<b>&lt;/link&gt;</b></td>
</tr>
<tr>
<td>pg_switch_wal</td>
<td><b>&lt;function&gt;</b>pg_switch_wal()<b>&lt;/function&gt;</b></td>
<td><b>&lt;link linkend=&ldquo;pg_switch_wal&rdquo;&gt;</b>&lt;function&gt;pg_switch_wal()&lt;/function&gt;<b>&lt;/link&gt;</b></td>
</tr>
</tbody>
</table>
<p><strong>Note</strong>: Keep in mind that keywords are written with an &ldquo;_&rdquo; (underscore) and links with a &ldquo;-&rdquo; (hyphen).</p>
<p>While building the documentation, it was also noticed that some link targets (<code>id</code>) hadn&rsquo;t been set at all, so these had to be adjusted as well:</p>
<pre><code> &lt;row&gt;
- &lt;entry role="func_table_entry"&gt;&lt;para role="func_signature"&gt;
+ &lt;entry id="pg-backup-start" role="func_table_entry"&gt;&lt;para role="func_signature"&gt;
 &lt;indexterm&gt;
 &lt;primary&gt;pg_backup_start&lt;/primary&gt;
</code></pre>
<h2 id="quality-assurance">Quality Assurance<a class="anchor-link" id="quality-assurance"></a></h2>
<p>Once all changes have been made, it&rsquo;s time for quality control. To do this, build the documentation locally:</p>
<pre><code>$ cd postgresql
$ ./configure
$ cd doc
$ make
</code></pre>
<p>Exact details on how this works are described <a href="https://www.postgresql.org/docs/18/docguide-build.html" target="_blank" title="Building the Documentation with Make">here</a>.</p>
<p>If the build finds any errors, they will be displayed and the build will be aborted. If everything runs smoothly, you can now use your browser of choice to check whether everything actually works as intended:</p>
<pre><code>$ firefox src/sgml/html/warm-standby.html
</code></pre>
<p>Something else I discovered later:</p>
<blockquote>
<p>Building the documentation can take very long. But there is a method to just check the correct syntax of the documentation files, which only takes a few seconds: [ <a href="https://www.postgresql.org/docs/18/docguide-build.html#DOCGUIDE-BUILD-SYNTAX-CHECK" target="_blank" title="Syntax Check">5</a> ]</p>
</blockquote>
<pre><code>$ make check
make -C ../src/backend generated-headers
make[1]: Entering directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend'
make -C ../include/catalog generated-headers
make[2]: Entering directory '/home/oli/fromdual/postgresql/docu/postgresql/src/include/catalog'
make[2]: Nothing to be done for 'generated-headers'.
make[2]: Leaving directory '/home/oli/fromdual/postgresql/docu/postgresql/src/include/catalog'
make -C nodes generated-header-symlinks
make[2]: Entering directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/nodes'
make[2]: Nothing to be done for 'generated-header-symlinks'.
make[2]: Leaving directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/nodes'
make -C utils generated-header-symlinks
make[2]: Entering directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils'
make -C adt jsonpath_gram.h
make[3]: Entering directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils/adt'
make[3]: 'jsonpath_gram.h' is up to date.
make[3]: Leaving directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils/adt'
make[2]: Leaving directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend/utils'
make[1]: Leaving directory '/home/oli/fromdual/postgresql/docu/postgresql/src/backend'
rm -rf '/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install
/usr/bin/mkdir -p '/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/log
make -C '..' DESTDIR='/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install install &gt;'/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/log/install.log 2&gt;&amp;1
make -j1 checkprep &gt;&gt;'/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/log/install.log 2&gt;&amp;1
PATH="/home/oli/fromdual/postgresql/docu/postgresql/tmp_install/usr/local/pgsql/bin:/home/oli/fromdual/postgresql/docu/postgresql/doc:$PATH" LD_LIBRARY_PATH="/home/oli/fromdual/postgresql/docu/postgresql/tmp_install/usr/local/pgsql/lib:$LD_LIBRARY_PATH" INITDB_TEMPLATE='/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/initdb-template initdb --auth trust --no-sync --no-instructions --lc-messages=C --no-clean '/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/initdb-template &gt;&gt;'/home/oli/fromdual/postgresql/docu/postgresql'/tmp_install/log/initdb-template.log 2&gt;&amp;1
</code></pre>
<h2 id="submitting-the-patch">Submitting the Patch<a class="anchor-link" id="submitting-the-patch"></a></h2>
<p>If everything works as intended and to your satisfaction, you can then proceed to create the patch and submit it:</p>
<pre><code>$ git commit -m 'some references on variables and functions added'
$ git format-patch -1 HEAD
</code></pre>
<p>This creates a file containing the commit comment: <code>0001-some-references-on-variables-and-functions-added.patch</code>.</p>
<p>Apparently, in the PostgreSQL project, you don&rsquo;t create a merge request to incorporate the patch back into the source code; instead, the patch must be sent to the appropriate mailing list and then merged into the main branch by a developer with merge/commit privileges. I&rsquo;ve now agreed with &ldquo;my&rdquo; committer that we&rsquo;ll hold the discussion about my patch on the <a href="https://www.postgresql.org/list/pgsql-docs/" target="_blank">pgsql-docs</a> mailing list.</p>
<p>Let&rsquo;s see how things go from here and how far I get with my patch&hellip;</p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/postgresql/improve-postgresql-documentation/">Making the PostgreSQL Documentation Even Better</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Fractional Chief Data Officer: 7 Proven Real-Time Analytics Wins</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/" />
      <id>https://minervadb.com/fractional-chief-data-officer-real-time-analytics/</id>
      <updated>2026-08-10T12:49:06+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>A Fractional Chief Data Officer from MinervaDB gives an enterprise board-level data leadership — strategy, architecture, governance and operations — on a part-time, fixed-fee basis. When that mandate is pointed squarely at real-time analytics, the [...]</p>
<p><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/">Fractional Chief Data Officer: 7 Proven Real-Time Analytics Wins</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<div class="mdb-wrap">
<p class="mdb-lede">A <strong>Fractional Chief Data Officer</strong> from MinervaDB gives an enterprise board-level data leadership &mdash; strategy, architecture, governance and operations &mdash; on a part-time, fixed-fee basis. When that mandate is pointed squarely at <strong>real-time analytics</strong>, the role stops being an organisational nicety and becomes a profit-and-loss instrument: it determines how quickly your business can see an event, decide on it, and act before the opportunity decays.</p>
<p>This article is deliberately written to be read by five different people at the same table. The CEO will find the commercial thesis. The CTO will find the reference architecture, the latency budget and the engineering standards. The CFO will find unit economics, total cost of ownership and payback. The board and investors will find the governance, risk and diligence position. All five are looking at the same estate; a <a href="https://minervadb.com/fractional-chief-data-officer/">Fractional Chief Data Officer</a> exists to make sure they are looking at the same numbers.</p>
<div class="mdb-kpi">
<div><b>900 ms</b><span>Typical end-to-end P99 target we design real-time pipelines to hold</span></div>
<div><b>40&ndash;60%</b><span>Analytical cost per terabyte scanned recovered in the first two quarters</span></div>
<div><b>&lt; 2 weeks</b><span>Time to a productive Fractional Chief Data Officer contribution</span></div>
<div><b>2&ndash;8 days</b><span>Executive commitment per month, fixed fee, thirty-day exit</span></div>
</div>
<div class="mdb-toc">
<p>What this article covers</p>
<ol>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#real-time-board">Why real-time analytics became a board-level question</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#what-cdo-owns">What a Fractional Chief Data Officer owns in a real-time estate</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#latency-budget">The latency budget: milliseconds as a managed asset</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#decision-decay">Decision decay: the economic case for real-time</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#seven-wins">Seven proven wins</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#technical-blueprint">The technical blueprint for the CTO</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#unit-economics">Unit economics for the CFO</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#governance-diligence">Governance, risk and diligence for the board and investors</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#first-90-days">The first 90 days</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#scorecard">The executive scorecard</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#engagement-models">Engagement models and commercials</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#comparison">Fractional versus full-time versus advisory</a></li>
<li><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/#faq">Frequently asked questions</a></li>
</ol>
</div>
<h2>Why Real-Time Analytics Became a Board-Level Question<a class="anchor-link" id="why-real-time-analytics-became-a-board-level-question"></a></h2>
<p>For most of the last decade, analytics was a reporting function. Data landed overnight, a warehouse transformed it, and the business read yesterday. That model is no longer competitive in any market where price, inventory, credit, fraud, capacity or customer intent move within the trading day. The shift is not technological fashion. It is a change in where margin is created.</p>
<p>Real-time analytics changes three things a board cares about. It shortens the interval between an event and a decision, which is where most recoverable value sits. It exposes operational truth continuously rather than in a monthly pack, which changes the quality of governance. And it converts analytics from a fixed reporting cost into a variable, attributable one &mdash; which is precisely why it needs an owner with executive authority, not a project team.</p>
<p>That owner is the problem. Streaming estates fail commercially far more often than they fail technically. Pipelines get built, dashboards refresh in seconds, and the organisation still argues about which revenue number is correct, still cannot attribute the cloud bill, and still cannot show an auditor where personal data flows. A <strong>Fractional Chief Data Officer</strong> is the corrective: one accountable executive who holds strategy, architecture, cost and compliance together across the entire real-time path.</p>
<div class="mdb-note"><strong>In one sentence.</strong> A Fractional Chief Data Officer converts a fast but ungoverned data estate into a governed, measurable, commercially useful real-time asset &mdash; at a fraction of the cost of a permanent executive, and with none of the hiring risk.</div>
<h2>What a Fractional Chief Data Officer Owns in a Real-Time Analytics Estate<a class="anchor-link" id="what-a-fractional-chief-data-officer-owns-in-a-real-time-analytics-estate"></a></h2>
<p>Real-time analytics is a chain, and a chain is owned end to end or not at all. The diagram below is the path an event travels from the moment it is committed in a system of record to the moment a human or a model acts on it. Every hop in that path has a latency cost, a failure mode, a cost line and a compliance implication. The Fractional Chief Data Officer owns all four dimensions across every hop.</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 240" width="100%" role="img" aria-label="Animated diagram of the real-time analytics pipeline a Fractional Chief Data Officer governs, from change data capture through Kafka and Flink to ClickHouse and the decision surface" xmlns="http://www.w3.org/2000/svg"><defs><lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#38bdf8"></stop><stop offset="0.5" stop-color="#818cf8"></stop><stop offset="1" stop-color="#a78bfa"></stop></lineargradient><filter x="-80%" y="-80%" width="260%" height="260%"><fegaussianblur stddeviation="4" result="b"></fegaussianblur><femerge><femergenode in="b"></femergenode><femergenode in="SourceGraphic"></femergenode></femerge></filter><path d="M155 107 H845" fill="none"></path></defs><rect x="0" y="0" width="1000" height="240" rx="12" fill="#0b1220"></rect><rect x="315" y="10" width="370" height="30" rx="15" fill="#0ea5e9"><animate attributename="opacity" values="0.12;0.34;0.12" dur="2.4s" repeatcount="indefinite"></animate></rect><text x="500" y="30" text-anchor="middle" font-size="12.5" font-weight="700" letter-spacing="1.2" fill="#7dd3fc" font-family="Helvetica,Arial,sans-serif">END-TO-END P99 LATENCY BUDGET &middot; 900 ms</text><line x1="155" y1="107" x2="845" y2="107" stroke="#1e3a5f" stroke-width="5" stroke-linecap="round"></line><line x1="155" y1="107" x2="845" y2="107" stroke="url(#mdbGA)" stroke-width="2.5" stroke-dasharray="10 16"><animate attributename="stroke-dashoffset" from="52" to="0" dur="1.1s" repeatcount="indefinite"></animate></line><circle r="6" fill="#38bdf8" filter="url(#mdbGlowA)"><animatemotion dur="4.5s" begin="0s" repeatcount="indefinite"><mpath href="#mdbPathA" xlink:href="#mdbPathA"></mpath></animatemotion></circle><circle r="6" fill="#818cf8" filter="url(#mdbGlowA)"><animatemotion dur="4.5s" begin="0.9s" repeatcount="indefinite"><mpath href="#mdbPathA" xlink:href="#mdbPathA"></mpath></animatemotion></circle><circle r="6" fill="#a78bfa" filter="url(#mdbGlowA)"><animatemotion dur="4.5s" begin="1.8s" repeatcount="indefinite"><mpath href="#mdbPathA" xlink:href="#mdbPathA"></mpath></animatemotion></circle><circle r="6" fill="#38bdf8" filter="url(#mdbGlowA)"><animatemotion dur="4.5s" begin="2.7s" repeatcount="indefinite"><mpath href="#mdbPathA" xlink:href="#mdbPathA"></mpath></animatemotion></circle><circle r="6" fill="#818cf8" filter="url(#mdbGlowA)"><animatemotion dur="4.5s" begin="3.6s" repeatcount="indefinite"><mpath href="#mdbPathA" xlink:href="#mdbPathA"></mpath></animatemotion></circle><g font-family="Helvetica,Arial,sans-serif"><g><rect x="15" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="85" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">SYSTEMS OF</text><text x="85" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">RECORD</text><text x="85" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">PostgreSQL &middot; MySQL</text><rect x="49" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="85" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">commit</text></g><g><rect x="181" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="251" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">CHANGE DATA</text><text x="251" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">CAPTURE</text><text x="251" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">Debezium &middot; logical WAL</text><rect x="215" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="251" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">~120 ms</text></g><g><rect x="347" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="417" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">EVENT</text><text x="417" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">BACKBONE</text><text x="417" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">Apache Kafka</text><rect x="381" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="417" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">~80 ms</text></g><g><rect x="513" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="583" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">STREAM</text><text x="583" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">PROCESSING</text><text x="583" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">Apache Flink</text><rect x="547" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="583" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">~150 ms</text></g><g><rect x="679" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="749" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">REAL-TIME</text><text x="749" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">STORE</text><text x="749" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">ClickHouse &middot; Druid</text><rect x="713" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="749" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">~200 ms</text></g><g><rect x="845" y="60" width="140" height="95" rx="14" fill="#122b45" stroke="url(#mdbGA)" stroke-width="1.5"></rect><text x="915" y="84" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">DECISION</text><text x="915" y="97" text-anchor="middle" font-size="11" font-weight="700" fill="#e2e8f0">SURFACE</text><text x="915" y="113" text-anchor="middle" font-size="9.5" fill="#94a3b8">BI &middot; API &middot; ML</text><rect x="879" y="122" width="72" height="20" rx="10" fill="#0ea5e9" opacity="0.2"></rect><text x="915" y="136" text-anchor="middle" font-size="10.5" font-weight="700" fill="#7dd3fc">~350 ms</text></g><polygon points="163,101 173,107 163,113" fill="#38bdf8"></polygon><polygon points="329,101 339,107 329,113" fill="#38bdf8"></polygon><polygon points="495,101 505,107 495,113" fill="#818cf8"></polygon><polygon points="661,101 671,107 661,113" fill="#818cf8"></polygon><polygon points="827,101 837,107 827,113" fill="#a78bfa"></polygon><line x1="85" y1="155" x2="85" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><line x1="251" y1="155" x2="251" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><line x1="417" y1="155" x2="417" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><line x1="583" y1="155" x2="583" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><line x1="749" y1="155" x2="749" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><line x1="915" y1="155" x2="915" y2="175" stroke="#1e3a5f" stroke-width="1.5" stroke-dasharray="3 3"></line><rect x="15" y="175" width="970" height="50" rx="12" fill="#0f2338" stroke="#1e3a5f" stroke-width="1.5"></rect><text x="500" y="196" text-anchor="middle" font-size="12.5" font-weight="700" letter-spacing="1" fill="#cbd5e1">FRACTIONAL CHIEF DATA OFFICER &middot; ONE ACCOUNTABLE EXECUTIVE ACROSS THE WHOLE PATH</text><text x="500" y="214" text-anchor="middle" font-size="10.5" fill="#64748b">Schema contracts &middot; exactly-once semantics &middot; latency SLOs &middot; access control &middot; cost per query &middot; retention and residency</text></g></svg></div>
<p class="mdb-cap">Figure 1 &mdash; The real-time analytics path a Fractional Chief Data Officer governs end to end, with the latency budget allocated hop by hop.</p>
<p>Each stage is a genuine engineering discipline. Capture is usually log-based change data capture reading the write-ahead log, following the mechanics described in the <a href="https://www.postgresql.org/docs/current/logical-replication.html" target="_blank" rel="noopener">PostgreSQL logical replication documentation</a> and implemented with <a href="https://debezium.io/documentation/reference/stable/index.html" target="_blank" rel="noopener">Debezium</a>. Transport is an ordered, replayable log, normally <a href="https://kafka.apache.org/documentation/" target="_blank" rel="noopener">Apache Kafka</a>. Processing is stateful stream computation with checkpointing, typically <a href="https://flink.apache.org/what-is-flink/flink-architecture/" target="_blank" rel="noopener">Apache Flink</a>. Serving is a column store tuned for high-cardinality, low-latency aggregation, such as ClickHouse and its <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree" target="_blank" rel="noopener">MergeTree family of table engines</a>.</p>
<p>What no vendor supplies is the arbitration between them. Which events are worth streaming at all? Which consumers are entitled to which fields? What is an acceptable staleness for a pricing decision versus a regulatory report? Who pays when a single badly written dashboard query scans forty terabytes? These are executive questions with engineering answers, and they are the daily work of a Fractional Chief Data Officer. MinervaDB backs that judgement with delivery capability through <a href="https://minervadb.com/clickhouse-consulting/">ClickHouse consulting</a>, <a href="https://minervadb.com/postgresql-consulting/">PostgreSQL consulting</a> and our <a href="https://minervadb.com/elite-high-performance-data-engineering-2/">high-performance data engineering</a> practice.</p>
<h2>The Latency Budget: How a Fractional Chief Data Officer Turns Milliseconds Into Margin<a class="anchor-link" id="the-latency-budget-how-a-fractional-chief-data-officer-turns-milliseconds-into-margin"></a></h2>
<p>Real-time is not a marketing adjective; it is a number with an owner. The single most useful artefact a Fractional Chief Data Officer introduces in the first month is a written latency budget: an explicit allocation of the end-to-end service level objective across every hop, with a named owner and an alert for each allocation. Once the budget exists, arguments about whether the platform is fast enough stop being subjective.</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 250" width="100%" role="img" aria-label="Animated latency budget showing how a Fractional Chief Data Officer allocates a 900 millisecond real-time analytics service level objective across capture, transport, processing, storage, query and render" xmlns="http://www.w3.org/2000/svg"><defs><filter x="-80%" y="-80%" width="260%" height="260%"><fegaussianblur stddeviation="3.5" result="b"></fegaussianblur><femerge><femergenode in="b"></femergenode><femergenode in="SourceGraphic"></femergenode></femerge></filter></defs><rect x="0" y="0" width="1000" height="250" rx="12" fill="#0b1220"></rect><g font-family="Helvetica,Arial,sans-serif"><text x="60" y="34" font-size="13" font-weight="700" letter-spacing="1.4" fill="#7dd3fc">LATENCY BUDGET &middot; COMMIT TO DECISION</text><text x="940" y="34" text-anchor="end" font-size="13" font-weight="700" fill="#34d399">ACHIEVED P99 900 ms</text><line x1="940" y1="62" x2="940" y2="150" stroke="#f43f5e" stroke-width="2" stroke-dasharray="5 5"></line><text x="934" y="58" text-anchor="end" font-size="10.5" fill="#fb7185" font-weight="700">SLO CEILING 1,000 ms</text><rect x="60" y="95" width="0" height="50" fill="#0ea5e9"><animate attributename="width" values="0;0;105.6;105.6;0" keytimes="0;0.025;0.1;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="165.6" y="95" width="0" height="50" fill="#22d3ee"><animate attributename="width" values="0;0;70.4;70.4;0" keytimes="0;0.0875;0.1625;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="236" y="95" width="0" height="50" fill="#818cf8"><animate attributename="width" values="0;0;132;132;0" keytimes="0;0.15;0.225;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="368" y="95" width="0" height="50" fill="#a78bfa"><animate attributename="width" values="0;0;176;176;0" keytimes="0;0.2125;0.2875;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="544" y="95" width="0" height="50" fill="#f472b6"><animate attributename="width" values="0;0;220;220;0" keytimes="0;0.275;0.35;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="764" y="95" width="0" height="50" fill="#fbbf24"><animate attributename="width" values="0;0;88;88;0" keytimes="0;0.3375;0.4125;0.95;1" dur="8s" repeatcount="indefinite"></animate></rect><rect x="60" y="95" width="880" height="50" fill="none" stroke="#1e3a5f" stroke-width="1.5" rx="4"></rect><text x="896" y="124" text-anchor="middle" font-size="9.5" fill="#34d399" font-weight="700">100 ms free</text><g><line x1="60" y1="80" x2="60" y2="150" stroke="#e2e8f0" stroke-width="1.5" opacity="0.85"></line><circle cx="60" cy="80" r="5" fill="#e2e8f0" filter="url(#mdbGlowB)"></circle><animatetransform attributename="transform" type="translate" values="0,0;0,0;792,0;792,0;0,0" keytimes="0;0.025;0.4125;0.95;1" dur="8s" repeatcount="indefinite"></animatetransform></g><line x1="60" y1="150" x2="940" y2="150" stroke="#334155" stroke-width="1.5"></line><g font-size="10" fill="#64748b"><text x="60" y="168" text-anchor="middle">0 ms</text><text x="280" y="168" text-anchor="middle">250 ms</text><text x="500" y="168" text-anchor="middle">500 ms</text><text x="720" y="168" text-anchor="middle">750 ms</text><text x="940" y="168" text-anchor="middle">1,000 ms</text></g><g font-size="11" font-weight="700"><g opacity="0"><line x1="112.8" y1="147" x2="112.8" y2="188" stroke="#0ea5e9" stroke-width="1"></line><text x="112.8" y="200" text-anchor="middle" fill="#0ea5e9">Capture 120 ms</text><text x="112.8" y="214" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">CDC lag</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.025;0.1;0.95;1" dur="8s" repeatcount="indefinite"></animate></g><g opacity="0"><line x1="200.8" y1="147" x2="200.8" y2="212" stroke="#22d3ee" stroke-width="1"></line><text x="200.8" y="224" text-anchor="middle" fill="#22d3ee">Transport 80 ms</text><text x="200.8" y="238" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">broker ack</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.0875;0.1625;0.95;1" dur="8s" repeatcount="indefinite"></animate></g><g opacity="0"><line x1="302" y1="147" x2="302" y2="188" stroke="#818cf8" stroke-width="1"></line><text x="302" y="200" text-anchor="middle" fill="#818cf8">Process 150 ms</text><text x="302" y="214" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">windowing</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.15;0.225;0.95;1" dur="8s" repeatcount="indefinite"></animate></g><g opacity="0"><line x1="456" y1="147" x2="456" y2="212" stroke="#a78bfa" stroke-width="1"></line><text x="456" y="224" text-anchor="middle" fill="#a78bfa">Store 200 ms</text><text x="456" y="238" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">insert to visible</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.2125;0.2875;0.95;1" dur="8s" repeatcount="indefinite"></animate></g><g opacity="0"><line x1="654" y1="147" x2="654" y2="188" stroke="#f472b6" stroke-width="1"></line><text x="654" y="200" text-anchor="middle" fill="#f472b6">Query 250 ms</text><text x="654" y="214" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">P99 aggregation</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.275;0.35;0.95;1" dur="8s" repeatcount="indefinite"></animate></g><g opacity="0"><line x1="808" y1="147" x2="808" y2="212" stroke="#fbbf24" stroke-width="1"></line><text x="808" y="224" text-anchor="middle" fill="#fbbf24">Render 100 ms</text><text x="808" y="238" text-anchor="middle" font-size="9.5" font-weight="400" fill="#64748b">client paint</text><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.3375;0.4125;0.95;1" dur="8s" repeatcount="indefinite"></animate></g></g></g></svg></div>
<p class="mdb-cap">Figure 2 &mdash; A published latency budget: every hop has an allocation, an owner and an alert. Unallocated headroom is a deliberate reserve, not luck.</p>
<p>Two disciplines make the budget real. The first is error-budget thinking, borrowed from site reliability engineering and set out in the <a href="https://sre.google/sre-book/service-level-objectives/" target="_blank" rel="noopener">Google SRE book chapter on service level objectives</a>: an objective without a consequence is a wish. The second is physical design. In a column store, latency and cost are both functions of sort order, partitioning, codecs and pre-aggregation &mdash; which is why a Fractional Chief Data Officer signs off storage layout the way a chief financial officer signs off capital expenditure.</p>
<pre class="mdb-code"><b>ClickHouse &middot; real-time ingest with a governed physical design</b><code>-- Ordered, replayable ingest. Consumer group, format and parallelism are
-- reviewed artefacts, not defaults inherited from a tutorial.
CREATE TABLE rt.order_event_queue
(
    event_time       DateTime64(3),
    tenant_id        UInt32,
    order_id         String,
    country          LowCardinality(String),
    channel          LowCardinality(String),
    net_amount       Decimal(18, 2),
    is_fraud_flagged UInt8
)
ENGINE = Kafka
SETTINGS kafka_broker_list   = 'kafka-01:9092,kafka-02:9092',
         kafka_topic_list    = 'commerce.orders.v2',
         kafka_group_name    = 'ch_rt_orders',
         kafka_format        = 'JSONEachRow',
         kafka_num_consumers = 4;

-- Serving table. Sort order comes from measured query patterns; retention and
-- tiering are signed off by the Fractional Chief Data Officer as cost policy.
CREATE TABLE rt.order_event
(
    event_date       Date DEFAULT toDate(event_time),
    event_time       DateTime64(3),
    tenant_id        UInt32,
    order_id         String,
    country          LowCardinality(String),
    channel          LowCardinality(String),
    net_amount       Decimal(18, 2),
    is_fraud_flagged UInt8
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, country, event_time)
TTL event_date + INTERVAL 6  MONTH TO VOLUME 'cold',
    event_date + INTERVAL 25 MONTH DELETE
SETTINGS index_granularity = 8192;

-- Pre-aggregate the handful of questions the business asks every minute,
-- so that a dashboard refresh never becomes a full-table scan.
CREATE MATERIALIZED VIEW rt.order_minute_mv TO rt.order_minute AS
SELECT toStartOfMinute(event_time)  AS minute,
       tenant_id,
       country,
       countState()                 AS orders_state,
       sumState(net_amount)         AS revenue_state,
       sumState(is_fraud_flagged)   AS flagged_state
FROM rt.order_event
GROUP BY minute, tenant_id, country;</code></pre>
<h2>Decision Decay: The Economic Case a Fractional Chief Data Officer Puts to the CFO<a class="anchor-link" id="decision-decay-the-economic-case-a-fractional-chief-data-officer-puts-to-the-cfo"></a></h2>
<p>The commercial argument for real-time analytics is not that faster is nicer. It is that the value of a decision decays, often steeply, from the moment the triggering event occurs. A fraud signal acted on in 400 milliseconds prevents a loss; the same signal in the overnight batch documents one. An abandoned basket recovered within the session converts; recovered tomorrow it annoys. A Fractional Chief Data Officer makes that decay curve explicit, attaches revenue to it, and uses it to size investment.</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 300" width="100%" role="img" aria-label="Animated decision decay curve used by a Fractional Chief Data Officer to show how the commercial value of a decision falls as analytics latency increases" xmlns="http://www.w3.org/2000/svg"><defs><lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#34d399"></stop><stop offset="0.45" stop-color="#fbbf24"></stop><stop offset="1" stop-color="#f43f5e"></stop></lineargradient><lineargradient x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#38bdf8" stop-opacity="0.35"></stop><stop offset="1" stop-color="#38bdf8" stop-opacity="0"></stop></lineargradient><filter x="-80%" y="-80%" width="260%" height="260%"><fegaussianblur stddeviation="4" result="b"></fegaussianblur><femerge><femergenode in="b"></femergenode><femergenode in="SourceGraphic"></femergenode></femerge></filter><path fill="none" d="M80,60 C180,72 250,140 340,178 C470,222 660,246 940,252"></path></defs><rect x="0" y="0" width="1000" height="300" rx="12" fill="#0b1220"></rect><g font-family="Helvetica,Arial,sans-serif"><text x="80" y="30" font-size="13" font-weight="700" letter-spacing="1.4" fill="#7dd3fc">DECISION VALUE DECAY</text><text x="940" y="30" text-anchor="end" font-size="12" font-weight="700" fill="#94a3b8">LATENCY IS A REVENUE VARIABLE</text><rect x="80" y="55" width="90" height="195" fill="#22c55e"><animate attributename="opacity" values="0.07;0.2;0.07" dur="2.6s" repeatcount="indefinite"></animate></rect><rect x="850" y="55" width="90" height="195" fill="#f43f5e"><animate attributename="opacity" values="0.07;0.2;0.07" dur="2.6s" begin="1.3s" repeatcount="indefinite"></animate></rect><text x="125" y="48" text-anchor="middle" font-size="9.5" font-weight="700" fill="#4ade80">REAL-TIME</text><text x="895" y="48" text-anchor="middle" font-size="9.5" font-weight="700" fill="#fb7185">BATCH T+24h</text><line x1="80" y1="55" x2="80" y2="250" stroke="#334155" stroke-width="1.5"></line><line x1="80" y1="250" x2="950" y2="250" stroke="#334155" stroke-width="1.5"></line><line x1="80" y1="157" x2="940" y2="157" stroke="#334155" stroke-width="1" stroke-dasharray="4 6"></line><g font-size="10" fill="#64748b"><text x="70" y="64" text-anchor="end">100%</text><text x="70" y="161" text-anchor="end">50%</text><text x="70" y="254" text-anchor="end">0%</text><text x="80" y="272" text-anchor="middle">event</text><text x="295" y="272" text-anchor="middle">1 min</text><text x="510" y="272" text-anchor="middle">1 hour</text><text x="725" y="272" text-anchor="middle">6 hours</text><text x="940" y="272" text-anchor="middle">24 hours</text><text x="500" y="290" text-anchor="middle" font-size="10.5" fill="#475569">Time elapsed between the business event and the decision taken on it</text></g><text transform="translate(28,200) rotate(-90)" font-size="10.5" fill="#475569">Recoverable value of the decision</text><path d="M80,60 C180,72 250,140 340,178 C470,222 660,246 940,252 L940,250 L80,250 Z" fill="url(#mdbFillC)" opacity="0"><animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.02;0.45;0.9;1" dur="9s" repeatcount="indefinite"></animate></path><path d="M80,60 C180,72 250,140 340,178 C470,222 660,246 940,252" fill="none" stroke="url(#mdbGC)" stroke-width="4" stroke-linecap="round" stroke-dasharray="1000"><animate attributename="stroke-dashoffset" values="1000;1000;0;0;1000" keytimes="0;0.02;0.45;0.9;1" dur="9s" repeatcount="indefinite"></animate></path><circle r="7" fill="#e2e8f0" filter="url(#mdbGlowC)"><animatemotion dur="9s" repeatcount="indefinite" keypoints="0;0;1;1;0" keytimes="0;0.02;0.45;0.9;1" calcmode="linear"><mpath href="#mdbCurveC" xlink:href="#mdbCurveC"></mpath></animatemotion></circle><circle cx="266" cy="157" r="5" fill="#fbbf24"></circle><circle cx="266" cy="157" r="5" fill="none" stroke="#fbbf24" stroke-width="2"><animate attributename="r" values="5;20;5" dur="2.2s" repeatcount="indefinite"></animate><animate attributename="opacity" values="0.9;0;0.9" dur="2.2s" repeatcount="indefinite"></animate></circle><text x="284" y="146" font-size="10.5" font-weight="700" fill="#fbbf24">Half the value gone within minutes</text><text x="284" y="132" font-size="9.5" fill="#94a3b8">This crossing point is what the investment case is really buying back</text></g></svg></div>
<p class="mdb-cap">Figure 3 &mdash; Decision decay. A Fractional Chief Data Officer prices the area under this curve, then designs the latency budget in Figure 2 to recover it.</p>
<h2>Seven Proven Wins a Fractional Chief Data Officer Delivers in Real-Time Analytics<a class="anchor-link" id="seven-proven-wins-a-fractional-chief-data-officer-delivers-in-real-time-analytics"></a></h2>
<p>These are the outcomes we contract to. They are stated as measurable changes rather than activities, because a Fractional Chief Data Officer engagement is only credible if the board can verify it.</p>
<div class="mdb-cards">
<div>
<h4>1. One certified real-time metric layer</h4>
<p>A small set of certified metrics &mdash; revenue, active customers, conversion, exposure &mdash; with a named owner, a written definition and a freshness service level. Finance, product and operations stop reconciling and start deciding.</p>
</div>
<div>
<h4>2. A latency SLO somebody actually owns</h4>
<p>An end-to-end objective decomposed hop by hop, instrumented, alerted and reported monthly. Performance stops being anecdotal and becomes a tracked commitment with an error budget.</p>
</div>
<div>
<h4>3. Analytical cost per terabyte cut, not capped</h4>
<p>Sort keys, codecs, pre-aggregation, tiering and retention are redesigned against real workloads. Typical outcome is a 40 to 60 per cent reduction in cost per terabyte scanned with equal or better latency.</p>
</div>
<div>
<h4>4. Pipelines that survive replay and schema change</h4>
<p>Exactly-once semantics, idempotent sinks, versioned schemas and a tested backfill path. Incidents become bounded operational events instead of week-long reconciliation projects.</p>
</div>
<div>
<h4>5. Real-time risk and fraud controls you can evidence</h4>
<p>Streaming rules and models applied within the decision window, with a full audit trail of what was known when. This is the control regulators and insurers ask to see.</p>
</div>
<div>
<h4>6. AI and ML served from the same governed stream</h4>
<p>Features computed once, registered, reused for training and inference, with drift monitoring. Models stop being pilots because the data path underneath them is already production-grade.</p>
</div>
<div>
<h4>7. Diligence-ready governance</h4>
<p>A data inventory, classification, lineage, retention and access model that survives an audit, an initial public offering readiness review or a buyer&rsquo;s technical diligence without a fire drill.</p>
</div>
</div>
<h2>The Technical Blueprint: What the CTO Gets From a Fractional Chief Data Officer<a class="anchor-link" id="the-technical-blueprint-what-the-cto-gets-from-a-fractional-chief-data-officer"></a></h2>
<p>Engineering teams do not need another strategy deck. They need decisions made, written down and defended. A Fractional Chief Data Officer supplies exactly that: a short set of non-negotiable standards for the streaming estate, each of which removes a recurring class of incident. The three that matter most are the data contract, exactly-once delivery semantics, and measured rather than asserted service levels.</p>
<h3>Data contracts for streams, reviewed like an API<a class="anchor-link" id="data-contracts-for-streams-reviewed-like-an-api"></a></h3>
<p>Every significant topic publishes a contract: owner, classification, freshness commitment, schema, quality rules and breaking-change policy. Producers cannot silently drop a field. Consumers can depend on a stated service level. Auditors have one artefact to inspect. Quality assertions run in continuous integration in the manner described by the <a href="https://docs.getdbt.com/docs/build/data-tests" target="_blank" rel="noopener">dbt data testing documentation</a>, and a failing contract blocks promotion.</p>
<pre class="mdb-code"><b>Data contract &middot; versioned in source control, enforced in the pipeline</b><code># data-contracts/commerce.orders.v2.yaml
apiVersion: minervadb.com/v1
kind: StreamContract
metadata:
  name: commerce.orders
  version: 2.1.0
  owner: commerce-platform
  steward: fractional-cdo-office
spec:
  classification: restricted
  transport: kafka
  partitions: 24
  keyField: order_id
  ordering: per-key
  deliverySemantics: exactly-once
  freshnessSlo: PT2S          # event time to queryable in ClickHouse
  availabilitySlo: 99.95
  retention: P7D              # log retention; serving store keeps 25 months
  schema:
    - name: order_id
      type: string
      required: true
    - name: customer_email
      type: string
      required: true
      pii: true
      masking: sha256
    - name: net_amount
      type: decimal(18,2)
      required: true
      constraints: ["&gt;= 0"]
  quality:
    - rule: uniqueness(order_id)
      threshold: 1.0
      severity: blocker
    - rule: lag_p99_seconds(event_time, ingested_at)
      threshold: 2
      severity: critical
  breakingChangePolicy: majorVersionOnly</code></pre>
<h3>Service levels measured, not asserted<a class="anchor-link" id="service-levels-measured-not-asserted"></a></h3>
<p>The monthly executive report is generated by a query, not written by a person. That single habit removes the most common failure of data leadership, which is a scorecard that quietly reflects opinion. The query below is the shape we deploy on day one of a Fractional Chief Data Officer engagement, and it is the same number the board sees.</p>
<pre class="mdb-code"><b>ClickHouse &middot; the freshness, quality and cost numbers behind the board pack</b><code>-- End-to-end freshness and cost, produced by instrumentation rather than opinion.
SELECT
    dataset,
    quantile(0.50)(dateDiff('millisecond', event_time, ingested_at)) AS p50_lag_ms,
    quantile(0.99)(dateDiff('millisecond', event_time, ingested_at)) AS p99_lag_ms,
    countIf(dateDiff('millisecond', event_time, ingested_at) &gt; slo_ms) * 100.0
        / count()                                                    AS slo_breach_pct,
    round(sum(read_bytes) / pow(1024, 4), 3)                         AS tb_scanned,
    round(sum(read_bytes) / pow(1024, 4) * 5.00, 2)                  AS est_cost_usd
FROM rt.pipeline_observability
WHERE ingested_at &gt;= now() - INTERVAL 30 DAY
GROUP BY dataset
HAVING slo_breach_pct &gt; 0.1 OR p99_lag_ms &gt; 2000
ORDER BY slo_breach_pct DESC;</code></pre>
<p>Where this work extends beyond governance into sustained operations, the Fractional Chief Data Officer draws on <a href="https://minervadb.com/minervadb-24-7-remote-dba-support/">24&times;7 remote DBA support</a>, <a href="https://minervadb.com/data-analytics-and-data-warehousing-support/">data analytics and warehousing support</a> and, for retrieval and recommendation workloads, our <a href="https://minervadb.com/vector-data-engineering/">vector data engineering</a> capability.</p>
<h2>Unit Economics: How a Fractional Chief Data Officer Reads the Real-Time Analytics Bill<a class="anchor-link" id="unit-economics-how-a-fractional-chief-data-officer-reads-the-real-time-analytics-bill"></a></h2>
<p>Real-time platforms are billed by consumption, which means physical design decisions land directly on the invoice. A CFO does not need to understand sort keys; a CFO needs the cost expressed per unit of business activity and trended. A Fractional Chief Data Officer builds that bridge, replacing a single opaque cloud line item with attributable unit costs that a finance function can actually manage.</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 265" width="100%" role="img" aria-label="Animated before and after chart of the unit economics a Fractional Chief Data Officer improves across cost per terabyte scanned, compute hours, event to decision latency and incident load" xmlns="http://www.w3.org/2000/svg"><defs><lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#22d3ee"></stop><stop offset="1" stop-color="#34d399"></stop></lineargradient></defs><rect x="0" y="0" width="1000" height="265" rx="12" fill="#0b1220"></rect><g font-family="Helvetica,Arial,sans-serif"><text x="40" y="32" font-size="13" font-weight="700" letter-spacing="1.4" fill="#7dd3fc">UNIT ECONOMICS &middot; BEFORE AND AFTER TWO QUARTERS</text><rect x="672" y="22" width="12" height="12" rx="3" fill="#334155"></rect><text x="690" y="32" font-size="11" fill="#94a3b8">Baseline</text><rect x="762" y="22" width="12" height="12" rx="3" fill="url(#mdbGD)"></rect><text x="780" y="32" font-size="11" fill="#94a3b8">Under a Fractional CDO</text><g font-size="11.5" fill="#cbd5e1"><text x="290" y="69" text-anchor="end">Cost per terabyte scanned</text><text x="290" y="107" text-anchor="end">Warehouse and cluster compute hours</text><text x="290" y="145" text-anchor="end">Event-to-decision latency</text><text x="290" y="183" text-anchor="end">Engineer hours lost to data incidents</text><text x="290" y="221" text-anchor="end">Fully loaded cost per certified metric</text></g><rect x="300" y="58" width="600" height="13" rx="6" fill="#334155"></rect><rect x="300" y="96" width="600" height="13" rx="6" fill="#334155"></rect><rect x="300" y="134" width="600" height="13" rx="6" fill="#334155"></rect><rect x="300" y="172" width="600" height="13" rx="6" fill="#334155"></rect><rect x="300" y="210" width="600" height="13" rx="6" fill="#334155"></rect><rect x="300" y="74" width="0" height="13" rx="6" fill="url(#mdbGD)"><animate attributename="width" values="0;0;276;276;0" keytimes="0;0.04;0.2;0.94;1" dur="7s" repeatcount="indefinite"></animate></rect><rect x="300" y="112" width="0" height="13" rx="6" fill="url(#mdbGD)"><animate attributename="width" values="0;0;348;348;0" keytimes="0;0.1;0.26;0.94;1" dur="7s" repeatcount="indefinite"></animate></rect><rect x="300" y="150" width="0" height="13" rx="6" fill="url(#mdbGD)"><animate attributename="width" values="0;0;24;24;0" keytimes="0;0.16;0.32;0.94;1" dur="7s" repeatcount="indefinite"></animate></rect><rect x="300" y="188" width="0" height="13" rx="6" fill="url(#mdbGD)"><animate attributename="width" values="0;0;210;210;0" keytimes="0;0.22;0.38;0.94;1" dur="7s" repeatcount="indefinite"></animate></rect><rect x="300" y="226" width="0" height="13" rx="6" fill="url(#mdbGD)"><animate attributename="width" values="0;0;306;306;0" keytimes="0;0.28;0.44;0.94;1" dur="7s" repeatcount="indefinite"></animate></rect><g font-size="12" font-weight="700" fill="#34d399"><text x="586" y="85" opacity="0">&minus;54%<animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.2;0.24;0.94;1" dur="7s" repeatcount="indefinite"></animate></text><text x="658" y="123" opacity="0">&minus;42%<animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.26;0.3;0.94;1" dur="7s" repeatcount="indefinite"></animate></text><text x="334" y="161" opacity="0">&minus;96%<animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.32;0.36;0.94;1" dur="7s" repeatcount="indefinite"></animate></text><text x="520" y="199" opacity="0">&minus;65%<animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.38;0.42;0.94;1" dur="7s" repeatcount="indefinite"></animate></text><text x="616" y="237" opacity="0">&minus;49%<animate attributename="opacity" values="0;0;1;1;0" keytimes="0;0.44;0.48;0.94;1" dur="7s" repeatcount="indefinite"></animate></text></g><text x="300" y="256" font-size="10" fill="#475569">Indicative ranges from MinervaDB engagements. Bars are normalised to the client baseline at engagement start.</text></g></svg></div>
<p class="mdb-cap">Figure 4 &mdash; Unit economics before and after a Fractional Chief Data Officer takes ownership of the real-time estate.</p>
<p>The mechanism is unglamorous and repeatable. Queries are profiled and the top decile by bytes scanned is redesigned or pre-aggregated. Retention is enforced instead of aspirational. Cold partitions move to cheaper storage on a schedule. Idle clusters are decommissioned rather than tolerated. Chargeback labels are applied so that every terabyte has an owner. MinervaDB runs this discipline continuously through our <a href="https://minervadb.com/cloud-database-optimization-finops/">cloud database optimisation and FinOps</a> practice, and it is what makes the investment case defensible rather than aspirational.</p>
<table class="mdb-tbl">
<thead>
<tr>
<th>Cost or risk line</th>
<th>Without executive data ownership</th>
<th>With a MinervaDB Fractional Chief Data Officer</th>
</tr>
</thead>
<tbody>
<tr>
<td>Leadership cost</td>
<td>Full-time CDO salary, bonus, equity and search fee</td>
<td>Fixed monthly fee for 2&ndash;8 principal days, thirty-day exit</td>
</tr>
<tr>
<td>Analytical compute</td>
<td>Grows with usage; no owner of query efficiency</td>
<td>Cost per terabyte scanned tracked and reduced quarter on quarter</td>
</tr>
<tr>
<td>Storage</td>
<td>Everything retained forever &ldquo;just in case&rdquo;</td>
<td>Tiering and retention enforced by declared policy</td>
</tr>
<tr>
<td>Engineering opportunity cost</td>
<td>Senior engineers absorbed by reconciliation and incidents</td>
<td>Incident load falls; capacity returns to product work</td>
</tr>
<tr>
<td>Decision quality</td>
<td>Contested numbers, decisions taken on stale data</td>
<td>Certified metrics with published freshness objectives</td>
</tr>
<tr>
<td>Regulatory and audit exposure</td>
<td>Unquantified; discovered during an audit</td>
<td>Inventoried, classified, evidenced and reported monthly</td>
</tr>
<tr>
<td>Diligence and valuation risk</td>
<td>Data findings become price adjustments</td>
<td>Governance pack maintained continuously, not assembled in panic</td>
</tr>
</tbody>
</table>
<h2>Governance, Risk and Diligence: The Board and Investor View<a class="anchor-link" id="governance-risk-and-diligence-the-board-and-investor-view"></a></h2>
<p>For a board, real-time analytics raises the stakes on governance rather than lowering them. Data moves faster, reaches more consumers and is embedded in automated decisions, which means an error propagates before anyone notices. Directors are entitled to ask a small number of hard questions, and a Fractional Chief Data Officer exists to answer them with evidence: where does personal data flow, who can read it, how long is it kept, what breaks if a pipeline fails, and what did we know at the moment a decision was automated?</p>
<p>Our governance model follows established practice rather than invention. Data management domains map to the <a href="https://dama.org/learning-resources/dama-data-management-body-of-knowledge-dmbok/" target="_blank" rel="noopener">DAMA Data Management Body of Knowledge</a>, and privacy obligations map to the processing principles in <a href="https://gdpr-info.eu/art-5-gdpr/" target="_blank" rel="noopener">Article 5 of the GDPR</a> and their regional equivalents. The value MinervaDB adds is not the framework; it is the engineering rigour with which the framework is made executable in a streaming estate.</p>
<p><img decoding="async" src="https://minervadb.com/wp-content/uploads/2026/08/fractional-chief-data-officer-reference-data-architecture.png" alt="Fractional Chief Data Officer reference data architecture for real-time analytics across SQL, NoSQL, NewSQL and column stores" width="1200" height="686" loading="lazy"></p>
<p class="mdb-cap">Figure 5 &mdash; The reference architecture a Fractional Chief Data Officer governs: fit-for-purpose storage under a single governance, security and FinOps plane.</p>
<p>Investors read this differently again. In diligence, data findings rarely kill a deal but frequently move the price. An estate with certified metrics, documented lineage, enforced retention and a measured cost base presents as a managed asset. The same estate without those artefacts presents as a liability with an unknown remediation cost, and it is discounted accordingly. Engaging a Fractional Chief Data Officer eighteen months before a raise or an exit is, in our experience, one of the cheapest forms of valuation protection available. Our perspective on <a href="https://minervadb.com/database-transformation-for-cios/">database transformation for CIOs</a> and our <a href="https://minervadb.com/gcc-data-leadership/">global capability centre data leadership</a> programme describe how that capability is sustained at scale.</p>
<h2>The First 90 Days With a MinervaDB Fractional Chief Data Officer<a class="anchor-link" id="the-first-90-days-with-a-minervadb-fractional-chief-data-officer"></a></h2>
<p>Every engagement follows the same evidence-led sequence: assess before advising, architect before building, and prove value on two or three real-time use cases before asking for a larger budget.</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 230" width="100%" role="img" aria-label="Animated 90 day roadmap for a MinervaDB Fractional Chief Data Officer engagement covering assess, architect, activate and operate phases" xmlns="http://www.w3.org/2000/svg"><defs><lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#38bdf8"></stop><stop offset="0.5" stop-color="#818cf8"></stop><stop offset="1" stop-color="#34d399"></stop></lineargradient></defs><rect x="0" y="0" width="1000" height="230" rx="12" fill="#0b1220"></rect><g font-family="Helvetica,Arial,sans-serif"><text x="70" y="30" font-size="13" font-weight="700" letter-spacing="1.4" fill="#7dd3fc">FRACTIONAL CHIEF DATA OFFICER &middot; ENGAGEMENT ROADMAP</text><line x1="70" y1="120" x2="930" y2="120" stroke="#1e3a5f" stroke-width="5" stroke-linecap="round"></line><line x1="70" y1="120" x2="930" y2="120" stroke="url(#mdbGE)" stroke-width="5" stroke-linecap="round" stroke-dasharray="860"><animate attributename="stroke-dashoffset" values="860;860;0;0;860" keytimes="0;0.03;0.6;0.93;1" dur="9s" repeatcount="indefinite"></animate></line><g><circle cx="122" cy="120" r="13" fill="#0b1220" stroke="#38bdf8" stroke-width="3"></circle><circle cx="122" cy="120" r="6" fill="#38bdf8"></circle><circle cx="122" cy="120" r="13" fill="none" stroke="#38bdf8" stroke-width="2"><animate attributename="r" values="13;32;13" dur="2.25s" begin="0s" repeatcount="indefinite"></animate><animate attributename="opacity" values="0.85;0;0.85" dur="2.25s" begin="0s" repeatcount="indefinite"></animate></circle><text x="122" y="72" text-anchor="middle" font-size="11" font-weight="700" fill="#38bdf8">DAYS 0&ndash;30</text><text x="122" y="92" text-anchor="middle" font-size="14" font-weight="700" fill="#e2e8f0">ASSESS</text><text x="122" y="156" text-anchor="middle" font-size="10" fill="#94a3b8">Inventory, baseline, risk register</text><text x="122" y="170" text-anchor="middle" font-size="10" fill="#64748b">Latency and cost measured, not estimated</text></g><g><circle cx="371" cy="120" r="13" fill="#0b1220" stroke="#60a5fa" stroke-width="3"></circle><circle cx="371" cy="120" r="6" fill="#60a5fa"></circle><circle cx="371" cy="120" r="13" fill="none" stroke="#60a5fa" stroke-width="2"><animate attributename="r" values="13;32;13" dur="2.25s" begin="2.25s" repeatcount="indefinite"></animate><animate attributename="opacity" values="0.85;0;0.85" dur="2.25s" begin="2.25s" repeatcount="indefinite"></animate></circle><text x="371" y="72" text-anchor="middle" font-size="11" font-weight="700" fill="#60a5fa">DAYS 31&ndash;60</text><text x="371" y="92" text-anchor="middle" font-size="14" font-weight="700" fill="#e2e8f0">ARCHITECT</text><text x="371" y="156" text-anchor="middle" font-size="10" fill="#94a3b8">Target architecture and data contracts</text><text x="371" y="170" text-anchor="middle" font-size="10" fill="#64748b">Investment case and twelve-month plan</text></g><g><circle cx="620" cy="120" r="13" fill="#0b1220" stroke="#818cf8" stroke-width="3"></circle><circle cx="620" cy="120" r="6" fill="#818cf8"></circle><circle cx="620" cy="120" r="13" fill="none" stroke="#818cf8" stroke-width="2"><animate attributename="r" values="13;32;13" dur="2.25s" begin="4.5s" repeatcount="indefinite"></animate><animate attributename="opacity" values="0.85;0;0.85" dur="2.25s" begin="4.5s" repeatcount="indefinite"></animate></circle><text x="620" y="72" text-anchor="middle" font-size="11" font-weight="700" fill="#818cf8">DAYS 61&ndash;90</text><text x="620" y="92" text-anchor="middle" font-size="14" font-weight="700" fill="#e2e8f0">ACTIVATE</text><text x="620" y="156" text-anchor="middle" font-size="10" fill="#94a3b8">Two or three real-time use cases in production</text><text x="620" y="170" text-anchor="middle" font-size="10" fill="#64748b">SLOs, cost guardrails and policy in CI</text></g><g><circle cx="878" cy="120" r="13" fill="#0b1220" stroke="#34d399" stroke-width="3"></circle><circle cx="878" cy="120" r="6" fill="#34d399"></circle><circle cx="878" cy="120" r="13" fill="none" stroke="#34d399" stroke-width="2"><animate attributename="r" values="13;32;13" dur="2.25s" begin="6.75s" repeatcount="indefinite"></animate><animate attributename="opacity" values="0.85;0;0.85" dur="2.25s" begin="6.75s" repeatcount="indefinite"></animate></circle><text x="878" y="72" text-anchor="middle" font-size="11" font-weight="700" fill="#34d399">MONTH 4 ONWARDS</text><text x="878" y="92" text-anchor="middle" font-size="14" font-weight="700" fill="#e2e8f0">OPERATE</text><text x="878" y="156" text-anchor="middle" font-size="10" fill="#94a3b8">Quarterly strategy and FinOps reviews</text><text x="878" y="170" text-anchor="middle" font-size="10" fill="#64748b">Succession plan for a permanent CDO</text></g><text x="500" y="208" text-anchor="middle" font-size="10.5" fill="#475569">Every phase produces a written artefact the board can read: risk register, strategy, working data products, monthly scorecard.</text></g></svg></div>
<p class="mdb-cap">Figure 6 &mdash; The MinervaDB Fractional Chief Data Officer engagement roadmap, from assessment to a recurring executive scorecard.</p>
<h2>The Executive Scorecard a Fractional Chief Data Officer Reports Against<a class="anchor-link" id="the-executive-scorecard-a-fractional-chief-data-officer-reports-against"></a></h2>
<p>The scorecard is agreed in the first month and reported every month thereafter, generated from instrumentation. Four numbers carry most of the signal for a real-time estate: are we fresh, are we correct, what does it cost, and how fast can we act?</p>
<div class="mdb-fig"><svg viewbox="0 0 1000 245" width="100%" role="img" aria-label="Animated executive scorecard gauges showing freshness attainment, data contract pass rate, cost per terabyte reduction and decision latency reduction under a Fractional Chief Data Officer" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="1000" height="245" rx="12" fill="#0b1220"></rect><g font-family="Helvetica,Arial,sans-serif"><text x="140" y="32" font-size="13" font-weight="700" letter-spacing="1.4" fill="#7dd3fc" text-anchor="start">MONTHLY EXECUTIVE SCORECARD &middot; GENERATED, NOT ASSERTED</text><g><circle cx="140" cy="118" r="52" fill="none" stroke="#1e3a5f" stroke-width="13"></circle><circle cx="140" cy="118" r="52" fill="none" stroke="#38bdf8" stroke-width="13" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="326.7" transform="rotate(-90 140 118)"><animate attributename="stroke-dashoffset" values="326.7;326.7;1.96;1.96;326.7" keytimes="0;0.05;0.4;0.92;1" dur="6s" repeatcount="indefinite"></animate></circle><text x="140" y="126" text-anchor="middle" font-size="22" font-weight="700" fill="#e2e8f0">99.4%</text><text x="140" y="200" text-anchor="middle" font-size="12" font-weight="700" fill="#cbd5e1">Freshness SLO</text><text x="140" y="216" text-anchor="middle" font-size="11" fill="#64748b">attainment across certified streams</text></g><g><circle cx="380" cy="118" r="52" fill="none" stroke="#1e3a5f" stroke-width="13"></circle><circle cx="380" cy="118" r="52" fill="none" stroke="#34d399" stroke-width="13" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="326.7" transform="rotate(-90 380 118)"><animate attributename="stroke-dashoffset" values="326.7;326.7;2.94;2.94;326.7" keytimes="0;0.1;0.45;0.92;1" dur="6s" repeatcount="indefinite"></animate></circle><text x="380" y="126" text-anchor="middle" font-size="22" font-weight="700" fill="#e2e8f0">99.1%</text><text x="380" y="200" text-anchor="middle" font-size="12" font-weight="700" fill="#cbd5e1">Data contract pass rate</text><text x="380" y="216" text-anchor="middle" font-size="11" fill="#64748b">across production pipelines, weekly</text></g><g><circle cx="620" cy="118" r="52" fill="none" stroke="#1e3a5f" stroke-width="13"></circle><circle cx="620" cy="118" r="52" fill="none" stroke="#fbbf24" stroke-width="13" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="326.7" transform="rotate(-90 620 118)"><animate attributename="stroke-dashoffset" values="326.7;326.7;150.3;150.3;326.7" keytimes="0;0.15;0.5;0.92;1" dur="6s" repeatcount="indefinite"></animate></circle><text x="620" y="126" text-anchor="middle" font-size="22" font-weight="700" fill="#e2e8f0">54%</text><text x="620" y="200" text-anchor="middle" font-size="12" font-weight="700" fill="#cbd5e1">Cost per terabyte scanned</text><text x="620" y="216" text-anchor="middle" font-size="11" fill="#64748b">reduction over two quarters</text></g><g><circle cx="860" cy="118" r="52" fill="none" stroke="#1e3a5f" stroke-width="13"></circle><circle cx="860" cy="118" r="52" fill="none" stroke="#a78bfa" stroke-width="13" stroke-linecap="round" stroke-dasharray="326.7" stroke-dashoffset="326.7" transform="rotate(-90 860 118)"><animate attributename="stroke-dashoffset" values="326.7;326.7;13.1;13.1;326.7" keytimes="0;0.2;0.55;0.92;1" dur="6s" repeatcount="indefinite"></animate></circle><text x="860" y="126" text-anchor="middle" font-size="22" font-weight="700" fill="#e2e8f0">96%</text><text x="860" y="200" text-anchor="middle" font-size="12" font-weight="700" fill="#cbd5e1">Event-to-decision latency</text><text x="860" y="216" text-anchor="middle" font-size="11" fill="#64748b">reduction versus overnight batch</text></g></g></svg></div>
<p class="mdb-cap">Figure 7 &mdash; Indicative twelve-month scorecard. Every figure is produced by a query the client can run independently.</p>
<h2>Fractional Chief Data Officer Engagement Models<a class="anchor-link" id="fractional-chief-data-officer-engagement-models"></a></h2>
<p>We offer three engagement shapes. All are fixed monthly fees with a named principal, a defined day commitment and a thirty-day exit. None involves a leverage pyramid or a junior delivery team.</p>
<div class="mdb-cards">
<div>
<h4>Advisory &mdash; 2 days per month</h4>
<p>Governance council chairing, architecture review and approval, quarterly board reporting and an escalation line for critical real-time decisions. Suited to organisations with a capable engineering team that lacks executive data leadership.</p>
</div>
<div>
<h4>Embedded &mdash; 4 to 6 days per month</h4>
<p>Everything in Advisory, plus hands-on ownership of the streaming roadmap, vendor selection, data contracts, latency SLOs, the cost programme and the hiring plan. The most common shape.</p>
</div>
<div>
<h4>Transformation &mdash; 8+ days per month</h4>
<p>A MinervaDB delivery pod behind the Fractional Chief Data Officer. Used for migrations, consolidations, regulatory remediation and post-acquisition integration where execution capacity is required alongside leadership.</p>
</div>
</div>
<p>Fixed-scope assessments of two to four weeks are also available, and many clients start there. Where a MinervaDB relationship already exists, the Fractional Chief Data Officer can be layered on top of <a href="https://minervadb.com/minervadb-consultative-support/">MinervaDB consultative support</a> without renegotiating the underlying operational contract.</p>
<h2>Fractional Chief Data Officer Versus a Full-Time Hire or an Advisory Firm<a class="anchor-link" id="fractional-chief-data-officer-versus-a-full-time-hire-or-an-advisory-firm"></a></h2>
<table class="mdb-tbl">
<thead>
<tr>
<th>Consideration</th>
<th>Full-time CDO hire</th>
<th>Strategy advisory firm</th>
<th>Contract architect</th>
<th>MinervaDB Fractional Chief Data Officer</th>
</tr>
</thead>
<tbody>
<tr>
<td>Time to productive contribution</td>
<td>Six to nine months including search</td>
<td>Four to eight weeks of discovery</td>
<td>Two to four weeks</td>
<td>Under two weeks</td>
</tr>
<tr>
<td>Annual cost of leadership</td>
<td>Salary, bonus and equity</td>
<td>Large fixed programme fee</td>
<td>Daily rate, no mandate</td>
<td>Fixed monthly fee, scalable</td>
</tr>
<tr>
<td>Hands-on real-time engineering depth</td>
<td>Variable</td>
<td>Generally weak</td>
<td>Strong but narrow</td>
<td>Principal-level across the estate</td>
</tr>
<tr>
<td>Executive authority</td>
<td>Full</td>
<td>Advisory only</td>
<td>None</td>
<td>Full, by written mandate</td>
</tr>
<tr>
<td>Accountability for outcomes</td>
<td>Yes</td>
<td>Recommendations only</td>
<td>Task level</td>
<td>Yes, against an agreed scorecard</td>
</tr>
<tr>
<td>Delivery capacity behind the role</td>
<td>Requires separate hiring</td>
<td>Costly and generalist</td>
<td>None</td>
<td>MinervaDB engineering pods on demand</td>
</tr>
<tr>
<td>Exit risk if it is not working</td>
<td>High and slow</td>
<td>Contractual</td>
<td>Low</td>
<td>Thirty days</td>
</tr>
</tbody>
</table>
<p>The honest position is this. At sufficient scale, a permanent Chief Data Officer is the right answer. A Fractional Chief Data Officer is the right answer before you reach that scale, while you are recovering from a stalled real-time programme, or while you are preparing the organisation so that a permanent hire succeeds rather than becomes your second attempt.</p>
<h2>Frequently Asked Questions About Fractional Chief Data Officer Services<a class="anchor-link" id="frequently-asked-questions-about-fractional-chief-data-officer-services"></a></h2>
<h3>What is a Fractional Chief Data Officer?<a class="anchor-link" id="what-is-a-fractional-chief-data-officer"></a></h3>
<p>A Fractional Chief Data Officer is a senior data executive engaged part-time on a fixed fee who carries the full mandate of a Chief Data Officer &mdash; strategy, architecture, governance, quality, security, cost and value realisation &mdash; without the salary, equity and hiring risk of a permanent appointment.</p>
<h3>How does a Fractional Chief Data Officer improve real-time analytics specifically?<a class="anchor-link" id="how-does-a-fractional-chief-data-officer-improve-real-time-analytics-specifically"></a></h3>
<p>By owning the whole path rather than a stage of it. That means a published latency budget, data contracts on every significant stream, exactly-once delivery semantics, a serving layer designed for the queries the business actually runs, and a cost model expressed per unit of business activity. Speed becomes a managed commitment instead of a demo.</p>
<h3>How much time does the role commit each month?<a class="anchor-link" id="how-much-time-does-the-role-commit-each-month"></a></h3>
<p>Typically two to eight days per month. Advisory engagements start at two days, embedded engagements run at four to six, and transformation programmes require eight or more with a MinervaDB delivery pod behind the role.</p>
<h3>Which technologies does the mandate cover?<a class="anchor-link" id="which-technologies-does-the-mandate-cover"></a></h3>
<p>The whole estate: relational systems such as PostgreSQL, MySQL, MariaDB and SQL Server; NoSQL platforms including MongoDB, Cassandra, Redis and DynamoDB; NewSQL engines such as CockroachDB, TiDB and YugabyteDB; streaming infrastructure including Kafka and Flink; column stores including ClickHouse, Druid, Snowflake, BigQuery and Redshift; and cloud native data platforms on AWS, Azure and Google Cloud.</p>
<h3>How quickly will we see measurable results?<a class="anchor-link" id="how-quickly-will-we-see-measurable-results"></a></h3>
<p>A prioritised risk register plus a measured latency and cost baseline within thirty days. An approved strategy and investment plan by day sixty. Working real-time data products and a board-readable scorecard by day ninety.</p>
<h3>Will a Fractional Chief Data Officer replace our existing data team?<a class="anchor-link" id="will-a-fractional-chief-data-officer-replace-our-existing-data-team"></a></h3>
<p>No. The role gives an existing team direction, standards, decision rights and executive cover. In most engagements the team becomes measurably more effective, and one of the deliverables is a hiring and capability plan for strengthening it further.</p>
<h3>Can the engagement transition to a permanent Chief Data Officer?<a class="anchor-link" id="can-the-engagement-transition-to-a-permanent-chief-data-officer"></a></h3>
<p>Yes, and we plan for it from the outset. Every artefact &mdash; strategy, standards, contracts, runbooks and the scorecard &mdash; is written for handover. Many clients use a Fractional Chief Data Officer precisely to prepare the organisation so that a permanent hire succeeds.</p>
<h3>How is data confidentiality handled?<a class="anchor-link" id="how-is-data-confidentiality-handled"></a></h3>
<p>Under a mutual non-disclosure agreement, with least-privilege access granted for the duration of the engagement only, and a preference for working inside your perimeter. Where regulation requires it, we work exclusively within your virtual private cloud with no data egress.</p>
<div class="mdb-cta">
<h3>Put an accountable data executive in place this quarter<a class="anchor-link" id="put-an-accountable-data-executive-in-place-this-quarter"></a></h3>
<p>If real-time analytics is on your roadmap, on your risk register or in your investment case, the constraint is rarely technology. It is ownership. A MinervaDB <a href="https://minervadb.com/fractional-chief-data-officer/">Fractional Chief Data Officer</a> supplies that ownership in weeks, at a fixed fee, with a thirty-day exit.</p>
<p>The first conversation is with the principal who would hold the mandate &mdash; an engineer who has run production data infrastructure at scale, never a salesperson. <a href="https://minervadb.com/contact-minervadb-book-an-appointment/">Book a conversation with a MinervaDB principal</a>, or read more on our <a href="https://minervadb.com/fractional-chief-data-officer/">Fractional Chief Data Officer services page</a>.</p>
</div>
</div>

<p><a href="https://minervadb.com/fractional-chief-data-officer-real-time-analytics/">Fractional Chief Data Officer: 7 Proven Real-Time Analytics Wins</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/" />
      <id>https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/</id>
      <updated>2026-08-10T15:34:54+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>Auree is building a cloud-independent platform for deploying and operating highly available open source databases, including MariaDB, inside customers’ own cloud accounts. Its Bring Your Own Cloud model allows organisations to choose their cloud provider, region, infrastructure size, and security environment while Auree automates database provisioning, monitoring, backups, clustering, and failover. …<br />
Continue reading \"MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.\"<br />
MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor. appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/">MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://www.auree.com/" data-type="link" data-id="https://www.auree.com/">Auree</a> is building a cloud-independent platform for deploying and operating highly available open source databases, including MariaDB, inside customers&rsquo; own cloud accounts. Its Bring Your Own Cloud model allows organisations to choose their cloud provider, region, infrastructure size, and security environment while Auree automates database provisioning, monitoring, backups, clustering, and failover. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.&rdquo;</span></a></p>
<p><a href="https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/">MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/mariadb-foundation-is-pleased-to-welcome-auree-as-a-silver-sponsor/">MariaDB Foundation is pleased to welcome Auree as a Silver Sponsor.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 12.3 LTS: Advances and Building Fault-Tolerant MariaDB Infrastructure at Internet Scale</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/" />
      <id>https://minervadb.com/mariadb-12-3-high-availability-internet-scale/</id>
      <updated>2026-08-10T09:10:03+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 12.3 is the current long-term support release of MariaDB Community Server. It reached Stable/GA on 28 May 2026 and is maintained until June 2029, making it the first LTS line since MariaDB 11.8. For [...]</p>
<p><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/">MariaDB 12.3 LTS: Advances and Building Fault-Tolerant MariaDB Infrastructure at Internet Scale</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><strong>MariaDB 12.3</strong> is the current long-term support release of MariaDB Community Server. It reached Stable/GA on 28 May 2026 and is maintained until June 2029, making it the first LTS line since MariaDB 11.8. For teams running transactional workloads at internet scale, MariaDB 12.3 is not a routine point upgrade: it rewrites the durability contract between the binary log and InnoDB, adds a full MySQL-compatible optimizer hint framework, and hardens Galera Cluster behaviour under state transfer and write-set conflict. This guide dissects those advancements at engine level and then shows how to compose them into a highly available, fault-tolerant MariaDB infrastructure that survives node, rack, availability-zone and region failure.</p>
<p>Everything below is written from the perspective of database reliability engineering: what changed, why it changes the physics of your write path, what it costs, and how to operationalise it. If you are still on 10.6, 10.11 or 11.4, treat this as an architecture review document rather than a changelog.</p>
<figure class="mdb-figure">
<div><img src="https://minervadb.com/wp-content/uploads/2026/08/mariadb-12-3-high-availability-architecture-diagram.jpg" alt="MariaDB 12.3 high availability and fault-tolerant reference architecture diagram for internet scale" width="989" height="739" loading="eager" decoding="async"></div><figcaption><strong>Figure 1.</strong> The MariaDB 12.3 internet-scale high availability reference architecture discussed in this article. An animated, interactive version appears in the architecture section below.</figcaption></figure>
<div class="mdb-toc">
<h2>Table of Contents<a class="anchor-link" id="table-of-contents"></a></h2>
<ol>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#structural">Why MariaDB 12.3 Is a Structural Release, Not a Point Upgrade</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#innodb-binlog">The Headline Advancement: InnoDB-Based Binary Log</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#optimizer">Optimizer Advancements in MariaDB 12.3</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#replication-galera">Replication and Galera Cluster Improvements</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#security">Security, Compatibility and Developer Surface</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#architecture">Reference Architecture: Highly Available MariaDB 12.3 at Internet Scale</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#configuration">Production Configuration Baselines</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#observability">Observability: SLIs That Predict a MariaDB Outage</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#failure-modes">Failure Modes and Anti-Patterns to Avoid</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#upgrade">Upgrading from MariaDB 11.8 to MariaDB 12.3</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#capacity">Benchmarking and Capacity Planning</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#faq">Frequently Asked Questions About MariaDB 12.3</a></li>
<li><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/#conclusion">Conclusion</a></li>
</ol>
</div>
<h2>Why MariaDB 12.3 Is a Structural Release, Not a Point Upgrade<a class="anchor-link" id="why-mariadb-12-3-is-a-structural-release-not-a-point-upgrade"></a></h2>
<p>MariaDB moved to a rolling-plus-LTS cadence several years ago. Rolling releases (12.0, 12.1, 12.2, and now the 13.0 RC line) carry features forward quickly; LTS releases consolidate them into a five-year maintenance window. MariaDB 12.3 is the consolidation point for everything merged since 11.8, which is why the delta looks unusually large.</p>
<p>Three of those changes alter architecture rather than syntax:</p>
<ol>
<li><strong>The binary log can now live inside InnoDB.</strong> That removes two-phase commit between the log and the storage engine, collapses the fsync budget of a commit, and makes the binlog crash-safe by inheritance rather than by configuration.</li>
<li><strong>The optimizer is now steerable.</strong> A comprehensive hint vocabulary lands in the parser, so query plans can be pinned per statement instead of per session or per server.</li>
<li><strong>Galera state transfer and conflict handling got cheaper.</strong> Incremental State Transfers skip redundant foreign key validation, and write-set application can be retried instead of aborting the applier.</li>
</ol>
<p>Add the metadata-lock scalability work, the segmented Aria page cache, parallel replication between two Galera clusters, and buffered audit logging, and MariaDB 12.3 becomes the first release in years where the default HA topology should be re-evaluated from first principles.</p>
<h2>The Headline Advancement in MariaDB 12.3: InnoDB-Based Binary Log<a class="anchor-link" id="the-headline-advancement-in-mariadb-12-3-innodb-based-binary-log"></a></h2>
<p>Historically MariaDB treated the binary log and InnoDB as two independent durable resources. Every commit therefore ran a two-phase commit protocol: prepare in InnoDB, write and sync the binlog, then commit in InnoDB. With <code>sync_binlog=1</code> and <code>innodb_flush_log_at_trx_commit=1</code>, a durable commit could cost multiple fsync operations and required a recovery-time reconciliation between the two logs after a crash.</p>
<p>From MariaDB 12.3 the binary log can instead be stored in InnoDB-managed, page-structured files that participate in the InnoDB redo log and crash recovery. The files still live on disk as discrete objects with an <code>.ibb</code> extension, but internally they are 16 KB pages with a CRC32 checksum per page, pre-allocated to <code>max_binlog_size</code> (1 GB by default) so that write amplification from file extension disappears.</p>
<h3>What actually changes in the commit path<a class="anchor-link" id="what-actually-changes-in-the-commit-path"></a></h3>
<p>The engine no longer needs to coordinate two logs, so the expensive cross-resource handshake disappears. At <code>innodb_flush_log_at_trx_commit=1</code> a commit performs a single coordinated fsync instead of the several required by the old protocol. Because the binlog and InnoDB always recover to a mutually consistent state, you can also run with <code>innodb_flush_log_at_trx_commit=0</code> or <code>2</code> and still guarantee that the binary log never diverges from table data after a crash &ndash; you are trading durability of the last few commits for throughput, not risking replication divergence.</p>
<p>Two long-standing configuration knobs become obsolete in this mode. <code>sync_binlog</code> is no longer required because crash safety is inherited from InnoDB, and <code>binlog_checksum</code> is unused because every page carries a CRC32 already. If you want integrity on the wire between primary and replica, enable TLS on the replication channel instead.</p>
<h3>Positioning becomes GTID-only<a class="anchor-link" id="positioning-becomes-gtid-only"></a></h3>
<p>The new implementation is GTID-native. There are no <code>.index</code> files, no GTID index files and no <code>.state</code> file. Instead the binary log periodically embeds GTID state records inside itself, by default every 2 MB, and that interval must be a power-of-two multiple of the 16 KB page size. When a replica connects, or when the server restarts, the log is scanned backwards from the most recent state record to recover the correct GTID position. </p>
<p>The practical consequence is that file-and-offset replication coordinates no longer exist on a primary running the InnoDB binlog: <code>SHOW BINLOG EVENTS</code> will generally report offsets of zero and you must navigate by GTID. The status counters <code>binlog_gtid_index_hit</code> and <code>binlog_gtid_index_miss</code> are also retired in this mode.</p>
<h3>Backup semantics improve<a class="anchor-link" id="backup-semantics-improve"></a></h3>
<p>Because the binlog is now InnoDB data, <code>mariadb-backup</code> includes it in a transactionally consistent way by default. That resolves a long-standing gap where physical backups and binary logs were captured by different mechanisms with slightly different consistency points, which is exactly the seam where point-in-time recovery used to fail during real incidents.</p>
<h3>When you should not enable it<a class="anchor-link" id="when-you-should-not-enable-it"></a></h3>
<p>The InnoDB-based binary log is not a universal default. Stay on the traditional implementation if any of the following apply to your MariaDB 12.3 deployment:</p>
<ul>
<li>You run <strong>Galera Cluster</strong>. Synchronous multi-master requires the classic binlog implementation.</li>
<li>Applications or tooling depend on <strong>filename/offset replication positions</strong> rather than GTIDs.</li>
<li>Third-party CDC pipelines, such as change-data-capture connectors, <strong>parse binlog files directly</strong> from disk.</li>
<li>Your replicas are still below MariaDB 12.3. Upgrade replicas first, then switch the primary.</li>
</ul>
<p>Note also that the relay log on replicas is unchanged and still uses the traditional format, so the improvement applies to the write path of the primary, not to the apply path of the replica.</p>
<figure class="mdb-figure">
<div class="mdb-svgwrap">
<svg viewbox="0 0 1200 430" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="binlogTitle binlogDesc">
<title>MariaDB 12.3 commit path: traditional two-phase commit binary log versus InnoDB-based binary log</title>
<desc>Animated comparison showing the traditional binary log requiring multiple fsync operations across a two-phase commit, versus the MariaDB 12.3 InnoDB-based binary log requiring a single coordinated fsync.</desc>
<defs>
<lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#7f1d1d"></stop><stop offset="100%" stop-color="#b91c1c"></stop></lineargradient>
<lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#065f46"></stop><stop offset="100%" stop-color="#10b981"></stop></lineargradient>
<filter><fegaussianblur stddeviation="3" result="b"></fegaussianblur><femerge><femergenode in="b"></femergenode><femergenode in="SourceGraphic"></femergenode></femerge></filter>
</defs>
<text x="600" y="34" text-anchor="middle" fill="#e2e8f0" font-family="Segoe UI,Helvetica,Arial,sans-serif" font-size="21" font-weight="700">MariaDB 12.3 Commit Path: Two-Phase Commit vs InnoDB-Based Binary Log</text>
<p><g font-family="Segoe UI,Helvetica,Arial,sans-serif">
  <rect x="40" y="70" width="1120" height="150" rx="12" fill="#111827" stroke="#7f1d1d" stroke-width="1.5"></rect>
  <text x="60" y="98" fill="#fca5a5" font-size="15" font-weight="700">LEGACY &mdash; binlog + InnoDB as two durable resources (XA / 2PC)</text>
  <line x1="70" y1="170" x2="1130" y2="170" stroke="#334155" stroke-width="2"></line>
  <g fill="url(#lgOld)">
    <rect x="90" y="128" width="150" height="42" rx="8"></rect><rect x="300" y="128" width="170" height="42" rx="8"></rect>
    <rect x="530" y="128" width="170" height="42" rx="8"></rect><rect x="760" y="128" width="170" height="42" rx="8"></rect>
    <rect x="960" y="128" width="150" height="42" rx="8"></rect>
  </g>
  <g fill="#fee2e2" font-size="12.5" text-anchor="middle">
    <text x="165" y="154">InnoDB PREPARE</text><text x="385" y="154">fsync redo log</text>
    <text x="615" y="154">write binlog event</text><text x="845" y="154">fsync binlog</text>
    <text x="1035" y="154">InnoDB COMMIT</text>
  </g>
  <g fill="#fca5a5" font-size="11.5" text-anchor="middle">
    <text x="385" y="192">fsync #1</text><text x="845" y="192">fsync #2</text><text x="1035" y="192">fsync #3 (group)</text>
  </g>
  <circle r="8" fill="#fbbf24" filter="url(#glow2)">
    <animatemotion dur="5s" repeatcount="indefinite" path="M 95,149 L 1110,149"></animatemotion>
  </circle>
</g></p>
<p><g font-family="Segoe UI,Helvetica,Arial,sans-serif">
  <rect x="40" y="245" width="1120" height="150" rx="12" fill="#111827" stroke="#10b981" stroke-width="1.5"></rect>
  <text x="60" y="273" fill="#6ee7b7" font-size="15" font-weight="700">MariaDB 12.3 &mdash; binlog stored in InnoDB page files (.ibb), one durable resource</text>
  <line x1="70" y1="345" x2="1130" y2="345" stroke="#334155" stroke-width="2"></line>
  <g fill="url(#lgNew)">
    <rect x="120" y="303" width="220" height="42" rx="8"></rect>
    <rect x="430" y="303" width="290" height="42" rx="8"></rect>
    <rect x="810" y="303" width="250" height="42" rx="8"></rect>
  </g>
  <g fill="#d1fae5" font-size="12.5" text-anchor="middle">
    <text x="230" y="329">write row events to .ibb pages</text>
    <text x="575" y="329">single coordinated fsync (redo + binlog)</text>
    <text x="935" y="329">COMMIT visible &amp; crash-safe</text>
  </g>
  <g fill="#6ee7b7" font-size="11.5" text-anchor="middle">
    <text x="575" y="367">fsync #1 &mdash; and only #1</text>
    <text x="935" y="367">recovery inherited from InnoDB</text>
  </g>
  <circle r="8" fill="#34d399" filter="url(#glow2)">
    <animatemotion dur="2.2s" repeatcount="indefinite" path="M 125,324 L 1055,324"></animatemotion>
  </circle>
</g>
<text x="600" y="418" text-anchor="middle" fill="#94a3b8" font-family="Segoe UI,Helvetica,Arial,sans-serif" font-size="12.5">Animation speed is proportional to commit latency. sync_binlog and binlog_checksum are no longer required in the new mode.</text>
</p></svg>
</div><figcaption><strong>Figure 2.</strong> MariaDB 12.3 collapses the multi-fsync two-phase commit into a single coordinated flush by storing the binary log inside InnoDB.</figcaption></figure>
<h2>Optimizer Advancements in MariaDB 12.3<a class="anchor-link" id="optimizer-advancements-in-mariadb-12-3"></a></h2>
<p>The second structural change in MariaDB 12.3 is that query plans became controllable at statement granularity. Until now, plan stability on MariaDB was largely a matter of session variables, <code>optimizer_switch</code> bitmasks and index hints. That is a blunt instrument in a multi-tenant fleet where one report can destabilise an OLTP workload.</p>
<h3>A complete optimizer hint vocabulary<a class="anchor-link" id="a-complete-optimizer-hint-vocabulary"></a></h3>
<p>MariaDB 12.3 ships a MySQL-compatible hint framework covering access methods, join strategy, join order, subquery handling and execution limits:</p>
<ul>
<li><strong>Access and algorithm hints:</strong> <code>NO_RANGE_OPTIMIZATION</code>, <code>NO_ICP</code>, <code>MRR</code> / <code>NO_MRR</code>, <code>BKA</code> / <code>NO_BKA</code>, <code>BNL</code> / <code>NO_BNL</code>, <code>[NO_]ROWID_FILTER</code>, <code>[NO_]INDEX_MERGE</code>.</li>
<li><strong>Index hints:</strong> <code>[NO_]INDEX</code>, <code>[NO_]JOIN_INDEX</code>, <code>[NO_]GROUP_INDEX</code>, <code>[NO_]ORDER_INDEX</code>.</li>
<li><strong>Join order hints:</strong> <code>JOIN_FIXED_ORDER</code>, <code>JOIN_ORDER</code>, <code>JOIN_PREFIX</code>, <code>JOIN_SUFFIX</code>.</li>
<li><strong>Subquery hints:</strong> <code>SEMIJOIN</code>, <code>SUBQUERY</code>, <code>[NO_]SPLIT_MATERIALIZED</code>, <code>[NO_]DERIVED_CONDITION_PUSHDOWN</code>, <code>[NO_]MERGE</code>.</li>
<li><strong>Guardrails:</strong> <code>MAX_EXECUTION_TIME</code>, plus <code>QB_NAME</code> and implicit query block names so hints can target a specific block of a nested statement.</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="">-- Pin a report query without touching global optimizer_switch
SELECT /*+ QB_NAME(agg) MAX_EXECUTION_TIME(4000) NO_BNL(o) JOIN_PREFIX(c, o) */
       c.region, SUM(o.amount)
FROM   customers c
JOIN   orders o ON o.customer_id = c.id
WHERE  o.created_at &gt;= NOW() - INTERVAL 1 DAY
GROUP  BY c.region;</pre>
<p>For internet-scale fleets this is the difference between a plan regression that pages an on-call engineer and one that is contained inside a single statement. Combine it with the extended optimizer trace, which in MariaDB 12.3 can record table and view definitions via <code>optimizer_record_context</code>, and post-incident plan forensics becomes reproducible.</p>
<h3>Reverse-ordered scans and DESC key parts<a class="anchor-link" id="reverse-ordered-scans-and-desc-key-parts"></a></h3>
<p>Several optimizations that previously fired only on forward scans now work in reverse order. Rowid filtering and Index Condition Pushdown both apply to reverse-ordered scans, and the loose index scan used for <code>GROUP BY</code> can now exploit indexes with <code>DESC</code> key parts. Descending-order pagination on time-series tables &ndash; the classic <code>ORDER BY created_at DESC LIMIT n</code> pattern &ndash; is the workload that benefits most. Optimizations for <code>GROUP BY</code> and <code>ORDER BY</code> can also use indexes defined on virtual columns, which finally makes generated-column indexing a first-class tuning technique. For a deeper treatment of join planning, see our analysis of the <a href="https://minervadb.com/mariadb-join-optimizer/">MariaDB join optimizer</a>.</p>
<h3>Engine-level throughput work<a class="anchor-link" id="engine-level-throughput-work"></a></h3>
<p>Metadata lock scalability was reworked, which matters on servers with tens of thousands of tables and aggressive DDL. The Aria storage engine gained a segmented key cache controlled by <code>aria_pagecache_segments</code> (default 1, maximum 128), reducing mutex contention on internal and temporary tables. Vector distance calculation was accelerated through extrapolation, which is relevant if you are using MariaDB as a vector store; we covered the storage layout in <a href="https://minervadb.com/understanding-vector-indexes-in-mariadb/">understanding vector indexes in MariaDB</a>.</p>
<h2>Replication and Galera Cluster Improvements in MariaDB 12.3<a class="anchor-link" id="replication-and-galera-cluster-improvements-in-mariadb-12-3"></a></h2>
<p>Asynchronous replication between two Galera clusters can now use parallel replication, governed by <code>slave_parallel_threads</code>. This is the topology most large deployments actually run: a synchronous cluster per region, stitched together asynchronously across regions. Until now, the cross-region channel was effectively single-threaded and became the ceiling on write throughput for the whole estate. Removing that ceiling changes regional DR from best-effort to viable.</p>
<p>Other replication-layer changes with operational weight:</p>
<ul>
<li>Row events larger than <code>max_packet_size</code> are now fragmented rather than failing the channel &ndash; the historical cause of stalled replication on wide BLOB writes.</li>
<li>Defaults for <code>MASTER_SSL_*</code> are configurable, so TLS on replication channels can be enforced fleet-wide instead of per <code>CHANGE MASTER</code> statement.</li>
<li>Temporary table behaviour in replication is now predictable and controlled by <code>create_tmp_table_binlog_formats</code>.</li>
<li><code>show_slave_auth_info</code> and <code>replicate_same_server_id</code> are proper system variables rather than start-up options only, and the server reports whether it started with <code>skip-slave-start</code>.</li>
</ul>
<p>On the Galera side, Incremental State Transfers no longer perform needless foreign key checks, which materially shortens the window during which a rejoining node loads the cluster. Write-set application can be retried instead of aborting, controlled by <code>wsrep_applier_retry_count</code>, which reduces spurious node evictions under hot-row contention. Packaging also changed: the Galera dependency has been removed from the server packages, so Galera is now an explicit install rather than an implicit one. If you operate Galera in production, our field notes on <a href="https://minervadb.com/troubleshooting-writes-in-galera-cluster/">troubleshooting writes in Galera Cluster</a> and <a href="https://minervadb.com/mariadb-galera-cluster-monitoring/">MariaDB Galera Cluster monitoring</a> pair directly with these changes.</p>
<h2>Security, Compatibility and Developer Surface in MariaDB 12.3<a class="anchor-link" id="security-compatibility-and-developer-surface-in-mariadb-12-3"></a></h2>
<p>Security work in MariaDB 12.3 is focused on key material and identity. Passphrase-protected TLS keys are supported through the new <code>ssl_passphrase</code> system variable, the file key management plugin for transparent data encryption supports SHA-256 on Linux, and <code>SET SESSION AUTHORIZATION</code> allows a privileged session to execute as another user &ndash; a cleaner primitive for connection-pool multiplexing and for auditing than credential sharing. </p>
<p><code>DROP USER</code> now warns when the account still has live sessions, and fails outright in Oracle mode. The audit plugin gained buffered logging via <code>server_audit_file_buffer_size</code>, records the client host and port rather than host alone, and reports the negotiated TLS version. That combination makes audit logging viable on high-QPS nodes where it was previously too expensive; see our guide to <a href="https://minervadb.com/mariadb-user-activity-logging/">MariaDB user activity logging</a>.</p>
<p>The compatibility surface widened significantly. MariaDB 12.3 adds the <code>caching_sha2_password</code> authentication plugin for MySQL clients, Oracle-style <code>TO_DATE()</code>, <code>TO_NUMBER()</code> and <code>TRUNC()</code>, the Oracle <code>(+)</code> outer-join operator in Oracle mode, associative arrays through <code>DECLARE TYPE ... TABLE OF ... INDEX BY</code>, weak <code>SYS_REFCURSOR</code> support with a <code>max_open_cursors</code> ceiling, cursors on prepared statements, the SQL standard <code>SET PATH</code> statement and <code>IS JSON</code> predicate, a basic XML data type, and the ability for <code>UPDATE</code> and <code>DELETE</code> to read from a CTE. Triggers can now fire on multiple events, foreign key constraint names only need to be unique per table, and the 32-level depth limit on JSON functions is gone. Nine new GIS functions improve MySQL 8 parity.</p>
<p>Six CVEs were fixed in the 12.3.2 GA build, the most severe carrying a CVSS v3.1 base score of 8.0. That alone justifies scheduling the upgrade rather than deferring it.</p>
<h2>Reference Architecture: Highly Available MariaDB 12.3 at Internet Scale<a class="anchor-link" id="reference-architecture-highly-available-mariadb-12-3-at-internet-scale"></a></h2>
<p>Availability is not a feature you enable; it is a property that emerges from how you arrange failure domains. The architecture below is the pattern MinervaDB deploys for workloads that must tolerate the loss of a node, a rack, an availability zone or an entire region without data loss and without a maintenance window. It layers synchronous replication inside a region for zero-RPO, asynchronous GTID replication across regions for geographic survivability, and a routing tier that makes failover invisible to the application.</p>
<figure class="mdb-figure">
<div class="mdb-svgwrap">
<svg viewbox="0 0 1240 880" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="haTitle haDesc">
<title>MariaDB 12.3 highly available and fault tolerant reference architecture for internet scale</title>
<desc>Three-region MariaDB 12.3 architecture: GeoDNS and stateless application tier feeding MaxScale routing layers, three-node Galera clusters per region across availability zones, cross-region asynchronous GTID replication, object storage backups with point in time recovery, Kubernetes operator control plane and observability.</desc>
<defs>
<lineargradient x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#1e3a8a"></stop><stop offset="100%" stop-color="#1d4ed8"></stop></lineargradient>
<lineargradient x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#134e4a"></stop><stop offset="100%" stop-color="#0f766e"></stop></lineargradient>
<lineargradient x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#4c1d95"></stop><stop offset="100%" stop-color="#6d28d9"></stop></lineargradient>
<lineargradient x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="#0ea5e9"></stop><stop offset="50%" stop-color="#6366f1"></stop><stop offset="100%" stop-color="#0ea5e9"></stop></lineargradient>
<filter><fegaussianblur stddeviation="4" result="bl"></fegaussianblur><femerge><femergenode in="bl"></femergenode><femergenode in="SourceGraphic"></femergenode></femerge></filter>
<marker viewbox="0 0 10 10" refx="9" refy="5" markerwidth="7" markerheight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#7dd3fc"></path></marker>
<marker viewbox="0 0 10 10" refx="9" refy="5" markerwidth="7" markerheight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#34d399"></path></marker>
</defs>
<g font-family="Segoe UI,Helvetica,Arial,sans-serif">
<text x="620" y="36" text-anchor="middle" fill="#f1f5f9" font-size="23" font-weight="700">MariaDB 12.3 LTS &mdash; Internet-Scale, Fault-Tolerant Reference Architecture</text>
<text x="620" y="60" text-anchor="middle" fill="#94a3b8" font-size="13.5">3 regions &middot; 9 synchronous nodes &middot; MaxScale routing &middot; cross-region async GTID &middot; object-storage PITR &middot; zero-RPO in region</text>
<p><rect x="50" y="78" width="1140" height="48" rx="10" fill="url(#gBar)" opacity="0.92"></rect>
<text x="620" y="107" text-anchor="middle" fill="#f8fafc" font-size="14.5" font-weight="600">Global Traffic Management &mdash; GeoDNS / Anycast GSLB &middot; health-checked &middot; latency-based steering &middot; regional drain</text></p>
<p><rect x="50" y="140" width="1140" height="46" rx="10" fill="#0f172a" stroke="#334155"></rect>
<text x="620" y="168" text-anchor="middle" fill="#cbd5e1" font-size="13.5">Stateless Application Tier &mdash; bounded connection pools &middot; idempotent writes &middot; exponential backoff with jitter &middot; circuit breakers</text></p>
<p><g stroke="#7dd3fc" stroke-width="2" fill="none" marker-end="url(#arw)" opacity="0.85">
  <path d="M 230 188 L 230 214" stroke-dasharray="6 5"><animate attributename="stroke-dashoffset" values="22;0" dur="1.1s" repeatcount="indefinite"></animate></path>
  <path d="M 620 188 L 620 214" stroke-dasharray="6 5"><animate attributename="stroke-dashoffset" values="22;0" dur="1.1s" repeatcount="indefinite"></animate></path>
  <path d="M 1010 188 L 1010 214" stroke-dasharray="6 5"><animate attributename="stroke-dashoffset" values="22;0" dur="1.1s" repeatcount="indefinite"></animate></path>
</g></p>
<p><g>
  <rect x="50" y="216" width="360" height="386" rx="14" fill="#0b1224" stroke="#1d4ed8" stroke-width="2"></rect>
  <text x="68" y="242" fill="#93c5fd" font-size="13.5" font-weight="700">REGION A &mdash; us-east &middot; ACTIVE / WRITE</text>
  <rect x="66" y="254" width="328" height="60" rx="9" fill="#111c3a" stroke="#3b82f6"></rect>
  <text x="230" y="275" text-anchor="middle" fill="#dbeafe" font-size="12.5" font-weight="600">MaxScale x2 (VIP / keepalived)</text>
  <text x="230" y="294" text-anchor="middle" fill="#93c5fd" font-size="11">readwritesplit &middot; causal_reads &middot; transaction replay &middot; auto-failover</text>
  <rect x="66" y="326" width="328" height="196" rx="9" fill="#0d1730" stroke="#3b82f6" stroke-dasharray="4 4"></rect>
  <text x="230" y="346" text-anchor="middle" fill="#bfdbfe" font-size="12" font-weight="600">MariaDB 12.3 Galera Cluster &mdash; segment 0</text>
  <g fill="url(#gA)" stroke="#60a5fa">
    <rect x="78" y="358" width="98" height="86" rx="8"></rect><rect x="181" y="358" width="98" height="86" rx="8"></rect><rect x="284" y="358" width="98" height="86" rx="8"></rect>
  </g>
  <g fill="#eff6ff" font-size="11.5" text-anchor="middle">
    <text x="127" y="382">AZ-a</text><text x="230" y="382">AZ-b</text><text x="333" y="382">AZ-c</text>
  </g>
  <g fill="#bfdbfe" font-size="10.5" text-anchor="middle">
    <text x="127" y="399">node-1</text><text x="230" y="399">node-2</text><text x="333" y="399">node-3</text>
    <text x="127" y="435">primary</text><text x="230" y="435">sync</text><text x="333" y="435">sync</text>
  </g>
  <g fill="none" stroke="#38bdf8">
    <circle cx="127" cy="415" r="5" fill="#38bdf8"><animate attributename="r" values="4;14;4" dur="2.4s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.4s" repeatcount="indefinite"></animate></circle>
    <circle cx="230" cy="415" r="5" fill="#38bdf8"><animate attributename="r" values="4;14;4" dur="2.4s" begin="0.8s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.4s" begin="0.8s" repeatcount="indefinite"></animate></circle>
    <circle cx="333" cy="415" r="5" fill="#38bdf8"><animate attributename="r" values="4;14;4" dur="2.4s" begin="1.6s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.4s" begin="1.6s" repeatcount="indefinite"></animate></circle>
  </g>
  <g stroke="#7dd3fc" stroke-width="1.8" fill="none" stroke-dasharray="5 4">
    <path d="M 176 460 L 181 460"></path>
    <path d="M 100 452 C 100 474, 360 474, 360 452"><animate attributename="stroke-dashoffset" values="36;0" dur="1.6s" repeatcount="indefinite"></animate></path>
  </g>
  <text x="230" y="496" text-anchor="middle" fill="#7dd3fc" font-size="10.5">wsrep quorum &middot; pc.weight &middot; gmcast.segment &middot; fast IST rejoin</text>
  <text x="230" y="512" text-anchor="middle" fill="#64748b" font-size="10.5">SST via mariadb-backup &middot; gcache sized for full AZ outage</text>
  <text x="68" y="542" fill="#94a3b8" font-size="11">Durability: innodb_flush_log_at_trx_commit=1, O_DIRECT, NVMe</text>
  <text x="68" y="560" fill="#94a3b8" font-size="11">Binlog: traditional format (Galera requirement in 12.3)</text>
  <text x="68" y="578" fill="#94a3b8" font-size="11">RPO in-region: 0 &middot; RTO on node loss: &lt; 5 s (proxy-side)</text>
</g></p>
<p><g>
  <rect x="440" y="216" width="360" height="386" rx="14" fill="#08161a" stroke="#0f766e" stroke-width="2"></rect>
  <text x="458" y="242" fill="#5eead4" font-size="13.5" font-weight="700">REGION B &mdash; eu-west &middot; WARM STANDBY / DR</text>
  <rect x="456" y="254" width="328" height="60" rx="9" fill="#0b2422" stroke="#14b8a6"></rect>
  <text x="620" y="275" text-anchor="middle" fill="#ccfbf1" font-size="12.5" font-weight="600">MaxScale x2 (idle writer, live readers)</text>
  <text x="620" y="294" text-anchor="middle" fill="#5eead4" font-size="11">promotion runbook &middot; scripted switchover &middot; STONITH fencing</text>
  <rect x="456" y="326" width="328" height="196" rx="9" fill="#08201e" stroke="#14b8a6" stroke-dasharray="4 4"></rect>
  <text x="620" y="346" text-anchor="middle" fill="#99f6e4" font-size="12" font-weight="600">MariaDB 12.3 Galera Cluster &mdash; segment 1</text>
  <g fill="url(#gB)" stroke="#2dd4bf">
    <rect x="468" y="358" width="98" height="86" rx="8"></rect><rect x="571" y="358" width="98" height="86" rx="8"></rect><rect x="674" y="358" width="98" height="86" rx="8"></rect>
  </g>
  <g fill="#f0fdfa" font-size="11.5" text-anchor="middle"><text x="517" y="382">AZ-a</text><text x="620" y="382">AZ-b</text><text x="723" y="382">AZ-c</text></g>
  <g fill="#99f6e4" font-size="10.5" text-anchor="middle">
    <text x="517" y="399">node-4</text><text x="620" y="399">node-5</text><text x="723" y="399">node-6</text>
    <text x="517" y="435">applier</text><text x="620" y="435">sync</text><text x="723" y="435">sync</text>
  </g>
  <g>
    <circle cx="517" cy="415" r="5" fill="#2dd4bf"><animate attributename="r" values="4;13;4" dur="2.8s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.8s" repeatcount="indefinite"></animate></circle>
    <circle cx="620" cy="415" r="5" fill="#2dd4bf"><animate attributename="r" values="4;13;4" dur="2.8s" begin="0.9s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.8s" begin="0.9s" repeatcount="indefinite"></animate></circle>
    <circle cx="723" cy="415" r="5" fill="#2dd4bf"><animate attributename="r" values="4;13;4" dur="2.8s" begin="1.8s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="2.8s" begin="1.8s" repeatcount="indefinite"></animate></circle>
  </g>
  <text x="620" y="496" text-anchor="middle" fill="#5eead4" font-size="10.5">parallel apply of cross-region stream (slave_parallel_threads)</text>
  <text x="620" y="512" text-anchor="middle" fill="#64748b" font-size="10.5">wsrep_applier_retry_count softens hot-row conflicts</text>
  <text x="458" y="542" fill="#94a3b8" font-size="11">Target RTO on region loss: &lt; 90 s (DNS + promotion)</text>
  <text x="458" y="560" fill="#94a3b8" font-size="11">Target RPO on region loss: &lt; 1 s (async lag budget)</text>
  <text x="458" y="578" fill="#94a3b8" font-size="11">Continuously restore-tested from object storage</text>
</g></p>
<p><g>
  <rect x="830" y="216" width="360" height="386" rx="14" fill="#150b26" stroke="#6d28d9" stroke-width="2"></rect>
  <text x="848" y="242" fill="#c4b5fd" font-size="13.5" font-weight="700">REGION C &mdash; ap-south &middot; READ SCALE-OUT</text>
  <rect x="846" y="254" width="328" height="60" rx="9" fill="#1c1033" stroke="#8b5cf6"></rect>
  <text x="1010" y="275" text-anchor="middle" fill="#ede9fe" font-size="12.5" font-weight="600">MaxScale read router (read-only service)</text>
  <text x="1010" y="294" text-anchor="middle" fill="#c4b5fd" font-size="11">max_slave_replication_lag &middot; sticky sessions for read-your-write</text>
  <rect x="846" y="326" width="328" height="196" rx="9" fill="#170e2b" stroke="#8b5cf6" stroke-dasharray="4 4"></rect>
  <text x="1010" y="346" text-anchor="middle" fill="#ddd6fe" font-size="12" font-weight="600">Asynchronous GTID replicas (MariaDB 12.3)</text>
  <g fill="url(#gC)" stroke="#a78bfa">
    <rect x="858" y="358" width="150" height="86" rx="8"></rect><rect x="1014" y="358" width="150" height="86" rx="8"></rect>
  </g>
  <g fill="#f5f3ff" font-size="11.5" text-anchor="middle"><text x="933" y="382">read replica</text><text x="1089" y="382">delayed replica</text></g>
  <g fill="#ddd6fe" font-size="10.5" text-anchor="middle">
    <text x="933" y="399">analytics + BI</text><text x="1089" y="399">MASTER_DELAY = 900</text>
    <text x="933" y="435">parallel apply</text><text x="1089" y="435">logical-corruption shield</text>
  </g>
  <circle cx="933" cy="415" r="5" fill="#a78bfa"><animate attributename="r" values="4;13;4" dur="3.2s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="3.2s" repeatcount="indefinite"></animate></circle>
  <circle cx="1089" cy="415" r="5" fill="#a78bfa"><animate attributename="r" values="4;13;4" dur="3.2s" begin="1.1s" repeatcount="indefinite"></animate><animate attributename="opacity" values="1;0.15;1" dur="3.2s" begin="1.1s" repeatcount="indefinite"></animate></circle>
  <text x="1010" y="496" text-anchor="middle" fill="#c4b5fd" font-size="10.5">InnoDB-based binlog candidates &mdash; GTID-only, no Galera</text>
  <text x="1010" y="512" text-anchor="middle" fill="#64748b" font-size="10.5">single coordinated fsync per commit on the write path</text>
  <text x="848" y="542" fill="#94a3b8" font-size="11">Serves regional read traffic at &lt; 20 ms p99</text>
  <text x="848" y="560" fill="#94a3b8" font-size="11">Never promoted to writer &mdash; capacity, not availability</text>
  <text x="848" y="578" fill="#94a3b8" font-size="11">Delayed replica rewinds human error without full restore</text>
</g></p>
<p><g>
  <rect x="50" y="618" width="1140" height="58" rx="10" fill="#0f172a" stroke="#10b981" stroke-width="1.5"></rect>
  <text x="70" y="640" fill="#6ee7b7" font-size="12.5" font-weight="700">CROSS-REGION ASYNCHRONOUS GTID REPLICATION</text>
  <text x="70" y="662" fill="#94a3b8" font-size="11">Galera-to-Galera with parallel replication &middot; TLS-enforced channels &middot; fragmented large row events &middot; monitored lag budget</text>
  <path d="M 380 668 L 1150 668" stroke="#10b981" stroke-width="2.5" fill="none" stroke-dasharray="8 6" marker-end="url(#arwG)">
    <animate attributename="stroke-dashoffset" values="56;0" dur="1.4s" repeatcount="indefinite"></animate>
  </path>
  <circle r="6" fill="#34d399" filter="url(#softGlow)"><animatemotion dur="3s" repeatcount="indefinite" path="M 380 668 L 1150 668"></animatemotion></circle>
  <circle r="6" fill="#34d399" filter="url(#softGlow)"><animatemotion dur="3s" begin="1.5s" repeatcount="indefinite" path="M 380 668 L 1150 668"></animatemotion></circle>
  <g stroke="#10b981" stroke-width="1.6" fill="none" stroke-dasharray="4 4" opacity="0.8">
    <path d="M 230 604 L 230 618"></path><path d="M 620 604 L 620 618"></path><path d="M 1010 604 L 1010 618"></path>
  </g>
</g></p>
<p><g font-size="11">
  <rect x="50" y="694" width="360" height="132" rx="12" fill="#0b1224" stroke="#334155"></rect>
  <text x="68" y="716" fill="#f1f5f9" font-size="12.5" font-weight="700">BACKUP, PITR &amp; DR</text>
  <text x="68" y="738" fill="#94a3b8">mariadb-backup full + incremental, streamed</text>
  <text x="68" y="756" fill="#94a3b8">S3 with versioning, object-lock, cross-region copy</text>
  <text x="68" y="774" fill="#94a3b8">InnoDB log archiving for point-in-time recovery</text>
  <text x="68" y="792" fill="#94a3b8">Binlogs captured consistently (InnoDB binlog mode)</text>
  <text x="68" y="810" fill="#94a3b8">Automated monthly restore drills, timed against RTO</text></g></p>
<p>  <rect x="440" y="694" width="360" height="132" rx="12" fill="#0b1224" stroke="#334155"></rect>
  <text x="458" y="716" fill="#f1f5f9" font-size="12.5" font-weight="700">KUBERNETES CONTROL PLANE</text>
  <text x="458" y="738" fill="#94a3b8">mariadb-operator CRDs: MariaDB, MaxScale, Backup</text>
  <text x="458" y="756" fill="#94a3b8">Rolling update: ReplicasFirstPrimaryLast</text>
  <text x="458" y="774" fill="#94a3b8">Blue/green multi-cluster upgrades, zero downtime</text>
  <text x="458" y="792" fill="#94a3b8">VolumeSnapshot physical backups, volume expansion</text>
  <text x="458" y="810" fill="#94a3b8">Automated TLS issuance and rotation, maintenance mode</text></p>
<p>  <rect x="830" y="694" width="360" height="132" rx="12" fill="#0b1224" stroke="#334155"></rect>
  <text x="848" y="716" fill="#f1f5f9" font-size="12.5" font-weight="700">OBSERVABILITY &amp; SLOs</text>
  <text x="848" y="738" fill="#94a3b8">wsrep_flow_control_paused &lt; 0.02</text>
  <text x="848" y="756" fill="#94a3b8">wsrep_local_recv_queue_avg &lt; 0.5</text>
  <text x="848" y="774" fill="#94a3b8">Seconds_Behind_Master p99 &lt; 1 s cross-region</text>
  <text x="848" y="792" fill="#94a3b8">Commit latency p99, fsync rate, history list length</text>
  <text x="848" y="810" fill="#94a3b8">Error budget burn alerts, not threshold spam</text>
</p></g>
<p><text x="620" y="856" text-anchor="middle" fill="#64748b" font-size="11.5">Pulsing nodes indicate live certification traffic; the green lane animates the cross-region GTID stream. Zero RPO inside a region, sub-second RPO across regions.</text>

</p></svg>
</div><figcaption><strong>Figure 3.</strong> Interactive view &mdash; a MariaDB 12.3 internet-scale topology: synchronous Galera inside each region, asynchronous GTID replication across regions, MaxScale routing, and object-storage backed point-in-time recovery.</figcaption></figure>
<h3>Tier 0 &mdash; Enumerate failure domains before choosing technology<a class="anchor-link" id="tier-0-enumerate-failure-domains-before-choosing-technology"></a></h3>
<p>Design starts by writing down what can fail and what each failure must cost you. A useful matrix for MariaDB 12.3 deployments looks like this:</p>
<table>
<thead>
<tr>
<th>Failure domain</th>
<th>Mechanism that absorbs it</th>
<th>Target RPO</th>
<th>Target RTO</th>
</tr>
</thead>
<tbody>
<tr>
<td>Single process / OOM kill</td>
<td>systemd restart + Galera IST rejoin</td>
<td>0</td>
<td>&lt; 30 s</td>
</tr>
<tr>
<td>Node or host failure</td>
<td>Galera quorum (2 of 3) + MaxScale re-route</td>
<td>0</td>
<td>&lt; 5 s</td>
</tr>
<tr>
<td>Rack / availability zone loss</td>
<td>One node per AZ, <code>gmcast.segment</code> awareness</td>
<td>0</td>
<td>&lt; 15 s</td>
</tr>
<tr>
<td>Region loss</td>
<td>Async GTID standby cluster + GSLB steering</td>
<td>&lt; 1 s</td>
<td>&lt; 90 s</td>
</tr>
<tr>
<td>Logical corruption / bad deploy</td>
<td>Delayed replica + PITR from object storage</td>
<td>Point-in-time</td>
<td>Minutes to hours</td>
</tr>
<tr>
<td>Storage / silent corruption</td>
<td>Page checksums, backup verification, restore drills</td>
<td>Point-in-time</td>
<td>Hours</td>
</tr>
</tbody>
</table>
<p>Notice that only two rows are solved by replication. The rest are solved by backups, delayed replicas and process discipline. Teams that equate high availability with clustering discover this during their first bad migration.</p>
<h3>Tier 1 &mdash; Synchronous replication inside the region<a class="anchor-link" id="tier-1-synchronous-replication-inside-the-region"></a></h3>
<p>Inside a region, use a three-node MariaDB 12.3 Galera cluster with exactly one node per availability zone. Three nodes is the minimum that tolerates the loss of one while retaining a majority; five nodes buys tolerance of two failures at the cost of higher certification latency, because every transaction must be replicated to every node before commit returns.</p>
<p>The parameters that decide whether the cluster is stable under load are not the ones people usually tune. Size <code>gcache.size</code> to cover the longest plausible node outage so a rejoining node can use an Incremental State Transfer rather than a full State Snapshot Transfer; a full SST on a multi-terabyte dataset is a self-inflicted outage. Set <code>wsrep_sst_method=mariabackup</code> so donors remain readable. </p>
<p>Use <code>gmcast.segment</code> so that inter-AZ traffic is not multiplied by the number of nodes. Tune <code>gcs.fc_limit</code> deliberately: flow control is Galera telling you the slowest node cannot keep up, and raising the limit hides the symptom while increasing the amount of data at risk. Finally, keep <code>wsrep_slave_threads</code> aligned with the number of independently writable tables rather than with CPU count.</p>
<p>MariaDB 12.3 improves two of the worst Galera failure modes directly. Incremental State Transfers no longer waste time re-validating foreign keys, so rejoin time drops on schemas with heavy referential integrity. And <code>wsrep_applier_retry_count</code> lets a node retry a conflicting write-set instead of dropping out of the cluster, which is exactly the behaviour you want on hot counters and sequence-like rows.</p>
<h3>Tier 2 &mdash; Asynchronous replication across regions<a class="anchor-link" id="tier-2-asynchronous-replication-across-regions"></a></h3>
<p>Synchronous replication across a wide-area link is an availability anti-pattern: it converts network latency into commit latency and a partition into an outage. Instead, replicate asynchronously from the active regional cluster to a standby cluster using GTIDs.</p>
<p>This is where MariaDB 12.3 changes the calculus. Asynchronous replication between two Galera clusters can now apply in parallel using <code>slave_parallel_threads</code>, so the cross-region channel no longer serialises the write throughput of an entire region. Combine it with <code>slave_parallel_mode=optimistic</code>, GTID-based <code>CHANGE MASTER TO ... master_use_gtid=slave_pos</code>, and enforced TLS defaults on the channel. Add a semi-synchronous acknowledgement with <code>rpl_semi_sync_master_wait_point=AFTER_SYNC</code> only if your RPO requirement is stricter than the replication lag you can actually sustain, and always configure a wait timeout so the primary degrades to asynchronous rather than stalling when the standby is unreachable. Our guide on <a href="https://minervadb.com/scaling-mariadb-horizontally/">horizontally scaling MariaDB</a> covers the read-scaling side of this topology.</p>
<h3>Tier 3 &mdash; The routing tier is what the application actually sees<a class="anchor-link" id="tier-3-the-routing-tier-is-what-the-application-actually-sees"></a></h3>
<p>Applications should never hold a connection to a database node. They connect to MaxScale, which owns topology knowledge and failure detection. Deploy at least two MaxScale instances behind a virtual IP or a Kubernetes service, configure the <code>readwritesplit</code> router, enable transaction replay so in-flight transactions survive a backend failover, and enable causal reads so a read issued immediately after a write is routed to a node that has applied it. Set <code>max_slave_replication_lag</code> to keep stale replicas out of the read pool.</p>
<p>MariaDB 12.3 also carries a connection redirection mechanism in the client/server protocol, which lets a proxy hand a client off to the correct node instead of proxying every packet. For very high connection counts this removes the proxy from the data path once routing is established. ProxySQL remains a valid alternative where query rewriting and fine-grained rule sets matter; see our notes on <a href="https://minervadb.com/troubleshooting-proxysql-01/">troubleshooting ProxySQL in high-velocity ingestion</a>.</p>
<h3>Tier 4 &mdash; Declarative operations on Kubernetes<a class="anchor-link" id="tier-4-declarative-operations-on-kubernetes"></a></h3>
<p>If you run on Kubernetes, the <a href="https://github.com/mariadb-operator/mariadb-operator" target="_blank" rel="noopener">mariadb-operator</a> expresses this whole topology as custom resources. It supports both asynchronous replication and synchronous Galera topologies, manages MaxScale as a first-class object, performs cluster-aware rolling updates that roll replica pods first and the primary last, and supports blue/green upgrades across two identical clusters so a version change becomes a traffic switch instead of a restart. Physical backups run through <code>mariadb-backup</code> and VolumeSnapshots, and binary log archiving enables point-in-time recovery. Our article on <a href="https://minervadb.com/tuning-mariadb-for-cloud/">tuning MariaDB for cloud and containerized environments</a> covers the resource-limit and storage-class decisions that make or break this layer.</p>
<h3>Tier 5 &mdash; Backups are the only true fault tolerance<a class="anchor-link" id="tier-5-backups-are-the-only-true-fault-tolerance"></a></h3>
<p>Replication propagates mistakes at the speed of the network. Backups do not. Run daily full plus hourly incremental <code>mariadb-backup</code> jobs streamed to object storage with versioning and object-lock enabled, replicate the bucket cross-region, and enable InnoDB log archiving so you can replay to a specific log sequence number. Under the InnoDB-based binary log, backups now include binlog files transactionally, which removes the historical seam between a physical backup and the binlog stream. </p>
<p>Then do the part most teams skip: restore automatically on a schedule, into an isolated environment, and measure the restore time against your stated RTO. A backup that has never been restored is a hypothesis. See our reference on <a href="https://minervadb.com/transfer-backed-up-data-to-a-mariadb-replica/">transferring backed-up data to a MariaDB replica</a>.</p>
<h2>Production Configuration Baselines for MariaDB 12.3<a class="anchor-link" id="production-configuration-baselines-for-mariadb-12-3"></a></h2>
<p>The following is a starting point for a 64-core, 512 GB, NVMe-backed MariaDB 12.3 node participating in a Galera cluster. Treat it as a hypothesis to be validated with your own workload, not a copy-paste answer.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="">[mariadb]
# --- Durability and memory ---
innodb_buffer_pool_size          = 360G
innodb_buffer_pool_instances     = 16
innodb_log_file_size             = 16G
innodb_flush_log_at_trx_commit   = 1
innodb_flush_method              = O_DIRECT
innodb_io_capacity               = 20000
innodb_io_capacity_max           = 40000
innodb_read_io_threads           = 16
innodb_write_io_threads          = 16
innodb_adaptive_hash_index       = OFF

# --- Replication (GTID everywhere) ---
log_bin
binlog_format                    = ROW
binlog_row_image                 = MINIMAL
gtid_strict_mode                 = ON
slave_parallel_threads           = 16
slave_parallel_mode              = optimistic
expire_logs_days                 = 7

# --- Galera ---
wsrep_on                         = ON
wsrep_provider                   = /usr/lib/galera/libgalera_smm.so
wsrep_sst_method                 = mariabackup
wsrep_slave_threads              = 16
wsrep_applier_retry_count        = 3
wsrep_provider_options           = "gcache.size=64G; gcs.fc_limit=64; gmcast.segment=0; evs.suspect_timeout=PT10S; evs.inactive_timeout=PT30S"

# --- Aria (internal + temporary tables) ---
aria_pagecache_buffer_size       = 8G
aria_pagecache_segments          = 16

# --- Observability and safety ---
server_audit_file_buffer_size    = 8M
max_execution_time               = 30000
optimizer_record_context         = ON</pre>
<p>On a non-Galera MariaDB 12.3 primary that uses GTID replication exclusively, you can additionally enable the InnoDB-based binary log and delete <code>sync_binlog</code> and <code>binlog_checksum</code> from your configuration entirely. Verify replica versions first.</p>
<h2>Observability: The SLIs That Actually Predict a MariaDB Outage<a class="anchor-link" id="observability-the-slis-that-actually-predict-a-mariadb-outage"></a></h2>
<p>Most MariaDB dashboards measure the wrong things. Query counts and buffer pool hit ratios are descriptive; they are rarely predictive. The signals below lead incidents by minutes to hours:</p>
<ul>
<li><strong><code>wsrep_flow_control_paused</code></strong> &mdash; the fraction of time the cluster was paused by flow control. Anything sustained above 0.02 means the slowest node is governing your write throughput.</li>
<li><strong><code>wsrep_local_recv_queue_avg</code></strong> &mdash; a rising average means an applier cannot keep up and a certification failure storm is likely.</li>
<li><strong><code>wsrep_cert_deps_distance</code></strong> &mdash; the achievable parallelism of your workload; it tells you whether raising <code>wsrep_slave_threads</code> would help at all.</li>
<li><strong>InnoDB history list length</strong> &mdash; unbounded growth means purge is behind, and purge lag is the silent precursor to a storage emergency.</li>
<li><strong>Commit latency p99 and fsync rate</strong> &mdash; the pair that reveals whether the InnoDB binary log change delivered the throughput it promised.</li>
<li><strong>Replication lag distribution, not average</strong> &mdash; cross-region RPO is defined by the tail, never by the mean.</li>
</ul>
<p>Alert on error-budget burn rate rather than static thresholds, and keep a runbook link in every alert. Our field guide to <a href="https://minervadb.com/mariadb-performance-monitoring-metrics/">MariaDB performance monitoring metrics</a> expands each of these into queries and thresholds, and <a href="https://minervadb.com/troubleshooting-mariadb-performance/">troubleshooting MariaDB performance</a> covers the diagnostic path once an alert fires.</p>
<h2>Failure Modes and Anti-Patterns to Avoid<a class="anchor-link" id="failure-modes-and-anti-patterns-to-avoid"></a></h2>
<ul>
<li><strong>Two-node Galera clusters.</strong> A two-node cluster has no majority; losing either node halts the survivor. Use three, or two plus a garbd arbitrator.</li>
<li><strong>Writing to all Galera nodes without partitioning.</strong> Multi-master is a topology, not a write strategy. Route writes for a given hot table to one node to avoid certification conflicts, even in MariaDB 12.3 where retries soften the impact.</li>
<li><strong>Raising <code>gcs.fc_limit</code> to silence flow control.</strong> You are increasing unreplicated data in flight while hiding the node that is actually slow.</li>
<li><strong>Enabling the InnoDB binary log on a Galera node.</strong> It is unsupported in that topology; verify before you roll a configuration change fleet-wide.</li>
<li><strong>Relying on <code>Seconds_Behind_Master</code> alone.</strong> It reports apply lag, not the transport gap. Use GTID position deltas and a heartbeat table.</li>
<li><strong>DDL without a plan.</strong> Schema changes in Galera can block the entire cluster under Total Order Isolation. Use Rolling Schema Upgrade or an online schema change tool with explicit checks.</li>
<li><strong>Untested failover.</strong> If you have not performed a switchover in the last quarter, your RTO is an estimate, not a commitment. Related reading: <a href="https://minervadb.com/mariadb-2025-high-availability-best-practices/">MariaDB high availability best practices</a> and <a href="https://minervadb.com/mariadb-deadlock-troubleshooting/">MariaDB deadlock troubleshooting</a>.</li>
</ul>
<h2>Upgrading from MariaDB 11.8 to MariaDB 12.3<a class="anchor-link" id="upgrading-from-mariadb-11-8-to-mariadb-12-3"></a></h2>
<p>Because MariaDB 12.3 is the first LTS after 11.8, the upgrade crosses four release boundaries of accumulated change. Three categories of breakage deserve a pre-flight check.</p>
<p><strong>New reserved words.</strong> <code>CONVERSION</code>, <code>ST_COLLECT</code> and <code>TO_DATE</code> are now reserved. Any identifier using them must be backtick-quoted. Grep your schema and application SQL before the maintenance window, not during it.</p>
<p><strong>Removed system variables.</strong> <code>big_tables</code>, <code>large_page_size</code> and <code>storage_engine</code> were removed in the 12.0 line. If they remain in <code>my.cnf</code>, the server will refuse to start.</p>
<p><strong>A GTID setting regression in 12.3.2.</strong> When a replica is upgraded from a pre-12.3 release directly to 12.3.2, the <code>master_use_gtid</code> setting from <code>CHANGE MASTER TO</code> is not carried over and resets to <code>DEFAULT</code>. This is fixed in 12.3.3. If you land on 12.3.2, re-apply <code>master_use_gtid</code> immediately after the upgrade and verify with <code>SHOW REPLICA STATUS</code>. Downgrades are unaffected.</p>
<p>The safe rollout order for a Galera plus async topology is: upgrade the DR region first, then read replicas, then the non-primary nodes of the active cluster one at a time, then the primary via a controlled MaxScale switchover. Take a verified <code>mariadb-backup</code> before the first node, run <code>mariadb-upgrade</code> where required, and keep the old binaries installed until you have completed a full business cycle on the new version. Follow the official <a href="https://mariadb.com/docs/server/server-management/install-and-upgrade-mariadb/upgrading" target="_blank" rel="noopener">MariaDB upgrade documentation</a> for platform specifics.</p>
<h2>Benchmarking and Capacity Planning for MariaDB 12.3<a class="anchor-link" id="benchmarking-and-capacity-planning-for-mariadb-12-3"></a></h2>
<p>Do not accept the throughput claims of any release &mdash; including this one &mdash; without measuring them on your own hardware and workload shape. A defensible benchmark for the MariaDB 12.3 binary log change looks like this:</p>
<ol>
<li>Capture a production workload sample and replay it, rather than running a synthetic uniform-random benchmark that no application resembles.</li>
<li>Measure with the traditional binlog at <code>sync_binlog=1</code>, <code>innodb_flush_log_at_trx_commit=1</code> as your control.</li>
<li>Re-measure with the InnoDB-based binlog at <code>innodb_flush_log_at_trx_commit=1</code>. Record commit latency percentiles, not averages, and record device-level fsync counts.</li>
<li>Run a third pass at <code>innodb_flush_log_at_trx_commit=2</code> to quantify what relaxed durability actually buys now that consistency between log and engine is guaranteed regardless.</li>
<li>Repeat each pass with a deliberately induced crash to confirm recovery time and GTID continuity.</li>
</ol>
<p>Capacity planning should then be expressed in headroom rather than utilisation: a cluster running at 70% of measured write capacity has no room to absorb the loss of a node, because the survivors inherit the load and the certification cost of the failure. Size for N-1 in region and N-1 across regions.</p>
<h2>Frequently Asked Questions About MariaDB 12.3<a class="anchor-link" id="frequently-asked-questions-about-mariadb-12-3"></a></h2>
<h3>Is MariaDB 12.3 a long-term support release?<a class="anchor-link" id="is-mariadb-12-3-a-long-term-support-release"></a></h3>
<p>Yes. MariaDB 12.3 reached Stable/GA on 28 May 2026 and is maintained until June 2029. It is the successor LTS to MariaDB 11.8.</p>
<h3>Should I enable the InnoDB-based binary log in MariaDB 12.3?<a class="anchor-link" id="should-i-enable-the-innodb-based-binary-log-in-mariadb-12-3"></a></h3>
<p>Enable it on GTID-only replication topologies where you control the tooling and all replicas run MariaDB 12.3 or later. Do not enable it on Galera Cluster nodes, on systems that depend on filename/offset positions, or where third-party tools parse binlog files from disk.</p>
<h3>Does MariaDB 12.3 change how Galera Cluster behaves?<a class="anchor-link" id="does-mariadb-12-3-change-how-galera-cluster-behaves"></a></h3>
<p>Yes, in three ways. Incremental State Transfers skip redundant foreign key checks, write-set application can be retried via <code>wsrep_applier_retry_count</code>, and asynchronous replication between two Galera clusters can now apply in parallel. Galera is also no longer a package dependency of the server, so it must be installed explicitly.</p>
<h3>What is the fastest safe upgrade path from MariaDB 10.11 to MariaDB 12.3?<a class="anchor-link" id="what-is-the-fastest-safe-upgrade-path-from-mariadb-10-11-to-mariadb-12-3"></a></h3>
<p>Upgrade in LTS steps &mdash; 10.11 to 11.4, 11.4 to 11.8, then 11.8 to MariaDB 12.3 &mdash; validating application compatibility at each stop. Skipping LTS boundaries is technically possible but leaves you without a tested rollback point.</p>
<h3>How many nodes do I need for a fault-tolerant MariaDB 12.3 cluster?<a class="anchor-link" id="how-many-nodes-do-i-need-for-a-fault-tolerant-mariadb-12-3-cluster"></a></h3>
<p>Three synchronous nodes per region, one per availability zone, plus at least one asynchronous standby cluster in a second region. That combination survives a node failure with zero data loss and a full region failure with sub-second RPO.</p>
<h3>Do optimizer hints in MariaDB 12.3 replace query tuning?<a class="anchor-link" id="do-optimizer-hints-in-mariadb-12-3-replace-query-tuning"></a></h3>
<p>No. Hints are a containment tool for plan regressions and a way to protect a fleet from a single pathological statement. Schema design, indexing and statistics remain the durable fix.</p>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>MariaDB 12.3 is the most consequential MariaDB release in several years because it changes the cost of durability rather than merely adding syntax. Storing the binary log inside InnoDB collapses the fsync budget of a commit and makes crash safety structural. The optimizer hint framework converts plan stability from a global gamble into a per-statement contract. Galera improvements shorten the rejoin window and reduce spurious evictions. None of that, however, produces availability on its own. Availability comes from arranging those primitives across failure domains: synchronous replication inside a region, asynchronous GTID replication across regions, a routing tier that hides failover from applications, declarative operations, and backups that are restored on a schedule rather than trusted on faith.</p>
<p>If you are planning a MariaDB 12.3 upgrade, a Galera redesign, or a move to multi-region topology, <a href="https://minervadb.com/minervadb-consultative-support-2/">MinervaDB consultative support</a> provides 24&times;7 engineering for MariaDB, MySQL and PostgreSQL infrastructure at internet scale. You can also review our <a href="https://minervadb.com/mariadb-on-vmware-ibm-storage-virtualize-whitepaper/">MariaDB storage and virtualization whitepaper</a> for the infrastructure layer beneath this architecture.</p>
<h3>Further reading and authoritative references<a class="anchor-link" id="further-reading-and-authoritative-references"></a></h3>
<ul>
<li><a href="https://mariadb.com/docs/release-notes/community-server/12.3/mariadb-12.3-changes-and-improvements" target="_blank" rel="noopener">MariaDB 12.3 changes and improvements (official release notes)</a></li>
<li><a href="https://mariadb.com/docs/server/server-management/server-monitoring-logs/binary-log/innodb-based-binary-log" target="_blank" rel="noopener">InnoDB-based binary log documentation</a></li>
<li><a href="https://mariadb.com/docs/galera-cluster" target="_blank" rel="noopener">MariaDB Galera Cluster documentation</a></li>
<li><a href="https://mariadb.com/docs/maxscale" target="_blank" rel="noopener">MariaDB MaxScale documentation</a></li>
<li><a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/gtid" target="_blank" rel="noopener">Global Transaction ID reference</a></li>
<li><a href="https://mariadb.com/docs/server/server-usage/backup-and-restore" target="_blank" rel="noopener">Backup and restore, including point-in-time recovery</a></li>
<li><a href="https://mariadb.org/download/" target="_blank" rel="noopener">Download MariaDB Server from MariaDB Foundation</a></li>
</ul>

<p><a href="https://minervadb.com/mariadb-12-3-high-availability-internet-scale/">MariaDB 12.3 LTS: Advances and Building Fault-Tolerant MariaDB Infrastructure at Internet Scale</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL and MariaDB High Availability vs. Disaster Recovery: What’s the Difference (and Why It Matters)</title>
      <link rel="alternate" type="text/html" href="https://www.continuent.com/resources/blog/mysql-ha-vs-dr-what-is-the-difference" />
      <id>https://www.continuent.com/resources/blog/mysql-ha-vs-dr-what-is-the-difference</id>
      <updated>2026-08-08T09:51:09+03:00</updated>
      <author><name>Continuent Team</name></author>
      <summary type="html"><![CDATA[<p>High availability keeps MySQL and MariaDB applications running through routine local failures, while disaster recovery restores service after a site or regional outage. This article explains how RTO, RPO, distance, synchronous replication and asynchronous replication shape each strategy, and how Continuent Tungsten Cluster combines local HA with multi-site DR.</p>
<p><a href="https://www.continuent.com/resources/blog/mysql-ha-vs-dr-what-is-the-difference">MySQL and MariaDB High Availability vs. Disaster Recovery: What’s the Difference (and Why It Matters)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>High availability keeps MySQL and MariaDB applications running through routine local failures, while disaster recovery restores service after a site or regional outage. This article explains how RTO, RPO, distance, synchronous replication and asynchronous replication shape each strategy, and how Continuent Tungsten Cluster combines local HA with multi-site DR.</p>

<p><a href="https://www.continuent.com/resources/blog/mysql-ha-vs-dr-what-is-the-difference">MySQL and MariaDB High Availability vs. Disaster Recovery: What’s the Difference (and Why It Matters)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The open way of Percona Search for MongoDB</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/the-open-way-of-percona-search-for-mongodb/" />
      <id>https://www.percona.com/blog/the-open-way-of-percona-search-for-mongodb/</id>
      <updated>2026-08-07T14:55:06+03:00</updated>
      <author><name>Radoslaw Szulgo</name></author>
      <summary type="html"><![CDATA[<p>Percona Search for MongoDB is Percona’s downstream distribution of mongot, the search engine that provides MongoDB’s full-text and vector search capabilities. With this addition, you can power your applications with AI and advanced search techniques – anywhere, and without vendor lock-in. It’s the same search engine that powers MongoDB Atlas Search.  Percona Search for MongoDB … Continued<br />
The post The open way of Percona Search for MongoDB appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/the-open-way-of-percona-search-for-mongodb/">The open way of Percona Search for MongoDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>Percona Search for MongoDB is Percona&rsquo;s downstream distribution of </span><span>mongot</span><span>, the search engine that provides MongoDB&rsquo;s full-text and vector search capabilities. With this addition, you can power your applications with AI and advanced search techniques &ndash; anywhere, and without vendor lock-in. It&rsquo;s the same search engine that powers MongoDB Atlas Search.&nbsp;</span></p>
<p><span>Percona Search for MongoDB runs as a separate </span><span>mongot</span><span>&nbsp;process alongside Percona Server for MongoDB. The deployment topology determines how many </span><span>mongot</span><span> instances are required and how search requests are routed. Applications and users continue to connect to </span><span>mongod</span><span> in a replica set, or to </span><span>mongos</span><span> in a sharded cluster &ndash; never directly to </span><span>mongot</span><span>.</span></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment.png"><img loading="lazy" decoding="async" class="aligncenter wp-image-51373 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment.png" alt="" width="2547" height="1428" srcset="https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment.png 2547w, https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment-300x168.png 300w, https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment-1024x574.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment-768x431.png 768w, https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment-1536x861.png 1536w, https://www.percona.com/wp-content/uploads/2026/08/mongot-deployment-2048x1148.png 2048w" sizes="auto, (max-width: 2547px) 100vw, 2547px"></a></p>
<p><span>On behalf of the entire product and engineering team for MongoDB at Percona, I&rsquo;m pleased to share that we&rsquo;re starting a </span><b>Technical Preview with version 1.70.3-1.</b></p>
<h2>The way is open. Search should be too.<a class="anchor-link" id="the-way-is-open-search-should-be-too"></a></h2>
<p><span>Before anything else, credit where it is due. MongoDB Inc. released full-text and vector search for self-managed deployments as GA in July 2026, and published the source for </span><span>mongot</span><span> &ndash; the same search engine that powers MongoDB Atlas Search. Opening up the engine behind a flagship commercial service is a significant step and precisely what makes this Technical Preview possible.&nbsp;</span></p>
<p><span>What we want to add is the next layer of openness: freedom to choose your embedding model, to run inference where your data already lives, and to operate search with the same automated backup, monitoring, and Kubernetes tooling you already expect from every other tier of your database. That is the Percona way, and this post is our map for getting there.</span></p>
<h2><b>What we found in mongot</b><a class="anchor-link" id="what-we-found-in-mongot"></a></h2>
<p><span>We went through the current release, reviewing everything needed to run it the way you want in production. Below is what we found, stated as plainly as we can, with the Percona plan attached to each item. None of these is a defect. They describe where today&rsquo;s release draws the line between the search engine and the operational layer around it. The operational layer is exactly where Percona has always done its work.</span></p>
<h3><b>Automatic embeddings and model choice&nbsp;</b><a class="anchor-link" id="automatic-embeddings-and-model-choice"></a></h3>
<p><span>This is the big one, and it needs a little setup to see properly.</span></p>
<p><span>Vector search doesn&rsquo;t search text. It searches vectors. If a user wants to find a document, they need to type a query that is first run through an embedding model and turned into an array of numbers. That conversion is not a one-time import step, either &ndash; it has to keep pace with your data, because a document whose text changed while its vector didn&rsquo;t is now quietly unfindable. No errors are raised. Query results simply get worse over time.</span></p>
<p><span>There are two ways to handle it.</span></p>
<h4><b>Manually</b></h4>
<p><span>You generate embeddings yourself and write the vectors into the document. This path is completely open, with no restrictions. It is also where a lot of vector search projects stall, because you have just taken ownership of an embedding pipeline: something has to watch inserts and updates, batch them, call a model, handle failures and retries, backfill the whole corpus when you change models, and guarantee every vector still matches the text next to it. That is a distributed-systems problem bolted onto a database that already solved distributed-systems problems. For a fixed corpus, you index once and forget &ndash; it is fine. For live operational data, it becomes a permanent tax on the team. In my humble opinion, it isn&rsquo;t the way to go for a production deployment at scale.</span></p>
<h4><b>Automatically</b></h4>
<p><span>You declare which field holds your text and which model to use &ndash; the </span><span>autoEmbed</span><span> type in the index definition &ndash; and the database generates the embeddings, keeps them in sync as the data changes, and accepts plain text at query time. This exists precisely because the manual path does not scale. It is the path the documentation leads with, the path every tutorial will use, and for most teams running search on data that changes, it is the only realistically maintainable option.</span></p>
<p><a href="https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings.png"><img loading="lazy" decoding="async" class="aligncenter wp-image-51386 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings.png" alt="" width="2509" height="1370" srcset="https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings.png 2509w, https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings-300x164.png 300w, https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings-1024x559.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings-768x419.png 768w, https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings-1536x839.png 1536w, https://www.percona.com/wp-content/uploads/2026/08/manual-vs-automatic-embeddings-2048x1118.png 2048w" sizes="auto, (max-width: 2509px) 100vw, 2509px"></a></p>
<p><span>Today, automatic embeddings are supported only with Voyage AI models: </span><span>voyage-4-large</span><span>, </span><span>voyage-4</span><span>, </span><span>voyage-4-lite</span><span>, and </span><span>voyage-code-3</span><span>. Three practical consequences follow from that.</span></p>
<ul>
<li aria-level="1"><b>Your data travels to a third-party service.</b><span> Every document you index and every query your users type are sent to Voyage AI&rsquo;s cloud for processing. The support tickets, the patient notes, the contracts, the internal wiki &ndash; whatever you actually store &ndash; are handled outside your perimeter, from a database you self-host on hardware you own. Voyage AI does offer an on-premises deployment, which comes with its own licensing and costs. Without it, an air-gapped deployment cannot use automatic embeddings, and neither can teams working under data-residency obligations, which covers most of regulated Europe.</span></li>
<li aria-level="1"><b>It&rsquo;s metered.</b><span> There is a free tier &ndash; 200 million tokens to get you started, less for specialized models &ndash; but it is capped on both volume and velocity, with requests and tokens per minute throttled. Beyond that, it runs roughly $0.02 to $0.12 per million tokens, on every reindex and every query your application serves.</span></li>
<li aria-level="1"><b>The model is chosen for you.</b><span> Not the one that performs best in your language. Not the domain model your data science team fine-tuned. Not a smaller open-weights model that is good enough for your use case.</span></li>
</ul>
<h4><b>The Percona plan</b></h4>
<p><span>We want automatic embeddings to be open, so you have a genuinely unlimited choice of models suited to your needs. We will start with everything that speaks to the OpenAI-compatible embeddings API, which already covers a large and growing ecosystem:</span></p>
<ul>
<li aria-level="1"><b>Ollama</b><span> &ndash; local, free, 100+ open models including </span><span>nomic-embed-text</span><span>, </span><span>mxbai-embed-large</span><span>, and </span><span>all-minilm</span></li>
<li aria-level="1"><b>vLLM</b><span> &ndash; self-hosted GPU inference</span></li>
<li aria-level="1"><b>llama.cpp server</b><span> &ndash; local CPU or GPU inference</span></li>
<li aria-level="1"><b>LocalAI</b><span> and </span><b>LM Studio</b></li>
<li aria-level="1"><b>Hugging Face Text Embeddings Inference (TEI)</b></li>
</ul>
<p><span>Over time, we intend to widen that further, toward the 25,000-model catalog the open ecosystem has already built. Cloud providers remain available to teams that prefer them. They just stop being the only option.</span></p>
<h3><b>Reranking</b><a class="anchor-link" id="reranking"></a></h3>
<p><b>Reranking</b><span> is the second half of how serious retrieval works. Vector search is fast because the query and the documents are embedded separately and never actually compared &ndash; the model sees your query, sees a document, and never sees them side by side. That approximation is what makes it possible to search millions of documents in milliseconds, and it is also why the top result is often merely in the right neighborhood rather than right. A reranker fixes that: it takes the top 50 or 100 candidates and runs each through a model that reads the query and the document together, scoring genuine relevance rather than vector proximity. In practice, this is usually the single largest accuracy improvement available in a retrieval pipeline, and it matters most for RAG, where the language model only ever sees the top handful of results. If the passage that answers the question is sitting at rank eight, your application behaves as though the answer does not exist.</span></p>
<p><span>The </span><span>$rerank</span><span> stage is available only on MongoDB Atlas.</span></p>
<h4><b>The Percona plan</b></h4>
<p><span>We intend to open reranking as well. Our initial target is </span><span>BAAI/bge-reranker-large</span><span>, a strong cross-encoder text-ranking model from the Beijing Academy of Artificial Intelligence, published on Hugging Face under the permissive MIT license.</span></p>
<h3><b> Contextual chunking and multimodal pipelines</b><a class="anchor-link" id="contextual-chunking-and-multimodal-pipelines"></a></h3>
<p><b>Contextual chunking</b><span> matters because embedding models have fixed context windows, so anything longer than a few paragraphs has to be split before it can be indexed. Split it naively on a character count, and you shred the meaning: a clause reading &ldquo;this must be renewed within 30 days&rdquo; is worthless when &ldquo;this&rdquo; was defined two chunks earlier. Contextual and late-chunking techniques embed each chunk with awareness of the surrounding document, so the retrieved passage still makes sense on its own. This is the difference between a RAG system that cites something useful and one that confidently quotes a fragment.</span></p>
<p><b>Multimodal pipelines</b><span> embed text and images into a single vector space, so a search for &ldquo;worn leather armchair, mid-century&rdquo; can match a photograph with no caption. Product catalogs, media archives, scanned paperwork, engineering diagrams &ndash; anywhere the information lives in the picture rather than the metadata.</span></p>
<h4><b>The Percona plan</b></h4>
<p><span>For both of these, the path available today is a Voyage cloud API, called and paid for per token, with your content leaving your network. Meanwhile, the open-weights ecosystem offers excellent cross-encoder rerankers such as </span><a href="https://bge-model.com/bge/bge_m3.html"><span>BGE-M3 from BAAI</span></a><span>, and </span><a href="https://github.com/mehdidc/clip_rerank"><span>CLIP-</span></a><span> and </span><a href="https://arxiv.org/pdf/2303.15343"><span>SigLIP-class</span></a><span> multimodal encoders that run comfortably on a single GPU, or on CPU if you are patient. None of them is wired in yet. We would like to change that.</span></p>
<h3><b>Search-index backup, restore, and recovery</b><a class="anchor-link" id="search-index-backup-restore-and-recovery"></a></h3>
<p><span>This is documented rather than absent, and it is worth reading closely to understand what it asks of you.</span></p>
<p><span>mongot</span><span> is not your primary data store, so a lost index can always be rebuilt from </span><span>mongod</span><span>. The docs note the trade-off in the same breath: index builds &ldquo;can be slow and in some cases can take days to complete.&rdquo; For anything with a recovery-time objective, days of degraded search after a disk failure need a faster answer.</span></p>
<p><span>That faster answer is a filesystem snapshot, and here is the whole procedure. Stop </span><span>mongot</span><span>, then snapshot its data directory with the tool of your choice &ndash; the docs provide a working LVM example. To restore, put the directory back, generate a fresh server identity, and restart. </span><span>mongot</span><span> then resumes replication from </span><span>mongod</span><span> and catches up.</span></p>
<p><span>It works. It is also entirely yours to build, and there are a few properties worth planning around:</span></p>
<ul>
<li aria-level="1"><span>No orchestration or scheduling, and no coordination with your database backup &ndash; so no consistent point-in-time across </span><span>mongod</span><span> and </span><span>mongot</span><span>.</span></li>
<li aria-level="1"><span>No object storage integration.</span></li>
<li aria-level="1"><span>The search process is stopped while the copy is taken.</span></li>
<li aria-level="1"><span>The snapshot has a shelf life. A </span><span>mongot</span><span> backup is valid only for as long as the change stream can carry it forward, so a snapshot older than your oplog retention window is detected as having fallen off the oplog and triggers the full rebuild you took the snapshot to avoid.</span></li>
<li aria-level="1"><span>On Kubernetes, MongoDB Controllers for Kubernetes does not back up or restore </span><span>mongot</span><span> volumes, and the docs recommend planning this with your storage platform.</span></li>
</ul>
<h4><b>The Percona plan</b></h4>
<p><span>Automated, scheduled, verified backups are a problem the open-source community solved for databases a long time ago, and search indexes deserve the same treatment. Percona Backup for MongoDB and Percona Operator for MongoDB are a natural fit: PBM to orchestrate search-index snapshots alongside the database backup it already handles, with fast index initialization from object storage &ndash; S3, Azure, GCS, MinIO &ndash; instead of a full change-stream replay. The goal is for search-index recovery to be something you configure once, rather than script.</span></p>
<h3><b>Observability</b><a class="anchor-link" id="observability"></a></h3>
<p><span>There is a </span><span>/metrics</span><span> endpoint that exposes a great deal. What isn&rsquo;t there yet is anything built on top of it. Atlas has a Search Metrics UI; for self-managed deployments, dashboards are, in the documentation&rsquo;s own phrasing, &ldquo;not provided in a UI component.&rdquo; Alerting is yours to define, as are log retention and diagnostic-data rotation.</span></p>
<p><span>To be concrete about what &ldquo;yours to define&rdquo; involves: the upstream docs publish a genuinely thoughtful set of seventeen recommended alerts across three severity tiers, each with example PromQL to adapt to your environment and thresholds to tune to your workload. The recommended approach is to implement the paging tier first, run it for a week, tune out false positives, then add the other two. That is good advice. It is also multi-week work, repeated for every deployment, before you have the monitoring that a hosted service provides on day one.</span></p>
<p><span>Some of the behaviors worth alerting on are genuinely subtle. </span><span>mongot</span><span> enforces three disk thresholds internally, and the docs note they take effect whether or not you are monitoring:</span></p>
<ol>
<li><span>Level 1: at 85% full, new index builds remain in </span><span>PENDING.&nbsp;</span></li>
<li><span>Level 2 &ndash; at 90%, steady-state replication is disabled &ndash; existing indexes stop receiving change events, and search begins serving stale results while the database itself reports healthy. </span></li>
<li><span>Level 3: at 95%, the process stops and requires disk space to be freed before it restarts cleanly. The middle threshold is the one worth wiring up carefully, because it doesn&rsquo;t announce itself.</span></li>
</ol>
<p><a href="https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard.png"><img loading="lazy" decoding="async" class="aligncenter wp-image-51390 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard.png" alt="" width="2285" height="1168" srcset="https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard.png 2285w, https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard-300x153.png 300w, https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard-1024x523.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard-768x393.png 768w, https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard-1536x785.png 1536w, https://www.percona.com/wp-content/uploads/2026/08/pmm-mongot-dashboard-2048x1047.png 2048w" sizes="auto, (max-width: 2285px) 100vw, 2285px"></a></p>
<h4><b>The Percona plan</b></h4>
<p><span>Percona Monitoring and Management is where this belongs. We believe that collecting these metrics, presenting them on turnkey dashboards, and shipping alert rules with sensible thresholds is exactly the kind of work that should be done once and shared, rather than rebuilt by every team. Sync lag, heap and JVM health, index build progress, executor queue depth, disk headroom &ndash; including an alert for the case above, so you learn that replication stopped before your users do.</span></p>
<h2><span>About the license</span><a class="anchor-link" id="about-the-license"></a></h2>
<p><a href="https://github.com/mongodb/mongot"><span>mongot</span></a><span> is published under the Server Side Public License, and so is our distribution. You can read every line of it on </span><a href="https://github.com/percona/percona-mongot"><span>GitHub</span></a><span>, and we add no restrictions of our own on top.</span></p>
<p><span>SSPL is source-available rather than OSI-approved open source, and we would rather say so than blur the term. What we can commit to is the part we control: </span></p>
<ul>
<li><span>Capabilities stay yours. </span></li>
<li><span>Self-hostable. </span></li>
<li><span>Air-gappable. </span></li>
<li><span>No metered API on the critical path. </span></li>
<li><span>Software remains open and free.</span></li>
</ul>
<p><span>Choice of automation, choice of model, choice of where the inference happens. That is the freedom we are working toward.</span></p>
<h2><span>Getting started</span><a class="anchor-link" id="getting-started"></a></h2>
<p><span>Percona Search for MongoDB requires Percona Server for MongoDB 8.3, which we shipped as a Technical Preview last week &ndash; the first Percona release carrying the </span><span>$search</span><span>, </span><span>$searchMeta</span><span>, </span><span>$vectorSearch</span><span>, </span><span>$rankFusion</span><span> and </span><span>$scoreFusion</span><span> stages that the search process plugs into.</span></p>
<ul>
<li aria-level="1"><b>Packages:</b><span> grab Percona Search for MongoDB 1.70.3-1 from </span><a href="https://www.percona.com/downloads/"><span>percona.com/downloads</span></a><span>.</span></li>
<li aria-level="1"><b>Installation and configuration:</b><span> see the </span><a href="https://docs.percona.com/percona-search-for-mongodb/install-mongot.html"><span>Percona Search for MongoDB documentation</span></a><span>.</span></li>
<li aria-level="1"><b>On Kubernetes:</b><span> search is supported in </span><a href="https://docs.percona.com/percona-operator-for-mongodb/RN/Kubernetes-Operator-for-PSMONGODB-RN1.23.0.html"><span>Percona Operator for MongoDB 1.23.0</span></a><span>, released last week. Enable </span><span>spec.search</span><span> &ndash; the </span><span>enabled</span><span> flag, a </span><span>mongot</span><span> image, </span><span>size: 1</span><span>, and a sized PVC &ndash; and the Operator deploys the search process, wires up its authentication and internal TLS, and keeps the index in sync for both replica sets and sharded clusters, one </span><span>mongot</span><span> per shard. Details in the </span><a href="https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/"><span>1.23.0 announcement</span></a><span>.</span></li>
</ul>
<h2><span>Before you deploy it</span><a class="anchor-link" id="before-you-deploy-it"></a></h2>
<p><span>This is a Technical Preview. Please don&rsquo;t run it in production yet.</span></p>
<p><span>Specifically, this version may not fully work with the rest of the Percona software for MongoDB:</span></p>
<ul>
<li aria-level="1"><b>Percona Backup for MongoDB (PBM)</b><span> doesn&rsquo;t yet cover search indexes.</span></li>
<li aria-level="1"><b>Percona Operator for MongoDB</b><span> search support, recently released in 1.23.0, is currently in tech preview and limited to 1 search node. More automation is coming in the next version.</span></li>
<li aria-level="1"><b>Percona Monitoring and Management&nbsp;</b><span>doesn&rsquo;t have search dashboards yet, but they&rsquo;re&nbsp;coming!</span></li>
</ul>
<p><span>Point it at a copy of your data. Then share with us where it breaks for you.</span></p>
<h2><span>Tell us what you need</span><a class="anchor-link" id="tell-us-what-you-need"></a></h2>
<p><span>Everything above is a position, which means it can be wrong. If we&rsquo;ve missed a limitation, picked the wrong first target, or left out the embedding provider you actually use, we would like to hear it:</span></p>
<ul>
<li aria-level="1"><b>Forum:</b> <a href="https://forums.percona.com/c/mongodb/24"><span>forums.percona.com</span></a></li>
<li aria-level="1"><b>Source:</b> <a href="https://github.com/percona/percona-mongot"><span>github.com/percona/percona-mongot</span></a></li>
</ul>
<p><span>Search and AI on your own data, on your own hardware, with the model you chose. That is what we are building, and that is what we mean by openness.</span></p>
<p><span>If you&rsquo;re not using Percona for MongoDB yet but you&rsquo;re interested in Percona Search for MongoDB, you might like to read how </span><a href="https://www.percona.com/customer-story/sailthru/"><span>Sailthru by Zeta cut more than $1 million a year</span></a><span> by migrating to Percona Server for MongoDB.</span></p>
<p><span>The way is open.</span></p>
<p><i><span>Disclaimer: Roadmap items are intentions, not delivery commitments. Scope and sequencing may change, and the fastest way to change them is to tell us what you need.</span></i></p>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/the-open-way-of-percona-search-for-mongodb/">The open way of Percona Search for MongoDB</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/the-open-way-of-percona-search-for-mongodb/">The open way of Percona Search for MongoDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Node.js Connector 3.5.4 and 3.4.7 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-4-and-3-4-7-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-4-and-3-4-7-now-available/</id>
      <updated>2026-08-07T17:48:20+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/Node.js 3.5.4, 3.4.7, 3.3.4, and 3.2.5 GA releases. Download […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-4-and-3-4-7-now-available/">MariaDB Node.js Connector 3.5.4 and 3.4.7 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/Node.js 3.5.4, 3.4.7, 3.3.4, and 3.2.5 GA releases. Download Now MariaDB Connector/Node.js 3.5.4 is a Stable (GA) release. Notable changes in this release include: Two connection options changed since 3.5.3: MariaDB Connector/Node.js 3.4.7 is a Stable (GA)&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-4-and-3-4-7-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-4-and-3-4-7-now-available/">MariaDB Node.js Connector 3.5.4 and 3.4.7 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The DuckDB MySQL engine at 500 GB</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/" />
      <id>https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/</id>
      <updated>2026-08-07T12:09:29+03:00</updated>
      <author><name>Evgeniy Patlan</name></author>
      <summary type="html"><![CDATA[<p>We ran DuckDB MySQL storage engine at scale factor 500. It is around 500 GB of raw TPC-H, three billion lineitem rows  on an 80-core server with 187 GB of RAM. Three engines on the same box: InnoDB, our MySQL+DuckDB engine, and plain DuckDB as the reference. Here is what came out. InnoDB finished 18 … Continued<br />
The post The DuckDB MySQL engine at 500 GB appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/">The DuckDB MySQL engine at 500 GB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span>We ran DuckDB MySQL storage engine at scale factor 500. It is around 500 GB of raw TPC-H, three billion </span><span>lineitem</span><span> rows&nbsp; on an 80-core server with 187 GB of RAM. Three engines on the same box: InnoDB, our MySQL+DuckDB engine, and plain DuckDB as the reference.</span></p>
<p><span>Here is what came out. InnoDB finished 18 of the 22 queries and spent more than 28 hours of query time on them. Four never finished. Our engine ran all 22 in about three minutes. It loaded the data 25 times faster than InnoDB, and it used 5 times less disk. On the queries it stays close to plain DuckDB, and on a few it is ahead.</span></p>
<p><span>It&rsquo;s still an experiment, not production software. Code and the benchmark harness are on GitHub under GPLv2: </span><a href="https://github.com/Percona-Lab/ducksdb-mysql-engine"><span>https://github.com/Percona-Lab/ducksdb-mysql-engine</span></a><span>.</span></p>
<h2><span>The machine, and how we ran it</span><a class="anchor-link" id="the-machine-and-how-we-ran-it"></a></h2>
<ul>
<li aria-level="1"><span>One server, 80 cores, 187.5 GB RAM.</span></li>
<li aria-level="1"><span>SF500: about 500 GB of raw CSV, 3,000,028,242 </span><span>lineitem</span><span> rows.</span></li>
<li aria-level="1"><span>Three engines, one at a time: InnoDB, our engine, native DuckDB.</span></li>
<li aria-level="1"><span>All of it through the harness in the repo (</span><span>bench/tb</span><span>), in Docker.</span></li>
</ul>
<p><span>Two details about how we ran it change how the numbers read.</span></p>
<p><span>The load streams. We generate a chunk of CSV, load it, delete it, then generate the next one. So the disk never holds more than one 20 GB chunk, which is the only reason 500 GB fits on the box at all.</span></p>
<p><span>And &ldquo;native DuckDB&rdquo; is not a second copy of the data. It opens the engine&rsquo;s own DuckDB file read-only and queries that. Same bytes on both sides. That keeps the comparison honest, and it means there is no separate native load time to report.</span></p>
<h2><span>Loading the data</span><a class="anchor-link" id="loading-the-data"></a></h2>
<table width="438">
<thead>
<tr>
<th><span>Engine</span></th>
<th><span>Load time</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><span>ENGINE=DuckDB (COPY fast path)</span></td>
<td><span>36m 05s</span></td>
</tr>
<tr>
<td><span>InnoDB (bulk LOAD DATA)</span></td>
<td><span>15h 21m</span></td>
</tr>
</tbody>
</table>
<p><span>InnoDB took 25.5 times longer. The engine hands </span><span>LOAD DATA</span><span> straight to a DuckDB </span><span>COPY</span><span> instead of going row by row through the handler, so the three billion </span><span>lineitem</span><span> rows go in in about nineteen minutes, and the whole set in thirty-six. InnoDB inserts row by row and builds the primary key as it goes. That is where the rest of the fifteen hours goes.</span></p>
<h2><span>Storage on disk</span><a class="anchor-link" id="storage-on-disk"></a></h2>
<table width="581">
<thead>
<tr>
<th><span>Component</span></th>
<th><span>Size</span></th>
<th><span>vs raw CSV</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><span>raw TPC-H CSV</span></td>
<td><span>500.0 GB</span></td>
<td><span>100%</span></td>
</tr>
<tr>
<td><span>ENGINE=DuckDB (</span><span>tpch.duckdb</span><span>)</span></td>
<td><span>132.4 GB</span></td>
<td><span>26% (3.78x smaller)</span></td>
</tr>
<tr>
<td><span>InnoDB (</span><span>tpch/*.ibd</span><span>)</span></td>
<td><span>673.2 GB</span></td>
<td><span>135%</span></td>
</tr>
</tbody>
</table>
<p><span>DuckDB stores columns and compresses them, so 500 GB of CSV comes down to 132 GB. InnoDB stores rows and carries the index with them, and it ends up bigger than the CSV it came from: 673 GB, five times the DuckDB file. The InnoDB </span><span>lineitem.ibd</span><span> on its own is 446 GB. That is more than three times our entire database.</span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-51356 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/chart-storage_fix.png" alt="" width="1186" height="659" srcset="https://www.percona.com/wp-content/uploads/2026/08/chart-storage_fix.png 1186w, https://www.percona.com/wp-content/uploads/2026/08/chart-storage_fix-300x167.png 300w, https://www.percona.com/wp-content/uploads/2026/08/chart-storage_fix-1024x569.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/chart-storage_fix-768x427.png 768w" sizes="auto, (max-width: 1186px) 100vw, 1186px"></p>
<p><i><span>Storage, lower is better. The DuckDB engine holds all of SF500 in 132 GB.</span></i></p>
<h2><span>Query time</span><a class="anchor-link" id="query-time"></a></h2>
<p><span>All 22 queries. Warm runs, minimum of a few, in seconds. InnoDB had a two-hour cap per query; the ones that hit it are marked DNF.</span><span><br>
</span></p>
<p>&nbsp;</p>
<table width="620">
<thead>
<tr>
<th><span>Query</span></th>
<th><span>InnoDB</span></th>
<th><span>MySQL+DuckDB (ours)</span></th>
<th><span>native DuckDB</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><span>Q1</span></td>
<td><span>11864.5</span></td>
<td><span>11.1</span></td>
<td><span>5.2</span></td>
</tr>
<tr>
<td><span>Q6</span></td>
<td><span>3539.4</span></td>
<td><span>1.3</span></td>
<td><span>4.1</span></td>
</tr>
<tr>
<td><span>Q9</span></td>
<td><span>DNF</span></td>
<td><span>17.1</span></td>
<td><span>18.1</span></td>
</tr>
<tr>
<td><span>Q13</span></td>
<td><span>DNF</span></td>
<td><span>17.1</span></td>
<td><span>10.4</span></td>
</tr>
<tr>
<td><span>Q18</span></td>
<td><span>3846.1</span></td>
<td><span>27.0</span></td>
<td><span>11.9</span></td>
</tr>
<tr>
<td><span>Q19</span></td>
<td><span>6672.3</span></td>
<td><span>2.4</span></td>
<td><span>8.6</span></td>
</tr>
<tr>
<td><span>Q21</span></td>
<td><span>14211.7</span></td>
<td><span>26.0</span></td>
<td><span>15.1</span></td>
</tr>
<tr>
<td><b>All 22</b></td>
<td><b>18/22 finished, ~28 h</b></td>
<td><b>185.6 s</b></td>
<td><b>152.7 s</b></td>
</tr>
</tbody>
</table>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-51359 size-full" src="https://www.percona.com/wp-content/uploads/2026/08/chart-query-times.png" alt="" width="2384" height="960" srcset="https://www.percona.com/wp-content/uploads/2026/08/chart-query-times.png 2384w, https://www.percona.com/wp-content/uploads/2026/08/chart-query-times-300x121.png 300w, https://www.percona.com/wp-content/uploads/2026/08/chart-query-times-1024x412.png 1024w, https://www.percona.com/wp-content/uploads/2026/08/chart-query-times-768x309.png 768w, https://www.percona.com/wp-content/uploads/2026/08/chart-query-times-1536x619.png 1536w, https://www.percona.com/wp-content/uploads/2026/08/chart-query-times-2048x825.png 2048w" sizes="auto, (max-width: 2384px) 100vw, 2384px"></p>
<p><i><span>SF500, all 22 queries, log scale, lower is better. Hatched InnoDB bars did not finish inside the cap.</span></i></p>
<p><span>Two things to take from this.</span></p>
<p><span>InnoDB is far behind, which is no surprise. Scanning three billion rows for a wide </span><span>GROUP BY</span><span> or a six-way join is the wrong job for a row store. Four queries (Q9, Q13, Q17, Q20) did not finish at all, and the eighteen that did add up to more than 28 hours. This is the exact problem the engine is for. It is not a mark against InnoDB, which is doing the transactional job it was built for.</span></p>
<p><span>The comparison worth reading is our engine against plain DuckDB, since both are the same DuckDB reading the same file. Over all 22 they are close: 186 seconds for ours, 153 for native. Query by query it goes both ways. On the selective ones ours is often faster &mdash; Q6 (1.3 vs 4.1), Q19 (2.4 vs 8.6), Q17, Q20. On the biggest joins native wins &mdash; Q18 (27 vs 12), Q21, Q1. That gap comes from settings, not data: the memory limit, the thread count, and running inside </span><span>mysqld</span><span> versus a bare CLI. Either way, both are around a thousand times faster than the row store.</span></p>
<h2><span>Correctness</span><a class="anchor-link" id="correctness"></a></h2>
<p><span>We checked the answers, not only the clock. For every query we compared our engine&rsquo;s output to native DuckDB&rsquo;s, numbers rounded to four decimals and the order ignored. 21 of 22 matched exactly. None mismatched. One was skipped because a result file came back empty on one side. So the engine gives the same answers as plain DuckDB.</span></p>
<h2><span>What this means, and where it stops</span><a class="anchor-link" id="what-this-means-and-where-it-stops"></a></h2>
<p><span>At 500 GB the small-scale picture holds and gets sharper. Analytical queries that took hours on InnoDB, or never finished, come back in seconds on the DuckDB engine. The load is far quicker, and the footprint is far smaller. All of it inside one MySQL server, with the tables queried the normal way.</span></p>
<p><span>The limits are the same as before:</span></p>
<ul>
<li aria-level="1"><span>It is for analytics, not OLTP. Point lookups and single-row work stay on the row path, where an index seek is the right tool.</span></li>
<li aria-level="1"><span>DuckDB runs inside </span><span>mysqld</span><span>, so a heavy query under a tight memory limit can go over budget. </span><span>DUCKSDB_MEMORY_LIMIT</span><span> and </span><span>DUCKSDB_TEMP_DIR</span><span> let it spill to disk instead of failing. We set a limit here so the big CTEs spill rather than get OOM-killed.</span></li>
<li aria-level="1"><span>Some queries still fall back to normal MySQL and run on the row path.</span></li>
<li aria-level="1"><span>It is one workload on one machine. The result is strong, but the engine is still an experiment, not something for production traffic.</span></li>
</ul>
<h2><span>Try it</span><a class="anchor-link" id="try-it"></a></h2>
<p><span>Pull the image and run your own queries:</span></p>
<p><span>docker run </span><span>-d</span> <span>-p</span><span> 3306:3306 </span><span>-e</span><span> MYSQL_ROOT_PASSWORD=secret </span><span></span><span><br>
</span><span>&nbsp; perconalab/ducksdb-mysql-engine:latest</span></p>
<p><span>The engine, the patches, and the harness that produced these numbers are on GitHub: </span><a href="https://github.com/Percona-Lab/ducksdb-mysql-engine"><span>https://github.com/Percona-Lab/ducksdb-mysql-engine</span></a><span>. The per-query numbers and the method are in the repo. If it breaks, or your hardware gives different numbers, open an issue.</span></p>
<p>The post <a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/">The DuckDB MySQL engine at 500 GB</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/the-duckdb-mysql-engine-at-500-gb/">The DuckDB MySQL engine at 500 GB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Retail Data Analytics: Ultimate 2026 Modern Retail Stack</title>
      <link rel="alternate" type="text/html" href="https://minervadb.com/retail-data-analytics-modern-retail-stack/" />
      <id>https://minervadb.com/retail-data-analytics-modern-retail-stack/</id>
      <updated>2026-08-07T10:45:01+03:00</updated>
      <author><name>MinervaDB Corporation</name></author>
      <summary type="html"><![CDATA[<p>Retail data analytics is the difference between a merchandiser who reprices a slow-moving SKU on Tuesday morning and one who discovers the markdown opportunity in a month-end deck. Modern retail runs on a data platform, [...]</p>
<p><a href="https://minervadb.com/retail-data-analytics-modern-retail-stack/">Retail Data Analytics: Ultimate 2026 Modern Retail Stack</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><strong>Retail data analytics</strong> is the difference between a merchandiser who reprices a slow-moving SKU on Tuesday morning and one who discovers the markdown opportunity in a month-end deck. Modern retail runs on a data platform, not a reporting team: store point-of-sale terminals, an e-commerce checkout, a warehouse management system, a loyalty engine, a marketplace feed and a dozen SaaS applications all emit events that must land, reconcile and become a single trusted number before the trading meeting. This guide walks through the exact technology stack MinervaDB deploys and supports for modern retail businesses, layer by layer, with reference diagrams, production SQL and the service level objectives that keep it honest.</p>
<p>Everything below is grounded in the vendor-neutral stack described in the MinervaDB <a href="https://minervadb.com/data-analytics-and-data-warehousing-support/">Data Analytics and Data Warehousing Support</a> practice: change data capture, streaming ingestion, lakehouse storage, warehouse and real-time OLAP compute, declarative transformation, a governed semantic layer, and 24&times;7 operational ownership across the whole chain. Retail simply stresses every one of those layers harder than most industries, because seasonality, promotions, returns and omnichannel identity all conspire to break naive models.</p>
<h2>Why retail data analytics breaks at modern retail scale<a class="anchor-link" id="why-retail-data-analytics-breaks-at-modern-retail-scale"></a></h2>
<p>Retail data analytics failures are rarely caused by one broken component. A typical incident chain looks like this: the e-commerce team ships a schema change that widens a product attribute, the change data capture connector emits a new Avro schema, a Kafka consumer group lags behind during a flash sale, a late-arriving returns partition breaks an incremental model, the cloud warehouse autoscales to absorb the retry storm, and by 08:00 the trading dashboard is eight hours stale while the monthly compute bill has doubled. Diagnosing that chain needs one team that understands OLTP internals, streaming semantics, distributed query execution and BI caching simultaneously.</p>
<p>Four characteristics make retail data analytics harder than the generic enterprise case. First, the grain is brutal: a mid-sized omnichannel retailer generates hundreds of millions of order lines, inventory movements and clickstream events per year, and every one of them can be amended by a return, a price adjustment or a partial refund. Second, identity is fragmented across a guest checkout, a loyalty card, an app login and a marketplace pseudonym, so customer conformity is a modelling problem before it is a marketing problem.</p>
<p>Third, time is not neutral: fiscal calendars, 4-5-4 retail weeks, trading-day comparisons and promotional overlaps mean a naive date dimension produces confidently wrong year-on-year numbers. Fourth, latency requirements are bimodal, because finance is happy with an hourly warehouse refresh while store operations and dynamic pricing need sub-second answers.</p>
<p>Good retail data analytics therefore needs two compute profiles behind a single semantic contract: an elastic cloud warehouse for governed, historical, finance-grade reporting, and a real-time OLAP engine for user-facing dashboards that must answer in milliseconds. The architecture below is how MinervaDB reconciles the two without duplicating metric logic.</p>
<h2>The modern retail data analytics reference architecture<a class="anchor-link" id="the-modern-retail-data-analytics-reference-architecture"></a></h2>
<p>Every retail data analytics engagement starts with a written architecture. The blueprint below shows six layers of data flow plus a cross-cutting engineering layer that an on-call team owns around the clock. Your stack may substitute Apache Iceberg for Delta Lake, or ClickHouse for BigQuery, but the failure modes, the SLOs and the review checkpoints stay the same.</p>
<figure><img decoding="async" title="Modern retail data analytics reference architecture diagram" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQ4MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJNb2Rlcm4gcmV0YWlsIGRhdGEgYW5hbHl0aWNzIHJlZmVyZW5jZSBhcmNoaXRlY3R1cmUgZGlhZ3JhbSIgc3R5bGU9IndpZHRoOjEwMCU7aGVpZ2h0OmF1dG87YmFja2dyb3VuZDojZmZmZmZmO2JvcmRlcjoxcHggc29saWQgI2Q3ZGRlNTtib3JkZXItcmFkaXVzOjEycHgiPjx0ZXh0IHg9IjYwMCIgeT0iMzYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxOCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMxMjI2M2YiPk1pbmVydmFEQiBSZWZlcmVuY2UgQXJjaGl0ZWN0dXJlIGZvciBNb2Rlcm4gUmV0YWlsIERhdGEgQW5hbHl0aWNzPC90ZXh0PjxyZWN0IHg9IjE1IiB5PSI4MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIyNTUiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iMTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMyIiByeD0iMTAiIGZpbGw9IiMxZjM4NjQiLz48cmVjdCB4PSIxNSIgeT0iMTAwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjEyIiBmaWxsPSIjMWYzODY0Ii8+PHRleHQgeD0iMTAwIiB5PSIxMDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPjEuIFJldGFpbCBzb3VyY2VzPC90ZXh0Pjx0ZXh0IHg9IjEwMCIgeT0iMTQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+UE9TIC8gRVBPUyB0ZXJtaW5hbHM8L3RleHQ+PHRleHQgeD0iMTAwIiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5FLWNvbW1lcmNlIChQb3N0Z3JlU1FMKTwvdGV4dD48dGV4dCB4PSIxMDAiIHk9IjIwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkVSUCBhbmQgZmluYW5jZSAoT3JhY2xlKTwvdGV4dD48dGV4dCB4PSIxMDAiIHk9IjIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPldNUyBhbmQgT01TIChNb25nb0RCKTwvdGV4dD48dGV4dCB4PSIxMDAiIHk9IjI2MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkxveWFsdHksIENSTSwgU2FhUyBBUElzPC90ZXh0Pjx0ZXh0IHg9IjEwMCIgeT0iMjkyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Q2xpY2tzdHJlYW0gYW5kIGFwcCBsb2dzPC90ZXh0Pjxwb2x5Z29uIHBvaW50cz0iMTkwLDE5OCAyMTAsMjA3IDE5MCwyMTYiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSIyMTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjI1NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSIyMTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMyIiByeD0iMTAiIGZpbGw9IiMyMjU3N2EiLz48cmVjdCB4PSIyMTUiIHk9IjEwMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzIyNTc3YSIvPjx0ZXh0IHg9IjMwMCIgeT0iMTAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj4yLiBJbmdlc3Rpb24gKyBDREM8L3RleHQ+PHRleHQgeD0iMzAwIiB5PSIxNDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5EZWJleml1bSBsb2ctYmFzZWQgQ0RDPC90ZXh0Pjx0ZXh0IHg9IjMwMCIgeT0iMTcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+S2Fma2EgLyBLaW5lc2lzIC8gUmVkcGFuZGE8L3RleHQ+PHRleHQgeD0iMzAwIiB5PSIyMDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5LYWZrYSBDb25uZWN0IHNpbmtzPC90ZXh0Pjx0ZXh0IHg9IjMwMCIgeT0iMjMyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+QWlyYnl0ZSAvIEZpdmV0cmFuPC90ZXh0Pjx0ZXh0IHg9IjMwMCIgeT0iMjYyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Rmxpbmsgc3RyZWFtIGVucmljaG1lbnQ8L3RleHQ+PHRleHQgeD0iMzAwIiB5PSIyOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5CdWxrIGFuZCBkZWx0YSBsb2FkZXJzPC90ZXh0Pjxwb2x5Z29uIHBvaW50cz0iMzkwLDE5OCA0MTAsMjA3IDM5MCwyMTYiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSI0MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjI1NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI0MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMyIiByeD0iMTAiIGZpbGw9IiMyYTdmNjIiLz48cmVjdCB4PSI0MTUiIHk9IjEwMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzJhN2Y2MiIvPjx0ZXh0IHg9IjUwMCIgeT0iMTAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj4zLiBMYWtlaG91c2U8L3RleHQ+PHRleHQgeD0iNTAwIiB5PSIxNDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5TMyAvIEFETFMgLyBHQ1M8L3RleHQ+PHRleHQgeD0iNTAwIiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5BcGFjaGUgSWNlYmVyZzwvdGV4dD48dGV4dCB4PSI1MDAiIHk9IjIwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkRlbHRhIExha2UgLyBBcGFjaGUgSHVkaTwvdGV4dD48dGV4dCB4PSI1MDAiIHk9IjIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlBhcnF1ZXQgKyBaU1REPC90ZXh0Pjx0ZXh0IHg9IjUwMCIgeT0iMjYyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+SW1tdXRhYmxlIHJhdyB6b25lPC90ZXh0Pjx0ZXh0IHg9IjUwMCIgeT0iMjkyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+UmVwbGF5YWJsZSBoaXN0b3J5PC90ZXh0Pjxwb2x5Z29uIHBvaW50cz0iNTkwLDE5OCA2MTAsMjA3IDU5MCwyMTYiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSI2MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjI1NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI2MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMyIiByeD0iMTAiIGZpbGw9IiM4YTVhMDAiLz48cmVjdCB4PSI2MTUiIHk9IjEwMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzhhNWEwMCIvPjx0ZXh0IHg9IjcwMCIgeT0iMTAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj40LiBXYXJlaG91c2UgKyBPTEFQPC90ZXh0Pjx0ZXh0IHg9IjcwMCIgeT0iMTQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+U25vd2ZsYWtlIC8gQmlnUXVlcnk8L3RleHQ+PHRleHQgeD0iNzAwIiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5SZWRzaGlmdCAvIFN5bmFwc2U8L3RleHQ+PHRleHQgeD0iNzAwIiB5PSIyMDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5EYXRhYnJpY2tzIFNRTCAvIFRyaW5vPC90ZXh0Pjx0ZXh0IHg9IjcwMCIgeT0iMjMyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Q2xpY2tIb3VzZSAvIERydWlkPC90ZXh0Pjx0ZXh0IHg9IjcwMCIgeT0iMjYyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+UGlub3QgLyBTdGFyUm9ja3M8L3RleHQ+PHRleHQgeD0iNzAwIiB5PSIyOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5EdWNrREIgZm9yIGVkZ2Ugc3RvcmVzPC90ZXh0Pjxwb2x5Z29uIHBvaW50cz0iNzkwLDE5OCA4MTAsMjA3IDc5MCwyMTYiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSI4MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjI1NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI4MTUiIHk9IjgwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMyIiByeD0iMTAiIGZpbGw9IiM2YjNmYTAiLz48cmVjdCB4PSI4MTUiIHk9IjEwMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzZiM2ZhMCIvPjx0ZXh0IHg9IjkwMCIgeT0iMTAyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj41LiBUcmFuc2Zvcm08L3RleHQ+PHRleHQgeD0iOTAwIiB5PSIxNDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5kYnQgbW9kZWxzIGFuZCB0ZXN0czwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjE3MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkFpcmZsb3cgLyBEYWdzdGVyIC8gUHJlZmVjdDwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjIwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlNDRCBUeXBlIDIgZGltZW5zaW9uczwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkluY3JlbWVudGFsIHJldGFpbCBtYXJ0czwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjI2MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPk1hdGVyaWFsaXNlZCByb2xsLXVwczwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjI5MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlNRTE1lc2ggLyBTcGFyayBTUUw8L3RleHQ+PHBvbHlnb24gcG9pbnRzPSI5OTAsMTk4IDEwMTAsMjA3IDk5MCwyMTYiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSIxMDE1IiB5PSI4MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIyNTUiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iMTAxNSIgeT0iODAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMzIiIHJ4PSIxMCIgZmlsbD0iI2E2M2Q0MCIvPjxyZWN0IHg9IjEwMTUiIHk9IjEwMCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iI2E2M2Q0MCIvPjx0ZXh0IHg9IjExMDAiIHk9IjEwMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+Ni4gU2VydmU8L3RleHQ+PHRleHQgeD0iMTEwMCIgeT0iMTQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+U2VtYW50aWMgLyBtZXRyaWNzIGxheWVyPC90ZXh0Pjx0ZXh0IHg9IjExMDAiIHk9IjE3MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkxvb2tlciAvIFBvd2VyIEJJIC8gVGFibGVhdTwvdGV4dD48dGV4dCB4PSIxMTAwIiB5PSIyMDIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5TdXBlcnNldCAvIE1ldGFiYXNlPC90ZXh0Pjx0ZXh0IHg9IjExMDAiIHk9IjIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkN1YmUgZW1iZWRkZWQgYW5hbHl0aWNzPC90ZXh0Pjx0ZXh0IHg9IjExMDAiIHk9IjI2MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPnBndmVjdG9yICsgTUwgZmVhdHVyZXM8L3RleHQ+PHRleHQgeD0iMTEwMCIgeT0iMjkyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+UmV2ZXJzZSBFVEwgdG8gUE9TIGFuZCBDUk08L3RleHQ+PHJlY3QgeD0iMTUiIHk9IjM2NSIgd2lkdGg9IjExNzAiIGhlaWdodD0iOTUiIHJ4PSIxMCIgZmlsbD0iI2VlZjRmZiIgc3Ryb2tlPSIjOWRiNGQ4Ii8+PHRleHQgeD0iNjAwIiB5PSIzOTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMy41IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzFmMzg2NCI+TWluZXJ2YURCIDI0eDcgY3Jvc3MtY3V0dGluZyByZXRhaWwgZGF0YSBhbmFseXRpY3MgZW5naW5lZXJpbmcgbGF5ZXI8L3RleHQ+PHRleHQgeD0iMTEyIiB5PSI0MjMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5GcmVzaG5lc3MgYW5kIHZvbHVtZSBTTE9zPC90ZXh0Pjx0ZXh0IHg9IjMwNyIgeT0iNDIzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+RGF0YSBxdWFsaXR5IGNvbnRyYWN0czwvdGV4dD48dGV4dCB4PSI1MDIiIHk9IjQyMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkxpbmVhZ2UgYW5kIGNhdGFsb2c8L3RleHQ+PHRleHQgeD0iNjk3IiB5PSI0MjMiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5QQ0kgRFNTIGFuZCBQSUkgbWFza2luZzwvdGV4dD48dGV4dCB4PSI4OTIiIHk9IjQyMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkZpbk9wcyBjb3N0IGd1YXJkcmFpbHM8L3RleHQ+PHRleHQgeD0iMTA4NyIgeT0iNDIzIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+MTUtbWludXRlIFAxIHJlc3BvbnNlPC90ZXh0Pjwvc3ZnPg==" alt="Modern retail data analytics reference architecture diagram"><figcaption><em>Figure 1: the MinervaDB reference architecture for modern retail data analytics, from POS and e-commerce sources through CDC, lakehouse, warehouse, transformation and the governed serving layer.</em></figcaption></figure>
<p>Three design principles govern this retail data analytics blueprint. First, the raw landing zone is immutable and replayable, so any downstream mart can be rebuilt from source without touching the production checkout database during peak trading. Second, transformation is declarative and version controlled, which makes every retail metric auditable and every change reviewable before it reaches a trading dashboard. Third, cost is a first-class SLO rather than a quarterly surprise: compute isolation, result caching and pre-aggregation are designed in from day one.</p>
<h2>The retail data analytics technology stack, layer by layer<a class="anchor-link" id="the-retail-data-analytics-technology-stack-layer-by-layer"></a></h2>
<p>MinervaDB is deliberately vendor-neutral, so the recommendation you get for retail data analytics is the one your access patterns, latency targets, concurrency profile and budget justify. The table below maps each layer of the stack to the retail workloads it actually serves.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Engines and tools</th>
<th>Retail workload fit</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Cloud data warehouse</strong></td>
<td>Snowflake, Google BigQuery, Amazon Redshift, Azure Synapse, Databricks SQL Warehouse</td>
<td>Finance-grade sales and margin reporting, category performance, supplier rebates, statutory and audit reporting</td>
</tr>
<tr>
<td><strong>Real-time OLAP</strong></td>
<td>ClickHouse, Apache Druid, Apache Pinot, StarRocks, Firebolt</td>
<td>Live store and basket dashboards, dynamic pricing, promotion monitoring, on-site search and recommendation telemetry</td>
</tr>
<tr>
<td><strong>Lakehouse and table formats</strong></td>
<td>Apache Iceberg, Delta Lake, Apache Hudi, Parquet, Trino, Presto, Apache Spark</td>
<td>Multi-year basket history, clickstream archives, ML training sets, engine portability without re-ingestion</td>
</tr>
<tr>
<td><strong>Streaming and CDC</strong></td>
<td>Apache Kafka, Debezium, Kafka Connect, Apache Flink, Kinesis, Pub/Sub, Redpanda</td>
<td>POS event capture, order and inventory replication, stock-out alerts, fraud and abuse signals</td>
</tr>
<tr>
<td><strong>Transformation and orchestration</strong></td>
<td>dbt, Apache Airflow, Dagster, Prefect, SQLMesh, Spark SQL</td>
<td>Conformed retail marts, tested margin definitions, reproducible restatements after returns and credit notes</td>
</tr>
<tr>
<td><strong>MPP and on-premises</strong></td>
<td>Greenplum, Vertica, Teradata, Exadata, PostgreSQL with Citus, DuckDB</td>
<td>Data-sovereign estates, hybrid migrations, in-store and edge analytics on constrained hardware</td>
</tr>
<tr>
<td><strong>Serving and BI</strong></td>
<td>Looker, Power BI, Tableau, Apache Superset, Metabase, Cube, pgvector</td>
<td>Governed trading dashboards, supplier-facing embedded analytics, retrieval-augmented merchandising assistants</td>
</tr>
</tbody>
</table>
<p>The point of a vendor-neutral retail data analytics stack is not novelty, it is substitution risk. Storing basket history in an open table format such as <a href="https://iceberg.apache.org/spec/" target="_blank" rel="noopener">Apache Iceberg</a> or <a href="https://delta.io/" target="_blank" rel="noopener">Delta Lake</a> means the warehouse engine becomes a swappable compute choice rather than a decade-long lock-in. Retailers who did this before the last round of cloud price changes moved workloads in weeks instead of quarters.</p>
<h2>Change data capture for POS and e-commerce events<a class="anchor-link" id="change-data-capture-for-pos-and-e-commerce-events"></a></h2>
<p>Every retail data analytics review at MinervaDB begins at the ingestion layer, because batch extraction against a live checkout database is the single most common cause of both stale dashboards and primary-database incidents. On Black Friday, a nightly SELECT over the orders table is not an extraction strategy, it is an outage waiting for a queue. MinervaDB replaces query-based extraction with log-based change data capture wherever the source engine allows, so the warehouse follows the write-ahead log instead of competing with customer transactions.</p>
<figure><img decoding="async" title="Retail data analytics change data capture and streaming ingestion pipeline diagram" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDQzMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJSZXRhaWwgZGF0YSBhbmFseXRpY3MgY2hhbmdlIGRhdGEgY2FwdHVyZSBhbmQgc3RyZWFtaW5nIGluZ2VzdGlvbiBwaXBlbGluZSBkaWFncmFtIiBzdHlsZT0id2lkdGg6MTAwJTtoZWlnaHQ6YXV0bztiYWNrZ3JvdW5kOiNmZmZmZmY7Ym9yZGVyOjFweCBzb2xpZCAjZDdkZGU1O2JvcmRlci1yYWRpdXM6MTJweCI+PHRleHQgeD0iNjAwIiB5PSIzNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzEyMjYzZiI+TG9nLWJhc2VkIENEQyBhbmQgc3RyZWFtaW5nIGluZ2VzdGlvbiBwYXRoIGZvciByZXRhaWwgZGF0YSBhbmFseXRpY3M8L3RleHQ+PHJlY3QgeD0iMTUiIHk9IjcwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjE0NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSIxNSIgeT0iNzAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMzAiIHJ4PSIxMCIgZmlsbD0iIzFmMzg2NCIvPjxyZWN0IHg9IjE1IiB5PSI4OCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzFmMzg2NCIvPjx0ZXh0IHg9IjEwMCIgeT0iOTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPk9MVFAgc291cmNlczwvdGV4dD48dGV4dCB4PSIxMDAiIHk9IjEyNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlBvc3RncmVTUUwgV0FMPC90ZXh0Pjx0ZXh0IHg9IjEwMCIgeT0iMTUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+TXlTUUwgYmlubG9nPC90ZXh0Pjx0ZXh0IHg9IjEwMCIgeT0iMTc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+T3JhY2xlIHJlZG8sIFBPUyBmaWxlczwvdGV4dD48dGV4dCB4PSIyMDAiIHk9IjEzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwIiBmaWxsPSIjNWE2YjdkIj4mbHQ7IDEgczwvdGV4dD48cG9seWdvbiBwb2ludHM9IjE5MCwxNDMgMjEwLDE1MiAxOTAsMTYxIiBmaWxsPSIjN2E4YWEwIi8+PHJlY3QgeD0iMjE1IiB5PSI3MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxNDUiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iMjE1IiB5PSI3MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIzMCIgcng9IjEwIiBmaWxsPSIjMjI1NzdhIi8+PHJlY3QgeD0iMjE1IiB5PSI4OCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzIyNTc3YSIvPjx0ZXh0IHg9IjMwMCIgeT0iOTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPkRlYmV6aXVtIENEQzwvdGV4dD48dGV4dCB4PSIzMDAiIHk9IjEyNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkthZmthIENvbm5lY3QgY2x1c3RlcjwvdGV4dD48dGV4dCB4PSIzMDAiIHk9IjE1MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkluY3JlbWVudGFsIHNuYXBzaG90PC90ZXh0Pjx0ZXh0IHg9IjMwMCIgeT0iMTc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+SGVhcnRiZWF0ICsgRExRPC90ZXh0Pjx0ZXh0IHg9IjQwMCIgeT0iMTMyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM1YTZiN2QiPiZsdDsgMiBzPC90ZXh0Pjxwb2x5Z29uIHBvaW50cz0iMzkwLDE0MyA0MTAsMTUyIDM5MCwxNjEiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSI0MTUiIHk9IjcwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjE0NSIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI0MTUiIHk9IjcwIiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjMwIiByeD0iMTAiIGZpbGw9IiMyYTdmNjIiLz48cmVjdCB4PSI0MTUiIHk9Ijg4IiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjEyIiBmaWxsPSIjMmE3ZjYyIi8+PHRleHQgeD0iNTAwIiB5PSI5MSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+S2Fma2EgdG9waWNzPC90ZXh0Pjx0ZXh0IHg9IjUwMCIgeT0iMTI2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+S2V5ZWQgYnkgb3JkZXIgaWQ8L3RleHQ+PHRleHQgeD0iNTAwIiB5PSIxNTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj4xMiBwYXJ0aXRpb25zLCBSRiAzPC90ZXh0Pjx0ZXh0IHg9IjUwMCIgeT0iMTc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+WlNURCwgYWNrcz1hbGw8L3RleHQ+PHRleHQgeD0iNjAwIiB5PSIxMzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzVhNmI3ZCI+Jmx0OyAzIHM8L3RleHQ+PHBvbHlnb24gcG9pbnRzPSI1OTAsMTQzIDYxMCwxNTIgNTkwLDE2MSIgZmlsbD0iIzdhOGFhMCIvPjxyZWN0IHg9IjYxNSIgeT0iNzAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMTQ1IiByeD0iMTAiIGZpbGw9IiNmN2Y5ZmMiIHN0cm9rZT0iI2M5ZDNlMCIvPjxyZWN0IHg9IjYxNSIgeT0iNzAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMzAiIHJ4PSIxMCIgZmlsbD0iIzhhNWEwMCIvPjxyZWN0IHg9IjYxNSIgeT0iODgiIHdpZHRoPSIxNzAiIGhlaWdodD0iMTIiIGZpbGw9IiM4YTVhMDAiLz48dGV4dCB4PSI3MDAiIHk9IjkxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj5GbGluayBlbnJpY2htZW50PC90ZXh0Pjx0ZXh0IHg9IjcwMCIgeT0iMTI2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+UHJvbW90aW9uIGFuZCBwcmljZSBqb2luPC90ZXh0Pjx0ZXh0IHg9IjcwMCIgeT0iMTUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+RlggYW5kIHRheCBydWxlczwvdGV4dD48dGV4dCB4PSI3MDAiIHk9IjE3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlN0b2NrLW91dCBkZXRlY3Rpb248L3RleHQ+PHRleHQgeD0iODAwIiB5PSIxMzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzVhNmI3ZCI+MzAtNjAgczwvdGV4dD48cG9seWdvbiBwb2ludHM9Ijc5MCwxNDMgODEwLDE1MiA3OTAsMTYxIiBmaWxsPSIjN2E4YWEwIi8+PHJlY3QgeD0iODE1IiB5PSI3MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxNDUiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iODE1IiB5PSI3MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIzMCIgcng9IjEwIiBmaWxsPSIjNmIzZmEwIi8+PHJlY3QgeD0iODE1IiB5PSI4OCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzZiM2ZhMCIvPjx0ZXh0IHg9IjkwMCIgeT0iOTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPlNpbmsgbG9hZGVyczwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjEyNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlNub3dwaXBlIHN0cmVhbWluZzwvdGV4dD48dGV4dCB4PSI5MDAiIHk9IjE1MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPkNsaWNrSG91c2UgS2Fma2EgZW5naW5lPC90ZXh0Pjx0ZXh0IHg9IjkwMCIgeT0iMTc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+SWNlYmVyZyAvIERlbHRhIHdyaXRlcjwvdGV4dD48dGV4dCB4PSIxMDAwIiB5PSIxMzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzVhNmI3ZCI+Jmx0OyA1IHM8L3RleHQ+PHBvbHlnb24gcG9pbnRzPSI5OTAsMTQzIDEwMTAsMTUyIDk5MCwxNjEiIGZpbGw9IiM3YThhYTAiLz48cmVjdCB4PSIxMDE1IiB5PSI3MCIgd2lkdGg9IjE3MCIgaGVpZ2h0PSIxNDUiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iMTAxNSIgeT0iNzAiIHdpZHRoPSIxNzAiIGhlaWdodD0iMzAiIHJ4PSIxMCIgZmlsbD0iI2E2M2Q0MCIvPjxyZWN0IHg9IjEwMTUiIHk9Ijg4IiB3aWR0aD0iMTcwIiBoZWlnaHQ9IjEyIiBmaWxsPSIjYTYzZDQwIi8+PHRleHQgeD0iMTEwMCIgeT0iOTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPlJhdyByZXRhaWwgem9uZTwvdGV4dD48dGV4dCB4PSIxMTAwIiB5PSIxMjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5BcHBlbmQtb25seSBoaXN0b3J5PC90ZXh0Pjx0ZXh0IHg9IjExMDAiIHk9IjE1MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlJlcGxheS1zYWZlIGJhY2tmaWxsPC90ZXh0Pjx0ZXh0IHg9IjExMDAiIHk9IjE3OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPlNvdXJjZSBvZiB0cnV0aDwvdGV4dD48cmVjdCB4PSIxNSIgeT0iMjQ1IiB3aWR0aD0iMTE3MCIgaGVpZ2h0PSI0OCIgcng9IjgiIGZpbGw9IiNmZmY3ZTYiIHN0cm9rZT0iI2UwYjg3OCIvPjx0ZXh0IHg9IjYwMCIgeT0iMjc0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM3YTUyMDAiPlNjaGVtYSBSZWdpc3RyeTogQXZybyBjb250cmFjdHMsIGJhY2t3YXJkLWNvbXBhdGliaWxpdHkgZ2F0ZXMgYW5kIGEgZGVhZC1sZXR0ZXIgdG9waWMgZm9yIHBvaXNvbiBQT1MgcmVjb3JkczwvdGV4dD48cmVjdCB4PSIxNSIgeT0iMzA4IiB3aWR0aD0iMTE3MCIgaGVpZ2h0PSI0OCIgcng9IjgiIGZpbGw9IiNlZWY3ZjAiIHN0cm9rZT0iIzhmYmY5ZiIvPjx0ZXh0IHg9IjYwMCIgeT0iMzM3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiMxYzVjM2EiPmRidCBpbmNyZW1lbnRhbCByZXRhaWwgbWFydHMsIHRoZW4gdGhlIHNlbWFudGljIGxheWVyLCB0cmFkaW5nIGRhc2hib2FyZHMgYW5kIHJldmVyc2UgRVRMIGJhY2sgaW50byBQT1MgYW5kIENSTTwvdGV4dD48dGV4dCB4PSI2MDAiIHk9IjM5MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyLjUiIGZpbGw9IiM1YTZiN2QiPklkZW1wb3RlbnQsIGV4YWN0bHktb25jZS1lZmZlY3RpdmUgbG9hZGluZyB3aXRoIG9mZnNldCBjaGVja3BvaW50cywgd2F0ZXJtYXJraW5nIGFuZCByZXBsYXktc2FmZSBiYWNrZmlsbHM8L3RleHQ+PC9zdmc+" alt="Retail data analytics change data capture and streaming ingestion pipeline diagram"><figcaption><em>Figure 2: the ingestion path MinervaDB hardens during retail data analytics onboarding, with schema contracts, dead-letter handling and replayable history.</em></figcaption></figure>
<p>The connector configuration below is the hardened Debezium baseline MinervaDB deploys for a retail PostgreSQL checkout database. Read the <a href="https://debezium.io/documentation/reference/stable/connectors/postgresql.html" target="_blank" rel="noopener">Debezium PostgreSQL connector documentation</a> alongside the <a href="https://www.postgresql.org/docs/current/logical-replication.html" target="_blank" rel="noopener">PostgreSQL logical replication documentation</a> before you change any of it.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-title="Debezium connector hardened for retail POS and order CDC">{
  "name": "retail-orders-cdc",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "plugin.name": "pgoutput",
    "database.hostname": "pg-checkout-primary.internal",
    "database.dbname": "retail",
    "slot.name": "dbz_retail_slot",
    "publication.autocreate.mode": "filtered",
    "table.include.list": "sales.orders,sales.order_lines,sales.returns,inventory.stock_movements,crm.customers",
    "topic.prefix": "retail",
    "snapshot.mode": "initial",
    "incremental.snapshot.chunk.size": 20480,
    "heartbeat.interval.ms": 10000,
    "heartbeat.action.query": "UPDATE dbz.heartbeat SET ts = now()",
    "decimal.handling.mode": "precise",
    "time.precision.mode": "adaptive_time_microseconds",
    "tombstones.on.delete": "true",
    "producer.override.compression.type": "zstd",
    "producer.override.acks": "all",
    "errors.tolerance": "all",
    "errors.deadletterqueue.topic.name": "dlq.retail",
    "errors.deadletterqueue.context.headers.enable": "true",
    "transforms": "route,unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.add.fields": "op,source.lsn,source.ts_ms",
    "transforms.unwrap.delete.handling.mode": "rewrite"
  }
}</pre>
<p>The heartbeat line is the detail that separates a retail data analytics pipeline that survives a quiet Sunday from one that fills the checkout database disk. Without a heartbeat, a low-traffic replication slot stops advancing its confirmed flush LSN and PostgreSQL retains write-ahead log segments indefinitely. Monitor slot lag in bytes, not just consumer lag in messages.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Replication slot and WAL retention watchdog on the retail source">SELECT slot_name,
       active,
       wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn))        AS retained_wal,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS unflushed,
       safe_wal_size
FROM   pg_replication_slots
WHERE  slot_type = 'logical'
ORDER  BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

-- Alert thresholds MinervaDB deploys by default on retail estates:
--   WARNING   retained_wal &gt; 10 GB  or wal_status = 'extended'
--   CRITICAL  retained_wal &gt; 40 GB  or wal_status IN ('unreserved','lost')
--   CRITICAL  slot inactive for more than 5 minutes during trading hours</pre>
<p>Broker tuning, consumer-lag triage and connector recovery for retail data analytics are handled by the same on-call rotation as the MinervaDB <a href="https://minervadb.com/kafka-support/">Apache Kafka support</a> practice, so nobody can hand an incident across a vendor boundary at 03:00. The upstream <a href="https://kafka.apache.org/documentation/" target="_blank" rel="noopener">Apache Kafka documentation</a> is the reference we tune against.</p>
<h2>Dimensional modelling for retail data analytics<a class="anchor-link" id="dimensional-modelling-for-retail-data-analytics"></a></h2>
<p>Modelling is the highest-leverage activity in retail data analytics, because badly modelled warehouses fail slowly and quietly. Metrics drift, joins fan out across promotions, storage grows faster than value, and analysts build a shadow estate of spreadsheets that nobody can reconcile at year end. MinervaDB starts by declaring the grain of every fact table, conforming the dimensions that finance, merchandising and supply chain all share, and separating logical modelling from physical layout so the same semantic contract can be materialised on Snowflake, BigQuery or ClickHouse.</p>
<p>For a modern retail business the canonical star is a sales fact at order-line grain, surrounded by conformed date, product, store, customer, promotion and channel dimensions. Returns are modelled as negative-quantity lines against the same fact rather than as a separate table, which keeps net sales additive and stops two dashboards disagreeing about revenue.</p>
<figure><img decoding="async" title="Retail data analytics star schema with conformed dimensions and an order line grain sales fact" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTUwIDcyMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJSZXRhaWwgZGF0YSBhbmFseXRpY3Mgc3RhciBzY2hlbWEgd2l0aCBjb25mb3JtZWQgZGltZW5zaW9ucyBhbmQgYW4gb3JkZXIgbGluZSBncmFpbiBzYWxlcyBmYWN0IiBzdHlsZT0id2lkdGg6MTAwJTtoZWlnaHQ6YXV0bztiYWNrZ3JvdW5kOiNmZmZmZmY7Ym9yZGVyOjFweCBzb2xpZCAjZDdkZGU1O2JvcmRlci1yYWRpdXM6MTJweCI+PHRleHQgeD0iNTc1IiB5PSIyNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzEyMjYzZiI+UmV0YWlsIHN0YXIgc2NoZW1hOiBmYWN0X3NhbGVzX2xpbmUgYXQgb25lLW9yZGVyLWxpbmUgZ3JhaW48L3RleHQ+PGxpbmUgeDE9IjE2NSIgeTE9IjEzMCIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PGxpbmUgeDE9IjU3NSIgeTE9IjEwNSIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PGxpbmUgeDE9Ijk4NSIgeTE9IjEzMCIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PGxpbmUgeDE9IjE2NSIgeTE9IjU2NSIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PGxpbmUgeDE9IjU3NSIgeTE9IjU5MCIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PGxpbmUgeDE9Ijk4NSIgeTE9IjU2NSIgeDI9IjU3NSIgeTI9IjM2MCIgc3Ryb2tlPSIjYTliNmM2IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWRhc2hhcnJheT0iNSA0Ii8+PHJlY3QgeD0iNDQwIiB5PSIyNTUiIHdpZHRoPSIyNzAiIGhlaWdodD0iMjE1IiByeD0iMTAiIGZpbGw9IiNmZGYxZjEiIHN0cm9rZT0iI2M5OGE4YSIgc3Ryb2tlLXdpZHRoPSIyIi8+PHJlY3QgeD0iNDQwIiB5PSIyNTUiIHdpZHRoPSIyNzAiIGhlaWdodD0iMzIiIHJ4PSIxMCIgZmlsbD0iI2E2M2Q0MCIvPjxyZWN0IHg9IjQ0MCIgeT0iMjc1IiB3aWR0aD0iMjcwIiBoZWlnaHQ9IjEyIiBmaWxsPSIjYTYzZDQwIi8+PHRleHQgeD0iNTc1IiB5PSIyNzciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMy41IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+ZmFjdF9zYWxlc19saW5lIChncmFpbjogb3JkZXIgbGluZSk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIzMDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5kYXRlX2tleSwgc3RvcmVfa2V5IChGSyk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIzMjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5wcm9kdWN0X2tleSwgY3VzdG9tZXJfa2V5IChGSyk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIzNDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5wcm9tb19rZXksIGNoYW5uZWxfa2V5IChGSyk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIzNjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5vcmRlcl9saW5lX2lkIChkZWdlbmVyYXRlKTwvdGV4dD48dGV4dCB4PSI1NzUiIHk9IjM4OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPnF1YW50aXR5LCBncm9zc19hbW91bnQ8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSI0MDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5kaXNjb3VudF9hbW91bnQsIG5ldF9hbW91bnQ8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSI0MjgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj50YXhfYW1vdW50LCBtYXJnaW5fYW1vdW50PC90ZXh0Pjx0ZXh0IHg9IjU3NSIgeT0iNDQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+cmV0dXJuX2ZsYWcsIGR3X2JhdGNoX2lkPC90ZXh0PjxyZWN0IHg9IjYwIiB5PSI2MCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIxNDAiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iNjAiIHk9IjYwIiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjMwIiByeD0iMTAiIGZpbGw9IiMyMjU3N2EiLz48cmVjdCB4PSI2MCIgeT0iNzgiIHdpZHRoPSIyMTAiIGhlaWdodD0iMTIiIGZpbGw9IiMyMjU3N2EiLz48dGV4dCB4PSIxNjUiIHk9IjgxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj5kaW1fZGF0ZTwvdGV4dD48dGV4dCB4PSIxNjUiIHk9IjExMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPmRhdGVfa2V5IChQSyk8L3RleHQ+PHRleHQgeD0iMTY1IiB5PSIxMzYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5jYWxlbmRhcl9kYXRlPC90ZXh0Pjx0ZXh0IHg9IjE2NSIgeT0iMTU5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+ZmlzY2FsX3dlZWssIHJldGFpbF80NTQ8L3RleHQ+PHRleHQgeD0iMTY1IiB5PSIxODIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5pc190cmFkaW5nX2RheTwvdGV4dD48cmVjdCB4PSI0NzAiIHk9IjM1IiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE0MCIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI0NzAiIHk9IjM1IiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjMwIiByeD0iMTAiIGZpbGw9IiMyYTdmNjIiLz48cmVjdCB4PSI0NzAiIHk9IjUzIiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjEyIiBmaWxsPSIjMmE3ZjYyIi8+PHRleHQgeD0iNTc1IiB5PSI1NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+ZGltX3Byb2R1Y3Q8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSI4OCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPnByb2R1Y3Rfa2V5IChQSyk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIxMTEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5za3UsIGVhbiwgYnJhbmQ8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSIxMzQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5jYXRlZ29yeSwgc3ViX2NhdGVnb3J5PC90ZXh0Pjx0ZXh0IHg9IjU3NSIgeT0iMTU3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+dW5pdF9jb3N0LCBzY2QyX2lzX2N1cnJlbnQ8L3RleHQ+PHJlY3QgeD0iODgwIiB5PSI2MCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIxNDAiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PHJlY3QgeD0iODgwIiB5PSI2MCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIzMCIgcng9IjEwIiBmaWxsPSIjOGE1YTAwIi8+PHJlY3QgeD0iODgwIiB5PSI3OCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzhhNWEwMCIvPjx0ZXh0IHg9Ijk4NSIgeT0iODEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPmRpbV9zdG9yZTwvdGV4dD48dGV4dCB4PSI5ODUiIHk9IjExMyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPnN0b3JlX2tleSAoUEspPC90ZXh0Pjx0ZXh0IHg9Ijk4NSIgeT0iMTM2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+c3RvcmVfY29kZSwgcmVnaW9uPC90ZXh0Pjx0ZXh0IHg9Ijk4NSIgeT0iMTU5IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Zm9ybWF0LCBzcW1fc2VsbGluZzwvdGV4dD48dGV4dCB4PSI5ODUiIHk9IjE4MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPm9wZW5lZF9vbiwgY2xvc2VkX29uPC90ZXh0PjxyZWN0IHg9IjYwIiB5PSI0OTUiIHdpZHRoPSIyMTAiIGhlaWdodD0iMTQwIiByeD0iMTAiIGZpbGw9IiNmN2Y5ZmMiIHN0cm9rZT0iI2M5ZDNlMCIvPjxyZWN0IHg9IjYwIiB5PSI0OTUiIHdpZHRoPSIyMTAiIGhlaWdodD0iMzAiIHJ4PSIxMCIgZmlsbD0iIzZiM2ZhMCIvPjxyZWN0IHg9IjYwIiB5PSI1MTMiIHdpZHRoPSIyMTAiIGhlaWdodD0iMTIiIGZpbGw9IiM2YjNmYTAiLz48dGV4dCB4PSIxNjUiIHk9IjUxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+ZGltX2N1c3RvbWVyPC90ZXh0Pjx0ZXh0IHg9IjE2NSIgeT0iNTQ4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Y3VzdG9tZXJfa2V5IChQSyk8L3RleHQ+PHRleHQgeD0iMTY1IiB5PSI1NzEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5jdXN0b21lcl9pZCAoTkspPC90ZXh0Pjx0ZXh0IHg9IjE2NSIgeT0iNTk0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+bG95YWx0eV90aWVyLCBzZWdtZW50PC90ZXh0Pjx0ZXh0IHg9IjE2NSIgeT0iNjE3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+c2NkMl92YWxpZF9mcm9tIC8gdG88L3RleHQ+PHJlY3QgeD0iNDcwIiB5PSI1MjAiIHdpZHRoPSIyMTAiIGhlaWdodD0iMTQwIiByeD0iMTAiIGZpbGw9IiNmN2Y5ZmMiIHN0cm9rZT0iI2M5ZDNlMCIvPjxyZWN0IHg9IjQ3MCIgeT0iNTIwIiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjMwIiByeD0iMTAiIGZpbGw9IiMwZjc2NmUiLz48cmVjdCB4PSI0NzAiIHk9IjUzOCIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzBmNzY2ZSIvPjx0ZXh0IHg9IjU3NSIgeT0iNTQxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj5kaW1fcHJvbW90aW9uPC90ZXh0Pjx0ZXh0IHg9IjU3NSIgeT0iNTczIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+cHJvbW9fa2V5IChQSyk8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSI1OTYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5wcm9tb19jb2RlLCBtZWNoYW5pYzwvdGV4dD48dGV4dCB4PSI1NzUiIHk9IjYxOSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPmZ1bmRpbmdfc291cmNlPC90ZXh0Pjx0ZXh0IHg9IjU3NSIgeT0iNjQyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+c3RhcnRzX29uLCBlbmRzX29uPC90ZXh0PjxyZWN0IHg9Ijg4MCIgeT0iNDk1IiB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE0MCIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48cmVjdCB4PSI4ODAiIHk9IjQ5NSIgd2lkdGg9IjIxMCIgaGVpZ2h0PSIzMCIgcng9IjEwIiBmaWxsPSIjOWEzNDEyIi8+PHJlY3QgeD0iODgwIiB5PSI1MTMiIHdpZHRoPSIyMTAiIGhlaWdodD0iMTIiIGZpbGw9IiM5YTM0MTIiLz48dGV4dCB4PSI5ODUiIHk9IjUxNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEzIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+ZGltX2NoYW5uZWw8L3RleHQ+PHRleHQgeD0iOTg1IiB5PSI1NDgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMS41IiBmaWxsPSIjMjIzMDNmIj5jaGFubmVsX2tleSAoUEspPC90ZXh0Pjx0ZXh0IHg9Ijk4NSIgeT0iNTcxIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEuNSIgZmlsbD0iIzIyMzAzZiI+Y2hhbm5lbCAoc3RvcmUsIHdlYiwgYXBwKTwvdGV4dD48dGV4dCB4PSI5ODUiIHk9IjU5NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPmZ1bGZpbG1lbnRfdHlwZTwvdGV4dD48dGV4dCB4PSI5ODUiIHk9IjYxNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExLjUiIGZpbGw9IiMyMjMwM2YiPm1hcmtldHBsYWNlX25hbWU8L3RleHQ+PHRleHQgeD0iNTc1IiB5PSI3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMi41IiBmaWxsPSIjNWE2YjdkIj5Db25mb3JtZWQgZGltZW5zaW9ucywgc3Vycm9nYXRlIGtleXMsIFNDRCBUeXBlIDIgaGlzdG9yeSBhbmQgb25lIGV4cGxpY2l0bHkgZGVjbGFyZWQgZmFjdCBncmFpbjwvdGV4dD48L3N2Zz4=" alt="Retail data analytics star schema with conformed dimensions and an order line grain sales fact"><figcaption><em>Figure 3: the conformed retail star schema MinervaDB reviews in every retail data analytics design audit.</em></figcaption></figure>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Retail warehouse DDL: SCD Type 2 dimension and order-line sales fact">-- Conformed retail dimension with SCD Type 2 history
CREATE TABLE IF NOT EXISTS dw.dim_product (
    product_key        BIGINT        NOT NULL,          -- surrogate key
    sku                VARCHAR(64)   NOT NULL,          -- natural / business key
    ean                VARCHAR(14),
    brand              VARCHAR(128),
    category           VARCHAR(128),
    sub_category       VARCHAR(128),
    unit_cost          NUMERIC(18,4),
    scd2_valid_from    TIMESTAMP     NOT NULL,
    scd2_valid_to      TIMESTAMP     NOT NULL DEFAULT TIMESTAMP '9999-12-31 00:00:00',
    scd2_is_current    BOOLEAN       NOT NULL DEFAULT TRUE,
    row_hash           VARCHAR(64)   NOT NULL,          -- change detection
    dw_loaded_at       TIMESTAMP     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT pk_dim_product PRIMARY KEY (product_key)
);

-- Fact table: grain is ONE ORDER LINE. Never mix grains in one retail fact.
CREATE TABLE IF NOT EXISTS dw.fact_sales_line (
    sale_line_id       BIGINT        NOT NULL,
    date_key           INTEGER       NOT NULL,
    store_key          BIGINT        NOT NULL,
    product_key        BIGINT        NOT NULL,
    customer_key       BIGINT        NOT NULL,
    promo_key          BIGINT,
    channel_key        BIGINT        NOT NULL,
    order_line_id      VARCHAR(64)   NOT NULL,          -- degenerate dimension
    quantity           NUMERIC(18,3) NOT NULL,          -- negative for returns
    gross_amount       NUMERIC(18,4) NOT NULL,
    discount_amount    NUMERIC(18,4) NOT NULL DEFAULT 0,
    net_amount         NUMERIC(18,4) NOT NULL,
    tax_amount         NUMERIC(18,4) NOT NULL DEFAULT 0,
    margin_amount      NUMERIC(18,4),
    return_flag        BOOLEAN       NOT NULL DEFAULT FALSE,
    dw_batch_id        BIGINT        NOT NULL,
    CONSTRAINT pk_fact_sales_line PRIMARY KEY (sale_line_id)
)
CLUSTER BY (date_key, store_key);                       -- Snowflake / BigQuery layout

-- Additivity guard: net sales must always reconcile to gross less discount
ALTER TABLE dw.fact_sales_line
  ADD CONSTRAINT ck_fact_sales_net
  CHECK (net_amount = gross_amount - discount_amount);</pre>
<p>Clustering choice matters more than most retail data analytics teams expect. Almost every trading query filters on a date range and a store or region, so <a href="https://docs.snowflake.com/en/user-guide/tables-clustering-keys" target="_blank" rel="noopener">clustering keys</a> on those two columns typically remove eighty to ninety-five percent of the bytes scanned. Getting this wrong is the most common reason a retailer sees a warehouse bill grow faster than sales.</p>
<h2>Physical design for sub-second store dashboards<a class="anchor-link" id="physical-design-for-sub-second-store-dashboards"></a></h2>
<p>Finance can wait thirty minutes. A store manager checking hourly sales against plan cannot, and neither can a pricing engine. For that half of retail data analytics MinervaDB materialises the same semantic fact onto a real-time OLAP engine as a sorted, compressed table with a projection for the highest-traffic dashboard filter. In practice this is the difference between a four-second dashboard and a forty-millisecond one.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="ClickHouse physical design for real-time retail dashboards">CREATE TABLE analytics.fact_sales_line
(
    event_date    Date,
    event_time    DateTime64(3, 'UTC'),
    store_id      UInt32              CODEC(T64, ZSTD(3)),
    product_id    UInt32              CODEC(T64, ZSTD(3)),
    customer_id   UInt64              CODEC(T64, ZSTD(3)),
    channel       LowCardinality(String),
    promo_code    LowCardinality(String),
    quantity      Decimal(18,3),
    net_amount    Decimal(18,4)       CODEC(ZSTD(3)),
    margin_amount Decimal(18,4)       CODEC(ZSTD(3)),
    ingested_at   DateTime DEFAULT now(),
    PROJECTION proj_store_daily
    (
        SELECT store_id, event_date, sum(net_amount), sum(quantity)
        GROUP BY store_id, event_date
    )
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (store_id, event_date, product_id)
TTL event_date + INTERVAL 36 MONTH TO VOLUME 'cold',
    event_date + INTERVAL 84 MONTH DELETE
SETTINGS index_granularity = 8192,
         min_bytes_for_wide_part = 10485760;

-- Incremental roll-up so trading dashboards never scan raw basket rows
CREATE MATERIALIZED VIEW analytics.mv_store_daily
ENGINE = SummingMergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (store_id, event_date)
AS SELECT store_id,
          event_date,
          sum(net_amount)    AS net_amount,
          sum(quantity)      AS quantity,
          sum(margin_amount) AS margin_amount
FROM analytics.fact_sales_line
GROUP BY store_id, event_date;</pre>
<p>The sort order, codecs and TTL tiering above follow the <a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree" target="_blank" rel="noopener">ClickHouse MergeTree documentation</a>, and this pattern is the backbone of the MinervaDB <a href="https://minervadb.com/clickhouse-consulting/">ClickHouse consulting</a> practice for real-time retail data analytics.</p>
<h2>Transformation with dbt: retail marts that reconcile<a class="anchor-link" id="transformation-with-dbt-retail-marts-that-reconcile"></a></h2>
<p>Transformation is where retail data analytics becomes a software engineering discipline, and it is the layer where support pays for itself fastest. MinervaDB standardises on version-controlled, tested and documented transformation code with staging, intermediate and mart layers, deterministic incremental strategies, and one semantic definition of net sales and margin that both finance and merchandising trust. Returns and credit notes arrive late, so retail models must self-heal rather than require a manual restatement.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="dbt incremental retail mart with a late-arrival window for returns">{{ config(
    materialized       = 'incremental',
    incremental_strategy = 'insert_overwrite',
    partition_by       = {'field': 'order_date', 'data_type': 'date', 'granularity': 'day'},
    cluster_by         = ['store_key', 'product_key'],
    on_schema_change   = 'append_new_columns',
    tags               = ['mart', 'retail', 'revenue']
) }}

WITH bounds AS (
    /* Reprocess a 7-day trailing window so late returns and price
       adjustments self-heal without a manual restatement. */
    SELECT DATEADD('day', -7, COALESCE(MAX(order_date), '1970-01-01')) AS lower_bound
    FROM {{ this }}
    {% if not is_incremental() %} WHERE FALSE {% endif %}
),

lines AS (
    SELECT o.order_line_id,
           o.order_date,
           o.customer_id,
           o.store_code,
           o.product_key,
           o.promo_code,
           o.channel,
           o.quantity,
           o.gross_amount,
           o.discount_amount,
           o.gross_amount - o.discount_amount AS net_amount
    FROM {{ ref('stg_retail__order_lines') }} o
    {% if is_incremental() %}
      WHERE o.order_date &gt;= (SELECT lower_bound FROM bounds)
    {% endif %}
)

SELECT {{ dbt_utils.generate_surrogate_key(['l.order_line_id']) }} AS sale_line_id,
       d.date_key,
       s.store_key,
       l.product_key,
       c.customer_key,
       pr.promo_key,
       ch.channel_key,
       l.order_line_id,
       l.order_date,
       l.quantity,
       l.gross_amount,
       l.discount_amount,
       l.net_amount,
       l.net_amount - (l.quantity * p.unit_cost) AS margin_amount,
       l.quantity &lt; 0                            AS return_flag
FROM lines l
JOIN {{ ref('dim_date') }}      d  ON d.calendar_date = l.order_date
JOIN {{ ref('dim_store') }}     s  ON s.store_code   = l.store_code
JOIN {{ ref('dim_customer') }}  c  ON c.customer_id  = l.customer_id AND c.scd2_is_current
JOIN {{ ref('dim_product') }}   p  ON p.product_key  = l.product_key AND p.scd2_is_current
LEFT JOIN {{ ref('dim_promotion') }} pr ON pr.promo_code = l.promo_code
JOIN {{ ref('dim_channel') }}   ch ON ch.channel      = l.channel</pre>
<p>Tests are what make retail data analytics defensible in a trading meeting. The contract below blocks a deploy if the grain breaks, if a foreign key dangles, or if the source stops arriving. The incremental strategy follows the <a href="https://docs.getdbt.com/docs/build/incremental-models" target="_blank" rel="noopener">dbt incremental models documentation</a>.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml" data-enlighter-title="dbt contracts, tests and freshness SLAs for retail marts">version: 2

sources:
  - name: retail_raw
    database: analytics_raw
    freshness:
      warn_after:  {count: 30, period: minute}
      error_after: {count: 90, period: minute}
    loaded_at_field: _ingested_at
    tables:
      - name: order_lines_stream
        columns:
          - name: order_line_id
            tests: [not_null, unique]

models:
  - name: fact_sales_line
    description: "Retail revenue fact at order-line grain. Owner: analytics-platform@minervadb.com"
    config:
      contract: {enforced: true}
    columns:
      - name: sale_line_id
        data_type: varchar
        constraints: [{type: not_null}, {type: primary_key}]
        tests: [unique, not_null]
      - name: store_key
        data_type: bigint
        tests:
          - relationships: {to: ref('dim_store'), field: store_key}
      - name: product_key
        data_type: bigint
        tests:
          - relationships: {to: ref('dim_product'), field: product_key}
      - name: net_amount
        data_type: numeric(18,4)
        tests:
          - dbt_expectations.expect_column_values_to_not_be_null
    tests:
      - dbt_utils.equal_rowcount:
          compare_model: ref('stg_retail__order_lines')
      - dbt_utils.recency:
          datepart: hour
          field: order_date
          interval: 24</pre>
<p>Orchestration is deliberately boring. A retail data analytics DAG should be idempotent, watermarked and capped in concurrency so a retry storm during peak trading cannot pile up and inflate the warehouse bill. The <a href="https://airflow.apache.org/docs/apache-airflow/stable/index.html" target="_blank" rel="noopener">Apache Airflow documentation</a> covers the scheduling semantics; the pattern below adds the retail-specific guard rails.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python" data-enlighter-title="Airflow DAG: watermarked, idempotent retail incremental load with quality gates">from datetime import datetime, timedelta
from airflow.decorators import dag, task
from airflow.providers.common.sql.operators.sql import SQLCheckOperator

DEFAULT_ARGS = {
    "owner": "minervadb-retail-analytics",
    "retries": 3,
    "retry_delay": timedelta(minutes=5),
    "retry_exponential_backoff": True,
    "execution_timeout": timedelta(hours=2),
}

@dag(
    dag_id="retail_sales_incremental",
    schedule="*/15 * * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
    max_active_runs=1,          # protects the warehouse from run pile-up on peak days
    default_args=DEFAULT_ARGS,
    tags=["retail", "warehouse", "incremental", "minervadb"],
)
def retail_sales_incremental():

    @task
    def resolve_watermark() -&gt; str:
        """Never trust wall-clock time: read the last committed watermark."""
        from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
        hook = SnowflakeHook(snowflake_conn_id="dw")
        low = hook.get_first(
            "SELECT COALESCE(MAX(dw_loaded_at), '1970-01-01') FROM dw.fact_sales_line"
        )[0]
        return low.isoformat()

    @task
    def merge_increment(watermark: str) -&gt; int:
        """MERGE is idempotent, so a retried task cannot double-count revenue."""
        from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
        hook = SnowflakeHook(snowflake_conn_id="dw")
        return hook.run(
            """
            MERGE INTO dw.fact_sales_line AS t
            USING raw.v_order_lines_enriched AS s
               ON t.sale_line_id = s.sale_line_id
            WHEN MATCHED THEN UPDATE SET
                 t.quantity   = s.quantity,
                 t.net_amount = s.net_amount,
                 t.return_flag = s.return_flag,
                 t.dw_loaded_at = s.updated_at
            WHEN NOT MATCHED THEN INSERT VALUES (
                 s.sale_line_id, s.date_key, s.store_key, s.product_key,
                 s.customer_key, s.promo_key, s.channel_key, s.order_line_id,
                 s.quantity, s.gross_amount, s.discount_amount, s.net_amount,
                 s.tax_amount, s.margin_amount, s.return_flag, s.batch_id)
            """,
            parameters={"watermark": watermark},
            handler=lambda cur: cur.rowcount,
        )

    freshness_gate = SQLCheckOperator(
        task_id="freshness_sla_gate",
        conn_id="dw",
        sql="""
            SELECT TIMESTAMPDIFF('minute', MAX(dw_loaded_at), CURRENT_TIMESTAMP()) &lt; 30
            FROM dw.fact_sales_line
        """,
    )

    reconciliation_gate = SQLCheckOperator(
        task_id="pos_to_warehouse_reconciliation",
        conn_id="dw",
        sql="""
            WITH src AS (SELECT SUM(net_amount) v FROM raw.pos_daily_totals
                         WHERE business_date = CURRENT_DATE() - 1),
                 dwh AS (SELECT SUM(net_amount) v FROM dw.fact_sales_line f
                         JOIN dw.dim_date d ON d.date_key = f.date_key
                         WHERE d.calendar_date = CURRENT_DATE() - 1)
            SELECT ABS(src.v - dwh.v) / NULLIF(src.v, 0) &lt; 0.001 FROM src, dwh """, ) merge_increment(resolve_watermark()) &gt;&gt; freshness_gate &gt;&gt; reconciliation_gate

retail_sales_incremental()</pre>
<h2>Real-time inventory, pricing and replenishment signals<a class="anchor-link" id="real-time-inventory-pricing-and-replenishment-signals"></a></h2>
<p>The operational half of retail data analytics is event-driven. Stock-outs, basket abandonment, promotion burn rate and click-and-collect readiness all have a shelf life measured in minutes, so they belong in a stream processor rather than a nightly batch. Apache Flink joins the order stream against inventory movements and emits an alert topic that reverse ETL pushes straight back into store apps and the pricing engine.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Flink SQL: rolling stock-out and sell-through detection for retail">-- Rolling 15-minute sell-through by store and SKU, with a stock-out signal
CREATE TABLE order_lines (
    order_line_id  STRING,
    store_id       INT,
    product_id     INT,
    quantity       DECIMAL(18,3),
    net_amount     DECIMAL(18,4),
    event_time     TIMESTAMP(3),
    WATERMARK FOR event_time AS event_time - INTERVAL '30' SECOND
) WITH (
    'connector' = 'kafka',
    'topic'     = 'retail.sales.order_lines',
    'properties.group.id' = 'flink-sell-through',
    'scan.startup.mode'   = 'group-offsets',
    'format'    = 'avro-confluent'
);

CREATE TABLE stock_on_hand (
    store_id    INT,
    product_id  INT,
    on_hand_qty DECIMAL(18,3),
    updated_at  TIMESTAMP(3),
    PRIMARY KEY (store_id, product_id) NOT ENFORCED
) WITH ('connector' = 'upsert-kafka', 'topic' = 'retail.inventory.soh',
        'key.format' = 'json', 'value.format' = 'json');

INSERT INTO retail_alerts
SELECT o.store_id,
       o.product_id,
       SUM(o.quantity)                                   AS sold_15m,
       MAX(s.on_hand_qty)                                AS on_hand,
       CASE WHEN MAX(s.on_hand_qty) &lt;= 0                     THEN 'STOCK_OUT'
            WHEN MAX(s.on_hand_qty) &lt; SUM(o.quantity) * 2 THEN 'REPLENISH_NOW' ELSE 'OK' END AS signal, TUMBLE_END(o.event_time, INTERVAL '15' MINUTE) AS window_end FROM order_lines o LEFT JOIN stock_on_hand FOR SYSTEM_TIME AS OF o.event_time AS s ON o.store_id = s.store_id AND o.product_id = s.product_id GROUP BY o.store_id, o.product_id, TUMBLE(o.event_time, INTERVAL '15' MINUTE) HAVING SUM(o.quantity) &gt; 0;</pre>
<p>Watermarking, exactly-once sinks and state backend sizing follow the <a href="https://nightlies.apache.org/flink/flink-docs-stable/" target="_blank" rel="noopener">Apache Flink documentation</a>. In a retail data analytics context the practical rule is that any signal a store colleague acts on within the hour should be produced by the stream, and any number that appears in a board pack should be produced by the warehouse from replayable history.</p>
<h2>Query performance and FinOps for retail data analytics<a class="anchor-link" id="query-performance-and-finops-for-retail-data-analytics"></a></h2>
<p>Warehouse performance work inside a retail data analytics engagement is evidence-driven. MinervaDB profiles the workload, ranks queries by total cost rather than by worst single execution, reads the physical plan, and fixes the root cause before discussing more compute. The usual retail culprits are a missing pre-aggregation, a promotion join that fans out, an unpruned partition, an implicit cast that defeats clustering, or a BI tool issuing one query per dashboard tile across four hundred stores.</p>
<table>
<thead>
<tr>
<th>Retail symptom</th>
<th>Usual root cause</th>
<th>MinervaDB remediation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Trading dashboard slow only at 08:00</td>
<td>Every regional manager opening the same shared warehouse at once</td>
<td>Workload isolation per persona, multi-cluster scaling policy, cache warm-up before store opening</td>
</tr>
<tr>
<td>Massive bytes scanned on basket queries</td>
<td>Partition pruning defeated by casts and functions on the date filter</td>
<td>Sargable date ranges, aligned data types, re-cluster on the real access pattern of date plus store</td>
</tr>
<tr>
<td>Spilling to remote storage during promotions</td>
<td>Promotion dimension joined on a non-unique mechanic key, fanning out order lines</td>
<td>Fix the grain, deduplicate upstream, stage the aggregate, right-size memory</td>
</tr>
<tr>
<td>Cost doubled month over month</td>
<td>Full refreshes, auto-suspend disabled, retry storms, unbounded supplier exports</td>
<td>Incremental strategy, suspend and timeout policies, budgets and per-category chargeback</td>
</tr>
<tr>
<td>Two dashboards disagree on net sales</td>
<td>Returns and discounts redefined inside the BI layer instead of one governed metric</td>
<td>Single semantic layer, certified retail marts, deprecation plan for shadow models</td>
</tr>
</tbody>
</table>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Find the retail queries that actually cost you money">-- Snowflake: rank by total cost, not by worst single execution
SELECT query_hash,
       ANY_VALUE(LEFT(query_text, 120))                        AS sample_sql,
       COUNT(*)                                                AS executions,
       ROUND(SUM(total_elapsed_time) / 1000 / 60, 1)           AS total_minutes,
       ROUND(AVG(total_elapsed_time) / 1000, 2)                AS avg_seconds,
       ROUND(SUM(bytes_scanned) / POWER(1024, 4), 3)           AS tb_scanned,
       ROUND(AVG(percentage_scanned_from_cache), 1)            AS pct_from_cache,
       SUM(bytes_spilled_to_remote_storage)                    AS remote_spill
FROM   snowflake.account_usage.query_history
WHERE  start_time &gt;= DATEADD('day', -7, CURRENT_TIMESTAMP())
  AND  execution_status = 'SUCCESS'
GROUP  BY query_hash
HAVING total_minutes &gt; 5
ORDER  BY total_minutes DESC
LIMIT  25;

-- The same triage on the real-time retail OLAP tier
SELECT normalized_query_hash,
       count()                              AS executions,
       round(avg(query_duration_ms))        AS avg_ms,
       formatReadableSize(sum(read_bytes))  AS read_total,
       round(sum(read_rows) / 1e9, 2)       AS billion_rows,
       formatReadableSize(max(memory_usage)) AS peak_memory
FROM   system.query_log
WHERE  type = 'QueryFinish' AND event_time &gt; now() - INTERVAL 7 DAY
GROUP  BY normalized_query_hash
ORDER  BY sum(query_duration_ms) DESC
LIMIT  25;</pre>
<p>Cost is treated as a reliability signal in retail data analytics: a pipeline that suddenly consumes three times its usual compute is almost always broken before it is expensive. Alerting therefore watches credits per run alongside duration and row counts, which is exactly the discipline behind MinervaDB <a href="https://minervadb.com/data-engineering/">high-performance data engineering</a>.</p>
<h2>SLOs, data quality and observability for retail data analytics<a class="anchor-link" id="slos-data-quality-and-observability-for-retail-data-analytics"></a></h2>
<p>There is no retail data analytics platform without service level objectives, only firefighting. Before MinervaDB accepts on-call responsibility we agree measurable objectives with merchandising, supply chain and finance, instrument them, and publish them on a dashboard both sides can see. The set below is the default deployed on day one of an engagement.</p>
<table>
<thead>
<tr>
<th>Service level objective</th>
<th>How it is measured</th>
<th>Default retail target</th>
</tr>
</thead>
<tbody>
<tr>
<td>Freshness of certified trading marts</td>
<td>Age of newest order line versus POS commit time</td>
<td>99% of intervals under 30 minutes</td>
</tr>
<tr>
<td>Pipeline success rate</td>
<td>Successful DAG runs including automatic retries</td>
<td>99.5% monthly, 99.9% in peak trading weeks</td>
</tr>
<tr>
<td>Dashboard query latency</td>
<td>p95 execution time for certified BI queries</td>
<td>Under 3 seconds on the warehouse, under 300 ms on real-time OLAP</td>
</tr>
<tr>
<td>POS to warehouse reconciliation</td>
<td>Row and net sales variance between source and warehouse</td>
<td>Under 0.1% daily, zero unexplained variance monthly</td>
</tr>
<tr>
<td>Unit economics</td>
<td>Compute credits or slot-hours per certified report</td>
<td>Flat or declining quarter over quarter</td>
</tr>
<tr>
<td>Recovery objectives</td>
<td>Tested restore and replay of the warehouse and lakehouse</td>
<td>RPO 15 minutes, RTO 4 hours, verified quarterly</td>
</tr>
</tbody>
</table>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Freshness, volume and drift monitor for retail marts">-- One row per certified retail mart: freshness, volume anomaly and null drift
WITH observed AS (
    SELECT 'dw.fact_sales_line'                                    AS object_name,
           MAX(dw_loaded_at)                                       AS last_loaded_at,
           COUNT(*)                                                AS row_count,
           COUNT_IF(store_key IS NULL) / NULLIF(COUNT(*), 0)       AS null_store_rate
    FROM   dw.fact_sales_line
    WHERE  dw_loaded_at &gt;= DATEADD('day', -1, CURRENT_TIMESTAMP())
),
baseline AS (
    SELECT object_name,
           AVG(row_count)        AS mean_rows,
           STDDEV_POP(row_count) AS sd_rows
    FROM   monitoring.mart_volume_history
    WHERE  observed_on &gt;= DATEADD('day', -28, CURRENT_DATE())
      AND  day_of_week = DAYOFWEEK(CURRENT_DATE())   -- retail is weekly-seasonal
    GROUP  BY object_name
)
SELECT o.object_name,
       TIMESTAMPDIFF('minute', o.last_loaded_at, CURRENT_TIMESTAMP()) AS staleness_minutes,
       o.row_count,
       ROUND((o.row_count - b.mean_rows) / NULLIF(b.sd_rows, 0), 2)   AS volume_z_score,
       ROUND(o.null_store_rate * 100, 3)                              AS null_store_pct,
       CASE
         WHEN TIMESTAMPDIFF('minute', o.last_loaded_at, CURRENT_TIMESTAMP()) &gt; 45 THEN 'PAGE_ONCALL'
         WHEN ABS((o.row_count - b.mean_rows) / NULLIF(b.sd_rows, 0)) &gt; 3          THEN 'PAGE_ONCALL'
         WHEN o.null_store_rate &gt; 0.001                                           THEN 'WARN'
         ELSE 'OK'
       END                                                            AS action
FROM observed o
JOIN baseline b USING (object_name);</pre>
<p>Note the weekly seasonality filter. Comparing a Saturday against a 28-day mean will page your on-call engineer every weekend, which is how alert fatigue starts. Retail data analytics monitoring must compare like trading days with like trading days, and must widen its bands automatically around known promotional peaks.</p>
<h2>Governance, PII and PCI DSS in retail data analytics<a class="anchor-link" id="governance-pii-and-pci-dss-in-retail-data-analytics"></a></h2>
<p>Governance is inseparable from retail data analytics. The warehouse is usually the widest-reaching copy of your customer data, which makes it the most consequential system in an audit. Loyalty records, delivery addresses, marketing consent flags and payment tokens all end up there, and regional data-residency rules mean a single global table is rarely acceptable. MinervaDB implements least-privilege role hierarchies, tag-based classification, dynamic masking, row-level policies and immutable audit trails, then produces the evidence assessors ask for.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="Tag-based classification, masking and row-level security for retail customer data">-- 1. Classify once, enforce everywhere
CREATE TAG IF NOT EXISTS governance.data_sensitivity
  ALLOWED_VALUES 'public', 'internal', 'confidential', 'pii', 'payment';

ALTER TABLE dw.dim_customer MODIFY COLUMN email_address
  SET TAG governance.data_sensitivity = 'pii';

-- 2. Column-level dynamic masking driven by role, not by view sprawl
CREATE OR REPLACE MASKING POLICY governance.mask_email AS (val STRING)
RETURNS STRING -&gt;
  CASE
    WHEN CURRENT_ROLE() IN ('DATA_PROTECTION_OFFICER', 'ANALYTICS_ADMIN') THEN val
    WHEN CURRENT_ROLE() IN ('MERCHANDISING_ANALYST', 'STORE_OPS')
         THEN REGEXP_REPLACE(val, '^[^@]+', '****')
    ELSE '***MASKED***'
  END;

ALTER TAG governance.data_sensitivity
  SET MASKING POLICY governance.mask_email FOR STRING;

-- 3. Row-level security for regional data residency across trading markets
CREATE OR REPLACE ROW ACCESS POLICY governance.market_rap AS (country_code CHAR(2))
RETURNS BOOLEAN -&gt;
  EXISTS (
    SELECT 1 FROM governance.role_market_map m
    WHERE m.role_name = CURRENT_ROLE()
      AND (m.country_code = country_code OR m.country_code = 'ALL')
  );

ALTER TABLE dw.dim_customer
  ADD ROW ACCESS POLICY governance.market_rap ON (country_code);

-- 4. Prove it: who touched loyalty PII in the last 30 days
SELECT user_name, role_name, query_start_time, LEFT(query_text, 100) AS statement
FROM   snowflake.account_usage.access_history a,
       LATERAL FLATTEN(input =&gt; a.base_objects_accessed) b
WHERE  b.value:"columns"[0]:"columnName"::STRING = 'EMAIL_ADDRESS'
  AND  query_start_time &gt;= DATEADD('day', -30, CURRENT_TIMESTAMP())
ORDER  BY query_start_time DESC;</pre>
<p>Retail estates routinely operate inside SOC 2, ISO 27001, PCI DSS and GDPR at the same time, plus regional residency regimes. The practical rule for retail data analytics is that raw pan data never enters the warehouse at all: tokenise at the payment gateway, land the token, and keep the cardholder data environment outside the analytics boundary entirely. That single decision removes most of the PCI DSS scope from your data platform.</p>
<h2>Personalisation, forecasting and vector search on the retail stack<a class="anchor-link" id="personalisation-forecasting-and-vector-search-on-the-retail-stack"></a></h2>
<p>Machine learning is not a separate platform, it is another consumer of the same conformed marts. Demand forecasting, size-curve optimisation, markdown planning, propensity scoring and next-best-offer models all train on the retail data analytics warehouse and serve from a feature store that shares lineage with the dashboards. Keeping features and metrics in the same repository is what stops a model and a board pack disagreeing about what a unit of demand is.</p>
<p>Semantic product discovery and retrieval-augmented merchandising assistants add a vector workload on top. Product descriptions, review text, supplier specifications and image embeddings live in a vector index alongside the catalogue, most often in PostgreSQL with <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener">pgvector</a> for smaller catalogues, and in a dedicated vector store once you pass a few hundred million embeddings. MinervaDB covers this under <a href="https://minervadb.com/vector-data-engineering/">vector data engineering</a>, and the operational concerns are familiar: index build time, recall versus latency, and the cost of re-embedding after every catalogue refresh.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="sql" data-enlighter-title="pgvector semantic product search for retail catalogue discovery">CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE catalogue.product_embedding (
    product_key   BIGINT PRIMARY KEY,
    sku           TEXT NOT NULL,
    category      TEXT NOT NULL,
    embedding     vector(1024) NOT NULL,
    refreshed_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- HNSW gives sub-10 ms recall at retail catalogue scale
CREATE INDEX idx_product_embedding_hnsw
    ON catalogue.product_embedding
    USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 128);

-- Hybrid search: semantic similarity constrained by merchandising rules
SELECT p.sku,
       p.category,
       1 - (p.embedding &lt;=&gt; :query_embedding) AS similarity,
       s.on_hand_qty
FROM   catalogue.product_embedding p
JOIN   inventory.stock_on_hand s ON s.product_key = p.product_key
WHERE  p.category = ANY(:allowed_categories)
  AND  s.on_hand_qty &gt; 0
ORDER  BY p.embedding &lt;=&gt; :query_embedding
LIMIT  24;</pre>
<p>Open table formats matter here too. If basket and clickstream history sits in Iceberg or Delta, a training job can read it with Spark or <a href="https://trino.io/docs/current/" target="_blank" rel="noopener">Trino</a> without exporting a copy, and a BI tool such as <a href="https://superset.apache.org/docs/intro" target="_blank" rel="noopener">Apache Superset</a> can query the same files. One copy, many engines, is the cheapest architectural decision in retail data analytics.</p>
<h2>A 90-day retail data analytics rollout plan<a class="anchor-link" id="a-90-day-retail-data-analytics-rollout-plan"></a></h2>
<p>MinervaDB does not begin by rewriting your platform. We measure it, remove the fragility that causes pages at 03:00, and only then invest in optimisation and automation. Every retail data analytics engagement follows the same deliverable-driven path, with written outputs and measured before-and-after benchmarks at each phase.</p>
<figure><img decoding="async" title="Ninety day retail data analytics rollout plan from discovery to 24x7 operations" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDMyMCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJOaW5ldHkgZGF5IHJldGFpbCBkYXRhIGFuYWx5dGljcyByb2xsb3V0IHBsYW4gZnJvbSBkaXNjb3ZlcnkgdG8gMjR4NyBvcGVyYXRpb25zIiBzdHlsZT0id2lkdGg6MTAwJTtoZWlnaHQ6YXV0bztiYWNrZ3JvdW5kOiNmZmZmZmY7Ym9yZGVyOjFweCBzb2xpZCAjZDdkZGU1O2JvcmRlci1yYWRpdXM6MTJweCI+PHRleHQgeD0iNjAwIiB5PSIzMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzEyMjYzZiI+VGhlIE1pbmVydmFEQiA5MC1kYXkgcmV0YWlsIGRhdGEgYW5hbHl0aWNzIGVuZ2FnZW1lbnQ8L3RleHQ+PGxpbmUgeDE9IjEzMCIgeTE9IjEwMCIgeDI9IjEwNzAiIHkyPSIxMDAiIHN0cm9rZT0iI2M5ZDNlMCIgc3Ryb2tlLXdpZHRoPSIzIi8+PHJlY3QgeD0iMjAiIHk9IjEyMCIgd2lkdGg9IjIwOCIgaGVpZ2h0PSIxNTAiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PGNpcmNsZSBjeD0iMTI0IiBjeT0iMTAwIiByPSIyMiIgZmlsbD0iIzFmMzg2NCIvPjx0ZXh0IHg9IjEyNCIgeT0iMTA3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTciIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj4xPC90ZXh0Pjx0ZXh0IHg9IjEyNCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM1YTZiN2QiPkRheSAwLTM8L3RleHQ+PHRleHQgeD0iMTI0IiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNC41IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iIzFmMzg2NCI+RGlzY292ZXI8L3RleHQ+PHRleHQgeD0iMTI0IiB5PSIyMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzIyMzAzZiI+QXJjaGl0ZWN0dXJlIGFuZCBjb3N0IGF1ZGl0PC90ZXh0Pjx0ZXh0IHg9IjEyNCIgeT0iMjIyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMyMjMwM2YiPldvcmtsb2FkIGFuZCBTTE8gaW52ZW50b3J5PC90ZXh0Pjx0ZXh0IHg9IjEyNCIgeT0iMjQ0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMyMjMwM2YiPlJpc2sgcmVnaXN0ZXIgYnkgc2V2ZXJpdHk8L3RleHQ+PHJlY3QgeD0iMjU4IiB5PSIxMjAiIHdpZHRoPSIyMDgiIGhlaWdodD0iMTUwIiByeD0iMTAiIGZpbGw9IiNmN2Y5ZmMiIHN0cm9rZT0iI2M5ZDNlMCIvPjxjaXJjbGUgY3g9IjM2MiIgY3k9IjEwMCIgcj0iMjIiIGZpbGw9IiMyMjU3N2EiLz48dGV4dCB4PSIzNjIiIHk9IjEwNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE3IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+MjwvdGV4dD48dGV4dCB4PSIzNjIiIHk9IjE1MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNWE2YjdkIj5EYXkgNC0xNDwvdGV4dD48dGV4dCB4PSIzNjIiIHk9IjE3MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0LjUiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMjI1NzdhIj5TdGFiaWxpc2U8L3RleHQ+PHRleHQgeD0iMzYyIiB5PSIyMDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzIyMzAzZiI+Rml4IENEQyBhbmQgREFHIGZyYWdpbGl0eTwvdGV4dD48dGV4dCB4PSIzNjIiIHk9IjIyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5Nb25pdG9yaW5nIGFuZCBhbGVydCBiYXNlbGluZTwvdGV4dD48dGV4dCB4PSIzNjIiIHk9IjI0NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5SdW5ib29rcyBhbmQgb24tY2FsbCBoYW5kb3ZlcjwvdGV4dD48cmVjdCB4PSI0OTYiIHk9IjEyMCIgd2lkdGg9IjIwOCIgaGVpZ2h0PSIxNTAiIHJ4PSIxMCIgZmlsbD0iI2Y3ZjlmYyIgc3Ryb2tlPSIjYzlkM2UwIi8+PGNpcmNsZSBjeD0iNjAwIiBjeT0iMTAwIiByPSIyMiIgZmlsbD0iIzJhN2Y2MiIvPjx0ZXh0IHg9IjYwMCIgeT0iMTA3IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTciIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjZmZmZmZmIj4zPC90ZXh0Pjx0ZXh0IHg9IjYwMCIgeT0iMTUwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IiM1YTZiN2QiPkRheSAxNS00NTwvdGV4dD48dGV4dCB4PSI2MDAiIHk9IjE3MiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0LjUiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjMmE3ZjYyIj5PcHRpbWlzZTwvdGV4dD48dGV4dCB4PSI2MDAiIHk9IjIwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5RdWVyeSBhbmQgbGF5b3V0IHR1bmluZzwvdGV4dD48dGV4dCB4PSI2MDAiIHk9IjIyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5HcmFpbiBhbmQgbW9kZWwgY29ycmVjdGlvbnM8L3RleHQ+PHRleHQgeD0iNjAwIiB5PSIyNDQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzIyMzAzZiI+Q29zdCBndWFyZHJhaWxzIGFuZCBidWRnZXRzPC90ZXh0PjxyZWN0IHg9IjczNCIgeT0iMTIwIiB3aWR0aD0iMjA4IiBoZWlnaHQ9IjE1MCIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48Y2lyY2xlIGN4PSI4MzgiIGN5PSIxMDAiIHI9IjIyIiBmaWxsPSIjNmIzZmEwIi8+PHRleHQgeD0iODM4IiB5PSIxMDciIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNyIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNmZmZmZmYiPjQ8L3RleHQ+PHRleHQgeD0iODM4IiB5PSIxNTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzVhNmI3ZCI+RGF5IDQ2LTkwPC90ZXh0Pjx0ZXh0IHg9IjgzOCIgeT0iMTcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQuNSIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM2YjNmYTAiPkF1dG9tYXRlPC90ZXh0Pjx0ZXh0IHg9IjgzOCIgeT0iMjAwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSGVsdmV0aWNhLEFyaWFsLHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTEiIGZpbGw9IiMyMjMwM2YiPkNJL0NEIGZvciBkYnQgYW5kIEFpcmZsb3c8L3RleHQ+PHRleHQgeD0iODM4IiB5PSIyMjIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzIyMzAzZiI+RGF0YSBxdWFsaXR5IGNvbnRyYWN0czwvdGV4dD48dGV4dCB4PSI4MzgiIHk9IjI0NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5TZWxmLWhlYWxpbmcgYmFja2ZpbGxzPC90ZXh0PjxyZWN0IHg9Ijk3MiIgeT0iMTIwIiB3aWR0aD0iMjA4IiBoZWlnaHQ9IjE1MCIgcng9IjEwIiBmaWxsPSIjZjdmOWZjIiBzdHJva2U9IiNjOWQzZTAiLz48Y2lyY2xlIGN4PSIxMDc2IiBjeT0iMTAwIiByPSIyMiIgZmlsbD0iI2E2M2Q0MCIvPjx0ZXh0IHg9IjEwNzYiIHk9IjEwNyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjE3IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2ZmZmZmZiI+NTwvdGV4dD48dGV4dCB4PSIxMDc2IiB5PSIxNTAiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzVhNmI3ZCI+T25nb2luZzwvdGV4dD48dGV4dCB4PSIxMDc2IiB5PSIxNzIiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNC41IiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI2E2M2Q0MCI+T3BlcmF0ZSAyNHg3PC90ZXh0Pjx0ZXh0IHg9IjEwNzYiIHk9IjIwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5Gb2xsb3ctdGhlLXN1biBvbi1jYWxsPC90ZXh0Pjx0ZXh0IHg9IjEwNzYiIHk9IjIyMiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBmaWxsPSIjMjIzMDNmIj5Nb250aGx5IHBlcmZvcm1hbmNlIHJldmlldzwvdGV4dD48dGV4dCB4PSIxMDc2IiB5PSIyNDQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJIZWx2ZXRpY2EsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgZmlsbD0iIzIyMzAzZiI+UXVhcnRlcmx5IHJvYWRtYXAgYW5kIFFCUjwvdGV4dD48dGV4dCB4PSI2MDAiIHk9IjMwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkhlbHZldGljYSxBcmlhbCxzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyLjUiIGZpbGw9IiM1YTZiN2QiPkV2ZXJ5IHBoYXNlIGVuZHMgd2l0aCB3cml0dGVuIGRlbGl2ZXJhYmxlcywgbWVhc3VyZWQgYmVuY2htYXJrcyBhbmQgYSBuYW1lZCBNaW5lcnZhREIgcHJpbmNpcGFsIGVuZ2luZWVyPC90ZXh0Pjwvc3ZnPg==" alt="Ninety day retail data analytics rollout plan from discovery to 24x7 operations"><figcaption><em>Figure 4: the MinervaDB onboarding path for a modern retail data analytics engagement.</em></figcaption></figure>
<p>The discovery report typically lands within three business days and identifies enough quick wins to cut warehouse spend by twenty to forty percent while removing the most common source of stale trading dashboards. Deeper modelling and performance gains accrue across the first ninety days, and the 24x7 rotation described in <a href="https://minervadb.com/24-7-emergency-dba-coverage/">emergency DBA coverage</a> underwrites the whole thing.</p>
<h2>Retail data analytics FAQ<a class="anchor-link" id="retail-data-analytics-faq"></a></h2>
<h3>What is the minimum viable retail data analytics stack?<a class="anchor-link" id="what-is-the-minimum-viable-retail-data-analytics-stack"></a></h3>
<p>For a single-brand retailer under about fifty million order lines a year: log-based CDC from the checkout database, an object-store landing zone in Parquet, one cloud warehouse, dbt for transformation, Airflow or Dagster for orchestration, and one BI tool with a governed semantic layer. Add a real-time OLAP engine only when a business process genuinely needs sub-second answers, not because a dashboard feels slow.</p>
<h3>Should retail data analytics run on a warehouse or a lakehouse?<a class="anchor-link" id="should-retail-data-analytics-run-on-a-warehouse-or-a-lakehouse"></a></h3>
<p>Both, and the boundary is economic rather than religious. Keep multi-year basket and clickstream history in an open table format on object storage where it is cheap and portable, and keep the certified marts that finance and merchandising depend on in the warehouse where concurrency, governance and query latency are best. Storing raw history in Iceberg or Delta preserves your right to change warehouse vendors later.</p>
<h3>How do you handle returns and credit notes without restating history?<a class="anchor-link" id="how-do-you-handle-returns-and-credit-notes-without-restating-history"></a></h3>
<p>Model returns as negative-quantity lines against the same order-line fact, and reprocess a trailing window on every incremental run so late arrivals self-heal. That combination keeps net sales additive, avoids a separate returns fact that nobody remembers to join, and removes the manual month-end restatement that plagues most retail data analytics teams.</p>
<h3>How fast can a retail data analytics platform realistically be?<a class="anchor-link" id="how-fast-can-a-retail-data-analytics-platform-realistically-be"></a></h3>
<p>With log-based CDC, a stream processor and a real-time OLAP tier, five to ten seconds from a till transaction to a store dashboard is routine. The certified warehouse marts that feed finance normally land within fifteen to thirty minutes. The limiting factor is almost never the engine, it is the transformation strategy and the willingness to pre-aggregate.</p>
<h3>Can MinervaDB own on-call for our retail pipelines?<a class="anchor-link" id="can-minervadb-own-on-call-for-our-retail-pipelines"></a></h3>
<p>Yes. On the Mission Critical tier MinervaDB holds the pager for pipelines, warehouses and BI availability, responds to P1 incidents within fifteen minutes, and delivers a written root cause analysis with a permanent fix rather than a restart. Full details are on the <a href="https://minervadb.com/data-analytics-and-data-warehousing-support/">Data Analytics and Data Warehousing Support</a> page.</p>
<h3>Do we have to migrate our existing platform?<a class="anchor-link" id="do-we-have-to-migrate-our-existing-platform"></a></h3>
<p>No. Most engagements begin as pure support on the incumbent retail data analytics stack, whether that is Snowflake, BigQuery, Redshift, Databricks, ClickHouse, Greenplum or PostgreSQL. Any migration MinervaDB later recommends is justified with measured benchmarks, a cost model and a reversible cutover plan.</p>
<h3>How do you reduce cloud warehouse cost without hurting trading dashboards?<a class="anchor-link" id="how-do-you-reduce-cloud-warehouse-cost-without-hurting-trading-dashboards"></a></h3>
<p>By eliminating waste before touching capacity: incremental instead of full refresh, pruning-friendly layouts, pre-aggregated roll-ups for the tiles everyone opens at 08:00, result caching, auto-suspend and statement timeouts, workload isolation so one supplier export cannot inflate a shared cluster, and per-category budgets with chargeback. Performance usually improves as cost falls.</p>
<h3>Does retail data analytics support cover our source databases too?<a class="anchor-link" id="does-retail-data-analytics-support-cover-our-source-databases-too"></a></h3>
<p>Yes. The same team supports the OLTP sources feeding the warehouse, including <a href="https://minervadb.com/postgresql-support/">PostgreSQL</a> and <a href="https://minervadb.com/mysql-support/">MySQL</a> checkout and catalogue databases, so replication slot health, binlog retention and CDC lag are never somebody else's problem.</p>
<h2>Talk to a MinervaDB retail data analytics expert<a class="anchor-link" id="talk-to-a-minervadb-retail-data-analytics-expert"></a></h2>
<p>Tell us which engines you run, where the pain is and what your trading calendar looks like. A MinervaDB principal engineer will review your retail data analytics architecture, quantify the risk and cost exposure, and show you exactly what full-stack support would change. No obligation and no scripted sales call. <a href="https://minervadb.com/contact-minervadb-book-an-appointment/">Book an appointment with MinervaDB</a> or read the <a href="https://minervadb.com/minervadb-consultative-support-2/">consultative support overview</a> first.</p>
<h3>Further reading from MinervaDB<a class="anchor-link" id="further-reading-from-minervadb"></a></h3>
<ul>
<li><a href="https://minervadb.com/data-analytics-and-data-warehousing-support/">Data Analytics and Data Warehousing Support</a></li>
<li><a href="https://minervadb.com/data-strategy-and-analytics/">Data strategy and analytics consulting</a></li>
<li><a href="https://minervadb.com/data-engineering/">Elite high-performance data engineering</a></li>
<li><a href="https://minervadb.com/kafka-support/">Apache Kafka support and streaming operations</a></li>
<li><a href="https://minervadb.com/clickhouse-consulting/">ClickHouse consulting for real-time analytics</a></li>
<li><a href="https://minervadb.com/vector-data-engineering/">Vector data engineering for AI workloads</a></li>
<li><a href="https://minervadb.com/24-7-emergency-dba-coverage/">24x7 emergency DBA coverage</a></li>
</ul>
<h3>Upstream documentation referenced in this guide<a class="anchor-link" id="upstream-documentation-referenced-in-this-guide"></a></h3>
<ul>
<li><a href="https://www.postgresql.org/docs/current/logical-replication.html" target="_blank" rel="noopener">PostgreSQL logical replication</a></li>
<li><a href="https://debezium.io/documentation/reference/stable/connectors/postgresql.html" target="_blank" rel="noopener">Debezium PostgreSQL connector</a></li>
<li><a href="https://kafka.apache.org/documentation/" target="_blank" rel="noopener">Apache Kafka documentation</a></li>
<li><a href="https://nightlies.apache.org/flink/flink-docs-stable/" target="_blank" rel="noopener">Apache Flink documentation</a></li>
<li><a href="https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/mergetree" target="_blank" rel="noopener">ClickHouse MergeTree engine</a></li>
<li><a href="https://iceberg.apache.org/spec/" target="_blank" rel="noopener">Apache Iceberg table specification</a></li>
<li><a href="https://delta.io/" target="_blank" rel="noopener">Delta Lake</a></li>
<li><a href="https://docs.getdbt.com/docs/build/incremental-models" target="_blank" rel="noopener">dbt incremental models</a></li>
<li><a href="https://airflow.apache.org/docs/apache-airflow/stable/index.html" target="_blank" rel="noopener">Apache Airflow documentation</a></li>
<li><a href="https://docs.snowflake.com/en/user-guide/tables-clustering-keys" target="_blank" rel="noopener">Snowflake clustering keys</a></li>
<li><a href="https://trino.io/docs/current/" target="_blank" rel="noopener">Trino documentation</a></li>
<li><a href="https://superset.apache.org/docs/intro" target="_blank" rel="noopener">Apache Superset documentation</a></li>
<li><a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener">pgvector for PostgreSQL</a></li>
</ul>
<p><em>MinervaDB Inc. delivers vendor-neutral retail data analytics support, database performance engineering and 24x7 data platform operations for modern retail businesses worldwide.</em></p>

<p><a href="https://minervadb.com/retail-data-analytics-modern-retail-stack/">Retail Data Analytics: Ultimate 2026 Modern Retail Stack</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Herding Goats Across Continents: How We Took 100 People From Around the World to Turkey for an Offsite (and Lived to Tell the Tale)</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/08/06/herding-goats-percona-turkey-offsite/" />
      <id>https://percona.community/blog/2026/08/06/herding-goats-percona-turkey-offsite/</id>
      <updated>2026-08-06T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Every great journey needs a guide.</p>
<p><a href="https://percona.community/blog/2026/08/06/herding-goats-percona-turkey-offsite/">Herding Goats Across Continents: How We Took 100 People From Around the World to Turkey for an Offsite (and Lived to Tell the Tale)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Every great journey needs a guide.</p>
<p>Ours just happened to be a mountain goat.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/08/turkey-goat-lego.jpg" alt="LEGO mountain goat with Percona branding on a laptop"></figure>
</p>
<p>Fresh off a brand-new Percona branding reveal &mdash; complete with a rugged mountain goat mascot, a custom-made goat cake, and even a (surprisingly inspirational) LEGO mountain goat &mdash; we set out to bring our global Software Engineering team together in Turkey.</p>
<p>And while planning an offsite for a fully remote, globally distributed team sounds simple enough&hellip; just wait until you try to do it.</p>
<p>We&rsquo;re talking about 100 people, 30+ countries, coordinating over 200,000 miles of travel, multiple time zones, passports, dietary restrictions, flight delays, and the occasional &ldquo;wait, what do you mean my passport expires in three months?&rdquo; <em>(you know who you are)</em> moment. Suddenly, you&rsquo;re not just planning a trip &mdash; you&rsquo;re orchestrating a small traveling universe.</p>
<p>Welcome to the step-by-step story of how we brought a global Percona team together in Turkey, and what it <em>really</em> takes to make something like this happen.</p>
<h2 id="step-one-accept-that-details-are-your-new-personality">Step One: Accept That Details Are Your New Personality<a class="anchor-link" id="step-one-accept-that-details-are-your-new-personality"></a></h2>
<p>When planning an offsite of this scale, you quickly learn one thing: there is no such thing as being &ldquo;too detailed.&rdquo;</p>
<p>There is also no such thing as too much information, or having too much collaboration, when it comes to working through the countless moving pieces.</p>
<p>Success at an event like this doesn&rsquo;t come from starting from scratch. It comes from leaning heavily on the people who have done it before. Their experiences, lessons learned, and even their past mistakes are incredibly valuable.</p>
<p>We needed to actively seek out those insights, ask questions, and build on what already existed rather than reinventing the wheel. If previous events weren&rsquo;t successful, you probably wouldn&rsquo;t be doing it again. Lean into previous experiences and successes.</p>
<p>At the same time, it&rsquo;s <strong>not just about repeating what&rsquo;s been done &mdash; it&rsquo;s about refining it.</strong> Each offsite should be a better, more thoughtful version of the last. That means taking previous experiences, identifying what worked and what didn&rsquo;t, and making deliberate improvements across logistics, communication, and attendee experience.</p>
<p>None of this happens last minute. To truly manage expectations and execute smoothly, planning conversations need to start early &mdash; ideally 6&ndash;8 months in advance <em>(even though the Percona Turkey retreat was accomplished in just four months)</em>. That runway gives teams the time to align on goals, pressure-test ideas, collaborate across functions, and work through the inevitable complexities that come with an event of this scale.</p>
<p>Success lies in thinking through <em>every single touchpoint</em> of the attendee experience. From the moment attendees board their flight, train, car, or motorcycle to the final farewell high-five, it&rsquo;s an experience.</p>
<p>Paying close attention to each touchpoint shows up in many moments, including:</p>
<ul>
<li>Branded coasters placed on every table setting during business meetings (yes, people notice)</li>
<li>Signage throughout the venue so no one ends up in a yoga class instead of a database workshop</li>
<li>Check-in gifts that reinforce the Percona brand (and maybe include a surprise or two&hellip; goat-related, naturally)</li>
<li>Carefully planned cocktail hours where &ldquo;networking&rdquo; magically turns into genuine connection</li>
<li>Group dinners where teammates finally meet the humans behind the Slack avatars</li>
<li>A candy swap featuring treats from every country represented &mdash; it doesn&rsquo;t need to be formal. Everyone piles their local candies on a table for the masses</li>
<li>A myriad of team-building activities spanning four days, including a competition to build the tallest free-standing printer-paper tower using only paper and tape <em>(sidenote: be prepared for every single team to think that they won and the other teams cheated)</em></li>
</ul>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/08/turkey-paper-tower.jpg" alt="Team building a paper tower at the Percona Turkey offsite"></figure>
</p>
<p>But what people remember most isn&rsquo;t just the agenda, t-shirts, stickers, goat swag, candy, or coffee breaks. It&rsquo;s the combination of these thoughtful touches woven into every moment.</p>
<p>It&rsquo;s not just logistics that make the trip &mdash; it&rsquo;s using those details and that collaboration to master storytelling.</p>
<h2 id="step-two-time-may-not-be-on-your-side-but-deadlines-and-effective-communication-are">Step Two: Time May Not Be on Your Side (But Deadlines and Effective Communication Are)<a class="anchor-link" id="step-two-time-may-not-be-on-your-side-but-deadlines-and-effective-communication-are"></a></h2>
<p>A trip like this doesn&rsquo;t come together overnight. But sometimes it needs to come together pretty close to overnight. That means a very well thought-out internal communications plan and schedule that answers as many questions as possible <em>before</em> they are even asked.</p>
<p>But no matter how solid a formal communications plan is, there will be moments when questions get asked that were already addressed &mdash; which is why finding other ways to communicate, collaborate, and involve stakeholders and attendees is <em>crucial</em>. Think dedicated Slack channels with full transparency and readily available information, weekly touchpoints with department heads who can convey updates and reminders to their teams, and frequent conversations with finance, human resources, and legal throughout.</p>
<p>Behind the scenes, you&rsquo;re looking at <strong>100&ndash;150 hours of internal planning</strong> &mdash; all before the first suitcase is packed.</p>
<p>With that level of complexity, time becomes both a constraint and a forcing function. Deadlines matter, timelines matter, and how you communicate along the way matters even more. Clear, consistent communication is what keeps everything moving forward when there are dozens of parallel workstreams and stakeholders involved.</p>
<p>Internally, communication has to be frequent, detailed, and often repetitive. There&rsquo;s no room for assumptions or &ldquo;I thought someone else had that covered.&rdquo; Every update, decision, dependency, and small detail needs to be shared openly and documented so nothing slips through the cracks.</p>
<p>The more visibility the team has, the more aligned everyone stays.</p>
<p><figure><img decoding="async" src="https://percona.community/blog/2026/08/turkey-dinner-toast.jpg" alt="Colleagues toasting at dinner during the Turkey offsite"></figure>
</p>
<p>That means being proactive, not reactive. Attendees need consistent touchpoints leading up to the trip: detailed itineraries, travel guidance, visa reminders, packing expectations, local logistics, and clear points of contact. Information should be centralized, easy to access, and reinforced multiple times so no one is left guessing.</p>
<p>During the event, communication doesn&rsquo;t stall. In fact, it accelerates &mdash; with adaptability, pivots, and clear, concise explanations.</p>
<p>Real-time updates, schedule reminders, transportation details, and contingency plans all need to be communicated clearly and quickly. When done right, attendees feel taken care of, confident, and fully present in the experience rather than worrying about logistics.</p>
<p>At the end of the day, strong communication &mdash; both internally and externally &mdash; is what turns a complex, global operation into something that feels seamless. It&rsquo;s not just about sharing information; it&rsquo;s about creating clarity, building trust, and ensuring every single person knows exactly where they need to be and when.</p>
<p>This also means over-communicating by design. Weekly check-ins turn into twice-weekly syncs as the event gets closer. Quick updates become detailed run-of-show documents. Slack threads involving all attendees, shared docs, and status trackers become the backbone of execution. It may feel like a lot, but that level of transparency is what prevents last-minute surprises.</p>
<p>Oh yeah &mdash; then there&rsquo;s vendor coordination and communication.</p>
<p>Hotels. Transportation. AV teams. Catering. Swag production. Signage. Shipping. Local experiences. Backup plans for your backup plans.</p>
<p>Each one comes with:</p>
<ul>
<li>Deadlines</li>
<li>Dependencies</li>
<li>Payment schedules</li>
<li>And the occasional &ldquo;we need final numbers by tomorrow or your menu disappears&rdquo; situation</li>
</ul>
<p>Miss a deadline? That&rsquo;s not just a small hiccup &mdash; it can mean:</p>
<ul>
<li>Increased costs</li>
<li>Limited availability</li>
<li>Or a last-minute scramble that no one wants to experience</li>
</ul>
<p><strong>Precision, effective communication, and timing aren&rsquo;t optional &mdash; they&rsquo;re everything.</strong></p>
<h2 id="step-three-build-the-experience-not-just-the-agenda">Step Three: Build the Experience, Not Just the Agenda<a class="anchor-link" id="step-three-build-the-experience-not-just-the-agenda"></a></h2>
<p>You can have the best presentations in the world, but that&rsquo;s not what people will remember.</p>
<p>What they <em>will</em> remember &mdash; and what they will certainly talk about afterwards:</p>
<ul>
<li>The first time they met a teammate in person after years of working together online</li>
<li>The laughs over dinner</li>
<li>The spontaneous conversations during coffee breaks</li>
<li>The shared &ldquo;we made it here&rdquo; energy</li>
</ul>
<p>For a company like Percona &mdash; <strong>100% remote, spanning 50+ countries</strong> &mdash; these experiences and moments aren&rsquo;t just nice-to-haves. They&rsquo;re essential.</p>
<p>An offsite like this creates:</p>
<ul>
<li>Stronger team bonding across regions and roles</li>
<li>Real human connections that make collaboration smoother</li>
<li>Cross-functional understanding that Slack threads simply can&rsquo;t replicate</li>
<li>A sense of belonging that transcends time zones</li>
</ul>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/08/turkey-card-game.jpg" alt="Colleagues playing cards together at the Turkey offsite"></figure>
</p>
<p>In short: it turns coworkers into teammates. As a remote-only company, Percona has a uniqueness with nearly 14% of employees who have been with the company for 10+ years. This isn&rsquo;t an accident &mdash; it&rsquo;s a direct result of the kind of networking, partnership, and friendships that these types of retreats create.</p>
<p>The agenda for an offsite retreat should be intentionally designed to educate, inspire, and elevate the overall experience &mdash; not just fill time on a schedule. Every presentation needs to be thoughtfully curated to add real value, ensuring it contributes meaningfully rather than feeling like content for content&rsquo;s sake.</p>
<h2 id="step-four-expect-the-unexpected-and-pack-snacks">Step Four: Expect the Unexpected (and Pack Snacks)<a class="anchor-link" id="step-four-expect-the-unexpected-and-pack-snacks"></a></h2>
<p>No matter how well you plan, something will go sideways.</p>
<p>Flights will be delayed. Luggage will take a scenic tour of another country. Someone will forget something important (realistically, multiple someones).</p>
<p>The key is not avoiding problems, but trusting yourself, your team, and your planning to be ready for them.</p>
<p>A few survival tips:</p>
<ul>
<li><strong>Always have contingency plans (plural). Seriously. Always. And seriously &mdash; plural.</strong> Hurricane hitting the beach on closing outdoor dinner night? Have a space ready to go and an action plan for communicating the pivot. All your swag held up in customs? Find the local flea market and craft shops and make your gifts local. Speaker forgot a slide advancer <em>and</em> the venue&rsquo;s failed <em>and</em> the venue&rsquo;s backup failed? Keep one on the ready with you at all times.</li>
<li><strong>Over-communicate everything.</strong> You can&rsquo;t tell people important things too many times.</li>
<li><strong>Build buffer time into the schedule.</strong> Imagine trying to get your three-year-old out the door early in the morning. It&rsquo;s like that, but with 100 adults. Give yourself some cushions.</li>
<li><strong>Keep a sense of humor handy</strong> &mdash; it&rsquo;s arguably your most valuable tool. You can diffuse a lot of tension with a little humorous quip. Humor brings the tension down, eases the panic, and grounds everyone. Even the most panic-stricken, reactionary executive you&rsquo;ve ever met enjoys a good laugh in tense moments.</li>
<li><strong>Slow down</strong> &mdash; panic happens when speed outmaneuvers thought. It&rsquo;s not a race, and you don&rsquo;t have to beat everyone to a solution. Trust yourself and trust your team, and slow down enough to think through clearly without the heaviness of undue pressure.</li>
</ul>
<h2 id="step-five-remember-why-youre-doing-it">Step Five: Remember Why You&rsquo;re Doing It<a class="anchor-link" id="step-five-remember-why-youre-doing-it"></a></h2>
<p>After the spreadsheets, the emails, the vendor calls, and the 47th revision of the rooming list, it&rsquo;s easy to forget the bigger picture.</p>
<p>But then the event starts.</p>
<p>People arrive. Conversations spark. Teams connect. Ideas flow. Energy builds.</p>
<p>And suddenly, all 100&ndash;150 hours (and then some) make perfect sense.</p>
<p>What you&rsquo;ve created isn&rsquo;t just an offsite &mdash; it&rsquo;s an experience that strengthens your team in ways no virtual meeting ever could.</p>
<p>Don&rsquo;t lose sight of your objective.</p>
<h2 id="final-thoughts-keep-climbing">Final Thoughts: Keep Climbing<a class="anchor-link" id="final-thoughts-keep-climbing"></a></h2>
<p>Bringing 100 people across the world to Turkey wasn&rsquo;t easy &mdash; but it was worth every detail, every deadline, and every late-night planning session.</p>
<p>When you return home from executing a trip like this, you will want to shut off all notifications and take a few days to yourself without having to resolve anything. But also make sure you ponder what you would have liked to see done differently. Would arranging transfers from the airport to the resort be more cost-effective and smooth than relying on taxis? Most likely. Would building in more leisure time and opportunities for attendees to explore the local surroundings be worth sacrificing one or two sessions of training? Absolutely.</p>
<p>Learn from that and adapt for next time.</p>
<p>Because at the end of the day, investing in your people &mdash; especially in a remote-first world &mdash; is one of the most impactful things you can do.</p>
<p>And if that investment comes with a mountain goat mascot, a LEGO companion, custom cakes, coasters, t-shirts, drinks, food, and out-of-shape pickup basketball games, then you will find a team that fully leans into the journey and the message.</p>
<p><figure><img decoding="async" src="https://percona.community/blog/2026/08/turkey-road-trip.jpg" alt="Two Perconians on a post-retreat road trip in Turkey"></figure>
</p>
<p>We came as individuals from across the globe.</p>
<p>On the day the retreat ended, I found myself in a rental car with a colleague I&rsquo;d never met in person before. Two co-workers from different sides of the globe, different cultures, and two very different paths that led us to that spot. We drove for a full day, exploring site after site and chatting and laughing about life, work, and music.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/08/turkey-team-photo.jpg" alt="The Software Engineering team at the Percona Turkey offsite"></figure>
</p>
<p>This interaction and moment doesn&rsquo;t happen without retreats like this.</p>
<p>We came together as members of the Software Engineering group at a place we all worked, and left as a unified team of Perconians &mdash; and as friends.</p>
<p><strong>That&rsquo;s your <em>real</em> ROI.</strong></p>

<p><a href="https://percona.community/blog/2026/08/06/herding-goats-percona-turkey-offsite/">Herding Goats Across Continents: How We Took 100 People From Around the World to Turkey for an Offsite (and Lived to Tell the Tale)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ClusterControl 2.5.0 brings ClickHouse support to on-prem, cloud and hybrid environments</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/clustercontrol-2-5-0-brings-clickhouse-support-to-on-prem-cloud-and-hybrid-environments/" />
      <id>https://severalnines.com/blog/clustercontrol-2-5-0-brings-clickhouse-support-to-on-prem-cloud-and-hybrid-environments/</id>
      <updated>2026-08-05T08:38:18+03:00</updated>
      <author><name>Kyle Buzzell</name></author>
      <summary type="html"><![CDATA[<p>ClusterControl 2.5.0 is here, and it marks a milestone for the platform: ClickHouse joins the family of supported database engines — and with it, a capability no analytics vendor’s cloud can offer you. Wherever you run ClusterControl — on-premises, in any cloud, or hybrid — you can now deploy ClickHouse either as a standalone OLAP […]<br />
The post ClusterControl 2.5.0 brings ClickHouse support to on-prem, cloud and hybrid environments appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/clustercontrol-2-5-0-brings-clickhouse-support-to-on-prem-cloud-and-hybrid-environments/">ClusterControl 2.5.0 brings ClickHouse support to on-prem, cloud and hybrid environments</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://severalnines.com/clustercontrol">ClusterControl</a> 2.5.0 is here, and it marks a milestone for the platform: <strong>ClickHouse joins the family of supported database engines</strong> &mdash; and with it, a capability no analytics vendor&rsquo;s cloud can offer you.</p>
<p>Wherever you run ClusterControl &mdash; on-premises, in any cloud, or hybrid &mdash; you can now deploy <a href="https://severalnines.com/clustercontrol/databases/clickhouse">ClickHouse</a> either as a <strong>standalone OLAP workload</strong> or as the <strong>analytics tier of a comprehensive database stack</strong>, operated alongside your transactional databases under a single control plane. Your environment, your infrastructure, your data.</p>
<p>This release also delivers a major upgrade to PostgreSQL backup workflows, introduces built-in usage metering for consumption-based operations, and brings back a user-favorite capability from ClusterControl v1. Let&rsquo;s dig in</p>
<h2 class="wp-block-heading" id="h-clickhouse-analytics-on-your-terms-in-any-environment">ClickHouse: analytics on your terms, in any environment<a class="anchor-link" id="clickhouse-analytics-on-your-terms-in-any-environment"></a></h2>
<p>Analytical workloads aren&rsquo;t nice-to-have anymore. Whether it&rsquo;s real-time dashboards, log analytics, or feeding features to AI systems, the OLAP tier is becoming as operationally critical as the transactional tier &mdash; and it deserves the same automation, monitoring, and sovereignty guarantees. ClickHouse support means you can run that tier on your own infrastructure, under your own control, with full lifecycle automation.</p>
<p>But running ClickHouse yourself has meant either adopting a vendor&rsquo;s cloud &mdash; with your analytical data leaving your environment &mdash; or hand-rolling deployment, monitoring, and operations. ClusterControl 2.5.0 gives you a third option: <strong>full lifecycle automation for ClickHouse on infrastructure you control</strong>, whether that&rsquo;s a single analytics node or the OLAP tier of your entire database estate.</p>
<p>With 2.5.0 you can:</p>
<ul class="wp-block-list">
<li><strong>Deploy automatically</strong> &mdash; single-node instances or replicated clusters with embedded Keeper, provisioned through the same workflow you already use for MySQL, PostgreSQL, MongoDB, and Redis</li>
<li><strong>Monitor and alert</strong> through ClusterControl&rsquo;s unified dashboards &mdash; one pane of glass across your transactional and analytical estate</li>
<li><strong>Back up and restore</strong> your ClickHouse clusters</li>
<li><strong>Scale</strong> as analytical workloads grow</li>
<li><strong>Import existing ClickHouse clusters</strong> <strong>into </strong>ClusterControl management, and drive operations from the s9s CLI and API as well as the UI </li>
<li><strong>Secure inter-node communication &mdash;</strong> SSL-encrypted links between cluster nodes with per-node certificates</li>
</ul>
<p>Run it standalone if analytics is all you need. Or run it as one tier of a comprehensive stack &mdash; ClickHouse for OLAP next to MySQL, PostgreSQL, MongoDB, and Redis for OLTP, with load balancers, backups, and access control managed the same way across all of them. Same workflow, same alerting, same operational muscle memory, in whatever environment your requirements dictate.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="603" src="https://severalnines.com/wp-content/uploads/2026/08/clustercontrol-clickhouse-keeper-cluster-topology-1024x603.png" alt="" class="wp-image-44339"></figure>
<h3 class="wp-block-heading" id="h-what-s-next-for-clickhouse-in-clustercontrol">What&rsquo;s next for ClickHouse in ClusterControl<a class="anchor-link" id="whats-next-for-clickhouse-in-clustercontrol"></a></h3>
<p>This release is the foundation, and the roadmap builds directly on it. Planned improvements include:</p>
<ul class="wp-block-list">
<li><strong>Replication from MySQL and PostgreSQL into ClickHouse</strong> &mdash; feed your analytics tier directly from your operational databases, targeted for the next release</li>
<li><strong>Sharded ClickHouse cluster deployments</strong> for horizontally scaled analytical workloads</li>
<li><strong>ClickHouse user and role management</strong> from the UI</li>
</ul>
<h2 class="wp-block-heading" id="h-postgresql-backups">PostgreSQL backups<a class="anchor-link" id="postgresql-backups"></a></h2>
<p>Two significant improvements land for <a href="https://severalnines.com/clustercontrol/databases/postgresql">PostgreSQL</a> in 2.5.0:</p>
<p><strong>Streaming backups directly to S3.</strong> ClusterControl now streams pg_basebackup output straight to S3-compatible object storage &mdash; Amazon S3, MinIO, Google Cloud Storage (S3 mode), DigitalOcean Spaces, Wasabi, and others. No local disk staging, no oversized temp volumes on your database hosts, and a shorter backup-to-cloud pipeline overall.</p>
<p><strong>Native incremental backups (PostgreSQL 17+).</strong> PostgreSQL 17 introduced incremental backup support in pg_basebackup, and ClusterControl 2.5.0 puts it to work &mdash; including S3 upload. For large databases, that means dramatically smaller and faster backups between full baselines.</p>
<figure class="wp-block-image size-full"><img decoding="async" src="https://severalnines.com/wp-content/uploads/2026/07/image2.png" alt="Postgres GUI backup wizard showing streaming and incremental backup improvements in ClusterControl v2.5.0" class="wp-image-44331"></figure>
<h2 class="wp-block-heading">Usage metering and operator billing (Pay-As-You-Go)<a class="anchor-link" id="usage-metering-and-operator-billing-pay-as-you-go"></a></h2>
<p>For operators, MSPs, and platform teams running database services on a consumption basis, 2.5.0 introduces built-in <strong>usage metering</strong>:</p>
<ul class="wp-block-list">
<li>Hourly usage snapshots collected per controller across your managed estate</li>
<li>On-demand billing reports &mdash; estate-wide or filtered by tag or cluster &mdash; with <strong>cryptographic sealing</strong> and independent verification</li>
<li>A dedicated <strong>operator billing page</strong> in the multi-controller UI, with JSON/CSV export</li>
</ul>
<p>It&rsquo;s the foundation for Pay-As-You-Go commercial models on infrastructure you control &mdash; a natural fit for Sovereign DBaaS operations. The feature is off by default and only surfaced when metering is enabled.</p>
<h2 class="wp-block-heading">Cluster-wide configuration management is back<a class="anchor-link" id="cluster-wide-configuration-management-is-back"></a></h2>
<p>By popular demand from ClusterControl v1: change a database parameter across <strong>every node in a MySQL cluster in a single action</strong>. Dynamic parameters are applied at runtime &mdash; no per-node edit-and-restart cycle, and less downtime for routine configuration changes in production.</p>
<h2 class="wp-block-heading" id="h-postgresql-database-user-management">PostgreSQL database user management<a class="anchor-link" id="postgresql-database-user-management"></a></h2>
<p>reate and manage users and roles from the UI, with fine-grained privileges down to schema and table level, lock/disable/enable lifecycle operations, pg_hba.conf editing with tracked changes, and user search and filtering</p>
<h2 class="wp-block-heading">Other noteworthy improvements<a class="anchor-link" id="other-noteworthy-improvements"></a></h2>
<ul class="wp-block-list">
<li><strong>Scalable controllers pool hardening</strong> &mdash; UI-driven upgrades of remote pool members, automatic alarms on version mismatch, safer pool joins, and better resilience for large fleets</li>
<li>Enhanced <strong><a href="https://severalnines.com/clustercontrol/solutions/kubernetes">Kubernetes</a> database support</strong> &mdash; clusters and backup schedules can now be deployed through GitOps as reviewable Git pull requests, plus structured logging, metrics, and default dashboards for observability, more reliable cluster health reporting, and operator compatibility and security updates</li>
<li><strong>Custom </strong><strong>pg_hba</strong><strong> configuration</strong> &mdash; define custom pg_hba.conf rules at deployment time, plus UI editing of entries &mdash; useful for multi-datacenter PostgreSQL topologies</li>
<li><strong>Audit log filtering and export</strong> in the UI</li>
<li><strong>ProxySQL management</strong> promoted from a pop-up dialog to a dedicated page</li>
<li><strong>Faster database deployments &mdash; </strong>multithreaded installation provisions 4 nodes in parallel for PostgreSQL and MySQL clusters, with optional additional customization.</li>
<li><strong>Redis / Valkey Sentinel logs</strong> now included in error reports for easier failover debugging</li>
<li><strong>Content-Security-Policy headers</strong> in the web UI, and improved resilience for long-running backup jobs</li>
</ul>
<h2 class="wp-block-heading">Get started today<a class="anchor-link" id="get-started-today"></a></h2>
<p>Get ClusterControl v2.5.0 as a new user by signing up for a free 30-day trial or upgrade your CC deployment to practically deploy ClickHouse or bring your current deployment under ClusterControl&rsquo;s management and access the other game-changing capabilities. In the meantime, full details can be found in the <a href="#">Release Notes</a>.</p>
<p>Questions about running ClickHouse or any other engine with ClusterControl?<a href="https://severalnines.com/contact"> Talk to us</a>.</p>
<h2 class="wp-block-heading" id="h-install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included">Install ClusterControl in 10-minutes!<br> <strong>Free 30-day&nbsp;</strong>Enterprise trial included<a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading" id="instructions">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc
sudo ./install-cc     # omit sudo if you run as root</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https://&lt;ClusterControl_host&gt;/</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<p>The post <a href="https://severalnines.com/blog/clustercontrol-2-5-0-brings-clickhouse-support-to-on-prem-cloud-and-hybrid-environments/">ClusterControl 2.5.0 brings ClickHouse support to on-prem, cloud and hybrid environments</a> appeared first on <a href="https://severalnines.com">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/clustercontrol-2-5-0-brings-clickhouse-support-to-on-prem-cloud-and-hybrid-environments/">ClusterControl 2.5.0 brings ClickHouse support to on-prem, cloud and hybrid environments</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL 8.0.17 GTID Crash Safety Improvement</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/08/mysql-8017-gtid-crash-safety-improvement.html" />
      <id>https://jfg-mysql.blogspot.com/2026/08/mysql-8017-gtid-crash-safety-improvement.html</id>
      <updated>2026-08-04T22:22:16+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I have known for some times that there is an interesting improvement in MySQL 8.0.17 regarding GTID Crash Safety, but I have not had the time nor the need to look into it before.&#160; When writing my last post (Understanding MySQL Replication \"fatal error 1236\": [...]), I saw something interesting related to this, and it is now time to cover this on my blog. From my point of view, this change is</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/08/mysql-8017-gtid-crash-safety-improvement.html">MySQL 8.0.17 GTID Crash Safety Improvement</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I have known for some times that there is an interesting improvement in MySQL 8.0.17 regarding GTID Crash Safety, but I have not had the time nor the need to look into it before.&amp;nbsp; When writing my last post (Understanding MySQL Replication "fatal error 1236": [&hellip;]), I saw something interesting related to this, and it is now time to cover this on my blog. From my point of view, this change is</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/08/mysql-8017-gtid-crash-safety-improvement.html">MySQL 8.0.17 GTID Crash Safety Improvement</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>What Is Data Sovereignty and How Does MariaDB Protect Sovereignty in Cloud Databases?</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/what-is-data-sovereignty-and-how-does-mariadb-protect-sovereignty-in-cloud-databases/" />
      <id>https://mariadb.com/resources/blog/what-is-data-sovereignty-and-how-does-mariadb-protect-sovereignty-in-cloud-databases/</id>
      <updated>2026-08-04T18:33:49+03:00</updated>
      <author><name>Mani Nagasundaram</name></author>
      <summary type="html"><![CDATA[<p>Every organization now operates in a world where data doesn’t just need to be secure – it needs to be […]</p>
<p><a href="https://mariadb.com/resources/blog/what-is-data-sovereignty-and-how-does-mariadb-protect-sovereignty-in-cloud-databases/">What Is Data Sovereignty and How Does MariaDB Protect Sovereignty in Cloud Databases?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Every organization now operates in a world where data doesn&rsquo;t just need to be secure &ndash; it needs to be sovereign. Where it&rsquo;s stored, who can access it, which laws govern it, and whether a foreign government can compel its disclosure are no longer legal footnotes. They&rsquo;re board-level risks, procurement requirements, and increasingly, deciding factors in which database a regulated business is even&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/what-is-data-sovereignty-and-how-does-mariadb-protect-sovereignty-in-cloud-databases/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/what-is-data-sovereignty-and-how-does-mariadb-protect-sovereignty-in-cloud-databases/">What Is Data Sovereignty and How Does MariaDB Protect Sovereignty in Cloud Databases?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Hear Ye, Hear Ye: A Guide to MariaDB’s Governance Model</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/" />
      <id>https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/</id>
      <updated>2026-08-04T18:07:22+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>Be it known: MariaDB Server now has a clearer, publicly documented governance framework covering technical roles, subsystem ownership, decision-making, response expectations and continuity.<br />
Open source begins with access to the code. …<br />
Continue reading \"Hear Ye, Hear Ye: A Guide to MariaDB’s Governance Model\"<br />
Hear Ye, Hear Ye: A Guide to MariaDB’s Governance Model appeared first on MariaDB.org</p>
<p><a href="https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/">Hear Ye, Hear Ye: A Guide to MariaDB’s Governance Model</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Be it known: MariaDB Server now has a clearer, publicly documented governance framework covering technical roles, subsystem ownership, decision-making, response expectations and continuity.<br>
Open source begins with access to the code. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Hear Ye, Hear Ye: A Guide to MariaDB&rsquo;s Governance Model&rdquo;</span></a></p>
<p><a rel="nofollow" href="https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/">Hear Ye, Hear Ye: A Guide to MariaDB&rsquo;s Governance Model</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a></p>

<p><a href="https://mariadb.org/hear-ye-hear-ye-a-guide-to-mariadbs-governance-model/">Hear Ye, Hear Ye: A Guide to MariaDB’s Governance Model</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Understanding MySQL Replication &#8220;fatal error 1236&#8221;: &#8220;Replica has more GTIDs than the source has, using the source&#8217;s SERVER_UUID&#8221;</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/08/understanding-mysql-replication-fatal-error-1236.html" />
      <id>https://jfg-mysql.blogspot.com/2026/08/understanding-mysql-replication-fatal-error-1236.html</id>
      <updated>2026-08-03T22:12:29+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This MySQL replication error&#160;— fatal error 1236&#160;/ Replica has more GTIDs than the source has, using the source\'s SERVER_UUID&#160;— shows the importance of thinking before acting. I am glad a non-DBA Colleague asked me about this error, because if he would just have restarted replication, it would have caused a much bigger mess.</p>
<p>Often, we are tempted&#160;— or pushed&#160;— to just</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/08/understanding-mysql-replication-fatal-error-1236.html">Understanding MySQL Replication &#8220;fatal error 1236&#8221;: &#8220;Replica has more GTIDs than the source has, using the source&#8217;s SERVER_UUID&#8221;</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This MySQL replication error&amp;nbsp;&mdash; fatal error 1236&amp;nbsp;/ Replica has more GTIDs than the source has, using the source&rsquo;s SERVER_UUID&amp;nbsp;&mdash; shows the importance of thinking before acting. I am glad a non-DBA Colleague asked me about this error, because if he would just have restarted replication, it would have caused a much bigger mess.</p>
<p>Often, we are tempted&amp;nbsp;&mdash; or pushed&amp;nbsp;&mdash; to just</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/08/understanding-mysql-replication-fatal-error-1236.html">Understanding MySQL Replication &#8220;fatal error 1236&#8221;: &#8220;Replica has more GTIDs than the source has, using the source&#8217;s SERVER_UUID&#8221;</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Wirekite becomes Silver Sponsor of MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/" />
      <id>https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/</id>
      <updated>2026-08-02T20:09:53+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>We are pleased to welcome Wirekite as a new Silver Sponsor of MariaDB Foundation.<br />
Wirekite is an enterprise data movement platform focused on high-performance extract, load, migration, and replication workflows. …<br />
Continue reading \"Wirekite becomes Silver Sponsor of MariaDB Foundation\"<br />
The post Wirekite becomes Silver Sponsor of MariaDB Foundation appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/">Wirekite becomes Silver Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We are pleased to welcome <a href="https://wirekite.io/">Wirekite</a> as a new <a href="https://mariadb.org/donate/#silver-tier-from-eur-5000-per-year">Silver Sponsor</a> of MariaDB Foundation.<br>
Wirekite is an enterprise data movement platform focused on high-performance extract, load, migration, and replication workflows. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Wirekite becomes Silver Sponsor of MariaDB Foundation&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/">Wirekite becomes Silver Sponsor of MariaDB Foundation</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/wirekite-becomes-silver-sponsor-of-mariadb-foundation/">Wirekite becomes Silver Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Stored Procedures memory consumption in Percona Server for MySQL</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/stored-procedures-memory-consumption-in-percona-server-for-mysql/" />
      <id>https://www.percona.com/blog/stored-procedures-memory-consumption-in-percona-server-for-mysql/</id>
      <updated>2026-07-31T11:50:51+03:00</updated>
      <author><name>Bogdan Degtyariov</name></author>
      <summary type="html"><![CDATA[<p>1. What it is about This investigation began as a performance comparison for different memory allocators. However, during benchmarking, I discovered unexpected effects deserving a more detailed explanation. I hope you find these findings both interesting and useful. Imagine you need to set up a MySQL database server. Every detail is planned: the operating system, … Continued<br />
The post Stored Procedures memory consumption in Percona Server for MySQL appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/stored-procedures-memory-consumption-in-percona-server-for-mysql/">Stored Procedures memory consumption in Percona Server for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2><span>1. What it is about</span><a class="anchor-link" id="1-what-it-is-about"></a></h2>
<p><span>This investigation began as a performance comparison for different memory allocators. However, during benchmarking, I discovered unexpected effects deserving a more detailed explanation. I hope you find these findings both interesting and useful.</span></p>
<p><span>Imagine you need to set up a MySQL database server. Every detail is planned: the operating system, the CPU architecture, the number of cores, the amount of RAM, the storage capacity and speed. On paper the hardware looks like it can handle the workload. But in reality, things rarely go exactly as planned. So, conducting a thorough stress test is the next thing to do.<br>
</span></p>
<p>&nbsp;</p>
<h2><span>2. Realities of stress testing</span><a class="anchor-link" id="2-realities-of-stress-testing"></a></h2>
<p><span>You configure your MySQL server setting the </span><b>innodb_buffer_pool_size</b><span> to 70-80% of your available RAM. This creates a large fast buffer for your data and indexes, reducing the need for slower disk input/output.</span></p>
<p><span>After a warmup period and a few hours of testing, everything looks great. The server is working at a steady pace, performance is stable. You tick the box &ndash; the server has passed the basic stress test. Thinking everything is fine, you consider leaving the test running over the weekend, expecting only minor fluctuations in performance.</span></p>
<p><span>However, when you check the status the next morning, you find that the CPU is idle and the </span><span>mysqld</span><span> process has vanished. Did it crash? You check the server error logs, but there is no record of a crash or a shutdown&mdash;not even a core dump. Then, you look at the system logs and find something unexpected:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">journalctl -k -g mysqld

Jun 09 07:29:14 beast-node7.tp.int.percona.com kernel: Out of memory:
Killed process 3936620 (mysqld) total-vm:194627592kB, anon-rss:183047480kB, file-rss:640kB, shmem-rss:0kB,
UID:955676158 pgtables:355860kB oom_score_adj:0</pre>
<p><span>It appears that </span><span>mysqld</span><span> ran out of memory and was terminated by the OOM (Out of Memory) killer after running for about 16 hours.</span></p>
<p><span>We will focus on Resident Set Size (RSS), which is the subset of Virtual Memory Size (VSZ). RSS is the most significant part of VSZ and other parts like swap (only 8Gb) do not make notable contributions.</span></p>
<p><span>The RSS reached 183GiB, significantly higher than the initial 145GiB (with the </span><b>innodb_buffer_pool_size</b><span> set to 135G). The </span><span>mysqld</span><span> process had grabbed nearly 40GiB of extra memory, which at first looked like a memory leak. I ran my stress tests on different versions of MySQL and Percona Server and found a recurring pattern: memory usage climbed steadily until the system killed the process.</span></p>
<p><span>I won&rsquo;t dive into the leak diagnosis here, but the result was clear: </span><span>mysqld</span><span> wasn&rsquo;t leaking memory in the traditional sense. However, we still had to explain that 40GiB growth.<br>
</span></p>
<p>&nbsp;</p>
<h2><span>3. Configuration and methodology</span><a class="anchor-link" id="3-configuration-and-methodology"></a></h2>
<p><span>The configuration was as follows:</span></p>
<table border="1" cellpadding="5">
<tbody>
<tr>
<td><span>Benchmark</span></td>
<td><span>TPC-C via HammerDB 6.0</span></td>
</tr>
<tr>
<td><span>CPU</span></td>
<td><span>Intel Xeon Gold 6230 (2&times;20 cores, HT = 80 logical CPUs)</span></td>
</tr>
<tr>
<td><span>RAM</span></td>
<td><span>187 GiB DDR4</span></td>
</tr>
<tr>
<td><span>Storage</span></td>
<td><span>NVMe SSD (2.9 TB) INTEL SSDPE2KE032T8</span></td>
</tr>
<tr>
<td><span>OS</span></td>
<td><span>Ubuntu 24.04, kernel 6.8.0-60-generic</span></td>
</tr>
<tr>
<td><span>DB Engines</span></td>
<td><span>Percona Server 8.4.8-8 (release build)</span><span><br>
</span><span>Percona Server 8.4.9-9 (internal build, unreleased)</span><span>Percona Server 9.7.0 (internal build, unreleased)</span></td>
</tr>
</tbody>
</table>
<p><span>The testing was done as follows:</span></p>
<table border="1" cellpadding="5">
<tbody>
<tr>
<td><span>Workload</span></td>
<td><span>3000 warehouses (~300 GB data)</span></td>
</tr>
<tr>
<td><span>Timing</span></td>
<td><span>15 min ramp-up, 20 hours measurement window</span></td>
</tr>
<tr>
<td><span>Connections</span></td>
<td><span>80 Virtual Users (to match the number of logical CPU cores). Connection lifetime is set for the entire duration of the test.</span></td>
</tr>
<tr>
<td><span>InnoDB buffer sweep</span></td>
<td><span>Starting from 150G down to 80G with 5G decrease</span></td>
</tr>
</tbody>
</table>
<p><span>What we wanted to achieve:</span></p>
<ul>
<li aria-level="1"><span>Create conditions when memory allocations and deallocations inside the database server are frequent.</span></li>
<li aria-level="1"><span>Utilize as much of physical memory as possible (at least 80%) by giving it to InnoDB Buffer Pool.</span></li>
<li aria-level="1"><span>Use all available CPU resources in the most efficient way to prevent threads contesting for execution time (the number of connections should match the number of logical CPU cores).</span></li>
<li aria-level="1"><span>Eliminate any layers that add overhead and get in the way of direct measuring of allocators frequency and efficiency. The connections will be established using a socket file.</span></li>
</ul>
<p><span>Servers configuration file:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag"># Make sure data dir is on NVMe
datadir=/nvme/data

# Thread Pool is enabled only for Percona Server
plugin-load-add=thread_pool.so
thread_pool_size=16
thread_pool_max_threads=5000
thread_pool_stall_limit=500

# Disable binary logging
skip-log-bin

# Connection settings
max_connections = 200

# Logging
log-error = /home/bogdan.degtyariov/servers/data/mysql-error.log
pid-file = /home/bogdan.degtyariov/servers/data/mysql.pid

# Socket
socket = /tmp/mysql-alloc-test.sock

# Disable SSL requirement
require_secure_transport = OFF

# Other settings
sql_mode = ""
wait_timeout = 288000        # 80 hours
interactive_timeout = 288000 # 80 hours

# Table settings
default-storage-engine = InnoDB

# InnoDB redo log configuration
innodb_redo_log_capacity = 32G

# Minimize flush overhead (not crash-safe, but optimal for testing)
innodb_flush_log_at_trx_commit = 0

# Memory configuration
innodb_buffer_pool_size = 150G # Configurable down to 80G
innodb_buffer_pool_instances = 16
innodb_io_capacity = 20000

# Performance optimizations
innodb_flush_method = O_DIRECT
innodb_log_buffer_size = 256M
innodb_doublewrite = OFF

# Transparent Huge Pages can be turned ON or OFF for the testing
large-pages = ON</pre>
<p>&nbsp;</p>
<h2><span>4. Where did the memory go?</span><a class="anchor-link" id="4-where-did-the-memory-go"></a></h2>
<p><span>Memory management is complex, so let&rsquo;s simplify. Applications rarely talk directly to the Linux kernel because the kernel typically works in 4KB pages, which is inefficient for developers. Instead, applications use allocators like </span><span>glibc</span><span> malloc, </span><span>jemalloc</span><span>, or </span><span>tcmalloc</span><span>. These tools handle memory operations by minimizing overhead, managing bookkeeping, and preventing fragmentation. Most importantly, they use caching.</span></p>
<p><span>When a program frees memory, the allocator rarely returns it to the OS immediately. Instead, it moves that memory into an internal &ldquo;free-list&rdquo; cache. Reusing memory from this cache is much faster than requesting new memory from the kernel.</span></p>
<p><span>Also, the Percona Server for MySQL and upstream MySQL Server use their own implementation of the memory arena allocator called MEM_ROOT. Historically MEM_ROT was architected decades ago when the standard Linux implementation of </span><span>glibc</span><span> memory allocator was slow and prone to lock contention in multithreaded programs.</span></p>
<p><span>Enabling memory profiling revealed that MEM_ROOT allocations for cursor metadata in stored routines was responsible for most of the additional memory acquired by the server process:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">sp_head::execute_procedure           (TPC-C stored procedure)
   &#9492;&#9472; sp_instr_copen::execute        (OPEN <cursor> statement)
       &#9492;&#9472; sp_cursor::open
           &#9492;&#9472; mysql_open_cursor
               &#9500;&#9472; Materialized_cursor::send_result_set_metadata  87831 MB (94.3%)
               &#9492;&#9472; Query_result_materialize::start_execution      5311 MB  (5.7%)
                   &#9492;&#9472; MEM_ROOT::Alloc / AllocBlock / ForceNewBlock</cursor></pre>
<p><span><strong>NOTE:</strong> 87G is a significant growth of memory allocation considering that in that run the server initially allocated ~85G with Innodb_buffer_pool_size=80G.</span></p>
<p><span>The problem happens regardless of the data size because the actual issue is in stored routines cursor metadata. When the stored procedure is called the memory allocated for cursor metadata is not freed. Over the course of many repeated calls to the same stored procedure the cumulative amount of memory for the cursor can reach any value.</span></p>
<p><span>The following graph demonstrates the memory growth in Percona Server 8.4.8-8 from ~80G to over ~180G in RSS and over 200G VSZ over the period of 24 hours.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-50941 size-full" src="https://www.percona.com/wp-content/uploads/2026/07/rss-vsz.png" alt="" width="1043" height="654" srcset="https://www.percona.com/wp-content/uploads/2026/07/rss-vsz.png 1043w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-300x188.png 300w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-1024x642.png 1024w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-768x482.png 768w" sizes="auto, (max-width: 1043px) 100vw, 1043px"></p>
<p><span>Thus, a bug was reported for Percona Server: </span><a href="https://perconadev.atlassian.net/browse/PS-11472"><span>https://perconadev.atlassian.net/browse/PS-11472</span></a></p>
<p><span>With Percona Server for MySQL 9.7.0-1 the RSS/VSZ growth was at a slower rate, but still noticeable and it was not flattening towards a stable horizontal line (the server was configured with a small amount of memory for innodb_buffer_pool_size=4G and run for 5 hours instead of 20).</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-50945" src="https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-ps-9.7.0.jpg" alt="" width="1043" height="663" srcset="https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-ps-9.7.0.jpg 1043w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-ps-9.7.0-300x191.jpg 300w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-ps-9.7.0-1024x651.jpg 1024w, https://www.percona.com/wp-content/uploads/2026/07/rss-vsz-ps-9.7.0-768x488.jpg 768w" sizes="auto, (max-width: 1043px) 100vw, 1043px"></p>
<p><span>Memory profiling showed the new allocations in version 9.7.0-1 were in the same place where cursor metadata is handled:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">sp_head::execute_procedure           (TPC-C stored procedure)
   &#9492;&#9472; sp_instr_copen::execute        (OPEN <cursor> statement)
       &#9492;&#9472; sp_cursor::open
           &#9492;&#9472; mysql_open_cursor
               &#9500;&#9472; Materialized_cursor::send_result_set_metadata
               &#9492;&#9472; Query_result_materialize::start_execution      
                   &#9492;&#9472; MEM_ROOT::Alloc / AllocBlock / ForceNewBlock 4,025.8 MB (99.6%)</cursor></pre>
<p>&nbsp;</p>
<h2><span>5. Possible workarounds</span><a class="anchor-link" id="5-possible-workarounds"></a></h2>
<p><span>My tests showed that OOM crashes happened consistently under two specific conditions:,</span></p>
<ol>
<li aria-level="1"><span>Connections are never closed and stay open permanently</span></li>
<li aria-level="1"><span>Connections ran queries at maximum speed without any pauses</span></li>
</ol>
<p><span>Also, when the connection lifetime was limited and users were made to close connection and reconnect after 1M transactions, the memory exhaustion stopped, and memory was freed correctly &ndash; all with only a minor impact on performance. To minimize the delays associated with creating a new connection thread on the server I used the connection pool functionality in HammerDB. When the connection lifetime is ended, the actual connection is not closed, but &ldquo;reset&rdquo; and reused. This frees the context accumulated during the connection activity and stimulates returning memory to the OS. This connection pool mechanism is more efficient than the open/close cycle for maintaining the connection lifetime.&nbsp;</span></p>
<p><span>I had two runs with reconnecting users: with and without connection pool. The graph demonstrates that using the pool improves the performance in this test.</span></p>
<p><span>Also, during another experiment with unlimited connection lifetime, adding a 0.5ms pause after a few transactions prevented the crashes, though performance dropped slightly more.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-50947" src="https://www.percona.com/wp-content/uploads/2026/07/qps-delay-1.jpg" alt="" width="1043" height="631" srcset="https://www.percona.com/wp-content/uploads/2026/07/qps-delay-1.jpg 1043w, https://www.percona.com/wp-content/uploads/2026/07/qps-delay-1-300x181.jpg 300w, https://www.percona.com/wp-content/uploads/2026/07/qps-delay-1-1024x620.jpg 1024w, https://www.percona.com/wp-content/uploads/2026/07/qps-delay-1-768x465.jpg 768w" sizes="auto, (max-width: 1043px) 100vw, 1043px"></p>
<p><span>The memory graphs have consistent periodic oscillations that never reach into the dangerous zone.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-50949" src="https://www.percona.com/wp-content/uploads/2026/07/zigzag.jpg" alt="" width="1043" height="656" srcset="https://www.percona.com/wp-content/uploads/2026/07/zigzag.jpg 1043w, https://www.percona.com/wp-content/uploads/2026/07/zigzag-300x189.jpg 300w, https://www.percona.com/wp-content/uploads/2026/07/zigzag-1024x644.jpg 1024w, https://www.percona.com/wp-content/uploads/2026/07/zigzag-768x483.jpg 768w" sizes="auto, (max-width: 1043px) 100vw, 1043px"></p>
<p>&nbsp;</p>
<h2><span>6. Summary</span><a class="anchor-link" id="6-summary"></a></h2>
<p><span>To sum it up: the observed MySQL&rsquo;s memory bloating is caused by a problem in the server cursor implementation not freeing metadata memory.&nbsp;</span></p>
<p><span>Under heavy, constant load, that memory accumulates to the amount which eventually causes an OOM crash. Capping how long connections stay active or adding a short pause between transactions, gives the server time to clean itself up. Normally the client side processing adds such pauses without need to do it on purpose.</span></p>
<p><span>Finally, it is important to remember that the best benchmark results do not always guarantee the best real-life performance.</span></p>
<p>The post <a href="https://www.percona.com/blog/stored-procedures-memory-consumption-in-percona-server-for-mysql/">Stored Procedures memory consumption in Percona Server for MySQL</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/stored-procedures-memory-consumption-in-percona-server-for-mysql/">Stored Procedures memory consumption in Percona Server for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Server for MongoDB 8.3 Technical Preview Is Now Available</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-server-for-mongodb-8-3-technical-preview-is-now-available/" />
      <id>https://www.percona.com/blog/percona-server-for-mongodb-8-3-technical-preview-is-now-available/</id>
      <updated>2026-07-30T17:36:14+03:00</updated>
      <author><name>Radoslaw Szulgo</name></author>
      <summary type="html"><![CDATA[<p>Percona Server for MongoDB 8.3 is available today as a Technical Preview. It is not for production. It is for your lab, your staging cluster, and your benchmark harness – and for sharing with us what works and what does not. Especially if this version is your segue to leverage upcoming full-text and vector search … Continued<br />
The post Percona Server for MongoDB 8.3 Technical Preview Is Now Available appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-server-for-mongodb-8-3-technical-preview-is-now-available/">Percona Server for MongoDB 8.3 Technical Preview Is Now Available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><b>Percona Server for MongoDB 8.3 is available today as a Technical Preview. It is not for production. It is for your lab, your staging cluster, and your benchmark harness &ndash; and for sharing with us what works and what does not. Especially if this version is your segue to leverage upcoming full-text and vector search capabilities. Many users have been asking when Percona would ship 8.2 or 8.3 &ndash; this is the answer. We are jumping straight to 8.3. In this blog post, I share everything you have to know before upgrading to 8.3. I gathered all available information, so you don&rsquo;t need to.&nbsp;&nbsp;</b></p>
<h2><span>Why this release matters</span><a class="anchor-link" id="why-this-release-matters"></a></h2>
<p><span>MongoDB 8.3 Community went GA in May 2026. This was the fourth significant MongoDB release in nearly 2 years. I have observed that the upstream MongoDB Community project is now moving faster than most organizations&rsquo; upgrades (adoption telemetry data later in this blog post). Nonetheless, Percona&rsquo;s job is to ensure that the free, enterprise-grade path does not fall behind, and you still can get the performance of the current release without giving up data-at-rest encryption with KMIP, HashiCorp Vault, or OpenBao, audit logging, external LDAP authentication, OpenID Connect, File copy-based initial sync, in-memory engine, or audit log with log redaction. All of that stays in Percona Server for MongoDB 8.3, and it stays free and open. This is the Percona way.</span></p>
<p><span>I&rsquo;ll not discover America by writing that the data layer now has to move at AI speed. Application teams are shipping agentic workloads today, and that&rsquo;s why performance and functional requirements are growing exponentially. Two years ago, we experienced occasional query retries and recall storms, critical security patches, and multi-region deployments could trade compliance for latency. Now, this is bread-and-butter we need to deal with every day.&nbsp;</span></p>
<p><span>I&rsquo;m happy to share that Percona has published Percona Server for MongoDB 8.3 in Technical Preview today. 8.3 is not one release forward from 8.0 &ndash; it is three. Everything that landed in the two minor releases (and 8.1 Rapid Release) in between arrives at once. Percona Server for MongoDB 8.3 is the most performant release so far! And besides the performance boost and many functional improvements, I&rsquo;ve described below, I&rsquo;m personally most excited about the fact that this release enables full-text search and vector search capabilities, so you can finally equip your applications with AI power using the same MongoDB. More about that in my next blog post &ndash; brace yourself! </span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50878 size-large" src="https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-1024x576.png" alt="" width="1024" height="576" srcset="https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-1024x576.png 1024w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-300x169.png 300w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-768x432.png 768w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-1536x864.png 1536w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-83-highlights-2048x1153.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></p>
<h2><span>What&rsquo;s new compared to 8.0</span><a class="anchor-link" id="whats-new-compared-to-8-0"></a></h2>
<p><span>Because we&rsquo;re skipping 8.1 and 8.2 releases, the delta is large. I grouped the highlights by what you&rsquo;d actually notice.</span></p>
<h3><b>Performance</b><a class="anchor-link" id="performance"></a></h3>
<p><span>Percona Server for MongoDB inherits performance optimization from the upstream MongoDB Community:</span></p>
<ul>
<li aria-level="1"><span>Up to </span><b>195%</b><span> higher throughput on time-series bulk insertions</span></li>
<li aria-level="1"><span>Up to </span><b>40%</b><span> on match filter queries</span></li>
<li aria-level="1"><span>Up to </span><b>20%</b><span> on queries against documents with arrays</span></li>
<li aria-level="1"><span>Up to </span><b>10%</b><span> on in-cache read workloads</span></li>
<li aria-level="1"><span>Up to </span><b>5%</b><span> reduction in CPU utilization</span></li>
</ul>
<p><span>Moreover, Percona Server for MongoDB 8.3 now offers faster initial sync, faster time-series bulk inserts, and reduced multi-planning costs for queries. As always, your mileage depends entirely on the shape of your workload.</span></p>
<h3><b>Query planning</b><a class="anchor-link" id="query-planning"></a></h3>
<p><span>The </span><b>Cost-Based Ranker (CBR)</b><span> is now the default plan selection mechanism for eligible queries. Multi-planning gets a short trial period; if it can&rsquo;t settle on a plan, CBR evaluates each plan node by estimated cost. New </span><span>serverStatus</span><span> counters under </span><span>metrics.query.cbr</span><span> and new </span><span>explain</span><span> output let you see when CBR was used and how often it won.&nbsp;</span></p>
<h3><b>Query language and aggregation</b><a class="anchor-link" id="query-language-and-aggregation"></a></h3>
<ul>
<li aria-level="1"><span>Native hybrid search (full-text search and vector search) is enabled with Percona Search for MongoDB (fork of </span><a href="https://github.com/mongodb/mongot"><span>mongodb/mongot</span></a><span> project) via</span><span> $scoreFusion</span><span> and </span><span>$rankFusion</span></li>
<li aria-level="1"><span>Array element indexes are now accessible in </span><span>$map</span><span>, </span><span>$filter</span><span>, and </span><span>$reduce</span><span> via the new </span><span>arrayIndexAs</span><span> field and the </span><span>$$IDX</span><span> system variable</span></li>
<li aria-level="1"><span>New expressions: </span><span>$subtype</span><span>, </span><span>$createObjectId</span><span>, </span><span>$hash</span><span>, </span><span>$hexHash</span><span>, </span><span>$serializeEJSON</span><span>, </span><span>$deserializeEJSON</span><span>, </span><span>$currentDate</span></li>
<li aria-level="1"><span>$convert</span><span> gains a </span><span>base</span><span> argument (base 2/8/10/16) and can convert strings representing arrays and objects, plus BinData <img decoding="async" src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="&harr;" class="wp-smiley"> numeric arrays in both directions</span></li>
<li aria-level="1"><span>$mergeObjects</span><span> now works inside </span><span>$setWindowFields</span></li>
<li aria-level="1"><span>$concatArrays</span><span> and </span><span>$setUnion</span><span> accumulators, the </span><span>$listClusterCatalog</span><span> stage, and </span><span>$lookup</span><span> across multiple encrypted collections for CSFLE and Queryable Encryption</span></li>
</ul>
<h3><b>Storage and operations</b><a class="anchor-link" id="storage-and-operations"></a></h3>
<p><span>The most operationally useful change for anyone running in containers is the </span><b>WiredTiger</b> <b>cache size, which can now be set as a percentage</b><span> of available memory via </span><span>&ndash;wiredTigerCacheSizePct</span><span> / </span><span>storage.wiredTiger.engineConfig.cacheSizePct</span><span>, instead of a fixed GB value. If you run Percona Server for MongoDB on Kubernetes via our Percona Operator for MongoDB. This alone is worth the upgrade!</span></p>
<p><span>Also worth knowing:</span></p>
<ul>
<li aria-level="1"><b>zstd negative compression levels</b><span> &ndash; the supported range widens to -7 through 22, trading ratio for speed</span></li>
<li aria-level="1"><b>Initial-sync index builds</b><span> now use 10% of available RAM by default, tunable, and bounded</span></li>
<li aria-level="1"><b>terminateSecondaryReadsOnOrphanCleanup</b><span> (on by default) terminates long-running secondary reads that started before a chunk migration was committed. Previously, those reads continued and could silently return incomplete results with no error. </span><span>orphanCleanupDelaySecs</span><span> moves from 900 to 3600 to accommodate this.</span></li>
</ul>
<h3><b>Sharding</b><a class="anchor-link" id="sharding"></a></h3>
<ul>
<li aria-level="1"><b>removeShard</b><b> is deprecated</b><span>, replaced by four commands that give you granular control over draining and removal: </span><span>startShardDraining</span><span>, </span><span>stopShardDraining</span><span>, </span><span>shardDrainingStatus</span><span>, </span><span>commitShardRemoval</span><span>. A parallel set exists for the embedded-to-dedicated config server transition.</span></li>
<li aria-level="1"><b>Sharded clusters: DDL operations and </b><b>applyOps</b><b> can now run only on </b><b>mongos</b><b> across</b><span> all sharded clusters.</span></li>
<li aria-level="1"><span>New </span><span>mongod &ndash;replicaSetConfigShardMaintenanceMode</span><span> converts a replica set primary directly into an embedded config shard, skipping the dedicated config server replica set step.</span></li>
<li aria-level="1"><span>Shard-level query stats now include queries that originated on </span><span>mongos</span><span>. Previously, most forwarded queries were invisible in shard-level stats.</span></li>
</ul>
<h3><b>Security</b><a class="anchor-link" id="security"></a></h3>
<ul>
<li aria-level="1"><span>Software Bills of Materials (SBOMs) are attached to our deliverables across all release distribution channels. SBOMs improve software supply chain transparency by documenting the components and dependencies included in a build. They are generated automatically as part of the release pipeline in the industry-standard </span><a href="https://cyclonedx.org/specification/overview/"><span>CycloneDX</span></a><span> format.</span></li>
<li aria-level="1"><span>New pre-auth connection resource limits (</span><span>capMemoryConsumptionForPreAuthBuffers</span><span>, </span><span>preAuthMaximumMessageSizeBytes</span><span>, </span><span>messageSizeErrorRateSec</span><span>)</span></li>
<li aria-level="1"><span>Ingress connection establishment rate limiting and per-application exemptions from ingress request rate limiting. See </span><a href="https://www.mongodb.com/docs/manual/reference/parameters/#mongodb-parameter-param.ingressRequestRateLimiterApplicationExemptions"><span>ingressRequestRateLimiterApplicationExemptions</span><span>.</span></a><span> for more.</span></li>
</ul>
<h3><b>Observability</b><a class="anchor-link" id="observability"></a></h3>
<ul>
<li aria-level="1"><b>Query memory tracking</b><span>: </span><span>inUseTrackedMemBytes</span><span> and </span><span>peakTrackedMemBytes</span><span> in </span><span>$currentOp</span><span>, profiler output, slow query logs, explain results, and </span><span>$planCacheStats</span></li>
<li aria-level="1"><b>Slow in-progress query logs</b><span>: a lightweight entry emitted once per query when it exceeds </span><span>slowOpInProgressThreshold</span><span>, so you can see a slow query while it is still running rather than after it finishes</span></li>
<li aria-level="1"><span>FTDC now collects </span><span>connPoolStats</span><span> for </span><span>mongod</span></li>
<li aria-level="1"><span>Standardized disk-spill metrics (</span><span>spills</span><span>, </span><span>spilledBytes</span><span>, </span><span>spilledRecords</span><span>, </span><span>spilledDataStorageSize</span><span>) in explain output</span></li>
<li aria-level="1"><span>New TTL, replication lag, and admission control metrics in </span><span>serverStatus</span></li>
</ul>
<h2>What&rsquo;s changed: Read this before you upgrade<a class="anchor-link" id="whats-changed-read-this-before-you-upgrade"></a></h2>
<p><b>Upgrade path.</b><span> To go from 8.0 directly to 8.3, your 8.0 deployment must have </span><span>featureCompatibilityVersion</span><span> set to </span><span>8.0</span><span>. Verify with </span><span>db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })</span><span>. All cluster members must be running before you start.</span></p>
<p><b>Geospatial indexes may need rebuilding.</b><span> Index generation now prioritizes GeoJSON over legacy numeric coordinates when a document contains both. If your documents have legacy numeric coordinates preceding GeoJSON coordinates and your existing indexes depend on the old ordering, rebuild them and re-verify your geospatial query results. Separately, </span><span>2dsphereIndexVersion</span><span> now defaults to 4.</span></p>
<p><b>Error codes changed.</b><span> Exceeding the </span><span>$facet</span><span> 100 MB limit now returns </span><span>ExceededMemoryLimit</span><span> (146) instead of </span><span>4031700</span><span>. Upserts producing an oversized BSON object return </span><span>10334</span> <span>BSONObjectTooLarge</span><span> instead of </span><span>17419</span><span>/</span><span>17420</span><span>. Anything in your stack that string-matches error codes needs updating.</span></p>
<p><b>$text</b><b> sorted-by-score queries can now fail.</b><span> The </span><span>TextOr</span><span> stage is capped at 100 MB. With </span><span>allowDiskUse: true,</span><span> it spills; with </span><span>false</span><span> the query errors out. Previously, it was unbounded, which is to say, previously, it could OOM your node instead.</span></p>
<p><b>Pre-epoch date arithmetic shifts by one second.</b> <span>$dateAdd</span><span> and </span><span>$dateSubtract</span><span>, with a non-millisecond unit, on dates before 1970-01-01 now return a result that is one second greater. This propagates into </span><span>$setWindowFields</span><span> and </span><span>$densify</span><span>.</span></p>
<p><b>Monitoring integrations will need attention.</b><span> The </span><span>service</span><span> field is removed from </span><span>serverStatus</span><span> output, and </span><span>cpuNanos</span><span> is moved from </span><span>operationMetrics</span><span> into </span><span>$queryStats</span><span> (Linux only).</span></p>
<p><b>Other behavior changes:</b> <span>$$CLUSTER_TIME</span><span> now throws an error in standalone deployments. </span><span>db.collection.validate({full: true})</span><span> no longer implicitly enables </span><span>checkBSONConformance</span><span>. </span><span>explain()</span><span> against a non-existent database on a sharded cluster no longer creates the database. Time series collections reject a </span><span>timeField</span><span> starting with </span><span>$</span><span> and reject an index named or hinted </span><span>&ldquo;_id_&rdquo;</span><span>.</span></p>
<p><b>One-way doors from sharding to the replica set.</b><span> A replica set that was previously a sharded cluster cannot be converted back into a sharded cluster &ndash; residual sharding metadata blocks it. And downgrading from 8.3 requires you to first drop 2dsphere version 4 indexes and update or drop any views, validators, or collection validation rules that use 8.3-only expressions.</span></p>
<h2><span>Don&rsquo;t stay behind: What the telemetry says about version adoption</span><a class="anchor-link" id="dont-stay-behind-what-the-telemetry-says-about-version-adoption"></a></h2>
<p><span>We analyzed anonymous product telemetry from Percona Server for MongoDB over the last 12 months to assess the share of active database instances. </span><b>Major version adoption takes more than a year for many users and organizations.</b><span> PSMDB 8.0 went from roughly a fifth of the installed base to nearly half over twelve months. That is healthy, and it is also slower than a release cadence of four significant upstream versions in 19 months. The gap between how quickly MongoDB ships and how quickly the installed base moves is why we are publishing a Technical Preview instead of delaying until a GA build. Also, that&rsquo;s a strong signal to move to version 8.0 if you haven&rsquo;t already, and then explore 8.3.&nbsp;</span></p>
<p><span>Moreover, if you run (or consider running) your Percona Server for MongoDB </span><b>in a container</b> <b>deployment &ndash;</b><span> Containerized deployments now account for about 24% of active instances, up from roughly 21% a year ago &ndash; so still Docker and Kubernetes for MongoDB are the thing, and this release improvement of percentage-based WiredTiger cache sizing is probably the most immediately useful thing.</span></p>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50879 size-large" src="https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956-1024x488.png" alt="" width="1024" height="488" srcset="https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956-1024x488.png 1024w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956-300x143.png 300w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956-768x366.png 768w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956-1536x732.png 1536w, https://www.percona.com/wp-content/uploads/2026/07/psmdb-version-adoption-360d-e1785430024956.png 1696w" sizes="auto, (max-width: 1024px) 100vw, 1024px"></p>
<p>&nbsp;</p>
<table width="1099">
<thead>
<tr>
<th><b>Version</b></th>
<th><b>Aug 2025</b></th>
<th><b>Jul 2026</b></th>
<th><b>Change</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>8.0</b></td>
<td><span>22.6%</span></td>
<td><span>47.6%</span></td>
<td><span>+25.0 pts</span></td>
</tr>
<tr>
<td><b>7.0</b></td>
<td><span>28.7%</span></td>
<td><span>33.1%</span></td>
<td><span>+4.4 pts</span></td>
</tr>
<tr>
<td><b>6.0</b></td>
<td><span>26.9%</span></td>
<td><span>16.0%</span></td>
<td><span>&minus;10.9 pts</span></td>
</tr>
<tr>
<td><b>5.0</b></td>
<td><span>22.9%</span></td>
<td><span>3.6%</span></td>
<td><span>&minus;19.3 pts</span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<h2><span>Technical Preview: What does that actually mean</span><a class="anchor-link" id="technical-preview-what-does-that-actually-mean"></a></h2>
<p><span>A Technical Preview build is complete enough to install, benchmark, and evaluate. It meets equal quality and packaging requirements as any other 8.0 patch. However, it has not undergone the full release qualification we require for a GA release with respect to ecosystem compatibility:</span></p>
<ul>
<li aria-level="1"><b>Percona Backup for MongoDB (PBM)</b><span>: logical backup and restore against an 8.3 server may cause some issues. Do not rely on it to protect anything you care about. Current known limitations are around logical backup and restore, and PITR. If you&rsquo;re interested in details and progress on those, follow our </span><a href="https://perconadev.atlassian.net/browse/PSMDB-2176"><span>Jira tickets</span></a><span>.</span></li>
<li aria-level="1"><b>Percona ClusterSync for MongoDB (PCSM)</b><span>: replication to or from an 8.3 cluster is unvalidated.</span></li>
<li aria-level="1"><b>Percona Monitoring and Management (PMM):</b><span> metric collection may be incomplete or fail outright, particularly given the </span><span>serverStatus</span><span> and </span><span>cpuNanos</span><span> changes described above.</span></li>
</ul>
<p><span>To be clear about what this is and isn&rsquo;t: upstream MongoDB Community 8.3 is a stable, production-suitable release with support through October 2029. The Technical Preview label applies to </span><b>Percona&rsquo;s build</b><span>, not to MongoDB 8.3 itself. It reflects where we are in qualifying our surrounding tooling against this version, and not a judgment about upstream stability.</span></p>
<p><span>So if you need a fully-integrated Percona Server for MongoDB today, </span><b>8.0 is still the answer</b><span>. The current release is 8.0.26-11. When our 8.3 build reaches GA, that changes.</span></p>
<h2><span>How to start</span><a class="anchor-link" id="how-to-start"></a></h2>
<p><span>We recommend installing (or upgrading) Percona Server for MongoDB using the official Percona repositories via the </span><a href="https://docs.percona.com/percona-software-repositories/index.html"><span>percona-release repository management tool</span></a><span> and your system&rsquo;s package manager. For further instructions, start with the <a href="https://docs.percona.com/percona-server-for-mongodb/8.3/install/index.html">quickstart guide</a> for the fresh installation or an </span><a href="https://docs.percona.com/percona-server-for-mongodb/8.3/install/upgrade-from-80.html"><span>upgrade procedure</span></a> from 8.0.</p>
<h2><span>Feedback needed</span><a class="anchor-link" id="feedback-needed"></a></h2>
<p><span>This is the part that matters. A Technical Preview is only worth shipping if it yields findings.</span></p>
<p><span>We are specifically interested in:</span></p>
<ul>
<li aria-level="1"><b>Percona-specific feature behavior</b><span>: data-at-rest encryption with KMIP or Vault, audit logging, external LDAP authentication, OpenID Connect, file copy-based initial sync, hot backup, and audit logging with log redaction.</span></li>
<li aria-level="1"><b>PBM, PCSM, and PMM interactions.</b><span> We know these might be limited. Knowing </span><i><span>how</span></i><span> they fail helps us prioritize and fix them within the next release cycle.</span></li>
<li aria-level="1"><b>Upgrade friction</b><span> from 8.0, especially on sharded clusters and around the geospatial index and </span><span>removeShard</span><span> changes.</span></li>
</ul>
<p><span>Post your findings in the </span><a href="https://forums.percona.com/c/mongodb/percona-server-for-mongodb/17"><b>Percona Server for MongoDB forum</b></a><span>. Include your topology, your workload shape, and the exact version you tested. Engineering reads that category directly, and feedback from this preview will shape what the GA build looks like.</span></p>
<p><span>If you find a security issue, please report it through </span><a href="https://www.percona.com/security/"><span>Percona Security</span></a><span> disclosure process rather than posting publicly.</span></p>
<hr>
<p><i><span>Percona Server for MongoDB is a free, source-available, drop-in replacement for MongoDB Community Edition with enterprise-grade features. Telemetry figures in this post are derived from anonymous product telemetry.</span></i></p>
<p>The post <a href="https://www.percona.com/blog/percona-server-for-mongodb-8-3-technical-preview-is-now-available/">Percona Server for MongoDB 8.3 Technical Preview Is Now Available</a> appeared first on <a href="https://www.percona.com/">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-server-for-mongodb-8-3-technical-preview-is-now-available/">Percona Server for MongoDB 8.3 Technical Preview Is Now Available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL Best Practice : not using date / time types, nor ENUM</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/07/best-practice-no-timestamp-nor-enum.html" />
      <id>https://jfg-mysql.blogspot.com/2026/07/best-practice-no-timestamp-nor-enum.html</id>
      <updated>2026-07-30T15:46:44+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>Today, I was reminded of a MySQL Best Practice, probably generalizable to all databases : using simple types, not complex types.&#160; Such complex types to avoid include the date and time data types (including TIMESTAMP) and ENUM.&#160; Let\'s see why.</p>
<p>A little history about this, Baron Schwartz, a MySQL Legend who is not involved in the community anymore, compared using the TIMESTAMP type to</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/07/best-practice-no-timestamp-nor-enum.html">MySQL Best Practice : not using date / time types, nor ENUM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Today, I was reminded of a MySQL Best Practice, probably generalizable to all databases : using simple types, not complex types.&amp;nbsp; Such complex types to avoid include the date and time data types (including TIMESTAMP) and ENUM.&amp;nbsp; Let&rsquo;s see why.</p>
<p>A little history about this, Baron Schwartz, a MySQL Legend who is not involved in the community anymore, compared using the TIMESTAMP type to</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/07/best-practice-no-timestamp-nor-enum.html">MySQL Best Practice : not using date / time types, nor ENUM</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Java Connector 3.5.10, 3.4.4, 3.3.6, and 2.7.15 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-10-3-4-4-3-3-6-and-2-7-15-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-java-connector-3-5-10-3-4-4-3-3-6-and-2-7-15-now-available/</id>
      <updated>2026-07-29T21:34:39+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/J 3.5.10, 3.4.4, 3.3.6, and 2.7.15 releases. Release Notes […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-10-3-4-4-3-3-6-and-2-7-15-now-available/">MariaDB Java Connector 3.5.10, 3.4.4, 3.3.6, and 2.7.15 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/J 3.5.10, 3.4.4, 3.3.6, and 2.7.15 releases. Download Now Notable items in this release include: Notable items in this release include: Notable items in this release include: Notable items in this release include: See&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-10-3-4-4-3-3-6-and-2-7-15-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-10-3-4-4-3-3-6-and-2-7-15-now-available/">MariaDB Java Connector 3.5.10, 3.4.4, 3.3.6, and 2.7.15 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>From a Production Problem to MariaDB: Headout’s Open-Source Contribution Journey</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/from-a-production-problem-to-mariadb-headouts-open-source-contribution-journey/" />
      <id>https://mariadb.org/from-a-production-problem-to-mariadb-headouts-open-source-contribution-journey/</id>
      <updated>2026-07-29T07:01:00+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>A production bottleneck at Headout led to a new MariaDB Server improvement. This is the story of how engineers, maintainers and AI-assisted development turned a real-world problem into an upstream open-source contribution.<br />
The post From a Production Problem to MariaDB: Headout’s Open-Source Contribution Journey appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/from-a-production-problem-to-mariadb-headouts-open-source-contribution-journey/">From a Production Problem to MariaDB: Headout’s Open-Source Contribution Journey</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A production bottleneck at Headout led to a new MariaDB Server improvement. This is the story of how engineers, maintainers and AI-assisted development turned a real-world problem into an upstream open-source contribution.</p>
<p>The post <a rel="nofollow" href="https://mariadb.org/from-a-production-problem-to-mariadb-headouts-open-source-contribution-journey/">From a Production Problem to MariaDB: Headout&rsquo;s Open-Source Contribution Journey</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/from-a-production-problem-to-mariadb-headouts-open-source-contribution-journey/">From a Production Problem to MariaDB: Headout’s Open-Source Contribution Journey</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 12.3: Faster Vector Search with Matryoshka Optimization</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-12-3-faster-vector-search-with-matryoshka-optimization/" />
      <id>https://mariadb.com/resources/blog/mariadb-12-3-faster-vector-search-with-matryoshka-optimization/</id>
      <updated>2026-07-28T18:29:55+03:00</updated>
      <author><name>Egor Ustinov</name></author>
      <summary type="html"><![CDATA[<p>Achieving Fast Vector Search in MariaDB MariaDB Server 12.3 makes vector search faster where it matters most: the high-recall levels […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-faster-vector-search-with-matryoshka-optimization/">MariaDB 12.3: Faster Vector Search with Matryoshka Optimization</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Server 12.3 makes vector search faster where it matters most: the high-recall levels that production AI workloads actually require. A new Matryoshka-aware optimization uses a cheap check on the first slice of each embedding to discard far-away candidates, then confirms the close ones with the full vector &mdash; so search stays just as accurate while doing far less work.</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-faster-vector-search-with-matryoshka-optimization/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-12-3-faster-vector-search-with-matryoshka-optimization/">MariaDB 12.3: Faster Vector Search with Matryoshka Optimization</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Backups Using the MySQL Clone Operation</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/backups-with-mysql-clone/" />
      <id>https://www.fromdual.com/blog/backups-with-mysql-clone/</id>
      <updated>2026-07-28T09:40:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We recently tested the PostgreSQL backup tool pg_basebackup and were very impressed with its remote backup functionality, which allows for both physical local and physical remote backups.<br />
This led us to wonder whether a physical remote backup is also possible using the “new” MySQL Server Clone feature, which was introduced in MySQL 8.0.17 (July 2019).<br />
The MySQL Clone operation can be used to create both a local and a remote copy of the database. The original idea behind this feature was likely to automatically create nodes in an InnoDB Cluster (similar to Percona XtraDB Cluster SST).<br />
Terminology used in the clone operation:</p>
<p>Donor (source database)<br />
Recipient (destination database)</p>
<p>The clone operation is initiated from the recipient. The data can be cloned to the recipient’s own directory or, alternatively, to a different directory.<br />
Preparations<br />
The plugin must be installed on both the Donor and the Recipient.<br />
SQL > INSTALL PLUGIN clone SONAME \'mysql_clone.so\';<br />
Query OK, 0 rows affected (0.01 sec)</p>
<p>SQL > SELECT PLUGIN_NAME, PLUGIN_STATUS<br />
 FROM INFORMATION_SCHEMA.PLUGINS<br />
 WHERE PLUGIN_NAME = \'clone\'<br />
;<br />
+-------------+---------------+<br />
&#124; PLUGIN_NAME &#124; PLUGIN_STATUS &#124;<br />
+-------------+---------------+<br />
&#124; clone &#124; ACTIVE &#124;<br />
+-------------+---------------+<br />
If you want to force the plugin to load on restart, it must be configured as follows in the MySQL configuration file (my.cnf):<br />
[mysqld]<br />
plugin_load_add = mysql_clone.so<br />
clone = FORCE_PLUS_PERMANENT<br />
Local Backup Using Clone<br />
This method can serve as a replacement for a physical backup solution (xtrabackup or MySQL Enterprise Backup (mysql_backup)). On the database acting as the recipient in this case, execute the following command:<br />
SQL > CLONE LOCAL DATA DIRECTORY = \'/mnt/backup/mysql_clone\';<br />
The following items are still missing for the clone operation:</p>
<p>All TLS keys (*.pem files).<br />
The auto.cnf file, which contains the server_uuid.<br />
The mysqld-auto.cnf file, which contains dynamically modified, persistent server configuration variables.<br />
The mysql_upgrade_history.<br />
The MySQL configuration file (my.cnf) as well as<br />
The binary logs.</p>
<p>$ cp ${datadir}/*auto.cnf ${datadir}/mysql_upgrade_history ${datadir}/*.pem /mnt/backup/mysql_clone/<br />
Restoring the database is quite simple:<br />
$ systemctl stop mysql<br />
$ rm -rf ${datadir}/*<br />
$ cp -a /mnt/backup/mysql_clone/* ${datadir}/<br />
$ chown -R mysql: ${datadir}/*<br />
$ systemctl start mysql<br />
The #clone folder is created by the clone operation and can be ignored, but must not be deleted.<br />
$ ls -lad /mnt/backup/mysql_clone/*<br />
...<br />
drwxr-x--- 2 dba dba 4096 Jul 27 14:52 \'#clone\'<br />
...<br />
It is automatically removed when the MySQL database is started. If you delete it anyway, you will receive the following error messages:<br />
[System] [MY-013576] [InnoDB] InnoDB initialization has started.<br />
[System] [MY-013577] [InnoDB] InnoDB initialization has ended.<br />
mysqld: Can\'t create/write to file \'./performance_schema/clone_status_385.sdi\' (OS errno 2 - No such file or directory)<br />
mysqld: Can\'t create file \'./performance_schema/clone_status_385.sdi\' (errno: 2 - No such file or directory)<br />
[ERROR] [MY-013272] [Clone] Plugin Clone reported: \'Client: PFS table creation failed.\'<br />
[ERROR] [MY-010202] [Server] Plugin \'clone\' init function returned error.<br />
The binary log position required for point-in-time recovery can be determined as follows:<br />
SQL > SELECT BINLOG_FILE, BINLOG_POSITION FROM performance_schema.clone_status;<br />
+-------------------------------+-----------------+<br />
&#124; BINLOG_FILE &#124; BINLOG_POSITION &#124;<br />
+-------------------------------+-----------------+<br />
&#124; boss_percona-84_binlog.000003 &#124; 1231898 &#124;<br />
+-------------------------------+-----------------+<br />
Remote Backup Using Clone<br />
To create a remote backup using the clone functionality, a minimally functional MySQL database is required on the remote system. Unfortunately, a simple process or tool is not sufficient for this.<br />
On the donor server, you need a user with the following privileges:<br />
SQL > CREATE USER \'backup_user\'@\'%\' IDENTIFIED BY \'secret\';<br />
SQL > GRANT BACKUP_ADMIN ON *.* TO \'backup_user\'@\'%\';<br />
In addition, the potential donor must be specified on the recipient server:<br />
SQL > SET GLOBAL clone_valid_donor_list = \'192.168.1.129:3306\';<br />
The remote backup is then performed as follows:<br />
SQL > CLONE INSTANCE FROM \'backup_user\'@\'192.168.1.129\':3306 IDENTIFIED BY \'secret\'<br />
DATA DIRECTORY = \'/mnt/backup/mysql_clone\';<br />
The missing files described above must now also be copied somehow:<br />
$ scp mysql@192.168.1.129:${datadir}/*auto.cnf /mnt/backup/mysql_clone/<br />
$ scp mysql@192.168.1.129:${datadir}/mysql_upgrade_history /mnt/backup/mysql_clone/<br />
$ scp mysql@192.168.1.129:${datadir}/*.pem /mnt/backup/mysql_clone/<br />
Restoring the database is done in the same way as described above.<br />
Conclusion<br />
The MySQL Clone operation is a cool feature that I neglected for a long time because it never occurred to me that it could also be used for backup purposes.<br />
I wouldn’t be surprised if the MySQL developers took a cue from PostgreSQL’s pg_basebackup when they implemented this feature.<br />
Unfortunately, to my knowledge, this feature is still completely missing in MariaDB. Too bad!<br />
Sources</p>
<p>General: The Clone Plugin<br />
There are a few minor limitations for the clone backup, which are described here: Clone Plugin Limitations.<br />
Monitoring the clone backup is described here: Monitoring Cloning Operations.<br />
Tuning the clone backup is described here: Clone System Variable Reference</p>
<p><a href="https://www.fromdual.com/blog/backups-with-mysql-clone/">Backups Using the MySQL Clone Operation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We recently tested the PostgreSQL backup tool <code>pg_basebackup</code> and were very impressed with its remote backup functionality, which allows for both physical local and physical remote backups.</p>
<p>This led us to wonder whether a physical remote backup is also possible using the &ldquo;new&rdquo; MySQL Server Clone feature, which was introduced in MySQL 8.0.17 (July 2019).</p>
<p>The MySQL Clone operation can be used to create both a local and a remote copy of the database. The original idea behind this feature was likely to automatically create nodes in an InnoDB Cluster (similar to Percona XtraDB Cluster SST).</p>
<p>Terminology used in the clone operation:</p>
<ul>
<li>Donor (source database)</li>
<li>Recipient (destination database)</li>
</ul>
<p>The clone operation is initiated from the recipient. The data can be cloned to the recipient&rsquo;s own directory or, alternatively, to a different directory.</p>
<h2 id="preparations">Preparations<a class="anchor-link" id="preparations"></a></h2>
<p>The plugin must be installed on both the Donor and the Recipient.</p>
<pre><code>SQL&gt; INSTALL PLUGIN clone SONAME 'mysql_clone.so';
Query OK, 0 rows affected (0.01 sec)

SQL&gt; SELECT PLUGIN_NAME, PLUGIN_STATUS
 FROM INFORMATION_SCHEMA.PLUGINS
 WHERE PLUGIN_NAME = 'clone'
;
+-------------+---------------+
| PLUGIN_NAME | PLUGIN_STATUS |
+-------------+---------------+
| clone | ACTIVE |
+-------------+---------------+
</code></pre>
<p>If you want to force the plugin to load on restart, it must be configured as follows in the MySQL configuration file (<code>my.cnf</code>):</p>
<pre><code>[mysqld]
plugin_load_add = mysql_clone.so
clone = FORCE_PLUS_PERMANENT
</code></pre>
<h2 id="local-backup-using-clone">Local Backup Using Clone<a class="anchor-link" id="local-backup-using-clone"></a></h2>
<p>This method can serve as a replacement for a physical backup solution (<code>xtrabackup</code> or MySQL Enterprise Backup (<code>mysql_backup</code>)). On the database acting as the recipient in this case, execute the following command:</p>
<pre><code>SQL&gt; CLONE LOCAL DATA DIRECTORY = '/mnt/backup/mysql_clone';
</code></pre>
<p>The following items are still missing for the clone operation:</p>
<ul>
<li>All TLS keys (<code>*.pem</code> files).</li>
<li>The <code>auto.cnf</code> file, which contains the <code>server_uuid</code>.</li>
<li>The <code>mysqld-auto.cnf</code> file, which contains dynamically modified, persistent server configuration variables.</li>
<li>The <code>mysql_upgrade_history</code>.</li>
<li>The MySQL configuration file (<code>my.cnf</code>) as well as</li>
<li>The binary logs.</li>
</ul>
<pre><code>$ cp ${datadir}/*auto.cnf ${datadir}/mysql_upgrade_history ${datadir}/*.pem /mnt/backup/mysql_clone/
</code></pre>
<p>Restoring the database is quite simple:</p>
<pre><code>$ systemctl stop mysql
$ rm -rf ${datadir}/*
$ cp -a /mnt/backup/mysql_clone/* ${datadir}/
$ chown -R mysql: ${datadir}/*
$ systemctl start mysql
</code></pre>
<p>The <code>#clone</code> folder is created by the clone operation and can be ignored, but must not be deleted.</p>
<pre><code>$ ls -lad /mnt/backup/mysql_clone/*
...
drwxr-x--- 2 dba dba 4096 Jul 27 14:52 '#clone'
...
</code></pre>
<p>It is automatically removed when the MySQL database is started. If you delete it anyway, you will receive the following error messages:</p>
<pre><code>[System] [MY-013576] [InnoDB] InnoDB initialization has started.
[System] [MY-013577] [InnoDB] InnoDB initialization has ended.
mysqld: Can't create/write to file './performance_schema/clone_status_385.sdi' (OS errno 2 - No such file or directory)
mysqld: Can't create file './performance_schema/clone_status_385.sdi' (errno: 2 - No such file or directory)
[ERROR] [MY-013272] [Clone] Plugin Clone reported: 'Client: PFS table creation failed.'
[ERROR] [MY-010202] [Server] Plugin 'clone' init function returned error.
</code></pre>
<p>The binary log position required for point-in-time recovery can be determined as follows:</p>
<pre><code>SQL&gt; SELECT BINLOG_FILE, BINLOG_POSITION FROM performance_schema.clone_status;
+-------------------------------+-----------------+
| BINLOG_FILE | BINLOG_POSITION |
+-------------------------------+-----------------+
| boss_percona-84_binlog.000003 | 1231898 |
+-------------------------------+-----------------+
</code></pre>
<h2 id="remote-backup-using-clone">Remote Backup Using Clone<a class="anchor-link" id="remote-backup-using-clone"></a></h2>
<p>To create a remote backup using the clone functionality, a minimally functional MySQL database is required on the remote system. Unfortunately, a simple process or tool is not sufficient for this.</p>
<p>On the donor server, you need a user with the following privileges:</p>
<pre><code>SQL&gt; CREATE USER 'backup_user'@'%' IDENTIFIED BY 'secret';
SQL&gt; GRANT BACKUP_ADMIN ON *.* TO 'backup_user'@'%';
</code></pre>
<p>In addition, the potential donor must be specified on the recipient server:</p>
<pre><code>SQL&gt; SET GLOBAL clone_valid_donor_list = '192.168.1.129:3306';
</code></pre>
<p>The remote backup is then performed as follows:</p>
<pre><code>SQL&gt; CLONE INSTANCE FROM 'backup_user'@'192.168.1.129':3306 IDENTIFIED BY 'secret'
DATA DIRECTORY = '/mnt/backup/mysql_clone';
</code></pre>
<p>The missing files described above must now also be copied somehow:</p>
<pre><code>$ scp mysql@192.168.1.129:${datadir}/*auto.cnf /mnt/backup/mysql_clone/
$ scp mysql@192.168.1.129:${datadir}/mysql_upgrade_history /mnt/backup/mysql_clone/
$ scp mysql@192.168.1.129:${datadir}/*.pem /mnt/backup/mysql_clone/
</code></pre>
<p>Restoring the database is done in the same way as described above.</p>
<h2 id="conclusion">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>The MySQL Clone operation is a cool feature that I neglected for a long time because it never occurred to me that it could also be used for backup purposes.</p>
<p>I wouldn&rsquo;t be surprised if the MySQL developers took a cue from PostgreSQL&rsquo;s <code>pg_basebackup</code> when they implemented this feature.</p>
<p>Unfortunately, to my knowledge, this feature is still completely missing in MariaDB. Too bad!</p>
<h2 id="sources">Sources<a class="anchor-link" id="sources"></a></h2>
<ul>
<li>General: <a href="https://dev.mysql.com/doc/refman/9.7/en/clone-plugin.html" target="_blank" rel="noopener">The Clone Plugin</a></li>
<li>There are a few minor limitations for the clone backup, which are described here: <a href="https://dev.mysql.com/doc/refman/9.7/en/clone-plugin-limitations.html" target="_blank" rel="noopener">Clone Plugin Limitations</a>.</li>
<li>Monitoring the clone backup is described here: <a href="https://dev.mysql.com/doc/refman/9.7/en/clone-plugin-monitoring.html" target="_blank" rel="noopener">Monitoring Cloning Operations</a>.</li>
<li>Tuning the clone backup is described here: <a href="https://dev.mysql.com/doc/refman/9.7/en/clone-plugin-option-variable-reference.html" target="_blank" rel="noopener">Clone System Variable Reference</a></li>
</ul>

<p><a href="https://www.fromdual.com/blog/backups-with-mysql-clone/">Backups Using the MySQL Clone Operation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Adobe Commerce Chooses MariaDB as Its Default Database Platform</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/adobe-commerce-chooses-mariadb-as-its-default-database-platform/" />
      <id>https://mariadb.org/adobe-commerce-chooses-mariadb-as-its-default-database-platform/</id>
      <updated>2026-07-27T12:49:48+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Adobe Commerce is making MariaDB its default and recommended database<br />
platform.<br />
The post Adobe Commerce Chooses MariaDB as Its Default Database Platform appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/adobe-commerce-chooses-mariadb-as-its-default-database-platform/">Adobe Commerce Chooses MariaDB as Its Default Database Platform</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Adobe Commerce is making MariaDB its default and recommended database<br>
platform. </p>
<p>The post <a rel="nofollow" href="https://mariadb.org/adobe-commerce-chooses-mariadb-as-its-default-database-platform/">Adobe Commerce Chooses MariaDB as Its Default Database Platform</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/adobe-commerce-chooses-mariadb-as-its-default-database-platform/">Adobe Commerce Chooses MariaDB as Its Default Database Platform</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/" />
      <id>https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/</id>
      <updated>2026-07-27T09:11:10+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>Global cloud hosting provider supports the continued development and adoption of open-source MariaDB<br />
MariaDB Foundation is pleased to welcome ScalaHosting as a Gold Sponsor, strengthening the relationship between the MariaDB community and one of the hosting industry’s established cloud infrastructure providers. …<br />
Continue reading \"ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation\"<br />
The post ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/">ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Global cloud hosting provider supports the continued development and adoption of open-source MariaDB<br>
MariaDB Foundation is pleased to welcome <a href="https://www.scalahosting.com/">ScalaHosting</a> as a <a href="https://mariadb.org/donate/#gold-tier-eur-50000-per-year">Gold Sponsor</a>, strengthening the relationship between the MariaDB community and one of the hosting industry&rsquo;s established cloud infrastructure providers. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/">ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/scalahosting-becomes-a-gold-sponsor-of-mariadb-foundation/">ScalaHosting Becomes a Gold Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Introducing 🍃MClusterAdmin: A Lightweight GUI Tool for MongoDB DBAs</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/27/introducing-mclusteradmin-a-lightweight-gui-tool-for-mongodb-dba/" />
      <id>https://percona.community/blog/2026/07/27/introducing-mclusteradmin-a-lightweight-gui-tool-for-mongodb-dba/</id>
      <updated>2026-07-27T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Over the years working on various MongoDB troubleshooting cases, I’ve been wondering how to handle the longish JSON outputs from the most common diagnostic commands, like rs.status(), sh.status(), or db.currentOp(), not to mention db.serverStatus()! They are just painful and slow to read. I even came up with various scripts to present the data in table format, similar to what we know from MySQL, but I was never satisfied with them.</p>
<p><a href="https://percona.community/blog/2026/07/27/introducing-mclusteradmin-a-lightweight-gui-tool-for-mongodb-dba/">Introducing 🍃MClusterAdmin: A Lightweight GUI Tool for MongoDB DBAs</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Over the years working on various MongoDB troubleshooting cases, I&rsquo;ve been wondering how to handle the longish JSON outputs from the most common diagnostic commands, like <code>rs.status()</code>, <code>sh.status()</code>, or <code>db.currentOp()</code>, not to mention <code>db.serverStatus()</code>! They are just painful and slow to read. I even came up with various scripts to present the data in <a href="https://github.com/kellyjonbrazil/jtbl" target="_blank" rel="noopener noreferrer">table format</a>, similar to what we know from MySQL, but I was never satisfied with them.</p>
<p>Finally, I thought it was futile to look for a universal solution that would yield human-friendly outputs in command-line sessions.</p>
<p>So, why not have a nice graphical interface to visualize replica sets, sharding state, connections, and more instead? I looked for available GUI tools for MongoDB and came to the impression that <strong>almost all of them are built</strong> <strong>for developers</strong>. They are great for browsing collections, editing documents, or composing queries, but when it comes to typical <strong>DBA</strong> daily tasks &mdash; checking replication health, inspecting replica set configuration, or understanding what is really going on inside a sharded cluster &mdash; we are really down to the mongo shell. There is nothing wrong with the shell client, of course, but some things are simply easier to digest when presented visually. Here, of course, <a href="https://docs.percona.com/percona-monitoring-and-management/3/install-pmm/install-pmm-client/connect-database/mongodb.html" target="_blank" rel="noopener noreferrer">PMM</a> does allow that, but it has a bit of a different purpose &ndash; long-term monitoring, and has to be installed and set up first. Besides, it does not do everything I wanted.</p>
<p>So, encouraged to experiment with vibe coding in Percona, I decided to experiment with filling this gap myself and started a small personal project: a lightweight <strong>DBA-oriented</strong> GUI interface called <strong>MClusterAdmin</strong>.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/07/mclusteradmin-picture.jpg" alt="MClusterAdmin"></figure>
</p>
<h3 id="project-description">Project description<a class="anchor-link" id="project-description"></a></h3>
<p>MClusterAdmin is a tool designed to be self-hosted, with a backend written in Go, while the frontend is in HTML and JavaScript. It offers a web UI interface featuring typical <strong>DBA perspective</strong> dashboards. The whole application ships as a single, small binary, with <strong>no agents</strong>, no external services, and no internal database of its own.</p>
<p>In order to try it, you need to copy the binary to a host that has access to the MongoDB servers to be monitored. I suggest running first locally on some test environment, like one created using <a href="https://github.com/PrzemekMalkowski/mlaunch-go" target="_blank" rel="noopener noreferrer">mlaunch</a>, <a href="https://github.com/zelmario/anydbver" target="_blank" rel="noopener noreferrer">anydbver</a>, <a href="https://github.com/percona/mongo_terraform_ansible" target="_blank" rel="noopener noreferrer">mongo_terraform_ansible</a>, or a similar sandbox tool. For <strong>Docker</strong> environments, you can use the already existing <a href="https://github.com/PrzemekMalkowski/mclusteradmin/pkgs/container/mclusteradmin" target="_blank" rel="noopener noreferrer">image</a>, just make sure your container shares the same network as the MongoDB cluster.</p>
<p>The tool has no authentication on its own. Very similarly to mongosh, the MongoDB URI and credentials you provide determine what it will be able to offer. You can connect a standalone instance, a replica set member, or a mongos router. The tool will discover the rest of the topology automatically and will establish individual connections to each member. No SSH access to the database hosts is needed.</p>
<p>With its tiny footprint, it should fit well into your cloud/K8S MongoDB deployments.</p>
<p>Let me be clear about what MClusterAdmin is <strong>not</strong>: it is not a data browser &mdash; you will not view or edit your collections&rsquo; documents with it. There are plenty of other tools for that! It is, again, not a long-term monitoring or alerting solution; for that, I strongly recommend <a href="https://docs.percona.com/percona-monitoring-and-management/3/" target="_blank" rel="noopener noreferrer">Percona Monitoring and Management (PMM)</a>. MClusterAdmin is designed for quick, interactive cluster inspection and simple administrative operations that a DBA performs many times a day.</p>
<p>I will point out the key features that I decided to implement further below, but I guess watching a quick <strong>demo presentation</strong> will allow you to judge the tool much faster:</p>
<div class="youtube__block">
<p>Link: <a href="https://youtu.be/RecQ7wtEV9g" class="youtube__link" target="_blank" rel="noopener">https://youtu.be/RecQ7wtEV9g</a></p>
</div>
<p>I don&rsquo;t think it makes sense to describe all the features I was able to implement so far in detail. I hope everything is intuitive enough so that you can see for yourself. The <a href="https://github.com/PrzemekMalkowski/mclusteradmin/blob/master/README.md" target="_blank" rel="noopener noreferrer">documentation</a> is available on GitHub if needed.</p>
<p>In short, my aim was to provide useful views covering <strong>replication topology</strong> and settings, sharding members and routers, but also overall database size statistics and their distribution among shards. This includes per-collection detailed sharding stats, data, and index sizes, etc. In addition to that, for each discovered MongoDB instance, you should be able to see basic WiredTiger usage stats, oplog details, including an on-demand breakdown of <strong>which collections changes generated the most recent oplog traffic</strong>, as well as some other most important server settings and usage summaries.</p>
<p>You will also find slow queries profiling, with the query explain module, as well as a quite functional users and roles management section.</p>
<p>The last <strong>security-focused</strong> section allows you to check whether authentication is enabled on each host, as well as what is the usage of each authentication mechanism.</p>
<h3 id="getting-started">Getting Started<a class="anchor-link" id="getting-started"></a></h3>
<p>Running MClusterAdmin takes a few seconds. Just download the suitable binary from the <a href="https://github.com/PrzemekMalkowski/mclusteradmin/releases" target="_blank" rel="noopener noreferrer">release page</a>, or compile it yourself:</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-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-0">
<div class="highlight">
<pre class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone https://github.com/PrzemekMalkowski/mclusteradmin.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> mclusteradmin
</span></span><span class="line"><span class="cl">go build -o mca .
</span></span><span class="line"><span class="cl">./mca</span></span></code></pre>
</div>
</div>
</div>
<p>Then open the relevant address (by default http://localhost:8787) in your browser and paste your MongoDB connection URI to connect. There is also a <code>--view-only</code> flag, which disables all mutating operations both in the UI and at the API level &mdash; handy when you just want a safe, read-only window into a cluster. TLS mode for the web service interface is supported as well.</p>
<h3 id="kubernetes-integration">Kubernetes integration<a class="anchor-link" id="kubernetes-integration"></a></h3>
<p>It is extremely easy to add MClusterAdmin as a diagnostic pod to your existing MongoDB cluster in Kubernetes. Just use the available <a href="https://github.com/PrzemekMalkowski/mclusteradmin/pkgs/container/mclusteradmin" target="_blank" rel="noopener noreferrer">Docker image</a> from the GitHub repository!</p>
<p>An example MClusterAdmin.yaml configuration can be as simple as:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-1">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">apiVersion: apps/v1
</span></span><span class="line"><span class="cl">kind: Deployment
</span></span><span class="line"><span class="cl">metadata:
</span></span><span class="line"><span class="cl"> name: MClusterAdmin
</span></span><span class="line"><span class="cl">spec:
</span></span><span class="line"><span class="cl"> replicas: 1
</span></span><span class="line"><span class="cl"> selector:
</span></span><span class="line"><span class="cl"> matchLabels:
</span></span><span class="line"><span class="cl"> app: MClusterAdmin
</span></span><span class="line"><span class="cl"> template:
</span></span><span class="line"><span class="cl"> metadata:
</span></span><span class="line"><span class="cl"> labels:
</span></span><span class="line"><span class="cl"> app: MClusterAdmin
</span></span><span class="line"><span class="cl"> spec:
</span></span><span class="line"><span class="cl"> containers:
</span></span><span class="line"><span class="cl"> - name: MClusterAdmin
</span></span><span class="line"><span class="cl"> image: ghcr.io/przemekmalkowski/mclusteradmin:latest
</span></span><span class="line"><span class="cl"> ports:
</span></span><span class="line"><span class="cl"> - containerPort: 8787</span></span></code></pre>
</div>
</div>
</div>
<h3 id="a-word-of-caution">A Word of Caution<a class="anchor-link" id="a-word-of-caution"></a></h3>
<p>MClusterAdmin is currently in <strong>beta</strong>, and it is a personal side project, so please do not point it at your production clusters just yet &mdash; test it in a safe environment first. The connected MongoDB user needs privileges matching the dashboards you want to use; the documentation breaks these down per feature, so you can follow the least-privilege approach instead of simply granting root.</p>
<h3 id="summary">Summary<a class="anchor-link" id="summary"></a></h3>
<p>MClusterAdmin was created as an attempt to fill the gap I found to be the case in the MongoDB community: a lightweight, replication- and sharding-aware GUI for MongoDB <strong>DBAs</strong>, free from any data-browsing ballast. The project is open source (GPLv3), and the code is available on <a href="https://github.com/PrzemekMalkowski/mclusteradmin" target="_blank" rel="noopener noreferrer">GitHub</a>. If you find it useful, miss a feature, or hit a bug, I would love to hear from you. Feedback is very welcome!</p>
<p><em>The article was created by a human.</em></p>

<p><a href="https://percona.community/blog/2026/07/27/introducing-mclusteradmin-a-lightweight-gui-tool-for-mongodb-dba/">Introducing 🍃MClusterAdmin: A Lightweight GUI Tool for MongoDB DBAs</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Queen and the “Half That Wasn’t Told”</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/the-queen-and-the-half-that-wasnt-told/" />
      <id>https://mariadb.org/the-queen-and-the-half-that-wasnt-told/</id>
      <updated>2026-07-26T18:55:29+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>The Queen of Sheba did not travel lightly.<br />
She arrived in Jerusalem with difficult questions, a large caravan, camels carrying spices, and an impressive quantity of gold. …<br />
Continue reading \"The Queen and the “Half That Wasn’t Told”\"<br />
The post The Queen and the “Half That Wasn’t Told” appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/the-queen-and-the-half-that-wasnt-told/">The Queen and the “Half That Wasn’t Told”</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The Queen of Sheba did not travel lightly.<br>
She arrived in Jerusalem with difficult questions, a large caravan, camels carrying spices, and an impressive quantity of gold. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/the-queen-and-the-half-that-wasnt-told/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;The Queen and the &ldquo;Half That Wasn&rsquo;t Told&rdquo;&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/the-queen-and-the-half-that-wasnt-told/">The Queen and the &ldquo;Half That Wasn&rsquo;t Told&rdquo;</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/the-queen-and-the-half-that-wasnt-told/">The Queen and the “Half That Wasn’t Told”</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>IBM continues as a Platinum Sponsor of MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/" />
      <id>https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/</id>
      <updated>2026-07-24T13:12:09+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>We are delighted to announce that IBM is continuing its support of MariaDB Foundation as a Platinum Sponsor.<br />
IBM’s sponsorship brings together two major enterprise computing platforms, IBM® …<br />
Continue reading \"IBM continues as a Platinum Sponsor of MariaDB Foundation\"<br />
The post IBM continues as a Platinum Sponsor of MariaDB Foundation appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/">IBM continues as a Platinum Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We are delighted to announce that IBM is continuing its support of MariaDB Foundation as a <a href="https://mariadb.org/donate/#platinum-tier-eur-100000-per-year">Platinum Sponsor</a>.<br>
IBM&rsquo;s sponsorship brings together two major enterprise computing platforms, IBM&reg; &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;IBM continues as a Platinum Sponsor of MariaDB Foundation&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/">IBM continues as a Platinum Sponsor of MariaDB Foundation</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/ibm-continues-as-a-platinum-sponsor-of-mariadb-foundation/">IBM continues as a Platinum Sponsor of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Say the Name: MariaDB, MySQL, and the Ecosystem We Share</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/say-the-name-mariadb-mysql-and-the-ecosystem-we-share/" />
      <id>https://mariadb.org/say-the-name-mariadb-mysql-and-the-ecosystem-we-share/</id>
      <updated>2026-07-24T06:58:58+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB and MySQL share history, tools, protocols and a large technical community. But compatibility does not mean identity—especially when reporting bugs. A MariaDB Connector/C issue submitted to the MySQL bug tracker offers a funny reminder to say the product’s actual name.<br />
The post Say the Name: MariaDB, MySQL, and the Ecosystem We Share appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/say-the-name-mariadb-mysql-and-the-ecosystem-we-share/">Say the Name: MariaDB, MySQL, and the Ecosystem We Share</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB and MySQL share history, tools, protocols and a large technical community. But compatibility does not mean identity&mdash;especially when reporting bugs. A MariaDB Connector/C issue submitted to the MySQL bug tracker offers a funny reminder to say the product&rsquo;s actual name.</p>
<p>The post <a rel="nofollow" href="https://mariadb.org/say-the-name-mariadb-mysql-and-the-ecosystem-we-share/">Say the Name: MariaDB, MySQL, and the Ecosystem We Share</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/say-the-name-mariadb-mysql-and-the-ecosystem-we-share/">Say the Name: MariaDB, MySQL, and the Ecosystem We Share</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Operator for MongoDB 1.23.0: ClusterSync Migration, Vector Search, and PVC Snapshot Backups</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/" />
      <id>https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/</id>
      <updated>2026-07-23T19:55:51+03:00</updated>
      <author><name>Slava Sarzhan</name></author>
      <summary type="html"><![CDATA[<p>Percona Operator for MongoDB 1.23.0 makes the operator a place you move to, not just a place you start. A new ClusterSync component clones a live source and follows its change streams, so leaving a hosted service is a short cutover rather than a long outage. Alongside it, this release adds semantic vector search and … Continued<br />
The post Percona Operator for MongoDB 1.23.0: ClusterSync Migration, Vector Search, and PVC Snapshot Backups appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/">Percona Operator for MongoDB 1.23.0: ClusterSync Migration, Vector Search, and PVC Snapshot Backups</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50444 size-full" src="https://www.percona.com/wp-content/uploads/2026/07/Cover-1000-x-420.png" alt="" width="1001" height="420"><br>
<b>Percona Operator for MongoDB 1.23.0</b> makes the operator a place you move to, not just a place you start. A new ClusterSync component clones a live source and follows its change streams, so leaving a hosted service is a short cutover rather than a long outage. Alongside it, this release adds semantic vector search and storage-layer snapshot backups, two features that matter most once the data is yours to run.</p>
<p><span style="font-weight: 400">The three headline features are </span><b>Percona ClusterSync for MongoDB</b><span style="font-weight: 400">, </span><b>vector search</b><span style="font-weight: 400">, and </span><b>PVC snapshot backups</b><span style="font-weight: 400">. ClusterSync clones and continuously replicates a live source into an operator-managed cluster. Vector search brings semantic queries to Percona Server for MongoDB. PVC snapshot backups move backups off the network path and onto the storage layer.</span></p>
<p><span style="font-weight: 400">This release also widens where you can run it, adding official Rancher Kubernetes Engine (RKE2) support and full ARM64 images. Much of what shipped here traces back to requests on </span><a href="https://forums.percona.com/"><span style="font-weight: 400">forums.percona.com</span></a><span style="font-weight: 400"> and the public issue tracker.</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">In this post, you&rsquo;ll learn about:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">ClusterSync migration and replication</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Vector search for semantic queries</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">PVC snapshot backups</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Other improvements worth knowing about<br>
</span></li>
</ul>
<p>&nbsp;</p>
<h2><b>Zero-Downtime Migration with Percona ClusterSync</b><a class="anchor-link" id="zero-downtime-migration-with-percona-clustersync"></a></h2>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50445 size-large" src="https://www.percona.com/wp-content/uploads/2026/07/clustersync-migration-1024x569.png" alt="" width="1024" height="569"></p>
<p><span style="font-weight: 400">Moving a live MongoDB database onto the operator has always been the awkward first step. Dump-and-restore needs a maintenance window sized to your data, and hand-built replication between a source and a target is fragile to set up and easy to get wrong. This release introduces <a href="https://docs.percona.com/percona-clustersync-for-mongodb/">Percona ClusterSync for MongoDB</a> (PCSM) as an operator-managed component, so the migration path is via a Kubernetes object rather than a runbook.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400"><b>Why it matters</b><br>
</span><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">The common case is </span><span style="font-weight: 400">migrating</span><span style="font-weight: 400"> a hosted MongoDB service, for example MongoDB Atlas, for an operator-managed Percona Server for MongoDB cluster you control end to end. A typical trigger in production is a hosted-service bill that climbs with the workload, or a compliance requirement to keep data inside your own VPC and region: a team running a user-profile store on Atlas points PCSM at it, lets the target catch up over a day or two while the application keeps serving from Atlas, then cuts over in a maintenance window measured in seconds. PCSM clones the existing data, then tracks ongoing changes through MongoDB change streams, so the target stays current while you validate it. When you are ready, you cut the application over during a short window rather than a long one. The same mechanism keeps a continuously updated replica for non-production use or a hybrid-cloud copy.<br>
<b><br>
</b></span></p>
<h3><span style="font-weight: 400"><b>How it works</b><br>
</span><a class="anchor-link" id="how-it-works"></a></h3>
<p><span style="font-weight: 400">PCSM runs as its own container, deployed and managed through a new </span><em><span style="font-weight: 400">PerconaServerMongoDBClusterSync</span></em><span style="font-weight: 400"> custom resource. It performs an initial clone from the source connection string, then consumes change stream events to apply subsequent writes to the target. A </span><span style="font-weight: 400">mode</span><span style="font-weight: 400"> field controls the lifecycle: </span><span style="font-weight: 400">running</span><span style="font-weight: 400"> starts or resumes replication, </span><span style="font-weight: 400">paused</span><span style="font-weight: 400"> holds it, and </span><span style="font-weight: 400">finalized</span><span style="font-weight: 400"> stops replication.</span></p>
<p>&nbsp;</p>
<h3><b>Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>

<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDBClusterSync
metadata:
  name: my-cluster-sync
spec:
  clusterName: my-target-cluster-name
  image: percona/percona-clustersync-mongodb:0.9.0
  # mode controls the PCSM lifecycle intent. Allowed values:
  #   running   - start/resume replication (default)
  #   paused    - pause an active replication
  #   finalized - stop replication
  mode: running
  source:
    uri: mongodb://source-cluster-mongos.source-namespace.svc.cluster.local:27017
    credentialsSecret: my-cluster-sync-source
  # excludeNamespaces lists MongoDB namespaces (db or db.collection) to skip.
  # excludeNamespaces:
  #   - admin
  #   - local</pre>
<p><em><span style="font-weight: 400">clusterName</span></em><span style="font-weight: 400"> names the operator-managed target that receives the data. </span><em><span style="font-weight: 400">source.uri</span></em><span style="font-weight: 400"> and </span><em><span style="font-weight: 400">source.credentialsSecret</span></em><span style="font-weight: 400"> point at the database you are migrating from, which can be Atlas, a self-managed replica set, or another operator cluster. </span><em><span style="font-weight: 400">mode</span></em><span style="font-weight: 400"> is the control you drive the cutover with: run to catch up, pause to hold, then finalize once the application points at the new cluster. The optional </span><em><span style="font-weight: 400">excludeNamespaces</span></em><span style="font-weight: 400"> list skips databases or collections you do not want to copy.</span><br>
&nbsp;</p>
<h3><b>Cutover and rollback</b><a class="anchor-link" id="cutover-and-rollback"></a></h3>
<p><span style="font-weight: 400">The cutover is yours to time, not the operator&rsquo;s. </span><span style="font-weight: 400">During the running replication</span><span style="font-weight: 400">, the target trails the source by the change-stream lag, which you watch until it is small and steady. You then stop writes on the source, let the last events drain, and repoint the application at the target cluster. Because the source keeps serving until you move the application, a rollback before cutover is simply leaving the application where it is. After cutover, treat the move as one-way once writes flow to the target, so verify the target thoroughly during the sync window rather than after.</span></p>
<blockquote>
<p><b>Note:</b><span style="font-weight: 400"> The PCSM component ships at version 0.9.0 with this release. Test the full migration and cutover against a staging copy before you run it on production data, and keep the source available until you have verified the target.</span></p>
</blockquote>
<p>&nbsp;</p>
<h2><b>Vector search for semantic queries</b><a class="anchor-link" id="vector-search-for-semantic-queries"></a></h2>
<p><span style="font-weight: 400">Vector search retrieves results by meaning rather than exact keyword match, which is the retrieval pattern behind semantic search and retrieval-augmented generation for AI applications. Teams that already store their data in MongoDB have had to copy vectors into a separate engine to do this, which adds a system to run and a pipeline to keep in sync. In production, this is the pattern behind a support tool that surfaces past tickets describing the same problem in different words, a product catalog that returns items by intent rather than exact keywords, and a RAG service that grounds a model on internal documents. This release lets you store and query vector data alongside your regular documents in Percona Server for MongoDB, so those workloads query one system instead of two.</span><br>
&nbsp;</p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span style="font-weight: 400">The operator deploys and manages the </span><span style="font-weight: 400">mongot</span><span style="font-weight: 400"> search process, wires its authentication and TLS to the rest of the cluster, and keeps the search index synchronized for both replica set and sharded deployments. Applications query the index through the same MongoDB connection they already use, so you add semantic search without a second client, a second driver, or a second set of credentials. You do not stand up or secure a separate search tier; the operator treats </span><span style="font-weight: 400">mongot</span><span style="font-weight: 400"> as another managed component of the cluster.</span><br>
&nbsp;</p>
<h3><b>Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span style="font-weight: 400">Enable the search component in the custom resource:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  search:
    enabled: true
    image: perconalab/percona-server-mongodb-operator:main-mongot
    size: 1
    storage:
      persistentVolumeClaim:
        resources:
          requests:
            storage: 10Gi
    resources:
      requests:
        cpu: "2"
        memory: 2Gi</pre>
<p><em><span style="font-weight: 400">size</span></em><span style="font-weight: 400"> sets how many search nodes to run, and </span><em><span style="font-weight: 400">storage</span></em><span style="font-weight: 400"> gives the search index its own <em>PersistentVolumeClaim</em> so it does not compete with the database volume. Size the </span><em><span style="font-weight: 400">resources</span></em><span style="font-weight: 400"> block to your index: vector indexes are memory-sensitive, so give </span><em><span style="font-weight: 400">mongot</span></em><span style="font-weight: 400"> enough headroom for the corpus you intend to query.</span></p>
<blockquote>
<p><b>Note:</b><span style="font-weight: 400"> Vector search is a tech preview in 1.23.0 and is not recommended for production yet. It requires Percona Server for MongoDB 8.3 or later.</span></p>
</blockquote>
<p>&nbsp;</p>
<h2><b>PVC snapshot backups</b><a class="anchor-link" id="pvc-snapshot-backups"></a></h2>
<p><span style="font-weight: 400">Logical and streamed physical backups both push data across the network to object storage, and for a multi-terabyte cluster, that path is the bottleneck. Backups run long, restores run longer, and both compete with production traffic for CPU and bandwidth. This release adds backups built on PersistentVolumeClaim snapshots, </span><span style="font-weight: 400">which takes the storage layer directly</span><span style="font-weight: 400">.</span></p>
<p>&nbsp;</p>
<h3><b>Why it matters</b><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">A PVC snapshot is a point-in-time copy of your data volumes taken at the storage layer through the Kubernetes VolumeSnapshot API. Because the operator asks the storage provider for a snapshot instead of streaming bytes out, a backup typically completes in seconds or minutes regardless of database size, and a restore is correspondingly fast. Two production situations show the difference: a nightly backup that no longer fits its window as a cluster grows past a few terabytes, and a staging refresh that ties up resources for hours while it restores a streamed copy. A storage-layer snapshot turns both into a near-instant operation. The speed comes from how the storage layer implements snapshots: instead of copying the whole volume, most backends record only the blocks that changed since the previous snapshot and reference the rest, so the cost tracks your change rate rather than the total database size. Snapshots also work with encrypted and TLS-enabled clusters, and they use fewer cluster resources because there is no long-running data-transfer job.</span></p>
<h3><a class="anchor-link" id=""></a></h3>
<p>&nbsp;</p>
<h3><b>Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span style="font-weight: 400">The operator takes snapshot backups in two ways: on demand through a </span><em><span style="font-weight: 400">PerconaServerMongoDBBackup</span></em><span style="font-weight: 400"> object, or on a schedule through a backup task. The scheduled form looks like this, using the </span><em><span style="font-weight: 400">external</span></em><span style="font-weight: 400"> type and a <em>VolumeSnapshotClass</em>:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  backup:
    tasks:
      - name: daily-snapshot
        enabled: false
        schedule: "0 0 * * *"
        retention:
          count: 1
          type: count
          deleteFromStorage: true
        type: external
        volumeSnapshotClass: YOUR-VOLUME-SNAPSHOT-CLASS</pre>
<p><em><span style="font-weight: 400">type: external</span></em><span style="font-weight: 400"> tells the operator to take a storage-layer snapshot rather than stream a backup, and </span><span style="font-weight: 400">volumeSnapshotClass</span><span style="font-weight: 400"> names the <em>VolumeSnapshotClass</em> your CSI driver provides. The </span><em><span style="font-weight: 400">retention</span></em><span style="font-weight: 400"> block prunes old snapshots on the schedule you set. Your storage provider must support the Kubernetes VolumeSnapshot API for this to work.</span></p>
<p><span style="font-weight: 400">Snapshot backups complement the streamed and logical backups the operator already supports; they do not replace them. Snapshots usually live in the same storage account and region as the volumes they copy, so keep a streamed backup to object storage for off-site and cross-region disaster recovery. A practical policy pairs frequent fast snapshots for quick local recovery with a less frequent streamed backup for durability, and the operator runs both from the same </span><em><span style="font-weight: 400">backup.tasks</span></em><span style="font-weight: 400"> list.</span></p>
<blockquote>
<p><b><br>
Note: </b><span style="font-weight: 400">PVC snapshot backups are a tech preview in 1.23.0 and are not recommended for production yet. Snapshot portability and retention semantics depend on your CSI driver, so test restores before you rely on them.</span></p>
</blockquote>
<p>&nbsp;</p>
<h2><b>Other improvements</b><a class="anchor-link" id="other-improvements"></a></h2>
<p><span style="font-weight: 400">Beyond the three headline features, 1.23.0 ships a set of enhancements that smooth day-two operations:</span></p>
<ul>
<li style="font-weight: 400"><b>Operator-generated connection string Secrets </b><span style="font-weight: 400">(</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1537"><span style="font-weight: 400">K8SPSMDB-1537</span></a><span style="font-weight: 400">): the operator now publishes a ready-to-use </span><b>MongoDB connection string </b><span style="font-weight: 400">(URI) in a Kubernetes Secret for the </span><i><span style="font-weight: 400">databaseAdmin</span></i><span style="font-weight: 400"> user. An application can read that one Secret and connect to it, instead of building the URI itself from Pod names, Services, TLS settings, and credentials.</span></li>
<li style="font-weight: 400"><b>Workload Identity for GCS backups</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1602"><span style="font-weight: 400">K8SPSMDB-1602</span></a><span style="font-weight: 400">): back up to Google Cloud Storage without storing a service-account JSON key in a Secret.</span></li>
<li style="font-weight: 400"><b>Oracle Cloud Infrastructure Object Storage</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1644"><span style="font-weight: 400">K8SPSMDB-1644</span></a><span style="font-weight: 400">) and </span><b>Alibaba Cloud OSS</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1519"><span style="font-weight: 400">K8SPSMDB-1519</span></a><span style="font-weight: 400">): two more native backup destinations.</span></li>
<li style="font-weight: 400"><b>Restore a collection under a different name</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1603"><span style="font-weight: 400">K8SPSMDB-1603</span></a><span style="font-weight: 400">): use selective.nsFrom and nsTo to restore one collection alongside the live one for inspection or recovery.</span></li>
<li style="font-weight: 400"><b>External nodes as arbiters</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1031"><span style="font-weight: 400">K8SPSMDB-1031</span></a><span style="font-weight: 400">): set </span><i><span style="font-weight: 400">arbiterOnly: true</span></i><span style="font-weight: 400"> on an external node to place a tie-breaker vote in a third location without a data-bearing member.</span></li>
<li style="font-weight: 400"><b>cert-manager ClusterIssuer and TLS policy </b><span style="font-weight: 400">(</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1413"><span style="font-weight: 400">K8SPSMDB-1413</span></a><span style="font-weight: 400">, </span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1458"><span style="font-weight: 400">K8SPSMDB-1458</span></a><span style="font-weight: 400">): point the operator at an existing </span><i><span style="font-weight: 400">ClusterIssuer</span></i><span style="font-weight: 400">, and use </span><i><span style="font-weight: 400">certManagementPolicy</span></i><span style="font-weight: 400"> to keep certificate lifecycle fully under your control.</span></li>
<li style="font-weight: 400"><b>Tunable reconciliation interval </b><span style="font-weight: 400">(</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1571"><span style="font-weight: 400">K8SPSMDB-1571</span></a><span style="font-weight: 400">): set </span><i><span style="font-weight: 400">RECONCILE_INTERVAL</span></i><span style="font-weight: 400"> to reduce Kubernetes API load on large fleets (default 5s).</span></li>
<li style="font-weight: 400"><b>Query Analytics via mongolog for PMM</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1546"><span style="font-weight: 400">K8SPSMDB-1546</span></a><span style="font-weight: 400">): choose mongolog as the QAN source in </span><a href="https://docs.percona.com/percona-monitoring-and-management/"><span style="font-weight: 400">Percona Monitoring and Management</span></a><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><b>Custom sidecar health probes</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1701"><span style="font-weight: 400">K8SPSMDB-1701</span></a><span style="font-weight: 400">, </span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1728"><span style="font-weight: 400">K8SPSMDB-1728</span></a><span style="font-weight: 400">) and </span><b>StatefulSet</b> <i><span style="font-weight: 400">revisionHistoryLimit</span></i><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1572"><span style="font-weight: 400">K8SPSMDB-1572</span></a><span style="font-weight: 400">): finer control over probes and rollout history.</span>&nbsp;</li>
</ul>
<p><span style="font-weight: 400">For the full list, including bug fixes, see the release notes linked below.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400"><b>Conclusion</b></span><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">Percona Operator for MongoDB 1.23.0 covers the arc from getting data in to keeping it safe: ClusterSync brings a live database onto the operator with a short cutover, vector search lets one system serve both documents and semantic queries, and PVC snapshot backups take the network out of the backup path. With RKE2 and full ARM64 support added, more of that runs on the platforms teams actually use. If there is a workflow you still script around the operator, tell us on the forum, since that is where releases like this one come from.<br>
</span></p>
<h2><span style="font-weight: 400"><br>
<b>Try Percona Operator for MongoDB 1.23.0<br>
</b></span><a class="anchor-link" id="try-percona-operator-for-mongodb-1-23-0"></a></h2>
<ul>
<li style="font-weight: 400"><b>Release notes</b><span style="font-weight: 400">: </span><a href="https://docs.percona.com/percona-operator-for-mongodb/RN/Kubernetes-Operator-for-PSMONGODB-RN1.23.0.html"><span style="font-weight: 400">Percona Operator for MongoDB 1.23.0 Release Notes</span></a></li>
<li style="font-weight: 400"><b>Documentation</b><span style="font-weight: 400">: </span><a href="https://docs.percona.com/percona-operator-for-mongodb/"><span style="font-weight: 400">Percona Operator for MongoDB docs</span></a></li>
<li style="font-weight: 400"><b>GitHub</b><span style="font-weight: 400">: </span><a href="https://github.com/percona/percona-server-mongodb-operator"><span style="font-weight: 400">percona/percona-server-mongodb-operator</span></a></li>
<li style="font-weight: 400"><b>Community Forum</b><span style="font-weight: 400">: </span><a href="https://forums.percona.com/"><span style="font-weight: 400">forums.percona.com</span></a><span style="font-weight: 400">: share your feedback, ask questions, or report issues</span></li>
</ul>
<h2><a class="anchor-link" id=""></a></h2>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/">Percona Operator for MongoDB 1.23.0: ClusterSync Migration, Vector Search, and PVC Snapshot Backups</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-operator-for-mongodb-1-23-0-clustersync-vector-search-pvc-snapshot-backups/">Percona Operator for MongoDB 1.23.0: ClusterSync Migration, Vector Search, and PVC Snapshot Backups</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>How to Migrate from MySQL Galera Cluster to Percona XtraDB Cluster</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/" />
      <id>https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/</id>
      <updated>2026-07-22T09:56:28+03:00</updated>
      <author><name>Dennis Kittrell</name></author>
      <summary type="html"><![CDATA[<p>On December 1, 2025, MariaDB announced that MySQL Galera Cluster will reach end of life on September 30, 2026. After that date, the MySQL build of Galera stops receiving maintenance and binary releases, and all new clustering features land only in MariaDB Galera Cluster. MariaDB’s recommended path is an in-place migration onto their own server. … Continued<br />
The post How to Migrate from MySQL Galera Cluster to Percona XtraDB Cluster appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/">How to Migrate from MySQL Galera Cluster to Percona XtraDB Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>On December 1, 2025, <a href="https://mariadb.com/resources/blog/upgrade-now-announcing-mysql-galera-cluster-in-place-migration-to-mariadb-galera-cluster/" target="_blank" rel="noopener">MariaDB announced</a> that MySQL Galera Cluster will reach end of life on September 30, 2026. After that date, the MySQL build of Galera stops receiving maintenance and binary releases, and all new clustering features land only in MariaDB Galera Cluster. MariaDB&rsquo;s recommended path is an in-place migration onto their own server.</p>
<p>If you run MySQL Galera Cluster today, that gives you a real decision to make, and not much time to make it. The good news is that you have more than one option, and the one most teams overlook keeps you on MySQL.</p>
<h2>You have two paths, not one<a class="anchor-link" id="you-have-two-paths-not-one"></a></h2>
<p>The deadline forces a move, but it does not force you onto MariaDB. There are two realistic destinations, and the difference between them is larger than it first appears, because one is a database engine change and the other is not.</p>
<div>
<table style="width: 100%;border-collapse: collapse;font-size: 15px;line-height: 1.5;margin: 1em 0">
<thead>
<tr>
<th style="padding: 10px 14px;text-align: left;background: #6c3fd6;color: #ffffff;border: 1px solid #5a33b8"></th>
<th style="padding: 10px 14px;text-align: left;background: #6c3fd6;color: #ffffff;border: 1px solid #5a33b8;font-weight: 600">MariaDB Galera Cluster</th>
<th style="padding: 10px 14px;text-align: left;background: #6c3fd6;color: #ffffff;border: 1px solid #5a33b8;font-weight: 600">Percona XtraDB Cluster (PXC)</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Server</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">MariaDB, a hard fork of MySQL</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Percona Server for MySQL, a drop-in compatible build of MySQL</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Nature of the move</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">Switch to a different database</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Server and distribution change within MySQL ecosystem</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Relationship to MySQL</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">A separate database with its own behavior and dialect</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">The same MySQL you already run, kept compatible</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">What changes when you migrate</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">New system tables, a different data dictionary, user accounts recreated by hand</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Stays within the MySQL family; schema and accounts carry over</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Clustering</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">MariaDB Galera Cluster</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Galera write-set replication on Percona&rsquo;s own open fork</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Ecosystem tooling</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">MariaDB&rsquo;s own backup and monitoring stack</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Percona XtraBackup, Percona Monitoring and Management, Percona Toolkit</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Kubernetes</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">MariaDB&rsquo;s Kubernetes operator</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Percona Operator for MySQL</td>
</tr>
<tr>
<td style="padding: 10px 14px;vertical-align: top;background: #f0eef7;border: 1px solid #e4e4ea;font-weight: 600">Support</td>
<td style="padding: 10px 14px;vertical-align: top;background: #ffffff;border: 1px solid #e4e4ea">MariaDB</td>
<td style="padding: 10px 14px;vertical-align: top;background: #f4f1fd;border: 1px solid #e4e4ea">Percona long-term support, no lock-in</td>
</tr>
</tbody>
</table>
</div>
<p>The pattern holds across every row. MariaDB Galera Cluster moves you to a different database and asks you to rebuild around it, while PXC keeps the database you already have and changes what sits underneath it. That is what makes the move below a distribution change rather than a re-platforming.</p>
<h2>&ldquo;In-place&rdquo; is still a database migration<a class="anchor-link" id="in-place-is-still-a-database-migration"></a></h2>
<p>MariaDB describes its path as near-zero downtime and in place. That is fair for the cluster mechanics, but it understates what is changing underneath. By MariaDB&rsquo;s own migration documentation, moving to MariaDB Galera Cluster means a different system table structure, a fundamentally different data dictionary, and user accounts and privileges that are not mapped one-to-one and must be recreated by hand.</p>
<p>In other words, you are not upgrading MySQL Galera Cluster. You are moving to a different database that also happens to use Galera. For many teams that is a larger project than the &ldquo;in-place&rdquo; label suggests, with application testing, account re-creation, and a new server to operate and support afterward.</p>
<h2>Why PXC is the natural landing spot<a class="anchor-link" id="why-pxc-is-the-natural-landing-spot"></a></h2>
<p>PXC treats this as continuity rather than conversion. It is MySQL, not a fork of it.</p>
<ul>
<li>It is built on Percona Server for MySQL, a drop-in compatible build of MySQL, so your schema, system tables, and user accounts carry over as they are.</li>
<li>Its clustering uses the same Galera write-set replication model you already run, on Percona&rsquo;s own open Galera fork, which we maintain and ship on our own schedule and on terms we control.</li>
<li>It keeps strong binary compatibility with MySQL and Percona Server for MySQL, and integrates with Percona XtraBackup, Percona Monitoring and Management, and both Kubernetes and traditional deployments.</li>
</ul>
<p>For a MySQL Galera Cluster user, that means the move is a server and distribution change inside the MySQL family, not a migration to a new database.</p>
<p>For a fuller version of this argument, see Marco Tusa&rsquo;s personal take: <a href="https://www.tusacentral.net/joomla/index.php/mysql-blogs/268-the-galera-crossroads-why-pxc-is-the-lifeline-for-mariadb-community-users" target="_blank" rel="noopener">The Galera Crossroads: Why PXC is the Lifeline for MariaDB Community Users</a>.</p>
<h2>Why PXC, not just the easier migration<a class="anchor-link" id="why-pxc-not-just-the-easier-migration"></a></h2>
<p>Staying on MySQL is the practical argument. There is also a case for choosing PXC on the merits, independent of how much migration effort each path takes.</p>
<ul>
<li><strong>It is genuinely MySQL, not a relative of it.</strong> PXC is Percona Server for MySQL, a drop-in compatible build of MySQL. MariaDB began as a MySQL fork but has diverged over the years and is no longer a drop-in replacement for MySQL. With PXC, your MySQL knowledge, queries, tooling, and application compatibility carry forward. With MariaDB, some of that has to be revisited.</li>
<li><strong>Open source, with nothing held back.</strong> Percona ships its software, including PXC and our Galera fork, as open source, with no enterprise-only tier gating the features you depend on. MariaDB operates as a commercial vendor, with proprietary and enterprise components alongside the community server. If freedom from lock-in is part of why you run open source databases, that difference matters.</li>
<li><strong>A steward with no competing database to sell you.</strong> This one is worth stating plainly. The company retiring the MySQL build of Galera is the same company recommending you move onto its own database. MariaDB owns Codership, the maintainer of Galera, and has set the end-of-life date while pointing those users to MariaDB Galera Cluster. Percona does not sell a competing database. Our interest is in keeping you successful on MySQL, which is the same interest you have.</li>
<li><strong>A long track record in the MySQL ecosystem.</strong> Percona has maintained MySQL-focused software for years, including Percona Server for MySQL, Percona XtraBackup, Percona Monitoring and Management, and Percona XtraDB Cluster, all under long-term support. Supporting MySQL users is not a new direction for us.</li>
</ul>
<p>Taken together, the question is not only which migration is easier. It is which project is built around keeping MySQL open, compatible, and independent, and which one benefits from MySQL Galera coming to an end.</p>
<h2>What the migration looks like<a class="anchor-link" id="what-the-migration-looks-like"></a></h2>
<p>Because PXC stays within the MySQL ecosystem, the migration is a distribution change rather than a re-platforming exercise, so the work is mostly planning, testing, and a controlled cutover.</p>
<p><strong>Before you start.</strong> Inventory your current cluster: the exact MySQL and Galera versions, node topology, wsrep settings, and any custom configuration. Confirm the PXC version that lines up with your MySQL version, so you are moving across a compatible boundary rather than changing major versions at the same time. Stand up a staging cluster that mirrors production, and capture a baseline backup with Percona XtraBackup before you touch anything.</p>
<p>The right approach depends mainly on how much downtime you can tolerate, ranging from a straightforward binary swap during a maintenance window to a near-online cutover for systems that must stay available. The full, step-by-step guide lives on <a href="http://docs.percona.com" target="_blank" rel="noopener">docs.percona.com</a>, where we keep it current as the tooling improves, and we will link it here once it is published.</p>
<p>Whichever path you take, the same disciplines apply: rehearse the whole thing on staging first, validate application behavior and query performance against PXC before production, and keep a tested rollback (a verified backup or an untouched source cluster) until you are confident. Plan any cutover for a low-traffic window and watch cluster and replication health closely for the first hours afterward.</p>
<h2>Start before the deadline<a class="anchor-link" id="start-before-the-deadline"></a></h2>
<p>September 30, 2026 is when maintenance and binary releases stop for MySQL Galera Cluster. Running an unmaintained cluster past that point means no security patches and no bug fixes, which is not where you want a mission-critical system to sit. The time it takes to test and validate a move now is worth far more than the risk of waiting.</p>
<p>If you are weighing your options, the short version is this: you do not have to leave MySQL to keep a supported, open source Galera cluster. PXC is here, it is maintained, and it is the closest thing to staying exactly where you are.</p>
<h2>Talk to us<a class="anchor-link" id="talk-to-us"></a></h2>
<p>If you want help mapping out a migration path, sizing the work, or pressure-testing your high availability strategy, reach out to your Percona contact, post in the <a href="https://forums.percona.com" target="_blank" rel="noopener">Percona community forums</a>, or connect with our team directly. We are happy to walk through it with you.</p>
<p>&nbsp;</p>
<hr>
<p><em>Written by Dennis Kittrell. Reviewed by Michal Nosek and Marco Tusa.</em></p>
<p><em>MySQL, MariaDB, and Galera Cluster are trademarks of their respective owners. Percona is not affiliated with, sponsored by, or endorsed by these owners.</em></p>
<p>The post <a href="https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/">How to Migrate from MySQL Galera Cluster to Percona XtraDB Cluster</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/migrate-mysql-galera-cluster-to-percona-xtradb-cluster/">How to Migrate from MySQL Galera Cluster to Percona XtraDB Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Deploying the MariaDB Privacy-First Stack Anywhere with Terraform</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/" />
      <id>https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/</id>
      <updated>2026-07-22T09:07:49+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>In my previous post, I introduced the MariaDB Privacy-First Stack.<br />
Nextcloud for collaboration, Passbolt for passwords and secrets, and MariaDB Server for the data. …<br />
Continue reading \"Deploying the MariaDB Privacy-First Stack Anywhere with Terraform\"<br />
The post Deploying the MariaDB Privacy-First Stack Anywhere with Terraform appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/">Deploying the MariaDB Privacy-First Stack Anywhere with Terraform</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In my previous post, I introduced the <a href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/">MariaDB Privacy-First Stack</a>.<br>
<a href="https://nextcloud.com">Nextcloud</a> for collaboration, <a href="https://www.passbolt.com/">Passbolt</a> for passwords and secrets, and MariaDB Server for the data. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Deploying the MariaDB Privacy-First Stack Anywhere with Terraform&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/">Deploying the MariaDB Privacy-First Stack Anywhere with Terraform</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/deploying-the-mariadb-privacy-first-stack-anywhere-with-terraform/">Deploying the MariaDB Privacy-First Stack Anywhere with Terraform</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/" />
      <id>https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/</id>
      <updated>2026-07-21T09:13:23+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Database migrations are rarely only about replacing one database server with another.<br />
In real production systems, especially in fintech, a migration is usually about reducing risk, keeping the application online, improving scalability, and giving teams more room to evolve the architecture without freezing product development. …<br />
Continue reading \"From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO\"<br />
The post From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/">From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Database migrations are rarely only about replacing one database server with another.<br>
In real production systems, especially in fintech, a migration is usually about reducing risk, keeping the application online, improving scalability, and giving teams more room to evolve the architecture without freezing product development. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/">From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/from-postgresql-12-to-mariadb-11-a-gradual-fintech-migration-with-23-lower-tco/">From PostgreSQL 12 to MariaDB 11: A Gradual Fintech Migration with 23% Lower TCO</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/" />
      <id>https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/</id>
      <updated>2026-07-20T10:54:10+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Have you ever modified a MariaDB configuration file, restarted the service, and immediately regretted it?<br />
You wanted to change:<br />
but accidentally wrote:<br />
One missing letter.<br />
That is enough to turn a perfectly healthy database server into a service that refuses to start. …<br />
Continue reading \"MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server\"<br />
The post MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/">MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Have you ever modified a MariaDB configuration file, restarted the service, and immediately regretted it?<br>
You wanted to change:<br>
but accidentally wrote:<br>
One missing letter.<br>
That is enough to turn a perfectly healthy database server into a service that refuses to start. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/">MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-validate-your-configuration-before-starting-the-server/">MariaDB 13.1 Feature in Focus: Validate Your Configuration Before Starting the Server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Using AI to modernize a Java project</title>
      <link rel="alternate" type="text/html" href="https://programmingbrain.com/2025/07/how-i-used-ai-coding-agents-to-modernize-a-java-library.html" />
      <id>https://programmingbrain.com/2025/07/how-i-used-ai-coding-agents-to-modernize-a-java-library.html</id>
      <updated>2026-07-20T08:05:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>How I used AI coding agents to modernize a Java library.</p>
<p><a href="https://programmingbrain.com/2025/07/how-i-used-ai-coding-agents-to-modernize-a-java-library.html">Using AI to modernize a Java project</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>How I used AI coding agents to modernize a Java library.</p>

<p><a href="https://programmingbrain.com/2025/07/how-i-used-ai-coding-agents-to-modernize-a-java-library.html">Using AI to modernize a Java project</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>TDE performance in PostgreSQL</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/20/tde-performance-in-postgresql/" />
      <id>https://percona.community/blog/2026/07/20/tde-performance-in-postgresql/</id>
      <updated>2026-07-20T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>What’s the impact of TDE on performance? People usually quickly throw together a few graphs with basic measurements and treat that as a complete answer, but the question is a bit more complex than that. In this blog post, I’ll try to explain it in a bit more detail: why showcasing a single graph isn’t good for anything other than marketing.</p>
<p><a href="https://percona.community/blog/2026/07/20/tde-performance-in-postgresql/">TDE performance in PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>What&rsquo;s the impact of TDE on performance?<br>
People usually quickly throw together a few graphs with basic measurements and treat that as a complete answer, but the question is a bit more complex than that.<br>
In this blog post, I&rsquo;ll try to explain it in a bit more detail: why showcasing a single graph isn&rsquo;t good for anything other than marketing.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/07/pg_tde_superfast.png" alt="It&rsquo;s SUPER FAST!"></figure>
</p>
<h2 id="agenda">Agenda<a class="anchor-link" id="agenda"></a></h2>
<p>Let me start by making something clear: this is a complex topic, and this will be a long blog post.</p>
<p>I decided to simplify several things to the level where it is accurate enough, but still relatively easy to understand.<br>
I also won&rsquo;t go into implementation details, the maths behind statistics, and I won&rsquo;t focus on generic topics like how to do low-noise benchmarking.</p>
<p>I am already planning on writing separate blog posts about some of these details in the future, but if you are interested in some of them, don&rsquo;t hesitate to ask!<br>
Feedback like that helps me figure out what topic to cover next.</p>
<p>As for this blog post, I want to cover the following topics:</p>
<ul>
<li>A generic introduction about the cost of encryption on modern CPUs</li>
<li>A short description of how transparent data-at-rest encryption typically integrates into PostgreSQL</li>
<li>An explanation of why relation encryption (encrypting the database objects) typically has no cost at all in most workloads, except a few specific operations</li>
<li>And finally showcasing how the typical way of implementing WAL encryption in TDE solutions can cause performance degradation in high WAL-churn scenarios</li>
</ul>
<h2 id="test-setup">Test setup<a class="anchor-link" id="test-setup"></a></h2>
<p>Benchmarks heavily depend on the computer used for running them.<br>
All of my tests were performed on an AMD Threadripper 3970X (32 cores), using an Intel Optane P5800X SSD.</p>
<p>While some of the measurements can be reproduced on typical desktop hardware, not all of them can.<br>
Some tests require many parallel workers and high memory bandwidth.<br>
The more interesting tests all measure write performance, which is difficult on typical M.2 SSDs:<br>
while some of them have peak write performance similar to Optane disks, they can only keep up with high write speeds for short bursts, quickly turning an otherwise CPU or memory limited test into an IO limited one.</p>
<h2 id="encryption-is-cheap-in-isolation">Encryption is cheap&hellip; in isolation<a class="anchor-link" id="encryption-is-cheap-in-isolation"></a></h2>
<p>When talking about encryption performance, technical people usually make one of two assumptions:</p>
<ul>
<li>Encryption is complex math we have to compute, so of course it will degrade our performance!</li>
<li>We are using things like full filesystem encryption (BitLocker, LUKS), swap encryption or TLS all the time, it&rsquo;s barely noticeable &ndash; encryption is cheap!</li>
</ul>
<p>And both groups are kind of right:<br>
Encryption is complex math, and it will degrade performance on older hardware.<br>
But because it is so common and required for everything, modern hardware has a specialized instruction set (AES-NI) that highly optimizes it for most workloads.</p>
<p>The following table shows some single threaded measurements on my test computer, produced with small C benchmarks:</p>
<table>
<thead>
<tr>
<th>What</th>
<th>Bandwidth (GB/s)</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Memory read</td>
<td>23</td>
<td>How quickly can we read memory</td>
</tr>
<tr>
<td>Memory copy</td>
<td>11</td>
<td>How quickly can we copy data in memory from one place to another</td>
</tr>
<tr>
<td>Disk sequential read</td>
<td>7.4</td>
<td>How quickly can we read from disk</td>
</tr>
<tr>
<td>Disk sequential write</td>
<td>6.1</td>
<td>How quickly can we write to disk</td>
</tr>
<tr>
<td>AES-128-CTR</td>
<td>8.0</td>
<td>How quickly can we perform 128 bit AES CTR operations</td>
</tr>
<tr>
<td>AES-256-CTR</td>
<td>6.5</td>
<td>How quickly can we perform 256 bit AES CTR operations</td>
</tr>
<tr>
<td>AES-128-XTS</td>
<td>7.3</td>
<td>How quickly can we perform 128 bit AES XTS operations</td>
</tr>
<tr>
<td>AES-256-XTS</td>
<td>5.6</td>
<td>How quickly can we perform 256 bit AES XTS operations</td>
</tr>
<tr>
<td>AES-128-GCM</td>
<td>3.8</td>
<td>How quickly can we perform 128 bit AES GCM operations</td>
</tr>
<tr>
<td>AES-256-GCM</td>
<td>3.6</td>
<td>How quickly can we perform 256 bit AES GCM operations</td>
</tr>
</tbody>
</table>
<p>The three encryption modes mentioned above are commonly used in many scenarios:</p>
<ul>
<li>XTS is commonly used for full disk encryption, and also by some TDE implementations</li>
<li>CTR is commonly used by TDE implementations, especially for WAL encryption</li>
<li>GCM is an authenticated encryption algorithm which can provide both encryption and data integrity validation for TDE and other solutions</li>
</ul>
<p>While the table doesn&rsquo;t mention it, I also want to point out that all reads and writes are sequential using a 4kB block size.<br>
This is important, because the performance of all of them degrades if we start using them differently: if we start encrypting much smaller blocks at a time, or keep reinitializing the stream with different parameters, the encryption numbers can degrade quickly.</p>
<p>We also have to remember that our goal isn&rsquo;t to encrypt a random stream in memory &ndash; we have to integrate encryption into an existing database system with its own established architecture.<br>
The above numbers showcase our bandwidth to encrypt or decrypt data if a CPU core is only working on that task.<br>
In reality, the CPU will also be doing other things at the same time, and can&rsquo;t spend all the time on encryption.</p>
<p>However, this won&rsquo;t necessarily make things worse.<br>
Most database workloads are not CPU bound, the bottleneck is usually either disk or memory bandwidth.<br>
With AES-NI, encryption operations often don&rsquo;t require additional memory bandwidth, which means that if a workload is already disk or memory limited, but we have free CPU cycles, we might get encryption for free or at little cost.</p>
<p>The real question isn&rsquo;t how quick encryption is, but how optimally we can integrate it into PostgreSQL.</p>
<h2 id="what-are-we-encrypting-exactly">What are we encrypting exactly?<a class="anchor-link" id="what-are-we-encrypting-exactly"></a></h2>
<p>That means we no longer have a single question.<br>
I can&rsquo;t give a single answer to <em>how fast is pg_tde, or any other data-at-rest encryption implementation?</em></p>
<p>Because a database does many things, reads and writes many different file types, and each of those has to be implemented differently.</p>
<p>In the case of pg_tde, we have two main areas:</p>
<ul>
<li>the encryption of database (relation) files</li>
<li>and the encryption of the write ahead log</li>
</ul>
<p>Other TDE implementations might encrypt other files too, for example temporary files, but I want to focus on the two areas supported by pg_tde, as these are the most significant from a performance perspective.</p>
<p>So let&rsquo;s look into the details of these separately.</p>
<h2 id="the-relation-files">The relation files<a class="anchor-link" id="the-relation-files"></a></h2>
<p>The quick summary for those only interested in the numbers:<br>
the impact for this is very little &ndash; for most operations, in the very difficult to measure category.<br>
The only exception to this is a few single threaded write heavy workloads, such as <code>CREATE TABLE AS SELECT ...</code>, <code>VACUUM FULL</code>, an <code>UPDATE</code> that updates all or most rows, or an <code>ALTER TABLE</code> that rewrites the entire table. In these, we can measure a 5-30% performance drop.</p>
<p>For other operations, such as typical sysbench workloads, or even specific tests like single worker sequential reads, that number is 2% or less.<br>
Typical measurements usually have some noise, a few percent even for properly configured setups, and much more for <em>&ldquo;let&rsquo;s just quickly execute sysbench&rdquo;</em>.<br>
Something in the 1-2% range can only be measured with specific server and hardware configuration, not in real-world setups.</p>
<p>The reason behind this is quite simple:<br>
Relation file reads and writes usually happen in entire blocks, with an 8k default size for PostgreSQL &ndash; it is very similar to OS level file system encryption.</p>
<p>A typical data-at-rest encryption implementation usually operates at the IO level: it encrypts immediately before we write a dirty buffer to disk, and it decrypts immediately after we read something from disk.</p>
<p>Decryption only happens when we actually have to read from disk. If the requested pages are already in the shared buffers, they are already decrypted, so there&rsquo;s no effect there.<br>
Encryption only happens when we are writing to disk. In a workload that isn&rsquo;t very write-heavy, all writes happen in the checkpointer and background writer, and backend processes never perform page writes directly, so encryption won&rsquo;t affect the QPS numbers at all.<br>
Even if a workload is so write-heavy that the server has to move some of the writes to the backend process, most likely the backend isn&rsquo;t CPU-bound, since most database workloads are either memory or IO limited.<br>
Unless we are writing a huge volume of data, such as the examples mentioned above, encryption won&rsquo;t be noticeable at all in these scenarios.</p>
<p>This read-write behavior is similar to full file system encryption, where the OS caches behave similarly, unless we keep calling <code>fsync</code> explicitly for writes.</p>
<p>Because the effect of encryption is so little, we have to be really careful how we set up our test environment, even for the heavy write scenarios where we can notice a somewhat larger difference.<br>
It&rsquo;s very easy to get this wrong, and then just measure noise.</p>
<p>For example, let&rsquo;s say that we don&rsquo;t want to spend too much time on initializing the dataset, and we only generate a 20GB dataset.<br>
We also set the shared buffers to 16GB because our test PC has more than enough RAM.<br>
This seems reasonable, but the problem is, once the data is loaded into the shared buffers, it will be permanently decrypted there, and 80% of our dataset fits into the buffers.<br>
Most read operations won&rsquo;t have to go to the disk, they find the requested page in the buffers, so we are not measuring encryption performance for them.</p>
<p>We might decide to keep the small dataset, and just use a small number for shared buffers, but then we are moving away from a production-like setup: is it realistic to run PostgreSQL with 128MB shared buffers in a real environment?<br>
Can the reduced shared buffer size affect the performance in some other ways?</p>
<p>And then we have to think about similar questions for writes.</p>
<p>Regardless of what we do, as I mentioned above, this is basically the same use case as file system encryption, and we already know that that&rsquo;s fast.<br>
If we are testing an encryption implementation, and we can see a significant performance degradation with only data file encryption, we found a bug, and should report it.</p>
<h3 id="why-is-ctas-so-slow">Why is CTAS so slow?<a class="anchor-link" id="why-is-ctas-so-slow"></a></h3>
<p>At the beginning of the previous section, I mentioned a few examples where we <em>can</em> measure a difference.</p>
<p>In my tests, I see the following worst-case performance degradation with them:</p>
<table>
<thead>
<tr>
<th>Command</th>
<th>Encryption Overhead</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>CREATE TABLE AS SELECT</code></td>
<td>30%</td>
</tr>
<tr>
<td><code>ALTER TABLE</code> performing a full rewrite</td>
<td>15%</td>
</tr>
<tr>
<td><code>VACUUM FULL</code></td>
<td>10%</td>
</tr>
<tr>
<td><code>UPDATE</code> all rows</td>
<td>10%</td>
</tr>
</tbody>
</table>
<p>These are all <em>worst case</em> numbers I was able to produce. This doesn&rsquo;t mean that all full table UPDATE operations will take 10% longer.</p>
<p>Also, in the case of <code>UPDATE</code>, the 10% measurement includes the time of the <code>CHECKPOINT</code>, as depending on the exact server configuration, some or most of the writes still happen in the checkpointer and background writer.<br>
The actual execution time of the query in the backend process was the same with and without encryption in all of my <code>UPDATE</code> measurements, unless I intentionally misconfigure the server.</p>
<p>A CTAS that simply copies a table is the worst performer because it doesn&rsquo;t have to do anything else.<br>
The other operations all have to do something extra with the data, but if we just duplicate one database table, then we normally only perform IO:<br>
read a page, write the page, repeat.</p>
<p>Except in our case, we have to read a page, decrypt the page, encrypt the page, write the page.<br>
We have to call encryption operations twice for every page: with AES-128-CTR at 8 GB/s, decrypting and then encrypting each page halves the effective bandwidth to around 4 GB/s, well below the disk speeds in my test setup.<br>
A CTAS operation is normally IO limited, but because of this doubled encryption work, it becomes encryption limited with TDE, even with the AES-NI instruction set.</p>
<p>I also want to explicitly repeat an important note from the beginning:<br>
this result requires a sustained disk IO that is faster than the CPU&rsquo;s encryption speed.<br>
Consumer grade SSDs can&rsquo;t sustain such speeds for long workloads, only for short bursts.<br>
It is only possible to reproduce this measurement on them with small datasets.<br>
With larger tables, the test will be limited by disk IO, not encryption.</p>
<h2 id="the-write-ahead-log">The write ahead log<a class="anchor-link" id="the-write-ahead-log"></a></h2>
<p>This is where things get more interesting.<br>
In my <a href="https://percona.community/blog/2026/07/08/pg_tde-our-fork-is-temporary-our-commitment-to-open-tde-is-not/">previous tde related blog post</a>, I mentioned that we are working on some encryption benchmarking, and that was about WAL performance.<br>
Also, one of the main changes in the upcoming pg_tde 2.2.3 release will be a significant improvement in this area.</p>
<p>To start with a similar summary:</p>
<ul>
<li>in my most-performant benchmark prototype, I can&rsquo;t measure a significant degradation even in a <strong>worst-case scenario workload</strong></li>
<li>with pg_tde before 2.2.3, the same test results in only 60% throughput, or a 40%+ performance degradation</li>
<li>with pg_tde 2.2.3, we were able to improve that test to the 80% range, reducing the performance degradation to 20%</li>
</ul>
<p>The 2.2.3 change (already merged, the release is upcoming) improves our current WAL encryption approach. The complete fix described later in this post is still in development.</p>
<p>This list also needs two important footnotes:</p>
<p>First, this is a &ldquo;worst-case scenario&rdquo;, not something typically executed in production workloads.<br>
It is a synthetic scenario exactly to stress test WAL bottlenecks.<br>
In a simple OLTP read-write scenario, there&rsquo;s only a few percent measurable difference for all pg_tde versions, less than 10%.</p>
<p>Second, this is a concurrency issue, and reproducing it requires a high core count test PC.<br>
In my test setup, I have 32 cores available.<br>
On different hardware, the results will be different.<br>
With 100+ core monster hardware, and even more threads, the performance degradation is most likely even worse for most implementations of WAL encryption, but I didn&rsquo;t perform tests on such setups.</p>
<h3 id="understanding-the-issue">Understanding the issue<a class="anchor-link" id="understanding-the-issue"></a></h3>
<p>To understand why this is so different from data file encryption, we have to look into how WAL works:</p>
<ol>
<li>When we perform any WAL logged write, the changes are first written to WAL, that&rsquo;s why it&rsquo;s called a write <strong>ahead</strong> log.</li>
<li>Even more specifically, the backend process (the server&rsquo;s handler for the specific client session) first constructs what it wants to write into the log</li>
<li>After that, in PostgreSQL we only have a single WAL log, and only one backend can flush it at a time.<br>
The backend process has to request a lock on the WAL, preventing other processes from interacting with it at the same time.</li>
<li>After writing the already constructed data to the in-memory buffer, we have to write it to disk and then immediately flush the buffer to disk, to make sure that it is durable, since this is the log we are using for crash recovery.</li>
<li>It can release that lock only after that write/flush was done.</li>
<li>After releasing the lock, another backend can take it, repeating the process from (3)</li>
</ol>
<pre class="mermaid">
sequenceDiagram
participant B1 as Backend 1
participant B2 as Backend 2
participant L as Single WAL lock
B1-&gt;&gt;B1: construct WAL record
B2-&gt;&gt;B2: construct WAL record
B1-&gt;&gt;L: acquire
Note over B1,L: encrypt + write + flush<br>(everyone else waits)
B2--xL: blocked
L--&gt;&gt;B1: release
B2-&gt;&gt;L: acquire
Note over B2,L: encrypt + write + flush
L--&gt;&gt;B2: release
</pre>
<p>The above information is a bit oversimplified, as WAL writes are more complex than this, but it is already enough to spot the concurrency issue hidden in it:<br>
only one process can write/flush the WAL at a time, so no matter how many cores we have, it won&rsquo;t get faster.<br>
In fact it is the opposite, since higher core count CPUs usually have worse single-thread performance.</p>
<p>Most WAL encryption implementations, including pg_tde, implement it similarly to data file encryption:<br>
we encrypt the WAL data immediately before writing it to disk, at the time when the backend already acquired the WAL lock.<br>
Not only do we do additional computations for the current session, we also prevent other sessions from doing anything during this extra time.</p>
<p>In my test setup with the above numbers, WAL writes were already CPU bound without encryption.<br>
Even if encryption is relatively cheap, if we don&rsquo;t have spare cycles, it will show up.<br>
That&rsquo;s bad enough already with a good implementation, but if we also manage to introduce a performance-hurting bug in this area of the code, we can easily end up with quite bad numbers.</p>
<h3 id="is-this-completely-fixable">Is this completely fixable?<a class="anchor-link" id="is-this-completely-fixable"></a></h3>
<p>As I hinted at the beginning of the WAL section, this issue is fixable.<br>
WAL encryption doesn&rsquo;t inherently require encrypting more data than data file encryption does. In fact, if done correctly, most of the time we&rsquo;ll have to encrypt even less.</p>
<p>The problem is that we have to do it in a more challenging part of the code.</p>
<p>From the above description, it might already be clear:<br>
we should encrypt the data after we constructed it, before taking the WAL lock!</p>
<p><strong>Current: encrypt inside the lock</strong></p>
<pre class="mermaid">
flowchart LR
A[construct record] --&gt; B[acquire lock]
subgraph lock [lock held, serialized]
direction LR
C[encrypt] --&gt; D[write + flush]
end
B --&gt; C
D --&gt; E[release lock]
</pre>
<p><strong>Fixed: encrypt before the lock</strong></p>
<pre class="mermaid">
flowchart LR
A[construct record] --&gt; C[encrypt]
C --&gt; B[acquire lock]
subgraph lock [lock held, serialized and shorter]
direction LR
D[write + flush]
end
B --&gt; D
D --&gt; E[release lock]
</pre>
<p>That solves both the concurrency limitation and another problem hidden there:<br>
imagine that we are writing many small records, one per transaction.<br>
A single WAL record might be less than 100 bytes, but a WAL page is 8kB.</p>
<p>Even if we do it at flush time, we don&rsquo;t have to encrypt the entire page, only what we filled so far, but on average even that results in 4kB data per flush.<br>
With a 100-byte WAL record, we can fit around 80 records into a single WAL page.<br>
We have written 8kB of real data, and encrypted 320kB of WAL to do it.<br>
There are of course some possible optimizations there, for example we completely ignored group commit, which will likely make that 320kB number much smaller, but it still remains significantly more.</p>
<p>While moving the encryption before the lock might seem like an easier choice, it has different challenges.<br>
For example, there&rsquo;s one I already mentioned in the beginning: if we start encrypting small blocks (and also changing the stream configuration, which is also related to this), we get worse encryption performance.</p>
<p>The naive implementation of this approach performs even worse than our earlier pg_tde implementation.<br>
However, if we apply some optimizations to it, it can reach similar &ldquo;unmeasurable&rdquo; levels as the data page encryption.</p>
<p>This improvement is not yet included in pg_tde, as it requires a completely different approach to WAL encryption compared to our previous implementations, and we haven&rsquo;t yet finished testing and measuring it.<br>
Mainly, in this blog post I only focused on the performance of <em>writing</em> the WAL, but we have to remember that we also have to <em>read</em> it in some cases.<br>
While the speed of crash recovery isn&rsquo;t that important in a happy scenario, when something bad happens it does matter how quickly we can get our server running again.</p>
<h3 id="whats-the-test-scenario">What&rsquo;s the test scenario?<a class="anchor-link" id="whats-the-test-scenario"></a></h3>
<p>Similarly to the write bottlenecks above, a normal OLTP read-write workload won&rsquo;t showcase significant degradation because of WAL encryption.<br>
This is exactly why I wrote such a long post: the interesting behavior only shows up in scenarios a quick benchmark never exercises.</p>
<p>We could simply publish a nice graph showing the typical numbers, claiming that our encryption is fast.<br>
We could treat the performance problem we fixed as a small footnote in our changelog, as it doesn&rsquo;t show up at all in that measurement.</p>
<p>But that wouldn&rsquo;t be honest, because the test setup and test scenarios do matter, and when we talk about performance, we should mention worst-case numbers.</p>
<p>To reach these bad numbers, we have to do one thing:<br>
generate a huge amount of WAL across many sessions.<br>
This is doable in many different ways, ours is basically the following:</p>
<ol>
<li>create a few tables, each with a few million rows</li>
<li>start executing <code>UPDATE t SET c=c+1 WHERE id IN (SELECT id FROM t ORDER BY random() LIMIT 100);</code></li>
<li>run step 2 in many sessions</li>
</ol>
<p>The query above is a simplified illustration. The actual test used sequential IDs, with the 100 random IDs generated by the benchmark tool and inserted directly into a prepared statement, so selecting the rows stays cheap and the workload is dominated by the WAL writes.</p>
<p>PostgreSQL has an option called <a href="https://wiki.postgresql.org/wiki/Full_page_writes" target="_blank" rel="noopener noreferrer"><code>full_page_writes</code></a>, which is enabled by default.<br>
With it, when we modify any database page for the first time after a checkpoint, it doesn&rsquo;t only WAL log the modified row, instead it logs the entire page, 8kB.<br>
In the above query, for every update, we modify 100 randomly selected rows in a table.<br>
Because they are randomly selected from millions of rows, they have a good chance of being on different pages.<br>
Let&rsquo;s say that with a specific dataset, on average we hit a 10% new page rate &ndash; one update has to do 10 full page writes.<br>
That means every single UPDATE we execute generates more than 80kB of WAL.</p>
<p>This is again an oversimplification, but it is a good enough approximation without going into details too much.</p>
<p>With 15,000 QPS, that&rsquo;s around 1.2 GB/s of WAL.<br>
From the table at the beginning, on my test PC AES-128-CTR has around 8 GB/s single core bandwidth.<br>
1.2 GB/s is 15% of that &ndash; and that assumes no context switching or other losses, meaning no matter how good the integration into the database is, if we do encryption at this volume while holding a global lock, we have to expect at least 15% performance drop with these numbers.<br>
In practice it&rsquo;s a bit more than that.</p>
<h2 id="summary">Summary<a class="anchor-link" id="summary"></a></h2>
<p>I hope this explanation was more useful than a single graph showcasing how good our performance with pg_tde is.<br>
The worst-case scenarios I described here are corner cases, most servers don&rsquo;t generate sustained gigabytes per second of WAL, even in production.</p>
<p>But I still think this is important to mention, as it is an easily overlooked detail caused by the combination of PostgreSQL&rsquo;s WAL architecture and the simple way most vendors add encryption to it.</p>
<p>I also didn&rsquo;t go into detail on many parts of this post.<br>
If I tried to explain everything in absolute detail, this would be ten times longer, and much harder to follow and understand.<br>
I do plan to touch on some of these subjects in separate blog posts, where we can focus only on those topics in more depth.<br>
If you have questions or suggestions about the topic, don&rsquo;t hesitate to reach out using our <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">community forums</a>!</p>

<p><a href="https://percona.community/blog/2026/07/20/tde-performance-in-postgresql/">TDE performance in PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ClickHouse Schema Design and Data Modeling</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/clickhouse-schema-design-and-data-modeling/" />
      <id>https://severalnines.com/blog/clickhouse-schema-design-and-data-modeling/</id>
      <updated>2026-07-17T12:48:53+03:00</updated>
      <author><name>Agus Syafaat</name></author>
      <summary type="html"><![CDATA[<p>Sometimes, we see ClickHouse queries that should normally complete in milliseconds take several seconds to finish or worse, time out entirely. When that happens, there is a good chance that the schema is the real culprit. The problem is often not the query itself, nor is it a hardware bottleneck. Instead, it can stem from […]<br />
The post ClickHouse Schema Design and Data Modeling appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/clickhouse-schema-design-and-data-modeling/">ClickHouse Schema Design and Data Modeling</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Sometimes, we see ClickHouse queries that should normally complete in milliseconds take several seconds to finish or worse, time out entirely. When that happens, there is a good chance that the schema is the real culprit. The problem is often not the query itself, nor is it a hardware bottleneck. Instead, it can stem from a schema design decision made months ago that nobody questioned at the time.</p>
<p>Schema design in ClickHouse is one of those tasks that appears to be a simple, one-time setup activity, but it often becomes a recurring operational concern. When the schema is designed poorly, problems tend to surface over time, including slow queries, oversized partitions, and mutation jobs that run for hours while production traffic continues to grow.&nbsp;</p>
<p>In this article, we will cover the core concepts of ClickHouse, systematically walk through design decisions, from core concepts and operational patterns to monitoring and evolution, with the goal of giving you a framework for making and maintaining schema decisions in production.</p>
<h2 class="wp-block-heading" id="h-core-concepts-for-clickhouse-schema-design">Core Concepts for ClickHouse Schema Design<a class="anchor-link" id="core-concepts-for-clickhouse-schema-design"></a></h2>
<h3 class="wp-block-heading" id="h-distributed-tables-local-tables-shards-and-replicas">Distributed Tables, Local Tables, Shards, and Replicas<a class="anchor-link" id="distributed-tables-local-tables-shards-and-replicas"></a></h3>
<p>Before writing a single CREATE TABLE, it helps to have a clear mental model of how ClickHouse actually stores and serves data across a cluster. ClickHouse divides data across shards with each shard holding a horizontal slice of the total dataset. Each shard can have one or more replicas for fault tolerance. The replicas within a shard hold identical data; the shards themselves hold different data.</p>
<p>The two table types you&rsquo;ll work with constantly are:</p>
<ul class="wp-block-list">
<li>Local tables (<code>ReplicatedMergeTree</code> and its variants): the actual storage layer. Each node stores its own local table containing its shard&rsquo;s data. Queries against a local table only see that node&rsquo;s data.</li>
<li>Distributed tables (<code>Distributed</code> engine): a logical routing layer that sits on top of the local tables. When you query a distributed table, ClickHouse fans the query out to all shards, collects the results, and merges them. Distributed tables don&rsquo;t store data themselves.</li>
</ul>
<p><strong>N.B. schema changes need to be applied to local tables on every node, and the distributed table definition needs to match. It sounds obvious, but it is a common source of confusion when onboarding teams who are used to a single-server database.</strong></p>
<p>Shard key selection matters for data distribution. A poorly chosen shard key (or <code>rand()</code> used as a lazy default) can lead to uneven data distribution, e.g. one shard holding 60% of the data while others hold 20% each &mdash; this creates hot spots and makes capacity planning unreliable. The shard key should distribute data evenly and, ideally, align with how you query, if most queries filter by <code>tenant_id</code>, sharding by <code>tenant_id</code> means queries for a single tenant hit one shard instead of all of them.</p>
<h3 class="wp-block-heading" id="h-partition-key-and-primary-key-sparse-index">Partition Key and Primary Key (Sparse Index)<a class="anchor-link" id="partition-key-and-primary-key-sparse-index"></a></h3>
<p>These two concepts trip up almost everyone coming from a relational background, because they sound like the same thing but serve entirely different purposes in ClickHouse. <strong>The partition key</strong> controls how data is physically divided into separate directories on disk. Each partition is stored and managed independently, which means:</p>
<ul class="wp-block-list">
<li>Queries that filter on the partition key can skip entire partitions without reading them (partition pruning)</li>
<li>Old data can be dropped by dropping a partition, instant, no heavy delete operation</li>
<li>Background merges only happen within a partition, not across them</li>
</ul>
<p>For time-series data, partitioning by month (<code>toYYYYMM(event_time)</code>) is the most common pattern. It gives you clean data lifecycle management (drop old months instantly) and good pruning behavior for time-bounded queries.</p>
<p><strong>The primary key</strong> in ClickHouse is not a uniqueness constraint, it&rsquo;s a sparse index. ClickHouse stores one index entry per 8192 rows (one granule), not one per row. This makes it memory-efficient even at billions of rows, but it means the primary key is designed for range scans and filtering, not point lookups.</p>
<p>The <code>ORDER BY</code> clause defines the physical sort order of data on disk, and the primary key must be a prefix of <code>ORDER BY</code>. This is worth saying clearly: the sort order is what makes your queries fast or slow. If your most common query filters by <code>(tenant_id, event_type, event_time)</code>, your <code>ORDER BY</code> should reflect that. Data is stored sorted by those columns, so ClickHouse can skip irrelevant granules efficiently.</p>
<p>Here&rsquo;s a concrete example that puts these together:</p>
<pre class="wp-block-code"><code>CREATE TABLE events_local
(
    tenant_id     UInt32,
    event_time    DateTime,
    event_type    LowCardinality(String),
    user_id       UInt64,
    session_id    UUID,
    properties    String,
    ingested_at   DateTime DEFAULT now()
)
ENGINE = ReplicatedMergeTree(
    '/clickhouse/tables/{shard}/events',
    '{replica}'
)
PARTITION BY toYYYYMM(event_time)
ORDER BY (tenant_id, event_type, event_time)
SETTINGS index_granularity = 8192;</code></pre>
<p>A few decisions that can be taken as below:</p>
<ul class="wp-block-list">
<li><code>LowCardinality(String)</code> for <code>event_type</code>, if this column has fewer than 10,000 distinct values, this encoding dramatically reduces storage and speeds up filtering.</li>
<li><code>PARTITION BY toYYYYMM(event_time)</code>, monthly partitions, suitable for a 12&ndash;18 month hot data retention window.</li>
<li><code>ORDER BY (tenant_id, event_type, event_time)</code>, optimized for queries that filter by tenant first, then by event type, then narrow by time range.</li>
<li>The ZooKeeper path uses <code>{shard}</code> and <code>{replica}</code> macros so the same DDL can be run on every node without modification.</li>
</ul>
<h3 class="wp-block-heading">Materialized Views and Aggregated Tables<a class="anchor-link" id="materialized-views-and-aggregated-tables"></a></h3>
<p>Materialized views in ClickHouse are not the same as in PostgreSQL. They are real-time incremental aggregations; every time data is inserted into the source table, the materialized view processes those rows and writes the aggregated result to a target table. There&rsquo;s no scheduled refresh and it happens synchronously with the insert.</p>
<p>This makes them powerful for pre-computing aggregations that would otherwise require scanning billions of rows at query time. A common pattern is to maintain hourly or daily rollup tables alongside the raw events table.&nbsp;</p>
<p>For example, create the target table for aggregated counts and later create the <code>MATERIALIZED VIEW</code> with aggregation.</p>
<pre class="wp-block-code"><code>CREATE TABLE events_hourly_agg
(
    tenant_id    UInt32,
    event_type   LowCardinality(String),
    hour         DateTime,
    event_count  AggregateFunction(count, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (tenant_id, event_type, hour);



CREATE MATERIALIZED VIEW events_to_hourly
TO events_hourly_agg
AS
SELECT
    tenant_id,
    event_type,
    toStartOfHour(event_time) AS hour,
    countState() AS event_count
FROM events_local
GROUP BY tenant_id, event_type, hour;</code></pre>
<p>Operationally, materialized views add write amplification, every insert into the source table triggers a write to the view&rsquo;s target table. For high-ingestion workloads, this is worth monitoring. They also need to be maintained when the source schema changes, which is often forgotten until something breaks.</p>
<h2 class="wp-block-heading">Operational Design Patterns<a class="anchor-link" id="operational-design-patterns"></a></h2>
<h3 class="wp-block-heading">Time-Series and Event Analytics Schemas<a class="anchor-link" id="time-series-and-event-analytics-schemas"></a></h3>
<p>The vast majority of ClickHouse deployments are built around time-series or event data clickstreams, application logs, metrics, IoT sensor readings. This is where ClickHouse&rsquo;s design shines, and there are well-established patterns to follow.</p>
<p>The core principle is time as the primary organizing dimension. Partition by time (monthly or weekly depending on data volume), and include <code>event_time</code> in the <code>ORDER BY</code> so range scans are efficient. Keep raw events immutable and resist the temptation to update them in place.</p>
<p>For retention management, the TTL clause handles automatic expiry without manual intervention:</p>
<pre class="wp-block-code"><code>TTL event_time + INTERVAL 90 DAY DELETE

TTL event_time + INTERVAL 30 DAY TO DISK 'cold_storage'</code></pre>
<p>The first script automatically deletes rows older than 90 days while the second scripts move cold data to a cheaper storage tier. This is operationally cleaner than scheduled delete jobs, which in ClickHouse would trigger heavy mutations.</p>
<h2 class="wp-block-heading">Bulk Ingestion vs. Real-Time Streaming<a class="anchor-link" id="bulk-ingestion-vs-real-time-streaming"></a></h2>
<p>How data arrives significantly affects schema and operational behavior. ClickHouse handles both, but they stress the system differently.</p>
<p>Bulk ingestion, i.e. large batch inserts; for example, nightly ETL from a data warehouse, is relatively forgiving. ClickHouse is designed for large INSERT batches, each batch creates one or a few data parts, and the background merge process handles compaction.&nbsp;</p>
<p>The risk is inserting too many small batches in rapid succession, which creates a flood of tiny parts that overwhelm the merge queue. The rule of thumb is: batch size matters more than frequency. Aim for inserts of at least 10,000 &ndash;100,000 rows per batch.</p>
<p>Real-time streaming via Kafka requires more care. The ClickHouse Kafka table engine or tools like Vector/Benthos handle ingestion, but the operational concern is the same: small, frequent inserts create merge pressure. Configure consumers to buffer and batch messages before inserting, and monitor <code>system.parts</code> for signs of part accumulation.</p>
<pre class="wp-block-code"><code>SELECT
    table,
    count() AS part_count,
    sum(rows) AS total_rows,
    formatReadableSize(sum(bytes_on_disk)) AS disk_size
FROM system.parts
WHERE active = 1
GROUP BY table
ORDER BY part_count DESC;</code></pre>
<p>A healthy table has tens to low hundreds of active parts. Thousands of parts is a warning sign that inserts are too small or merges are falling behind.</p>
<h3 class="wp-block-heading">Multi-Tenant Schema Isolation<a class="anchor-link" id="multi-tenant-schema-isolation"></a></h3>
<p>If your ClickHouse cluster serves multiple tenants, you need to decide early how to isolate their data. The main options are:</p>
<ul class="wp-block-list">
<li>Database-per-tenant: each tenant gets their own database (and potentially their own set of tables). Clean isolation, simple access control, but doesn&rsquo;t scale past a few dozen tenants without becoming a management burden.</li>
<li>Table-per-tenant: all tenants share a database, each with their own table. Works at moderate scale but schema changes need to be applied to every tenant table, which is operationally painful at hundreds of tenants.</li>
<li>Shared table with <code>tenant_id</code> column: all tenant data in one table, filtered by <code>tenant_id</code>. This is the most operationally maintainable pattern at scale. The key requirement is that <code>tenant_id</code> must be the leading column in <code>ORDER BY</code> so that per-tenant queries efficiently skip irrelevant data without a full scan.</li>
</ul>
<pre class="wp-block-code"><code>ORDER BY (tenant_id, event_type, event_time)</code></pre>
<p>With this sort order, a query filtering on <code>tenant_id = 42</code> skips all granules that don&rsquo;t contain that tenant&rsquo;s data, making it effectively as fast as if the table contained only that tenant&rsquo;s rows.</p>
<h2 class="wp-block-heading">Schema Evolution and Operational Impact<a class="anchor-link" id="schema-evolution-and-operational-impact"></a></h2>
<h3 class="wp-block-heading">Adding Columns, Partitions, and Handling Mutations<a class="anchor-link" id="adding-columns-partitions-and-handling-mutations"></a></h3>
<p>Schema changes in ClickHouse are generally safer than in OLTP databases, but they are not without operational cost. Adding a column is fast and non-blocking. ClickHouse uses lazy evaluation, the new column returns a default value for existing rows without rewriting data on disk. It is one of the rare DDL operations you can run in production without much anxiety:</p>
<pre class="wp-block-code"><code>ALTER TABLE events_local ON CLUSTER my_cluster
ADD COLUMN geo_country LowCardinality(String) DEFAULT '';</code></pre>
<p>Dropping a column triggers a background data rewrite (mutation) to remove that column from existing parts. This is heavier and can be slow on large tables. Mutations are expensive in ClickHouse. For example when you run the following command, ClickHouse does not update the row in place but finds all parts with the matching condition, creates new versions of those parts with the modification already applied, replaces old parts after processing and continues serving queries while mutations run in the background.</p>
<pre class="wp-block-code"><code>ALTER TABLE events_local
UPDATE status = 'processed'
WHERE id = 123; </code></pre>
<p>The guidance here is simple: avoid mutations in hot paths. For data corrections, prefer inserting corrected rows and using a <code>ReplacingMergeTree</code> or <code>CollapsingMergeTree</code> engine to handle deduplication, rather than updating rows in place.</p>
<p>If you must run a mutation, monitor its progress:</p>
<pre class="wp-block-code"><code>SELECT
    command,
    parts_to_do,
    is_done,
    latest_fail_reason
FROM system.mutations
WHERE table = 'events_local' AND is_done = 0;</code></pre>
<h2 class="wp-block-heading">Monitoring Schema-Related Issues<a class="anchor-link" id="monitoring-schema-related-issues"></a></h2>
<h3 class="wp-block-heading">Identifying Slow Queries and Partition Problems<a class="anchor-link" id="identifying-slow-queries-and-partition-problems"></a></h3>
<p>The most useful table in ClickHouse for day-to-day schema health monitoring is <code>system.query_log</code>. Queries that are reading an unexpectedly high number of rows relative to what they return are usually a sign of poor partition pruning or an <code>ORDER BY</code> that doesn&rsquo;t align with the filter. <strong>Skipping indexes</strong> (secondary indexes in ClickHouse) are often added with good intentions but not actually used. Check whether they&rsquo;re being utilized by execute the following:</p>
<pre class="wp-block-code"><code>SELECT
    table,
    name,
    type,
    expr
FROM system.data_skipping_indices
WHERE database = 'mydb';</code></pre>
<p>Then cross-reference with <code>system.query_log</code> to see if queries against that table are actually benefiting, if <code>read_rows</code> remains high after adding an index, it may not be matching the query pattern.</p>
<h3 class="wp-block-heading">Capacity Planning for Growth<a class="anchor-link" id="capacity-planning-for-growth"></a></h3>
<p>Schema decisions have long-term storage implications that are not always obvious at design time. A few metrics are worth tracking regularly, such those included in this storage growth per table over time monitoring query below:</p>
<pre class="wp-block-code"><code>SELECT
    table,
    formatReadableSize(sum(bytes_on_disk)) AS total_size,
    sum(rows) AS total_rows,
    count() AS part_count,
    max(modification_time) AS last_modified
FROM system.parts
WHERE active = 1 AND database = 'mydb'
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;</code></pre>
<p>Track the table with total size, rows, partition count on a weekly basis and plot the trend. A table that grows 20% month-over-month with a 90 day TTL will eventually reach a stable size but a table with no TTL and unbounded growth will eventually cause disk pressure that affects the entire cluster.</p>
<p>Partition-level granularity is also useful for anticipating when TTL drops will occur and what storage they will free:</p>
<pre class="wp-block-code"><code>SELECT
    partition,
    formatReadableSize(sum(bytes_on_disk)) AS size,
    sum(rows) AS rows,
    count() AS parts
FROM system.parts
WHERE active = 1 AND table = 'events_local'
GROUP BY partition
ORDER BY partition DESC;</code></pre>
<p>The above query shows the size per partition for the ClickHouse table <code>events_local</code>.&nbsp;</p>
<h2 class="wp-block-heading">Integrating with Your Multi-Database Environment<a class="anchor-link" id="integrating-with-your-multi-database-environment"></a></h2>
<h3 class="wp-block-heading">Data Flow from OLTP to ClickHouse<a class="anchor-link" id="data-flow-from-oltp-to-clickhouse"></a></h3>
<p>Most ClickHouse deployments exist downstream of an OLTP database. Orders come in through PostgreSQL, user events flow through MySQL, and ClickHouse ingests and aggregates that data for analytics. This pipeline introduces a class of schema problems that don&rsquo;t exist in single-database setups.</p>
<p>The OLTP schema and the ClickHouse schema should not be the same schema. OLTP tables are normalized, they are designed to minimize write amplification and enforce referential integrity. ClickHouse schemas are deliberately denormalized, trading write efficiency for read efficiency. A join that&rsquo;s trivial in PostgreSQL can be expensive in ClickHouse at scale, so the right pattern is to resolve joins at ingestion time, pushing denormalized, enriched records into ClickHouse rather than replicating normalized tables and joining at query time.</p>
<p>This means the ingestion pipeline, whether it&rsquo;s Kafka, Debezium CDC, Airbyte, or a custom ETL, is also a transformation layer. Fields get renamed, types get cast, related records get joined and flattened, and low-cardinality string fields get encoded appropriately. Operationally, this pipeline is part of the schema: changes to it have the same impact as changes to the table definition.</p>
<h3 class="wp-block-heading">Managing Model Changes, Versioning, and Rollback<a class="anchor-link" id="managing-model-changes-versioning-and-rollback"></a></h3>
<p>When the upstream OLTP schema changes eg: a new column added to <code>orders</code>, a field renamed in <code>users</code>. The downstream ClickHouse schema and the ingestion pipeline both need to change in a coordinated way. Without a versioning discipline, these changes become brittle and difficult to roll back &mdash; a few practices that hold up well in production:</p>
<ul class="wp-block-list">
<li>Treat DDL as code: The schema changes should live in version-controlled migration files (tools like Flyway or a custom migration runner), not applied ad-hoc from a SQL client. Every <code>ALTER TABLE</code> that went to production should be traceable to a commit.</li>
<li>Add before you remove: When renaming a column or changing a type, add the new column first and allow both the old and new column to coexist during a transition window. Update the ingestion pipeline to write to both, then cut over queries to the new column, then drop the old one. This avoids a hard cutover that can&rsquo;t be rolled back.</li>
<li>Schema rollback is hard therefore plan for it: Dropping a column or partition key change is not easily reversible. Before applying significant schema changes, take a backup of the affected table (or at minimum its most recent partition) so that recovery is possible without a full cluster restore.</li>
<li>Document the lineage: For each ClickHouse table, maintain a short document describing where the data comes from, what transformations are applied, and what downstream queries or dashboards depend on it. When a schema change is proposed, this lineage makes the blast radius obvious before anything is applied.</li>
</ul>
<h2 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>ClickHouse schema design is not something that you set once, but is something you evolve over time. The implication is that choices you make when creating a table today do not just affect today&rsquo;s queries but quietly shape how your system performs months down the line, from how efficiently queries run to how painful or painless future schema changes turn out to be.</p>
<p>Some points worth keeping in mind when designing the schema and data modeling: <strong>design</strong> your <code>ORDER BY</code> for readers, not writers; <strong>structure</strong> your sort key around how people query the data, not around the order it arrives in; <strong>partition</strong> by time and but avoid slicing things so finely that merge overhead becomes its own problem; <strong>Be deliberate</strong> with data types. <code>LowCardinality</code> and <code>AggregateFunction</code> are powerful tools, but only when applied with clear intent. Reaching for them out of habit rather than purpose tends to backfire. Your <strong>ingestion pipeline</strong> is part of your schema &mdash; how data flows in isn&rsquo;t separate from how it&rsquo;s stored, think of them as one connected system.&nbsp;</p>
<p>And remember, keep an eye on the correct metrics from the start. Schema issues seldom make themselves known in an obvious way. Identifying them through regular monitoring is much less expensive than dealing with the aftermath. The central idea here is that decisions regarding schemas have a cumulative effect. Positive choices subtly simplify all other aspects, while negative ones discreetly complicate them.</p>
<p>The post <a href="https://severalnines.com/blog/clickhouse-schema-design-and-data-modeling/">ClickHouse Schema Design and Data Modeling</a> appeared first on <a href="https://severalnines.com">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/clickhouse-schema-design-and-data-modeling/">ClickHouse Schema Design and Data Modeling</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Perconians at WeAreDevelopers World Congress 2026: Agents Everywhere, Security Wake-Up Calls, and Buzzword Bingo</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/17/wearedevelopers-2026/" />
      <id>https://percona.community/blog/2026/07/17/wearedevelopers-2026/</id>
      <updated>2026-07-17T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>On July 9–10, the two of us - Sandra (Engineering, Percona for MongoDB) and Radek (Product, Percona for MongoDB) - packed our backpacks and headed to Berlin for the WeAreDevelopers World Congress Europe 2026 (WAD). The 11th edition of the congress gathered 15,000 developers and 500+ speakers for two intense days, and we came back with full notebooks, fresh ideas, and one very clear message from the industry.</p>
<p><a href="https://percona.community/blog/2026/07/17/wearedevelopers-2026/">Perconians at WeAreDevelopers World Congress 2026: Agents Everywhere, Security Wake-Up Calls, and Buzzword Bingo</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>On July 9&ndash;10, the two of us &ndash; <strong>Sandra</strong> (Engineering, Percona for MongoDB) and <strong>Radek</strong> (Product, Percona for MongoDB) &ndash; packed our backpacks and headed to Berlin for the <a href="https://www.wearedevelopers.com/world-congress" target="_blank" rel="noopener noreferrer">WeAreDevelopers World Congress Europe 2026</a> (WAD). The 11th edition of the congress gathered <strong>15,000 developers and 500+ speakers</strong> for two intense days, and we came back with full notebooks, fresh ideas, and one very clear message from the industry.</p>

<p>
At Percona, we genuinely love getting out of our daily routine to learn what&rsquo;s new. It is incredibly refreshing to escape the daily grind of sprints and stand-ups just to listen and absorb. Hearing how other teams are tackling scale and complexity reminds us that we are all solving different flavors of the same core problems. These events act as a catalyst for innovation, sparking conversations that inevitably push our own boundaries. Ultimately, taking a couple of days to zoom out and see where the industry is heading helps us build what Sandra calls a <em>mental index</em> &ndash; concepts that might not solve today&rsquo;s ticket, but will absolutely pay off six months from now.</p>
<p>While we took notes across a huge variety of topics, one common thread stood out above the rest. This post is our attempt to share that index with you, with links so you can dig deeper into whatever catches your eye.</p>
<p>Spoiler: if you played buzzword bingo with &ldquo;agentic AI,&rdquo; you&rsquo;d have won in the first hour.</p>
<h2 id="the-one-big-theme-agentic-ai-surprise">The one big theme: Agentic AI (surprise!)<a class="anchor-link" id="the-one-big-theme-agentic-ai-surprise"></a></h2>
<p>Every conference right now has an AI theme, but WAD went deep: adoption stories, best practices, building and optimizing RAG pipelines, and &ndash; importantly &ndash; what happens to security when agents write and ship code.</p>
<p>Our personal top takeaways:</p>
<ul>
<li><strong>Security matters more than ever in the age of AI.</strong> More on that below &ndash; this one deserves its own section.</li>
<li><strong>Agents are only as good as their context.</strong> Intent, mission, purpose, style, goals, success metrics &ndash; the teams winning with AI are the ones writing this down for their agents.</li>
<li><strong>Understand <em>why</em> you do what you do.</strong> Agents won&rsquo;t think about that for us. The engineering judgment moves up the stack; it doesn&rsquo;t disappear.</li>
</ul>
<h2 id="the-sdlc-is-dead---the-agentic-assembly-line-keynote">&ldquo;The SDLC is dead&rdquo; &ndash; the Agentic Assembly Line keynote<a class="anchor-link" id="the-sdlc-is-dead-the-agentic-assembly-line-keynote"></a></h2>
<p>Thomas Dohmke (CEO of Entire, previously CEO of GitHub) opened with a demo-filled keynote about where developers and agents are headed. A few numbers that made the whole room sit up:</p>
<ul>
<li>Teams using coding agents ship <strong>5&times; more code</strong> (measured by PRs), and PRs are <strong>3&times; bigger</strong> than 18 months ago.</li>
<li><strong>20%+ of AI-generated changes are accepted without human review.</strong> (Brave? Terrifying? Discuss.)</li>
<li>Some products &ndash; Codex, notably &ndash; are now written <em>only</em> by AI.</li>
</ul>
<p>His conclusion: <strong>the Software Development Lifecycle as we know it is dead.</strong> The DevOps loop is evolving into what he called <a href="https://medium.com/@tentenco/what-is-ralph-loop-a-new-era-of-autonomous-coding-96a4bb3e2ac8" target="_blank" rel="noopener noreferrer">&ldquo;The Ralph Loop&rdquo;</a> &ndash; a much faster path from code to production, with developers acting as verifiers, because agents still fail often. The winning team, in his view, is the one whose agents understand the company&rsquo;s mission, values, and purpose.</p>
<p>One practical problem he highlighted: when you code with agents, context fragments across chats, prompts, sessions, and branches. Tools like <a href="https://entire.io/" target="_blank" rel="noopener noreferrer">entire.io</a> now store the chat sessions behind each PR right in the GitHub repository &ndash; so the <em>intent</em> behind a change doesn&rsquo;t evaporate. You may want to check this tool out! Let&rsquo;s see how the GitHub, we know today, evolves over the next decade.</p>
<h2 id="retrieval-is-the-weakest-link-in-your-rag">Retrieval is the weakest link in your RAG<a class="anchor-link" id="retrieval-is-the-weakest-link-in-your-rag"></a></h2>
<p>One of our favorite technical talks, by Tomek Porozynski (deepsense.ai), tackled the &ldquo;R&rdquo; in RAG (Retrieval Augmented Generation). General embedding models are trained on public data &ndash; they know general language, <strong>not your business</strong>.</p>
<p>Basic retrieval falls short: keyword search misses semantic context, and vector search misses exact terms, multi-step logic, document-wide context, and internal jargon. There&rsquo;s no single fix &ndash; you pick the right tool for the job.</p>

<p>For domain-specific knowledge, the fix is to fine-tune the embedding model on your own domain, so the vector space itself shifts to reflect your terminology and the real relationships between your terms.</p>
<p>The mechanics are surprisingly approachable: reshape the vector space through relative distances (pull matching pairs closer, push mismatched pairs apart), using either <strong>triplet loss</strong> or <strong>MultipleNegativesRankingLoss</strong> &ndash; and with the <a href="https://sbert.net/" target="_blank" rel="noopener noreferrer">Sentence Transformers</a> toolkit, the latter is literally one import away.</p>
<p>If you want to try it yourself, the speaker shared <a href="https://github.com/ontaptom/workshops/tree/main/notebooks" target="_blank" rel="noopener noreferrer">hands-on Colab notebooks</a>.</p>
<h2 id="the-security-wake-up-call-surviving-the-vulnpocalypse">The security wake-up call: surviving the &ldquo;Vulnpocalypse&rdquo;<a class="anchor-link" id="the-security-wake-up-call-surviving-the-vulnpocalypse"></a></h2>
<p>Adrian Mouat (Chainguard) delivered the talk that stuck with us the most. Advanced AI models can now autonomously discover and weaponize zero-day vulnerabilities at machine speed &ndash; effectively <strong>erasing the traditional patch window</strong>. Especially with the rise of <a href="https://www.anthropic.com/claude/mythos" target="_blank" rel="noopener noreferrer">Anthrophic Mythos</a> model, this might lead to Vulnpocalypse!</p>
<p>This is very serious for open source: attackers can point LLMs at public codebases, while underfunded maintainers face an overwhelming volume of newly discovered bugs. As people who live and breathe open source databases, this hits close to home.</p>
<p>The defenses he proposed:</p>
<ul>
<li><strong>Fight AI with AI</strong> &ndash; proactively scan your own infrastructure and find vulnerabilities before attackers do.</li>
<li><strong>Minimize your attack surface</strong> &ndash; fewer dependencies, and consider AI-written snippets over pulling in vulnerable third-party libraries.</li>
<li><strong>Strict hygiene</strong> &ndash; immediate patching and eliminating long-lived access tokens are non-negotiable.</li>
<li><strong>Industry coalitions</strong> &ndash; rapid-response groups like <a href="https://www.chainguard.dev/athena" target="_blank" rel="noopener noreferrer">Athena</a> share mitigations at machine speed, while &ldquo;Akrites&rdquo; safely funnels fixes back into upstream open source projects.</li>
</ul>
<p>At Percona, we&rsquo;re already evaluating joining these coalitions &ndash; stay tuned!</p>
<p>Related: Isha Salania (Microsoft) showed how <strong>confidential computing</strong> extends encryption to data <em>in use</em> &ndash; your prompts, retrieved chunks, and keys living in encrypted memory. For anyone building sovereign RAG systems on top of databases, this end-to-end view of data protection is worth understanding &ndash; and it&rsquo;s going to raise expectations for queryable encryption across the whole database ecosystem.</p>
<h2 id="mcp-doesnt-suck---your-agent-does">&ldquo;MCP doesn&rsquo;t suck &ndash; your agent does&rdquo;<a class="anchor-link" id="mcp-doesnt-suck-your-agent-does"></a></h2>
<p>Best talk title of the conference, courtesy of Jan Curn (Apify). The problem: most agents load <em>all</em> available tool schemas into the context window upfront, causing context rot, slow performance, and rapidly burning tokens. Their answer is <strong>mcpc</strong> &ndash; a lightweight CLI that enables <em>progressive tool discovery</em>: the agent fetches only the tool schemas it needs, on demand, and chains workflows through native code execution. Add OAuth 2.1 and sandboxed proxy connections, and you get a much saner MCP setup.</p>
<h2 id="more-gems-worth-your-time">More gems worth your time<a class="anchor-link" id="more-gems-worth-your-time"></a></h2>
<ul>
<li><strong>From SDLC to ADLC.</strong> Marcin Wawryszczuk (Andersen) argued that AI speeds up <em>coding</em> but not the <em>release cycle</em> &ndash; the industry needs an Agentic Delivery Lifecycle where agents help with requirements, architecture, docs, and pipelines, while engineers keep authority over architecture, governance, and validation.</li>
<li><strong>Don&rsquo;t lock in your AI tooling too early.</strong> Angie Jones shared how Block bought access to many tools and let engineers run with them &ndash; what works for a web developer doesn&rsquo;t work for a mobile or JVM developer, and that diversity of feedback is gold. Standardize when you see workflows succeeding repeatedly, not because a vendor made a good pitch.</li>
<li><strong>LLMs in the wild.</strong> GetYourGuide&rsquo;s data scientist Giampaolo Casolla and MLOps engineer Steven Mi walked through keeping an AI-driven recommendation system alive in production. Real numbers, real trade-offs.</li>
<li><strong>Platform-as-a-Product.</strong> Dominik Schmidle (Giant Swarm) on why internal platforms fail: happy users won&rsquo;t save your platform if the C-level sees it as pure cost. Know your user <em>and</em> your decision-maker &ndash; and do internal marketing.</li>
<li><strong>Werner Vogels (CTO, Amazon) fireside chat.</strong> Invisible work is important and worth sharing. Stay curious, never stop learning &ndash; and he recommended the book <a href="https://www.amazon.de/Ask-Your-Developer-Software-Developers/dp/0063018292" target="_blank" rel="noopener noreferrer"><em>Ask Your Developer</em></a> by Jeff Lawson.</li>
</ul>
<h2 id="the-expo-floor">The expo floor<a class="anchor-link" id="the-expo-floor"></a></h2>
<p>Between the talks, we&rsquo;ve also hung out at the Percona booth &ndash; yes, we&rsquo;ve been there the entire two days and chatting with 100+ visitors about what we love the most &ndash; databases!</p>

<p>We&rsquo;ve also visited our neighbours at the expo hall and had great conversations with them, too &ndash; but there was one that stood out:</p>
<p><a href="https://www.qodo.ai/" target="_blank" rel="noopener noreferrer"><strong>Qodo</strong></a> (formerly CodiumAI): AI code review that indexes your entire repository, so reviews understand the architectural &ldquo;why&rdquo; behind a change &ndash; and it&rsquo;s <a href="https://github.com/marketplace/qodo-merge-pro-for-open-source" target="_blank" rel="noopener noreferrer">free for open source projects</a>. We&rsquo;re excited to try it on our own projects.</p>
<h2 id="parting-words">Parting words<a class="anchor-link" id="parting-words"></a></h2>
<p>Two quotes from the WeAreDevelopers founders stayed with us. From CPO Thomas Pamminger:</p>
<blockquote>
<p>AI didn&rsquo;t make me worse at my job &ndash; it made it easier to be worse without noticing. Charles Eames, asked what he&rsquo;d delegate, said: <strong>never the understanding.</strong></p>
</blockquote>
<p>And from CEO Sead Ahmetovi&#263;:</p>
<blockquote>
<p>Someone, somewhere, will depend on what you ship next. That&rsquo;s not a burden &ndash; that&rsquo;s the whole point, because your work matters. Let&rsquo;s do it well.</p>
</blockquote>
<p>That&rsquo;s a pretty good summary of why we go to these events: to keep understanding, not just shipping.</p>
<p>If any of the topics above sparked something &ndash; RAG fine-tuning, supply chain security, vector search in databases &ndash; chat with us on the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">Percona Community Forum</a> or just drop a comment here. We&rsquo;d love to hear what <em>you</em> took away from WAD if you were there.</p>
<p>See you at the next event!</p>

<p><a href="https://percona.community/blog/2026/07/17/wearedevelopers-2026/">Perconians at WeAreDevelopers World Congress 2026: Agents Everywhere, Security Wake-Up Calls, and Buzzword Bingo</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Introducing Mountaineers: A Way to Say Thank You</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/16/introducing-mountaineers/" />
      <id>https://percona.community/blog/2026/07/16/introducing-mountaineers/</id>
      <updated>2026-07-16T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>You filed a bug report at 11pm because you’d already done the work to isolate it. You answered a forum question that had been sitting unanswered for three days. You wrote a PR. You spent an hour on a call telling us what’s broken about a tool you use every day. None of that is small, and none of it should go unnoticed.</p>
<p><a href="https://percona.community/blog/2026/07/16/introducing-mountaineers/">Introducing Mountaineers: A Way to Say Thank You</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>You filed a bug report at 11pm because you&rsquo;d already done the work to isolate it. You answered a forum question that had been sitting unanswered for three days. You wrote a PR. You spent an hour on a call telling us what&rsquo;s broken about a tool you use every day. None of that is small, and none of it should go unnoticed.</p>
<p>That&rsquo;s what Mountaineers are. It&rsquo;s how we recognize the time and energy you put into this community &mdash; and reward it.</p>
<h2 id="why-we-built-this">Why we built this<a class="anchor-link" id="why-we-built-this"></a></h2>
<p>Every contribution to this community costs you something: time, expertise, patience. Writing reproduction steps for a bug isn&rsquo;t free. Neither is answering the same kind of question for the fifth newcomer this month, or sitting down with our engineering team to walk through how you actually use Percona Operators in production.</p>
<p>We see that. Mountaineers are our way of tracking it properly and giving something back &mdash; recognition, access, and yes, swag.</p>
<h2 id="what-counts">What counts<a class="anchor-link" id="what-counts"></a></h2>
<p>This isn&rsquo;t just about code. If you&rsquo;ve assumed that contributing means opening a pull request or nothing, that&rsquo;s not how this works. Points come from:</p>
<ul>
<li><strong>GitHub</strong> &mdash; issues, PRs, and merged contributions</li>
<li><strong>Forum</strong> &mdash; starting discussions, replying, and accepted solutions</li>
<li><strong>Content</strong> &mdash; blog posts, tutorials, and video appearances, including through our <a href="https://percona.community/blog/2026/05/22/write-for-percona-community/" target="_blank" rel="noopener noreferrer">Community Writers Program</a>, where you also get paid for published posts</li>
<li><strong>Direct feedback</strong> &mdash; 1:1 sessions with our engineering team and survey responses</li>
</ul>
<p>That last one matters more than people think. If you want to tell us what works, what doesn&rsquo;t, and how you&rsquo;re actually using our tools day to day, we want that conversation. Talk to engineering directly, or write it up for the blog. Either way, it counts.</p>
<h2 id="how-the-climb-works">How the climb works<a class="anchor-link" id="how-the-climb-works"></a></h2>
<p>Everyone starts at Basecamp. From there, the more you contribute &mdash; and the more places you contribute &mdash; the higher you climb. Show up across GitHub, the forum, and content in the same month, and your points multiply. We&rsquo;re not trying to make this complicated: more engagement, more recognition, faster.</p>
<p>Points convert into real rewards. Stickers and digital badges at the entry tier. T-shirts and water bottles as you climb. Hoodies and tech accessories further up. All those who begin the climb will receive a serialized Challenge Coin &mdash; the kind of thing you can&rsquo;t buy, only earn.</p>
<p>The people who consistently show up across the board get invited to take part in Percona Live: roadmap sessions, early access, time with the people building the tools you use.</p>
<h2 id="you-dont-need-a-long-resume-to-start">You don&rsquo;t need a long resume to start<a class="anchor-link" id="you-dont-need-a-long-resume-to-start"></a></h2>
<p>If you&rsquo;ve filed one bug report with clear reproduction steps, answered one forum question, or have an opinion about a tool you use that you&rsquo;ve never told us &mdash; you already have something to bring. We built Mountaineers to recognize the full range of ways people show up, not just the most visible ones.</p>
<p><strong><a href="https://forums.percona.com/signup" target="_blank" rel="noopener noreferrer">Sign up for Mountaineers</a></strong> and your GitHub and forum activity start counting from day one.</p>
<p>Want the full detail on points, rungs, and rewards? Read the <a href="https://percona.community/ascent/mountaineers/" target="_blank" rel="noopener noreferrer">Mountaineers program page</a>.</p>

<p><a href="https://percona.community/blog/2026/07/16/introducing-mountaineers/">Introducing Mountaineers: A Way to Say Thank You</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Multi-Cluster Replication, FIPS Mode, and More with MariaDB Enterprise Kubernetes Operator 26.06</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/multi-cluster-replication-fips-mode-and-more-with-mariadb-enterprise-kubernetes-operator-26-06/" />
      <id>https://mariadb.com/resources/blog/multi-cluster-replication-fips-mode-and-more-with-mariadb-enterprise-kubernetes-operator-26-06/</id>
      <updated>2026-07-15T17:13:46+03:00</updated>
      <author><name>Egor Ustinov</name></author>
      <summary type="html"><![CDATA[<p>What is the MariaDB Enterprise Kubernetes Operator? The MariaDB Enterprise Kubernetes Operator makes it easier to run and manage MariaDB […]</p>
<p><a href="https://mariadb.com/resources/blog/multi-cluster-replication-fips-mode-and-more-with-mariadb-enterprise-kubernetes-operator-26-06/">Multi-Cluster Replication, FIPS Mode, and More with MariaDB Enterprise Kubernetes Operator 26.06</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The MariaDB Enterprise Kubernetes Operator makes it easier to run and manage MariaDB databases in Kubernetes. It automates day-to-day work such as deployment, scaling, backups, recovery, security configuration, and upgrades, reducing the manual effort of running MariaDB databases on Kubernetes. For more information, see the MariaDB Enterprise Kubernetes Operator page.</p>
<p><a href="https://mariadb.com/resources/blog/multi-cluster-replication-fips-mode-and-more-with-mariadb-enterprise-kubernetes-operator-26-06/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/multi-cluster-replication-fips-mode-and-more-with-mariadb-enterprise-kubernetes-operator-26-06/">Multi-Cluster Replication, FIPS Mode, and More with MariaDB Enterprise Kubernetes Operator 26.06</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Database Index Optimizer</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/database_index_optimizer/" />
      <id>https://www.fromdual.com/blog/database_index_optimizer/</id>
      <updated>2026-07-15T14:55:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Recently, a client asked me if the “time-consuming” task of checking indexes could be left to an index optimizer. Of course it can…<br />
What exactly do we want to check?</p>
<p>Tables without a Primary Key<br />
Duplicate indexes<br />
Partially redundant indexes<br />
Unused indexes</p>
<p>MariaDB, MySQL, and Percona Server<br />
Tables without a Primary Key<br />
SQL > SELECT DISTINCT t.table_schema, t.table_name<br />
 FROM information_schema.tables AS t<br />
 LEFT JOIN information_schema.columns AS c ON t.table_schema = c.table_schema AND t.table_name = c.table_name<br />
 AND c.column_key = \"PRI\"<br />
 WHERE t.table_schema NOT IN (\'information_schema\', \'mysql\', \'performance_schema\')<br />
 AND c.table_name IS NULL AND t.table_type NOT IN(\'VIEW\', \'SEQUENCE\')<br />
 AND t.table_schema = \'testtest\'<br />
;<br />
+--------------+------------+<br />
&#124; table_schema &#124; table_name &#124;<br />
+--------------+------------+<br />
&#124; testtest &#124; archived &#124;<br />
+--------------+------------+<br />
1 row in set<br />
Source: Tables without a Primary Key<br />
Duplicate indexes<br />
SQL > SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index<br />
 FROM sys.schema_redundant_indexes<br />
 WHERE redundant_index_columns = dominant_index_columns<br />
 AND table_schema = \'testtest\'<br />
;<br />
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+<br />
&#124; table_name &#124; redundant_index_name &#124; redundant_index_columns &#124; dominant_index_name &#124; dominant_index_columns &#124; sql_drop_index &#124;<br />
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+<br />
&#124; archived &#124; dupl2 &#124; category_id &#124; dupl1 &#124; category_id &#124; ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` &#124;<br />
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+<br />
1 row in set<br />
Source: Duplicate and redundant indices<br />
Partially redundant indexes<br />
SQL > SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index<br />
 FROM sys.schema_redundant_indexes<br />
 WHERE table_schema = \'testtest\'<br />
;<br />
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+<br />
&#124; table_name &#124; redundant_index_name &#124; redundant_index_columns &#124; dominant_index_name &#124; dominant_index_columns &#124; sql_drop_index &#124;<br />
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+<br />
&#124; access &#124; customer &#124; customer &#124; customer_2 &#124; customer,callerid_internal &#124; ALTER TABLE `testtest`.`access` DROP INDEX `customer` &#124;<br />
&#124; access &#124; customer &#124; customer &#124; customer_3 &#124; customer,callerid_external &#124; ALTER TABLE `testtest`.`access` DROP INDEX `customer` &#124;<br />
&#124; active_customers &#124; uniqueid &#124; uniqueid &#124; PRIMARY &#124; uniqueid,scustomer &#124; ALTER TABLE `testtest`.`active_customers` DROP INDEX `uniqueid` &#124;<br />
&#124; analytics_include &#124; analytics &#124; analytics &#124; PRIMARY &#124; analytics,feature,dtype,dnumber &#124; ALTER TABLE `testtest`.`analytics_include` DROP INDEX `analytics` &#124;<br />
&#124; archived &#124; dupl2 &#124; category_id &#124; dupl1 &#124; category_id &#124; ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` &#124;<br />
...<br />
&#124; texts_media &#124; uniqueid &#124; uniqueid &#124; PRIMARY &#124; uniqueid,filename &#124; ALTER TABLE `testtest`.`texts_media` DROP INDEX `uniqueid` &#124;<br />
&#124; unlimited_access &#124; customer &#124; customer &#124; customer_2 &#124; customer,callerid_internal &#124; ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` &#124;<br />
&#124; unlimited_access &#124; customer &#124; customer &#124; customer_3 &#124; customer,callerid_external &#124; ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` &#124;<br />
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+<br />
26 rows in set<br />
Source: Duplicate and redundant indices<br />
Unused indexes<br />
SQL > SELECT object_name, index_name<br />
 FROM sys.schema_unused_indexes<br />
 WHERE object_schema = \'testtest\'<br />
;<br />
+------------------------+------------------------+<br />
&#124; object_name &#124; index_name &#124;<br />
+------------------------+------------------------+<br />
&#124; access &#124; customer_3 &#124;<br />
&#124; access &#124; customer_2 &#124;<br />
&#124; actions &#124; class &#124;<br />
&#124; actions &#124; action &#124;<br />
&#124; active &#124; channel &#124;<br />
...<br />
&#124; urls &#124; customer &#124;<br />
&#124; voucher_batches &#124; customer &#124;<br />
&#124; vouchers &#124; batch &#124;<br />
+------------------------+------------------------+<br />
413 rows in set<br />
Note:</p>
<p>For MariaDB, the PERFORMANCE_SCHEMA must be enabled first.<br />
The information is accurate as of the last database restart. If an index was last used BEFORE the most recent restart, it will be shown here as unused.</p>
<p>Source: Unused indexes<br />
And now with PostgreSQL<br />
Tables without a Primary Key<br />
SQL > SELECT tab.table_schema, tab.table_name<br />
 FROM information_schema.tables tab<br />
 LEFT JOIN information_schema.table_constraints tco<br />
 ON tab.table_schema = tco.table_schema<br />
 AND tab.table_name = tco.table_name<br />
 AND tco.constraint_type = \'PRIMARY KEY\'<br />
 WHERE tab.table_type = \'BASE TABLE\'<br />
 AND tab.table_schema NOT IN (\'pg_catalog\', \'information_schema\')<br />
 AND tco.constraint_name IS NULL<br />
 ORDER BY table_schema, table_name<br />
;<br />
 table_schema &#124; table_name<br />
--------------+------------<br />
 public &#124; archived<br />
(1 row)<br />
Source: Find tables without primary keys (PKs) in PostgreSQL database<br />
Duplicate indexes<br />
Based on the MySQL sys schema:<br />
SQL > WITH schema_flattened_keys AS (<br />
 SELECT sai.relid, sai.indexrelid<br />
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name<br />
 , CASE pi.indisunique WHEN \'f\' THEN 1 ELSE 0 END AS non_unique<br />
 , index_columns.columns AS index_columns<br />
 FROM pg_stat_all_indexes AS sai<br />
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid<br />
 JOIN (<br />
 SELECT attrelid, string_agg(attname, \',\' ORDER BY attnum ASC) AS columns<br />
 FROM pg_attribute GROUP BY attrelid<br />
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid<br />
 WHERE sai.schemaname NOT IN (\'pg_toast\', \'pg_catalog\')<br />
)<br />
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name<br />
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique<br />
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique<br />
 , CONCAT(\'ALTER TABLE \', redundant_keys.table_schema, \'.\', redundant_keys.table_name, \' DROP INDEX \', redundant_keys.index_name, \'\') AS sql_drop_index<br />
 FROM schema_flattened_keys redundant_keys<br />
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name<br />
 WHERE (redundant_keys.index_name < > dominant_keys.index_name<br />
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)<br />
 AND ((redundant_keys.non_unique > dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))<br />
 )<br />
 OR ((POSITION(CONCAT(redundant_keys.index_columns,\',\') IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))<br />
 OR ((POSITION(CONCAT(dominant_keys.index_columns,\',\') IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))<br />
 )<br />
 AND redundant_keys.index_columns = dominant_keys.index_columns<br />
;<br />
 table_schema &#124; table_name &#124; redundant_index_name &#124; redundant_index_columns &#124; redundant_index_non_unique &#124; dominant_index_name &#124; dominant_index_columns &#124; dominant_index_non_unique &#124; sql_drop_index<br />
--------------+------------+----------------------+-------------------------+----------------------------+---------------------+------------------------+---------------------------+----------------------------------------------<br />
 public &#124; archived &#124; dupl1 &#124; category_id &#124; 1 &#124; dupl2 &#124; category_id &#124; 1 &#124; ALTER TABLE public.archived DROP INDEX dupl1<br />
 public &#124; archived &#124; dupl2 &#124; category_id &#124; 1 &#124; dupl1 &#124; category_id &#124; 1 &#124; ALTER TABLE public.archived DROP INDEX dupl2<br />
(2 rows)<br />
Source: Duplicate and redundant indices<br />
Partially redundant indexes<br />
Based on the MySQL sys schema:<br />
SQL > WITH schema_flattened_keys AS (<br />
 SELECT sai.relid, sai.indexrelid<br />
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name<br />
 , CASE pi.indisunique WHEN \'f\' THEN 1 ELSE 0 END AS non_unique<br />
 , index_columns.columns AS index_columns<br />
 FROM pg_stat_all_indexes AS sai<br />
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid<br />
 JOIN (<br />
 SELECT attrelid, string_agg(attname, \',\' ORDER BY attnum ASC) AS columns<br />
 FROM pg_attribute GROUP BY attrelid<br />
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid<br />
 WHERE sai.schemaname NOT IN (\'pg_toast\', \'pg_catalog\')<br />
)<br />
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name<br />
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique<br />
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique<br />
 , CONCAT(\'ALTER TABLE \', redundant_keys.table_schema, \'.\', redundant_keys.table_name, \' DROP INDEX \', redundant_keys.index_name, \'\') AS sql_drop_index<br />
 FROM schema_flattened_keys redundant_keys<br />
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name<br />
 WHERE (redundant_keys.index_name < > dominant_keys.index_name<br />
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)<br />
 AND ((redundant_keys.non_unique > dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))<br />
 )<br />
 OR ((POSITION(CONCAT(redundant_keys.index_columns,\',\') IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))<br />
 OR ((POSITION(CONCAT(dominant_keys.index_columns,\',\') IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))<br />
 )<br />
;<br />
 table_schema &#124; table_name &#124; redundant_index_name &#124; redundant_index_columns &#124; redundant_index_non_unique &#124; dominant_index_name &#124; dominant_index_columns &#124; dominant_index_non_unique &#124; sql_drop_index<br />
--------------+-----------------------+------------------------------------------+-------------------------+----------------------------+-------------------------------------------------+-----------------------------------------+---------------------------+---------------------------------------------------------------------------------------------<br />
 public &#124; numbers &#124; numbers_customer_idx &#124; customer &#124; 1 &#124; numbers_customer_text_dtype_text_dnumber_idx &#124; customer,text_dtype,text_dnumber &#124; 1 &#124; ALTER TABLE public.numbers DROP INDEX numbers_customer_idx<br />
 public &#124; numbers &#124; numbers_customer_idx &#124; customer &#124; 1 &#124; numbers_customer_fax_dtype_fax_dnumber_idx &#124; customer,fax_dtype,fax_dnumber &#124; 1 &#124; ALTER TABLE public.numbers DROP INDEX numbers_customer_idx<br />
 public &#124; numbers &#124; numbers_customer_idx &#124; customer &#124; 1 &#124; numbers_pkey &#124; customer,stype,snumber &#124; 0 &#124; ALTER TABLE public.numbers DROP INDEX numbers_customer_idx<br />
 public &#124; numbers &#124; numbers_dtype_idx &#124; dtype &#124; 1 &#124; numbers_dtype_dnumber_idx &#124; dtype,dnumber &#124; 1 &#124; ALTER TABLE public.numbers DROP INDEX numbers_dtype_idx<br />
 public &#124; number_callers &#124; number_callers_dtype_idx &#124; dtype &#124; 1 &#124; number_callers_dtype_dnumber_idx &#124; dtype,dnumber &#124; 1 &#124; ALTER TABLE public.number_callers DROP INDEX number_callers_dtype_idx<br />
 public &#124; prefixes &#124; prefixes_customer_idx &#124; customer &#124; 1 &#124; prefixes_customer_dtype_dnumber_idx &#124; customer,dtype,dnumber &#124; 1 &#124; ALTER TABLE public.prefixes DROP INDEX prefixes_customer_idx<br />
 public &#124; number_times &#124; number_times_dtype_idx &#124; dtype &#124; 1 &#124; number_times_dtype_dnumber_idx &#124; dtype,dnumber &#124; 1 &#124; ALTER TABLE public.number_times DROP INDEX number_times_dtype_idx<br />
 public &#124; phones &#124; phones_customer_idx &#124; customer &#124; 1 &#124; phones_customer_callerid_location_idx &#124; customer,callerid_location &#124; 1 &#124; ALTER TABLE public.phones DROP INDEX phones_customer_idx<br />
 public &#124; phones &#124; phones_customer_idx &#124; customer &#124; 1 &#124; phones_customer_callerid_external_idx &#124; customer,callerid_external &#124; 1 &#124; ALTER TABLE public.phones DROP INDEX phones_customer_idx<br />
 public &#124; phones &#124; phones_customer_idx &#124; customer &#124; 1 &#124; phones_customer_callerid_internal_idx &#124; customer,callerid_internal &#124; 1 &#124; ALTER TABLE public.phones DROP INDEX phones_customer_idx<br />
 public &#124; phones_hardware &#124; phones_hardware_phone_idx &#124; phone &#124; 1 &#124; phones_hardware_phone_hardware_address_idx &#124; phone,hardware_address &#124; 0 &#124; ALTER TABLE public.phones_hardware DROP INDEX phones_hardware_phone_idx<br />
 public &#124; speeddials &#124; speeddials_stype_idx &#124; stype &#124; 1 &#124; speeddials_stype_snumber_idx &#124; stype,snumber &#124; 1 &#124; ALTER TABLE public.speeddials DROP INDEX speeddials_stype_idx<br />
 public &#124; speeddials &#124; speeddials_dtype_idx &#124; dtype &#124; 1 &#124; speeddials_dtype_dnumber_idx &#124; dtype,dnumber &#124; 1 &#124; ALTER TABLE public.speeddials DROP INDEX speeddials_dtype_idx<br />
 public &#124; mailbox_destinations &#124; mailbox_destinations_context_mailbox_idx &#124; context,mailbox &#124; 1 &#124; mailbox_destinations_pkey &#124; context,mailbox,dcustomer,dtype,dnumber &#124; 0 &#124; ALTER TABLE public.mailbox_destinations DROP INDEX mailbox_destinations_context_mailbox_idx<br />
 public &#124; outgroup_times &#124; outgroup_times_outgroup_idx &#124; outgroup &#124; 1 &#124; outgroup_times_outgroup_name_idx &#124; outgroup,name &#124; 0 &#124; ALTER TABLE public.outgroup_times DROP INDEX outgroup_times_outgroup_idx<br />
 public &#124; ingroup_times &#124; ingroup_times_ingroup_idx &#124; ingroup &#124; 1 &#124; ingroup_times_ingroup_name_idx &#124; ingroup,name &#124; 0 &#124; ALTER TABLE public.ingroup_times DROP INDEX ingroup_times_ingroup_idx<br />
 public &#124; active_customers &#124; active_customers_uniqueid_idx &#124; uniqueid &#124; 1 &#124; active_customers_pkey &#124; uniqueid,scustomer &#124; 0 &#124; ALTER TABLE public.active_customers DROP INDEX active_customers_uniqueid_idx<br />
 public &#124; access &#124; access_customer_idx &#124; customer &#124; 1 &#124; access_customer_callerid_external_idx &#124; customer,callerid_external &#124; 1 &#124; ALTER TABLE public.access DROP INDEX access_customer_idx<br />
 public &#124; access &#124; access_customer_idx &#124; customer &#124; 1 &#124; access_customer_callerid_internal_idx &#124; customer,callerid_internal &#124; 1 &#124; ALTER TABLE public.access DROP INDEX access_customer_idx<br />
 public &#124; unlimited_access &#124; unlimited_access_customer_idx &#124; customer &#124; 1 &#124; unlimited_access_customer_callerid_external_idx &#124; customer,callerid_external &#124; 1 &#124; ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx<br />
 public &#124; unlimited_access &#124; unlimited_access_customer_idx &#124; customer &#124; 1 &#124; unlimited_access_customer_callerid_internal_idx &#124; customer,callerid_internal &#124; 1 &#124; ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx<br />
 public &#124; texts &#124; texts_dcustomer_idx &#124; dcustomer &#124; 1 &#124; texts_dcustomer_dtype_dnumber_idx &#124; dcustomer,dtype,dnumber &#124; 1 &#124; ALTER TABLE public.texts DROP INDEX texts_dcustomer_idx<br />
 public &#124; texts_media &#124; texts_media_uniqueid_idx &#124; uniqueid &#124; 1 &#124; texts_media_pkey &#124; uniqueid,filename &#124; 0 &#124; ALTER TABLE public.texts_media DROP INDEX texts_media_uniqueid_idx<br />
 public &#124; number_calleridgroups &#124; number_calleridgroups_dtype_idx &#124; dtype &#124; 1 &#124; number_calleridgroups_dtype_dnumber_idx &#124; dtype,dnumber &#124; 1 &#124; ALTER TABLE public.number_calleridgroups DROP INDEX number_calleridgroups_dtype_idx<br />
 public &#124; analytics_include &#124; analytics_i &#124; analytics &#124; 1 &#124; analytics_include_pkey &#124; analytics,feature,dtype,dnumber &#124; 0 &#124; ALTER TABLE public.analytics_include DROP INDEX analytics_i<br />
 public &#124; archived &#124; dupl1 &#124; category_id &#124; 1 &#124; dupl2 &#124; category_id &#124; 1 &#124; ALTER TABLE public.archived DROP INDEX dupl1<br />
 public &#124; archived &#124; dupl2 &#124; category_id &#124; 1 &#124; dupl1 &#124; category_id &#124; 1 &#124; ALTER TABLE public.archived DROP INDEX dupl2<br />
(27 rows)<br />
Source: Duplicate and redundant indices<br />
Unused indexes<br />
SQL > SELECT relid::regclass AS table, indexrelid::regclass AS index<br />
 , pg_size_pretty(pg_relation_size(indexrelid::regclass)) AS index_size<br />
 , idx_tup_read, idx_tup_fetch, idx_scan<br />
 FROM pg_stat_user_indexes<br />
 JOIN pg_index USING (indexrelid)<br />
 WHERE idx_scan = 0<br />
 AND indisunique IS FALSE<br />
;<br />
 table &#124; index &#124; index_size &#124; idx_tup_read &#124; idx_tup_fetch &#124; idx_scan<br />
------------------------+-----------------------------------------------------------------+------------+--------------+---------------+----------<br />
 customers &#124; customers_prefix_idx &#124; 16 kB &#124; 0 &#124; 0 &#124; 0<br />
 customers &#124; customers_parent_idx &#124; 16 kB &#124; 0 &#124; 0 &#124; 0<br />
 customers &#124; customers_email_idx &#124; 16 kB &#124; 0 &#124; 0 &#124; 0<br />
 customers &#124; customers_affiliate_customer_idx &#124; 16 kB &#124; 0 &#124; 0 &#124; 0<br />
 customers &#124; customers_bill_ref_idx &#124; 16 kB &#124; 0 &#124; 0 &#124; 0<br />
...<br />
 analytics_include &#124; analytics_i &#124; 8192 bytes &#124; 0 &#124; 0 &#124; 0<br />
 archived &#124; dupl1 &#124; 8192 bytes &#124; 0 &#124; 0 &#124; 0<br />
 archived &#124; dupl2 &#124; 8192 bytes &#124; 0 &#124; 0 &#124; 0<br />
(413 rows)<br />
Sources:</p>
<p>Unused Indexes<br />
Postgresql: Monitor unused indexes</p>
<p>PG Assistant<br />
At the Swiss PGDay2026(s), Bertrand Hartwig presented his tool PG Assistant. In that context, I wanted to try it out right away…<br />
PG Assistant was able to find missing Primary Keys and duplicate indexes. It didn’t show me any partially redundant or unused indexes, but that might just be on my end…</p>
<p> PG Assistant: Dashboard / Dev advisor</p>
<p> PG Assistant: Global Advisor / Dev advisor</p>
<p> PG Assistant: Strictly duplicate unused index</p>
<p>Intallation of PG Assistant<br />
$ apt update<br />
$ apt install python3 python3.13-venv unzip pip<br />
$ wget https://github.com/beh74/pgassistant-community/archive/refs/heads/main.zip<br />
$ unzip main.zip<br />
$ cd pgassistant-community-main/<br />
$ python3 -m venv env<br />
$ source env/bin/activate<br />
$ pip3 install -r requirements.txt<br />
$ export FLASK_APP=run.py<br />
$ flask run --host=0.0.0.0 --port=80<br />
Then connect to the displayed URL using a web browser.<br />
A user must be created in the database first:<br />
SQL > CREATE ROLE pgassistant WITH LOGIN SUPERUSER PASSWORD \'secret\';<br />
and the pg_hba.conf file must be adapted.<br />
Addendum<br />
You can find unused indexes using the PG Assistant as follows: Database Objects ➜ Indexes ➜ Status: Unused ➜ “NO INDEX ACTIVITY”</p>
<p> PG Assistant: Unused Indexes</p>
<p><a href="https://www.fromdual.com/blog/database_index_optimizer/">Database Index Optimizer</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Recently, a client asked me if the &ldquo;time-consuming&rdquo; task of checking indexes could be left to an index optimizer. Of course it can&hellip;</p>
<p>What exactly do we want to check?</p>
<ul>
<li>Tables without a Primary Key</li>
<li>Duplicate indexes</li>
<li>Partially redundant indexes</li>
<li>Unused indexes</li>
</ul>
<h2 id="mariadb-mysql-and-percona-server">MariaDB, MySQL, and Percona Server<a class="anchor-link" id="mariadb-mysql-and-percona-server"></a></h2>
<h3 id="tables-without-a-primary-key">Tables without a Primary Key<a class="anchor-link" id="tables-without-a-primary-key"></a></h3>
<pre><code>SQL&gt; SELECT DISTINCT t.table_schema, t.table_name
 FROM information_schema.tables AS t
 LEFT JOIN information_schema.columns AS c ON t.table_schema = c.table_schema AND t.table_name = c.table_name
 AND c.column_key = "PRI"
 WHERE t.table_schema NOT IN ('information_schema', 'mysql', 'performance_schema')
 AND c.table_name IS NULL AND t.table_type NOT IN('VIEW', 'SEQUENCE')
 AND t.table_schema = 'testtest'
;
+--------------+------------+
| table_schema | table_name |
+--------------+------------+
| testtest | archived |
+--------------+------------+
1 row in set
</code></pre>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#tables-without-primary-key" target="_blank" rel="noopener">Tables without a Primary Key</a></p>
<h3 id="duplicate-indexes">Duplicate indexes<a class="anchor-link" id="duplicate-indexes"></a></h3>
<pre><code>SQL&gt; SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index
 FROM sys.schema_redundant_indexes
 WHERE redundant_index_columns = dominant_index_columns
 AND table_schema = 'testtest'
;
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
| table_name | redundant_index_name | redundant_index_columns | dominant_index_name | dominant_index_columns | sql_drop_index |
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
| archived | dupl2 | category_id | dupl1 | category_id | ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` |
+------------+----------------------+-------------------------+---------------------+------------------------+------------------------------------------------------+
1 row in set
</code></pre>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank" rel="noopener">Duplicate and redundant indices</a></p>
<h3 id="partially-redundant-indexes">Partially redundant indexes<a class="anchor-link" id="partially-redundant-indexes"></a></h3>
<pre><code>SQL&gt; SELECT table_name, redundant_index_name, redundant_index_columns, dominant_index_name, dominant_index_columns, sql_drop_index
 FROM sys.schema_redundant_indexes
 WHERE table_schema = 'testtest'
;
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
| table_name | redundant_index_name | redundant_index_columns | dominant_index_name | dominant_index_columns | sql_drop_index |
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
| access | customer | customer | customer_2 | customer,callerid_internal | ALTER TABLE `testtest`.`access` DROP INDEX `customer` |
| access | customer | customer | customer_3 | customer,callerid_external | ALTER TABLE `testtest`.`access` DROP INDEX `customer` |
| active_customers | uniqueid | uniqueid | PRIMARY | uniqueid,scustomer | ALTER TABLE `testtest`.`active_customers` DROP INDEX `uniqueid` |
| analytics_include | analytics | analytics | PRIMARY | analytics,feature,dtype,dnumber | ALTER TABLE `testtest`.`analytics_include` DROP INDEX `analytics` |
| archived | dupl2 | category_id | dupl1 | category_id | ALTER TABLE `testtest`.`archived` DROP INDEX `dupl2` |
...
| texts_media | uniqueid | uniqueid | PRIMARY | uniqueid,filename | ALTER TABLE `testtest`.`texts_media` DROP INDEX `uniqueid` |
| unlimited_access | customer | customer | customer_2 | customer,callerid_internal | ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` |
| unlimited_access | customer | customer | customer_3 | customer,callerid_external | ALTER TABLE `testtest`.`unlimited_access` DROP INDEX `customer` |
+-------------------+----------------------+-------------------------+---------------------+---------------------------------+-------------------------------------------------------------------+
26 rows in set
</code></pre>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank" rel="noopener">Duplicate and redundant indices</a></p>
<h3 id="unused-indexes">Unused indexes<a class="anchor-link" id="unused-indexes"></a></h3>
<pre><code>SQL&gt; SELECT object_name, index_name
 FROM sys.schema_unused_indexes
 WHERE object_schema = 'testtest'
;
+------------------------+------------------------+
| object_name | index_name |
+------------------------+------------------------+
| access | customer_3 |
| access | customer_2 |
| actions | class |
| actions | action |
| active | channel |
...
| urls | customer |
| voucher_batches | customer |
| vouchers | batch |
+------------------------+------------------------+
413 rows in set
</code></pre>
<p><strong>Note</strong>:</p>
<ul>
<li>For MariaDB, the <code>PERFORMANCE_SCHEMA</code> must be enabled first.</li>
<li>The information is accurate as of the last database restart. If an index was last used BEFORE the most recent restart, it will be shown here as unused.</li>
</ul>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#unused-indexes" target="_blank" rel="noopener">Unused indexes</a></p>
<h2 id="and-now-with-postgresql">And now with PostgreSQL<a class="anchor-link" id="and-now-with-postgresql"></a></h2>
<h3 id="tables-without-a-primary-key-1">Tables without a Primary Key<a class="anchor-link" id="tables-without-a-primary-key"></a></h3>
<pre><code>SQL&gt; SELECT tab.table_schema, tab.table_name
 FROM information_schema.tables tab
 LEFT JOIN information_schema.table_constraints tco
 ON tab.table_schema = tco.table_schema
 AND tab.table_name = tco.table_name 
 AND tco.constraint_type = 'PRIMARY KEY'
 WHERE tab.table_type = 'BASE TABLE'
 AND tab.table_schema NOT IN ('pg_catalog', 'information_schema')
 AND tco.constraint_name IS NULL
 ORDER BY table_schema, table_name
;
 table_schema | table_name 
--------------+------------
 public | archived
(1 row)
</code></pre>
<p>Source: <a href="https://dataedo.com/kb/query/postgresql/find-tables-without-primary-keys" target="_blank" rel="noopener">Find tables without primary keys (PKs) in PostgreSQL database</a></p>
<h3 id="duplicate-indexes-1">Duplicate indexes<a class="anchor-link" id="duplicate-indexes"></a></h3>
<p>Based on the MySQL <code>sys</code> schema:</p>
<pre><code>SQL&gt; WITH schema_flattened_keys AS (
 SELECT sai.relid, sai.indexrelid
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name
 , CASE pi.indisunique WHEN 'f' THEN 1 ELSE 0 END AS non_unique
 , index_columns.columns AS index_columns
 FROM pg_stat_all_indexes AS sai
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid
 JOIN (
 SELECT attrelid, string_agg(attname, ',' ORDER BY attnum ASC) AS columns
 FROM pg_attribute GROUP BY attrelid
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid
 WHERE sai.schemaname NOT IN ('pg_toast', 'pg_catalog')
)
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique
 , CONCAT('ALTER TABLE ', redundant_keys.table_schema, '.', redundant_keys.table_name, ' DROP INDEX ', redundant_keys.index_name, '') AS sql_drop_index
 FROM schema_flattened_keys redundant_keys
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name
 WHERE (redundant_keys.index_name &lt;&gt; dominant_keys.index_name
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)
 AND ((redundant_keys.non_unique &gt; dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))
 )
 OR ((POSITION(CONCAT(redundant_keys.index_columns,',') IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))
 OR ((POSITION(CONCAT(dominant_keys.index_columns,',') IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))
 )
 AND redundant_keys.index_columns = dominant_keys.index_columns
;
 table_schema | table_name | redundant_index_name | redundant_index_columns | redundant_index_non_unique | dominant_index_name | dominant_index_columns | dominant_index_non_unique | sql_drop_index 
--------------+------------+----------------------+-------------------------+----------------------------+---------------------+------------------------+---------------------------+----------------------------------------------
 public | archived | dupl1 | category_id | 1 | dupl2 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl1
 public | archived | dupl2 | category_id | 1 | dupl1 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl2
(2 rows)
</code></pre>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank" rel="noopener">Duplicate and redundant indices</a></p>
<h3 id="partially-redundant-indexes-1">Partially redundant indexes<a class="anchor-link" id="partially-redundant-indexes"></a></h3>
<p>Based on the MySQL <code>sys</code> schema:</p>
<pre><code>SQL&gt; WITH schema_flattened_keys AS (
 SELECT sai.relid, sai.indexrelid
 , sai.schemaname AS table_schema, sai.relname AS table_name, sai.indexrelname AS index_name
 , CASE pi.indisunique WHEN 'f' THEN 1 ELSE 0 END AS non_unique
 , index_columns.columns AS index_columns
 FROM pg_stat_all_indexes AS sai
 JOIN pg_index AS pi ON pi.indexrelid = sai.indexrelid
 JOIN (
 SELECT attrelid, string_agg(attname, ',' ORDER BY attnum ASC) AS columns
 FROM pg_attribute GROUP BY attrelid
 ) AS index_columns ON index_columns.attrelid = sai.indexrelid
 WHERE sai.schemaname NOT IN ('pg_toast', 'pg_catalog')
)
SELECT redundant_keys.table_schema AS table_schema, redundant_keys.table_name AS table_name, redundant_keys.index_name AS redundant_index_name
 , redundant_keys.index_columns AS redundant_index_columns, redundant_keys.non_unique AS redundant_index_non_unique
 , dominant_keys.index_name AS dominant_index_name, dominant_keys.index_columns AS dominant_index_columns, dominant_keys.non_unique AS dominant_index_non_unique
 , CONCAT('ALTER TABLE ', redundant_keys.table_schema, '.', redundant_keys.table_name, ' DROP INDEX ', redundant_keys.index_name, '') AS sql_drop_index
 FROM schema_flattened_keys redundant_keys
 JOIN schema_flattened_keys dominant_keys ON redundant_keys.table_schema = dominant_keys.table_schema AND redundant_keys.table_name = dominant_keys.table_name
 WHERE (redundant_keys.index_name &lt;&gt; dominant_keys.index_name
 AND ((redundant_keys.index_columns = dominant_keys.index_columns)
 AND ((redundant_keys.non_unique &gt; dominant_keys.non_unique) OR (redundant_keys.non_unique = dominant_keys.non_unique))
 )
 OR ((POSITION(CONCAT(redundant_keys.index_columns,',') IN dominant_keys.index_columns) = 1) AND (redundant_keys.non_unique = 1))
 OR ((POSITION(CONCAT(dominant_keys.index_columns,',') IN redundant_keys.index_columns) = 1) AND (dominant_keys.non_unique = 0))
 )
;
 table_schema | table_name | redundant_index_name | redundant_index_columns | redundant_index_non_unique | dominant_index_name | dominant_index_columns | dominant_index_non_unique | sql_drop_index 
--------------+-----------------------+------------------------------------------+-------------------------+----------------------------+-------------------------------------------------+-----------------------------------------+---------------------------+---------------------------------------------------------------------------------------------
 public | numbers | numbers_customer_idx | customer | 1 | numbers_customer_text_dtype_text_dnumber_idx | customer,text_dtype,text_dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_customer_idx | customer | 1 | numbers_customer_fax_dtype_fax_dnumber_idx | customer,fax_dtype,fax_dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_customer_idx | customer | 1 | numbers_pkey | customer,stype,snumber | 0 | ALTER TABLE public.numbers DROP INDEX numbers_customer_idx
 public | numbers | numbers_dtype_idx | dtype | 1 | numbers_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.numbers DROP INDEX numbers_dtype_idx
 public | number_callers | number_callers_dtype_idx | dtype | 1 | number_callers_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_callers DROP INDEX number_callers_dtype_idx
 public | prefixes | prefixes_customer_idx | customer | 1 | prefixes_customer_dtype_dnumber_idx | customer,dtype,dnumber | 1 | ALTER TABLE public.prefixes DROP INDEX prefixes_customer_idx
 public | number_times | number_times_dtype_idx | dtype | 1 | number_times_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_times DROP INDEX number_times_dtype_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_location_idx | customer,callerid_location | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones | phones_customer_idx | customer | 1 | phones_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.phones DROP INDEX phones_customer_idx
 public | phones_hardware | phones_hardware_phone_idx | phone | 1 | phones_hardware_phone_hardware_address_idx | phone,hardware_address | 0 | ALTER TABLE public.phones_hardware DROP INDEX phones_hardware_phone_idx
 public | speeddials | speeddials_stype_idx | stype | 1 | speeddials_stype_snumber_idx | stype,snumber | 1 | ALTER TABLE public.speeddials DROP INDEX speeddials_stype_idx
 public | speeddials | speeddials_dtype_idx | dtype | 1 | speeddials_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.speeddials DROP INDEX speeddials_dtype_idx
 public | mailbox_destinations | mailbox_destinations_context_mailbox_idx | context,mailbox | 1 | mailbox_destinations_pkey | context,mailbox,dcustomer,dtype,dnumber | 0 | ALTER TABLE public.mailbox_destinations DROP INDEX mailbox_destinations_context_mailbox_idx
 public | outgroup_times | outgroup_times_outgroup_idx | outgroup | 1 | outgroup_times_outgroup_name_idx | outgroup,name | 0 | ALTER TABLE public.outgroup_times DROP INDEX outgroup_times_outgroup_idx
 public | ingroup_times | ingroup_times_ingroup_idx | ingroup | 1 | ingroup_times_ingroup_name_idx | ingroup,name | 0 | ALTER TABLE public.ingroup_times DROP INDEX ingroup_times_ingroup_idx
 public | active_customers | active_customers_uniqueid_idx | uniqueid | 1 | active_customers_pkey | uniqueid,scustomer | 0 | ALTER TABLE public.active_customers DROP INDEX active_customers_uniqueid_idx
 public | access | access_customer_idx | customer | 1 | access_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.access DROP INDEX access_customer_idx
 public | access | access_customer_idx | customer | 1 | access_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.access DROP INDEX access_customer_idx
 public | unlimited_access | unlimited_access_customer_idx | customer | 1 | unlimited_access_customer_callerid_external_idx | customer,callerid_external | 1 | ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx
 public | unlimited_access | unlimited_access_customer_idx | customer | 1 | unlimited_access_customer_callerid_internal_idx | customer,callerid_internal | 1 | ALTER TABLE public.unlimited_access DROP INDEX unlimited_access_customer_idx
 public | texts | texts_dcustomer_idx | dcustomer | 1 | texts_dcustomer_dtype_dnumber_idx | dcustomer,dtype,dnumber | 1 | ALTER TABLE public.texts DROP INDEX texts_dcustomer_idx
 public | texts_media | texts_media_uniqueid_idx | uniqueid | 1 | texts_media_pkey | uniqueid,filename | 0 | ALTER TABLE public.texts_media DROP INDEX texts_media_uniqueid_idx
 public | number_calleridgroups | number_calleridgroups_dtype_idx | dtype | 1 | number_calleridgroups_dtype_dnumber_idx | dtype,dnumber | 1 | ALTER TABLE public.number_calleridgroups DROP INDEX number_calleridgroups_dtype_idx
 public | analytics_include | analytics_i | analytics | 1 | analytics_include_pkey | analytics,feature,dtype,dnumber | 0 | ALTER TABLE public.analytics_include DROP INDEX analytics_i
 public | archived | dupl1 | category_id | 1 | dupl2 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl1
 public | archived | dupl2 | category_id | 1 | dupl1 | category_id | 1 | ALTER TABLE public.archived DROP INDEX dupl2
(27 rows)
</code></pre>
<p>Source: <a href="https://www.fromdual.com/blog/mysql-performance-schema-hints/#duplicate-and-redundant-indices" target="_blank" rel="noopener">Duplicate and redundant indices</a></p>
<h3 id="unused-indexes-1">Unused indexes<a class="anchor-link" id="unused-indexes"></a></h3>
<pre><code>SQL&gt; SELECT relid::regclass AS table, indexrelid::regclass AS index
 , pg_size_pretty(pg_relation_size(indexrelid::regclass)) AS index_size
 , idx_tup_read, idx_tup_fetch, idx_scan
 FROM pg_stat_user_indexes 
 JOIN pg_index USING (indexrelid) 
 WHERE idx_scan = 0 
 AND indisunique IS FALSE
;
 table | index | index_size | idx_tup_read | idx_tup_fetch | idx_scan 
------------------------+-----------------------------------------------------------------+------------+--------------+---------------+----------
 customers | customers_prefix_idx | 16 kB | 0 | 0 | 0
 customers | customers_parent_idx | 16 kB | 0 | 0 | 0
 customers | customers_email_idx | 16 kB | 0 | 0 | 0
 customers | customers_affiliate_customer_idx | 16 kB | 0 | 0 | 0
 customers | customers_bill_ref_idx | 16 kB | 0 | 0 | 0
...
 analytics_include | analytics_i | 8192 bytes | 0 | 0 | 0
 archived | dupl1 | 8192 bytes | 0 | 0 | 0
 archived | dupl2 | 8192 bytes | 0 | 0 | 0
(413 rows)
</code></pre>
<p>Sources:</p>
<ul>
<li><a href="https://wiki.postgresql.org/wiki/Index_Maintenance#Unused_Indexes" target="_blank" rel="noopener">Unused Indexes</a></li>
<li><a href="https://jmorano.moretrix.com/2014/02/postgresql-monitor-unused-indexes/" target="_blank" rel="noopener">Postgresql: Monitor unused indexes</a></li>
</ul>
<h3 id="pg-assistant">PG Assistant<a class="anchor-link" id="pg-assistant"></a></h3>
<p>At the <a href="https://2026.pgday.ch/schedule/" target="_blank" rel="noopener">Swiss PGDay2026</a>(s), Bertrand Hartwig presented his tool <a href="https://github.com/beh74/pgassistant-community" target="_blank" rel="noopener">PG Assistant</a>. In that context, I wanted to try it out right away&hellip;</p>
<p>PG Assistant was able to find missing Primary Keys and duplicate indexes. It didn&rsquo;t show me any partially redundant or unused indexes, but that might just be on my end&hellip;</p>
<figure>
 <a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111018.png" title="full size"><img decoding="async" src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111018_640x509.png" alt="pgAssistant-1"></a><figcaption>PG Assistant: Dashboard / Dev advisor</figcaption></figure>
<p></p>
<figure>
 <a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111135.png" title="full size"><img decoding="async" src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111135_640x533.png" alt="pgAssistant-2"></a><figcaption>PG Assistant: Global Advisor / Dev advisor</figcaption></figure>
<p></p>
<figure>
 <a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111230.png" title="full size"><img decoding="async" src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260715_111230_640x532.png" alt="pgAssistant-3"></a><figcaption>PG Assistant: Strictly duplicate unused index</figcaption></figure>
<p></p>
<h4 id="intallation-of-pg-assistant">Intallation of PG Assistant</h4>
<pre><code>$ apt update
$ apt install python3 python3.13-venv unzip pip
$ wget https://github.com/beh74/pgassistant-community/archive/refs/heads/main.zip
$ unzip main.zip 
$ cd pgassistant-community-main/
$ python3 -m venv env
$ source env/bin/activate
$ pip3 install -r requirements.txt
$ export FLASK_APP=run.py
$ flask run --host=0.0.0.0 --port=80
</code></pre>
<p>Then connect to the displayed URL using a web browser.</p>
<p>A user must be created in the database first:</p>
<pre><code>SQL&gt; CREATE ROLE pgassistant WITH LOGIN SUPERUSER PASSWORD 'secret';
</code></pre>
<p>and the <code>pg_hba.conf</code> file must be adapted.</p>
<h2 id="addendum">Addendum<a class="anchor-link" id="addendum"></a></h2>
<p>You can find unused indexes using the PG Assistant as follows: Database Objects &#10140; Indexes &#10140; Status: Unused &#10140; &ldquo;NO INDEX ACTIVITY&rdquo;</p>
<figure>
 <a href="https://www.fromdual.com/images/pgAssistant_Screenshot_20260716_093730.png" title="volle Gr&ouml;sse"><img decoding="async" src="https://www.fromdual.com/images/pgAssistant_Screenshot_20260716_093730_640x459.png" alt="pgAssistant-4"></a><figcaption>PG Assistant: Unused Indexes</figcaption></figure>
<p></p>

<p><a href="https://www.fromdual.com/blog/database_index_optimizer/">Database Index Optimizer</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL Meta Commands that save time every day</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/postgresql-meta-commands-that-save-time-every-day/" />
      <id>https://www.percona.com/blog/postgresql-meta-commands-that-save-time-every-day/</id>
      <updated>2026-07-15T12:44:13+03:00</updated>
      <author><name>Sonia Valeja</name></author>
      <summary type="html"><![CDATA[<p>When most people start working with PostgreSQL, they quickly learn SQL: [crayon-6a57891fbdcdf304804307/] But very soon, another world opens up inside psql — a set of commands that don’t look like SQL, don’t end with semicolons. These are PostgreSQL Meta Commands, and they quietly power the daily workflow of almost every experienced DBA. Meta commands are … Continued<br />
The post PostgreSQL Meta Commands that save time every day appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/postgresql-meta-commands-that-save-time-every-day/">PostgreSQL Meta Commands that save time every day</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">When most people start working with PostgreSQL, they quickly learn SQL:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">SELECT * FROM employees;</pre>
<p><span style="font-weight: 400">But very soon, another world opens up inside </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> &mdash; a set of commands that don&rsquo;t look like SQL, don&rsquo;t end with semicolons.</span></p>
<p><span style="font-weight: 400">These are </span><b>PostgreSQL Meta Commands</b><span style="font-weight: 400">, and they quietly power the daily workflow of almost every experienced DBA.</span></p>
<p><span style="font-weight: 400">Meta commands are not about querying data &mdash; they are about </span><b>navigating, inspecting, and controlling the PostgreSQL session/database efficiently</b><span style="font-weight: 400">.</span></p>
<h3><span style="font-weight: 400">What exactly are Meta Commands?</span><a class="anchor-link" id="what-exactly-are-meta-commands"></a></h3>
<p><span style="font-weight: 400">Meta commands are special instructions interpreted by </span><span style="font-weight: 400">psql</span><span style="font-weight: 400">, not PostgreSQL itself.</span></p>
<p><span style="font-weight: 400">That means:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">They are </span><b>not SQL</b></li>
<li style="font-weight: 400"><span style="font-weight: 400">They execute instantly on the client side</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">They are specific to the </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> terminal tool</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">They do not end with semicolon like SQL statements</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The main focus area for meta commands is database interaction and not the interaction with the data in the database.</span></li>
</ul>
<h3><strong>Cheat Sheet (Quick Reference)&nbsp;</strong><a class="anchor-link" id="cheat-sheet-quick-reference"></a></h3>
<p><span style="font-weight: 400">The most commonly used meta commands are as follows. There are many more apart from these, however, below are the most frequently used ones:</span></p>
<h3><span style="font-weight: 400">Connect and Manage Sessions</span><a class="anchor-link" id="connect-and-manage-sessions"></a></h3>
<p><span style="font-weight: 400">These commands help discover databases, establish connections, and verify the current session.</span></p>
<table style="height: 33px" border="1" width="701">
<tbody>
<tr>
<td><span style="font-weight: 400">c</span></td>
<td><span style="font-weight: 400">Connect to another database&nbsp;</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">l</span></td>
<td><span style="font-weight: 400">List all the databases available in the cluster</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">l+</span></td>
<td><span style="font-weight: 400">List all the databases available in the cluster with more details, like DB Size, etc</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">conninfo</span></td>
<td><span style="font-weight: 400">Displays information about the current database connection</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400">Please find the example of the commands used to connect and manage sessions in the screenshot below:</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-50262" src="https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.39.10-AM-300x134.png" alt="" width="300" height="134"></p>
<h3><span style="font-weight: 400">Inspect Database Objects </span><span style="font-weight: 400">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><a class="anchor-link" id="inspect-database-objects"></a></h3>
<p><span style="font-weight: 400">The</span><span style="font-weight: 400"><br>
			<span id="urvanov-syntax-highlighter-6a57891fbdce5734642619" class="urvanov-syntax-highlighter-syntax urvanov-syntax-highlighter-syntax-inline  crayon-theme-classic crayon-theme-classic-inline urvanov-syntax-highlighter-font-monaco" style="font-size: 12px !important;line-height: 15px !important;font-size: 12px !important"><span class="crayon-pre urvanov-syntax-highlighter-code" style="font-size: 12px !important;line-height: 15px !important;font-size: 12px !important"><span class="crayon-sy"></span><span class="crayon-v">d</span></span></span>&nbsp;</span><span style="font-weight: 400"> family of commands is one of the most powerful features of<br>
			<span id="urvanov-syntax-highlighter-6a57891fbdce8854379316" class="urvanov-syntax-highlighter-syntax urvanov-syntax-highlighter-syntax-inline  crayon-theme-classic crayon-theme-classic-inline urvanov-syntax-highlighter-font-monaco" style="font-size: 12px !important;line-height: 15px !important;font-size: 12px !important"><span class="crayon-pre urvanov-syntax-highlighter-code" style="font-size: 12px !important;line-height: 15px !important;font-size: 12px !important"><span class="crayon-v">psql</span></span></span>&nbsp;</span><span style="font-weight: 400">. These commands can be used to discover database objects, inspect their definitions, and view additional metadata.</span></p>
<table border="1">
<tbody>
<tr>
<td><span style="font-weight: 400">d</span></td>
<td><span style="font-weight: 400">Describe database objects or list objects visible in the current search path.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">d object_name</span></td>
<td><span style="font-weight: 400">Describe a specific table, view, sequence, or other database object.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">d+ object_name</span></td>
<td><span style="font-weight: 400">Display extended information about an object.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">dt</span></td>
<td><span style="font-weight: 400">List tables. Supports schema names and wildcard patterns.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">di</span></td>
<td><span style="font-weight: 400">List indexes. Supports wildcard patterns.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">dn</span></td>
<td><span style="font-weight: 400">List schemas in the current database.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">du</span></td>
<td><span style="font-weight: 400">List database roles.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">db</span></td>
<td><span style="font-weight: 400">List tablespaces</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">dx</span></td>
<td><span style="font-weight: 400">List installed extensions</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">df</span></td>
<td><span style="font-weight: 400">List functions and procedures</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">sf function name</span></td>
<td><span style="font-weight: 400">Displays the source code of the specific function/procedure</span></td>
</tr>
</tbody>
</table>
<h4></h4>
<h4><span style="font-weight: 400">Using object names and wildcards</span></h4>
<p><span style="font-weight: 400">Most object-inspection commands accept object names, schema-qualified names, and wildcard patterns.</span></p>
<p><span style="font-weight: 400">For example:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">dt</pre>
<p><span style="font-weight: 400">Lists all tables in the current search path.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">dt public.*</pre>
<p><span style="font-weight: 400">Lists all tables in the public schema.</span></p>
<p><span style="font-weight: 400">The same pattern matching is supported by several other meta-commands, including </span><span style="font-weight: 400">di, df,</span><span style="font-weight: 400"> and the</span><span style="font-weight: 400"> d</span><span style="font-weight: 400"> family.</span></p>
<p><span style="font-weight: 400">Please find the example of the </span><span style="font-weight: 400">d </span><span style="font-weight: 400">family</span> <span style="font-weight: 400">commands in the screenshot below:</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-50263" src="https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.43.18-AM-300x103.png" alt="" width="300" height="103"></p>
<h3><span style="font-weight: 400">Format Query Results</span><a class="anchor-link" id="format-query-results"></a></h3>
<p><span style="font-weight: 400">Several meta-commands are available to improve the readability of query output, particularly when working with wide result sets.</span></p>
<table border="1">
<tbody>
<tr>
<td><span style="font-weight: 400">x [on|off|auto]</span></td>
<td><span style="font-weight: 400">Toggle expanded (vertical) display</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">o filename</span></td>
<td><span style="font-weight: 400">Redirect query output to a file or pipe.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">o</span></td>
<td><span style="font-weight: 400">Restore query output to the terminal.</span></td>
</tr>
</tbody>
</table>
<h3><a class="anchor-link" id=""></a></h3>
<h3><span style="font-weight: 400">Monitor Query Executions</span><a class="anchor-link" id="monitor-query-executions"></a></h3>
<p><span style="font-weight: 400">These commands assist in measuring query performance and repeatedly executing queries for monitoring purposes.</span></p>
<table border="1">
<tbody>
<tr>
<td><span style="font-weight: 400">timing [on|off]</span></td>
<td><span style="font-weight: 400">Toggle Query execution timing</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">watch seconds</span></td>
<td><span style="font-weight: 400">Re-execute the current query at the specified interval</span></td>
</tr>
</tbody>
</table>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-50264" src="https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.52.04-AM-300x125.png" alt="" width="300" height="125"></p>
<h3><span style="font-weight: 400">Execute and Automate tasks</span><a class="anchor-link" id="execute-and-automate-tasks"></a></h3>
<p><span style="font-weight: 400">These commands simplify repetitive tasks and enable integration between</span><span style="font-weight: 400"> psql,</span><span style="font-weight: 400"> SQL scripts, and the operating system</span></p>
<table border="1">
<tbody>
<tr>
<td><span style="font-weight: 400">i filename</span></td>
<td><span style="font-weight: 400">Execute the commands from the file</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">gexec</span></td>
<td><span style="font-weight: 400">Execute each field returned by a query as an SQL statement.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">! command</span></td>
<td><span style="font-weight: 400">Execute a shell command without leaving a </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> prompt</span></td>
</tr>
</tbody>
</table>
<p><img decoding="async" loading="lazy" class="alignnone size-medium wp-image-50265" src="https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.53.25-AM-300x75.png" alt="" width="300" height="75"></p>
<h3><span style="font-weight: 400">Get Help</span><a class="anchor-link" id="get-help"></a></h3>
<p><span style="font-weight: 400">Built-in help commands provide quick access to both </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> meta-command documentation and PostgreSQL SQL syntax without leaving the terminal.</span></p>
<table border="1">
<tbody>
<tr>
<td><span style="font-weight: 400">?</span></td>
<td><span style="font-weight: 400">Display all available </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> meta-commands.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">h</span></td>
<td><span style="font-weight: 400">List SQL commands for which syntax help is available.</span></td>
</tr>
<tr>
<td><span style="font-weight: 400">h command</span></td>
<td><span style="font-weight: 400">Display syntax help for a specific SQL command.</span></td>
</tr>
</tbody>
</table>
<h3><a class="anchor-link" id=""></a></h3>
<h3><span style="font-weight: 400">What is .psqlrc?</span><a class="anchor-link" id="what-is-psqlrc"></a></h3>
<p><span style="font-weight: 400">.psqlrc</span><span style="font-weight: 400"> is a startup file in the home directory that </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> reads when a session begins. It can hold meta-commands and SQL that run before the first prompt. The main benefit is consistent defaults &mdash; timing, formatting, and a custom prompt &mdash; without repeating setup each time, which speeds daily work and reduces connection mistakes across databases.</span></p>
<p><span style="font-weight: 400">A minimal</span><span style="font-weight: 400"> .psqlrc</span><span style="font-weight: 400"> might look like this:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">timing on 
x auto</pre>
<p><span style="font-weight: 400">These settings load automatically on every new</span> <span style="font-weight: 400">psql</span><span style="font-weight: 400"> session as highlighted below:</span></p>
<p><img decoding="async" loading="lazy" class="alignnone size-medium wp-image-50266" src="https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.54.34-AM-300x77.png" alt="" width="300" height="77"></p>
<h3><span style="font-weight: 400">Conclusion</span><a class="anchor-link" id="conclusion"></a></h3>
<p><span style="font-weight: 400">PostgreSQL is powerful because of SQL &mdash; but for DBAs, </span><span style="font-weight: 400">psql</span><span style="font-weight: 400"> meta commands make daily management far easier and more efficient.</span></p>
<p><span style="font-weight: 400">Most developers use only a handful like </span><span style="font-weight: 400">dt</span><span style="font-weight: 400"> or </span><span style="font-weight: 400">d</span><span style="font-weight: 400">. But experienced DBAs rely on a much broader toolkit to:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Investigate production issues faster</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Navigate systems efficiently</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Reduce reliance on repetitive SQL</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Repetitive tasks can be automated</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Debug complex problems quickly</span></li>
</ul>
<p><span style="font-weight: 400">An easy way to understand the relationship between SQL and PostgreSQL meta commands is to compare them to driving a car.</span></p>
<p><b>SQL is like driving the car</b><span style="font-weight: 400"> &mdash; it is the primary means of reaching a destination. It is used to retrieve, insert, update, and delete data, enabling applications and users to interact with the information stored in the database.</span></p>
<p><b>Meta commands, on the other hand, are like the car&rsquo;s dashboard.</b><span style="font-weight: 400"> While the dashboard does not move the vehicle, it provides essential information such as speed, fuel level, engine health, navigation status, and warning indicators. Driving without a dashboard is certainly possible, but it would mean operating with limited visibility into the vehicle&rsquo;s condition and performance.</span></p>
<p><span style="font-weight: 400">Similarly, SQL is responsible for manipulating and retrieving data, whereas PostgreSQL meta commands provide valuable insight into the database environment itself. They help administrators inspect database objects, navigate schemas, monitor sessions, examine roles and privileges, review object definitions, and perform numerous administrative tasks efficiently.</span></p>
<p><span style="font-weight: 400">In essence, SQL enables interaction with the </span><b>data</b><span style="font-weight: 400">, while meta commands enable interaction with the </span><b>PostgreSQL environment</b><span style="font-weight: 400">. Together, they form a complementary toolkit that allows database professionals to work more effectively, troubleshoot issues faster, and administer PostgreSQL with greater confidence.</span></p>
<p>The post <a href="https://www.percona.com/blog/postgresql-meta-commands-that-save-time-every-day/">PostgreSQL Meta Commands that save time every day</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/postgresql-meta-commands-that-save-time-every-day/">PostgreSQL Meta Commands that save time every day</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Inside MySQL 9.7 LTS Features</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/inside-mysql-9-7-lts-features/" />
      <id>https://www.percona.com/blog/inside-mysql-9-7-lts-features/</id>
      <updated>2026-07-15T05:00:23+03:00</updated>
      <author><name>Anil Joshi</name></author>
      <summary type="html"><![CDATA[<p>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 … Continued<br />
The post Inside MySQL 9.7 LTS Features appeared first on Percona.</p>
<p><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://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">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 style="font-weight: 400">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 style="font-weight: 400">Let&rsquo;s discuss each of these features below with some examples and usage.</span></p>
<h2><span style="font-weight: 400">Flow-control monitoring in Group Replication</span><a class="anchor-link" id="flow-control-monitoring-in-group-replication"></a></h2>
<p><span style="font-weight: 400">Flow control monitoring has been improved and provides more granularity by introducing the additional status variables listed below.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Gr_flow_control_throttle_count : It denotes the number of transactions that have been throttled.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Gr_flow_control_throttle_time_sum :It denotes the time in microseconds that transactions have been throttled.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Gr_flow_control_throttle_active_count :It denotes the number of transactions currently being throttled.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">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 style="font-weight: 400">To use these status variables, we must install the &ldquo;</span><b>Group Replication Flow Control Statistics&rdquo;&nbsp; </b><span style="font-weight: 400">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 style="font-weight: 400">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>

<h2><span style="font-weight: 400">Multi-threaded applier extended statistics</span><a class="anchor-link" id="multi-threaded-applier-extended-statistics"></a></h2>
<p><span style="font-weight: 400">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 style="font-weight: 400">This requires installing the &ldquo;</span><b>Replication Applier Metrics&rdquo; </b><span style="font-weight: 400">component.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; Install component 'file://component_replication_applier_metrics';</pre>
<p><span style="font-weight: 400">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 &ldquo;</span><b>replication_applier_metrics&rdquo;</b><span style="font-weight: 400"> 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 style="font-weight: 400">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>&ldquo;replication_applier_progress_by_worker&rdquo;</b><span style="font-weight: 400"> 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_workerG;
*************************** 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>

<h2><span style="font-weight: 400">Automatic eviction &amp; rejoin</span><a class="anchor-link" id="automatic-eviction-rejoin"></a></h2>
<p><span style="font-weight: 400">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&rsquo;s high availability and overall performance.</span></p>
<p><span style="font-weight: 400">This requires installing the &ldquo;</span><b>group replication resource manager&rdquo;</b><span style="font-weight: 400"> component.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">mysql&gt; INSTALL COMPONENT 'file://component_group_replication_resource_manager';</pre>
<p><span style="font-weight: 400">Once the component is available,&nbsp; we can use various options to decide the node expulsion policy.</span></p>
<p><b>1) Applier channel</b></p>
<p><span style="font-weight: 400">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 style="font-weight: 400">If lag exceeds&nbsp; &ldquo;</span><b>applier_channel_lag&rdquo;</b><span style="font-weight: 400">&nbsp;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 style="font-weight: 400"><br>
</span><b>2)</b> <b>Recovery Channel</b><b></b></p>
<p><span style="font-weight: 400">Similarly, we can define a threshold for the group member recovery process to attempt to rejoin the cluster.&nbsp;</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 style="font-weight: 400">If the secondary&rsquo;s recovery lag exceeds &ldquo;</span><strong>recovery_channel_lag&rdquo;</strong><span style="font-weight: 400">, 10 times or more in succession, the server is expelled from the group.&nbsp;</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 style="font-weight: 400">We can also define an expelled condition based on the group member&rsquo;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 style="font-weight: 400">If the memory usage exceeds </span><strong>memory_used_limit</strong><span style="font-weight: 400">&nbsp;% 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 style="font-weight: 400">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 style="font-weight: 400">The expelled node can attempt to automatically rejoin based on the value of the </span><b>group_replication_autorejoin_tries</b><span style="font-weight: 400"> 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 style="font-weight: 400">If the node cannot join, it will perform the behaviour specified in the </span><b>group_replication_exit_state_action </b><span style="font-weight: 400">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 style="font-weight: 400">After a server is evicted from the group (for whatever reason), it gets a </span><b>grace period</b><span style="font-weight: 400"> (</span><b>group_replication_resource_manager</b><span style="font-weight: 400">) when it rejoins. During this period, the Resource Manager won&rsquo;t immediately kick it out again, even if it&rsquo;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>

<h2><span style="font-weight: 400">Up-to-date aware Primary election</span><a class="anchor-link" id="up-to-date-aware-primary-election"></a></h2>
<p><span style="font-weight: 400">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 style="font-weight: 400">Here is how the Group Replication Manager performs the most up-to-date primary selection prior to MySQL v9.7.</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">The lowest MySQL version is checked for each member.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">If more than one member is running the lowest MySQL Server version, each member&rsquo;s weight is determined by the &ldquo;</span>group_replication_member_weight&rdquo;<span style="font-weight: 400">&nbsp;system variable.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">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 &ldquo;</span>server_uuid&rdquo;<span style="font-weight: 400">&nbsp;of each group member. The member with the lowest server UUID is chosen as the new primary.<br>
</span></li>
</ol>
<p><span style="font-weight: 400">In MySQL version 9.7, &ldquo;</span><b>group_replication_elect_prefers_most_updated&rdquo;</b><span style="font-weight: 400">&nbsp;was introduced, so the failover will be determined by </span><b>how many transactions are in the secondary backlog</b><span style="font-weight: 400">. Basically the secondary with the least backlog will be selected as Primary.</span></p>
<p><span style="font-weight: 400">Now, it will consider the</span><b> &ldquo;most up-to-date&rdquo; </b><span style="font-weight: 400">node first,</span> <span style="font-weight: 400">then &ldquo;</span><b>weight&rdquo;</b><span style="font-weight: 400"> and then &ldquo;</span><b>UUID&rdquo;</b><span style="font-weight: 400">.&nbsp;</span></p>
<p><span style="font-weight: 400">To use &ldquo;</span><b>group_replication_elect_prefers_most_updated&rdquo;</b><span style="font-weight: 400">, we need to install the &ldquo;</span><b>Group Replication Primary Election</b><span style="font-weight: 400">&rdquo; 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 style="font-weight: 400">By default, the most up-to-date group member selection is enabled. We need to make sure it&rsquo;s enabled on all Group Members.&nbsp;</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 style="font-weight: 400">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 style="font-weight: 400">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 style="font-weight: 400">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>

<h2><span style="font-weight: 400">MySQL JSON duality views</span><a class="anchor-link" id="mysql-json-duality-views"></a></h2>
<p><span style="font-weight: 400">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 style="font-weight: 400">Let&rsquo;s see a quick scenario below on how it works.</span></p>
<p><span style="font-weight: 400">Below are two relational tables from which we obtain aggregated information in JSON format.&nbsp;</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>

<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 style="font-weight: 400">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 style="font-weight: 400">,</span><b>v_product_type</b><span style="font-weight: 400">,</span><b>v_product_type</b><span style="font-weight: 400"> ), and the complete details of the</span><b> product details </b><span style="font-weight: 400">table are fetched into the (</span><b>product</b><span style="font-weight: 400">) 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>

<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 style="font-weight: 400">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>

<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 style="font-weight: 400">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>

<h2><span style="font-weight: 400">Hypergraph Optimizer</span><a class="anchor-link" id="hypergraph-optimizer"></a></h2>
<p><span style="font-weight: 400">With the Hypergraph Optimiser, we now have more </span><span style="font-weight: 400">advanced optimisation for complex queries and a broader set of Join plans than the older traditional method, missing earlier. By using &ldquo;</span><b>Join hypergraph</b><span style="font-weight: 400">&rdquo;, 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>

<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 style="font-weight: 400"><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>

<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 style="font-weight: 400">We can see that with &ldquo;</span><b>hypergraph_optimizer=enabled&rdquo;, </b><span style="font-weight: 400">the query execution time is almost 8x faster.</span></p>
<p><span style="font-weight: 400">The performance difference might not be noticeable with a few joins or a smaller table&rsquo;s data set, but with more complex joins, it can yield better performance. In the above example, we can see that when</span><b> &ldquo;hypergraph_optimizer=enabled&rdquo;</b><span style="font-weight: 400">, the optimiser replaces &ldquo;</span><b>Nested loop inner join</b><span style="font-weight: 400">&rdquo; with &ldquo;</span><b>Inner</b> <b>hash join</b><span style="font-weight: 400">&rdquo;, which is generally better for large datasets.&nbsp;</span></p>
<h2><span style="font-weight: 400">Higher version source allowed</span><a class="anchor-link" id="higher-version-source-allowed"></a></h2>
<p><span style="font-weight: 400">Now, it&rsquo;s possible that a lower version replica can connect to a higher version source when the major versions differ. That means we don&rsquo;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 style="font-weight: 400">Of course, we have to be cautious not to run any such feature or change on the source that doesn&rsquo;t support lower-version replicas.</span></p>
<p><b>Please note &ndash;</b><span style="font-weight: 400"> This won&rsquo;t be applicable to previous releases, say (8.4, 8.0), as they didn&rsquo;t restrict such replication connectivity. It would be useful for 9.7 or the next major release.</span></p>
<p><span style="font-weight: 400">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>

<h2><a class="anchor-link" id=""></a></h2>
<h2><span style="font-weight: 400">Summary</span><a class="anchor-link" id="summary"></a></h2>
<p><span style="font-weight: 400">The above discussion highlights key advancements in MySQL 9.7 LTS, ranging from some innovative or operational improvements to developer-centric features such as &ldquo;JSON Duality&rdquo; Views. Also, the &ldquo;Hypergraph Optimiser&rdquo; is now available for community release, which was previously exclusive to MySQL Heatwave/Enterprise.&nbsp; 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 style="font-weight: 400">: It&rsquo;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 style="font-weight: 400"> higer severity CVE&rsquo;s</span></a><span style="font-weight: 400">. If you are using </span><b>Percona Server for MySQL (PS), </b><span style="font-weight: 400">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 style="font-weight: 400">and are shipping the fixed 9.7.1 version directly</span><b>.</b></p>
<p><b>Still, it&rsquo;s highly recommended</b><span style="font-weight: 400"> 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 style="font-weight: 400">&nbsp;</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>

<p><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://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Hidden Gem: Online Schema Change without pt-osc</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/" />
      <id>https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/</id>
      <updated>2026-07-14T09:12:43+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>When people hear “online schema change” in the MySQL and MariaDB world, many immediately think about pt-online-schema-change. And for good reasons: for years, changing a large table in production was one of those tasks that could ruin your day. …<br />
Continue reading \"MariaDB Hidden Gem: Online Schema Change without pt-osc\"<br />
The post MariaDB Hidden Gem: Online Schema Change without pt-osc appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/">MariaDB Hidden Gem: Online Schema Change without pt-osc</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When people hear &ldquo;online schema change&rdquo; in the MySQL and MariaDB world, many immediately think about pt-online-schema-change. And for good reasons: for years, changing a large table in production was one of those tasks that could ruin your day. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Hidden Gem: Online Schema Change without pt-osc&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/">MariaDB Hidden Gem: Online Schema Change without pt-osc</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-hidden-gem-online-schema-change-without-pt-osc/">MariaDB Hidden Gem: Online Schema Change without pt-osc</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MyDumper Locking Mechanisms Revisited: Introducing SAFE_NO_LOCK</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/mydumper-locking-mechanisms-revisited-introducing-safe_no_lock/" />
      <id>https://www.percona.com/blog/mydumper-locking-mechanisms-revisited-introducing-safe_no_lock/</id>
      <updated>2026-07-13T12:24:41+03:00</updated>
      <author><name>David Ducos</name></author>
      <summary type="html"><![CDATA[<p>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 … Continued<br />
The post MyDumper Locking Mechanisms Revisited: Introducing SAFE_NO_LOCK appeared first on Percona.</p>
<p><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://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![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&rsquo;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&rsquo;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>?<a class="anchor-link" id="what-is-sync-thread-lock-mode"></a></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&rsquo;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<a class="anchor-link" id="understanding-safe_no_lock"></a></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 style="font-weight: 400">When using NO_LOCK, if the threads don&rsquo;t actually hit the same point in time&mdash;meaning they fail to synchronize&mdash;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<a class="anchor-link" id="choosing-the-right-mode"></a></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)<a class="anchor-link" id="auto-the-default"></a></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)<a class="anchor-link" id="ftwrl-flush-tables-with-read-lock"></a></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 style="font-weight: 400">Downside: It </span><b>blocks writes</b><span style="font-weight: 400"> across the entire instance during synchronization, which can cause a queue cascade </span><b>on a busy production server</b><span style="font-weight: 400">.</span></p>
<h3>GTID<a class="anchor-link" id="gtid"></a></h3>
<p><span style="font-weight: 400">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<a class="anchor-link" id="safe_no_lock"></a></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 style="font-weight: 400"><span style="font-weight: 400">On highly sensitive production systems, where </span><b>global write locks are absolutely forbidden</b><span style="font-weight: 400"> 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 style="font-weight: 400">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<a class="anchor-link" id="no_lock"></a></h3>
<p>What it does: Attempts lockless synchronization but logs a warning and proceeds even if consistency fails.</p>
<p><span style="font-weight: 400">When to use it: Rarely, if ever, in the production primary server. It is </span><b>acceptable for staging environments</b><span style="font-weight: 400">, 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<a class="anchor-link" id="lock_all"></a></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<a class="anchor-link" id="conclusion"></a></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>

<p><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://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Running DuckDB as a MySQL 9.7 storage engine</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/running-duckdb-as-a-mysql-9-7-storage-engine/" />
      <id>https://www.percona.com/blog/running-duckdb-as-a-mysql-9-7-storage-engine/</id>
      <updated>2026-07-10T13:38:16+03:00</updated>
      <author><name>Evgeniy Patlan</name></author>
      <summary type="html"><![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 … Continued<br />
The post Running DuckDB as a MySQL 9.7 storage engine appeared first on Percona.</p>
<p><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://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p align="justify"><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">ducksdb-mysql-engine</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">is an experimental build of MySQL 9.7 where a table you mark </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">ENGINE=DuckDB</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">It&rsquo;s an experiment, not production software. It patches </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">mysqld</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">and has rough edges, which we list at the end. Source is on GitHub under GPLv2:</span></span></span><a href="https://github.com/EvgeniyPatlan/ducksdb-mysql-engine"><span style="color: #000000"> &nbsp; &nbsp; </span><span style="color: #1155cc"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><u>https://github.com/EvgeniyPatlan/ducksdb-mysql-engine</u></span></span></span></a><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">.</span></span></span></p>
<h2 class="western" align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>Why we made it</b></span></span></span><a class="anchor-link" id="why-we-made-it"></a></h2>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">MySQL is great for transactions and slow at analytics. A wide </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">GROUP BY</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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&rsquo;re running two systems and the pipeline between them.</span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">We wanted the table itself to be the column store, with the heavy queries offloaded for you. Mark it </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">ENGINE=DuckDB</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">, query it the way you always have, and DuckDB does the analytical work.</span></span></span></p>
<h2 class="western" align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>What it actually is</b></span></span></span><a class="anchor-link" id="what-it-actually-is"></a></h2>
<p align="justify"><a href="https://duckdb.org/"><span style="color: #1155cc"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><u>DuckDB</u></span></span></span></a> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">is an in-process columnar query engine, basically SQLite for OLAP. It stores data by column and it&rsquo;s built for scans and aggregations, which is exactly what a row store is bad at.</span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">We&rsquo;re not the first to put it behind a relational table. Alibaba&rsquo;s</span></span></span><a href="https://github.com/alibaba/AliSQL"> <span style="color: #1155cc"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><u>AliSQL</u></span></span></span></a> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">has had a built-in DuckDB engine for a while. MariaDB shipped</span></span></span><a href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/"> <span style="color: #1155cc"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><u>MariaDB DuckDB</u></span></span></span></a> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">about a week ago, while we were already building ours. 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&rsquo;s in the benchmarks below.</span></span></span></p>
<h2 class="western" align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>How it hooks into MySQL</b></span></span></span><a class="anchor-link" id="how-it-hooks-into-mysql"></a></h2>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">MySQL doesn&rsquo;t have a </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">select_handler</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">, the API MariaDB uses to grab a whole </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">SELECT</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">and run it inside an engine. We added our own: a </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">handlerton::pushdown_select</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">hook.</span></span></span></p>
<p><img loading="lazy" 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"></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><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></span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">The engine is compiled into </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">mysqld</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">, and each schema is one DuckDB file under the datadir. Three patches do the integration, and all three are generic, so they&rsquo;ll fire for any engine that exposes the hook:</span></span></span></p>
<ul>
<li><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">The hook runs at the end of <span style="font-family: Times New Roman, serif">JOIN::optimize()</span>. If every base table in the block is one engine that has the hook, that engine looks at the optimized <span style="font-family: Times New Roman, serif">JOIN</span>, and if it can translate the whole query it sets <span style="font-family: Times New Roman, serif">JOIN::override_executor_func</span> (which the executor already checks in <span style="font-family: Times New Roman, serif">sql_union.cc</span>). The query gets regenerated as DuckDB SQL, prepared once, run, and the aggregated result is staged into a temp table. <span style="font-family: Times New Roman, serif">EXPLAIN</span> is left alone.</span></span></span></li>
<li><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">A server-side <span style="font-family: Times New Roman, serif">LOAD DATA INFILE</span> goes into a DuckDB <span style="font-family: Times New Roman, serif">COPY</span> instead of crawling through <span style="font-family: Times New Roman, serif">write_row</span> row by row. At 600M rows that&rsquo;s a 20-minute load instead of 80.</span></span></span></li>
<li><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">For single-engine statements we clear <span style="font-family: Times New Roman, serif">OPTIMIZER_SWITCH_SEMIJOIN</span> in <span style="font-family: Times New Roman, serif">prepare</span>, so <span style="font-family: Times New Roman, serif">IN</span>, <span style="font-family: Times New Roman, serif">EXISTS</span>, <span style="font-family: Times New Roman, serif">NOT IN</span> and <span style="font-family: Times New Roman, serif">NOT EXISTS</span> stay as subqueries the builder can render instead of getting rewritten into semijoin nests it can&rsquo;t recognize.</span></span></span></li>
</ul>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">The builder only renders a node when the output is provably identical to what MySQL would return. If it can&rsquo;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 </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">REAL</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<p>&nbsp;</p>
<h2 class="western"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>Getting started</b></span></span></span><a class="anchor-link" id="getting-started"></a></h2>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">The fastest way in is the image:</span></span></span></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 
evgeniypatlan/test-images:mysql-9.7-duckdb-v0.2.0</pre>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Make a table, put a few rows in, run an aggregate:</span></span></span></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>

<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>&nbsp;</p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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 </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">Ducksdb_pushdown_count</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">status variable:</span></span></span></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>&nbsp;</p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">If you would rather build it, you need the MySQL 9.7 tree under </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">vendor/mysql-server/</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">and a DuckDB prefix, then:</span></span></span></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>&nbsp;</p>
<h2 class="western"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>Does it actually go fast?</b></span></span></span><a class="anchor-link" id="does-it-actually-go-fast"></a></h2>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<h3 class="western"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: medium"><b>SF10, around 60 million lineitem rows</b></span></span></span><a class="anchor-link" id="sf10-around-60-million-lineitem-rows"></a></h3>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">A handful of rows here; the whole table is in </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">docs/tpch_engine_comparison.md</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">:</span></span></span></p>
<table width="643" cellspacing="0" cellpadding="7">
<tbody>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Query</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">InnoDB</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: medium">MySQL+DuckDB</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: medium">MariaDB +DuckDB</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Native DuckDB</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q1</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">&gt;180</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.77</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.84</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.77</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q5</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">127.6</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.53</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.46</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.71</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q7</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">145.0</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.45</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.38</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.67</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q9</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">&gt;180</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.52</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">2.30</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.78</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q18</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">101.7</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.35</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.39</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">1.31</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Q19</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">120.4</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.15</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.67</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">0.83</span></span></span></td>
</tr>
<tr>
<td width="58"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">All 22&nbsp;</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Finished 16/22</span></span></span></td>
<td width="118"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">15.1s</span></span></span></td>
<td width="144"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">13.3s</span></span></span></td>
<td width="125"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">16.3</span></span></span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p align="center"><img loading="lazy" 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"></p>
<p align="center"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><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></span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<h3 class="western"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: medium"><b>SF100, around 600 million lineitem rows</b></span></span></span><a class="anchor-link" id="sf100-around-600-million-lineitem-rows"></a></h3>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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:</span></span></span></p>
<table width="640" cellspacing="0" cellpadding="7">
<tbody>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Query</span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: medium">MySQL+DuckDB</span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: medium">MariaDB+DuckDB</span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">Native DuckDB</span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q1</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>15.25</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>6.50</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>5.50</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q9</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>20.97</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>115.29</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>19.54</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q10</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>10.64</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>ERR</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>8.14</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q13</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>19.75</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>ERR</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>13.27</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q18</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>14.88</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>29.62</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>11.08</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Q19</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>2.91</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>7.98</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>6.61</i></span></span></span></td>
</tr>
<tr>
<td width="142"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>Correct</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>22/22</i></span></span></span></td>
<td width="146"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>20/22</i></span></span></span></td>
<td width="148"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><i>22/22</i></span></span></span></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><img loading="lazy" 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"></p>
<p align="center"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><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></span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">At 600 million rows ours is still correct on all 22 and stays close to plain DuckDB. MariaDB&rsquo;s engine drops two queries (Q10, and Q13 on its column-list syntax) and is a lot slower on the big joins &ndash; Q9 took 115 seconds against our 21 and native&rsquo;s 20.</span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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&rsquo;s memory so it spills to disk instead of getting OOM-killed inside </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">mysqld</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">, and under that cap Q15&rsquo;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.</span></span></span></p>
<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">And one number we did not expect: loading those 600 million rows took about 20 minutes with our engine (the </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">COPY</span></span></span> <span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">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.</span></span></span></p>
<h2 class="western"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: x-large"><b>Try it, then tell us</b></span></span></span><a class="anchor-link" id="try-it-then-tell-us"></a></h2>
<p><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">If any of this sounds useful, pull the image and throw your own queries at it:</span></span></span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret 
evgeniypatlan/test-images:mysql-9.7-duckdb-v0.2.0</pre>

<p align="justify"><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">The source is on GitHub (GPLv2), patches and benchmark harness included:</span></span></span><a href="https://github.com/EvgeniyPatlan/ducksdb-mysql-engine"> <span style="color: #1155cc"><span style="font-family: Arial, sans-serif"><span style="font-size: small"><u>https://github.com/EvgeniyPatlan/ducksdb-mysql-engine</u></span></span></span></a><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">. The full per-query benchmark and how we measured it live in </span></span></span><span style="color: #000000"><span style="font-family: Times New Roman, serif"><span style="font-size: small">docs/tpch_engine_comparison.md</span></span></span><span style="color: #000000"><span style="font-family: Arial, sans-serif"><span style="font-size: small">.</span></span></span></p>
<p>&nbsp;</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>

<p><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://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/" />
      <id>https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/</id>
      <updated>2026-07-10T04:29:32+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Some contributions improve MariaDB Server by adding new capabilities.<br />
Some go further: they start from a concrete production problem with an existing feature, not a bug, but a design limitation, solve it upstream, and leave the whole ecosystem better off. …<br />
Continue reading \"MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine\"<br />
The post MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/">MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Some contributions improve MariaDB Server by adding new capabilities.<br>
Some go further: they start from a concrete production problem with an existing feature, not a bug, but a design limitation, solve it upstream, and leave the whole ecosystem better off. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/">MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-blob-text-json-and-geometry-support-in-the-heap-engine/">MariaDB 13.1 Feature in Focus: BLOB, TEXT, JSON and GEOMETRY Support in the HEAP Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>An Easy Path from MySQL to MariaDB: Introducing MariaDB Migrator</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/an-easy-path-from-mysql-to-mariadb-introducing-mariadb-migrator/" />
      <id>https://mariadb.com/resources/blog/an-easy-path-from-mysql-to-mariadb-introducing-mariadb-migrator/</id>
      <updated>2026-07-09T18:05:57+03:00</updated>
      <author><name>Manoj Vakeel</name></author>
      <summary type="html"><![CDATA[<p>Migrating from MySQL to MariaDB is not considered to be an overly complex process.  After all, the two databases share […]</p>
<p><a href="https://mariadb.com/resources/blog/an-easy-path-from-mysql-to-mariadb-introducing-mariadb-migrator/">An Easy Path from MySQL to MariaDB: Introducing MariaDB Migrator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Migrating from MySQL to MariaDB is not considered to be an overly complex process. After all, the two databases share the same DNA, as MariaDB was born as a fork initiated by MySQL. Organizations can often make the switch without the complex code rewrites or architectural overhauls required when migrating to entirely different database systems like, say PostgreSQL. However&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/an-easy-path-from-mysql-to-mariadb-introducing-mariadb-migrator/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/an-easy-path-from-mysql-to-mariadb-introducing-mariadb-migrator/">An Easy Path from MySQL to MariaDB: Introducing MariaDB Migrator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Using MariaDB Serverless Cloud Deployment for Uneven AI Workloads</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/using-mariadb-serverless-cloud-deployment-for-uneven-ai-workloads/" />
      <id>https://mariadb.com/resources/blog/using-mariadb-serverless-cloud-deployment-for-uneven-ai-workloads/</id>
      <updated>2026-07-08T20:24:19+03:00</updated>
      <author><name>Alejandro Duarte</name></author>
      <summary type="html"><![CDATA[<p>The promise of “serverless” is attractive to AI developers. Serverless is a cloud computing model that abstracts away infrastructure management, […]</p>
<p><a href="https://mariadb.com/resources/blog/using-mariadb-serverless-cloud-deployment-for-uneven-ai-workloads/">Using MariaDB Serverless Cloud Deployment for Uneven AI Workloads</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The promise of &ldquo;serverless&rdquo; is attractive to AI developers. Serverless is a cloud computing model that abstracts away infrastructure management, allowing developers to focus on building applications that scale automatically based on demand while paying only for the resources they consume. It sounds great, but are developers having success using serverless architecture that truly supports the high&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/using-mariadb-serverless-cloud-deployment-for-uneven-ai-workloads/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/using-mariadb-serverless-cloud-deployment-for-uneven-ai-workloads/">Using MariaDB Serverless Cloud Deployment for Uneven AI Workloads</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/" />
      <id>https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/</id>
      <updated>2026-07-08T14:18:05+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Interview with Federico Razzoli, nominated in the Community Leadership category.<br />
The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. …<br />
Continue reading \"MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli\"<br />
The post MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/">MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Interview with Federico Razzoli, nominated in the Community Leadership category.<br>
The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/">MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-federico-razzoli/">MariaDB Foundation Sea Lion Champions Nominees: Federico Razzoli</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>pg_tde: our fork is temporary, our commitment to open TDE is not</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/08/pg_tde-our-fork-is-temporary-our-commitment-to-open-tde-is-not/" />
      <id>https://percona.community/blog/2026/07/08/pg_tde-our-fork-is-temporary-our-commitment-to-open-tde-is-not/</id>
      <updated>2026-07-08T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Recently we noticed a LinkedIn post promoting open_pg_tde, a fork of our pg_tde, claiming to be more open. I looked at the repository, and have to disagree with their claim. In this blog post, I’ll explain why.</p>
<p><a href="https://percona.community/blog/2026/07/08/pg_tde-our-fork-is-temporary-our-commitment-to-open-tde-is-not/">pg_tde: our fork is temporary, our commitment to open TDE is not</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Recently we noticed a LinkedIn post promoting <a href="https://github.com/commandprompt/open_pg_tde" target="_blank" rel="noopener noreferrer">open_pg_tde</a>, a fork of our <a href="https://github.com/percona/pg_tde" target="_blank" rel="noopener noreferrer">pg_tde</a>, claiming to be more open.<br>
I looked at the repository, and have to disagree with their claim.<br>
In this blog post, I&rsquo;ll explain why.</p>
<p>The short version: open_pg_tde needs the exact same modified PostgreSQL that pg_tde does &ndash; TDE isn&rsquo;t possible without those upstream changes.<br>
The only difference is delivery: they ship those changes as a patch file users have to apply by hand, while we provide a ready-made branch.</p>
<h2 id="upstreaming-tde">Upstreaming TDE!<a class="anchor-link" id="upstreaming-tde"></a></h2>
<p>The main point of open_pg_tde is that it is more open, as it is not gated behind a vendor fork.<br>
A bit later I&rsquo;ll go into details why I think that&rsquo;s incorrect from a technical point of view, but before that, I want to make something clear:<br>
we do not want to keep TDE in our fork, we are actively working on upstreaming it!</p>
<p>The LinkedIn post also linked to a pgedge blog post written at the end of May, <a href="https://www.pgedge.com/blog/why-postgres-lacks-transparent-data-encryption" target="_blank" rel="noopener noreferrer">Why Postgres Lacks Transparent Data Encryption</a>.</p>
<p>While it seems to be optimistic about the future of encryption in the community version, it also missed that we had not <a href="https://2026.pgconf.dev/session/559" target="_blank" rel="noopener noreferrer">one</a>, but <a href="https://2026.pgconf.dev/session/738" target="_blank" rel="noopener noreferrer">two</a> sessions about it at pgconf.dev, the first one organized by Kai Wagner from Percona, and the second one by Ants Aasma from Cybertec.</p>
<p>We had some really good discussions during those sessions, and ended up with lots of &ldquo;homework&rdquo;:<br>
things we wanted to explore and benchmark before continuing the discussion on pgsql-hackers, where the discussion will soon continue.</p>
<p>With this I want to emphasize that Percona is 100% behind making PostgreSQL transparent data encryption open, as part of the community.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/07/pg_tde_upstreaming.png" alt="&nbsp;"></figure>
</p>
<p>Our fork was born out of necessity, not because we wanted to have our own version.<br>
In fact, if you look back into the <a href="https://github.com/percona/pg_tde/commits/main/" target="_blank" rel="noopener noreferrer">git history of pg_tde</a>, or at our <a href="https://www.percona.com/blog/protect-your-postgresql-database-with-pg_tde-safe-and-secure/" target="_blank" rel="noopener noreferrer">earlier blog posts</a>, we first tried to make it work without any upstream patch. Unfortunately, that didn&rsquo;t work out.</p>
<h2 id="why-do-we-have-our-fork">Why do we have our fork?<a class="anchor-link" id="why-do-we-have-our-fork"></a></h2>
<p>The open_pg_tde documentation publishes the following comparison table:</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/07/open_pg_tde_comparison.png" alt="&nbsp;"></figure>
</p>
<p>The table is clearly AI generated, and has some misleading and/or inconsistent elements.<br>
Here is what we think a more honest comparison looks like:</p>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>pg_tde</th>
<th>open_pg_tde</th>
</tr>
</thead>
<tbody>
<tr>
<td>Requires a patched PostgreSQL</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>How the patches are delivered</td>
<td>Ready-made fork/branch</td>
<td>Patch file, applied by hand</td>
</tr>
<tr>
<td>Open source patches, cherry-pickable</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Actually vendor locked</td>
<td>No</td>
<td>No</td>
</tr>
<tr>
<td>API-version safety check</td>
<td>Yes (<code>PERCONA_API_VERSION</code>)</td>
<td>No, it was removed</td>
</tr>
<tr>
<td>Guards against mismatched-package corruption</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Supports PostgreSQL 16, 17, 18</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Encrypts temporary files at rest</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Supports all KMIP- and Vault-compatible key providers</td>
<td>Yes</td>
<td>Yes</td>
</tr>
</tbody>
</table>
<p>I&rsquo;ll leave out the discussion about temporary files in this blog post.<br>
That&rsquo;s a complex topic by itself, it raises many questions, and we have good reasons why we are still thinking about it instead of shipping a quick version prototyped with Claude.<br>
It is something I plan to talk about later, on its own.</p>
<p>Back to the comparison: it claims that pg_tde is vendor locked because it only works with our fork.<br>
I don&rsquo;t think that&rsquo;s the case:<br>
Our fork is completely open source, anybody can go to our <a href="https://github.com/percona/postgres/" target="_blank" rel="noopener noreferrer">GitHub</a> and cherry-pick our patches manually.</p>
<p>open_pg_tde doesn&rsquo;t remove the need for these patches: it can&rsquo;t, TDE requires them.<br>
It just ships them as a patch file users apply manually, rather than as a branch.<br>
Both approaches modify PostgreSQL identically.</p>
<p>The fork exists for one sole reason:<br>
convenience, we want to make the life of our users easier.<br>
Checking out a fork is easier than manually applying patch files.</p>
<p>So the difference between pg_tde and open_pg_tde is not <em>whether</em> you patch PostgreSQL, you do, either way.<br>
It&rsquo;s <em>how</em> those patches reach you.</p>
<h2 id="percona_api_version">PERCONA_API_VERSION<a class="anchor-link" id="percona_api_version"></a></h2>
<p>The open_pg_tde commit that <a href="https://github.com/commandprompt/open_pg_tde/commit/55ed7f1c4ad5bb31ab085e378699377c04158d09#diff-2b9df17a765260aa1b6bc32fedb30dee1d5f80f975252c2bf6308e5097591aac" target="_blank" rel="noopener noreferrer">removes &ldquo;vendor lock-in&rdquo;</a> is mainly a documentation change:<br>
it adds the patch file, and asks users to apply it manually.</p>
<p>It has one single code change other than that:<br>
in our upstream fork we define <code>PERCONA_API_VERSION</code> and check against it, and open_pg_tde removes both.</p>
<p>While it has PERCONA in its name, and because of that it has been misinterpreted as vendor lock-in, that isn&rsquo;t why we added it:<br>
it&rsquo;s to prevent accidents.</p>
<p>And with encryption, which modifies the storage of data, <strong>accidents might mean data corruption</strong>.</p>
<p>PostgreSQL normally is very stable:<br>
minor versions contain only bugfixes, the API/ABI is very stable, a minor upgrade is considered easy and safe.</p>
<p>However, when you start applying patches manually, you break this promise:<br>
is your patch as stable as PostgreSQL itself?</p>
<p>The reality is that it&rsquo;s not.<br>
pg_tde is backported to earlier major versions. Both pg_tde and open_pg_tde support PostgreSQL 16, and we could backport it to even earlier versions.</p>
<p>This means that the patch has to apply to multiple major versions, and if we have to make a breaking change in it?<br>
Then we have to make that change in all major versions!</p>
<p>Our API version isn&rsquo;t about locking users to our fork, it&rsquo;s a safety net.<br>
It is there to make sure that upgrades happen correctly, and our users don&rsquo;t accidentally mix pg_tde and PostgreSQL packages that aren&rsquo;t 100% compatible, but seem to work.</p>
<p>Imagine this:</p>
<ol>
<li>You have a working PostgreSQL + pg_tde installation</li>
<li>There&rsquo;s an update, you upgrade PostgreSQL</li>
<li>What you didn&rsquo;t notice is that we also updated pg_tde.<br>
You forgot to update that package, or for some reason intentionally didn&rsquo;t update it yet, and because we didn&rsquo;t break the API boundary, everything seems to work.<br>
Except, we did change some internal details of how our patch works.<br>
It doesn&rsquo;t cause any visible issues at first, but slowly some pages of your database are becoming unreadable, or you start getting segmentation faults when accessing a table&hellip;</li>
</ol>
<p>The above of course is a hypothetical scenario, we didn&rsquo;t release any dangerous update like that.</p>
<p>But the point is, even if we have to, in pg_tde, we have safeguards.<br>
In our solution the API version check catches this: at the 3rd step, instead of a slow data corruption, we present you with an early error stating that you should fix your system.</p>
<p>In the above scenario, let&rsquo;s say that in the first step you have a working installation where both the server and pg_tde have <code>PERCONA_API_VERSION=1</code>.<br>
After that, you only update the server, and the new version now has <code>PERCONA_API_VERSION=2</code>.<br>
Since the extension is still at the previous version 1, the server will fail immediately at startup, reminding you that these two packages are not compatible.</p>
<p>Maybe we could have called it something different, <code>TDE_PATCH_VERSION</code>, or something like that.<br>
But the goal is still the same, it is an important safeguard, please do not remove it!<br>
Currently open_pg_tde doesn&rsquo;t protect against this type of mismatch.</p>
<p>In fact, in our latest release, not yet included in open_pg_tde, we did increment PERCONA_API_VERSION because of a slightly incompatible change.<br>
We don&rsquo;t expect any data corruption possibilities from it, it was a very minor API change, but we want to play things safe, as keeping the data of our users secure is our first priority.</p>
<h2 id="patches-welcome">Patches welcome!<a class="anchor-link" id="patches-welcome"></a></h2>
<p>If we look into the git history of open_pg_tde, it&rsquo;s mostly (AI written) documentation changes.<br>
Other than the removal of the API check I explained above, there are two actual code changes:</p>
<ul>
<li>One is the addition of the AES-XTS algorithm for encrypting relation pages</li>
<li>Another is the support for temporary file encryption</li>
</ul>
<p>We would be happy to start a discussion of any of these features, or even others.<br>
If you want to contribute, please do not hesitate, and contact us.</p>
<p>This is the strength of open source: it allows contributions and open discussions.<br>
While the team behind open_pg_tde, or anybody else, can of course fork pg_tde this way (because we are open and do not gatekeep features), the energy spent on that could be used to make pg_tde better in a shared community effort.</p>
<p>You can open a <a href="https://github.com/percona/pg_tde/pulls" target="_blank" rel="noopener noreferrer">Pull Request</a>, or a <a href="https://github.com/percona/pg_tde/issues" target="_blank" rel="noopener noreferrer">GitHub Issue</a>, or even use our <a href="https://perconadev.atlassian.net/projects/PG/issues/" target="_blank" rel="noopener noreferrer">Jira</a>, all options are equally good!</p>
<p>Even an issue/PR stating &ldquo;please call PERCONA_API_VERSION differently&rdquo;, we are not against changing that if the consensus is that it should be something different.</p>
<p>But please don&rsquo;t create forks without even reaching out to your &ldquo;upstream&rdquo;.<br>
That only increases fragmentation, and makes the life of our common audience harder.</p>
<p>What we ask for is simple: let&rsquo;s make PostgreSQL better with our work, not harder to use.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/07/open_pg_tde_do_not_fork.png" alt="&nbsp;"></figure></p>

<p><a href="https://percona.community/blog/2026/07/08/pg_tde-our-fork-is-temporary-our-commitment-to-open-tde-is-not/">pg_tde: our fork is temporary, our commitment to open TDE is not</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Updated MariaDB C++ and ODBC Connectors now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/updated-mariadb-c-and-odbc-connectors-now-available/" />
      <id>https://mariadb.com/resources/blog/updated-mariadb-c-and-odbc-connectors-now-available/</id>
      <updated>2026-07-07T15:23:24+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Connector/C++ 1.1.8 and 1.0.7 and MariaDB Connector/ODBC 3.2.9 and 3.1.23. […]</p>
<p><a href="https://mariadb.com/resources/blog/updated-mariadb-c-and-odbc-connectors-now-available/">Updated MariaDB C++ and ODBC Connectors now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Connector/C++ 1.1.8 and 1.0.7 and MariaDB Connector/ODBC 3.2.9 and 3.1.23. All are Stable (GA) releases. Download Now See the release notes and changelogs for more details and visit mariadb.com/downloads/connectors to download.</p>
<p><a href="https://mariadb.com/resources/blog/updated-mariadb-c-and-odbc-connectors-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/updated-mariadb-c-and-odbc-connectors-now-available/">Updated MariaDB C++ and ODBC Connectors now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-operator-for-mysql-1-2-0-cross-site-replication-encrypted-backups-storage-autoscaling/" />
      <id>https://www.percona.com/blog/percona-operator-for-mysql-1-2-0-cross-site-replication-encrypted-backups-storage-autoscaling/</id>
      <updated>2026-07-07T13:19:26+03:00</updated>
      <author><name>Slava Sarzhan</name></author>
      <summary type="html"><![CDATA[<p>  Percona Operator for MySQL 1.2.0 is out, and it closes three gaps that platform teams hit once a MySQL deployment grows past a single cluster. Picture a fleet that has outgrown one region: you want a warm replica cluster in a second data center, backups in object storage that pass an auditor’s encryption check, … Continued<br />
The post Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-operator-for-mysql-1-2-0-cross-site-replication-encrypted-backups-storage-autoscaling/">Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50188 size-large" src="https://www.percona.com/wp-content/uploads/2026/07/Hero-2400-x-880-1024x376.png" alt="" width="1024" height="376"></p>
<p>&nbsp;</p>
<p><b>Percona Operator for MySQL 1.2.0</b><span style="font-weight: 400"> is out, and it closes three gaps that platform teams hit once a MySQL deployment grows past a single cluster. Picture a fleet that has outgrown one region: you want a warm replica cluster in a second data center, backups in object storage that pass an auditor&rsquo;s encryption check, and volumes that grow before they fill at 3 a.m. Until now, each of those meant scripting around the operator. This release brings all three into the custom resource.</span></p>
<p><span style="font-weight: 400">The three headline features are </span><b>cross-site replication for Group Replication</b><span style="font-weight: 400">, </span><b>encrypted backups</b><span style="font-weight: 400">, and </span><b>automatic storage scaling</b><span style="font-weight: 400">. Each one turns a manual, error-prone procedure into a declarative field you set once and let the operator reconcile.</span></p>
<p><span style="font-weight: 400">The operator is open source and runs on any CNCF-certified Kubernetes distribution. Many of the changes in this release come straight from what users asked for on </span><a href="https://forums.percona.com/"><span style="font-weight: 400">forums.percona.com</span></a><span style="font-weight: 400"> and in the public issue tracker, from disaster-recovery topologies to backup encryption to storage that keeps up with data growth.</span></p>
<p><span style="font-weight: 400">In this post, you&rsquo;ll learn about:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Cross-site replication for Group Replication clusters</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Encrypted backups to S3, GCS, and Azure</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Automatic storage scaling for MySQL data volumes</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Other improvements worth knowing about</span></li>
</ul>
<h2><b><br>
Cross-site replication for Group Replication</b><a class="anchor-link" id="cross-site-replication-for-group-replication"></a></h2>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50189 size-large" src="https://www.percona.com/wp-content/uploads/2026/07/clusterset-topology-1024x551.png" alt="" width="1024" height="551"></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">Group Replication gives you a self-healing, multi-primary-capable cluster inside one Kubernetes cluster. What it does not give you on its own is a second site. If the region hosting your cluster goes down, Group Replication cannot fail over to hardware it does not know about. Teams have solved this by hand-wiring asynchronous replication between clusters and babysitting it, which is exactly the kind of stateful glue an operator should own.</span></p>
<p>&nbsp;</p>
<h3><b>Why it matters</b><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">A disaster-recovery topology is only useful if it is reproducible and observable. Hand-built replication links drift: someone changes a credential, a channel stalls, and nobody notices until the failover that was supposed to save you does not work. Declaring the topology as a Kubernetes object means the operator reconciles it continuously, and the same manifest recreates it in staging, in a runbook test, and in the real event.</span></p>
<p>&nbsp;</p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span style="font-weight: 400">The operator adds a new custom resource, </span><strong>PerconaServerMySQLClusterSet</strong><span style="font-weight: 400">, that groups two or more Group Replication clusters into a single set with one primary. The operator drives MySQL Shell to build the InnoDB ClusterSet, wires the replica clusters to the primary, and tracks the topology in the resource&rsquo;s status. A replica cluster provisions from the primary using a chosen recovery method, so you do not stage data manually.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: ps.percona.com/v1
kind: PerconaServerMySQLClusterSet
metadata:
  name: my-cluster-set
  finalizers:
    - percona.com/clusterset-dissolve
spec:
#  unsafeFlags:
#    forcedFailover: false
#    forcedClusterRemoval: false
  primaryCluster: pscluster1
  credentialsSecret:
    name: ps-cluster1-secrets
    key: clusterset
  sslMode: AUTO
  createReplicaClusterOptions:
    recoveryMethod: clone
  clusters:
    - innodbClusterName: pscluster1
      endpoints:
      - host: ps-cluster1-mysql-primary.default.svc.cluster.local
    - innodbClusterName: pscluster2
      endpoints:
      - host: ps-cluster2-mysql-0.ps-cluster2-mysql.default.svc.cluster.local
  mysqlshellRunner:
    image: percona/percona-server:8.4.10-10.1</pre>
<p><strong>primaryCluster</strong><span style="font-weight: 400"> names the source of truth. Each entry under </span><strong>clusters</strong><span style="font-weight: 400"> points at a Group Replication cluster by its InnoDB cluster name and reachable endpoints, so the clusters can live in separate namespaces or separate Kubernetes clusters joined by routable DNS. </span><strong>createReplicaClusterOptions.recoveryMethod: clone</strong><span style="font-weight: 400"> tells the replica to seed itself with a full clone. The </span><strong>percona.com/clusterset-dissolve</strong><span style="font-weight: 400"> finalizer ensures the operator tears the ClusterSet down cleanly instead of leaving orphaned replication channels behind.</span><br>
&nbsp;</p>
<h3><b>Failover and cleanup</b><a class="anchor-link" id="failover-and-cleanup"></a></h3>
<p><span style="font-weight: 400">Once the set exists, the operator keeps the replica clusters attached to the primary and surfaces the topology in the resource&rsquo;s status, so you can see which cluster is primary and whether every replica is connected without shelling into MySQL Shell. A planned switchover promotes a replica to primary. The </span><span style="font-weight: 400">unsafeFlags</span><span style="font-weight: 400"> block gates the disruptive paths for when the primary is already gone.</span></p>
<blockquote>
<p><b>Note:</b><span style="font-weight: 400"> The unsafeFlags block gates disruptive operations such as forced failover and forced cluster removal. Leave these off for normal operation and reach for them only in a controlled recovery, since a forced failover can diverge history if the old primary comes back.</span></p>
</blockquote>
<p>&nbsp;</p>
<h2><b>Encrypted backups</b><a class="anchor-link" id="encrypted-backups"></a></h2>
<p><span style="font-weight: 400">Backups are the copy of your data most likely to leave the cluster&rsquo;s security boundary. They land in an object-storage bucket, get replicated across a provider&rsquo;s regions, and often live longer than the database that produced them. If they are not encrypted before they leave the pod, a bucket misconfiguration or a leaked credential exposes the whole dataset. This release lets the operator encrypt backup data as it is </span></p>
<p>&nbsp;</p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span style="font-weight: 400">Backups in the operator run on </span><a href="https://docs.percona.com/percona-xtrabackup/8.4/"><span style="font-weight: 400">Percona XtraBackup</span></a><span style="font-weight: 400">. XtraBackup encrypts the stream with its </span><span style="font-weight: 400">xbcrypt</span><span style="font-weight: 400"> component before uploading, so the data is ciphertext at rest in the bucket and stays that way until you restore it with the same key. You supply the key through a Kubernetes Secret and reference that Secret from the backup configuration. The operator never bakes the key into a manifest or an image.</span></p>
<p>&nbsp;</p>
<h3><b>Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span style="font-weight: 400">Point a storage target at an encryption-key Secret with </span><strong>encryptionKeySecret</strong><span style="font-weight: 400">:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: ps.percona.com/v1
kind: PerconaServerMySQL
metadata:
  name: cluster1
spec:
  backup:
    storages:
      s3-us-west:
        type: s3
        encryptionKeySecret:
          key: encryptionKey
          name: my-s3-encryption-key-secret
        s3:
          bucket: S3-BACKUP-BUCKET-NAME-HERE
          credentialsSecret: cluster1-s3-credentials
          region: us-west-2</pre>
<p><span style="font-weight: 400">The same </span><strong>encryptionKeySecret</strong><span style="font-weight: 400"> field works under S3, GCS, and Azure storage targets, so a multi-cloud backup policy uses one consistent mechanism. You can also set an </span><strong>encryptionKeySecret</strong><span style="font-weight: 400"> at the </span><strong>backup</strong><span style="font-weight: 400"> level to apply one key across every storage target instead of repeating it per bucket. The referenced Secret holds the key under the </span><strong>encryptionKey</strong><span style="font-weight: 400"> data field.</span></p>
<p>&nbsp;</p>
<h3><b>Restoring an encrypted backup</b><a class="anchor-link" id="restoring-an-encrypted-backup"></a></h3>
<p><span style="font-weight: 400">Encryption is transparent on the way back in. When you restore, the operator reads the same Secret, hands the key to XtraBackup, and decrypts the stream before it prepares the data directory. The only hard requirement is that the key still exists: the restore fails fast if the Secret is missing or holds a different key than the one that produced the backup. Encryption also composes with backup compression, so you keep the smaller footprint and the ciphertext-at-rest guarantee at the same time.</span></p>
<blockquote>
<p><b>Note:</b><span style="font-weight: 400"> Keep the encryption key safe and versioned outside the cluster. A backup encrypted with a key you have lost is not recoverable. Treat the key with the same care as the backups themselves.</span></p>
</blockquote>
<p>&nbsp;</p>
<h2><b>Automatic storage scaling</b><a class="anchor-link" id="automatic-storage-scaling"></a></h2>
<p><span style="font-weight: 400">Running out of disk is one of the fastest ways to take a database down, and it rarely happens at a convenient hour. The operator has supported manual volume expansion since an earlier release: you raise </span><strong>resources.requests.storage</strong><span style="font-weight: 400">, apply, and the operator grows the <strong>PersistentVolumeClaim</strong> for you. That still requires a human to notice the trend and act. Version 1.2.0 adds automatic scaling that watches usage and grows the volume on its own.</span></p>
<p>&nbsp;</p>
<h3><b>Why it matters</b><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">Storage growth is predictable in aggregate and unpredictable in timing. A batch import, a retention change, or an unexpected traffic spike can eat headroom faster than an on-call engineer can respond. Letting the operator resize before the volume fills turns a page someone at 3 a.m. incident into a log line, as long as your storage class supports online expansion.</span></p>
<p>&nbsp;</p>
<h3><b>How it works</b><a class="anchor-link" id="how-it-works"></a></h3>
<p><span style="font-weight: 400">You enable volume expansion, then define an autoscaling policy. The operator monitors each data PVC and, when usage crosses the threshold, grows the volume by a fixed step up to a ceiling you set. Because it builds on the Kubernetes volume-expansion API, the underlying storage class must have </span><strong>AllowVolumeExpansion: true</strong><span style="font-weight: 400">.</span></p>
<p>&nbsp;</p>
<h3><b>Wiring it up</b><a class="anchor-link" id="wiring-it-up"></a></h3>

<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: ps.percona.com/v1
kind: PerconaServerMySQL
metadata:
  name: cluster1
spec:
  enableVolumeExpansion: true
  storageScaling:
    enableVolumeScaling: true
    autoscaling:
      enabled: true
      growthStep: 2Gi
      maxSize: 10Gi
      triggerThresholdPercent: 80</pre>
<p>&nbsp;</p>
<p><strong>triggerThresholdPercent</strong><span style="font-weight: 400"> is the fill level that triggers a resize (default </span><span style="font-weight: 400">80</span><span style="font-weight: 400">, allowed range 50 to 95). </span><strong>growthStep</strong><span style="font-weight: 400"> is how much capacity each resize adds (default </span><strong>2Gi</strong><span style="font-weight: 400">), and </span><strong>maxSize</strong><span style="font-weight: 400"> caps total growth so a runaway workload cannot expand a volume without bound. The operator validates the relationship between these fields: </span><strong>autoscaling</strong><span style="font-weight: 400"> cannot be enabled unless </span><strong>enableVolumeScaling</strong><span style="font-weight: 400"> is on. For teams that prefer an external controller to own resizing, </span><strong>enableExternalAutoscaling</strong><span style="font-weight: 400"> hands that responsibility off instead.</span></p>
<p><span style="font-weight: 400">The operator records each resize in the cluster status under </span><strong>storageAutoscaling</strong><span style="font-weight: 400">, including the count of resizes and the timestamp of the last one. That gives you an audit trail and a signal worth alerting on: a volume that keeps hitting its </span><strong>growthStep</strong><span style="font-weight: 400"> is telling you the workload has changed, and a volume approaching </span><strong>maxSize</strong><span style="font-weight: 400"> is telling you to plan capacity before the ceiling stops the next resize.</span></p>
<blockquote>
<p><b>Note: </b><span style="font-weight: 400">PVC expansion is one-way. Kubernetes can grow a volume but cannot shrink it, so set maxSize deliberately. Confirm your storage class allows expansion before you rely on this in production.</span></p>
<p>&nbsp;</p>
</blockquote>
<h2><b>Other improvements</b><a class="anchor-link" id="other-improvements"></a></h2>
<p><span style="font-weight: 400">Beyond the three headline features, 1.2.0 ships a set of enhancements that smooth day-two operations:</span></p>
<ul>
<li style="font-weight: 400"><b>Dedicated root user Secret</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-689"><span style="font-weight: 400">K8SPS-689</span></a><span style="font-weight: 400">): the operator publishes root connection details in a dedicated Secret, so applications and tooling read one predictable object instead of parsing several.</span></li>
<li style="font-weight: 400"><b>Disable NodePort allocation for LoadBalancer Services</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-496"><span style="font-weight: 400">K8SPS-496</span></a><span style="font-weight: 400">): set </span><span style="font-weight: 400">allocateLoadBalancerNodePorts: false</span><span style="font-weight: 400"> to stop Kubernetes from opening NodePorts you never use behind a cloud load balancer.</span></li>
<li style="font-weight: 400"><b>Custom cluster naming for PMM</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-627"><span style="font-weight: 400">K8SPS-627</span></a><span style="font-weight: 400">): give a cluster a stable display name so multi-region and multi-namespace fleets stay legible in </span><a href="https://docs.percona.com/percona-monitoring-and-management/"><span style="font-weight: 400">Percona Monitoring and Management</span></a><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><b>Vault encryption Secret validation</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-487"><span style="font-weight: 400">K8SPS-487</span></a><span style="font-weight: 400">): the operator validates the Vault Secret and reports problems in status immediately, instead of failing later during an operation.</span></li>
<li style="font-weight: 400"><b>Concurrent reconciliation</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-434"><span style="font-weight: 400">K8SPS-434</span></a><span style="font-weight: 400">): tune how many clusters the operator reconciles at once through an environment variable, which helps a single operator manage a larger fleet.</span></li>
<li style="font-weight: 400"><b>Independent </b><b>mysql-monit</b><b> sidecar resources</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-742"><span style="font-weight: 400">K8SPS-742</span></a><span style="font-weight: 400">): set CPU and memory for the monitoring sidecar separately from the database container.</span></li>
<li style="font-weight: 400"><b>Orchestrator API authentication</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-19"><span style="font-weight: 400">K8SPS-19</span></a><span style="font-weight: 400">): the Orchestrator API now requires valid credentials.</span></li>
<li style="font-weight: 400"><b>Binlog storage configuration in restore objects</b><span style="font-weight: 400"> (</span><a href="https://perconadev.atlassian.net/browse/K8SPS-716"><span style="font-weight: 400">K8SPS-716</span></a><span style="font-weight: 400">): point a restore at the binlog storage it needs for point-in-time recovery.</span></li>
</ul>
<p><span style="font-weight: 400">For the full list, including bug fixes, see the release notes linked below.</span></p>
<p>&nbsp;</p>
<h2><b>Conclusion</b><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">Percona Operator for MySQL 1.2.0 extends the operator across the parts of the lifecycle that used to need custom glue: replication across sites, encryption of data that leaves the cluster, and storage that keeps up with growth. Platform teams running MySQL fleets on Kubernetes get declarative control over disaster recovery, a cleaner path through a security review, and one less 3 a.m. page. If there is a topology or a control you still have to script around, tell us on the forum, since that feedback is where releases like this one come from.</span></p>
<p>&nbsp;</p>
<h2><b>Try Percona Operator for MySQL 1.2.0</b><a class="anchor-link" id="try-percona-operator-for-mysql-1-2-0"></a></h2>
<ul>
<li style="font-weight: 400"><b>Release notes</b><span style="font-weight: 400">: </span><a href="https://docs.percona.com/percona-operator-for-mysql/ps/ReleaseNotes/Kubernetes-Operator-for-PS-RN1.2.0.html"><span style="font-weight: 400">Percona Operator for MySQL 1.2.0 Release Notes</span></a></li>
<li style="font-weight: 400"><b>Documentation</b><span style="font-weight: 400">: </span><a href="https://docs.percona.com/percona-operators/"><span style="font-weight: 400">Percona Operator for MySQL docs</span></a></li>
<li style="font-weight: 400"><b>GitHub</b><span style="font-weight: 400">: </span><a href="https://github.com/percona/percona-server-mysql-operator"><span style="font-weight: 400">percona/percona-server-mysql-operator</span></a></li>
<li style="font-weight: 400"><b>Community Forum</b><span style="font-weight: 400">: </span><a href="https://forums.percona.com/c/mysql-mariadb/percona-kubernetes-operator-for-mysql/28"><span style="font-weight: 400">forums.percona.com</span></a><span style="font-weight: 400">: share your feedback, ask questions, or report issues</span></li>
</ul>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/percona-operator-for-mysql-1-2-0-cross-site-replication-encrypted-backups-storage-autoscaling/">Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-operator-for-mysql-1-2-0-cross-site-replication-encrypted-backups-storage-autoscaling/">Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/comparing-migration-methods-from-the-crunchy-data-postgresql-operator-to-the-percona-operator-for-postgresql/" />
      <id>https://www.percona.com/blog/comparing-migration-methods-from-the-crunchy-data-postgresql-operator-to-the-percona-operator-for-postgresql/</id>
      <updated>2026-07-07T13:01:43+03:00</updated>
      <author><name>Chetan Shivashankar</name></author>
      <summary type="html"><![CDATA[<p>Migrating a production PostgreSQL database on Kubernetes is not only about moving data from one operator to another. It is also about choosing the right trade-off between downtime, operational complexity, rollback safety, cost, and business risk. Practical migration paths from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL are described here.  1. … Continued<br />
The post Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/comparing-migration-methods-from-the-crunchy-data-postgresql-operator-to-the-percona-operator-for-postgresql/">Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">Migrating a production PostgreSQL database on Kubernetes is not only about moving data from one operator to another. It is also about choosing the right trade-off between downtime, operational complexity, rollback safety, cost, and business risk.</span></p>
<p><span style="font-weight: 400">Practical migration paths from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL are described </span><a href="https://docs.percona.com/percona-operator-for-postgresql/3.0.0/migrate-from-crunchy.html"><span style="font-weight: 400">here</span></a><span style="font-weight: 400">.&nbsp;</span></p>
<h4><b>1. Migration to standby cluster utilizing the same pgBackRest repository</b></h4>
<p><span style="font-weight: 400">In this method, the Percona cluster is created as a standby and points to the same pgBackRest repository used by the Crunchy Data PostgreSQL Operator cluster. This means the object storage and the path are exactly the same for both clusters.</span></p>
<p><span style="font-weight: 400">With the above configuration, the Percona standby restores the initial backup from the shared repository and replays archived WAL. The method is described </span><a href="https://docs.percona.com/percona-operator-for-postgresql/3.0.0/standby-backup.html#configure-dr-site"><span style="font-weight: 400">here</span></a><span style="font-weight: 400">.</span></p>
<h4><strong>2.Migration to Standby Cluster with Streaming Replication</strong></h4>
<p><strong>&nbsp;&nbsp;</strong><span style="font-weight: 400">In this approach, the standby cluster initiates a </span><a href="https://www.postgresql.org/docs/current/app-pgbasebackup.html"><span style="font-weight: 400">pg_basebackup</span></a><span style="font-weight: 400"> for the initial restore and uses native postgres streaming for sync. The method is described </span><a href="https://docs.percona.com/percona-operator-for-postgresql/3.0.0/standby-streaming.html"><span style="font-weight: 400">here</span></a><span style="font-weight: 400">.</span></p>
<h4><b>3.Backup and Restore&nbsp;</b></h4>
<p><span style="font-weight: 400">In this method, a backup is taken from the Crunchy cluster and restored to a cluster managed by the Percona Operator for PostgreSQL.</span></p>
<h4><b>4.Migration by reusing the persistent volume</b></h4>
<p><span style="font-weight: 400">In this method, the existing Crunchy Data PostgreSQL Operator primary&rsquo;s PGDATA persistent volume is used. The process is: stop writes to the Crunchy Data PostgreSQL Operator cluster, delete the cluster while retaining the persistent volume, clear the old persistent volume claim reference, and create a Percona cluster whose PVC selector binds to that same retained PV. PostgreSQL then starts on the existing data directory without a restore.</span></p>
<p><span style="font-weight: 400">Each method works, but each one is suitable for a different operational scenario. A migration strategy that is ideal for a small development database may be risky for a large production system with heavy write traffic. Similarly, a method that gives the lowest downtime may require more preparation and validation.</span></p>
<p><span style="font-weight: 400">This post compares the four migration approaches so users can choose the most suitable approach.</span></p>
<h2><span style="font-weight: 400">Factors to Consider Before Migration</span><a class="anchor-link" id="factors-to-consider-before-migration"></a></h2>
<p><span style="font-weight: 400">There is no single best migration strategy for every PostgreSQL workload. A production migration usually has to balance several requirements:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">How Much Downtime is Acceptable?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">What is the Network Connectivity Status Between the Clusters?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">What is the RTO/RPO When Something Goes Wrong?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">How Large is the Database?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">How Write-Heavy is the Workload?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">How Quickly Must the System be Rolled Back?</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Is the Migration Happening Across Namespaces, Clusters, Storage Classes, or Cloud?</span></li>
</ul>
<h2><span style="font-weight: 400">Comparison of Migration Methods</span><a class="anchor-link" id="comparison-of-migration-methods"></a></h2>
<table dir="ltr" border="1" cellspacing="0" cellpadding="0" data-sheets-root="1" data-sheets-baot="1">
<colgroup>
<col width="157">
<col width="259">
<col width="235">
<col width="224">
<col width="227"></colgroup>
<tbody>
<tr>
<td></td>
<td>Same pgBackRest Repository</td>
<td>Streaming Replication</td>
<td>Backup and Restore</td>
<td>Reuse the Persistent volume</td>
</tr>
<tr>
<td>Implementation</td>
<td>Primary archives WAL &rarr; shared repo &rarr; standby does a restore + fetches WAL via pgBackRest archive-get</td>
<td>Primary streams WAL over TCP &rarr; standby pg_basebackup + WAL receiver</td>
<td>Take backup from primary -&gt; Restore to new cluster</td>
<td>Same volume is reused by the Percona Operator for PostgreSQL</td>
</tr>
<tr>
<td>Initial Seed</td>
<td>Restore from an existing backup with pgBackRest</td>
<td>pg_basebackup from live primary</td>
<td>Restore from an existing backup with pgBackRest</td>
<td>Volume contains the entire data</td>
</tr>
<tr>
<td>Primary to standby sync</td>
<td>WAL is fetched with pgBackRest archive-get</td>
<td>Native PostgreSQL streaming</td>
<td>WAL is fetched with pgBackRest archive-get</td>
<td>N/A</td>
</tr>
<tr>
<td>Performance impact to the primary</td>
<td>No extra impact</td>
<td>Slight impact due to the pg_basebackup and streaming</td>
<td>No extra impact</td>
<td>Primary will be down for the duration of the migration</td>
</tr>
<tr>
<td>Network dependencies</td>
<td>No network connectivity is required between the primary and standby clusters.</td>
<td>Network connectivity needed between primary and standby nodes.</td>
<td>No network connectivity is required between the primary and standby clusters.</td>
<td>No Dependency</td>
</tr>
<tr>
<td>Object storage dependency</td>
<td>Object storage used for WAL should be accessible by both primary and standby</td>
<td>No dependency</td>
<td>Object storage used for WAL should be accessible by both primary and standby</td>
<td>No dependency</td>
</tr>
<tr>
<td>Downtime</td>
<td>During cutover from the primary to the standby, writes must be blocked until the standby catches up with the primary, or the cutover should be performed during a period of low write activity to allow the standby to catch up.</td>
<td>During cutover from the primary to the standby, writes must be blocked until the standby catches up with the primary, or the cutover should be performed during a period of low write activity to allow the standby to catch up.</td>
<td>During cutover from the primary to the standby, writes must be blocked until the standby catches up with the primary, or the cutover should be performed during a period of low write activity to allow the standby to catch up.</td>
<td>Complete downtime till the new cluster is started</td>
</tr>
<tr>
<td>Rollback</td>
<td>Use the older crunchy cluster.<br>
Easy if there were no writes done on standby. If there were any writes done on standby, data consistency needs to be checked before rolling back.</td>
<td>Use the older crunchy cluster.<br>
Easy if there were no writes done on standby. If there were any writes done on standby, data consistency needs to be checked before rolling back</td>
<td>Use the older crunchy cluster.<br>
Easy if there were no writes done on standby. If there were any writes done on standby, data consistency needs to be checked before rolling back</td>
<td>Easy to rollback; no issues with data inconsistency, unless the data volume get&rsquo;s corrupted.</td>
</tr>
<tr>
<td>Business continuity risks</td>
<td>If migration fails for some reason, it is easy to fall back to the Crunchy Data PostgreSQL Operator cluster.</td>
<td>If migration fails for some reason, it is easy to fall back to the Crunchy Data PostgreSQL Operator cluster.</td>
<td>If migration fails for some reason, it is easy to fall back to the Crunchy Data PostgreSQL Operator cluster.</td>
<td>Can fallback to using the Crunchy Data PostgreSQL Operator cluster if migration fails. If the data volume gets corrupted, full restore needs to be done from backup. RTO/RPO depends on the dataset size and the WAL pushed to the object storage.</td>
</tr>
<tr>
<td>Cost /Resources utilization</td>
<td>For the duration of migration, there will be 2 clusters which adds up to the resources and the cost.</td>
<td>For the duration of migration, there will be 2 clusters which adds up to the resources and the cost.</td>
<td>For the duration of migration, there will be 2 clusters which adds up to the resources and the cost.</td>
<td>No additional resources / cost needed.</td>
</tr>
<tr>
<td>Compatible with other / custom backup solution</td>
<td>Backups should be taken with pgbackrest</td>
<td>Any backup solution can be used by the Crunchy Data PostgreSQL Operator</td>
<td>Backups should be taken with pgbackrest</td>
<td>Any backup solution can be used by the Crunchy Data PostgreSQL Operator</td>
</tr>
<tr>
<td>Client DNS caching issues</td>
<td>Clients might refer to the older entry due to caching entries. This will be reflected for short period of TTL expiry or caching rule set at client after the migration</td>
<td>Clients might refer to the older entry due to caching entries. This will be reflected for short period of TTL expiry or caching rule set at client after the migration</td>
<td>Clients might refer to the older entry due to caching entries. This will be reflected for short period of TTL expiry or caching rule set at client after the migration</td>
<td>No issues</td>
</tr>
<tr>
<td>Migrating to different kubernetes cluster</td>
<td>Possible</td>
<td>Possible</td>
<td>Possible</td>
<td>Not possible</td>
</tr>
</tbody>
</table>
<h2><a class="anchor-link" id=""></a></h2>
<h2><span style="font-weight: 400">Conclusion</span><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">There is no &ldquo;one-size-fits-all&rdquo; solution for migrating PostgreSQL workloads on Kubernetes. Choosing the right strategy requires a careful balance between acceptable downtime, operational complexity, and business continuity requirements. For example, below there are some scenarios which list the suitable approaches</span></p>
<ul>
<li style="font-weight: 400"><b>For scenarios where zero downtime is not strictly required but minimal operational impact is preferred</b><span style="font-weight: 400">, migration via a shared pgBackRest repository could be a feasible solution.</span></li>
<li style="font-weight: 400"><b>For environments where network latency is low and real-time synchronization is feasible</b><span style="font-weight: 400">, Streaming Replication could be a feasible solution.</span></li>
<li style="font-weight: 400"><b>For simpler migrations where network connectivity between clusters is not feasible</b><span style="font-weight: 400">, a standard Backup and Restore will work.</span></li>
<li style="font-weight: 400"><b>For rapid migrations involving massive datasets where storage mobility is not required</b><span style="font-weight: 400">, reusing the existing persistent volume can significantly reduce migration time.</span></li>
</ul>
<p><span style="font-weight: 400">Before proceeding, we recommend conducting a dry run in a staging environment to validate your chosen method against your specific network topology and workload requirements. By carefully evaluating these trade-offs, you can ensure a secure, efficient transition to the Percona Operator for PostgreSQL.</span></p>
<p>The post <a href="https://www.percona.com/blog/comparing-migration-methods-from-the-crunchy-data-postgresql-operator-to-the-percona-operator-for-postgresql/">Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/comparing-migration-methods-from-the-crunchy-data-postgresql-operator-to-the-percona-operator-for-postgresql/">Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>TAF 3.0 — Results Backend With Automated Performance Change Detection</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/" />
      <id>https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/</id>
      <updated>2026-07-07T10:55:01+03:00</updated>
      <author><name>Jonathan Miller</name></author>
      <summary type="html"><![CDATA[<p>TAF 3.0 introduces the new TAF Results Backend, a structured results database and parser pipeline that delivers fully automated performance change detection. …<br />
Continue reading \"TAF 3.0 — Results Backend With Automated Performance Change Detection\"<br />
The post TAF 3.0 — Results Backend With Automated Performance Change Detection appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/">TAF 3.0 — Results Backend With Automated Performance Change Detection</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://github.com/MariaDB/TAF">TAF</a> 3.0 introduces the new <a href="https://github.com/MariaDB/TAF">TAF</a> Results Backend, a structured results database and parser pipeline that delivers fully automated performance change detection. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;TAF 3.0 &mdash; Results Backend With Automated Performance Change Detection&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/">TAF 3.0 &mdash; Results Backend With Automated Performance Change Detection</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/taf-3-0-results-backend-with-automated-performance-change-detection/">TAF 3.0 — Results Backend With Automated Performance Change Detection</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Server Plugins: disabled functions</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-server-plugins-disabled-functions/" />
      <id>https://mariadb.org/mariadb-server-plugins-disabled-functions/</id>
      <updated>2026-07-06T08:33:57+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>During the last MariaDB Foundation Board Meeting (24 June 2026), Barry shared how it can be difficult to deploy an upgrade immediately and that they sometimes have to wait for one that fixes security bugs. …<br />
Continue reading \"MariaDB Server Plugins: disabled functions\"<br />
The post MariaDB Server Plugins: disabled functions appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-server-plugins-disabled-functions/">MariaDB Server Plugins: disabled functions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>During the <a href="https://mariadb.org/bodminutes/2026-06-24/">last MariaDB Foundation Board Meeting</a> (24 June 2026), Barry shared how it can be difficult to deploy an upgrade immediately and that they sometimes have to wait for one that fixes security bugs. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-server-plugins-disabled-functions/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Server Plugins: disabled functions&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-server-plugins-disabled-functions/">MariaDB Server Plugins: disabled functions</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-server-plugins-disabled-functions/">MariaDB Server Plugins: disabled functions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Cross-site Disaster Recovery with Percona Operator for MySQL</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/" />
      <id>https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/</id>
      <updated>2026-07-06T07:42:27+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![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><a href="https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/">Cross-site Disaster Recovery with Percona Operator for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![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 id="understanding-innodb-clusterset">Understanding InnoDB ClusterSet<a class="anchor-link" id="understanding-innodb-clusterset"></a></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&rsquo;s build a simple cross-site disaster recovery setup.</p>

<h2 id="setting-up-clusterset">Setting up ClusterSet<a class="anchor-link" id="setting-up-clusterset"></a></h2>
<p>We&rsquo;ll create the simplest useful ClusterSet: two Group Replication clusters named <code>dc1</code> and <code>dc2</code>.</p>
<p>In this example:<br>
<code>dc1</code> is the primary cluster.<br>
<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 id="creating-a-primary-cluster">Creating a primary cluster<a class="anchor-link" id="creating-a-primary-cluster"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-0">
<div class="highlight">
<pre 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 id="creating-the-replica-cluster">Creating the replica cluster<a class="anchor-link" id="creating-the-replica-cluster"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-1">
<div class="highlight">
<pre 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"># &lt;- set this!</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 id="sharing-cluster-credentials">Sharing cluster credentials<a class="anchor-link" id="sharing-cluster-credentials"></a></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 id="linking-the-clusters">Linking the clusters<a class="anchor-link" id="linking-the-clusters"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-2">
<div class="highlight">
<pre 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&rsquo;s data and GTID history, so ClusterSet only needs to catch it up from the primary&rsquo;s binary logs instead of transferring the full dataset again.</p>
</blockquote>
<h3 id="verifying-it-worked">Verifying it worked<a class="anchor-link" id="verifying-it-worked"></a></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 id="planned-switchover">Planned Switchover<a class="anchor-link" id="planned-switchover"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-3">
<div class="highlight">
<pre 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 id="emergency-failover">Emergency Failover<a class="anchor-link" id="emergency-failover"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-4">
<div class="highlight">
<pre 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 id="adding-and-removing-clusters">Adding and removing clusters<a class="anchor-link" id="adding-and-removing-clusters"></a></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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-5">
<div class="highlight">
<pre 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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-6">
<div class="highlight">
<pre 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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-7">
<div class="highlight">
<pre 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 id="wrapping-up">Wrapping up<a class="anchor-link" id="wrapping-up"></a></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 id="further-reading">Further reading<a class="anchor-link" id="further-reading"></a></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>

<p><a href="https://percona.community/blog/2026/07/06/cross-site-disaster-recovery-with-percona-operator-for-mysql/">Cross-site Disaster Recovery with Percona Operator for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 Feature in Focus: DENY / Negative Grants</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/" />
      <id>https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/</id>
      <updated>2026-07-03T14:04:29+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 13.1 Preview is full of nice things.<br />
Some are immediately visible to developers, like the new JSON operators. Some are very useful to DBAs, such as configuration validation. …<br />
Continue reading \"MariaDB 13.1 Feature in Focus: DENY / Negative Grants\"<br />
The post MariaDB 13.1 Feature in Focus: DENY / Negative Grants appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/">MariaDB 13.1 Feature in Focus: DENY / Negative Grants</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://mariadb.org/download/?t=mariadb&amp;p=mariadb&amp;r=13.1.0&amp;os=Linux&amp;cpu=x86_64&amp;i=systemd&amp;mirror=bouwhuis">MariaDB 13.1</a> Preview is full of nice things.<br>
Some are immediately visible to developers, like the new JSON operators. Some are very useful to DBAs, such as configuration validation. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 Feature in Focus: DENY / Negative Grants&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/">MariaDB 13.1 Feature in Focus: DENY / Negative Grants</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-13-1-feature-in-focus-deny-negative-grants/">MariaDB 13.1 Feature in Focus: DENY / Negative Grants</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Continuent joins MariaDB Foundation as a Silver Sponsor</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/" />
      <id>https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/</id>
      <updated>2026-07-03T05:07:29+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is pleased to welcome Continuent as a new Silver Sponsor.<br />
Continuent develops solutions for organizations running business-critical applications on MariaDB and other MySQL-compatible databases. …<br />
Continue reading \"Continuent joins MariaDB Foundation as a Silver Sponsor\"<br />
The post Continuent joins MariaDB Foundation as a Silver Sponsor appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/">Continuent joins MariaDB Foundation as a Silver Sponsor</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is pleased to welcome <a href="https://www.continuent.com/">Continuent</a> as a new <a href="https://mariadb.org/donate/#silver-tier-from-eur-5000-per-year">Silver Sponsor.</a><br>
Continuent develops solutions for organizations running business-critical applications on MariaDB and other MySQL-compatible databases. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Continuent joins MariaDB Foundation as a Silver Sponsor&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/">Continuent joins MariaDB Foundation as a Silver Sponsor</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/continuent-joins-mariadb-foundation-as-a-silver-sponsor/">Continuent joins MariaDB Foundation as a Silver Sponsor</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/" />
      <id>https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/</id>
      <updated>2026-07-03T04:42:07+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Server has long supported a flexible plugin architecture. Plugins allow developers to extend server functionality in areas such as data types, auditing, storage engines, information schema tables, and more. …<br />
Continue reading \"Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages\"<br />
The post Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/">Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Server has long supported a flexible <a href="https://mariadb.org/plugins/">plugin architecture</a>. Plugins allow developers to extend server functionality in areas such as data types, auditing, storage engines, information schema tables, and more. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/">Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/lowering-the-barrier-for-mariadb-plugin-development-plugins-in-more-languages/">Lowering the Barrier for MariaDB Plugin Development: Plugins in More Languages</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Nextcloud renews its Silver sponsorship of MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/" />
      <id>https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/</id>
      <updated>2026-07-02T21:58:37+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that Nextcloud has renewed its Silver sponsorship for another year.<br />
Nextcloud and MariaDB are widely used together by organisations that want greater control over their data and infrastructure. …<br />
Continue reading \"Nextcloud renews its Silver sponsorship of MariaDB Foundation\"<br />
The post Nextcloud renews its Silver sponsorship of MariaDB Foundation appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/">Nextcloud renews its Silver sponsorship of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that <a href="https://nextcloud.com/">Nextcloud</a> has renewed its <a href="https://mariadb.org/donate/#silver-tier-from-eur-5000-per-year">Silver sponsorship</a> for another year.<br>
Nextcloud and MariaDB are widely used together by organisations that want greater control over their data and infrastructure. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Nextcloud renews its Silver sponsorship of MariaDB Foundation&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/">Nextcloud renews its Silver sponsorship of MariaDB Foundation</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/nextcloud-renews-its-silver-sponsorship-of-mariadb-foundation/">Nextcloud renews its Silver sponsorship of MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Still on MySQL 5.7 or 8.0? Those high-severity CVE fixes are covered</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/mysql-8-0-eol-support-cve-fixes-covered/" />
      <id>https://www.percona.com/blog/mysql-8-0-eol-support-cve-fixes-covered/</id>
      <updated>2026-07-02T08:01:50+03:00</updated>
      <author><name>Dennis Kittrell</name></author>
      <summary type="html"><![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 … Continued<br />
The post Still on MySQL 5.7 or 8.0? Those high-severity CVE fixes are covered appeared first on Percona.</p>
<p><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://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. If you&rsquo;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&rsquo;t have to do anything to qualify for them. We&rsquo;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&rsquo;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<a class="anchor-link" id="what-we-did"></a></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 can pull the updated builds from the usual private repository.</p>
<h2>Why this matters if you&rsquo;re still on 5.7 or 8.0<a class="anchor-link" id="why-this-matters-if-youre-still-on-5-7-or-8-0"></a></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&rsquo;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<a class="anchor-link" id="where-to-go-from-here"></a></h2>
<p>If you&rsquo;re on 5.7 or 8.0 and don&rsquo;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>&nbsp;</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" data-token-index="1">@Dennis Kittrell</span><span class="notion-enable-hover" data-token-index="2"> &ndash; Reviewed by </span><span class="notion-text-mention-token notion-enable-hover notion-focusable-token" 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" 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>

<p><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://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL Autovacuum Internals and Benchmark</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/07/01/postgresql-autovacuum-internals-benchmark/" />
      <id>https://percona.community/blog/2026/07/01/postgresql-autovacuum-internals-benchmark/</id>
      <updated>2026-07-01T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>PostgreSQL Autovacuum Internals and Benchmark Introduction Vacuum, or more precisely autovacuum, is the most important automatic maintenance task in PostgreSQL. It is key for performance, but also for long-term database survival. If it runs too often, it can damage performance. If it does not run often enough, performance can suffer. With too few workers, it takes too long. With too many, it consumes resources. If the maintenance work memory is not enough, the load can multiply due to multiple index scans. If you disable it completely, it will rise from the dead and run without limits.</p>
<p><a href="https://percona.community/blog/2026/07/01/postgresql-autovacuum-internals-benchmark/">PostgreSQL Autovacuum Internals and Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1 id="postgresql-autovacuum-internals-and-benchmark">PostgreSQL Autovacuum Internals and Benchmark<a class="anchor-link" id="postgresql-autovacuum-internals-and-benchmark"></a></h1>
<h2 id="introduction">Introduction<a class="anchor-link" id="introduction"></a></h2>
<p>Vacuum, or more precisely autovacuum, is the most important automatic maintenance task in PostgreSQL. It is key for performance, but also for long-term database survival. If it runs too often, it can damage performance. If it does not run often enough, performance can suffer. With too few workers, it takes too long. With too many, it consumes resources. If the maintenance work memory is not enough, the load can multiply due to multiple index scans. If you disable it completely, it will rise from the dead and run without limits.</p>
<p>I guess you get it. It is critical to understand what autovacuum does and how it does it.</p>
<p>Autovacuum is triggered when certain row count thresholds are crossed. In the final part of this post we describe a benchmark we run to validate if modified rows is the right approach to trigger automatic vacuum execution or we should consider something different like page based thresholds. We will also measure the impact index in vacuum.</p>
<p>This blog post explains how autovacuum works, but some previous basic understanding of PostgreSQL internals is required.</p>
<p>Here are the terms you&rsquo;ll need, feel free to skip if you already know them:</p>
<ul>
<li><strong>MVCC (multi-version concurrency control)</strong>: Rather than overwrite a row, PostgreSQL keeps multiple versions of it. This is used to provide consistent views to the different transactions running at the same time, which is why obsolete row versions pile up when there are long running transactions and/or tables are not vacuumed. MVCC is used by transactions to determine row visibility.</li>
<li><strong>Tuple</strong>: One on-disk version of a row. A row updated three times leaves behind three tuples.</li>
<li><strong>Dead tuple</strong>: A tuple that is not visible to any transaction. Reclaiming these is the vacuum&rsquo;s main job.</li>
<li><strong>Heap</strong>: A table&rsquo;s main structure, where the tuples live. Indexes are separate structures.</li>
<li><strong>Page (block)</strong>: The 8 KB unit PostgreSQL reads and writes. A heap is an array of pages. If a page is dirty, it means it contains data that hasn&rsquo;t been written to disk yet.</li>
<li><strong>TID</strong>: A tuple&rsquo;s address: which page, which slot. Inside of the pages there is an array that points to the actual row position in the page, the slot is the position in that array. This way row space inside of the page can be reorganized without changing the TID. Index entries are TIDs pointing into the heap.</li>
<li><strong>Vacuum</strong>: The operation that removes dead tuples (and does a few things more).</li>
<li><strong>Autovacuum</strong>: Vacuum that PostgreSQL runs for you, in the background, on its own schedule.</li>
<li><strong>Visibility map (VM)</strong>: A small per-table bitmap flagging which pages have all tuples visible or frozen (see freezing).</li>
<li><strong>Freezing</strong>: Stamping old tuples as permanently visible, so their transaction IDs are no longer relevant (see Transaction ID wraparound). Here permanent is a bit misleading, if the row is modified, the permanent tuple will become a dead tuple and will be removed by vacuum.</li>
<li><strong>Transaction ID (XID) wraparound</strong>: Each transaction is assigned an ID. This ID identifies which transactions modified which rows and is thus critical for visibility. The problem is that the transaction counter is finite and eventually wraps around. To avoid problems, older rows must be marked as permanently visible (frozen), this way their transaction id becomes irrelevant.</li>
<li><strong>Bloat</strong>: Space allocated by dead tuples, as dead tuples are not visible, it is wasted space. Vacuum works to reduce it, but can&rsquo;t always reverse it.</li>
<li><strong>Shared buffers</strong>: PostgreSQL&rsquo;s in-memory page cache. Nearly all reads and writes pass through it.</li>
<li><strong>WAL (write-ahead log)</strong>: Every change is logged here before it touches a data page, so the database can recover after a crash.</li>
<li><strong>Checkpoint</strong>: The point at which modified (&ldquo;dirty&rdquo;) pages in shared buffers are written out to the data files.</li>
</ul>
<h2 id="launcher--worker-architecture">Launcher &amp; Worker Architecture<a class="anchor-link" id="launcher-worker-architecture"></a></h2>
<p>The <strong>autovacuum launcher</strong> is a background process that starts autovacuum workers. Its goal is to start one worker per database every <code>autovacuum_naptime</code> seconds (default: 1 min). With N databases, this means the launcher starts a new worker roughly every <code>autovacuum_naptime / N</code> seconds, round-robin across databases. But it is not the launcher that starts the workers. It requests the postmaster to fork an <strong>autovacuum worker</strong> for the chosen database.</p>
<p>Workers, once spawned, run independently until they finish all eligible tables in their assigned database and then exit. Up to <code>autovacuum_max_workers</code> (default 3) workers can run concurrently, and there is no restriction on how many of those may be in the same database. If a database has many tables that need vacuuming, it can run multiple concurrent vacuum workers. In this case, workers coordinate to avoid vacuuming the same table.</p>
<p>A database approximately has a worker assigned every &ldquo;nap time seconds&rdquo; or later. A worker is assigned even if there are no tables requiring vacuum.</p>
<h2 id="table-selection--prioritization">Table Selection &amp; Prioritization<a class="anchor-link" id="table-selection-prioritization"></a></h2>
<p>This is the process inside a worker:</p>
<ol>
<li>Scans <code>pg_class</code> to enumerate all tables in the database, then fetches per-relation statistics (dead tuple counts, etc.) from the cumulative statistics system (pgstat) for each one.</li>
<li>Compares each table&rsquo;s dead-tuple count against the vacuum threshold (see the formula below).</li>
<li>Also checks if the table needs an ANALYZE (separate threshold).</li>
<li>Also checks for <strong>anti-wraparound</strong>: if <code>pg_class.relfrozenxid</code> age exceeds <code>autovacuum_freeze_max_age</code> (default 200M transactions), or if <code>pg_class.relminmxid</code> age exceeds <code>autovacuum_multixact_freeze_max_age</code> (default 400M), the table is vacuumed regardless of all thresholds and even <code>autovacuum_enabled = off</code> on the table.</li>
</ol>
<h3 id="prioritization">Prioritization<a class="anchor-link" id="prioritization"></a></h3>
<p>There is no table-level priority sorting in a database. A worker vacuums tables in the order they are collected from the <code>pg_class</code> scan. <code>do_autovacuum()</code> in <code>src/backend/postmaster/autovacuum.c</code> iterates the <code>table_oids</code> list directly. The worker claims each table sequentially by marking it as &ldquo;mine&rdquo; in shared memory (this triggers a brief lock to the memory structure so two workers don&rsquo;t pick the same table at the same time), and calls <code>table_recheck_autovac()</code> to re-read catalog/pgstat and confirm the table still needs work (as it could have been already vacuumed by another worker). Anti-wraparound urgency is handled one level up, at database selection: the launcher&rsquo;s <code>do_start_worker()</code> preferentially dispatches a worker to whichever database is closest to the wraparound limit. So there is no dead-tuple-count-based ordering of tables. Within a database, processing order is effectively catalog order.</p>
<h2 id="how-vacuum-finds-pages-to-process">How Vacuum Finds Pages to Process<a class="anchor-link" id="how-vacuum-finds-pages-to-process"></a></h2>
<p>Once a table is selected, the worker doesn&rsquo;t blindly scan every page. It uses the <strong>visibility map</strong> (VM) to skip pages that do not need vacuuming.</p>
<h3 id="the-visibility-map">The Visibility Map<a class="anchor-link" id="the-visibility-map"></a></h3>
<p>Every table has an associated visibility map, a bitmap with two bits per heap page:</p>
<ol>
<li>All-visible bit: every tuple on the page is visible to all current and future transactions. During a normal (non-aggressive) vacuum, this page can generally be skipped. There are no dead tuples to reclaim. However, even all-visible pages may be visited in some cases, such as for eager freezing or readahead optimization (pages are read sequentially even if some of them are not needed).</li>
<li>All-frozen bit: every tuple on the page is frozen, marked with the <code>HEAP_XMIN_FROZEN</code> infomask bits (since PostgreSQL 9.4, the value of <code>xmin</code> is <strong>preserved</strong> for forensics rather than physically overwritten with <code>FrozenTransactionId</code> although a lot of people still think the xmin is changed). The page can be skipped even during aggressive/anti-wraparound vacuum. An aggressive vacuum must visit all pages that are <em>not</em> all-frozen to freeze as many tuples as possible.</li>
</ol>
<p>The VM makes vacuuming efficient because it reduces the number of pages to visit while searching for dead tuples. If a 10GB table has dead tuples on only 50 pages, vacuum reads the VM (around 320KB for a 10GB heap, 2 bits per 8KB page) and then focuses on those 50 pages rather than the full 10GB. We already mentioned that a normal vacuum can also visit some additional pages for eager freezing or readahead, but the VM still eliminates the vast majority of random I/O.</p>
<p>Visibility-map bits are cleared by backends running DML statements and usually set by autovacuum workers or manually triggered vacuum operations.</p>
<p>The VM is also used by index-only scans to determine whether visiting the heap page to validate tuple visibility is needed. If the page in the VM is marked as &ldquo;all-visible,&rdquo; then visibility checks are not required and we don&rsquo;t need that extra access, improving performance significantly.</p>
<h3 id="the-scan-process">The Scan Process<a class="anchor-link" id="the-scan-process"></a></h3>
<p>The worker performs a <strong>sequential scan of the heap</strong>, but guided by the VM:</p>
<ol>
<li>Read the VM to identify pages that are NOT all-visible and may contain dead tuples.</li>
<li>For each such page, read it into shared buffers (if not already there).</li>
<li>Examine each tuple&rsquo;s header (<code>t_xmin</code>, <code>t_xmax</code>, <code>t_infomask</code>) to determine if the tuple is dead, meaning it was deleted or updated, and no running transaction can see it anymore.</li>
<li>Dead tuples are collected into an in-memory <strong>dead-TID store</strong>, since PG17 a <code>TidStore</code>, a compact adaptive-radix-tree keyed by block number that replaced the old sorted <code>ItemPointer</code> array and its hard 1 GB cap. Its size is bounded by <code>autovacuum_work_mem</code> (default -1, which falls back to <code>maintenance_work_mem</code>, default 64MB). For manual <code>VACUUM</code>, <code>maintenance_work_mem</code> is used directly.</li>
<li>If the work memory fills up before the table is fully scanned, the worker pauses the heap scan, processes the accumulated dead tuples (index cleanup + heap cleanup), then resumes the heap scan from where it stopped. This means a single vacuum of a large, heavily updated table may involve multiple passes through the indexes.</li>
</ol>
<h3 id="limiting-cache-impact-with-the-buffer-ring">Limiting Cache Impact with the Buffer Ring<a class="anchor-link" id="limiting-cache-impact-with-the-buffer-ring"></a></h3>
<p>The step 2 above says &ldquo;read the heap page into shared buffers&rdquo;), but if vacuum has to pull every page it scans into <code>shared_buffers</code>, vacuuming a large table would evict the pages that other queries depend on, trashing the cache during a maintenance task. PostgreSQL prevents this with a <strong>buffer access strategy</strong>, commonly called a <strong>ring buffer</strong>.</p>
<p>Rather than allocating pages all over the shared pool, vacuum uses a small <strong>ring</strong> of shared pool pages that it reuses circularly: when it needs a buffer for a new page, and the ring is full, it recycles the oldest buffer in the ring instead of claiming another from <code>shared_buffers</code>. If vacuum needs a page already in the shared pool, that page is not added to the ring. The ring size is set by <code>vacuum_buffer_usage_limit</code>, default 2 MB in PG18 (256 buffers of 8 KB), ranges from 128 kB to 16 GB, with a limit of 1/8 of <code>shared_buffers</code> (you can set it higher, but it will limited to that value). A value of <code>0</code> disables the ring entirely, letting vacuum use as much of <code>shared_buffers</code> as it needs. The same limit applies to <code>ANALYZE</code> and to autovacuum (which runs the same vacuum code). The <code>VACUUM</code> command accepts a per-statement <code>BUFFER_USAGE_LIMIT</code> option.</p>
<p>The ring has consequences: <strong>When the buffer being recycled is still dirty, vacuum must write it out before reusing the slot</strong>. As WAL is written before the page, we have to flush any outstanding WAL for that page first. So once vacuum dirties more pages than the ring can hold, it begins doing <strong>its own writes</strong> inline rather than leaving them all for the checkpointer or background writer. This may look as a trade-off as the ring caps vacuum&rsquo;s cache footprint, but requires vacuum to perform some of its own write-back (and WAL flushing) as it runs. But if a page was read into the ring, probably that pages was not very active and will not be read again soon. Raising <code>vacuum_buffer_usage_limit</code> (or setting it to <code>0</code>) relaxes the limit: a faster vacuum. But a faster vacuum that will evict more active pages and later will require more work by the checkpointer.</p>
<h3 id="determining-tuple-liveness">Determining Tuple Liveness<a class="anchor-link" id="determining-tuple-liveness"></a></h3>
<p>For each tuple on a non-all-visible page, vacuum checks:</p>
<ul>
<li><code>t_xmin</code> (inserting transaction identified): Check whether it committed. If the inserting transaction aborted, the tuple is dead immediately.</li>
<li><code>t_xmax</code> (deleting/updating transaction identifier): Check whether it committed and is older than the oldest running transaction (<code>OldestXmin</code>). If so, no active transaction can see this tuple version and the tuple is dead.</li>
<li>Vacuum, like the other backends, reads <code>pg_xact</code> (the commit log / CLOG) to determine transaction commit status, and sets <strong>hint bits</strong> (<code>HEAP_XMIN_COMMITTED</code>, <code>HEAP_XMAX_COMMITTED</code>, etc.) on tuple headers so future accesses don&rsquo;t need to re-check <code>pg_xact</code>. Changing the hint bits marks the page as dirty, but does not save that change in the WAL, unless specified in the configuration (checksums enabled or wal_log_hints). The purpose of the hint bits is help future transactions know the outcome of the inserting/modifying transactions without checking the commit log.</li>
</ul>
<p><code>OldestXmin</code> is the oldest transaction ID that any running transaction might still need to see, also known sometimes as the xmin horizon. Tuples deleted or replaced by transactions newer than <code>OldestXmin</code> <strong>cannot be vacuumed</strong> because some active transactions might still need them. This is why long-running transactions limit the space that vacuum can reclaim.</p>
<h2 id="what-happens-to-heap-pages">What Happens to Heap Pages<a class="anchor-link" id="what-happens-to-heap-pages"></a></h2>
<p>Once dead tuples are identified on a page:</p>
<ol>
<li>Dead tuple line pointers are set to <code>LP_DEAD</code> during the heap scan phase. Later, after index cleanup removes all dangling index references, vacuum performs a second heap pass that converts these to <code>LP_UNUSED</code>, making the slots available for reuse. (For tables with no indexes, vacuum can mark <code>LP_UNUSED</code> immediately since there are no index pointers to worry about.)</li>
<li>The page is compacted. Live tuples are shuffled toward the high end of the page, and free space is consolidated in the middle (between the line pointer array and the tuple data area). This is called <strong>page pruning/defragmentation</strong>. It updates the page&rsquo;s <code>pd_lower</code> (end-of-line pointers) and <code>pd_upper</code> (start-of-tuple data) to reflect the new free space.</li>
<li>The page is marked dirty in shared buffers. It will be written back to disk by the background writer, at the next checkpoint or if the ring buffer is full and vacuum needs that space. This is the <code>vacuum_cost_page_dirty</code> cost event (adds 20 to the cost global cost of running vacuum operations).</li>
<li>The VM is updated. If, after removing dead tuples, every remaining tuple on the page is visible to all transactions, the all-visible bit is set. During aggressive/anti-wraparound vacuum, if all tuples are also frozen, the all-frozen bit is set.</li>
<li>The FSM (Free Space Map) tree is updated periodically (every <code>VACUUM_FSM_EVERY_PAGES</code> pages or after heap/index cleanup pass, not after every individual page is added to the FSM) to advertise newly available space, so future DML operations can reuse it.</li>
</ol>
<h3 id="heap-truncation">Heap Truncation<a class="anchor-link" id="heap-truncation"></a></h3>
<p>After processing all pages, vacuum checks whether the <strong>last pages</strong> of the heap file are entirely empty (all dead tuples were removed and there are no live tuples). If so, it <strong>truncates the file</strong>, physically shrinking it and returning disk space to the OS. This is the only situation where vacuum reduces the on-disk size of a table. The space reclaimed in the middle of the file is reused, not returned to the filesystem.</p>
<p>Truncation requires an <strong>AccessExclusiveLock</strong> lock during the truncation, which can cause a short stall on concurrent access. Table truncation can be disabled per-table or globally with <code>vacuum_truncate = off</code>.</p>
<h2 id="index-cleanup">Index Cleanup<a class="anchor-link" id="index-cleanup"></a></h2>
<p>Index cleanup is also required and is often an expensive part of vacuum. Indexes must be cleaned because they contain pointers (TIDs) to heap tuples. If the corresponding heap tuple is dead, the index entry becomes a <strong>dangling pointer</strong> and must be removed.</p>
<h3 id="the-process">The Process<a class="anchor-link" id="the-process"></a></h3>
<ol>
<li>After the heap scan (or after <code>maintenance_work_mem</code> fills), vacuum has its dead-TID store populated (block-ordered).</li>
<li>For <strong>each index</strong> on the table, vacuum calls the index access method&rsquo;s <code>ambulkdelete</code> function. For B-tree indexes, this invokes <code>btbulkdelete()</code> then <code>btvacuumscan()</code>, which scans the <strong>entire index in physical order</strong> (every page except the metapage, including all leaf pages), checking every index entry&rsquo;s TID against the dead-TID store. Matching entries are removed. (See <code>btvacuumscan()</code> in <code>src/backend/access/nbtree/nbtree.c</code>, which processes each page with <code>btvacuumpage()</code>.)</li>
<li>Only <strong>after</strong> all indexes are cleaned does vacuum go back and clean the heap pages (mark <code>LP_UNUSED</code>, compact). This is handled by <code>lazy_vacuum_all_indexes()</code> followed by the heap cleanup phase in <a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/access/heap/vacuumlazy.c" target="_blank" rel="noopener noreferrer"><code>src/backend/access/heap/vacuumlazy.c</code></a>.</li>
</ol>
<p>The order of operations is important: index entries must be removed <strong>before</strong> their heap tuple slots are recycled, otherwise an index scan could follow a pointer to a slot that now holds a different, unrelated tuple, returning incorrect results.</p>
<h3 id="why-index-vacuum-is-expensive">Why Index Vacuum Is Expensive<a class="anchor-link" id="why-index-vacuum-is-expensive"></a></h3>
<p>The PostgreSQL documentation for the <a href="https://www.postgresql.org/docs/18/index-functions.html" target="_blank" rel="noopener noreferrer"><code>ambulkdelete</code> interface</a> states:</p>
<blockquote>
<p><em>This is a &ldquo;bulk delete&rdquo; operation that is intended to be implemented by <strong>scanning the whole index</strong> and checking each entry to see if it should be deleted.</em></p>
</blockquote>
<p>There is <strong>no partial index scan optimization</strong>. The design requires scanning the index completely. The dead TIDs are sorted by heap location, but index entries are ordered by key value, so there is no way to locate only the affected index pages without scanning all leaf pages.</p>
<p>Consequences:</p>
<ul>
<li>Each index is completely scanned for every vacuum cycle. For a table with 5 indexes and 100GB of index data, vacuum reads 500GB of index pages each full pass.</li>
<li>If the space for dead-TID is too small and the heap scan must pause mid-way, <code>ambulkdelete</code> is called <strong>multiple times</strong>, once per batch of dead TIDs. The documentation states: &ldquo;Because of limited <code>maintenance_work_mem</code>, <code>ambulkdelete</code> might need to be called more than once when many tuples are to be deleted.&rdquo; Each call performs a full index scan. With a 100GB table, 64MB of work memory, and 5 indexes, this can result in dozens of full index scans. (PG17&rsquo;s <code>TidStore</code> packs far more dead TIDs into the same memory, so this multi-pass case is much rarer than it was in previous versions.)</li>
<li>This is why increasing <code>autovacuum_work_mem</code> (or <code>maintenance_work_mem</code>) for vacuum-heavy workloads can be required. If the autovacuum operation is written into the log (<code>log_autovacuum_min_duration</code>), look for <code>index scans</code>.</li>
</ul>
<h3 id="index-cleanup-optimizations">Index Cleanup Optimizations<a class="anchor-link" id="index-cleanup-optimizations"></a></h3>
<ul>
<li>Bypass optimization (near-zero dead tuples): when 2% or fewer of the table&rsquo;s pages contain <code>LP_DEAD</code> items and the accumulated dead-TID storage stays under 32MB, vacuum enters bypass mode: it skips both index cleanup and the second heap-vacuuming pass, avoiding a full index scan as the benefit is reduced. This avoids the jump between &ldquo;zero dead tuples is instant&rdquo; and &ldquo;one dead tuple requires multiple full index scans&rdquo;. (See <code>BYPASS_THRESHOLD_PAGES</code> in <code>vacuumlazy.c</code>.)</li>
<li><code>INDEX_CLEANUP</code> parameter: <code>AUTO</code> (default) allows the bypass optimization; <code>OFF</code> forces vacuum to always skip index vacuuming (accepting index bloat); <code>ON</code> forces full index vacuuming every time. The <code>OFF</code> setting is useful for emergency situations where you need vacuum to advance <code>relfrozenxid</code> quickly. (See <a href="https://www.postgresql.org/docs/18/sql-vacuum.html" target="_blank" rel="noopener noreferrer">VACUUM documentation</a>.)</li>
<li>B-tree &ldquo;page deletion&rdquo;: when a B-tree leaf page becomes empty after vacuum removes all its entries, the page is marked as deleted and can be recycled. The file does not reduce its size, but the pages can be reused later.</li>
<li>Simple B-tree tuple deletion: when a query visits a dead tuple via an index scan, it can mark that pointer as dead in the index itself. If, at a later time, more space is needed in that page, instead of performing a split, the index entries pointing to dead tuples can be removed to make room for the new entry.</li>
<li>Bottom-up deletion (PG14+): B-tree indexes can proactively remove known-dead entries during page splits, reducing the work left for vacuum.</li>
</ul>
<h2 id="concurrency-vacuum-vs-active-backends">Concurrency: Vacuum vs. Active Backends<a class="anchor-link" id="concurrency-vacuum-vs-active-backends"></a></h2>
<p>Vacuum runs concurrently with normal database operations. It does <strong>not</strong> lock the table exclusively (it takes a <code>ShareUpdateExclusiveLock</code>, which conflicts only with other vacuums, <code>ALTER TABLE</code>, and certain <code>CREATE INDEX</code> operations).</p>
<h3 id="page-level-locking">Page-Level Locking<a class="anchor-link" id="page-level-locking"></a></h3>
<p>When vacuum needs to read or modify a heap page it uses the common shared buffer access locks:</p>
<ol>
<li>For reading (AKA identifying dead tuples), vacuum acquires a shared <strong>buffer content lock</strong> on the shared buffer.</li>
<li>For pruning and freezing (AKA removing dead tuples, setting vm flags), vacuum requires a <strong>buffer cleanup lock</strong>. This is an exclusive lock (no other backend can hold a lock on the buffer). In a non-aggressive vacuum, if the cleanup lock cannot be obtained immediately (another transaction has a shared lock for example), vacuum <strong>skips pruning/freezing on that page</strong> and moves on. An aggressive (anti-wraparound) vacuum will wait for the lock instead.</li>
<li>These locks are held only for the duration of the in-memory page operation and should be very fast. They do <strong>not</strong> block concurrent <code>SELECT</code> or <code>DML</code> on other pages.</li>
</ol>
<h3 id="what-happens-when-a-backend-reads-a-page-being-vacuumed">What Happens When a Backend Reads a Page Being Vacuumed<a class="anchor-link" id="what-happens-when-a-backend-reads-a-page-being-vacuumed"></a></h3>
<ul>
<li>If vacuum is <strong>currently modifying</strong> the page (holding the cleanup lock): the backend waits until vacuum releases the lock, then reads the page in its post-vacuum state. The backend sees only live tuples. The dead ones have just been removed. This is safe because the dead tuples were invisible to the backend&rsquo;s snapshot anyway.</li>
<li>If vacuum <strong>skipped</strong> the page (could not get the cleanup lock): dead tuples remain on the page. They are invisible to backends via MVCC visibility checks and will be cleaned up in a future vacuum cycle.</li>
<li>If vacuum has <strong>not yet reached</strong> the page: the backend reads normally. Dead tuples are still present but invisible to the backend&rsquo;s MVCC snapshot. They are skipped during visibility checks.</li>
</ul>
<h3 id="what-happens-when-a-backend-writes-while-vacuum-runs">What Happens When a Backend Writes While Vacuum Runs<a class="anchor-link" id="what-happens-when-a-backend-writes-while-vacuum-runs"></a></h3>
<ul>
<li>INSERT into a vacuumed page: vacuum freed space, the FSM knows about it, the inserter uses that space. No conflict.</li>
<li>UPDATE/DELETE on the same table: concurrent DML does not conflict with vacuum&rsquo;s <code>ShareUpdateExclusiveLock</code>. If a backend deletes/updates a tuple on a page vacuum hasn&rsquo;t reached yet, vacuum will find and clean it (if committed by then). If the tuple is on a page vacuum already passed, it will be caught by the next vacuum cycle.</li>
<li>UPDATE/DELETE on a page vacuum is currently processing: the buffer lock serializes access. If vacuum removes dead tuples and the backend then updates a live tuple on the same page, there&rsquo;s no conflict because they operate on different tuple slots.</li>
</ul>
<h3 id="index-scan-during-index-cleanup">Index Scan During Index Cleanup<a class="anchor-link" id="index-scan-during-index-cleanup"></a></h3>
<p>While vacuum scans an index to remove dead entries, concurrent index scans by backends can proceed normally. B-tree indexes use a <strong>pin-based</strong> protocol that avoids vacuum deleting a page that any backend has pinned. Specifically, vacuum marks pages as half-dead first, and only recycles them when no backend holds a pin. This ensures index scans never follow a pointer to a recycled page.</p>
<h2 id="threshold-formula">Threshold Formula<a class="anchor-link" id="threshold-formula"></a></h2>
<p>A table becomes eligible for autovacuum when its dead-tuple count crosses a threshold. As of PG18 the calculation is limited by <code>autovacuum_vacuum_max_threshold</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-0">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">vacuum_threshold = Min(
</span></span><span class="line"><span class="cl"> autovacuum_vacuum_max_threshold,
</span></span><span class="line"><span class="cl"> autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples
</span></span><span class="line"><span class="cl">)</span></span></code></pre>
</div>
</div>
</div>
<p>Defaults:</p>
<ul>
<li><code>autovacuum_vacuum_threshold = 50</code></li>
<li><code>autovacuum_vacuum_scale_factor = 0.2</code></li>
<li><code>autovacuum_vacuum_max_threshold = 100,000,000</code> (<strong>new in PG18</strong>).</li>
</ul>
<p><code>autovacuum_vacuum_max_threshold</code> is used to avoid massive tables requiring a huge number of dead tuples before firing vacuum.</p>
<h3 id="insert-triggered-vacuum-pg13">Insert-triggered vacuum (PG13+)<a class="anchor-link" id="insert-triggered-vacuum-pg13"></a></h3>
<p>A table also becomes eligible based on inserts alone. Since PG18 the scale-factor term is multiplied by the <strong>unfrozen fraction</strong> of the table:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-1">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">vacuum_insert_threshold =
</span></span><span class="line"><span class="cl"> autovacuum_vacuum_insert_threshold
</span></span><span class="line"><span class="cl"> + autovacuum_vacuum_insert_scale_factor * reltuples * (1 - relallfrozen / relpages)</span></span></code></pre>
</div>
</div>
</div>
<p>Defaults:</p>
<ul>
<li><code>autovacuum_vacuum_insert_threshold = 1000</code></li>
<li><code>autovacuum_vacuum_insert_scale_factor = 0.2</code></li>
</ul>
<p>The <code>(1 - relallfrozen / relpages)</code> is used to avoid time between runs constantly growing for tables that are mostly inserted. In previous versions, as the table grows the number of inserted rows required to trigger a vacuum used to grow also. With this optimization, the number of inserts required to trigger vacuum tends to be more constant.</p>
<h3 id="analyze-trigger">ANALYZE trigger<a class="anchor-link" id="analyze-trigger"></a></h3>
<p>The following formula applies to determine if ANALYZE is required:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-2">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">changed_tuples &gt; analyze_threshold + analyze_scale_factor * reltuples</span></span></code></pre>
</div>
</div>
</div>
<p>Defaults: <code>autovacuum_analyze_threshold = 50</code>, <code>autovacuum_analyze_scale_factor = 0.1</code>.</p>
<p>Per-table overrides via <code>ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = ...)</code> that take precedence over default configuration.</p>
<h2 id="cost-based-vacuum-throttling">Cost-Based Vacuum Throttling<a class="anchor-link" id="cost-based-vacuum-throttling"></a></h2>
<p>The cost-based mechanism is linked directly to the page-level operations described above. Every time vacuum touches a page, it incurs a cost depending on what happened:</p>
<p>Vacuum I/O is throttled via a cost/delay mechanism shared with manual <code>VACUUM</code>:</p>
<ul>
<li><code>vacuum_cost_page_hit</code> = 1, page already in shared buffers (cheap: no I/O, only CPU to inspect tuples)</li>
<li><code>vacuum_cost_page_miss</code> = 10 (PG17 and earlier; <strong>changed to 2 in PG18</strong>), page read from OS into shared buffers (may still be in OS page cache, so not necessarily a physical disk read)</li>
<li><code>vacuum_cost_page_dirty</code> = 20, vacuum modified the page (removed dead tuples, compacted it). This is the most expensive because it generates a dirty buffer that must eventually be written to disk by the background writer/checkpointer</li>
</ul>
<p>These costs are <strong>additive per page</strong>. On PG17 (miss = 10): a page read from disk and then modified costs <strong>30</strong>. On PG18 (miss = 2): the same scenario costs <strong>22</strong>. A page already in shared buffers (hit = 1) that gets modified costs <strong>21</strong> on both versions.</p>
<p>The cost limit is <strong>shared across all running autovacuum workers</strong>. If 3 workers are active, each effectively gets <code>200 / 3</code> or around <code>66</code> cost budget per cycle. This means adding more workers doesn&rsquo;t linearly increase I/O as all workers get their cost limit reduced. The global limit is <code>autovacuum_vacuum_cost_limit</code>, that by default is -1, meaning it inherits <code>vacuum_cost_limit</code>, which by default is 200.</p>
<p>The workers accumulate cost points as operations happen. When, for a specific worker, the accumulated total reaches its assigned limit, the worker will sleeps for <code>autovacuum_vacuum_cost_delay</code> (default 2ms).</p>
<h3 id="example">Example<a class="anchor-link" id="example"></a></h3>
<p>With defaults (limit=200, delay=2ms), one worker on <strong>PG17</strong> (miss=10):</p>
<ul>
<li>If all pages are a miss + dirty write (cost 30 each): 200/30 ~ <strong>6 pages</strong>, then sleep 2ms, giving ~3,000 pages/sec.</li>
<li>If every page is already in shared buffers and gets modified (hit + dirty = 21): 200/21 ~ <strong>9 pages</strong>, then sleep 2ms, giving ~4,500 pages/sec.</li>
<li>If all pages are a shared-buffer hit with no modifications (cost 1 each): 200 pages, then sleep 2ms, giving ~100,000 pages/sec.</li>
</ul>
<p>On <strong>PG18</strong> (miss=2), miss + dirty reduces the cost to 22 per page, so throughput for cold pages rises to 200/22 ~ 9 pages per cycle.</p>
<p>If we have 3 workers sharing the limit, then each gets 66 cost/cycle, so throughput per worker drops proportionally.</p>
<p>For large this default is often <strong>too conservative</strong>. Common tuning: raise <code>autovacuum_vacuum_cost_limit</code> to 1000-2000 and/or reduce <code>cost_delay</code> to 0 on critical tables.</p>
<p>The manual <code>VACUUM</code> parameter <code>vacuum_cost_delay</code> defaults to 0 (no throttling). Autovacuum workers use <code>autovacuum_vacuum_cost_delay</code>, which has the default value of 2ms since PG12 (earlier versions defaulted to 20ms). Per-table storage parameters <code>autovacuum_vacuum_cost_delay</code> / <code>autovacuum_vacuum_cost_limit</code> override the globals for that specific table. This a way to tune the impact of high-churn tables on the shared cost.</p>
<h2 id="what-drives-vacuum-cost">What Drives Vacuum Cost<a class="anchor-link" id="what-drives-vacuum-cost"></a></h2>
<p>As we&rsquo;ve seen, autovacuum is triggered by the number of dead or inserted tuples. But is the real cost driven by the number of dead tuples, or by the number of pages it has to visit and clean?.</p>
<p>We designed a benchmark to try to discover which is the real cost driver for autovacuum operations.</p>
<h3 id="the-question">The question<a class="anchor-link" id="the-question"></a></h3>
<p>We have two hypotheses that we want to analyze:</p>
<ol>
<li>Whether autovacuum cost is driven by the <strong>count of dead tuples</strong> or by the <strong>number of heap pages</strong> those dead tuples are spread across.</li>
<li>How the <strong>number of indexes</strong> amplifies that cost.</li>
</ol>
<h3 id="the-table-and-the-key-variable">The table and the key variable<a class="anchor-link" id="the-table-and-the-key-variable"></a></h3>
<p>We will use a single table for every run of the benchmark. We will drop and recreate the table each time:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-3">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">bench_table</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w"> </span><span class="c1">-- sequential 1 .. 10,000,000
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w"> </span><span class="n">val</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">padding</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="c1">-- repeat('x', 96)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>The padding column fixes the row width at 128 bytes, giving around <strong>58 rows per 8 KB page</strong>. For 10M rows we will have <strong>172,414 heap pages</strong> or 1.3 GB. We fill the table, then run <code>VACUUM FREEZE</code> so every page starts <strong>all-visible and all-frozen</strong> to have a clean baseline. Then we create 0 to 5 <strong>redundant B-tree indexes</strong>, all on <code>id</code>. Each index is a separate physical structure that vacuum must scan in full.</p>
<p>The independent variable here is <em>the distribution of dead tuples</em>. We use two strategies delete the <strong>same number of rows</strong>. They differ only in which pages are touched:</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>DELETE predicate (10%)</th>
<th>Pages dirtied</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>compact</strong></td>
<td><code>WHERE id &lt;= 1,000,000</code></td>
<td>first ~10% of pages (low ids = first physical pages)</td>
</tr>
<tr>
<td><strong>spread</strong></td>
<td><code>WHERE id % 10 = 0</code></td>
<td>100% of pages (every page loses between 5 and 6 rows of its 58 rows)</td>
</tr>
</tbody>
</table>
<p>For this benchmark, we use <code>DELETE</code> rather than <code>UPDATE</code> so no new tuple versions are created. The table does not grow and no index entries are added (no leaf page splits).</p>
<h3 id="the-test-matrix">The test matrix<a class="anchor-link" id="the-test-matrix"></a></h3>
<p>We have 6 index counts (0-5), multiplied by 3 dead-tuple percentages (10/25/50%) and 2 distributions gives us 36 combinations. We repeated each combination 10 times for a total of 360 runs.</p>
<h3 id="how-a-single-run-is-measured">How a single run is measured<a class="anchor-link" id="how-a-single-run-is-measured"></a></h3>
<ol>
<li>Recreate the table, fill it with data, <code>VACUUM FREEZE</code>, build the required indexes for the test (autovacuum disabled on the table throughout setup).</li>
<li><code>DELETE</code> to generate the dead tuples for this combination.</li>
<li>Pre-test <code>CHECKPOINT</code>: flush the buffers dirtied during setup, so the post-test checkpoint will only account for pages autovacuum makes dirty.</li>
<li>Reset the shared I/O counters: <code>pg_stat_reset_shared('io' | 'bgwriter' | 'checkpointer')</code>. Note that we do not call <code>pg_stat_reset()</code>, which would zero <code>n_dead_tup</code> and prevent autovacuum from triggering.</li>
<li>Record the start time, then enable autovacuum on the table with parameters that should trigger autovacuum (<code>autovacuum_vacuum_threshold = 1</code>, <code>autovacuum_vacuum_scale_factor = 0</code>). As <code>autovacuum_naptime</code> is 1s, the launcher should pick the table up approximately within a second.</li>
<li>Poll every 0.5 s until vacuum is done: <code>last_autovacuum</code> is after start_time<code>and</code>n_dead_tup = 0`.</li>
<li>Stop the clock, force a post-test checkpoint, and collect metrics.</li>
</ol>
<h3 id="what-is-captured-and-from-where">What is captured, and from where<a class="anchor-link" id="what-is-captured-and-from-where"></a></h3>
<table>
<thead>
<tr>
<th>Signal</th>
<th>Source</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Wall-clock <code>duration_s</code></td>
<td><code>clock_gettime</code> (monotonic)</td>
<td>approximate time (vacuum+nap)</td>
</tr>
<tr>
<td>Autovacuum-worker reads/hits/writes</td>
<td><code>pg_stat_io</code>, filtered to <code>backend_type = 'autovacuum worker'</code></td>
<td>isolates the worker from every other process (PG18)</td>
</tr>
<tr>
<td>Heap vs index blocks</td>
<td><code>pg_statio_user_tables</code> (before/after diff)</td>
<td>specific table data</td>
</tr>
<tr>
<td>Write breakdown</td>
<td><code>pg_stat_io</code> per backend + <code>pg_stat_checkpointer</code></td>
<td>who wrote the dirty pages</td>
</tr>
<tr>
<td>Dirty-page count / completion</td>
<td><code>pg_visibility_map_summary</code> (from <code>pg_visibility</code>)</td>
<td></td>
</tr>
</tbody>
</table>
<p>I/O is recorded two ways: <strong>operation counts</strong> (<code>reads</code>/<code>writes</code>, which can be multi-block) and <strong>byte-derived page counts</strong> (<code>read_bytes</code>/<code>write_bytes</code> &divide; 8192, exact regardless of multi-block coalescing). We decided to look at the byte-derived counts. Each combination&rsquo;s 10 iterations are aggregated as a <strong>median with an interquartile (Q1-Q3) band</strong>.</p>
<h3 id="results">Results<a class="anchor-link" id="results"></a></h3>
<p>This is the test environment we used:</p>
<ul>
<li>PostgreSQL: 18.4 (PGDG, <code>pg_visibility</code> contrib)</li>
<li>OS / host: Ubuntu 24.04 LTS, x86_64, 4 dedicated vCPU / 15 GiB RAM, SSD-backed</li>
<li>Execution: benchmark runs locally on the DB host over the Unix socket (no network in the timing path)</li>
<li>Key GUCs:
<ul>
<li><code>shared_buffers = 4GB</code></li>
<li><code>maintenance_work_mem = 1GB</code></li>
<li><code>work_mem = 64MB</code></li>
<li><code>autovacuum_naptime = 1s</code></li>
<li><code>autovacuum_vacuum_cost_delay = 2ms</code></li>
<li><code>autovacuum_vacuum_cost_limit = 200</code></li>
<li><code>vacuum_cost_page_miss = 2</code></li>
<li><code>checkpoint_timeout = 15min</code></li>
<li><code>max_wal_size = 4GB</code></li>
<li><code>full_page_writes = on</code></li>
<li><code>track_io_timing = on</code></li>
</ul>
</li>
<li>Per-table triggers:
<ul>
<li><code>autovacuum_vacuum_threshold = 1</code></li>
<li><code>autovacuum_vacuum_scale_factor = 0</code></li>
</ul>
</li>
<li>Workload:
<ul>
<li>Table with 10,000,000 rows (172,414 heap pages, ~1.3 GB)</li>
<li>2.4 GB working set with 5 indexes (around 225 MB each), fits completely in <code>shared_buffers</code></li>
</ul>
</li>
<li>Sampling: 36 combinations x 10 iterations = 360 runs</li>
</ul>
<p>The whole 2.4 GB working set (heap plus five 225 MB indexes) is resident in <code>shared_buffers</code>. The <code>maintenance_work_mem = 1 GB</code> keeps index cleanup single-pass. Autovacuum throttling is at the default (<code>cost_delay = 2ms</code>). Durations below are 10-iteration medians.</p>
<p>Hypothesis 1: it&rsquo;s pages, not tuples. At 0 indexes:</p>
<table>
<thead>
<tr>
<th>dead tuples</th>
<th style="text-align: right">compact</th>
<th style="text-align: right">spread</th>
<th style="text-align: right">spread / compact</th>
</tr>
</thead>
<tbody>
<tr>
<td>10% (1.0M)</td>
<td style="text-align: right">5.0 s</td>
<td style="text-align: right">43.6 s</td>
<td style="text-align: right">8.7x</td>
</tr>
<tr>
<td>25% (2.5M)</td>
<td style="text-align: right">11.5 s</td>
<td style="text-align: right">43.6 s</td>
<td style="text-align: right">3.8x</td>
</tr>
<tr>
<td>50% (5.0M)</td>
<td style="text-align: right">21.8 s</td>
<td style="text-align: right">43.4 s</td>
<td style="text-align: right">2.0x</td>
</tr>
</tbody>
</table>
<p>First thing we see is that the <strong>spread column is almost constant</strong> (43.6, 43.6, 43.4 s) as the dead-tuple count goes from 1M to 5M, because in every case 100% of pages are dirtied. This means that pages visited is the cost driver. Meanwhile the compact column scales because, for compact, more dead tuples means more pages become dirty. When we have 50% dead tuples, the cost is half the spread.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_duration_compact_vs_spread.png" alt="Autovacuum duration, compact vs spread, one panel per dead-tuple percentage. Spread (dashed) sits far above compact (solid) and is nearly flat across 10/25/50%."></figure>
</p>
<p>The following chart plots every run by <em>dirty-page percentage</em> rather than dead-tuple count.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_dirty_pages_vs_duration.png" alt="Scatter of autovacuum duration against percentage of pages dirty before vacuum; points rise with dirty-page %, colored by index count."></figure>
</p>
<p>For the second hypothesis, we see that indexes amplify the cost. Each redundant index adds a near-constant increment (compact 50%): 21.8, 28.1, 32.6, 37.6, 42.3, 47.4 s. Around 5s per index. The number of dead pages also adds some cost, but if the number of dead pages is reduced, then the impact of indexes is lower (compact 10%)</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_duration_by_indexes.png" alt="Autovacuum duration versus number of indexes, compact and spread panels; each line rises roughly linearly with index count."></figure>
</p>
<p>The I/O data shows some <strong>distribution-specific</strong> behaviors. For spread 50%, heap blocks accessed hold at <strong>547,323</strong> across 1-5 indexes (the heap is scanned once, guided by the VM) while index blocks grow <strong>+27,422 per index</strong>. Each <code>ambulkdelete</code> is a full leaf scan. Compact 50% behaves differently: the heap io is lower (<strong>288,699</strong>, since fewer pages have dead tuples) but index blocks climb <strong>4x faster (~+109,533 per index)</strong>. Deleting contiguous id ranges empties whole B-tree leaf pages, adding page-deletion and recycling work (B-trees recycle fully-empty pages rather than merging partially-filled ones) on top of the scan. One thing worth noting is that heap access is <strong>not</strong> flat from 0 to 1 index, it jumps (spread 374,903 to 547,323) because index cleanup forces vacuum&rsquo;s second heap pass. After that, it is flat only across 1-5 indexes.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_heap_vs_index_io.png" alt="Stacked heap-versus-index blocks accessed by index count; heap roughly constant across 1&ndash;5 indexes while index I/O grows linearly, more steeply for compact than spread."></figure>
</p>
<p>The write breakdown tells us that who writes vacuum&rsquo;s dirtied pages is not fixed. With zero or one index, the autovacuum worker writes almost nothing: it dirties heap buffers and leaves them for the checkpointer, which flushes all of them in those cases. But as indexes are added, the <strong>worker&rsquo;s own writes increase</strong>: spread 50% goes 0, 27k, 54k, 82k, 109k, 136k pages written by the worker for 0-5 indexes (compact 50%: 0, 0, 14k, 27k, 41k, 55k). This is the effect of the <strong>buffer ring</strong> (see <em>Limiting Cache Impact</em> above): once index cleanup dirties more pages than the 2 MB ring can hold, the worker has to write and evict them itself rather than defer to the checkpointer. So the write cost shifts from the checkpointer toward the worker as index count grows, a direct consequence of the cache-protecting ring.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_write_breakdown.png" alt="Write breakdown by process; at low index counts the checkpointer does nearly all writes, but the autovacuum worker&rsquo;s own writes grow with index count."></figure>
</p>
<p>Finally we have a heat map of the durations across all 36 combinations:</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/pep_autovacuum_duration_heatmap.png" alt="Heatmap of autovacuum duration for every distribution/dead-percentage/index-count combination."></figure>
</p>
<p>So our conclusion is, as expected, that <strong>dirty pages is the main cost driver for autovacuum, followed by the number of indexes</strong>. And that we may have the same number of dead rows with completely different autovacuum costs.</p>
<h3 id="what-this-benchmark-does-and-does-not-show">What this benchmark does and does not show<a class="anchor-link" id="what-this-benchmark-does-and-does-not-show"></a></h3>
<p>The result is clear, but getting a clear result using a synthetic workload should be read with care. Our benchmark intentionally avoids complexity. And complexity is what makes production vacuum hard.</p>
<ul>
<li>We built a <code>VACUUM</code> benchmark wearing autovacuum&rsquo;s clothes. With one table, <code>threshold = 1</code>, <code>scale_factor = 0</code>, and <code>naptime = 1s</code>, we measure the isolated work of a single worker on one table. It says nothing about the parts that are specific of autovacuum: launcher table-selection, <code>autovacuum_max_workers</code> contention, or the cost limit shared across workers. The scheduling dynamics are often critical, and they are absent here.</li>
<li>The workload we use in the benchmark, as usual for a benchmark, is synthetic. Dead tuples come from a single bulk <code>DELETE</code> on an idle table. There is no concurrency. There are no long-running transaction holding back <code>OldestXmin</code>. Real vacuum routinely skips pages it can&rsquo;t get a cleanup lock on (see <em>Concurrency</em>). In our case, this never happens, which is why every run cleans to 100% all-visible. The benchmark measures vacuum on an idealized table, not production churn.</li>
<li>Everything fits in memory, so this is not an I/O-bound situation. With the whole table in <code>shared_buffers</code>, reads are mostly buffer hits, and writes are largely deferred to the checkpointer (the worker itself writes little except index pages at higher index counts). The &ldquo;cost&rdquo; being measured is page visits and CPU, plus deferred checkpoint writes. The vacuums that are I/O-bound due to scans of heaps and indexes that don&rsquo;t fit in cache, are painful. This case is excluded from the benchmark and we may say that the conclusions are about <em>logical</em> work, rather than IO bound operations. We consider that IO bound operations could show greater differences, but we did not test them.</li>
<li>Redundant identical indexes don&rsquo;t generalize. Five B-tree indexes on the integer column is a trick used to make index work scale linearly. Real indexes differ in width, key type, correlation, bloat, fill factor, and bottom-up-deletion behavior. The measured slope (5 s per index; 27,422 index blocks per index for spread, but 109,533 for compact) is specific to this shape (even the slope is distribution-dependent) and can not be directly extrapolated to wide, composite, or text indexes.</li>
<li>Freezing was excluded. The <code>VACUUM FREEZE</code> baseline ignored freezing. Anti-wraparound / aggressive vacuums, which must visit every not-all-frozen page and are often the most disruptive events in production, are not part of this benchmark. Besides, the bypass optimization and multi-pass <code>ambulkdelete</code> (under <code>maintenance_work_mem</code> pressure) is avoided, and this makes vacuum runtime nonlinear and hard to predict in production.</li>
</ul>
<p>Our findings are valid: vacuum cost is driven by dirty pages, not by dead-tuple count, and the mechanism behind it is clear. But the benchmark used a single table that fits entirely in memory, recently frozen, and ran with no concurrent activity. Under those conditions, both runtime and the page-visit counts grew linearly with the number of dirty pages and the number of indexes. It is not clear if these results can be extrapolated to a busy, larger-than-RAM production system. This is left for a future exercise.</p>
<h2 id="references">References<a class="anchor-link" id="references"></a></h2>
<h3 id="postgresql-documentation">PostgreSQL Documentation<a class="anchor-link" id="postgresql-documentation"></a></h3>
<ul>
<li><a href="https://www.postgresql.org/docs/18/index-functions.html" target="_blank" rel="noopener noreferrer">Index Access Method Functions</a>. Defines the <code>ambulkdelete</code> and <code>amvacuumcleanup</code> interfaces; documents that bulk delete is &ldquo;intended to be implemented by scanning the whole index&rdquo;</li>
<li><a href="https://www.postgresql.org/docs/18/sql-vacuum.html" target="_blank" rel="noopener noreferrer">VACUUM SQL Command</a>. <code>INDEX_CLEANUP</code> parameter (<code>AUTO</code>/<code>ON</code>/<code>OFF</code>) and <code>PARALLEL</code> option for index vacuum</li>
<li><a href="https://www.postgresql.org/docs/18/btree.html#BTREE-IMPLEMENTATION" target="_blank" rel="noopener noreferrer">B-Tree Implementation</a>. B-tree structure, deduplication, and bottom-up deletion (page-deletion and recycling mechanics are detailed in the <code>nbtree/README</code> listed under Source Code below)</li>
</ul>
<h3 id="postgresql-source-code">PostgreSQL Source Code<a class="anchor-link" id="postgresql-source-code"></a></h3>
<ul>
<li><a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/access/nbtree/nbtree.c" target="_blank" rel="noopener noreferrer"><code>src/backend/access/nbtree/nbtree.c</code></a>. B-tree vacuum implementation: <code>btbulkdelete()</code> then <code>btvacuumscan()</code> (physical-order scan of all index pages except the metapage), <code>btvacuumpage()</code> (per-page processing)</li>
<li><a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/access/heap/vacuumlazy.c" target="_blank" rel="noopener noreferrer"><code>src/backend/access/heap/vacuumlazy.c</code></a>. Main VACUUM implementation: heap scan, dead tuple collection, <code>lazy_vacuum_all_indexes()</code> (iterates all indexes calling <code>ambulkdelete</code>), bypass optimization (<code>BYPASS_THRESHOLD_PAGES = 0.02</code>)</li>
<li><a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/include/access/nbtree.h" target="_blank" rel="noopener noreferrer"><code>src/include/access/nbtree.h</code></a>. B-tree data structures and constants</li>
<li><a href="https://github.com/postgres/postgres/blob/REL_18_STABLE/src/backend/access/nbtree/README" target="_blank" rel="noopener noreferrer"><code>src/backend/access/nbtree/README</code></a>. Design notes on B-tree page deletion, recycling, and the half-dead/pin-based deletion protocol</li>
</ul>

<p><a href="https://percona.community/blog/2026/07/01/postgresql-autovacuum-internals-benchmark/">PostgreSQL Autovacuum Internals and Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Building a Modern Analytics Stack Around ClickHouse</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/building-a-modern-analytics-stack-around-clickhouse/" />
      <id>https://severalnines.com/blog/building-a-modern-analytics-stack-around-clickhouse/</id>
      <updated>2026-07-01T10:35:21+03:00</updated>
      <author><name>Sebastian Insausti</name></author>
      <summary type="html"><![CDATA[<p>Historically, relational databases did double duty. The same PostgreSQL / MySQL instance that handled your application’s writes also answered your business questions. A well-indexed schema, a few GROUP BY reports, done. And that works, right up until it doesn’t: the reports get slower, the dashboards start eating the same I/O budget as the application; or, […]<br />
The post Building a Modern Analytics Stack Around ClickHouse appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/building-a-modern-analytics-stack-around-clickhouse/">Building a Modern Analytics Stack Around ClickHouse</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Historically, relational databases did double duty. The same <a href="https://severalnines.com/clustercontrol/databases/postgresql">PostgreSQL</a> / <a href="https://severalnines.com/clustercontrol/databases/mysql">MySQL</a> instance that handled your application&rsquo;s writes also answered your business questions. A well-indexed schema, a few GROUP BY reports, done. And that works, right up until it doesn&rsquo;t: the reports get slower, the dashboards start eating the same I/O budget as the application; or, the sheer volume of events buries a row-oriented engine that was designed for point lookups, not scans over a billion rows.</p>
<p>The industry&rsquo;s answer has been polyglot persistence &mdash; pick the right engine for each job. Caching went to <a href="https://severalnines.com/clustercontrol/databases/redis">Redis</a>. Telemetry went to Prometheus. And analytics, increasingly, goes to a columnar OLAP engine, and among the open-source options, ClickHouse is the one that keeps coming up. This post covers where ClickHouse sits in a modern data stack, how it coexists with the relational systems you already run, and what actually changes for the team that has to keep it all healthy.</p>
<h2 class="wp-block-heading" id="h-clickhouse-primer">ClickHouse Primer<a class="anchor-link" id="clickhouse-primer"></a></h2>
<p>Introduced by Yandex, it&rsquo;s an open-source, column-oriented DBMS built for analytical workloads, not for managing transactional records. Its core characteristics center around data structure, processing architecture and ingestion model.</p>
<h3 class="wp-block-heading" id="h-columnar-storage">Columnar storage<a class="anchor-link" id="columnar-storage"></a></h3>
<p>A row store reads entire rows even when your query only touches two columns out of fifty. ClickHouse stores each column as its own compressed file on disk, so a query over event_date and revenue reads exactly those two columns and nothing else. Combine that with vectorized execution, processing column values in batches using SIMD instructions, and you get the headline numbers ClickHouse is known for: billions of rows per second scanned on ordinary hardware.</p>
<h3 class="wp-block-heading" id="h-distributed-processing">Distributed processing<a class="anchor-link" id="distributed-processing"></a></h3>
<p><a href="https://severalnines.com/blog/clickhouse-scaling-and-sharding-best-practices/">Scaling out works through sharding and replication</a>. A distributed table engine fans queries out across shards and merges the results, while a Keeper-based coordination layer, Apache ZooKeeper or ClickHouse Keeper, keeps replicas consistent. That said, don&rsquo;t reach for a cluster on day one. A single node with plenty of RAM and fast NVMe storage goes a surprisingly long way; bring in sharding when query latency or ingest volume actually demands it.</p>
<h3 class="wp-block-heading" id="h-real-time-ingestion">Real-time ingestion<a class="anchor-link" id="real-time-ingestion"></a></h3>
<p>This is where ClickHouse separates itself from the nightly-batch warehouse model: it can ingest millions of rows per second and make them queryable almost immediately. The MergeTree engine family writes incoming data as immutable parts on disk and merges them asynchronously in the background, conceptually similar to an LSM tree. Variants like ReplacingMergeTree and AggregatingMergeTree handle deduplication and pre-aggregation during those merges, without the lock contention you&rsquo;d fight in an MVCC row store.</p>
<h2 class="wp-block-heading" id="h-the-modern-analytics-stack-components">The Modern Analytics Stack: Components<a class="anchor-link" id="the-modern-analytics-stack-components"></a></h2>
<p>Before placing ClickHouse on the map, it helps to name the layers of the map itself. The diagram below is the reference architecture we&rsquo;ll use for the rest of the post.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="660" src="https://severalnines.com/wp-content/uploads/2026/06/modern-clickhouse-data-platform-architecture-1024x660.png" alt="Comprehensive enterprise data platform diagram showing ingestion from multiple data sources through Apache Kafka into ClickHouse, serving BI tools and data science under a data governance layer." class="wp-image-44180"></figure>
<h3 class="wp-block-heading">Data ingestion and streaming<a class="anchor-link" id="data-ingestion-and-streaming"></a></h3>
<p>Data arrives through two channels. The first is Change Data Capture: tools like Debezium or PeerDB read your OLTP database&rsquo;s replication log, MySQL&rsquo;s binlog, PostgreSQL&rsquo;s logical replication, and publish row-level change events to Kafka. The second is direct instrumentation: clickstream events, API logs, telemetry, and IoT data that applications push straight to Kafka without touching a relational database at all.</p>
<h3 class="wp-block-heading">Storage and compute engine<a class="anchor-link" id="storage-and-compute-engine"></a></h3>
<p>ClickHouse sits in the middle as the analytical store. It&rsquo;s not a data lake; it manages its own storage rather than querying files in object storage, and it&rsquo;s not a cloud-only warehouse either; it runs on-prem just as happily. Think of it as the query engine closest to your data consumers: low latency, high concurrency, and able to absorb high-velocity inserts from Kafka or batch pipelines without falling behind.</p>
<h3 class="wp-block-heading">Visualization and BI layer<a class="anchor-link" id="visualization-and-bi-layer"></a></h3>
<p>ClickHouse speaks a MySQL-compatible wire protocol and exposes a native HTTP interface, so nearly any BI tool with a SQL data source can connect to it. Grafana, Apache Superset, Metabase, Tableau, Looker, DBeaver. In most cases, the setup is nothing more than a JDBC/ODBC driver or the HTTP endpoint URL.</p>
<h3 class="wp-block-heading">Governance, metadata, and security<a class="anchor-link" id="governance-metadata-and-security"></a></h3>
<p>A fast query engine alone doesn&rsquo;t make a production stack. Schema registries, e.g., Confluent Schema Registry, Apicurio, enforce contract compatibility on Kafka topics and data catalogs, e.g. DataHub, track lineage and ownership. On the ClickHouse side, you get TLS for client and inter-replica traffic, row-level access policies, and column-level access control.</p>
<h2 class="wp-block-heading">Database Type Comparison<a class="anchor-link" id="database-type-comparison"></a></h2>
<p>Before continuing, it&rsquo;s worth grounding all of this in a side-by-side comparison: a traditional OLTP store, a conventional OLAP warehouse, and ClickHouse.</p>
<figure class="wp-block-table">
<table class="has-fixed-layout">
<tbody>
<tr>
<td><strong>Characteristic</strong></td>
<td><strong>OLTP</strong></td>
<td><strong>Traditional OLAP</strong></td>
<td><strong>ClickHouse</strong></td>
</tr>
<tr>
<td>Primary workload</td>
<td>Transactional reads/writes, point lookups</td>
<td>Batch analytics, historical reporting</td>
<td>Real-time and historical analytics</td>
</tr>
<tr>
<td>Storage model</td>
<td>Row-oriented</td>
<td>Columnar</td>
<td>Columnar (MergeTree family)</td>
</tr>
<tr>
<td>Ingestion latency</td>
<td>Sub-millisecond per row</td>
<td>Minutes to hours (batch COPY/LOAD)</td>
<td>Sub-second (streaming inserts)</td>
</tr>
<tr>
<td>Query latency (analytical)</td>
<td>Seconds to minutes (degrades with row count)</td>
<td>Seconds to tens of seconds</td>
<td>Milliseconds to low seconds</td>
</tr>
<tr>
<td>Concurrent write/read isolation</td>
<td>Full MVCC / ACID</td>
<td>Limited; primarily append-only</td>
<td>Eventual consistency; no row-level locking</td>
</tr>
<tr>
<td>UPDATE / DELETE support</td>
<td>Native, row-level</td>
<td>Supported but costly</td>
<td>Supported via ALTER mutations (async, expensive)</td>
</tr>
<tr>
<td>Typical row scale</td>
<td>Millions&ndash;low billions</td>
<td>Billions&ndash;trillions (with partitioning)</td>
<td>Billions&ndash;trillions (single node to cluster)</td>
</tr>
<tr>
<td>Horizontal scaling</td>
<td>Read replicas; sharding is complex</td>
<td>Native MPP / auto-scaling</td>
<td>Distributed tables with sharding + replication</td>
</tr>
<tr>
<td>Compression ratio</td>
<td>1&ndash;2&times; (page-level compression)</td>
<td>3&ndash;8&times;</td>
<td>5&ndash;20&times; (per-column codec selection)</td>
</tr>
<tr>
<td>Joins</td>
<td>Full join support, optimised for FK lookups</td>
<td>Full join support</td>
<td>Supported; large-scale joins require careful schema design (denormalization preferred)</td>
</tr>
<tr>
<td>Full-text search</td>
<td>Basic LIKE / full-text indexes</td>
<td>Minimal</td>
<td>Bloom filters; not a replacement for dedicated search engines</td>
</tr>
<tr>
<td>Deployment options</td>
<td>Self-managed, RDS/Cloud SQL, etc.</td>
<td>Managed cloud only (typically)</td>
<td>Self-managed, ClickHouse Cloud, or local binary</td>
</tr>
<tr>
<td>Operational tooling</td>
<td>Mature: ClusterControl, Percona, pg_upgrade</td>
<td>Vendor-managed</td>
<td>Growing: ClusterControl, Altinity Operator for K8s</td>
</tr>
<tr>
<td>Best fit</td>
<td>Application state, financial records, user data</td>
<td>Compliance reporting, long-retention warehousing</td>
<td>Real-time dashboards, event analytics, observability</td>
</tr>
</tbody>
</table>
</figure>
<h2 class="wp-block-heading">How ClickHouse Co-exists with Other Databases<a class="anchor-link" id="how-clickhouse-co-exists-with-other-databases"></a></h2>
<p>ClickHouse is additive. Your OLTP database keeps doing what it does best; the work is in getting data from one to the other cleanly.</p>
<h3 class="wp-block-heading">OLTP stores &rarr; ETL/CDC &rarr; ClickHouse<a class="anchor-link" id="oltp-stores-%e2%86%92-etl-cdc-%e2%86%92-clickhouse"></a></h3>
<p>CDC-based replication is the standard pattern. MySQL or PostgreSQL remains the source of truth for transactional integrity. Debezium (or an equivalent) tails the transaction log and pushes change events into Kafka, and from there, ClickHouse picks them up, either through its native Kafka engine or through a pipeline built on something like Flink or dbt.</p>
<p>One design decision deserves real thought here: do you replicate raw CDC events, or pre-aggregated facts? Raw events keep every aggregation option open downstream, but they cost more storage, and you&rsquo;ll need to manage ReplacingMergeTree carefully to handle upserts. Pre-aggregating on its way in keeps ClickHouse lean and queries simple, but locks you into whatever aggregation schema you chose. There&rsquo;s no universally right answer; just be deliberate about it.</p>
<h3 class="wp-block-heading">Handling slow and fast lanes<a class="anchor-link" id="handling-slow-and-fast-lanes"></a></h3>
<p>Not all analytics data moves at the same speed. A typical e-commerce platform has both:</p>
<ul class="wp-block-list">
<li>Fast lane: clickstream events (page views, add-to-cart, checkout steps) arriving at 10,000 to 100,000 events per second through Kafka, ingested directly by ClickHouse and queryable within seconds.</li>
<li>Slow lane: order records replicated from MySQL through Debezium, a few hundred per minute. Volume is low enough that pipeline latency barely matters.</li>
</ul>
<p>The payoff of landing both in ClickHouse is that a single SQL query can join real-time funnel data against historical order revenue. Try that against the production MySQL instance, and you&rsquo;ll be waiting a while, and so will your application.</p>
<h3 class="wp-block-heading">Hybrid workloads and real-time dashboards<a class="anchor-link" id="hybrid-workloads-and-real-time-dashboards"></a></h3>
<p>The usual end state: ClickHouse takes all analytical queries, and the OLTP database keeps all transactional reads and writes. A dashboard showing orders in the last five minutes or conversion by funnel step runs entirely on ClickHouse, with data that&rsquo;s maybe 5&ndash;30 seconds behind the actual transactions. For almost any business analytics use case, that lag is a non-issue, and the queries come back orders of magnitude faster than the equivalent aggregation on MySQL would.</p>
<h2 class="wp-block-heading">Operational Implications for Support and Ops Teams<a class="anchor-link" id="operational-implications-for-support-and-ops-teams"></a></h2>
<p>Adding ClickHouse to a heterogeneous environment creates new responsibilities. Here&rsquo;s what actually changes for the team on call.</p>
<h3 class="wp-block-heading">Data pipelines, latency, and data freshness<a class="anchor-link" id="data-pipelines-latency-and-data-freshness"></a></h3>
<p>Once analytics moves to ClickHouse, queries no longer hit the authoritative source. Every dashboard now carries an implicit freshness guarantee, and that guarantee is only as good as the pipeline behind it. Define the SLA explicitly, &ldquo;metrics are at most 60 seconds stale&rdquo;, and write it down. Then monitor end-to-end lag, not just whether each component is up. If Kafka consumer lag creeps up because ClickHouse inserts are slowing down, your dashboards quietly go stale without a single error being thrown.</p>
<p>Worth watching:</p>
<ul class="wp-block-list">
<li>Kafka consumer group lag</li>
<li>ClickHouse insert latency and the asynchronous insert logs</li>
<li>Debezium connector status and CDC production rate</li>
<li>The time delta between the Debezium source timestamp and arrival in ClickHouse</li>
</ul>
<h3 class="wp-block-heading">Schema evolution, partitions, and materialized views<a class="anchor-link" id="schema-evolution-partitions-and-materialized-views"></a></h3>
<p>ClickHouse is forgiving about schema changes, with one exception. Adding, dropping, or renaming columns is fast and cheap because columnar storage means each column lives in its own files. Changing a column&rsquo;s type is the expensive one: it triggers a mutation that rewrites that column&rsquo;s data in every part. Plan type changes; don&rsquo;t sweat the rest.</p>
<p>Partitioning is a first-class operational lever. Partition by a date truncation, and old data can be dropped instantly with <code>DROP PARTITION</code>; retention policies become nearly free. Just don&rsquo;t make partitions too wide; if you partition by year instead of month, you lose the precision that makes the technique useful.</p>
<p>Materialized views deserve respect. In ClickHouse they&rsquo;re insert triggers: every insert into the source table fires the view and writes pre-aggregated results to a target table, incrementally and in real time, no manual REFRESH like PostgreSQL. Extremely useful for running aggregates, but a badly written view sits directly in the insert path, so it can drag down ingest throughput on the source table. Test them under load.</p>
<h3 class="wp-block-heading">Resource isolation and multi-tenant considerations<a class="anchor-link" id="resource-isolation-and-multi-tenant-considerations"></a></h3>
<p>Resource control works through user profiles and quotas: <a href="https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/">max memory per query, concurrent queries per user, and CPU threads</a>. If multiple teams share one cluster, per-team profiles are what stop someone&rsquo;s unoptimized ad-hoc query from starving the dashboards everyone else depends on. There&rsquo;s no per-query isolation at the container or cgroup level, though. If you need hard isolation, the boundary is a separate instance, or, a separate service on ClickHouse Cloud. For most teams starting out, one instance with sensible profiles is plenty.</p>
<h2 class="wp-block-heading">Tools and Management in Multi-Database Operations<a class="anchor-link" id="tools-and-management-in-multi-database-operations"></a></h2>
<p>Run MySQL, PostgreSQL, and ClickHouse side by side, and you&rsquo;re suddenly maintaining three backup toolchains, three monitoring integrations, and three sets of alert rules, unless something unifies them. That&rsquo;s the operational case for <a href="https://severalnines.com/clustercontrol">ClusterControl</a>: one management plane across relational and analytical clusters.</p>
<h3 class="wp-block-heading">Why unified management matters in heterogeneous stacks<a class="anchor-link" id="why-unified-management-matters-in-heterogeneous-stacks"></a></h3>
<p>Every engine has its own backup format, replication model, and metrics vocabulary. MySQL backups mean binary logs and snapshot tooling; PostgreSQL means WAL archiving; ClickHouse has its own BACKUP commands. Run each from its own toolchain, and you can&rsquo;t answer a question as basic as &ldquo;is everything in my estate backed up and verified within the last 24 hours?&rdquo; without stitching together data from three places.</p>
<p>ClusterControl pulls that into one plane:</p>
<ul class="wp-block-list">
<li>Unified backup scheduling and verification for MySQL, <a href="https://severalnines.com/clustercontrol/databases/mariadb">MariaDB</a>, PostgreSQL, ClickHouse, and more</li>
<li>Centralized metrics collection with dashboards per database type</li>
<li>Alert routing to Slack, PagerDuty, email, etc, regardless of which engine fired the alert</li>
<li>Topology visualization, health, lag, and failover state across every cluster</li>
</ul>
<h3 class="wp-block-heading">Implementation checklist for operational readiness<a class="anchor-link" id="implementation-checklist-for-operational-readiness"></a></h3>
<ul class="wp-block-list">
<li>Document data freshness SLAs for every ClickHouse table fed by a pipeline</li>
<li>Monitor Kafka consumer lag, e.g. Prometheus or Kafka Exporter</li>
<li>Set per-profile memory and execution-time limits in ClickHouse</li>
<li>Automate partition retention with DROP PARTITION jobs</li>
<li>Test ClickHouse backup and restore, including materialized view recovery</li>
<li>Write down schema change procedures and their cost implications</li>
<li>Track CDC pipeline health and binlog positions</li>
<li>Route ClickHouse alerts through the same channels as everything else</li>
<li>Write runbooks for the predictable failures: Kafka leader elections, CDC connector restarts, and merge backlogs</li>
</ul>
<h2 class="wp-block-heading">Ingestion Example<a class="anchor-link" id="ingestion-example"></a></h2>
<p>The following examples show two common patterns for getting data into ClickHouse: reading directly from a Kafka topic using the Kafka table engine, and bulk-loading from a CSV file.</p>
<h3 class="wp-block-heading">Pattern 1: Real-time ingestion from Kafka<a class="anchor-link" id="pattern-1-real-time-ingestion-from-kafka"></a></h3>
<p>ClickHouse&rsquo;s Kafka engine acts as a consumer of a Kafka topic. You create a Kafka engine table that describes the topic connection, and then a materialized view that pipes rows from that engine table into a MergeTree storage table. The Kafka engine table itself does not store data; it is only a consumer interface.</p>
<h4 class="wp-block-heading">ClickHouse SQL &ndash; Kafka engine and materialized view</h4>
<p><strong>1. Create the destination storage table (MergeTree)</strong></p>
<pre class="wp-block-code"><code>CREATE TABLE events.pageviews
(
&nbsp;&nbsp;&nbsp;&nbsp;event_time &nbsp; DateTime,
&nbsp;&nbsp;&nbsp;&nbsp;session_id &nbsp; UUID,
&nbsp;&nbsp;&nbsp;&nbsp;user_id&nbsp; &nbsp; &nbsp; UInt64,
&nbsp;&nbsp;&nbsp;&nbsp;page_path&nbsp; &nbsp; String,
&nbsp;&nbsp;&nbsp;&nbsp;referrer &nbsp; &nbsp; String,
&nbsp;&nbsp;&nbsp;&nbsp;device_type&nbsp; LowCardinality(String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (user_id, event_time);</code></pre>
<p><strong>2. Create the Kafka engine table (consumer interface, no data stored here)</strong></p>
<pre class="wp-block-code"><code>CREATE TABLE events.pageviews_kafka
(
&nbsp;&nbsp;&nbsp;&nbsp;event_time &nbsp; DateTime,
&nbsp;&nbsp;&nbsp;&nbsp;session_id &nbsp; UUID,
&nbsp;&nbsp;&nbsp;&nbsp;user_id&nbsp; &nbsp; &nbsp; UInt64,
&nbsp;&nbsp;&nbsp;&nbsp;page_path&nbsp; &nbsp; String,
&nbsp;&nbsp;&nbsp;&nbsp;referrer &nbsp; &nbsp; String,
&nbsp;&nbsp;&nbsp;&nbsp;device_type&nbsp; String
)
ENGINE = Kafka
SETTINGS
&nbsp;&nbsp;&nbsp;&nbsp;kafka_broker_list &nbsp; &nbsp; = 'kafka-broker1:9092,kafka-broker2:9092',
&nbsp;&nbsp;&nbsp;&nbsp;kafka_topic_list&nbsp; &nbsp; &nbsp; = 'analytics.pageviews',
&nbsp;&nbsp;&nbsp;&nbsp;kafka_group_name&nbsp; &nbsp; &nbsp; = 'clickhouse-analytics-consumer',
&nbsp;&nbsp;&nbsp;&nbsp;kafka_format&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = 'JSONEachRow',
&nbsp;&nbsp;&nbsp;&nbsp;kafka_num_consumers &nbsp; = 4,
&nbsp;&nbsp;&nbsp;&nbsp;kafka_skip_broken_messages = 5;</code></pre>
<p><strong>3. Materialized view: pipes rows from Kafka engine into MergeTree</strong></p>
<pre class="wp-block-code"><code>CREATE MATERIALIZED VIEW events.pageviews_mv
TO events.pageviews
AS
SELECT
&nbsp;&nbsp;&nbsp;&nbsp;event_time,
&nbsp;&nbsp;&nbsp;&nbsp;session_id,
&nbsp;&nbsp;&nbsp;&nbsp;user_id,
&nbsp;&nbsp;&nbsp;&nbsp;page_path,
&nbsp;&nbsp;&nbsp;&nbsp;referrer,
&nbsp;&nbsp;&nbsp;&nbsp;device_type
FROM events.pageviews_kafka;</code></pre>
<p>Once the materialized view is created, ClickHouse begins polling the Kafka topic automatically. Consumed messages are inserted into events.pageviews as MergeTree parts. Consumer offset tracking is handled by the Kafka consumer group; restart tolerance and at-least-once delivery are built in. Set kafka_skip_broken_messages to a non-zero value in production to prevent a malformed message from stalling the consumer.</p>
<h3 class="wp-block-heading">Pattern 2: Bulk load from CSV<a class="anchor-link" id="pattern-2-bulk-load-from-csv"></a></h3>
<p>For historical data migrations or batch loads, ClickHouse accepts CSV input directly from the command line or via its HTTP interface.</p>
<h4 class="wp-block-heading">Shell &ndash; bulk insert from CSV via clickhouse-client</h4>
<p><strong>1. Insert a CSV file with a header row into an existing table</strong></p>
<pre class="wp-block-code"><code>clickhouse-client 
&nbsp;&nbsp;&nbsp;&nbsp;--host ch-server.internal 
&nbsp;&nbsp;&nbsp;&nbsp;--port 9000 
&nbsp;&nbsp;&nbsp;&nbsp;--user analytics_writer 
&nbsp;&nbsp;&nbsp;&nbsp;--password &rdquo;${CH_PASSWORD}&rdquo; 
&nbsp;&nbsp;&nbsp;&nbsp;--query &rdquo;INSERT INTO events.pageviews FORMAT CSVWithNames&rdquo; 
&nbsp;&nbsp;&nbsp;&nbsp;&lt; /data/exports/pageviews_2024.csv</code></pre>
<p><strong>2. Alternatively, using the HTTP interface (suitable for remote or scripted loads)</strong></p>
<pre class="wp-block-code"><code>curl -X POST 
&rdquo;http://ch-server.internal:8123/?query=INSERT+INTO+events.pageviews+FORMAT+CSVWithNames&amp;user=analytics_writer&amp;password=${CH_PASSWORD}&rdquo; 
--data-binary @/data/exports/pageviews_2024.csv</code></pre>
<p>For large CSV loads, prefer splitting the file into chunks of 1&ndash;10 million rows and inserting each chunk as a separate INSERT batch. ClickHouse performs optimal part creation at insert batch sizes in this range. Very large single inserts, i.e. hundreds of millions of rows can produce oversized initial parts that take a long time to merge, potentially impacting query performance during the load.</p>
<h2 class="wp-block-heading">Case Study: E-Commerce Analytics Stack<a class="anchor-link" id="case-study-e-commerce-analytics-stack"></a></h2>
<p>An e-commerce example of integrating ClickHouse with MySQL and Kafka. A mid-sized platform uses this production stack:</p>
<ul class="wp-block-list">
<li>MySQL 8.0 (via ClusterControl) for transactional core data</li>
<li>Redis for session and cart state</li>
<li>Kafka for microservices event routing</li>
</ul>
<p>The challenge: analytical queries cause production latency in MySQL. The solution adds ClickHouse as a dedicated tier for real-time dashboards without altering existing deployments.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="945" height="1024" src="https://severalnines.com/wp-content/uploads/2026/06/clickhouse-cdc-kafka-realtime-analytics-pipeline-945x1024.png" alt="Architecture diagram of a real-time data streaming pipeline featuring MySQL, Debezium CDC, Apache Kafka, ClickHouse, and Grafana, monitored by ClusterControl." class="wp-image-44181"></figure>
<h3 class="wp-block-heading">What changes for the operations team<a class="anchor-link" id="what-changes-for-the-operations-team"></a></h3>
<p>Adding ClickHouse introduces a new cluster to the managed estate. Since ClusterControl already handles the MySQL cluster, bringing ClickHouse under the same management plane ensures:</p>
<ul class="wp-block-list">
<li>ClickHouse and MySQL backups are configured and verified through a single interface.</li>
<li>Performance metrics for both ClickHouse and MySQL share the same Grafana dashboards.</li>
<li>Alerting for disk usage, replication lag, and Kafka consumer lag is centrally managed.</li>
</ul>
<p>While the platform engineering team oversees the Debezium connector and Kafka cluster, their performance remains critical for ClickHouse data freshness.</p>
<h2 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>ClickHouse fills a real gap: fast, scalable analytics over high-volume event data, at query latencies no row-oriented database can reach at scale. It doesn&rsquo;t replace MySQL or PostgreSQL; it sits beside them, consuming their change streams and your application events, and takes the analytical load off your transactional tier. For ops teams, four principles carry most of the weight:</p>
<ul class="wp-block-list">
<li>Treat pipeline health as a first-class concern. Your query results are only as fresh as the pipeline feeding them. Watch Kafka lag and CDC health with the same rigor you give replication lag.</li>
<li>Model for analytics, not normalization. Wide, denormalized tables are how ClickHouse wants to work. If denormalizing feels wrong to your relational instincts, that discomfort is usually the sign you&rsquo;re doing it right.</li>
<li>Plan for schema evolution early. Adding and dropping columns is cheap; changing column types is not. Have the procedure written before you need it.</li>
<li>Centralize management. Fragmented tooling fragments visibility. One plane for backups, alerts, and monitoring across the whole fleet pays for its setup cost quickly.</li>
</ul>
<p>ClickHouse isn&rsquo;t the answer to every workload. But if your analytical queries have outgrown your OLTP tier, it&rsquo;s one of the most direct and operationally manageable ways out. Start with a single node, replicate one or two high-value streams from Kafka, and measure. The added complexity is modest; the capability gained is not.</p>
<p>The post <a href="https://severalnines.com/blog/building-a-modern-analytics-stack-around-clickhouse/">Building a Modern Analytics Stack Around ClickHouse</a> appeared first on <a href="https://severalnines.com">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/building-a-modern-analytics-stack-around-clickhouse/">Building a Modern Analytics Stack Around ClickHouse</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Community Docker Images: keeping the operator open without a vendor registry lock-in</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/postgresql-community-images-operator/" />
      <id>https://www.percona.com/blog/postgresql-community-images-operator/</id>
      <updated>2026-06-30T14:09:35+03:00</updated>
      <author><name>Slava Sarzhan</name></author>
      <summary type="html"><![CDATA[<p>PostgreSQL community images address a real gap in how a Kubernetes database operator earns your trust. Running a database operator on Kubernetes means trusting two things: the code, and the container images the operator pulls. The code is on GitHub, easy to inspect, easy to fork. The container images, the registry that hosts them, and the … Continued<br />
The post Community Docker Images: keeping the operator open without a vendor registry lock-in appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/postgresql-community-images-operator/">Community Docker Images: keeping the operator open without a vendor registry lock-in</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" class="aligncenter wp-image-50112 size-large" src="https://www.percona.com/wp-content/uploads/2026/06/MDES-1088-Blog-image-Percona-Operator-for-PostgreSQL-V3.0.0-hero-1-1024x375.jpg" alt="" width="1024" height="375"></p>
<p><strong>PostgreSQL community images</strong>&nbsp;address a real gap in how a Kubernetes database operator earns your trust. Running a database operator on Kubernetes means trusting two things: the code, and the container images the operator pulls. The code is on GitHub, easy to inspect, easy to fork. The container images, the registry that hosts them, and the license that governs them all sit with the vendor, and any of those three can change without the source repository changing at all. Starting with Percona Operator for PostgreSQL 3.0.0, you can run the operator against community images you build yourself from the official PostgreSQL packages on download.postgresql.org, in a registry you control.</p>
<p>&nbsp;</p>
<div class="markdown-heading">
<h2 class="heading-element">TL;DR<a class="anchor-link" id="tldr"></a></h2>
</div>
<ul>
<li><strong>Community Docker Images: tech preview in PGO 3.0.0, official in 3.1.0.</strong>&nbsp;Point the operator at upstream-built PostgreSQL images instead of the Percona Distribution images.</li>
<li><strong>Build them yourself from the official PostgreSQL source.</strong>&nbsp;The Dockerfiles pull packages from download.postgresql.org (the PGDG repositories), so the trust chain runs from PGDG to your registry with no vendor in the middle.</li>
<li><strong>There are limits.</strong>&nbsp;Anything Percona-specific (TDE in our distribution build, for example) does not exist in an upstream-built image. That trade is intentional.</li>
</ul>
<p>In this post:</p>
<ul>
<li>How open source gets diluted in practice</li>
<li>Why distributions exist anyway, honestly</li>
<li>How Community Docker Images work</li>
<li>Limits of the upstream path</li>
<li>What to try, what to tell us</li>
</ul>
<p>&nbsp;</p>
<div class="markdown-heading">
&nbsp;
<h2 class="heading-element">How open source gets diluted<a class="anchor-link" id="how-open-source-gets-diluted"></a></h2>
</div>
<p>Open source has changed in the last few years, and not always for the better. Companies have learned that you can keep a project&rsquo;s source code fully open and still capture most of the lock-in by quietly closing the parts that matter in production: the release artifacts, the container images, the supported OS list, the certified Kubernetes distributions, the marketplace listings.</p>
<div class="markdown-heading">
&nbsp;
<h3><a class="anchor-link" id=""></a></h3>
<h3 class="heading-element">Same project, closed artifacts<a class="anchor-link" id="same-project-closed-artifacts"></a></h3>
</div>
<p>You can have a fully community CNCF project that does not appear on the Red Hat Marketplace except as a paid Enterprise edition. Similarly, you can have a vendor that ships one packaging in the community and a richer one in Enterprise with the features you actually need in production. The license still says &ldquo;open source.&rdquo; The practical experience says &ldquo;you depend on us.&rdquo; And the source repository&rsquo;s license is not the only license that matters here: a vendor can change the license, the trademark policy, or the distribution terms on the container images alone, while leaving the source repository untouched. That has happened in the PostgreSQL operator space recently, and the community noticed.</p>
<div class="markdown-heading">
&nbsp;
<h3><a class="anchor-link" id=""></a></h3>
<h3 class="heading-element">Why the community is right to be wary<a class="anchor-link" id="why-the-community-is-right-to-be-wary"></a></h3>
</div>
<p>Nobody outside the vendor can predict when a license will change, when a feature will move behind a paywall, or when an external contribution will get rejected because it competes with an Enterprise feature. Recent history has plenty of examples and the PostgreSQL community has been paying attention. When this community resists vendor-controlled distributions, it is not nostalgia. It is a rational read of where things have gone before.</p>
<p>I work on Percona&rsquo;s PostgreSQL operator, so I see this conversation from the vendor side. The skepticism is fair. The honest question for us is what to do about it.<br>
&nbsp;</p>
<h2><a class="anchor-link" id=""></a></h2>
<h2><strong>Why distributions exist anyway</strong><a class="anchor-link" id="why-distributions-exist-anyway"></a></h2>
<p><span style="font-weight: 400">Acknowledging the community&rsquo;s concerns does not mean distributions are pointless. There are real reasons to ship one, and pretending otherwise makes for bad blog posts.</span><br>
&nbsp;</p>
<h3><strong>What a distribution buys you</strong><a class="anchor-link" id="what-a-distribution-buys-you"></a></h3>
<p><span style="font-weight: 400">A vendor-built distribution lets the vendor:</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Control the build process, dependencies, and defaults so they fit a specific user shape.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Ship hotfixes faster, because the whole release path sits in one place.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Fork PostgreSQL itself when something the upstream community will not accept, or can take years to accept, matters to customers, such as Transparent Data Encryption.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">For a Kubernetes operator, ship images with exactly the tools and extensions the operator supports, and skip everything else. The CVE surface stays smaller.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Give QA and Service teams a predictable environment. &ldquo;We support extensions A, B, C and not D, X, Z&rdquo; is only honest if QA actually exercises A, B, C and the Service team can work with them in the production environment.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Give customers one accountable party for the full release cycle, from hotfix through package availability. Some teams explicitly need that contract for compliance and audit reasons.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">And yes, less positive reasons that we covered above also apply, which is exactly the part the community keeps pointing at.</span></li>
</ol>
<p>&nbsp;</p>
<h3><strong>The trade-off you accept</strong><a class="anchor-link" id="the-trade-off-you-accept"></a></h3>
<p><span style="font-weight: 400">If you run the vendor distribution, you accept that the vendor&rsquo;s registry, image policy, and supported-extension matrix become part of your stack. If the vendor changes any of that, your operator deployment changes with it. That is not hypothetical for users who have lived through it on other products.</span></p>
<p><span style="font-weight: 400">So the real question is whether you can keep the benefits a distribution provides for the users who want them, while leaving an honest, supported door open for users who do not. That is the door PGO 3.0.0 opens.</span></p>
<p>&nbsp;</p>
<h2>Community PostgreSQL Images in PGO 3.0.0<a class="anchor-link" id="community-postgresql-images-in-pgo-3-0-0"></a></h2>
<p><span style="font-weight: 400">Starting with Percona Operator for PostgreSQL 3.0.0, the operator can run against images built from upstream PostgreSQL packages, not just the Percona Distribution images. This is what we are calling </span><b>Community PostgreSQL Images</b><span style="font-weight: 400">. In 3.0.0, the feature ships as a tech preview. In 3.1.0, these images become part of our official release cycle and are fully documented.</span></p>
<p><span style="font-weight: 400">One of the main advantages of Community Docker Images is that the community can request or contribute any extension that does not exist in the official Percona PostgreSQL distribution. TimescaleDB and Citus are the first examples: the community asked for them, and we shipped both in the Community Images set from day one.</span></p>
<p>&nbsp;</p>
<h2>How to use &ldquo;Community PostgreSQL images&rdquo;<a class="anchor-link" id="how-to-use-community-postgresql-images"></a></h2>
<p><span style="font-weight: 400">The operator does not care where the image came from, as long as the image meets the operator&rsquo;s runtime expectations&nbsp;</span></p>
<p><span style="font-weight: 400">A typical CR using a community image looks like this:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: pgv2.percona.com/v2
kind: PerconaPGCluster
metadata:
  name: cluster1
spec:
  image: registry.example.com/postgresql-community:18
  postgresVersion: 18
  proxy:
    pgBouncer:
      image: registry.example.com/pgbouncer-community:1.23
  backups:
    pgbackrest:
      image: registry.example.com/pgbackrest-community:2.51
  # other spec fields unchanged from a normal CR</pre>
<p><span style="font-weight: 400">The fields that change are </span><span style="font-weight: 400">spec.image</span><span style="font-weight: 400">, </span><span style="font-weight: 400">spec.proxy.pgBouncer.image</span><span style="font-weight: 400">, and </span><span style="font-weight: 400">spec.backups.pgbackrest.image</span><span style="font-weight: 400">. You can build and publish all three images under your own registry, with your own tags if that helps you track versions. The operator drives the rest of the deployment the same way it always has: instances, backups, replication, monitoring, all of it.</span></p>
<p>&nbsp;</p>
<h3>What ships are in each image<a class="anchor-link" id="what-ships-are-in-each-image"></a></h3>
<p><span style="font-weight: 400">Each Community Docker Image is a thin layer over the chosen base (UBI9 or UBI8) plus the packages the operator needs for that role. Where you see </span><span style="font-weight: 400">{N}</span><span style="font-weight: 400">, substitute the PostgreSQL major you build for (17, 18, and so on).<br>
</span></p>
<p><strong><code>postgres</code>&nbsp;image</strong>&nbsp;(e.g.&nbsp;<code>postgres17</code>):</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>postgresql{N}-server</code></td>
<td>PostgreSQL server</td>
</tr>
<tr>
<td><code>postgresql{N}-contrib</code></td>
<td>contrib modules</td>
</tr>
<tr>
<td><code>pg_repack_{N}</code></td>
<td>online table/index reorganization</td>
</tr>
<tr>
<td><code>pgaudit_{N}</code></td>
<td>audit logging</td>
</tr>
<tr>
<td><code>set_user_{N}</code></td>
<td>privilege escalation control</td>
</tr>
<tr>
<td><code>pgvector_{N}</code></td>
<td>vector similarity search</td>
</tr>
<tr>
<td><code>wal2json_{N}</code></td>
<td>WAL to JSON logical decoding</td>
</tr>
<tr>
<td><code>pg_cron_{N}</code></td>
<td>in-database cron scheduler</td>
</tr>
<tr>
<td><code>pgbackrest&amp;lt;/code&gt;</code></td>
<td>backup/restore tool</td>
</tr>
<tr>
<td><code>patroni</code></td>
<td>HA cluster manager</td>
</tr>
<tr>
<td><code>timescaledb-2-postgresql-{N}</code></td>
<td>time-series extension (x86_64 only; EL9 only for PG18)</td>
</tr>
<tr>
<td><code>citus_{N}</code></td>
<td>distributed PostgreSQL (PG16+ only)</td>
</tr>
</tbody>
</table>
<p><strong><code>pgbackrest</code>&nbsp;image</strong>:</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pgbackrest</code></td>
<td>backup/restore tool only</td>
</tr>
</tbody>
</table>
<p><strong><code>pgbouncer</code>&nbsp;image</strong>:</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>pgbouncer</code></td>
<td>connection pooler only</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><span style="font-weight: 400">The split is intentional. The postgres image ships the full operator-aware runtime. The backup and proxy images stay minimal. As a result, the operator&rsquo;s components are in separate failure domains and shrink the attack surface of each container.</span></p>
<p>&nbsp;</p>
<h3>Limits worth being honest about<a class="anchor-link" id="limits-worth-being-honest-about"></a></h3>
<p><span style="font-weight: 400">A community image is not a Percona Distribution image. Two practical consequences:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Distribution-only features will not work. Transparent Data Encryption, for example, lives in the Percona Distribution build. A community image built from upstream PostgreSQL does not include it. If you depend on TDE, run the distribution image.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Support boundaries are different. Percona Support is responsible for the Percona Distribution images and the operator code. A community image you built yourself </span></li>
</ul>
<p><span style="font-weight: 400">Ultimately, these are the right trade-offs. The point of community images is to give you transparency and control. Taking care of your own image is part of that deal. At the same time, we publish all three images under </span><span style="font-weight: 400">perconalab/percona-postgresql-operator</span><span style="font-weight: 400"> on Docker Hub so you can evaluate the tech preview without standing up your own build pipeline first. </span><span style="font-weight: 400">perconalab</span><span style="font-weight: 400"> is Percona&rsquo;s non-production namespace, so use those images for testing. For production, build and sign your own.</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">UBI9 (EL9):</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">docker.io/perconalab/percona-postgresql-operator:main-postgres14-community
docker.io/perconalab/percona-postgresql-operator:main-postgres15-community
docker.io/perconalab/percona-postgresql-operator:main-postgres16-community
docker.io/perconalab/percona-postgresql-operator:main-postgres17-community
docker.io/perconalab/percona-postgresql-operator:main-postgres18-community
docker.io/perconalab/percona-postgresql-operator:main-pgbackrest-community
docker.io/perconalab/percona-postgresql-operator:main-pgbouncer-community
docker.io/perconalab/percona-postgresql-operator:main-upgrade-community</pre>
<p><span style="font-weight: 400">UBI8 (EL8):</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">docker.io/perconalab/percona-postgresql-operator:main-ubi8-postgres14-community
docker.io/perconalab/percona-postgresql-operator:main-ubi8-postgres15-community
docker.io/perconalab/percona-postgresql-operator:main-ubi8-postgres16-community
docker.io/perconalab/percona-postgresql-operator:main-ubi8-postgres17-community
docker.io/perconalab/percona-postgresql-operator:main-ubi8-postgres18-community
docker.io/perconalab/percona-postgresql-operator:main-ubi8-upgrade-community</pre>
<p>&nbsp;</p>
<h3>How to build the images<a class="anchor-link" id="how-to-build-the-images"></a></h3>
<p><span style="font-weight: 400">The Dockerfile, the package list, and a sample CI job ship in </span><a href="https://github.com/percona/percona-docker/tree/main/postgresql-containers/community"><span style="font-weight: 400">percona-docker/postgresql-containers/community.</span></a><span style="font-weight: 400"> The build is a regular </span><span style="font-weight: 400">make</span><span style="font-weight: 400"> target on top of </span><span style="font-weight: 400">docker buildx</span><span style="font-weight: 400">, so you can run it on any multi-platform builder.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag"># Prerequisites: docker buildx with a multi-platform builder
docker buildx create --use --name multiarch

# Build and push all PostgreSQL community images (UBI9 / EL9)
git clone https://github.com/percona/percona-docker
cd percona-docker/postgresql-containers/community
make all TAG=1.0.0 REGISTRY=myrepo/percona-postgresql-operator

# Or a single image
make postgres17 TAG=1.0.0 REGISTRY=myrepo/percona-postgresql-operator

# UBI8 / EL8 variants
make all-ubi8 TAG=1.0.0-ubi8 REGISTRY=myrepo/percona-postgresql-operator</pre>
<p><em><span style="font-weight: 400">make all</span></em><span style="font-weight: 400"> builds all three images (postgres, pgBouncer, pgBackRest) so they stay version-aligned. Override </span><em><span style="font-weight: 400">REGISTRY</span></em><span style="font-weight: 400"> and </span><em><span style="font-weight: 400">TAG</span></em><span style="font-weight: 400"> to point at your own namespace and tagging scheme. Once the images are in your registry, plug them into the CR fields shown earlier, and the operator picks them up.</span></p>
<p><span style="font-weight: 400">Full build documentation: </span><a href="https://github.com/percona/percona-docker/blob/main/postgresql-containers/community/README.md"><span style="font-weight: 400">percona-docker/postgresql-containers/community/README.md.</span></a></p>
<p>&nbsp;</p>
<h3>How to contribute<a class="anchor-link" id="how-to-contribute"></a></h3>
<p><span style="font-weight: 400">Community images live in </span><a href="https://github.com/percona/percona-docker"><span style="font-weight: 400">percona/percona-docker</span></a><span style="font-weight: 400">, and the build is driven by a </span><span style="font-weight: 400">transform.py</span><span style="font-weight: 400"> generator that produces the Dockerfiles under </span><span style="font-weight: 400">build/</span><span style="font-weight: 400">. The files under </span><span style="font-weight: 400">build/</span><span style="font-weight: 400"> are regenerated on every sync, so contributions go through the generator, never through the generated files.</span></p>
<p><span style="font-weight: 400">Full contribution guide: </span><a href="https://github.com/percona/percona-docker/blob/main/postgresql-containers/community/CONTRIBUTING.md"><span style="font-weight: 400">community/CONTRIBUTING.md</span></a><span style="font-weight: 400">.</span></p>
<p>&nbsp;</p>
<h3>How to provide feedback<a class="anchor-link" id="how-to-provide-feedback"></a></h3>
<p><span style="font-weight: 400">Two channels, depending on the shape of the feedback:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">GitHub issue on </span><a href="https://github.com/percona/percona-postgresql-operator"><span style="font-weight: 400">percona/percona-postgresql-operator</span></a><span style="font-weight: 400"> with the </span><span style="font-weight: 400">community-images</span><span style="font-weight: 400"> label. Use this for bug reports, missing extensions, build problems, and concrete requests. The label keeps all <em>community-image</em> reports in one filter the team watches.</span></li>
</ul>
<p>&nbsp;</p>
<h2>What&rsquo;s next<a class="anchor-link" id="whats-next"></a></h2>
<p><span style="font-weight: 400">The first step was taking full engineering ownership of Percona Operator for PostgreSQL as an independent project, so the roadmap, the release cadence, and the governance live with one team that the community can talk to directly. Community </span><b>PostgreSQL</b><span style="font-weight: 400"> Images are the next step in that same commitment. If the community adopts this path, we have ideas for what to invest in next.</span></p>
<p><span style="font-weight: 400">We will let the community tell us. If this is useful, we keep investing here. We are ready to add more features to the operator around </span><b>Community Images</b><span style="font-weight: 400">. Conversely, if nobody adopts it, that is also a signal, and an honest one.</span></p>
<p><span style="font-weight: 400">Try the tech preview in 3.0.0. Open an issue if the build flow is rougher than it should be. Tell us what you want next on the forum or directly on GitHub.</span></p>
<p>&nbsp;</p>
<h2>Try It Out<a class="anchor-link" id="try-it-out"></a></h2>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Percona Operator for PostgreSQL docs: </span><a href="https://docs.percona.com/percona-operator-for-postgresql/"><span style="font-weight: 400">https://docs.percona.com/percona-operator-for-postgresql/</span></a></li>
<li style="font-weight: 400"><span style="font-weight: 400">GitHub: </span><a href="https://github.com/percona/percona-postgresql-operator"><span style="font-weight: 400">https://github.com/percona/percona-postgresql-operator</span></a></li>
<li><span style="font-weight: 400">Community Forum: </span><a href="https://forums.percona.com/"><span style="font-weight: 400">https://forums.percona.com</span></a><span style="font-weight: 400">, share your feedback, ask questions, or report issues</span></li>
</ul>
<p>The post <a href="https://www.percona.com/blog/postgresql-community-images-operator/">Community Docker Images: keeping the operator open without a vendor registry lock-in</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/postgresql-community-images-operator/">Community Docker Images: keeping the operator open without a vendor registry lock-in</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Debugging with Ephemeral Containers</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/debugging-with-ephemeral-containers/" />
      <id>https://www.percona.com/blog/debugging-with-ephemeral-containers/</id>
      <updated>2026-06-30T13:52:25+03:00</updated>
      <author><name>Chetan Shivashankar</name></author>
      <summary type="html"><![CDATA[<p>Debugging applications in Kubernetes can be tricky. Containers are designed to be small, immutable, and purpose-built. That is great for production, but not always ideal when something breaks. Many production images are minimal or distroless. They may not include tools that are useful for troubleshooting. In some cases, the application container may already be crashing, … Continued<br />
The post Debugging with Ephemeral Containers appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/debugging-with-ephemeral-containers/">Debugging with Ephemeral Containers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">Debugging applications in Kubernetes can be tricky. Containers are designed to be small, immutable, and purpose-built. That is great for production, but not always ideal when something breaks. Many production images are minimal or distroless. They may not include tools that are useful for troubleshooting.</span></p>
<p><span style="font-weight: 400">In some cases, the application container may already be crashing, which means </span><span style="font-weight: 400">kubectl exec</span><span style="font-weight: 400"> is not useful. In other cases, accessing the nodes may not be possible. Since Pods are immutable, it is impossible to add another container for troubleshooting.</span></p>
<p><span style="font-weight: 400">This is where ephemeral containers help.</span></p>
<h2><span style="font-weight: 400">What Are Ephemeral Containers?</span><a class="anchor-link" id="what-are-ephemeral-containers"></a></h2>
<p><span style="font-weight: 400">In Kubernetes, </span><b>ephemeral containers</b><span style="font-weight: 400"> are a special type of container designed to run temporarily inside an existing Pod. Their primary purpose is to help administrators and developers troubleshoot, inspect, and debug live applications without disrupting the running service. They are </span><b>not</b><span style="font-weight: 400"> meant to run application workloads. Instead, they are designed for operational debugging.</span></p>
<p><span style="font-weight: 400">An ephemeral container is not added to a Pod by editing <span style="color: #0000ff"><code></code></span></span><span style="font-weight: 400;color: #0000ff">spec.containers</span><span style="font-weight: 400"><span style="color: #0000ff">.</span> Kubernetes treats it differently from regular containers. When an ephemeral container is created, a request is sent to the Kubernetes API server using the Pod&rsquo;s <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">ephemeralcontainers</span></span><span style="font-weight: 400"> subresource. This distinction is important because most of a Pod&rsquo;s spec is immutable after creation; Kubernetes does not allow you to simply edit a running Pod and append a normal container to <span style="color: #0000ff"><code></code></span></span><span style="font-weight: 400;color: #0000ff">spec.containers</span><span style="font-weight: 400">.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  ephemeralContainers:
  - name: &lt;debug container-name&gt;
    image: &lt;image&gt;
    targetContainerName: &lt;&gt; # (Optional)If ephemeral container needs to have same pid namespace of a running container.</pre>

<h2><span style="font-weight: 400">Key Characteristics of Ephemeral Containers</span><a class="anchor-link" id="key-characteristics-of-ephemeral-containers"></a></h2>
<p><span style="font-weight: 400">Unlike standard containers, ephemeral containers have the following characteristics:</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">They do not have guaranteed CPU or memory resources (</span><span style="font-weight: 400">limits</span><span style="font-weight: 400"> or </span><span style="font-weight: 400">requests</span><span style="font-weight: 400">).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">If an ephemeral container crashes or completes its task, it will never restart.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">They do not include fields such as ports, </span><span style="font-weight: 400">livenessProbe</span><span style="font-weight: 400">, or </span><span style="font-weight: 400">readinessProbe</span><span style="font-weight: 400">.</span></li>
</ol>
<h2><span style="font-weight: 400">Examples</span><a class="anchor-link" id="examples"></a></h2>
<p><span style="font-weight: 400">For testing, the Percona Operator for MySQL based on XtraDB Cluster is installed. The installation steps can be found </span><a href="https://docs.percona.com/percona-operator-for-mysql/pxc/kubectl.html"><span style="font-weight: 400">here</span></a><span style="font-weight: 400">. Once the installation is complete, the running pods should look similar to the following:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag"># kubectl get po
NAME                                              READY   STATUS      RESTARTS   AGE
cluster1-haproxy-0                                2/2     Running     0          17h
cluster1-haproxy-1                                2/2     Running     0          17h
cluster1-haproxy-2                                2/2     Running     0          16h
cluster1-pxc-0                                    3/3     Running     0          17h
cluster1-pxc-1                                    3/3     Running     0          17h
cluster1-pxc-2                                    3/3     Running     0          16h
percona-xtradb-cluster-operator-6b5f75f65-fpjxr   1/1     Running     0          17h
xb-cron-cluster1-fs-pvc-20266250025-372f8-hmrmh   0/1     Completed   0          7h9m</pre>
<p><b>NOTE: It is important to note that this behavior depends on your environment, specifically whether you have the required privileges or Security Context Constraints (SCC) in place. The commands below are for demonstration purposes and do not necessarily follow all best practices, such as avoiding generic <code>ubuntu</code> images, long-running shells like <code>bash</code>, or running containers as root.</b><b>Always run ephemeral containers with a strong focus on security, especially in production systems.</b></p>
<p><span style="font-weight: 400">Let&rsquo;s look at some examples of how ephemeral containers can be useful.</span></p>
<h2><span style="font-weight: 400">1. Ephemeral Container with shared network namespace</span><a class="anchor-link" id="1-ephemeral-container-with-shared-network-namespace"></a></h2>
<p><span style="font-weight: 400">When an ephemeral container is created in a Pod, it shares the network namespace of all other containers in that Pod. This is particularly useful for troubleshooting network-related issues.</span></p>
<p><span style="font-weight: 400">Let&rsquo;s create an ephemeral container named <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">debug-1</span></span><span style="font-weight: 400"> in the MySQL pod <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">cluster1-pxc-0</span></span><span style="font-weight: 400">:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl debug pod/cluster1-pxc-0 --image=ubuntu --container=debug-1 -ti -- bash
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.</pre>
<p><span style="font-weight: 400">When we check the processes in the container, only the bash process that was started when the container was created is running.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@cluster1-pxc-0:/# ps -elf
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root           1       0  0  80   0 -  1192 do_wai 07:16 pts/0    00:00:00 bash
4 R root           9       1  0  80   0 -  1701 -      07:16 pts/0    00:00:00 ps -elf</pre>
<p><span style="font-weight: 400">However, port 3306 is open because the MySQL process in the primary container shares the same network namespace as our debug container.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@cluster1-pxc-0:/# netstat -tlnp | grep :3306
tcp        0      0 10.42.0.18:33062        0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::33060                :::*                    LISTEN      -</pre>
<p><span style="font-weight: 400">This capability is highly effective for analyzing network traffic, egress connectivity, and local service availability.</span><span style="font-weight: 400">Let&rsquo;s examine the Pod spec and status to see how ephemeral containers are represented.</span></p>
<p><span style="font-weight: 400">The following is the spec of the ephemeral container. Note the <code><span style="color: #0000ff">securityContext</span></code></span>, which we will discuss later in this post:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get po cluster1-pxc-0 -oyaml | yq .spec.ephemeralContainers    
- command:
    - bash
  image: ubuntu
  imagePullPolicy: Always
  name: debug-1
  resources: {}
  securityContext:
    capabilities:
      add:
        - SYS_PTRACE
  stdin: true
  terminationMessagePath: /dev/termination-log
  terminationMessagePolicy: File
  tty: true</pre>
<p><span style="font-weight: 400">Status of ephemeral containers</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get po cluster1-pxc-0 -oyaml | yq .status.ephemeralContainerStatuses
- containerID: containerd://4f656ada1679595109a9ac4c5bae916dc35404d7a45818eacef83aabd6d8509b
  image: docker.io/library/ubuntu:latest
  imageID: docker.io/library/ubuntu@sha256:53958ec7b67c2c9355df922dd08dbf0360611f8c3cdb656875e81873db9ffdba
  lastState: {}
  name: debug-1
  ready: false
  resources: {}
  restartCount: 0
  state:
    running:
      startedAt: "2026-06-25T07:16:22Z"
  user:
    linux:
      gid: 0
      supplementalGroups:
        - 0
        - 1001
      uid: 0</pre>

<h2><span style="font-weight: 400">2. Ephemeral Container with shared network namespace, pid namespace</span><a class="anchor-link" id="2-ephemeral-container-with-shared-network-namespace-pid-namespace"></a></h2>
<p><span style="font-weight: 400">While sharing the network namespace is useful, sharing the PID namespace to inspect running processes can be beneficial in many debugging scenarios.</span></p>
<p><span style="font-weight: 400">Let&rsquo;s create an ephemeral container named <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">debug-2</span></span><span style="font-weight: 400"> in the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">cluster1-pxc-0</span></span><span style="font-weight: 400"> Pod, specifically targeting the PID namespace of the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">pxc</span></span><span style="font-weight: 400"> container:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl debug pod/cluster1-pxc-0 --image=ubuntu --container=debug-2 --target=pxc -ti -- bash
Targeting container "pxc". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.</pre>
<p><span style="font-weight: 400">A key change from the previous command is the addition of the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">--target=pxc</span></span><span style="font-weight: 400"> flag. This creates an ephemeral container that shares the PID namespace of the </span><span style="font-weight: 400">pxc</span><span style="font-weight: 400"> container.</span></p>
<p><span style="font-weight: 400">When we check the processes in the container, we can see the MySQL processes.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@cluster1-pxc-0:/# ps -elf
F S UID          PID    PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S 1001           1       0  4  80   0 - 832724 do_pol Jun24 ?       00:50:54 mysqld --wsrep_start_position=9d4e0c3e-6fd5-11f1-aab4-1a8a26ce607c:28
4 S 1001          90       1  0  80   0 - 306929 futex_ Jun24 ?       00:00:00 /var/lib/mysql/mysql-state-monitor
1 Z 1001        2999       1  0  80   0 -     0 -      Jun24 ?        00:00:00 [wsrep_sst_xtrab] &lt;defunct&gt;
4 S root      173053       0  0  80   0 -  1192 do_wai 08:01 pts/0    00:00:00 bash
4 R root      173081  173053  0  80   0 -  1701 -      08:01 pts/0    00:00:00 ps -elf</pre>
<p><span style="font-weight: 400">We can also verify this by network stats</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@cluster1-pxc-0:/# netstat -anlp | grep 3306
tcp        0      0 10.42.0.18:33062        0.0.0.0:*               LISTEN      1/mysqld            
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      1/mysqld            
tcp        0      0 10.42.0.18:59330        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:33062        10.42.1.4:52464         TIME_WAIT   -                   
tcp        0      0 10.42.0.18:36008        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:33062        10.42.1.4:52448         TIME_WAIT   -                   
tcp        0      0 10.42.0.18:33756        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:43086        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:43206        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:44256        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:43220        10.42.0.18:33062        TIME_WAIT   -                   
tcp        0      0 10.42.0.18:44258        10.42.0.18:33062        TIME_WAIT   -                   
tcp6       0      0 :::33060                :::*                    LISTEN      1/mysqld</pre>

<h2>3. <span style="font-weight: 400">Ephemeral Container with shared network namespace, shared pid namespace, shared volume</span><a class="anchor-link" id="3-ephemeral-container-with-shared-network-namespace-shared-pid-namespace-shared-volume"></a></h2>
<p><span style="font-weight: 400">In some cases, you may need to collect dumps, check database files, or inspect logs, which requires access to the filesystem. However, each container has its own mount namespace, and mount namespaces cannot be shared directly.</span><span style="font-weight: 400">A volume mounted in a Pod, however, can be shared across containers, including ephemeral containers.</span></p>
<p><span style="font-weight: 400">Let&rsquo;s check the volumes present in the <code></code></span><span style="font-weight: 400;color: #0000ff">cluster1-pxc-0</span><span style="font-weight: 400"> pod and how they are mounted to the </span><span style="font-weight: 400">pxc</span><span style="font-weight: 400"> container.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get po cluster1-pxc-0 -o yaml | yq '.spec.volumes[] | select(.name == "datadir")'
name: datadir
persistentVolumeClaim:
  claimName: datadir-cluster1-pxc-0

% kubectl get po cluster1-pxc-0 -o yaml | yq '.spec.containers[] | select(.name == "pxc")| .volumeMounts[] | select(.name == "datadir")' 
mountPath: /var/lib/mysql
name: datadir</pre>
<p><span style="font-weight: 400">As seen above, the persistent volume holding the database files is mounted at </span><b>/var/lib/mysql</b><span style="font-weight: 400">.</span></p>
<p><span style="font-weight: 400">Let&rsquo;s create an ephemeral container named <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">debug-3</span></span><span style="font-weight: 400"> in the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">cluster1-pxc-0</span></span><span style="font-weight: 400"> pod, sharing the PID namespace of the </span><span style="font-weight: 400">pxc</span><span style="font-weight: 400"> container and mounting the </span><span style="font-weight: 400">datadir</span><span style="font-weight: 400"> volume at <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">/db-mount</span></span><span style="font-weight: 400">:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl patch po cluster1-pxc-0 --subresource=ephemeralcontainers -p '
{
    "spec":
    {
        "ephemeralContainers":
        [
            {
                "name": "debug-3",
                "command": ["bash"],
                "image": "ubuntu",
                "targetContainerName": "pxc",
                "stdin": true,
                "tty": true,
                "volumeMounts": [{
                    "mountPath": "/db-mount",
                    "name": "datadir",
                    "readOnly": true
                }]
            }
        ]
    }
}'
pod/cluster1-pxc-0 patched</pre>
<p><span style="font-weight: 400">The command above creates the ephemeral container in the pod. To access it, we will attach to the running debug container:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl attach -ti pod/cluster1-pxc-0 -c debug-3
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
root@cluster1-pxc-0:/# cd /db-mount
root@cluster1-pxc-0:/db-mount# ls
'#ib_16384_0.dblwr'                 binlog.000001        gvwstate.dat                 mysql                     mysqlx.sock.lock       pxc-entrypoint.sh
'#ib_16384_1.dblwr'                 binlog.000002        ib_buffer_pool               mysql-state-monitor       notify.sock            readiness-check.sh
 '#innodb_redo'                     binlog.000003        ibdata1                      mysql-state-monitor.log   peer-list              sys
 '#innodb_temp'                     binlog.index         ibtmp1                       mysql.ibd                 performance_schema     undo_001
 audit_filter.20260624T140457.log   cluster1-pxc-0.pid   innobackup.backup.full.log   mysql.state               pmm-prerun.sh          undo_002
 audit_filter.log                   galera.cache         innobackup.backup.log        mysql_upgrade_history     private_key.pem        version_info
 auth_plugin                        get-pxc-state        liveness-check.sh            mysqld-error.log          public_key.pem         wsrep_cmd_notify_handler.sh
 auto.cnf                           grastate.dat         logrotate.status             mysqlx.sock               pxc-configure-pxc.sh</pre>
<p><span style="font-weight: 400">As demonstrated, the database files located at /var/lib/mysql are now accessible at /db-mount within the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">debug-3</span></span><span style="font-weight: 400"> container.</span></p>
<p><span style="font-weight: 400">Since the volume was mounted with the </span><span style="color: #0000ff"><b>&ldquo;readOnly&rdquo;: true</b></span><span style="font-weight: 400"> parameter, no writes can be performed.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@cluster1-pxc-0:/db-mount# touch test
touch: cannot touch 'test': Read-only file system</pre>
<p><span style="font-weight: 400">If you require write permissions, simply omit the </span><span style="color: #0000ff"><b>&ldquo;readOnly&rdquo;: true</b></span><span style="font-weight: 400"> flag.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl patch po cluster1-pxc-0 --subresource=ephemeralcontainers -p '
{
    "spec":
    {
        "ephemeralContainers":
        [
            {
                "name": "debug-4",
                "command": ["bash"],
                "image": "ubuntu",
                "targetContainerName": "pxc",
                "stdin": true,
                "tty": true,
                "volumeMounts": [{
                    "mountPath": "/db-mount",
                    "name": "datadir"
                }]
            }
        ]
    }
}'
pod/cluster1-pxc-0 patched</pre>
<p><span style="font-weight: 400">Now, attach to the container and create a file in the volume mount:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl attach -ti pod/cluster1-pxc-0 -c debug-4                  
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
root@cluster1-pxc-0:/# touch /db-mount/test
root@cluster1-pxc-0:/# ls /db-mount/test
/db-mount/test</pre>

<h2><span style="font-weight: 400">4. Ephemeral Container on a Kubernetes node&rsquo;s namespace and filesystem</span><a class="anchor-link" id="4-ephemeral-container-on-a-kubernetes-nodes-namespace-and-filesystem"></a></h2>
<p><span style="font-weight: 400">If you need to examine system logs (such as kernel logs or dmesg) but do not have SSH access to the nodes, you can run an ephemeral container directly on the node&rsquo;s namespace and filesystem.</span></p>
<p><span style="font-weight: 400">Let&rsquo;s check the nodes of the Kubernetes cluster and run an ephemeral container directly on one:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get nodes
NAME                 STATUS   ROLES                AGE   VERSION
chetan-1-36-node-1   Ready    control-plane,etcd   6d    v1.36.1+k3s1
chetan-1-36-node-2   Ready    control-plane,etcd   6d    v1.36.1+k3s1
chetan-1-36-node-3   Ready    control-plane,etcd   6d    v1.36.1+k3s1</pre>
<p><span style="font-weight: 400">Run an ephemeral container:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl debug node/chetan-1-36-node-1 --image=ubuntu --container=debug-5 -ti -- bash
Creating debugging pod node-debugger-chetan-1-36-node-1-hfgms with container debug-5 on node chetan-1-36-node-1.
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
If you don't see a command prompt, try pressing enter.
root@chetan-1-36-node-1:/#</pre>
<p><span style="font-weight: 400">Node&rsquo;s filesystem can be accessed at /host.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">root@chetan-1-36-node-1:/#  ls /host
bin                boot  etc   lib                lib64       media  opt   root  sbin                snap  swapfile  tmp  var
bin.usr-is-merged  dev   home  lib.usr-is-merged  lost+found  mnt    proc  run   sbin.usr-is-merged  srv   sys       usr
root@chetan-1-36-node-1:/#  ls /host/var/log/dmesg 
/host/var/log/dmesg</pre>
<p><span style="font-weight: 400">Behind the scenes, a Pod with the name </span><span style="font-weight: 400">node-debugger-&lt;node-name&gt;-&lt;hash&gt;</span><span style="font-weight: 400"> is created.</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get po -l app.kubernetes.io/managed-by=kubectl-debug
NAME                                     READY   STATUS      RESTARTS   AGE
node-debugger-chetan-1-36-node-1-hfgms   0/1     Completed   0          33m</pre>
<p><span style="font-weight: 400">Let&rsquo;s check the spec of the debug pod</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">% kubectl get po -l app.kubernetes.io/managed-by=kubectl-debug
NAME                                     READY   STATUS      RESTARTS   AGE
node-debugger-chetan-1-36-node-1-hfgms   0/1     Completed   0          33m

Let&rsquo;s check the spec of the debug pod

% kubectl get po node-debugger-chetan-1-36-node-1-hfgms -oyaml | yq .spec
containers:
  - command:
      - bash
    image: ubuntu
    imagePullPolicy: Always
    name: debug-5
    resources: {}
    stdin: true
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    tty: true
    volumeMounts:
      - mountPath: /host
        name: host-root
      - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
        name: kube-api-access-4lj9h
        readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
hostIPC: true
hostNetwork: true
hostPID: true
nodeName: chetan-1-36-node-1
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Never
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
  - operator: Exists
volumes:
  - hostPath:
      path: /
      type: ""
    name: host-root
  - name: kube-api-access-4lj9h
    projected:
      defaultMode: 420
      sources:
        - serviceAccountToken:
            expirationSeconds: 3607
            path: token
        - configMap:
            items:
              - key: ca.crt
                path: ca.crt
            name: kube-root-ca.crt
        - downwardAPI:
            items:
              - fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
                path: namespace</pre>
<p><span style="font-weight: 400">Some key observations from the spec are the following:&nbsp;</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">hostIPC: true  -&gt; Share host&rsquo;s IPC namespace
hostNetwork: true  -&gt; Share Host Node Network
hostPID: true  -&gt; Share host&rsquo;s PID namespace

volumes:
  - hostPath:
      path: /      -&gt; Use Host&rsquo;s file system 
      type: ""
    name: host-root

    volumeMounts:
      - mountPath: /host
        name: host-root</pre>
<p><span style="font-weight: 400">The specifications above indicate excessive permissions for a regular application; consequently, these might be disabled by your system administrator via security policies.</span></p>
<h2><span style="font-weight: 400">Profile of an Ephemeral Container</span><a class="anchor-link" id="profile-of-an-ephemeral-container"></a></h2>
<p><span style="font-weight: 400">An interesting option for the </span><span style="font-weight: 400">kubectl debug</span><span style="font-weight: 400"> command is <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">--profile</span></span><span style="font-weight: 400">.</span></p>
<p><span style="font-weight: 400">The official documentation provides the following:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">--profile string     Default: "general"
Options are "general", "baseline", "restricted", "netadmin" or "sysadmin". Defaults to "general"</pre>
<p><span style="font-weight: 400">These profiles define the </span><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html"><span style="font-weight: 400">capabilities</span></a><span style="font-weight: 400"> associated with the </span><a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/"><span style="font-weight: 400">SecurityContext</span></a><span style="font-weight: 400"> and determine how the host&rsquo;s filesystem and namespaces are accessed. The security context details for each profile are listed below; further details are available in the </span><a href="https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/debug/profiles.go"><span style="font-weight: 400">source code</span></a><span style="font-weight: 400">.</span></p>
<table dir="ltr" border="1" cellspacing="0" cellpadding="0" data-sheets-root="1" data-sheets-baot="1">
<colgroup>
<col width="157">
<col width="355">
<col width="291"></colgroup>
<tbody>
<tr>
<td style="text-align: center"><span style="color: #000080"><strong>Profile</strong></span></td>
<td style="text-align: center"><span style="color: #000080"><strong>Debug Pod(SecurityContext)</strong></span></td>
<td style="text-align: center"><span style="color: #000080"><strong>Debug Node(SecurityContext)</strong></span></td>
</tr>
<tr>
<td style="padding-left: 40px">general</td>
<td style="padding-left: 40px">Add SYS_PTRACE cap</td>
<td style="padding-left: 40px">Attach host &ldquo;/&rdquo; filesystem.
<p>No SecurityContext</p>
<p>Use HostNetwork, HostPID Namespace,HostIPC Namespace</p></td>
</tr>
<tr style="padding-left: 40px">
<td style="padding-left: 40px">baseline</td>
<td style="padding-left: 40px">No SecurityContext</td>
<td style="padding-left: 40px">No SecurityContext</td>
</tr>
<tr style="padding-left: 40px">
<td style="padding-left: 40px">restricted</td>
<td style="padding-left: 40px">Drop ALL capabilities
<p>runAsNonRoot: true</p>
<p>allowPrivilegeEscalation: false</p>
<p>seCompProfile: RuntimeDefault</p></td>
<td style="padding-left: 40px">Drop ALL capabilities
<p>runAsNonRoot: true</p>
<p>allowPrivilegeEscalation: false</p>
<p>seCompProfile: RuntimeDefault</p></td>
</tr>
<tr style="padding-left: 40px">
<td style="padding-left: 40px">netadmin</td>
<td style="padding-left: 40px">Add NET_ADMIN, NET_RAW Cap</td>
<td style="padding-left: 40px">Add NET_ADMIN, NET_RAW Cap
<p>Use HostNetwork, HostPID Namespace,HostIPC Namespace</p></td>
</tr>
<tr style="padding-left: 40px">
<td style="padding-left: 40px">sysadmin</td>
<td style="padding-left: 40px">privileged: true
<p>Use HostNetwork, HostPID Namespace,HostIPC Namespace</p></td>
<td style="padding-left: 40px">privileged: true
<p>Use HostNetwork, HostPID Namespace,HostIPC Namespace</p>
<p>Attach host &ldquo;/&rdquo; filesystem.</p></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">The pod&rsquo;s execution behavior depends on the profile chosen when running the </span><span style="font-weight: 400">kubectl debug</span><span style="font-weight: 400"> command.</span></p>
<h2><span style="font-weight: 400">Caveats with Ephemeral Containers</span><a class="anchor-link" id="caveats-with-ephemeral-containers"></a></h2>
<p><span style="font-weight: 400">Even though ephemeral containers are useful, there are several caveats and potential security risks that users should be aware of:</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Ephemeral containers may be able to inspect processes, network traffic, environment variables, mounted volumes, or service account context depending on the Pod and cluster configuration.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">They can bypass the security benefits of distroless or minimal images.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Ephemeral containers can expose secrets</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Once an ephemeral container is added, it remains part of the Pod spec; it is not possible to remove it.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">As long as the ephemeral container&rsquo;s main process is running, you can attach to it using </span><span style="font-weight: 400">kubectl attach</span><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Ephemeral containers behaviour is unpredictable, containers might terminate abruptly depending on the resource configuration and utilization of the pod.</span></li>
</ol>
<h2><span style="font-weight: 400">Guardrails and Best practices with Ephemeral Containers</span><a class="anchor-link" id="guardrails-and-best-practices-with-ephemeral-containers"></a></h2>
<p><span style="font-weight: 400">Ephemeral containers are powerful, but they can create operational and security risks if used carelessly. Adhering to the following guardrails and best practices can help mitigate these risks:</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Control ephemeral containers access through RBAC. Only necessary users should have the privilege.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Avoid using the <code></code></span><span style="font-weight: 400"><span style="color: #0000ff">sysadmin</span></span><span style="font-weight: 400"> profile when possible. The </span><span style="font-weight: 400">restricted</span><span style="font-weight: 400"> profile is better suited for maintaining a strong security posture.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Always use approved, scanned images that contain only the tools required for debugging, rather than generic images like </span><span style="font-weight: 400">ubuntu</span><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Avoid running debug containers with long-running processes like <code><span style="color: #0000ff">sleep infinity</span></code> or <code><span style="color: #0000ff">bash</span></code>. Instead, run specific tools or commands that perform the required action and then terminate.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Audit ephemeral containers usage.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Recycle Pods that contain ephemeral containers whenever possible (e.g., during maintenance windows), especially if the ephemeral process has not terminated.</span></li>
</ol>
<h2><span style="font-weight: 400">Conclusion</span><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">Ephemeral containers are a powerful troubleshooting tool for Kubernetes workloads, especially when application images are minimal, distroless, or missing debugging utilities. They allow engineers to inspect a running Pod without rebuilding the image or restarting the workload.</span></p>
<p><span style="font-weight: 400">However, they should be treated as controlled operational access, not as a default debugging shortcut. Ephemeral containers can expose sensitive runtime details such as processes, environment variables, mounted volumes, and network state.</span></p>
<p><span style="font-weight: 400">Always restrict usage with proper RBAC. Use approved debug images, prefer the least-privileged debug profile, and reserve powerful profiles such as </span><span style="font-weight: 400">sysadmin</span><span style="font-weight: 400"> for &ldquo;break-glass&rdquo; scenarios only.</span></p>
<p><span style="font-weight: 400">Debug sessions should be short-lived, intentional, and tied to a real troubleshooting need. In short, ephemeral containers improve debuggability, but they must be used with clear security, audit, and operational guardrails.</span></p>
<p>The post <a href="https://www.percona.com/blog/debugging-with-ephemeral-containers/">Debugging with Ephemeral Containers</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/debugging-with-ephemeral-containers/">Debugging with Ephemeral Containers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Why I haven’t run my databases on Kubernetes</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/why-i-havent-run-my-databases-on-kubernetes/" />
      <id>https://www.percona.com/blog/why-i-havent-run-my-databases-on-kubernetes/</id>
      <updated>2026-06-30T12:48:45+03:00</updated>
      <author><name>Chetan Shivashankar</name></author>
      <summary type="html"><![CDATA[<p>A few years ago, if there was a discussion on “Should we run databases on Kubernetes?”, there were more people saying no than yes. One of the common answers was, “No. Kubernetes is for stateless workloads. Keep your databases outside.” Thankfully, today the discussion is no longer about whether we should run databases on Kubernetes, … Continued<br />
The post Why I haven’t run my databases on Kubernetes appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/why-i-havent-run-my-databases-on-kubernetes/">Why I haven’t run my databases on Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">A few years ago, if there was a discussion on &ldquo;Should we run databases on Kubernetes?&rdquo;, there were more people saying no than yes. One of the common answers was, &ldquo;No. Kubernetes is for stateless workloads. Keep your databases outside.&rdquo;</span></p>
<p><span style="font-weight: 400">Thankfully, today the discussion is no longer about </span><i><span style="font-weight: 400">whether</span></i><span style="font-weight: 400"> we should run databases on Kubernetes, but </span><i><span style="font-weight: 400">how</span></i><span style="font-weight: 400"> we can run them better on Kubernetes.</span></p>
<p><span style="font-weight: 400">In this post, we will look at some of the common arguments brought up against running databases on Kubernetes.</span></p>
<h2><span style="font-weight: 400">1. Kubernetes was designed for stateless workloads</span><a class="anchor-link" id="1-kubernetes-was-designed-for-stateless-workloads"></a></h2>
<p><span style="font-weight: 400">By far the most common concern</span></p>
<p><span style="font-weight: 400">Even though Kubernetes was initially used mainly for stateless workloads, many additions have been made since its initial version to accommodate stateful workloads. Features like </span><span style="font-weight: 400">StatefulSet</span><span style="font-weight: 400"> and </span><span style="font-weight: 400">PersistentVolumes</span><span style="font-weight: 400"> were introduced, and storage usage has been streamlined through the Container Storage Interface (CSI). Furthermore, the platform itself has evolved to support system-level capabilities like </span><a href="https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/"><span style="font-weight: 400">Swap space</span></a><span style="font-weight: 400"> to better manage memory-intensive databases.</span></p>
<p><span style="font-weight: 400">Today, a large number of users are successfully running stateful workloads on K8s. According to the latest CNCF Annual Survey Report, stateful containers have become a standard practice, with </span><a href="https://www.cncf.io/wp-content/uploads/2026/01/CNCF_Annual_Survey_Report_final.pdf"><span style="font-weight: 400">79% of Innovators</span></a><span style="font-weight: 400"> now running stateful applications in production.</span></p>
<h2><span style="font-weight: 400">2. Is my data safe on Kubernetes?</span><a class="anchor-link" id="2-is-my-data-safe-on-kubernetes"></a></h2>
<p><span style="font-weight: 400">Is my data safe on Kubernetes even when pods, nodes, or the entire Kubernetes cluster go down?</span></p>
<p><span style="font-weight: 400">This sounds terrifying at first, but cloud-native architecture is built with failure in mind. Pods die, nodes crash, and entire clusters can fail. The golden rule of running databases on Kubernetes is that your data must never depend on the lifecycle of the compute layer.</span></p>
<p><span style="font-weight: 400">This is where the absolute decoupling of compute and storage becomes critical. The underlying storage infrastructure exists entirely independent of the Pod, Node, and even the Kubernetes cluster itself. As long as your storage backend is architected correctly, your data is preserved regardless of what happens to the compute environment.</span></p>
<p><span style="font-weight: 400">For a database, this tiered isolation changes everything:</span></p>
<ul>
<li style="font-weight: 400"><b>If a Pod or Node dies:</b><span style="font-weight: 400"> Kubernetes automatically schedules a replacement Pod and reattaches it to the existing, intact storage volume.</span></li>
<li style="font-weight: 400"><b>If the entire Kubernetes Cluster goes down:</b><span style="font-weight: 400"> Because the data lives safely outside the cluster boundary, you can spin up a completely new Kubernetes cluster, connect it to the existing storage backend, and restore your database operations.</span></li>
</ul>
<p><span style="font-weight: 400">For replicated databases, a Kubernetes Operator can automate this resilience, detecting failures, promoting replicas, and reconciling your state across nodes, or even helping orchestrate disaster recovery across entirely different clusters.</span></p>
<h2><span style="font-weight: 400">3. Won&rsquo;t My Database Experience Downtime When Pods or Nodes Go Down?</span><a class="anchor-link" id="3-wont-my-database-experience-downtime-when-pods-or-nodes-go-down"></a></h2>
<p><span style="font-weight: 400">What if a Pod or a node running a database goes down? Will the database experience downtime?</span></p>
<p><span style="font-weight: 400">While failover concepts remain similar to traditional environments, Kubernetes transforms this into an automated, declarative process via Operators</span><span style="font-weight: 400">. Kubernetes automatically schedules a replacement Pod and reattaches it to the existing, intact storage volume. If the database is properly configured with a redundant, highly available configuration (recommended configuration), the remaining healthy Pods will continue to serve traffic. When utilizing Kubernetes solutions, you are delivering a data service powered by specific database technology, where high availability is maintained through a cluster of N Pods. The critical shift in mindset here is to focus on the service&rsquo;s resilience rather than the survival of individual Pods; our goal is to optimize the overall service, not the individual Pod, a distinction that is often misunderstood.</span></p>
<h2><span style="font-weight: 400">4. I have already built a lot of custom automation in our in-house environment. Why should I switch to Operators?</span><a class="anchor-link" id="4-i-have-already-built-a-lot-of-custom-automation-in-our-in-house-environment-why-should-i-switch-to-operators"></a></h2>
<p><span style="font-weight: 400">While custom scripts might work well today, proprietary automation always comes with heavy long-term maintenance overhead. To scale effectively, automation should be split into two distinct layers: </span><b>infrastructure</b><span style="font-weight: 400"> and </span><b>database management</b><span style="font-weight: 400">.</span></p>
<p><span style="font-weight: 400">Because Kubernetes is widely adopted and continuously updated by the global community, it handles the infrastructure layer (compute, networking, and storage) out of the box. By adopting a good </span><b>Operator</b><span style="font-weight: 400">, complex database-specific logic is offloaded. Ultimately, using Operators prevents you from reinventing the wheel and lets the teams focus on application value rather than maintaining custom infrastructure code.</span></p>
<h2><span style="font-weight: 400">5. Managing Pods, PVCs and configuration is painful</span><a class="anchor-link" id="5-managing-pods-pvcs-and-configuration-is-painful"></a></h2>
<p><span style="font-weight: 400">Manually managing StatefulSets, PersistentVolumeClaims (PVCs), and various other Kubernetes objects to run a database on Kubernetes can be tedious.</span></p>
<p><span style="font-weight: 400">This is where database operators emerge as game-changers.</span></p>
<p><span style="font-weight: 400">An operator is a Kubernetes-native controller that watches custom resources and continuously works to move the actual system toward the desired state. As a user, you describe something like: </span><i><span style="font-weight: 400">&ldquo;I want a database cluster with three instances, backups enabled, this storage, these resources, this version, monitoring enabled, and this replication setup.&rdquo;</span></i><span style="font-weight: 400"> The operator then handles everything under the hood, including managing Kubernetes objects and implementing the operational logic.</span></p>
<p><span style="font-weight: 400">Instead of playing the role of a mechanic who has to assemble all the parts and make everything run, you simply get into a car built by expert mechanics and drive it. The operator abstracts away the complexity so you can focus on the outcome rather than the implementation details.</span></p>
<h2><span style="font-weight: 400">6. I can configure bare-metal or VM nodes exactly how I want for database workloads, but Kubernetes makes this level of customization impossible.</span><a class="anchor-link" id="6-i-can-configure-bare-metal-or-vm-nodes-exactly-how-i-want-for-database-workloads-but-kubernetes-makes-this-level-of-customization-impossible"></a></h2>
<p class="isSelectedEnd">A very valid concern, especially for database engineers accustomed to carefully tuned virtual machines or bare-metal servers. Running databases inside Pods does introduce an additional abstraction layer. There may be a very small category of specialized, legacy &ldquo;pet&rdquo; databases that require highly specific bare-metal tuning and extreme isolation.</p>
<p class="isSelectedEnd">That said, the vast majority of configurations are achievable on Kubernetes. There are a few exceptions; for example, certain low-level storage tuning options may be limited by CSI driver abstractions. However, these are edge cases that rarely affect modern production deployments and do not impact the vast majority of database workloads.</p>
<p>For almost all modern production use cases, Kubernetes is more than capable of running databases reliably and efficiently.</p>
<h2><span style="font-weight: 400">7. Managed databases are way better than running on kubernetes</span><a class="anchor-link" id="7-managed-databases-are-way-better-than-running-on-kubernetes"></a></h2>
<p><span style="font-weight: 400">Managed databases are a great fit for many use cases, as they remove significant complexity and operational overhead.</span></p>
<p><span style="font-weight: 400">However, they do come with a few caveats:</span></p>
<ul>
<li style="font-weight: 400"><b>Cost at Scale:</b><span style="font-weight: 400"> Managed databases can become incredibly expensive compared to running databases yourself using Kubernetes Operators, especially as your data scales.</span></li>
<li style="font-weight: 400"><b>Vendor Lock-in:</b><span style="font-weight: 400"> When you rely on a cloud provider&rsquo;s managed service, you are typically locked into their ecosystem, making it difficult and costly to migrate away.</span></li>
<li style="font-weight: 400"><b>Configuration Limits:</b><span style="font-weight: 400"> In many cases, cloud vendors restrict your control, making it impossible to apply deep custom configurations or install specific database extensions that your application might require.</span></li>
</ul>
<p><span style="font-weight: 400">In short, many of the concerns raised are no longer relevant today or do not apply to the majority of use cases.</span></p>
<h2><span style="font-weight: 400">Conclusion</span><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">It&rsquo;s safe to say that databases run and run well on Kubernetes if configured well. In many cases, the shift is cultural rather than technical.</span></p>
<p><span style="font-weight: 400">Running databases on Kubernetes offers the advantage of preventing vendor lock-in. Even though database migration tools exist, cloud migrations remain difficult in practice. This is especially true with major cloud vendors, where your data layer is often tightly integrated with a complex web-native ecosystem of services.</span></p>
<p><span style="font-weight: 400">Operators excel at automating complex Day-2 operations like failovers, backups, and rolling updates. Running both your application and data layers under a single Kubernetes cluster provides a unified control plane for automation. This setup seamlessly aligns with GitOps workflows and becomes incredibly efficient when managing a large fleet of databases at scale.</span></p>
<p><span style="font-weight: 400">We have seen users derive incredible value by running databases on Kubernetes with operators. Some use it to manage standard database instances, while others have built their own fully fledged, internal DBaaS (Database-as-a-Service) platforms, and the list is long. Selecting the right Operator makes all the difference on this journey. Opting for free and open-source operators like the </span><a href="https://docs.percona.com/percona-operators/"><span style="font-weight: 400">Percona Operators</span></a><span style="font-weight: 400"> not only provides enterprise-grade databases but also saves your teams significant time and money.</span></p>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/why-i-havent-run-my-databases-on-kubernetes/">Why I haven&rsquo;t run my databases on Kubernetes</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/why-i-havent-run-my-databases-on-kubernetes/">Why I haven’t run my databases on Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Building Smart Semantic Search using PostgreSQL and pgvector. Part 3 &#8211; Hybrid Search, Percona Blog, and Widget Improvements</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/06/30/semantic-search-on-postgresql-part-3/" />
      <id>https://percona.community/blog/2026/06/30/semantic-search-on-postgresql-part-3/</id>
      <updated>2026-06-30T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Here I write about what I changed after the first launch: hybrid search, filters and counts on the search page, the widget layout, indexer hardening, and the Percona Blog in the index. People often type short words or names, and pure vector search is weak at that. When I turned search on about a month ago I asked for feedback in Part 1, and most of what follows came from that.</p>
<p><a href="https://percona.community/blog/2026/06/30/semantic-search-on-postgresql-part-3/">Building Smart Semantic Search using PostgreSQL and pgvector. Part 3 &#8211; Hybrid Search, Percona Blog, and Widget Improvements</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Here I write about what I changed after the first launch: hybrid search, filters and counts on the search page, the widget layout, indexer hardening, and the Percona Blog in the index. People often type short words or names, and pure vector search is weak at that. When I turned search on about a month ago I asked for feedback in Part 1, and most of what follows came from that.</p>
<p><a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">Part 1</a> is the introduction and stack overview. <a href="https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/">Part 2</a> is Postgres, chunks, SQL, and the indexer.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/search-part-3-search-page-tabs-pgbackuprest.jpg" alt="Search page with pgbackrest, filter tabs and match quality slider"></figure>
</p>
<h2 id="a-month-in-production">A month in production<a class="anchor-link" id="a-month-in-production"></a></h2>
<p>The API and indexer run in Docker on EC2. The engine is PostgreSQL with <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer">pgvector</a>. On the site there is a search widget in the blog header and a full results page at <a href="https://percona.community/search/" target="_blank" rel="noopener noreferrer">percona.community/search/</a>. Both call the same API. Search has been up since launch. I log queries for debugging and picked up feedback from colleagues, mostly in conversation and from my own tests, not from an analytics dashboard.</p>
<p>I keep <code>search_history</code> in Postgres as an engineering log: timings, regressions after hybrid changes, vector vs keyword splits. To be honest, the first month is mostly my smoke tests. Lots of <code>test</code>, the same names repeated while I debugged person-search, random widget checks. A &ldquo;top user queries&rdquo; chart from that log would look like internal QA, not real audience insight, so I am not publishing one here.</p>
<p>From feedback and from what I tried by hand, three kinds of queries kept showing up.</p>
<p>Short ones are a single product token (<code>pgbackrest</code>, <code>timescaledb</code>) or a person&rsquo;s name (contributor or Percona Blog author). Long phrases like &ldquo;zero downtime database migration&rdquo; or &ldquo;replication lag troubleshooting&rdquo; still work fine with vector-only search, as in parts 1 and 2. People also want filters by content type and honest counts on the tabs, so it does not feel broken when the UI shows 30 cards but the tab says 900 matched.</p>
<p>That shaped what I worked on in June. The table below is illustrative, not a leaderboard from production logs.</p>
<table>
<thead>
<tr>
<th>Example query</th>
<th>Type</th>
<th>Why it matters</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>timescaledb</code></td>
<td>keyword</td>
<td>one word, weak vector, needs text</td>
</tr>
<tr>
<td><code>PMM</code></td>
<td>keyword</td>
<td>short product name</td>
</tr>
<tr>
<td><code>Peter Zaitsev</code></td>
<td>person</td>
<td>contributor profile plus author articles</td>
</tr>
<tr>
<td><code>slow queries mysql</code></td>
<td>hybrid</td>
<td>short tech phrase, not pure semantics</td>
</tr>
<tr>
<td><code>zero downtime database migration</code></td>
<td>semantic</td>
<td>long query as designed</td>
</tr>
</tbody>
</table>
<p>In <code>/demo</code> under History I left the log and the Word statistics tab for myself. When traffic grows and I filter out test noise, that view will be more useful.</p>
<h2 id="widget-and-the-search-page">Widget and the search page<a class="anchor-link" id="widget-and-the-search-page"></a></h2>
<p>The widget has two modes, the popup in the header and the full <code>/search/</code> page (see the screenshot at the top of the post for tabs, counts, and the match quality slider).</p>
<p>From feedback I added filter tabs with multi-select (All, Blog, Percona Blog, Events, Talks, Contributors). The choice goes into the URL as <code>?type=blog,talk</code>. Tabs show counts in parentheses. Without a query that is indexed totals from public <code>GET /health</code>. After a search it is match counts per type from a separate <code>COUNT</code> in the API, without loading every card. There is a &ldquo;Minimum match quality&rdquo; slider for <code>min_score</code>, saved in <code>localStorage</code> and the URL. Search statistics (timings, vector vs keyword, score range) sit behind a compact link instead of taking half the page. Cards on <code>/search/</code> have a preview image, type, score, and a shorter excerpt.</p>
<p>I set the search page content width to 960px with tabs and controls centered. Small thing, but it reads better on mobile and desktop.</p>
<p>The next two screenshots use <code>Peter Zaitsev</code> as the person-search demo. <code>-pz-</code> in the filenames is shorthand for that query.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/search-part-3-search-stat-tabs-pz.jpg" alt="Search statistics for Peter Zaitsev, timings, hybrid split, tab match counts"></figure>
</p>
<p>The popup in the site header uses the same API. Metadata stays compact above the results.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/search-part-3-widget-pz-top.jpg" alt="Header popup, Peter Zaitsev, contributor profile first"></figure>
</p>
<h2 id="why-short-queries-hurt-pure-vector-search">Why short queries hurt pure vector search<a class="anchor-link" id="why-short-queries-hurt-pure-vector-search"></a></h2>
<p>The embedding model is trained on phrases and context. A one-token query like <code>pgbackrest</code> or two words without a clear topic like <code>Peter Zaitsev</code> gives a short vector with a weak signal. In 768 dimensions many irrelevant chunks still land &ldquo;not too far away&rdquo;, especially across 18k chunks.</p>
<p>With <code>Peter Zaitsev</code>, vector search pulled random old posts that mention the name in the body. The contributor profile and recent author articles were not on top. With <code>pgbackrest</code>, semantics blurred and the top hits were &ldquo;something about backup&rdquo;, not necessarily pgBackRest.</p>
<p>A long query like &ldquo;how to reduce replication lag on PostgreSQL&rdquo; is a different story. Query and documents are rich in context and cosine similarity behaves predictably. For that I kept vector-only.</p>
<h2 id="hybrid-search-options">Hybrid search options<a class="anchor-link" id="hybrid-search-options"></a></h2>
<p>I needed a stronger keyword leg for short queries without breaking semantic search for long ones.</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Pros</th>
<th>Cons / why not now</th>
</tr>
</thead>
<tbody>
<tr>
<td>PostgreSQL FTS (<code>to_tsvector</code>, <code>ts_rank</code>)</td>
<td>built-in, GIN indexes</td>
<td>dictionaries, stemming for names and brands</td>
</tr>
<tr>
<td><code>pg_trgm</code></td>
<td>good for typos</td>
<td>heavier at scale, extra indexes</td>
</tr>
<tr>
<td>OpenSearch / Elasticsearch</td>
<td>mature BM25</td>
<td>another cluster, I skipped this in Part 1</td>
</tr>
<tr>
<td>ILIKE plus heuristic score</td>
<td>fast to ship, one Postgres, easy to debug</td>
<td>not full BM25, <code>%pattern%</code> without GIN slows down at huge scale</td>
</tr>
</tbody>
</table>
<p>I started with ILIKE as step one. Something to compare against, with a clear upgrade path to FTS, trigram, or RRF. At community scale, about 7k documents, it is acceptable for now.</p>
<h2 id="how-hybrid-search-works-in-the-api">How hybrid search works in the API<a class="anchor-link" id="how-hybrid-search-works-in-the-api"></a></h2>
<h3 id="query-mode">Query mode<a class="anchor-link" id="query-mode"></a></h3>
<p><code>detect_search_mode()</code> in <code>search_hybrid.py</code> picks one of three modes.</p>
<ul>
<li><code>keyword</code> for one token (<code>timescaledb</code>, <code>audit_log</code>)</li>
<li><code>person</code> for 2-4 name-like tokens (<code>Peter Zaitsev</code>), with stop words like <code>percona</code>, <code>mysql</code></li>
<li><code>semantic</code> for everything else, vector only</li>
</ul>
<h3 id="two-search-legs">Two search legs<a class="anchor-link" id="two-search-legs"></a></h3>
<p>For <code>keyword</code> and <code>person</code> I run vector search as before (best chunk per document, <code>score &gt;= min_score</code>, <code>LIMIT N</code>) and keyword SQL on <code>pages</code>, plus a chunk body check when needed:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-0">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">author</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">'%Peter Zaitsev%'</span><span class="w"> </span><span class="k">ESCAPE</span><span class="w"> </span><span class="s1">''</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">title</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">'%Peter Zaitsev%'</span><span class="w"> </span><span class="k">ESCAPE</span><span class="w"> </span><span class="s1">''</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">description</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">'%Peter Zaitsev%'</span><span class="w"> </span><span class="k">ESCAPE</span><span class="w"> </span><span class="s1">''</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="err">&hellip;</span><span class="w"> </span><span class="n">tags</span><span class="p">,</span><span class="w"> </span><span class="n">chunk</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="err">&hellip;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">author</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="err">&hellip;</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">title</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="err">&hellip;</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ELSE</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="k">END</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="nb">date</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="n">NULLS</span><span class="w"> </span><span class="k">LAST</span></span></span></code></pre>
</div>
</div>
</div>
<p>For <code>person</code> I add ILIKE on each name part in author and title. Heuristic score 0.72-0.99 in Python (<code>score_keyword_row</code>). Exact author match ranks above a mention in the body.</p>
<p>ILIKE and user input. The SQL above uses literals for readability. In code every pattern is <code>ILIKE %s ESCAPE '\'</code> and psycopg2 binds the value. The query string is never concatenated into SQL, so classic injection like <code>' OR 1=1 --</code> does not apply. Parameters alone are not enough for ILIKE because <code>%</code> and <code>_</code> are wildcards inside the pattern. I escape <code></code>, <code>%</code>, and <code>_</code> in Python, wrap the term in <code>%&hellip;%</code>, and set <code>ESCAPE '\'</code> in SQL so a user cannot widen the match with their own <code>%</code>. Chunk body checks use <code>POSITION(LOWER(%s) IN LOWER(chunk_text))</code> with the same bound parameter.</p>
<h3 id="merge">Merge<a class="anchor-link" id="merge"></a></h3>
<p><code>merge_search_results()</code> unions by <code>slug</code>. If both legs match the same document, I keep the higher score. Then sort by score, then recency for dated content. For <code>person</code>, the matching contributor profile goes first.</p>
<p>The user&rsquo;s <code>min_score</code> is applied after merge so weak vector-only hits do not slip through when the threshold is raised.</p>
<h3 id="tab-counts">Tab counts<a class="anchor-link" id="tab-counts"></a></h3>
<p>For badges like <code>Percona Blog (905)</code> the API runs <code>COUNT(DISTINCT slug) &hellip; GROUP BY content_type</code> with the same keyword conditions, no row <code>LIMIT</code>. The UI still shows 30 best cards. The tab number is how many matched in total.</p>
<p>The response includes <code>stats.search_mode</code> (<code>semantic</code>, <code>keyword</code>, or <code>person</code>) and timings split into <code>vector_db_ms</code> and <code>keyword_db_ms</code>.</p>
<pre class="mermaid">
flowchart LR
Q["Query"] --&gt; D{detect_search_mode}
D --&gt;|semantic| V["vector_search"]
D --&gt;|keyword / person| V
D --&gt;|keyword / person| K["keyword_search ILIKE"]
V --&gt; M["merge by slug"]
K --&gt; M
M --&gt; F["filter min_score"]
F --&gt; R["&le; limit cards"]
K --&gt; C["COUNT for tabs"]
</pre>
<h2 id="the-bug-that-broke-person-search-in-production">The bug that broke person search in production<a class="anchor-link" id="the-bug-that-broke-person-search-in-production"></a></h2>
<p>After I shipped hybrid search, queries like <code>Peter Zaitsev</code> returned 500. The widget showed &ldquo;Oops, sorry &ndash; something went wrong on our end.&rdquo; Semantic queries still worked.</p>
<p>API logs:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-2">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">psycopg2.errors.InternalError_: could not load library "/usr/pgsql-18/lib/llvmjit.so":
</span></span><span class="line"><span class="cl">undefined symbol: _ZSt21__glibcxx_assert_failPKciS0_S0_</span></span></code></pre>
</div>
</div>
</div>
<p>Hybrid person search builds a heavy plan with <code>ILIKE</code> and <code>POSITION(LOWER(...) IN chunk_text)</code> over 18k chunks. PostgreSQL tried to JIT-compile it and failed loading <code>llvmjit.so</code>. The same error is described on the <a href="https://forums.percona.com/t/llvmjit-so-fails-to-load-in-percona-postgresql-17-container/40690" target="_blank" rel="noopener noreferrer">Percona forum</a>. I set <code>jit = off</code> on every API connection and search worked again.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-3">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SET</span><span class="w"> </span><span class="n">jit</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">off</span><span class="p">;</span><span class="w"> </span><span class="c1">-- on every API connection</span></span></span></code></pre>
</div>
</div>
</div>
<p>For tab <code>COUNT</code> in person mode I count on <code>pages</code> without scanning chunks. Author and title cover name search. Full keyword search with body still runs with <code>jit = off</code>.</p>
<p>On PG 18 with pgvector and text subqueries, test hybrid on person and keyword queries, not only long phrases. The planner behaves differently.</p>
<h2 id="percona-blog-in-one-index">Percona Blog in one index<a class="anchor-link" id="percona-blog-in-one-index"></a></h2>
<p>The community site already had blog posts, events, talks, and contributors. Without the <a href="https://www.percona.com/blog/" target="_blank" rel="noopener noreferrer">official Percona Blog</a> search felt incomplete. Talks and community posts link there every day.</p>
<h3 id="what-i-had-to-build">What I had to build<a class="anchor-link" id="what-i-had-to-build"></a></h3>
<ol>
<li>New <code>content_type</code>: <code>percona_blog</code>, its own RSS feed at <code>https://www.percona.com/blog/feed/</code>, separate crawler rules.</li>
<li>Same pipeline as the rest: RSS, HTML, chunks, embedding, <code>pages</code> and <code>community_nomic</code>. Same model <code>nomic-embed-text-v1</code>, prefixes <code>search_document:</code> and <code>search_query:</code> unchanged.</li>
<li>WordPress and RSS quirks on Percona Blog. Images in Open Graph, author in metadata, full text only on the HTML page. I added <code>fetch_percona_blog_image()</code> and a <code>percona_blog</code> branch in <code>crawler.py</code>.</li>
<li>Scale. Roughly 6200+ new documents. The index went from about 800 to about 7000 pages and 18,000 chunks. HNSW on a single Postgres on EC2 still copes, but a full re-index takes hours, not minutes.</li>
</ol>
<h3 id="indexer-after-crashes">Indexer after crashes<a class="anchor-link" id="indexer-after-crashes"></a></h3>
<p>The first full Percona Blog crawl failed several times. Worker OOM, Docker restarts, a long RSS walk. I hardened the indexer:</p>
<table>
<thead>
<tr>
<th>Problem</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Re-crawl from scratch after a crash</td>
<td><code>skip_known</code>, skip URLs already in <code>pages</code></td>
</tr>
<tr>
<td>Resume mid-RSS</td>
<td>start feed page near <code>indexed_count // 10</code></td>
</tr>
<tr>
<td>Cancel did not work</td>
<td><code>cancel_requested</code> checks between RSS pages, before fetch and encode</td>
</tr>
<tr>
<td>OOM during embedding</td>
<td><code>EMBED_BATCH_SIZE=8</code>, batched <code>model.encode()</code></td>
</tr>
<tr>
<td>No visibility</td>
<td><code>progress_log</code> on <code>indexer_runs</code>, live log in demo</td>
</tr>
<tr>
<td>Stuck task after kill</td>
<td>on worker startup honour cancel, re-queue resume tasks</td>
</tr>
</tbody>
</table>
<p>Same worker and <code>index_queue</code>, but behavior closer to production ETL than a one-off script.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/search-part-3-sources.jpg" alt="Admin dashboard, documents, chunks, and percona_blog in the index"></figure>
</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/search-part-3-index.jpg" alt="Percona Blog indexing, progress log and current URL"></figure>
</p>
<h2 id="whats-next">What&rsquo;s next<a class="anchor-link" id="whats-next"></a></h2>
<ul>
<li>PostgreSQL FTS instead of bare ILIKE for the keyword leg</li>
<li>Pagination on <code>/search/</code> if I need to go past 30. Nobody needs all 900 author posts on one screen, &ldquo;load 30 more&rdquo; is enough</li>
<li>More sources from the Part 1 roadmap: video, GitHub, forum</li>
</ul>
<h2 id="try-it-yourself">Try it yourself<a class="anchor-link" id="try-it-yourself"></a></h2>
<p>Popup search is in the <a href="https://percona.community" target="_blank" rel="noopener noreferrer">percona.community</a> header. Full results are at <a href="https://percona.community/search/?q=zero+downtime+database+migration" target="_blank" rel="noopener noreferrer">percona.community/search/</a>.</p>
<p>Examples to try. These are demos of different modes, not a top from the log.</p>
<table>
<thead>
<tr>
<th>Query</th>
<th>Mode</th>
<th>What to check</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zero downtime database migration</code></td>
<td>semantic</td>
<td>long phrase, vector-only</td>
</tr>
<tr>
<td><code>replication lag troubleshooting</code></td>
<td>semantic</td>
<td>same</td>
</tr>
<tr>
<td><code>pgbackrest</code></td>
<td>keyword</td>
<td>one-word product, hybrid</td>
</tr>
<tr>
<td><code>Peter Zaitsev</code></td>
<td>person</td>
<td>contributor plus author articles</td>
</tr>
<tr>
<td><code>best pizza recipe napoli</code></td>
<td>semantic</td>
<td>off-topic, empty above <code>min_score</code></td>
</tr>
</tbody>
</table>
<p>As in <a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">Part 1</a>, I am not publishing the search service code. It is built for percona.community. These posts share observations and ideas you can adapt. Vector search schema and SQL are in <a href="https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/">Part 2</a>. The database is open-source <a href="https://docs.percona.com/postgresql/18/index.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL</a>.</p>
<p>If you are building something similar or hit an edge case, leave a comment. In Part 1 I asked for feedback and it led to this post.</p>

<p><a href="https://percona.community/blog/2026/06/30/semantic-search-on-postgresql-part-3/">Building Smart Semantic Search using PostgreSQL and pgvector. Part 3 &#8211; Hybrid Search, Percona Blog, and Widget Improvements</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Skipping Percona Server for MySQL 8.4.9 and 9.7.0</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-server-mysql-8-4-9-9-7-0-skipped/" />
      <id>https://www.percona.com/blog/percona-server-mysql-8-4-9-9-7-0-skipped/</id>
      <updated>2026-06-29T15:19:11+03:00</updated>
      <author><name>Dennis Kittrell</name></author>
      <summary type="html"><![CDATA[<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. These fixes arrived through Oracle’s new monthly Critical Security Patch Updates (CSPUs), which Oracle announced begin May … Continued<br />
The post Skipping Percona Server for MySQL 8.4.9 and 9.7.0 appeared first on Percona.</p>
<p><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://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Upstream MySQL published an out-of-schedule release this week with two high-severity CVE fixes. We&rsquo;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&rsquo;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&rsquo;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&rsquo;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&rsquo;s happening<a class="anchor-link" id="whats-happening"></a></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&rsquo;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&rsquo; content.</p>
<h2>Who this affects<a class="anchor-link" id="who-this-affects"></a></h2>
<p>If you were waiting specifically for 8.4.9 or 9.7.0, those versions won&rsquo;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&rsquo;t tracking a specific version number, nothing changes for you.</p>
<h2>What to do<a class="anchor-link" id="what-to-do"></a></h2>
<p>Nothing urgent. Upgrade to the next Percona Server for MySQL releases as you normally would once they&rsquo;re published. We&rsquo;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<a class="anchor-link" id="what-to-expect-going-forward"></a></h2>
<p>Oracle&rsquo;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&rsquo;t change. We&rsquo;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>

<p><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://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/" />
      <id>https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/</id>
      <updated>2026-06-29T04:43:34+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. …<br />
Continue reading \"MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh\"<br />
The post MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/">MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/">MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-fariha-shaikh/">MariaDB Foundation Sea Lion Champions Nominees: Fariha Shaikh</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Preparing Your Analytical Databases for Agents</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/preparing-your-analytical-databases-for-agents/" />
      <id>https://vettabase.com/preparing-your-analytical-databases-for-agents/</id>
      <updated>2026-06-27T15:15:52+03:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>Agents can query databases: this has always been the dream of many data people! But there is some work to do to make this process reliable, secure, and valuable for your team.</p>
<p><a href="https://vettabase.com/preparing-your-analytical-databases-for-agents/">Preparing Your Analytical Databases for Agents</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">Agents can query databases: this has always been the dream of many data people! Sure, SQL resembles English, so it makes it relatively easy to express your questions and get answers. The key here is <em>relatively</em>. You still need to think about your schema, which tables need to be joined to gather the information you need, recall column names, and follow a precise syntax.</p>
<p class="wp-block-paragraph">Agents save you that effort. You express your query in English, you get a result. Well, you might still have to wait if you have a lot of data and the query is complex, but during that time you can switch to other tasks. It feels like waving a magic wand and seeing your desires materialise&hellip; but it&rsquo;s not exactly like that. Because both you and LLMs can make mistakes, especially if your schema isn&rsquo;t clear.</p>
<p class="wp-block-paragraph">And that&rsquo;s why I tell people that their schemas should be prepared for agents. Let&rsquo;s see what this means.</p>
<h2 class="wp-block-heading">Security<a class="anchor-link" id="security"></a></h2>
<p class="wp-block-paragraph">The first thing to address is security. Make sure that agents and tools that read data can only access data they should be able to access, and they can only perform the authorised operations. For example, typically they must not be able to add, modify or delete any data. And if your database contains PII data (such as customer names, anagraphic data and contacts), you typically don&rsquo;t want your AI to be able to query those data.</p>
<p class="wp-block-paragraph">In some cases, the data that the agent or tool should be able to see depend on the user. A sales representative might be authorised to see her own customers and performance data, but not her colleagues data. A member of the legal team might be authorised to see contracts that other users shouldn&rsquo;t see.</p>
<p class="wp-block-paragraph">Databases have all the features you need to implement this level of security. It&rsquo;s important that the agent or tool uses a dedicated database user, or if necessary, a database user that is dedicated to the AI&rsquo;s current human user. Databases support different permissions for every type of supported operations &ndash; typically, you only want to grant AI the <code>SELECT</code> permission. That permission can be granted at schema, table, or column level. Implementing row-level permission is slightly more complex, but feasable. If the DBMS you use doesn&rsquo;t support <a href="https://vettabase.com/row-level-security-policy-in-postgresql/" data-type="post" data-id="38404">a specific way</a> to do this, you can still implement row-based permissions <a href="https://vettabase.com/mariadb-mysql-using-views-to-grant-or-deny-row-level-privileges/" data-type="post" data-id="38278">using stored procedures and views</a>.</p>
<h2 class="wp-block-heading">Configuration<a class="anchor-link" id="configuration"></a></h2>
<p class="wp-block-paragraph">If you&rsquo;re not using a database designed for analytical workloads (such as ClickHouse, Snowflake or Amazon Redshift) you should really configure it to optimise complex, long-running queries. We won&rsquo;t dig into the details, because they greatly depend on which database you use.</p>
<p class="wp-block-paragraph">Regardless which technology you use, it&rsquo;s important to make sure that:</p>
<ul class="wp-block-list">
<li>Your queries timeouts are long enough to permit analytical queries. I often insist that, for OLTP, granting all queries a timeout that is longer than 5 seconds is unreasonable. The user has already left the page, and the only effect of keeping the query running is consuming more database resources. But for analytical workloads, it would be utopic to assume than 10 minutes is a long time.</li>
<li>On the other hand, enough is enough. A query cannot run for days and make the database unresponsive or too slow for all other users. There should be a reasonable timeout. Some technologies or external tools allow to create more complex rules, and kill the queries that are using too many resources.</li>
</ul>
<h2 class="wp-block-heading">Database Design<a class="anchor-link" id="database-design"></a></h2>
<p class="wp-block-paragraph">Every table must express an entity or a relationship between entities, in a standard, clear, and clean way.</p>
<p class="wp-block-paragraph">When a table is a sparse matrix, the relationships between the objects it contains are unclear. When a table contains multiple entities, its meaning itself is unclear.</p>
<p class="wp-block-paragraph">Consider <a href="https://www.odoo-community.org/" rel="noopener">Odoo</a>&lsquo;s schema. Odoo is a most important open source CRM, and it&rsquo;s currently growing very fast in Europe. But &ndash; sorry to be blunt here &ndash; its PostgreSQL schema is poorly designed. Tables often have multiple meanings. Or they have one meaning for the developers, but this doesn&rsquo;t match the customers&rsquo; business objects and processes.</p>
<p class="wp-block-paragraph">For example, both customers and suppliers were stored in the <code>res_partner</code> table. In older versions, they could be distinguished thanks to boolean columns called <code>customer</code> and <code>supplier</code>. In newer versions, it got worse: to find customers we need to filter by <code>customer_rank &gt; 0</code>, and to find suppliers we need to filter by <code>supplier_rank &gt; 0</code>. While LLMs tend to know this, expect them to make many mistakes when you ask them to write complex queries that involve multiple unclear tables.</p>
<h2 class="wp-block-heading">Clear Naming<a class="anchor-link" id="clear-naming"></a></h2>
<p class="wp-block-paragraph">Unclear naming can easily confuse models. If a column tells you how many products of a certain type are in a certain warehouse, that column should be named <code>quantity</code>. It&rsquo;s also acceptable to call it <code>qty</code>, because it&rsquo;s a common abbreviation that models have encountered many times during their training. Calling it <code>count</code> can occasionally lead to confusion. Calling it <code>num</code> or <code>n</code> is a great way to get wrong results &ndash; and yes, I&rsquo;ve seen columns called <code>num</code> or <code>n</code>.</p>
<p class="wp-block-paragraph">The model needs to know the <em>type</em> of columns, too, so it can produce correct syntax ( <code>text_col='text' AND int_col=1</code>). If the typing is confusing, it can lead to errors. When naming is acceptable but not very clear, wrong typing can mislead models. For example, <code>dob</code> usually means Date of Birth, but it can have many other meanings. At least one of which relates to people too: Degree of Burglary. Using the proper type can avoid funny mistakes.</p>
<p class="wp-block-paragraph">See also my article <a href="https://vettabase.com/why-your-database-deserves-consistent-names-and-types/" data-type="post" data-id="366159">Why Your Database Deserves Consistent Names and Types</a>.</p>
<h2 class="wp-block-heading">Comments<a class="anchor-link" id="comments"></a></h2>
<p class="wp-block-paragraph">Tables, columns, views, and basically every schema object can have a comment. Use comments whenever the meaning of a column isn&rsquo;t obvious. Use it to clarify acronyms, to indicate the format of text columns, and above all to clarify obscure cases.</p>
<p class="wp-block-paragraph">Don&rsquo;t use comments when the meaning of something cannot realistically confuse the reader, avoiding added noise. Remember: humans and models need to focus their attention towards the right things to avoid making mistakes. Noise makes this difficult.</p>
<div id="awgt_1abfaecb7e8c" class="awgt-alert-content-wrap">#awgt_1abfaecb7e8c .awgt-alert-box {max-width: 100%;background: transparent;border-color: #007cba;}#awgt_1abfaecb7e8c .awgt-alert-content {color: #5a5a5a;padding: 15px 30px;}#awgt_1abfaecb7e8c .awgt-alert-content p {font-size: 16px;line-height: 24px;}#awgt_1abfaecb7e8c .awgt-alert-content a {color: #f44336;}#awgt_1abfaecb7e8c legend.awgt-alert-icon svg {fill: #007cba;width: 25px;height: 25px;}#awgt_1abfaecb7e8c legend.awgt-alert-icon {margin-left: 5%;padding: 0 12px;}#awgt_1abfaecb7e8c .awgt-alert-content.awgt-drop-cap p:first-child:first-letter {color: #000000;font-size: 40px;margin-top: 0px;}#awgt_1abfaecb7e8c .awgt-alert-content.awgt-first-bold &gt; p &gt; strong:first-child {color: #000000;padding-right: 5px;font-size: 16px;}#awgt_1abfaecb7e8c .awgt-alert-lay-3 .awgt-alert-content.awgt-first-bold &gt; p &gt; strong:first-child {display: block;padding-right: 0px !important;padding-bottom: 5px;}#awgt_1abfaecb7e8c fieldset.awgt-alert-box.awgt-alert-lay-3 legend.awgt-alert-icon:before {border-color: transparent transparent #007cba transparent;filter: brightness(0.7);}#awgt_1abfaecb7e8c fieldset.awgt-alert-box.awgt-alert-lay-3 legend.awgt-alert-icon:after {border: 26px solid #007cba;}
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>Also, schemas change over time. Make sure that comments change along with the schema, rather than becoming obsolete.</p>
</div>
</fieldset>
</div>
<h2 class="wp-block-heading">Views<a class="anchor-link" id="views"></a></h2>
<p class="wp-block-paragraph">Views don&rsquo;t eliminate complexity, they add up to existing complexity. The main undesirable consequences of this are that:</p>
<ol class="wp-block-list">
<li>The query planner / optimiser might ignore some indexes in the underlying tables, making your queries unnecessarily slow. In technical terms: views can prevent a predicate pushdown, forcing a view materialisation.</li>
<li>If models still learn about the underlying tables, they can get confused by multiple levels of complexity.</li>
</ol>
<p class="wp-block-paragraph">That said, if used wisely, views can make some queries simpler to create. If the model can avoid writing a JOIN involving many tables, filters and aggregations by using a single view, this will increase the chances that the rest of the query is correct and clean.</p>
<p class="wp-block-paragraph">You can also use views to <a href="https://vettabase.com/mariadb-mysql-using-views-to-grant-or-deny-row-level-privileges/" data-type="post" data-id="38278">hide sensitive information</a> from a model.</p>
<h2 class="wp-block-heading">Documentation and Data Dictionary<a class="anchor-link" id="documentation-and-data-dictionary"></a></h2>
<p class="wp-block-paragraph">Schemas need to be documented. Agents didn&rsquo;t eliminate this need, they made it more urgent.</p>
<p class="wp-block-paragraph">Suppose you ask &ldquo;which warehouses are critically full?&rdquo;. Maybe your schema has confusing naming, and warehouses are in a table called <code>store</code>. Maybe &ldquo;critically full&rdquo; is a precise concept used by your team, but its explanation can&rsquo;t be found in the data. Maybe it even has a different meaning for different teams (far from uncommon in medium/big companies).</p>
<p class="wp-block-paragraph">In the simplest cases, it&rsquo;s usually desirable to send some schema documentation to the models using the <em>system prompt</em> &ndash; the initial message that the agent sends to the model, followed by anything the user wrote. If you have dozens of tables, this is unlikely to be sufficient.</p>
<p class="wp-block-paragraph">For more complex cases, the system prompt should only include:</p>
<ul class="wp-block-list">
<li>A generic explanation of the business domain and the schema.</li>
<li>A dictionary of the business terms that might be found in the user&rsquo;s request. If necessary, this dictionary should vary from team to team.</li>
<li>Documentation of naming rules that are used consistently across the schema.</li>
<li>Documentation of the most common tables &ndash; the ones that have a good chance of being referenced in a query.</li>
<li>A list of entity groups &ndash; for example: customer entities, inventory entities, payment entities, etc. Entities are tables and views. Some entities might be part of multiple groups.</li>
</ul>
<p class="wp-block-paragraph">With this information, the model should understand:</p>
<ul class="wp-block-list">
<li>How to compose very common queries.</li>
<li>Which groups it needs more information about to fulfill a user request.</li>
</ul>
<p class="wp-block-paragraph">The model will be instructed to let the agent know, in a machine-readable format, if it needs informaiton about some entity groups, and which ones. When this happens, the model will be provided with additional documentation. In the AI jargon, these pieces of additional documentation are called resources.</p>
<div id="awgt_32adfa4ad343" class="awgt-alert-content-wrap">#awgt_32adfa4ad343 .awgt-alert-box {max-width: 100%;background: transparent;border-color: #007cba;}#awgt_32adfa4ad343 .awgt-alert-content {color: #5a5a5a;padding: 15px 30px;}#awgt_32adfa4ad343 .awgt-alert-content p {font-size: 16px;line-height: 24px;}#awgt_32adfa4ad343 .awgt-alert-content a {color: #f44336;}#awgt_32adfa4ad343 legend.awgt-alert-icon svg {fill: #007cba;width: 25px;height: 25px;}#awgt_32adfa4ad343 legend.awgt-alert-icon {margin-left: 5%;padding: 0 12px;}#awgt_32adfa4ad343 .awgt-alert-content.awgt-drop-cap p:first-child:first-letter {color: #000000;font-size: 40px;margin-top: 0px;}#awgt_32adfa4ad343 .awgt-alert-content.awgt-first-bold &gt; p &gt; strong:first-child {color: #000000;padding-right: 5px;font-size: 16px;}#awgt_32adfa4ad343 .awgt-alert-lay-3 .awgt-alert-content.awgt-first-bold &gt; p &gt; strong:first-child {display: block;padding-right: 0px !important;padding-bottom: 5px;}#awgt_32adfa4ad343 fieldset.awgt-alert-box.awgt-alert-lay-3 legend.awgt-alert-icon:before {border-color: transparent transparent #007cba transparent;filter: brightness(0.7);}#awgt_32adfa4ad343 fieldset.awgt-alert-box.awgt-alert-lay-3 legend.awgt-alert-icon:after {border: 26px solid #007cba;}
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>Remember that models, just like humans, understand examples and patterns better than long explanations. For example, it&rsquo;s better to omit an explanation and tell the model that a string matches this format: &lt;project id-or-label=&rdquo;?&rdquo;&gt;&lt;customer id=&rdquo;?&rdquo;&gt;&lt;ticket title=&rdquo;?&rdquo;/&gt;&lt;/customer&gt;&lt;/project&gt;</p>
</div>
</fieldset>
</div>
<h2 class="wp-block-heading">SQL Advanced Features and Dialects<a class="anchor-link" id="sql-advanced-features-and-dialects"></a></h2>
<p class="wp-block-paragraph">SQL has many advanced features that are incredibly powerful, but not widely used. To make things worse, every dialect has non-standard variations that normally work on one DBMS only. This often confuses models. I&rsquo;ve seen cases where they suggest using MariaDB temporal tables features on PostgreSQL, or where they use PostgreSQL type conversion syntax in a query for MySQL.</p>
<p class="wp-block-paragraph">It is a good idea to use a set of <a href="http://agentskills.io/" rel="noopener">agent skills</a> or <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/resources" rel="noopener">MCP resources</a> to inform the model about the syntaxes that are supported by the DBMS you use. For example, MariaDB Foundation maintains a set of <a href="https://github.com/MariaDB/skills" rel="noopener">MariaDB skills</a> that you can easily make available to your agents.</p>
<h2 class="wp-block-heading">Semantic Layers<a class="anchor-link" id="semantic-layers"></a></h2>
<p class="wp-block-paragraph">There is a movement in the data industry toward Semantic Layers (like dbt Semantic Layer, Cube, LookML). You don&rsquo;t have to necessarily adopt one of them, as you can build your semantics in a more reliable, cheaper and cleaner way by following the good practices listed here. However, I want to mention that semantic layers exist for the sake of completeness.</p>
<h2 class="wp-block-heading">Training<a class="anchor-link" id="training"></a></h2>
<p class="wp-block-paragraph">This is the last link of the chain, but make no mistake: you want to make sure it&rsquo;s not the weakest! A technology can&rsquo;t save humans from using it in a bad way.</p>
<p class="wp-block-paragraph">It&rsquo;s important to train AI users. They need to know:</p>
<ul class="wp-block-list">
<li>How to formulate a good question;</li>
<li>How to make sure the model understands a question;</li>
<li>How to make sure the model has enough information to emit an informed answer;</li>
<li>How to make sure the model has made a correct reasoning;</li>
<li>How to spot hallucinations.</li>
</ul>
<p class="wp-block-paragraph">This is important to avoid the trap of AI&rsquo;s hallucinations.</p>
<p class="wp-block-paragraph">AI training is not part of our services, but we can direct you to reliable partners.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">Many schemas are far from having a clean, self-documenting structure. You need to make sure that models understand user requests and translate them into correct SQL by fixing the schema where needed, and by adding relevant documentation. Changing the schema is better in terms of correctness of the results and token saving, but the required effort is not always reasonable. With clear, precise, well-structured documentation it is still possible to obtain good SQL queries from a model.</p>
<p class="wp-block-paragraph">We can help you prepare your schemas for agents. <a href="https://vettabase.com/contact/" data-type="page" data-id="11">Contact us</a> to discuss what we can do.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p><p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/preparing-your-analytical-databases-for-agents/">Preparing Your Analytical Databases for Agents</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Enterprise Server Q2 2026 Maintenance Releases</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-enterprise-server-q2-2026-maintenance-releases/" />
      <id>https://mariadb.com/resources/blog/mariadb-enterprise-server-q2-2026-maintenance-releases/</id>
      <updated>2026-06-26T22:25:04+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>New maintenance releases for MariaDB Enterprise Server: 11.8.8-5, 11.4.12-9, and 10.6.27-23 are now available. Download Now Notable Release Updates MariaDB […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-q2-2026-maintenance-releases/">MariaDB Enterprise Server Q2 2026 Maintenance Releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>New maintenance releases for MariaDB Enterprise Server: 11.8.8-5, 11.4.12-9, and 10.6.27-23 are now available. Download Now MariaDB Enterprise Server is an enhanced, hardened and secured version of MariaDB Community Server that delivers enterprise reliability, stability and long-term support as well as greater operational efficiency when it comes&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-q2-2026-maintenance-releases/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-enterprise-server-q2-2026-maintenance-releases/">MariaDB Enterprise Server Q2 2026 Maintenance Releases</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/" />
      <id>https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/</id>
      <updated>2026-06-26T12:37:50+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>I hear this sentence a lot: “We care about privacy.”<br />
Good.<br />
But then you look a bit closer.<br />
Files are on some cloud platform. Nobody is completely sure which settings were changed two years ago. …<br />
Continue reading \"MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server\"<br />
The post MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/">MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I hear this sentence a lot: &ldquo;We care about privacy.&rdquo;<br>
Good.<br>
But then you look a bit closer.<br>
Files are on some cloud platform. Nobody is completely sure which settings were changed two years ago. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/">MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-privacy-first-stack-nextcloud-passbolt-and-mariadb-server/">MariaDB Privacy-First Stack: Nextcloud, Passbolt and MariaDB Server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Why PostgreSQL needs an AI usage policy</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/06/26/why-postgresql-needs-an-ai-usage-policy/" />
      <id>https://percona.community/blog/2026/06/26/why-postgresql-needs-an-ai-usage-policy/</id>
      <updated>2026-06-26T08:42:27+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We often hear that open source is about people.</p>
<p><a href="https://percona.community/blog/2026/06/26/why-postgresql-needs-an-ai-usage-policy/">Why PostgreSQL needs an AI usage policy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We often hear that open source is about people.</p>
<p>People who contribute their time and, in a way, parts of their lives to work on software that is available for everyone without limitations and without licensing costs.</p>
<p>The more popular a project becomes, the more often we also hear about the need for sustainable open source. Nothing surprising here. Often projects start off as &ldquo;scratching ones itch&rdquo; and it&rsquo;s very appreciated when others notice the work done. The more time passes and the more the work becomes appreciated, the higher the chances that there will be a need to spend more time on the project.</p>
<p>When projects graduate from a hobby project to software used by thousands of users, or even a foundational building block in production, things get interesting.</p>
<p>At that point, we may hope to see new contributors joining the project. This would normally be a good thing. But is it still the same in the AI hype era, where anyone can generate almost any content and claim it as their own?</p>
<p>AI was supposed to be a killer of open source.&nbsp;After all, a lot of publicly available code from open source communities was part of what AI systems trained on.&nbsp;The fear was that as it would become so easy to create our own software, there would not be as much need for the existing open source projects. While this was the hype speaking, we can notice another trend. It became much easier to propose patches, detect and report security threats, or submit code reviews. Even without any developer experience or coding capabilities.</p>
<h3 id="how-sustainable-is-that-for-the-human-maintainers">How sustainable is that for the human maintainers?<a class="anchor-link" id="how-sustainable-is-that-for-the-human-maintainers"></a></h3>
<p>It is easy to imagine that there is a very fine line between positively helpful and overwhelming. As with anything unwanted, AI-generated code or text can be harmful to many open source projects. Especially those with a single maintainer treating their project as a spare time hobby and suddenly experiencing a waterfall of <a href="https://en.wikipedia.org/wiki/AI_slop" target="_blank" rel="noopener noreferrer">AI-slop</a>.</p>
<p><figure><img decoding="async" src="https://percona.community/blog/2026/06/Jan-ps3.png" alt="RPCS3 plead to vibe coders social media post"></figure>
</p>
<p><a href="https://github.com/RPCS3/rpcs3" target="_blank" rel="noopener noreferrer">Playstation 3 emulator project</a> recently <a href="https://x.com/rpcs3/status/2053248922974605431?lang=en" target="_blank" rel="noopener noreferrer">RPCS3 posted a plea</a> to the vibe coders to stop the AI-generated abuse already and they are not alone in this problem.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/Jan-fosdem-curl.png" alt="FOSDEM 2026 Daniel Stenberg presentation"></figure>
</p>
<p>Daniel Stenberg from the <a href="https://curl.se/" target="_blank" rel="noopener noreferrer">curl project</a> captured this well in <a href="https://fosdem.org/2026/schedule/event/B7YKQ7-oss-in-spite-of-ai/" target="_blank" rel="noopener noreferrer">his FOSDEM 2026 talk</a> summarizing that: &ldquo;AI gives us the worst and the best, simultaneously.&rdquo;</p>
<p>In the same talk, he discussed how curl had to stop its bug bounty program. Curl has also posted on the <a href="https://curl.se/dev/contribute.html#on-ai-use-in-curl" target="_blank" rel="noopener noreferrer">rules of AI use</a>. Even that was not enough, which led to the &ldquo;<a href="https://daniel.haxx.se/blog/2026/06/15/curl-summer-of-bliss/" target="_blank" rel="noopener noreferrer">curl summer of bliss</a>&rdquo;, where they will:</p>
<blockquote>
<p>not accept or otherwise handle any vulnerability reports during the month of July 2026.</p>
</blockquote>
<p>Security reports are an especially sensitive case. An AI-generated vulnerability report is not harmless. Someone has to read it, reproduce it, evaluate it and decide whether it is real. Even when the issue is not there, the work is still very real. Like it or not but when it&rsquo;s unfounded work that proves a ai-generated false it is abusive.</p>
<p>Knowing that some projects adopt AI-focused policies, I searched for examples of such policies, using AI obviously &#128578;, and stumbled upon <a href="https://github.com/melissawm/open-source-ai-contribution-policies" target="_blank" rel="noopener noreferrer">a very useful (open source!) list that already gathers this kind of information</a>.</p>
<p>Further analysis of the resources linked in the list, as of June 2026, shows that most policies allow assisted use, but not &ldquo;AI as the contributor.&rdquo;</p>
<p>Commonly allowed uses include:</p>
<ul>
<li>drafting code,</li>
<li>generating tests,</li>
<li>improving docs,</li>
<li>debugging,</li>
<li>summarizing, or asking an LLM for help.</li>
</ul>
<p>All of that is usually acceptable as long as the human reviews and owns the result.</p>
<p>Typically banned practices include:</p>
<ul>
<li>fully AI-generated PRs with little human engagement</li>
<li>AI-generated &ldquo;good first issue&rdquo; work</li>
<li>AI as co-author</li>
<li>automated AI code reviews</li>
<li>unreviewed agentic output</li>
</ul>
<p>It is completely understandable that experienced developers and communities say &ldquo;no&rdquo; to submissions of low quality. That would not be sustainable. Maintainers already carry a lot of invisible work, and AI can easily multiply that work if contributors treat it as a shortcut instead of a tool.</p>
<p>What is very positive for the future of AI-enhanced work is that the general direction seems to be acceptance, as long as there is a human-in-the-loop.</p>
<p>AI-enhanced work, as long as a human was involved, is possible across a range of open source products: <a href="https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Apache Airflow</a>, <a href="https://datafusion.apache.org/contributor-guide/index.html#ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Apache DataFusion</a>, <a href="https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code" target="_blank" rel="noopener noreferrer">Arrow</a>, <a href="https://github.com/cloudnative-pg/governance/blob/main/AI_POLICY.md" target="_blank" rel="noopener noreferrer">CloudNativePG (CNPG)</a>, <a href="https://devguide.python.org/getting-started/ai-tools/index.html" target="_blank" rel="noopener noreferrer">CPython</a>, <a href="https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/submitting-patches/#ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Django</a>, <a href="https://firefox-source-docs.mozilla.org/contributing/ai-coding.html" target="_blank" rel="noopener noreferrer">Firefox</a>, <a href="https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines" target="_blank" rel="noopener noreferrer">Flutter</a>, <a href="https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md" target="_blank" rel="noopener noreferrer">Ghostty</a>, <a href="https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#ai-contribution-policy" target="_blank" rel="noopener noreferrer">Gitea</a>, <a href="https://github.com/Homebrew/brew/blob/main/CONTRIBUTING.md#artificial-intelligencelarge-language-model-aillm-usage" target="_blank" rel="noopener noreferrer">Homebrew</a>, <a href="https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance" target="_blank" rel="noopener noreferrer">Kubernetes</a>, <a href="https://kernel.org/doc/html/next/process/coding-assistants.html" target="_blank" rel="noopener noreferrer">Linux Kernel</a>, <a href="https://llvm.org/docs//AIToolPolicy.html" target="_blank" rel="noopener noreferrer">LLVM</a>, <a href="https://matplotlib.org/devdocs/devel/contribute.html#generative-ai" target="_blank" rel="noopener noreferrer">Matplotlib</a>, <a href="https://numpy.org/devdocs/dev/ai_policy.html" target="_blank" rel="noopener noreferrer">NumPy</a>, <a href="https://pandas.pydata.org/docs/dev/development/contributing.html#automated-contributions-policy" target="_blank" rel="noopener noreferrer">Pandas</a>, <a href="https://github.com/pytorch/pytorch/blob/main/CONTRIBUTING.md#ai-assisted-development" target="_blank" rel="noopener noreferrer">PyTorch</a>, <a href="https://scipy.github.io/devdocs/dev/conduct/ai_policy.html" target="_blank" rel="noopener noreferrer">SciPy</a>, <a href="https://docs.sympy.org/dev/contributing/ai-generated-code-policy.html" target="_blank" rel="noopener noreferrer">SymPy</a>, <a href="https://docs.wagtail.org/en/latest/contributing/general_guidelines.html#general-coding-guidelines" target="_blank" rel="noopener noreferrer">Wagtail</a>, <a href="https://github.com/zulip/zulip/blob/main/CONTRIBUTING.md#ai-use-policy-and-guidelines" target="_blank" rel="noopener noreferrer">Zulip</a>, and <a href="https://github.com/zulip/zulip/blob/main/CONTRIBUTING.md#ai-use-policy-and-guidelines" target="_blank" rel="noopener noreferrer">others</a>.</p>
<p>What is interesting is that, at this moment, PostgreSQL does not have any official policy of this sort available.</p>
<h3 id="slonik-says-i-havent-noticed">Slonik says &ldquo;I haven&rsquo;t noticed&hellip;&rdquo;<a class="anchor-link" id="slonik-says-i-havent-noticed"></a></h3>
<p>While this may be a problem that does not directly touch PostgreSQL as a database server, it already has an impact on the PostgreSQL ecosystem, which consists of many other extensions and tools.</p>
<p>The reason may be quite trivial. Even with AI, the entry threshold for PostgreSQL core hacking is still higher than for many other tools. Hackers communicate through mailing lists, and even with the adoption of modern tools like Hackorum.dev, it is still not that easy to work with PostgreSQL compared with many other, more tempting projects.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/Jan-waterfall.png" alt="Beware of elephants drowning in AI slop"></figure>
</p>
<p>The issue, as I often see it for PostgreSQL, is that there is not much leadership for the wider ecosystem from the core project. Availability of responsible AI usage policies for the ecosystem could make maintainers&rsquo; lives easier. And let&rsquo;s be honest, for many smaller projects, creating such policies from scratch is a burden they could be spared.</p>
<p>Seems like any help would be appreciated.</p>
<h3 id="what-now-is-this-over-was-this-a-rant">What now? Is this over? Was this a rant?<a class="anchor-link" id="what-now-is-this-over-was-this-a-rant"></a></h3>
<p>I like to say, and repeat myself, that &ldquo;AI usage in open source is all about respect.&rdquo; To me, this is enough to say all that is needed. People need to communicate. This was meant as a start of the discussion.</p>
<p><a href="https://2026.pgconf.eu/" target="_blank" rel="noopener noreferrer">PGConf.EU</a> is coming in October, as well as many smaller meetups this year. There will be lots of space for hallway track discussions and hopefully some outcomes. Not to mention async communication channels. What I hope is that we can leverage all these channels to propose some solutions, experiment, and get better.</p>
<p>Let this be a call to action to help us all be more reasonable and more respectful of other people&rsquo;s time.</p>
<p>With this in mind,</p>
<details>
<summary>check out my original text before I refined it with AI if you want to see how it changed.</summary>
<p>Often we hear how open source is the people. The people who contribute their time, in a way their lives, to produce software available for everyone without limitations. Without licensing cost on the users.</p>
<p>The more a project becomes popular the higher chances we also hear about the need for sustainable open source from it. Nothing surprising here. At first we want others to notice the work we&rsquo;ve done. The more times pass and the work becomes appreciated, the higher chances that there will be a need to spend more time on the project.</p>
<p>When it graduates from a hobby project to software used by thousands of users or even a production founding block things become really interesting. Now we may hope to see new contributors joining the project. This would normally be a good thing but is it the same in the AI hype era where anyone can generate any content and claim it their own?</p>
<p>AI was supposed to be open source killer because it will become so easy to create our own software and not use open source. While this was the hype speaking, we notice another trend. It became way easier to propose patches, detect and report security threats or submit code reviews. Even without any developer experience or any coding capabilities.</p>
<h4 id="how-sustainable-for-the-human-maintainers-is-that">How sustainable for the human maintainers is that?</h4>
<p>It&rsquo;s easy to imagine that there is a very fine line between positively helpful and overwhelming. As with anything unwanted, the AI generated unwanted code or texts can be harmful to the many ope source projects. Especially those with a single maintainer treating their project as a spare time hobby and experiencing a waterfall of <a href="https://en.wikipedia.org/wiki/AI_slop" target="_blank" rel="noopener noreferrer">AI-slop</a>.</p>
<p><figure><img decoding="async" src="https://percona.community/blog/2026/06/Jan-ps3.png" alt="RPCS3 plead to vibe coders social media post"></figure>
</p>
<p>Playstation 3 emulator RPCS3 posted a plead to the vibe coders to stop the AI-generated abuse already and they are not alone in this problem.</p>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/Jan-fosdem-curl.png" alt="FOSDEM 2026 Daniel Stenberg presentation"></figure>
</p>
<p>As Daniel Stenberg from curl says (check out his talk during FOSDEM 2026) &ldquo;AI gives us the worst and the best &ndash; simultaneously&rdquo;</p>
<p>Seeing that <a href="https://curl.se/" target="_blank" rel="noopener noreferrer">curl</a> had to stop their bug bounty program (as discussed in the talk above) and even this was not enough and ended up in the &ldquo;<a href="https://daniel.haxx.se/blog/2026/06/15/curl-summer-of-bliss/" target="_blank" rel="noopener noreferrer">curl summer of bliss</a>&rdquo; where they will:</p>
<blockquote>
<p>not accept or otherwise handle any vulnerability reports during the month of July 2026.</p>
</blockquote>
<p>Security reports are an especially sensitive case. A wrong AI-generated vulnerability report is not harmless. Someone has to read it, reproduce it, evaluate it and decide whether it is real. Even when the issue is not there, the work is still very real. Like it or not but when it&rsquo;s unfounded work that proves a ai-generated false it is abusive.</p>
<p>Knowing that some projects adopt policies, I searched (using AI obviously &#128578;) for examples of such policies and stumbled upon <a href="https://github.com/melissawm/open-source-ai-contribution-policies" target="_blank" rel="noopener noreferrer">a very useful list that gathers such information already</a>.</p>
<p>Further analysis of the resources linked in the list (state in June 2026) shows that:</p>
<ul>
<li>Most policies allow assisted use, not &ldquo;AI as the contributor.&rdquo;</li>
<li>Commonly allowed uses include drafting code, generating tests, improving docs, debugging, summarizing, or asking an LLM for help, as long as the human reviews and owns the result.</li>
<li>Typically banned practices include:
<ul>
<li>Fully AI-generated PRs with little human engagement</li>
<li>AI-generated &ldquo;good first issue&rdquo; work</li>
<li>AI as co-author</li>
<li>Automated AI code reviews</li>
<li>Unreviewed agentic output</li>
</ul>
</li>
</ul>
<p>It&rsquo;s only understandable that experienced developers and Communities say &ldquo;no&rdquo; to low quality submissions. That would not be sustainable. What is very positive for the future of AI enhanced work is that the repo shows general acceptance as long as there is a &ldquo;human in the loop&rdquo;. AI enhanced work as long as human was involved is possible across a range of open source products: <a href="https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Apache Airflow</a>, <a href="https://datafusion.apache.org/contributor-guide/index.html#ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Apache DataFusion</a>, <a href="https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code" target="_blank" rel="noopener noreferrer">Arrow</a>, <a href="https://github.com/cloudnative-pg/governance/blob/main/AI_POLICY.md" target="_blank" rel="noopener noreferrer">CloudNativePG (CNPG)</a>, <a href="https://devguide.python.org/getting-started/ai-tools/index.html" target="_blank" rel="noopener noreferrer">CPython</a>, <a href="https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/submitting-patches/#ai-assisted-contributions" target="_blank" rel="noopener noreferrer">Django</a>, <a href="https://firefox-source-docs.mozilla.org/contributing/ai-coding.html" target="_blank" rel="noopener noreferrer">Firefox</a>, <a href="https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines" target="_blank" rel="noopener noreferrer">Flutter</a>, <a href="https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md" target="_blank" rel="noopener noreferrer">Ghostty</a>, <a href="https://github.com/go-gitea/gitea/blob/main/CONTRIBUTING.md#ai-contribution-policy" target="_blank" rel="noopener noreferrer">Gitea</a>, <a href="https://github.com/Homebrew/brew/blob/main/CONTRIBUTING.md#artificial-intelligencelarge-language-model-aillm-usage" target="_blank" rel="noopener noreferrer">Homebrew</a>, <a href="https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance" target="_blank" rel="noopener noreferrer">Kubernetes</a>, <a href="https://kernel.org/doc/html/next/process/coding-assistants.html" target="_blank" rel="noopener noreferrer">Linux Kernel</a>, <a href="https://llvm.org/docs//AIToolPolicy.html" target="_blank" rel="noopener noreferrer">LLVM</a>, <a href="https://matplotlib.org/devdocs/devel/contribute.html#generative-ai" target="_blank" rel="noopener noreferrer">Matplotlib</a>, <a href="https://numpy.org/devdocs/dev/ai_policy.html" target="_blank" rel="noopener noreferrer">NumPy</a>, <a href="https://pandas.pydata.org/docs/dev/development/contributing.html#automated-contributions-policy" target="_blank" rel="noopener noreferrer">Pandas</a>, <a href="https://github.com/pytorch/pytorch/blob/main/CONTRIBUTING.md#ai-assisted-development" target="_blank" rel="noopener noreferrer">PyTorch</a>, <a href="https://scipy.github.io/devdocs/dev/conduct/ai_policy.html" target="_blank" rel="noopener noreferrer">SciPy</a>, <a href="https://docs.sympy.org/dev/contributing/ai-generated-code-policy.html" target="_blank" rel="noopener noreferrer">SymPy</a>, <a href="https://docs.wagtail.org/en/latest/contributing/general_guidelines.html#general-coding-guidelines" target="_blank" rel="noopener noreferrer">Wagtail</a>, <a href="https://github.com/zulip/zulip/blob/main/CONTRIBUTING.md#ai-use-policy-and-guidelines" target="_blank" rel="noopener noreferrer">Zulip</a>, and <a href="https://github.com/zulip/zulip/blob/main/CONTRIBUTING.md#ai-use-policy-and-guidelines" target="_blank" rel="noopener noreferrer">others</a>.</p>
<h3 id="slonik-says-i-havent-noticed-1">Slonik says &ldquo;I haven&rsquo;t noticed&hellip;&rdquo;<a class="anchor-link" id="slonik-says-i-havent-noticed"></a></h3>
<p><figure>
<img decoding="async" src="https://percona.community/blog/2026/06/Jan-waterfall.png" alt="Beware of elephants drowning in AI slop"></figure>
</p>
<p>What is interesting that at this moment PostgreSQL does not have any official policy of this sort available. While this may be a problem that does not touch PostgreSQL as a database server it already has an impact on the PostgreSQL ecosystem consisting of many other extensions and tools. The reason may be quite trivial &ndash; even with AI the entry threshold for PostgreSQL core hacking is still higher than any other tool. Hackers communicate via mailing lists and even with adoption of modern tools like Hackorum.dev it&rsquo;s still not that easy to work with PostgreSQL comparing to a lot of other more tempting tools.</p>
<p>The issue as I often see it for PostgreSQL is that there is not much leadership for the ecosystem from the core. Availability of responsible AI usage policies for the Ecosystem could make the life of maintainers easier and let&rsquo;s be honest, for a lot of smaller projects that&rsquo;s a burden they could be spared. Seems like any help would be appreciated.</p>
<h4 id="what-now-is-this-over-was-this-a-rant-1">What now? Is this over? Was this a rant?</h4>
<p>I like to say, and repeat myself that &ldquo;AI usage in open source is all about the respect&rdquo;. To me this is enough to say all that&rsquo;s needed. People need to communicate. This was meant as a start of the discussion.</p>
<p>PGConf.EU is coming in October, as well as many smaller meetups this year. Lots of space for hallway track discussion and hopefully some outcomes. Not to mention async communication channels. What I hope is we can leverage all these channels to propose some solutions, experiment and get better.</p>
<p>Let it be a call to action to help us all be more reasonable and respectful to others time.</p>
<p>With this in mind check out my original text I refined with AI if you want to see how it changed. Because of course I polished it to some extent to ensure that the grammar and phrasing is cleaner and crispier &#128578;</p>
</details>
<p>Because of course I polished it a little to make sure the grammar and phrasing are cleaner and crispier &#128578;</p>

<p><a href="https://percona.community/blog/2026/06/26/why-postgresql-needs-an-ai-usage-policy/">Why PostgreSQL needs an AI usage policy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Passbolt renews its support for MariaDB Foundation</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/" />
      <id>https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/</id>
      <updated>2026-06-25T06:23:50+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that Passbolt has renewed its Silver sponsorship for another year, continuing its long-term support for the MariaDB open-source ecosystem. …<br />
Continue reading \"Passbolt renews its support for MariaDB Foundation\"<br />
The post Passbolt renews its support for MariaDB Foundation appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/">Passbolt renews its support for MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is pleased to announce that <a href="https://www.passbolt.com/">Passbolt</a> has renewed its <a href="https://mariadb.org/donate/#silver-tier-from-eur-5000-per-year">Silver sponsorship</a> for another year, continuing its long-term support for the MariaDB open-source ecosystem. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Passbolt renews its support for MariaDB Foundation&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/">Passbolt renews its support for MariaDB Foundation</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/passbolt-renews-its-support-for-mariadb-foundation/">Passbolt renews its support for MariaDB Foundation</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Aqtra Joins MariaDB Foundation as a Gold Sponsor</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/" />
      <id>https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/</id>
      <updated>2026-06-24T08:31:00+03:00</updated>
      <author><name>Anna Widenius</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Foundation is pleased to welcome Aqtra Platform as a new Gold Sponsor.<br />
Aqtra is a Development Infrastructure Layer (DIL) platform for building ERP solutions, business applications, internal and external portals, and workflows that connect multiple systems. …<br />
Continue reading \"Aqtra Joins MariaDB Foundation as a Gold Sponsor\"<br />
The post Aqtra Joins MariaDB Foundation as a Gold Sponsor appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/">Aqtra Joins MariaDB Foundation as a Gold Sponsor</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Foundation is pleased to welcome <a href="http://Aqtra%20Joins%20MariaDB%20Foundation%20as%20a%20Gold%20Sponsor%20MariaDB%20Foundation%20is%20pleased%20to%20welcome%20Aqtra%20as%20a%20new%20Gold%20Sponsor.%20Aqtra%20is%20a%20Development%20Infrastructure%20Layer%20(DIL)%20platform%20for%20building%20ERP%20solutions,%20business%20applications,%20internal%20and%20external%20portals,%20and%20workflows%20that%20connect%20multiple%20systems.%20The%20platform%20provides%20the%20underlying%20architecture,%20governance,%20integration,%20and%20runtime%20capabilities%20required%20to%20develop%20and%20operate%20business%20applications%20at%20scale,%20helping%20organisations%20automate%20complex%20processes%20without%20building%20and%20maintaining%20every%20application%20from%20scratch.%20As%20part%20of%20the%20next%20stage%20of%20its%20platform%20evolution,%20Aqtra%20has%20selected%20MariaDB%20Server%20as%20the%20strategic%20database%20foundation%20for%20its%20next-generation%20architecture.%20By%20joining%20MariaDB%20Foundation%20as%20a%20Gold%20Sponsor,%20Aqtra%20is%20strengthening%20its%20connection%20with%20the%20MariaDB%20ecosystem%20and%20creating%20a%20foundation%20for%20broader%20collaboration%20around%20MariaDB-powered%20business%20automation.%20Building%20Business%20Applications%20on%20MariaDB%20Businesses%20often%20depend%20on%20many%20different%20systems%20for%20finance,%20purchasing,%20inventory,%20sales,%20reporting,%20human%20resources,%20customer%20management,%20and%20internal%20approvals.%20Connecting%20these%20systems%20can%20become%20expensive%20and%20difficult%20to%20maintain.%20Even%20a%20relatively%20simple%20business%20process%20may%20require%20data%20to%20move%20between%20several%20applications,%20with%20custom%20integrations,%20manual%20steps,%20and%20separate%20user%20interfaces.%20Aqtra%20provides%20a%20unified%20platform%20for%20building%20applications%20and%20workflows%20around%20an%20organisation%E2%80%99s%20data,%20processes,%20and%20existing%20systems.%20These%20can%20include%20ERP%20applications,%20procurement%20and%20supplier%20portals,%20CRM%20systems,%20inventory%20management,%20reporting%20solutions,%20HR%20applications,%20help%20desks,%20customer%20portals,%20and%20other%20operational%20tools.%20MariaDB%20Server%20will%20serve%20as%20the%20open%20source%20relational%20database%20foundation%20beneath%20the%20Aqtra%20platform.%20For%20the%20end%20user,%20the%20database%20may%20remain%20largely%20invisible.%20They%20interact%20with%20the%20applications,%20portals,%20reports,%20and%20workflows%20that%20help%20them%20run%20their%20business.%20Underneath%20those%20applications,%20MariaDB%20provides%20the%20data%20layer%20required%20to%20store%20and%20manage%20business%20information,%20application%20configuration,%20workflow%20state,%20and%20operational%20data.%20Aqtra%E2%80%99s%20decision%20therefore%20represents%20an%20important%20form%20of%20MariaDB%20adoption:%20MariaDB%20becomes%20part%20of%20the%20platform%20architecture%20and,%20through%20it,%20part%20of%20the%20applications%20deployed%20for%20Aqtra%20customers.%20Rather%20than%20being%20adopted%20for%20a%20single%20application,%20MariaDB%20becomes%20part%20of%20the%20underlying%20infrastructure%20used%20to%20deliver%20entire%20business%20solution%20stacks.%20As%20Aqtra%20is%20deployed%20across%20customers,%20industries,%20and%20cloud%20environments,%20MariaDB%20becomes%20a%20foundational%20component%20of%20each%20resulting%20application%20ecosystem.%20From%20Cloud%20Infrastructure%20to%20Business%20Automation%20Aqtra%20is%20designed%20to%20run%20within%20infrastructure%20selected%20by%20the%20customer%20or%20its%20service%20provider.%20This%20creates%20an%20opportunity%20for%20cloud%20providers,%20hosting%20companies,%20managed%20service%20providers,%20and%20other%20infrastructure%20partners%20to%20offer%20more%20than%20computing,%20storage,%20and%20hosting.%20Through%20Aqtra,%20infrastructure%20providers%20can%20extend%20their%20role%20beyond%20infrastructure%20delivery%20and%20offer%20a%20complete%20business%20application%20platform%20built%20on%20open%20technologies,%20with%20MariaDB%20serving%20as%20the%20core%20data%20foundation.%20A%20MariaDB-powered%20Aqtra%20deployment%20can%20enable%20providers%20to%20offer:%20ERP%20and%20business%20applications%20hosted%20within%20the%20customer%E2%80%99s%20selected%20infrastructure%20Internal,%20customer,%20supplier,%20and%20partner%20portals%20Automation%20of%20workflows%20spanning%20multiple%20systems%20Greater%20control%20over%20data%20location%20and%20deployment%20architecture%20An%20open%20source%20database%20foundation%20for%20business-critical%20applications%20A%20platform%20that%20can%20expand%20as%20the%20customer%E2%80%99s%20requirements%20grow%20This%20model%20is%20particularly%20relevant%20for%20organisations%20that%20need%20greater%20control%20over%20their%20data%20and%20infrastructure,%20including%20companies%20operating%20in%20regulated%20industries,%20public-sector%20organisations,%20and%20businesses%20looking%20for%20alternatives%20to%20fragmented%20collections%20of%20SaaS%20products.%20Expanding%20the%20MariaDB%20Ecosystem%20The%20collaboration%20between%20Aqtra%20and%20MariaDB%20Foundation%20will%20build%20on%20Aqtra%E2%80%99s%20adoption%20of%20MariaDB%20Server%20as%20a%20core%20component%20of%20its%20Development%20Infrastructure%20Layer%20architecture.%20The%20two%20organisations%20will%20explore%20opportunities%20to%20document%20the%20resulting%20architecture,%20develop%20practical%20deployment%20and%20integration%20materials,%20and%20present%20Aqtra%20as%20part%20of%20the%20broader%20ecosystem%20of%20applications%20and%20platforms%20built%20on%20MariaDB.%20The%20partnership%20will%20also%20focus%20on%20helping%20cloud%20and%20infrastructure%20providers%20understand%20how%20MariaDB%20and%20Aqtra%20can%20work%20together%20as%20a%20complete%20data%20and%20application%20platform.%20Potential%20areas%20of%20collaboration%20include:%20Technical%20and%20architectural%20content%20Deployment%20guides%20and%20reference%20architectures%20MariaDB%20Ecosystem%20Hub%20visibility%20Joint%20webinars,%20presentations,%20and%20case%20studies%20Cloud%20and%20service-provider%20deployment%20models%20Business%20automation%20and%20ERP-focused%20Solution%20Stacks%20MariaDB-powered%20ERP%20and%20business%20automation%20reference%20architectures%20Solution%20stacks%20for%20cloud%20providers%20and%20managed%20service%20providers%20Through%20these%20activities,%20Aqtra%20will%20be%20able%20to%20share%20its%20MariaDB%20experience%20with%20users,%20developers,%20and%20infrastructure%20partners,%20while%20MariaDB%20Foundation%20gains%20an%20important%20new%20application-platform%20use%20case.%20MariaDB%20Adoption%20Through%20Application%20Platforms%20MariaDB%20adoption%20often%20happens%20beneath%20the%20applications%20users%20interact%20with%20every%20day.%20A%20company%20may%20never%20make%20a%20direct%20database%20selection%20for%20every%20business%20application%20it%20uses.%20Instead,%20it%20chooses%20a%20platform,%20service,%20or%20solution%20whose%20architecture%20already%20includes%20MariaDB.%20Application%20platforms%20such%20as%20Aqtra%20can%20therefore%20play%20an%20important%20role%20in%20growing%20the%20MariaDB%20ecosystem.%20By%20embedding%20MariaDB%20into%20a%20reusable%20application%20infrastructure%20layer,%20adoption%20can%20scale%20across%20many%20applications,%20customers,%20and%20deployment%20environments%20without%20requiring%20each%20organisation%20to%20independently%20standardise%20on%20a%20database%20platform.%20As%20the%20platform%20is%20deployed%20for%20new%20customers,%20industries,%20and%20infrastructure%20environments,%20MariaDB%20becomes%20part%20of%20each%20resulting%20application%20architecture.%20%E2%80%9CAqtra%E2%80%99s%20decision%20to%20build%20its%20next%20platform%20database%20layer%20on%20MariaDB%20demonstrates%20how%20MariaDB%20can%20serve%20as%20the%20dependable%20open%20source%20foundation%20beneath%20a%20broad%20range%20of%20business%20applications.%20Aqtra%20brings%20a%20distinctive%20combination%20of%20model-driven%20development,%20ERP%20functionality,%20portals,%20and%20cross-system%20workflow%20automation,%20while%20opening%20new%20opportunities%20with%20cloud%20and%20infrastructure%20providers.%20We%20are%20delighted%20to%20welcome%20Aqtra%20as%20a%20Gold%20Sponsor%20of%20MariaDB%20Foundation.%E2%80%9D%20Anna%20Widenius%20CEO,%20MariaDB%20Foundation%20%E2%80%9CAt%20Aqtra,%20we%20are%20building%20a%20Development%20Infrastructure%20Layer%20that%20enables%20organisations,%20cloud%20providers,%20and%20service%20partners%20to%20create%20and%20operate%20business%20applications,%20ERP%20solutions,%20portals,%20and%20automation%20services%20on%20a%20common%20foundation.%20MariaDB%20stood%20out%20as%20a%20mature,%20reliable,%20and%20truly%20open%20database%20platform%20that%20aligns%20with%20our%20long-term%20architectural%20vision.%20By%20joining%20MariaDB%20Foundation%20as%20a%20Gold%20Sponsor,%20we%20are%20not%20only%20adopting%20MariaDB%20as%20a%20strategic%20technology%20component%20but%20also%20supporting%20the%20ecosystem%20that%20helps%20organisations%20build%20and%20operate%20business-critical%20applications%20on%20open%20infrastructure.%E2%80%9D%20Ilia%20Kors%20CTO%20&amp;%20Co-Founder,%20Aqtra%20Supporting%20the%20Future%20of%20MariaDB%20Server%20Aqtra%E2%80%99s%20Gold%20sponsorship%20directly%20supports%20MariaDB%20Foundation%E2%80%99s%20work%20to%20advance%20MariaDB%20Server,%20facilitate%20technical%20collaboration,%20and%20ensure%20that%20MariaDB%20remains%20open,%20accessible,%20and%20dependable%20for%20organisations%20around%20the%20world.%20It%20also%20gives%20Aqtra%20a%20closer%20connection%20with%20the%20MariaDB%20community%20as%20the%20company%20develops%20its%20MariaDB-based%20architecture%20and%20expands%20the%20platform%20across%20new%20infrastructure%20environments.%20We%20warmly%20welcome%20Aqtra%20to%20the%20MariaDB%20Foundation%20sponsor%20community%20and%20look%20forward%20to%20working%20together%20to%20expand%20the%20role%20of%20MariaDB%20in%20ERP,%20business%20automation,%20and%20cross-system%20application%20development.%20Learn%20more%20about%20Aqtra:%20https://aqtra.io/%20Learn%20more%20about%20MariaDB%20Foundation%20sponsorship:%20https://mariadb.org/donate/">Aqtra</a> Platform as a new Gold Sponsor.<br>
Aqtra is a Development Infrastructure Layer (DIL) platform for building ERP solutions, business applications, internal and external portals, and workflows that connect multiple systems. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Aqtra Joins MariaDB Foundation as a Gold Sponsor&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/">Aqtra Joins MariaDB Foundation as a Gold Sponsor</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/aqtra-joins-mariadb-foundation-as-a-gold-sponsor/">Aqtra Joins MariaDB Foundation as a Gold Sponsor</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 Preview: This One Is Full of Community Goodies!</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/" />
      <id>https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/</id>
      <updated>2026-06-23T05:12:41+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>We just announced the availability of a preview of the MariaDB 13.1 series.<br />
MariaDB 13.1 is a rolling release preview, and, as usual, this is the right moment to test what is coming, give feedback, and help us polish the next MariaDB Server release. …<br />
Continue reading \"MariaDB 13.1 Preview: This One Is Full of Community Goodies!\"<br />
The post MariaDB 13.1 Preview: This One Is Full of Community Goodies! appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/">MariaDB 13.1 Preview: This One Is Full of Community Goodies!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We <a href="https://mariadb.org/mariadb-13-1-preview-available/">just announced the availability of a preview of the MariaDB 13.1</a> series.<br>
MariaDB 13.1 is a rolling release preview, and, as usual, this is the right moment to test what is coming, give feedback, and help us polish the next MariaDB Server release. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 Preview: This One Is Full of Community Goodies!&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/">MariaDB 13.1 Preview: This One Is Full of Community Goodies!</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-13-1-preview-this-one-is-full-of-community-goodies/">MariaDB 13.1 Preview: This One Is Full of Community Goodies!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Do not uselessly grant CREATE and ALTER TABLE</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/06/do-not-uselessly-grant-create-and-alter-table.html" />
      <id>https://jfg-mysql.blogspot.com/2026/06/do-not-uselessly-grant-create-and-alter-table.html</id>
      <updated>2026-06-20T22:52:52+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This lesson should have been learned with the CREATE TABLE of death, but it is worth a refresh.</p>
<p>Do not uselessly grant CREATE and ALTER TABLE</p>
<p>The reason I am posting this reminder is that another crashing bug related to DDL came to my attention.&#160; This bug is only fixed in a recent version of MySQL (probably not affecting 5.6 and 5.7), so if you are running the latest 8.0 or 8.4, you should</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/06/do-not-uselessly-grant-create-and-alter-table.html">Do not uselessly grant CREATE and ALTER TABLE</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This lesson should have been learned with the CREATE TABLE of death, but it is worth a refresh.</p>
<p>Do not uselessly grant CREATE and ALTER TABLE</p>
<p>The reason I am posting this reminder is that another crashing bug related to DDL came to my attention.&amp;nbsp; This bug is only fixed in a recent version of MySQL (probably not affecting 5.6 and 5.7), so if you are running the latest 8.0 or 8.4, you should</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/06/do-not-uselessly-grant-create-and-alter-table.html">Do not uselessly grant CREATE and ALTER TABLE</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 13.1 preview available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-13-1-preview-available/" />
      <id>https://mariadb.org/mariadb-13-1-preview-available/</id>
      <updated>2026-06-20T20:24:51+03:00</updated>
      <author><name>Sergei</name></author>
      <summary type="html"><![CDATA[<p>We are pleased to announce the availability of a preview of the MariaDB 13.1 series. MariaDB 13.1 will be a rolling release. …<br />
Continue reading \"MariaDB 13.1 preview available\"<br />
The post MariaDB 13.1 preview available appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-13-1-preview-available/">MariaDB 13.1 preview available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We are pleased to announce the availability of a preview of the <a href="https://mariadb.com/docs/release-notes/community-server/13.1/mariadb-13.1-changes-and-improvements" target="_blank" rel="noreferrer noopener">MariaDB 13.1</a> series. MariaDB 13.1 will be a <a href="https://mariadb.com/kb/en/mariadb-release-model/" target="_blank" rel="noreferrer noopener">rolling release</a>. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-13-1-preview-available/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB 13.1 preview available&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-13-1-preview-available/">MariaDB 13.1 preview available</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-13-1-preview-available/">MariaDB 13.1 preview available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>CPU-bound sysbench on a large server: Postgres 12 to 19 beta1</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/06/cpu-bound-sysbench-on-large-server.html" />
      <id>https://smalldatum.blogspot.com/2026/06/cpu-bound-sysbench-on-large-server.html</id>
      <updated>2026-06-20T00:18:58+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results from sysbench on a small server with Postgres versions 12 through 19 beta1. Sysbench is run with high concurrency (40 connections) and a cached database. The purpose is to search for changes in performance.Postgres remains boring, it is hard to find performance regressions.tl;dr for Postgres 17 to 19there are no regressionsthroughput on the read-only-count test improves by ~3X in 19 beta1 thanks to a better query plantl;dr for Postgres 12 to 19there are few regressions, throughput might have dropped by up to 5% on a few range query teststhere are a few large improvements for read-only teststhere are many large improvements for write-heavy testsBuilds, configuration and hardwareI compiled Postgres from source for versions 12.22, 13.23, 14.23, 15.18, 16.14, 17.10, 18.4 and 19 beta1.I used a 48-core server from Hetzneran ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4128G RAMUbuntu 24.04Configuration files for Postgres:the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions 12, 13, 14, 15, 16 and 17.for Postgres 18 and 19 I used conf.diff.cx10b_c32r128 (x10b_c32r128) which is as close as possible to the Postgres 17 config and uses io_method=syncBenchmarkI used sysbench and my usage is explained here. I now run 32 of the 42 microbenchmarks listed in that blog post. Most test only one type of SQL statement. Benchmarks are run with the database cached by Postgres.The read-heavy microbenchmarks are run for 600 seconds and the write-heavy for 1200 seconds. The benchmark is run with 40 clients and 8 tables with 10M rows per table. The database is cached.The purpose is to search for regressions from new CPU overhead and mutex contention. I use the small server with low concurrency to find regressions from new CPU overheads and then larger servers with high concurrency to find regressions from new CPU overheads and mutex contention.The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads -- both more and less synthetic.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide charts below with relative QPS (rQPS). The relative QPS is the following:(QPS for some version) / (QPS for base version)When the relative QPS is > 1 then some version is faster than base version.  When it is < 1 then there might be a regression. Values from iostat and vmstat divided by QPS are also provided here. These can help to explain why something is faster or slower because it shows how much HW is used per request.Here, base version is either Postgres 12.23 or 17.10 and some version is a more recent version. I use 12.23 as the base version to identify regressions over a long period of time. And then I use 17.10 as the base version to confirm there aren\'t recent, large regressions.I describe performance changes (changes to relative QPS) in terms of basis points. Performance changes by one basis point when the difference in rQPS is 0.01. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.Results: point queries, version 17 to 19Summary:there are no regressinsRelative to: PG 17.10col-1 : PG 18.4col-2 : PG 19 beta1col-1   col-21.00    0.99    hot-points1.01    1.01    point-query1.00    1.00    points-covered-pk0.98    0.99    points-covered-si1.01    1.00    points-notcovered-pk1.00    1.00    points-notcovered-si1.01    1.01    random-points_range=101.02    1.00    random-points_range=1001.00    1.00    random-points_range=1000Results: point queries, version 12 to 19Summarythere are no regressionsthroughput for the hot-points test improves by ~2X in versions 17.10, 18.4 and 19betaRelative to: PG 12.22col-1 : PG 13.23col-2 : PG 14.23col-3 : PG 15.18col-4 : PG 16.14col-5 : PG 17.10col-6 : PG 18.4col-7 : PG 19 beta1col-1   col-2   col-3   col-4   col-5   col-6   col-71.00    0.90    0.97    1.03    2.34    2.35    2.31    hot-points1.00    1.01    1.03    1.04    1.03    1.04    1.03    point-query1.02    1.04    1.04    1.07    1.04    1.04    1.04    points-covered-pk1.01    1.07    1.04    1.04    1.04    1.03    1.04    points-covered-si0.98    1.01    1.03    1.02    1.00    1.01    1.00    points-notcovered-pk0.99    1.03    1.03    1.01    1.02    1.02    1.01    points-notcovered-si0.99    1.01    1.03    1.03    1.00    1.01    1.01    random-points_range=100.99    1.02    1.04    1.04    1.01    1.03    1.01    random-points_range=1001.00    1.02    1.02    1.03    1.01    1.02    1.01    random-points_range=1000Results: range queries without aggregation, version 17 to 19Summarythere are no regressionswhile 19 beta1 has a better result on the scan test, that test has more variance with Postgres so I am reluctant to judge this without more resultsRelative to: PG 17.10col-1 : PG 18.4col-2 : PG 19 beta1col-1   col-20.98    0.99    range-covered-pk0.97    0.99    range-covered-si0.99    0.99    range-notcovered-pk1.02    1.01    range-notcovered-si0.96    1.07    scanResults: range queries without aggregation, version 12 to 19Summarythere are no regressionsscan throughput has improved a lot from version 12 to 19Relative to: PG 12.22col-1 : PG 13.23col-2 : PG 14.23col-3 : PG 15.18col-4 : PG 16.14col-5 : PG 17.10col-6 : PG 18.4col-7 : PG 19 beta1col-1   col-2   col-3   col-4   col-5   col-6   col-70.99    1.03    1.04    1.04    1.03    1.00    1.02    range-covered-pk0.99    1.04    1.04    1.04    1.03    1.00    1.03    range-covered-si1.00    1.00    1.00    0.99    1.00    0.99    0.99    range-notcovered-pk1.00    1.01    1.01    0.99    1.00    1.02    1.01    range-notcovered-si1.09    1.27    1.10    1.21    1.19    1.14    1.28    scanResults: range queries with aggregation, version 17 to 19Summarythere are no regressionsthroughput on the read-only-count test is ~3X better thanks to a new query plan. This improvement was also visible on my small serverRelative to: PG 17.10col-1 : PG 18.4col-2 : PG 19 beta1col-1   col-21.03    3.30    read-only-count1.02    0.99    read-only-distinct1.00    0.97    read-only-order0.99    0.99    read-only_range=100.99    0.99    read-only_range=1001.01    1.00    read-only_range=100001.03    1.01    read-only-simple1.03    1.01    read-only-sumResults: range queries with aggregation, version 12 to 19Summarythere might be a few small regressions, but losing 5% throughput from version 12 to 19 isn\'t a big dealthroughput on the read-only-count test is ~3X better thanks to a new query plan. This improvement was also visible on my small serverRelative to: PG 12.22col-1 : PG 13.23col-2 : PG 14.23col-3 : PG 15.18col-4 : PG 16.14col-5 : PG 17.10col-6 : PG 18.4col-7 : PG 19 beta1col-1   col-2   col-3   col-4   col-5   col-6   col-71.01    0.95    0.96    0.97    0.93    0.95    3.06    read-only-count1.00    0.98    0.98    0.98    0.96    0.98    0.95    read-only-distinct1.00    0.98    0.98    1.00    0.99    0.99    0.97    read-only-order0.99    1.00    1.01    1.00    1.01    0.99    1.00    read-only_range=100.99    1.00    1.00    1.00    1.01    1.00    0.99    read-only_range=1001.00    0.97    1.02    1.03    1.04    1.05    1.03    read-only_range=100001.00    0.97    0.99    0.97    0.95    0.98    0.96    read-only-simple1.00    0.96    0.97    0.97    0.94    0.97    0.95    read-only-sumResults: writes, version 17 to 19Summarythere are no regressionsRelative to: PG 17.10col-1 : PG 18.4col-2 : PG 19 beta1col-1   col-20.99    0.99    delete1.02    1.02    insert1.00    0.98    read-write_range=100.99    0.99    read-write_range=1001.01    1.03    update-index1.01    0.98    update-inlist0.98    1.01    update-nonindex1.01    1.03    update-one1.00    1.00    update-zipf0.97    0.99    write-onlyResults: writes, version 12 to 19Summarythere are no regressionsmany large improvements arrived in version 17 and remain in 19 beta1Relative to: PG 12.22col-1 : PG 13.23col-2 : PG 14.23col-3 : PG 15.18col-4 : PG 16.14col-5 : PG 17.10col-6 : PG 18.4col-7 : PG 19 beta1col-1   col-2   col-3   col-4   col-5   col-6   col-70.99    1.11    1.13    1.10    1.28    1.27    1.27    delete1.02    1.17    1.16    1.19    1.23    1.25    1.25    insert1.00    1.20    1.22    1.20    1.24    1.24    1.22    read-write_range=100.99    1.04    1.05    1.04    1.06    1.05    1.04    read-write_range=1000.98    1.08    1.05    0.94    1.84    1.85    1.90    update-index1.00    1.07    1.06    1.05    1.12    1.13    1.10    update-inlist1.01    1.07    1.07    0.86    1.87    1.84    1.88    update-nonindex1.04    0.96    0.96    1.10    1.39    1.41    1.43    update-one1.01    1.05    1.07    0.96    1.63    1.62    1.63    update-zipf0.99    1.11    1.13    1.09    1.41    1.37    1.40    write-only
</p>
<p><a href="https://smalldatum.blogspot.com/2026/06/cpu-bound-sysbench-on-large-server.html">CPU-bound sysbench on a large server: Postgres 12 to 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results from sysbench on a small server with Postgres versions 12 through 19 beta1. Sysbench is run with high concurrency (40 connections) and a cached database. The purpose is to search for changes in performance.</p>
<p>Postgres remains boring, it is hard to find performance regressions.</p>
<p>tl;dr for Postgres 17 to 19</p>

<ul style="text-align: left">
<li>there are no regressions</li>
<li>throughput on the read-only-count test improves by ~3X in 19 beta1 thanks to a better query plan</li>
</ul>
<div>
<p>tl;dr for Postgres 12 to 19</p>

<ul>
<li>there are few regressions, throughput might have dropped by up to 5% on a few range query tests</li>
<li>there are a few large improvements for read-only tests</li>
<li>there are many large improvements for write-heavy tests</li>
</ul>
</div>
<div>
<div>
<div>
<div style="background-color: white"><b>Builds, configuration and hardware</b></div>
<div style="background-color: white">
<div>I compiled Postgres from source for versions 12.22, 13.23, 14.23, 15.18, 16.14, 17.10, 18.4 and 19 beta1.</div>
<div></div>
<div><span style="font-family: inherit">I used a 48-core server from Hetzner</span></div>
<div>
<ul>
<li>an ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled</li>
<li>2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4</li>
<li>128G RAM</li>
<li>Ubuntu 24.04</li>
</ul>
<div>
<div><span style="font-family: inherit">Configuration files for Postgres:</span></div>
<div>
<ul>
<li><span style="font-family: inherit">the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions </span><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1219_o2nofp/conf.diff.cx10a_c32r128">12</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1315_o2nofp/conf.diff.cx10a_c32r128">13</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1412_o2nofp/conf.diff.cx10a_c32r128">14</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg157_o2nofp/conf.diff.cx10a_c32r128">15</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg163_o2nofp/conf.diff.cx10a_c32r128">16</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg17beta1_o2nofp/conf.diff.cx10a_c32r128">17</a>.</li>
<li>for Postgres 18 and 19 I used <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10b_c32r128" style="font-family: inherit">conf.diff.cx10b_c32r128</a><span style="font-family: inherit"> </span><span style="font-family: inherit">(x10b_c32r128) which is as close as possible to the Postgres 17 config and </span>uses io_method=sync</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div><b>Benchmark</b></div>
<div>
<div></div>
<div>I used sysbench and my usage is <a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. I now run 32 of the 42 microbenchmarks listed in that blog post. Most test only one type of SQL statement. Benchmarks are run with the database cached by Postgres.</div>
<div>The read-heavy microbenchmarks are run for 600 seconds and the write-heavy for 1200 seconds. The benchmark is run with 40 clients and 8 tables with 10M rows per table. The database is cached.</div>
</div>
</div>
<div></div>
<div>The purpose is to search for regressions from new CPU overhead and mutex contention. I use the small server with low concurrency to find regressions from new CPU overheads and then larger servers with high concurrency to find regressions from new CPU overheads and mutex contention.</div>
</div>
<div>
<div></div>
<div>The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads &mdash; both more and less synthetic.</div>
<div></div>
<div>
<div style="font-family: inherit"><b>Results</b></div>
<div><span>
<div style="font-family: inherit"></div>
<div style="font-family: inherit"><span style="font-family: inherit">The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div style="font-family: inherit">I provide charts below with relative QPS (rQPS). The relative QPS is the following:</div>
<div style="font-family: inherit">
<div></div>
<blockquote><p>(QPS for some version) / (QPS for base version)</p></blockquote>
</div>
<div><span style="font-family: inherit">When the relative QPS is &gt; 1 then </span><i style="font-family: inherit">some version</i><span style="font-family: inherit"> is faster than </span><i style="font-family: inherit">base version</i><span style="font-family: inherit">.&nbsp; When it is &lt; 1 then there might be a regression. </span><span><span style="font-family: inherit">Values from iostat and vmstat divided by QPS are also </span><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/aug25.sb.mem.in.1u.50m.600s.900s.pn53/o.met.all" style="font-family: inherit">provided here</a><span style="font-family: inherit">. These can help to explain why something is faster or slower because it shows how much HW is used per request.</span></span></div>
<div><span><br></span></div>
<div><span>Here, <i>base version</i> is either Postgres 12.23 or 17.10 and <i>some version</i> is a more recent version. I use 12.23 as the base version to identify regressions over a long period of time. And then I use 17.10 as the base version to confirm there aren&rsquo;t recent, large regressions.
<p><span>I describe performance changes (changes to relative QPS) in terms of basis points. Performance changes by one </span><i style="font-family: inherit"><b>basis point</b></i><span> when the difference in rQPS is 0.01. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.</span></p></span></div>
<div><span><span><br></span></span></div>
<div><b>Results: point queries, version 17 to 19</b></div>
<div><span><span><br></span></span></div>
<div><span><span>Summary:</span></span></div>
<div>
<ul style="text-align: left">
<li><span><span>there are no regressins</span></span></li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 17.10</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 18.4</div>
<div>col-2 : PG 19 beta1</div>
<div></div>
<div>
<div>col-1&nbsp; &nbsp;col-2</div>
<div>1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; hot-points</div>
<div>1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; point-query</div>
<div>1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; points-covered-pk</div>
<div>0.98&nbsp; &nbsp; 0.99&nbsp; &nbsp; points-covered-si</div>
<div>1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; points-notcovered-pk</div>
<div>1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; points-notcovered-si</div>
<div>1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=10</div>
<div>1.02&nbsp; &nbsp; 1.00&nbsp; &nbsp; random-points_range=100</div>
<div>1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; random-points_range=1000</div>
</div>
<p></p></span></span></div>
<div><span><span><br></span></span></div>
<div><span><span><b>Results: point queries, version 12 to 19</b></span></span></div>
<div><span><span><b><br></b></span></span></div>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there are no regressions</li>
<li>throughput for the hot-points test improves by ~2X in versions 17.10, 18.4 and 19beta</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 12.22</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 13.23</div>
<div>col-2 : PG 14.23</div>
<div>col-3 : PG 15.18</div>
<div>col-4 : PG 16.14</div>
<div>col-5 : PG 17.10</div>
<div>col-6 : PG 18.4</div>
<div>col-7 : PG 19 beta1</div>
<p></p></span></span></div>
<div><span><span style="font-family: courier;font-size: x-small"><br></span></span></div>
<div><span><span>
<div><span style="font-family: courier;font-size: x-small">col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7</span></div>
<div><span style="font-family: courier;font-size: x-small">1.00&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.97&nbsp; &nbsp; 1.03&nbsp; &nbsp; <span style="background-color: #d9ead3">2.34</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">2.35</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">2.31</span>&nbsp; &nbsp; hot-points</span></div>
<div><span style="font-family: courier;font-size: x-small">1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; point-query</span></div>
<div><span style="font-family: courier;font-size: x-small">1.02&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; points-covered-pk</span></div>
<div><span style="font-family: courier;font-size: x-small">1.01&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.04&nbsp; &nbsp; points-covered-si</span></div>
<div><span style="font-family: courier;font-size: x-small">0.98&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span style="font-family: courier;font-size: x-small">0.99&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.01&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span style="font-family: courier;font-size: x-small">0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=10</span></div>
<div><span style="font-family: courier;font-size: x-small">0.99&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=100</span></div>
<div><span style="font-family: courier;font-size: x-small">1.00&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=1000</span></div>
<p></p></span></span></div>
<p></p></span></div>
</div>
<div><br style="background-color: white"></div>
</div>
<div>
<div><span><b>Results: range queries without aggregation, version 17 to 19</b></span></div>
<div><span><br></span></div>
<div>
<div>Summary</div>
<div>
<ul>
<li>there are no regressions</li>
<li>while 19 beta1 has a better result on the scan test, that test has more variance with Postgres so I am reluctant to judge this without more results</li>
</ul>
</div>
</div>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 17.10</span></div>
<div><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 18.4</div>
<div>col-2 : PG 19 beta1</div>
<p></p></span></div>
<div><span style="font-family: courier;font-size: x-small"><br></span></div>
<div><span>
<div>
<div><span style="font-family: courier;font-size: x-small">col-1&nbsp; &nbsp;col-2</span></div>
<div><span style="font-family: courier;font-size: x-small">0.98&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-covered-pk</span></div>
<div><span style="font-family: courier;font-size: x-small">0.97&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-covered-si</span></div>
<div><span style="font-family: courier;font-size: x-small">0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span style="font-family: courier;font-size: x-small">1.02&nbsp; &nbsp; 1.01&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span style="font-family: courier;font-size: x-small">0.96&nbsp; &nbsp; <span style="background-color: #d9ead3">1.07</span>&nbsp; &nbsp; scan</span></div>
</div>
<p></p></span></div>
<div><span>
<div><span><span><br></span></span></div>
<div><span><span><b>Results: range queries without aggregation, version 12 to 19</b></span></span></div>
<div><span><span><br></span></span></div>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there are no regressions</li>
<li>scan throughput has improved a lot from version 12 to 19</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 12.22</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 13.23</div>
<div>col-2 : PG 14.23</div>
<div>col-3 : PG 15.18</div>
<div>col-4 : PG 16.14</div>
<div>col-5 : PG 17.10</div>
<div>col-6 : PG 18.4</div>
<div>col-7 : PG 19 beta1</div>
<div></div>
<p></p></span></span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7</div>
<div>0.99&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.02&nbsp; &nbsp; range-covered-pk</div>
<div>0.99&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.03&nbsp; &nbsp; range-covered-si</div>
<div>1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-notcovered-pk</div>
<div>1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; 0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.01&nbsp; &nbsp; range-notcovered-si</div>
<div><span style="background-color: #d9ead3">1.09</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.27</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.10</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.21</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.19</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.14</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.28</span>&nbsp; &nbsp; scan</div>
<p></p></span></span></div>
<p></p></span></div>
<div><span>
<div></div>
<div><span><span><b>Results: range queries with aggregation, version 17 to 19</b></span></span></div>
<div><span><span><br></span></span></div>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there are no regressions</li>
<li>throughput on the read-only-count test is ~3X better thanks to a new query plan. This improvement <a href="https://smalldatum.blogspot.com/2026/06/postgres-19-beta1-vs-sysbench-on-small.html">was also visible</a> on my small server</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 17.10</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 18.4</div>
<div>col-2 : PG 19 beta1</div>
<div></div>
<div>
<div>col-1&nbsp; &nbsp;col-2</div>
<div>1.03&nbsp; &nbsp; <span style="background-color: #d9ead3">3.30</span>&nbsp; &nbsp; read-only-count</div>
<div>1.02&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-only-distinct</div>
<div>1.00&nbsp; &nbsp; 0.97&nbsp; &nbsp; read-only-order</div>
<div>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-only_range=10</div>
<div>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-only_range=100</div>
<div>1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only_range=10000</div>
<div>1.03&nbsp; &nbsp; 1.01&nbsp; &nbsp; read-only-simple</div>
<div>1.03&nbsp; &nbsp; 1.01&nbsp; &nbsp; read-only-sum</div>
</div>
<p></p></span></span></div>
<div><span><span>
<div><span><span><br></span></span></div>
<div><span><span><b>Results: range queries with aggregation, version 12 to 19</b></span></span></div>
<div><span><span><br></span></span></div>
<div><span><span>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there might be a few small regressions, but losing 5% throughput from version 12 to 19 isn&rsquo;t a big deal</li>
<li>throughput on the read-only-count test is ~3X better thanks to a new query plan. This improvement <a href="https://smalldatum.blogspot.com/2026/06/postgres-19-beta1-vs-sysbench-on-small.html">was also visible</a> on my small server</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 12.22</span></div>
<div><span style="font-family: courier;font-size: x-small">col-1 : PG 13.23</span></div>
<div><span style="font-family: courier;font-size: x-small">col-2 : PG 14.23</span></div>
<div><span style="font-family: courier;font-size: x-small">col-3 : PG 15.18</span></div>
<div><span style="font-family: courier;font-size: x-small">col-4 : PG 16.14</span></div>
<div><span style="font-family: courier;font-size: x-small">col-5 : PG 17.10</span></div>
<div><span style="font-family: courier;font-size: x-small">col-6 : PG 18.4</span></div>
<div><span style="font-family: courier;font-size: x-small">col-7 : PG 19 beta1</span></div>
<div><span style="font-family: courier;font-size: x-small"><br></span></div>
<p></p></span></span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7</div>
<div>1.01&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.95&nbsp; &nbsp; <span style="background-color: #d9ead3">3.06</span>&nbsp; &nbsp; read-only-count</div>
<div>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.98&nbsp; &nbsp; <span style="background-color: #fff2cc">0.95</span>&nbsp; &nbsp; read-only-distinct</div>
<div>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.97&nbsp; &nbsp; read-only-order</div>
<div>0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only_range=10</div>
<div>0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-only_range=100</div>
<div>1.00&nbsp; &nbsp; 0.97&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.03&nbsp; &nbsp; read-only_range=10000</div>
<div>1.00&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.98&nbsp; &nbsp; <span style="background-color: #fff2cc">0.96</span>&nbsp; &nbsp; read-only-simple</div>
<div>1.00&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.97&nbsp; &nbsp; <span style="background-color: #fff2cc">0.95</span>&nbsp; &nbsp; read-only-sum</div>
<p></p></span></span></div>
<p></p></span></span></div>
<div><span><span><br></span></span></div>
<div><span><span>
<div><span><span><b>Results: writes, version 17 to 19</b></span></span></div>
<div><span><span><br></span></span></div>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there are no regressions</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 17.10</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 18.4</div>
<div>col-2 : PG 19 beta1</div>
<div></div>
<div>
<div>col-1&nbsp; &nbsp;col-2</div>
<div>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; delete</div>
<div>1.02&nbsp; &nbsp; 1.02&nbsp; &nbsp; insert</div>
<div>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; read-write_range=10</div>
<div>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-write_range=100</div>
<div>1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; update-index</div>
<div>1.01&nbsp; &nbsp; 0.98&nbsp; &nbsp; update-inlist</div>
<div>0.98&nbsp; &nbsp; 1.01&nbsp; &nbsp; update-nonindex</div>
<div>1.01&nbsp; &nbsp; 1.03&nbsp; &nbsp; update-one</div>
<div>1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; update-zipf</div>
<div>0.97&nbsp; &nbsp; 0.99&nbsp; &nbsp; write-only</div>
</div>
<p></p></span></span></div>
<div><span><span>
<div><span><span><br></span></span></div>
<div><span><span><b>Results: writes, version 12 to 19</b></span></span></div>
<div><span><span><b><br></b></span></span></div>
<div>Summary</div>
<div>
<ul style="text-align: left">
<li>there are no regressions</li>
<li>many large improvements arrived in version 17 and remain in 19 beta1</li>
</ul>
</div>
<div><span style="font-family: courier;font-size: small">Relative to: PG 12.22</span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1 : PG 13.23</div>
<div>col-2 : PG 14.23</div>
<div>col-3 : PG 15.18</div>
<div>col-4 : PG 16.14</div>
<div>col-5 : PG 17.10</div>
<div>col-6 : PG 18.4</div>
<div>col-7 : PG 19 beta1</div>
<div></div>
<p></p></span></span></div>
<div><span><span style="font-family: courier;font-size: x-small">
<div>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7</div>
<div>0.99&nbsp; &nbsp; 1.11&nbsp; &nbsp; 1.13&nbsp; &nbsp; 1.10&nbsp; &nbsp; <span style="background-color: #d9ead3">1.28</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.27</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.27</span>&nbsp; &nbsp; delete</div>
<div>1.02&nbsp; &nbsp; 1.17&nbsp; &nbsp; 1.16&nbsp; &nbsp; 1.19&nbsp; &nbsp; <span style="background-color: #d9ead3">1.23</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.25</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.25</span>&nbsp; &nbsp; insert</div>
<div>1.00&nbsp; &nbsp; 1.20&nbsp; &nbsp; 1.22&nbsp; &nbsp; 1.20&nbsp; &nbsp; <span style="background-color: #d9ead3">1.24</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.24</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.22</span>&nbsp; &nbsp; read-write_range=10</div>
<div>0.99&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.04&nbsp; &nbsp; read-write_range=100</div>
<div>0.98&nbsp; &nbsp; 1.08&nbsp; &nbsp; 1.05&nbsp; &nbsp; 0.94&nbsp; &nbsp; <span style="background-color: #d9ead3">1.84</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.85</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.90</span>&nbsp; &nbsp; update-index</div>
<div>1.00&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.13&nbsp; &nbsp; 1.10&nbsp; &nbsp; update-inlist</div>
<div>1.01&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.07&nbsp; &nbsp; 0.86&nbsp; &nbsp; <span style="background-color: #d9ead3">1.87</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.84</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.88</span>&nbsp; &nbsp; update-nonindex</div>
<div>1.04&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 1.10&nbsp; &nbsp; <span style="background-color: #d9ead3">1.39</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.41</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.43</span>&nbsp; &nbsp; update-one</div>
<div>1.01&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.07&nbsp; &nbsp; 0.96&nbsp; &nbsp; <span style="background-color: #d9ead3">1.63</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.62</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.63</span>&nbsp; &nbsp; update-zipf</div>
<div>0.99&nbsp; &nbsp; 1.11&nbsp; &nbsp; 1.13&nbsp; &nbsp; 1.09&nbsp; &nbsp; <span style="background-color: #d9ead3">1.41</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.37</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.40</span>&nbsp; &nbsp; write-only</div>
<p></p></span></span></div>
<p></p></span></span></div>
<p></p></span></span></div>
<p></p></span></div>

<p><a href="https://smalldatum.blogspot.com/2026/06/cpu-bound-sysbench-on-large-server.html">CPU-bound sysbench on a large server: Postgres 12 to 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Simple tool to build MariaDB commits for performance-change analysis</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/" />
      <id>https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/</id>
      <updated>2026-06-18T21:10:43+03:00</updated>
      <author><name>Jonathan Miller</name></author>
      <summary type="html"><![CDATA[<p>Tracking down changes in database performance is one of the hardest parts of engineering, especially when the change is buried somewhere in a long commit history. …<br />
Continue reading \"Simple tool to build MariaDB commits for performance-change analysis\"<br />
The post Simple tool to build MariaDB commits for performance-change analysis appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/">Simple tool to build MariaDB commits for performance-change analysis</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Tracking down changes in database performance is one of the hardest parts of engineering, especially when the change is buried somewhere in a long commit history. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;Simple tool to build MariaDB commits for performance-change analysis&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/">Simple tool to build MariaDB commits for performance-change analysis</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/simple-tool-to-build-mariadb-commits-for-performance-change-analysis/">Simple tool to build MariaDB commits for performance-change analysis</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Vector in Laravel: insights on choosing an embedding model</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/" />
      <id>https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/</id>
      <updated>2026-06-18T06:18:15+03:00</updated>
      <author><name>Robert Silén</name></author>
      <summary type="html"><![CDATA[<p>laravel-mariadb-vector is an open-source project by Erik Ros, bringing MariaDB’s native vector search to Laravel’s Eloquent ORM. In his guest post, Erik shares how it works, and his insights about picking an embedding model. …<br />
Continue reading \"MariaDB Vector in Laravel: insights on choosing an embedding model\"<br />
The post MariaDB Vector in Laravel: insights on choosing an embedding model appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/">MariaDB Vector in Laravel: insights on choosing an embedding model</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://packagist.org/packages/devilsberg/laravel-mariadb-vector">laravel-mariadb-vector</a>&nbsp;is an open-source project by Erik Ros, bringing MariaDB&rsquo;s native vector search to Laravel&rsquo;s Eloquent ORM. In his guest post, Erik shares how it works, and his insights about picking an embedding model. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Vector in Laravel: insights on choosing an embedding model&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/">MariaDB Vector in Laravel: insights on choosing an embedding model</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-vector-in-laravel-insights-on-choosing-an-embedding-model/">MariaDB Vector in Laravel: insights on choosing an embedding model</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Security advisory: CVE-2026-9740 and CVE-2026-11933 in Percona Server for MongoDB</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/security-advisory-cve-2026-9740-and-cve-2026-11933-in-percona-server-for-mongodb/" />
      <id>https://www.percona.com/blog/security-advisory-cve-2026-9740-and-cve-2026-11933-in-percona-server-for-mongodb/</id>
      <updated>2026-06-17T14:24:02+03:00</updated>
      <author><name>Radoslaw Szulgo</name></author>
      <summary type="html"><![CDATA[<p>TL;DR: This advisory covers the two most important high-severity memory-safety vulnerabilities affecting MongoDB Community and our downstream Percona Server for MongoDB – CVE-2026-11933 and CVE-2026-9740. Both will be addressed in a single coordinated patch release, bundled with other recently revealed lower-scored CVE fixes: CVE-2026-9753, CVE-2026-9752, CVE-2026-9751, CVE-2026-9750, CVE-2026-9749, CVE-2026-9748, CVE-2026-9747, CVE-2026-9746, CVE-2026-9743, and CVE-2026-9741. Fixes land … Continued<br />
The post Security advisory: CVE-2026-9740 and CVE-2026-11933 in Percona Server for MongoDB appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/security-advisory-cve-2026-9740-and-cve-2026-11933-in-percona-server-for-mongodb/">Security advisory: CVE-2026-9740 and CVE-2026-11933 in Percona Server for MongoDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p data-sourcepos="7:1-7:352;461-812"><span style="font-weight: 400"><strong>TL;DR:</strong>&nbsp;This advisory covers the two most important high-severity memory-safety vulnerabilities affecting MongoDB Community and our downstream Percona Server for MongoDB &ndash; </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-11933"><i><span style="font-weight: 400">CVE-2026-11933</span></i></a><span style="font-weight: 400"> and </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9740"><i><span style="font-weight: 400">CVE-2026-9740</span></i></a><span style="font-weight: 400">. Both will be addressed in a single coordinated patch release, bundled with other recently revealed lower-scored CVE fixes: </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9753"><span style="font-weight: 400">CVE-2026-9753</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9752"><span style="font-weight: 400">CVE-2026-9752</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9751"><span style="font-weight: 400">CVE-2026-9751</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9750"><span style="font-weight: 400">CVE-2026-9750</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9749"><span style="font-weight: 400">CVE-2026-9749</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9748"><span style="font-weight: 400">CVE-2026-9748</span></a><span style="font-weight: 400">, </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9747"><span style="font-weight: 400">CVE-2026-9747</span></a><span style="font-weight: 400">, </span><a href="http://cve-2026-9746"><span style="font-weight: 400">CVE-2026-9746</span></a><span style="font-weight: 400">, </span><a href="http://cve-2026-9743"><span style="font-weight: 400">CVE-2026-9743</span></a><span style="font-weight: 400">, and </span><a href="https://www.cve.org/CVERecord?id=CVE-2026-9741"><span style="font-weight: 400">CVE-2026-9741</span></a><span style="font-weight: 400">.</span></p>
<p>Fixes land in Percona Server for MongoDB patch window starting next week. The first high-vulnerability issue has nothing between it and your <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> process except your firewall. The second has a configuration off-switch you can flip during a maintenance window. &nbsp;Read on to understand why, how, and what.</p>
<h2 data-sourcepos="11:1-11:58;915-972"><img loading="lazy" decoding="async" class="aligncenter wp-image-49971 size-large" src="https://www.percona.com/wp-content/uploads/2026/06/blog-hero-June-2026-1024x576.png" alt="" width="1024" height="576"><a class="anchor-link" id=""></a></h2>
<h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" data-sourcepos="11:1-11:58;915-972">CVE-2026-9740 &mdash; the one that does not need credentials<a class="anchor-link" id="cve-2026-9740-the-one-that-does-not-need-credentials"></a></h2>
<p>A stack overflow in the BSON validator, specifically in the BSONColumn interleaved-reference handling. The validator&rsquo;s depth tracking resets on mutual recursion between validation functions, so a sufficiently nested input exhausts the thread&rsquo;s stack before any explicit limit fires. The result: <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> crashes.</p>
<p>CVSS 8.7. High severity. The reason it lands in High instead of merely Medium is the prerequisite for exploitation &ndash; there is none.</p>
<p>The attacker needs network reachability to a <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> listener. No credentials, no prior session, and no application interaction. One crafted message over the wire and the process is down. Repeated crashes are trivially repeatable, so an attacker who can reach the port can keep the instance offline for as long as they keep that reachability. The urgency of this issue comes from the audience &ndash; everyone with a TCP route to your database.</p>
<p>Upstream tracking: <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://jira.mongodb.org/browse/SERVER-125063">SERVER-125063</a>. Affected versions are Percona Server for MongoDB 8.0 &le; 8.0.23-10 and PSMDB 7.0 &le; 7.0.34-19. The vulnerable BSONColumn code path was introduced in 7.0, so 6.0 and earlier are not in scope for this one.</p>
<h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" data-sourcepos="25:1-25:55;2365-2419">CVE-2026-11933 &mdash; the one that does need credentials and permissions to read<a class="anchor-link" id="cve-2026-11933-the-one-that-does-need-credentials-and-permissions-to-read"></a></h2>
<p><span style="font-weight: 400">The vulnerable code path is inside MongoDB Server&rsquo;s server-side JavaScript engine, specifically in the BSON-to-array conversion routine. When a BSON document is materialized as a JavaScript array for use inside a server-side script, the engine can reach a state where it accesses memory that has already been freed. An attacker who can submit input that flows into that conversion path can shape what happens at the point of access.</span></p>
<p><b>Server-side JavaScript is reachable from the following surfaces:</b></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">The </span><span style="font-weight: 400">$where</span><span style="font-weight: 400"> query operator (deprecated in 8.0).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The </span><span style="font-weight: 400">$function</span><span style="font-weight: 400"> aggregation expression (deprecated in 8.0).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The </span><span style="font-weight: 400">$accumulator</span><span style="font-weight: 400"> aggregation expression (deprecated in 8.0).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The </span><span style="font-weight: 400">mapReduce</span><span style="font-weight: 400"> command (deprecated since 5.0).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">JavaScript functions stored in </span><a href="http://system.js"><span style="font-weight: 400">system.js</span></a><span style="font-weight: 400">.</span></li>
</ol>
<p><span style="font-weight: 400">MongoDB logs a warning when you run deprecated functions.</span></p>
<p><b><br>
</b><b>Prerequisites for exploitation:</b></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">The attacker must be authenticated to MongoDB.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The attacker must hold any role that permits running queries or aggregations against a collection. The built-in </span><span style="font-weight: 400">read</span><span style="font-weight: 400"> role on a single database is sufficient.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Server-side JavaScript must be enabled on the </span><span style="font-weight: 400">mongod</span><span style="font-weight: 400"> instance. This is the default; many production deployments leave it enabled even when they do not use it.</span></li>
</ol>
<p>CVSS 8.8. High severity. Two demonstrated outcomes:</p>
<ul>
<li>Information disclosure (reading other content out of the <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> process memory) and</li>
<li>Denial of Service (crashing it).</li>
</ul>
<p>Upstream tracking: <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://jira.mongodb.org/browse/SERVER-128125">SERVER-128125</a>. Affected versions: every supported and End of Life Percona Server for MongoDB major from 4.4 through 8.0.</p>
<p data-sourcepos="31:1-31:160;3100-3259"><img loading="lazy" decoding="async" class="aligncenter wp-image-49973 size-full" src="https://www.percona.com/wp-content/uploads/2026/06/blog-11933-attack-flow.png" alt="" width="1600" height="860"></p>
<h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" data-sourcepos="43:1-43:37;4603-4639">The good news and bad news<a class="anchor-link" id="the-good-news-and-bad-news"></a></h2>
<p>CVE-2026-11933 has a configuration off-switch. If your application does not use server-side JavaScript &mdash; <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">$where</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">$function</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">$accumulator</code>, <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mapReduce</code>, or stored <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">system.js</code> functions &mdash; you can disable server-side JavaScript on the server, removing the attack surface entirely until you patch.</p>
<h3><b>How to check whether your applications use server-side JavaScript before disabling:</b><a class="anchor-link" id="how-to-check-whether-your-applications-use-server-side-javascript-before-disabling"></a></h3>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Enable MongoDB profiling at level 2 (all operations) on a representative mongod server for a representative time window. See details in </span><a href="https://www.mongodb.com/docs/manual/tutorial/manage-the-database-profiler/"><span style="font-weight: 400">Manage the database profiler</span></a><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Search the </span><span style="font-weight: 400">system.profile</span><span style="font-weight: 400"> collection for operations that include </span><span style="font-weight: 400">$where</span><span style="font-weight: 400">, </span><span style="font-weight: 400">$function</span><span style="font-weight: 400">, </span><span style="font-weight: 400">$accumulator</span><span style="font-weight: 400">, or </span><span style="font-weight: 400">mapReduce</span><span style="font-weight: 400">.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Inspect application code paths and stored aggregation pipelines for the same operators. Check </span><span style="font-weight: 400">system.js</span><span style="font-weight: 400"> in each database for stored functions.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">If any usage exists, treat disabling as not viable for those deployments and rely on patching plus the defense-in-depth controls below.</span></li>
</ol>
<h3><b>How to disable server-side JavaScript:</b><a class="anchor-link" id="how-to-disable-server-side-javascript"></a></h3>
<p>Add to your configuration file for <code>mongod</code>and <code>mongos</code>:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">security: 
  javascriptEnabled: false</pre>
<p>Or pass <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">--noscripting</code> on the command line. <span style="font-weight: 400">See the reference documentation for details about </span><a href="https://www.mongodb.com/docs/manual/reference/configuration-options/#mongodb-setting-security.javascriptEnabled"><span style="font-weight: 400">MongoDB Setting:&nbsp; security.javascriptEnabled</span></a><span style="font-weight: 400">.</span></p>
<p>After a restart, any operation that reaches for server-side JavaScript will return an error. That is the catch: if your application <em>does</em> use one of those operators, this is not a viable mitigation for you, and you have to wait for the patch. If you are not sure whether your application uses them, turn on the database profiler at level 2 on a representative replica for a window long enough to be representative, then grep the profile collection for the operator names. Several teams have done this exercise in the last forty-eight hours and learned the answer is <em>&ldquo;no, we don&rsquo;t actually use any of that.&rdquo;</em> The cost of disabling is then the cost of a <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> or <code>mongos</code> restart.</p>
<p data-sourcepos="58:1-58:168;5736-5903">That was good news. Now the bad news:&nbsp;CVE-2026-9740 has no equivalent off-switch. The BSON validator is core to every client message; it cannot be disabled. Patch and network controls are the only options.</p>
<h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" data-sourcepos="60:1-60:30;5905-5934">What is shipping, and when<a class="anchor-link" id="what-is-shipping-and-when"></a></h2>
<p>The fixes for both CVEs will land in a single coordinated patch release for each supported major:</p>
<ul class="[li_&amp;]:mb-0 [li_&amp;]:mt-1 [li_&amp;]:gap-1 [&amp;:not(:last-child)_ul]:pb-1 [&amp;:not(:last-child)_ol]:pb-1 list-disc flex flex-col gap-1 pl-8 mb-3" data-sourcepos="64:1-66:206;6035-6428">
<li><strong>Percona Server for MongoDB 7.0 series</strong> &mdash; fix targeted for&nbsp; <strong>June 23, 2026</strong>.</li>
<li><strong>Percona Server for MongoDB 8.0 series</strong> &mdash; fix targeted for&nbsp; <strong>June 25, 2026</strong>.</li>
<li><strong>Percona Server for MongoDB 6.0 series</strong> &mdash; fix targeted for&nbsp; <strong>June 25, 2026 </strong>(for CVE-2026-11933).</li>
</ul>
<p>All dates are targets, not commitments. Plan one upgrade window covering all CVEs.</p>
<p>Percona is&nbsp;<strong>not</strong> building binary packages for the 5.x line. We&rsquo;re being upfront about that &mdash; the calculus on extended support has a limit, and 5.x is past it for us. If you have a hard requirement on 5.x and the time pressure to meet it, the source is available for building. Percona customers on 5.x can open a ticket, and we&rsquo;ll work on the case individually.</p>
<p>As usual, you can download patches from your package manager or Percona&nbsp;<a href="https://www.percona.com/downloads/">Software Downloads</a>&nbsp;page.</p>
<p>On <strong>Kubernetes via the Percona Operator for MongoDB</strong>: same drill as usual. When the patched image is published, edit the image tag in your&nbsp;<code>PerconaServerMongoDB</code>&nbsp;custom resource and let the operator roll the cluster. Don&rsquo;t wait for the June operator release to do it for you. See details in our documentation on how to&nbsp;<a href="https://docs.percona.com/percona-operator-for-mongodb/update-db.html">Upgrade Percona Server for MongoDB</a>.&nbsp;You do not need to wait for an operator release to apply a security fix.</p>
<h2 class="text-text-100 mt-3 -mb-1 text-[1.125rem] font-bold" data-sourcepos="72:1-72:24;7030-7053">What to do this week<a class="anchor-link" id="what-to-do-this-week"></a></h2>
<p>In order of urgency, for most deployments:</p>
<ol class="[li_&amp;]:mb-0 [li_&amp;]:mt-1 [li_&amp;]:gap-1 [&amp;:not(:last-child)_ul]:pb-1 [&amp;:not(:last-child)_ol]:pb-1 list-decimal flex flex-col gap-1 pl-8 mb-3" data-sourcepos="76:1-79:226;7099-7810">
<li><strong>Confirm your <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> or <code>mongos</code> listeners are not reachable from any source you would not trust with a shell on the host.</strong> If you find an exposure, fix that first. CVE-2026-9740 turns any such exposure into a DoS primitive.</li>
<li><strong>For deployments that do not use server-side JavaScript, disable it.</strong> Full mitigation for CVE-2026-11933 within a single <code class="bg-text-200/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]">mongod</code> restart.</li>
<li><strong>Plan your upgrade window</strong> for the week the relevant fixed release lands. One window. Both CVEs. Plus, the others scored lower.</li>
<li><strong>Audit which roles in your deployment can run ad-hoc queries or aggregations.</strong> The bar for CVE-2026-11933 is the standard read role, so the population of potential attackers is larger than for most memory-safety defects.</li>
</ol>
<p>One closing point, because it has come up several times in customer conversations this week. For a deployment behind tight network controls, the post-authenticated bug is the more urgent one. For a deployment reachable from broader networks &mdash; public cloud, shared internal LANs, multi-tenant infrastructure &mdash; the pre-authenticated bug is. Triage by <em>your</em> exposure, not by <em>their</em> CVSS.</p>
<hr class="border-border-200 border-t-0.5 my-3 mx-1.5">
<p><em>Questions, or a deployment you&rsquo;re not sure how to triage? Find us on the <a class="underline underline underline-offset-2 decoration-1 decoration-current/40 hover:decoration-current focus:decoration-current" href="https://forums.percona.com/">Percona Forum</a>, or, for customers, in the support portal.</em></p>
<p><i><span style="font-weight: 400">Reviewed by Ivan Groenewold.</span></i><i><span style="font-weight: 400">&nbsp;Vetted for technical accuracy as of June 17, 2026.</span></i></p>
<p>The post <a href="https://www.percona.com/blog/security-advisory-cve-2026-9740-and-cve-2026-11933-in-percona-server-for-mongodb/">Security advisory: CVE-2026-9740 and CVE-2026-11933 in Percona Server for MongoDB</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/security-advisory-cve-2026-9740-and-cve-2026-11933-in-percona-server-for-mongodb/">Security advisory: CVE-2026-9740 and CVE-2026-11933 in Percona Server for MongoDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The insert benchmark on a small server, IO-bound workload : Postgres 19 beta1</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server-io.html" />
      <id>https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server-io.html</id>
      <updated>2026-06-17T01:29:04+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for Postgres versions 19 beta1, 18.4 and 17.10 with the Insert Benchmark on a small server using a cached and CPU-bound workload. I also used MySQL 8.4.8 to see where performance was different.Postgres continues to be boring in a good way. It is hard to find performance regressions. tl;drcreate index (the l.x step) is faster in Postgres 19beta1. A Postgres expert told me that the sort algorithm was changed to be more CPU efficientthe write heavy steps (l.i1, l.i2) are 15% and 9% faster in 19 beta1 vs Postgres 17.10the second write heavy step (l.i2) is more than 20X faster in MySQL 8.4.8 vs Postgres thanks to the CPU overhead from get_actual_variable_range. I have written about this before.Builds, configuration and hardwareI compiled Postgres from source using -O2 -fno-omit-frame-pointer for versions 19 beta1, 18.4 and 17.10.I compiled MySQL 8.4.8 from source as well.The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.For 17.10 the config file is named conf.diff.cx10a_c8r32 (cx10a) and is here.For Postgres 18 and 19 the config file is conf.diff.cx10b_c8r32 (cx10b) which is as similar as possible to the config for version 17.For MySQL 8.4.8 the config file is my.cnf.cz12a_c8r32.The BenchmarkThe benchmark is explained here and is run with 1 client.The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 3600 seconds each.The benchmark steps are:l.i0insert 800M rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts 4M rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and 1M rows are inserted and deleted per table.Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResultsThe performance summary with charts is here.This table lists relative QPS per benchmark step and relative QPS is:    (QPS for my version / QPS for Postgres 17.10)The background in the table cells is blue for big improvements and yellow for regressions. There are no regressions here. The improvements here for Postgres 19 beta1 are similar to what I reported for the cached workload.The index create (l.x) step is much faster in 19.10. I usually ignore results on this step but I am curious if something was done in 19.10 to improve index create. A Postgres expert told me that the sort algorithm for index create was changed in version 19 to be more CPU efficient.For the write-heavy steps (l.i1, l.i2):there are large improvements in 19 beta1 (15% and 9%). The CPU overhead is lower in 19 beta1 compared to 17.10 (see cpupq here).throughput for the l.i2 step is more than 20X larger for MySQL than for Postgres. From vmstat I see that the CPU overhead (cpupq here) is more than 10X larger with Postgres vs MySQL. From flamegraphs the problem is the CPU overhead in get_actual_variable_range. I have written about this before (see here). The Postgres query planner uses too much CPU skipping old versions to figure out selectivity for a query and there are too many old versions because Postgres doesn\'t collect them ASAP, vacuum takes time. The flamegraphs are in subdirectories here.For the range query steps (qr100, qr500, qr1000) throughput is ~3% less in 19 beta1 vs 17.10 and ~1% less in 18.4 vs 17.10. For 19 beta1 there is a small increase in CPU overhead (see cpupq here, here and here). I already have flamegraphs for MySQL 8.4.8 and Postgres 19 beta1, soon I will have them for Postgres 17.10 and 18.4 to try and explain this.dbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp1000PG 17.101.001.001.001.001.001.001.001.001.001.00PG 18.41.011.031.001.000.981.000.990.990.990.99PG 19 beta11.011.151.051.090.971.010.961.010.971.00MySQL 8.4.80.770.890.7621.620.611.070.660.930.850.84</p>
<p><a href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server-io.html">The insert benchmark on a small server, IO-bound workload : Postgres 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for Postgres versions 19 beta1, 18.4 and 17.10 with the <a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">Insert Benchmark</a> on a small server using a cached and CPU-bound workload. I also used MySQL 8.4.8 to see where performance was different.</p>

<p>Postgres continues to be boring in a good way. It is hard to find performance regressions.</p>
<p>&nbsp;tl;dr</p>

<ul style="text-align: left">
<li>create index (the l.x step) is faster in Postgres 19beta1. A Postgres expert told me that the sort algorithm was changed to be more CPU efficient</li>
<li>the write heavy steps (l.i1, l.i2) are 15% and 9% faster in 19 beta1 vs Postgres 17.10</li>
<li>the second write heavy step (l.i2) is more than 20X faster in MySQL 8.4.8 vs Postgres thanks to the CPU overhead from get_actual_variable_range. I have <a href="https://www.google.com/search?q=site%3Asmalldatum.blogspot.com+get_actual_variable_range">written about this</a> before.</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled Postgres from source using&nbsp;<i>-O2 -fno-omit-frame-pointer</i>&nbsp;for versions 19 beta1, 18.4 and 17.10.
<p>I compiled MySQL 8.4.8 from source as well.</p></div>
<div>The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.</div>
<div></div>
<div>For 17.10 the config file is named conf.diff.cx10a_c8r32 (cx10a) and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg172_o2nofp/conf.diff.cx10a_c8r32">is here</a>.</div>

<div>For Postgres 18 and 19&nbsp;the config file is&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_c8r32">conf.diff.cx10b_c8r32</a>&nbsp;(cx10b) which is as similar as possible to the config for version 17.</div>
</div>
<div></div>
<div>For MySQL 8.4.8 the config file is <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8406_rel_o2nofp/etc/my.cnf.cz12a_c8r32">my.cnf.cz12a_c8r32</a>.</div>
<div></div>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is <a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a> and is run with 1 client.</div>
<div></div>
<div>The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 3600 seconds each.</div>
<div></div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert 800M rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts 4M rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and 1M rows are inserted and deleted per table.</li>
<li>Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div><b>Results</b></div>
<div></div>
<div>The performance summary with charts <a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#summary">is here</a>.</div>
<div></div>
<div>This table lists relative QPS per benchmark step and relative QPS is:<br>&nbsp; &nbsp; (QPS for my version / QPS for Postgres 17.10)
<p>The background in the table cells is blue for big improvements and yellow for regressions. There are no regressions here.&nbsp;</p></div>
<div></div>
<div>The improvements here for Postgres 19 beta1 are similar to <a href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server.html">what I reported</a> for the cached workload.</div>
<div></div>
<div>The index create (l.x) step is much faster in 19.10. I usually ignore results on this step but I am curious if something was done in 19.10 to improve index create. A Postgres expert told me that the sort algorithm for index create was changed in version 19 to be more CPU efficient.</div>
<div></div>
<div>For the write-heavy steps (l.i1, l.i2):</div>
<div>
<ul style="text-align: left">
<li>there are large improvements in 19 beta1 (15% and 9%). The CPU overhead is lower in 19 beta1 compared to 17.10 (<a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#l.i1.metrics">see cpupq here</a>).</li>
<li>throughput for the l.i2 step is more than 20X larger for MySQL than for Postgres. From vmstat I see that the CPU overhead (<a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#l.i2.metrics">cpupq here</a>) is more than 10X larger with Postgres vs MySQL. From flamegraphs the problem is the CPU overhead in get_actual_variable_range. I have written about this before (<a href="https://www.google.com/search?q=site%3Asmalldatum.blogspot.com+get_actual_variable_range">see here</a>). The Postgres query planner uses too much CPU skipping old versions to figure out selectivity for a query and there are too many old versions because Postgres doesn&rsquo;t collect them ASAP, vacuum takes time. The flamegraphs are in <a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/jun26.pn53.ib.pg19b1.my848/io/svg.all">subdirectories here</a>.</li>
</ul>
</div>
<div>For the range query steps (qr100, qr500, qr1000) throughput is ~3% less in 19 beta1 vs 17.10 and ~1% less in 18.4 vs 17.10. For 19 beta1 there is a small increase in CPU overhead (see cpupq <a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#qr100.L1.metrics">here</a>, <a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#qr500.L3.metrics">here</a> and <a href="https://mdcallag.github.io/reports/jun26.ib.pn53.io.800m.5m.3600s.1u.pg.my/all.html#qr1000.L5.metrics">here</a>). I already have flamegraphs for MySQL 8.4.8 and Postgres 19 beta1, soon I will have them for Postgres 17.10 and 18.4 to try and explain this.</div>
<div></div>
<div>
<div>
<table border="1" cellpadding="8" style="color: black">
<tbody>
<tr>
<th><span style="font-size: x-small">dbms</span></th>
<th><span style="font-size: x-small">l.i0</span></th>
<th><span style="font-size: x-small">l.x</span></th>
<th><span style="font-size: x-small">l.i1</span></th>
<th><span style="font-size: x-small">l.i2</span></th>
<th><span style="font-size: x-small">qr100</span></th>
<th><span style="font-size: x-small">qp100</span></th>
<th><span style="font-size: x-small">qr500</span></th>
<th><span style="font-size: x-small">qp500</span></th>
<th><span style="font-size: x-small">qr1000</span></th>
<th><span style="font-size: x-small">qp1000</span></th>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">PG 17.10</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">PG 18.4</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.03</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.98</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">PG 19 beta1</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.15</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.05</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.09</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.97</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.96</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.97</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">MySQL 8.4.8</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.77</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.89</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.76</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">21.62</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.61</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.07</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.66</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.93</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.85</span></td>
<td id="clo" style="background-color: #ffdd81;text-align: right"><span style="font-size: x-small">0.84</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server-io.html">The insert benchmark on a small server, IO-bound workload : Postgres 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB R2DBC Connector 1.4.1 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-1-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-1-now-available/</id>
      <updated>2026-06-16T22:24:39+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/R2DBC 1.4.1 GA release. Download Now Release Notes MariaDB […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-1-now-available/">MariaDB R2DBC Connector 1.4.1 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/R2DBC 1.4.1 GA release. Download Now MariaDB Connector/R2DBC 1.4.1 is a Stable (GA) release. Notable items in this release include: See the Connector/R2DBC 1.4.1 release notes page for details and visit mariadb.com/downloads/connectors/connectors-data-access/r2dbc-connector/</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-1-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-r2dbc-connector-1-4-1-now-available/">MariaDB R2DBC Connector 1.4.1 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>High Performance Real-Time Analytics on MariaDB Cloud: MariaDB Exa Technical Preview</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/high-performance-real-time-analytics-on-mariadb-cloud-mariadb-exa-technical-preview/" />
      <id>https://mariadb.com/resources/blog/high-performance-real-time-analytics-on-mariadb-cloud-mariadb-exa-technical-preview/</id>
      <updated>2026-06-16T14:59:25+03:00</updated>
      <author><name>Allen Herrera</name></author>
      <summary type="html"><![CDATA[<p>We are excited to announce the technical preview of MariaDB Exa on MariaDB Cloud. This release brings high-performance Hybrid Transactional […]</p>
<p><a href="https://mariadb.com/resources/blog/high-performance-real-time-analytics-on-mariadb-cloud-mariadb-exa-technical-preview/">High Performance Real-Time Analytics on MariaDB Cloud: MariaDB Exa Technical Preview</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We are excited to announce the technical preview of MariaDB Exa on MariaDB Cloud. This release brings high-performance Hybrid Transactional and Analytical Processing (HTAP) directly into the MariaDB environment by integrating Exasol&rsquo;s massively parallel processing (MPP) engine. By removing the requirement for complex ETL pipelines, MariaDB Exa enables analytics on live transactional data at up to&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/high-performance-real-time-analytics-on-mariadb-cloud-mariadb-exa-technical-preview/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/high-performance-real-time-analytics-on-mariadb-cloud-mariadb-exa-technical-preview/">High Performance Real-Time Analytics on MariaDB Cloud: MariaDB Exa Technical Preview</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Server 10.6 Reaches End of Life on July 6th</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/" />
      <id>https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/</id>
      <updated>2026-06-16T12:26:27+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB Server 10.6 has been with us for a long time. It was the first MariaDB LTS release under the current release model, and it has served many users, distributions, applications, and production environments very well. …<br />
Continue reading \"MariaDB Server 10.6 Reaches End of Life on July 6th\"<br />
The post MariaDB Server 10.6 Reaches End of Life on July 6th appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/">MariaDB Server 10.6 Reaches End of Life on July 6th</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB Server 10.6 has been with us for a long time. It was the first MariaDB LTS release under the current release model, and it has served many users, distributions, applications, and production environments very well. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Server 10.6 Reaches End of Life on July 6th&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/">MariaDB Server 10.6 Reaches End of Life on July 6th</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-server-10-6-reaches-end-of-life-on-july-6th/">MariaDB Server 10.6 Reaches End of Life on July 6th</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Extending pt-archiver with a Partition-Aware Plug-in for Fast Retention Policy Enforcement</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/extending-pt-archiver-with-a-partition-aware-plug-in-for-fast-retention-policy-enforcement/" />
      <id>https://www.percona.com/blog/extending-pt-archiver-with-a-partition-aware-plug-in-for-fast-retention-policy-enforcement/</id>
      <updated>2026-06-16T11:31:51+03:00</updated>
      <author><name>Corrado Pandiani</name></author>
      <summary type="html"><![CDATA[<p>Managing data retention policies is one of the most common operational tasks in MySQL. Applications continuously generate transactional, audit, logging, telemetry, and event data. Over time, these tables can grow to billions of rows, causing: Larger backups Longer recovery times Reduced buffer pool efficiency Slower index maintenance Increased storage costs Degraded query performance To address … Continued<br />
The post Extending pt-archiver with a Partition-Aware Plug-in for Fast Retention Policy Enforcement appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/extending-pt-archiver-with-a-partition-aware-plug-in-for-fast-retention-policy-enforcement/">Extending pt-archiver with a Partition-Aware Plug-in for Fast Retention Policy Enforcement</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Managing data retention policies is one of the most common operational tasks in MySQL.</p>
<p>Applications continuously generate transactional, audit, logging, telemetry, and event data. Over time, these tables can grow to billions of rows, causing:</p>
<ul>
<li>Larger backups</li>
<li>Longer recovery times</li>
<li>Reduced buffer pool efficiency</li>
<li>Slower index maintenance</li>
<li>Increased storage costs</li>
<li>Degraded query performance</li>
</ul>
<p>To address these problems, organizations typically implement retention policies based on dates or timestamps. Examples include deleting events older than 90 days or purging session data older than 30 days and so forth. The deleted data can then eventually be archived somewhere else, like in another DBMS or on external files.</p>
<p>One of the most widely used tools for implementing these policies in MySQL ecosystems is pt-archiver, part of the Percona Toolkit.</p>
<p>This article provides a review of what pt-archiver is and how to use it, but in particular it focuses on the fact this tool is not partitioning aware, and this can make the deletion phase more costly. The article shows how to extend pt-archiver with a Perl plugin to make it aware of partitioning.</p>
<p>&nbsp;</p>
<h2>What is pt-archiver?<a class="anchor-link" id="what-is-pt-archiver"></a></h2>
<p>pt-archiver is a command-line utility from Percona Toolkit designed to:</p>
<ul>
<li>Archive rows from MySQL tables</li>
<li>Purge rows from MySQL tables</li>
<li>Move data between tables into the local database or a remote one</li>
<li>Export rows into files</li>
</ul>
<p>In a few words: implementing retention policies safely.</p>
<p>The tool processes rows incrementally in chunks, avoiding massive transactions and reducing impact on production systems.</p>
<p>Example:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=mydb,t=events 
&nbsp;&nbsp;--where "created_at &amp;lt; '2026-05-01'" 
&nbsp;&nbsp;--purge 
&nbsp;&nbsp;--limit 1000 
&nbsp;&nbsp;--commit-each</pre>
<p>This command:</p>
<ul>
<li>Scans rows matching the WHERE condition</li>
<li>Processes them in chunks of 1000 rows</li>
<li>Commits every chunk</li>
<li>Deletes matching rows from the source table</li>
</ul>
<p>pt-archiver provides several advantages compared to ad-hoc DELETE statements.</p>
<p>Instead of running:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">DELETE FROM events
WHERE created_at &amp;lt; '2026-05-01';</pre>
<p>which may:</p>
<ul>
<li>Lock rows for a long time</li>
<li>Generate massive undo/redo logs</li>
<li>Create replication lag</li>
<li>Exhaust transaction logs</li>
</ul>
<p>pt-archiver processes rows incrementally to make the process overhead less impactful for the database performance.</p>
<p>pt-archiver implementation permits flexible archival strategies</p>
<p>Rows can be copied to another table on a remote host, exported to files or removed completely</p>
<p>More details: <a href="https://docs.percona.com/percona-toolkit/pt-archiver.html#extending">ps://docs.percona.com/percona-toolkit/pt-archiver.html</a></p>
<h2><a class="anchor-link" id=""></a></h2>
<h3>Example: Copy rows to a remote archive table<a class="anchor-link" id="example-copy-rows-to-a-remote-archive-table"></a></h3>
<p>The following example archives rows older than 90 days from a local table into an archive table hosted on a remote MySQL server:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=sales,t=orders,u=archiver,p=secret 
&nbsp;&nbsp;--dest h=archive-server,D=archive,t=orders_archive,u=archiver,p=secret 
&nbsp;&nbsp;--where "created_at &amp;lt; '2026-05-01'" 
&nbsp;&nbsp;--limit 1000 
&nbsp;&nbsp;--commit-each 
&nbsp;&nbsp;--progress 10000 
&nbsp;&nbsp;--statistics</pre>
<p>In this example:</p>
<ul>
<li><span style="color: #339966">&ndash;source</span> defines the source table</li>
<li><span style="color: #339966">&ndash;dest</span> defines the remote archive destination</li>
<li><span style="color: #339966">&ndash;where</span> selects rows eligible for archival</li>
<li><span style="color: #339966">&ndash;limit</span> controls batch size</li>
<li><span style="color: #339966">&ndash;commit-each</span> commits every batch independently to reduce transaction overhead</li>
</ul>
<p>&ndash;<span style="color: #339966">-progress</span> reports progress every 10,000 rows</p>
<p>If rows should be removed from the source table after being copied, add <span style="color: #339966">&ndash;purge</span></p>
<h3>Example: Export rows to a file<a class="anchor-link" id="example-export-rows-to-a-file"></a></h3>
<p>The following example exports rows older than one year into a text file:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=sales,t=orders,u=archiver,p=secret 
&nbsp;&nbsp;--where "created_at &amp;lt; NOW() - INTERVAL 1 YEAR" 
&nbsp;&nbsp;--file '/tmp/orders_archive_%Y-%m-%d.txt' 
&nbsp;&nbsp;--output-format csv 
&nbsp;&nbsp;--limit 1000 
&nbsp;&nbsp;--commit-each 
&nbsp;&nbsp;--progress 10000 
&nbsp;&nbsp;--statistics</pre>
<p>In this example:</p>
<ul>
<li><span style="color: #339966">&ndash;file</span> specifies the output file</li>
<li>&ndash;<span style="color: #339966">-output-format csv</span> exports rows in CSV format</li>
<li>Date placeholders in the filename are expanded automatically</li>
</ul>
<p>Rows can optionally be deleted from the source table by adding <span style="color: #339966">&ndash;purge</span></p>
<p>This allows pt-archiver to be used both for data retention and for offline archival workflows.</p>
<h1><a class="anchor-link" id=""></a></h1>
<h2>The Hidden Cost of DELETE Statements<a class="anchor-link" id="the-hidden-cost-of-delete-statements"></a></h2>
<p>Although pt-archiver is much safer than massive DELETE operations, it still fundamentally relies on DELETE statements.</p>
<p>This is a critical point.</p>
<p>Even when there are proper indexes, the rows are processed in chunks, and transactions are small; the large-scale DELETE operations remain expensive.</p>
<p>Deleting rows is expensive in InnoDB because it involves:</p>
<ul>
<li>Locating rows via indexes</li>
<li>Modifying clustered indexes</li>
<li>Modifying secondary indexes</li>
<li>Generating undo logs</li>
<li>Generating redo logs</li>
<li>Purge thread processing</li>
<li>Replication event generation</li>
<li>Page fragmentation</li>
</ul>
<p>When deleting billions of rows, the overhead becomes enormous.</p>
<p>Indexes help for sure, but only partially.</p>
<p>Consider:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">DELETE FROM events
WHERE created_at &amp;lt; '2024-01-01';</pre>
<p>If <span style="color: #339966">created_at</span> is indexed, MySQL can efficiently locate rows.</p>
<p>However, locating rows efficiently is only part of the cost. The actual delete operations still require all those things we mentioned above.</p>
<p>At considerable scale, this becomes expensive.</p>
<h1><a class="anchor-link" id=""></a></h1>
<h2>Why RANGE Partitioning is Superior for Retention Policies<a class="anchor-link" id="why-range-partitioning-is-superior-for-retention-policies"></a></h2>
<p>For time-based retention policies, partitioning is often dramatically more efficient. In particular, RANGE partitioning is very useful for these cases.</p>
<p>Example:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">CREATE TABLE events (
&nbsp;&nbsp;&nbsp;&nbsp;id BIGINT NOT NULL,
&nbsp;&nbsp;&nbsp;&nbsp;created_at DATETIME NOT NULL,
&nbsp;&nbsp;&nbsp;&nbsp;payload JSON,
&nbsp;&nbsp;&nbsp;&nbsp;PRIMARY KEY(id, created_at)
)

PARTITION BY RANGE (TO_DAYS(created_at)) (
&nbsp;&nbsp;&nbsp;&nbsp;PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
&nbsp;&nbsp;&nbsp;&nbsp;PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
&nbsp;&nbsp;&nbsp;&nbsp;PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01'))
);</pre>
<p>With partitioning, dropping old data becomes:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">ALTER TABLE events DROP PARTITION p202604;</pre>
<p>This operation is dramatically faster than running a DELETE.</p>
<p>Dropping a partition:</p>
<ul>
<li>Removes an entire physical partition</li>
<li>Avoids row-by-row DELETE</li>
<li>Avoids undo generation for each row</li>
<li>Avoids secondary index maintenance per row</li>
<li>Minimizes redo generation</li>
<li>Is nearly metadata-only</li>
</ul>
<p>This can remove millions or billions of rows in a matter of seconds without the same large cost of DELETE.</p>
<h1><a class="anchor-link" id=""></a></h1>
<h2>The Problem: pt-archiver is Not Partition-Aware<a class="anchor-link" id="the-problem-pt-archiver-is-not-partition-aware"></a></h2>
<p>Unfortunately, pt-archiver does not automatically understand partitioning strategies.</p>
<p>Even if the table is partitioned or the retention policy perfectly matches partition boundaries, pt-archiver still executes DELETE statements.</p>
<p>Example:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--where "created_at &amp;lt; NOW() - INTERVAL 90 DAY" 
&nbsp;&nbsp;--purge</pre>
<p>Internally, this still produces <strong><span style="color: #339966">DELETE &hellip;</span></strong> instead of <strong><span style="color: #339966">ALTER TABLE &hellip; DROP PARTITION &hellip;</span></strong></p>
<p>This means organizations may lose the major operational benefits of partitioning, or they need to implement custom scripts for managing the selection of rows to copy using pt-archiver and then use DROP PARTITION separately from the tool. That is doable, and to be honest, not too complicated, but why not make pt-archiver aware of partitioning for some specific use cases?</p>
<h1><a class="anchor-link" id=""></a></h1>
<h2>Extending pt-archiver with Pulg-ins<a class="anchor-link" id="extending-pt-archiver-with-pulg-ins"></a></h2>
<p>Fortunately, pt-archiver supports Perl plug-ins.</p>
<p>A plug-in can do plenty of things. Like: inspect runtime conditions, interact with MySQL, override behaviors, and execute custom logic</p>
<p>This gives us an opportunity to implement partition-aware retention handling.</p>
<p>The plug-in can:</p>
<ol>
<li>Inspect partition definitions</li>
<li>Analyze the WHERE condition</li>
<li>Determine which partitions are fully expired</li>
<li>Execute ALTER TABLE DROP PARTITION</li>
<li>Prevent row-by-row DELETE processing</li>
</ol>
<p>This approach combines the scheduling/orchestration power of pt-archiver with the efficiency of partition pruning.</p>
<h3>Plug-in Design<a class="anchor-link" id="plug-in-design"></a></h3>
<p>Our plug-in will:</p>
<ul>
<li>Connect using the pt-archiver DB handle</li>
<li>Inspect INFORMATION_SCHEMA.PARTITIONS</li>
<li>Identify partitions older than the retention cutoff</li>
<li>Issue DROP PARTITION statements</li>
<li>Log actions</li>
<li>Skip DELETE processing</li>
</ul>
<p>Assumptions:</p>
<ul>
<li>The table is RANGE partitioned</li>
<li>Partitions are DATETIME based using the TO_DAYS() function to define ranges</li>
<li>Partition naming convention contains dates</li>
<li>Retention policy aligns with partition boundaries; if the plugin cannot determine a specific boundary, pt-archiver does nothing</li>
</ul>
<h1><a class="anchor-link" id=""></a></h1>
<h2>Full Perl Plug-in for pt-archiver<a class="anchor-link" id="full-perl-plug-in-for-pt-archiver"></a></h2>

<pre class="urvanov-syntax-highlighter-plain-tag">package pt_archiver_partition_drop;

use strict;
use warnings;

sub new {
&nbsp;&nbsp;&nbsp;&nbsp;my ($class, %args) = @_;
&nbsp;&nbsp;&nbsp;&nbsp;my $self = {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dbh&nbsp; &nbsp; &nbsp; &nbsp; =&amp;gt; $args{dbh},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;db &nbsp; &nbsp; &nbsp; &nbsp; =&amp;gt; $args{db},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tbl&nbsp; &nbsp; &nbsp; &nbsp; =&amp;gt; $args{tbl},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;statistics =&amp;gt; {},
&nbsp;&nbsp;&nbsp;&nbsp;};

&nbsp;&nbsp;&nbsp;&nbsp;bless $self, $class;
&nbsp;&nbsp;&nbsp;&nbsp;return $self;
}

sub statistics {
&nbsp;&nbsp;&nbsp;&nbsp;my ($self) = @_;
&nbsp;&nbsp;&nbsp;&nbsp;return $self-&amp;gt;{statistics};
}


sub before_begin {
&nbsp;&nbsp;&nbsp;&nbsp;my ($self) = @_;
 &nbsp;&nbsp;&nbsp;my $dbh = $self-&amp;gt;{dbh} or die "Missing dbh from pt-archivern";
&nbsp;&nbsp;&nbsp;&nbsp;my $db&nbsp; = $self-&amp;gt;{db}&nbsp; or die "Missing db from pt-archiver plugin argsn";
&nbsp;&nbsp;&nbsp;&nbsp;my $tbl = $self-&amp;gt;{tbl} or die "Missing tbl from pt-archiver plugin argsn";
&nbsp;&nbsp;&nbsp;&nbsp;my $where&nbsp; = _get_cmdline_option('where');
&nbsp;&nbsp;&nbsp;&nbsp;my $dryrun = $ENV{PT_PARTITION_DROP_DRY_RUN} ? 1 : 0;

&nbsp;&nbsp;&nbsp;&nbsp;die "Missing --where from original command linen" unless $where;

&nbsp;&nbsp;&nbsp;&nbsp;print "PLUGIN before_begin calledn";
&nbsp;&nbsp;&nbsp;&nbsp;print "DB=$db TABLE=$tbln";
&nbsp;&nbsp;&nbsp;&nbsp;print "WHERE=$wheren";
&nbsp;&nbsp;&nbsp;&nbsp;print "PLUGIN_DRY_RUN=$dryrunn";

&nbsp;&nbsp;&nbsp;&nbsp;my ($column, $cutoff_date) = _parse_where($where);

&nbsp;&nbsp;&nbsp;&nbsp;my $partitions = _get_partitions($dbh, $db, $tbl);

&nbsp;&nbsp;&nbsp;&nbsp;if (!@$partitions) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "Table `$db`.`$tbl` is not partitioned. Refusing DELETE.n";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(0);
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;my $partition_expr = $partitions-&amp;gt;[0]-&amp;gt;{expression};
&nbsp;&nbsp;&nbsp;&nbsp;die "Missing PARTITION_EXPRESSIONn"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless defined $partition_expr &amp;amp;&amp;amp; length $partition_expr;

&nbsp;&nbsp;&nbsp;&nbsp;print "Partition expression: $partition_exprn";

&nbsp;&nbsp;&nbsp;&nbsp;my $cutoff_value = _evaluate_cutoff(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$dbh,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$partition_expr,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$column,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$cutoff_date,
&nbsp;&nbsp;&nbsp;&nbsp;);

&nbsp;&nbsp;&nbsp;&nbsp;print "Cutoff date: $cutoff_daten";
&nbsp;&nbsp;&nbsp;&nbsp;print "Cutoff boundary value: $cutoff_valuen";

&nbsp;&nbsp;&nbsp;&nbsp;my $matched;

&nbsp;&nbsp;&nbsp;&nbsp;for my $p (@$partitions) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if !defined $p-&amp;gt;{description};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if uc($p-&amp;gt;{description}) eq 'MAXVALUE';

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($p-&amp;gt;{description} == $cutoff_value) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$matched = $p;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;last;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}


&nbsp;&nbsp;&nbsp;&nbsp;if (!$matched) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "No exact partition boundary matches cutoff $cutoff_value. Refusing DELETE.n";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(0);
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;print "Matched boundary partition: $matched-&amp;gt;{name}, position $matched-&amp;gt;{position}n";

&nbsp;&nbsp;&nbsp;&nbsp;my @drop;

&nbsp;&nbsp;&nbsp;&nbsp;for my $p (@$partitions) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if !defined $p-&amp;gt;{description};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;next if uc($p-&amp;gt;{description}) eq 'MAXVALUE';

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($p-&amp;gt;{position} &amp;lt;= $matched-&amp;gt;{position}) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;push @drop, $p-&amp;gt;{name};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "Eligible for DROP: $p-&amp;gt;{name}, boundary $p-&amp;gt;{description}n";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;if (!@drop) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "No partitions eligible for DROP. Refusing DELETE.n";
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;exit(0);
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;my $sql = sprintf(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"ALTER TABLE %s.%s DROP PARTITION %s",
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_quote_ident($db),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_quote_ident($tbl),
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;join(", ", map { _quote_ident($_) } @drop),
&nbsp;&nbsp;&nbsp;&nbsp;);

&nbsp;&nbsp;&nbsp;&nbsp;print "SQL: $sqln";

&nbsp;&nbsp;&nbsp;&nbsp;if ($dryrun) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "PT_PARTITION_DROP_DRY_RUN enabled. Not executing DROP PARTITION.n";
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$dbh-&amp;gt;do($sql);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;print "Dropped partitions: " . join(", ", @drop) . "n";
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;$self-&amp;gt;{statistics}-&amp;gt;{partitions_dropped} = scalar @drop;

&nbsp;&nbsp;&nbsp;&nbsp;exit(0);
}


sub _parse_where {
&nbsp;&nbsp;&nbsp;&nbsp;my ($where) = @_;

&nbsp;&nbsp;&nbsp;&nbsp;$where =~ s/^s+|s+$//g;

&nbsp;&nbsp;&nbsp;&nbsp;die "Only WHERE format supported: created_at &amp;lt; 'YYYY-MM-DD'n"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless $where =~ /^`?([A-Za-z0-9_]+)`?s*&amp;lt;s*'(d{4}-d{2}-d{2})'s*$/;

&nbsp;&nbsp;&nbsp;&nbsp;return ($1, $2);
}

sub _evaluate_cutoff {
&nbsp;&nbsp;&nbsp;&nbsp;my ($dbh, $partition_expr, $column, $cutoff_date) = @_;

&nbsp;&nbsp;&nbsp;&nbsp;my $expr = $partition_expr;
&nbsp;&nbsp;&nbsp;&nbsp;$expr =~ s/`//g;

&nbsp;&nbsp;&nbsp;&nbsp;die "Partition expression does not reference column `$column`: $partition_exprn"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless $expr =~ /bQ$columnEb/i;

&nbsp;&nbsp;&nbsp;&nbsp;$expr =~ s/bQ$columnEb/'$cutoff_date'/ig;

&nbsp;&nbsp;&nbsp;&nbsp;die "Unsafe generated expression: $exprn"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless $expr =~ /^[A-Za-z0-9_s()+-*/,.'":]+$/;

&nbsp;&nbsp;&nbsp;&nbsp;my $sql = "SELECT $expr";

&nbsp;&nbsp;&nbsp;&nbsp;print "Boundary evaluation SQL: $sqln";

&nbsp;&nbsp;&nbsp;&nbsp;my ($value) = $dbh-&amp;gt;selectrow_array($sql);

&nbsp;&nbsp;&nbsp;&nbsp;die "Cannot evaluate cutoff expression: $sqln"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless defined $value;

&nbsp;&nbsp;&nbsp;&nbsp;return $value;
}

sub _get_partitions {
&nbsp;&nbsp;&nbsp;&nbsp;my ($dbh, $db, $tbl) = @_;

&nbsp;&nbsp;&nbsp;&nbsp;my $sql = q{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SELECT
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PARTITION_NAME,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PARTITION_DESCRIPTION,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PARTITION_EXPRESSION,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;PARTITION_ORDINAL_POSITION
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FROM INFORMATION_SCHEMA.PARTITIONS
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;WHERE TABLE_SCHEMA = ?
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AND TABLE_NAME = ?
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AND PARTITION_NAME IS NOT NULL
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ORDER BY PARTITION_ORDINAL_POSITION
&nbsp;&nbsp;&nbsp;&nbsp;};

&nbsp;&nbsp;&nbsp;&nbsp;my $sth = $dbh-&amp;gt;prepare($sql);
&nbsp;&nbsp;&nbsp;&nbsp;$sth-&amp;gt;execute($db, $tbl);
&nbsp;&nbsp;&nbsp;&nbsp;my @partitions;

&nbsp;&nbsp;&nbsp;&nbsp;while (my $row = $sth-&amp;gt;fetchrow_hashref()) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;push @partitions, {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;name&nbsp; &nbsp; &nbsp; &nbsp; =&amp;gt; $row-&amp;gt;{PARTITION_NAME},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;description =&amp;gt; $row-&amp;gt;{PARTITION_DESCRIPTION},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;expression&nbsp; =&amp;gt; $row-&amp;gt;{PARTITION_EXPRESSION},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;position&nbsp; &nbsp; =&amp;gt; $row-&amp;gt;{PARTITION_ORDINAL_POSITION},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;return @partitions;
}


sub _get_cmdline_option {

&nbsp;&nbsp;&nbsp;&nbsp;my ($name) = @_;

&nbsp;&nbsp;&nbsp;&nbsp;my $opt = "--$name";

&nbsp;&nbsp;&nbsp;&nbsp;for (my $i = 0; $i &amp;lt; @ARGV; $i++) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($ARGV[$i] eq $opt &amp;amp;&amp;amp; defined $ARGV[$i + 1]) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return $ARGV[$i + 1];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($ARGV[$i] =~ /^Q$optE=(.*)$/) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return $1;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;if (open my $fh, '&amp;lt;', "/proc/$$/cmdline") {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;local $/;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;my $raw = &amp;lt;$fh&amp;gt;;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;close $fh;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;my @cmd = split //, $raw;

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (my $i = 0; $i &amp;lt; @cmd; $i++) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($cmd[$i] eq $opt &amp;amp;&amp;amp; defined $cmd[$i + 1]) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return $cmd[$i + 1];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($cmd[$i] =~ /^Q$optE=(.*)$/) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return $1;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}

&nbsp;&nbsp;&nbsp;&nbsp;return undef;
}



sub _quote_ident {

&nbsp;&nbsp;&nbsp;&nbsp;my ($ident) = @_;

&nbsp;&nbsp;&nbsp;&nbsp;die "Invalid identifier: $identn"
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;unless defined $ident &amp;amp;&amp;amp; $ident =~ /^[A-Za-z0-9_]+$/;

&nbsp;&nbsp;&nbsp;&nbsp;return "`$ident`";
}

1;</pre>
<p>Create the file named&nbsp; <b>pt_archiver_partition_drop.pm</b> into the <b>/usr/local/share/perl5</b> path.</p>
<p>Also set the environment variable <b>PERL5LIB</b> to let pt-archiver where to find the Perl package</p>
<pre class="urvanov-syntax-highlighter-plain-tag">export PERL5LIB=/usr/local/share/perl5</pre>

<h1><a class="anchor-link" id=""></a></h1>
<h2>Example Usage<a class="anchor-link" id="example-usage"></a></h2>
<p>First, create the partitioned table events and insert some fake data.</p>
<pre class="urvanov-syntax-highlighter-plain-tag">DROP TABLE IF EXISTS events;


CREATE TABLE events (
&nbsp;&nbsp;id BIGINT NOT NULL,
&nbsp;&nbsp;created_at DATETIME NOT NULL,
&nbsp;&nbsp;payload JSON DEFAULT NULL,
&nbsp;&nbsp;PRIMARY KEY (id, created_at)
)
PARTITION BY RANGE (TO_DAYS(created_at)) (
&nbsp;&nbsp;PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
&nbsp;&nbsp;PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
&nbsp;&nbsp;PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
&nbsp;&nbsp;PARTITION pmax VALUES LESS THAN MAXVALUE
);

INSERT INTO events (id, created_at, payload) VALUES

-- p202604
(1,&nbsp; '2026-04-01 08:00:00', JSON_OBJECT('event', 'login',&nbsp; &nbsp; 'user', 'alice')),
(2,&nbsp; '2026-04-03 09:15:00', JSON_OBJECT('event', 'view', &nbsp; &nbsp; 'page', 'home')),
(3,&nbsp; '2026-04-05 10:30:00', JSON_OBJECT('event', 'click',&nbsp; &nbsp; 'button', 'signup')),
(4,&nbsp; '2026-04-08 11:45:00', JSON_OBJECT('event', 'search', &nbsp; 'term', 'mysql')),
(5,&nbsp; '2026-04-10 12:00:00', JSON_OBJECT('event', 'purchase', 'amount', 100)),
(6,&nbsp; '2026-04-14 13:20:00', JSON_OBJECT('event', 'logout', &nbsp; 'user', 'alice')),
(7,&nbsp; '2026-04-18 14:35:00', JSON_OBJECT('event', 'download', 'file', 'report.pdf')),
(8,&nbsp; '2026-04-22 15:50:00', JSON_OBJECT('event', 'upload', &nbsp; 'file', 'image.png')),
(9,&nbsp; '2026-04-26 16:05:00', JSON_OBJECT('event', 'click',&nbsp; &nbsp; 'button', 'buy')),
(10, '2026-04-30 23:59:59', JSON_OBJECT('event', 'month_end')),

-- p202605

(11, '2026-05-01 00:00:00', JSON_OBJECT('event', 'login',&nbsp; &nbsp; 'user', 'bob')),
(12, '2026-05-03 08:10:00', JSON_OBJECT('event', 'view', &nbsp; &nbsp; 'page', 'pricing')),
(13, '2026-05-06 09:20:00', JSON_OBJECT('event', 'search', &nbsp; 'term', 'percona')),
(14, '2026-05-09 10:30:00', JSON_OBJECT('event', 'purchase', 'amount', 250)),
(15, '2026-05-12 11:40:00', JSON_OBJECT('event', 'logout', &nbsp; 'user', 'bob')),
(16, '2026-05-16 12:50:00', JSON_OBJECT('event', 'download', 'file', 'backup.sql')),
(17, '2026-05-20 13:00:00', JSON_OBJECT('event', 'upload', &nbsp; 'file', 'data.csv')),
(18, '2026-05-24 14:10:00', JSON_OBJECT('event', 'click',&nbsp; &nbsp; 'button', 'subscribe')),
(19, '2026-05-28 15:20:00', JSON_OBJECT('event', 'view', &nbsp; &nbsp; 'page', 'docs')),
(20, '2026-05-31 23:59:59', JSON_OBJECT('event', 'month_end')),

-- p202606

(21, '2026-06-01 00:00:00', JSON_OBJECT('event', 'login',&nbsp; &nbsp; 'user', 'carol')),
(22, '2026-06-03 08:05:00', JSON_OBJECT('event', 'search', &nbsp; 'term', 'partitioning')),
(23, '2026-06-06 09:15:00', JSON_OBJECT('event', 'view', &nbsp; &nbsp; 'page', 'dashboard')),
(24, '2026-06-09 10:25:00', JSON_OBJECT('event', 'purchase', 'amount', 500)),
(25, '2026-06-12 11:35:00', JSON_OBJECT('event', 'logout', &nbsp; 'user', 'carol')),
(26, '2026-06-16 12:45:00', JSON_OBJECT('event', 'login',&nbsp; &nbsp; 'user', 'dave')),
(27, '2026-06-20 13:55:00', JSON_OBJECT('event', 'download', 'file', 'archive.zip')),
(28, '2026-06-24 14:05:00', JSON_OBJECT('event', 'upload', &nbsp; 'file', 'video.mp4')),
(29, '2026-06-28 15:15:00', JSON_OBJECT('event', 'click',&nbsp; &nbsp; 'button', 'checkout')),
(30, '2026-06-30 23:59:59', JSON_OBJECT('event', 'month_end')),

-- pmax
(31, '2026-07-01 00:00:00', JSON_OBJECT('event', 'login',&nbsp; &nbsp; 'user', 'eve')),
(32, '2026-07-05 08:30:00', JSON_OBJECT('event', 'view', &nbsp; &nbsp; 'page', 'future')),
(33, '2026-07-10 09:45:00', JSON_OBJECT('event', 'search', &nbsp; 'term', 'maxvalue')),
(34, '2026-08-01 10:00:00', JSON_OBJECT('event', 'purchase', 'amount', 750)),
(35, '2026-09-01 11:15:00', JSON_OBJECT('event', 'retained_future'));</pre>
<p>&nbsp;</p>
<p>Now you can run the following command to delete all rows before the 1st of May, which, by the way, matches the entire first partition in the table.</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=mydb,t=events,m=pt_archiver_partition_drop 
&nbsp;&nbsp;--where "created_at &amp;lt; '2026-05-01'" 
&nbsp;&nbsp;--purge</pre>
<p>&nbsp;</p>
<p>Notice the Perl plugin must be indicated with the <b>m</b> option in the DSN string.</p>
<p>In practice:</p>
<ul>
<li>pt-archiver initializes</li>
<li>The plug-in runs</li>
<li>Partitions are dropped</li>
<li>No DELETE statements are executed</li>
</ul>
<p>Here is what you get from the execution of the above command:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">PLUGIN before_begin called
DB=mydb TABLE=events
WHERE=created_at &amp;lt; '2026-05-01'
PLUGIN_DRY_RUN=0
Partition expression: to_days(`created_at`)
Boundary evaluation SQL: SELECT to_days('2026-05-01')
Cutoff date: 2026-05-01
Cutoff boundary value: 740102
Matched boundary partition: p202604, position 1
Eligible for DROP: p202604, boundary 740102
SQL: ALTER TABLE `mydb`.`events` DROP PARTITION `p202604`
Dropped partitions: p202604</pre>
<p>You can simply verify the table has been managed correctly:</p>
<p><span style="color: #339966">SELECT * FROM mydb.events;</span></p>
<p><span style="color: #339966">SHOW CREATE TABLE mydb.events;</span></p>
<p>&nbsp;</p>
<p>Now TRUNCATE the table and recreate the data and try now to specify the where conditions that match a RANGE that is not the first in the list of the boundaries.</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=mydb,t=events,m=pt_archiver_partition_drop 
&nbsp;&nbsp;--where "created_at &amp;lt; '2026-06-01'" 
&nbsp;&nbsp;--purge</pre>
<p>You should get:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">PLUGIN before_begin called
DB=mydb TABLE=events
WHERE=created_at &amp;lt; '2026-06-01'
PLUGIN_DRY_RUN=0
Partition expression: to_days(`created_at`)
Boundary evaluation SQL: SELECT to_days('2026-06-01')
Cutoff date: 2026-06-01
Cutoff boundary value: 740133
Matched boundary partition: p202605, position 2
Eligible for DROP: p202604, boundary 740102
Eligible for DROP: p202605, boundary 740133
SQL: ALTER TABLE `mydb`.`events` DROP PARTITION `p202604`, `p202605`
Dropped partitions: p202604, p202605</pre>
<p>In this case, two partitions have been identified and dropped.</p>
<p>&nbsp;</p>
<p>Truncate the table and recreate the data again. Try now to provide a WHERE condition that does not match any of the boundaries in the RANGE.</p>
<pre class="urvanov-syntax-highlighter-plain-tag">pt-archiver 
&nbsp;&nbsp;--source h=localhost,D=mydb,t=events,m=pt_archiver_partition_drop 
&nbsp;&nbsp;--where "created_at &amp;lt; '2026-04-25'" 
&nbsp;&nbsp;--purge</pre>
<p>&nbsp;</p>
<p>You get the following:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">PLUGIN before_begin called
DB=mydb TABLE=events
WHERE=created_at &amp;lt; '2026-04-25'
PLUGIN_DRY_RUN=0
Partition expression: to_days(`created_at`)
Boundary evaluation SQL: SELECT to_days('2026-04-25')
Cutoff date: 2026-04-25
Cutoff boundary value: 740096
No exact partition boundary matches cutoff 740096. Refusing DELETE.</pre>
<p>As expected, the tool now refuses to execute anything if it doesn&rsquo;t find an exact match.</p>
<p>&nbsp;</p>
<h2>Operational Benefits<a class="anchor-link" id="operational-benefits"></a></h2>
<p>This approach provides major advantages.</p>
<p>Dropping partitions is vastly faster than deleting rows, and minimal binary logging is needed, compared to billions of row deletes. There is no massive transactional overhead for managing undo logs and purging. You get then a better InnoDB Buffer Pool stability because of less page churn.</p>
<p>In the end, retention jobs are completed quickly and consistently in a predictable way and at the minimal cost.</p>
<p>&nbsp;</p>
<h2>Important Caveats<a class="anchor-link" id="important-caveats"></a></h2>
<h3>Partition Boundaries Must Match Retention Policy<a class="anchor-link" id="partition-boundaries-must-match-retention-policy"></a></h3>
<p>If partitions contain mixed retention windows, DROP PARTITION may remove too much data. For this reason, ensure correct partition design.</p>
<p>Recommended:</p>
<ul>
<li>daily partitions</li>
<li>weekly partitions</li>
<li>monthly partitions</li>
</ul>
<p>aligned with business retention requirements.</p>
<h3>Metadata Locks<a class="anchor-link" id="metadata-locks"></a></h3>
<p><span style="color: #339966">ALTER TABLE DROP PARTITION</span> still acquires metadata locks.</p>
<p>Test carefully in production.</p>
<h3>Backup Awareness<a class="anchor-link" id="backup-awareness"></a></h3>
<p>Ensure dropped partitions are no longer needed before removal or use pt-archiver to also copy the data into a remote server or dump the data into a CSV file before running the DROP PARTITION.</p>
<p>&nbsp;</p>
<h2>Possible Enhancements<a class="anchor-link" id="possible-enhancements"></a></h2>
<p>The plug-in can be extended further.</p>
<p>Potential improvements:</p>
<ul>
<li>Support for daily partitions</li>
<li>Support for UNIX timestamp partitions</li>
<li>Dry-run reporting</li>
<li>Automatic partition creation</li>
<li>Push Slack notifications</li>
<li>Export Prometheus metrics</li>
<li>Safety checks for replicas</li>
<li>GTID-aware orchestration</li>
<li>Integration with pt-online-schema-change workflows</li>
</ul>
<p>These are just some ideas I had meanwhile doing my tests. What you can do by implementing a Perl plugin is only limited by your imagination and your real needs.</p>
<h1><a class="anchor-link" id=""></a></h1>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>pt-archiver remains an excellent tool for implementing retention policies and archival workflows.</p>
<p>However, DELETE-based purging becomes increasingly expensive at scale, even with proper indexing and chunked processing.</p>
<p>For large time-series or historical datasets, RANGE partitioning is often a dramatically superior strategy.</p>
<p>The challenge is that pt-archiver does not natively leverage partition-level operations.</p>
<p>Fortunately, its Perl plug-in architecture allows advanced users to extend its behavior and implement partition-aware cleanup logic.</p>
<p>By combining:</p>
<ul>
<li>pt-archiver orchestration</li>
<li>MySQL RANGE partitioning</li>
<li>Custom Perl plug-ins</li>
</ul>
<p>Organizations can achieve:</p>
<ul>
<li>Faster retention enforcement</li>
<li>Lower operational overhead</li>
<li>Smaller replication impact</li>
<li>Dramatically improved scalability</li>
</ul>
<p>For large MySQL deployments, this hybrid approach can turn multi-hour purge operations into near-instant metadata operations.</p>
<p>The use case presented in this article is limited to a specific scenario, but you can reuse it or customize it if you have a different kind of RANGE partitioning, for example, not using TO_DAYS().</p>
<p>Take this as just an example of how you can extend pt-archiver. What you can do for real is driven by your needs and/or only limited by your imagination.</p>
<p>More info about extending pt-archiver:<br>
<a href="https://docs.percona.com/percona-toolkit/pt-archiver.html#extending">https://docs.percona.com/percona-toolkit/pt-archiver.html#extending</a></p>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/extending-pt-archiver-with-a-partition-aware-plug-in-for-fast-retention-policy-enforcement/">Extending pt-archiver with a Partition-Aware Plug-in for Fast Retention Policy Enforcement</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/extending-pt-archiver-with-a-partition-aware-plug-in-for-fast-retention-policy-enforcement/">Extending pt-archiver with a Partition-Aware Plug-in for Fast Retention Policy Enforcement</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Java Connector 3.5.9, 3.4.3, 3.3.5, and 2.7.14 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-9-3-4-3-3-3-5-and-2-7-14-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-java-connector-3-5-9-3-4-3-3-3-5-and-2-7-14-now-available/</id>
      <updated>2026-06-15T18:14:03+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/J 3.5.9, 3.4.3, 3.3.5, and 2.7.14 releases. Download Now […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-9-3-4-3-3-3-5-and-2-7-14-now-available/">MariaDB Java Connector 3.5.9, 3.4.3, 3.3.5, and 2.7.14 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/J 3.5.9, 3.4.3, 3.3.5, and 2.7.14 releases. Download Now Notable items in this release include: Notable items in this release include: Notable items in this release include: Notable items in this release include: See&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-9-3-4-3-3-3-5-and-2-7-14-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-java-connector-3-5-9-3-4-3-3-3-5-and-2-7-14-now-available/">MariaDB Java Connector 3.5.9, 3.4.3, 3.3.5, and 2.7.14 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Group Replication VS Percona XtraDB Cluster: The True Cost of Consistency</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/group-replication-vs-percona-xtradb-cluster-the-true-cost-of-consistency/" />
      <id>https://www.percona.com/blog/group-replication-vs-percona-xtradb-cluster-the-true-cost-of-consistency/</id>
      <updated>2026-06-15T06:58:17+03:00</updated>
      <author><name>Marco Tusa</name></author>
      <summary type="html"><![CDATA[<p>Overview When building high-availability MySQL environments, the choice between MySQL Group Replication (GR) and Percona XtraDB Cluster (PXC) often comes down to how they handle the eternal database dilemma: data consistency versus performance.        While both provide “synchronous-like” replication, they approach the problem of stale reads—reading data that has been committed on one node but not … Continued<br />
The post Group Replication VS Percona XtraDB Cluster: The True Cost of Consistency appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/group-replication-vs-percona-xtradb-cluster-the-true-cost-of-consistency/">Group Replication VS Percona XtraDB Cluster: The True Cost of Consistency</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2><span style="font-weight: 400">Overview</span><a class="anchor-link" id="overview"></a></h2>
<p><span style="font-weight: 400">When building high-availability MySQL environments, the choice between MySQL Group Replication (GR) and Percona XtraDB Cluster (PXC) often comes down to how they handle the eternal database dilemma: data consistency versus performance.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<img loading="lazy" decoding="async" class=" wp-image-49842 alignright" src="https://www.percona.com/wp-content/uploads/2026/06/dolphin_vs_goath_small.jpg" alt="" width="546" height="273"></span></p>
<p><span style="font-weight: 400">While both provide &ldquo;synchronous-like&rdquo; replication, they approach the problem of </span><b>stale reads</b><span style="font-weight: 400">&mdash;reading data that has been committed on one node but not yet applied on another&mdash;in distinct ways. Understanding these differences, and the performance penalties associated with fixing them, is critical for any production environment.</span></p>
<h3><span style="font-weight: 400">Technology Overviews</span><a class="anchor-link" id="technology-overviews"></a></h3>
<p><b>MySQL Group Replication (GR)</b></p>
<p><span style="font-weight: 400">Group Replication is the native, albeit more recent, high-availability solution built by Oracle for MySQL. It is based on a distributed state machine architecture and uses the Paxos consensus protocol.</span></p>
<ul>
<li style="font-weight: 400"><b>Mechanism:</b><span style="font-weight: 400"> When a transaction is committed, it is sent to all group members. The members must agree (consensus) on the order of transactions. Once a majority agrees, the transaction is &ldquo;certified&rdquo; and committed on the originator.</span></li>
<li style="font-weight: 400"><b>Replication Type:</b> <i><span style="font-weight: 400">Virtually synchronous.</span></i><span style="font-weight: 400"> The consensus ensures the data is received and ordered across nodes, but the actual applying of the data to the database happens asynchronously in the background.</span></li>
</ul>
<p><b>Percona XtraDB Cluster (PXC)</b></p>
<p><span style="font-weight: 400">PXC is an open-source enterprise solution based on Percona Server for MySQL and the Galera Replication library, which is the first and most mature virtually synchronous solution for MySQL.</span></p>
<ul>
<li style="font-weight: 400"><b>Mechanism:</b><span style="font-weight: 400"> When a node commits a transaction, it sends it to all other members of the Primary component (active group). All nodes must certify the transaction (check for conflicts), this is done on each node in the cluster, including the node that originates the write-set, before the originating node can finalize the commit.</span></li>
<li style="font-weight: 400"><b>Replication Type:</b> <i><span style="font-weight: 400">Strictly synchronous (up to the certification level)</span></i><span style="font-weight: 400">, asynchronous afterward. If the certification test fails, the node drops the write-set and the cluster rolls back the original transaction. If the test succeeds, however, the transaction commits and the write-set is applied to the rest of the cluster.</span></li>
</ul>
<h2><span style="font-weight: 400">The Battle Against &ldquo;Stale Reads&rdquo;: Why It Matters</span><a class="anchor-link" id="the-battle-against-stale-reads-why-it-matters"></a></h2>
<p><span style="font-weight: 400">The most critical distinction for developers is whether a SELECT query on </span><b>Node B</b><span style="font-weight: 400"> will immediately see the INSERT just performed on </span><b>Node A</b><span style="font-weight: 400">.</span></p>
<p><span style="font-weight: 400">In a distributed system, there is a microsecond-to-millisecond gap between a transaction being globally ordered (everyone knows it happened) and being locally applied (the data is physically readable in the table). Reading executed on a secondary during this gap results in a </span><b>stale read</b><span style="font-weight: 400">.</span></p>
<h3><span style="font-weight: 400">Why is avoiding stale reads so critical?</span><a class="anchor-link" id="why-is-avoiding-stale-reads-so-critical"></a></h3>
<p><span style="font-weight: 400">While a stale read might just mean a user temporarily sees their old profile picture after updating it, in many business cases, it breaks the application&rsquo;s core logic:</span></p>
<ol>
<li style="font-weight: 400"><b>Financial Transactions:</b><span style="font-weight: 400"> A user deposits $100 on the Primary node and immediately refreshes their balance page, which reads from a Replica. If the read is stale, the balance hasn&rsquo;t updated. The user panics, thinking their money is lost.</span></li>
<li style="font-weight: 400"><b>E-commerce &amp; Inventory:</b><span style="font-weight: 400"> A customer buys the last item in stock. The next user immediately loads the product page. A stale read tells the second user the item is still available, leading to a cancelled order and a frustrated customer.</span></li>
<li style="font-weight: 400"><b>Security &amp; Access:</b><span style="font-weight: 400"> A user changes their password or updates a critical permission. If the next authentication request hits a node lagging by just a fraction of a second, their valid login might be rejected, or a revoked session might still be active.</span></li>
</ol>
<p><span style="font-weight: 400">To prevent these scenarios, we must tell the database to enforce strict consistency. But how do GR and PXC handle this, and what does it cost?</span></p>
<h3><span style="font-weight: 400">Consistency Controls Comparison</span><a class="anchor-link" id="consistency-controls-comparison"></a></h3>
<p><span style="font-weight: 400">Both Group Replication and Percona XtraDB Cluster provide built-in mechanisms to enforce consistency and eliminate stale reads when your application demands it. However, they approach this problem using entirely different variables and distinct levels of granularity. The table below breaks down the specific controls each technology offers, highlighting exactly what it takes to force a node to serve fresh data.</span></p>
<table>
<thead>
<tr>
<th><b>Feature</b></th>
<th><b>MySQL Group Replication</b></th>
<th><b>Percona XtraDB Cluster</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Default Behavior</b></td>
<td><span style="font-weight: 400">Reads on secondaries may be stale because the applier thread might be lagging after consensus.</span></td>
<td><span style="font-weight: 400">Reads on secondaries may be stale due to asynchronous background applying.</span></td>
</tr>
<tr>
<td><b>Stale Read Fix</b></td>
<td><span style="font-weight: 400">Uses the group_replication_consistency variable.</span></td>
<td><span style="font-weight: 400">Uses the wsrep-sync-wait variable.</span></td>
</tr>
<tr>
<td><b>Consistency Levels</b></td>
<td><span style="font-weight: 400">Offers EVENTUAL, BEFORE, AFTER, and BEFORE_AND_AFTER.</span></td>
<td><span style="font-weight: 400">Offers granular levels from 0 (default, no checks) up to 7 (checks on all READ, UPDATE, DELETE, INSERT, and REPLACE statements).</span></td>
</tr>
<tr>
<td><b>The Fix</b></td>
<td><span style="font-weight: 400">Setting to AFTER ensures the next read is fresh.</span></td>
<td><span style="font-weight: 400">Setting to 7 ensures we have a comparable scenario with GR. However in PXC setting wsrep_sync_wait = 1 will be enough to avoid stale reads.</span></td>
</tr>
</tbody>
</table>
<h2><span style="font-weight: 400">The True Cost of Being Consistent</span><a class="anchor-link" id="the-true-cost-of-being-consistent"></a></h2>
<p><span style="font-weight: 400">If we know stale reads are bad, why don&rsquo;t we just enforce strict consistency everywhere?&nbsp;</span></p>
<p><span style="font-weight: 400">An image can help to understand:</span></p>
<p><img loading="lazy" decoding="async" class="wp-image-49841 alignnone" src="https://www.percona.com/wp-content/uploads/2026/06/dirty_comparative2-1024x566.png" alt="" width="695" height="384"></p>
<p><span style="font-weight: 400">Because in distributed databases, </span><b>consistency is incredibly expensive.</b><span style="font-weight: 400"> To test this, we used a 3-node internal lab environment to run a Sysbench-based TPC-C derivative test (50/50 read/write split, running for 600 seconds, scaling from 1 to 1024 threads).</span></p>
<p><span style="font-weight: 400">You can find the detailed machine specifications </span><a href="https://github.com/Tusamarco/blogs/blob/master/testmachine/chaos_test_machine.txt"><b>here</b></a><span style="font-weight: 400">. The benchmarks were executed using a TPC-C derivative test based on </span><a href="https://github.com/Tusamarco/sysbench-tpcc"><b>sysbench</b></a><span style="font-weight: 400">. Finally&mdash;and crucially&mdash;you can review the </span><a href="https://github.com/Tusamarco/blogs/blob/master/testmachine/ps_vs_pxc_configuration.md"><b>configuration files</b></a><span style="font-weight: 400"> used for the tests. I maintained the same baseline MySQL configuration across the board, only adjusting the parameters specific to each replication technology.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Scenario 1: Default (Relaxed) Consistency</span><a class="anchor-link" id="scenario-1-default-relaxed-consistency"></a></h3>
<p><i><span style="font-weight: 400">(GR = EVENTUAL, PXC = wsrep-sync-wait 0)</span></i></p>
<p><span style="font-weight: 400">I want to remind, that MySQL CE and Percona Server are running using Group Replication, while PXC is using galera.</span></p>
<p><span style="font-weight: 400">With default settings, both systems allow stale reads.</span></p>
<p><img loading="lazy" decoding="async" class="alignnone size-large wp-image-49838" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_eventual_run_tpcc_RepeatableRead-1024x597.png" alt="" width="1024" height="597"></p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49837" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_eventual_run_tpcc_ReadCommitted-1024x597.png" alt="" width="1024" height="597"></p>
<p><span style="font-weight: 400">Both technologies scales well up to 128 threads:</span></p>
<ul>
<li style="font-weight: 400"><b>Group Replication</b><span style="font-weight: 400"> performs exceptionally well, handling up to 15K operations/sec before dropping off after 128 threads.</span></li>
<li style="font-weight: 400"><b>PXC (Galera)</b><span style="font-weight: 400"> is slightly less efficient at peak but scales very nicely and predictably.</span></li>
</ul>
<p><span style="font-weight: 400">At this level, the lag between the moment of commit and the moment the server returns the answer is minimal. But we are entirely exposed to stale reads.</span></p>
<h3><span style="font-weight: 400">Scenario 2: Enforced Consistency (The Cost)</span><a class="anchor-link" id="scenario-2-enforced-consistency-the-cost"></a></h3>
<p><i><span style="font-weight: 400">(GR = AFTER, PXC = wsrep-sync-wait 7)</span></i></p>
<p><span style="font-weight: 400">When we configure the servers to prevent stale reads, the systems must wait for transactions to be fully applied before returning a read. This is where the architectural differences become glaringly apparent:</span></p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49835" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_after_run_tpcc_ReadCommitted-1024x597.png" alt="" width="1024" height="597"> <img decoding="async" loading="lazy" class="alignnone size-large wp-image-49836" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_after_run_tpcc_RepeatableRead-1024x597.png" alt="" width="1024" height="597"></p>
<ul>
<li style="font-weight: 400"><b>PXC (Galera):</b><span style="font-weight: 400"> Performance drops but not too much from a peak of ~9K ops/sec (in the previous test)&nbsp; to roughly </span><b>~8.5K ops/sec</b><span style="font-weight: 400">. This is a hit but not huge and the database remains highly functional and stable.</span></li>
<li style="font-weight: 400"><b>Group Replication:</b><span style="font-weight: 400"> Performance catastrophically drops from ~15K ops/sec (in the previous test) to a staggering </span><b>~3.8K ops/sec</b><span style="font-weight: 400">.</span></li>
</ul>
<h3><span style="font-weight: 400">This is the crucial takeaway</span><a class="anchor-link" id="this-is-the-crucial-takeaway"></a></h3>
<p><span style="font-weight: 400">Enforcing strict consistency in Group Replication results in a massive ~75% performance penalty. The latency between the commit and the server response increases significantly compared to PXC.&nbsp;</span></p>
<h2><span style="font-weight: 400">The intermediate way</span><a class="anchor-link" id="the-intermediate-way"></a></h2>
<p><span style="font-weight: 400">There is another approach which is to inject the higher consistency only when it is really needed.</span></p>
<p><b>The Solution: Session-Level Consistency</b><span style="font-weight: 400"> You do not need, and should not use, full consistency at the global level for general cases. Instead, force consistency </span><i><span style="font-weight: 400">only when and where it is critical</span></i><span style="font-weight: 400">.</span></p>
<p><span style="font-weight: 400">While for Group Replication there is no support for SQL injection hints like SELECT /*+ SET_VAR(&hellip;) */, you can enforce this at the session level right before a critical read:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">SET SESSION group_replication_consistency = 'AFTER';
-- OR for PXC:
SET SESSION wsrep_sync_wait = 7;</pre>
<p>&nbsp;</p>
<p><span style="font-weight: 400">To note that&nbsp; PXC offers more flexibility and you can use hints:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">select /*+ SET_VAR(wsrep_sync_wait=7) */ @@session.wsrep_sync_wait ,@@global.wsrep_sync_wait;
+---------------------------+--------------------------+
| @@session.wsrep_sync_wait | @@global.wsrep_sync_wait |
+---------------------------+--------------------------+
|                         7 |                        0 |
+---------------------------+--------------------------+</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">By isolating these variables to specific sessions (like the immediate redirect after a password change or a checkout process), you ensure data integrity exactly where the business requires it, while allowing the rest of your application to enjoy the high-speed performance of relaxed consistency.&nbsp;</span></p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49839" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_partial_run_tpcc_ReadCommitted-1024x597.png" alt="" width="1024" height="597"> <img decoding="async" loading="lazy" class="alignnone size-large wp-image-49840" src="https://www.percona.com/wp-content/uploads/2026/06/CHAOS_tpcc_PXC_VS_PS_partial_run_tpcc_RepeatableRead-1024x597.png" alt="" width="1024" height="597"></p>
<p><b>PXC:</b><span style="font-weight: 400"> The performance drop is minimal and the solution is able to provide a consistent delivery with nice scalability up to 256 threads.</span></p>
<p><b>Group Replication: </b><span style="font-weight: 400">The solution suffers from a significant drop, not as if we set the AFTER condition at global level, but still we see a drop of ~52%.&nbsp;</span></p>
<p><span style="font-weight: 400">Comparing the two solutions we can see that PXC is able to deal with the additional requested consistency better.&nbsp;</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Additional differences</span><a class="anchor-link" id="additional-differences"></a></h2>
<p><span style="font-weight: 400">But these are not the only differences we can immediately see.</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">Performing a comparison about resources utilization, we can see that while both solutions </span><i><span style="font-weight: 400">move</span></i><span style="font-weight: 400"> the same amount of data as IO operations:</span></p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49846" src="https://www.percona.com/wp-content/uploads/2026/06/pxc_vs_gr_disk_util-1024x500.png" alt="" width="1024" height="500"></p>
<p>&nbsp;</p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49847" src="https://www.percona.com/wp-content/uploads/2026/06/pxc_vs_gr_memory_used-1024x514.png" alt="" width="1024" height="514"></p>
<p><span style="font-weight: 400">Yes, for exactly the same load and traffic Group Replication </span><i><span style="font-weight: 400">consumes</span></i><span style="font-weight: 400"><strong> 8GB</strong> more than PXC, which in this environment represents 26% memory more, over total available.</span></p>
<p><img decoding="async" loading="lazy" class="alignnone size-large wp-image-49845" src="https://www.percona.com/wp-content/uploads/2026/06/pxc_vs_gr_cpu-1024x507.png" alt="" width="1024" height="507"></p>
<p><span style="font-weight: 400">Cost that is reflected also as CPU utilization.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Conclusion: How to Survive the Cost</span><a class="anchor-link" id="conclusion-how-to-survive-the-cost"></a></h2>
<p><span style="font-weight: 400">How impactful is enforcing strict consistency at a global level in a production environment? </span><b>Massively.</b><span style="font-weight: 400"> If you blindly enforce strict consistency globally without understanding your architecture, you will decimate your database throughput. Here is the reality of how the two solutions handle that tax:</span></p>
<ul>
<li style="font-weight: 400"><b>The Group Replication Reality:</b><span style="font-weight: 400"> By default (using </span><span style="font-weight: 400">EVENTUAL</span><span style="font-weight: 400"> consistency), MySQL Group Replication behaves essentially as semi-synchronous replication paired with an automated topology manager </span><i><span style="font-weight: 400">(see <a href="https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/">The Failover Brownout: Rethinking High Availability in MySQL Group Replication</a>)</span></i><span style="font-weight: 400">. The Primary is allowed to forge ahead and serve traffic even if the Secondaries are lagging significantly behind. The moment you demand strict consistency, the Primary is violently tethered back to the rest of the cluster, and its performance drops off a cliff as it waits for the slowest node.</span></li>
<li style="font-weight: 400"><b>The PXC Advantage:</b><span style="font-weight: 400"> Percona XtraDB Cluster (PXC) absorbs the &ldquo;consistency penalty&rdquo; much more gracefully. While varying consistency levels exist in PXC, adjusting them does not cause the same dramatic throughput shock seen in MGR. This is because PXC enforces a virtually synchronous, high-consistency baseline from the start. It simply does not allow the node receiving writes to deviate too far from the rest of the cluster. You pay a baseline performance tax upfront, but in exchange, you get guaranteed, ironclad High Availability out of the box.</span></li>
</ul>
<p><b>The Final Verdict</b><span style="font-weight: 400"> Modifying consistency values at the global server level should only be done after rigorous load testing and a complete understanding of the performance tax you are about to pay.</span></p>
<p><span style="font-weight: 400">Ultimately, it comes down to choosing the right tool for your specific SLA:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">If your architecture demands a true, virtually synchronous solution with strict High Availability out of the box, </span><b>PXC</b><span style="font-weight: 400"> is the purpose-built engine for the job.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">If you are looking for a highly automated, semi-synchronous solution, </span><b>Group Replication</b><span style="font-weight: 400"> delivers excellent default performance&mdash;but tuning it to mimic PXC&rsquo;s strict consistency will cost you heavily in throughput.</span></li>
</ul>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">References</span><a class="anchor-link" id="references"></a></h2>
<p><a href="https://mariadb.com/docs/galera-cluster/galera-architecture/certification-based-replication"><span style="font-weight: 400">https://www.google.com/url?q=https://mariadb.com/docs/galera-cluster/galera-architecture/certification-based-replication&amp;sa=D&amp;source=docs&amp;ust=1777342808813139&amp;usg=AOvVaw3SAf2g7NO9d681ZJ0VVEMB</span></a></p>
<p><a href="https://docs.percona.com/percona-xtradb-cluster/5.7/wsrep-system-index.html#wsrep_sync_wait"><span style="font-weight: 400">https://docs.percona.com/percona-xtradb-cluster/5.7/wsrep-system-index.html#wsrep_sync_wait</span></a></p>
<p>The post <a href="https://www.percona.com/blog/group-replication-vs-percona-xtradb-cluster-the-true-cost-of-consistency/">Group Replication VS Percona XtraDB Cluster: The True Cost of Consistency</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/group-replication-vs-percona-xtradb-cluster-the-true-cost-of-consistency/">Group Replication VS Percona XtraDB Cluster: The True Cost of Consistency</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Failover Brownout: Rethinking High Availability in MySQL Group Replication</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/" />
      <id>https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/</id>
      <updated>2026-06-15T06:57:24+03:00</updated>
      <author><name>Marco Tusa</name></author>
      <summary type="html"><![CDATA[<p>It is time to talk again about Flow control and group replication. This time with a special eye on the use of Group Replication in the Kubernetes context. In this article we will dig a bit on how it works and what are the various side effects.    The problem Recently I was refining the … Continued<br />
The post The Failover Brownout: Rethinking High Availability in MySQL Group Replication appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/">The Failover Brownout: Rethinking High Availability in MySQL Group Replication</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><span style="font-weight: 400">It is time to talk again about Flow control and group replication. This time with a special eye on the use of Group Replication in the Kubernetes context. In this article we will dig a bit on how it works and what are the various side effects.&nbsp;</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">The problem</span><a class="anchor-link" id="the-problem"></a></h2>
<p><span style="font-weight: 400">Recently I was refining the calculation I use in the </span><a href="https://github.com/Tusamarco/mysqloperatorcalculator"><span style="font-weight: 400">MySQL calculator for Operator</span></a><span style="font-weight: 400"> given I was constantly encountering a very serious problem with the Percona Server Operator.</span></p>
<p><span style="font-weight: 400">The problem is that when the deployment was/is serving a high level of traffic, it will, no matter what, end up in getting OMMKill by the K8 system.&nbsp;</span></p>
<p><span style="font-weight: 400">This because the pod was gradually consuming more and more memory, reaching the memory limit set in the CR specification.&nbsp;</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">Now let me clarify a few things, to get straight to the facts.</span></p>
<p><span style="font-weight: 400">Kubernetes itself does not OOMKill a pod for hitting its memory limit, the mechanism works as described below with mention on how Working Set Size (WSS) is calculated, and how OOMKills are triggered, and in the resource sections, the links to the official documentation and source code.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">1. The Reality of OOMKills vs. Kubelet Evictions</span><a class="anchor-link" id="1-the-reality-of-oomkills-vs-kubelet-evictions"></a></h3>
<p><span style="font-weight: 400">It is crucial to distinguish between what the Linux kernel does and what Kubernetes does:</span></p>
<ul>
<li style="font-weight: 400"><b>OOMKilled (Exit Code 137):</b><span style="font-weight: 400"> This is executed entirely by the </span><b>Linux kernel&rsquo;s OOM Killer</b><span style="font-weight: 400">, not Kubernetes. When we set a memory limit in our Pod spec, Kubernetes translates that into a Linux cgroup constraint (</span><span style="font-weight: 400">memory.limit_in_bytes</span><span style="font-weight: 400"> for cgroups v1, or </span><span style="font-weight: 400">memory.max</span><span style="font-weight: 400"> for cgroups v2). If our container attempts to allocate more memory than this hard limit, and the kernel cannot reclaim any page cache (like inactive files), the kernel directly intervenes and terminates the process.</span></li>
<li style="font-weight: 400"><b>Node-Pressure Evictions:</b><span style="font-weight: 400"> This is where Kubernetes actively observes memory. The </span><span style="font-weight: 400">kubelet</span><span style="font-weight: 400"> monitors the </span><span style="font-weight: 400">working_set_bytes</span><span style="font-weight: 400"> metric to protect the </span><i><span style="font-weight: 400">node</span></i><span style="font-weight: 400"> from running out of memory. If the node&rsquo;s memory drops below an eviction threshold, Kubernetes will actively evict pods to prevent the kernel from initiating a system-wide OOM kill.</span></li>
</ul>
<h3><span style="font-weight: 400">2. How Working Set Size (WSS) is Calculated for the container</span><a class="anchor-link" id="2-how-working-set-size-wss-is-calculated-for-the-container"></a></h3>
<p><span style="font-weight: 400">Kubernetes monitors container memory via </span><b>cAdvisor</b><span style="font-weight: 400">, which is integrated directly into the </span><span style="font-weight: 400">kubelet</span><span style="font-weight: 400">. cAdvisor calculates the Working Set Size by taking the total memory usage and subtracting the inactive file cache (memory that the kernel can easily reclaim if it faces memory pressure).</span></p>
<p><span style="font-weight: 400">Because active file caches and anonymous memory (like our application&rsquo;s heap) cannot be easily evicted, this working set metric is the most accurate representation of the memory your container is forcing the system to hold.</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">The Calculation &amp; cgroups Evolution The core mathematical calculation is </span><i><span style="font-weight: 400">Memory Usage</span></i><i><span style="font-weight: 400"> &ndash; </span></i><i><span style="font-weight: 400">Inactive File Cache</span></i><span style="font-weight: 400">, but </span><i><span style="font-weight: 400">how</span></i><span style="font-weight: 400"> cAdvisor fetches this data from the Linux kernel depends entirely on your node&rsquo;s cgroup version. Modern cAdvisor relies heavily on the </span><span style="font-weight: 400">opencontainers/runc/libcontainer</span><span style="font-weight: 400"> library to read these raw cgroup files:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">cgroups v1: cAdvisor starts with the raw usage from </span><span style="font-weight: 400">memory.usage_in_bytes</span><span style="font-weight: 400"> and subtracts the reclaimable cache found under the </span><span style="font-weight: 400">total_inactive_file</span><span style="font-weight: 400"> key.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">cgroups v2 (Unified): cAdvisor starts with the raw usage from </span><span style="font-weight: 400">memory.current</span><span style="font-weight: 400"> and subtracts the reclaimable cache found under the </span><span style="font-weight: 400">inactive_file</span><span style="font-weight: 400"> key.</span></li>
</ul>
<p>&nbsp;</p>
<p><span style="font-weight: 400">The Underlying Code Logic While older versions used a static </span><span style="font-weight: 400">setMemoryStats</span><span style="font-weight: 400"> function, modern Kubernetes branches handle this dynamically. The logic executes the following flow before reporting back to the </span><span style="font-weight: 400">kubelet</span><span style="font-weight: 400">:</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Detects Version: It identifies whether the node runs cgroups v1 or v2 to determine the correct inactive file key name.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Fetch Usage: It pulls the raw memory usage from the container.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Subtract Cache: It looks up the inactive file value and safely subtracts it from the usage (including a safeguard to ensure the working set never drops below zero).</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Report Metric: It sets this final calculated value as </span><span style="font-weight: 400">container_memory_working_set_bytes</span><span style="font-weight: 400">, which the </span><span style="font-weight: 400">kubelet</span><span style="font-weight: 400"> then uses to decide if the node is under memory pressure.</span></li>
</ol>
<h2><span style="font-weight: 400">Back to us&nbsp;</span><a class="anchor-link" id="back-to-us"></a></h2>
<p><span style="font-weight: 400">At the end the point is that if our pod reaches the limit and we ARE NOT using the new </span><a href="https://docs.google.com/document/d/1WSoJxaAPMP4tdiT_-U4YwgoHBI8zKJ0Hw-y6iUXJQBc/edit#bookmark=id.59zqn83hsx1p"><span style="font-weight: 400">swap feature</span></a><span style="font-weight: 400"> existing in Kubernetes, our pod will be brutally killed, and in 99% of the cases our production will suffer a lot. !Ops spoiler!</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">To clearly understand what was causing the issue about this memory consumption and having my calculator fail, I started to collect the information about the memory usage in MySQL itself.</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">SELECT EVENT_NAME,CURRENT_NUMBER_OF_BYTES_USED / 1024 / 1024 AS current_usage_mb FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME like &lsquo;memory/%&rsquo; and EVENT_NAME not like &lsquo;memory/performance%&rsquo;&nbsp; order by current_usage_mb desc limit 25;</span></p>
<p><span style="font-weight: 400">Which will give you and output like this:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">+---------------------------------------+------------------+
| EVENT_NAME                            | current_usage_mb |
+---------------------------------------+------------------+
| memory/innodb/buf_buf_pool            |   46398.92578125 |
| memory/group_rpl/GCS_XCom::xcom_cache |    1066.66179943 |
| memory/group_rpl/certification_info   |      92.45250702 |
| memory/innodb/log_buffer_memory       |      64.00096130 |
| memory/sql/TABLE                      |      49.90627003 |
| memory/innodb/memory                  |      34.68734741 |
| memory/innodb/ut0link_buf             |      24.00006104 |
| memory/innodb/lock0lock               |      21.40064240 |
| memory/mysqld_openssl/openssl_malloc  |       9.51009655 |
| memory/innodb/read0read               |       8.19496155 |
| memory/mysys/KEY_CACHE                |       8.00215149 |
| memory/innodb/sync0arr                |       7.03147125 |
| memory/innodb/ha_innodb               |       6.87006950 |
| memory/innodb/lock_sys                |       5.25009155 |
| memory/sql/log_sink_pfs               |       5.00003052 |
| memory/innodb/ut0pool                 |       4.00017548 |
| memory/sql/dd::objects                |       2.83031464 |
| memory/innodb/std                     |       2.72618866 |
| memory/innodb/os0file                 |       2.63054657 |
| memory/innodb/os0event                |       2.34302521 |
| memory/sql/TABLE_SHARE::mem_root      |       2.31734467 |
| memory/innodb/trx0trx                 |       2.22647858 |
| memory/temptable/physical_ram         |       1.00003052 |
| memory/sql/dd::String_type            |       0.94942093 |
| memory/innodb/btr0pcur                |       0.89743423 |
+---------------------------------------+------------------+</pre>
<p>&nbsp;</p>
<p><span style="font-weight: 400">Plus I used PMM to collect memory information&nbsp;</span></p>
<p><img decoding="async" loading="lazy" class="alignnone wp-image-49857 size-medium_large" src="https://www.percona.com/wp-content/uploads/2026/06/allocation_with_incidents_describe-768x395.jpg" alt="" width="768" height="395"></p>
<p><span style="font-weight: 400">To simulate the load I used the sysbench-tpcc (tpc-c derivate test) variant and run the tests simulating a load of 1024 threads against a cluster based on machine with 16 Core and 64Gb volumes ~3k IOPS, so not gigantic but not small.&nbsp;</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">The finding was almost immediate:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">+---------------------------------------+------------------+
| EVENT_NAME                            | current_usage_mb |
+---------------------------------------+------------------+
| memory/innodb/buf_buf_pool            |   46398.92578125 |
| memory/group_rpl/certification_info   |    1431.67934418 | &lt;constantly increasing
| memory/group_rpl/GCS_XCom::xcom_cache |    1066.63542366 |
| memory/sql/Gtid_set::Interval_chunk   |      95.52413940 |
| memory/innodb/log_buffer_memory       |      64.00096130 |
| memory/sql/TABLE                      |      48.17613125 |
| memory/innodb/memory                  |      35.08897400 |
| memory/innodb/ut0link_buf             |      24.00006104 |
| memory/innodb/lock0lock               |      21.40064240 |
| memory/innodb/read0read               |      14.86782837 |
| memory/mysqld_openssl/openssl_malloc  |      12.05916119 |
| memory/mysys/KEY_CACHE                |       8.00215149 |
| memory/innodb/sync0arr                |       7.03147125 |
| memory/innodb/ha_innodb               |       6.84074974 |
| memory/innodb/lock_sys                |       5.25009155 |
| memory/sql/log_sink_pfs               |       5.00003052 |
| memory/innodb/ut0pool                 |       4.00017548 |
| memory/sql/dd::objects                |       2.82012177 |
| memory/innodb/std                     |       2.72515869 |
| memory/innodb/os0file                 |       2.63054657 |
| memory/innodb/os0event                |       2.35884857 |
| memory/innodb/trx0trx                 |       2.22647858 |
| memory/sql/TABLE_SHARE::mem_root      |       1.83777618 |
| memory/innodb/trx0undo                |       1.26304626 |
| memory/mysys/lf_node                  |       1.08828735 |
+---------------------------------------+------------------+</pre>
<p>&nbsp;</p>
<p><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">Ok then &hellip; What is the certification info???</span></p>
<h2><span style="font-weight: 400">What is group_rpl/certification_info?</span><a class="anchor-link" id="what-is-group_rpl-certification_info"></a></h2>
<p><span style="font-weight: 400">In MySQL, </span><span style="font-weight: 400">memory/group_rpl/certification_info</span><span style="font-weight: 400"> is a Performance Schema memory instrument. It tracks the exact amount of RAM allocated to store the Certification Database (or Certification Info).</span></p>
<p><span style="font-weight: 400">In Group Replication, nodes do not lock rows across the network while a transaction is executing. Instead, transactions execute locally and optimistically. When it is time to commit, the transaction undergoes a </span><i><span style="font-weight: 400">Certification Process</span></i><span style="font-weight: 400"> to ensure no other concurrent transaction in the cluster has modified the exact same rows. The </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> buffer is the in-memory hash map that makes this conflict detection possible.</span></p>
<h3><span style="font-weight: 400">1. What is it used for?</span><a class="anchor-link" id="1-what-is-it-used-for"></a></h3>
<p><span style="font-weight: 400">The </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> structure acts as a tracking ledger for recently modified rows.</span></p>
<p><span style="font-weight: 400">Here is how it works under the hood:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Key-Value Pair: It is fundamentally an in-memory dictionary. The </span><i><span style="font-weight: 400">key</span></i><span style="font-weight: 400"> is the hash of a modified row (extracted from the transaction&rsquo;s &ldquo;write set&rdquo;), and the </span><i><span style="font-weight: 400">value</span></i><span style="font-weight: 400"> is the Global Transaction Identifier (GTID) of the transaction that successfully modified it.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Conflict Detection: When a new transaction attempts to commit, it broadcasts its write set and the &ldquo;snapshot version&rdquo; of the database it saw when it started. The certifier cross-references the incoming transaction&rsquo;s write set against the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> map.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Decision: If the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> shows that a row was modified by a newer GTID that the incoming transaction did not &ldquo;see&rdquo; when it started, a conflict is flagged, and the transaction is aborted. If no conflict exists, the transaction is certified, and the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> map is updated with the new write set and GTID.</span></li>
</ul>
<p><span style="font-weight: 400">The primary does not hold onto this memory out of stubbornness; it does so because purging that data too early would destroy the cluster&rsquo;s consistency in the event of a failover.</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">In Group Replication, garbage collection for the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> buffer is not triggered just because a transaction commits on the primary. It is triggered by a concept called the Stable Set.&nbsp;</span></p>
<p><span style="font-weight: 400">Every node in the cluster periodically broadcasts a message to the rest of the group saying, </span><i><span style="font-weight: 400">&ldquo;Here are the GTIDs I have successfully applied to my disk.&rdquo;</span></i><span style="font-weight: 400"> The cluster then calculates a </span><i><span style="font-weight: 400">global low watermark</span></i><span style="font-weight: 400">. This watermark is the highest transaction GTID that </span><i><span style="font-weight: 400">every single member</span></i><span style="font-weight: 400"> of the group has successfully applied. Garbage collection is only allowed to purge write-sets from the certification database that fall </span><i><span style="font-weight: 400">below</span></i><span style="font-weight: 400"> this global watermark. </span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">To note that this purge is a synchronous operation during which writes are forbidden.</span></p>
<h3><span style="font-weight: 400">2. How the Apply Queue Stalls the Watermark</span><a class="anchor-link" id="2-how-the-apply-queue-stalls-the-watermark"></a></h3>
<p><span style="font-weight: 400">When a secondary node starts lagging, its </span><i><span style="font-weight: 400">applier queue</span></i><span style="font-weight: 400"> grows. This means the secondary is receiving transactions from the network quickly, but its SQL thread is too slow to actually execute them and commit them to disk.</span></p>
<p><span style="font-weight: 400">Because the secondary hasn&rsquo;t applied these transactions, it cannot report those GTIDs back to the group as &ldquo;finished.&rdquo;</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The lagging secondary&rsquo;s local watermark stalls.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Therefore, the </span><i><span style="font-weight: 400">global low watermark</span></i><span style="font-weight: 400"> for the entire cluster stalls.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Because the global watermark hasn&rsquo;t moved forward, the </span><span style="font-weight: 400">garbage_collect</span><span style="font-weight: 400"> function on the primary (and all other nodes) says, </span><i><span style="font-weight: 400">&ldquo;I am not allowed to delete any write-sets yet.&rdquo;</span></i></li>
<li style="font-weight: 400"><span style="font-weight: 400">As the primary continues to process new writes, the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> memory buffer grows continuously.</span></li>
</ul>
<h3><span style="font-weight: 400">3. Why the Primary Cannot Purge Early</span><a class="anchor-link" id="3-why-the-primary-cannot-purge-early"></a></h3>
<p><span style="font-weight: 400">we might wonder: </span><i><span style="font-weight: 400">If the transaction is already committed on the primary, why does the primary care if the secondary has applied it? Why not just drop the write-set from its own memory?</span></i></p>
<p><span style="font-weight: 400">The answer comes down to </span><i><span style="font-weight: 400">Failover Safety</span></i><span style="font-weight: 400"> and </span><i><span style="font-weight: 400">Distributed Conflict Detection</span></i><span style="font-weight: 400">. GR is a shared-nothing, decentralized architecture. Even if you are running in Single-Primary&nbsp; mode (keep this in mind will be important later), the underlying engine uses the exact same logic as Multi-Primary mode.&nbsp;</span></p>
<p><span style="font-weight: 400">Here is why the primary is forbidden from purging that data:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Failover Scenario: Imagine our primary node crashes right now. The lagging secondary (which still has a massive apply queue) is immediately elected as the new primary.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Conflict Risk: As the new primary, it starts accepting new writes from your application. However, it still has thousands of old transactions in its applier queue that it hasn&rsquo;t written to disk yet!</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Necessity of the Buffer: When a new write comes in, the new primary </span><i><span style="font-weight: 400">must</span></i><span style="font-weight: 400"> check if that write conflicts with any of the pending transactions in its apply queue. It does this by checking the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> map. If the old primary had purged the global certification data early, the new primary wouldn&rsquo;t have the write-sets for those pending transactions. It would blindly accept the new write, causing a massive data conflict and breaking the replication group entirely.</span></li>
</ul>
<p><span style="font-weight: 400">Fine Marco, then what is the effect of this?</span></p>
<p>&nbsp;</p>
<p><span style="font-weight: 400">Well, drums roll &hellip;</span></p>
<p><span style="font-weight: 400">&hellip; When a secondary node is elected as the new primary during a failover, it does not immediately open the floodgates to new writes. </span><b>It keeps its </b><b><i>super_read_only</i></b><b> variable set to ON until it has completely drained its local apply queue of all transactions that were certified prior to the election.</b></p>
<p><span style="font-weight: 400">This is an intentional design choice to guarantee that the new primary&rsquo;s state is completely consistent with the old primary before it starts accepting new data.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">4. Immediate Write Rejections (No Built-in Queuing)</span><a class="anchor-link" id="4-immediate-write-rejections-no-built-in-queuing"></a></h3>
<p><span style="font-weight: 400">The most critical impact to understand is that the new primary does not queue or pause new incoming writes while it catches up. It outright rejects them.</span></p>
<p><span style="font-weight: 400">If our application or proxy routes a COMMIT, INSERT, UPDATE, or DELETE to the new primary while it is still processing the old queue, MySQL will immediately throw an error back to the client:</span></p>
<p><span style="font-weight: 400">ERROR 1290 (HY000): The MySQL server is running with the &ndash;super-read-only option so it cannot execute this statement</span></p>
<h3><span style="font-weight: 400">5. The &ldquo;Brownout&rdquo; Window (Write Outage)</span><a class="anchor-link" id="5-the-brownout-window-write-outage"></a></h3>
<p><span style="font-weight: 400">Because of this behavior, a failover in MySQL Group Replication does not instantly restore write availability. Our cluster experiences a &ldquo;brownout&rdquo;, a period where reads might succeed, but writes are entirely blocked.</span></p>
<p><span style="font-weight: 400">The duration of this write outage is directly proportional to the size of the apply queue.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">If the secondary was fully caught up, write availability is restored in milliseconds.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">If the secondary was lagging by 50 minutes, your application will suffer a 50 minute write outage while the node applies the backlog.</span></li>
</ul>
<h3><span style="font-weight: 400">6. Impact on Proxies (e.g., MySQL Router or ProxySQL)</span><a class="anchor-link" id="6-impact-on-proxies-e-g-mysql-router-or-proxysql"></a></h3>
<p><span style="font-weight: 400">If we are using a proxy layer to route your database traffic, the apply queue dictates how the proxy behaves during the transition:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">MySQL Router: It continuously monitors the cluster topology and the super_read_only flag. Even though the node has technically been elected primary, Router will not open the read-write port to it until the apply queue drains and super_read_only flips to OFF. Depending on your application timeouts, client connections will either hang waiting for a writable connection or fail completely.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">ProxySQL: Similar to Router, if it is configured to check for the read_only state, it will temporarily quarantine the new primary from the write hostgroup.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">HAProxy (in Operator): Monitor both Primary state and read_only state, but it expose the Primary to writes causing the application to fail (bug we need to fix)&nbsp;&nbsp;</span></li>
</ul>
<h3><span style="font-weight: 400">7. Read Traffic and Stale Data</span><a class="anchor-link" id="7-read-traffic-and-stale-data"></a></h3>
<p><span style="font-weight: 400">During this catch-up phase, the node will accept incoming </span><span style="font-weight: 400">SELECT</span><span style="font-weight: 400"> queries (since it is still a valid database). However, because it is actively churning through the old primary&rsquo;s backlog, the data being read is temporarily stale.</span></p>
<p><span style="font-weight: 400">If your application reads a row that is sitting in the apply queue but hasn&rsquo;t been committed to disk yet, it will get the old version of that row.</span></p>
<h2><span style="font-weight: 400">Why Flow Control is Critical</span><a class="anchor-link" id="why-flow-control-is-critical"></a></h2>
<p><span style="font-weight: 400">Because a large apply queue turns a seamless failover into a severe, application-breaking write outage, Group Replication includes the Flow Control feature.</span></p>
<p><span style="font-weight: 400">Flow Control monitors the size of the apply queues across all secondaries. If a secondary starts lagging too far behind, Flow Control should actively throttle the write throughput on the </span><i><span style="font-weight: 400">current</span></i><span style="font-weight: 400"> primary to allow the lagging node to catch up. It is essentially a trade-off: we accept a slight performance hit during normal operations to guarantee that your database recovers almost instantly during a failover.</span></p>
<p><b>However, this is not what really happens</b><span style="font-weight: 400">.</span></p>
<h3><span style="font-weight: 400">1. It is Reactive, Not Proactive (The Polling Blind Spot)</span><a class="anchor-link" id="1-it-is-reactive-not-proactive-the-polling-blind-spot"></a></h3>
<p><span style="font-weight: 400">Flow control does not intercept and evaluate every single transaction in real-time. Instead, it relies on a periodic polling interval governed by </span><span style="font-weight: 400">group_replication_flow_control_period</span><span style="font-weight: 400"> (which defaults to 1 second).</span></p>
<p><span style="font-weight: 400">Once a second, the cluster checks the size of the apply queues and the certifier queues.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Vulnerability: If our application generates a massive spike of 50,000 writes in 500 milliseconds, the primary will happily accept and certify all of them. Flow control will not even notice the spike until the next 1 second polling interval hits. By the time it decides to apply a throttle, the damage is already done, and the secondary&rsquo;s queue is already overflowing.</span></li>
</ul>
<h3><span style="font-weight: 400">2. The PID Controller&rsquo;s &ldquo;Soft Brake&rdquo; Math</span><a class="anchor-link" id="2-the-pid-controllers-soft-brake-math"></a></h3>
<p><span style="font-weight: 400">When flow control does decide to throttle, it does not simply freeze the primary. It uses a PID (Proportional-Integral-Derivative) controller algorithm to calculate a &ldquo;write quota&rdquo; (the maximum number of transactions the primary is allowed to commit in the next second).</span></p>
<p><span style="font-weight: 400">The PID controller is deliberately tuned to be gentle. It wants to gracefully degrade performance rather than cause immediate application timeouts.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">When the secondary&rsquo;s queue breaches the </span><span style="font-weight: 400">group_replication_flow_control_applier_threshold</span><span style="font-weight: 400"> (default 25,000 transactions), the PID controller reduces the primary&rsquo;s quota incrementally.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Failure Point: If the primary&rsquo;s incoming write rate is astronomically higher than the secondary&rsquo;s disk IO capacity, this incremental &ldquo;step down&rdquo; in the quota is too slow. The primary is still allowed to write, say, 10,000 transactions per second, while the secondary is only applying 2,000. The queue continues to grow aggressively despite the throttle being &ldquo;active.&rdquo;</span></li>
</ul>
<h3><span style="font-weight: 400">3. The Concurrency Mismatch (Parallel vs. Serial)</span><a class="anchor-link" id="3-the-concurrency-mismatch-parallel-vs-serial"></a></h3>
<p><span style="font-weight: 400">This is often the silent killer that defeats flow control. Flow control makes mathematical assumptions about how fast the secondary </span><i><span style="font-weight: 400">should</span></i><span style="font-weight: 400"> be able to apply transactions based on recent history.</span></p>
<p><span style="font-weight: 400">However, the primary node might be executing writes using hundreds of highly concurrent threads. The secondary relies on the parallel applier to keep up. If the incoming workload suddenly includes transactions that cannot be parallelized, such as writes hitting overlapping rows, cascading foreign key updates, or DDL statements, the secondary&rsquo;s applier instantly drops from executing in parallel down to a single, serialized thread.</span></p>
<p><span style="font-weight: 400">When this serialization happens, the secondary&rsquo;s applier rate plummets instantly. Flow control, which only checks in once a second and adjusts gradually, cannot brake the primary fast enough to compensate for the secondary suddenly dropping to a crawl.</span></p>
<h2><span style="font-weight: 400">What can we do?</span><a class="anchor-link" id="what-can-we-do"></a></h2>
<p><span style="font-weight: 400">At the moment of writing there are only two things that can be done.</span></p>
<ol>
<li style="font-weight: 400"><span style="font-weight: 400">Make Flow control more aggressive</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Increase the number of replication appliers</span></li>
</ol>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">1. Making Flow Control More Aggressive</span><a class="anchor-link" id="1-making-flow-control-more-aggressive"></a></h3>
<p><span style="font-weight: 400">We can configure Flow Control to be a bit more aggressive. It will still remain a </span><i><span style="font-weight: 400">suggestion</span></i><span style="font-weight: 400"> but a strong one.</span></p>
<p><span style="font-weight: 400">How it works (The Configuration):</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Lower the Threshold: By reducing </span><span style="font-weight: 400">group_replication_flow_control_applier_threshold</span><span style="font-weight: 400"> (default is 25,000) to something like 1,000 or 500, we force the PID controller to kick in almost immediately when a spike occurs.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Remove the Safety Net: By keeping&nbsp; </span><span style="font-weight: 400">group_replication_flow_control_min_quota</span><span style="font-weight: 400"> to </span><span style="font-weight: 400">0 </span><span style="font-weight: 400">(default), we remove the minimum write guarantee. If the secondary falls behind, Flow Control is allowed to throttle the primary&rsquo;s writes down to zero, also if this will never happen.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Increase the Sensitivity: We can tweak the PID controller&rsquo;s math (using the derivative and proportional tuning variables) to react much more aggressively to queue growth.</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400"> &nbsp; &nbsp; &nbsp; group_replication_flow_control_hold_percent=100</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400"> &nbsp; &nbsp; &nbsp; group_replication_flow_control_release_percent=5</span></li>
</ul>
<p>&nbsp;</p>
<p><b>The reality check, does it work?:</b></p>
<p><span style="font-weight: 400">If the expectation is to have a rigid control over the applier queue on the lagging secondary, then the answer is </span><b>NO</b><span style="font-weight: 400">. No matter what, at the moment flow control is not designed to act as we are used to in PXC (Percona Xtradb Cluster), where we have a rigid control of the pending queue also at the cost of delaying the writes. In Group Replication&nbsp; the Flow Control will never bring the write to 0, the unfortunate aspect is that the mechanism is not enough to keep the queue under control.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">2. Increasing Replication Appliers&nbsp;</span><a class="anchor-link" id="2-increasing-replication-appliers"></a></h3>
<p><span style="font-weight: 400">To help the secondary chew through the queue faster, we can increase the number of parallel threads it uses to write to disk.</span></p>
<p><b>How it works</b><span style="font-weight: 400">: We can increase the </span><span style="font-weight: 400">replica_parallel_workers</span><span style="font-weight: 400"> (formerly </span><span style="font-weight: 400">slave_parallel_workers</span><span style="font-weight: 400">) setting. GR is exceptionally smart about this. Because of the certification process we discussed earlier, GR already knows exactly which transactions modify which rows. It uses a writeset-based dependency tracker to safely hand off non-conflicting transactions to multiple worker threads simultaneously.</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">The formula that is normally used to calculate the number of replication workers is to set 2.5 workers for each available core. IE if we have 14000m CPUs in our CR (K8) then we can assign ~35 workers, this is definitely higher than the default value of 4.&nbsp;&nbsp;&nbsp;</span></p>
<p><b>The reality check, does it work?</b><span style="font-weight: 400">:&nbsp; </span><b>Yes</b><span style="font-weight: 400">, but only if our workload allows it.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Catch &ndash; The Serialization Wall: Parallel appliers only work if the transactions do not conflict. If our application has 50 concurrent threads all trying to update the same &ldquo;inventory count&rdquo; row, or updating a highly contentious table, those transactions </span><i><span style="font-weight: 400">cannot</span></i><span style="font-weight: 400"> be parallelized. The secondary&rsquo;s coordinator thread will see the row-level conflicts and force those transactions to wait in line and execute sequentially. We could allocate 128 parallel workers, but 127 of them will sit idle while one thread does all the work.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Catch &ndash; Context Switching: More threads do not magically create more disk IOPS. If we set the workers too high (e.g., beyond the physical CPU core count or disk IO capacity), the secondary&rsquo;s InnoDB engine will spend more time context-switching and fighting over internal mutex locks than actually committing data. In many cases, over-allocating parallel workers actually </span><i><span style="font-weight: 400">slows down</span></i><span style="font-weight: 400"> the apply rate.</span></li>
</ul>
<h2><span style="font-weight: 400">Do we have any conclusions?</span><a class="anchor-link" id="do-we-have-any-conclusions"></a></h2>
<h3><span style="font-weight: 400">1. If HA is the goal, enforce Strict Flow Control</span><a class="anchor-link" id="1-if-ha-is-the-goal-enforce-strict-flow-control"></a></h3>
<p><span style="font-weight: 400">If our absolute top priority is High Availability, specifically achieving a near-zero Recovery Time Objective (RTO), we must configure an aggressive flow control.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Logic: Fast failovers require small apply queues. To guarantee a small apply queue, we must strictly throttle the primary the millisecond the secondary starts to lag.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Trade-off: we are protecting the cluster&rsquo;s failover readiness at the expense of application write latency. If there is a massive write spike, our application will face timeouts and connection errors, but if the primary server suddenly catches fire, our database will recover and elect a new primary almost instantly.</span></li>
</ul>
<p><span style="font-weight: 400">The problem is that Group Replication is not able to act like that today, this is something we eventually need to implement to have better HA.</span></p>
<h3><span style="font-weight: 400">2. If Performance is the goal, relax Flow Control</span><a class="anchor-link" id="2-if-performance-is-the-goal-relax-flow-control"></a></h3>
<p><span style="font-weight: 400">If our top priority is keeping the application fast and ensuring </span><span style="font-weight: 400">COMMIT</span><span style="font-weight: 400"> latencies remain extremely low, we should relax flow control or rely on the generous defaults.</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Logic: By relaxing flow control, we allow the primary to run at the absolute maximum speed its local disks and CPU allow. It does not care if the secondaries fall behind. Our application users remain happy and experience zero throttling.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The Trade-off: We are accepting severe risks to your HA posture. If the primary crashes while the secondaries have a massive apply queue, we will suffer a long write outage (the brownout) while the new primary catches up. Additionally, we are accepting the risk that the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> memory buffer will grow significantly on the primary and eventually have the pod OOMKilled .</span></li>
</ul>
<h3><span style="font-weight: 400">3. Is this not what Asynchronous replication with semy-sync offers?</span><a class="anchor-link" id="3-is-this-not-what-asynchronous-replication-with-semy-sync-offers"></a></h3>
<p>&nbsp;</p>
<h4><i><span style="font-weight: 400">1. The Similarities</span></i></h4>
<p><span style="font-weight: 400">If we look purely at how a single transaction flows and how a failover behaves, GR and Semi-Sync look like twins:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The Durability Guarantee: </span><i><span style="font-weight: 400">Semi-Sync:</span></i><span style="font-weight: 400"> The primary waits to commit until at least one secondary confirms it has received the transaction and written it to its local Relay Log.&nbsp;</span>
<ul>
<li style="font-weight: 400"><i><span style="font-weight: 400">GR:</span></i><span style="font-weight: 400"> The primary waits to commit until a majority quorum of nodes confirm they have received the transaction, certified it, and written it to their local relay logs.</span></li>
</ul>
</li>
<li style="font-weight: 400"><span style="font-weight: 400">The Failover Delay (The Queue):&nbsp; In both systems, the secondary receiving the data does not mean the secondary has applied the data to its InnoDB tables.</span>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">If a crash happens, both systems require the new primary to completely execute its pending queue (Relay Log for Semi-Sync, Apply Queue for GR) before it is safe to accept new writes.</span></li>
</ul>
</li>
</ul>
<h4><i><span style="font-weight: 400">2. The Crucial Differences</span></i></h4>
<p><span style="font-weight: 400">If they behave so similarly, why use GR at all? </span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">The differences lie entirely in automation, consensus, and split-brain protection. Semi-Sync is just a data transport mechanism; GR is a full state-machine cluster.</span></p>
<p><span style="font-weight: 400">Here is what GR gives you that Semi-Sync does not:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Automatic Election and Orchestration:</span>
<ul>
<li style="font-weight: 400"><i><span style="font-weight: 400">Semi-Sync:</span></i><span style="font-weight: 400"> If the primary dies, Semi-Sync does nothing. The cluster sits there broken. You must rely on external tools (like Orchestrator or manual DBA intervention) to detect the crash, pick the most up-to-date secondary, wait for its relay log to apply, disable </span><span style="font-weight: 400">read_only</span><span style="font-weight: 400">, and re-point the application.</span></li>
<li style="font-weight: 400"><i><span style="font-weight: 400">GR:</span></i><span style="font-weight: 400"> The cluster detects the failure natively. The remaining nodes use Paxos consensus to elect a new primary automatically, manage the queue drain natively via the </span><span style="font-weight: 400">super_read_only</span><span style="font-weight: 400"> flip we discussed, and self-heal.</span></li>
</ul>
</li>
<li style="font-weight: 400"><span style="font-weight: 400">Split-Brain Protection (Network Partitions):</span>
<ul>
<li style="font-weight: 400"><i><span style="font-weight: 400">Semi-Sync:</span></i><span style="font-weight: 400"> If our network splits in half, an external failover tool might accidentally promote a secondary while the old primary is still alive and accepting writes. We now have a split-brain, and our data is permanently corrupted.</span></li>
<li style="font-weight: 400"><i><span style="font-weight: 400">GR:</span></i><span style="font-weight: 400"> GR enforces strict quorum. If a network split happens, the side of the network with the minority of nodes will automatically fence itself off and refuse all writes. Split-brain is mathematically prevented.</span></li>
</ul>
</li>
<li style="font-weight: 400"><span style="font-weight: 400">The Certification Database:</span>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">As we established, GR requires the certification map to ensure the new primary doesn&rsquo;t accept writes that conflict with its unapplied queue. Semi-Sync does not have this; it relies entirely on the external failover tool to guarantee no writes touch the new primary until the relay log is 100% applied.</span></li>
</ul>
</li>
</ul>
<h4><i><span style="font-weight: 400">3. Final observation</span></i></h4>
<p><span style="font-weight: 400">If we are using Single-Primary GR with relaxed flow control, we have essentially built a highly-automated, consensus-driven version of Semi-Sync replication.&nbsp;</span></p>
<p><span style="font-weight: 400">We have the exact same apply-queue bottleneck during failover, but we have traded the need for external orchestrator tools for built-in Paxos consensus and native split-brain protection.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Conclusions (for real)</span><a class="anchor-link" id="conclusions-for-real"></a></h2>
<p><span style="font-weight: 400">When we run MySQL on a traditional, dedicated Virtual Machine, memory limits are &ldquo;soft.&rdquo; If the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> database explodes and consumes an extra 10GB of RAM because of the applier lag, the Linux OS might start aggressively swapping inactive pages to disk, but the MySQL process usually survives. Performance degrades, but the database stays online.</span></p>
<p><span style="font-weight: 400">In Kubernetes, memory limits are &ldquo;hard.&rdquo; As we discussed earlier, Kubernetes enforces pod memory limits via cgroups v2 (</span><span style="font-weight: 400">memory.max</span><span style="font-weight: 400">). The Linux kernel&rsquo;s OOM Killer has no understanding of database quorum, failover states, or apply queues. It only sees math: </span><i><span style="font-weight: 400">Working Set Size &gt; </span></i><i><span style="font-weight: 400">memory.max</span></i><i><span style="font-weight: 400"> = Terminate Process (Exit Code 137).</span></i></p>
<h3><span style="font-weight: 400">The Chain Reaction of Relaxed Flow Control in k8s</span><a class="anchor-link" id="the-chain-reaction-of-relaxed-flow-control-in-k8s"></a></h3>
<p><span style="font-weight: 400">If we prioritize &ldquo;performance&rdquo; by relaxing Flow Control in a Kubernetes environment, we are essentially setting a ticking time bomb. Here is the chain of events:</span></p>
<ol>
<li style="font-weight: 400"><b>The Spike</b><span style="font-weight: 400">: Our application experiences a massive write spike.</span></li>
<li style="font-weight: 400"><b>The Queue</b><span style="font-weight: 400">: The secondary pod&rsquo;s disk cannot keep up, and its applier queue grows to 1,000,000 transactions.</span></li>
<li style="font-weight: 400"><b>The Memory Sprawl</b><span style="font-weight: 400">: Because the queue is large, the global low-watermark stalls. The Primary pod is forbidden from garbage collecting the </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> map. The in-memory hash map balloons in size.</span></li>
<li style="font-weight: 400"><b>The Execution</b><span style="font-weight: 400">: The </span><i><span style="font-weight: 400">memory.current</span></i><span style="font-weight: 400"> metric will reach the </span><i><span style="font-weight: 400">memory.max</span></i><span style="font-weight: 400">, kernel will trigger the OMMKill process. First action will be to try to free the page.cache related to the process. If the purge is successful and the memory.current is less than </span><i><span style="font-weight: 400">memory.max</span></i><span style="font-weight: 400"> then the process will persist, otherwise the kernel will kill it. </span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400">We can use the WSS metric to predict a successful OMMKill.</span><span style="font-weight: 400"><br>
</span><span style="font-weight: 400"> The Primary pod&rsquo;s Working Set Size (WSS) breaches its Kubernetes memory limit, this is a fair estimate not an absolute value.</span></li>
<li style="font-weight: 400"><b>The Catastrophe</b><span style="font-weight: 400">: The Linux OOM Killer instantly assassinates the Primary MySQL process.</span></li>
</ol>
<p><span style="font-weight: 400">Because we tried to avoid a few seconds of write latency by keeping relaxed Flow Control, we inadvertently caused a hard crash of the primary database pod, with long write downtime.</span></p>
<h3><span style="font-weight: 400">The Architectural Law</span><a class="anchor-link" id="the-architectural-law"></a></h3>
<p><span style="font-weight: 400">Therefore, here is my statement as architectural law for containerized environments: </span><b>In Kubernetes, High Availability and Pod stability are so intrinsically linked that Flow Control </b><b><i>must</i></b><b> act as hard as it can to cap the apply queue.</b></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">We cannot allow unbounded memory growth in a container. The only way to bound </span><span style="font-weight: 400">certification_info</span><span style="font-weight: 400"> memory is to bound the apply queue.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The only way to bound the apply queue is with strict, aggressive Flow Control.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Increasing the number of replication appliers helps but is not the conclusive answer.</span></li>
</ul>
<p><span style="font-weight: 400">In a Kubernetes environment, we must tune </span><span style="font-weight: 400">group_replication_flow_control_applier_threshold</span><span style="font-weight: 400"> to a strict, low number, and accept that during massive traffic spikes, our application </span><i><span style="font-weight: 400">will</span></i><span style="font-weight: 400"> experience write throttling. It is infinitely better for our application&rsquo;s connection pool to wait 2 seconds for a </span><span style="font-weight: 400">COMMIT</span><span style="font-weight: 400"> to succeed than for the primary database pod to be violently OOMKilled by the kernel, and have to wait for minutes or hours to recover write capabilities.</span></p>
<h3><span style="font-weight: 400">Note</span><a class="anchor-link" id="note"></a></h3>
<p><span style="font-weight: 400">Just as a mention this is exactly how Percona Operator with Percona Xtradb Cluster works. To be more specific, PXC and in general solutions based on Galera have a Flow Control mechanism that enforces the queue to be inside hard limits. While this more invasive control may be noticeable at application level, it guarantees that the other nodes are not lagging behind the primary and this is why it is a stronger HA solution in the Kubernetes environment.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Reference</span><a class="anchor-link" id="reference"></a></h2>
<p><a href="https://github.com/Tusamarco/mysqloperatorcalculator"><span style="font-weight: 400">https://github.com/Tusamarco/mysqloperatorcalculator</span></a></p>
<p><span style="font-weight: 400">Managing Resources and OOMKills: </span><a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"><span style="font-weight: 400">Resource Management for Pods and Containers</span></a> <i><span style="font-weight: 400">(This page details how memory limits are enforced reactively by the Linux kernel via OOM kills).</span></i></p>
<p><span style="font-weight: 400">How WSS triggers Evictions: </span><a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/"><span style="font-weight: 400">Node-pressure Eviction</span></a> <i><span style="font-weight: 400">(This page explicitly details how the </span></i><i><span style="font-weight: 400">kubelet</span></i><i><span style="font-weight: 400"> uses the </span></i><i><span style="font-weight: 400">memory.available</span></i><i><span style="font-weight: 400"> signal, which is derived from node capacity minus the working set size).</span></i></p>
<p><span style="font-weight: 400">Latest changes. </span><a href="https://github.com/google/cadvisor/blob/195858077459e69455fd9621fcbaeaf377d69d0e/container/libcontainer/handler.go#L865"><span style="font-weight: 400">Pointer to the code</span></a><span style="font-weight: 400">&nbsp;</span></p>
<p><span style="font-weight: 400">Swap Memory Management (Core Concepts &amp; Configuration): </span><a href="https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/"><span style="font-weight: 400">https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/</span></a></p>
<p>The post <a href="https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/">The Failover Brownout: Rethinking High Availability in MySQL Group Replication</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/the-failover-brownout-rethinking-high-availability-in-mysql-group-replication/">The Failover Brownout: Rethinking High Availability in MySQL Group Replication</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/" />
      <id>https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/</id>
      <updated>2026-06-15T06:30:16+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Interview with Sylvain Arbaudie, nominated in the Technical Excellence category.<br />
The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. …<br />
Continue reading \"MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie\"<br />
The post MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/">MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Interview with Sylvain Arbaudie, nominated in the Technical Excellence category.<br>
The MariaDB Foundation Sea Lion Champions program celebrates the people and organizations who help make the MariaDB ecosystem stronger, more open, and more useful for everyone. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/">MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sylvain-arbaudie/">MariaDB Foundation Sea Lion Champions Nominees: Sylvain Arbaudie</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>HammerDB tproc-c on a large server, Postgres 14 to 19 beta1</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/06/hammerdb-tproc-c-on-large-server.html" />
      <id>https://smalldatum.blogspot.com/2026/06/hammerdb-tproc-c-on-large-server.html</id>
      <updated>2026-06-13T01:00:03+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for HammerDB tproc-c on a large server using MySQL and Postgres. I am new to HammerDB and still figuring out how to explain and present results so I will keep this simple and just share graphs without explaining the results.tl;drThere are small regressions in versions 16, 17 and 18NOPM usually improves a small amount in 19 beta1 relative to 18Builds, configuration and hardwareI compiled Postgres versions from source: 14.22, 14.23, 15.17, 15.18, 16.13, 16.14, 17.9, 17.10, 18.0, 18.1, 18.2, 18.3, 18.4 and 19 beta1.I used a 48-core server from Hetzneran ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4128G RAMUbuntu 24.04Postgres configuration files:prior to version 18 the config file is named conf.diff.cx10a50g_c32r128 (x10a_c32r128) and is here for versions 14, 15, 16 and 17.for Postgres 18 and 19 I used conf.diff.cx10b_c32r128 (x10b_c32r128) with io_method=sync to be similar to the config used for versions 14 through 17.BenchmarkThe benchmark is tproc-c from HammerDB. The tproc-c benchmark is derived from TPC-C.The benchmark was run for several workloads:vu=10, wh=1000 - 10 virtual users, 1000 warehousesvu=20, wh=1000 - 20 virtual users, 1000 warehousesvu=40, wh=1000 - 40 virtual users, 1000 warehousesvu=10, wh=2000 - 10 virtual users, 2000 warehousesvu=20, wh=2000 - 20 virtual users, 2000 warehousesvu=40, wh=2000 - 40 virtual users, 2000 warehousesvu=10, wh=4000 - 10 virtual users, 4000 warehousesvu=20, wh=4000 - 20 virtual users, 4000 warehousesvu=40, wh=4000 - 40 virtual users, 4000 warehousesThe wh=1000 workloads are less heavy on IO. The wh=4000 workloads are more heavy on IO.The benchmark for Postgres is run by a variant of this script which depends on scripts here.stored procedures are enabledpartitioning is used because the warehouse count is >= 1000a 5 minute rampup is usedthen performance is measured for 60 minutesResultsMy analysis at this point is simple -- I only consider average throughput. Eventually I will examine throughput over time and efficiency (CPU and IO).On the charts that follow y-axis does not start at 0 to improve readability at the risk of overstating the differences. The y-axis shows relative throughput. There might be a regression when the relative throughput is less than 1.0. There might be an improvement when it is > 1.0. The relative throughput is:(NOPM for some-version / NOPM for base-version)The base version is Postgres 14.22.A spreadsheet with absolute and relative values for NOPM is here.Results: vu=10, wh=1000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1Results: vu=20, wh=1000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1Results: vu=40, wh=1000Summary:There are small regressions in versions 17 and 18 while NOPM improves is 19 beta1Results: vu=10, wh=2000Summary:There are small regressions in version 18 while NOPM improves is 19 beta1Results: vu=20, wh=2000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1Results: vu=40, wh=2000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1There is no result for 18.1 because of a bug in my test scriptsResults: vu=10, wh=4000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1Results: vu=20, wh=4000Summary:There are small regressions in versions 16, 17 and 18Results: vu=40, wh=4000Summary:There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</p>
<p><a href="https://smalldatum.blogspot.com/2026/06/hammerdb-tproc-c-on-large-server.html">HammerDB tproc-c on a large server, Postgres 14 to 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for <a href="https://www.hammerdb.com/">HammerDB</a> tproc-c on a large server using MySQL and Postgres. I am new to HammerDB and still figuring out how to explain and present results so I will keep this simple and just share graphs without explaining the results.</p>
<p>tl;dr</p>

<ul style="text-align: left"></ul>

<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18</li>
<li>NOPM usually improves a small amount in 19 beta1 relative to 18</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>
<div>
<div></div>
<div>I compiled Postgres versions from source: 14.22, 14.23, 15.17, 15.18, 16.13, 16.14, 17.9, 17.10, 18.0, 18.1, 18.2, 18.3, 18.4 and 19 beta1.</div>
</div>
<div></div>
<div>
<div>I used a 48-core server from Hetzner</div>
<div>
<ul>
<li>an ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled</li>
<li>2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4</li>
<li>128G RAM</li>
<li>Ubuntu 24.04</li>
</ul>
<div>
<div><span style="font-family: inherit">Postgres configuration files:</span></div>
<div>
<ul>
<li><span style="font-family: inherit">prior to version 18 the config file is named conf.diff.cx10a50g_c32r128 (x10a_c32r128) and is here for versions </span><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1412_o2nofp/conf.diff.cx10a50g_c32r128">14</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg157_o2nofp/conf.diff.cx10a50g_c32r128">15</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg163_o2nofp/conf.diff.cx10a50g_c32r128">16</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg17beta1_o2nofp/conf.diff.cx10a50g_c32r128">17</a>.</li>
<li>for Postgres 18 and 19 I used <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10b50g_c32r128">conf.diff.cx10b_c32r128</a> (x10b_c32r128) with io_method=sync to be similar to the config used for versions 14 through 17.</li>
</ul>
<div>
<div><b>Benchmark</b>
<div></div>
</div>
<div></div>
<div>The benchmark is <a href="https://www.hammerdb.com/docs/ch03.html">tproc-c</a> from <a href="https://www.hammerdb.com/">HammerDB</a>. The tproc-c benchmark is derived from TPC-C.
<p>The benchmark was run for several workloads:</p></div>
<div>
<ul>
<li>vu=10, wh=1000 &ndash; 10 virtual users, 1000 warehouses</li>
<li>vu=20, wh=1000 &ndash; 20 virtual users, 1000 warehouses</li>
<li>vu=40, wh=1000 &ndash; 40 virtual users, 1000 warehouses</li>
<li>vu=10, wh=2000 &ndash; 10 virtual users, 2000 warehouses</li>
<li>vu=20, wh=2000 &ndash; 20 virtual users, 2000 warehouses</li>
<li>vu=40, wh=2000 &ndash; 40 virtual users, 2000 warehouses</li>
<li>vu=10, wh=4000 &ndash; 10 virtual users, 4000 warehouses</li>
<li>vu=20, wh=4000 &ndash; 20 virtual users, 4000 warehouses</li>
<li>vu=40, wh=4000 &ndash; 40 virtual users, 4000 warehouses</li>
</ul>
<div>The wh=1000 workloads are less heavy on IO. The wh=4000 workloads are more heavy on IO.</div>
<div></div>
<div>The benchmark for Postgres is run by a variant of <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/jan26.tprocc.pn53.pg/allpg.N.sh">this script</a> which depends on <a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/jan26.tprocc.pn53.pg/testscripts">scripts here</a>.</div>
<div>
<ul>
<li>stored procedures are enabled</li>
<li>partitioning is used because the warehouse count is &gt;= 1000</li>
<li>a 5 minute rampup is used</li>
<li>then performance is measured for 60 minutes</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div>
<div>
<div><b>Results</b></div>
<div><b><br></b></div>
<div>My analysis at this point is simple &mdash; I only consider average throughput. Eventually I will examine throughput over time and efficiency (CPU and IO).</div>
<div></div>
<div>On the charts that follow y-axis does not start at 0 to improve readability <b>at the risk of overstating the differences</b>. The y-axis shows relative throughput. There might be a regression when the relative throughput is less than 1.0. There might be an improvement when it is &gt; 1.0. The relative throughput is:</div>
</div>
<blockquote style="border-color: currentcolor;border-style: none;border-width: medium;border: none;margin: 0px 0px 0px 40px;padding: 0px"><p>(NOPM for <i>some-version</i> / NOPM for <i>base-version</i>)</p></blockquote>
<p>The base version is Postgres 14.22.</p>
<p>A spreadsheet with absolute and relative values for NOPM <a href="https://docs.google.com/spreadsheets/d/1OTrl-jTwfBUzl32B6R2Az2WL8ysDy6f2Iljg3zMfdAo/edit?usp=sharing">is here</a>.</p>
<p><b>Results: vu=10, wh=1000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgm3Ts72epDSYxW4mDZw_9bNJ5dPgLwMn07XAKtJWiGhIro3-Jo5e7lEdSFrKexARX1da3mGuaR17ANgwpudMAXGvDCGats5zdNANPFge4cT2-au-utPsMrHiXHucKtNU8aoQbQGSt_108RIwqy1EmmSyBWcEN8fcPhEwGlDE4C5AmbDmUkv9rltQ-i5j2W/s600/relative%20NOPM_%201000%20warehouses,%2010%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgm3Ts72epDSYxW4mDZw_9bNJ5dPgLwMn07XAKtJWiGhIro3-Jo5e7lEdSFrKexARX1da3mGuaR17ANgwpudMAXGvDCGats5zdNANPFge4cT2-au-utPsMrHiXHucKtNU8aoQbQGSt_108RIwqy1EmmSyBWcEN8fcPhEwGlDE4C5AmbDmUkv9rltQ-i5j2W/w640-h396/relative%20NOPM_%201000%20warehouses,%2010%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=20, wh=1000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4g__41do5Fskg5kucMPbOZOMgUKsBc11ntmPhSsmQxUA2XBFfJLnn0YrHrprKtJPgOcHEA-viSbbVnStEHbL5Gy2GWxaYRzfQQ76EJDLUiFyMn2XqnEao9K5ZB2Ne5fOY304OE8hMxAhtRcHOye8g0P7U8Z9iFTde_nc6DIznuZM_CDdDxDScZ5oQgsUk/s600/relative%20NOPM_%201000%20warehouses,%2020%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4g__41do5Fskg5kucMPbOZOMgUKsBc11ntmPhSsmQxUA2XBFfJLnn0YrHrprKtJPgOcHEA-viSbbVnStEHbL5Gy2GWxaYRzfQQ76EJDLUiFyMn2XqnEao9K5ZB2Ne5fOY304OE8hMxAhtRcHOye8g0P7U8Z9iFTde_nc6DIznuZM_CDdDxDScZ5oQgsUk/w640-h396/relative%20NOPM_%201000%20warehouses,%2020%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=40, wh=1000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZf60hioew3Mutsk-R2jGKFh1uvw-ljdyuN_-HEnulX34bCAJZlzLpyXkAuwynurAluRi_m6wCw5aKH6raptWcN16G13Fgol_8a-WV4gMslq4WIKIJ5EAcKK3l5Df7SVXF60ev13SuyFemwOaGn9jh1lNlHAW5TKCY745rCdgK-BDzQGfdt0WDkP2QxPtj/s600/relative%20NOPM_%201000%20warehouses,%2040%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiZf60hioew3Mutsk-R2jGKFh1uvw-ljdyuN_-HEnulX34bCAJZlzLpyXkAuwynurAluRi_m6wCw5aKH6raptWcN16G13Fgol_8a-WV4gMslq4WIKIJ5EAcKK3l5Df7SVXF60ev13SuyFemwOaGn9jh1lNlHAW5TKCY745rCdgK-BDzQGfdt0WDkP2QxPtj/w640-h396/relative%20NOPM_%201000%20warehouses,%2040%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=10, wh=2000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in version 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3X2SQitxMnSt9I4kvHyJCm-Jjnk4tp07RFfu2J8aaq_XoVdr5ABj-A_JJIB1g7BrHo4MpkJQSSPdsH1_3V75Nv_960Ba9rnfbW6_VBKC1vsiUJCQ-0_lhuz0th2PrplLR1JqQUlcRCvk8p2WsCfi-Cx6eGEm2BAxd6IR9S7I6PvdIw57EvO0NjgwWP87x/s600/relative%20NOPM_%202000%20warehouses,%2010%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3X2SQitxMnSt9I4kvHyJCm-Jjnk4tp07RFfu2J8aaq_XoVdr5ABj-A_JJIB1g7BrHo4MpkJQSSPdsH1_3V75Nv_960Ba9rnfbW6_VBKC1vsiUJCQ-0_lhuz0th2PrplLR1JqQUlcRCvk8p2WsCfi-Cx6eGEm2BAxd6IR9S7I6PvdIw57EvO0NjgwWP87x/w640-h396/relative%20NOPM_%202000%20warehouses,%2010%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=20, wh=2000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqeXwXngBg6LPDxjgODIDS_XTcB4y-KqFNg8VsV3goBCzx6AX5Qf6oPg7p7aOtAjNCtmGrx9C8QidDxiKHoD-YW9Xc5fYsljaC4pcTUUr8RlXLVLTxKH2zzW6jScbPRpRRbgN65BKv-0zrZ4tFjPDlwO-lBgqa_Zcn-uYjCEOzoA5PZt4t15ENvEV1tiVR/s600/relative%20NOPM_%202000%20warehouses,%2020%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjqeXwXngBg6LPDxjgODIDS_XTcB4y-KqFNg8VsV3goBCzx6AX5Qf6oPg7p7aOtAjNCtmGrx9C8QidDxiKHoD-YW9Xc5fYsljaC4pcTUUr8RlXLVLTxKH2zzW6jScbPRpRRbgN65BKv-0zrZ4tFjPDlwO-lBgqa_Zcn-uYjCEOzoA5PZt4t15ENvEV1tiVR/w640-h396/relative%20NOPM_%202000%20warehouses,%2020%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=40, wh=2000</b></p>
<p>Summary:</p>

<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
<li>There is no result for 18.1 because of a bug in my test scripts</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWg70aQKWVWL6QuQuy_rXmnB-Y-SN9FR1w7vKbb2TELL50feiivF-T8iYBTvVDEjzbAEqXICOzAJJFn8BJZwvqkoqLze6_76tUm-84luHscpRouH2yHJ7IA-TuMEUMdel0QljbqZ4cqBJIx1qANR3gv5rNvaqlmEAdKMjFnzi8Aiw2faTDuNmxEXEBhrf1/s600/relative%20NOPM_%202000%20warehouses,%2040%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWg70aQKWVWL6QuQuy_rXmnB-Y-SN9FR1w7vKbb2TELL50feiivF-T8iYBTvVDEjzbAEqXICOzAJJFn8BJZwvqkoqLze6_76tUm-84luHscpRouH2yHJ7IA-TuMEUMdel0QljbqZ4cqBJIx1qANR3gv5rNvaqlmEAdKMjFnzi8Aiw2faTDuNmxEXEBhrf1/w640-h396/relative%20NOPM_%202000%20warehouses,%2040%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=10, wh=4000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq5B0YaewnHC3323e_8hMgC7LOa9xZMfTfkQct_EC1Q5YgbiLKDb6SZh7k1lMGzTP_ngq_kGzzIbUaH2AAE9d6ih0bjxXaugGaBQGvBwCjqhQH5XxXeKQFiPS8Itu2XLkvN5yTueNOW1rqnk8IfqCFVgRk5BlUY5BRfcqbVHgE5abHLMLl1rfHy-o2gq7L/s600/relative%20NOPM_%204000%20warehouses,%2010%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiq5B0YaewnHC3323e_8hMgC7LOa9xZMfTfkQct_EC1Q5YgbiLKDb6SZh7k1lMGzTP_ngq_kGzzIbUaH2AAE9d6ih0bjxXaugGaBQGvBwCjqhQH5XxXeKQFiPS8Itu2XLkvN5yTueNOW1rqnk8IfqCFVgRk5BlUY5BRfcqbVHgE5abHLMLl1rfHy-o2gq7L/w640-h396/relative%20NOPM_%204000%20warehouses,%2010%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=20, wh=4000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDDMWDhUQC6sr4Oh36Blj8C4I4QQRAQxDv_JpU908j335B4ML-ul062sy4l2x08k2o_GTIfjardMpiFjyF54LRHT_5kxHXCbwwhCMvetGNFFB-fw_4CFtIrGhcbSBvWDJK1BQZaoZXJG0aOs2o4mw0Yj6TnCNmjVMPm5YZJYix1VgqFpP-UAbc1XdDeOxR/s600/relative%20NOPM_%204000%20warehouses,%2020%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDDMWDhUQC6sr4Oh36Blj8C4I4QQRAQxDv_JpU908j335B4ML-ul062sy4l2x08k2o_GTIfjardMpiFjyF54LRHT_5kxHXCbwwhCMvetGNFFB-fw_4CFtIrGhcbSBvWDJK1BQZaoZXJG0aOs2o4mw0Yj6TnCNmjVMPm5YZJYix1VgqFpP-UAbc1XdDeOxR/w640-h396/relative%20NOPM_%204000%20warehouses,%2020%20virtual%20users.png" width="640"></a></div>
<p><b>Results: vu=40, wh=4000</b></p>
<p>Summary:</p>


<ul style="text-align: left">
<li>There are small regressions in versions 16, 17 and 18 while NOPM improves is 19 beta1</li>
</ul>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4OGWuarn45EqrYg0CJljnMjxxma62kaQWRQ9vpFBfDM6lkGTMU2zrGSGCJHY35D7L4GTI-Ad6JzOtehh5HfwMPgHOzEzbtKBv2-vWGWbkrFMUCfar5RJm3i9kADaP6XsqCH3NSrpzQ3_EZa_uCDLR_XoGCgxOvLWdaq1FtGOadNhXCtbsLN0XXX7w6ygW/s600/relative%20NOPM_%204000%20warehouses,%2040%20virtual%20users.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4OGWuarn45EqrYg0CJljnMjxxma62kaQWRQ9vpFBfDM6lkGTMU2zrGSGCJHY35D7L4GTI-Ad6JzOtehh5HfwMPgHOzEzbtKBv2-vWGWbkrFMUCfar5RJm3i9kADaP6XsqCH3NSrpzQ3_EZa_uCDLR_XoGCgxOvLWdaq1FtGOadNhXCtbsLN0XXX7w6ygW/w640-h396/relative%20NOPM_%204000%20warehouses,%2040%20virtual%20users.png" width="640"></a></div>
<p></p>
</div>
<div>
<div></div>
</div>
</div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/06/hammerdb-tproc-c-on-large-server.html">HammerDB tproc-c on a large server, Postgres 14 to 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB + DuckDB: A New Playground for Analytics – A First Look at the New Storage Engine</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/" />
      <id>https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/</id>
      <updated>2026-06-12T11:53:16+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>MariaDB just announced it has learned to quack: the new DuckDB storage engine has joined the large family of storage engines in MariaDB Server. …<br />
Continue reading \"MariaDB + DuckDB: A New Playground for Analytics – A First Look at the New Storage Engine\"<br />
The post MariaDB + DuckDB: A New Playground for Analytics – A First Look at the New Storage Engine appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/">MariaDB + DuckDB: A New Playground for Analytics – A First Look at the New Storage Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB just <a href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/">announced </a>it has learned to quack: the new DuckDB storage engine has joined the large family of storage engines in MariaDB Server. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB + DuckDB: A New Playground for Analytics &ndash; A First Look at the New Storage Engine&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/">MariaDB + DuckDB: A New Playground for Analytics &ndash; A First Look at the New Storage Engine</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-duckdb-a-new-playground-for-analytics-a-first-look-at-the-new-storage-engine/">MariaDB + DuckDB: A New Playground for Analytics – A First Look at the New Storage Engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Guide Multi-Cluster MongoDB on GKE with MCS, Percona Operator</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/06/12/multi-cluster-mongodb-percona-operator/" />
      <id>https://percona.community/blog/2026/06/12/multi-cluster-mongodb-percona-operator/</id>
      <updated>2026-06-12T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Multi-Cluster MongoDB on GKE with MCS Guide Deploying the Percona Operator for MongoDB across two GKE clusters using Multi-Cluster Services (MCS)</p>
<p><a href="https://percona.community/blog/2026/06/12/multi-cluster-mongodb-percona-operator/">Guide Multi-Cluster MongoDB on GKE with MCS, Percona Operator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1 id="multi-cluster-mongodb-on-gke-with-mcs-guide">Multi-Cluster MongoDB on GKE with MCS Guide<a class="anchor-link" id="multi-cluster-mongodb-on-gke-with-mcs-guide"></a></h1>
<p>Deploying the Percona Operator for MongoDB across two GKE clusters using Multi-Cluster Services (MCS)</p>
<p>This guide walks through deploying a highly available MongoDB replica set that spans two GKE clusters using the <a href="https://github.com/percona/percona-server-mongodb-operator" target="_blank" rel="noopener noreferrer">Percona Operator for MongoDB</a> and <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-services" target="_blank" rel="noopener noreferrer">GKE Multi-Cluster Services (MCS)</a>.</p>
<h2 id="architecture-overview">Architecture Overview<a class="anchor-link" id="architecture-overview"></a></h2>
<p>Both clusters belong to the same <strong>GKE Fleet</strong>. MCS gives each cluster DNS names for the<br>
other cluster&rsquo;s services (<code>*.psmdb.svc.clusterset.local</code>). <strong><code>externalNodes</code></strong> in the<br>
Percona CR tells MongoDB to use those names as replica-set members. MCS provides<br>
cross-cluster DNS; <code>externalNodes</code> wires MongoDB to use it.</p>
<h3 id="what-runs-on-each-cluster">What runs on each cluster<a class="anchor-link" id="what-runs-on-each-cluster"></a></h3>
<p>Each site runs a <strong>sharded</strong> MongoDB cluster (not a single 6-node replset):</p>
<pre class="mermaid">
flowchart TB
subgraph Main["Main cluster, Operator MANAGED"]
direction TB
MO["mongos &times;3"]
MC["cfg replset: cfg-0, cfg-1, cfg-2"]
MR["shard rs0: rs0-0, rs0-1, rs0-2"]
MO --&gt; MC
MO --&gt; MR
end
subgraph Replica["Replica cluster, Operator UNMANAGED"]
direction TB
RO["mongos &times;3"]
RC["cfg replset: cfg-0, cfg-1, cfg-2"]
RR["shard rs0: rs0-0, rs0-1, rs0-2"]
RO --&gt; RC
RO --&gt; RR
end
MC |"6 members, config servers"| RC
MR |"6 members, shard data"| RR
</pre>
<p>Once interconnected, each replset has <strong>6 members</strong> (3 on main + 3 on replica). One<br>
PRIMARY per replset; the rest are SECONDARY.</p>
<h3 id="mcs-is-bidirectional">MCS is bidirectional<a class="anchor-link" id="mcs-is-bidirectional"></a></h3>
<p>Both clusters <strong>export</strong> their own services and <strong>import</strong> the other cluster&rsquo;s services:</p>
<pre class="mermaid">
flowchart LR
subgraph Main["Main cluster"]
ExpM["ServiceExportn(main services)"]
ImpM["ServiceImportn(replica services)"]
end
subgraph Replica["Replica cluster"]
ExpR["ServiceExportn(replica services)"]
ImpR["ServiceImportn(main services)"]
end
ExpM --&gt;|"MCS Fleet"| ImpR
ExpR --&gt;|"MCS Fleet"| ImpM
ImpM --&gt; DNS["*.psmdb.svc.clusterset.local"]
ImpR --&gt; DNS
</pre>
<p>Each cluster sees <strong>18 ServiceImports</strong>, 9 from main + 9 from replica.</p>
<h2 id="prerequisites">Prerequisites<a class="anchor-link" id="prerequisites"></a></h2>
<ul>
<li><code>gcloud</code> CLI installed and authenticated</li>
<li><code>kubectl</code> installed</li>
<li><code>yq</code> installed (<code>brew install yq</code> on macOS or <code>apt install yq</code> on Linux)</li>
<li>A GCP project with billing enabled</li>
<li>Owner or Editor role on the project</li>
</ul>
<p>If you want to see all the command in a Readmefile, see the Github repository <a href="https://github.com/edithturn/psmdb-operator-multicluster-demo/blob/main/README.md" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2 id="file-layout">File Layout<a class="anchor-link" id="file-layout"></a></h2>
<p>After completing this guide you will have:</p>
<p><strong>Kubeconfigs</strong> (in <code>~/.kube/psmdb-demo/</code>, outside this repo):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-2">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">~/.kube/psmdb-demo/gcp-main_config # kubeconfig for main cluster
</span></span><span class="line"><span class="cl">~/.kube/psmdb-demo/gcp-replica_config # kubeconfig for replica cluster</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Manifests and exports</strong> (in this working directory):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-3">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">cr-main.yaml # Main cluster initial config
</span></span><span class="line"><span class="cl">cr-main-after.yaml # Main cluster config with externalNodes
</span></span><span class="line"><span class="cl">cr-replica.yaml # Replica cluster config
</span></span><span class="line"><span class="cl">cr-replica-after.yaml # Replica cluster config with externalNodes</span></span></code></pre>
</div>
</div>
</div>
<p>The following files are local only, created during the guide, listed in <code>.gitignore</code>, <strong>do not commit</strong> (contain passwords, TLS keys, and encryption keys):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-4">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">my-cluster-secrets.yml # exported from main (do not apply directly)
</span></span><span class="line"><span class="cl">main-cluster-ssl.yml # exported from main (do not apply directly)
</span></span><span class="line"><span class="cl">main-cluster-ssl-internal.yml # exported from main (do not apply directly)
</span></span><span class="line"><span class="cl">my-cluster-name-mongodb-encryption-key.yml # exported from main (do not apply directly)
</span></span><span class="line"><span class="cl">my-cluster-secrets-replica.yaml # modified for replica, apply this
</span></span><span class="line"><span class="cl">replica-cluster-ssl.yml # modified for replica, apply this
</span></span><span class="line"><span class="cl">replica-cluster-ssl-internal.yml # modified for replica, apply this
</span></span><span class="line"><span class="cl">my-cluster-name-mongodb-encryption-key-replica.yml # modified for replica, apply this</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p><strong>Why two versions of cr-main.yaml?</strong><br>
The initial <code>cr-main.yaml</code> deploys the cluster without knowing the replica node addresses.<br>
After the replica cluster is running and ServiceImports are confirmed, <code>cr-main-after.yaml</code><br>
adds <code>externalNodes</code> to interconnect the two clusters. This avoids DNS failures during<br>
initial deployment.</p>
</blockquote>
<h2 id="step-1-set-your-project-id">Step 1: Set your project ID<a class="anchor-link" id="step-1-set-your-project-id"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-5">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PROJECT_ID</span><span class="o">=</span>your_project_id</span></span></code></pre>
</div>
</div>
</div>
<p>Verify:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-6">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="nv">$PROJECT_ID</span></span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-2-enable-required-gcp-apis">Step 2: Enable required GCP APIs<a class="anchor-link" id="step-2-enable-required-gcp-apis"></a></h2>
<p>These APIs are required for MCS, Fleet, and Workload Identity to work.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-7">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud services <span class="nb">enable</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> multiclusterservicediscovery.googleapis.com <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> gkehub.googleapis.com <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> cloudresourcemanager.googleapis.com <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> trafficdirector.googleapis.com <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> dns.googleapis.com <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --project <span class="nv">$PROJECT_ID</span></span></span></code></pre>
</div>
</div>
</div>
<p>Expected output: each API shows <code>Enabling API...</code> then <code>Operation finished successfully</code>.</p>
<h2 id="step-3-create-two-gke-clusters">Step 3: Create two GKE clusters<a class="anchor-link" id="step-3-create-two-gke-clusters"></a></h2>
<p>Both clusters must be created with <code>--workload-metadata=GKE_METADATA</code> and <code>--workload-pool</code><br>
to enable Workload Identity Federation, which is required by the MCS importer.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-8">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Main cluster</span>
</span></span><span class="line"><span class="cl">gcloud container clusters create main-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --zone us-central1-a <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --machine-type n1-standard-4 <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --num-nodes<span class="o">=</span><span class="m">3</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --workload-metadata<span class="o">=</span>GKE_METADATA <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --workload-pool<span class="o">=</span><span class="nv">$PROJECT_ID</span>.svc.id.goog
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Replica cluster</span>
</span></span><span class="line"><span class="cl">gcloud container clusters create replica-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --zone us-central1-a <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --machine-type n1-standard-4 <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --num-nodes<span class="o">=</span><span class="m">3</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --workload-metadata<span class="o">=</span>GKE_METADATA <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --workload-pool<span class="o">=</span><span class="nv">$PROJECT_ID</span>.svc.id.goog</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p>Both clusters use <code>us-central1-a</code> here for simplicity. In a production setup,<br>
use different zones or regions (e.g. <code>us-east1-b</code>) for the replica to achieve<br>
true regional isolation.</p>
</blockquote>
<h2 id="step-4-enable-mcs-and-register-clusters-to-the-fleet">Step 4: Enable MCS and register clusters to the Fleet<a class="anchor-link" id="step-4-enable-mcs-and-register-clusters-to-the-fleet"></a></h2>
<p>GKE uses a Fleet to group clusters. There is exactly one Fleet per GCP project,<br>
automatically named after the project ID. MCS works across all clusters in the same Fleet.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-9">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Enable MCS at the Fleet level</span>
</span></span><span class="line"><span class="cl">gcloud container fleet multi-cluster-services <span class="nb">enable</span> --project <span class="nv">$PROJECT_ID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register main cluster to the Fleet</span>
</span></span><span class="line"><span class="cl">gcloud container fleet memberships register main-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --gke-cluster us-central1-a/main-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --enable-workload-identity
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Register replica cluster to the Fleet</span>
</span></span><span class="line"><span class="cl">gcloud container fleet memberships register replica-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --gke-cluster us-central1-a/replica-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --enable-workload-identity</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-5-grant-iam-permissions-to-the-mcs-importer">Step 5: Grant IAM permissions to the MCS Importer<a class="anchor-link" id="step-5-grant-iam-permissions-to-the-mcs-importer"></a></h2>
<p>The MCS Importer is a GKE-managed pod in the <code>gke-mcs</code> namespace on each cluster.<br>
Its job is to watch for <code>ServiceExport</code> resources and create <code>ServiceImport</code> objects<br>
on other clusters. It needs read access to your VPC network configuration to do this.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-10">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get the numeric project number (different from the project ID string)</span>
</span></span><span class="line"><span class="cl"><span class="nv">PROJECT_NUMBER</span><span class="o">=</span><span class="k">$(</span>gcloud projects describe <span class="nv">$PROJECT_ID</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --format<span class="o">=</span><span class="s2">"value(projectNumber)"</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grant compute.networkViewer to the MCS importer service account</span>
</span></span><span class="line"><span class="cl">gcloud projects add-iam-policy-binding <span class="nv">$PROJECT_ID</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --member <span class="s2">"principal://iam.googleapis.com/projects/</span><span class="nv">$PROJECT_NUMBER</span><span class="s2">/locations/global/workloadIdentityPools/</span><span class="nv">$PROJECT_ID</span><span class="s2">.svc.id.goog/subject/ns/gke-mcs/sa/gke-mcs-importer"</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --role <span class="s2">"roles/compute.networkViewer"</span></span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-6-verify-mcs-is-active-on-both-clusters">Step 6: Verify MCS is active on both clusters<a class="anchor-link" id="step-6-verify-mcs-is-active-on-both-clusters"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-11">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud container fleet multi-cluster-services describe --project <span class="nv">$PROJECT_ID</span></span></span></code></pre>
</div>
</div>
</div>
<p>Expected output, both clusters must show <code>code: OK</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">yaml</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-12">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">membershipStates</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">projects/XXXXXXX/locations/us-central1/memberships/main-cluster</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">code</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Firewall successfully updated</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">projects/XXXXXXX/locations/us-central1/memberships/replica-cluster</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">code</span><span class="p">:</span><span class="w"> </span><span class="l">OK</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">Firewall successfully updated</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">resourceState</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">state</span><span class="p">:</span><span class="w"> </span><span class="l">ACTIVE</span></span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p>If you see <code>code: PENDING</code> wait 2&ndash;3 minutes and re-run. If you see errors,<br>
check that both clusters were created with <code>--workload-pool</code> and the IAM<br>
binding in Step 5 was applied successfully.</p>
</blockquote>
<h2 id="step-7-generate-kubeconfig-files">Step 7: Generate kubeconfig files<a class="anchor-link" id="step-7-generate-kubeconfig-files"></a></h2>
<blockquote>
<p><strong>Security:</strong> Kubeconfig files contain credentials that grant access to your clusters.<br>
Keep both files in <code>~/.kube/psmdb-demo</code> only, do not copy them elsewhere, commit them<br>
to version control, or share them with anyone.</p>
</blockquote>
<p>Store kubeconfig files in a dedicated directory outside this project:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-13">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.kube/psmdb-demo
</span></span><span class="line"><span class="cl">chmod <span class="m">700</span> ~/.kube/psmdb-demo</span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-14">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Generate kubeconfig for main cluster</span>
</span></span><span class="line"><span class="cl"><span class="nv">KUBECONFIG</span><span class="o">=</span>~/.kube/psmdb-demo/gcp-main_config gcloud container clusters <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> get-credentials main-cluster --zone us-central1-a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Generate kubeconfig for replica cluster</span>
</span></span><span class="line"><span class="cl"><span class="nv">KUBECONFIG</span><span class="o">=</span>~/.kube/psmdb-demo/gcp-replica_config gcloud container clusters <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> get-credentials replica-cluster --zone us-central1-a
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> ~/.kube/psmdb-demo/gcp-main_config ~/.kube/psmdb-demo/gcp-replica_config</span></span></code></pre>
</div>
</div>
</div>
<p>Verify both files were created:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-15">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls -la ~/.kube/psmdb-demo/gcp-main_config ~/.kube/psmdb-demo/gcp-replica_config</span></span></code></pre>
</div>
</div>
</div>
<p>Verify each connects to the correct cluster:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-16">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl --kubeconfig ~/.kube/psmdb-demo/gcp-main_config get nodes
</span></span><span class="line"><span class="cl">kubectl --kubeconfig ~/.kube/psmdb-demo/gcp-replica_config get nodes</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p><strong>Two terminals, set up once:</strong> Open <strong>two terminal windows</strong> for the rest of this<br>
guide. Run each export <strong>once</strong> when you open the terminal, you do not need to repeat<br>
it in later steps unless you open a new window:</p>
<table>
<thead>
<tr>
<th>Terminal</th>
<th>Cluster</th>
<th>Run once when opening the terminal</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Terminal 1</strong></td>
<td>Main</td>
<td><code>export KUBECONFIG=~/.kube/psmdb-demo/gcp-main_config</code></td>
</tr>
<tr>
<td><strong>Terminal 2</strong></td>
<td>Replica</td>
<td><code>export KUBECONFIG=~/.kube/psmdb-demo/gcp-replica_config</code></td>
</tr>
</tbody>
</table>
<p>Verify:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-17">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get nodes</span></span></code></pre>
</div>
</div>
</div>
<p>From Step 8 onward, every <code>kubectl</code> block is labeled <strong>Terminal 1</strong> or <strong>Terminal 2</strong><br>
only. Run the command in the matching terminal.<br>
Re-export only if you open a <strong>new</strong> terminal window.</p>
</blockquote>
<p>Example: This is how the cluster looks like:</p>
<p><strong>Terminal 1 &middot; main cluster</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-18" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-18">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ kubectl get nodes
</span></span><span class="line"><span class="cl">NAME STATUS ROLES AGE VERSION
</span></span><span class="line"><span class="cl">gke-main-cluster-default-pool-9c0082b4-19wj Ready  68m v1.35.3-gke.2190000
</span></span><span class="line"><span class="cl">gke-main-cluster-default-pool-9c0082b4-q78p Ready  68m v1.35.3-gke.2190000
</span></span><span class="line"><span class="cl">gke-main-cluster-default-pool-9c0082b4-rb6r Ready  68m v1.35.3-gke.2190000</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Terminal 2 &middot; replica cluster</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-19" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-19">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ kubectl get nodes
</span></span><span class="line"><span class="cl">NAME STATUS ROLES AGE VERSION
</span></span><span class="line"><span class="cl">gke-replica-cluster-default-pool-3f3e6f2b-1qkb Ready  56m v1.35.3-gke.2190000
</span></span><span class="line"><span class="cl">gke-replica-cluster-default-pool-3f3e6f2b-gl5j Ready  56m v1.35.3-gke.2190000
</span></span><span class="line"><span class="cl">gke-replica-cluster-default-pool-3f3e6f2b-h6hk Ready  56m v1.35.3-gke.2190000</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-8-grant-cluster-admin-permissions-to-your-account">Step 8: Grant cluster-admin permissions to your account<a class="anchor-link" id="step-8-grant-cluster-admin-permissions-to-your-account"></a></h2>
<p>GCP project access and Kubernetes permissions inside each cluster are separate,<br>
Step 7&rsquo;s kubeconfig lets you authenticate, but from Step 9 onward you need<br>
cluster-wide rights to install the operator and deploy MongoDB. Main and replica are<br>
independent clusters with their own RBAC, so run the same command on each; a binding<br>
on one does not apply to the other.</p>
<p><strong>Terminal 1:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-20" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-20">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create clusterrolebinding cluster-admin-binding <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --clusterrole cluster-admin <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --user <span class="k">$(</span>gcloud config get-value core/account<span class="k">)</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Terminal 2:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-21" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-21">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create clusterrolebinding cluster-admin-binding <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --clusterrole cluster-admin <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --user <span class="k">$(</span>gcloud config get-value core/account<span class="k">)</span></span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p>If you see <code>AlreadyExists</code> on either cluster, the binding was already created in a<br>
previous session. This is not an error; continue to the next step.</p>
</blockquote>
<p>Verify on both clusters, each should return <code>yes</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-22" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-22">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl auth can-i <span class="s1">'*'</span> <span class="s1">'*'</span> --all-namespaces <span class="c1"># Terminal 1</span>
</span></span><span class="line"><span class="cl">kubectl auth can-i <span class="s1">'*'</span> <span class="s1">'*'</span> --all-namespaces <span class="c1"># Terminal 2</span></span></span></code></pre>
</div>
</div>
</div>
<hr>
<h2 id="step-9-create-namespace-and-install-the-operator-on-both-clusters">Step 9: Create namespace and install the Operator on both clusters<a class="anchor-link" id="step-9-create-namespace-and-install-the-operator-on-both-clusters"></a></h2>
<p>The namespace <strong>must be identical on both clusters</strong>. The MCS DNS name includes<br>
the namespace (e.g. <code>rs0.psmdb.svc.clusterset.local</code>). If the namespaces differ,<br>
nodes cannot find each other.</p>
<p><strong>Terminal 1 (main cluster):</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-23" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-23">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create namespace psmdb
</span></span><span class="line"><span class="cl">kubectl config set-context --current --namespace<span class="o">=</span>psmdb
</span></span><span class="line"><span class="cl">kubectl apply --server-side <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -f https://raw.githubusercontent.com/percona/percona-server-mongodb-operator/v1.20.1/deploy/bundle.yaml <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Terminal 2 (replica cluster):</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-24" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-24">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create namespace psmdb
</span></span><span class="line"><span class="cl">kubectl config set-context --current --namespace<span class="o">=</span>psmdb
</span></span><span class="line"><span class="cl">kubectl apply --server-side <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -f https://raw.githubusercontent.com/percona/percona-server-mongodb-operator/v1.20.1/deploy/bundle.yaml <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Verify the Operator is running on each cluster:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-25" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-25">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Terminal 1</span>
</span></span><span class="line"><span class="cl">kubectl get pods
</span></span><span class="line"><span class="cl">NAME READY STATUS RESTARTS AGE
</span></span><span class="line"><span class="cl">percona-server-mongodb-operator-6877fcf797-stv4s 1/1 Running <span class="m">0</span> 33s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Terminal 2</span>
</span></span><span class="line"><span class="cl">kubectl get pods
</span></span><span class="line"><span class="cl">NAME READY STATUS RESTARTS AGE
</span></span><span class="line"><span class="cl">percona-server-mongodb-operator-6877fcf797-gslpz 1/1 Running <span class="m">0</span> 9s</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-10-create-the-main-cluster">Step 10: Create the Main cluster<a class="anchor-link" id="step-10-create-the-main-cluster"></a></h2>
<p>Run all commands in <strong>Terminal 1</strong> (main cluster).</p>
<p>Create <code>cr-main.yaml</code>:</p>
<blockquote>
<p><strong>Important notes:</strong></p>
<ul>
<li><code>type: ClusterIP</code> is <strong>required</strong> for MCS, LoadBalancer will not work</li>
<li><code>multiCluster.DNSSuffix: svc.clusterset.local</code> enables cross-cluster DNS</li>
<li><code>crVersion: 1.20.1</code>, use a released version only. The Operator derives the<br>
init container image tag from <code>crVersion</code>.</li>
</ul>
</blockquote>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-26" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-26">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; cr-main.yaml <span class="s">&lt;&lt; 'EOF'
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: psmdb.percona.com/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: PerconaServerMongoDB
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s"> name: main-cluster
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s"> crVersion: 1.20.1
</span></span></span><span class="line"><span class="cl"><span class="s"> image: percona/percona-server-mongodb:7.0.14-8-multi
</span></span></span><span class="line"><span class="cl"><span class="s"> updateStrategy: SmartUpdate
</span></span></span><span class="line"><span class="cl"><span class="s"> multiCluster:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> DNSSuffix: svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> upgradeOptions:
</span></span></span><span class="line"><span class="cl"><span class="s"> apply: disabled
</span></span></span><span class="line"><span class="cl"><span class="s"> schedule: "0 2 * * *"
</span></span></span><span class="line"><span class="cl"><span class="s"> secrets:
</span></span></span><span class="line"><span class="cl"><span class="s"> users: my-cluster-name-secrets
</span></span></span><span class="line"><span class="cl"><span class="s"> encryptionKey: my-cluster-name-mongodb-encryption-key
</span></span></span><span class="line"><span class="cl"><span class="s"> replsets:
</span></span></span><span class="line"><span class="cl"><span class="s"> - name: rs0
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> sharding:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> configsvrReplSet:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> mongos:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply it:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-27" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-27">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-main.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Watch until status is <code>ready</code> (takes 3&ndash;5 minutes):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-28" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-28">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get psmdb -n psmdb -w</span></span></code></pre>
</div>
</div>
</div>
<p>Expected output:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-29" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-29">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">kubectl get psmdb -n psmdb
</span></span><span class="line"><span class="cl">NAME ENDPOINT STATUS AGE
</span></span><span class="line"><span class="cl">main-cluster main-cluster-mongos.psmdb.svc.cluster.local:27017 ready 13m</span></span></code></pre>
</div>
</div>
</div>
<p>Verify ServiceExport resources were created (takes up to 5 minutes after ready):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-30" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-30">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get serviceexport -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Expected output:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-31" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-31">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME AGE
</span></span><span class="line"><span class="cl">main-cluster-cfg 27m
</span></span><span class="line"><span class="cl">main-cluster-cfg-0 27m
</span></span><span class="line"><span class="cl">main-cluster-cfg-1 27m
</span></span><span class="line"><span class="cl">main-cluster-cfg-2 26m
</span></span><span class="line"><span class="cl">main-cluster-mongos 27m
</span></span><span class="line"><span class="cl">main-cluster-rs0 27m
</span></span><span class="line"><span class="cl">main-cluster-rs0-0 27m
</span></span><span class="line"><span class="cl">main-cluster-rs0-1 27m
</span></span><span class="line"><span class="cl">main-cluster-rs0-2 26m</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-11-export-secrets-from-the-main-cluster">Step 11: Export secrets from the Main cluster<a class="anchor-link" id="step-11-export-secrets-from-the-main-cluster"></a></h2>
<p>Run all commands in <strong>Terminal 1</strong> (main cluster).</p>
<p>The Replica cluster runs in <code>unmanaged: true</code> mode and cannot generate its own<br>
TLS certificates or credentials. It must receive exact copies of the Main cluster secrets:</p>
<ul>
<li>Without TLS secrets &rarr; pods never start</li>
<li>Without user credentials &rarr; pods start but fail liveness checks and restart continuously</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-32" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-32">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get secret my-cluster-name-secrets -n psmdb -o yaml &gt; my-cluster-secrets.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get secret main-cluster-ssl -n psmdb -o yaml &gt; main-cluster-ssl.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get secret main-cluster-ssl-internal -n psmdb -o yaml &gt; main-cluster-ssl-internal.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get secret my-cluster-name-mongodb-encryption-key -n psmdb -o yaml &gt; <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span>my-cluster-name-mongodb-encryption-key.yml</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-12-modify-secrets-for-the-replica-cluster">Step 12: Modify secrets for the Replica cluster<a class="anchor-link" id="step-12-modify-secrets-for-the-replica-cluster"></a></h2>
<p>The exported secrets contain cluster-specific metadata that must be removed before<br>
applying to another cluster. The <code>resourceVersion</code> and <code>uid</code> fields are unique to the<br>
Main cluster and cause a conflict error if reused unchanged.</p>
<p>The secret <strong>data</strong> (passwords, TLS certificates, encryption key) is copied as-is,<br>
the replica must use the same credentials to join the same MongoDB deployment. The<br>
Kubernetes secret <strong>names</strong> for user credentials and the encryption key stay the same<br>
(<code>my-cluster-name-secrets</code>, <code>my-cluster-name-mongodb-encryption-key</code>) because<br>
<code>cr-replica.yaml</code> references those exact names. Only the TLS secrets are renamed<br>
(<code>main-cluster-ssl</code> &rarr; <code>replica-cluster-ssl</code>) via <code>sed</code>; the <code>yq</code> step strips stale<br>
metadata, it does not rename those two secrets.</p>
<blockquote>
<p><strong>Linux vs macOS:</strong> <code>sed -i ''</code> is macOS-only syntax.<br>
On Linux, use <code>sed -i</code> without the empty string argument.</p>
</blockquote>
<p><strong>Terminal 1 (main cluster)</strong>, modify the exported files locally:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-33" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-33">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Secret 1, user credentials</span>
</span></span><span class="line"><span class="cl">yq <span class="nb">eval</span> <span class="s1">'del(.metadata.ownerReferences, .metadata.annotations,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.creationTimestamp, .metadata.resourceVersion,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.selfLink, .metadata.uid)'</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> my-cluster-secrets.yml &gt; my-cluster-secrets-replica.yaml
</span></span><span class="line"><span class="cl">sed -i <span class="s1">'s/main-cluster/replica-cluster/g'</span> my-cluster-secrets-replica.yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Secret 2, SSL client certificates</span>
</span></span><span class="line"><span class="cl">yq <span class="nb">eval</span> <span class="s1">'del(.metadata.ownerReferences, .metadata.annotations,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.creationTimestamp, .metadata.resourceVersion,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.selfLink, .metadata.uid)'</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> main-cluster-ssl.yml &gt; replica-cluster-ssl.yml
</span></span><span class="line"><span class="cl">sed -i <span class="s1">'s/main-cluster/replica-cluster/g'</span> replica-cluster-ssl.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Secret 3, SSL internal replication certificates</span>
</span></span><span class="line"><span class="cl">yq <span class="nb">eval</span> <span class="s1">'del(.metadata.ownerReferences, .metadata.annotations,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.creationTimestamp, .metadata.resourceVersion,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.selfLink, .metadata.uid)'</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> main-cluster-ssl-internal.yml &gt; replica-cluster-ssl-internal.yml
</span></span><span class="line"><span class="cl">sed -i <span class="s1">'s/main-cluster/replica-cluster/g'</span> replica-cluster-ssl-internal.yml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Secret 4, encryption key</span>
</span></span><span class="line"><span class="cl">yq <span class="nb">eval</span> <span class="s1">'del(.metadata.ownerReferences, .metadata.annotations,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.creationTimestamp, .metadata.resourceVersion,
</span></span></span><span class="line"><span class="cl"><span class="s1"> .metadata.selfLink, .metadata.uid)'</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> my-cluster-name-mongodb-encryption-key.yml &gt; <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> my-cluster-name-mongodb-encryption-key-replica.yml
</span></span><span class="line"><span class="cl">sed -i <span class="s1">'s/main-cluster/replica-cluster/g'</span> <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> my-cluster-name-mongodb-encryption-key-replica.yml</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p><strong>Important:</strong> If you delete and recreate the Main cluster, re-export all four<br>
secrets before applying to the Replica. The <code>resourceVersion</code> and <code>uid</code> change<br>
on every cluster recreation, stale values cause a conflict error.</p>
</blockquote>
<p><strong>Terminal 2 (replica cluster)</strong>, apply and verify:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-34" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-34">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f my-cluster-secrets-replica.yaml -n psmdb
</span></span><span class="line"><span class="cl">kubectl apply -f replica-cluster-ssl.yml -n psmdb
</span></span><span class="line"><span class="cl">kubectl apply -f replica-cluster-ssl-internal.yml -n psmdb
</span></span><span class="line"><span class="cl">kubectl apply -f my-cluster-name-mongodb-encryption-key-replica.yml -n psmdb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get secrets -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Expected output should include:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-35" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-35">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME TYPE DATA AGE
</span></span><span class="line"><span class="cl">my-cluster-name-mongodb-encryption-key Opaque 1 8s
</span></span><span class="line"><span class="cl">my-cluster-name-secrets Opaque 10 33s
</span></span><span class="line"><span class="cl">replica-cluster-ssl kubernetes.io/tls 3 24s
</span></span><span class="line"><span class="cl">replica-cluster-ssl-internal kubernetes.io/tls 3 16s</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-13-create-the-replica-cluster">Step 13: Create the Replica cluster<a class="anchor-link" id="step-13-create-the-replica-cluster"></a></h2>
<p>Run all commands in <strong>Terminal 2</strong> (replica cluster).</p>
<p>Create <code>cr-replica.yaml</code>:</p>
<blockquote>
<p><strong>Key differences from cr-main.yaml:</strong></p>
<ul>
<li><code>unmanaged: true</code> prevents the Operator from initializing a new replica set,<br>
avoiding split-brain with the Main cluster&rsquo;s Operator</li>
<li><code>updateStrategy: RollingUpdate</code>, SmartUpdate is not supported on unmanaged clusters</li>
<li>SSL secrets are explicitly referenced because the Operator does not generate them here</li>
</ul>
</blockquote>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-36" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-36">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; cr-replica.yaml <span class="s">&lt;&lt; 'EOF'
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: psmdb.percona.com/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: PerconaServerMongoDB
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s"> name: replica-cluster
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s"> unmanaged: true
</span></span></span><span class="line"><span class="cl"><span class="s"> crVersion: 1.20.1
</span></span></span><span class="line"><span class="cl"><span class="s"> image: percona/percona-server-mongodb:7.0.14-8-multi
</span></span></span><span class="line"><span class="cl"><span class="s"> updateStrategy: RollingUpdate
</span></span></span><span class="line"><span class="cl"><span class="s"> multiCluster:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> DNSSuffix: svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> upgradeOptions:
</span></span></span><span class="line"><span class="cl"><span class="s"> apply: disabled
</span></span></span><span class="line"><span class="cl"><span class="s"> schedule: "0 2 * * *"
</span></span></span><span class="line"><span class="cl"><span class="s"> secrets:
</span></span></span><span class="line"><span class="cl"><span class="s"> users: my-cluster-name-secrets
</span></span></span><span class="line"><span class="cl"><span class="s"> encryptionKey: my-cluster-name-mongodb-encryption-key
</span></span></span><span class="line"><span class="cl"><span class="s"> ssl: replica-cluster-ssl
</span></span></span><span class="line"><span class="cl"><span class="s"> sslInternal: replica-cluster-ssl-internal
</span></span></span><span class="line"><span class="cl"><span class="s"> replsets:
</span></span></span><span class="line"><span class="cl"><span class="s"> - name: rs0
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> sharding:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> configsvrReplSet:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> mongos:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply it:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-37" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-37">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-replica.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Watch until status is <code>ready</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-38" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-38">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get psmdb -n psmdb -w</span></span></code></pre>
</div>
</div>
</div>
<p>Expected output:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-39" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-39">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get pods
</span></span><span class="line"><span class="cl">NAME READY STATUS RESTARTS AGE
</span></span><span class="line"><span class="cl">percona-server-mongodb-operator-6877fcf797-gslpz 1/1 Running <span class="m">0</span> 119m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-0 1/1 Running <span class="m">11</span> <span class="o">(</span>25s ago<span class="o">)</span> 43m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-1 1/1 Running <span class="m">10</span> <span class="o">(</span>7m49s ago<span class="o">)</span> 43m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-2 1/1 Running <span class="m">10</span> <span class="o">(</span>7m25s ago<span class="o">)</span> 42m
</span></span><span class="line"><span class="cl">replica-cluster-mongos-0 0/1 Running <span class="m">10</span> <span class="o">(</span>6m46s ago<span class="o">)</span> 42m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-0 1/1 Running <span class="m">11</span> <span class="o">(</span>22s ago<span class="o">)</span> 43m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-1 1/1 Running <span class="m">10</span> <span class="o">(</span>7m17s ago<span class="o">)</span> 43m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-2 1/1 Running <span class="m">10</span> <span class="o">(</span>7m21s ago<span class="o">)</span> 42m</span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-40" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-40">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get pods
</span></span><span class="line"><span class="cl">NAME READY STATUS RESTARTS AGE
</span></span><span class="line"><span class="cl">percona-server-mongodb-operator-6877fcf797-gslpz 1/1 Running <span class="m">0</span> 113m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-0 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>108s ago<span class="o">)</span> 37m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-1 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>72s ago<span class="o">)</span> 36m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-2 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>48s ago<span class="o">)</span> 36m
</span></span><span class="line"><span class="cl">replica-cluster-mongos-0 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>9s ago<span class="o">)</span> 36m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-0 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>104s ago<span class="o">)</span> 37m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-1 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>40s ago<span class="o">)</span> 36m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-2 0/1 CrashLoopBackOff <span class="m">9</span> <span class="o">(</span>44s ago<span class="o">)</span> 36m</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p><strong>Expected behavior before interconnect (Step 15):</strong> The replica cluster runs with<br>
<code>unmanaged: true</code>, so the Operator starts mongoc pods but does <strong>not</strong> initialize a<br>
separate replica set, that happens on the main cluster after you add <code>externalNodes</code><br>
in Step 15. While waiting, replica pods may show <code>CrashLoopBackOff</code> with many<br>
restarts. This is usually the liveness probe timing out, not mongoc crashing. It is<br>
common for <code>cfg</code> and <code>rs0</code> pods to settle to <code>1/1 Running</code> before interconnect;<br>
<code>mongos</code> often stays <code>0/1</code> the longest. <code>kubectl get psmdb</code> may not show <code>ready</code><br>
yet, that is expected. Continue to Steps 14 and 15.<br>
If pods keep restarting <strong>after</strong> Step 15, re-check the secrets from Steps 11&ndash;12.</p>
</blockquote>
<p>Verify ServiceExport resources were created (takes up to 5 minutes after ready):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-41" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-41">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get serviceexport -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Expected output:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-42" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-42">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">NAME AGE
</span></span><span class="line"><span class="cl">replica-cluster-cfg 59m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-0 59m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-1 58m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-2 58m
</span></span><span class="line"><span class="cl">replica-cluster-mongos 59m
</span></span><span class="line"><span class="cl">replica-cluster-rs0 59m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-0 59m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-1 58m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-2 57m</span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-14-verify-serviceimports-on-both-clusters">Step 14: Verify ServiceImports on both clusters<a class="anchor-link" id="step-14-verify-serviceimports-on-both-clusters"></a></h2>
<p>After both clusters are running, the MCS controller creates <code>ServiceImport</code> objects<br>
automatically. This takes approximately 5 minutes after the ServiceExports appear.</p>
<p><strong>Terminal 1 (main cluster):</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-43" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-43">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get serviceimport -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Terminal 2 (replica cluster):</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-44" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-44">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get serviceimport -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Each cluster should show <strong>18 total ServiceImports</strong>, 9 for each cluster.<br>
Example output on the replica cluster:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-45" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-45">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME TYPE IP AGE
</span></span><span class="line"><span class="cl">main-cluster-cfg Headless 127m
</span></span><span class="line"><span class="cl">main-cluster-cfg-0 ClusterSetIP ["34.118.239.158"] 127m
</span></span><span class="line"><span class="cl">main-cluster-cfg-1 ClusterSetIP ["34.118.230.45"] 125m
</span></span><span class="line"><span class="cl">main-cluster-cfg-2 ClusterSetIP ["34.118.237.3"] 123m
</span></span><span class="line"><span class="cl">main-cluster-mongos ClusterSetIP ["34.118.230.127"] 127m
</span></span><span class="line"><span class="cl">main-cluster-rs0 Headless 127m
</span></span><span class="line"><span class="cl">main-cluster-rs0-0 ClusterSetIP ["34.118.237.28"] 127m
</span></span><span class="line"><span class="cl">main-cluster-rs0-1 ClusterSetIP ["34.118.230.37"] 125m
</span></span><span class="line"><span class="cl">main-cluster-rs0-2 ClusterSetIP ["34.118.226.30"] 123m
</span></span><span class="line"><span class="cl">replica-cluster-cfg Headless 62m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-0 ClusterSetIP ["34.118.231.166"] 62m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-1 ClusterSetIP ["34.118.234.146"] 59m
</span></span><span class="line"><span class="cl">replica-cluster-cfg-2 ClusterSetIP ["34.118.225.208"] 59m
</span></span><span class="line"><span class="cl">replica-cluster-mongos ClusterSetIP ["34.118.239.237"] 62m
</span></span><span class="line"><span class="cl">replica-cluster-rs0 Headless 62m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-0 ClusterSetIP ["34.118.228.53"] 62m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-1 ClusterSetIP ["34.118.238.50"] 59m
</span></span><span class="line"><span class="cl">replica-cluster-rs0-2 ClusterSetIP ["34.118.232.241"] 59m</span></span></code></pre>
</div>
</div>
</div>
<p>If any are missing, check the MCS importer logs on the affected cluster:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-46" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-46">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl logs -n gke-mcs -l k8s-app<span class="o">=</span>gke-mcs-importer --tail<span class="o">=</span><span class="m">30</span> <span class="c1"># run in Terminal 1 or 2</span></span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-15-interconnect-the-clusters-add-externalnodes">Step 15: Interconnect the clusters (add externalNodes)<a class="anchor-link" id="step-15-interconnect-the-clusters-add-externalnodes"></a></h2>
<p><code>ServiceImport</code> objects give each cluster a way to resolve DNS names for services<br>
in other clusters. <code>externalNodes</code> tells MongoDB to actually use those addresses<br>
as replica set members. Both are needed, ServiceImport is the phone book,<br>
externalNodes is the instruction to call.</p>
<p><strong>Why two voting and one non-voting external node?</strong><br>
Adding two voting nodes (<code>votes: 1</code>) and one non-voting node (<code>votes: 0</code>) from the<br>
other site prevents split-brain. If the network between sites is severed, neither<br>
side can accidentally promote a new Primary using only its external nodes.</p>
<h3 id="15a-add-replica-nodes-to-main-cluster">15a: Add Replica nodes to Main cluster<a class="anchor-link" id="15a-add-replica-nodes-to-main-cluster"></a></h3>
<p>Run in <strong>Terminal 1</strong> (main cluster).</p>
<p>Copy <code>cr-main.yaml</code> to <code>cr-main-after.yaml</code> and add an <strong><code>externalNodes</code></strong> block under<br>
<code>replsets.rs0</code> and under <code>sharding.configsvrReplSet</code>, everything else stays the same.</p>
<p>Create <code>cr-main-after.yaml</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-47" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-47">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; cr-main-after.yaml <span class="s">&lt;&lt; 'EOF'
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: psmdb.percona.com/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: PerconaServerMongoDB
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s"> name: main-cluster
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s"> crVersion: 1.20.1
</span></span></span><span class="line"><span class="cl"><span class="s"> image: percona/percona-server-mongodb:7.0.14-8-multi
</span></span></span><span class="line"><span class="cl"><span class="s"> updateStrategy: SmartUpdate
</span></span></span><span class="line"><span class="cl"><span class="s"> multiCluster:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> DNSSuffix: svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> upgradeOptions:
</span></span></span><span class="line"><span class="cl"><span class="s"> apply: disabled
</span></span></span><span class="line"><span class="cl"><span class="s"> schedule: "0 2 * * *"
</span></span></span><span class="line"><span class="cl"><span class="s"> secrets:
</span></span></span><span class="line"><span class="cl"><span class="s"> users: my-cluster-name-secrets
</span></span></span><span class="line"><span class="cl"><span class="s"> encryptionKey: my-cluster-name-mongodb-encryption-key
</span></span></span><span class="line"><span class="cl"><span class="s"> replsets:
</span></span></span><span class="line"><span class="cl"><span class="s"> - name: rs0
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> externalNodes:
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-rs0-0.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-rs0-1.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-rs0-2.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> sharding:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> configsvrReplSet:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> externalNodes:
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-cfg-0.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-cfg-1.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: replica-cluster-cfg-2.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> mongos:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-48" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-48">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-main-after.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<h3 id="15b-add-main-nodes-to-replica-cluster">15b: Add Main nodes to Replica cluster<a class="anchor-link" id="15b-add-main-nodes-to-replica-cluster"></a></h3>
<p>Run in <strong>Terminal 2</strong> (replica cluster).</p>
<p>Copy <code>cr-replica.yaml</code> to <code>cr-replica-after.yaml</code> and add an <strong><code>externalNodes</code></strong> block under<br>
<code>replsets.rs0</code> and under <code>sharding.configsvrReplSet</code>, everything else stays the same.</p>
<p>Create <code>cr-replica-after.yaml</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-49" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-49">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; cr-replica-after.yaml <span class="s">&lt;&lt; 'EOF'
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: psmdb.percona.com/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: PerconaServerMongoDB
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s"> name: replica-cluster
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s"> unmanaged: true
</span></span></span><span class="line"><span class="cl"><span class="s"> crVersion: 1.20.1
</span></span></span><span class="line"><span class="cl"><span class="s"> image: percona/percona-server-mongodb:7.0.14-8-multi
</span></span></span><span class="line"><span class="cl"><span class="s"> updateStrategy: RollingUpdate
</span></span></span><span class="line"><span class="cl"><span class="s"> multiCluster:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> DNSSuffix: svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> upgradeOptions:
</span></span></span><span class="line"><span class="cl"><span class="s"> apply: disabled
</span></span></span><span class="line"><span class="cl"><span class="s"> schedule: "0 2 * * *"
</span></span></span><span class="line"><span class="cl"><span class="s"> secrets:
</span></span></span><span class="line"><span class="cl"><span class="s"> users: my-cluster-name-secrets
</span></span></span><span class="line"><span class="cl"><span class="s"> encryptionKey: my-cluster-name-mongodb-encryption-key
</span></span></span><span class="line"><span class="cl"><span class="s"> ssl: replica-cluster-ssl
</span></span></span><span class="line"><span class="cl"><span class="s"> sslInternal: replica-cluster-ssl-internal
</span></span></span><span class="line"><span class="cl"><span class="s"> replsets:
</span></span></span><span class="line"><span class="cl"><span class="s"> - name: rs0
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> externalNodes:
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-rs0-0.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-rs0-1.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-rs0-2.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> sharding:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> configsvrReplSet:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> externalNodes:
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-cfg-0.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-cfg-1.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 1
</span></span></span><span class="line"><span class="cl"><span class="s"> - host: main-cluster-cfg-2.psmdb.svc.clusterset.local
</span></span></span><span class="line"><span class="cl"><span class="s"> votes: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> priority: 0
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> enabled: true
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s"> volumeSpec:
</span></span></span><span class="line"><span class="cl"><span class="s"> persistentVolumeClaim:
</span></span></span><span class="line"><span class="cl"><span class="s"> resources:
</span></span></span><span class="line"><span class="cl"><span class="s"> requests:
</span></span></span><span class="line"><span class="cl"><span class="s"> storage: 3Gi
</span></span></span><span class="line"><span class="cl"><span class="s"> mongos:
</span></span></span><span class="line"><span class="cl"><span class="s"> size: 3
</span></span></span><span class="line"><span class="cl"><span class="s"> expose:
</span></span></span><span class="line"><span class="cl"><span class="s"> type: ClusterIP
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-50" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-50">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-replica-after.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<blockquote>
<p><strong>After interconnect:</strong> Pods may restart on both clusters while MongoDB reconfigures<br>
the replica sets, brief <code>CrashLoopBackOff</code> on replica is normal. Wait until all<br>
pods are <code>1/1 Running</code> before continuing to Step 16.</p>
</blockquote>
<h2 id="step-16-verify-cross-cluster-replication">Step 16: Verify cross-cluster replication<a class="anchor-link" id="step-16-verify-cross-cluster-replication"></a></h2>
<p>Run in <strong>Terminal 1</strong> (main cluster).</p>
<p>Get the clusterAdmin password:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-51" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-51">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl get secret my-cluster-name-secrets <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -n psmdb <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s2">"{.data.MONGODB_CLUSTER_ADMIN_PASSWORD}"</span> <span class="p">|</span> base64 --decode</span></span></code></pre>
</div>
</div>
</div>
<p>Connect to the main cluster config server:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-52" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-52">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it main-cluster-cfg-0 -n psmdb -- /bin/bash</span></span></code></pre>
</div>
</div>
</div>
<p>Inside the pod:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-53" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-53">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mongosh admin -u clusterAdmin -p </span></span></code></pre>
</div>
</div>
</div>
<p>Check replica set members:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">javascript</span><button class="code-block__copy" type="button" data-copy-target="codeblock-54" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-54">
<div class="highlight">
<pre class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">rs</span><span class="p">.</span><span class="nx">status</span><span class="p">().</span><span class="nx">members</span></span></span></code></pre>
</div>
</div>
</div>
<p>Expected output, 6 members total, all using <code>svc.clusterset.local</code> DNS names:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">javascript</span><button class="code-block__copy" type="button" data-copy-target="codeblock-55" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-55">
<div class="highlight">
<pre class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">cfg</span> <span class="p">[</span><span class="nx">direct</span><span class="o">:</span> <span class="nx">primary</span><span class="p">]</span> <span class="nx">admin</span><span class="o">&gt;</span> <span class="nx">rs</span><span class="p">.</span><span class="nx">status</span><span class="p">().</span><span class="nx">members</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'PRIMARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">17202</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">electionTime</span><span class="o">:</span> <span class="nx">Timestamp</span><span class="p">({</span> <span class="nx">t</span><span class="o">:</span> <span class="mi">1780921358</span><span class="p">,</span> <span class="nx">i</span><span class="o">:</span> <span class="mi">2</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">electionDate</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s1">'2026-06-08T12:22:38.000Z'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">self</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">17034</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-2.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">16861</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">3214</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">3181</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-2.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">3164</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-2.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">14</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span></span></span></code></pre>
</div>
</div>
</div>
<p>If all 6 members appear with <code>health: 1</code>, cross-cluster replication is working.</p>
<h2 id="step-17-test-the-switchover-process">Step 17: Test the switchover process<a class="anchor-link" id="step-17-test-the-switchover-process"></a></h2>
<p>In a multi-cluster deployment, <strong>only one Operator should actively manage the replica<br>
set at a time</strong>, otherwise both sites could try to reconfigure MongoDB and cause<br>
split-brain.</p>
<p>Until now, the <strong>main</strong> Operator was in charge (<code>unmanaged</code> not set, so managed by<br>
default). The <strong>replica</strong> Operator only kept pods running (<code>unmanaged: true</code>) and<br>
did not drive failover or replica-set changes.</p>
<p>This step simulates a site failover in two moves:</p>
<ol>
<li><strong>Main &rarr; unmanaged</strong>: main Operator stops managing the replica set.</li>
<li><strong>Replica &rarr; managed</strong>: replica Operator takes over and can elect a new PRIMARY.</li>
</ol>
<p>Apply both changes below, then verify MongoDB elects a new PRIMARY on the replica side.</p>
<p><strong>Terminal 1 (main cluster)</strong>, release Operator control on main:</p>
<p>Edit <code>cr-main-after.yaml</code> under <code>spec:</code>, add <code>unmanaged: true</code> and change<br>
<code>updateStrategy</code> from <code>SmartUpdate</code> to <code>RollingUpdate</code> (SmartUpdate requires a<br>
managed cluster):</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-56" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-56">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">unmanaged</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">updateStrategy</span><span class="p">:</span><span class="w"> </span><span class="l">RollingUpdate</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-57" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-57">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-main-after.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Terminal 2 (replica cluster)</strong>, give Operator control on replica:</p>
<p>Edit <code>cr-replica-after.yaml</code> under <code>spec:</code>, change <code>unmanaged: true</code> to<br>
<code>unmanaged: false</code> so the replica Operator can manage failover and replica-set<br>
reconfiguration:</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-58" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-58">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w"> </span><span class="nt">unmanaged</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span></span></span></code></pre>
</div>
</div>
</div>
<p>Apply:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-59" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-59">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl apply -f cr-replica-after.yaml -n psmdb</span></span></code></pre>
</div>
</div>
</div>
<p>Verify a new PRIMARY was elected on the replica side (<strong>Terminal 2</strong>):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-60" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-60">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl <span class="nb">exec</span> -it replica-cluster-cfg-0 -n psmdb -- /bin/bash</span></span></code></pre>
</div>
</div>
</div>
<p>Inside the pod:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-61" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-61">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mongosh admin -u clusterAdmin -p </span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">javascript</span><button class="code-block__copy" type="button" data-copy-target="codeblock-62" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-62">
<div class="highlight">
<pre class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">rs</span><span class="p">.</span><span class="nx">status</span><span class="p">().</span><span class="nx">members</span></span></span></code></pre>
</div>
</div>
</div>
<p>Expected: <code>replica-cluster-cfg-0</code> is PRIMARY, main-side members are SECONDARY:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">javascript</span><button class="code-block__copy" type="button" data-copy-target="codeblock-63" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-63">
<div class="highlight">
<pre class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">19106</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">self</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">18938</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-2.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">18765</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'main-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'PRIMARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">5118</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">electionTime</span><span class="o">:</span> <span class="nx">Timestamp</span><span class="p">({</span> <span class="nx">t</span><span class="o">:</span> <span class="mi">1780940264</span><span class="p">,</span> <span class="nx">i</span><span class="o">:</span> <span class="mi">1</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">electionDate</span><span class="o">:</span> <span class="nx">ISODate</span><span class="p">(</span><span class="s1">'2026-06-08T17:37:44.000Z'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-1.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">5085</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">_id</span><span class="o">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-2.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">health</span><span class="o">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">state</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">stateStr</span><span class="o">:</span> <span class="s1">'SECONDARY'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">uptime</span><span class="o">:</span> <span class="mi">5068</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">pingMs</span><span class="o">:</span> <span class="nx">Long</span><span class="p">(</span><span class="s1">'0'</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">lastHeartbeatMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceHost</span><span class="o">:</span> <span class="s1">'replica-cluster-cfg-0.psmdb.svc.clusterset.local:27017'</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">syncSourceId</span><span class="o">:</span> <span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">infoMessage</span><span class="o">:</span> <span class="s1">''</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configVersion</span><span class="o">:</span> <span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">configTerm</span><span class="o">:</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span></span></span></code></pre>
</div>
</div>
</div>
<h2 id="step-18-cleanup">Step 18: Cleanup<a class="anchor-link" id="step-18-cleanup"></a></h2>
<p>To remove the GKE clusters when you are done:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-64" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content" id="codeblock-64">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud container clusters delete main-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --zone us-central1-a <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --quiet
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud container clusters delete replica-cluster <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --zone us-central1-a <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --quiet</span></span></code></pre>
</div>
</div>
</div>
<h2 id="references">References<a class="anchor-link" id="references"></a></h2>
<ul>
<li>
<p><a href="https://www.percona.com/blog/deploying-percona-operator-for-mongodb-across-gke-clusters-with-mcs/" target="_blank" rel="noopener noreferrer">Original blog post by Ivan Groenewold</a></p>
</li>
<li>
<p><a href="https://docs.percona.com/percona-operator-for-mongodb/replication-mcs.html" target="_blank" rel="noopener noreferrer">Percona Operator for MongoDB, Multi-Cluster Services</a></p>
</li>
<li>
<p><a href="https://cloud.google.com/kubernetes-engine/docs/concepts/multi-cluster-services" target="_blank" rel="noopener noreferrer">GKE Multi-Cluster Services overview</a></p>
</li>
<li>
<p><a href="https://docs.percona.com/percona-operator-for-mongodb/replication-mcs-gke.html" target="_blank" rel="noopener noreferrer">GKE MCS setup, Percona docs</a></p>
</li>
<li>
<p><a href="https://www.mongodb.com/docs/manual/core/replica-set-elections/" target="_blank" rel="noopener noreferrer">MongoDB replica set elections</a></p>
</li>
<li>
<p><a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/1645-multi-cluster-services-api/README.md" target="_blank" rel="noopener noreferrer">Kubernetes MCS API KEP-1645</a></p>
</li>
</ul>

<p><a href="https://percona.community/blog/2026/06/12/multi-cluster-mongodb-percona-operator/">Guide Multi-Cluster MongoDB on GKE with MCS, Percona Operator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Write-heavy sysbench tests, a large server, modern Postgres and MySQL</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/06/write-heavy-sysbench-tests-large-server.html" />
      <id>https://smalldatum.blogspot.com/2026/06/write-heavy-sysbench-tests-large-server.html</id>
      <updated>2026-06-12T01:16:04+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for modern Postgres and MySQL using write-heavy tests from sysbench and a large server. I think there are regressions in Postgres that arrive in some of versions 16, 17, 18 and 19 beta1 but I am far from certain and this blog post is just another step in my journey to figure that out.tl;drPostgres suffers a lot from throughput variation while MySQL+InnoDB does notInnoDB gets much better average throughput on 6 of 10 tests, similar throughput one one and then Postgres does better on 3 of 10 testsFor tests from which I provided vmstat and iostat results, Postgres does more write IO per operation. In some cases InnoDB uses more CPU, in other cases it does not.Builds, configuration and hardwareI compiled:Postgres from source for versions 15.17, 16.13, 17.9 and 18.3.MySQL from source for version 8.4.7I used a 48-core server from Hetzneran ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4128G RAMUbuntu 24.04Configuration files for Postgres:the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions 15, 16 and 17.for Postgres 18 I used conf.diff.cx10b_c32r128 (x10b_c32r128) which is as close as possible to the Postgres 17 config and uses io_method=syncBenchmarkI used sysbench and my usage is explained here. Normally I run 32 of the 42 microbenchmarks listed in that blog post using tables small enough to be cached by the DBMS. Most test only one type of SQL statement.The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads -- both more and less synthetic.But I did things differently here:I only run the write-heavy tests (to save time)The tables are larger than memory and cannot be cachedEach test (microbenchmark) is run for 2 hours when I normally run each for 15 minutesAfter each test a vacuum is doneThe purpose is to search for regressions from new CPU overhead and mutex contention related to MVCC GC (vacuum for Postgres, purge for InnoDB).ResultsI provide charts below with relative QPS. The relative QPS is the following:(QPS for some version) / (QPS for Postgres 15.17)When the relative QPS is > 1 then some version is faster than base version.  When it is < 1 then there might be a regression. When the relative QPS is 1.2 then some version is about 20% faster than base version.The per-test results from vmstat and iostat can help to explain why something is faster or slower because it shows how much HW is used per request, including CPU overhead per operation (cpu/o) and context switches per operation (cs/o) which are often a proxy for mutex contention.Results: writesThe table below has relative QPS for Postgres 16 to 19 and then InnoDB all relative to the throughput for Postgres 15.17. Columns 1 to 4 have results for Postgres and the numbers in yellow highlight the tests where there is a regression in Postgres. For column 5 (MySQL with InnoDB) the numbers in yellow and red indicate tests where InnoDB\'s throughput is less than Postgres. And then the numbers in green indicate tests where InnoDB\'s throughput is much larger than Postgres.Note that when relative QPS (rQPS) is 0.90 then throughput dropped by ~10%.Summary:throughput for Postgres drops after version 15.17. I don\'t know yet whether this is a regression.throughput for InnoDB is much better than Postgres in 6 of 10 tests, similar in one test, and much worse in 3 of 10 tests.The sections that follow this one have more detail on results from the update-index, update-zipf tests and insert tests.Relative to: Postgres 15.17col-1 : Postgres 16.13col-2 : Postgres 17.9col-3 : Postgres 18.3col-4 : Postgres 19 beta1col-5 : MySQL 8.4.7col-1   col-2   col-3   col-4   col-50.94    0.97    0.98    1.02    1.88    update-inlist0.94    0.90    0.88    0.92    1.43    update-index0.91    0.86    0.87    0.92    1.19    update-nonindex0.96    0.99    0.98    0.98    0.71    update-one0.92    0.83    0.81    0.85    0.93    update-zipf0.95    0.93    0.84    0.81    1.71    write-only0.94    0.94    0.90    0.92    1.14    read-write_range=100.95    0.96    0.95    0.95    1.93    read-write_range=1000.89    0.82    0.80    0.84    1.01    delete1.05    1.05    1.01    1.10    0.53    insertResults: update-indexSummary:Postgres suffers from too much varianceAverage throughput is ~1.55X larger for InnoDB than for PostgresPer operation, Postgres does ~1.20X more write IO (KB written) to storage than InnoDBPer operation, InnoDB uses more CPU and does more context switches. While autovacuum was enabled and was likely running during the test, my measurements exclude the manual vacuum done at the end of each test.iostat, vmstat normalized by operation rater/s     rMB/s   w/s     wMB/s   r/o     rKB/o   wKB/o   o/s     dbms35503.0 373.7   58795.7 1345.1  1.375   14.824  53.351  25817   PG 19b133140.6 517.8   53449.6 1735.3  0.827   13.226  44.326  40090   MySQL 8.4.7cs/s    cpu/s   cs/o    cpu/o   dbms176167  14.4     6.824  .000557 PG 19b1661395  41.9    16.498  .001046 MySQL 8.4.7Results: update-zipfSummary:Postgres suffers from too much varianceAverage throughput is ~1.09X larger for InnoDB than for PostgresPer operation, Postgres does ~1.30X more write IO (KB written) to storage than InnoDBPer operation, InnoDB uses more CPU and does more context switches. While autovacuum was enabled and was likely running during the test, my measurements exclude the manual vacuum done at the end of each test.iostat, vmstat normalized by operation rater/s     rMB/s   w/s     wMB/s   r/o     rKB/o   wKB/o   o/s     dbms55595.5 620.7   64264.4 1352.3  0.622   7.110   15.490  89396   PG 19b127405.9 428.2   37465.1 1133.6  0.282   4.508   11.933  97270   MySQL 8.4.7cs/s    cpu/s   cs/o    cpu/o   dbms424392  27.2     4.747  .000304 PG 19b11213054 44.5    12.471  .000458 MySQL 8.4.7Results: insertSummary:Postgres suffers from too much varianceAverage throughput is ~2.06X larger for Postgres than for InnoDBPer operation, Postgres does ~1.67X more write IO (KB written) to storage than InnoDBPer operation, Postgres uses more CPU and does more context switches. This is the opposite of what happens above for update-index and update-zipf.iostat, vmstat normalized by operation rater/s     rMB/s   w/s     wMB/s   r/o     rKB/o   wKB/o   o/s     dbms1615.5  56.0    15321.7 1170.9  0.007   0.242   5.059   237009  PG 19b13.6     0.1     8275.4  340.7   0.000   0.000   3.029   115155  MySQL 8.4.7cs/s    cpu/s   cs/o    cpu/o   dbms1214563 46.0    10.547  .000399 PG 19b1800827  50.5     3.379  .000213 MySQL 8.4.7
</p>
<p><a href="https://smalldatum.blogspot.com/2026/06/write-heavy-sysbench-tests-large-server.html">Write-heavy sysbench tests, a large server, modern Postgres and MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for modern Postgres and MySQL using write-heavy tests from sysbench and a large server. I think there are regressions in Postgres that arrive in some of versions 16, 17, 18 and 19 beta1 but I am far from certain and this blog post is just another step in my journey to figure that out.</p>
<p>tl;dr</p>

<ul style="text-align: left">
<li>Postgres suffers a lot from throughput variation while MySQL+InnoDB does not</li>
<li>InnoDB gets much better average throughput on 6 of 10 tests, similar throughput one one and then Postgres does better on 3 of 10 tests</li>
<li>For tests from which I provided vmstat and iostat results, Postgres does more write IO per operation. In some cases InnoDB uses more CPU, in other cases it does not.</li>
</ul>
<div style="background-color: white"><b>Builds, configuration and hardware</b></div>
<div>
<div style="background-color: white">
<div>I compiled:</div>
<div>
<ul>
<li>Postgres from source for versions 15.17, 16.13, 17.9 and 18.3.</li>
<li>MySQL from source for version 8.4.7</li>
</ul>
</div>
<div><span style="font-family: inherit">I used a 48-core server from Hetzner</span></div>
<div>
<ul>
<li>an ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled</li>
<li>2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4</li>
<li>128G RAM</li>
<li>Ubuntu 24.04</li>
</ul>
<div>
<div><span style="font-family: inherit">Configuration files for Postgres:</span></div>
<div>
<ul>
<li><span style="font-family: inherit">the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions </span><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg157_o2nofp/conf.diff.cx10a_c32r128">15</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg163_o2nofp/conf.diff.cx10a_c32r128">16</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg17beta1_o2nofp/conf.diff.cx10a_c32r128">17</a>.</li>
<li>for Postgres 18 I used <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10b_c32r128" style="font-family: inherit">conf.diff.cx10b_c32r128</a><span style="font-family: inherit"> </span><span style="font-family: inherit">(x10b_c32r128) which is as close as possible to the Postgres 17 config and </span>uses io_method=sync</li>
</ul>
<div>
<div>
<div>
<div><b>Benchmark</b></div>
<div>
<div></div>
<div>I used sysbench and my usage is <a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. Normally I run 32 of the 42 microbenchmarks listed in that blog post using tables small enough to be cached by the DBMS. Most test only one type of SQL statement.</div>
<div></div>
<div>The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads &mdash; both more and less synthetic.</div>
<div></div>
<div>But I did things differently here:</div>
<div>
<ul style="text-align: left">
<li>I only run the write-heavy tests (to save time)</li>
<li>The tables are larger than memory and cannot be cached</li>
<li>Each test (microbenchmark) is run for 2 hours when I normally run each for 15 minutes</li>
<li>After each test a vacuum is done</li>
</ul>
</div>
</div>
</div>
<div>The purpose is to search for regressions from new CPU overhead and mutex contention related to MVCC GC (vacuum for Postgres, purge for InnoDB).</div>
</div>
<div></div>
<div>
<div style="font-family: inherit"><b>Results</b></div>
<div><span>
<div style="font-family: inherit"></div>
<div style="font-family: inherit">I provide charts below with relative QPS. The relative QPS is the following:</div>
<div style="font-family: inherit">
<div></div>
<blockquote><p>(QPS for some version) / (QPS for Postgres 15.17)</p></blockquote>
</div>
<div><span style="font-family: inherit">When the relative QPS is &gt; 1 then </span><i style="font-family: inherit">some version</i><span style="font-family: inherit"> is faster than <i>base version</i></span><span style="font-family: inherit">.&nbsp; When it is &lt; 1 then there might be a regression. When the relative QPS is 1.2 then <i>some version</i> is about 20% faster than </span><i>base version</i><span style="font-family: inherit">.</span></div>
<div><span style="font-family: inherit"><br></span></div>
<div><span style="font-family: inherit">The per-test results from vmstat and iostat </span><span style="font-family: inherit">can help to explain why something is faster or slower because it shows how much HW is used per request, including CPU overhead per operation (cpu/o) and context switches per operation (cs/o) which are often a proxy for mutex contention.</span></div>
<div><span style="font-family: inherit"><br></span></div>
<div><span style="font-family: inherit"><b>Results: writes</b></span></div>
<div><span style="font-family: inherit"><br></span></div>
<div><span style="font-family: inherit">The table below has relative QPS for Postgres 16 to 19 and then InnoDB all relative to the throughput for Postgres 15.17. Columns 1 to 4 have results for Postgres and the numbers in yellow highlight the tests where there is a regression in Postgres. For column 5 (MySQL with InnoDB) the numbers in yellow and red indicate tests where InnoDB&rsquo;s throughput is less than Postgres. And then the numbers in green indicate tests where InnoDB&rsquo;s throughput is much larger than Postgres.</span></div>
<div><span style="font-family: inherit"><br></span></div>
<div><span style="font-family: inherit">Note that when relative QPS (rQPS) is 0.90 then throughput dropped by ~10%.</span></div>
<div><span style="font-family: inherit"><br>Summary:
<ul style="text-align: left">
<li>throughput for Postgres drops after version 15.17. I don&rsquo;t know yet whether this is a regression.</li>
<li>throughput for InnoDB is much better than Postgres in 6 of 10 tests, similar in one test, and much worse in 3 of 10 tests.</li>
</ul>
<div>The sections that follow this one have more detail on results from the update-index, update-zipf tests and insert tests.</div>
<div></div>
<p></p></span></div>
<div><span>
<div><span style="font-family: courier">Relative to: Postgres 15.17</span></div>
<div><span style="font-family: courier">col-1 : Postgres 16.13</span></div>
<div><span style="font-family: courier">col-2 : Postgres 17.9</span></div>
<div><span style="font-family: courier">col-3 : Postgres 18.3</span></div>
<div><span style="font-family: courier">col-4 : Postgres 19 beta1</span></div>
<div><span style="font-family: courier">col-5 : MySQL 8.4.7</span></div>
<div><span style="font-family: courier"><br></span></div>
<div><span style="font-family: courier">col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5</span></div>
<div><span style="font-family: courier">0.94&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.02&nbsp; &nbsp; <span style="background-color: #d9ead3">1.88</span>&nbsp; &nbsp; update-inlist</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.94&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.88&nbsp; &nbsp; 0.92</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.43</span>&nbsp; &nbsp; update-index</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.91&nbsp; &nbsp; 0.86&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.92</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.19</span>&nbsp; &nbsp; update-nonindex</span></div>
<div><span style="font-family: courier">0.96&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; <span style="background-color: #f4cccc">0.71</span>&nbsp; &nbsp; update-one</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.92&nbsp; &nbsp; 0.83&nbsp; &nbsp; 0.81&nbsp; &nbsp; 0.85</span>&nbsp; &nbsp; <span style="background-color: #fff2cc">0.93</span>&nbsp; &nbsp; update-zipf</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.95&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.84&nbsp; &nbsp; 0.81</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.71</span>&nbsp; &nbsp; write-only</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.92</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.14</span>&nbsp; &nbsp; read-write_range=10</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95</span>&nbsp; &nbsp; <span style="background-color: #d9ead3">1.93</span>&nbsp; &nbsp; read-write_range=100</span></div>
<div><span style="font-family: courier"><span style="background-color: #fff2cc">0.89&nbsp; &nbsp; 0.82&nbsp; &nbsp; 0.80&nbsp; &nbsp; 0.84</span>&nbsp; &nbsp; 1.01&nbsp; &nbsp; delete</span></div>
<div><span style="font-family: courier">1.05&nbsp; &nbsp; 1.05&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.10&nbsp; &nbsp; <span style="background-color: #f4cccc">0.53</span>&nbsp; &nbsp; insert</span></div>
<div></div>
<div><b>Results: update-index</b></div>
<div></div>
<div>Summary:</div>
<div>
<ul style="text-align: left">
<li>Postgres suffers from too much variance</li>
<li>Average throughput is ~1.55X larger for InnoDB than for Postgres</li>
<li>Per operation, Postgres does ~1.20X more write IO (KB written) to storage than InnoDB</li>
<li>Per operation, InnoDB uses more CPU and does more context switches. While autovacuum was enabled and was likely running during the test, my measurements exclude the manual vacuum done at the end of each test.</li>
</ul>
</div>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4AIH6vKcDIjgnw-Y-8WvPaxGwZO3rBf_kDeSjiQBgtEH8XWXhDWd7Ft2gGXPwc_BXVxgXhSTn6AWmFjtJLB83l4Igbd6TUPAH9-8jf2IZ0gPGR0ixMoZdTqR4a9DJnQjrmltwkxKqDc9RWvtTCLO4N-TmU1BTSZhbh5P1GHESDSi6oN1OvV91UnPJdoxk/s600/update-index_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4AIH6vKcDIjgnw-Y-8WvPaxGwZO3rBf_kDeSjiQBgtEH8XWXhDWd7Ft2gGXPwc_BXVxgXhSTn6AWmFjtJLB83l4Igbd6TUPAH9-8jf2IZ0gPGR0ixMoZdTqR4a9DJnQjrmltwkxKqDc9RWvtTCLO4N-TmU1BTSZhbh5P1GHESDSi6oN1OvV91UnPJdoxk/w640-h396/update-index_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" width="640"></a></div>
<div>
<div><span style="font-family: courier;font-size: x-small">iostat, vmstat normalized by operation rate</span></div>
<div><span style="font-family: courier;font-size: x-small">r/s&nbsp; &nbsp; &nbsp;rMB/s&nbsp; &nbsp;w/s&nbsp; &nbsp; &nbsp;wMB/s&nbsp; &nbsp;r/o&nbsp; &nbsp; &nbsp;rKB/o&nbsp; &nbsp;wKB/o&nbsp; &nbsp;o/s&nbsp; &nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">35503.0 373.7&nbsp; &nbsp;58795.7 1345.1&nbsp; 1.375&nbsp; &nbsp;14.824&nbsp; <span style="background-color: #fff2cc">53.351</span>&nbsp; <span style="background-color: #fff2cc">25817</span>&nbsp; &nbsp;PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">33140.6 517.8&nbsp; &nbsp;53449.6 1735.3&nbsp; 0.827&nbsp; &nbsp;13.226&nbsp; <span style="background-color: #d9ead3">44.326</span>&nbsp; <span style="background-color: #d9ead3">40090</span>&nbsp; &nbsp;MySQL 8.4.7</span></div>
<div><span style="font-family: courier;font-size: x-small"><br></span></div>
<div><span style="font-family: courier;font-size: x-small">cs/s&nbsp; &nbsp; cpu/s&nbsp; &nbsp;cs/o&nbsp; &nbsp; cpu/o&nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">176167&nbsp; 14.4&nbsp; &nbsp; &nbsp;<span style="background-color: #d9ead3">6.824</span>&nbsp;&nbsp;<span style="background-color: #d9ead3">.000557</span> PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">661395&nbsp; 41.9&nbsp; &nbsp; <span style="background-color: #fff2cc">16.498</span>&nbsp; <span style="background-color: #fff2cc">.001046</span> MySQL 8.4.7</span></div>
</div>
<div><b><br></b></div>
<div><b>Results: update-zipf</b></div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>Postgres suffers from too much variance</li>
<li>Average throughput is ~1.09X larger for InnoDB than for Postgres</li>
<li>Per operation, Postgres does ~1.30X more write IO (KB written) to storage than InnoDB</li>
<li>Per operation, InnoDB uses more CPU and does more context switches. While autovacuum was enabled and was likely running during the test, my measurements exclude the manual vacuum done at the end of each test.</li>
</ul>
</div>
</div>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBpMvt9NeKpNXTBkF99Qdgjhy5cA4QFu__1yTl5KMkGpLKxlPUJ-WnNJAHDQI72w1-ySz3b-tmL6j2P5G-ogs0WNl9ZA0hezzA9CyxRSpigd87RoU1rgiCgpm1xjUEpNitGyq6eXGPUrECd2P7nWoQH_l504xfdelFU2bKb3yXJ18WpRLogreQXJCGxc6e/s600/update-zipf_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhBpMvt9NeKpNXTBkF99Qdgjhy5cA4QFu__1yTl5KMkGpLKxlPUJ-WnNJAHDQI72w1-ySz3b-tmL6j2P5G-ogs0WNl9ZA0hezzA9CyxRSpigd87RoU1rgiCgpm1xjUEpNitGyq6eXGPUrECd2P7nWoQH_l504xfdelFU2bKb3yXJ18WpRLogreQXJCGxc6e/w640-h396/update-zipf_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" width="640"></a></div>
<div>
<div><span style="font-family: courier;font-size: x-small">iostat, vmstat normalized by operation rate</span></div>
<div><span style="font-family: courier;font-size: x-small">r/s&nbsp; &nbsp; &nbsp;rMB/s&nbsp; &nbsp;w/s&nbsp; &nbsp; &nbsp;wMB/s&nbsp; &nbsp;r/o&nbsp; &nbsp; &nbsp;rKB/o&nbsp; &nbsp;wKB/o&nbsp; &nbsp;o/s&nbsp; &nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">55595.5 620.7&nbsp; &nbsp;64264.4 1352.3&nbsp; 0.622&nbsp; &nbsp;7.110&nbsp; &nbsp;<span style="background-color: #fff2cc">15.490</span>&nbsp; <span style="background-color: #fff2cc">89396</span>&nbsp; &nbsp;PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">27405.9 428.2&nbsp; &nbsp;37465.1 1133.6&nbsp; 0.282&nbsp; &nbsp;4.508&nbsp; &nbsp;<span style="background-color: #d9ead3">11.933</span>&nbsp; <span style="background-color: #d9ead3">97270</span>&nbsp; &nbsp;MySQL 8.4.7</span></div>
<div><span style="font-family: courier;font-size: x-small"><br></span></div>
<div><span style="font-family: courier;font-size: x-small">cs/s&nbsp; &nbsp; cpu/s&nbsp; &nbsp;cs/o&nbsp; &nbsp; cpu/o&nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">424392&nbsp; 27.2&nbsp; &nbsp; &nbsp;<span style="background-color: #d9ead3">4.747</span>&nbsp;&nbsp;<span style="background-color: #d9ead3">.000304</span> PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">1213054 44.5&nbsp; &nbsp; <span style="background-color: #fff2cc">12.471</span>&nbsp; <span style="background-color: #fff2cc">.000458</span> MySQL 8.4.7</span></div>
</div>
<div><b><br></b></div>
<div><b>Results: insert</b></div>
<p></p></span></div>
<p></p></span></div>
</div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>Postgres suffers from too much variance</li>
<li>Average throughput is ~2.06X larger for Postgres than for InnoDB</li>
<li>Per operation, Postgres does ~1.67X more write IO (KB written) to storage than InnoDB</li>
<li>Per operation, Postgres uses more CPU and does more context switches. This is the opposite of what happens above for update-index and update-zipf.</li>
</ul>
</div>
</div>
<div class="separator" style="clear: both;text-align: center"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWbY3tiGRuHceEWnN5sIEPuufQGA49wSYP1MmCAb1lGL79Jh0uvRyv2bGaQZo2uDTkWTfLVIONEObrQajeUZ1qxJPYZ6EnjMI9Nkb5XV3x6w_rrgNoqA9qtVDNsW09QcM89pht9VVBklAWeQPmfRt6zZuYU_YoXzo7VOINjy9gh5-b0GjN4WrW5AjaV3W-/s600/insert_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" style="margin-left: 1em;margin-right: 1em"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWbY3tiGRuHceEWnN5sIEPuufQGA49wSYP1MmCAb1lGL79Jh0uvRyv2bGaQZo2uDTkWTfLVIONEObrQajeUZ1qxJPYZ6EnjMI9Nkb5XV3x6w_rrgNoqA9qtVDNsW09QcM89pht9VVBklAWeQPmfRt6zZuYU_YoXzo7VOINjy9gh5-b0GjN4WrW5AjaV3W-/w640-h396/insert_%20Postgres%2019b1%20and%20MySQL%208.4.7.png" width="640"></a></div>
<div></div>
<div>
<div><span style="font-family: courier;font-size: x-small">iostat, vmstat normalized by operation rate</span></div>
<div><span style="font-family: courier;font-size: x-small">r/s&nbsp; &nbsp; &nbsp;rMB/s&nbsp; &nbsp;w/s&nbsp; &nbsp; &nbsp;wMB/s&nbsp; &nbsp;r/o&nbsp; &nbsp; &nbsp;rKB/o&nbsp; &nbsp;wKB/o&nbsp; &nbsp;o/s&nbsp; &nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">1615.5&nbsp; 56.0&nbsp; &nbsp; 15321.7 1170.9&nbsp; 0.007&nbsp; &nbsp;0.242&nbsp; &nbsp;<span style="background-color: #fff2cc">5.059</span>&nbsp; &nbsp;<span style="background-color: #d9ead3">237009</span>&nbsp; PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">3.6&nbsp; &nbsp; &nbsp;0.1&nbsp; &nbsp; &nbsp;8275.4&nbsp; 340.7&nbsp; &nbsp;0.000&nbsp; &nbsp;0.000&nbsp; &nbsp;<span style="background-color: #d9ead3">3.029</span>&nbsp; &nbsp;<span style="background-color: #fff2cc">115155</span>&nbsp; MySQL 8.4.7</span></div>
<div><span style="font-family: courier;font-size: x-small"><br></span></div>
<div><span style="font-family: courier;font-size: x-small">cs/s&nbsp; &nbsp; cpu/s&nbsp; &nbsp;cs/o&nbsp; &nbsp; cpu/o&nbsp; &nbsp;dbms</span></div>
<div><span style="font-family: courier;font-size: x-small">1214563 46.0&nbsp; &nbsp; <span style="background-color: #fff2cc">10.547</span>&nbsp; <span style="background-color: #fff2cc">.000399</span> PG 19b1</span></div>
<div><span style="font-family: courier;font-size: x-small">800827&nbsp; 50.5&nbsp; &nbsp; &nbsp;<span style="background-color: #d9ead3">3.379</span>&nbsp; <span style="background-color: #d9ead3">.000213</span> MySQL 8.4.7</span></div>
</div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/06/write-heavy-sysbench-tests-large-server.html">Write-heavy sysbench tests, a large server, modern Postgres and MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The insert benchmark on a small server, cached workload : Postgres 19 beta1</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server.html" />
      <id>https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server.html</id>
      <updated>2026-06-11T17:05:28+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for Postgres versions 19 beta1, 18.4 and 17.10 with the Insert Benchmark on a small server using a cached and CPU-bound workload.Postgres continues to be boring in a good way. It is hard to find performance regressions. tl;drI don\'t see regressions here in 19 beta1I see some improvements here in 19 beta1index create (l.x) is faster but the step is short-running so I don\'t assume much from thisthe write-heavy steps (l.i1, l.i2) are faster and CPU overhead is lower in 19 beta1, I hope to explain why the CPU overhead is lower, but that waits for another day.Builds, configuration and hardwareI compiled Postgres from source using -O2 -fno-omit-frame-pointer for versions 19 beta1, 18.4 and 17.10.The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.For 17.10 the config file is named conf.diff.cx10a_c8r32 (cx10a) and is here.For Postgres 18 and 19 the config file is conf.diff.cx10b_c8r32 (cx10b) which is as similar as possible to the config for version 17.The BenchmarkThe benchmark is explained here and is run with 1 client.The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 3600 seconds each.The benchmark steps are:l.i0insert 30M rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts 40M rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and 10M rows are inserted and deleted per table.Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResultsThe performance summary with charts is here.This table lists relative QPS per benchmark step and relative QPS is:    (QPS for my version / QPS for Postgres 17.10)The background in the table cells is blue for big improvements and yellow for regressions. There are no regressions here. The index create (l.x) step is much faster in 19.10. I usually ignore results on this step but I am curious if something was done in 19.10 to improve index create. But this step takes between 1 and 2 minutes and I am reluctant to assume too much from a short running step.For the write-heavy steps (l.i1, l.i2)there are small improvements in 18.4there are large improvements in 19 beta1. The CPU overhead is lower in 19 beta1 compared to 17.10, ~15% lower for l.i1 and ~10% lower for l.i2. Hopefully I can explain why. But the lower CPU overhead might explain the improved performance in 19 beta1. Some of the metrics from iostat and vmstat are here.dbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp100017.101.001.001.001.001.001.001.001.001.001.0018.41.001.031.021.070.991.001.001.001.011.0019 beta11.011.161.231.220.991.000.990.991.001.00</p>
<p><a href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server.html">The insert benchmark on a small server, cached workload : Postgres 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for Postgres versions 19 beta1, 18.4 and 17.10 with the <a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">Insert Benchmark</a> on a small server using a cached and CPU-bound workload.</p>

<p>Postgres continues to be boring in a good way. It is hard to find performance regressions.</p>
<p>&nbsp;tl;dr</p>

<ul style="text-align: left">
<li>I don&rsquo;t see regressions here in 19 beta1</li>
<li>I see some improvements here in 19 beta1</li>
<ul>
<li>index create (l.x) is faster but the step is short-running so I don&rsquo;t assume much from this</li>
<li>the write-heavy steps (l.i1, l.i2) are faster and CPU overhead is lower in 19 beta1, I hope to explain why the CPU overhead is lower, but that waits for another day.</li>
</ul>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled Postgres from source using&nbsp;<i>-O2 -fno-omit-frame-pointer</i>&nbsp;for versions 19 beta1, 18.4 and 17.10.</div>
<div>The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.</div>
<div></div>
<div>For 17.10 the config file is named conf.diff.cx10a_c8r32 (cx10a) and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg172_o2nofp/conf.diff.cx10a_c8r32">is here</a>.</div>

<div>For Postgres 18 and 19&nbsp;the config file is&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_c8r32">conf.diff.cx10b_c8r32</a>&nbsp;(cx10b) which is as similar as possible to the config for version 17.</div>
</div>
<div></div>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is <a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a> and is run with 1 client.</div>
<div></div>
<div>
<div>The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 3600 seconds each.</div>
</div>
<div></div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert 30M rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts 40M rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and 10M rows are inserted and deleted per table.</li>
<li>Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div><b>Results</b></div>
</div>
</div>
</div>
</div>
<div></div>
<div>The performance summary with charts <a href="https://mdcallag.github.io/reports/jun26.ib.pn52.mem.30m.50m.3600s.1u.pg/all.html#summary">is here</a>.</div>
<div></div>
<div>This table lists relative QPS per benchmark step and relative QPS is:<br>&nbsp; &nbsp; (QPS for my version / QPS for Postgres 17.10)
<p>The background in the table cells is blue for big improvements and yellow for regressions. There are no regressions here.&nbsp;</p></div>
<div></div>
<div>The index create (l.x) step is much faster in 19.10. I usually ignore results on this step but I am curious if something was done in 19.10 to improve index create. But this step takes between 1 and 2 minutes and I am reluctant to assume too much from a short running step.</div>
<div></div>
<div>For the write-heavy steps (l.i1, l.i2)</div>
<div>
<ul style="text-align: left">
<li>there are small improvements in 18.4</li>
<li>there are large improvements in 19 beta1. The CPU overhead is lower in 19 beta1 compared to 17.10, ~15% lower for l.i1 and ~10% lower for l.i2. Hopefully I can explain why. But the lower CPU overhead might explain the improved performance in 19 beta1. Some of the metrics from iostat and vmstat <a href="https://mdcallag.github.io/reports/jun26.ib.pn52.mem.30m.50m.3600s.1u.pg/all.html#l.i1.metrics">are here</a>.</li>
</ul>
</div>
<div>
<table border="1" cellpadding="8" style="color: black">
<tbody>
<tr>
<th><span style="font-size: x-small">dbms</span></th>
<th><span style="font-size: x-small">l.i0</span></th>
<th><span style="font-size: x-small">l.x</span></th>
<th><span style="font-size: x-small">l.i1</span></th>
<th><span style="font-size: x-small">l.i2</span></th>
<th><span style="font-size: x-small">qr100</span></th>
<th><span style="font-size: x-small">qp100</span></th>
<th><span style="font-size: x-small">qr500</span></th>
<th><span style="font-size: x-small">qp500</span></th>
<th><span style="font-size: x-small">qr1000</span></th>
<th><span style="font-size: x-small">qp1000</span></th>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">17.10</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">18.4</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.03</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.02</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.07</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
</tr>
<tr>
<td style="text-align: right"><span style="font-size: x-small">19 beta1</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.01</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.16</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.23</span></td>
<td id="chi" style="background-color: #81fff9;text-align: right"><span style="font-size: x-small">1.22</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">0.99</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00</span></td>
<td style="text-align: right"><span style="font-size: x-small">1.00<br></span></td>
</tr>
</tbody>
</table>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/06/the-insert-benchmark-on-small-server.html">The insert benchmark on a small server, cached workload : Postgres 19 beta1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB shortens maintenance period from 5 to 3 years</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/mariadb-shortens-maintenance-period-from-5-to-3-years/" />
      <id>https://www.fromdual.com/blog/mariadb-shortens-maintenance-period-from-5-to-3-years/</id>
      <updated>2026-06-11T07:34:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Somehow this news slipped past me: MariaDB has shortened the support period for the long-term releases of the MariaDB Community Server from 5 to 3 years. OK, I guess that’s not really surprising — I’ve been offline for a good month…<br />
MariaDB Server LTS Release Support Periods</p>
<p> Release<br />
 GA date<br />
 EoL date<br />
 Duration</p>
<p> 12.3<br />
 28 May 2026<br />
 Jun 2029<br />
 3 years</p>
<p> 11.8<br />
 4 Jun 2025<br />
 4 Jun 2028<br />
 3 years</p>
<p> 11.4<br />
 29 May 2024<br />
 29 May 2029<br />
 5 years</p>
<p> 10.11<br />
 16 Feb 2023<br />
 16 Feb 2028<br />
 5 years</p>
<p> 10.6<br />
 6 Jul 2021<br />
 6 Jul 2026<br />
 5 years</p>
<p> 10.5<br />
 24 Jun 2020<br />
 24 Jun 2025<br />
 5 years</p>
<p> 10.4<br />
 18 Jun 2019<br />
 18 Jun 2024<br />
 5 years</p>
<p> 10.3<br />
 25 May 2018<br />
 25 May 2023<br />
 5 years</p>
<p> 10.2<br />
 23 May 2017<br />
 23 May 2022<br />
 5 years</p>
<p> 10.1<br />
 17 Oct 2015<br />
 17 Oct 2020<br />
 5 years</p>
<p> 10.0<br />
 31 Mar 2014<br />
 31 Mar 2019<br />
 5 years</p>
<p>Source: MariaDB Server long-term release maintenance periods<br />
I am curious to see how all the distributions will handle this. They have significantly longer support periods, after all.<br />
And what about the competitors — the other databases?<br />
Debian</p>
<p>Debian Long Term Support (LTS) is a project to extend the lifetime of all Debian stable releases to (at least) 5 years.<br />
Source: Debian Long Term Support</p>
<p> Version<br />
 Name<br />
 Release<br />
 ext-LTS<br />
 EoL<br />
 Duration</p>
<p> Debian 13<br />
 trixie<br />
 2025-08-09<br />
 2030-07-01<br />
 2035-06-30<br />
 5 / 10 years</p>
<p> Debian 12<br />
 bookworm<br />
 2023-06-10<br />
 2028-07-01<br />
 2033-06-30<br />
 5 / 10 years</p>
<p> Debian 11<br />
 bullseye<br />
 2021-08-14<br />
 2026-09-01<br />
 2031-06-30<br />
 5 / 10 years</p>
<p> Debian 10<br />
 buster<br />
 2019-06-06<br />
 2024-07-01<br />
 2029-06-30<br />
 5 / 10 years</p>
<p> Debian 9<br />
 stretch<br />
 2017-06-17<br />
 2022-07-01<br />
 2027-06-30<br />
 5 / 10 years</p>
<p> Debian 8<br />
 jessie<br />
 2015-04-26<br />
 2020-07-01<br />
 2025-06-30<br />
 5 / 10 years</p>
<p> Debian 7<br />
 wheezy<br />
 2013-05-04<br />
 2018-06-01<br />
 2020-06-30<br />
 5 / 7 years</p>
<p>Source: Extended Long Term Support<br />
Ubuntu</p>
<p> Version<br />
 Name<br />
 Release<br />
 End of Support<br />
 EoL<br />
 Duration</p>
<p> Ubuntu 26.04 LTS<br />
 Resolute Raccoon<br />
 23. April 2026<br />
 May 2031<br />
 April 2041<br />
 5 / 15 years</p>
<p> Ubuntu 24.04 LTS<br />
 Noble Numbat<br />
 25. April 2024<br />
 June 2029<br />
 April 2039<br />
 5 / 15 years</p>
<p> Ubuntu 22.04 LTS<br />
 Jammy Jellyfish<br />
 21. April 2022<br />
 June 2027<br />
 April 2037<br />
 5 / 15 years</p>
<p> Ubuntu 20.04 LTS<br />
 Focal Fossa<br />
 23. April 2020<br />
 May 2025<br />
 April 2035<br />
 5 / 15 years</p>
<p> Ubuntu 18.04 LTS<br />
 Bionic Beaver<br />
 26. April 2018<br />
 June 2023<br />
 April 2033<br />
 5 / 15 years</p>
<p> Ubuntu 16.04 LTS<br />
 Xenial Xerus<br />
 21. April 2016<br />
 April 2021<br />
 April 2031<br />
 5 / 15 years</p>
<p> Ubuntu 14.04 LTS<br />
 Trusty Tahr<br />
 17. April 2014<br />
 April 2019<br />
 April 2029<br />
 5 / 15 years</p>
<p>Source: List of releases<br />
Rocky Linux</p>
<p> Release<br />
 Codename<br />
 Release Date<br />
 Active Support End<br />
 End of Life<br />
 Duration</p>
<p> Rocky Linux 10<br />
 Red Quartz<br />
 June 11, 2025<br />
 May 31, 2030<br />
 May 31, 2035<br />
 5 / 10 years</p>
<p> Rocky Linux 9<br />
 Blue Onyx<br />
 July 14, 2022<br />
 May 31, 2027<br />
 May 31, 2032<br />
 5 / 10 years</p>
<p> Rocky Linux 8<br />
 Green Obsidian<br />
 May 1, 2021<br />
 May 31, 2024<br />
 May 31, 2029<br />
 3 / 8 years</p>
<p>Source: Rocky Linux Release and Version Guide<br />
Oracle / MySQL Releases</p>
<p> Release<br />
 GA Date<br />
 Premier Support End<br />
 Extended Support End<br />
 Duration</p>
<p> MySQL 9.7<br />
 Apr 2026<br />
 Apr 2031<br />
 Apr 2034<br />
 5 / 8 years</p>
<p> MySQL 8.4<br />
 Apr 2024<br />
 Apr 2029<br />
 Apr 2032<br />
 5 / 8 years</p>
<p> MySQL 8.0<br />
 Apr 2018<br />
 Apr 2025<br />
 Apr 2026<br />
 7 years / 8 years</p>
<p> MySQL 5.7<br />
 Oct 2015<br />
 Oct 2020<br />
 Oct 2023<br />
 5 years / 8 years</p>
<p> MySQL 5.6<br />
 Feb 2013<br />
 Feb 2018<br />
 Feb 2021<br />
 5 years / 8 years</p>
<p> MySQL 5.5<br />
 Dec 2010<br />
 Dec 2015<br />
 Dec 2018<br />
 5 years / 8 years</p>
<p> MySQL 5.1<br />
 Dec 2008<br />
 Dec 2013<br />
 Not Available<br />
 5 years</p>
<p> MySQL 5.0<br />
 Oct 2005<br />
 Dec 2011<br />
 Not Available<br />
 6 years</p>
<p>Source: Oracle Lifetime Support Policy<br />
Percona<br />
Percona Distribution for PostgreSQL (PDPG) und Percona Server for MySQL (PS): At least 5 years, if I am interpreting the support matrix correctly…<br />
Source: Percona Release Lifecycle Overview<br />
OurSQL / VillageSQL<br />
No finished software is available yet, and thus no support policies, as far as I know. Is that even planned at all?<br />
Source: OurSQL und VillageSQL<br />
PostgreSQL</p>
<p> Version<br />
 First Release<br />
 Final Release<br />
 Duration</p>
<p> 18<br />
 September 25, 2025<br />
 November 14, 2030<br />
 5 years</p>
<p> 17<br />
 September 26, 2024<br />
 November 8, 2029<br />
 5 years</p>
<p> 16<br />
 September 14, 2023<br />
 November 9, 2028<br />
 5 years</p>
<p> 15<br />
 October 13, 2022<br />
 November 11, 2027<br />
 5 years</p>
<p> 14<br />
 September 30, 2021<br />
 November 12, 2026<br />
 5 years</p>
<p> 13<br />
 September 24, 2020<br />
 November 13, 2025<br />
 5 years</p>
<p> 12<br />
 October 3, 2019<br />
 November 21, 2024<br />
 5 years</p>
<p> 11<br />
 October 18, 2018<br />
 November 9, 2023<br />
 5 years</p>
<p> 10<br />
 October 5, 2017<br />
 November 10, 2022<br />
 5 years</p>
<p>Source: Versioning Policy<br />
Further sources</p>
<p>MariaDB 10.6 Changes &#038; Improvements<br />
MariaDB 10.6 is a long-term maintenance stable version. The first stable release was in July 2021, and it will be maintained until July 2026.<br />
MariaDB 10.11 Changes &#038; Improvements<br />
MariaDB 10.11 is a long-term maintenance release series, maintained until February 2028.<br />
MariaDB 11.4 Changes &#038; Improvements<br />
MariaDB 11.4 is a current long-term series, maintained until May 2029.<br />
MariaDB 11.8 Changes &#038; Improvements<br />
MariaDB 11.8 is a long-term release, maintained until June 2028.<br />
MariaDB 12.3 Changes &#038; Improvements<br />
MariaDB 12.3 is a long term release, maintained until June 2029.</p>
<p><a href="https://www.fromdual.com/blog/mariadb-shortens-maintenance-period-from-5-to-3-years/">MariaDB shortens maintenance period from 5 to 3 years</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Somehow this news slipped past me: MariaDB has shortened the support period for the long-term releases of the MariaDB Community Server from 5 to 3 years. OK, I guess that&rsquo;s not really surprising &mdash; I&rsquo;ve been offline for a good month&hellip;</p>
<h2 id="mariadb-server-lts-release-support-periods">MariaDB Server LTS Release Support Periods<a class="anchor-link" id="mariadb-server-lts-release-support-periods"></a></h2>
<table>
<thead>
<tr>
<th>Release</th>
<th>GA date</th>
<th>EoL date</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>12.3</td>
<td>28 May 2026</td>
<td>Jun 2029</td>
<td><strong>3 years</strong></td>
</tr>
<tr>
<td>11.8</td>
<td>4 Jun 2025</td>
<td>4 Jun 2028</td>
<td><strong>3 years</strong></td>
</tr>
<tr>
<td>11.4</td>
<td>29 May 2024</td>
<td>29 May 2029</td>
<td>5 years</td>
</tr>
<tr>
<td>10.11</td>
<td>16 Feb 2023</td>
<td>16 Feb 2028</td>
<td>5 years</td>
</tr>
<tr>
<td>10.6</td>
<td>6 Jul 2021</td>
<td>6 Jul 2026</td>
<td>5 years</td>
</tr>
<tr>
<td>10.5</td>
<td>24 Jun 2020</td>
<td>24 Jun 2025</td>
<td>5 years</td>
</tr>
<tr>
<td>10.4</td>
<td>18 Jun 2019</td>
<td>18 Jun 2024</td>
<td>5 years</td>
</tr>
<tr>
<td>10.3</td>
<td>25 May 2018</td>
<td>25 May 2023</td>
<td>5 years</td>
</tr>
<tr>
<td>10.2</td>
<td>23 May 2017</td>
<td>23 May 2022</td>
<td>5 years</td>
</tr>
<tr>
<td>10.1</td>
<td>17 Oct 2015</td>
<td>17 Oct 2020</td>
<td>5 years</td>
</tr>
<tr>
<td>10.0</td>
<td>31 Mar 2014</td>
<td>31 Mar 2019</td>
<td>5 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://mariadb.org/about/#maintenance-policy" target="_blank" rel="noopener">MariaDB Server long-term release maintenance periods</a></p>
<p>I am curious to see how all the distributions will handle this. They have significantly longer support periods, after all.</p>
<p>And what about the competitors &mdash; the other databases?</p>
<h2 id="debian">Debian<a class="anchor-link" id="debian"></a></h2>
<blockquote>
<p>Debian Long Term Support (LTS) is a project to extend the lifetime of all Debian stable releases to (at least) 5 years.</p>
</blockquote>
<p>Source: <a href="https://wiki.debian.org/LTS" target="_blank" rel="noopener">Debian Long Term Support</a></p>
<table>
<thead>
<tr>
<th>Version</th>
<th>Name</th>
<th>Release</th>
<th>ext-LTS</th>
<th>EoL</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>Debian 13</td>
<td>trixie</td>
<td>2025-08-09</td>
<td>2030-07-01</td>
<td>2035-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 12</td>
<td>bookworm</td>
<td>2023-06-10</td>
<td>2028-07-01</td>
<td>2033-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 11</td>
<td>bullseye</td>
<td>2021-08-14</td>
<td>2026-09-01</td>
<td>2031-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 10</td>
<td>buster</td>
<td>2019-06-06</td>
<td>2024-07-01</td>
<td>2029-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 9</td>
<td>stretch</td>
<td>2017-06-17</td>
<td>2022-07-01</td>
<td>2027-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 8</td>
<td>jessie</td>
<td>2015-04-26</td>
<td>2020-07-01</td>
<td>2025-06-30</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Debian 7</td>
<td>wheezy</td>
<td>2013-05-04</td>
<td>2018-06-01</td>
<td>2020-06-30</td>
<td>5 / 7 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://wiki.debian.org/LTS/Extended" target="_blank" rel="noopener">Extended Long Term Support</a></p>
<h2 id="ubuntu">Ubuntu<a class="anchor-link" id="ubuntu"></a></h2>
<table>
<thead>
<tr>
<th>Version</th>
<th>Name</th>
<th>Release</th>
<th>End of Support</th>
<th>EoL</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ubuntu 26.04 LTS</td>
<td>Resolute Raccoon</td>
<td>23. April 2026</td>
<td>May 2031</td>
<td>April 2041</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 24.04 LTS</td>
<td>Noble Numbat</td>
<td>25. April 2024</td>
<td>June 2029</td>
<td>April 2039</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 22.04 LTS</td>
<td>Jammy Jellyfish</td>
<td>21. April 2022</td>
<td>June 2027</td>
<td>April 2037</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 20.04 LTS</td>
<td>Focal Fossa</td>
<td>23. April 2020</td>
<td>May 2025</td>
<td>April 2035</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 18.04 LTS</td>
<td>Bionic Beaver</td>
<td>26. April 2018</td>
<td>June 2023</td>
<td>April 2033</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 16.04 LTS</td>
<td>Xenial Xerus</td>
<td>21. April 2016</td>
<td>April 2021</td>
<td>April 2031</td>
<td>5 / 15 years</td>
</tr>
<tr>
<td>Ubuntu 14.04 LTS</td>
<td>Trusty Tahr</td>
<td>17. April 2014</td>
<td>April 2019</td>
<td>April 2029</td>
<td>5 / 15 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://documentation.ubuntu.com/project/release-team/list-of-releases/" target="_blank" rel="noopener">List of releases</a></p>
<h2 id="rocky-linux">Rocky Linux<a class="anchor-link" id="rocky-linux"></a></h2>
<table>
<thead>
<tr>
<th>Release</th>
<th>Codename</th>
<th>Release Date</th>
<th>Active Support End</th>
<th>End of Life</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>Rocky Linux 10</td>
<td>Red Quartz</td>
<td>June 11, 2025</td>
<td>May 31, 2030</td>
<td>May 31, 2035</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Rocky Linux 9</td>
<td>Blue Onyx</td>
<td>July 14, 2022</td>
<td>May 31, 2027</td>
<td>May 31, 2032</td>
<td>5 / 10 years</td>
</tr>
<tr>
<td>Rocky Linux 8</td>
<td>Green Obsidian</td>
<td>May 1, 2021</td>
<td>May 31, 2024</td>
<td>May 31, 2029</td>
<td>3 / 8 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://wiki.rockylinux.org/rocky/version/#current-supported-releases" target="_blank" rel="noopener">Rocky Linux Release and Version Guide</a></p>
<h2 id="oracle--mysql-releases">Oracle / MySQL Releases<a class="anchor-link" id="oracle-mysql-releases"></a></h2>
<table>
<thead>
<tr>
<th>Release</th>
<th>GA Date</th>
<th>Premier Support End</th>
<th>Extended Support End</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>MySQL 9.7</td>
<td>Apr 2026</td>
<td>Apr 2031</td>
<td>Apr 2034</td>
<td>5 / 8 years</td>
</tr>
<tr>
<td>MySQL 8.4</td>
<td>Apr 2024</td>
<td>Apr 2029</td>
<td>Apr 2032</td>
<td>5 / 8 years</td>
</tr>
<tr>
<td>MySQL 8.0</td>
<td>Apr 2018</td>
<td>Apr 2025</td>
<td>Apr 2026</td>
<td>7 years / 8 years</td>
</tr>
<tr>
<td>MySQL 5.7</td>
<td>Oct 2015</td>
<td>Oct 2020</td>
<td>Oct 2023</td>
<td>5 years / 8 years</td>
</tr>
<tr>
<td>MySQL 5.6</td>
<td>Feb 2013</td>
<td>Feb 2018</td>
<td>Feb 2021</td>
<td>5 years / 8 years</td>
</tr>
<tr>
<td>MySQL 5.5</td>
<td>Dec 2010</td>
<td>Dec 2015</td>
<td>Dec 2018</td>
<td>5 years / 8 years</td>
</tr>
<tr>
<td>MySQL 5.1</td>
<td>Dec 2008</td>
<td>Dec 2013</td>
<td>Not Available</td>
<td>5 years</td>
</tr>
<tr>
<td>MySQL 5.0</td>
<td>Oct 2005</td>
<td>Dec 2011</td>
<td>Not Available</td>
<td>6 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://www.oracle.com/us/support/library/lifetime-support-technology-069183.pdf" target="_blank" rel="noopener">Oracle Lifetime Support Policy</a></p>
<h2 id="percona">Percona<a class="anchor-link" id="percona"></a></h2>
<p>Percona Distribution for PostgreSQL (PDPG) und Percona Server for MySQL (PS): At least 5 years, if I am interpreting the support matrix correctly&hellip;</p>
<p>Source: <a href="https://www.percona.com/release-lifecycle-overview/" target="_blank" rel="noopener">Percona Release Lifecycle Overview</a></p>
<h2 id="oursql--villagesql">OurSQL / VillageSQL<a class="anchor-link" id="oursql-villagesql"></a></h2>
<p>No finished software is available yet, and thus no support policies, as far as I know. Is that even planned at all?</p>
<p>Source: <a href="https://oursqlfoundation.org/" target="_blank" rel="noopener">OurSQL</a> und <a href="https://villagesql.com/" target="_blank" rel="noopener">VillageSQL</a></p>
<h2 id="postgresql">PostgreSQL<a class="anchor-link" id="postgresql"></a></h2>
<table>
<thead>
<tr>
<th>Version</th>
<th>First Release</th>
<th>Final Release</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td>18</td>
<td>September 25, 2025</td>
<td>November 14, 2030</td>
<td>5 years</td>
</tr>
<tr>
<td>17</td>
<td>September 26, 2024</td>
<td>November 8, 2029</td>
<td>5 years</td>
</tr>
<tr>
<td>16</td>
<td>September 14, 2023</td>
<td>November 9, 2028</td>
<td>5 years</td>
</tr>
<tr>
<td>15</td>
<td>October 13, 2022</td>
<td>November 11, 2027</td>
<td>5 years</td>
</tr>
<tr>
<td>14</td>
<td>September 30, 2021</td>
<td>November 12, 2026</td>
<td>5 years</td>
</tr>
<tr>
<td>13</td>
<td>September 24, 2020</td>
<td>November 13, 2025</td>
<td>5 years</td>
</tr>
<tr>
<td>12</td>
<td>October 3, 2019</td>
<td>November 21, 2024</td>
<td>5 years</td>
</tr>
<tr>
<td>11</td>
<td>October 18, 2018</td>
<td>November 9, 2023</td>
<td>5 years</td>
</tr>
<tr>
<td>10</td>
<td>October 5, 2017</td>
<td>November 10, 2022</td>
<td>5 years</td>
</tr>
</tbody>
</table>
<p>Source: <a href="https://www.postgresql.org/support/versioning/" target="_blank" rel="noopener">Versioning Policy</a></p>
<h2 id="further-sources">Further sources<a class="anchor-link" id="further-sources"></a></h2>
<ul>
<li><a href="https://mariadb.com/docs/release-notes/community-server/10.6/what-is-mariadb-106" target="_blank" rel="noopener">MariaDB 10.6 Changes &amp; Improvements</a><br>
<em>MariaDB 10.6 is a long-term maintenance stable version. The first stable release was in July 2021, and it will be maintained until July 2026.</em></li>
<li><a href="https://mariadb.com/docs/release-notes/community-server/10.11/what-is-mariadb-1011" target="_blank" rel="noopener">MariaDB 10.11 Changes &amp; Improvements</a><br>
<em>MariaDB 10.11 is a long-term maintenance release series, maintained until February 2028.</em></li>
<li><a href="https://mariadb.com/docs/release-notes/community-server/11.4/what-is-mariadb-114" target="_blank" rel="noopener">MariaDB 11.4 Changes &amp; Improvements</a><br>
<em>MariaDB 11.4 is a current long-term series, maintained until May 2029.</em></li>
<li><a href="https://mariadb.com/docs/release-notes/community-server/11.8/what-is-mariadb-118" target="_blank" rel="noopener">MariaDB 11.8 Changes &amp; Improvements</a><br>
<em>MariaDB 11.8 is a long-term release, maintained until June 2028.</em></li>
<li><a href="https://mariadb.com/docs/release-notes/community-server/12.3/mariadb-12.3-changes-and-improvements" target="_blank" rel="noopener">MariaDB 12.3 Changes &amp; Improvements</a><br>
<em>MariaDB 12.3 is a long term release, maintained until June 2029.</em></li>
</ul>

<p><a href="https://www.fromdual.com/blog/mariadb-shortens-maintenance-period-from-5-to-3-years/">MariaDB shortens maintenance period from 5 to 3 years</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Connector/C 3.4.9, and 3.3.19 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-connector-c-3-4-9-and-3-3-19-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-connector-c-3-4-9-and-3-3-19-now-available/</id>
      <updated>2026-06-10T17:42:44+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Connector/C 3.4.9, and 3.3.19. Download Now Release Notes and Changelogs […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-connector-c-3-4-9-and-3-3-19-now-available/">MariaDB Connector/C 3.4.9, and 3.3.19 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of MariaDB Connector/C 3.4.9, and 3.3.19. Download Now Notable items: Notable items: See the release notes and changelogs for more details and visit mariadb.com/downloads/connectors to download.</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-connector-c-3-4-9-and-3-3-19-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-connector-c-3-4-9-and-3-3-19-now-available/">MariaDB Connector/C 3.4.9, and 3.3.19 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Vector Support Upstreamed to Open-WebUI: Single-Database RAG Just Got Faster and Simpler</title>
      <link rel="alternate" type="text/html" href="https://shatteredsilicon.net/mariadb-vector-open-webui/" />
      <id>https://shatteredsilicon.net/mariadb-vector-open-webui/</id>
      <updated>2026-06-10T12:21:46+03:00</updated>
      <author><name>Gordan Bobic</name></author>
      <summary type="html"><![CDATA[<p>At Shattered Silicon, we live at the intersection of high-performance databases and production-grade AI. As an open-source contributor in the MariaDB ecosystem and a serious player bridging relational databases with modern AI workloads, we are excited to share our upstream contribution to one of the most popular self-hosted AI platforms: Open-WebUI. Why MariaDB Vector Changes […]<br />
The post MariaDB Vector Support Upstreamed to Open-WebUI: Single-Database RAG Just Got Faster and Simpler appeared first on Shattered Silicon.</p>
<p><a href="https://shatteredsilicon.net/mariadb-vector-open-webui/">MariaDB Vector Support Upstreamed to Open-WebUI: Single-Database RAG Just Got Faster and Simpler</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>At Shattered Silicon, we live at the intersection of high-performance databases and production-grade AI. As an open-source contributor in the MariaDB ecosystem and a serious player bridging relational databases with modern AI workloads, we are excited to share our upstream contribution to one of the most popular self-hosted AI platforms: Open-WebUI. Why MariaDB Vector Changes [&hellip;]</p>
<p>The post <a rel="nofollow" href="https://shatteredsilicon.net/mariadb-vector-open-webui/">MariaDB Vector Support Upstreamed to Open-WebUI: Single-Database RAG Just Got Faster and Simpler</a> appeared first on <a rel="nofollow" href="https://shatteredsilicon.net">Shattered Silicon</a>.</p>

<p><a href="https://shatteredsilicon.net/mariadb-vector-open-webui/">MariaDB Vector Support Upstreamed to Open-WebUI: Single-Database RAG Just Got Faster and Simpler</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 – May 2026’s releases: thank you for your contributions</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/" />
      <id>https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/</id>
      <updated>2026-06-10T11:38:37+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>On May… we have released an update of our 5 current LTS releases:<br />
These new releases contain a large amount of external contributions. The number of contributors is constantly growing, which is great! …<br />
Continue reading \"MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 – May 2026’s releases: thank you for your contributions\"<br />
The post MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 – May 2026’s releases: thank you for your contributions appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/">MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 – May 2026’s releases: thank you for your contributions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>On May&hellip; we have released an update of our 5 current LTS releases:<br>
These new releases contain a large amount of external contributions. The number of contributors is constantly growing, which is great! &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 &ndash; May 2026&rsquo;s releases: thank you for your contributions&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/">MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 &ndash; May 2026&rsquo;s releases: thank you for your contributions</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-server-12-3-11-8-11-4-10-11-10-6-may-2026s-releases-thank-you-for-your-contributions/">MariaDB Server 12.3, 11.8, 11.4, 10.11, 10.6 – May 2026’s releases: thank you for your contributions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>File and post data confusion in PHP</title>
      <link rel="alternate" type="text/html" href="https://www.sjoerdlangkemper.nl/2026/06/10/files-post-confusion-in-laminas/" />
      <id>https://www.sjoerdlangkemper.nl/2026/06/10/files-post-confusion-in-laminas/</id>
      <updated>2026-06-10T05:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>PHP has several superglobal variables which contain values from the request or the environment. These differ in whether they contain trustworthy data or not:</p>
<p><a href="https://www.sjoerdlangkemper.nl/2026/06/10/files-post-confusion-in-laminas/">File and post data confusion in PHP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>PHP has several superglobal variables which contain values from the request or the environment. These differ in whether they contain trustworthy data or not:</p>

<p><a href="https://www.sjoerdlangkemper.nl/2026/06/10/files-post-confusion-in-laminas/">File and post data confusion in PHP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Node.js Connector 3.5.3 and 3.4.6 now available</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-3-and-3-4-6-now-available/" />
      <id>https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-3-and-3-4-6-now-available/</id>
      <updated>2026-06-09T19:46:19+03:00</updated>
      <author><name>Daniel Bartholomew</name></author>
      <summary type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/Node.js 3.5.3 and 3.4.6 GA releases. Download Now Release […]</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-3-and-3-4-6-now-available/">MariaDB Node.js Connector 3.5.3 and 3.4.6 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB is pleased to announce the immediate availability of the MariaDB Connector/Node.js 3.5.3 and 3.4.6 GA releases. Download Now MariaDB Connector/Node.js 3.5.3 is a Stable (GA) release. Notable changes in this release include: MariaDB Connector/Node.js 3.4.6 is a Stable (GA) release. Notable changes in this release include: See&hellip;</p>
<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-3-and-3-4-6-now-available/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/mariadb-node-js-connector-3-5-3-and-3-4-6-now-available/">MariaDB Node.js Connector 3.5.3 and 3.4.6 now available</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Comprehensive Self-Service Backups for Continuous Data Protection in MariaDB Cloud</title>
      <link rel="alternate" type="text/html" href="https://mariadb.com/resources/blog/comprehensive-self-service-backups-for-continuous-data-protection-in-mariadb-cloud/" />
      <id>https://mariadb.com/resources/blog/comprehensive-self-service-backups-for-continuous-data-protection-in-mariadb-cloud/</id>
      <updated>2026-06-09T18:57:49+03:00</updated>
      <author><name>Naman Shah</name></author>
      <summary type="html"><![CDATA[<p>The MariaDB Cloud Backup Service provides organizations with a fully managed service for continuous data protection, mitigating risks from hardware […]</p>
<p><a href="https://mariadb.com/resources/blog/comprehensive-self-service-backups-for-continuous-data-protection-in-mariadb-cloud/">Comprehensive Self-Service Backups for Continuous Data Protection in MariaDB Cloud</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The MariaDB Cloud Backup Service provides organizations with a fully managed service for continuous data protection, mitigating risks from hardware failure, zonal disruptions, data corruption, and cyberattacks. By offering a comprehensive API and intuitive interface, this service allows companies to automate recovery strategies tailored to specific compliance and business continuity requirements.</p>
<p><a href="https://mariadb.com/resources/blog/comprehensive-self-service-backups-for-continuous-data-protection-in-mariadb-cloud/" rel="nofollow">Source</a></p>

<p><a href="https://mariadb.com/resources/blog/comprehensive-self-service-backups-for-continuous-data-protection-in-mariadb-cloud/">Comprehensive Self-Service Backups for Continuous Data Protection in MariaDB Cloud</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/" />
      <id>https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/</id>
      <updated>2026-06-09T16:30:22+03:00</updated>
      <author><name>Roman Nozdrin</name></author>
      <summary type="html"><![CDATA[<p>An early look at the DuckDB storage engine for MariaDB — columnar, vectorized analytics that live right next to your transactional tables.<br />
The problem<br />
MariaDB’s InnoDB is excellent at what it was built for: transactions. …<br />
Continue reading \"DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.\"<br />
The post DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack. appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/">DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>An early look at the DuckDB storage engine for MariaDB &mdash; columnar, vectorized analytics that live right next to your transactional tables.<br>
The problem<a id="the-problem"></a><br>
MariaDB&rsquo;s InnoDB is excellent at what it was built for: transactions. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/">DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/duckdb-storage-engine-for-mariadb-when-the-sea-lion-learns-to-quack/">DuckDB Storage Engine for MariaDB. When the Sea Lion Learns to Quack.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Managing ClickHouse Resources in Multi-Tenant  Environments</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/" />
      <id>https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/</id>
      <updated>2026-06-09T11:20:22+03:00</updated>
      <author><name>Sucahyo Ardy Prasetiyo</name></author>
      <summary type="html"><![CDATA[<p>When people first deploy ClickHouse, their initial reaction is often surprise. Queries that used to take minutes now finish in seconds. Dashboards feel instant even when reading billions of rows. To see this in action, here is a simple aggregation query running against a 200 million row events table: ClickHouse delivers exceptional speed, scanning 200 […]<br />
The post Managing ClickHouse Resources in Multi-Tenant Environments appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/">Managing ClickHouse Resources in Multi-Tenant  Environments</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When people first deploy ClickHouse, their initial reaction is often surprise. Queries that used to take minutes now finish in seconds. Dashboards feel instant even when reading billions of rows. </p>
<p>To see this in action, here is a simple aggregation query running against a 200 million row events table:</p>
<pre class="wp-block-code"><code>SELECT customer_id, count()
FROM my_db.events GROUP BY customer_id ORDER BY count() DESC;</code></pre>
<p>ClickHouse delivers exceptional speed, scanning 200 million rows in under a second on a 3-node cluster. This efficiency powers real-time analytics and observability platforms.</p>
<p>However, production environments are often multi-tenant, where dashboards, ETL pipelines, and background processes share CPU, memory, and disk resources. Without proper resource management, greedy workloads can saturate the system, causing performance degradation across all tasks.</p>
<p>This post explores operational strategies for managing ClickHouse in shared environments. Using a live 3-node replicated cluster with 200 million rows, we demonstrate how to identify contention, implement workload scheduling, and validate system stability.</p>
<h2 class="wp-block-heading" id="h-understanding-resource-contention-in-clickhouse">Understanding Resource Contention in ClickHouse<a class="anchor-link" id="understanding-resource-contention-in-clickhouse"></a></h2>
<p>ClickHouse is built for analytical processing. It scans large datasets fast by spreading work across many CPU threads at the same time. That design is what makes it so quick. But it also means that when multiple workloads run together, they start competing for the same resources at the same time.</p>
<p>This is different from databases like MySQL or PostgreSQL. In those systems, contention usually shows up as lock waits or transaction conflicts. In ClickHouse, the problem is almost always infrastructure saturation.</p>
<p>Take this query running against our 200 million row events table:</p>
<pre class="wp-block-code"><code>SELECT customer_id, count()
FROM my_db.events GROUP BY customer_id ORDER BY count() DESC;</code></pre>
<p>This query looks simple but it scans all 200 million rows, builds aggregation buffers in memory, uses multiple CPU threads in parallel, and reads a significant amount of data from disk. Run one and the cluster handles it fine. Run several at the same time and things start to break down. You can verify this directly by checking the query log:</p>
<pre class="wp-block-code"><code>SELECT query_duration_ms, read_rows, read_bytes, memory_usage
FROM system.query_log
WHERE type = 'QueryFinish' AND query LIKE '%customer_id%'
ORDER BY event_time DESC LIMIT 5;</code></pre>
<p>This gets even more complicated in multi-tenant environments. A tenant can be a different team, a different application, or a different customer all sharing the same cluster at the same time. The challenges are real. One heavy query slows down everyone else. </p>
<p>Without proper row policies data can leak between tenants. Without resource controls one tenant can consume everything and leave nothing for others. And without careful schema design, performance problems become very hard to fix later. These are not just performance problems. In multi-tenant environments they become operational risks.</p>
<p>When contention builds up, operators start noticing these symptoms: CPU stays near 100% even between queries, dashboard responses get slower, replication starts falling behind, merge queues keep growing, insert throughput drops, and network pressure is also real. In our 3 node setup every insert gets replicated to two other nodes at the same time. </p>
<p>During heavy inserts, replication traffic and query traffic compete for the same network interface and replication lag starts climbing:</p>
<pre class="wp-block-code"><code>SELECT replica_name, absolute_delay, queue_size, inserts_in_queue
FROM system.replicas
ORDER BY absolute_delay DESC;</code></pre>
<p>ClickHouse is not broken when this happens. It is doing exactly what it was designed to do, which is use every available resource to finish analytical work as fast as possible. The job of the operator is to make sure no single workload takes more than its fair share.</p>
<p><strong>That is what the rest of this article is about.</strong></p>
<h3 class="wp-block-heading" id="h-cpu-contention-and-thread-management">CPU Contention and Thread Management<a class="anchor-link" id="cpu-contention-and-thread-management"></a></h3>
<p>In shared ClickHouse environments, CPU contention is frequent because the system defaults to using maximum threads for speed. While effective for single queries, concurrent workloads compete for threads, overwhelming the CPU.</p>
<p>A common way to control this is with the <code>max_threads</code> setting: <code>SET max_threads = 4;</code></p>
<p>The first reaction most people have is, &ldquo;Why would I want to make my queries slower?&rdquo; The honest answer is that fewer threads does not always mean slower. Sometimes it means faster.</p>
<p>We tested this directly on our 3 node cluster with 200 million rows. We ran the same query under different conditions and checked the query log:</p>
<pre class="wp-block-code"><code>SELECT query_duration_ms, read_rows, Settings['max_threads'] AS max_threads 
FROM system.query_log
WHERE type = 'QueryFinish' AND query LIKE '%customer_id%' ORDER BY event_time DESC LIMIT 4;</code></pre>
<p>In a shared cluster the benefit becomes even more obvious. When 10 analysts run queries at the same time on a 32 core server and each query tries to use 16 threads, that is 160 threads competing for 32 cores. The CPU scheduler gets overwhelmed and everything slows down together. By giving each query fewer threads the cluster stays stable and responsive for everyone.</p>
<p>Think of it this way. A single lane highway moves fast until everyone tries to use it at once. Splitting into more lanes and slowing everyone down slightly keeps traffic moving for all users.</p>
<p><strong>When lowering <code>max_threads</code> makes sense:</strong></p>
<ul class="wp-block-list">
<li>A shared cluster where many users run queries at the same time</li>
<li>Dashboard workloads that need consistent low latency</li>
<li>Environments where insert pipelines and merges need to keep running alongside analytical queries</li>
</ul>
<p><strong>When raising <code>max_threads</code> makes sense:</strong></p>
<ul class="wp-block-list">
<li>A dedicated batch environment running a small number of heavy jobs</li>
<li>Overnight ETL workloads where the cluster is mostly idle</li>
<li>Single user environments where there is no competition for resources</li>
</ul>
<p>The most important thing to understand is that more threads is not always better. The right value always depends on your hardware, your data, and how many workloads are sharing the cluster at the same time.</p>
<h3 class="wp-block-heading" id="h-memory-management-and-query-stability">Memory Management and Query Stability<a class="anchor-link" id="memory-management-and-query-stability"></a></h3>
<p>Memory is the next resource that gets squeezed in a shared ClickHouse environment. Analytical queries are hungry for memory. Operations like GROUP BY, JOIN, sorting, DISTINCT, and distributed aggregations all need to build large temporary buffers while they run.</p>
<p>The setting that controls this is <code>SET max_memory_usage = '1G';</code></p>
<p>This limits how much memory a single query can use. Most people assume that giving queries more memory is always better because they finish faster. In practice that thinking is one of the fastest ways to destabilize a shared cluster.</p>
<p>Our 3 node cluster is a good real world example of this. Each node has 4GB of total RAM with no swap configured. Here is the actual memory picture on each node:</p>
<pre class="wp-block-code"><code>               total        used        free      
available
Mem:           4.0Gi       1.7Gi       2.1Gi       
2.3Gi
Swap:             0B          0B          0B</code></pre>
<p>ClickHouse is already consuming around 415MB just to keep the server running. That leaves roughly 2.3GB actually available for queries, merges, replication, and the operating system to share.</p>
<p>The default <code>max_memory_usage</code> is set to 0 which means unlimited. On a node with no swap that is dangerous. If a query tries to allocate more memory than the node has available, the operating system will immediately kill the ClickHouse process. There is no swap to fall back on. The process just dies. You can verify your current memory usage and limit with these queries:</p>
<pre class="wp-block-code"><code>SELECT metric, value FROM system.metrics WHERE metric LIKE '%Memory%';
SELECT name, value FROM system.settings WHERE name = 'max_memory_usage';</code></pre>
<p>On our cluster the result looks like this:</p>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="694" height="120" src="https://severalnines.com/wp-content/uploads/2026/05/clickhouse-system-metrics-memory-tracking.png" alt="ClickHouse system metrics output (likely from system.metrics) detailing active memory tracking counters, including total MemoryTracking at ~389.14 million bytes and serialization cache sizes." class="wp-image-43522"></figure>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="557" height="57" src="https://severalnines.com/wp-content/uploads/2026/05/clickhouse-settings-max-memory-usage.png" alt="Query output showing the max_memory_usage setting or profile parameter value, currently set to 0 (typically indicating unlimited or unrestricted memory for the context)." class="wp-image-43523"></figure>
<p>The fix is a per query limit in the user config and a server wide cap in <code>/etc/clickhouse-server/config.d/memory.xml</code>. For our environment we set <code>max_memory_usage</code> to 1GB per query and <code>max_server_memory_usage</code> to 3GB total. This leaves 1GB free for the OS, Keeper, and background processes.</p>
<p>When the limit is hit users will see <code>MEMORY_LIMIT_EXCEEDED</code>. That error is actually a good sign. It means the limit is working and protecting the node from going down entirely.</p>
<p>But setting limits too low creates the opposite problem. Some workloads genuinely need large buffers. If limits are too tight legitimate queries start failing.</p>
<p><strong>When lowering <code>max_memory_usage</code> makes sense:</strong></p>
<ul class="wp-block-list">
<li>A shared cluster with many concurrent users</li>
<li>Nodes with limited RAM and no swap like our environment</li>
<li>Environments prone to sudden traffic spikes</li>
</ul>
<p><strong>When raising <code>max_memory_usage</code> makes sense:</strong></p>
<ul class="wp-block-list">
<li>Isolated reporting workloads running on a schedule</li>
<li>Heavy ETL jobs running during off peak hours</li>
<li>Dedicated nodes with higher memory capacity</li>
</ul>
<p>On our 4GB nodes with no swap, keeping memory limits tight is not optional; it is what keeps the cluster alive.</p>
<h3 class="wp-block-heading" id="h-disk-i-o-and-merge-pressure">Disk I/O and Merge Pressure<a class="anchor-link" id="disk-i-o-and-merge-pressure"></a></h3>
<p>Disk behavior in ClickHouse is very different from most traditional databases because of how the MergeTree engine works. Every insert gets written as a new immutable part on disk. A background process continuously merges these small parts into larger ones to keep storage efficient and queries fast. Without merges, parts accumulate, queries slow down, and storage becomes fragmented.</p>
<p>The most common way operators create merge problems without realizing it is by inserting data in very small batches. We simulated this on our cluster by running 1000 single row inserts in a loop. The parts count jumped significantly with each insert. You can see this directly by checking parts before and after:</p>
<pre class="wp-block-code"><code>SELECT database, table, count() AS parts_count, sum(rows) AS total_rows
FROM system.parts
WHERE active = 1 AND database = 'my_db'
GROUP BY database, table;</code></pre>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="645" height="90" src="https://severalnines.com/wp-content/uploads/2026/05/clickhouse-table-parts-count-total-rows.png" alt="ClickHouse query result tracking table health metrics for my_db.events, indicating a parts_count of 71 across a massive dataset of 200 million total rows." class="wp-image-43524"></figure>
<p>Each tiny insert creates a new part on disk. This is what people call a merge explosion. The merge queue builds up faster than ClickHouse can clear it, disk I/O gets saturated from background merges competing with foreground queries, replication falls behind, and query performance drops because ClickHouse has to scan many more physical files.</p>
<p>The fix is simple. Insert data in large batches instead of small ones. Instead of 1 row at a time, insert at least 10,000 rows per batch. When we loaded 200 million rows in large batches the part count stayed manageable throughout.</p>
<p>You can monitor merge activity at any time with:</p>
<pre class="wp-block-code"><code>SELECT database, table, elapsed, progress, num_parts
FROM system.merges
ORDER BY elapsed DESC;</code></pre>
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="678" height="59" src="https://severalnines.com/wp-content/uploads/2026/05/clickhouse-active-merges-in-progress.png" alt="Monitoring output from system.merges showing an active background data part merge operation (merges_in_progress: 1) currently running on the my_db.events MergeTree table." class="wp-image-43525"></figure>
<p>Operators can also tune background merge concurrency with <code>background_pool_size</code>. Higher values help clear backlogs faster but on our 4GB nodes with no swap, more merge threads means more memory and disk I/O competing with foreground queries at the same time.</p>
<p><strong>When increasing <code>background_pool_size</code> makes sense:</strong></p>
<ul class="wp-block-list">
<li>Merge queues are growing consistently and not clearing</li>
<li>Disks have spare I/O capacity</li>
<li>Nodes have enough RAM to handle additional merge threads</li>
</ul>
<p><strong>When keeping <code>background_pool_size</code> lower makes sense:</strong></p>
<ul class="wp-block-list">
<li>Disks are already saturated</li>
<li>Nodes have limited RAM like our 4GB environment</li>
<li>Query latency is more important than insert throughput</li>
</ul>
<p>Higher values do not automatically mean better performance. On constrained hardware like ours, keeping merge concurrency modest is what keeps queries responsive while background work continues steadily.</p>
<h2 class="wp-block-heading" id="h-workload-scheduling-and-prioritization">Workload Scheduling and Prioritization<a class="anchor-link" id="workload-scheduling-and-prioritization"></a></h2>
<p>Even with thread and memory limits in place, a shared cluster still struggles when different workload types compete for the same resources at the same time. Dashboard queries need millisecond responses. ETL jobs can take minutes. Without scheduling, both are treated equally and dashboards suffer. ClickHouse solves this with a workload scheduling system that controls how disk IO, CPU threads, and query slots are shared between workloads.</p>
<pre class="wp-block-code"><code>root
&#9500;&#9472;&#9472; realtime
&#9474;   &#9500;&#9472;&#9472; dashboards
&#9474;   &#9492;&#9472;&#9472; api_queries
&#9500;&#9472;&#9472; batch
&#9474;   &#9500;&#9472;&#9472; etl
&#9474;   &#9492;&#9472;&#9472; exports
&#9492;&#9472;&#9472; background
   &#9500;&#9472;&#9472; merges
   &#9492;&#9472;&#9472; replication</code></pre>
<h3 class="wp-block-heading" id="h-scheduling-hierarchy-and-resource-definitions">Scheduling Hierarchy and Resource Definitions<a class="anchor-link" id="scheduling-hierarchy-and-resource-definitions"></a></h3>
<p>The foundation of workload scheduling in ClickHouse is the concept of a resource. A resource represents a shared physical asset that multiple workloads compete for. ClickHouse supports three types: disk IO, CPU threads, and query slots.</p>
<p>Start by defining what resources exist on your cluster:</p>
<pre class="wp-block-code"><code>CREATE RESOURCE disk_read (READ ANY DISK);
CREATE RESOURCE disk_write (WRITE ANY DISK);
CREATE RESOURCE cpu (MASTER THREAD, WORKER THREAD);
CREATE RESOURCE query (QUERY);</code></pre>
<p>The READ and WRITE disk definitions are important. They let you control read and write IO separately. In a shared cluster, dashboard read traffic and insert write traffic compete for the same disk bandwidth. Separating them gives you independent control over each.</p>
<p>Once resources are defined, build a workload hierarchy on top of them. The root workload sits at the top and distributes resources down to everything below it:</p>
<pre class="wp-block-code"><code>CREATE WORKLOAD root
SETTINGS
    max_concurrent_threads = 50,
    max_concurrent_queries = 50,
    max_queries_per_second = 20;

CREATE WORKLOAD realtime IN root SETTINGS priority = 1;
CREATE WORKLOAD batch IN root SETTINGS priority = 10;
CREATE WORKLOAD background IN root SETTINGS priority = 100;</code></pre>
<p>You can also apply bandwidth limits per resource directly on a workload. This caps read bandwidth at 100 MB/s and write bandwidth at 50 MB/s:</p>
<pre class="wp-block-code"><code>CREATE WORKLOAD all IN root
SETTINGS
    max_bytes_per_second = 104857600 FOR disk_read,
    max_bytes_per_second = 52428800 FOR disk_write;</code></pre>
<p>The root workload manages resource distribution across the hierarchy. High-priority &ldquo;realtime&rdquo; traffic like dashboards requires fast, consistent responses. The &ldquo;batch&rdquo; branch handles latency-tolerant tasks such as ETL pipelines, while &ldquo;background&rdquo; operations like replication run steadily without impacting foreground performance.</p>
<p>You can verify which workloads exist on your cluster:</p>
<pre class="wp-block-code"><code>SELECT * FROM system.workloads;
SELECT * FROM system.resources;</code></pre>
<p>In ClickHouse lower priority numbers mean higher priority. Realtime gets served first, then batch, then background. Assign users to workloads by creating dedicated users:</p>
<pre class="wp-block-code"><code>CREATE USER dashboard_user IDENTIFIED BY 'dashboard123'
SETTINGS workload = 'realtime';

CREATE USER analyst IDENTIFIED BY 'analyst123'
SETTINGS workload = 'batch';</code></pre>
<p>You can also assign workloads through the user config file for existing users. Add the workload setting to <code>/etc/clickhouse-server/users.d/default-password.xml</code></p>
<p><strong>N.B. A common mistake is giving all workloads equal priority.</strong> When a heavy batch job and a lightweight dashboard query compete equally, the batch job almost always wins because it consumes more resources per query. Proper prioritization flips this; the batch job still runs, it just waits its turn when realtime traffic needs resources first.</p>
<h3 class="wp-block-heading" id="h-memory-overcommit-and-query-queueing">Memory Overcommit and Query Queueing<a class="anchor-link" id="memory-overcommit-and-query-queueing"></a></h3>
<p>Memory overcommit controls what happens when total memory demand from all running queries exceeds what is physically available. On our 4GB nodes with no swap this is critical. Without overcommit controls, if multiple queries simultaneously try to allocate more memory than is available the OS kills the ClickHouse process immediately.</p>
<p>ClickHouse handles this by waiting briefly for other queries to release memory before terminating the most overcommitted query first. This is much safer than having no limit at all:</p>
<pre class="wp-block-code"><code>SET max_memory_usage = 1073741824;
SET memory_usage_overcommit_max_wait_microseconds = 5000000;</code></pre>
<p>Instead of the entire node going down, only the most memory hungry query gets cancelled. Everything else keeps running. On our 4GB nodes this is the difference between a graceful query failure and a full cluster crash.</p>
<p>Query queueing handles overload at the concurrency level. When more queries arrive than the cluster can handle they queue up instead of all running at once. You can set this at the server level <code>in /etc/clickhouse-server/config.d/cluster.xml</code>. Or via workload scheduling:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE WORKLOAD root SETTINGS
    max_concurrent_threads = 50,
    max_concurrent_queries = 50,
    max_queries_per_second = 20;</code></pre>
<p>On our 4GB nodes, 50 concurrent queries is a safe ceiling. New queries that arrive when the limit is hit wait for a slot instead of crashing the node. Monitor active and queued queries at any time:</p>
<pre class="wp-block-code"><code>SELECT query, elapsed, memory_usage, read_rows
FROM system.processes
ORDER BY elapsed DESC;</code></pre>
<p>When lowering <code>max_concurrent_queries</code> makes sense:</p>
<ul class="wp-block-list">
<li>Nodes with limited RAM like our 4GB environment</li>
<li>Clusters with no swap configured</li>
<li>Environments where query stability matters more than raw throughput</li>
</ul>
<p>When raising <code>max_concurrent_queries</code> makes sense:</p>
<ul class="wp-block-list">
<li>Nodes with large amounts of RAM and fast disks</li>
<li>Clusters serving many lightweight queries simultaneously</li>
<li>Environments where queries are short and memory usage per query is low</li>
</ul>
<p>A cluster managing a queue is more often more stable than one where unlimited queries run simultaneously. On constrained hardware, queueing is not a limitation but what keeps the cluster alive under pressure. When workload scheduling makes the most difference:</p>
<ul class="wp-block-list">
<li>Customer facing dashboards sharing a cluster with internal ETL jobs</li>
<li>Clusters serving multiple teams with different SLA requirements</li>
<li>Environments where insert pipelines and analytical queries run simultaneously</li>
</ul>
<p><strong>N.B. The goal is not to make batch jobs slow.</strong> The goal is to make sure realtime workloads stay fast even when the cluster is under pressure.</p>
<h2 class="wp-block-heading" id="h-isolation-strategies-in-multi-tenant-environments">Isolation Strategies in Multi-Tenant Environments<a class="anchor-link" id="isolation-strategies-in-multi-tenant-environments"></a></h2>
<p>Everything we have covered so far assumes different workloads share the same cluster. That works well up to a point; but, some organizations eventually reach a scale where sharing creates too much risk. One bad query from one tenant can still affect everyone else no matter how carefully the limits are tuned &mdash; this is the noisy neighbor problem. The solution depends on how much isolation you actually need. There are three main ways organizations handle this depending on their scale and operational maturity.</p>
<h3 class="wp-block-heading" id="h-approach-1-shared-cluster-with-schema-level-isolation">Approach 1: Shared Cluster with Schema Level Isolation<a class="anchor-link" id="approach-1-shared-cluster-with-schema-level-isolation"></a></h3>
<p>This is the most common starting point. All tenants share the same cluster and the same table. Isolation is handled through schema design and row policies.</p>
<p>The most important thing to get right is the schema. Including <code>tenant_id</code> in the sorting key makes a significant difference:</p>
<pre class="wp-block-code"><code>CREATE TABLE my_db.events ON CLUSTER my_cluster
(
    tenant_id       UInt32,
    event_date      Date,
    event_id        UInt64,
    customer_id     UInt32,
    event_type      LowCardinality(String),
    event_timestamp DateTime,
    metadata        String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (tenant_id, event_date, customer_id, event_id);</code></pre>
<p>With <code>tenant_id</code> first in the sort key, ClickHouse physically stores each tenant&rsquo;s data together on disk. A query filtering by <code>tenant_id</code> only reads that tenant&rsquo;s data and skips everything else.</p>
<p>For dashboard workloads, pre-aggregate data per tenant into materialized views instead of letting tenants query the raw table directly:</p>
<pre class="wp-block-code"><code>CREATE MATERIALIZED VIEW my_db.events_tenant1_mv
ENGINE = SummingMergeTree()
ORDER BY (event_date, customer_id)
AS SELECT
    event_date,
    customer_id,
    count() AS event_count
FROM my_db.events
WHERE tenant_id = 1
GROUP BY event_date, customer_id;</code></pre>
<p>This keeps tenant queries physically separated and pre-computed so one tenant&rsquo;s heavy scan cannot slow down another&rsquo;s dashboard.</p>
<p>Then enforce data isolation with restrictive row policies:</p>
<pre class="wp-block-code"><code>-- Grant access
GRANT SELECT ON my_db.events TO tenant1_user;
GRANT SELECT ON my_db.events TO tenant2_user;

-- Create restrictive row policies
CREATE ROW POLICY tenant1_policy ON my_db.events
AS RESTRICTIVE
FOR SELECT USING tenant_id = 1
TO tenant1_user;

CREATE ROW POLICY tenant2_policy ON my_db.events
AS RESTRICTIVE
FOR SELECT USING tenant_id = 2
TO tenant2_user;

-- Verify policies
SELECT short_name, select_filter, is_restrictive, apply_to_list
FROM system.row_policies
WHERE table = 'events';</code></pre>
<p>On our cluster with 200 million rows distributed across 5 tenants, each tenant user can only see their own 40 million rows and gets zero results when querying other tenant data.</p>
<h3 class="wp-block-heading" id="h-approach-2-database-level-isolation">Approach 2: Database Level Isolation<a class="anchor-link" id="approach-2-database-level-isolation"></a></h3>
<p>A step up from row policies. Each tenant gets their own database but shares the same cluster infrastructure:</p>
<pre class="wp-block-code"><code>CREATE DATABASE tenant1_db ON CLUSTER my_cluster;
CREATE DATABASE tenant2_db ON CLUSTER my_cluster;
CREATE TABLE tenant1_db.events ON CLUSTER my_cluster
(
    event_date      Date,
    event_id        UInt64,
    customer_id     UInt32,
    event_type      LowCardinality(String),
    event_timestamp DateTime,
    metadata        String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/tenant1/events', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, customer_id, event_id);</code></pre>
<p>This gives cleaner separation and makes per tenant storage, backups, and access controls easier to manage. The tradeoff is more tables to maintain as tenant count grows.</p>
<h3 class="wp-block-heading" id="h-approach-3-cluster-level-isolation">Approach 3: Cluster Level Isolation<a class="anchor-link" id="approach-3-cluster-level-isolation"></a></h3>
<p>The strongest form of isolation. Different workload types get entirely separate clusters:</p>
<figure class="wp-block-table">
<table class="has-fixed-layout">
<tbody>
<tr>
<td>Ingestion cluster</td>
<td>handles high throughput data loading</td>
</tr>
<tr>
<td>Dashboard cluster</td>
<td>optimized for low latency concurrent reads</td>
</tr>
<tr>
<td>ETL cluster</td>
<td>reserved for heavy transformation jobs</td>
</tr>
</tbody>
</table>
</figure>
<p>This eliminates the noisy neighbor problem completely. The tradeoff is higher infrastructure cost and more operational complexity.</p>
<h3 class="wp-block-heading" id="h-using-settings-to-prioritize-workloads">Using Settings to Prioritize Workloads<a class="anchor-link" id="using-settings-to-prioritize-workloads"></a></h3>
<p>Beyond isolation approach, individual query settings give operators per query control over resource consumption per tenant without changing global settings:</p>
<pre class="wp-block-code"><code>-- Heavy report query - limit resources
SELECT customer_id, count()
FROM my_db.events
WHERE tenant_id = 1
GROUP BY customer_id
SETTINGS max_threads = 4, max_memory_usage = 1073741824, workload = 'batch';

-- Dashboard query - allow more resources
SELECT count()
FROM my_db.events
WHERE tenant_id = 1
AND event_date = today()
SETTINGS max_threads = 8, workload = 'realtime';</code></pre>
<p>A heavy analytical report from one tenant can be throttled while their dashboard queries remain fast.</p>
<h3 class="wp-block-heading" id="h-choosing-the-right-approach">Choosing the Right Approach<a class="anchor-link" id="choosing-the-right-approach"></a></h3>
<figure class="wp-block-table">
<table class="has-fixed-layout">
<tbody>
<tr>
<td>Approach</td>
<td>Best for</td>
<td>Tradeoff</td>
</tr>
<tr>
<td>Shared cluster with row policies</td>
<td>Small tenant count, limited hardware</td>
<td>Noisy neighbor risk remains</td>
</tr>
<tr>
<td>Separate databases per tenant</td>
<td>Medium tenant count, cleaner isolation</td>
<td>More tables to manage</td>
</tr>
<tr>
<td>Dedicated clusters</td>
<td>Large scale, strict SLAs</td>
<td>Higher cost and complexity</td>
</tr>
</tbody>
</table>
</figure>
<p>Most organizations start with Approach 1, move to Approach 2 as tenant count grows, and only adopt Approach 3 when SLA requirements become strict enough to justify the cost. For our 3 node cluster with 4GB RAM per node, Approach 1 with row policies, materialized views, and workload assignment is the most practical starting point.</p>
<h2 class="wp-block-heading" id="h-operational-best-practices">Operational Best Practices<a class="anchor-link" id="operational-best-practices"></a></h2>
<p>Resource issues in ClickHouse rarely announce themselves immediately. A cluster can look perfectly healthy from the outside while merge queues, memory pressure, or replication lag quietly build up internally. By the time users start complaining the problem has usually been growing for a while. This is why operational visibility and proper configuration are just as important as the tuning settings we covered in earlier sections.</p>
<h3 class="wp-block-heading" id="h-setting-up-workload-classes-and-assigning-quotas">Setting Up Workload Classes and Assigning Quotas<a class="anchor-link" id="setting-up-workload-classes-and-assigning-quotas"></a></h3>
<pre class="wp-block-code"><code>CREATE WORKLOAD realtime IN root SETTINGS priority = 1;
CREATE WORKLOAD batch IN root SETTINGS priority = 10;
CREATE WORKLOAD background IN root SETTINGS priority = 100;</code></pre>
<p>Then assign users to workloads:</p>
<pre class="wp-block-code"><code>CREATE USER dashboard_user IDENTIFIED BY 'dashboard123'
SETTINGS workload = 'realtime';

CREATE USER analyst IDENTIFIED BY 'analyst123'
SETTINGS workload = 'batch';</code></pre>
<p>Quotas add a second layer of control on top of workload priority. Even if a user has high priority, quotas prevent them from consuming unlimited resources over time:</p>
<pre class="wp-block-code"><code>-- Tenant users: 1000 queries per hour, max 10 billion rows read
CREATE QUOTA tenant_quota
    FOR INTERVAL 1 HOUR
    MAX queries = 1000,
    MAX read_rows = 10000000000
    TO tenant1_user, tenant2_user;

-- Analysts: 100 queries per hour, max 5 billion rows read
CREATE QUOTA analyst_quota
    FOR INTERVAL 1 HOUR
    MAX queries = 100,
    MAX read_rows = 5000000000
    TO analyst;</code></pre>
<h2 class="wp-block-heading" id="h-monitoring-resource-usage">Monitoring Resource Usage<a class="anchor-link" id="monitoring-resource-usage"></a></h2>
<p><a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">Good monitoring practices</a> catch problems before they become visible to users. On our 3 node cluster with 200 million rows we focus on five key signals.</p>
<p><strong>Query latency</strong> is usually the first visible sign of contention:</p>
<pre class="wp-block-code"><code>SELECT query_duration_ms, read_rows, memory_usage, query
FROM system.query_log
WHERE type = 'QueryFinish'
AND event_time &gt;= now() - INTERVAL 10 MINUTE
ORDER BY query_duration_ms DESC
LIMIT 10;</code></pre>
<p><strong>Replication lag</strong> signals network pressure or overloaded replicas:</p>
<pre class="wp-block-code"><code>SELECT replica_name, absolute_delay, queue_size, inserts_in_queue
FROM system.replicas
ORDER BY absolute_delay DESC;</code></pre>
<p><strong>Parts growth</strong> indicates merge pressure from small inserts:</p>
<pre class="wp-block-code"><code>SELECT database, table, count() AS parts_count, sum(rows) AS total_rows
FROM system.parts
WHERE active = 1 AND database = 'my_db'
GROUP BY database, table
ORDER BY parts_count DESC;</code></pre>
<p>For a full cluster health snapshot combine all signals into one query:</p>
<pre class="wp-block-code"><code>SELECT
    (SELECT count() FROM system.processes) AS active_queries,
    (SELECT count() FROM system.merges) AS active_merges,
    (SELECT max(absolute_delay) FROM system.replicas) AS max_replication_delay,
    (SELECT max(queue_size) FROM system.replicas) AS max_replication_queue,
    (SELECT count() FROM system.parts WHERE active = 1 AND database = 'my_db') AS parts_count,
    (SELECT value FROM system.metrics WHERE metric = 'MemoryTracking' LIMIT 1) AS memory_used_bytes;</code></pre>
<p>Run this regularly and you will catch problems before they reach users.</p>
<p>One important thing to keep in mind is that tuning ClickHouse rarely eliminates a bottleneck completely. It usually just moves it somewhere else. Increasing <code>background_pool_size</code> may clear the merge queue faster but adds more disk I/O pressure. Lowering <code>max_memory_usage</code> may stabilize the cluster but some queries will start failing. The system tables covered in this section are your best tool for observing exactly what changed after each adjustment.</p>
<p>The best way to understand ClickHouse resource management is not to read about it but to test it directly on a real cluster with real data and watch what happens.</p>
<h2 class="wp-block-heading" id="h-integrating-with-ops-tooling">Integrating with Ops Tooling<a class="anchor-link" id="integrating-with-ops-tooling"></a></h2>
<p>Running ClickHouse in production is not just about tuning settings and writing good queries. At some point the cluster needs to integrate with the broader operational infrastructure that the rest of your organization already uses. Alerts need to fire before users notice problems. Capacity needs to grow before resource pressure becomes a crisis. And in organizations running multiple database technologies, policies need to be enforced consistently across all of them.</p>
<h3 class="wp-block-heading" id="h-alerts-for-resource-exhaustion">Alerts for Resource Exhaustion<a class="anchor-link" id="alerts-for-resource-exhaustion"></a></h3>
<p>ClickHouse exposes metrics via its HTTP interface that can be scraped by Prometheus or any compatible monitoring system: <code>curl http://server1:8123/metrics</code></p>
<figure class="wp-block-table">
<table class="has-fixed-layout">
<tbody>
<tr>
<td>Metric</td>
<td>Warning</td>
<td>Critical</td>
</tr>
<tr>
<td>Memory usage</td>
<td>&gt; 2.5GB</td>
<td>&gt; 3GB</td>
</tr>
<tr>
<td>Replication delay</td>
<td>&gt; 30s</td>
<td>&gt; 300s</td>
</tr>
<tr>
<td>Active merges</td>
<td>&gt; 10</td>
<td>&gt; 20</td>
</tr>
<tr>
<td>Concurrent queries</td>
<td>&gt; 40</td>
<td>&gt; 50</td>
</tr>
<tr>
<td>Parts count</td>
<td>&gt; 500</td>
<td>&gt; 1000</td>
</tr>
</tbody>
</table><figcaption class="wp-element-caption"><strong>Recommended alert thresholds for our 4GB nodes</strong></figcaption></figure>
<h2 class="wp-block-heading" id="h-scaling-out-nodes-and-shards-when-resource-pressure-builds">Scaling Out Nodes and Shards When Resource Pressure Builds<a class="anchor-link" id="scaling-out-nodes-and-shards-when-resource-pressure-builds"></a></h2>
<p>Our current setup is 1 shard with 3 replicas. When pressure builds consistently across all nodes it is a signal to grow. Scale up by adding more CPU or memory to existing nodes. Scale out by adding more shards to distribute data and query load across more hardware.</p>
<p>Signs it is time to scale out:</p>
<ul class="wp-block-list">
<li>CPU stays above 80% consistently,</li>
<li>memory errors appear regularly,</li>
<li>merge queues keep growing despite tuning,</li>
<li>and replication lag keeps climbing.</li>
</ul>
<h2 class="wp-block-heading" id="h-using-unified-management-to-enforce-policies-across-databases">Using Unified Management to Enforce Policies Across Databases<a class="anchor-link" id="using-unified-management-to-enforce-policies-across-databases"></a></h2>
<p>Organizations running ClickHouse alongside MySQL or PostgreSQL face the challenge of managing resource policies, backups, and monitoring separately for each technology. Purpose built database management tools provide a unified management layer across heterogeneous database environments. From a single interface operators can monitor ClickHouse alongside other databases, enforce consistent backup policies, manage user access across multiple clusters, and get unified alerting across all database technologies. The goal is not to replace ClickHouse native tooling. It is to reduce operational overhead when running ClickHouse as part of a larger database fleet.</p>
<h2 class="wp-block-heading" id="h-conclusion">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>ClickHouse is genuinely fast. But in shared environments, speed without governance becomes a liability. Throughout this article we ran real workloads against 200 million rows on constrained 4GB nodes to show exactly how contention happens and how to control it.<br>The key takeaways are simple. </p>
<p>Lower <code>max_threads</code> in shared clusters. Set <code>max_memory_usage</code> explicitly especially on nodes with no swap. Insert in large batches to avoid merge explosions. Assign workload classes so dashboards always get priority over batch jobs. Put your primary identifier column first in the sort key and enforce row policies per tenant.</p>
<p>Before going to production with a shared cluster run through this quick checklist:</p>
<p>Primary identifier column is first in the sort key<br>Row policies are set to <code>AS RESTRICTIVE</code><br><code>max_memory_usage</code> and <code>max_server_memory_usage</code> are explicitly set<br>Workload hierarchy is defined with realtime, batch, and background<br>Quotas are assigned per user type<br>Inserts are batched at minimum 10,000 rows<br>Health snapshot query is running regularly</p>
<p>Good ClickHouse operations are not about maximizing every resource. They are about finding the right balance between throughput, latency, fairness, and stability for your specific workload.</p>
<p>The post <a href="https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/">Managing ClickHouse Resources in Multi-Tenant  Environments</a> appeared first on <a href="https://severalnines.com">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/managing-clickhouse-resources-in-multi-tenant-environments/">Managing ClickHouse Resources in Multi-Tenant  Environments</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Operator for MySQL (PXC) 1.20.0: Automatic Storage Resizing, TLS Certificate Rotation, and ARM64 Support</title>
      <link rel="alternate" type="text/html" href="https://www.percona.com/blog/percona-operator-for-mysql-pxc-1-20-0-automatic-storage-resizing-tls-rotation-arm64/" />
      <id>https://www.percona.com/blog/percona-operator-for-mysql-pxc-1-20-0-automatic-storage-resizing-tls-rotation-arm64/</id>
      <updated>2026-06-09T10:25:46+03:00</updated>
      <author><name>Slava Sarzhan</name></author>
      <summary type="html"><![CDATA[<p>Percona Operator for MySQL PXC 1.20.0 is out today, and it addresses three long-requested operational headaches: storage that grows on its own before it fills up, TLS certificates that rotate without cluster downtime, and images that run natively on ARM64. Disk-full incidents on PXC clusters often arrive at 2 AM when monitoring alerts fire, and … Continued<br />
The post Percona Operator for MySQL (PXC) 1.20.0: Automatic Storage Resizing, TLS Certificate Rotation, and ARM64 Support appeared first on Percona.</p>
<p><a href="https://www.percona.com/blog/percona-operator-for-mysql-pxc-1-20-0-automatic-storage-resizing-tls-rotation-arm64/">Percona Operator for MySQL (PXC) 1.20.0: Automatic Storage Resizing, TLS Certificate Rotation, and ARM64 Support</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img loading="lazy" decoding="async" class="aligncenter wp-image-49249 size-large" src="https://www.percona.com/wp-content/uploads/2026/06/Hero-1-1024x376.png" alt="" width="1024" height="376"></p>
<p><span style="font-weight: 400">Percona Operator for MySQL PXC 1.20.0 is out today, and it addresses three long-requested operational headaches: storage that grows on its own before it fills up, TLS certificates that rotate without cluster downtime, and images that run natively on ARM64.</span></p>
<p><span style="font-weight: 400">Disk-full incidents on PXC clusters often arrive at 2 AM when monitoring alerts fire, and someone has to manually expand PVCs before writes grind to a halt. Certificate rotations have traditionally meant a carefully timed series of kubectl edits with real downtime risk. And ARM64 hardware has been increasingly common in dev clusters and cost-optimized cloud node pools, where x86-only images created extra friction. 1.20.0 addresses all three in a single release.</span></p>
<div data-line="17" data-line-type="change-addition" data-line-index="17,16">The operator is open source and runs on any CNCF-conformant Kubernetes distribution, including GKE, EKS, AKS, and OpenShift. <span data-diff-span="">It supports </span>Kubernetes 1.33 through 1.36 and PXC 8.4, 8.0, and 5.7.</div>
<p>&nbsp;</p>
<p><span style="font-weight: 400">In this post, you&rsquo;ll learn about:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Automatic PVC storage resizing with configurable thresholds and a hard cap</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Zero-downtime TLS certificate rotation via a new Secret naming convention</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Native ARM64 support across all operator images</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">PITR validation that catches misconfigured targets before restores begin</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Configurable leader election for high-latency or unstable networks</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Other improvements in this release</span></li>
</ul>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Automatic Storage Resizing</span><a class="anchor-link" id="automatic-storage-resizing"></a></h2>
<p><img loading="lazy" decoding="async" class="aligncenter wp-image-49251 size-large" src="https://www.percona.com/wp-content/uploads/2026/06/storage-resizing-1024x563.png" alt="" width="1024" height="563"></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Why it matters</span><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">A full data volume is the most common cause of unplanned maintenance on a PXC cluster. Until now, avoiding it required external monitoring, manual </span><span style="color: #ff6600"><i><span style="font-weight: 400">kubectl patch pvc</span></i></span><span style="font-weight: 400"> steps, and waiting for the storage class to honor the resize. Even with good alerting, the operator itself had no mechanism to react: it could only expand PVCs when you changed the spec by hand.</span></p>
<p>1.20.0 introduces built-in storage autoscaling. The operator polls each PVC&rsquo;s actual disk usage, and when usage crosses a configured threshold, it automatically expands the claim. You set the trigger percentage, the step size per resize event, and an optional upper bound. <span data-diff-span="">The operator handles everything else</span>.</p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">How it works</span><a class="anchor-link" id="how-it-works"></a></h3>
<p>The autoscaler runs inside the normal reconcile loop. It reads <em><span style="color: #ff6600">status.capacity.storage</span></em> from each PXC PVC, compares current usage against <em><span style="color: #ff6600">triggerThresholdPercent</span></em>, and issues a PVC resize when the threshold is crossed. <span data-diff-span="">It sets a </span><em><span style="color: #ff6600">percona.com/pvc-resize-in-progress</span></em> annotation on the CR while an expansion is active. This annotation blocks concurrent rolling restarts or upgrades from starting<span data-diff-span="">,</span> so <span data-diff-span="">nothing disrupts </span>the cluster mid-resize.</p>
<p>You can also set&nbsp;<em><span style="color: #ff6600">enableExternalAutoscaling: true</span></em><span style="color: #ff6600">&nbsp;</span>if an external tool, such as KEDA, already manages PVC sizes for your cluster.&nbsp;When <span data-diff-span="">you enable external autoscaling</span>, the built-in loop skips its resize check entirely to avoid conflicts.</p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Wiring it up</span><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span style="font-weight: 400">Add </span><span style="color: #ff6600"><i><span style="font-weight: 400">storageScaling</span></i></span><span style="font-weight: 400"> to your </span><span style="color: #ff6600"><i><span style="font-weight: 400">PerconaXtraDBCluster</span></i></span><span style="font-weight: 400"> spec:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
  name: cluster1
spec:
  crVersion: 1.20.0
  storageScaling:
    enableVolumeScaling: true
    autoscaling:
      enabled: true
      triggerThresholdPercent: 80   # resize when a PVC is 80% full
      growthStep: 2Gi               # add 2Gi per resize event
      maxSize: 100Gi                # never grow beyond 100Gi per PVC
#     enableExternalAutoscaling: false</pre>
<p><span data-diff-span="">Any </span>PVC expansion <span data-diff-span="">requires <span style="color: #ff6600"><em>enableVolumeScaling: true</em></span></span>, whether the autoscaler or a manual spec change<span data-diff-span=""> triggers it</span>. Setting <span style="color: #ff6600"><em>autoscaling.enabled: true</em></span> enables the threshold-based path on top of that. Leave the <em><span style="color: #ff6600">autoscaling</span></em> block out if you only want to permit manual spec-driven resizes.</p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Caveats</span><a class="anchor-link" id="caveats"></a></h3>
<p><span style="font-weight: 400">Storage expansion requires a StorageClass with </span><span style="color: #ff6600"><i><span style="font-weight: 400">allowVolumeExpansion: true</span></i><i><span style="font-weight: 400">.</span></i></span><span style="font-weight: 400"> Check before enabling:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">kubectl get storageclass 
  -o jsonpath='{range .items[*]}{.metadata.name}{"t"}{.allowVolumeExpansion}{"n"}{end}'</pre>
<p><span style="font-weight: 400">Autoscaling applies only to PXC data volumes. If your storage class or CSI driver handles expansion externally, use </span><span style="color: #ff6600"><i><span style="font-weight: 400">enableExternalAutoscaling: true</span></i></span><span style="font-weight: 400"> to prevent the two mechanisms from racing.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Automated TLS Certificate Rotation</span><a class="anchor-link" id="automated-tls-certificate-rotation"></a></h2>
<h3><span style="font-weight: 400">Why it matters</span><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">Rotating TLS certificates on a live PXC cluster has always carried risk. The Galera protocol requires all nodes to trust each other&rsquo;s CA simultaneously. Swap the CA on one node before the others accept it, and inter-node communication breaks. The safe approach requires a three-phase CA swap with rolling restarts between each phase: a process that is easy to get wrong under time pressure.</span></p>
<p><span style="font-weight: 400">1.20.0 formalizes this into a first-class operator workflow. Create a Secret named </span><span style="color: #ff6600"><i><span style="font-weight: 400">&lt;ssl-secret&gt;-new </span></i></span><span style="font-weight: 400">containing the replacement credentials, and the operator runs the full three-phase rotation automatically, pausing for rolling restarts between each step.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">How it works</span><a class="anchor-link" id="how-it-works"></a></h3>
<p>The rotation proceeds in three steps <span data-diff-span="">that </span>the operator<span data-diff-span=""> coordinates</span>:</p>
<ol>
<li style="font-weight: 400"><b>Combined CA phase</b><span style="font-weight: 400">. The old CA and new CA are merged into a single </span><span style="color: #ff6600"><i><span style="font-weight: 400">ca.crt</span></i></span><span style="font-weight: 400"> and pushed to all nodes. Every node now trusts both roots.</span></li>
<li style="font-weight: 400"><b>New leaf phase.</b><span style="font-weight: 400"> The new </span><span style="color: #ff6600"><i><span style="font-weight: 400">tls.crt</span></i></span><span style="font-weight: 400"> and</span><span style="color: #ff6600"><i><span style="font-weight: 400"> tls.key</span></i></span><span style="font-weight: 400"> are pushed node by node with a rolling restart. New leaf certs are signed by the new CA, and the combined CA means all nodes trust them.</span></li>
<li style="font-weight: 400"><b>New CA only phase.</b><span style="font-weight: 400"> The combined </span><span style="color: #ff6600"><i><span style="font-weight: 400">ca.crt</span></i></span><span style="font-weight: 400"> is replaced with the new CA only. The old root is removed. Another rolling restart completes the rotation.</span></li>
</ol>
<p><span style="font-weight: 400">When step 3 completes, the operator automatically deletes the </span><span style="color: #ff6600"><i><span style="font-weight: 400">-new</span></i></span><span style="font-weight: 400"> Secret. The cluster never loses TLS connectivity between nodes during the process.</span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Wiring it up</span><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span style="font-weight: 400">Given a cluster named </span><span style="color: #ff6600"><i><span style="font-weight: 400">cluster1 </span></i></span><span style="font-weight: 400">using the default SSL Secret </span><span style="color: #ff6600"><i><span style="font-weight: 400">cluster1-ssl</span></i></span><span style="font-weight: 400">, create the replacement:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">kubectl create secret generic cluster1-ssl-new 
  --from-file=ca.crt=new-ca.crt 
  --from-file=tls.crt=new-server.crt 
  --from-file=tls.key=new-server.key</pre>
<p><span data-diff-span="">You do not need </span>to <span data-diff-span="">change </span>the <span style="color: #ff6600"><em>PerconaXtraDBCluster</em></span> CR. The operator detects the <em><span style="color: #ff6600">-new</span></em> Secret on the next reconcile and starts the rotation. No <span style="color: #ff6600"><em>kubectl patch</em></span> on the CR, no operator restart.</p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Caveats</span><a class="anchor-link" id="caveats"></a></h3>
<p><span style="font-weight: 400">The operator does not yet surface rotation progress in</span><span style="color: #ff6600"><i><span style="font-weight: 400"> .status.conditions</span></i></span><span style="font-weight: 400">. Monitor the rotation by watching PXC pods restart in sequence and checking that the </span><span style="color: #ff6600"><i><span style="font-weight: 400">-new </span></i></span><span style="font-weight: 400">Secret is eventually gone:</span></p>
<pre class="urvanov-syntax-highlighter-plain-tag">kubectl get pods -w -l app.kubernetes.io/component=pxc
kubectl get secret cluster1-ssl-new  # should 404 when rotation is complete</pre>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">ARM64 Support</span><a class="anchor-link" id="arm64-support"></a></h2>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">Why it matters</span><a class="anchor-link" id="why-it-matters"></a></h3>
<p><span style="font-weight: 400">AWS Graviton3, Google Axion, and Azure Cobalt100 instances deliver better price-to-performance on memory-intensive workloads like PXC. Previously, running the operator on ARM64 nodes required cross-architecture scheduling workarounds or explicit node exclusions for operator pods. All PXC operator images now publish native </span><span style="color: #ff6600"><i><span style="font-weight: 400">linux/arm64 </span></i></span><span style="font-weight: 400">layers alongside </span><span style="color: #ff6600"><i><span style="font-weight: 400">nodeSelector<br>
</span></i></span></p>
<p>&nbsp;</p>
<h3><span style="font-weight: 400">What is covered</span><a class="anchor-link" id="what-is-covered"></a></h3>
<p><span style="font-weight: 400">Every image in the PXC operator stack ships multi-arch manifests in 1.20.0:</span></p>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">The operator manager image</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The PXC xtrabackup sidecar</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The log collector (Fluentbit-based)</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">The init container</span></li>
</ul>
<p>This release also fixes a logrotate crash on ARM64 (<a href="https://perconadev.atlassian.net/browse/K8SPXC-1821">K8SPXC-1821</a>) <span data-diff-span="">that </span>a missing dependency in the ARM64 container layer<span data-diff-span=""> caused</span>. 1.20.0<span data-diff-span=""> ships the fix</span>.<br>
&nbsp;</p>
<h3><span style="font-weight: 400">Wiring it up</span><a class="anchor-link" id="wiring-it-up"></a></h3>
<p><span data-diff-span="">You do not need any configuration change</span>. Pull the 1.20.0 operator image and Kubernetes schedules it on whichever architecture is available. To pin PXC pods explicitly to ARM64 nodes, add a <span style="color: #ff6600"><em>nodeSelector</em></span> or node affinity in the <em><span style="color: #ff6600">spec.pxc</span></em> block:</p>
<pre class="urvanov-syntax-highlighter-plain-tag">spec:
  pxc:
    nodeSelector:
      kubernetes.io/arch: arm64</pre>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Other Improvements</span><a class="anchor-link" id="other-improvements"></a></h2>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">PITR target validation before restore begins (</span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1318"><span style="font-weight: 400">K8SPXC-1318</span></a><span style="font-weight: 400">, </span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1634"><span style="font-weight: 400">K8SPXC-1634</span></a><span style="font-weight: 400">, </span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1635"><span style="font-weight: 400">K8SPXC-1635</span></a><span style="font-weight: 400">, </span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1793"><span style="font-weight: 400">K8SPXC-1793</span></a><span style="font-weight: 400">): The operator now validates PITR targets (type, GTID, timestamp) against available binary logs before starting a restore. <span data-diff-span="">It catches a </span>misconfigured target <span data-diff-span="">before it pauses</span> the cluster, rather than after.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Configurable leader election (</span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1805"><span style="font-weight: 400">K8SPXC-1805</span></a><span style="font-weight: 400">): Three new environment variables tune leader election timing for high-latency or flaky network environments.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">SST retry limit (</span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1619"><span style="font-weight: 400">K8SPXC-1619</span></a><span style="font-weight: 400">): A new </span><span style="color: #ff6600"><i><span style="font-weight: 400">spec.pxc.sstRetryCount</span></i></span><span style="font-weight: 400"> field caps the number of State Snapshot Transfer retry attempts, preventing a node that repeatedly fails SST from looping indefinitely.</span></li>
<li style="font-weight: 400"><span style="font-weight: 400">Custom logrotate configuration (</span><a href="https://perconadev.atlassian.net/browse/K8SPXC-1789"><span style="font-weight: 400">K8SPXC-1789</span></a><span style="font-weight: 400">): Supply a custom logrotate config via a ConfigMap reference in </span><span style="color: #ff6600"><i><span style="font-weight: 400">spec.logcollector.logRotate </span></i></span><span style="font-weight: 400">for fine-grained control over log rotation for PXC and utility containers.</span></li>
<li>Enhanced full cluster crash recovery&nbsp;(<a class="text-[var(--accent)] hover:underline underline-offset-[1px] outline-none hide-focus-ring ring-focus rounded-r2" href="https://perconadev.atlassian.net/browse/K8SPXC-1828" target="_blank" rel="noopener noreferrer">K8SPXC-1828</a>): 1.20.0 hardens the crash recovery path to prevent potential data loss after sudden node power-offs.</li>
</ul>
<p>&nbsp;</p>
<blockquote>
<p><b><i>Deprecation notice:</i></b><i><span style="font-weight: 400"> PMM2 monitoring integration is deprecated in 1.20.0. Migrate to PMM 3 before version 1.22.0, when PMM2 support will be removed.</span></i></p>
</blockquote>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Conclusion</span><a class="anchor-link" id="conclusion"></a></h2>
<p><span style="font-weight: 400">PXC Operator 1.20.0 turns three previously manual steps into operator-managed concerns: disk growth, certificate rotation, and ARM64 scheduling. Combined with PITR validation improvements and configurable leader election, this release reduces the operational surface area for clusters running under production pressure. If you run into edge cases with automatic storage resizing or TLS rotation, the community forum is the right place to share them.</span></p>
<p>&nbsp;</p>
<h2><span style="font-weight: 400">Try It Out</span><a class="anchor-link" id="try-it-out"></a></h2>
<ul>
<li style="font-weight: 400"><span style="font-weight: 400">Release notes: </span><a href="https://docs.percona.com/percona-operator-for-mysql/pxc/ReleaseNotes/Kubernetes-Operator-for-PXC-RN1.20.0.html"><span style="font-weight: 400">Percona Operator for MySQL (PXC) 1.20.0 Release Notes</span></a></li>
<li style="font-weight: 400"><span style="font-weight: 400">GitHub: </span><a href="https://github.com/percona/percona-xtradb-cluster-operator"><span style="font-weight: 400">percona/percona-xtradb-cluster-operator</span></a></li>
<li style="font-weight: 400"><span style="font-weight: 400">Public roadmap: </span><a href="https://github.com/orgs/percona/projects/10/views/5"><span style="font-weight: 400">Percona public roadmap</span></a><span style="font-weight: 400">. See what is coming and vote on priorities.</span></li>
<li style="font-weight: 400">Community Forum: <a href="https://forums.percona.com/">forums.percona.com</a>. Share feedback, ask questions, or report issues.</li>
</ul>
<p>&nbsp;</p>
<p>The post <a href="https://www.percona.com/blog/percona-operator-for-mysql-pxc-1-20-0-automatic-storage-resizing-tls-rotation-arm64/">Percona Operator for MySQL (PXC) 1.20.0: Automatic Storage Resizing, TLS Certificate Rotation, and ARM64 Support</a> appeared first on <a href="https://www.percona.com">Percona</a>.</p>

<p><a href="https://www.percona.com/blog/percona-operator-for-mysql-pxc-1-20-0-automatic-storage-resizing-tls-rotation-arm64/">Percona Operator for MySQL (PXC) 1.20.0: Automatic Storage Resizing, TLS Certificate Rotation, and ARM64 Support</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/" />
      <id>https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/</id>
      <updated>2026-06-09T05:55:04+03:00</updated>
      <author><name>Frédéric Descamps</name></author>
      <summary type="html"><![CDATA[<p>Interview with Mark Callaghan, nominated in the Technical Excellence category.<br />
I had the pleasure of speaking with Mark Callaghan, recently nominated for the MariaDB Sea Lion Champions program in the “Technical Excellence” …<br />
Continue reading \"MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan\"<br />
The post MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/">MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Interview with Mark Callaghan, nominated in the Technical Excellence category.<br>
I had the pleasure of speaking with Mark Callaghan, recently nominated for the MariaDB Sea Lion Champions program in the &ldquo;Technical Excellence&rdquo; &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/">MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-mark-callaghan/">MariaDB Foundation Sea Lion Champions Nominees: Mark Callaghan</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>SpacemiT K3 is a compelling RISC-V AI CPU, but difficult to buy</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/" />
      <id>https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/</id>
      <updated>2026-06-09T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The RISC-V CPU architecture has been gaining a lot of popularity since it launched in 2014, and now that the industry is standardizing on the RVA23 level that includes vector support as a mandatory extension, we are likely to see a lot more edge- and IoT devices with the ability to run local LLMs at reasonable speed, and most importantly at very compelling prices.<br />
SpacemiT is a Chinese RISC-V CPU manufacturer that launched on May 11th, 2026, their long-anticipated next-gen RISC-V AI chip K3. It is among the earliest RISC-V CPUs that adhere to the RVA23 standard and performance-wise it is quite capable, providing 130 KDMIPS general computing power, 60 TOPS on INT4 which translates to about 15 tokens per second when running a 30 billion parameter large language model.<br />
The aspect that really makes it stand out is:</p>
<p>the RISC-V CPU architecture is open source,<br />
the price point is within reach of home and small business users and<br />
the overall feature set makes it an ideal platform to build local and offline AI systems.</p>
<p>SpacemiT also develops their own Debian-based Linux distribution Bianbu OS, and seems to have collaboration going on with the wider community. Their community site seems active, and they also have a dedicated X account @spacemit_riscv and Reddit account r/spacemit_riscv posting relevant progress info on Linux kernel upstreaming activities. The X account is also responsive, as evidenced by its replies to my questions.<br />
Canonical lists the SpacemiT K3 pico-ITX and K3 CoM260 Kit on its official Ubuntu for RISC-V partner-built hardware page, which strengthens the perception that upstream Linux support is being taken seriously. The SpacemiT folks also gave an interesting talk at the 2026 Ubuntu Summit that includes a peek into their roadmap with future K3, K7 and K9 models.<br />
For technical details, see SpacemiT’s K3 pico-ITX documentation, the Jetson Orin Nano-compatible K3 CoM260 board documentation and documentation of the K3 processor itself.</p>
<p>Comparing the resellers<br />
SpacemiT does not sell anything directly to consumers. Instead you need to buy a board that includes the K3 chip from an integrator. Currently the main resellers are:</p>
<p>Milk-V<br />
Sipeed<br />
Banana Pi<br />
Firefly<br />
DeepComputing</p>
<p>All of the above are Chinese companies that ship to customers both inside and outside China. DeepComputing stands out as the only one that actually has done real integration and ships the K3 on a custom board, while the others simply resell the SpacemiT-produced K3 pico-ITX and K3 CoM260 Kit.<br />
Milk-V<br />
Milk-V is a RISC-V specialized integrator, as the name already implies. They sell the K3 under the name Jupiter2. Of all the K3 pico-ITX reseller product pages, the Jupiter2 presentation is the nicest and most detailed. Unfortunately their order page at arace.tech only states that it is a “pre-order” with no information about shipping schedule, taxes, or other details like what SSD is included (if any). Based on the pictures it does ship with a Milk-V branded case. The 32 GB RAM lists at 504 EUR, which is a very reasonable price. The @MilkV_Official account on X recently promoted the K3.<br />
Documentation and support<br />
As of this writing, the Milk-V Jupiter2 documentation site is just a stub and has no actual content, and only two links to the SpacemiT K3 documentation site. For support there is a web forum with a dedicated Jupiter2 section. There is also a Matrix space, but unlike their other products, there is no dedicated Jupiter (neither v1 nor v2) channel.<br />
Community size and open source involvement<br />
At least one prior Milk-V product was certified by Canonical, which indicates there is some collaboration in progress. Canonical also lists the Milk-V Titan on its official Ubuntu for RISC-V partner-built hardware page.<br />
Sipeed<br />
The Sipeed K3 announcement is well written (in English) with all the relevant details and links to additional PDF manuals. However, their main page at sipeed.com says nothing about the K3, so one must know the subpage URL to access it. They offer both the K3 CoM260 kit compatible with Jetson Orin Nano carrier boards, and the stand-alone K3 pico-ITX-sized motherboard. The CoM260 kit is only 10 USD cheaper than the full pico-ITX motherboard, so choosing the latter is a no-brainer if starting from scratch. The pico-ITX model with 32 GB DDR5 RAM sells for 639 USD. The product page does not mention anything about hard disk size, so you don’t really know exactly what you will be getting if placing an order. There is no indication about case, Wi-Fi antennas or power supply either, so most likely they are not included.<br />
Their store.sipeed.com website does not work at all, and their Taobao and AliExpress stores are not public and only accessible to registered users. The order page also says nothing about shipping time, delivery time, or taxes. The X account @SipeedIO is active and recently posted pictures of shipments in progress.<br />
Documentation and support<br />
The main documentation wiki does not yet have any K3 content at the time of writing. There is a Discord channel for general RISC-V discussion, and their MaixHub also has a discussion board, but I didn’t find anything K3-specific.<br />
Community size and open source involvement<br />
Sipeed has had at least one of their previous devices certified by Canonical, which indicates they are active in the community.<br />
Note that the other RISC-V company SiFive that also has had hardware certified and officially supported by Canonical is a different company, despite the very similar name.<br />
Banana Pi<br />
Banana Pi announced that they offer both the K3 CoM260 kit and the K3 pico-ITX motherboard version. Their product page for the K3 confusingly shows a MediaTek product in the page banner rather than the SpacemiT K3. Based on the product description and the fact they renamed the product as BPI-SM10, it seems to ship with some carrier board. The product pictures look identical to the SpacemiT documentation and there is no picture of the carrier board, and details are very sparse. The pico-ITX version with 8 GB RAM and 128 GB SSD sells for 293 USD and the CoM260 developer kit with the same specs sells for 287 USD and the 32 GB RAM with 128 GB SSD model sells for 595 USD. The shop page shows only five orders so far and items are currently out of stock. As there was no 32 GB RAM version of the pico-ITX available at all, this isn’t an option for me as I want to run 30B parameter models that need the larger memory version.<br />
Of all of these resellers, the Banana Pi website seems the most outdated. It does not have a search feature, it is not mobile-friendly, pictures can’t be pinched to zoom in and so forth. Product names are also almost all identical, and as the product listings only show the beginning of the product name, figuring out what product is what requires extra effort that just makes the online purchase experience plain bad.<br />
Documentation and support<br />
I was only able to find the documentation page for the CoM260 kit, but none for the pico-ITX version. For support there is a forum, but the category list does not show any section for K3, and the forum search prohibits using the search term “k3” as too short.<br />
Community size and open source involvement<br />
Banana Pi has a long history in the ARM single-board computer market, but their presence in the RISC-V ecosystem is still growing. Their X account @sinovoip has posted only once about the K3 and otherwise promotes their ARM boards. However, their community culture page does express a commitment to open hardware in general, but there is no visible K3-specific community activity.<br />
Firefly<br />
Firefly’s K3 product page is comprehensive. Based on the details, they do not offer the K3 pico-ITX variant at all, but only the K3 CoM260 board inside the AIBOX-K3 Firefly RISC-V Edge Mini PC product. This is a feature-complete offering with a Jetson Orin Nano carrier board and case. The AIBOX-K3 with 32 GB RAM and 128 GB SSD in a case sells for 689 USD in their own Firefly.store. Unfortunately it only has HDMI and there is no USB-C with DisplayPort support, which is a deal-breaker for me personally.<br />
Interestingly, Firefly also offers rack-mounted servers with K3 as the CPU.<br />
Documentation and support<br />
The wiki link on the product page is broken. The Firefly wiki does have a section for the AIBOX-K3, but it too has a broken link. It seems that as of the time of writing, there is no wiki section for this product yet.<br />
For support there is a web forum, which does have at least one K3 thread covering guides such as Hermes Agent installation, though broader K3-specific sections are still sparse.<br />
Community size and open source involvement<br />
Firefly’s X account @TeeFirefly has had no posts since 2024, and their GitLab/T-Firefly shows mostly 2024 activity, with only one repository updated in 2025 and nothing in 2026. Historically they have built a moderate community around their ARM-based Rockchip boards, with active forums and wiki contributions for those product lines. Their RISC-V K3 offerings are newer, and likely need a lot more polish to be attractive products overall.<br />
DeepComputing<br />
Last, but certainly not least, is the laptop manufacturer DeepComputing that offers a Framework laptop compatible motherboard with the SpacemiT K3 chip. They also sell the plain motherboard, or with the Cooler Master case, which allows one to easily connect it to an external monitor and keyboard and use it as a desktop computer. The plain board with 32 GB RAM and no SSD sells for about 882 EUR. Shipping of the first batch is expected to start by end of June 2026. Their X account @DeepComputingio promotes this DC-ROMA RISC-V Mainboard III as their flagship product, so they seem to put a lot of effort into it.<br />
The overall product design and packaging seems good. Of all the K3 resellers and integrators that I was able to find, DeepComputing is the only one that actually designs their own boards with the K3 processor, while all the other vendors above are simply reselling the vanilla K3 boards with or without a case.<br />
After reviewing all these options I decided to buy the DC-ROMA RISC-V Mainboard III for Framework Laptop 13 with 32 GB RAM, 1 TB SSD and the Cooler Master case, totalling about 1100 EUR.<br />
Documentation and support<br />
DeepComputing maintains product information for their RISC-V hardware at github.com/DC-DeepComputing/Framework, with documentation of the newest Mainboard III (FML13V05) still being finalized ahead of the first batch shipment. They provide community support through Discord and web forum, although the latter has very little activity.<br />
Community size and open source involvement<br />
DeepComputing has established itself as a pioneer in RISC-V laptops, beginning with the DC-ROMA. I have seen their stand at FOSDEM, which shows they are genuinely active in the open source community. Canonical lists DeepComputing’s first mainboard / FML13V01 on its official Ubuntu for RISC-V partner-built hardware page, and it seems likely that they will continue to collaborate with Canonical with the new model once it ships. While the underlying Linux enablement depends on SpacemiT’s upstream efforts, DeepComputing’s involvement helps bridge the gap between reference hardware and consumer-ready products.</p>
<p>Conclusion<br />
After weighing all the options, I ended up placing an order with DeepComputing for their custom K3 board with the Cooler Master case. Despite the premium price, the active community support and the properly documented promise of a complete, working system made it easy to place an order with confidence.<br />
The SpacemiT K3 is poised to be one of the most significant RISC-V chips for local AI workloads, thanks to its RVA23 compliance and high tokens per second potential. Yet the buying experience in mid-2026 remains fragmented and incomplete. Hopefully this is just because the product is new, and they will get the purchase experience polished soon.<br />
What struck me most during this process was how poor the customer experience is across nearly all of these vendor websites: broken links, missing search functions, outdated product banners, pages that show the wrong product entirely, and no information about shipping times, stock levels, taxes, and so on. One wonders why these companies don’t fully invest in their web presence.<br />
Personally I would assume they likely have enough customers already, primarily through domestic channels like Taobao and JD.com, that they do not feel any pressure to improve their international-facing sites. However, I did also review what was offered on Taobao, and the product details were very incomplete there too. Taobao, however, has a built-in live chat with almost all sellers, which can be used to ask questions and thus compensate for missing product details.<br />
I don’t fully understand why the sales process seems unpolished. The websites feel almost like an afterthought – a checkbox to claim global reach while the real business apparently happens elsewhere via closed platforms or via inaccessible reseller channels. It is a frustrating reminder that in the RISC-V hardware world, the technology may be open and global, but the purchase experience is less so.</p>
<p><a href="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/">SpacemiT K3 is a compelling RISC-V AI CPU, but difficult to buy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/spacemit-k3.jpg" alt="Featured image of post SpacemiT K3 is a compelling RISC-V AI CPU, but difficult to buy"></p>
<p>The RISC-V CPU architecture has been gaining a lot of popularity since it launched in 2014, and now that the industry is standardizing on the RVA23 level that includes vector support as a mandatory extension, we are likely to see a lot more edge- and IoT devices with the ability to run local LLMs at reasonable speed, and most importantly at very compelling prices.</p>
<p><a class="link" href="https://www.spacemit.com/" target="_blank" rel="noopener">SpacemiT</a> is a Chinese RISC-V CPU manufacturer that launched on May 11th, 2026, their <a class="link" href="https://canonical.com/blog/spacemit-announces-availability-of-ubuntu-on-k3-k1-series" target="_blank" rel="noopener">long-anticipated next-gen RISC-V</a> AI chip <a class="link" href="https://www.spacemit.com/products/keystone/k3" target="_blank" rel="noopener">K3</a>. It is among the earliest RISC-V CPUs that adhere to the <a class="link" href="https://www.heise.de/en/news/RISC-V-and-Linux-Ubuntu-25-10-forces-brand-new-processors-10538066.html" target="_blank" rel="noopener">RVA23 standard</a> and performance-wise it is quite capable, providing 130 KDMIPS general computing power, 60 TOPS on INT4 which translates to about 15 tokens per second when running a 30 billion parameter large language model.</p>
<p>The aspect that really makes it stand out is:</p>
<ul>
<li>the <a class="link" href="https://en.wikipedia.org/wiki/RISC-V" target="_blank" rel="noopener">RISC-V CPU architecture is open source</a>,</li>
<li>the price point is within reach of home and small business users and</li>
<li>the overall feature set makes it an ideal platform to build <strong><em>local</em> and <em>offline</em> AI systems</strong>.</li>
</ul>
<p>SpacemiT also develops their own Debian-based Linux distribution Bianbu OS, and seems to have collaboration going on with the wider community. Their <a class="link" href="https://www.spacemit.com/community" target="_blank" rel="noopener">community site</a> seems active, and they also have a dedicated <a class="link" href="https://x.com/spacemit_riscv" target="_blank" rel="noopener">X account @spacemit_riscv</a> and <a class="link" href="https://www.reddit.com/r/spacemit_riscv/comments/1t5yimh/upstream-progress-updates/" target="_blank" rel="noopener">Reddit account r/spacemit_riscv</a> posting relevant progress info on Linux kernel upstreaming activities. The X account is also responsive, as evidenced by <a class="link" href="https://x.com/ottokekalainen/status/2056375593722356207" target="_blank" rel="noopener">its replies to my questions</a>.</p>
<p>Canonical lists the SpacemiT K3 pico-ITX and K3 CoM260 Kit on its official <a class="link" href="https://ubuntu.com/download/risc-v/partner-built" target="_blank" rel="noopener">Ubuntu for RISC-V partner-built hardware page</a>, which strengthens the perception that upstream Linux support is being taken seriously. The SpacemiT folks also gave an interesting <a class="link" href="https://www.youtube.com/watch?v=BaY2l17OBRQ" target="_blank" rel="noopener">talk at the 2026 Ubuntu Summit</a> that includes a peek into their roadmap with future K3, K7 and K9 models.</p>
<p>For technical details, see SpacemiT&rsquo;s <a class="link" href="https://www.spacemit.com/community/development-kit/k3-pico-itx" target="_blank" rel="noopener">K3 pico-ITX documentation</a>, the Jetson Orin Nano-compatible <a class="link" href="https://www.spacemit.com/community/development-kit/k3-com260" target="_blank" rel="noopener">K3 CoM260 board documentation</a> and <a class="link" href="https://www.spacemit.com/community/document/info?lang=en&amp;nodepath=hardware/key_stone/k3" target="_blank" rel="noopener">documentation of the K3 processor itself</a>.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/spacemit-k3-pico-itx-and-k3-com260-kit.webp" width="1200" height="628" loading="lazy" alt="The SpacemiT K3 pico-ITX board and the K3 CoM260 board side-by-side (not to scale)" class="gallery-image" data-flex-grow="191" data-flex-basis="458px">
</p>
<h2 id="comparing-the-resellers"><a href="#comparing-the-resellers" class="header-anchor"></a>Comparing the resellers<br>
<a class="anchor-link" id="comparing-the-resellers"></a></h2>
<p>SpacemiT does not sell anything directly to consumers. Instead you need to buy a board that includes the K3 chip from an integrator. Currently the main resellers are:</p>
<ul>
<li><a class="link" href="#milkv">Milk-V</a></li>
<li><a class="link" href="#sipeed">Sipeed</a></li>
<li><a class="link" href="#banana-pi">Banana Pi</a></li>
<li><a class="link" href="#firefly">Firefly</a></li>
<li><a class="link" href="#deepcomputing">DeepComputing</a></li>
</ul>
<p>All of the above are Chinese companies that ship to customers both inside and outside China. DeepComputing stands out as the only one that actually has done real integration and ships the K3 on a custom board, while the others simply resell the SpacemiT-produced K3 pico-ITX and K3 CoM260 Kit.</p>
<h2 id="milk-v"><a href="#milk-v" class="header-anchor"></a>Milk-V<br>
<a class="anchor-link" id="milk-v"></a></h2>
<p>Milk-V is a RISC-V specialized integrator, as the name already implies. They sell the K3 under the name <a class="link" href="https://milkv.io/jupiter2" target="_blank" rel="noopener">Jupiter2</a>. Of all the K3 pico-ITX reseller product pages, the Jupiter2 presentation is the nicest and most detailed. Unfortunately their <a class="link" href="https://arace.tech/products/milk-v-jupiter-2" target="_blank" rel="noopener">order page at arace.tech</a> only states that it is a &ldquo;pre-order&rdquo; with no information about shipping schedule, taxes, or other details like what SSD is included (if any). Based on the pictures it does ship with a Milk-V branded case. The 32 GB RAM lists at 504 EUR, which is a very reasonable price. The <a class="link" href="https://x.com/MilkV_Official" target="_blank" rel="noopener">@MilkV_Official account on X</a> recently promoted the K3.</p>
<h3 id="documentation-and-support"><a href="#documentation-and-support" class="header-anchor"></a>Documentation and support<br>
<a class="anchor-link" id="documentation-and-support"></a></h3>
<p>As of this writing, the <a class="link" href="https://milkv.io/docs/jupiter2/" target="_blank" rel="noopener">Milk-V Jupiter2 documentation site</a> is just a stub and has no actual content, and only two links to the SpacemiT K3 documentation site. For support there is a web forum with a <a class="link" href="https://community.milkv.io/c/jupiter/jupiter2/19" target="_blank" rel="noopener">dedicated Jupiter2 section</a>. There is also a <a class="link" href="https://matrix.to/#/#milk-v:matrix.org" target="_blank" rel="noopener">Matrix space</a>, but unlike their other products, there is no dedicated Jupiter (neither v1 nor v2) channel.</p>
<h3 id="community-size-and-open-source-involvement"><a href="#community-size-and-open-source-involvement" class="header-anchor"></a>Community size and open source involvement<br>
<a class="anchor-link" id="community-size-and-open-source-involvement"></a></h3>
<p>At least one prior Milk-V product <a class="link" href="https://canonical.com/blog/canonical-enables-ubuntu-on-milk-v-mars" target="_blank" rel="noopener">was certified by Canonical</a>, which indicates there is some collaboration in progress. Canonical also lists the <a class="link" href="https://ubuntu.com/download/risc-v/partner-built" target="_blank" rel="noopener">Milk-V Titan</a> on its official Ubuntu for RISC-V partner-built hardware page.</p>
<h2 id="sipeed"><a href="#sipeed" class="header-anchor"></a>Sipeed<br>
<a class="anchor-link" id="sipeed"></a></h2>
<p>The <a class="link" href="https://sipeed.com/k3" target="_blank" rel="noopener">Sipeed K3 announcement</a> is well written (in English) with all the relevant details and links to additional PDF manuals. However, their main page at <a class="link" href="https://sipeed.com/" target="_blank" rel="noopener">sipeed.com</a> says nothing about the K3, so one must know the subpage URL to access it. They offer both the K3 CoM260 kit compatible with Jetson Orin Nano carrier boards, and the stand-alone K3 pico-ITX-sized motherboard. The CoM260 kit is only 10 USD cheaper than the full pico-ITX motherboard, so choosing the latter is a no-brainer if starting from scratch. The pico-ITX model with 32 GB DDR5 RAM sells for 639 USD. The product page does not mention anything about hard disk size, so you don&rsquo;t really know exactly what you will be getting if placing an order. There is no indication about case, Wi-Fi antennas or power supply either, so most likely they are not included.</p>
<p>Their <a class="link" href="http://store.sipeed.com" target="_blank" rel="noopener">store.sipeed.com</a> website does not work at all, and their Taobao and AliExpress stores are not public and only accessible to registered users. The order page also says nothing about shipping time, delivery time, or taxes. The <a class="link" href="https://x.com/SipeedIO/status/2055549071931404291" target="_blank" rel="noopener">X account @SipeedIO</a> is active and recently posted pictures of shipments in progress.</p>
<h3 id="documentation-and-support-1"><a href="#documentation-and-support-1" class="header-anchor"></a>Documentation and support<br>
<a class="anchor-link" id="documentation-and-support"></a></h3>
<p>The main <a class="link" href="https://wiki.sipeed.com/" target="_blank" rel="noopener">documentation wiki</a> does not yet have any K3 content at the time of writing. There is a <a class="link" href="https://discord.com/channels/1359800784375644291/1503600021646479500" target="_blank" rel="noopener">Discord channel for general RISC-V discussion</a>, and their MaixHub also has a discussion board, but I didn&rsquo;t find anything K3-specific.</p>
<h3 id="community-size-and-open-source-involvement-1"><a href="#community-size-and-open-source-involvement-1" class="header-anchor"></a>Community size and open source involvement<br>
<a class="anchor-link" id="community-size-and-open-source-involvement"></a></h3>
<p>Sipeed has had at least one of their previous devices <a class="link" href="https://ubuntu.com/blog/canonical-enables-ubuntu-on-sipeeds-licheerv-risc-v-board" target="_blank" rel="noopener">certified by Canonical</a>, which indicates they are active in the community.</p>
<p>Note that the other RISC-V company <a class="link" href="https://ubuntu.com/tutorials/how-to-install-ubuntu-on-risc-v-hifive-boards" target="_blank" rel="noopener">SiFive</a> that <a class="link" href="https://canonical.com/blog/sifive-eswin-computing-and-canonical-announce-availability-of-ubuntu-on-the-hifive-premier-p550" target="_blank" rel="noopener">also</a> has had hardware certified and officially supported by Canonical is a different company, despite the very similar name.</p>
<h2 id="banana-pi"><a href="#banana-pi" class="header-anchor"></a>Banana Pi<br>
<a class="anchor-link" id="banana-pi"></a></h2>
<p><a class="link" href="https://banana-pi.org/en/product-news/591.html" target="_blank" rel="noopener">Banana Pi announced</a> that they offer both the K3 CoM260 kit and the K3 pico-ITX motherboard version. Their <a class="link" href="https://banana-pi.org/en/core-board-and-kit/207.html" target="_blank" rel="noopener">product page for the K3</a> confusingly shows a MediaTek product in the page banner rather than the SpacemiT K3. Based on the product description and the fact they renamed the product as <em>BPI-SM10</em>, it seems to ship with some carrier board. The product pictures look identical to the SpacemiT documentation and there is no picture of the carrier board, and details are very sparse. The <a class="link" href="https://www.bpi-shop.com/products/k3-pico-itx-spacemit-k3-8-cores--60tops-al-performance-wifi6.html" target="_blank" rel="noopener">pico-ITX version</a> with 8 GB RAM and 128 GB SSD sells for 293 USD and the <a class="link" href="https://www.bpi-shop.com/products/bpi-sm10-k3-com260.html" target="_blank" rel="noopener">CoM260 developer kit</a> with the same specs sells for 287 USD and the 32 GB RAM with 128 GB SSD model sells for 595 USD. The shop page shows only five orders so far and items are currently out of stock. As there was no 32 GB RAM version of the pico-ITX available at all, this isn&rsquo;t an option for me as I want to run 30B parameter models that need the larger memory version.</p>
<p>Of all of these resellers, the <strong>Banana Pi website seems the most outdated</strong>. It does not have a search feature, it is not mobile-friendly, pictures can&rsquo;t be pinched to zoom in and so forth. Product names are also almost all identical, and as the product listings only show the beginning of the product name, figuring out what product is what requires extra effort that just makes the online purchase experience plain bad.</p>
<h3 id="documentation-and-support-2"><a href="#documentation-and-support-2" class="header-anchor"></a>Documentation and support<br>
<a class="anchor-link" id="documentation-and-support"></a></h3>
<p>I was only able to find the <a class="link" href="https://docs.banana-pi.org/en/BPI-SM10/BananaPi_BPI-SM10" target="_blank" rel="noopener">documentation page for the CoM260 kit</a>, but none for the pico-ITX version. For support there is a <a class="link" href="https://forum.banana-pi.org/" target="_blank" rel="noopener">forum</a>, but the category list does not show any section for K3, and the forum search prohibits using the search term &ldquo;k3&rdquo; as too short.</p>
<h3 id="community-size-and-open-source-involvement-2"><a href="#community-size-and-open-source-involvement-2" class="header-anchor"></a>Community size and open source involvement<br>
<a class="anchor-link" id="community-size-and-open-source-involvement"></a></h3>
<p>Banana Pi has a long history in the ARM single-board computer market, but their presence in the RISC-V ecosystem is still growing. Their <a class="link" href="https://x.com/sinovoip" target="_blank" rel="noopener">X account @sinovoip</a> has posted only once about the K3 and otherwise promotes their ARM boards. However, their <a class="link" href="https://banana-pi.org/en/community-culture/" target="_blank" rel="noopener">community culture page</a> does express a commitment to open hardware in general, but there is no visible K3-specific community activity.</p>
<h2 id="firefly"><a href="#firefly" class="header-anchor"></a>Firefly<br>
<a class="anchor-link" id="firefly"></a></h2>
<p><a class="link" href="https://en.t-firefly.com/p/aibox-k3" target="_blank" rel="noopener">Firefly&rsquo;s K3 product page</a> is comprehensive. Based on the details, they do not offer the K3 pico-ITX variant at all, but only the K3 CoM260 board inside the AIBOX-K3 Firefly RISC-V Edge Mini PC product. This is a feature-complete offering with a Jetson Orin Nano carrier board and case. The AIBOX-K3 with 32 GB RAM and 128 GB SSD in a case sells for 689 USD in their own <a class="link" href="https://www.firefly.store/products/aibox-k3-risc-v-edge-mini-pc?variant=46857894821972" target="_blank" rel="noopener">Firefly.store</a>. Unfortunately it only has HDMI and there is no USB-C with DisplayPort support, which is a deal-breaker for me personally.</p>
<p>Interestingly, Firefly also offers <a class="link" href="https://www.firefly.store/blogs/news/firefly-k3-series-launches-with-powerful-risc-v-chips-supports-30b-ai-models" target="_blank" rel="noopener">rack-mounted servers with K3</a> as the CPU.</p>
<h3 id="documentation-and-support-3"><a href="#documentation-and-support-3" class="header-anchor"></a>Documentation and support<br>
<a class="anchor-link" id="documentation-and-support"></a></h3>
<p>The wiki link on the product page is broken. The <a class="link" href="https://en.t-firefly.com/wiki" target="_blank" rel="noopener">Firefly wiki</a> does have a section for the AIBOX-K3, but it too has a broken link. It seems that as of the time of writing, there is no wiki section for this product yet.</p>
<p>For support there is a <a class="link" href="https://bbs.t-firefly.com/" target="_blank" rel="noopener">web forum</a>, which does have at least <a class="link" href="https://bbs.t-firefly.com/forum.php?mod=viewthread&amp;tid=66517&amp;extra=page%3D1" target="_blank" rel="noopener">one K3 thread</a> covering guides such as Hermes Agent installation, though broader K3-specific sections are still sparse.</p>
<h3 id="community-size-and-open-source-involvement-3"><a href="#community-size-and-open-source-involvement-3" class="header-anchor"></a>Community size and open source involvement<br>
<a class="anchor-link" id="community-size-and-open-source-involvement"></a></h3>
<p>Firefly&rsquo;s <a class="link" href="https://x.com/TeeFirefly" target="_blank" rel="noopener">X account @TeeFirefly</a> has had no posts since 2024, and their <a class="link" href="https://gitlab.com/T-Firefly" target="_blank" rel="noopener">GitLab/T-Firefly</a> shows mostly 2024 activity, with only one repository updated in 2025 and nothing in 2026. Historically they have built a moderate community around their ARM-based Rockchip boards, with active forums and wiki contributions for those product lines. Their RISC-V K3 offerings are newer, and likely need a lot more polish to be attractive products overall.</p>
<h2 id="deepcomputing"><a href="#deepcomputing" class="header-anchor"></a>DeepComputing<br>
<a class="anchor-link" id="deepcomputing"></a></h2>
<p>Last, but certainly not least, is the laptop manufacturer <a class="link" href="https://deepcomputing.io" target="_blank" rel="noopener">DeepComputing</a> that offers a <a class="link" href="https://deepcomputing.io/product/dc-roma-risc-v-mainboard-iii/" target="_blank" rel="noopener">Framework laptop compatible motherboard with the SpacemiT K3 chip</a>. They also sell the plain motherboard, or with the Cooler Master case, which allows one to easily connect it to an external monitor and keyboard and use it as a desktop computer. The plain board with 32 GB RAM and no SSD sells for about 882 EUR. Shipping of the first batch is expected to start by end of June 2026. Their <a class="link" href="https://x.com/DeepComputingio" target="_blank" rel="noopener">X account @DeepComputingio</a> promotes this DC-ROMA RISC-V Mainboard III as their flagship product, so they seem to put a lot of effort into it.</p>
<p>The overall product design and packaging seems good. Of all the K3 resellers and integrators that I was able to find, <strong>DeepComputing is the only one that actually designs their own boards</strong> with the K3 processor, while all the other vendors above are simply reselling the vanilla K3 boards with or without a case.</p>
<p><strong>After reviewing all these options I decided to buy the <a class="link" href="https://store.deepcomputing.io/products/dc-roma-risc-v-mainboard-iii-for-framework-laptop-13?variant=51310183088292" target="_blank" rel="noopener">DC-ROMA RISC-V Mainboard III</a></strong> for Framework Laptop 13 with 32 GB RAM, 1 TB SSD and the Cooler Master case, totalling about 1100 EUR.</p>
<h3 id="documentation-and-support-4"><a href="#documentation-and-support-4" class="header-anchor"></a>Documentation and support<br>
<a class="anchor-link" id="documentation-and-support"></a></h3>
<p>DeepComputing maintains product information for their RISC-V hardware at <a class="link" href="https://github.com/DC-DeepComputing/Framework" target="_blank" rel="noopener">github.com/DC-DeepComputing/Framework</a>, with documentation of the newest <em>Mainboard III (FML13V05)</em> still being finalized ahead of the first batch shipment. They provide community support through <a class="link" href="https://discord.com/invite/DycykxSxWH" target="_blank" rel="noopener">Discord</a> and <a class="link" href="https://deepcomputing.discourse.group/" target="_blank" rel="noopener">web forum</a>, although the latter has very little activity.</p>
<h3 id="community-size-and-open-source-involvement-4"><a href="#community-size-and-open-source-involvement-4" class="header-anchor"></a>Community size and open source involvement<br>
<a class="anchor-link" id="community-size-and-open-source-involvement"></a></h3>
<p>DeepComputing has established itself as a pioneer in RISC-V laptops, beginning with the DC-ROMA. I have seen their stand at FOSDEM, which shows they are genuinely active in the open source community. Canonical lists <a class="link" href="https://ubuntu.com/download/risc-v/partner-built" target="_blank" rel="noopener">DeepComputing&rsquo;s first mainboard / FML13V01</a> on its official Ubuntu for RISC-V partner-built hardware page, and it seems likely that they will continue to collaborate with Canonical with the new model once it ships. While the underlying Linux enablement depends on SpacemiT&rsquo;s upstream efforts, DeepComputing&rsquo;s involvement helps bridge the gap between reference hardware and consumer-ready products.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/deepcomputing-cool-master-spacemit-k3.webp" width="531" height="381" loading="lazy" alt="DeepComputing K3 board in the Cooler Master case" class="gallery-image" data-flex-grow="139" data-flex-basis="334px">
</p>
<h2 id="conclusion"><a href="#conclusion" class="header-anchor"></a>Conclusion<br>
<a class="anchor-link" id="conclusion"></a></h2>
<p>After weighing all the options, I ended up placing an order with DeepComputing for their custom K3 board with the Cooler Master case. Despite the premium price, the active community support and the properly documented promise of a complete, working system made it easy to place an order with confidence.</p>
<p>The SpacemiT K3 is poised to be one of the most significant RISC-V chips for local AI workloads, thanks to its RVA23 compliance and high tokens per second potential. Yet the buying experience in mid-2026 remains fragmented and incomplete. Hopefully this is just because the product is new, and they will get the purchase experience polished soon.</p>
<p>What struck me most during this process was how poor the customer experience is across nearly all of these vendor websites: broken links, missing search functions, outdated product banners, pages that show the wrong product entirely, and no information about shipping times, stock levels, taxes, and so on. One wonders why these companies don&rsquo;t fully invest in their web presence.</p>
<p>Personally I would assume they <strong>likely have enough customers already,</strong> primarily through domestic channels like <em>Taobao</em> and <em>JD.com</em>, that they do not feel any pressure to improve their international-facing sites. However, I did also review what was offered on Taobao, and the product details were very incomplete there too. Taobao, however, has a built-in live chat with almost all sellers, which can be used to ask questions and thus compensate for missing product details.</p>
<p>I don&rsquo;t fully understand why the sales process seems unpolished. The websites feel almost like an afterthought &ndash; a checkbox to claim global reach while the real business apparently happens elsewhere via closed platforms or via inaccessible reseller channels. It is a frustrating reminder that in the RISC-V hardware world, the technology may be open and global, but the purchase experience is less so.</p>

<p><a href="https://optimizedbyotto.com/post/buying-spacemit-k3-risc-v-ai-cpu/">SpacemiT K3 is a compelling RISC-V AI CPU, but difficult to buy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava</title>
      <link rel="alternate" type="text/html" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/" />
      <id>https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/</id>
      <updated>2026-06-08T08:41:43+03:00</updated>
      <author><name>Kaj Arnö</name></author>
      <summary type="html"><![CDATA[<p>Interview with Sumit Srivastava, nominated in the Adoption &#038; Industry Impact category.<br />
I had the pleasure of speaking with Sumit Srivastava, SVP Business Development &#038; Products at Tayana, a Bangalore-based telecom software company. …<br />
Continue reading \"MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava\"<br />
The post MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava appeared first on MariaDB.org.</p>
<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/">MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Interview with Sumit Srivastava, nominated in the Adoption &amp; Industry Impact category.<br>
I had the pleasure of speaking with Sumit Srivastava, SVP Business Development &amp; Products at Tayana, a Bangalore-based telecom software company. &hellip; </p>
<p class="link-more"><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/" class="more-link">Continue reading<span class="screen-reader-text"> &ldquo;MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava&rdquo;</span></a></p>
<p>The post <a rel="nofollow" href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/">MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava</a> appeared first on <a rel="nofollow" href="https://mariadb.org">MariaDB.org</a>.</p>

<p><a href="https://mariadb.org/mariadb-foundation-sea-lion-champions-nominees-sumit-srivastava/">MariaDB Foundation Sea Lion Champions Nominees: Sumit Srivastava</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html" />
      <id>https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html</id>
      <updated>2026-06-04T14:17:32+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>A few months ago, I was asking myself how to insert in two tables in a single round-trip to the database.  I wanted to do that to optimize a process.  My optimization involved splitting a table in two, which would need inserting in two tables atomically.  The downside was changing an auto-commit INSERT to a transaction with two inserts, which was changing the shape of the workload</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html">Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A few months ago, I was asking myself how to insert in two tables in a single round-trip to the database.&nbsp; I wanted to do that to optimize a process.&nbsp; My optimization involved splitting a table in two, which would need inserting in two tables atomically.&nbsp; The downside was changing an auto-commit INSERT to a transaction with two inserts, which was changing the shape of the workload</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html">Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html" />
      <id>https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html</id>
      <updated>2026-06-04T14:17:32+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>A few months ago, I was asking myself how to insert in two tables in a single round-trip to the database.  I wanted to do that to optimize a process.  My optimization involved splitting a table in two, which would need inserting in two tables atomically.  The downside was changing an auto-commit INSERT to a transaction with two inserts, which was changing the shape of the workload</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html">Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>A few months ago, I was asking myself how to insert in two tables in a single round-trip to the database.&nbsp; I wanted to do that to optimize a process.&nbsp; My optimization involved splitting a table in two, which would need inserting in two tables atomically.&nbsp; The downside was changing an auto-commit INSERT to a transaction with two inserts, which was changing the shape of the workload</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/06/inserting-in-two-tables-in-single-round-trip.html">Inserting in Two Tables in a Single Round-Trip with JSON Duality Views in MySQL 9.7</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>I got swarmed by a replication issue</title>
      <link rel="alternate" type="text/html" href="https://medium.com/@arbaudie.it/i-got-swarmed-by-a-replication-issue-556c179783cc?source=rss-c779d007e7fe------2" />
      <id>https://medium.com/@arbaudie.it/i-got-swarmed-by-a-replication-issue-556c179783cc?source=rss-c779d007e7fe------2</id>
      <updated>2026-06-02T20:18:02+03:00</updated>
      <author><name>ArBauDie.IT</name></author>
      <summary type="html"><![CDATA[<p>I recently worked with a client who runs a mature CI/CD pipeline on GitLab. Docker Swarm as the orchestrator, config files versioned and pulled at deploy time, the whole nine yards. They had been deploying standalone MariaDB instances this way for a while, without a hitch.Then came the ask : stand up an async replication cluster. One primary, two replicas, one MaxScale instance sitting in front of it all. Quite classical, nothing out of the ordinary really.First deployment ? Smooth. Replication is running, MaxScale routing reads to the replicas, writes to the primary. We are all happy.Then we had to redeploy the replicas.Every time we redeployed the replica containers, replication broke. Every time, the fix was the same manual ceremony : connect to each replica, run CHANGE MASTER TO, START SLAVE, check SHOW SLAVE STATUS. Everything works fine after that.Until the next redeployment that is.Same config files, same image. No changes, just as promised by the CI/CD pipeline. Or so i thought.The error log is always your first stop in this situation. And it told us exactly what was wrong :[ERROR] Failed to open the relay log \'./568165be8cc3-relay-bin.000002\' (relay_log_pos 4463864)[ERROR] Could not find target log during relay log initialization[ERROR] Failed to initialize the master info structureThe replica was looking for a relay log file that did not exist. But it took me a while (3 hours actually) to connect the dots as i focused on the last line for a while.Here is the thing about MariaDB relay log file naming : by default MariaDB derives the relay log basename from the server\'s hostname. On a bare metal or VM setup, that hostname is immutable. You set it once, it never changes.In Docker, not so much. By default, Docker does set a container’s hostname to its short container ID — a random hash that changes at every docker run or container recreation. Swarm makes it even worse as it also rotates task IDs on every redeployment, so even if we would try and rely on some predictable naming pattern, Swarm would break it further. A task that was replica_1.1.xk3f8a9b2c becomes replica_1.1.yz9q2m7nkp after redeployment.So on the previous deployment, the relay logs were named something like xk3f8a9b2c-relay-bin.000001. On the new deployment, the relay log index file references those old names. The new container starts up, checks the index, finds filenames that don\'t match its own generated basename (yz9q2m7nkp-relay-bin.000001) , can\'t locate the relay logs, and replication fails to resume.The root cause : i had not explicitly set the relay log basename in configuration files. I had set everything else. Just not that.Fixing it only took one line in my.cnf. Yes, one line and voilà !relay_log     = relay-binHardcoding the relay log basename decouples replication state from container identity. Whatever hostname Docker Swarm decides to assign to the replica container, the relay log filenames stay constant. Replication resumes cleanly.While you’re at it, a few other directives are worth reviewing in any containerized replication setup :log_slave_updates = 1 — useful if you ever plan to chain replicas or use the replicas as a source for another downstream replica. Good habit regardless.relay_log_purge = 1 — keeps relay logs cleaned up automatically. In a container environment with limited storage you really want this on.relay_log_recovery = 1 — instructs the replica to recover relay log state from the master position on startup rather than relying on the relay log index. A solid safety net in ephemeral environments.expire_logs_days = 5 — instructs the replica to delete any binary log file in which the last event is older than 5 days. Helps with disk capacity planning.MaxScale was configured to monitor replication state on the replicas. And it did exactly what it was supposed to : the moment replication stopped, it pulled both replicas out of the read pool and flagged them as unavailable.MaxScale hid this bug from the app as expected. It also helped notice the issue. Proper replication monitoring in your proxy layer is not optional.One missing line in a configuration file ended up with three hours of head-scratching because of the following (wrong) assumptions :Docker Swarm and stateful services do not share the same concept of identity. Containers are stateless disposable resources, cattle. Rename, kill, redeploy as you see fit. Databases on the other hand are very much stateful. Stability is an implicit contract, including the hostname.Config-as-code is not the same as config completeness. The GitLab-driven deployment pipeline was running smooth. The config was versioned, reviewed, deployed automatically. The pipeline does exactly what it is instructed to, it does not tell what one forgot.Any MariaDB system variable that derives its default from a system value is a redeployment time-bomb in orchestrated environments. relay_log is the one that got our focus here. But the same logic applies to many other variables. If you run MariaDB in a stateless environment set these explicitly. Always.Stateful services in stateless environments will always bite back if you let the environment make assumptions on your behalf. The fix is one line but the overall lesson is to never let the orchestrator decide what you should control.Lessons learned !!If you’ve ever spent an afternoon staring at SHOW SLAVE STATUS wondering why a cluster that worked yesterday doesn\'t work today after a \"routine\" redeployment — I hope this saves you some trouble.Do not hesitate to reach out if you want to discuss your replication architecture or containerized database setup.</p>
<p><a href="https://medium.com/@arbaudie.it/i-got-swarmed-by-a-replication-issue-556c179783cc?source=rss-c779d007e7fe------2">I got swarmed by a replication issue</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I recently worked with a client who runs a mature CI/CD pipeline on GitLab. Docker Swarm as the orchestrator, config files versioned and pulled at deploy time, the whole nine yards. They had been deploying standalone MariaDB instances this way for a while, without a&nbsp;hitch.</p>
<p>Then came the ask&nbsp;: stand up an async replication cluster. One primary, two replicas, one MaxScale instance sitting in front of it all. Quite classical, nothing out of the ordinary&nbsp;really.</p>
<p>First deployment&nbsp;? Smooth. Replication is running, MaxScale routing reads to the replicas, writes to the primary. We are all&nbsp;happy.</p>
<p>Then we had to redeploy the replicas.</p>
<p>Every time we redeployed the replica containers, replication broke. <br>Every time, the fix was the same manual ceremony&nbsp;: connect to each replica, run CHANGE MASTER TO, START SLAVE, check SHOW SLAVE STATUS. Everything works fine after&nbsp;that.</p>
<p>Until the next redeployment that&nbsp;is.</p>
<p>Same config files, same image. No changes, just as promised by the CI/CD pipeline. Or so i&nbsp;thought.</p>
<p>The error log is always your first stop in this situation. And it told us exactly what was wrong&nbsp;:</p>
<pre>[ERROR] Failed to open the relay log './568165be8cc3-relay-bin.000002' (relay_log_pos 4463864)<br>[ERROR] Could not find target log during relay log initialization<br>[ERROR] Failed to initialize the master info structure</pre>
<p>The replica was looking for a relay log file that did not exist. But it took me a while (3 hours actually) to connect the dots as i focused on the last line for a&nbsp;while.</p>
<p>Here is the thing about MariaDB relay log file naming&nbsp;: by default MariaDB derives the relay log basename from the server&rsquo;s hostname. On a bare metal or VM setup, that hostname is immutable. You set it once, it never&nbsp;changes.</p>
<p>In Docker, not so much. By default, Docker does set a container&rsquo;s hostname to its short container ID&#8202;&mdash;&#8202;a random hash that changes at every docker run or container recreation. Swarm makes it even worse as it also rotates task IDs on every redeployment, so even if we would try and rely on some predictable naming pattern, Swarm would break it further. A task that was replica_1.1.xk3f8a9b2c becomes replica_1.1.yz9q2m7nkp after redeployment.</p>
<p>So on the previous deployment, the relay logs were named something like xk3f8a9b2c-relay-bin.000001. On the new deployment, the relay log index file references those old names. The new container starts up, checks the index, finds filenames that don&rsquo;t match its own generated basename (yz9q2m7nkp-relay-bin.000001)&nbsp;, can&rsquo;t locate the relay logs, and replication fails to&nbsp;resume.</p>
<p>The root cause&nbsp;: i had not explicitly set the relay log basename in configuration files. I had set everything else. Just not&nbsp;that.</p>
<p>Fixing it only took one line in my.cnf. Yes, one line and voil&agrave;&nbsp;!</p>
<pre>relay_log          = relay-bin</pre>
<p>Hardcoding the relay log basename decouples replication state from container identity. Whatever hostname Docker Swarm decides to assign to the replica container, the relay log filenames stay constant. Replication resumes&nbsp;cleanly.</p>
<p>While you&rsquo;re at it, a few other directives are worth reviewing in any containerized replication setup&nbsp;:</p>
<ul>
<li>log_slave_updates = 1&#8202;&mdash;&#8202;useful if you ever plan to chain replicas or use the replicas as a source for another downstream replica. Good habit regardless.</li>
<li>relay_log_purge = 1&#8202;&mdash;&#8202;keeps relay logs cleaned up automatically. In a container environment with limited storage you really want this&nbsp;on.</li>
<li>relay_log_recovery = 1&#8202;&mdash;&#8202;instructs the replica to recover relay log state from the master position on startup rather than relying on the relay log index. A solid safety net in ephemeral environments.</li>
<li>expire_logs_days = 5&#8202;&mdash;&#8202;instructs the replica to delete any binary log file in which the last event is older than 5 days. Helps with disk capacity planning.</li>
</ul>
<p>MaxScale was configured to monitor replication state on the replicas. And it did exactly what it was supposed to&nbsp;: the moment replication stopped, it pulled both replicas out of the read pool and flagged them as unavailable.</p>
<p>MaxScale hid this bug from the app as expected. It also helped notice the issue. Proper replication monitoring in your proxy layer is not optional.</p>
<p>One missing line in a configuration file ended up with three hours of head-scratching because of the following (wrong) assumptions&nbsp;:</p>
<ol>
<li><strong>Docker Swarm and stateful services do not share the same concept of identity.</strong> Containers are stateless disposable resources, cattle. Rename, kill, redeploy as you see fit. Databases on the other hand are very much stateful. Stability is an implicit contract, including the hostname.</li>
<li><strong>Config-as-code is not the same as config completeness.</strong> The GitLab-driven deployment pipeline was running smooth. The config was versioned, reviewed, deployed automatically. The pipeline does exactly what it is instructed to, it does not tell what one&nbsp;forgot.</li>
<li><strong>Any MariaDB system variable that derives its default from a system value is a redeployment time-bomb in orchestrated environments.</strong> relay_log is the one that got our focus here. But the same logic applies to many other variables. If you run MariaDB in a stateless environment set these explicitly. Always.</li>
</ol>
<p>Stateful services in stateless environments will always bite back if you let the environment make assumptions on your behalf. The fix is one line but the overall lesson is to never let the orchestrator decide what you should&nbsp;control.</p>
<p>Lessons learned&nbsp;!!</p>
<p>If you&rsquo;ve ever spent an afternoon staring at SHOW SLAVE STATUS wondering why a cluster that worked yesterday doesn&rsquo;t work today after a &ldquo;routine&rdquo; redeployment&#8202;&mdash;&#8202;I hope this saves you some&nbsp;trouble.</p>
<p>Do not hesitate to <a href="https://arbaudie.it/">reach out</a> if you want to discuss your replication architecture or containerized database&nbsp;setup.</p>
<p><img loading="lazy" decoding="async" src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=556c179783cc" width="1" height="1" alt=""></p>

<p><a href="https://medium.com/@arbaudie.it/i-got-swarmed-by-a-replication-issue-556c179783cc?source=rss-c779d007e7fe------2">I got swarmed by a replication issue</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Percona Community Slack is open — come hang out</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/06/02/percona-community-slack-open/" />
      <id>https://percona.community/blog/2026/06/02/percona-community-slack-open/</id>
      <updated>2026-06-02T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The Percona Community Slack is open — come hang out There’s a new place for the people behind the databases to actually talk to each other.</p>
<p><a href="https://percona.community/blog/2026/06/02/percona-community-slack-open/">The Percona Community Slack is open — come hang out</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1>The Percona Community Slack is open &mdash; come hang out<a class="anchor-link" id="the-percona-community-slack-is-open-come-hang-out"></a></h1>
<p>There&rsquo;s a new place for the people behind the databases to actually talk to each other.</p>
<p>The Percona Community Slack is open. Right now it&rsquo;s one channel &mdash; General &mdash; and that&rsquo;s intentional. It&rsquo;s a place for DBAs, developers, contributors, and database people of all kinds to meet, swap stories, and get to know who else is out there running open source databases for a living. No silos. No sub-channels for every topic. Just a room.</p>
<h2>What it&rsquo;s for<a class="anchor-link" id="what-its-for"></a></h2>
<p>Come here to talk shop. Share what you&rsquo;re building, breaking, or fixing. Post about the migration that went sideways, the config that finally clicked, the pager incident you survived. Ask the kind of questions that belong in a conversation rather than a ticket &mdash; &ldquo;how do other people handle X?&rdquo; is exactly the right energy.</p>
<p>It&rsquo;s also where we&rsquo;ll share events we&rsquo;re attending and, when we have tickets or a spare seat, offer them to the community first. If Percona is heading to a conference near you, this is where you&rsquo;ll hear about it. And if you&rsquo;re going somewhere yourself &mdash; a meetup, a conference, a local user group &mdash; tell us. There might be community members nearby who want to meet up.</p>
<p>That&rsquo;s the point, really. Less broadcast, more conversation.</p>
<h2>What it&rsquo;s not for<a class="anchor-link" id="what-its-not-for"></a></h2>
<p>Technical support questions belong on the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">Percona Community Forums</a>. Forum answers are searchable and don&rsquo;t disappear into scrollback. Percona engineers and experienced community members watch the forums for questions. Your problem is more likely to get a useful answer there &mdash; and it&rsquo;ll help the person who hits the same issue three months from now.</p>
<p>If you post a support question in Slack, expect to be pointed to the forums. That&rsquo;s not a brush-off.</p>
<h2>A few things that make this work<a class="anchor-link" id="a-few-things-that-make-this-work"></a></h2>
<p><strong>Introduce yourself.</strong> One or two sentences about what you work on and where in the world you are. That&rsquo;s it. You don&rsquo;t need a bio.</p>
<p><strong>Share what you&rsquo;re up to.</strong> An event you&rsquo;re going to, a tool you&rsquo;ve been testing, a war story from production. The low-key post about a thing you just dealt with is exactly what people come here for.</p>
<p><strong>Lurk freely.</strong> You don&rsquo;t have to post to belong. Read, learn, jump in when you have something to say.</p>
<h2>The short version of the rules<a class="anchor-link" id="the-short-version-of-the-rules"></a></h2>
<p>Be the person you&rsquo;d want to share an on-call rotation with.</p>
<p>Treat everyone as a peer. Assume good faith. No harassment. Critique technology on technical merits. Don&rsquo;t cold-DM people with pitches. Keep private things private. If something needs a moderator&rsquo;s attention, DM one directly &mdash; reports stay confidential.</p>
<h2>Come in<a class="anchor-link" id="come-in"></a></h2>
<p>If you&rsquo;re a DBA, a developer, a contributor, or just someone who runs databases and occasionally wants to talk to other people who run databases &mdash; you belong here.</p>
<p><a href="https://join.slack.com/t/percona/shared_invite/zt-3zqzw80xz-864PxCOIiiilYSVMnoN5ow" target="_blank" rel="noopener noreferrer">Join the Percona Community Slack &rarr;</a></p>

<p><a href="https://percona.community/blog/2026/06/02/percona-community-slack-open/">The Percona Community Slack is open — come hang out</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 2 &#8211; Postgres Layer</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/" />
      <id>https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/</id>
      <updated>2026-05-31T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>I’ll explain how I built the Postgres layer for semantic vector search on the Percona Community website: pgvector, chunks, two table modifications, the database schema, how the indexer populates Postgres, and what the SELECT statement looks like during a search.</p>
<p><a href="https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/">Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 2 &#8211; Postgres Layer</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I&rsquo;ll explain how I built the <strong>Postgres layer</strong> for semantic vector search on the Percona Community website: pgvector, chunks, two table modifications, the database schema, how the indexer populates Postgres, and <strong>what the SELECT statement looks like during a search</strong>.</p>
<blockquote>
<p><a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">Part 1</a>: why semantic search, what&rsquo;s already working on the site, the widget, and an overview of the stack.</p>
</blockquote>
<p><figure><img decoding="async" width="2496" height="1708" src="https://percona.community/blog/2026/05/search-part-2-postgres-website-search_hu_40abd7714926c564.webp" alt="Percona.community website search" loading="lazy"></figure>
</p>
<h2>Architecture<a class="anchor-link" id="architecture"></a></h2>
<p>Search runs separately from the website at <strong>search.percona.community</strong>: FastAPI, a background indexer, and PostgreSQL with pgvector are all in a single Docker Compose file. <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a> remains static on Hugo and GitHub Pages, it doesn&rsquo;t write directly to the database.</p>
<pre class="mermaid">
flowchart TB
subgraph users["Users"]
direction TB
User(["User"]) --&gt; Widget["Widget &middot; percona.community"]
Admin(["Admin"]) --&gt; Dash["Admin dashboard &middot; /demo"]
Site["Site &middot; RSS + HTML"]
end
subgraph app["Application"]
direction TB
API["FastAPI &middot; search.percona.community"]
Model["nomic-embed-text-v1"]
Worker["Indexer worker"]
API --&gt;|embed query| Model
Model --&gt;|embedding| API
Worker --&gt;|embed chunks| Model
Model --&gt;|embeddings| Worker
end
subgraph data["Database"]
direction TB
DB[("PostgreSQL + pgvector")]
end
Widget --&gt; API
Dash --&gt; API
Site --&gt; Worker
API --&gt;|read vectors &middot; write queue/history| DB
Worker --&gt;|write vectors &middot; read queue| DB
</pre>
<h3>Search<a class="anchor-link" id="search"></a></h3>
<p>A visitor enters a query into the widget. The widget sends a <code>POST /search</code> request to FastAPI. The service computes the query embedding with nomic with the prefix <code>search_query:</code> and searches for the nearest vectors in Postgres. The widget knows nothing about pgvector, it only receives JSON with links.</p>
<h3>Admin dashboard<a class="anchor-link" id="admin-dashboard"></a></h3>
<p>On the same FastAPI service I run an admin dashboard at <code>/demo</code>: test queries, search history, a database summary, viewing documents and chunks. The dashboard does not talk to Postgres directly, it only calls the API; the API reads and writes Postgres (<code>search_history</code>, <code>index_queue</code>, search results).</p>
<p><figure><img decoding="async" width="2364" height="1706" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-status_hu_494ee04c729ede44.webp" alt="Admin dashboard - Dashboard" loading="lazy"></figure>
</p>
<h3>Indexing<a class="anchor-link" id="indexing"></a></h3>
<p>To refresh the index, I click <strong>Start Indexing</strong> in the dashboard, that hits <code>POST /index/start</code>. The same endpoint can be called from outside: a GitHub webhook after a push to the site repo, cron, or curl while debugging. FastAPI enqueues the job in <code>index_queue</code>. A worker in the indexer container picks it up, downloads RSS and HTML from the site, splits text into chunks, computes vectors with nomic (<code>search_document:</code>), and writes to <code>pages</code>, <code>community_nomic</code>, and <code>indexer_runs</code>. The crawl runs in the background and does not block HTTP.</p>
<p><figure><img decoding="async" width="2262" height="1648" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-index_hu_45ebee06af7e8f31.webp" alt="Admin dashboard - Indexing" loading="lazy"></figure>
</p>
<p>Important limitation: the indexer and the API must use the same embedding model. The query vector and the vectors in the database must be from the same space, otherwise, cosine similarity doesn&rsquo;t make sense.</p>
<h2>pgvector in Postgres<a class="anchor-link" id="pgvector-in-postgres"></a></h2>
<p>For semantic search, you don&rsquo;t need an LLM, but an <strong>embedding model</strong>: a string as input, a vector as output. I chose <strong>nomic-embed-text-v1</strong>, 768-dimensional, running via <code>sentence-transformers</code> on the CPU, without a paid API.</p>
<p>I&rsquo;m using <strong><a href="https://docs.percona.com/postgresql/18/index.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL 18</a></strong>, pgvector is already included in the distribution; <code>CREATE EXTENSION vector</code>, and you&rsquo;re done (<a href="https://docs.percona.com/postgresql/18/enable-extensions.html#pgvector" target="_blank" rel="noopener noreferrer">documentation</a>).</p>
<p>The basic structure is a column with <strong>fixed dimensions</strong> for the model:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">vector</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">chunks</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="nb">SERIAL</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">chunk_text</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="n">vector</span><span class="p">(</span><span class="mi">768</span><span class="p">)</span><span class="w"> </span><span class="c1">-- exactly 768 under nomic
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>In the project, the table is named <code>community_nomic</code>: the prefix <code>community_</code> (site) + the model key <code>nomic</code>. I&rsquo;m setting up a comparison of embedding models: <strong>each</strong> model has <strong>its own</strong> vector table (<code>community_</code>), because the dimensions and embedding spaces are different, so they can&rsquo;t be mixed in a single table. Currently, there is one model in the project, <strong>nomic-embed-text-v1</strong>, 768 dimensions; later, I can add a second table <code>community_</code> and switch the index/API via <code>EMBEDDING_MODEL_KEY</code>.</p>
<p>pgvector compares vectors with several <strong>distance operators</strong>. I search with <strong>cosine distance</strong> (the <code></code> operator in SQL): the smaller the distance, the closer the match. In the widget and API I show <strong>similarity</strong>, not the raw distance, <code>similarity = 1 - distance</code>, so a higher score means a better hit. The operators:</p>
<table>
<thead>
<tr>
<th>Operator</th>
<th>When useful</th>
</tr>
</thead>
<tbody>
<tr>
<td><code></code></td>
<td>L2 (Euclidean)</td>
</tr>
<tr>
<td><code></code></td>
<td>inner product</td>
</tr>
<tr>
<td><code></code></td>
<td><strong>cosine</strong>, my choice for nomic</td>
</tr>
</tbody>
</table>
<p>Simplified search for &ldquo;nearest chunks&rdquo;:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">chunk_text</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">score</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">community_nomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>The threshold in the API is <code>min_score</code> (my default is <strong>0.52</strong>): anything lower is discarded. On beta I tuned this number for a while, the results changed noticeably depending on this single parameter.</p>
<p>To avoid scanning the entire table as the index grows, I set up an <strong>HNSW</strong> index (approximate nearest neighbor search):</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">community_nomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">hnsw</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">vector_cosine_ops</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>At this scale, a separate vector database wasn&rsquo;t necessary, a single Postgres instance handles metadata, vectors, and search.</p>
<h2>Postgres in Docker: <code>docker-compose</code><a class="anchor-link" id="postgres-in-docker-docker-compose"></a></h2>
<p>I set up the stack using <strong>Docker Compose</strong>, Postgres, the API, and the indexer are all in containers, with the same setup locally and in production. Production, <strong>EC2 on AWS</strong> (<code>search.percona.community</code>), an ARM instance, using the same <code>docker-compose</code>.</p>
<p>In <code>docker-compose.yml</code>, Postgres on Percona looks like this (on Mac ARM):</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-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">postgres</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">percona/percona-distribution-postgresql:18.1-3-arm64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">community_search</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">"5433:5432"</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pgdata:/var/lib/postgresql/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./init:/docker-entrypoint-initdb.d</span></span></span></code></pre>
</div>
</div>
</div>
<p>In <code>init/01-enable-pgvector.sql</code>, include only <code>CREATE EXTENSION IF NOT EXISTS vector</code>. If you&rsquo;re developing on <strong>x86</strong>, <strong>use</strong> amd64 in the image tag instead of <code>arm64</code>, see the options in the <a href="https://docs.percona.com/postgresql/18/index.html" target="_blank" rel="noopener noreferrer">Percona documentation</a>. I left <strong>arm64</strong> on both Mac and EC2: the configuration is the same.</p>
<p>I view the tables and data in <strong>pgAdmin</strong>. The <code>pages</code>, <code>community_nomic</code>, and service tables themselves are created when the API and indexer start using <code>ensure_*</code> functions in the code: these are <code>CREATE TABLE IF NOT EXISTS</code> and <code>CREATE INDEX IF NOT EXISTS</code>, not a separate migration directory.</p>
<h2>Indexing and Chunking<a class="anchor-link" id="indexing-and-chunking"></a></h2>
<p>The site doesn&rsquo;t write directly to the database, the database is populated by an <strong>indexer</strong>: a worker fetches RSS and HTML from <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a>, splits the text into chunks, computes embeddings, and writes the rows to <code>community_nomic</code> and <code>pages</code>. The widget and API only read what has already been written during searches.</p>
<h3>Why Chunks<a class="anchor-link" id="why-chunks"></a></h3>
<p>At first, I tried <strong>a single vector for the entire article</strong>. I quickly ran into three problems:</p>
<ul>
<li>Long text takes longer to encode and consumes more memory;</li>
<li>The model has an input length limit;</li>
<li>a single vector for long text <strong>blurs</strong> the meaning, a query about a specific paragraph doesn&rsquo;t map well to the &ldquo;averaged&rdquo; embedding of the entire article.</li>
</ul>
<p>I settled on a <strong>400-word</strong> window with a <strong>50</strong>-word overlap (<code>chunker.py</code>). Each chunk is a separate line with its own <code>embedding</code>.</p>
<p>The first version of the chunker sliced <strong>only the body</strong> of the article, without the title, author, date, or tags. For queries like &ldquo;articles by a certain author,&rdquo; the results were off: the model saw the text but not the document&rsquo;s context. I added <strong>metadata to each chunk</strong>, a <code>Title / Author / Date / Tags / Type</code> block at the beginning of each fragment before calculating the vector.</p>
<p><figure><img decoding="async" width="2068" height="1574" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-chunking_hu_448f58d17c5cf19c.webp" alt="Admin dashboard - Chunking" loading="lazy"></figure>
</p>
<p>When searching, the API finds the closest chunks, but the card shows <strong>the best chunk for the document</strong> (one <code>slug</code>, one card). Without this, a long article would clutter the results with multiple lines.</p>
<h2>Database Schema: Two Revisions of the Chunk Tables<a class="anchor-link" id="database-schema-two-revisions-of-the-chunk-tables"></a></h2>
<p>I revised the chunk storage schema <strong>twice</strong>, and separately added utility tables for background indexing and search logs.</p>
<h3>Version 1: Everything in a Single Table<a class="anchor-link" id="version-1-everything-in-a-single-table"></a></h3>
<p>The first working schema was <strong>a single table for all chunks and document information</strong>: each row represented a single article fragment, and it also contained duplicated page metadata (<strong>url, title, author, date, tags, content_type</strong>) along with <code>chunk_text</code> and <code>embedding</code>.</p>
<p>Pros: one <code>INSERT</code>, one <code>SELECT</code>, no joins.</p>
<p>Cons I encountered:</p>
<ul>
<li>one article, dozens of identical copies of title and author;</li>
<li>when updating a page, it&rsquo;s easy to get out of sync (one title in chunk #0, another in chunk #3);</li>
<li>fetching the image and description for the card from <code>chunk_text</code> was unreliable.</li>
</ul>
<p>Conclusion: A <strong>vector layer</strong> and a <strong>card in the UI</strong> serve different purposes.</p>
<p>The code still includes <code>_migrate_chunks_table</code>: when the API and indexer start up (inside <code>ensure_content_table</code>), it drops any extra columns from the chunk table if they are left over from the old prototype.</p>
<h3>Version 2: <code>pages</code> + <code>community_nomic</code><a class="anchor-link" id="version-2-pages-community_nomic"></a></h3>
<p>I split the data into two tables:</p>
<ul>
<li><strong><code>pages</code></strong>, one row per document: url, title, type, author, date, tags, images, description.</li>
<li><strong><code>community_nomic</code></strong>, only chunks: slug, chunk_index, chunk_text, embedding.</li>
</ul>
<p>They are linked by <code>slug</code> (stable key from the URL). Search: find the nearest chunks in <code>community_nomic</code>, assemble the card from <code>pages</code>.</p>
<p>In the admin dashboard I can open any indexed document and see what landed in <code>pages</code> (metadata, image, description) and what text was split into chunks.</p>
<p><figure><img decoding="async" width="2234" height="1192" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-details_hu_280bec26c6ad4f25.webp" alt="Admin dashboard, document details (pages) and chunks" loading="lazy"></figure>
</p>
<p>HNSW on <code>community_nomic</code>:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">INDEX</span><span class="w"> </span><span class="p">...</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">community_nomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">hnsw</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="n">vector_cosine_ops</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p>That&rsquo;s why, when switching models, I don&rsquo;t reuse <code>community_nomic</code>; instead, I create a new table and re-index it. A single search query involves vectors from <strong>only one</strong> model, both during indexing and in the API.</p>
<h2>Indexer: RSS, HTTP, and Queue<a class="anchor-link" id="indexer-rss-http-and-queue"></a></h2>
<p>The indexer is a separate container that crawls <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a> and populates the database. It starts with <strong>RSS</strong>, four feeds:</p>
<ul>
<li><a href="https://percona.community/blog/index.xml" target="_blank" rel="noopener noreferrer">blog/index.xml</a></li>
<li><a href="https://percona.community/events/index.xml" target="_blank" rel="noopener noreferrer">events/index.xml</a></li>
<li><a href="https://percona.community/talks/index.xml" target="_blank" rel="noopener noreferrer">talks/index.xml</a></li>
<li><a href="https://percona.community/contributors/index.xml" target="_blank" rel="noopener noreferrer">contributors/index.xml</a></li>
</ul>
<p>RSS feeds contain a title, link, date, author, tags, and often a short description, but <strong>not the full text of the article</strong>. For each entry, I perform an <strong>HTTP GET</strong> on the HTML page and extract the main content (in <code>crawler.py</code>). If the HTML is empty, I fall back to the description from the RSS feed.</p>
<p>The <strong>Index</strong> and <strong>Status</strong> tabs in the dashboard, without them, debugging the crawl and embedding would have been a guessing game.</p>
<p><figure><img decoding="async" width="2016" height="1622" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-index-running_hu_8d5557a28fba57a9.webp" alt="Admin dashboard - Index Running" loading="lazy"></figure>
</p>
<p><figure><img decoding="async" width="1986" height="690" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-index-history_hu_50a243ec8175dee5.webp" alt="Admin dashboard - Index history" loading="lazy"></figure>
</p>
<p><figure><img decoding="async" width="2078" height="1324" src="https://percona.community/blog/2026/05/search-part-2-postgres-dashboard-index-stats_hu_c4c215e93a9a0c6a.webp" alt="Admin dashboard - Index overview" loading="lazy"></figure>
</p>
<h2>Table Schema<a class="anchor-link" id="table-schema"></a></h2>
<p>All tables are created when the API and indexer start (<code>ensure_*</code> in code, <code>CREATE TABLE IF NOT EXISTS</code>, <code>CREATE INDEX IF NOT EXISTS</code>). There is no separate migrations folder. I don&rsquo;t use foreign keys between search and utility tables: reindexing deletes and re-inserts rows by <code>slug</code>, and the queue tables are only loosely linked.</p>
<h3>Search data<a class="anchor-link" id="search-data"></a></h3>
<p><strong><code>pages</code></strong> and <strong><code>community_nomic</code></strong> are linked by <code>slug</code> (no FK). The indexer writes both; the API reads them on <code>POST /search</code>.</p>
<h4>pages</h4>
<p>One row per document.</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>slug</code></td>
<td>TEXT</td>
<td>primary key, stable key from the URL</td>
</tr>
<tr>
<td><code>url</code></td>
<td>TEXT</td>
<td>canonical link (UNIQUE)</td>
</tr>
<tr>
<td><code>content_type</code></td>
<td>TEXT</td>
<td>blog, event, talk, contributor</td>
</tr>
<tr>
<td><code>title</code></td>
<td>TEXT</td>
<td>card title</td>
</tr>
<tr>
<td><code>date</code></td>
<td>TEXT</td>
<td>publication date from RSS/HTML</td>
</tr>
<tr>
<td><code>author</code></td>
<td>TEXT</td>
<td>author name</td>
</tr>
<tr>
<td><code>tags</code></td>
<td>TEXT[]</td>
<td>tags for search and chunk metadata</td>
</tr>
<tr>
<td><code>image_url</code></td>
<td>TEXT</td>
<td>full image from the site</td>
</tr>
<tr>
<td><code>image_thumb_url</code></td>
<td>TEXT</td>
<td>smaller image for the widget popup</td>
</tr>
<tr>
<td><code>description</code></td>
<td>TEXT</td>
<td>short description for the card</td>
</tr>
<tr>
<td><code>updated_at</code></td>
<td>TIMESTAMPTZ</td>
<td>last time the row was indexed</td>
</tr>
</tbody>
</table>
<h4>community_nomic</h4>
<p>Chunks and vectors (table name = site + model key).</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>SERIAL</td>
<td>primary key</td>
</tr>
<tr>
<td><code>slug</code></td>
<td>TEXT</td>
<td>link to <code>pages</code></td>
</tr>
<tr>
<td><code>chunk_index</code></td>
<td>INT</td>
<td>chunk position in the document (UNIQUE with <code>slug</code>)</td>
</tr>
<tr>
<td><code>chunk_text</code></td>
<td>TEXT</td>
<td>text passed to the embedding model</td>
</tr>
<tr>
<td><code>embedding</code></td>
<td>vector(768)</td>
<td>nomic vector for cosine search</td>
</tr>
</tbody>
</table>
<h3>Utility<a class="anchor-link" id="utility"></a></h3>
<p>Three small tables for indexing and debugging.</p>
<h4>index_queue</h4>
<p>Pending jobs. Written by the API.</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>SERIAL</td>
<td>primary key</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>TIMESTAMPTZ</td>
<td>when the job was queued</td>
</tr>
<tr>
<td><code>status</code></td>
<td>TEXT</td>
<td>pending, running, done, cancelled</td>
</tr>
<tr>
<td><code>model</code></td>
<td>TEXT</td>
<td>embedding model key (<code>nomic</code>)</td>
</tr>
<tr>
<td><code>feeds</code></td>
<td>TEXT</td>
<td>RSS feed URLs (comma-separated)</td>
</tr>
<tr>
<td><code>crawl_delay</code></td>
<td>FLOAT</td>
<td>pause between HTTP requests (seconds)</td>
</tr>
<tr>
<td><code>limit_per_type</code></td>
<td>INT</td>
<td>cap per content type (partial reindex)</td>
</tr>
<tr>
<td><code>run_id</code></td>
<td>INT</td>
<td><code>indexer_runs.id</code> once the worker starts</td>
</tr>
<tr>
<td><code>cancel_requested</code></td>
<td>BOOLEAN</td>
<td>cancel flag from the dashboard</td>
</tr>
</tbody>
</table>
<h4>indexer_runs</h4>
<p>Crawl progress. Written by the indexer worker.</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>SERIAL</td>
<td>primary key</td>
</tr>
<tr>
<td><code>started_at</code></td>
<td>TIMESTAMPTZ</td>
<td>run start</td>
</tr>
<tr>
<td><code>finished_at</code></td>
<td>TIMESTAMPTZ</td>
<td>run end</td>
</tr>
<tr>
<td><code>status</code></td>
<td>TEXT</td>
<td>running, done, error, cancelled</td>
</tr>
<tr>
<td><code>model</code></td>
<td>TEXT</td>
<td>embedding model key</td>
</tr>
<tr>
<td><code>total_docs</code></td>
<td>INT</td>
<td>documents processed</td>
</tr>
<tr>
<td><code>total_chunks</code></td>
<td>INT</td>
<td>chunks written</td>
</tr>
<tr>
<td><code>current_url</code></td>
<td>TEXT</td>
<td>page being crawled</td>
</tr>
<tr>
<td><code>current_doc_num</code></td>
<td>INT</td>
<td>document counter</td>
</tr>
<tr>
<td><code>errors</code></td>
<td>INT</td>
<td>error count</td>
</tr>
<tr>
<td><code>message</code></td>
<td>TEXT</td>
<td>status or error text</td>
</tr>
</tbody>
</table>
<h4>search_history</h4>
<p>Search log. Written by the API on each <code>POST /search</code>.</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Type</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>SERIAL</td>
<td>primary key</td>
</tr>
<tr>
<td><code>created_at</code></td>
<td>TIMESTAMPTZ</td>
<td>query time</td>
</tr>
<tr>
<td><code>query</code></td>
<td>TEXT</td>
<td>user query</td>
</tr>
<tr>
<td><code>content_type</code></td>
<td>TEXT</td>
<td>filter: all or one type</td>
</tr>
<tr>
<td><code>limit_requested</code></td>
<td>INT</td>
<td>requested result limit</td>
</tr>
<tr>
<td><code>results_count</code></td>
<td>INT</td>
<td>rows returned</td>
</tr>
<tr>
<td><code>chunks_in_index</code></td>
<td>INT</td>
<td>snapshot: chunk count at query time</td>
</tr>
<tr>
<td><code>by_type</code></td>
<td>JSONB</td>
<td>hit counts per content type</td>
</tr>
<tr>
<td><code>prepare_ms</code></td>
<td>REAL</td>
<td>API timing breakdown</td>
</tr>
<tr>
<td><code>model_load_ms</code></td>
<td>REAL</td>
<td>model load time</td>
</tr>
<tr>
<td><code>embed_ms</code></td>
<td>REAL</td>
<td>embedding time</td>
</tr>
<tr>
<td><code>db_ms</code></td>
<td>REAL</td>
<td>Postgres search time</td>
</tr>
<tr>
<td><code>format_ms</code></td>
<td>REAL</td>
<td>JSON formatting time</td>
</tr>
<tr>
<td><code>total_ms</code></td>
<td>REAL</td>
<td>end-to-end time</td>
</tr>
<tr>
<td><code>model</code></td>
<td>TEXT</td>
<td>embedding model key</td>
</tr>
</tbody>
</table>
<h3>Indexes<a class="anchor-link" id="indexes"></a></h3>
<p>Created in the same <code>ensure_*</code> functions as the tables. Besides primary keys and <code>UNIQUE</code> on <code>pages.url</code> and <code>(slug, chunk_index)</code> in <code>community_nomic</code>:</p>
<ul>
<li><strong><code>pages_content_type_idx</code></strong> on <code>content_type</code>, filter by blog / event / talk / contributor in search;</li>
<li><strong><code>community_nomic_embedding_idx</code></strong>, <strong>HNSW</strong> on <code>embedding</code> (<code>vector_cosine_ops</code>); without it, nearest-neighbor search would scan the whole table as chunks grow;</li>
<li><strong><code>community_nomic_slug_idx</code></strong> on <code>slug</code>, delete all chunks for one document on reindex;</li>
<li><strong><code>search_history_created_at_idx</code></strong>, recent queries first in the dashboard History tab.</li>
</ul>
<p><code>index_queue</code> and <code>indexer_runs</code> only have a serial primary key, few rows, a full scan is fine.</p>
<h2>How Postgres Responds to a Search Query<a class="anchor-link" id="how-postgres-responds-to-a-search-query"></a></h2>
<p>The API receives the query text, computes a vector with nomic (<code>search_query:</code> + text), and runs SQL that finds the nearest chunks and joins row metadata from <code>pages</code>.</p>
<h3>The First Query Was Naive<a class="anchor-link" id="the-first-query-was-naive"></a></h3>
<p>At first, I did what the pgvector tutorials suggest, &ldquo;find the 20 closest vectors&rdquo;:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">chunk_index</span><span class="p">,</span><span class="w"> </span><span class="n">chunk_text</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="p">(</span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">score</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">community_nomic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>The query <strong>worked</strong>, but the results were incorrect from a UI perspective. It returns <strong>20 chunks</strong>, not <strong>20 documents</strong>. A long article with fifteen chunks could take up <strong>half the list</strong> with a single <code>slug</code>; a short post with one good paragraph didn&rsquo;t make it to the top. The user sees <strong>pages</strong> (cards with links), but we search the database by <strong>chunks</strong>, I close that gap in SQL.</p>
<h3>What I do now<a class="anchor-link" id="what-i-do-now"></a></h3>
<ol>
<li><strong>Join</strong> <code>community_nomic</code> + <code>pages</code> by <code>slug</code>.</li>
<li><code>ROW_NUMBER() PARTITION BY slug</code>, I keep <strong>one</strong> best chunk per document.</li>
<li><code>WHERE score &gt;= min_score</code> (default <strong>0.52</strong>).</li>
<li><code>ORDER BY score DESC LIMIT N</code>.</li>
</ol>
<p>Simplified version of the final query:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">ranked</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">content_type</span><span class="p">,</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">slug</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">score</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">ROW_NUMBER</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">slug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">embedding</span><span class="w"> </span><span class="o"></span><span class="w"> </span><span class="err">$</span><span class="n">query_vector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">rn</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">community_nomic</span><span class="w"> </span><span class="k">c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">INNER</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">pages</span><span class="w"> </span><span class="n">p</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">p</span><span class="p">.</span><span class="n">slug</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">slug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c1">-- AND p.content_type = 'blog' -- optional: filter by type
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">best_per_page</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">ranked</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">rn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">title</span><span class="p">,</span><span class="w"> </span><span class="n">content_type</span><span class="p">,</span><span class="w"> </span><span class="n">slug</span><span class="p">,</span><span class="w"> </span><span class="n">score</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">best_per_page</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">score</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">52</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">score</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Filtering by content type<a class="anchor-link" id="filtering-by-content-type"></a></h3>
<p>The site has blog, event, talk, and contributor, in the widget and on <code>/search/</code>, you can search for <strong>all at once</strong> or a single type. In the API, this is the <code>content_type</code> field in <code>POST /search</code>; in SQL, <code>AND p.content_type = %s</code> is added when a single type is selected.</p>
<h3>Sort order in the widget<a class="anchor-link" id="sort-order-in-the-widget"></a></h3>
<p>In SQL, results are ranked by similarity (<code>ORDER BY score DESC</code>), &ldquo;what matches the query best?&rdquo;</p>
<p>On a community site, <strong>recent material often matters as much as the top semantic match</strong>. An older article might score 0.71 while a newer post on the same topic scores 0.66. I still build the shortlist in SQL (one best chunk per document, <code>min_score</code> threshold), but the API then <strong>re-sorts blog, event, and talk by publication date</strong>, newest first. Contributors and rows without a date stay at the bottom.</p>
<p>The widget still shows the <strong>similarity score</strong> on each card so you can see why the page was included:</p>
<p><figure><img decoding="async" width="2426" height="1700" src="https://percona.community/blog/2026/05/search-part-2-postgres-widget-scores_hu_18e89fd6122bbc80.webp" alt="Widget search results with similarity scores" loading="lazy"></figure>
</p>
<h2>Summary<a class="anchor-link" id="summary"></a></h2>
<p><strong>Postgres layer</strong>: I set this up without a separate vector DB, using pgvector in Percona, two table modifications for chunking, auxiliary tables for background indexing, HNSW, and SQL with &ldquo;best-fit chunk per document.&rdquo; The indexer processes RSS and HTML; I manage the database in pgAdmin.</p>
<p>Currently, the search index has about <strong>803</strong> documents and <strong>1,656</strong> vectors, thousands of rows, not billions. This is a community-scale setup: a single Postgres instance on EC2, embedding on the CPU, HNSW on all chunks, the solutions above were chosen with this in mind. When I add videos, GitHub issues, and the forum, the volume will grow, then I&rsquo;ll re-evaluate the indexing time and hardware.</p>
<h3>Note from the author<a class="anchor-link" id="note-from-the-author"></a></h3>
<p>About <strong>six months ago</strong>, I already tried to set up something similar to Postgres + vectors using AI agents. Back then, I kept running into the same issues: a clunky <strong>startup</strong> of the environment, the <strong>schema</strong> and its <strong>modifications</strong>, <strong>initializing Percona Distribution for PostgreSQL</strong>, and pgvector, the agent would either skip a step or suggest incompatible configuration snippets.</p>
<p>This time, with <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a>, went better: the agent set up Compose, <code>ensure_*</code>, search SQL, and the admin dashboard, without that series of failures at startup. More time was spent on the logic (chunks, <code>min_score</code>, result ordering) rather than on &ldquo;why the database won&rsquo;t start.&rdquo;</p>
<p>If you try this setup yourself or notice any inaccuracies, please leave a comment.</p>

<p><a href="https://percona.community/blog/2026/05/31/semantic-search-on-postgresql-part-2/">Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 2 &#8211; Postgres Layer</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 1 &#8211; Introduction</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/" />
      <id>https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/</id>
      <updated>2026-05-29T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Type “zero downtime database migration” into the site’s search bar and you’ll get articles and talks about database migration with minimal downtime, even if those words aren’t in the titles or content. This is semantic search on PostgreSQL and pgvector, without paid embedding APIs or a separate vector database. In this series I’ll cover how it works and why I chose this stack.</p>
<p><a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 1 &#8211; Introduction</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Type &ldquo;zero downtime database migration&rdquo; into the site&rsquo;s search bar and you&rsquo;ll get articles and talks about database migration with minimal downtime, even if those words aren&rsquo;t in the titles or content. This is <strong>semantic search</strong> on <strong>PostgreSQL</strong> and <strong><a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer">pgvector</a></strong>, without paid embedding APIs or a separate vector database. In this series I&rsquo;ll cover how it works and why I chose this stack.</p>
<p>I&rsquo;ll walk through how and why I built the search for our community site: blog, events, talks, and profiles. The post should help if you want to repeat the approach or need a practical case study on simple components. If you&rsquo;ve done something similar, I&rsquo;d like to hear your feedback.</p>
<p><figure><img decoding="async" width="2238" height="1698" src="https://percona.community/blog/2026/05/search-part-1-intro-kubernetes_hu_be512b221b02d108.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Introduction" loading="lazy"></figure>
</p>
<h2>Context: Website, Search, and Task<a class="anchor-link" id="context-website-search-and-task"></a></h2>
<p>The community team has a website on <strong>Hugo</strong>, an open source static site generator, hosted for free on <strong>GitHub Pages</strong>. The site has articles, events, talks, videos, and more.</p>
<blockquote>
<p>If you&rsquo;re thinking of starting your own, I recommend checking out these examples: <a href="https://blog.koehntopp.info/" target="_blank" rel="noopener noreferrer">blog.koehntopp.info</a>, <a href="https://openeverest.io/" target="_blank" rel="noopener noreferrer">openeverest.io</a>, <a href="https://perconalive.com/" target="_blank" rel="noopener noreferrer">perconalive.com</a>, <a href="https://oursqlfoundation.org/" target="_blank" rel="noopener noreferrer">oursqlfoundation.org</a></p>
</blockquote>
<p>But a Hugo site is a collection of HTML files without a backend. Search or filters only work via frontend JS or an external service. For a long time our site had no search at all. Then <strong>Kai Wagner</strong> contributed a JS search for the blog that matched exact words (<a href="https://percona.community/blog" target="_blank" rel="noopener noreferrer">percona.community/blog</a>).</p>
<p>Recently our community lead <strong>Laura Czajkowski</strong> asked for smart AI search on the site. We tried several off-the-shelf products; they were either too expensive or a poor fit. We also want search to cover more than the site itself eventually: videos from other platforms, the forum, our GitHub repos, and maybe documentation later.</p>
<p>I suggested building it ourselves. Modern AI assistants are good enough for a prototype like this. Below I&rsquo;ll explain the stack.</p>
<h2>What We&rsquo;ll Do<a class="anchor-link" id="what-well-do"></a></h2>
<p>The site stays on Hugo and GitHub Pages. The search service runs <strong>separately</strong>; for this architecture that&rsquo;s the sensible option. The goal is simple: the user types a query in plain language and gets a list of semantically relevant links.</p>
<p>Kai&rsquo;s keyword search was a step forward, but it doesn&rsquo;t catch <strong>meaning</strong>. Type &ldquo;postgresql&rdquo; and you get pages where the word appears. An article about slow queries or replication may be missing if the wording is different. <strong>Semantic search</strong> works differently: the query and documents become <strong>vectors</strong>, numeric representations of meaning (<strong>embedding</strong>). Similar meaning lands nearby in vector space even when the words differ. A query like &ldquo;how to speed up slow queries in MySQL&rdquo; can surface tuning and optimization content without those words in the title.</p>
<p>Why not another engine? <strong><a href="https://opensearch.org/" target="_blank" rel="noopener noreferrer">OpenSearch</a></strong> is a solid open-source option: full-text and vector search, mature ecosystem. I also looked at <strong><a href="https://manticoresearch.com/" target="_blank" rel="noopener noreferrer">Manticore Search</a></strong>. Both work, but <strong>semantics</strong> still need an embedding pipeline (model at index time and on each query). That&rsquo;s another service to run beside the model.</p>
<p>I wanted my own stack on <strong>Postgres</strong> with pgvector: a practical experiment, not a hunt for the perfect search product. <strong>PostgreSQL with <a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer">pgvector</a></strong> keeps page metadata, chunks, vectors, and query history in one database. <strong><a href="https://docs.percona.com/postgresql/18/index.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL 18</a></strong> ships pgvector in the distribution; run <code>CREATE EXTENSION vector</code> and you&rsquo;re set.</p>
<p>The plan has four parts:</p>
<ol>
<li><strong>Widget</strong> on the site: search field and results (plain JS; Hugo unchanged).</li>
<li><strong>API</strong>: takes the query, embeds it with the same model as indexing, searches the DB, returns JSON links.</li>
<li><strong>Indexer</strong>: background worker that reads RSS/HTML, chunks text, embeds, writes to the DB.</li>
<li><strong>PostgreSQL + pgvector</strong>: one database for metadata, chunks, vectors, and search history.</li>
</ol>
<p>Hugo stays static; the smart parts live in a separate service. No separate vector DB, no paid embedding API, no RAG chat, only links.</p>
<p>The diagram shows two flows: <strong>search</strong> (user query) and <strong>indexing</strong> (refresh the DB on demand or on a schedule). Top to bottom, from the user:</p>
<pre class="mermaid">
flowchart TB
User(["&#128100; User"])
Widget["&#128269; JS widget<br>percona.community &middot; GitHub Pages"]
API["&#9889; FastAPI<br>search.percona.community"]
Model["&#129504; Embedding model<br>shared &middot; API &amp; indexer"]
DB[("&#128452;&#65039; PostgreSQL + pgvector")]
Content["&#128240; Content<br>blog &middot; events &middot; talks"]
Indexer["&#128229; Indexer worker"]
User --&gt;|"&#9312; query"| Widget
Widget --&gt;|"&#9313; POST /search"| API
API |embed query| Model
API |"&#9314; vector search"| DB
API --&gt;|"&#9315; results"| Widget
Widget --&gt; User
Content --&gt;|"A. RSS + HTML"| Indexer
Indexer |embed chunks| Model
Indexer --&gt;|"B. chunks + vectors"| DB
style User fill:#e1f5ff
style Widget fill:#fff4e6
style Content fill:#fff9e6
style API fill:#ffe6e6
style Model fill:#fff0f5
style Indexer fill:#f0e6ff
style DB fill:#e6ffe6
</pre>
<p>The diagram shows the shared <strong>embedding model</strong>; worth stating explicitly anyway. <strong>The indexer and the API must use the same model.</strong> Query vectors and stored vectors must share one space or search is meaningless. Don&rsquo;t mix Nomic at index time with OpenAI at query time, for example. The widget only sends text; it doesn&rsquo;t know which model runs behind the API.</p>
<p>On paper it looked simple. In practice I changed the database schema <strong>three times</strong> and tuned ranking so blog posts didn&rsquo;t crowd out events and talks. The <strong>similarity threshold</strong> mattered more than I expected: one parameter, large swing in results. Still, within a few days we had a working beta on the live site. Here&rsquo;s what shipped.</p>
<h2>The Result (Spoiler)<a class="anchor-link" id="the-result-spoiler"></a></h2>
<p>It took about <strong>three unhurried days</strong> and roughly <strong>$20 in Cursor tokens</strong> to build, debug, and deploy. Try it on <strong><a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a></strong> (search icon in the header) or <strong><a href="https://percona.community/search/" target="_blank" rel="noopener noreferrer">percona.community/search/</a></strong>.</p>
<p>The index currently covers the site: blog, events, talks, member profiles. Video from other platforms, the forum, and GitHub are planned; the design should allow new sources without replacing the stack.</p>
<p>This is <strong>beta</strong>: the content is public and search isn&rsquo;t business-critical, but I watch stability and security.</p>
<h3>Website Widget<a class="anchor-link" id="website-widget"></a></h3>
<p>The header has a search icon. Click it to get an input field and a popup with results, <strong>similarity score</strong> (0 to 1, how close the hit is in meaning), and API latency. The site stays static; the widget calls <code>search.percona.community</code> and renders JSON. &ldquo;All results&rdquo; opens <code>/search/</code>.</p>
<p>Try it on <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a>, e.g. <code>slow queries mysql tuning</code> or <code>kubernetes operator database</code>. Comments welcome if something feels off.</p>
<p><figure><img decoding="async" width="2414" height="1696" src="https://percona.community/blog/2026/05/search-part-1-intro-pz-talks_hu_1dcd2ecc4916d346.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Widget" loading="lazy"></figure>
</p>
<h3>Full Results Page<a class="anchor-link" id="full-results-page"></a></h3>
<p>A separate <code>/search/</code> page with filters by content type, cards, and links.</p>
<p><figure><img decoding="async" width="2726" height="1714" src="https://percona.community/blog/2026/05/search-part-1-intro-page_hu_ef00d7ba277d81ba.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Search Page" loading="lazy"></figure>
</p>
<p><a href="https://percona.community/search/?q=Postgres+backup+solutions&amp;type=blog" target="_blank" rel="noopener noreferrer">Example</a></p>
<h3>API<a class="anchor-link" id="api"></a></h3>
<p><strong>FastAPI</strong> at <code>https://search.percona.community</code>: embed the query, search Postgres, return JSON with links, scores, and timings (model vs database).</p>
<p>The service runs on <strong>AWS EC2</strong> in Docker Compose: API, indexer, Postgres.</p>
<h3>Demo Dashboard<a class="anchor-link" id="demo-dashboard"></a></h3>
<p>The Cursor AI agent handled a lot of the boilerplate, so I also built a <strong>dev dashboard</strong> (<code>/demo</code>) to test search, run indexing, inspect history, and browse indexed chunks. Not for production, but it saved debugging time.</p>
<p>Demo Dashboard</p>
<figure><img decoding="async" width="2300" height="1616" src="https://percona.community/blog/2026/05/search-part-1-intro-demo-search_hu_b171c38bfdc07801.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Demo Dashboard Search" loading="lazy"></figure>

<p>Search history: making search better</p>
<p><figure><img decoding="async" width="2190" height="1000" src="https://percona.community/blog/2026/05/search-part-1-intro-demo-history_hu_70c2a3d3114aef9f.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Demo Dashboard Search history" loading="lazy"></figure>
</p>
<p>Indexing status, to see when search data was last updated</p>
<p><figure><img decoding="async" width="2434" height="1478" src="https://percona.community/blog/2026/05/search-part-1-intro-demo-status_hu_8b567ed32a4500bb.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Demo Dashboard Indexing status" loading="lazy"></figure>
</p>
<p>Indexed documents with the ability to view data and chunks.</p>
<p><figure><img decoding="async" width="2486" height="1574" src="https://percona.community/blog/2026/05/search-part-1-intro-demo-pages_hu_f026d5ac3a4d0b12.webp" alt="Smart Semantic Search using PostgreSQL and pgvector - Demo Dashboard Indexed documents" loading="lazy"></figure>
</p>
<h3>What I Used<a class="anchor-link" id="what-i-used"></a></h3>
<p>Briefly, <strong>why</strong> this stack (deeper comparison in <strong>part two</strong>):</p>
<ul>
<li>
<p><strong><a href="https://www.postgresql.org/" target="_blank" rel="noopener noreferrer">PostgreSQL</a></strong> + <strong><a href="https://github.com/pgvector/pgvector" target="_blank" rel="noopener noreferrer">pgvector</a></strong>: vectors and metadata in one DB. Cosine similarity plus an HNSW index is enough at community scale. (<a href="https://docs.percona.com/postgresql/18/enable-extensions.html#pgvector" target="_blank" rel="noopener noreferrer">pgvector in Percona docs</a>)</p>
</li>
<li>
<p><strong><a href="https://docs.percona.com/postgresql/18/index.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL 18</a></strong>: PostgreSQL with pgvector and a Docker image. Vanilla Postgres works too if you install the extension; I used Percona to try &ldquo;their&rdquo; Postgres + pgvector in a real deploy.</p>
</li>
<li>
<p><strong><a href="https://www.python.org/" target="_blank" rel="noopener noreferrer">Python</a></strong> + <strong><a href="https://fastapi.tiangolo.com/" target="_blank" rel="noopener noreferrer">FastAPI</a></strong>: fast API setup, OpenAPI included, good libraries for crawl/embed/Postgres.</p>
</li>
<li>
<p><strong><a href="https://huggingface.co/nomic-ai/nomic-embed-text-v1" target="_blank" rel="noopener noreferrer">nomic-embed-text-v1</a></strong> + <strong><a href="https://www.sbert.net/" target="_blank" rel="noopener noreferrer">sentence-transformers</a></strong>: open model, 768 dims, CPU-friendly, no per-chunk API bill. Index and query must use the <strong>same</strong> model; Nomic fits. I&rsquo;ll compare others later.</p>
</li>
<li>
<p><strong><a href="https://gohugo.io/" target="_blank" rel="noopener noreferrer">Hugo</a></strong> + <strong>JavaScript</strong>: thin widget on existing static site.</p>
</li>
<li>
<p><strong><a href="https://www.docker.com/" target="_blank" rel="noopener noreferrer">Docker</a></strong> / <strong>Docker Compose</strong>: same layout locally and on EC2.</p>
</li>
<li>
<p><strong><a href="https://aws.amazon.com/ec2/" target="_blank" rel="noopener noreferrer">AWS EC2</a></strong> + <strong>nginx</strong>: HTTPS on <code>search.percona.community</code>, CORS for GitHub Pages.</p>
</li>
<li>
<p><strong>AI-assisted development</strong> (I used <a href="https://cursor.com/" target="_blank" rel="noopener noreferrer">Cursor</a>): the agent handled boilerplate, wiring, and Docker fixes. I reviewed everything. Any similar AI coding tool would work; the point is having one.</p>
</li>
</ul>
<h3>How long it took<a class="anchor-link" id="how-long-it-took"></a></h3>
<ul>
<li><strong>~6 hours</strong> with an AI coding assistant to a first prototype: crawl, API, Docker, basic demo;</li>
<li><strong>~2 more days</strong> for schema changes, per-type ranking, embed/page widget, search history, dashboard, indexer fixes, EC2 deploy;</li>
<li><strong>~$20</strong> in AI assistant tokens total.</li>
</ul>
<p>Without AI I&rsquo;d have stretched the same work over weeks. With the agent I mostly wrote tasks, checked output, and fixed edges.</p>
<h3>About the code and repository<a class="anchor-link" id="about-the-code-and-repository"></a></h3>
<p>I&rsquo;m not publishing the repo yet. The code is tied to <strong>percona.community</strong>: our RSS feeds, content types, Hugo widget, EC2 layout. It&rsquo;s an internal prototype, not a reusable library.</p>
<p>If you wanted a drop-in repo: porting someone else&rsquo;s monolith often takes longer than rebuilding from a clear sketch. Part two will have architecture, schema, and stack notes enough for a Cursor agent (or similar) to rebuild for <strong>your</strong> feeds and UI.</p>
<p>Interested in a <strong>generic open source</strong> or <strong>search-as-a-service</strong> version? Say so in the comments; I&rsquo;m weighing whether it&rsquo;s worth a separate project.</p>
<h3>What&rsquo;s Next<a class="anchor-link" id="whats-next"></a></h3>
<p>Try search on <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a> and comment what you find, especially where semantics beat the old substring search.</p>
<p>Part <strong>two</strong> will go inside: schema (including those three rewrites), chunking, HNSW, per-type result caps, and a local Docker Compose walkthrough.</p>

<p><a href="https://percona.community/blog/2026/05/29/semantic-search-on-postgresql-part-1/">Building Smart Semantic Search using PostgreSQL and pgvector. Case Study &#8211; Part 1 &#8211; Introduction</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Write for the Percona Community</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/22/write-for-percona-community/" />
      <id>https://percona.community/blog/2026/05/22/write-for-percona-community/</id>
      <updated>2026-05-22T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>You’ve fixed something gnarly in production this year. You’ve migrated a database that nobody wanted to touch. You’ve built something on top of Percona Operators, or Percona Toolkit, or Percona Monitoring and Management (PMM), and you’ve learned things along the way that aren’t written down anywhere yet.</p>
<p><a href="https://percona.community/blog/2026/05/22/write-for-percona-community/">Write for the Percona Community</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>You&rsquo;ve fixed something gnarly in production this year. You&rsquo;ve migrated a database that nobody wanted to touch. You&rsquo;ve built something on top of Percona Operators, or Percona Toolkit, or Percona Monitoring and Management (PMM), and you&rsquo;ve learned things along the way that aren&rsquo;t written down anywhere yet.</p>
<p>Write it up. We&rsquo;ll publish it, and we&rsquo;ll pay you.</p>
<h2>What we&rsquo;re doing<a class="anchor-link" id="what-were-doing"></a></h2>
<p>The Percona Community Writers Program publishes technical posts from the people actually using these tools &mdash; DBAs, developers, contributors, and engineers running real workloads. Posts go up on <a href="https://percona.community/blog" target="_blank" rel="noopener noreferrer">percona.community/blog</a> under your name, with your bio and links.</p>
<p>For every post we publish, you get:</p>
<ul>
<li><strong>$350</strong> paid out after publication</li>
<li><strong>Community engagement points</strong> you can redeem in our swag store for t-shirts, stickers, and other items</li>
</ul>
<p>The points stack across contributions. The more you write, the more you collect.</p>
<h3>A note on payment<a class="anchor-link" id="a-note-on-payment"></a></h3>
<p><em>Not everyone can accept payment for writing &mdash; employment contracts, tax situations, visa rules, and conflict-of-interest policies all get in the way. If that&rsquo;s you, we&rsquo;ll donate the same $350 to an open source project or community of your choice on your behalf. Tell us who to send it to when you pitch.</em></p>
<h2>What we want to read<a class="anchor-link" id="what-we-want-to-read"></a></h2>
<p>Anything you&rsquo;ve done with the Percona stack &mdash; or alongside it &mdash; that another engineer would learn from. Some directions to consider:</p>
<ul>
<li><strong>Percona Operators</strong> &mdash; running databases on Kubernetes, scaling decisions, upgrade paths, what surprised you</li>
<li><strong>Percona Toolkit</strong> &mdash; how you use specific tools in your day-to-day, scripts you&rsquo;ve built around them, edge cases</li>
<li><strong>Migrations</strong> &mdash; moving between versions, between database engines, on-premises to cloud, the parts that aren&rsquo;t in the docs</li>
<li><strong>Troubleshooting</strong> &mdash; a real incident, what you saw, what fixed it, what you&rsquo;d do differently</li>
<li><strong>Percona Monitoring and Management (PMM)</strong> &mdash; dashboards you&rsquo;ve built, alerts that actually catch things, integrations</li>
<li><strong>Databases themselves</strong> &mdash; MySQL, PostgreSQL, MongoDB, MariaDB, Valkey, anything in the open source database world you&rsquo;re hands-on with</li>
</ul>
<p>We&rsquo;re not only interested in Percona-product posts. If you&rsquo;re active in the wider open source database community &mdash; contributing to MySQL, PostgreSQL, Valkey, or anywhere else &mdash; we want to hear about that work too. Your projects, your perspective, your hard-won opinions.</p>
<h2>Standards<a class="anchor-link" id="standards"></a></h2>
<p>Every submission is reviewed by the community team for technical accuracy and grammar before it goes live. We&rsquo;re not gatekeeping &mdash; we&rsquo;re making sure your name goes on something solid.</p>
<p>One firm rule: <strong>no AI-generated content</strong>. We run every submission through <a href="https://gptzero.me/" target="_blank" rel="noopener noreferrer">GPTZero</a> and it has to come back clean. We&rsquo;re publishing your voice and your experience, not a model&rsquo;s summary of either. If you used AI to help draft, that&rsquo;s fine &mdash; but the post needs to read as yours and pass the check.</p>
<h2>How to start<a class="anchor-link" id="how-to-start"></a></h2>
<p>Pitch us first. A couple of sentences on what you want to write about and why you&rsquo;re the person to write it is enough. We&rsquo;ll reply with feedback, a timeline, and any direction that helps you write a stronger post.</p>
<p>You don&rsquo;t need to be a published writer. You need to have done something and be willing to explain how. A 900-word post about how you debugged a replication lag issue last quarter is more valuable than a 3,000-word survey of the database landscape.</p>
<p>Send pitches and questions to the Percona Community team &mdash; by filling in <strong><a href="https://share.hsforms.com/2quoru-zrSli2l-89aiiJggg9e0" target="_blank" rel="noopener noreferrer">this form</a></strong>.</p>
<div class="hs-form-frame" data-region="na1" data-form-id="aaea2bbb-eceb-4a58-b697-ef3d6a288982" data-portal-id="758664"></div>
<h3>Open topics: blog, talks, guides<a class="anchor-link" id="open-topics-blog-talks-guides"></a></h3>
<p>Not sure where to start? Here are some directions we&rsquo;d love to see covered. Pick one, narrow it down to something you&rsquo;ve actually done, and pitch us.</p>
<p><strong>Databases</strong></p>
<ul>
<li>Automating database setup for production in under a few hours</li>
<li>Backup and disaster recovery strategies that hold up</li>
<li>Failure stories &mdash; what broke, what you learned</li>
</ul>
<p><strong>DevOps and reliability</strong></p>
<ul>
<li>Database Reliability Engineering (DBRE) in practice</li>
<li>Site Reliability Engineering (SRE) applied to databases</li>
<li>Monitoring and SLAs that mean something</li>
<li>Useful scripts you actually run in production</li>
<li>Testing and QA for database changes</li>
</ul>
<p><strong>Distributed computing</strong></p>
<ul>
<li>Consensus algorithms and real-world implementations</li>
<li>Synchronous vs asynchronous replication &mdash; trade-offs and where each fits</li>
</ul>
<p><strong>How-tos</strong></p>
<ul>
<li>Moving from a single node to a cluster (any DB engine)</li>
<li>Batch processing patterns</li>
<li>Stream processing patterns</li>
<li>Metrics that actually tell you something</li>
</ul>
<p><strong>Open source</strong></p>
<ul>
<li>Measuring your open source project&rsquo;s success</li>
<li>Bug squashing done right</li>
<li>Licensing &mdash; what to know before you pick one</li>
<li>Vendor lock-in and how to spot it early</li>
</ul>
<hr>
<p>We pay engineers to share what they&rsquo;ve learned. That&rsquo;s the whole offer. If you&rsquo;ve got something worth writing, write it.</p>
<h2>Content Ownership and Licensing<a class="anchor-link" id="content-ownership-and-licensing"></a></h2>
<p>Contributors to the Percona Community Blog retain copyright of their work. By submitting content, authors grant Percona a non-exclusive, worldwide, royalty-free license to publish, distribute, and promote the content as part of the Percona Community platform. Unless otherwise specified, all community blog posts are published under the Creative Commons Attribution 4.0 International (CC BY 4.0) license.</p>

<p><a href="https://percona.community/blog/2026/05/22/write-for-percona-community/">Write for the Percona Community</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Knowing when new open source database engine versions release on Amazon Aurora and Amazon RDS</title>
      <link rel="alternate" type="text/html" href="https://aws.amazon.com/blogs/database/knowing-when-new-open-source-database-engine-versions-release-on-amazon-aurora-and-amazon-rds/" />
      <id>https://aws.amazon.com/blogs/database/knowing-when-new-open-source-database-engine-versions-release-on-amazon-aurora-and-amazon-rds/</id>
      <updated>2026-05-21T18:36:46+03:00</updated>
      <author><name>Betty Chun</name></author>
      <summary type="html"><![CDATA[<p>In this post, we share the version currency timelines for Aurora and RDS open source engines. We also explain why timelines differ across engines and how you can use them to plan your upgrades.</p>
<p><a href="https://aws.amazon.com/blogs/database/knowing-when-new-open-source-database-engine-versions-release-on-amazon-aurora-and-amazon-rds/">Knowing when new open source database engine versions release on Amazon Aurora and Amazon RDS</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>If you&rsquo;re running or considering <a href="https://aws.amazon.com/rds/aurora/" target="_blank" rel="noopener">Amazon Aurora</a> with PostgreSQL or MySQL compatibility, you&rsquo;ve likely wondered, &ldquo;When will the latest community version be available on AWS?&rdquo; The same question applies if you run <a href="https://aws.amazon.com/rds/" target="_blank" rel="noopener">Amazon Relational Database Service</a> (Amazon RDS) for PostgreSQL, MySQL, or MariaDB. Whether you want the newest features quickly or prefer to standardize on stable long-term support (LTS) versions, our release timelines help you plan upgrades and maintenance cycles. In this post, we share the version currency timelines for Aurora and RDS open source engines. We also explain why timelines differ across engines and how you can use them to plan your upgrades.</p>
<p>Today, we are publishing version currency timelines for Aurora and RDS open source engines. The timelines apply to new major and minor versions going forward and define when you and your teams can expect new versions on AWS. With this predictability, you can plan maintenance windows, upgrade cycles, and Aurora LTS adoption for workloads that prioritize long-term stability.</p>
<table border="1px" cellpadding="10px" width="100%">
<tbody>
<tr>
<td><strong>Database engine</strong></td>
<td><strong>Release type</strong></td>
<td><strong>Timeline</strong></td>
</tr>
<tr>
<td rowspan="2"><strong><a href="https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-release-calendar.html" rel="noopener" target="_blank">RDS for PostgreSQL</a></strong></td>
<td>Minor versions</td>
<td>Within 7 days of community release</td>
</tr>
<tr>
<td>Major versions</td>
<td>Within 30 days of the community <code>.1</code> release</td>
</tr>
<tr>
<td rowspan="2"><strong><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.VersionMgmt.html" rel="noopener" target="_blank">RDS for MySQL</a></strong></td>
<td>Minor versions</td>
<td>Within 30 days of community release</td>
</tr>
<tr>
<td>Major versions</td>
<td>Within 6 months of community <code>.1</code> release (Oracle MySQL LTS majors)</td>
</tr>
<tr>
<td rowspan="2"><strong><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MariaDB.Concepts.VersionMgmt.html" rel="noopener" target="_blank">RDS for MariaDB</a></strong></td>
<td>Minor versions</td>
<td>Within 30 days of community release</td>
</tr>
<tr>
<td>Major versions</td>
<td>Within 3 months of community&rsquo;s first patch release</td>
</tr>
<tr>
<td rowspan="3"><strong><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/aurorapostgresql-release-calendar.html" rel="noopener" target="_blank">Aurora PostgreSQL</a></strong></td>
<td>Minor versions</td>
<td>Within 3 months of community release</td>
</tr>
<tr>
<td>Major versions</td>
<td>Within 8 months of the community <code>.1</code> release</td>
</tr>
<tr>
<td>Aurora LTS per major</td>
<td>Within 12 months of Aurora major GA</td>
</tr>
<tr>
<td rowspan="3"><strong><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.release-calendars.html" rel="noopener" target="_blank">Aurora MySQL</a></strong></td>
<td>Minor versions</td>
<td>Within 3 months of community release</td>
</tr>
<tr>
<td>Major versions</td>
<td>Within 12 months of community <code>.1</code> release (Oracle MySQL LTS majors)</td>
</tr>
<tr>
<td>Aurora LTS per major</td>
<td>Within 12 months of Aurora major GA</td>
</tr>
</tbody>
</table>
<p>For the current schedule of upcoming and recently shipped versions, including specific version numbers and target dates, see the release calendar linked from each engine name in the table. </p>
<h2>Why timelines differ across engines<a class="anchor-link" id="why-timelines-differ-across-engines"></a></h2>
<p>The timelines differ by engine because the upstream development and integration models differ. PostgreSQL and MariaDB communities develop in the open, which lets us start validation early. MySQL commits are available closer to public releases. RDS runs the community engine on managed infrastructure, so after a community release passes validation it can ship quickly. Aurora adds a distributed storage layer, Global Database, and serverless capabilities underneath PostgreSQL- and MySQL-compatible engines. Every new version goes through additional validation to verify that those capabilities continue to function correctly. This is why Aurora timelines are longer than RDS timelines for the same engine. Aurora also offers Long-Term Support releases for multi-year stability on a single minor version.</p>
<h2>How we choose major version starting points<a class="anchor-link" id="how-we-choose-major-version-starting-points"></a></h2>
<p>For PostgreSQL, our first production release of a new major version is typically based on the community <code>.1</code> release rather than <code>.0</code>. The <code>.1</code> release generally arrives roughly three months after the initial major release. It incorporates the first round of bug fixes and security patches identified during early production deployments, which provides a more stable starting point for production workloads.</p>
<p>MariaDB follows a similar pattern. The published major version timelines are measured from the community&rsquo;s first patch release for a new major version rather than the initial <code>.0</code> release. This gives customers a more mature production baseline to target.</p>
<p>For MySQL, the major version timelines apply to Oracle MySQL LTS major releases and are measured from the corresponding <code>.1</code> release. This aligns the timelines to the first patch release after the initial LTS major becomes generally available.</p>
<h2>What this means for your upgrade planning<a class="anchor-link" id="what-this-means-for-your-upgrade-planning"></a></h2>
<p>Published version currency timelines give you and your teams earlier visibility into release planning and upgrade scheduling. With RDS for PostgreSQL minor versions arriving within 7 days of community release, teams can stay current on security patches and bug fixes with relatively little operational planning. You can enable automatic minor version upgrades to receive patches during maintenance windows. You can also use <a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_upgrade_rollout.html" rel="noopener" target="_blank">AWS Organizations upgrade rollout policies</a> to manage deployment sequencing across your development, test, and production environments, or apply upgrades manually based on your own operational processes.</p>
<p>Earlier visibility into major version timelines helps your teams adopt new database capabilities on your own schedule. Knowing when a new version is expected on Aurora or RDS gives teams more time to review release notes, validate application behavior, and prepare rollout plans ahead of adoption. With RDS Database Preview, you get early access to PostgreSQL and MySQL major versions in a non-production environment so you can test application compatibility in advance. With Blue/Green Deployments, you can validate changes before cutover and transition production traffic with minimal downtime.</p>
<p>With Aurora LTS releases, you can prioritize operational stability over rapid feature adoption. Your teams can remain on a stable minor baseline for multiple years while aligning major version upgrades with broader application and infrastructure roadmaps.</p>
<p>For workloads approaching or beyond community end-of-life timelines, Amazon RDS Extended Support gives you additional time to finish upgrades while continuing to receive critical security updates.</p>
<h2>Learn more<a class="anchor-link" id="learn-more"></a></h2>
<p>For detailed upgrade procedures and release guidance, see the <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/CHAP_AuroraOverview.html" target="_blank" rel="noopener">Amazon Aurora User Guide</a> and the <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Welcome.html" target="_blank" rel="noopener">Amazon RDS User Guide</a>.</p>
<hr>
<h2>About the authors<a class="anchor-link" id="about-the-authors"></a></h2>
<footer>
<div class="blog-author-box">
<div class="blog-author-image">
   <img loading="lazy" decoding="async" class="alignleft size-full" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2026/05/18/DB5644a1.jpg" alt="Betty Chun" width="100" height="100">
  </div>
<h3 class="lb-h4">Betty Chun<a class="anchor-link" id="betty-chun"></a></h3>
<p><a target="_blank" href="https://www.linkedin.com/in/betty-chun-3b8811/" rel="noopener">Betty</a> is a Principal Product Marketing Manager at AWS. She focuses on relational database services, such as Amazon Aurora. She is based on Seattle and enjoys cooking and the outdoors.</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/11/24/DBBLOG-5173-14.png" alt="Keyur Diwan" width="120" height="160">
  </div>
<h3 class="lb-h4">Keyur Diwan<a class="anchor-link" id="keyur-diwan"></a></h3>
<p><a href="https://www.linkedin.com/in/keyurdiwan/" target="_blank" rel="noopener">Keyur</a> is a Principal Product Manager with Amazon Aurora/RDS in Seattle, where he builds next-generation capabilities in managed PostgreSQL, Blue/Green deployments, seamless upgrades, security, and analytics technologies such as HTAP, ZETL, and CDC streaming.</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2026/05/22/image-10.png" alt="Abhinav Dhandh" width="120" height="160">
  </div>
<h3 class="lb-h4">Abhinav Dhandh<a class="anchor-link" id="abhinav-dhandh"></a></h3>
<p><a target="_blank" href="https://www.linkedin.com/in/abhinav-dhandh-35139418/" rel="noopener">Abhinav</a> is a Product Management Leader at AWS, where he leads a team responsible for the vision, delivery, and growth of Amazon Aurora and RDS open source database engines. His team&rsquo;s focus areas include horizontal scaling, migrations, multi-cloud experiences, and agentic AI experiences that help customers operate and evolve their database workloads.</p>
</div>
</footer>

<p><a href="https://aws.amazon.com/blogs/database/knowing-when-new-open-source-database-engine-versions-release-on-amazon-aurora-and-amazon-rds/">Knowing when new open source database engine versions release on Amazon Aurora and Amazon RDS</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/" />
      <id>https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/</id>
      <updated>2026-05-21T10:22:01+03:00</updated>
      <author><name>Kyle Buzzell</name></author>
      <summary type="html"><![CDATA[<p>In December, we introduced how Model Context Protocol could make ClusterControl easier to work with from AI assistants. Since then, Severalnines has expanded that MCP direction across its database operations platforms with ClusterControl MCP and CCX MCP. The latest ClusterControl MCP is the major update, providing a more robust implementation with 69 tools and 20 […]<br />
The post AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In December, we introduced how Model Context Protocol could make <a href="https://severalnines.com/clustercontrol">ClusterControl</a> easier to work with from AI assistants. Since then, Severalnines has expanded that MCP direction across its database operations platforms with <strong>ClusterControl MCP</strong> and <strong>CCX MCP</strong>.</p>
<p>The latest ClusterControl MCP is the major update, providing a more robust implementation with <strong>69 tools and 20 MCP resources / templates</strong> for production database operations across ClusterControl-managed environments. CCX MCP is the companion MCP server for <a href="https://severalnines.com/ccx">CCX</a>, bringing AI-assisted workflows to managed cloud database operations.</p>
<p>Together, they give Severalnines users a practical way to inspect, troubleshoot, and act on database infrastructure from MCP-compatible clients such as Claude Desktop, Claude Code, OpenAI Codex, and other tools that support MCP.</p>
<h2 class="wp-block-heading">What is new in ClusterControl MCP?<a class="anchor-link" id="what-is-new-in-clustercontrol-mcp"></a></h2>
<p>N.B. For a full breakdown, <a href="https://severalnines.com/blog/enhancing-database-operations-with-clustercontrol-and-model-context-protocol-mcp/">go to our updated original ClusterControl MCP blog post</a>.</p>
<p>ClusterControl MCP 1.0 moves beyond the earlier MCP concept and provides broader coverage across daily database operations. You can ask questions such as:</p>
<ul class="wp-block-list">
<li>&ldquo;List all my database clusters and their status.&rdquo;</li>
<li>&ldquo;Show me the topology of cluster 2.&rdquo;</li>
<li>&ldquo;Are there any active alarms across all clusters?&rdquo;</li>
<li>&ldquo;What backup jobs have run on cluster 3?&rdquo;</li>
<li>&ldquo;Show me the top queries by wait time on cluster 1.&rdquo;</li>
<li>&ldquo;Are there any tables without primary keys?&rdquo;</li>
<li>&ldquo;Show me recent transaction deadlocks.&rdquo;</li>
<li>&ldquo;List the log files collected from cluster 1.&rdquo;</li>
<li>&ldquo;Who made changes to cluster 3 in the last hour?&rdquo;</li>
</ul>
<p>You can also prepare actions such as:</p>
<ul class="wp-block-list">
<li>&ldquo;Run a backup on cluster 1 right now.&rdquo;</li>
<li>&ldquo;Create a nightly backup schedule at 02:00.&rdquo;</li>
<li>&ldquo;Put db1.example.com into maintenance from 22:00 to 23:00 UTC.&rdquo;</li>
<li>&ldquo;Create a read-only database user for reporting.&rdquo;</li>
<li>&ldquo;Set max_connections to 500 on db1.example.com.&rdquo;</li>
<li>&ldquo;Restore backup #42 to cluster 1.&rdquo;</li>
</ul>
<p>Write operations use a dry-run-first model. The assistant describes what would happen before anything is executed, and high-risk operations include extra warnings.</p>
<h2 class="wp-block-heading">Example: move from alarm to evidence faster<a class="anchor-link" id="example-move-from-alarm-to-evidence-faster"></a></h2>
<p>A common operational flow starts with a broad question:</p>
<p>&ldquo;Are there any active alarms across all clusters?&rdquo;</p>
<p>From there, you can drill down:</p>
<ul class="wp-block-list">
<li>&ldquo;Show me alarms for cluster 3.&rdquo;</li>
<li>&ldquo;Show me the CMON log for cluster 3 from the last hour.&rdquo;</li>
<li>&ldquo;Summarize the warnings by component and hostname.&rdquo;</li>
</ul>
<p>That is where the 1.0 implementation becomes useful. It is not just returning a static dashboard view. It can help you move across related operational data: alarms, jobs, CMON controller logs, database server logs, topology, backup history, maintenance windows, and audit events.</p>
<h2 class="wp-block-heading">Example: inspect and manage backups conversationally<a class="anchor-link" id="example-inspect-and-manage-backups-conversationally"></a></h2>
<p>Backups are another area where ClusterControl MCP 1.0 adds practical coverage. You can ask:</p>
<ul class="wp-block-list">
<li>&ldquo;When was the last successful backup on my MongoDB cluster?&rdquo;</li>
<li>&ldquo;Show me only failed backups on cluster 1.&rdquo;</li>
<li>&ldquo;Does cluster 1 have a backup schedule configured?&rdquo;</li>
</ul>
<p>And then prepare a change:</p>
<p>&ldquo;Create a nightly backup schedule at 02:00 on cluster 1 using xtrabackup.&rdquo;</p>
<p>The assistant first returns a dry-run preview. Only after confirmation does it execute the change.</p>
<h2 class="wp-block-heading">Installing ClusterControl MCP<a class="anchor-link" id="installing-clustercontrol-mcp"></a></h2>
<p>ClusterControl MCP packages are published through the Severalnines repository alongside other ClusterControl components.</p>
<p>Debian / Ubuntu:</p>
<pre class="wp-block-code"><code>apt-get install clustercontrol-mcp</code></pre>
<p>RHEL / Rocky / AlmaLinux:</p>
<pre class="wp-block-code"><code>yum install clustercontrol-mcp</code></pre>
<p>The binary installs to:</p>
<pre class="wp-block-code"><code>/usr/bin/cmon-mcp</code></pre>
<p>The package also installs:</p>
<pre class="wp-block-code"><code>/etc/systemd/system/cmon-mcp.service
/etc/default/cmon-mcp</code></pre>
<h2 class="wp-block-heading">Setting up ClusterControl MCP in stdio mode<a class="anchor-link" id="setting-up-clustercontrol-mcp-in-stdio-mode"></a></h2>
<p>First, we&rsquo;ll start with stdio mode for when the AI client runs the MCP server locally, such as Claude Desktop or Claude Code.</p>
<p>Claude Desktop configuration:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "clustercontrol": {
     "command": "cmon-mcp",
     "env": {
       "CMON_ENDPOINT": "https://your-cc-host:9501",
       "CMON_USERNAME": "admin",
       "CMON_PASSWORD": "your-password"
     }
   }
 }
}</code></pre>
<p>Restart Claude Desktop. The hammer icon confirms that the MCP server loaded.</p>
<p>For Claude Code:</p>
<pre class="wp-block-code"><code>claude mcp add clustercontrol -- cmon-mcp 
 -endpoint https://your-cc-host:9501 
 -username admin 
 -password your-password</code></pre>
<h2 class="wp-block-heading">Setting up ClusterControl MCP in HTTP mode<a class="anchor-link" id="setting-up-clustercontrol-mcp-in-http-mode"></a></h2>
<p>Use HTTP mode for OpenAI Codex, team access, or multi-client access. Edit:</p>
<pre class="wp-block-code"><code>/etc/default/cmon-mcp</code></pre>
<p>Example:</p>
<pre class="wp-block-code"><code>CMON_ENDPOINT=https://127.0.0.1:9501
CMON_USERNAME=admin
CMON_KEY_FILE=/etc/clustercontrol/id_rsa

MCP_BIND_ADDRESS=0.0.0.0:3000
MCP_BASE_URL=http://your-cc-host:3000
MCP_AUTH_TOKEN=</code></pre>
<p>Generate a strong token:</p>
<pre class="wp-block-code"><code>openssl rand -hex 32</code></pre>
<p>Restart the service:</p>
<pre class="wp-block-code"><code>systemctl restart cmon-mcp
journalctl -u cmon-mcp -n 20</code></pre>
<p>Connect OpenAI Codex:</p>
<pre class="wp-block-code"><code>codex --mcp-server-uri http://your-cc-host:3000/mcp 
     --mcp-header "Authorization: Bearer "</code></pre>
<p>Connect Claude Code over SSE:</p>
<pre class="wp-block-code"><code>claude mcp add clustercontrol --transport sse http://your-cc-host:3000/sse 
 --header "Authorization: Bearer "</code></pre>
<p>Connect Claude Desktop over SSE:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "clustercontrol": {
     "type": "sse",
     "url": "http://your-cc-host:3000/sse",
     "headers": {
       "Authorization": "Bearer "
     }
   }
 }
}</code></pre>
<h2 class="wp-block-heading">CCX MCP: AI-Assisted Workflows for CCX<a class="anchor-link" id="ccx-mcp-ai-assisted-workflows-for-ccx"></a></h2>
<p>As noted upfront, <strong>CCX MCP</strong> brings the MCP-based workflow to Severalnines users running managed cloud databases in CCX. It lets MCP-compatible AI clients interact with CCX datastores, cloud providers, plans, databases, users, firewall rules, backups, parameter groups, and performance data.</p>
<p>Typical prompts include:</p>
<ul class="wp-block-list">
<li>&ldquo;List my datastores.&rdquo;</li>
<li>&ldquo;Create a PostgreSQL cluster.&rdquo;</li>
<li>&ldquo;Get the connection string for my production database.&rdquo;</li>
<li>&ldquo;Add 10.0.0.0/24 as a trusted source.&rdquo;</li>
<li>&ldquo;Show me the slowest queries.&rdquo;</li>
<li>&ldquo;List available backups for this datastore.&rdquo;</li>
</ul>
<p>CCX MCP supports PostgreSQL, MySQL / Percona, MariaDB, Redis, Valkey, and Microsoft SQL Server. It also includes protection behavior for destructive operations, which are blocked by default unless protection is explicitly disabled.</p>
<h2 class="wp-block-heading">Installing and setting up CCX MCP<a class="anchor-link" id="installing-and-setting-up-ccx-mcp"></a></h2>
<p>CCX MCP can be installed from npm:</p>
<pre class="wp-block-code"><code>npm install @severalnines/ccx-mcp</code></pre>
<p>Or used directly through <code>npx</code> in your MCP client configuration:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "ccx": {
     "command": "npx",
     "args": ["-y", "@severalnines/ccx-mcp"],
     "env": {
       "CCX_BASE_URL": "https://app.myccx.io",
       "CCX_USERNAME": "your-email@example.com",
       "CCX_PASSWORD": "your-password"
     }
   }
 }
}</code></pre>
<p>OAuth2 is also supported:</p>
<pre class="wp-block-code"><code>{
 "CCX_CLIENT_ID": "your-client-id",
 "CCX_CLIENT_SECRET": "your-client-secret"
}</code></pre>
<h3 class="wp-block-heading">Claude Code<a class="anchor-link" id="claude-code"></a></h3>
<p>For Claude Code, register the CCX MCP server in one command. No manual config file editing is required:</p>
<pre class="wp-block-code"><code>claude mcp add ccx -- npx -y @severalnines/ccx-mcp@latest 
 --endpoint https://app.myccx.io 
 --client-id  
 --client-secret </code></pre>
<p>Create OAuth2 credentials in the CCX UI under <strong>Account &gt; Security</strong>.</p>
<p>Then restart Claude Code, or run <code>/mcp</code> and reconnect. After that, you are ready to start using CCX MCP from your Claude Code session.</p>
<h2 class="wp-block-heading">Wrapping up<a class="anchor-link" id="wrapping-up"></a></h2>
<p>The original ClusterControl MCP work showed how AI assistants could become useful in database operations when connected to the right operational context. The latest version makes that idea much more complete, providing a broader and safer operational interface across clusters, jobs, alarms, backups, logs, performance, users, maintenance, audit, and configuration. <a href="https://docs.severalnines.com/clustercontrol/latest/reference-manuals/clustercontrol-mcp/">Go here for more information on how it works and detailed documentation.</a></p>
<p>For CCX and service provider users, CCX MCP provides the companion interface for managed cloud database operations. Together, they give database teams a practical way to use AI where it matters: inside real operational workflows, with context, control, and safety.</p>
<p>The post <a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/" />
      <id>https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/</id>
      <updated>2026-05-21T10:22:01+03:00</updated>
      <author><name>Kyle Buzzell</name></author>
      <summary type="html"><![CDATA[<p>In December, we introduced how Model Context Protocol could make ClusterControl easier to work with from AI assistants. Since then, Severalnines has expanded that MCP direction across its database operations platforms with ClusterControl MCP and CCX MCP. The latest ClusterControl MCP is the major update, providing a more robust implementation with 69 tools and 20 […]<br />
The post AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In December, we introduced how Model Context Protocol could make <a href="https://severalnines.com/clustercontrol">ClusterControl</a> easier to work with from AI assistants. Since then, Severalnines has expanded that MCP direction across its database operations platforms with <strong>ClusterControl MCP</strong> and <strong>CCX MCP</strong>.</p>
<p>The latest ClusterControl MCP is the major update, providing a more robust implementation with <strong>69 tools and 20 MCP resources / templates</strong> for production database operations across ClusterControl-managed environments. CCX MCP is the companion MCP server for <a href="https://severalnines.com/ccx">CCX</a>, bringing AI-assisted workflows to managed cloud database operations.</p>
<p>Together, they give Severalnines users a practical way to inspect, troubleshoot, and act on database infrastructure from MCP-compatible clients such as Claude Desktop, Claude Code, OpenAI Codex, and other tools that support MCP.</p>
<h2 class="wp-block-heading">What is new in ClusterControl MCP?<a class="anchor-link" id="what-is-new-in-clustercontrol-mcp"></a></h2>
<p>N.B. For a full breakdown, <a href="https://severalnines.com/blog/enhancing-database-operations-with-clustercontrol-and-model-context-protocol-mcp/">go to our updated original ClusterControl MCP blog post</a>.</p>
<p>ClusterControl MCP 1.0 moves beyond the earlier MCP concept and provides broader coverage across daily database operations. You can ask questions such as:</p>
<ul class="wp-block-list">
<li>&ldquo;List all my database clusters and their status.&rdquo;</li>
<li>&ldquo;Show me the topology of cluster 2.&rdquo;</li>
<li>&ldquo;Are there any active alarms across all clusters?&rdquo;</li>
<li>&ldquo;What backup jobs have run on cluster 3?&rdquo;</li>
<li>&ldquo;Show me the top queries by wait time on cluster 1.&rdquo;</li>
<li>&ldquo;Are there any tables without primary keys?&rdquo;</li>
<li>&ldquo;Show me recent transaction deadlocks.&rdquo;</li>
<li>&ldquo;List the log files collected from cluster 1.&rdquo;</li>
<li>&ldquo;Who made changes to cluster 3 in the last hour?&rdquo;</li>
</ul>
<p>You can also prepare actions such as:</p>
<ul class="wp-block-list">
<li>&ldquo;Run a backup on cluster 1 right now.&rdquo;</li>
<li>&ldquo;Create a nightly backup schedule at 02:00.&rdquo;</li>
<li>&ldquo;Put db1.example.com into maintenance from 22:00 to 23:00 UTC.&rdquo;</li>
<li>&ldquo;Create a read-only database user for reporting.&rdquo;</li>
<li>&ldquo;Set max_connections to 500 on db1.example.com.&rdquo;</li>
<li>&ldquo;Restore backup #42 to cluster 1.&rdquo;</li>
</ul>
<p>Write operations use a dry-run-first model. The assistant describes what would happen before anything is executed, and high-risk operations include extra warnings.</p>
<h2 class="wp-block-heading">Example: move from alarm to evidence faster<a class="anchor-link" id="example-move-from-alarm-to-evidence-faster"></a></h2>
<p>A common operational flow starts with a broad question:</p>
<p>&ldquo;Are there any active alarms across all clusters?&rdquo;</p>
<p>From there, you can drill down:</p>
<ul class="wp-block-list">
<li>&ldquo;Show me alarms for cluster 3.&rdquo;</li>
<li>&ldquo;Show me the CMON log for cluster 3 from the last hour.&rdquo;</li>
<li>&ldquo;Summarize the warnings by component and hostname.&rdquo;</li>
</ul>
<p>That is where the 1.0 implementation becomes useful. It is not just returning a static dashboard view. It can help you move across related operational data: alarms, jobs, CMON controller logs, database server logs, topology, backup history, maintenance windows, and audit events.</p>
<h2 class="wp-block-heading">Example: inspect and manage backups conversationally<a class="anchor-link" id="example-inspect-and-manage-backups-conversationally"></a></h2>
<p>Backups are another area where ClusterControl MCP 1.0 adds practical coverage. You can ask:</p>
<ul class="wp-block-list">
<li>&ldquo;When was the last successful backup on my MongoDB cluster?&rdquo;</li>
<li>&ldquo;Show me only failed backups on cluster 1.&rdquo;</li>
<li>&ldquo;Does cluster 1 have a backup schedule configured?&rdquo;</li>
</ul>
<p>And then prepare a change:</p>
<p>&ldquo;Create a nightly backup schedule at 02:00 on cluster 1 using xtrabackup.&rdquo;</p>
<p>The assistant first returns a dry-run preview. Only after confirmation does it execute the change.</p>
<h2 class="wp-block-heading">Installing ClusterControl MCP<a class="anchor-link" id="installing-clustercontrol-mcp"></a></h2>
<p>ClusterControl MCP packages are published through the Severalnines repository alongside other ClusterControl components.</p>
<p>Debian / Ubuntu:</p>
<pre class="wp-block-code"><code>apt-get install clustercontrol-mcp</code></pre>
<p>RHEL / Rocky / AlmaLinux:</p>
<pre class="wp-block-code"><code>yum install clustercontrol-mcp</code></pre>
<p>The binary installs to:</p>
<pre class="wp-block-code"><code>/usr/bin/cmon-mcp</code></pre>
<p>The package also installs:</p>
<pre class="wp-block-code"><code>/etc/systemd/system/cmon-mcp.service
/etc/default/cmon-mcp</code></pre>
<h2 class="wp-block-heading">Setting up ClusterControl MCP in stdio mode<a class="anchor-link" id="setting-up-clustercontrol-mcp-in-stdio-mode"></a></h2>
<p>First, we&rsquo;ll start with stdio mode for when the AI client runs the MCP server locally, such as Claude Desktop or Claude Code.</p>
<p>Claude Desktop configuration:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "clustercontrol": {
     "command": "cmon-mcp",
     "env": {
       "CMON_ENDPOINT": "https://your-cc-host:9501",
       "CMON_USERNAME": "admin",
       "CMON_PASSWORD": "your-password"
     }
   }
 }
}</code></pre>
<p>Restart Claude Desktop. The hammer icon confirms that the MCP server loaded.</p>
<p>For Claude Code:</p>
<pre class="wp-block-code"><code>claude mcp add clustercontrol -- cmon-mcp 
 -endpoint https://your-cc-host:9501 
 -username admin 
 -password your-password</code></pre>
<h2 class="wp-block-heading">Setting up ClusterControl MCP in HTTP mode<a class="anchor-link" id="setting-up-clustercontrol-mcp-in-http-mode"></a></h2>
<p>Use HTTP mode for OpenAI Codex, team access, or multi-client access. Edit:</p>
<pre class="wp-block-code"><code>/etc/default/cmon-mcp</code></pre>
<p>Example:</p>
<pre class="wp-block-code"><code>CMON_ENDPOINT=https://127.0.0.1:9501
CMON_USERNAME=admin
CMON_KEY_FILE=/etc/clustercontrol/id_rsa

MCP_BIND_ADDRESS=0.0.0.0:3000
MCP_BASE_URL=http://your-cc-host:3000
MCP_AUTH_TOKEN=</code></pre>
<p>Generate a strong token:</p>
<pre class="wp-block-code"><code>openssl rand -hex 32</code></pre>
<p>Restart the service:</p>
<pre class="wp-block-code"><code>systemctl restart cmon-mcp
journalctl -u cmon-mcp -n 20</code></pre>
<p>Connect OpenAI Codex:</p>
<pre class="wp-block-code"><code>codex --mcp-server-uri http://your-cc-host:3000/mcp 
     --mcp-header "Authorization: Bearer "</code></pre>
<p>Connect Claude Code over SSE:</p>
<pre class="wp-block-code"><code>claude mcp add clustercontrol --transport sse http://your-cc-host:3000/sse 
 --header "Authorization: Bearer "</code></pre>
<p>Connect Claude Desktop over SSE:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "clustercontrol": {
     "type": "sse",
     "url": "http://your-cc-host:3000/sse",
     "headers": {
       "Authorization": "Bearer "
     }
   }
 }
}</code></pre>
<h2 class="wp-block-heading">CCX MCP: AI-Assisted Workflows for CCX<a class="anchor-link" id="ccx-mcp-ai-assisted-workflows-for-ccx"></a></h2>
<p>As noted upfront, <strong>CCX MCP</strong> brings the MCP-based workflow to Severalnines users running managed cloud databases in CCX. It lets MCP-compatible AI clients interact with CCX datastores, cloud providers, plans, databases, users, firewall rules, backups, parameter groups, and performance data.</p>
<p>Typical prompts include:</p>
<ul class="wp-block-list">
<li>&ldquo;List my datastores.&rdquo;</li>
<li>&ldquo;Create a PostgreSQL cluster.&rdquo;</li>
<li>&ldquo;Get the connection string for my production database.&rdquo;</li>
<li>&ldquo;Add 10.0.0.0/24 as a trusted source.&rdquo;</li>
<li>&ldquo;Show me the slowest queries.&rdquo;</li>
<li>&ldquo;List available backups for this datastore.&rdquo;</li>
</ul>
<p>CCX MCP supports PostgreSQL, MySQL / Percona, MariaDB, Redis, Valkey, and Microsoft SQL Server. It also includes protection behavior for destructive operations, which are blocked by default unless protection is explicitly disabled.</p>
<h2 class="wp-block-heading">Installing and setting up CCX MCP<a class="anchor-link" id="installing-and-setting-up-ccx-mcp"></a></h2>
<p>CCX MCP can be installed from npm:</p>
<pre class="wp-block-code"><code>npm install @severalnines/ccx-mcp</code></pre>
<p>Or used directly through <code>npx</code> in your MCP client configuration:</p>
<pre class="wp-block-code"><code>{
 "mcpServers": {
   "ccx": {
     "command": "npx",
     "args": ["-y", "@severalnines/ccx-mcp"],
     "env": {
       "CCX_BASE_URL": "https://app.myccx.io",
       "CCX_USERNAME": "your-email@example.com",
       "CCX_PASSWORD": "your-password"
     }
   }
 }
}</code></pre>
<p>OAuth2 is also supported:</p>
<pre class="wp-block-code"><code>{
 "CCX_CLIENT_ID": "your-client-id",
 "CCX_CLIENT_SECRET": "your-client-secret"
}</code></pre>
<h3 class="wp-block-heading">Claude Code<a class="anchor-link" id="claude-code"></a></h3>
<p>For Claude Code, register the CCX MCP server in one command. No manual config file editing is required:</p>
<pre class="wp-block-code"><code>claude mcp add ccx -- npx -y @severalnines/ccx-mcp@latest 
 --endpoint https://app.myccx.io 
 --client-id  
 --client-secret </code></pre>
<p>Create OAuth2 credentials in the CCX UI under <strong>Account &gt; Security</strong>.</p>
<p>Then restart Claude Code, or run <code>/mcp</code> and reconnect. After that, you are ready to start using CCX MCP from your Claude Code session.</p>
<h2 class="wp-block-heading">Wrapping up<a class="anchor-link" id="wrapping-up"></a></h2>
<p>The original ClusterControl MCP work showed how AI assistants could become useful in database operations when connected to the right operational context. The latest version makes that idea much more complete, providing a broader and safer operational interface across clusters, jobs, alarms, backups, logs, performance, users, maintenance, audit, and configuration. <a href="https://docs.severalnines.com/clustercontrol/latest/reference-manuals/clustercontrol-mcp/">Go here for more information on how it works and detailed documentation.</a></p>
<p>For CCX and service provider users, CCX MCP provides the companion interface for managed cloud database operations. Together, they give database teams a practical way to use AI where it matters: inside real operational workflows, with context, control, and safety.</p>
<p>The post <a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/ai-assisted-production-database-ops-with-clustercontrol-mcp-and-ccx-mcp/">AI-Assisted Production Database Ops with ClusterControl MCP and CCX MCP</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Backrest&#8217;s back, alright!</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/19/backrests-back-alright/" />
      <id>https://percona.community/blog/2026/05/19/backrests-back-alright/</id>
      <updated>2026-05-19T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Events unfolded quickly over the course of a couple of weeks starting on 27 April 2026, when a message appeared on the pgBackRest project announcing: that the repository would be archived and active maintenance would stop.</p>
<p><a href="https://percona.community/blog/2026/05/19/backrests-back-alright/">Backrest&#8217;s back, alright!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Events unfolded quickly over the course of a couple of weeks starting on 27 April 2026, when a <a href="https://pgbackrest.org/news.html" target="_blank" rel="noopener noreferrer">message appeared on the pgBackRest project announcing</a>:<br>
that the repository would be archived and active maintenance would stop.</p>
<p><figure><img decoding="async" width="2068" height="1206" src="https://percona.community/blog/2026/05/Jan-pgb-news-1_hu_a93328b56bbefc92.webp" alt="blog/2026/05/Jan-pgb-news-1.png" loading="lazy"></figure>
</p>
<p>For many in the PostgreSQL ecosystem, this landed like a shock. <a href="https://pgbackrest.org/" target="_blank" rel="noopener noreferrer">pgBackRest</a> is one of the most widely used backup and recovery tools for PostgreSQL, deeply embedded in production environments across enterprises large and small. Now it was suddenly described as &ldquo;<a href="https://mydbanotebook.org/posts/pgbackrest-is-dead.-now-what/" target="_blank" rel="noopener noreferrer">dead</a>&rdquo;, &ldquo;<a href="https://www.gabrielebartolini.it/articles/2026/04/why-the-cycle-of-open-source-sustainability-needs-to-be-virtuous/" target="_blank" rel="noopener noreferrer">EOL</a>&rdquo;, or &ldquo;<a href="https://news.ycombinator.com/item?id=47919997" target="_blank" rel="noopener noreferrer">abandoned</a>&rdquo;. The trigger was clear: its long-time maintainer, after more than a decade of work, announced he could no longer continue without sustainable funding and would archive the repository.<br>
i<br>
That message spread fast. The interpretation spread even faster.</p>
<p>And it was wrong.</p>
<h2>This wasn&rsquo;t EOL<a class="anchor-link" id="this-wasnt-eol"></a></h2>
<p>Open source software doesn&rsquo;t simply &ldquo;go end of life&rdquo; in the way proprietary software does. There is no vendor switch flipped to OFF. No license revoked. No binaries disappearing overnight.</p>
<p>What actually happens is more subtle and more important:</p>
<ul>
<li>Maintainers step away</li>
<li>Funding runs out</li>
<li>Work stops</li>
</ul>
<p>That&rsquo;s not EOL. That&rsquo;s a sustainability gap.</p>
<p><a href="https://github.com/pgbackrest/pgbackrest" target="_blank" rel="noopener noreferrer">pgBackRest</a> didn&rsquo;t die. It hit a problem seen too often in open source world: a critical piece of infrastructure maintained by fewer and fewer people, until it ultimately depended on one person being able to justify working on it full time.</p>
<h2>The real problem<a class="anchor-link" id="the-real-problem"></a></h2>
<p>The message from the maintainer was not about abandoning the project. It was about reality:</p>
<blockquote>
<p>maintaining a widely used tool requires time, and time requires funding</p>
</blockquote>
<p>For years, pgBackRest was supported through corporate sponsorship from mainly one vendor. When that disappeared due to the Crunchy Data acquisition, so did the ability to keep investing the same level of effort.</p>
<p>This is the &ldquo;<a href="https://xkcd.com/2347/" target="_blank" rel="noopener noreferrer">Nebraska guy problem</a>&rdquo; in action: software used by a large part of the industry, sustained by a very small number of people.</p>
<p>Yes, anyone can fork the project (and some already did), but:</p>
<ul>
<li>trust doesn&rsquo;t fork</li>
<li>community doesn&rsquo;t fork</li>
<li>sustainability definitely doesn&rsquo;t fork</li>
</ul>
<p>A fork without coordination creates fragmentation without adding real value and that weakens the ecosystem. What pgBackRest needed was not a replacement, but continuity.</p>
<h2>The danger of bad framing<a class="anchor-link" id="the-danger-of-bad-framing"></a></h2>
<p>Calling the project &ldquo;dead&rdquo; shifted the conversation in the wrong direction.</p>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/05/Jan-pgb-not-dead_hu_85bcece67808178.webp" alt="blog/2026/05/Jan-pgb-not-dead.png" loading="lazy"></figure>
</p>
<p>Instead of asking:</p>
<blockquote>
<p>how do we keep this project healthy?</p>
</blockquote>
<p>the discussion drifted at best toward:</p>
<blockquote>
<p>what is the strategic solution here?</p>
</blockquote>
<p>and more often to:</p>
<blockquote>
<p>what do we replace it with?</p>
</blockquote>
<p>and</p>
<blockquote>
<p>what do we name our fork?</p>
</blockquote>
<p>That&rsquo;s a natural reaction, but it&rsquo;s not a good one.</p>
<p>Critical infrastructure should not be treated as disposable. Doing so erodes trust in the solutions we rely on and weakens the ecosystem. These foundational pieces should be treated as a shared responsibility so that the entire community becomes stronger.</p>
<h2>What happened next<a class="anchor-link" id="what-happened-next"></a></h2>
<p>Behind the scenes, things moved quickly, with coordination between David and companies active in the PostgreSQL community.</p>
<p><figure><img decoding="async" width="2134" height="1084" src="https://percona.community/blog/2026/05/Jan-pgb-news-2_hu_f4016fa9b115ee82.webp" alt="blog/2026/05/Jan-pgb-news-2.png" loading="lazy"></figure>
</p>
<p>Conversations started across companies, contributors and the wider ecosystem. The goal wasn&rsquo;t to &ldquo;rescue&rdquo; pgBackRest, but to do something far more valuable: to restore a sustainable model around it.</p>
<p>This is what open source actually requires: not heroics, but coordination.</p>
<h2>So what&rsquo;s with pgBackRest?<a class="anchor-link" id="so-whats-with-pgbackrest"></a></h2>
<p>It&rsquo;s all good. Well, better.</p>
<p><figure><img decoding="async" width="1536" height="1024" src="https://percona.community/blog/2026/05/Jan-pgb-back-cover_hu_491d36ba3bcded32.webp" alt="blog/2026/05/Jan-pgb-back-cover.png" loading="lazy"></figure>
</p>
<p>The short version:</p>
<ul>
<li><a href="https://pgbackrest.org/news.html#will-continue" target="_blank" rel="noopener noreferrer">Multiple companies coordinated together</a> to <a href="https://www.globenewswire.com/news-release/2026/05/19/3297383/0/en/open-source-stays-open-percona-sponsors-pgbackrest-to-keep-postgresql-backups-running.html" target="_blank" rel="noopener noreferrer">ensure continued funding and support around pgBackRest</a></li>
<li>Engineering effort is now being shared more broadly to expand the contributor and maintainer base</li>
<li>Discussions around longer term sustainability and governance in the PostgreSQL ecosystem accelerated significantly</li>
<li><strong>Percona</strong> played an active role in coordinating these efforts, contributing engineering resources, and helping bring organizations together around a sustainable path forward</li>
</ul>
<p><figure><img decoding="async" width="2134" height="1914" src="https://percona.community/blog/2026/05/Jan-pgb-news-3_hu_450f770f15b12e4f.webp" alt="blog/2026/05/Jan-pgb-news-3.png" loading="lazy"></figure>
</p>
<p>The project was never closed.</p>
<h2>The way (forward) is open<a class="anchor-link" id="the-way-forward-is-open"></a></h2>
<p>pgBackRest&rsquo;s situation is not unique. It&rsquo;s a signal.</p>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/05/Jan-pgb-back_hu_b092c398056379ed.webp" alt="blog/2026/05/Jan-pgb-back.png" loading="lazy"></figure>
</p>
<p>The PostgreSQL ecosystem depends on a wide range of tools that don&rsquo;t have the same visibility, or funding, as the database itself. That gap is becoming harder to ignore.</p>
<p>There&rsquo;s growing alignment on a few things:</p>
<ul>
<li>sustainability needs to be intentional</li>
<li>funding needs to be easier to organize</li>
<li>engineering effort needs to be shared</li>
</ul>
<p>Whether that leads to an umbrella foundation or another model, one thing is clear: the ecosystem needs structures that support both users and maintainers.</p>

<p><a href="https://percona.community/blog/2026/05/19/backrests-back-alright/">Backrest&#8217;s back, alright!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Our Experience at MongoDB.local London 2026: The Era of AI Agents, Badges, and Surviving on Chips!</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/11/our-experience-at-mongodb.local-london-2026-the-era-of-ai-agents-badges-and-surviving-on-chips/" />
      <id>https://percona.community/blog/2026/05/11/our-experience-at-mongodb.local-london-2026-the-era-of-ai-agents-badges-and-surviving-on-chips/</id>
      <updated>2026-05-11T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>On May 7th, Keith (Quality Engineer, Percona for MongoDB) and I had the super cool opportunity to head over to MongoDB.local London! The event was amazing and packed with insights about where the database ecosystem is heading.</p>
<p><a href="https://percona.community/blog/2026/05/11/our-experience-at-mongodb.local-london-2026-the-era-of-ai-agents-badges-and-surviving-on-chips/">Our Experience at MongoDB.local London 2026: The Era of AI Agents, Badges, and Surviving on Chips!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>On May 7th, <strong>Keith</strong> (Quality Engineer, Percona for MongoDB) and I had the super cool opportunity to head over to <a href="https://www.mongodb.com/events/mongodb-local/london" target="_blank" rel="noopener noreferrer">MongoDB.local London</a>! The event was amazing and packed with insights about where the database ecosystem is heading.</p>
<p>If there was one massive takeaway from the day, it was this: <strong>We are officially in the Era of AI and &ldquo;Agentic&rdquo; Systems.</strong> During the event, the message was clear: we are shifting from basic LLMs (that just answer a prompt and forget it) to autonomous AI Agents that follow a continuous loop of Perception &rarr; Planning &rarr; Action. MongoDB&rsquo;s President and CEO, CJ Desai, repeated a powerful phrase:</p>
<blockquote>
<p>While AI models change rapidly, the Data Layer is the constant.</p>
</blockquote>
<p><figure><img decoding="async" width="1376" height="893" src="https://percona.community/blog/2026/05/mongodb-desai_hu_912d92aa6b06692d.webp" alt="ceo" loading="lazy"></figure>
</p>
<p>Here is a look at our day, what we learned, and the fun we had along the way!</p>
<p><figure><img decoding="async" width="1376" height="1002" src="https://percona.community/blog/2026/05/mongodb-team_hu_4eee40ce0e67bf17.webp" alt="team" loading="lazy"></figure>
</p>
<h3>Arriving Early and Chasing Badges<a class="anchor-link" id="arriving-early-and-chasing-badges"></a></h3>
<p>We got a great tip before the event: arrive early to get a head start on the gamified learning! MongoDB had a super nice setup where you could take tests on Credly to earn knowledge badges.</p>
<p>We jumped right in. I got a <strong>MongoDB Overview badge</strong>, but Keith was on a mission. He completed three different tests (including MongoDB for Developers) and unlocked some cool swag: a really cute, high-quality bag! It was a brilliant way to get attendees engaged right from the morning.</p>
<p><figure><img decoding="async" width="1018" height="724" src="https://percona.community/blog/2026/05/mongodb-skills_hu_dfaf421f5927c84d.webp" alt="skills" loading="lazy"></figure>
</p>
<p><a href="https://www.credly.com/organizations/mongodb/collections/mongodb-skill-badges/badge_templates" target="_blank" rel="noopener noreferrer">Here</a> are more badges in case you want to get yours!</p>
<p><figure><img decoding="async" width="1268" height="961" src="https://percona.community/blog/2026/05/mongodb-badges_hu_d3fd3baeff71149b.webp" alt="badges" loading="lazy"></figure>
</p>
<h3>General Session Highlights<a class="anchor-link" id="general-session-highlights"></a></h3>
<p>We spent a lot of our time in the main room for the General Session, and the announcements were packed with impressive numbers and tech:</p>
<ul>
<li><strong>MongoDB 8.3 is Fast:</strong> Osmar Olivo (Senior Director, Database Product Management) shared that the new version brings up to 35% more write throughput, 45% more read throughput, and 15% more for ACID transactions.</li>
<li><strong>The Scale is Real:</strong> We learned that Stripe uses MongoDB to process over $1 trillion in payments volume every year (maintaining 5 nines of availability!). Osmar framed this perfectly: that is 1.5% of the global GDP running through MongoDB.</li>
<li><strong>LangGraph.js Store Integration:</strong> This was a big one for developers. MongoDB is positioning itself as the &ldquo;memory hard drive&rdquo; for AI agents. By supporting JavaScript and TypeScript, they are making it super easy for companies to use their existing web developers to build complex AI workflows.</li>
<li><strong>Hugging Face Partnership:</strong> They are scaling with MongoDB Atlas to support over 3 million models, officially tying themselves to the &ldquo;GitHub for AI.&rdquo;</li>
</ul>
<p>Feel free to explore the recorded sessions for more: <a href="https://www.youtube.com/watch?v=mHOQWeuoreM&amp;t=1877s" target="_blank" rel="noopener noreferrer">MongoDB.local London 2026</a></p>
<h3>Guest Speakers<a class="anchor-link" id="guest-speakers"></a></h3>
<p><strong>Ulku Rowe</strong> (CIO, Commercial Business at Lloyds Banking Group) talked about this being the &ldquo;Decade of AI.&rdquo; Lloyds is actively upskilling their current engineers through an internal &ldquo;AI Academy&rdquo; built in partnership with Cambridge University! She emphasized that as they build out this infrastructure, partnerships are absolutely critical to their success.</p>
<p>We also heard from <strong>Alex Holt</strong> from ElevenLabs, a company focused on producing the absolute best, human-sounding voice AI. Their scale is wild: they have 40 million agents running and hit $500 million in Annual Recurring Revenue in just 3 years! Alex mentioned that because many enterprises don&rsquo;t know how to build agents yet, ElevenLabs uses &ldquo;forward deployed engineers&rdquo; to sit directly with customers to build, deploy, and prove the ROI of their voice agents.</p>
<p><figure><img decoding="async" width="1366" height="857" src="https://percona.community/blog/2026/05/mongodb-eleven_hu_56f02d97a48c8a54.webp" alt="eleven" loading="lazy"></figure>
</p>
<h3>The Hands-on Workshop and Our Lunch &ldquo;Diet&rdquo;<a class="anchor-link" id="the-hands-on-workshop-and-our-lunch-diet"></a></h3>
<p>Later in the day, we attended a hands-on workshop: <strong>Designing Memory Systems for AI Agents</strong>, hands-on workshop about how AI agents can remember information and use it later to give better responses. We used Python and MongoDB Atlas to build memory into an AI agent and learned how to store, search, update, and manage that memory.<br>
The setup was good, everything was prepared in advance so we could focus on executing the commands and truly understanding the concepts. At the end, we answered some questions and earned another badge!</p>
<p><figure><img decoding="async" width="1264" height="750" src="https://percona.community/blog/2026/05/mongodb-workshop_hu_73b6877a1ca1c922.webp" alt="badges" loading="lazy"></figure>
</p>
<p>However, the workshop ran until 1:00 PM. One of our friends had warned us to &ldquo;go for food fast,&rdquo; but we were too focused on the workshop! By the time we made it to the lunch area, all the main food was completely sold out.</p>
<p><strong>How did we survive?</strong> Chips, candies, and a lot of beverages. Between the sodas, coffee, and tea, we kept our energy, but it was definitely a funny learning experience for next time! I can imagine Keith arriving home for dinner!!</p>
<p><figure><img decoding="async" src="https://percona.community/blog/2026/05/mongodb-gif.gif" alt="badges"></figure>
</p>
<h3>Exploring the Sponsor Hall (And Doing a Podcast!)<a class="anchor-link" id="exploring-the-sponsor-hall-and-doing-a-podcast"></a></h3>
<p>We spent our afternoon speaking with sponsors and even got to participate in a quick podcast focusing on AI and how Atlas is being used as a strong platform for these projects!</p>
<p><figure><img decoding="async" width="1176" height="791" src="https://percona.community/blog/2026/05/mongodb-podcast_hu_34afb51909661144.webp" alt="podcast" loading="lazy"></figure>
</p>
<p>The person being interviewed was <strong>Bikram Das</strong>, who is Chief Data Architect at Tata Consulting Services, and we had a great chat with him. TCS and MongoDB have partnered on a super impressive real-time payment and fraud detection platform. They use autonomous AI agents to instantly assess risk, investigate anomalies, and route safe transactions to networks like Visa and SWIFT without any downtime.</p>
<p>We also talked with IBM folks; they showed us their &ldquo;plug-and-play&rdquo; enterprise AI foundation. They are focused on letting large companies safely deploy AI agents without having to completely rip out and rebuild their current data infrastructure.</p>
<p>We also stopped by the Accenture booth! They are actively working on integrating AI directly into their platforms so they can offer smarter, more advanced solutions to their customers.</p>
<h3>Wrapping Up!<a class="anchor-link" id="wrapping-up"></a></h3>
<p>To cap off a great day, MongoDB had one last treat. If you took less than 3 minutes to fill out the end-of-event survey, they handed you a super nice pair of socks. <em>(We love community ideas like this!)</em>.</p>
<p><figure><img decoding="async" width="534" height="651" src="https://percona.community/blog/2026/05/mongodb-socks_hu_de3306139b649507.webp" alt="podcast" loading="lazy"></figure>
</p>
<p>Overall, <strong>MongoDB.local London</strong> was a great experience. It was a nice space to learn, connect, have hands-on experience, and see exactly how the database world is evolving to meet the Agentic AI era head-on.</p>
<p>See you at the next event!</p>
<p><figure><img decoding="async" width="699" height="614" src="https://percona.community/blog/2026/05/mongodb-percona_hu_e0b941a7bf1554f2.webp" alt="bye" loading="lazy"></figure></p>

<p><a href="https://percona.community/blog/2026/05/11/our-experience-at-mongodb.local-london-2026-the-era-of-ai-agents-badges-and-surviving-on-chips/">Our Experience at MongoDB.local London 2026: The Era of AI Agents, Badges, and Surviving on Chips!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Meet the Percona Community team</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/07/meet-the-percona-community-team/" />
      <id>https://percona.community/blog/2026/05/07/meet-the-percona-community-team/</id>
      <updated>2026-05-07T09:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We’ve just landed on X and Mastodon, and before the first real post goes out, we wanted to do something we don’t do often enough: introduce ourselves.</p>
<p><a href="https://percona.community/blog/2026/05/07/meet-the-percona-community-team/">Meet the Percona Community team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We&rsquo;ve just landed on X and Mastodon, and before the first real post goes out, we wanted to do something we don&rsquo;t do often enough: introduce ourselves.</p>
<p>If you&rsquo;ve been to Percona Live, a Percona.connect, a PGConf, KubeCon, FOSDEM, or pretty much any open source database event in the past few years, you&rsquo;ve probably already met one of us. We&rsquo;re the people behind the booth, on stage, organising the speakers, herding the giant Jenga set, or trying to convince you to play a quick game of chess between sessions. Now we&rsquo;re also the people behind @PerconaCommunity on X and our new Mastodon account on the fediverse.</p>
<p>Each of us will sign our posts with our initials, so you&rsquo;ll always know who you&rsquo;re talking to. Here&rsquo;s who we are.</p>
<h2>Laura Czajkowski &ndash; Director of Community (LC)<a class="anchor-link" id="laura-czajkowski-director-of-community-lc"></a></h2>
<p>Laura runs the team. She&rsquo;s been in open source community work since the early 2000s, starting at the University of Limerick&rsquo;s Skynet computer society and going on to lead community at Canonical (Ubuntu), MongoDB, Couchbase, Vonage, Solace, and Dragonfly before joining Percona. Former Ubuntu LoCo Council and Community Council member. Outside work she&rsquo;s a Munster and Ireland rugby fan, runs a book club, plays tennis, and books regular trips to Disney World. Find her at <a href="https://laura.community/" target="_blank" rel="noopener noreferrer">laura.community</a>.</p>
<h2>Alastair Turner &ndash; Postgres Community Advocate (AT)<a class="anchor-link" id="alastair-turner-postgres-community-advocate-at"></a></h2>
<p>Alastair has been working with databases since 1995, settling on Postgres around 2002. If you&rsquo;ve spoken to anyone at Percona about PostgreSQL, Kubernetes, Transparent Data Encryption, or extensions, there&rsquo;s a good chance it was him. He writes regularly on the <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">Percona Community blog</a> and speaks at PGConf events across Europe and North America. He&rsquo;s particularly interested in how open source communities work together &ndash; and what they can learn from each other.</p>
<h2>Daniil Bazhenov &ndash; Senior Community Manager (DB)<a class="anchor-link" id="daniil-bazhenov-senior-community-manager-db"></a></h2>
<p>Daniil organises our conference speakers, runs the Percona Forums, and has been a long-time contributor to the Percona Community blog. If you&rsquo;ve ever submitted a talk to Percona Live or asked a question on forums.percona.com, you&rsquo;ve crossed paths with him. He writes hands-on technical content too &ndash; GitOps with ArgoCD, PMM monitoring, Percona Everest from source &ndash; and hosts the Russian-language Percona Podcast.</p>
<h2>Kyle Flanagan &ndash; Global Manager, Events (KF)<a class="anchor-link" id="kyle-flanagan-global-manager-events-kf"></a></h2>
<p>Kyle is the reason any of our events actually happen. He runs Percona&rsquo;s global events programme, from Percona Live and Percona.connect to our presence at Open Source Summit, KubeCon, and dozens of regional events each year. Before Percona, he ran executive events at Utah Valley University. If you&rsquo;ve grabbed a sticker at one of our booths, Kyle probably packed the box it came in.</p>
<h2>Edith Puclla &ndash; Technology Evangelist (EP)<a class="anchor-link" id="edith-puclla-technology-evangelist-ep"></a></h2>
<p>Originally from Peru, now based in London, Edith is a CNCF Ambassador, Docker Captain, and Data on Kubernetes Ambassador. Her background is in DevOps and infrastructure &ndash; Kubernetes, GPUs, Linux, distributed systems &ndash; and she contributes to translating Kubernetes documentation into Spanish through SIG-Operators. She&rsquo;s a regular speaker at FOSDEM, KubeCon, Cloud Native Rejekts, and Percona University events across Latin America.</p>
<h2>Why we&rsquo;re doing this<a class="anchor-link" id="why-were-doing-this"></a></h2>
<p>We spend a lot of our time at events because that&rsquo;s where the most useful conversations happen &ndash; the ones over coffee, at the booth, in the hallway between talks. Being on social gives us a way to keep those conversations going when we&rsquo;re not in the same room. Expect event updates, contributor shout-outs, things we&rsquo;ve found useful, and the occasional opinion. If we&rsquo;ve shared it, we&rsquo;ve actually read it.</p>
<p>Photo below was taken at our recent team offsite in Antalya &ndash; five people who genuinely like working together, in case the smiles don&rsquo;t give it away.</p>
<p><figure><img decoding="async" width="1974" height="1249" src="https://percona.community/blog/2026/05/community-team-with-names_hu_10e256588ef14484.webp" alt="The Percona Community team in Antalya" loading="lazy"></figure>
</p>
<p><strong>Find us:</strong></p>
<ul>
<li>X: <a href="https://x.com/PerconaBytes" target="_blank" rel="noopener noreferrer">@PerconaBytes</a></li>
<li>Mastodon: <a href="https://mastodon.social/@PerconaBytes" target="_blank" rel="noopener noreferrer">@PerconaBytes</a></li>
<li>Forums: <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">forums.percona.com</a></li>
<li>Community blog: <a href="https://percona.community/" target="_blank" rel="noopener noreferrer">percona.community</a></li>
</ul>
<p>Come say hi. If we&rsquo;re at an event near you, the booth is open &ndash; and so is the giant Jenga.</p>

<p><a href="https://percona.community/blog/2026/05/07/meet-the-percona-community-team/">Meet the Percona Community team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>How I Stopped Babysitting My Coding Agent (With Dotfiles)</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/05/how-i-stopped-babysitting-my-coding-agent-with-dotfiles/" />
      <id>https://percona.community/blog/2026/05/05/how-i-stopped-babysitting-my-coding-agent-with-dotfiles/</id>
      <updated>2026-05-05T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Most developers at least try to use coding agents for development-related tasks, but babysitting LLMs and managing their permissions is no fun. Completely skipping permission checks is a dangerous idea on your main machine, and setting up containers or VMs for sandboxing is a pain. Can we do better?</p>
<p><a href="https://percona.community/blog/2026/05/05/how-i-stopped-babysitting-my-coding-agent-with-dotfiles/">How I Stopped Babysitting My Coding Agent (With Dotfiles)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Most developers at least try to use coding agents for development-related tasks, but babysitting LLMs and managing their permissions is no fun.<br>
Completely skipping permission checks is a dangerous idea on your main machine, and setting up containers or VMs for sandboxing is a pain.<br>
Can we do better?</p>
<h3>The autonomy problem<a class="anchor-link" id="the-autonomy-problem"></a></h3>
<p>If you work in software development, you have most certainly heard the phrase:</p>
<blockquote>
<p>Let&rsquo;s just use an LLM to solve it!</p>
</blockquote>
<p>People tend to forget that it&rsquo;s a bit more complicated than this:<br>
anybody can easily use LLMs, of course, but using them properly is a different question.<br>
Ideally, we could all just download a simple tool, give it some instructions, and relax:</p>
<p><figure><img decoding="async" width="1280" height="853" src="https://percona.community/blog/2026/05/ai-gardening_hu_42a05c1f0289c8d8.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<div class="admonition admonition--warning">
<p>Disclaimer: your employer might not approve if you do gardening during work hours; I suggest choosing a different activity in this case!</p>
</div>
<p>In all seriousness, every panel in the above image contains details that people tend to ignore, which either results in inefficient workflows or the creation of slop.</p>
<p>We can&rsquo;t talk about all of them in one go; it would be overly long and complex.<br>
I&rsquo;ll only focus on panel 2:<br>
what can we do to ensure our uninterrupted <del>gardening</del> normal work?</p>
<p>If you simply download Claude/Codex and start using the CLI tool, VS Code extension, or anything else, you&rsquo;ll quickly get bored of all the babysitting.</p>
<blockquote>
<p>Hey, user, can I execute another slightly different <code>ls</code> command?</p>
</blockquote>
<p>Either you decide it isn&rsquo;t worth the effort because of all the interruptions, or you start blindly hitting Enter: &ldquo;of course I approve, it should be safe&hellip;&rdquo;</p>
<ol>
<li>Are you really thoroughly reviewing every command it throws at you?</li>
<li>Even that 100-line bash script the TUI doesn&rsquo;t display properly, because it wouldn&rsquo;t fit on the screen?</li>
<li>Have you ever seen an agent circumvent directory permissions by accessing the restricted files through a one-off script instead?</li>
</ol>
<p>Fine-grained permissions of course exist, and in theory, you could try to configure something like that.<br>
But let&rsquo;s be honest, most of us won&rsquo;t take the time, and we likely won&rsquo;t notice if (3) happens as part of a long script.</p>
<h3>Let the AI run free<a class="anchor-link" id="let-the-ai-run-free"></a></h3>
<p>That&rsquo;s the point where you might discover the other option:<br>
completely disabling the permission system and letting the AI do whatever it wants.</p>
<p>Nothing can go wrong, it&rsquo;s only on your machine, right?</p>
<p><figure><img decoding="async" width="1280" height="786" src="https://percona.community/blog/2026/05/ai-running_hu_bf25d1ba233085f2.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>Except that:</p>
<ul>
<li>it will also have full network access, both for reading and posting</li>
<li>it can read all your secrets: its own OAuth token, your SSH key, and so on&hellip;</li>
<li>do you load your SSH key into ssh-agent? That&rsquo;s convenient so you don&rsquo;t have to enter your password every time, but do you also have a hardware key you have to touch on every use, or can the AI force-push your repository and later say</li>
</ul>
<blockquote>
<p>You are absolutely right! I shouldn&rsquo;t have done that. If you have backups you can restore them with the following steps: &hellip;</p>
</blockquote>
<p>Or it might end up in any number of similar situations.<br>
Coding agents aren&rsquo;t malicious by design, but they can be subject to prompt injection from the web, or simply reach dumb conclusions.<br>
There&rsquo;s a good reason why Claude, for example, calls this option <code>--dangerously-skip-permissions</code>.</p>
<h3>Put them in a cage!<a class="anchor-link" id="put-them-in-a-cage"></a></h3>
<p>The next obvious choice is to let them run free, but only within a cell:<br>
run the agent inside a container or virtual machine, where it can only access what you let it.</p>
<p>This, however, costs us some convenience, as we face new issues:</p>
<ul>
<li>If we completely separate the environment, we can&rsquo;t access it from our main system.<br>
Allowing AI tools to push to your repo without confirmation is a bad idea, but maybe you yourself should be able to push somehow?<br>
Or to verify the changes in a more complex, outside environment?</li>
<li>Our environment and the AI&rsquo;s environment are different&hellip; which means we have to set up both.<br>
I hope your project is easy to bootstrap, with proper scripting so you don&rsquo;t have to do this by hand.<br>
But is your development environment also easy to bootstrap?</li>
</ul>
<p>There are some existing, ready-to-use solutions: for example, both Claude Code and OpenAI Codex have support for <a href="https://containers.dev/" target="_blank" rel="noopener noreferrer">devcontainers</a>.<br>
If you want an easy setup, these can be an option.</p>
<p>However, I wanted more:<br>
to replicate my main setup exactly &ndash; the same compilers, tools, shell and editor settings, and so on.<br>
The AI tools should have the same executables available.<br>
If I have to edit or do something directly in the container, I shouldn&rsquo;t be surprised by something working differently.</p>
<p>That&rsquo;s when I remembered: I already have a <a href="https://github.com/dutow/dotfiles" target="_blank" rel="noopener noreferrer">dotfiles</a> repo. Can I make it even better for this use case?</p>
<h3>Automate all the things!<a class="anchor-link" id="automate-all-the-things"></a></h3>
<p>The idea of dotfiles is simple:<br>
a repository where you store your configuration, so when you reinstall your system, or when you have to start using another one, you can quickly replicate your preferred settings.<br>
Editors, shells, git &ndash; everything works the same, without spending hours figuring it all out again.</p>
<p>The problem is that it usually only focuses on configuring an already properly installed system.<br>
When you only buy a new PC every few years, or system administrators already set up every server you have to use before your first login, this isn&rsquo;t a big issue.</p>
<p>But when you want to be able to quickly set up and iterate with throwaway systems?<br>
Then you need better automation!</p>
<p>This is also a solved problem; tools like Ansible and Puppet exist.</p>
<p>The idea is simple:</p>
<ul>
<li>instead of manually setting up your system, use an automation tool to install and configure everything</li>
<li>you can leverage free CI services to make sure that your scripts work when run on a clean system</li>
<li>while docker/podman traditionally uses its own setup scripting, it is possible to build an image using the same automation tool instead</li>
<li>the result? Main PC, containers, virtual machines, and quick VPS instances all behaving exactly the same way!</li>
</ul>
<p>The downside is, of course, that you either have to reinstall your main PC once your new setup is good enough, or accept that it will be slightly different until you do so.<br>
I went with the reinstall; it&rsquo;s easy once you have things working.</p>
<p>And if you don&rsquo;t know any of these tools?<br>
That&rsquo;s the best part &ndash; we&rsquo;re using AI, and AI knows them well.</p>
<h3>A side note on architecture<a class="anchor-link" id="a-side-note-on-architecture"></a></h3>
<p>The focus of this blog post is panel 2, not the others.<br>
But I want to at least mention that the architecture and human review, including design review, are as important as with any other AI-driven software project.</p>
<p>If you completely vibe-code it and create an unmaintainable, sloppy dotfiles configuration, you are going to regret it later. This is your everyday work environment.</p>
<p>After the initial idea, when I started to think more about my requirements, I quickly realized that I want something generic.</p>
<p>First, I want to install a different set of packages depending on where I am installing them: containers, WSL instances, or real machines.<br>
My laptop needs slightly different settings compared to my desktop.</p>
<p>Second, I want to be able to do this on multiple distributions.<br>
Previously it was really annoying when I had to debug a distro-specific bug, unless it happened to involve one of my primary Linux distributions.<br>
I am also using a different OS on my work laptop and personal desktop PC because of company requirements.</p>
<p>With a proper Ansible setup, I can make all of these work seamlessly, even autodetecting the environment, and verifying all important configurations on CI for every commit.</p>
<p>Your requirements will most likely be different.<br>
Think about these beforehand and structure your repository accordingly!</p>
<h3>Containers or virtual machines?<a class="anchor-link" id="containers-or-virtual-machines"></a></h3>
<p>So far I mentioned both as alternatives, and both have their pros and cons.</p>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Container</th>
<th>Virtual machine</th>
</tr>
</thead>
<tbody>
<tr>
<td>Resource overhead</td>
<td>Low</td>
<td>Higher</td>
</tr>
<tr>
<td>Spin-up time</td>
<td>Seconds</td>
<td>Minutes</td>
</tr>
<tr>
<td>Host integration (mounts, networks)</td>
<td>Easy, direct</td>
<td>Network only</td>
</tr>
<tr>
<td>Isolation from host</td>
<td>Partial</td>
<td>Strong</td>
</tr>
<tr>
<td>GUI / IDE support</td>
<td>Limited, terminal-friendly</td>
<td>Full desktop</td>
</tr>
<tr>
<td>Privileged tools (GDB, GPU)</td>
<td>Extra capabilities required</td>
<td>Native, inside the VM</td>
</tr>
<tr>
<td>Credential storage</td>
<td>Shares host&rsquo;s filesystem</td>
<td>Must duplicate (SSH key, hardware key)</td>
</tr>
</tbody>
</table>
<p>For now, I went with containers.<br>
With a few helper scripts I can mount specific directories from the host OS, and I can also specify which docker/podman network the new container should join.<br>
This lets me start up my docker-compose development clusters directly on my main OS, and lets the agent access the development/test database and other containers for its work using the shared network.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">dcont run --mount `pwd` --network hackorum_default --context main-dev</span></span></code></pre>
</div>
</div>
</div>
<p>I even have a <code>context</code> parameter, which lets me keep multiple independent AI configurations: different system-level CLAUDE.md, plugin set, hooks, and so on.<br>
Underneath, this is just a few specific mounts and symlinks, but the advantage is huge:</p>
<ul>
<li>I can quickly experiment without fearing that I&rsquo;ll break my main workflows</li>
<li>I have completely separate setups for development and review work, without them conflicting with each other</li>
</ul>
<p>The upsides are easy integration, lower resource overhead, and quicker spin-up.<br>
I can mount directories directly from the host, and easily interact with docker containers running on the host.</p>
<p>The downside comes from that same integration:<br>
everything is still on the host, and the more access you give to the container, the less secure it becomes.<br>
Tools like GDB and GPU access require extra privileges, and you might have to relax SELinux features for the container.</p>
<p>The privilege problem, and the possibility of giving the container too much access, is a real risk.<br>
Docker, which runs as root on the host, and rootless podman, which maps the container root to the current host user, behave very differently if something is misconfigured &ndash; but neither protects the data accessible to the running user.</p>
<p>You can tighten the defaults with flags like <code>--cap-drop=ALL</code>, <code>--security-opt=no-new-privileges</code>, and read-only mounts where possible, but these only narrow the attack surface; they don&rsquo;t fix what you mount in.<br>
Which means what you mount matters more than which runtime you pick.</p>
<h4>Mounts and credentials</h4>
<p><code>.env</code> files, for example, can be challenging:<br>
these can contain API keys, passwords, and other secrets required by the application, which ideally shouldn&rsquo;t be accessible to the coding agent.<br>
I started using two levels of them &ndash; one in the project folder with only generic data, and another one level above containing sensitive login information for external services.<br>
This way, when I mount the project folder, the container can&rsquo;t access the sensitive <code>.env</code> file.</p>
<p>There are also some special files to watch out for:<br>
mounting <code>/var/run/docker.sock</code> into the container, for example, can break the sandbox completely, as it grants access equivalent to root on the host.</p>
<h4>When to pick a VM instead</h4>
<p>A container also isn&rsquo;t a full-fledged desktop.<br>
Personally, I am used to working in terminals; I like tools like tmux or neovim.<br>
But if you prefer desktop applications and IDEs, a full virtual machine might be a better option.</p>
<p>Full virtual machines aren&rsquo;t more difficult to set up and give you a complete GUI, but they raise a different question:<br>
how do you set everything up without accidental credential leaks?</p>
<p>You either rely on network synchronization between your main OS and the virtual machine &ndash; pushing to remotes only from the main OS &ndash; or you give the virtual machine a hardware key and store your SSH key on it.</p>
<p>Agents can of course always access and leak their own API keys; we can&rsquo;t do anything about that with 100% certainty.<br>
But we can aim to reduce their ability to leak anything else, by minimizing what they physically have access to.</p>
<h3>An example setup<a class="anchor-link" id="an-example-setup"></a></h3>
<p>You can check out my <a href="https://github.com/dutow/dotfiles" target="_blank" rel="noopener noreferrer">dotfiles</a> for inspiration.<br>
It should be only that:<br>
something you can look at while designing your own version.</p>
<p>It is designed for my workflows, and yours are most likely different.<br>
You also shouldn&rsquo;t blindly trust a script somebody else&rsquo;s LLM generated.</p>
<h4>The helper script</h4>
<p>The repository has a readme; the most interesting part is probably <a href="https://github.com/dutow/dotfiles/blob/master/dcont" target="_blank" rel="noopener noreferrer">the script I mentioned earlier</a>, which builds and runs the containers.</p>
<p>It is long and complex, and deals with additional details I didn&rsquo;t even mention here, to keep this introduction from getting too involved.</p>
<p>The basic idea, however, is easy to summarize.<br>
A basic docker command is simple:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">docker run -it ubuntu:latest /bin/bash</span></span></code></pre>
</div>
</div>
</div>
<p>But it also gets complicated quickly:</p>
<ul>
<li>what folders need mounting? (the project, specific directories for tools)</li>
<li>which networks to join?</li>
<li>do we have to set up specific hardware, like a GPU?</li>
<li>do we need specific access permissions for some software?</li>
<li>and so on</li>
</ul>
<p>The command quickly becomes longer and longer, and copy-pasting it from notes or shell history isn&rsquo;t fun.<br>
It is also most likely project-specific.<br>
You want different mounts, different contexts for AIs, different specific permissions.<br>
All this should be configurable, and still simple.</p>
<p>In my case, most of my projects also have <code>.env</code> files set up, which makes it a no-brainer to also support configuration through environment variables.</p>
<p>Most of the time, all I have to do is <code>cd</code> into the project directory and execute <code>dcont</code> without any extra parameters. That starts up a ready-to-use, project-specific setup, and I can immediately start typing instructions to Claude.</p>
<h4>The Ansible part</h4>
<p>I already mentioned this before, but didn&rsquo;t go into the details:<br>
you can build docker or podman images with Ansible.</p>
<p>Normally this isn&rsquo;t that useful:<br>
if the only goal is a container cluster, a Containerfile is much easier to use, and more efficient for rebuilding, since it automatically detects which layers have to be rebuilt.</p>
<p>If the goal, however, is to replicate the same setup on a real host and in a container, the picture is different.<br>
These images are only meant for local use, so layering and image size don&rsquo;t matter &ndash; we&rsquo;ll never upload them.</p>
<p>Build times are also secondary.<br>
Even if a rebuild is needed once or twice a day, you can continue using the previous version in the meantime and switch later.<br>
And it&rsquo;s not like we can&rsquo;t do proper incremental builds with it; Ansible supports that too &ndash; it&rsquo;s just a bit slower than how containers normally do it.</p>
<p>This is included in the same script, and the solution is surprisingly simple:</p>
<ol>
<li>start a container with <code>sleep infinity</code></li>
<li>copy the dotfiles repo into it, since it&rsquo;s already checked out on the host</li>
<li>run the same dotfiles/Ansible script as on other hosts (with proper parameters)</li>
<li>set up a proper user</li>
<li>commit the image</li>
</ol>
<p>The same could be done using a <code>Containerfile</code>, but what&rsquo;s the advantage?<br>
The image isn&rsquo;t shareable or reusable anyway, and some operations are easier to implement directly in bash.<br>
This process also leaves open the possibility of doing incremental builds, instead of always rerunning the installation script from scratch.</p>
<h3>The unsaid part: network access<a class="anchor-link" id="the-unsaid-part-network-access"></a></h3>
<p>In all of the sandboxing discussion above, I quietly ignored the question of network access:<br>
if you give unrestricted network access to an LLM agent, you can have a bad time.</p>
<p>Prompt injection exists, even if AI companies try to make it harder and harder.</p>
<p>For most use cases, a complete network ban is also a bad idea for productivity and code quality, which makes this another complex, open-ended question with its own options and tradeoffs &ndash; out of scope for this already long blog post.</p>
<p>I hope the information I provided here was useful, and that you can improve your AI setup based on it!</p>

<p><a href="https://percona.community/blog/2026/05/05/how-i-stopped-babysitting-my-coding-agent-with-dotfiles/">How I Stopped Babysitting My Coding Agent (With Dotfiles)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>InnoDB Redo Log Sizing: Stop Guessing, Start Measuring</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/05/02/innodb-redo-log-sizing-stop-guessing-start-measuring/" />
      <id>https://percona.community/blog/2026/05/02/innodb-redo-log-sizing-stop-guessing-start-measuring/</id>
      <updated>2026-05-02T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Introduction Many MySQL configurations inherit redo log sizing from defaults, aging blog posts, or configuration folklore.</p>
<p><a href="https://percona.community/blog/2026/05/02/innodb-redo-log-sizing-stop-guessing-start-measuring/">InnoDB Redo Log Sizing: Stop Guessing, Start Measuring</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Introduction<a class="anchor-link" id="introduction"></a></h2>
<p>Many MySQL configurations inherit redo log sizing from defaults, aging blog posts, or configuration folklore.</p>
<p><code>innodb_redo_log_capacity</code> gets set once&hellip; and then quietly fades into the background.</p>
<p>But redo log capacity directly shapes how efficiently MySQL absorbs writes, manages checkpoint pressure, and handles burst-heavy workloads.</p>
<p>Set it too low, and aggressive flushing can throttle throughput.<br>
Set it too high, and crash recovery can become painfully long.</p>
<p>Redo logs are more than crash insurance.</p>
<p>They are part of your write-performance architecture.</p>
<blockquote>
<p>Redo logs are the shock absorbers of write-heavy MySQL. Too small, and performance jolts. Too large, and recovery drags.</p>
</blockquote>
<h2>Why Redo Logs Matter<a class="anchor-link" id="why-redo-logs-matter"></a></h2>
<p>InnoDB redo logs are often described as crash recovery journals, but that description undersells their real operational value.</p>
<p>Redo logs function as a write buffer between committed transactions and eventual data file writes.</p>
<p>When a transaction commits:</p>
<ul>
<li>Changes are written to the redo log first</li>
<li>Dirty pages remain in memory</li>
<li>Data pages are flushed later</li>
</ul>
<p>This write-ahead logging (WAL) design allows MySQL to:</p>
<ul>
<li>Absorb bursts of write activity</li>
<li>Reduce immediate random disk writes</li>
<li>Smooth checkpoint behavior</li>
<li>Preserve durability</li>
</ul>
<p>Redo logs act like pressure regulators in a write-heavy system.</p>
<p>They absorb pressure spikes so the entire system doesn&rsquo;t thrash every time demand increases.</p>
<p>Without enough redo capacity, MySQL has less room to absorb write bursts before it must flush aggressively.</p>
<h2>Checkpoint Age and Flushing Pressure<a class="anchor-link" id="checkpoint-age-and-flushing-pressure"></a></h2>
<p>Redo log sizing becomes most visible when checkpoint pressure builds.</p>
<p>Checkpoint age represents how far current write activity has advanced beyond the last durable checkpoint:</p>
<p><code>Checkpoint Age = Current LSN - Last Checkpoint LSN</code></p>
<p>As checkpoint age approaches total redo capacity:</p>
<ul>
<li>Adaptive flushing intensifies</li>
<li>Page cleaners become more aggressive</li>
<li>Dirty pages flush faster</li>
<li>Disk I/O spikes</li>
<li>Latency often becomes unstable</li>
</ul>
<p>This is where undersized redo logs can trigger flush storms.</p>
<blockquote>
<p>MySQL isn&rsquo;t writing more data. It&rsquo;s being forced to write sooner and less efficiently.</p>
</blockquote>
<h3>Useful metrics<a class="anchor-link" id="useful-metrics"></a></h3>
<ul>
<li><code>Innodb_checkpoint_age</code></li>
<li><code>Innodb_buffer_pool_pages_dirty</code></li>
<li><code>Innodb_data_fsyncs</code></li>
<li><code>Innodb_log_waits</code></li>
</ul>
<blockquote>
<p>When redo space shrinks, MySQL doesn&rsquo;t stop writing. It starts panicking earlier.</p>
</blockquote>
<h2>Symptoms of Undersized Redo<a class="anchor-link" id="symptoms-of-undersized-redo"></a></h2>
<p>Small redo logs rarely announce themselves directly.</p>
<p>Instead, they often masquerade as generalized storage or write-performance issues.</p>
<h3>Common warning signs<a class="anchor-link" id="common-warning-signs"></a></h3>
<ul>
<li>Periodic write stalls</li>
<li>Spikes in fsync activity</li>
<li>Sharp increases in page cleaner workload</li>
<li>TPS drops during burst traffic</li>
<li>Dirty page percentage volatility</li>
<li>Stable CPU, unstable write latency</li>
</ul>
<h3>A common misdiagnosis<a class="anchor-link" id="a-common-misdiagnosis"></a></h3>
<p>Many systems blame disks when the real issue is insufficient redo headroom.</p>
<p>If writes are arriving faster than redo can comfortably buffer them, MySQL is forced into reactive flushing patterns.</p>
<p>The problem may not be disk speed.</p>
<p>It may be timing pressure.</p>
<h2>Measuring with Status Counters<a class="anchor-link" id="measuring-with-status-counters"></a></h2>
<p>Redo log sizing should be based on observed workload, not memory percentages or inherited defaults.</p>
<h3>Step 1: Measure redo generation rate<a class="anchor-link" id="step-1-measure-redo-generation-rate"></a></h3>
<p>Use:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">ENGINE</span><span class="w"> </span><span class="n">INNODB</span><span class="w"> </span><span class="n">STATUS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Track:</p>
<ul>
<li>Log sequence number</li>
<li>Log flushed up to</li>
<li>Last checkpoint at</li>
</ul>
<p>Measure LSN growth over time:</p>
<p><code>Redo Generation Rate = (LSN delta) / elapsed time</code></p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<p>If LSN grows by 4 GB over one hour:</p>
<ul>
<li>1 GB redo capacity = frequent pressure</li>
<li>4 GB redo capacity = ~1 hour buffer</li>
<li>8 GB redo capacity = larger burst tolerance</li>
</ul>
<h3>Step 2: Watch for log stress<a class="anchor-link" id="step-2-watch-for-log-stress"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_log_waits'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_os_log%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Key metric<a class="anchor-link" id="key-metric"></a></h3>
<p><code>Innodb_log_waits</code></p>
<p>If this value increases, transactions are waiting for log free space.</p>
<p>That is one of the clearest signs your redo logs may be too small.</p>
<p><code>Innodb_log_waits</code> is less a tuning suggestion and more a smoke alarm.</p>
<h2>Practical Sizing Strategy<a class="anchor-link" id="practical-sizing-strategy"></a></h2>
<p>Forget percentage-of-RAM formulas.</p>
<p>Redo logs should be sized around workload intensity.</p>
<p><strong>A practical starting point:</strong></p>
<p>Size redo capacity to hold 30 to 60 minutes of peak redo generation</p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<p>Peak redo generation = 6 GB/hour</p>
<p><strong>Minimum:</strong></p>
<p>30 minutes = 3 GB</p>
<p><strong>Safer:</strong></p>
<p>60 minutes = 6 GB</p>
<p><strong>Heavy burst environments:</strong></p>
<p>Larger sizing may reduce flush volatility further</p>
<h2>Trade-Offs<a class="anchor-link" id="trade-offs"></a></h2>
<h3>Smaller Redo Logs<a class="anchor-link" id="smaller-redo-logs"></a></h3>
<p><strong>Pros:</strong></p>
<ul>
<li>Faster crash recovery</li>
<li>Lower storage footprint</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Increased checkpoint pressure</li>
<li>More aggressive flushing</li>
<li>Greater write instability</li>
</ul>
<h3>Larger Redo Logs<a class="anchor-link" id="larger-redo-logs"></a></h3>
<p><strong>Pros:</strong></p>
<ul>
<li>Better burst absorption</li>
<li>Smoother sustained write performance</li>
<li>Reduced flush storms</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Longer crash recovery</li>
<li>Delayed visibility into pressure buildup</li>
</ul>
<h2>Common Mistakes<a class="anchor-link" id="common-mistakes"></a></h2>
<ol>
<li>
<p><strong>Treating redo like buffer pool sizing</strong><br>
Redo capacity is about write throughput buffering, not memory caching.</p>
</li>
<li>
<p><strong>Ignoring Innodb_log_waits</strong><br>
This can leave obvious pressure invisible until performance suffers.</p>
</li>
<li>
<p><strong>Oversizing without testing recovery</strong><br>
Large redo logs may improve runtime but worsen restart scenarios.</p>
</li>
<li>
<p><strong>Sizing for average load instead of peak</strong><br>
Redo logs exist to absorb pressure spikes, not calm periods.</p>
</li>
</ol>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>The right redo log size isn&rsquo;t about maximizing a configuration value.</p>
<p>It&rsquo;s about matching capacity to workload behavior.</p>
<ul>
<li>Too small, and MySQL becomes reactive.</li>
<li>Too large, and crash recovery becomes the hidden tax.</li>
</ul>
<p>When redo logs are properly sized, they fade into the background.</p>
<p>They quietly absorb bursts, smooth checkpoint behavior, and preserve performance consistency under pressure.</p>
<blockquote>
<p>Redo logs work best when they disappear into the background, quietly absorbing pressure instead of creating it.</p>
</blockquote>
<p>Stop guessing.</p>
<p>Measure your workload, observe your log pressure, and size with intent.</p>

<p><a href="https://percona.community/blog/2026/05/02/innodb-redo-log-sizing-stop-guessing-start-measuring/">InnoDB Redo Log Sizing: Stop Guessing, Start Measuring</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Open source doesn’t die. It gets unfunded.</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/30/open-source-doesnt-die-it-gets-unfunded/" />
      <id>https://percona.community/blog/2026/04/30/open-source-doesnt-die-it-gets-unfunded/</id>
      <updated>2026-04-30T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>If you are using PostgreSQL in any capacity very likely this week has started for you with a bang. pgBackRest, one of the most known tools for PostgreSQL, praised for the scalable and reliable way to do backups has announced that the project is currently archived.</p>
<p><a href="https://percona.community/blog/2026/04/30/open-source-doesnt-die-it-gets-unfunded/">Open source doesn’t die. It gets unfunded.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>If you are using PostgreSQL in any capacity very likely this week has started for you with a bang. pgBackRest, one of the most known tools for PostgreSQL, praised for the scalable and reliable way to do backups has announced that the project is currently archived.</p>
<h2>Archived, <a href="https://www.reddit.com/r/PostgreSQL/comments/1sx2ttg/comment/oilzdag/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button" target="_blank" rel="noopener noreferrer">you mean EOL</a>?<a class="anchor-link" id="archived-you-mean-eol"></a></h2>
<p><figure><img decoding="async" width="1418" height="350" src="https://percona.community/blog/2026/04/opensourcedoesntdie-reddit_hu_9a813996646fa88a.webp" alt="blog/2026/04/opensourcedoesntdie-reddit.png" loading="lazy"></figure>
</p>
<p>No! Open source software rarely has a hard &ldquo;end of life.&rdquo; What it does have are maintainership gaps and those can be just as serious.</p>
<p>It&rsquo;s different when PostgreSQL community announces a major version EOL. This happens because Community chooses to not to support it and move on to focus on newer versions.</p>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/04/opensourcedoesntdie-thisisopensource_hu_65c6d17dd0fd1a8d.webp" alt="blog/2026/04/opensourcedoesntdie-thisisopensource.png" loading="lazy"></figure>
</p>
<p>Reading the message from David Steele, the long-time primary maintainer of pgBackRest you will not find &ldquo;end of life&rdquo; term. The project is marked read-only and no longer actively maintained, but that is not the same as being permanently dead.</p>
<p><figure><img decoding="async" width="1890" height="1762" src="https://percona.community/blog/2026/04/opensourcedoesntdie-maintenance_hu_3f66e23a0cf5d417.webp" alt="blog/2026/04/opensourcedoesntdie-maintenance.png" loading="lazy"></figure>
</p>
<p>pgBackRest is not &ldquo;end of life.&rdquo; There is no governing body declaring support ended. What happened is simpler and more common in open source: the maintainer can no longer afford to continue.</p>
<h2>So what happened then?<a class="anchor-link" id="so-what-happened-then"></a></h2>
<p>This requires some story telling and I don&rsquo;t think that I can do it better than <a href="https://mydbanotebook.org/about/" target="_blank" rel="noopener noreferrer">L&aelig;titia Avrot</a> already did in her <a href="https://mydbanotebook.org/posts/pgbackrest-is-dead.-now-what/#what-happened" target="_blank" rel="noopener noreferrer">blogpost</a> (though I do not like the title):</p>
<blockquote>
<p>Crunchy Data, which had sponsored&nbsp;<code>pgBackRest</code>&nbsp;for most of its life and employed David, was sold. After that, David spent months looking for a position that would let him keep working on the project. He also tried to secure independent sponsorship. Neither worked out. He needs to make a living. The project requires sustained effort which he can no longer provide without being paid for it.</p>
</blockquote>
<p>This is the issue. An experienced developer, who wants to work on the project (that a big chunk of enterprises use) finds himself to be the &ldquo;Nebraska guy&rdquo; from <a href="https://xkcd.com/2347/" target="_blank" rel="noopener noreferrer">XKCD comic</a>.</p>
<p>When you look at the situation we&rsquo;re in this is the classic &ldquo;Nebraska guy problem&rdquo;: critical infrastructure maintained by a single person. pgBackRest is widely used in production, yet its sustainability dependson one individual being able to justify working on it. That does not seem fair and David did right to point this out with his move.</p>
<p>Of course, if anyone in the community chose to, they can still maintain the project by forking it. But why, since the problem is elsewhere?</p>
<p>Most people understand that engineers need to be paid for their work. What not everyone realizes is that the free for all software that the open source license provides does not mean free as in beer. Someone still needs to fund it!</p>
<p>Unfortunately &ldquo;someone&rdquo; almost certainly is going to be &ldquo;no-one&rdquo; unless &ldquo;anyone&rdquo; realizes they are going to miss the software if nobody maintains it anymore.</p>
<p>While there&rsquo;s a claim to be made that:</p>
<blockquote>
<p>Companies are as good as they have to and as bad as they are allowed to</p>
</blockquote>
<p>And often we see that an entity uses software they do not have to pay license fees for, treating this as cost optimization. There is also a large chunk of organizations that realize this is not a good long term strategy. Actively lowering the operational risk is important.</p>
<p>This is where foundations typically kick in: providing an easy way for organizations to contribute and ensure the longevity and healthiness of the projects. But PostgreSQL does not (yet) have one.</p>
<h2>Where are we now?<a class="anchor-link" id="where-are-we-now"></a></h2>
<p>There&rsquo;s a lot of backchannel talks happening, join them and represent the open source point of view.</p>
<p>One example of such a channel is <a href="https://www.reddit.com/r/PostgreSQL/comments/1sx2ttg/pgbackrest_is_no_longer_being_maintained/" target="_blank" rel="noopener noreferrer">Reddit</a> though it requires quite a lot of karma (a Reddit thing) and not everyone will find it easy to successfully post there.</p>
<p>While there you can join Telegram, Slack or Discord discussions some of the channels there are private so we wanted to provide an open and visible place where you can let us know what is your stance and for this purpose <a href="https://forums.percona.com/t/pgbackrest-is-eol/40720" target="_blank" rel="noopener noreferrer">(Percona Community Forum thread is available)</a>.</p>
<p>A lot of blog posts have been written on this subject, check out <a href="https://planet.postgresql.org/" target="_blank" rel="noopener noreferrer">Planet PostgreSQL</a> to find some of them! I particularly enjoyed some of them, the <a href="https://proopensource.it/blog/postgresql-ecosystem-problems-2026" target="_blank" rel="noopener noreferrer">one</a> from <a href="https://proopensource.it/stefanie-janine-stoelting.html" target="_blank" rel="noopener noreferrer">Stefanie Janine St&ouml;lting</a>, I feel I am mostly aligned with. PostgreSQL needs an Ecosystem Umbrella Foundation</p>
<h2>The future of open source is on us<a class="anchor-link" id="the-future-of-open-source-is-on-us"></a></h2>
<p>Reading that a project is EOL is triggering to me. When long-time maintainer announced plans to step away after more than a decade of work, instead of focusing on what the problem is that caused him to do so and how to solve the issue.Naming it &ldquo;dead&rdquo; complicate things even further. Labeling the project as &ldquo;dead&rdquo; doesn&rsquo;t solve the problem. Rather, it accelerates the wrong response. Users start looking for replacements instead of asking how to sustain the project.</p>
<p>This is not the way, young Padawan!</p>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/04/opensourcedoesntdie-youngpadawan_hu_2d04686e1ca153e9.webp" alt="blog/2026/04/opensourcedoesntdie-youngpadawan.png" loading="lazy"></figure>
</p>
<p>We need a body that helps both users and authors by:</p>
<ol>
<li>Providing governance and a helping hand to the ecosystem. Yes, this is also funding</li>
<li>Providing guarantees of healthiness. This means users will have it easier to know the tools are in good shape.</li>
</ol>
<h2>So what&rsquo;s with pgBackRest<a class="anchor-link" id="so-whats-with-pgbackrest"></a></h2>
<p>While we talk here in the public, a lot of decisions are being made and Percona among other companies is working towards resolving this situation.</p>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/04/opensourcedoesntdie-allyouneed_hu_ba1a21dd7e603445.webp" alt="blog/2026/04/opensourcedoesntdie-allyouneed.png" loading="lazy"></figure>
</p>
<p>Have patience. Work is already underway behind the scenes, and the situation is evolving. There will be positive news resolving the situation coming soon, as Open Source doesn&rsquo;t die!</p>

<p><a href="https://percona.community/blog/2026/04/30/open-source-doesnt-die-it-gets-unfunded/">Open source doesn’t die. It gets unfunded.</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>OIDC error scenarios</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/30/oidc-error-scenarios/" />
      <id>https://percona.community/blog/2026/04/30/oidc-error-scenarios/</id>
      <updated>2026-04-30T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Last time, in OIDC in PostgreSQL: With Keycloak, we created a working demo setup that was able to successfully authenticate a user using OIDC.</p>
<p><a href="https://percona.community/blog/2026/04/30/oidc-error-scenarios/">OIDC error scenarios</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Last time, in <a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/">OIDC in PostgreSQL: With Keycloak</a>, we created a working demo setup that was able to successfully authenticate a user using OIDC.</p>
<p>In this blog post, we follow the same example, but instead of the success story, we explore how OAuth keeps our PostgreSQL servers secure.</p>
<p>We won&rsquo;t focus on complex attack vectors, like the examples in the <a href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/">second blog post</a> in the OIDC series.<br>
Instead of social engineering, we&rsquo;ll look at practical errors, misconfigurations and honest mistakes &ndash; understanding error messages and how to fix them.</p>
<h3>Improved test setup<a class="anchor-link" id="improved-test-setup"></a></h3>
<p>Along with the step by step tutorial, previously we also linked a <a href="https://github.com/Percona-Lab/pg_oidc_validator/tree/main/examples/keycloak" target="_blank" rel="noopener noreferrer">docker/podman compose configuration</a>.<br>
This is still available, and we even improved it for testing the error scenarios.</p>
<p>If you want to update the configuration manually instead, this is what changed: we duplicated everything!</p>
<ul>
<li>Instead of a single testuser, we have two: <code>testuser</code> and <code>testuser2</code>, both using the same <code>asdfasdf</code> password</li>
<li>Instead of one client, we have two: <code>pgtest</code> and <code>pgtest2</code></li>
<li>Instead of one scope, we have two: <code>pgscope</code>, <code>pgscope2</code></li>
<li>Instead of one realm, we have two &ndash; containing exactly the same setup: <code>pgrealm</code> and <code>wrongrealm</code></li>
</ul>
<p>A role named <code>pgrole</code> is also defined.<br>
The <code>pgtest2</code> client and <code>pgscope2</code> scope both require the <code>pgrole</code> role.<br>
Only <code>testuser2</code> is assigned this role; <code>testuser</code> does not have it.</p>
<p>The following table summarizes the access matrix:</p>
<table>
<thead>
<tr>
<th></th>
<th>pgtest</th>
<th>pgtest2</th>
<th>pgscope</th>
<th>pgscope2</th>
</tr>
</thead>
<tbody>
<tr>
<td>testuser</td>
<td>OK</td>
<td>denied</td>
<td>OK</td>
<td>denied</td>
</tr>
<tr>
<td>testuser2</td>
<td>OK</td>
<td>OK</td>
<td>OK</td>
<td>OK</td>
</tr>
</tbody>
</table>
<h3>Success despite a FATAL error?<a class="anchor-link" id="success-despite-a-fatal-error"></a></h3>
<p>However, before we start using all these additional items, let&rsquo;s go back to the end of the Keycloak story, where we succeeded in logging in.<br>
Or did we?<br>
While <code>psql</code> logged us in, if we checked the server error log, we could see the following there:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"</span></span></code></pre>
</div>
</div>
</div>
<p>But if we are observant enough, this message is logged before we even go to the device authentication website of Keycloak, and enter the authentication code.<br>
This isn&rsquo;t a real error, it&rsquo;s just a side effect of how OAuth is implemented internally, and will most likely be fixed in PostgreSQL 19, the FATAL message will no longer show up.</p>
<p>As for PostgreSQL 18, unfortunately, we have to live with this.<br>
This also means that we can&rsquo;t rely on simply looking for OAuth authentication errors in the server log, because all OAuth authentication failures will result in exactly the same message, no matter if they are logged because of this harmless situation or because of a real authentication issue.</p>
<p>A workaround is to rely on the validators instead: since the server is unaware of the exact error situation anyway &ndash; it delegates validation to the validator &ndash; these plugins will print out much more detailed <em>log messages</em>.<br>
We&rsquo;ll see some examples later with pg_oidc_validator, as we explore the error scenarios.</p>
<p>However, keep in mind that the sentence above says <em>log messages</em>, and not <em>errors</em> or <em>fatal errors</em>.<br>
Validators are not allowed to print out ERROR and FATAL messages for authentication failures, so users have to look for WARNING or LOG level messages for the details.<br>
In practice, PostgreSQL will still print the same generic FATAL error message about OAuth bearer authentication failing &ndash; but it will appear after the validator-specific WARNING or LOG messages that contain the actual diagnostic information.</p>
<h3>Why does it happen?<a class="anchor-link" id="why-does-it-happen"></a></h3>
<p>Earlier we already established that PostgreSQL validates that the client and the server use the same issuer, but didn&rsquo;t go into more detail than this.</p>
<p>Usually when a service validates user input, it does so on the server.<br>
The main reason for this is that developers can trust the backend, controlled by administrators, while they can&rsquo;t trust the frontend, potentially used by malicious users.</p>
<p>But are we validating user input in this case?<br>
Why does the user even have to specify the issuer, since the server already knows it, it&rsquo;s in the HBA configuration?</p>
<p>Because this check isn&rsquo;t the server validating the user, it&rsquo;s the opposite:<br>
the user validating the server.</p>
<ol>
<li>The client sends an empty connection request to the server.</li>
<li>The server confirms that we are using OAuth, and sends back its issuer URL.</li>
<li>The client checks whether the issuer it is planning to use &ndash; or has already used, if it already has a valid token &ndash; matches the one sent by the server.
<ul>
<li>If it doesn&rsquo;t match, it aborts the login attempt and prints an error.</li>
<li>If it does match, it continues with a real authentication attempt.</li>
</ul>
</li>
</ol>
<pre class="mermaid">
sequenceDiagram
participant C as psql (Client)
participant S as PostgreSQL Server
participant K as Keycloak
C-&gt;&gt;S: Connection request (no token)
S--&gt;&gt;S: No token, auth fails
Note right of S: FATAL logged here (harmless side effect)
S-&gt;&gt;C: OAuth challenge + issuer URL
C--&gt;&gt;C: Compare issuer URL with oauth_issuer
alt Issuer mismatch
C--&gt;&gt;C: Abort with error
else Issuer matches
C-&gt;&gt;K: Device authorization flow
K-&gt;&gt;C: Access token
C-&gt;&gt;S: Connection request (with token)
S-&gt;&gt;S: Validator checks token
S-&gt;&gt;C: Authentication success
end
</pre>
<p>The FATAL error in the server log is a side effect of the first empty authentication attempt, that wasn&rsquo;t fixed in time before the PG18 release.</p>
<h3>Why do we need this check?<a class="anchor-link" id="why-do-we-need-this-check"></a></h3>
<p>With the improved configuration, we can test what happens when we specify the wrong issuer:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">bin/psql -h 127.0.0.1 'dbname=postgres oauth_issuer=https://keycloak:8443/realms/wrongrealm oauth_client_id=pgtest'
</span></span><span class="line"><span class="cl">psql: error: connection to server at "127.0.0.1", port 5432 failed: server's discovery document at https://keycloak:8443/realms/pgrealm/.well-known/openid-configuration (issuer "https://keycloak:8443/realms/pgrealm") is incompatible with oauth_issuer (https://keycloak:8443/realms/wrongrealm)</span></span></code></pre>
</div>
</div>
</div>
<p>Notice that compared to the correct command, which had the pgrealm, we are using the other Keycloak realm.<br>
And if we check the server log, we can see that there are no additional log messages there &ndash; we see a single OAuth FATAL error, which is not a real error, just the side effect we are investigating.<br>
This error is entirely on the client side.</p>
<p>And that brings us back to the question:<br>
why do we need this?</p>
<p>On one hand, it helps us prevent honest mistakes early.<br>
In our example, wrongrealm and pgrealm are exactly the same &ndash; they have users with the same name, scopes with the same names, clients with the same names.<br>
If there&rsquo;s a misconfiguration, and the server and the client use different realms in a similar setup, everything would seem to work &ndash; the user trying to log in would be able to log in, get a token, psql would send it to the server&hellip;<br>
and then on the server the validator would reject it &ndash; assuming that it is a good validator, like pg_oidc_validator.<br>
No harm done &ndash; other than disclosing a token to the server that shouldn&rsquo;t have been sent there &ndash;, but figuring out what the problem is could take a while.</p>
<p>On the other hand: what if we aren&rsquo;t dealing with a malicious user, but a malicious server?</p>
<p>In the previous attack vectors we showcased, the attacker was always a third party:<br>
somebody who wanted to steal access to the database server.</p>
<p>But we don&rsquo;t necessarily need a different unknown adversary, it could be the server we are using:<br>
do we absolutely know and trust its administrators?<br>
Sometimes yes, sometimes no.</p>
<p>Those administrators might be aware that we are also using OAuth for something else, and might plot to gain access to it.<br>
So instead of sending us the issuer we expect, the server sends us something else &ndash; for example a spoofed site, tricking us to complete login into a different service.</p>
<p>Remember the earlier situation where the Fake Photo Gallery Website used Client ID spoofing to gain access to the PostgreSQL Database?<br>
This situation is basically the same &ndash; the only difference is that this time PostgreSQL Database is trying to gain access to Photo Gallery.</p>
<pre class="mermaid">
sequenceDiagram
participant U as User (psql)
participant M as Malicious PostgreSQL Server
participant SSO as Other service
U-&gt;&gt;M: Connection request
M-&gt;&gt;U: OAuth challenge + spoofed issuer URL (instead of expected issuer)
Note over U: Without issuer check, user proceeds
U-&gt;&gt;SSO: Authenticates, thinking it is for PostgreSQL
SSO-&gt;&gt;U: Access token (valid for a different service)
U-&gt;&gt;M: Sends token to server
Note over M: Malicious server now holds a token valid for the other service
</pre>
<p>The client-side issuer check prevents this: psql compares the issuer URL from the server against the <code>oauth_issuer</code> it was configured with, and aborts if they don&rsquo;t match.</p>
<p>While requiring the client to specify the issuer may seem redundant, it&rsquo;s an important safeguard that prevents tokens from being disclosed to malicious servers.</p>
<h3>Can we verify the validator?<a class="anchor-link" id="can-we-verify-the-validator"></a></h3>
<p>If we specify an incorrect issuer, the client rejects it before completing the OAuth flow &ndash; that&rsquo;s great, but this means the validator isn&rsquo;t part of the picture.<br>
Can we even test that a validator handles this situation correctly, to verify that it properly rejects an attempt with an incorrect issuer?<br>
Security-aware users might want to double check that somebody using a modified psql is also properly rejected.</p>
<p>This is possible: in our next blog post, we&rsquo;ll see how to implement custom clients outside psql, possibly using other OAuth flows.<br>
In that scenario, we&rsquo;ll be able to send custom tokens to the server, which has many uses &ndash; one of which is internal testing of OAuth validators.</p>
<p>Rest assured, pg_oidc_validator handles this correctly &ndash; and we&rsquo;ll show you how to verify it yourself in the next post.<br>
If you are using a different validator, stay tuned to see how you can verify it!</p>
<p>With pg_oidc_validator, you will see something like this in the server log:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">WARNING: OAuth validation failed with exception: claim value does not match expected value
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>Here &ldquo;claim value does not match expected value&rdquo; means that a field (claim) in the JWT doesn&rsquo;t match our expectation.<br>
While this might seem generic, currently pg_oidc_validator only validates exactly one field in this way: the issuer.</p>
<p>On the client side, you can see the following generic error message:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Connection error: connection to server at "127.0.0.1", port 5432 failed: retrying connection with new bearer token
</span></span><span class="line"><span class="cl">connection to server at "127.0.0.1", port 5432 failed: FATAL: OAuth bearer authentication failed for user "testuser"</span></span></code></pre>
</div>
</div>
</div>
<p>Which is generally true for most OAuth errors &ndash; validators are expected not to provide detailed information about why they reject a connection back to the client, to limit the information available to potential attackers.</p>
<h3>Signature failure<a class="anchor-link" id="signature-failure"></a></h3>
<p>For some it might be surprising that we are getting an error about the issuer, and not about the token.<br>
Why is that?</p>
<p>The reason we use JWTs for access tokens is because they are cryptographically signed tokens.<br>
While they contain the payload in clear text, the token ends with a signature, a proof that it was generated by the issuer we trust.</p>
<p>This means that if the token was generated by a different issuer, it is signed by a different key.</p>
<p>However, the order of operations inside the validator is different:<br>
first we validate the fields in the cleartext data we have strong expectations about &ndash; in this case the issuer.<br>
Then, after that&rsquo;s valid, we also verify that the signature matches the public key of the issuer.</p>
<p>Since in the above situation the issuer is different, we never get to the point of signature validation.</p>
<p>To do that, somebody has to tamper with the token.<br>
For example, an attacker realizes that we require a specific scope, and since JWTs contain everything in clear text, decides to edit the <code>scp</code> claim and insert <code>pgscope</code> into it.<br>
In that situation, the issuer matches, the validator verifies the signature, and we end up with a different error:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">WARNING: OAuth validation failed with exception: failed to verify signature: VerifyFinal failed
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>The client side error message didn&rsquo;t change with this &ndash; this is clearly an attack attempt, we do not have to provide nice error messages for malicious users.</p>
<h3>What about expired tokens?<a class="anchor-link" id="what-about-expired-tokens"></a></h3>
<p>Another interesting scenario you might wonder about is token lifetime:<br>
in OAuth, tokens have a limited period in which they are valid.</p>
<p>PostgreSQL currently has no facilities to enforce token lifetime when a connection is active &ndash; once somebody is logged in, they stay logged in until they disconnect for some reason &ndash;, but validators are expected to validate that tokens are still valid at least during authentication.</p>
<p>Similarly to the previous situation, testing this without a custom client isn&rsquo;t possible, as psql always asks for a new token during the connection attempt, there is no way to send an earlier token with it.</p>
<p>As in the previous example, this scenario is rejected by pg_oidc_validator, which logs the following message on the server:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">WARNING: OAuth validation failed with exception: token expired
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>On the client side, you can only see the same generic error message as before.</p>
<p>While this doesn&rsquo;t seem too user friendly, keep in mind that both of these errors can only happen with faulty clients.<br>
Clients can, and should verify both the issuer and the expiration time before connecting to the server, and they should be able to provide nice error messages to the users based on that.</p>
<h3>Scope mismatch<a class="anchor-link" id="scope-mismatch"></a></h3>
<p>After the previous two situations, which are untestable with <code>psql</code>, let&rsquo;s move to the realm of errors which don&rsquo;t require custom code.</p>
<p>In the first and second blog posts we tried to emphasize how important scopes are in OAuth, how they can help prevent accidents.<br>
Obviously, validators have to make sure that all the scopes the server asked for are present in the received token.<br>
Having more scopes isn&rsquo;t an issue &ndash; sometimes clients use the same token for multiple services &ndash;, but missing a required scope should be an error.</p>
<p>To verify what happens in this situation, we can simply modify the pg_hba line to include a scope that doesn&rsquo;t exist on the server, for example adding <code>fooscope</code>:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email fooscope",map=kcmap</span></span></code></pre>
</div>
</div>
</div>
<p>And then we can connect with psql as before:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">bin/psql -h 127.0.0.1 'dbname=postgres oauth_issuer=https://keycloak:8443/realms/pgrealm oauth_client_id=pgtest'</span></span></code></pre>
</div>
</div>
</div>
<p>Which should result in the following detailed error message in the server log:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">LOG: Authorization failed because of scope mismatch. Required scopes: email, fooscope, pgscope. Received scopes: email, pgscope, profile
</span></span><span class="line"><span class="cl">LOG: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Validator failed to authorize the provided token.
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email fooscope",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>Similarly to the previous scenarios, this is completely validator specific, we can only showcase our validator.</p>
<p>This scenario also depends on the OAuth flow used and the identity provider.<br>
<strong>Note:</strong> Keycloak, for example, permits unknown scopes for the device flow &ndash; it simply ignores them and returns the scopes it can.<br>
However, it doesn&rsquo;t do that for other flows &ndash; the Token Endpoint rejects unknown scopes with an error and doesn&rsquo;t provide an access token.</p>
<p>On the client side, the error is the same as before &ndash; no details about what&rsquo;s missing.<br>
Which is fine in this situation, as this is clearly a configuration error, something the administrators have to figure out and fix.</p>
<p>Now let&rsquo;s see the error slightly differently.</p>
<p>The above example worked with the unmodified keycloak setup, described in the previous blog, but we have an improved test setup for this one.<br>
Instead of using a non existent foo scope, let&rsquo;s change our requirement to <code>pgscope2</code>, which requires <code>pgrole</code>:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope2 email",map=kcmap</span></span></code></pre>
</div>
</div>
</div>
<p>And similarly add <code>testuser2</code> to pg_ident, so both can log in:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># MAPNAME SYSTEM-USERNAME DATABASE-USERNAME
</span></span><span class="line"><span class="cl">kcmap testuser@example.com testuser
</span></span><span class="line"><span class="cl">kcmap testuser2@example.com testuser2</span></span></code></pre>
</div>
</div>
</div>
<p>In this new setup, we transformed the configuration problem into a permission issue where testuser2 can log in and testuser can not.</p>
<p>The error message on the client side is unchanged, it still doesn&rsquo;t say &ldquo;permission denied&rdquo; or &ldquo;scope mismatch&rdquo;, or anything like that.<br>
This is debatable, but it is still mainly a task for administrators, and not the user:<br>
somebody will have to investigate the permission setup on keycloak, and fix it, if testuser also needs access to the server.</p>
<h3>Unknown user<a class="anchor-link" id="unknown-user"></a></h3>
<p>Another common error source is a problem with the user mapping.<br>
In our example we are using a pg_ident file with an email, but it would be similar with other configurations.</p>
<p>Regardless of the setup, there are many reasons why we can&rsquo;t properly look up a username:</p>
<ul>
<li>using an incorrect field for <code>authn_field</code></li>
<li>missing an entry from <code>pg_ident</code></li>
<li>having a typo in the name either in <code>pg_ident</code> or in keycloak</li>
<li>and so on</li>
</ul>
<p>In all situations, the error message for this case won&rsquo;t be generated in the validator, but in the PostgreSQL user mapping code instead.<br>
For example, if you previously added <code>testuser2</code> to the ident file, comment it out and try to log in with it again:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">LOG: no match in usermap "kcmap" for user "testuser" authenticated as "testuser2@example.com"
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>In an alternative configuration &ndash; which is not part of the sample keycloak configuration &ndash; it is possible to create a custom claim &ldquo;postgres_username&rdquo; on keycloak, and skip the map file completely.<br>
In this situation, a mismatched username would result in a slightly different error message:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">LOG: provided user name (testuser) and authenticated user name (testuser2) do not match
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email""</span></span></code></pre>
</div>
</div>
</div>
<h3>Connection problems<a class="anchor-link" id="connection-problems"></a></h3>
<p>While it usually isn&rsquo;t a configuration or permission problem, it is possible that we have a network issue:<br>
either a localized routing error, where the client can connect to the identity provider but the server can&rsquo;t, or a situation where the identity provider / network crashed between obtaining the access token and verifying it on the server.</p>
<p>The client executable has an access token and sends it to the server, which then has to validate it without being able to communicate with the identity provider.<br>
This is another situation which is difficult to validate with <code>psql</code>, but it is relatively easy with a custom client.</p>
<p>Our OIDC validator has to connect to the identity provider for two reasons:</p>
<ul>
<li>One, to retrieve the discovery document which contains the URL of the JWKS endpoint &ndash; which stores the public keys of the issuer</li>
<li>Two, to retrieve the public keys using that JWKS endpoint</li>
</ul>
<p>The validator also follows HTTP Cache headers:<br>
for example, if the server allows caching the keys for 4 days, the validator only retrieves them for the first attempt, and then keeps using them for that time.<br>
After it passes, it connects to the server one more time, and if it again receives a 4 day window, it will keep using the keys for 4 more days.<br>
This means that with a proper provider setup, the validator might not even notice a short service loss.</p>
<p>Fortunately for our testing, but not so fortunately for production use, keycloak doesn&rsquo;t support JWKS caching at all.</p>
<p>An inaccessible OIDC server will result in logs similar to:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">WARNING: OAuth validation failed with exception: HTTP request failed: Could not connect to server
</span></span><span class="line"><span class="cl">FATAL: OAuth bearer authentication failed for user "testuser"
</span></span><span class="line"><span class="cl">DETAIL: Connection matched file "/pg_hba.conf" line 119: "host all all 127.0.0.1/32 oauth issuer=https://keycloak:8443/realms/pgrealm,scope="pgscope email",map=kcmap"</span></span></code></pre>
</div>
</div>
</div>
<p>Where the exact error message depends on the situation &ndash; a timeout, internal server error, etc, all would result in slightly different error messages, while a timeout would also slow down the response time of the authentication attempt.</p>
<h3>Let&rsquo;s run without errors!<a class="anchor-link" id="lets-run-without-errors"></a></h3>
<p>We hope these examples will be useful for everybody. To avoid errors, to diagnose problems, and to simply understand the security model and guarantees given by OAuth and validators.</p>
<p>While this is not an all-inclusive list, as we can&rsquo;t possibly cover every error scenario in a setup involving several components, it covers the most common scenarios, and should address all possible security problems.</p>
<p>In our next blog post, we&rsquo;ll focus on a practical, minimal development example:<br>
while currently only the provided command line tools support OAuth, <code>libpq</code> already has the infrastructure in it to implement custom OAuth logic, allowing users to integrate it into their applications &ndash; we&rsquo;ll provide examples how it is doable.</p>

<p><a href="https://percona.community/blog/2026/04/30/oidc-error-scenarios/">OIDC error scenarios</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>pgBackRest is archived, what now?</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/28/pgbackrest-is-archived-what-now/" />
      <id>https://percona.community/blog/2026/04/28/pgbackrest-is-archived-what-now/</id>
      <updated>2026-04-28T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>pgBackRest is an open source backup and restore tool for PostgreSQL. It’s fair to say it’s one of the most popular options, widely used across the PostgreSQL ecosystem.</p>
<p><a href="https://percona.community/blog/2026/04/28/pgbackrest-is-archived-what-now/">pgBackRest is archived, what now?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://github.com/pgbackrest/pgbackrest" target="_blank" rel="noopener noreferrer">pgBackRest</a> is an open source backup and restore tool for PostgreSQL. It&rsquo;s fair to say it&rsquo;s one of the most popular options, widely used across the PostgreSQL ecosystem.</p>
<p>On 27 April 2026, pgBackRest maintainer David Steele announced on <a href="https://www.linkedin.com/posts/davidsteele_after-a-lot-of-thought-i-have-decided-to-share-7454442611911655424-mVMS?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAD3qpgBKSXefFXDYJlyIbIdar9mZh-NYBw" target="_blank" rel="noopener noreferrer">LinkedIn</a> and in the <a href="https://github.com/pgbackrest/pgbackrest" target="_blank" rel="noopener noreferrer">GitHub repository</a> that the project is becoming <del>unmaintained</del> archived, starting with:</p>
<blockquote>
<p>TL;DR: pgBackRest is no longer being maintained. If you fork pgBackRest, please select a new name for your project.</p>
</blockquote>
<div>
<p><figure><img decoding="async" width="1086" height="1216" src="https://percona.community/blog/2026/04/Jan-david-li_hu_95835d8f53db9d64.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
</div>
<p>If you&rsquo;re reading this, you&rsquo;re likely either affected or at least concerned. In this short write up I will do my best to calm your nerves, present short term as well as more long term ideas and options.</p>
<h2>Where are we now &ndash; the status quo<a class="anchor-link" id="where-are-we-now-the-status-quo"></a></h2>
<p>pgBackRest is a critical part of the PostgreSQL ecosystem, and nobody seriously expects it to simply disappear. What happens next is now up to the community.<br>
One possible outcome is the emergence of multiple forks of pgBackRest. That raises the risk of fragmentation or, put bluntly, <del>Clone</del> Fork Wars.</p>
<div>
<p><figure><img decoding="async" width="1536" height="1024" src="https://percona.community/blog/2026/04/Jan-forks_hu_4282ae308d071fad.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
</div>
<p>That said, there has already been a significant amount of discussion across the community, and one thing is clear:</p>
<p>The PostgreSQL community acknowledges the problem and wants change.</p>
<p>The challenge now is twofold:</p>
<ul>
<li>What can we do immediately to stabilize the situation?</li>
<li>What direction should we take long term, without overcomplicating the short-term response?</li>
</ul>
<h2>What is Percona planning<a class="anchor-link" id="what-is-percona-planning"></a></h2>
<p><a href="https://docs.percona.com/postgresql/14/solutions/backup-recovery.html#pgbackrest" target="_blank" rel="noopener noreferrer">Percona includes pgBackRest</a> in the <a href="https://docs.percona.com/postgresql/14/index.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL</a> as the recommended backup and restore solution. From our perspective, it remains the most mature, enterprise-ready and reliable option available. While alternatives like WAL-G or Barman are well regarded, our recommendation remains unchanged.</p>
<p>To emphasize the message:</p>
<blockquote>
<p>the current situation does <u>not</u> impact our recommendation.</p>
</blockquote>
<p>Percona will continue supporting pgBackRest. What that support looks like in terms of maintainership and collaboration with other organizations is still being actively discussed and will take time to solidify.</p>
<p>The immediate priority is to avoid fragmentation. We want to ensure we don&rsquo;t end up with multiple independent forks maintained in isolation.</p>
<p>If you are a Percona customer, you remain fully supported. Please continue reporting issues through standard support channels. For our community users, we encourage you to use the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">Percona Community Forums</a>, we will do our best to help there.</p>
<h2>The power of open source community<a class="anchor-link" id="the-power-of-open-source-community"></a></h2>
<p>In an era where we often hear about companies reducing teams due to AI-driven cost optimization, it&rsquo;s easy to forget that software is still built and maintained by people. This is especially true in open source.</p>
<p>Two observations are worth calling out:</p>
<ol>
<li>People need sustainable funding, work cannot be assumed to be purely voluntary.</li>
<li>A healthy open source project should not depend on a single company or individual.</li>
</ol>
<p>The current situation is, to some extent, a result of the opposite model. pgBackRest development was largely driven by a single company and later single maintainer, <a href="https://github.com/dwsteele" target="_blank" rel="noopener noreferrer">David Steele</a>, with sponsorship from Crunchy Data. While others have contributed (e.g.i <a href="https://github.com/sfrost" target="_blank" rel="noopener noreferrer">Stephen Frost</a> and Stefan Fercot &ndash; <a href="https://github.com/pgstef" target="_blank" rel="noopener noreferrer">pgstef</a>), and there was a wider team maintaining the project in the past, recently the project effectively relied on one primary maintainer.</p>
<p>I think it&rsquo;s fair to say we&rsquo;ve seen a fair share <a href="https://xkcd.com/2347/" target="_blank" rel="noopener noreferrer">xkcd #2347</a> posted all over the internet over the course of last 24h. So here&rsquo;s one more:</p>
<div>
<p><figure><img decoding="async" width="2236" height="2814" src="https://percona.community/blog/2026/04/Jan-comic-neb_hu_5371ac63ef109e55.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
</div>
<p>To avoid repeating this pattern, we (along with other vendors) are deliberately taking time before jumping into forks or immediate solutions. The goal is to find a sustainable, collaborative model rather than rushing into fragmentation.</p>
<p>For comparison, it took the Linux Foundation 6 days to respond to the <a href="https://github.com/redis/redis/pull/13157" target="_blank" rel="noopener noreferrer">Redis license change</a> by <a href="https://www.linuxfoundation.org/press/linux-foundation-launches-open-source-valkey-community" target="_blank" rel="noopener noreferrer">launching Valkey</a>. While this situation is different as there&rsquo;s no license change in pgBackRest, it illustrates that meaningful coordination takes time.</p>
<p>This is exactly where the open source community can demonstrate its strength.</p>
<h2>What are the long term options?<a class="anchor-link" id="what-are-the-long-term-options"></a></h2>
<p>This situation is particularly surprising to me personally, as I recently referenced David&rsquo;s proposed transparent funding model in <a href="https://www.postgresql.eu/events/pgconfde2026/" target="_blank" rel="noopener noreferrer">my talk</a> at <a href="http://pgconf.de/" target="_blank" rel="noopener noreferrer">PGConf.DE</a> just last week.</p>
<div>
<p><figure><img decoding="async" width="688" height="2228" src="https://percona.community/blog/2026/04/Jan-david-money_hu_2bf084722e534aaf.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
</div>
<p>The idea, distributing funding across organizations that rely on the project, seemed like a promising path toward a more sustainable ecosystem. In hindsight, it appears that adoption of this model was either too slow or insufficient to support ongoing maintenance.</p>
<p>Looking ahead, several long-term options are being discussed within the community:</p>
<ul>
<li>Establishing a foundation-backed project (similar to models used by <a href="https://codeberg.org/" target="_blank" rel="noopener noreferrer">Codeberg</a> or the Linux Foundation)</li>
<li>Creating a coordinated, multi-vendor stewardship model</li>
<li>In more extreme scenarios, moving critical tooling closer to the PostgreSQL core ecosystem</li>
</ul>
<p>These discussions are ongoing. If you&rsquo;re attending <a href="https://2026.pgconf.dev/" target="_blank" rel="noopener noreferrer">PGConf.Dev</a>, this will almost certainly be a major topic, especially in the extensions ecosystem track of community sessions in the <a href="https://2026.pgconf.dev/schedule/tuesday" target="_blank" rel="noopener noreferrer">Canfor</a> room on Tuesday.</p>
<h2>So what should I do now?<a class="anchor-link" id="so-what-should-i-do-now"></a></h2>
<div>
<p><figure><img decoding="async" width="1402" height="1122" src="https://percona.community/blog/2026/04/Jan-what-now_hu_b262983f692a82be.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
</div>
<p>In short, nothing but wait. Yes, this means:</p>
<blockquote>
<p>Keep on using pgBackRest as you did!</p>
</blockquote>
<p>If your company is relying on pgBackRest, now is the time to engage. If you have capacity for this, please join the discussion (we&rsquo;ve kicked off a thread on <a href="https://forums.percona.com/t/pgbackrest-archival-discussion/40725?u=jan_wieremjewicz" target="_blank" rel="noopener noreferrer">Percona Community Forums</a> if you are looking for a place to join this topic)</p>
<p>Rest assured that you can follow the updates from us, we will be messaging about the progress made in regards to establishing the future for pgBackRest.</p>
<p>One thing to clear is: are there any immediate risks?</p>
<blockquote>
<p>Not new ones. There is the uncertainty that this is not a comfortable feeling. Rest assured that the longevity of the solution is not in jeopardy as we do have an obligation to our customer and user base to make sure the project is continued.</p>
</blockquote>

<p><a href="https://percona.community/blog/2026/04/28/pgbackrest-is-archived-what-now/">pgBackRest is archived, what now?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Incremental backups in Percona Kubernetes Operator for MySQL</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/17/incremental-backups-in-percona-kubernetes-operator-for-mysql/" />
      <id>https://percona.community/blog/2026/04/17/incremental-backups-in-percona-kubernetes-operator-for-mysql/</id>
      <updated>2026-04-17T10:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Starting with version 1.1.0, the Percona Kubernetes Operator for MySQL now supports incremental backups. This feature lets you backup only the changed data since the last backup, instead of copying your entire dataset each time. The result is dramatically smaller backup sizes, faster backup windows, and lower cloud storage costs.</p>
<p><a href="https://percona.community/blog/2026/04/17/incremental-backups-in-percona-kubernetes-operator-for-mysql/">Incremental backups in Percona Kubernetes Operator for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Starting with version 1.1.0, the Percona Kubernetes Operator for MySQL now supports <strong>incremental backups</strong>. This feature lets you backup only the changed data since the last backup, instead of copying your entire dataset each time. The result is dramatically smaller backup sizes, faster backup windows, and lower cloud storage costs.</p>
<p>In this post, we&rsquo;ll walk through how the feature works under the hood, how to configure it, and what to keep in mind when designing your backup strategy.</p>
<h2>How Incremental Backups Work in Percona XtraBackup<a class="anchor-link" id="how-incremental-backups-work-in-percona-xtrabackup"></a></h2>
<p>The foundation of this feature is <a href="https://docs.percona.com/percona-xtrabackup/latest/" target="_blank" rel="noopener noreferrer">Percona XtraBackup (PXB)</a>, an open source backup tool for MySQL. PXB has supported incremental backups for a while, and the operator now brings that capability into the backup workflow.</p>
<p>Every InnoDB data page carries a <strong>Log Sequence Number (LSN)</strong>, which is a monotonically increasing counter that records when the page was last modified. When PXB takes an incremental backup, it scans data pages and copies only those with an LSN newer than a reference point. The output is a set of compact <code>.delta</code> files instead of full tablespace copies.</p>
<p>Each backup produces an <code>xtrabackup_checkpoints</code> file:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">backup_type = full-backuped
</span></span><span class="line"><span class="cl">from_lsn = 0
</span></span><span class="line"><span class="cl">to_lsn = 7345291
</span></span><span class="line"><span class="cl">last_lsn = 7345291</span></span></code></pre>
</div>
</div>
</div>
<p>Each incremental&rsquo;s <code>from_lsn</code> must equal the previous backup&rsquo;s <code>to_lsn</code>.</p>
<h2>Using Incremental Backups with the Operator<a class="anchor-link" id="using-incremental-backups-with-the-operator"></a></h2>
<h3>On-Demand Incremental Backup<a class="anchor-link" id="on-demand-incremental-backup"></a></h3>
<p>First, you need a full backup to serve as the base:</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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre 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">PerconaServerMySQLBackup</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">weekly-full</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">clusterName</span><span class="p">:</span><span class="w"> </span><span class="l">my-cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">storageName</span><span class="p">:</span><span class="w"> </span><span class="l">s3-us</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">full</span></span></span></code></pre>
</div>
</div>
</div>
<p>Once the full backup succeeds, create an incremental:</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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre 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">PerconaServerMySQLBackup</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">daily-inc-1</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">clusterName</span><span class="p">:</span><span class="w"> </span><span class="l">my-cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">storageName</span><span class="p">:</span><span class="w"> </span><span class="l">s3-us</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">incremental</span></span></span></code></pre>
</div>
</div>
</div>
<p>The operator automatically discovers the latest succeeded full backup for the same cluster and storage, fetches its LSN, and creates an incremental backup. If you want to pin a specific base, simply use the <code>incrementalBaseBackupName</code> field:</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-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><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">type</span><span class="p">:</span><span class="w"> </span><span class="l">incremental</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">incrementalBaseBackupName</span><span class="p">:</span><span class="w"> </span><span class="l">weekly-full</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Scheduled Backups: Full + Incremental<a class="anchor-link" id="scheduled-backups-full-incremental"></a></h3>
<p>The real power comes from combining full and incremental schedules. Here&rsquo;s an example: weekly full backups with daily incrementals:</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-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><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">backup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schedule</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">weekly-full</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 0 * * 0"</span><span class="w"> </span><span class="c"># Sunday midnight</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">keep</span><span class="p">:</span><span class="w"> </span><span class="m">4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">storageName</span><span class="p">:</span><span class="w"> </span><span class="l">s3-us</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">full</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">daily-incremental</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 0 * * 1-6"</span><span class="w"> </span><span class="c"># Monday through Saturday</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">storageName</span><span class="p">:</span><span class="w"> </span><span class="l">s3-us</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">incremental</span></span></span></code></pre>
</div>
</div>
</div>
<p>The <code>keep</code> rotation policy is chain-aware: it counts only full backups and automatically cascade-deletes all dependent incrementals when a full backup is rotated out.</p>
<h3>Restoring from an Incremental Backup<a class="anchor-link" id="restoring-from-an-incremental-backup"></a></h3>
<p>The <code>PerconaServerMySQLRestore</code> custom resource allows you to restore from any point in an incremental, similar to restoring a full backup:</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"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre 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">PerconaServerMySQLRestore</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">restore-to-wednesday</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">clusterName</span><span class="p">:</span><span class="w"> </span><span class="l">my-cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">backupName</span><span class="p">:</span><span class="w"> </span><span class="l">daily-inc-3</span></span></span></code></pre>
</div>
</div>
</div>
<p>The operator handles the complexity behind the scenes:</p>
<ol>
<li>Discovers the full chain by listing the cloud storage directory</li>
<li>Downloads and prepares the base full backup</li>
<li>Applies each incremental in sequence</li>
<li>Applies the final incremental and rolls back uncommitted transactions</li>
<li>Moves the prepared data back to the MySQL data directory</li>
</ol>
<p>You don&rsquo;t need to know which backup is the base or how many incrementals are in the chain, the operator figures it out.</p>
<h2>How It Works Under the Hood<a class="anchor-link" id="how-it-works-under-the-hood"></a></h2>
<h3>Storage Layout<a class="anchor-link" id="storage-layout"></a></h3>
<p>The operator uses a specific directory convention to encode backup chains without any requiring any additional metadata:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">s3://bucket/prefix/
</span></span><span class="line"><span class="cl"> my-cluster-2026-04-06-full/ # base full backup
</span></span><span class="line"><span class="cl"> my-cluster-2026-04-06-full.incr/ # incremental chain directory
</span></span><span class="line"><span class="cl"> my-cluster-2026-04-07T000000-incr/ # Monday's incremental
</span></span><span class="line"><span class="cl"> my-cluster-2026-04-08T000000-incr/ # Tuesday's incremental
</span></span><span class="line"><span class="cl"> my-cluster-2026-04-09T000000-incr/ # Wednesday's incremental</span></span></code></pre>
</div>
</div>
</div>
<p>The <code>.incr/</code> suffix creates a self-describing structure. Any cluster with access to the storage bucket can reconstruct the chain, making cross-cluster restores straightforward.</p>
<h3>The Backup Flow<a class="anchor-link" id="the-backup-flow"></a></h3>
<p>Here&rsquo;s what happens when you create an incremental backup:</p>
<ol>
<li><strong>Resolve the base.</strong> The controller finds the latest succeeded full backup (or the one you specified) and annotates the incremental CR with <code>percona.com/base-backup-name</code>.</li>
<li><strong>Fetch the LSN.</strong> The controller calls the xtrabackup sidecar&rsquo;s <code>/backup/checkpoint-info</code> endpoint. The sidecar downloads <code>xtrabackup_checkpoints</code> from the previous backup via <code>xbcloud get</code>, parses it, and returns the <code>to_lsn</code>.</li>
<li><strong>Launch the backup job.</strong> A Kubernetes Job is created with the <code>INCREMENTAL_LSN</code> environment variable set.</li>
<li><strong>Stream to storage.</strong> The sidecar runs <code>xtrabackup --backup --stream=xbstream --incremental-lsn=</code> and pipes the output through <code>xbcloud put</code> to the cloud destination.</li>
</ol>
<h3>Chain Integrity Protection<a class="anchor-link" id="chain-integrity-protection"></a></h3>
<p>The operator enforces chain integrity at multiple levels:</p>
<ul>
<li><strong>Deletion guards:</strong> Only the latest incremental in a chain can be deleted. Attempting to delete a mid-chain backup is blocked using finalizers.</li>
<li><strong>Cascade deletion:</strong> Deleting a full backup automatically removes all dependent incrementals, from newest to oldest.</li>
<li><strong>Concurrent backup prevention:</strong> The controller uses a Lease-based mechanism to prevent multiple incremental backups from running at the same time.</li>
</ul>
<h2>Designing Your Backup Strategy<a class="anchor-link" id="designing-your-backup-strategy"></a></h2>
<h3>When to Use Incremental Backups<a class="anchor-link" id="when-to-use-incremental-backups"></a></h3>
<p>Incremental backups shine when:</p>
<ul>
<li><strong>Your database is large but change rate is low.</strong> A 1 TB database with 2% daily change produces ~20 GB incremental backups instead of 1 TB full backups.</li>
<li><strong>You need frequent backup points.</strong> Run hourly incrementals with minimal overhead.</li>
<li><strong>Cloud storage costs matter.</strong> Example: with about <strong>2%</strong> of the data changing each day, <strong>one full backup</strong> plus <strong>six daily incrementals</strong> needs roughly <strong>one-fifth</strong> the space of keeping <strong>six separate full backups</strong> over the same week.</li>
</ul>
<h3>What to Keep in Mind<a class="anchor-link" id="what-to-keep-in-mind"></a></h3>
<ul>
<li><strong>All chain members must use the same storage backend.</strong> You can&rsquo;t mix S3 and GCS within a chain.</li>
<li><strong>Chain integrity is critical.</strong> If a backup in the chain is corrupted, all subsequent incrementals in that chain become unrestorable. Regular full backups provide recovery checkpoints.</li>
<li><strong>Restore time increases with chain length.</strong> Each incremental adds a prepare step. For very long chains, consider more frequent full backups.</li>
</ul>
<h2>Try It Out<a class="anchor-link" id="try-it-out"></a></h2>
<p>Incremental backups are available in Percona Operator for MySQL version 1.1.0 and later. If you&rsquo;re already running the operator, upgrade your CRDs and add a <code>type: incremental</code> schedule to your backup configuration.</p>
<p><!-- TODO --></p>
<ul>
<li><a href="https://percona.community/blog/2026/04/17/incremental-backups-in-percona-kubernetes-operator-for-mysql/">Operator documentation: Backups</a></li>
<li><a href="https://docs.percona.com/percona-xtrabackup/latest/create-incremental-backup.html" target="_blank" rel="noopener noreferrer">Percona XtraBackup: Incremental backups</a></li>
<li><a href="https://github.com/percona/percona-server-mysql-operator" target="_blank" rel="noopener noreferrer">GitHub: percona/percona-server-mysql-operator</a></li>
</ul>
<p>Have questions or feedback? Join the conversation on the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">Percona Community Forum</a> or open an issue on GitHub. We&rsquo;d love to hear how incremental backups are working for your MySQL-on-Kubernetes deployments.</p>

<p><a href="https://percona.community/blog/2026/04/17/incremental-backups-in-percona-kubernetes-operator-for-mysql/">Incremental backups in Percona Kubernetes Operator for MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ClickHouse Monitoring and Observability Decision Points</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/" />
      <id>https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/</id>
      <updated>2026-04-17T07:00:00+03:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>Given ClickHouse’s ability to execute complex analytical queries across terabytes of data in a single operation, proper monitoring and observability is critical. Its distributed architecture and scalability add layers of complexity, as multi-node clusters require careful coordination monitoring across shards and replicas to ensure data consistency and availability. Adding to the operational pressure is users’ […]<br />
The post ClickHouse Monitoring and Observability Decision Points appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Given ClickHouse&rsquo;s ability to execute complex analytical queries across terabytes of data in a single operation, proper monitoring and observability is critical. Its distributed architecture and scalability add layers of complexity, as multi-node clusters require careful coordination monitoring across shards and replicas to ensure data consistency and availability.</p>
<p>Adding to the operational pressure is users&rsquo; expectations of real-time analytics with sub-second response times. Meeting them requires staying ahead of performance issues rather than simply reacting to them. Therefore, monitoring isn&rsquo;t enough for <a href="https://severalnines.com/clustercontrol/databases/clickhouse">ClickHouse</a>, you need full observability.</p>
<p>So what&rsquo;s the difference? Observability gives you the complete picture by bringing together three essential components: metrics, logs, and traces. Combined with smart alerting, you can catch issues early and keep your system running smoothly. For sysadmins and ops teams managing large-scale data in ClickHouse, this approach makes all the difference.&nbsp;</p>
<p>In this post, I&rsquo;ll lay out the strategies, techniques and tooling needed to proactively optimize ClickHouse performance and maintain peak efficiency. First up, why monitoring and observability are essential for ClickHouse.</p>
<h2 class="wp-block-heading">Monitoring and observability is essential for ClickHouse<a class="anchor-link" id="monitoring-and-observability-is-essential-for-clickhouse"></a></h2>
<p>An efficient observability and monitoring strategy facilitates the following operations:</p>
<ul class="wp-block-list">
<li>Early detection of performance bottlenecks &ndash; identifying slow queries, resource contention, and inefficient table designs before they impact user experience.</li>
<li>Maintaining and ensuring query optimization &ndash; tracking query execution patterns to fine-tune indexes, partitioning strategies, and materialized views for maximum throughput.</li>
<li>Determining resource utilization &ndash; monitoring CPU, memory, disk, and network to prevent resource exhaustion and maintain consistent performance under heavy workloads.</li>
<li>Observing data pipeline health &ndash; observing data ingestion rates, transforming / loading of&nbsp; data (ETL), replication lag, and merge operations to ensure data freshness and reliability</li>
<li>Enabling proactive capacity planning &ndash; analyzing usage trends and growth patterns to scale infra before hitting limits, helping to determine horizontal vs. vertical scaling.&nbsp;</li>
<li>Troubleshooting distributed queries &ndash; Gaining visibility into multi-node query execution, network latency, and inter-node communication in clustered environments</li>
<li>Ensuring high availability &ndash; Monitoring replica synchronization, failover mechanisms, and cluster health to minimize downtime.</li>
<li>Tracking data quality and consistency &ndash; Validating data integrity, detecting anomalies, and ensuring compliance with SLAs.</li>
</ul>
<p>Without proper observability, even the most optimized ClickHouse deployment can suffer from hidden inefficiencies, unexpected failures, and degraded performance that only becomes apparent when it&rsquo;s too late.</p>
<h2 class="wp-block-heading">Key ClickHouse metrics to track<a class="anchor-link" id="key-clickhouse-metrics-to-track"></a></h2>
<p>It&rsquo;s worth noting that, without monitoring and observability in a ClickHouse cluster, you&rsquo;re flying blind in an environment where the following scenario shall be the cluster&rsquo;s state:</p>
<ul class="wp-block-list">
<li>A single poorly optimized query can consume cluster resources</li>
<li>Silent data quality issues can corrupt analytics</li>
<li>Replication lag can lead to inconsistent results</li>
<li>Resource exhaustion can cascade across the entire cluster</li>
</ul>
<p>Conversely, there are key areas that need to be tracked in order to monitor the database performance and analyze the health of your ClickHouse environment. We&rsquo;ll go through them.</p>
<h3 class="wp-block-heading">Key observability components in ClickHouse<a class="anchor-link" id="key-observability-components-in-clickhouse"></a></h3>
<p>There are extensive system tables for observability in ClickHouse which are very useful for observability to collect metrics, logs, and traces. These read-only tables are located in the system database and can be detached, but not dropped. The tables provide information about:</p>
<ul class="wp-block-list">
<li>Server states, processes (both internal and external), and environment.</li>
<li>Options used when the ClickHouse binary was built.</li>
</ul>
<p>The list of system tables below are commonly the source of insights you can rely for observability:</p>
<ul class="wp-block-list">
<li><code>system.query_log</code> &ndash; detailed query execution history</li>
<li><code>system.processes</code> &ndash; currently running queries</li>
<li><code>system.events</code> &ndash; cumulative event counters</li>
<li><code>system.parts</code> &ndash; information about data parts</li>
<li><code>system.metrics</code> &ndash; real-time metrics (current values)</li>
<li><code>system.dashboards</code> &ndash; queries used by /dashboard page accessible though HTTP&nbsp;</li>
<li><code>system.asynchronous_metrics</code> &ndash; periodically calculated metrics</li>
<li><code>system.backups </code>&ndash; all BACKUP or RESTORE operations and other details.</li>
<li><code>system.disks</code> &ndash; information about disks defined in the server configuration.</li>
<li><code>system.replicas</code> &ndash; replication status</li>
<li><code>system.clusters</code> &ndash; cluster configuration and health</li>
</ul>
<h3 class="wp-block-heading">Key areas to monitor in ClickHouse<a class="anchor-link" id="key-areas-to-monitor-in-clickhouse"></a></h3>
<p>Practices used to administer database clusters, especially OLAP databases, depend heavily on the key areas that are regularly examined and observed. Monitoring key metrics in ClickHouse is essential to achieving and maintaining fast, reliable, and cost efficient analytical workloads.&nbsp;</p>
<p>By tracking metrics such as query latency, error rates, insert throughput, number of parts, and replication lag, you gain early visibility into performance bottlenecks and operational risks before they impact users. The goal of maintaining and applying best practices is to troubleshoot incidents quickly and preserve predictable behavior as your data and traffic grow.&nbsp;</p>
<p>Hence, these are the areas and key metrics that need your eyes and attention:</p>
<h4 class="wp-block-heading">Query performance</h4>
<ul class="wp-block-list">
<li>Queries per second</li>
<li>Query execution time</li>
<li>Read / write throughput</li>
<li>Number of rows processed</li>
</ul>
<h4 class="wp-block-heading">Performance &amp; resource utilization</h4>
<ul class="wp-block-list">
<li>CPU utilization based on the queries applied</li>
<li>Query latency &amp; throughput (by query type, user, or workload class)</li>
<li>Memory consumption</li>
<li>Disk I/O operations and latency</li>
<li>Merge performance (queue size, merge times)</li>
<li>Network bandwidth</li>
</ul>
<h4 class="wp-block-heading">Disk operations</h4>
<ul class="wp-block-list">
<li>Insert rate and volume</li>
<li>Merge operations frequency</li>
<li>Number of active parts</li>
<li>Data compression ratio</li>
</ul>
<h4 class="wp-block-heading">Cluster health</h4>
<ul class="wp-block-list">
<li>Replica synchronization and replication queue status</li>
<li>Coordination service (ZooKeeper / ClickHouse Keeper) session&nbsp;</li>
<li>Quorum status</li>
<li>Node availability and role health (ingest/query/background)</li>
<li>Failed queries (rate, error types)</li>
<li>Connection pool usage and connection failures</li>
</ul>
<h4 class="wp-block-heading">Network and topology (especially important for hybrid / on&#8209;prem)</h4>
<ul class="wp-block-list">
<li>Inter-replica and inter-shard latency and errors</li>
<li>Cross&#8209;DC / cross&#8209;region traffic metrics</li>
<li>Bandwidth utilization for replication and large reads / writes</li>
</ul>
<h4 class="wp-block-heading">Reliability and operations</h4>
<ul class="wp-block-list">
<li>Backup and restore status</li>
<li>Schema changes &amp; mutation queue status</li>
<li>Coordination service stability (leader elections, latency spikes)</li>
</ul>
<h2 class="wp-block-heading">Alerting strategy<a class="anchor-link" id="alerting-strategy"></a></h2>
<p>An alerting strategy for ClickHouse should mainly prioritize those areas that are going to hit critical thresholds quickly e.g., resource exhaustion, query failures, etc. over noise. Using tools such as Prometheus / VictoriaMetrics + Grafana + Alertmanager + PagerDuty / Slack is ideal.</p>
<h3 class="wp-block-heading">Critical metrics and alert thresholds<a class="anchor-link" id="critical-metrics-and-alert-thresholds"></a></h3>
<p>Below are good thresholds, organized by category that&rsquo;ll get you started, as well their sources:</p>
<h4 class="wp-block-heading">System usage resource</h4>
<ul class="wp-block-list">
<li>CPU usage
<ul class="wp-block-list">
<li>Set a threshold around &gt;80% for 5min and throw a <em>Warning</em> alert.</li>
<li>Source:<em> </em><code>system.metrics</code></li>
</ul>
</li>
<li>Memory usage
<ul class="wp-block-list">
<li>Consider a threshold of &gt;90% then throw a <em>Critical </em>alert.</li>
<li>Source:<em> </em><code>system.metrics</code></li>
</ul>
</li>
<li>Disk space
<ul class="wp-block-list">
<li>Consider a threshold of &gt;85% used then throw a <em>Critical </em>alert.</li>
<li>Source:<code> system.disks</code></li>
</ul>
</li>
<li>Disk I/O wait
<ul class="wp-block-list">
<li>Set a threshold around &gt;85% used then throw a <em>Critical </em>alert.</li>
<li>Source:<em> </em>via <code>procfs</code> or <code>sysfs</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Query performance</h4>
<ul class="wp-block-list">
<li>Query duration (P95):
<ul class="wp-block-list">
<li>Set a threshold for &gt;10s and throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Failed queries
<ul class="wp-block-list">
<li>Set a threshold for &gt;5% rate and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Concurrent queries
<ul class="wp-block-list">
<li>Set a threshold when the total number reaches &gt;100&nbsp; and throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Query queue size
<ul class="wp-block-list">
<li>Set a threshold when the total number reaches &gt; 50&nbsp; and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Data Ingestion</h4>
<ul class="wp-block-list">
<li>Insert rate drop
<ul class="wp-block-list">
<li>Set a threshold when the rate drops &lt; 50% then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.events</code></li>
</ul>
</li>
<li>Insert failures
<ul class="wp-block-list">
<li>Set a threshold for &gt;1% rate and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Merge operations (MergeTree / ReplicatedMergeTree)</h4>
<ul class="wp-block-list">
<li>Parts count
<ul class="wp-block-list">
<li>Set a threshold &gt;300 per partition then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.parts</code></li>
</ul>
</li>
<li>Merge queue size
<ul class="wp-block-list">
<li>Set a threshold for &gt;100 then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Background tasks
<ul class="wp-block-list">
<li>Set a threshold when Pool saturation &gt;80% then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Mutations running
<ul class="wp-block-list">
<li>Set a threshold for mutations stuck &gt; 1h then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.mutations</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Replication</h4>
<ul class="wp-block-list">
<li>Replication lag
<ul class="wp-block-list">
<li>Set a threshold &gt;300 per partition then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.parts</code></li>
</ul>
</li>
<li>Replication queue
<ul class="wp-block-list">
<li>Set a threshold for &gt;100 tasks then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.replication_queue</code></li>
</ul>
</li>
<li>Replica status
<ul class="wp-block-list">
<li>Set a threshold when is_readonly=1 (something went broke in the replication) then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.replicas</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Cluster health</h4>
<ul class="wp-block-list">
<li>Node availability
<ul class="wp-block-list">
<li>Throw <em>Critical </em>alert if node(s) goes down.</li>
<li>Source: Prometheus or VictoriaMetrics</li>
</ul>
</li>
<li>Distributed query failures
<ul class="wp-block-list">
<li>Set a threshold for &gt;2% then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Network latency
<ul class="wp-block-list">
<li>Set a threshold when &gt;100ms inter-node communication then throw a <em>Warning </em>alert.</li>
<li>Source:<em> </em>via <code>procfs</code> or <code>sysfs</code></li>
</ul>
</li>
</ul>
<h2 class="wp-block-heading">Dashboard best practices<a class="anchor-link" id="dashboard-best-practices"></a></h2>
<p>It&rsquo;s important to have a dashboard that is designed with a set of core principles that prioritize ease of use, performance, and flexibility&nbsp; and is easy to set up and configure. From open-source to enterprise-grade tools, there are options. Relying on open-source tools, it&rsquo;s widely common to use Prometheus / VictoriaMetrics + Grafana + AlertManager integrated with other 3rd party tools, such as PagerDuty, Slack, OpsGenie, etc. to send high-severity alerts.</p>
<p>Any tool you are evaluating should have these functionalities:</p>
<ul class="wp-block-list">
<li>Scraping: Use the ClickHouse built-in Prometheus-friendly output or a dedicated exporter (like the official one) to collect metrics.</li>
<li>Cardinality: ClickHouse metrics can have high cardinality e.g., many unique query IDs.</li>
<li>Recording Rules: Pre-calculate frequently queried or computationally expensive metrics.</li>
</ul>
<h3 class="wp-block-heading">Managing alerts<a class="anchor-link" id="managing-alerts"></a></h3>
<p>Let&rsquo;s look at Alertmanager as an example, which handles de-duplication, grouping, inhibition, and routing of alerts to receivers like Slack or PagerDuty. This allows to do the following:</p>
<h4 class="wp-block-heading">Actionable and clear alerts</h4>
<ul class="wp-block-list">
<li>Specificity: Use specific messages that are clear and specific to the intention of the alert such as <em>ClickHouse: High Insert Latency on Cluster X.</em></li>
<li>Context: Include labels (severity, instance, cluster) and annotations (summary, description, runbook links) in your Prometheus alert rules.</li>
<li>Severity levels: Assign clear severity labels (<em>critical, warning</em>) to all alerts to ensure proper routing to PagerDuty (for on-call) versus Slack (for general awareness).</li>
</ul>
<h4 class="wp-block-heading">Reduce alert fatigue</h4>
<ul class="wp-block-list">
<li>Grouping: Configure Alertmanager to group similar alerts e.g., group by alertname and cluster into a single notification. This prevents a cascade of alerts from overwhelming the on-call person.</li>
<li>Inhibition: Use inhibition rules to suppress less critical alerts when a major one is firing. For example, if the ClickHouseInstanceDown alert is active for a host, inhibit alerts for DiskSpaceLow on the same host, as the host being down is the root cause.</li>
<li>Throttling / timing: Utilize the for clause in Prometheus rules e.g., <em>for: 5m</em> to ensure a condition persists before an alert fires, avoiding alerts for transient issues (flapping). Configure <code>group_wait</code>, <code>group_interval</code>, and <code>repeat_interval</code> in Alertmanager to control notification frequency.</li>
</ul>
<h3 class="wp-block-heading">Integration with third-party tooling<a class="anchor-link" id="integration-with-third-party-tooling"></a></h3>
<p>Using the example of Slack / PagerDuty for third-party integration tools, below are the recommendations and guidelines you can follow:</p>
<ul class="wp-block-list">
<li>Routing: Define routing tree in Alertmanager&rsquo;s configuration to send high-severity alerts (<em>critical</em>) to PagerDuty for on-call immediate response and lower-severity alerts (warning, info) to Slack channels for visibility.</li>
<li>PagerDuty: Integrate using the Events API V2. Set <code>send_resolved: true</code> to automatically resolve PagerDuty incidents when the Prometheus alert is cleared. Use event rules in PagerDuty to filter or enrich events.</li>
<li>Slack: Create a dedicated channel for incident coordination. Leverage notification templates to standardize the look and feel of alerts, including buttons for acknowledging / resolving incidents directly from Slack (especially if using the PagerDuty integration).</li>
</ul>
<h3 class="wp-block-heading">Commercial or enterprise-grade tools<a class="anchor-link" id="commercial-or-enterprise-grade-tools"></a></h3>
<p>ClickHouse also offers ClickStack which is a production-grade observability platform built specifically for ClickHouse. It features unifying logs, traces, metrics and sessions in a single high-performance solution. Designed for monitoring and debugging complex systems, ClickStack enables developers and SREs to trace issues end-to-end without switching between tools or manually stitching together data using timestamps or correlation IDs.&nbsp;</p>
<p>If it suits your budget, enterprise tools are highly advisable as it offers less headache, suitability for your environment, ease and comfortability to maintain the software as you rely on the maintainer itself plus the support mechanism that the software offers.&nbsp;</p>
<h2 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>In the end, getting monitoring and observability right for ClickHouse is the difference between passively assuming the cluster is healthy and having hard, real-time data that tells you exactly what&rsquo;s happening across queries, resources, and internals. When you focus on the right metrics, set up alerts that really matter, and build dashboards that answer real questions instead of just looking good, you put yourself in a much stronger position. You can catch issues early, understand what&rsquo;s happening under the hood, and scale your cluster with a lot more confidence.</p>
<p>You also don&rsquo;t need to have everything perfect on day one. Start simple: keep an eye on performance and resource usage, set alerts around the parts of the system you care about most, and build a few dashboards your team will actually check. As your ClickHouse workloads grow, you can refine what you track, adjust your alert thresholds, and evolve your setup over time.</p>
<p>By treating observability as an ongoing practice, ClickHouse becomes a system you understand, trust, and can tune as you go, instead of a blackbox you&rsquo;re constantly second-guessing.</p>
<h2 class="wp-block-heading">Try ClickHouse yourself free for 30 days, install ClusterControl now!<a class="anchor-link" id="try-clickhouse-yourself-free-for-30-days-install-clustercontrol-now"></a></h2>
<h3 class="wp-block-heading">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc
sudo ./install-cc     # omit sudo if you run as root</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<p>The post <a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>ClickHouse Monitoring and Observability Decision Points</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/" />
      <id>https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/</id>
      <updated>2026-04-17T07:00:00+03:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>Given ClickHouse’s ability to execute complex analytical queries across terabytes of data in a single operation, proper monitoring and observability is critical. Its distributed architecture and scalability add layers of complexity, as multi-node clusters require careful coordination monitoring across shards and replicas to ensure data consistency and availability. Adding to the operational pressure is users’ […]<br />
The post ClickHouse Monitoring and Observability Decision Points appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Given ClickHouse&rsquo;s ability to execute complex analytical queries across terabytes of data in a single operation, proper monitoring and observability is critical. Its distributed architecture and scalability add layers of complexity, as multi-node clusters require careful coordination monitoring across shards and replicas to ensure data consistency and availability.</p>
<p>Adding to the operational pressure is users&rsquo; expectations of real-time analytics with sub-second response times. Meeting them requires staying ahead of performance issues rather than simply reacting to them. Therefore, monitoring isn&rsquo;t enough for <a href="https://severalnines.com/clustercontrol/databases/clickhouse">ClickHouse</a>, you need full observability.</p>
<p>So what&rsquo;s the difference? Observability gives you the complete picture by bringing together three essential components: metrics, logs, and traces. Combined with smart alerting, you can catch issues early and keep your system running smoothly. For sysadmins and ops teams managing large-scale data in ClickHouse, this approach makes all the difference.&nbsp;</p>
<p>In this post, I&rsquo;ll lay out the strategies, techniques and tooling needed to proactively optimize ClickHouse performance and maintain peak efficiency. First up, why monitoring and observability are essential for ClickHouse.</p>
<h2 class="wp-block-heading">Monitoring and observability is essential for ClickHouse<a class="anchor-link" id="monitoring-and-observability-is-essential-for-clickhouse"></a></h2>
<p>An efficient observability and monitoring strategy facilitates the following operations:</p>
<ul class="wp-block-list">
<li>Early detection of performance bottlenecks &ndash; identifying slow queries, resource contention, and inefficient table designs before they impact user experience.</li>
<li>Maintaining and ensuring query optimization &ndash; tracking query execution patterns to fine-tune indexes, partitioning strategies, and materialized views for maximum throughput.</li>
<li>Determining resource utilization &ndash; monitoring CPU, memory, disk, and network to prevent resource exhaustion and maintain consistent performance under heavy workloads.</li>
<li>Observing data pipeline health &ndash; observing data ingestion rates, transforming / loading of&nbsp; data (ETL), replication lag, and merge operations to ensure data freshness and reliability</li>
<li>Enabling proactive capacity planning &ndash; analyzing usage trends and growth patterns to scale infra before hitting limits, helping to determine horizontal vs. vertical scaling.&nbsp;</li>
<li>Troubleshooting distributed queries &ndash; Gaining visibility into multi-node query execution, network latency, and inter-node communication in clustered environments</li>
<li>Ensuring high availability &ndash; Monitoring replica synchronization, failover mechanisms, and cluster health to minimize downtime.</li>
<li>Tracking data quality and consistency &ndash; Validating data integrity, detecting anomalies, and ensuring compliance with SLAs.</li>
</ul>
<p>Without proper observability, even the most optimized ClickHouse deployment can suffer from hidden inefficiencies, unexpected failures, and degraded performance that only becomes apparent when it&rsquo;s too late.</p>
<h2 class="wp-block-heading">Key ClickHouse metrics to track<a class="anchor-link" id="key-clickhouse-metrics-to-track"></a></h2>
<p>It&rsquo;s worth noting that, without monitoring and observability in a ClickHouse cluster, you&rsquo;re flying blind in an environment where the following scenario shall be the cluster&rsquo;s state:</p>
<ul class="wp-block-list">
<li>A single poorly optimized query can consume cluster resources</li>
<li>Silent data quality issues can corrupt analytics</li>
<li>Replication lag can lead to inconsistent results</li>
<li>Resource exhaustion can cascade across the entire cluster</li>
</ul>
<p>Conversely, there are key areas that need to be tracked in order to monitor the database performance and analyze the health of your ClickHouse environment. We&rsquo;ll go through them.</p>
<h3 class="wp-block-heading">Key observability components in ClickHouse<a class="anchor-link" id="key-observability-components-in-clickhouse"></a></h3>
<p>There are extensive system tables for observability in ClickHouse which are very useful for observability to collect metrics, logs, and traces. These read-only tables are located in the system database and can be detached, but not dropped. The tables provide information about:</p>
<ul class="wp-block-list">
<li>Server states, processes (both internal and external), and environment.</li>
<li>Options used when the ClickHouse binary was built.</li>
</ul>
<p>The list of system tables below are commonly the source of insights you can rely for observability:</p>
<ul class="wp-block-list">
<li><code>system.query_log</code> &ndash; detailed query execution history</li>
<li><code>system.processes</code> &ndash; currently running queries</li>
<li><code>system.events</code> &ndash; cumulative event counters</li>
<li><code>system.parts</code> &ndash; information about data parts</li>
<li><code>system.metrics</code> &ndash; real-time metrics (current values)</li>
<li><code>system.dashboards</code> &ndash; queries used by /dashboard page accessible though HTTP&nbsp;</li>
<li><code>system.asynchronous_metrics</code> &ndash; periodically calculated metrics</li>
<li><code>system.backups </code>&ndash; all BACKUP or RESTORE operations and other details.</li>
<li><code>system.disks</code> &ndash; information about disks defined in the server configuration.</li>
<li><code>system.replicas</code> &ndash; replication status</li>
<li><code>system.clusters</code> &ndash; cluster configuration and health</li>
</ul>
<h3 class="wp-block-heading">Key areas to monitor in ClickHouse<a class="anchor-link" id="key-areas-to-monitor-in-clickhouse"></a></h3>
<p>Practices used to administer database clusters, especially OLAP databases, depend heavily on the key areas that are regularly examined and observed. Monitoring key metrics in ClickHouse is essential to achieving and maintaining fast, reliable, and cost efficient analytical workloads.&nbsp;</p>
<p>By tracking metrics such as query latency, error rates, insert throughput, number of parts, and replication lag, you gain early visibility into performance bottlenecks and operational risks before they impact users. The goal of maintaining and applying best practices is to troubleshoot incidents quickly and preserve predictable behavior as your data and traffic grow.&nbsp;</p>
<p>Hence, these are the areas and key metrics that need your eyes and attention:</p>
<h4 class="wp-block-heading">Query performance</h4>
<ul class="wp-block-list">
<li>Queries per second</li>
<li>Query execution time</li>
<li>Read / write throughput</li>
<li>Number of rows processed</li>
</ul>
<h4 class="wp-block-heading">Performance &amp; resource utilization</h4>
<ul class="wp-block-list">
<li>CPU utilization based on the queries applied</li>
<li>Query latency &amp; throughput (by query type, user, or workload class)</li>
<li>Memory consumption</li>
<li>Disk I/O operations and latency</li>
<li>Merge performance (queue size, merge times)</li>
<li>Network bandwidth</li>
</ul>
<h4 class="wp-block-heading">Disk operations</h4>
<ul class="wp-block-list">
<li>Insert rate and volume</li>
<li>Merge operations frequency</li>
<li>Number of active parts</li>
<li>Data compression ratio</li>
</ul>
<h4 class="wp-block-heading">Cluster health</h4>
<ul class="wp-block-list">
<li>Replica synchronization and replication queue status</li>
<li>Coordination service (ZooKeeper / ClickHouse Keeper) session&nbsp;</li>
<li>Quorum status</li>
<li>Node availability and role health (ingest/query/background)</li>
<li>Failed queries (rate, error types)</li>
<li>Connection pool usage and connection failures</li>
</ul>
<h4 class="wp-block-heading">Network and topology (especially important for hybrid / on&#8209;prem)</h4>
<ul class="wp-block-list">
<li>Inter-replica and inter-shard latency and errors</li>
<li>Cross&#8209;DC / cross&#8209;region traffic metrics</li>
<li>Bandwidth utilization for replication and large reads / writes</li>
</ul>
<h4 class="wp-block-heading">Reliability and operations</h4>
<ul class="wp-block-list">
<li>Backup and restore status</li>
<li>Schema changes &amp; mutation queue status</li>
<li>Coordination service stability (leader elections, latency spikes)</li>
</ul>
<h2 class="wp-block-heading">Alerting strategy<a class="anchor-link" id="alerting-strategy"></a></h2>
<p>An alerting strategy for ClickHouse should mainly prioritize those areas that are going to hit critical thresholds quickly e.g., resource exhaustion, query failures, etc. over noise. Using tools such as Prometheus / VictoriaMetrics + Grafana + Alertmanager + PagerDuty / Slack is ideal.</p>
<h3 class="wp-block-heading">Critical metrics and alert thresholds<a class="anchor-link" id="critical-metrics-and-alert-thresholds"></a></h3>
<p>Below are good thresholds, organized by category that&rsquo;ll get you started, as well their sources:</p>
<h4 class="wp-block-heading">System usage resource</h4>
<ul class="wp-block-list">
<li>CPU usage
<ul class="wp-block-list">
<li>Set a threshold around &gt;80% for 5min and throw a <em>Warning</em> alert.</li>
<li>Source:<em> </em><code>system.metrics</code></li>
</ul>
</li>
<li>Memory usage
<ul class="wp-block-list">
<li>Consider a threshold of &gt;90% then throw a <em>Critical </em>alert.</li>
<li>Source:<em> </em><code>system.metrics</code></li>
</ul>
</li>
<li>Disk space
<ul class="wp-block-list">
<li>Consider a threshold of &gt;85% used then throw a <em>Critical </em>alert.</li>
<li>Source:<code> system.disks</code></li>
</ul>
</li>
<li>Disk I/O wait
<ul class="wp-block-list">
<li>Set a threshold around &gt;85% used then throw a <em>Critical </em>alert.</li>
<li>Source:<em> </em>via <code>procfs</code> or <code>sysfs</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Query performance</h4>
<ul class="wp-block-list">
<li>Query duration (P95):
<ul class="wp-block-list">
<li>Set a threshold for &gt;10s and throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Failed queries
<ul class="wp-block-list">
<li>Set a threshold for &gt;5% rate and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Concurrent queries
<ul class="wp-block-list">
<li>Set a threshold when the total number reaches &gt;100&nbsp; and throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Query queue size
<ul class="wp-block-list">
<li>Set a threshold when the total number reaches &gt; 50&nbsp; and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Data Ingestion</h4>
<ul class="wp-block-list">
<li>Insert rate drop
<ul class="wp-block-list">
<li>Set a threshold when the rate drops &lt; 50% then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.events</code></li>
</ul>
</li>
<li>Insert failures
<ul class="wp-block-list">
<li>Set a threshold for &gt;1% rate and throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Merge operations (MergeTree / ReplicatedMergeTree)</h4>
<ul class="wp-block-list">
<li>Parts count
<ul class="wp-block-list">
<li>Set a threshold &gt;300 per partition then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.parts</code></li>
</ul>
</li>
<li>Merge queue size
<ul class="wp-block-list">
<li>Set a threshold for &gt;100 then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Background tasks
<ul class="wp-block-list">
<li>Set a threshold when Pool saturation &gt;80% then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.metrics</code></li>
</ul>
</li>
<li>Mutations running
<ul class="wp-block-list">
<li>Set a threshold for mutations stuck &gt; 1h then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.mutations</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Replication</h4>
<ul class="wp-block-list">
<li>Replication lag
<ul class="wp-block-list">
<li>Set a threshold &gt;300 per partition then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.parts</code></li>
</ul>
</li>
<li>Replication queue
<ul class="wp-block-list">
<li>Set a threshold for &gt;100 tasks then throw a <em>Warning </em>alert.</li>
<li>Source: <code>system.replication_queue</code></li>
</ul>
</li>
<li>Replica status
<ul class="wp-block-list">
<li>Set a threshold when is_readonly=1 (something went broke in the replication) then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.replicas</code></li>
</ul>
</li>
</ul>
<h4 class="wp-block-heading">Cluster health</h4>
<ul class="wp-block-list">
<li>Node availability
<ul class="wp-block-list">
<li>Throw <em>Critical </em>alert if node(s) goes down.</li>
<li>Source: Prometheus or VictoriaMetrics</li>
</ul>
</li>
<li>Distributed query failures
<ul class="wp-block-list">
<li>Set a threshold for &gt;2% then throw a <em>Critical </em>alert.</li>
<li>Source: <code>system.query_log</code></li>
</ul>
</li>
<li>Network latency
<ul class="wp-block-list">
<li>Set a threshold when &gt;100ms inter-node communication then throw a <em>Warning </em>alert.</li>
<li>Source:<em> </em>via <code>procfs</code> or <code>sysfs</code></li>
</ul>
</li>
</ul>
<h2 class="wp-block-heading">Dashboard best practices<a class="anchor-link" id="dashboard-best-practices"></a></h2>
<p>It&rsquo;s important to have a dashboard that is designed with a set of core principles that prioritize ease of use, performance, and flexibility&nbsp; and is easy to set up and configure. From open-source to enterprise-grade tools, there are options. Relying on open-source tools, it&rsquo;s widely common to use Prometheus / VictoriaMetrics + Grafana + AlertManager integrated with other 3rd party tools, such as PagerDuty, Slack, OpsGenie, etc. to send high-severity alerts.</p>
<p>Any tool you are evaluating should have these functionalities:</p>
<ul class="wp-block-list">
<li>Scraping: Use the ClickHouse built-in Prometheus-friendly output or a dedicated exporter (like the official one) to collect metrics.</li>
<li>Cardinality: ClickHouse metrics can have high cardinality e.g., many unique query IDs.</li>
<li>Recording Rules: Pre-calculate frequently queried or computationally expensive metrics.</li>
</ul>
<h3 class="wp-block-heading">Managing alerts<a class="anchor-link" id="managing-alerts"></a></h3>
<p>Let&rsquo;s look at Alertmanager as an example, which handles de-duplication, grouping, inhibition, and routing of alerts to receivers like Slack or PagerDuty. This allows to do the following:</p>
<h4 class="wp-block-heading">Actionable and clear alerts</h4>
<ul class="wp-block-list">
<li>Specificity: Use specific messages that are clear and specific to the intention of the alert such as <em>ClickHouse: High Insert Latency on Cluster X.</em></li>
<li>Context: Include labels (severity, instance, cluster) and annotations (summary, description, runbook links) in your Prometheus alert rules.</li>
<li>Severity levels: Assign clear severity labels (<em>critical, warning</em>) to all alerts to ensure proper routing to PagerDuty (for on-call) versus Slack (for general awareness).</li>
</ul>
<h4 class="wp-block-heading">Reduce alert fatigue</h4>
<ul class="wp-block-list">
<li>Grouping: Configure Alertmanager to group similar alerts e.g., group by alertname and cluster into a single notification. This prevents a cascade of alerts from overwhelming the on-call person.</li>
<li>Inhibition: Use inhibition rules to suppress less critical alerts when a major one is firing. For example, if the ClickHouseInstanceDown alert is active for a host, inhibit alerts for DiskSpaceLow on the same host, as the host being down is the root cause.</li>
<li>Throttling / timing: Utilize the for clause in Prometheus rules e.g., <em>for: 5m</em> to ensure a condition persists before an alert fires, avoiding alerts for transient issues (flapping). Configure <code>group_wait</code>, <code>group_interval</code>, and <code>repeat_interval</code> in Alertmanager to control notification frequency.</li>
</ul>
<h3 class="wp-block-heading">Integration with third-party tooling<a class="anchor-link" id="integration-with-third-party-tooling"></a></h3>
<p>Using the example of Slack / PagerDuty for third-party integration tools, below are the recommendations and guidelines you can follow:</p>
<ul class="wp-block-list">
<li>Routing: Define routing tree in Alertmanager&rsquo;s configuration to send high-severity alerts (<em>critical</em>) to PagerDuty for on-call immediate response and lower-severity alerts (warning, info) to Slack channels for visibility.</li>
<li>PagerDuty: Integrate using the Events API V2. Set <code>send_resolved: true</code> to automatically resolve PagerDuty incidents when the Prometheus alert is cleared. Use event rules in PagerDuty to filter or enrich events.</li>
<li>Slack: Create a dedicated channel for incident coordination. Leverage notification templates to standardize the look and feel of alerts, including buttons for acknowledging / resolving incidents directly from Slack (especially if using the PagerDuty integration).</li>
</ul>
<h3 class="wp-block-heading">Commercial or enterprise-grade tools<a class="anchor-link" id="commercial-or-enterprise-grade-tools"></a></h3>
<p>ClickHouse also offers ClickStack which is a production-grade observability platform built specifically for ClickHouse. It features unifying logs, traces, metrics and sessions in a single high-performance solution. Designed for monitoring and debugging complex systems, ClickStack enables developers and SREs to trace issues end-to-end without switching between tools or manually stitching together data using timestamps or correlation IDs.&nbsp;</p>
<p>If it suits your budget, enterprise tools are highly advisable as it offers less headache, suitability for your environment, ease and comfortability to maintain the software as you rely on the maintainer itself plus the support mechanism that the software offers.&nbsp;</p>
<h2 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>In the end, getting monitoring and observability right for ClickHouse is the difference between passively assuming the cluster is healthy and having hard, real-time data that tells you exactly what&rsquo;s happening across queries, resources, and internals. When you focus on the right metrics, set up alerts that really matter, and build dashboards that answer real questions instead of just looking good, you put yourself in a much stronger position. You can catch issues early, understand what&rsquo;s happening under the hood, and scale your cluster with a lot more confidence.</p>
<p>You also don&rsquo;t need to have everything perfect on day one. Start simple: keep an eye on performance and resource usage, set alerts around the parts of the system you care about most, and build a few dashboards your team will actually check. As your ClickHouse workloads grow, you can refine what you track, adjust your alert thresholds, and evolve your setup over time.</p>
<p>By treating observability as an ongoing practice, ClickHouse becomes a system you understand, trust, and can tune as you go, instead of a blackbox you&rsquo;re constantly second-guessing.</p>
<h2 class="wp-block-heading">Try ClickHouse yourself free for 30 days, install ClusterControl now!<a class="anchor-link" id="try-clickhouse-yourself-free-for-30-days-install-clustercontrol-now"></a></h2>
<h3 class="wp-block-heading">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc
sudo ./install-cc     # omit sudo if you run as root</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<p>The post <a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/clickhouse-monitoring-and-observability-decision-points/">ClickHouse Monitoring and Observability Decision Points</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html" />
      <id>https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html</id>
      <updated>2026-04-14T19:53:00+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>You read this right, symbolic links (symlinks) are unsafe in MySQL since at least 8.0.39.  As always, it is a little more complicated than that, but if you are using symbolic links and in certain conditions, you risk a crash.  I think it is important to raise awareness on this, hence this post.</p>
<p>My attention was brought to this via the now private Bug #120156: MySQL 8.0.39/8.0.42</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html">Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>You read this right, symbolic links (symlinks) are unsafe in MySQL since at least 8.0.39.&nbsp; As always, it is a little more complicated than that, but if you are using symbolic links and in certain conditions, you risk a crash.&nbsp; I think it is important to raise awareness on this, hence this post.</p>
<p>My attention was brought to this via the now private Bug&nbsp;#120156: MySQL 8.0.39/8.0.42</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html">Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html" />
      <id>https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html</id>
      <updated>2026-04-14T19:53:00+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>You read this right, symbolic links (symlinks) are unsafe in MySQL since at least 8.0.39.  As always, it is a little more complicated than that, but if you are using symbolic links and in certain conditions, you risk a crash.  I think it is important to raise awareness on this, hence this post.</p>
<p>My attention was brought to this via the now private Bug #120156: MySQL 8.0.39/8.0.42</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html">Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>You read this right, symbolic links (symlinks) are unsafe in MySQL since at least 8.0.39.&nbsp; As always, it is a little more complicated than that, but if you are using symbolic links and in certain conditions, you risk a crash.&nbsp; I think it is important to raise awareness on this, hence this post.</p>
<p>My attention was brought to this via the now private Bug&nbsp;#120156: MySQL 8.0.39/8.0.42</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/04/symlinks-are-unsafe-in-mysql.html">Symlinks are Unsafe since MySQL 8.0.39 (and maybe even before)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL 9.7.0 vs sysbench on a small server</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/mysql-970-vs-sysbench-on-small-server.html" />
      <id>https://smalldatum.blogspot.com/2026/04/mysql-970-vs-sysbench-on-small-server.html</id>
      <updated>2026-04-10T18:00:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results from sysbench on a small server with MySQL 9.7.0 and 8.4.8. Sysbench is run with low concurrency (1 thread) and a cached database. The purpose is to search for changes in performance, often from new CPU overheads.I tested MySQL 9.7.0 with and without the hypergraph optimizer enabled. I don\'t expect it to help much because the queries run here are simple. I hope to learn it doesn\'t hurt performance in that case.tl;drThroughput improves on two tests with the Hypergraph optimizer in 9.7.0 because they get better query plans.One read-only test and several write-heavy tests have small regressions from 8.4.8 to 9.7.0. This might be from new CPU overheads but I don\'t see obvious problems in the flamegraphs. Builds, configuration and hardwareI compiled MySQL from source for versions 8.4.8 and 9.7.0.The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it are here. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.The my.cnf files os here for 8.4. I call this the z12a configs and variants of it are used for MySQL 5.6 through 8.4.For 9.7 I use two configs:z13aThis is as close as possible to z12a and adds two options to undo changes to the default values for two gtid-related options that arrived in 9.6. z13bThis is like z13a but then enables the hypergraph optimizerAll DBMS versions use the latin1 character set as explained here.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide tables below with relative QPS. When the relative QPS is &#62; 1 then some version is faster than the base version. When it is &#60; 1 then there might be a regression.  The relative QPS (rQPS) is:(QPS for some version) / (QPS for MySQL 8.4.8) Results: point queriesI describe performance changes (changes to relative QPS, rQPS) in terms of basis points. Performance changes by one basis point when the difference in rQPS is 0.01. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.Throughput with MySQL 9.7.0 is similar to 8.4.8 except for point-query where there are regressions as rQPS drops by 5 and 7 basis points. The point-query test uses simple queries that fetch one column from one row by PK. From vmstat metrics the CPU overhead per query for 9.7.0 is ~8% larger than for 8.4.8, with and without the hypergraph optimizer. I don&#039;t see anything obvious in the flamegraphs.z13a    z13b0.99    1.01    hot-points0.95    0.93    point-query0.99    1.01    points-covered-pk1.00    1.01    points-covered-si0.98    1.00    points-notcovered-pk0.99    1.01    points-notcovered-si1.00    1.02    random-points_range=10000.99    1.01    random-points_range=1000.96    1.00    random-points_range=10Results: range queries without aggregationI describe performance changes (changes to relative QPS, rQPS) in terms of basis points. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.Throughput with MySQL 9.7.0 is similar to 8.4.8. I am skeptical there is a regression for the scan test with the z13b config. I suspect that is noise.z13a    z13b0.99    0.99    range-covered-pk0.99    0.99    range-covered-si0.99    0.99    range-notcovered-pk0.98    0.98    range-notcovered-si1.00    0.96    scanResults: range queries with aggregationI describe performance changes (changes to relative QPS, rQPS) in terms of basis points. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.There might be small regressions in several tests with rQPS dropping by a few points but I will ignore that for now.There is a large improvement for the read-only-distinct test with the z13b config. The query for this test is select distinct c from sbtest where id between ? and ? order by c. The reason for the performance improvment is that the hypergraph optimizer chooses a better plan, see here.There is a large improvement for the read-only test with range=10000. This test uses the read-only version of the classic sysbench transaction (see here). One of the queries it runs is the query used by read-only-distinct. So it benefits from the better plan for that query. z13a    z13b0.97    0.97    read-only-count0.98    1.26    read-only-distinct0.96    0.95    read-only-order0.99    1.15    read-only_range=100000.97    1.00    read-only_range=1000.96    0.97    read-only_range=100.99    0.99    read-only-simple0.97    0.96    read-only-sumResults: writesI describe performance changes (changes to relative QPS, rQPS) in terms of basis points. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.There might be several small regressions here. I don&#039;t see obvious problems in the flamegraphs.z13a    z13b0.95    0.92    delete1.00    1.01    insert0.97    0.98    read-write_range=1000.96    0.95    read-write_range=100.97    0.96    update-index0.97    0.92    update-inlist0.95    0.93    update-nonindex0.95    0.92    update-one0.95    0.93    update-zipf0.97    0.95    write-only</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/mysql-970-vs-sysbench-on-small-server.html">MySQL 9.7.0 vs sysbench on a small server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results from sysbench on a small server with MySQL 9.7.0 and 8.4.8. Sysbench is run with low concurrency (1 thread) and a cached database. The purpose is to search for changes in performance, often from new CPU overheads.</p>
<p>I tested MySQL 9.7.0 with and without the hypergraph optimizer enabled. I don&rsquo;t expect it to help much because the queries run here are simple. I hope to learn it doesn&rsquo;t hurt performance in that case.</p>
<p>tl;dr</p>

<ul>
<li>Throughput improves on two tests with the Hypergraph optimizer in 9.7.0 because they get better query plans.</li>
<li>One read-only test and several write-heavy tests have small regressions from 8.4.8 to 9.7.0. This might be from new CPU overheads but I don&rsquo;t see obvious problems in the flamegraphs.&nbsp;</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>

<div></div>

<div>I compiled MySQL from source for versions 8.4.8 and 9.7.0.</div>
</div>
<p>The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2022/10/small-servers-for-performance-testing-v4.html">are here</a>. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.</p>
<p>The my.cnf files os here for&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8406_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.4</a>. I call this the z12a configs and variants of it are used for MySQL 5.6 through 8.4.</p>
<p>For 9.7 I use two configs:</p>

<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13a_c8r32">z13a</a></li>
<ul>
<li>This is as close as possible to z12a and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13a_c8r32#L76-L77">adds two options</a>&nbsp;to undo changes to the default values for two gtid-related options that arrived in 9.6.&nbsp;</li>
</ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13b_c8r32">z13b</a></li>
<ul>
<li>This is like z13a but then <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13b_c8r32#L79">enables the hypergraph optimizer</a></li>
</ul>
</ul>
<p>All DBMS versions use the latin1 character set as&nbsp;<a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">explained here</a>.</p>
<p><b>Benchmark</b></p>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.</div>
<div>The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div>I provide tables below with relative QPS.&nbsp;<span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than the</span><span>&nbsp;</span><i>base version.</i><span>&nbsp;When it is &lt; 1 then there might be a regression.&nbsp;&nbsp;</span><span>The relative QPS (<b>rQPS</b>) is:</span></div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for MySQL 8.4.8)<span>&nbsp;</span></p></blockquote>
<p><b>Results: point queries</b></p>
<div>
<div><span>I describe performance changes (changes to relative QPS, rQPS) in terms of basis points. Performance changes by one </span><i><b>basis point</b></i><span>&nbsp;when the difference in rQPS is 0.01. When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.</span></div>
<div><span><br></span></div>
<div><span>This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.</span></div>
<div>
<ul>
<li>Throughput with MySQL 9.7.0 is similar to 8.4.8 except for point-query where there are regressions as rQPS drops by 5 and 7 basis points. The point-query test uses simple queries that fetch one column from one row by PK. From <a href="https://gist.github.com/mdcallag/bc910b227be30911a6f87d0dac3ec6d0#file-o-met-my8408plus-L17-L19">vmstat metrics</a> the CPU overhead per query for 9.7.0 is ~8% larger than for 8.4.8, with and without the hypergraph optimizer. I don&rsquo;t see anything obvious in the flamegraphs.</li>
</ul>
</div>
</div>
<div><span>z13a&nbsp; &nbsp; z13b</span></div>
<div><span>
<div><span>0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; hot-points</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; <span>0.93</span>&nbsp; &nbsp; point-query</span></div>
<div><span>0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span>1.00&nbsp; &nbsp; 1.02&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; 1.00&nbsp; &nbsp; random-points_range=10</span></div>
<div></div>
<p></p></span></div>
<div><b>Results: range queries without aggregation</b></div>
<div>
<div>
<div><span><br></span></div>
<div><span>I describe performance changes (changes to relative QPS, rQPS) in terms of basis points. </span><span>When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.</span></div>
</div>
<div><span><br></span></div>
<div>This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.</div>
<div>
<ul>
<li>Throughput with MySQL 9.7.0 is similar to 8.4.8. I am skeptical there is a regression for the scan test with the z13b config. I suspect that is noise.</li>
</ul>
</div>
<div><span>z13a&nbsp; &nbsp; z13b</span></div>
<div>
<div><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-covered-pk</span></div>
<div><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span>1.00&nbsp; &nbsp; 0.96&nbsp; &nbsp; scan</span></div>
</div>
<div><b><br></b></div>
<div><b>Results: range queries with aggregation</b></div>
<div>
<div>
<div><span><br></span></div>
<div><span>I describe performance changes (changes to relative QPS, rQPS) in terms of basis points. </span><span>When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.</span></div>
</div>
<div><span><br></span></div>
<div>This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.</div>
<div>
<ul>
<li>There might be small regressions in several tests with rQPS dropping by a few points but I will ignore that for now.</li>
<li>There is a large improvement for the read-only-distinct test with the z13b config. The query for this test is <i>select distinct c from sbtest where id between ? and ? order by c</i>. The reason for the performance improvment is that the hypergraph optimizer chooses a better plan, see <a href="https://gist.github.com/mdcallag/f532ef990d7f06df0338f431ad75147f">here</a>.</li>
<li>There is a large improvement for the read-only test with range=10000. This test uses the read-only version of the classic sysbench transaction (see <a href="https://github.com/mdcallag/mytools/blob/master/bench/sysbench.lua/lua/oltp_read_only.lua">here</a>). One of the queries it runs is the query used by read-only-distinct. So it benefits from the better plan for that query.&nbsp;</li>
</ul>
</div>
<div><span>z13a&nbsp; &nbsp; z13b</span></div>
<div>
<div><span>0.97&nbsp; &nbsp; 0.97&nbsp; &nbsp; read-only-count</span></div>
<div><span>0.98&nbsp; &nbsp; <span>1.26</span>&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; read-only-order</span></div>
<div><span>0.99&nbsp; &nbsp; <span>1.15</span>&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.97&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; 0.97&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.97&nbsp; &nbsp; 0.96&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div><b><br></b></div>
<div><b>Results: writes</b></div>
<div>
<div>
<div><span><br></span></div>
<div><span>I describe performance changes (changes to relative QPS, rQPS) in terms of basis points. </span><span>When rQPS decreases from 0.95 to 0.85 then it changed by 10 basis points.</span></div>
</div>
<div><span><br></span></div>
<div>This shows the rQPS for MySQL 9.7.0 using both the z13a and z13b configs. It is relative to the throughput from MySQL 8.4.8.</div>
<div>
<ul>
<li>There might be several small regressions here. I don&rsquo;t see obvious problems in the flamegraphs.</li>
</ul>
</div>
<div>
<div><span>z13a&nbsp; &nbsp; z13b</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; <span>0.92</span>&nbsp; &nbsp; delete</span></div>
<div><span>1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; insert</span></div>
<div><span>0.97&nbsp; &nbsp; 0.98&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>0.97&nbsp; &nbsp; 0.96&nbsp; &nbsp; update-index</span></div>
<div><span>0.97&nbsp; &nbsp; <span>0.92</span>&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.95&nbsp; &nbsp; <span>0.93</span>&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.95&nbsp; &nbsp; <span>0.92</span>&nbsp; &nbsp; update-one</span></div>
<div><span>0.95&nbsp; &nbsp; 0.93&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.97&nbsp; &nbsp; 0.95&nbsp; &nbsp; write-only</span></div>
</div>
</div>
</div>
</div>
</div>
<p></p></span></div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/04/mysql-970-vs-sysbench-on-small-server.html">MySQL 9.7.0 vs sysbench on a small server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Sysbench vs MySQL on a small server: another way to view the regressions</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mysql-on-small-server-n.html" />
      <id>https://smalldatum.blogspot.com/2026/04/sysbench-vs-mysql-on-small-server-n.html</id>
      <updated>2026-04-09T19:31:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This post provides another way to see the performance regressions in MySQL from versions 5.6 to 9.7. It complements what I shared in a recent post. The workload here is cached by InnoDB and my focus is on regressions from new CPU overheads. The good news is that there are few regressions after 8.0. The bad news is that there were many prior to that and these are unlikely to be undone.tl;drfor point queriesthere are large regressions from 5.6.51 to 5.7.44, 5.7.44 to 8.0.28 and 8.0.28 to 8.0.45there are few regressions from 8.0.45 to 8.4.8 to 9.7.0for range queries without aggregationthere are large regressions from 5.6.51 to 5.7.44 and 5.7.44 to 8.0.28there are mostly small regressions from 8.0.28 to 8.0.45, but scan has a large regressionthere are few regressions from 8.0.45 to 8.4.8 to 9.7.0for range queries with aggregationthere are large regressions from 5.6.51 to 5.7.44 with two improvementsthere are large regressions from 5.7.44 to 8.0.28there are small regressions from 8.0.28 to 8.0.45there are few regressions from 8.0.45 to 8.4.8 to 9.7.0for writesthere are large regressions from 5.6.51 to 5.7.44 and 5.7.44 to 8.0.28there are small regressions from 8.0.28 to 8.0.45there are few regressions from 8.0.45 to 8.4.8there are a few small regressions from 8.4.8 to 9.7.0Builds, configuration and hardwareI compiled MySQL from source for versions 5.6.51, 5.7.44, 8.0.28, 8.0.45, 8.4.8 and 9.7.0.The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it are here. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.The my.cnf files are here for 5.6, 5.7 and 8.4. I call these the z12a configs.For 9.7 I use the z13a config. It is as close as possible to z12a and adds two options for gtid-related features to undo a default config change that arrived in 9.6. All DBMS versions use the latin1 character set as explained here.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide tables below with relative QPS. When the relative QPS is &#62; 1 then some version is faster than the base version. When it is &#60; 1 then there might be a regression.  The relative QPS (rQPS) is:(QPS for some version) / (QPS for base version) Results: point queriesMySQL 5.6.51 gets from 1.18X to 1.61X more QPS than 9.7.0 on point queries. It is easier for me to write about this in terms of relative QPS (rQPS) which is as low as 0.62 for MySQL 9.7.0 vs 5.6.51. I define a basis point to mean a change of 0.01 in rQPS.Summary:from 5.6.51 to 9.7.0the median regression is a drop in rQPS of 27 basis pointsfrom 5.6.51 to 5.7.44the median regression is a drop in rQPS of 11 basis pointsfrom 5.7.44 to 8.0.28the median regression is a drop in rQPS of 25 basis pointsfrom 8.0.28 to 8.0.457 of 9 tests get more QPS with 8.0.452 tests have regressions where rQPS drops by ~6 basis pointsfrom 8.0.45 to 8.4.8there are few regressionsfrom 8.4.8 to 9.7.0there are few regressionsThis has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.the largest regression is an rQPS drop of 38 basis points for point-query. Compared to most of the other tests in this section, this query does less work in the storage engine which implies the regression is from code above the storage engine.the smallest regression is an rQPS drop of 15 basis points for random-points_range=1000. The regression for the same query with a shorter range (=10, =100) is larger. That implies, at least for this query, that the regression is for something above the storage engine (optimizer, parser, etc).the median regression is an rQPS drop of 27 basis points0.65    hot-points0.62    point-query0.72    points-covered-pk0.78    points-covered-si0.73    points-notcovered-pk0.76    points-notcovered-si0.85    random-points_range=10000.73    random-points_range=1000.66    random-points_range=10This has: (QPS for 5.7.44) / (QPS for 5.6.51)the largest regression is an rQPS drop of 14 basis points for hot-points.the next largest regression is an rQPS drop of 13 basis points for random-points with range=10. The regressions for that query are smaller when a larger range is used =100, =1000 and this implies the problem is above the storage engine. the median regression is an rQPS drop of 11 basis points0.86    hot-points0.90    point-query0.89    points-covered-pk0.90    points-covered-si0.89    points-notcovered-pk0.88    points-notcovered-si1.00    random-points_range=10000.89    random-points_range=1000.87    random-points_range=10This has: (QPS for 8.0.28) / (QPS for 5.7.44)the largest regression is an rQPS drop of 66 basis points for random-points with range=1000. The regression for that same query with smaller ranges (=10, =100) is smaller. This implies the problem is in the storage engine.the second largest regression is an rQPS drop of 35 basis points for hot-pointsthe median regression is an rQPS drop of 25 basis points0.65    hot-points0.82    point-query0.74    points-covered-pk0.75    points-covered-si0.76    points-notcovered-pk0.84    points-notcovered-si0.34    random-points_range=10000.75    random-points_range=1000.86    random-points_range=10This has: (QPS for 8.0.45) / (QPS for 8.0.28)at last, there are many improvements. Some are from a fix for bug 102037 which I found with help from sysbenchthe regressions, with rQPS drops by ~6 basis points, are for queries that do less work in the storage engine relative to the other tests in this section1.20    hot-points0.93    point-query1.13    points-covered-pk1.19    points-covered-si1.09    points-notcovered-pk1.04    points-notcovered-si2.48    random-points_range=10001.12    random-points_range=1000.94    random-points_range=10This has: (QPS for 8.4.8) / (QPS for 8.0.45)there are few regressions from 8.0.45 to 8.4.80.99    hot-points0.96    point-query0.99    points-covered-pk0.98    points-covered-si1.00    points-notcovered-pk0.99    points-notcovered-si1.00    random-points_range=10001.00    random-points_range=1000.98    random-points_range=10This has: (QPS for 9.7.0) / (QPS for 8.4.8)there are few regressions from 8.4.8 to 9.7.00.99    hot-points0.95    point-query0.99    points-covered-pk1.00    points-covered-si0.98    points-notcovered-pk0.99    points-notcovered-si1.00    random-points_range=10000.99    random-points_range=1000.96    random-points_range=10Results: range queries without aggregationMySQL 5.6.51 gets from 1.35X to 1.52X more QPS than 9.7.0 on range queries without aggregation. It is easier for me to write about this in terms of relative QPS (rQPS) which is as low as 0.66 for MySQL 9.7.0 vs 5.6.51. I define a basis point to mean a change of 0.01 in rQPS.Summary:from 5.6.51 to 9.7.0the median regression is drop in rQPS of 33 basis pointsfrom 5.6.51 to 5.7.44the median regression is a drop in rQPS of 16 basis pointsfrom 5.7.44 to 8.0.28the median regression is a drop in rQPS ~10 basis pointsfrom 8.0.28 to 8.0.45the median regression is a drop in rQPS of 5 basis pointsfrom 8.0.45 to 8.4.8there are few regressions from 8.0.45 to 8.4.8from 8.4.8 to 9.7.0there are few regressions from 8.4.8 to 9.7.0This has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.all tests have large regressions with an rQPS drop that ranges from 26 to 34 basis pointsthe median regression is an rQPS drop of 33 basis points0.66    range-covered-pk0.67    range-covered-si0.66    range-notcovered-pk0.74    range-notcovered-si0.67    scanThis has: (QPS for 5.7.44) / (QPS for 5.6.51)all tests have large regressions with an rQPS drop that ranges from 12 to 17 basis pointsthe median regression is an rQPS drop of 16 basis points0.85    range-covered-pk0.84    range-covered-si0.84    range-notcovered-pk0.88    range-notcovered-si0.83    scanThis has: (QPS for 8.0.28) / (QPS for 5.7.44)4 of 5 tests have regressions with an rQPS drop that ranges from 10 to 14 basis pointsthe median regression is ~10 basis pointsrQPS improves for the scan test0.86    range-covered-pk0.89    range-covered-si0.90    range-notcovered-pk0.90    range-notcovered-si1.04    scanThis has: (QPS for 8.0.45) / (QPS for 8.0.28)all tests are slower in 8.0.45 than 8.0.28, but the regression for 3 of 5 is</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mysql-on-small-server-n.html">Sysbench vs MySQL on a small server: another way to view the regressions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This post provides another way to see the performance regressions in MySQL from versions 5.6 to 9.7. It complements what I shared in a&nbsp;<a href="https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html">recent post</a>. The workload here is cached by InnoDB and my focus is on regressions from new CPU overheads.&nbsp;</p>
<p>The good news is that there are few regressions after 8.0. The bad news is that there were many prior to that and these are unlikely to be undone.</p>

<ul></ul>

<p>tl;dr</p>

<ul>
<li>for point queries</li>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44, 5.7.44 to 8.0.28 and 8.0.28 to 8.0.45</li>
<li>there are few regressions from 8.0.45 to 8.4.8 to 9.7.0</li>
</ul>
<li>for range queries without aggregation</li>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44 and 5.7.44 to 8.0.28</li>
<li>there are mostly small regressions from 8.0.28 to 8.0.45, but scan has a large regression</li>
<li>there are few regressions from 8.0.45 to 8.4.8 to 9.7.0</li>
</ul>
<li>for range queries with aggregation</li>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44 with two improvements</li>
<li>there are large regressions from 5.7.44 to 8.0.28</li>
<li>there are small regressions from 8.0.28 to 8.0.45</li>
<li>there are few regressions from 8.0.45 to 8.4.8 to 9.7.0</li>
</ul>
<li>for writes</li>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44 and 5.7.44 to 8.0.28</li>
<li>there are small regressions from 8.0.28 to 8.0.45</li>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
<li>there are a few small regressions from 8.4.8 to 9.7.0</li>
</ul>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>

<div></div>

<div>I compiled MySQL from source for versions 5.6.51, 5.7.44, 8.0.28, 8.0.45, 8.4.8 and 9.7.0.</div>
</div>
<p>The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2022/10/small-servers-for-performance-testing-v4.html">are here</a>. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.</p>
<p>The my.cnf files are here for&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my5651_rel_o2nofp/etc/my.cnf.cz12a_c8r32">5.6</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my5744_rel_o2nofp/etc/my.cnf.cz12a_c8r32">5.7</a>&nbsp;and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8406_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.4</a>. I call these the z12a configs.</p>
<p>For 9.7 I use the <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13a_c8r32">z13a</a>&nbsp;config. It is as close as possible to z12a and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz13a_c8r32#L76-L77">adds two options</a> for gtid-related features to undo a default config change that arrived in 9.6.&nbsp;</p>
<p>All DBMS versions use the latin1 character set as <a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">explained here</a>.</p>
<p><b>Benchmark</b></p>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.</div>
<div>The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div>I provide tables below with relative QPS.&nbsp;<span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than the</span><span>&nbsp;</span><i>base version.</i><span>&nbsp;When it is &lt; 1 then there might be a regression.&nbsp;&nbsp;</span><span>The relative QPS (<b>rQPS</b>) is:</span></div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for base version)<span>&nbsp;</span></p></blockquote>
</div>
<div><b>Results: point queries</b></div>
<div>
<div></div>
<div><span>MySQL 5.6.51 gets from 1.18X to 1.61X more QPS than 9.7.0 on point queries. It is easier for me to write about this in terms of relative QPS (rQPS) which is as low as 0.62 for MySQL 9.7.0 vs 5.6.51. I define a <i><b>basis point</b></i> to mean a change of 0.01 in rQPS.</span></div>
<div><span><br></span></div>
<div><span>Summary:</span></div>
<div>
<ul>
<li><span>from 5.6.51 to 9.7.0</span></li>
<ul>
<li><span>the median regression is a drop in rQPS of 27 basis points</span></li>
</ul>
<li><span>from 5.6.51 to 5.7.44</span></li>
<ul>
<li>the median regression is a drop in rQPS of 11 basis points</li>
</ul>
<li><span>from 5.7.44 to 8.0.28</span></li>
<ul>
<li>the median regression is a drop in rQPS of 25 basis points</li>
</ul>
<li><span>from 8.0.28 to 8.0.45</span></li>
<ul>
<li>7 of 9 tests get more QPS with 8.0.45</li>
<li>2 tests have regressions where rQPS drops by ~6 basis points</li>
</ul>
<li><span>from 8.0.45 to 8.4.8</span></li>
<ul>
<li>there are few regressions</li>
</ul>
<li><span>from 8.4.8 to 9.7.0</span></li>
<ul>
<li>there are few regressions</li>
</ul>
</ul>
</div>
<div>
<div>
<div>This has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.</div>
<div>
<ul>
<li>the largest regression is an rQPS drop of 38 basis points for point-query. Compared to most of the other tests in this section, this query does less work in the storage engine which implies the regression is from code above the storage engine.</li>
<li>the smallest regression is an rQPS drop of 15 basis points for random-points_range=1000. The regression for the same query with a shorter range (=10, =100) is larger. That implies, at least for this query, that the regression is for something above the storage engine (optimizer, parser, etc).</li>
<li>the median regression is an rQPS drop of 27 basis points</li>
</ul>
</div>
<div>
<div><span>0.65&nbsp; &nbsp; hot-points</span></div>
<div><span><span>0.62</span>&nbsp; &nbsp; point-query</span></div>
<div><span>0.72&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.78&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.73&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.76&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span><span>0.85</span>&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.73&nbsp; &nbsp; random-points_range=100</span></div>
<div><span><span>0.66</span>&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
</div>
<div>This has: (QPS for 5.7.44) / (QPS for 5.6.51)</div>
<div>
<ul>
<li>the largest regression is an rQPS drop of 14 basis points for hot-points.</li>
<li>the next largest regression is an rQPS drop of 13 basis points for random-points with range=10. The regressions for that query are smaller when a larger range is used =100, =1000 and this implies the problem is above the storage engine.&nbsp;</li>
<li>the median regression is an rQPS drop of 11 basis points</li>
</ul>
</div>
<div>
<div><span><span>0.86</span>&nbsp; &nbsp; hot-points</span></div>
<div><span>0.90&nbsp; &nbsp; point-query</span></div>
<div><span>0.89&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.90&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.89&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.88&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span><span>1.00</span>&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.89&nbsp; &nbsp; random-points_range=100</span></div>
<div><span><span>0.87</span>&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.28) / (QPS for 5.7.44)</div>
<div>
<ul>
<li>the largest regression is an rQPS drop of 66 basis points for random-points with range=1000. The regression for that same query with smaller ranges (=10, =100) is smaller. This implies the problem is in the storage engine.</li>
<li>the second largest regression is an rQPS drop of 35 basis points for hot-points</li>
<li>the median regression is an rQPS drop of 25 basis points</li>
</ul>
</div>
<div>
<div><span><span>0.65</span>&nbsp; &nbsp; hot-points</span></div>
<div><span>0.82&nbsp; &nbsp; point-query</span></div>
<div><span>0.74&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.75&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.76&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.84&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span><span>0.34</span>&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.75&nbsp; &nbsp; random-points_range=100</span></div>
<div><span><span>0.86</span>&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.45) / (QPS for 8.0.28)</div>
<div>
<ul>
<li>at last, there are many improvements. Some are from a fix for <a href="https://bugs.mysql.com/bug.php?id=102037">bug 102037</a> which I found with help from sysbench</li>
<li>the regressions, with rQPS drops by ~6 basis points, are for queries that do less work in the storage engine relative to the other tests in this section</li>
</ul>
</div>
<div>
<div><span><span>1.20</span>&nbsp; &nbsp; hot-points</span></div>
<div><span><span>0.93</span>&nbsp; &nbsp; point-query</span></div>
<div><span><span>1.13</span>&nbsp; &nbsp; points-covered-pk</span></div>
<div><span><span>1.19</span>&nbsp; &nbsp; points-covered-si</span></div>
<div><span><span>1.09</span>&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span><span>1.04</span>&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span><span>2.48</span>&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span><span>1.12</span>&nbsp; &nbsp; random-points_range=100</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; random-points_range=10</span></div>
<div></div>
<div>This has: (QPS for 8.4.8) / (QPS for 8.0.45)</div>
<div>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
</div>
</div>
<div>
<div><span>0.99&nbsp; &nbsp; hot-points</span></div>
<div><span>0.96&nbsp; &nbsp; point-query</span></div>
<div><span>0.99&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.98&nbsp; &nbsp; points-covered-si</span></div>
<div><span>1.00&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.99&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span>1.00&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>1.00&nbsp; &nbsp; random-points_range=100</span></div>
<div><span>0.98&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
<div><span>This has: (QPS for 9.7.0) / (QPS for 8.4.8)</span></div>
<div>
<ul>
<li>there are few regressions from 8.4.8 to 9.7.0</li>
</ul>
</div>
<div><span><span>0.99&nbsp; &nbsp; hot-points</span></span></div>
<div><span><span>
<div>0.95&nbsp; &nbsp; point-query</div>
<div>0.99&nbsp; &nbsp; points-covered-pk</div>
<div>1.00&nbsp; &nbsp; points-covered-si</div>
<div>0.98&nbsp; &nbsp; points-notcovered-pk</div>
<div>0.99&nbsp; &nbsp; points-notcovered-si</div>
<div>1.00&nbsp; &nbsp; random-points_range=1000</div>
<div>0.99&nbsp; &nbsp; random-points_range=100</div>
<div>0.96&nbsp; &nbsp; random-points_range=10</div>
<p></p></span></span></div>
</div>
<div></div>
<div>
<div><b>Results: range queries without aggregation</b></div>
<div></div>
<div>MySQL 5.6.51 gets from 1.35X to 1.52X more QPS than 9.7.0 on range queries without aggregation. It is easier for me to write about this in terms of relative QPS (rQPS) which is as low as 0.66 for MySQL 9.7.0 vs 5.6.51. I define a&nbsp;<i><b>basis point</b></i>&nbsp;to mean a change of 0.01 in rQPS.</div>
<div>
<div><span>Summary:</span></div>
<div>
<ul>
<li><span>from 5.6.51 to 9.7.0</span></li>
<ul>
<li>the median regression is drop in rQPS of 33 basis points</li>
</ul>
<li><span>from 5.6.51 to 5.7.44</span></li>
<ul>
<li>the median regression is a drop in rQPS of 16 basis points</li>
</ul>
<li><span>from 5.7.44 to 8.0.28</span></li>
<ul>
<li>the median regression is a drop in rQPS ~10 basis points</li>
</ul>
<li><span>from 8.0.28 to 8.0.45</span></li>
<ul>
<li>the median regression is a drop in rQPS of 5 basis points</li>
</ul>
<li><span>from 8.0.45 to 8.4.8</span></li>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
<li><span>from 8.4.8 to 9.7.0</span></li>
<ul>
<li>there are few regressions from 8.4.8 to 9.7.0</li>
</ul>
</ul>
</div>
</div>
<div><span>This has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.</span></div>
<div>
<ul>
<li>all tests have large regressions with an rQPS drop that ranges from 26 to 34 basis points</li>
<li>the median regression is an rQPS drop of 33 basis points</li>
</ul>
</div>
<div><span>0.66</span><span>&nbsp; &nbsp; range-covered-pk</span></div>
<div>
<div><span><span>0.67</span>&nbsp; &nbsp; range-covered-si</span></div>
<div><span><span>0.66</span>&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span><span>0.74</span>&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span><span>0.67</span>&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>This has: (QPS for 5.7.44) / (QPS for 5.6.51)</div>
<div>
<ul>
<li>all tests have large regressions with an rQPS drop that ranges from 12 to 17 basis points</li>
<li>the median regression is an rQPS drop of 16 basis points</li>
</ul>
</div>
<div>
<div><span><span>0.85&nbsp; &nbsp; range-covered-pk</span></span></div>
<div><span><span>0.84&nbsp; &nbsp; range-covered-si</span></span></div>
<div><span><span>0.84&nbsp; &nbsp; range-notcovered-pk</span></span></div>
<div><span><span>0.88</span><span>&nbsp; &nbsp; range-notcovered-si</span></span></div>
<div><span><span>0.83</span><span>&nbsp; &nbsp; scan</span></span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.28) / (QPS for 5.7.44)</div>
<div>
<ul>
<li>4 of 5 tests have regressions with an rQPS drop that ranges from 10 to 14 basis points</li>
<li>the median regression is ~10 basis points</li>
<li>rQPS improves for the scan test</li>
</ul>
</div>
<div><span><span>0.86</span>&nbsp; &nbsp; range-covered-pk</span></div>
<div>
<div><span>0.89&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.90&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.90&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span><span>1.04</span>&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.45) / (QPS for 8.0.28)</div>
<div>
<ul>
<li>all tests are slower in 8.0.45 than 8.0.28, but the regression for 3 of 5 is &lt;= 5 basis points</li>
<li>rQPS in the scan test drops by 21 basis points</li>
<li>the median regression is an rQPS drop of 5 basis points</li>
</ul>
</div>
<div>
<div><span>0.96&nbsp; &nbsp; range-covered-pk</span></div>
<div><span>0.95&nbsp; &nbsp; range-covered-si</span></div>
<div><span><span>0.91</span>&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.96&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span><span>0.79</span>&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.4.8) / (QPS for 8.0.45)</div>
<div>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
</div>
<div><span>0.95&nbsp; &nbsp; range-covered-pk</span></div>
<div>
<div><span>0.95&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.98&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.99&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span>0.98&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>This has: (QPS for 9.7.0) / (QPS for 8.4.8)</div>
<div>
<ul>
<li>there are few regressions from 8.4.8 to 9.7.0</li>
</ul>
</div>
<div><span>0.99&nbsp; &nbsp; range-covered-pk</span></div>
<div>
<div><span>0.99&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.99&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.98&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span>1.00&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
</div>
<div>
<div><b>Results: range queries with aggregation</b></div>
<div></div>
<div>
<div>
<div><span>Summary:</span></div>
<div>
<ul>
<li><span>from 5.6.51 to 9.7.0 rQPS</span></li>
<ul>
<li>the median result is a drop in rQPS of ~30 basis points</li>
</ul>
<li><span>from 5.6.51 to 5.7.44</span></li>
<ul>
<li>the median result is a drop in rQPS of ~10 basis points</li>
</ul>
<li><span>from 5.7.44 to 8.0.28</span></li>
<ul>
<li>the median result is a drop in rQPS of ~12 basis points</li>
</ul>
<li><span>from 8.0.28 to 8.0.45</span></li>
<ul>
<li>the median result is an rQPS drop of 5 basis points</li>
</ul>
<li><span>from 8.0.45 to 8.4.8</span></li>
<ul>
<li><span>there are few regressions from 8.0.45 to 8.4.8</span></li>
</ul>
<li><span>from 8.4.8 to 9.7.0</span></li>
<ul>
<li><span>there are few regressions from 8.4.8 to 9.7.0</span></li>
</ul>
</ul>
</div>
</div>
<div>This has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.</div>
<div>
<ul>
<li>the median result is a drop in rQPS of ~30 basis points</li>
<li>rQPS for the read-only-distinct test improves by 25 basis point</li>
</ul>
</div>
<div>
<div><span>0.67&nbsp; &nbsp; read-only-count</span></div>
<div><span><span>1.25</span>&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.75&nbsp; &nbsp; read-only-order</span></div>
<div><span>1.02&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.74&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.66&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.69&nbsp; &nbsp; read-only-simple</span></div>
<div><span><span>0.66</span>&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>This has: (QPS for 5.7.44) / (QPS for 5.6.51)</div>
<div>
<ul>
<li>the median result is an rQPS drop of ~10 basis points</li>
<li>rQPS improves by 45 basis points for read-only-distinct and by 23 basis points for read-only with the largest range (=10000)</li>
</ul>
</div>
<div><span>0.86&nbsp; &nbsp; read-only-count</span></div>
<div>
<div><span><span>1.45</span>&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.93&nbsp; &nbsp; read-only-order</span></div>
<div><span><span>1.23</span>&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.96&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.88&nbsp; &nbsp; read-only_range=10</span></div>
<div><span><span>0.85</span>&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.86&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.28) / (QPS for 5.7.44)</div>
<div>
<ul>
<li>the median result is an rQPS drop of ~12 basis points</li>
</ul>
</div>
<div>
<div><span>0.91&nbsp; &nbsp; read-only-count</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.89&nbsp; &nbsp; read-only-order</span></div>
<div><span><span>0.86</span>&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.87&nbsp; &nbsp; read-only_range=100</span></div>
<div><span><span>0.85</span>&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.90&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.87&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.45) / (QPS for 8.0.28)</div>
<div>
<ul>
<li>the median result is an rQPS drop of 5 basis points</li>
</ul>
</div>
<div>
<div><span><span>0.89</span>&nbsp; &nbsp; read-only-count</span></div>
<div><span>0.95&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.95&nbsp; &nbsp; read-only-order</span></div>
<div><span><span>0.97</span>&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.94&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.95&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.93&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.93&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.4.8) / (QPS for 8.0.45)</div>
<div>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
</div>
<div>
<div><span>0.99&nbsp; &nbsp; read-only-count</span></div>
<div><span>0.98&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.99&nbsp; &nbsp; read-only-order</span></div>
<div><span>1.00&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.98&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.97&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.97&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.98&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>This has: (QPS for 9.7.0) / (QPS for 8.4.8)</div>
<div>
<ul>
<li><span>there are few regressions from 8.4.8 to 9.7.0</span></li>
</ul>
<div>
<div><span>0.97&nbsp; &nbsp; read-only-count</span></div>
<div><span>0.98&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.96&nbsp; &nbsp; read-only-order</span></div>
<div><span>0.99&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.97&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.99&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.97&nbsp; &nbsp; read-only-sum</span></div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results: writes</b></div>
<div></div>
<div>
<div>
<div><span>Summary:</span></div>
<div>
<ul>
<li><span>from 5.6.51 to 9.7.0 rQPS&nbsp;</span></li>
<ul>
<li><span>the median result is a drop in rQPS of ~33 basis points</span></li>
</ul>
<li><span>from 5.6.51 to 5.7.44</span></li>
<ul>
<li>the median result is an rQPS drop of ~13 basis points</li>
</ul>
<li><span>from 5.7.44 to 8.0.28</span></li>
<ul>
<li>the median result is an rQPS drop of ~18 basis points</li>
</ul>
<li><span>from 8.0.28 to 8.0.45</span></li>
<ul>
<li>the median result is an rQPS drop of 9 basis points</li>
</ul>
<li><span>from 8.0.45 to 8.4.8</span></li>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
<li><span>from 8.4.8 to 9.7.0</span></li>
<ul>
<li>the median result is an rQPS drop of 4 basis points</li>
</ul>
</ul>
</div>
</div>
<div><span>This has (QPS for 9.7.0) / (QPS for 5.6.51) and is followed by tables that show the difference between the latest point release in adjacent versions.</span></div>
<div>
<ul>
<li><span>the median result is an rQPS drop of ~33 basis points</span></li>
</ul>
</div>
<div><span>0.56&nbsp; &nbsp; delete</span></div>
<div>
<div><span><span>0.54</span>&nbsp; &nbsp; insert</span></div>
<div><span>0.72&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.66&nbsp; &nbsp; read-write_range=10</span></div>
<div><span><span>0.88</span>&nbsp; &nbsp; update-index</span></div>
<div><span>0.74&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.60&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.58&nbsp; &nbsp; update-one</span></div>
<div><span>0.60&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.67&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>This has: (QPS for 5.7.44) / (QPS for 5.6.51)</div>
<div>
<ul>
<li>the median result is an rQPS drop of ~13 basis points</li>
<li>rQPS improves by 21 basis points for update-index and by 5 basis points for update-inlist</li>
</ul>
</div>
<div>
<div><span>0.82&nbsp; &nbsp; delete</span></div>
<div><span><span>0.80</span>&nbsp; &nbsp; insert</span></div>
<div><span>0.94&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.88&nbsp; &nbsp; read-write_range=10</span></div>
<div><span><span>1.21</span>&nbsp; &nbsp; update-index</span></div>
<div><span>1.05&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.86&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.85&nbsp; &nbsp; update-one</span></div>
<div><span>0.86&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.94&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.28) / (QPS for 5.7.44)</div>
<div>
<ul>
<li>the median result is an rQPS drop of ~18 basis points</li>
</ul>
</div>
<div>
<div><span>0.80&nbsp; &nbsp; delete</span></div>
<div><span><span>0.77</span>&nbsp; &nbsp; insert</span></div>
<div><span>0.87&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.85&nbsp; &nbsp; read-write_range=10</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; update-index</span></div>
<div><span>0.79&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.81&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.80&nbsp; &nbsp; update-one</span></div>
<div><span>0.81&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.83&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.0.45) / (QPS for 8.0.28)</div>
<div>
<ul>
<li>the median result is an rQPS drop of 9 basis points</li>
</ul>
</div>
<div>
<div><span>0.91&nbsp; &nbsp; delete</span></div>
<div><span>0.90&nbsp; &nbsp; insert</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; read-write_range=100</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; read-write_range=10</span></div>
<div><span><span>0.80</span>&nbsp; &nbsp; update-index</span></div>
<div><span>0.92&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.91&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.92&nbsp; &nbsp; update-one</span></div>
<div><span>0.91&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.89&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>This has: (QPS for 8.4.8) / (QPS for 8.0.45)</div>
<div>
<ul>
<li>there are few regressions from 8.0.45 to 8.4.8</li>
</ul>
</div>
<div>
<div><span>0.98&nbsp; &nbsp; delete</span></div>
<div><span>0.98&nbsp; &nbsp; insert</span></div>
<div><span>0.98&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.98&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>0.99&nbsp; &nbsp; update-index</span></div>
<div><span>0.99&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.99&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.99&nbsp; &nbsp; update-one</span></div>
<div><span>0.99&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.99&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>This has: (QPS for 9.7.0) / (QPS for 8.4.8)</div>
<div>
<ul>
<li>the median result is an rQPS drop of 4 basis points</li>
</ul>
<div>
<div><span><span>0.95</span>&nbsp; &nbsp; delete</span></div>
<div><span>1.00&nbsp; &nbsp; insert</span></div>
<div><span>0.97&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>0.97&nbsp; &nbsp; update-index</span></div>
<div><span>0.97&nbsp; &nbsp; update-inlist</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; update-nonindex</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; update-one</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.97&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
</div>
</div>
</div>
</div>
</div>
<p></p></span></div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mysql-on-small-server-n.html">Sysbench vs MySQL on a small server: another way to view the regressions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 32-core server</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to_8.html" />
      <id>https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to_8.html</id>
      <updated>2026-04-08T21:41:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for MariaDB versions 10.2 through 13.0 vs the Insert Benchmark on a 32-core server. The goal is to see how performance changes over time to find regressions or highlight improvements. My previous post has results from a 24-core server.  Differences between these servers include:RAM - 32-core server has 128G, 24-core server has 64Gfsync latency - 32-core has an SSD with high fsync latency, while it is fast on the 24-core serversockets - 32-core server has 1 CPU socket, 24-core server has twoCPU maker  - 32-core server uses an AMD Threadripper, 24-core server has an Intel Xeoncores - obviously it is 32 vs 24, Intel HT and AMD SMT are disabledThe results here for modern MariaDB are great for the CPU-bound workload but not for the IO-bound workload.. They were great for both on the 24-core server. The regressions are likely caused by the extra fsync calls that are done because the equivalent of equivalent of innodb_flush_method =O_DIRECT_NO_FSYNC was lost with the new options that replace innodb_flush_method starting in MariaDB 11.4. I created MDEV-33545 to request support for it. The workaround is to use an SSD that doesn\'t have high fsync latency, which is always a good idea, but not always possible.tl;drfor a CPU-bound workloadthe write-heavy steps are much faster in 13.0.0 than 10.2.30the read-heavy steps get similar QPS in 13.0.0 and 10.2.30this is similar to the results on the 24-core serverfor an IO-bound workloadthe initial load (l.i0) is much faster in 13.0.0 than 10.2.30the random write step (l.i1) is slower in 13.0.0 than 10.2.30 because fsync latencythe range query step (qr100) gets similar QPS in 13.0.0 and 10.2.30the point query step (qp100) is much slower in 13.0.0 than 10.2.30 because fsync latencyBuilds, configuration and hardwareI compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.The server has 24-cores, 2-sockets and 64G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. Intel HT is disabled.The my.cnf files are here for: 10.2, 10.3, 10.4, 10.5, 10.6, 10.11, 11.4, 11.8, 12.3 and 13.0. For MariaDB 10.11.16 I used both the z12a config, as I did for all 10.x releases, and also used the z12b config. The difference is that the z12a config uses innodb_flush_method =O_DIRECT_NO_FSYNC while the z12b config uses =O_DIRECT. And the z12b config is closer to the configs used for MariaDB because with the new variables that replaced innodb_flush_method, we lose support for the equivalent of =O_DIRECT_NO_FSYNC.And I write about this because the extra fsync calls that are done when the z12b config is used have a large impact on throughput on a server that uses an SSD with high fsync latency, which causes perf regressions for all DBMS versions that used the z12b config -- 10.11.16, 11.4, 11.8, 12.3 and 13.0.The BenchmarkThe benchmark is explained here and is run with 12 clients with a table per client. I repeated it with two workloads:CPU-boundthe values for X, Y, Z are 10M, 16M, 4MIO-boundthe values for X, Y, Z are 300M, 4M, 1MThe point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.The benchmark steps are:l.i0insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: overviewThe performance reports are here for the CPU-bound and IO-bound workloads.The summary sections from the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is MariaDB 10.2.30.When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000This statement doesn&#039;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with red for = 1.05 and grey for values between 0.95 and 1.05.Results: CPU-boundThe performance summary is here.The summary per benchmark step, where rQPS means relative QPS.l.i0MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.47CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see here)l.xI will ignore thisl.i1, l.i2MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.50 and 1.37CPU per write (cpupq) is much smaller in 13.0.0 than 10.2.30 (see here)qr100, qr500, qr1000MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see here)qp100, qp500, qp1000MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see here)Results: IO-boundThe performance summary is here.The summary per benchmark step, where rQPS means relative QPS.l.i0MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.25CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see here)l.xI will ignore thisl.i1, l.i2MariaDB 13.0.0 is slower than 10.2.30 for l.i1, rQPS is 0.68MariaDB 13.0.0 is faster than 10.2.30 for l.i2, rQPS is 1.31. I suspect it is faster on l.i2 because it inherits less MVCC GC debt from l.i1 because it was slower on l.i1. So I won&#039;t celebrate this result and will focus on l.i1.From the normalized vmstat and iostat metrics I don&#039;t see anything obvious. But I do see a reduction in storage reads/s (rps) and storage read MB/s (rMBps). And this reduction starts in 10.11.16 with the z12b config and continues to 13.0.0. This does not occur on the earlier releases that are eable to use the z12a config. So I am curious if the extra fsyncs are the root cause.From the iostat summary for l.i1 that includes average values for all iostat columns, and these are not divided by QPS, what I see a much higher rate for fsyncs (f/s) as well as an increase in read latency. For MariaDB 10.11.16 the value for r_await is 0.640 with the z12a config vs 0.888 with the z12b config. I assume that more frequent fsync calls hurt read latency. The iostat results don&#039;t look great for either the z12a or z12b config and the real solution is to avoid using an SSD with high fsync latency, but that isn&#039;t always possible.qr100, qr500, qr1000no DBMS versions were able to sustain the target write rate for qr500 or qr1000 so I ignore them. This server needs more IOPs capacity -- a second SSD, and both SSDs needs power loss protection to reduce fsync latency.MariaDB 13.0.0 and 10.2.30 have similar performance, rQPS is 0.96. The qr100 step for MariaDB 13.0.0 might not suffer from fsync latency like the qp100 step because it does less read IO per query than qp100 (see rpq here).qp100, qp500, qp1000no DBMS versions were able to sustain the target write rate for qp500 or qp1000 so I ignore them. This server needs more IOPs capacity -- a second SSD, and both SSDs needs power loss protection to reduce fsync latency.MariaDB 13.0.0 is slower than 10.2.30, rQPS is 0.62From the normalized vmstat and iostat metrics there are increases in CPU per query (cpupq) and storage reads per query (rpq) for all DBMS versions that use the z12b config (see here).From the iostat summary for qp100 that includes average values for all iostat columns the read latency increases for all DBMS versions that use the z12b config. I blame interference from the extra fsync calls.</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to_8.html">The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 32-core server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for MariaDB versions 10.2 through 13.0 vs the&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">Insert Benchmark</a>&nbsp;on a 32-core server. The goal is to see how performance changes over time to find regressions or highlight improvements. My <a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html">previous post</a> has results from a 24-core server.&nbsp; Differences between these servers include:</p>
<ul>
<li>RAM &ndash; 32-core server has 128G, 24-core server has 64G</li>
<li>fsync latency &ndash; 32-core has an SSD with <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>, while it is fast on the 24-core server</li>
<li>sockets &ndash; 32-core server has 1 CPU socket, 24-core server has two</li>
<li>CPU maker&nbsp; &ndash; 32-core server uses an AMD Threadripper, 24-core server has an Intel Xeon</li>
<li>cores &ndash; obviously it is 32 vs 24, Intel HT and AMD SMT are disabled</li>
</ul>
<p>The results here for modern MariaDB are great for the CPU-bound workload but not for the IO-bound workload.. They were great for both on the 24-core server. The regressions are likely caused by the extra fsync calls that are done because the equivalent of equivalent of innodb_flush_method =O_DIRECT_NO_FSYNC was lost with the new options that replace innodb_flush_method starting in MariaDB 11.4. I created <a href="https://jira.mariadb.org/browse/MDEV-33545">MDEV-33545</a> to request support for it. The workaround is to use an SSD that doesn&rsquo;t have high fsync latency, which is always a good idea, but not always possible.</p>
<p>tl;dr</p>

<ul>
<li>for a CPU-bound workload</li>
<ul>
<li>the write-heavy steps are much faster in 13.0.0 than 10.2.30</li>
<li>the read-heavy steps get similar QPS in 13.0.0 and 10.2.30</li>
<li>this is similar to the <a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html">results on the 24-core server</a></li>
</ul>
<li>for an IO-bound workload</li>
<ul>
<li>the initial load (l.i0) is much faster in 13.0.0 than 10.2.30</li>
<li>the random write step (l.i1) is slower in 13.0.0 than 10.2.30 because fsync latency</li>
<li>the range query step (qr100) gets similar QPS in 13.0.0 and 10.2.30</li>
<li>the point query step (qp100) is much slower in 13.0.0 than 10.2.30 because fsync latency</li>
</ul>
</ul>
<div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.</div>
<div></div>
<div>The server has 24-cores, 2-sockets and 64G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. Intel HT is disabled.</div>
</div>
<div></div>
<div>The my.cnf files are here for:&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100244_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.2</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100339_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.3</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100433_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.4</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100524_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.5</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100617_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.6</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma101107_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.11</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma110401_rel_withdbg/etc/my.cnf.cz12b_c32r128">11.4</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma110803_rel_withdbg/etc/my.cnf.cz12b_c32r128">11.8</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_c32r128">12.3 and 13.0</a>.&nbsp;</div>
<div></div>
<div>For MariaDB 10.11.16 I used both the <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma101107_rel_withdbg/etc/my.cnf.cz12a_c32r128">z12a</a> config, as I did for all 10.x releases, and also used the <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma101107_rel_withdbg/etc/my.cnf.cz12b_c32r128">z12b</a> config. The difference is that the z12a config uses&nbsp;innodb_flush_method =O_DIRECT_NO_FSYNC while the z12b config uses =O_DIRECT. And the z12b config is closer to the configs used for MariaDB because with the new variables that replaced innodb_flush_method, we lose support for the equivalent of =O_DIRECT_NO_FSYNC.
<p>And I write about this because the extra fsync calls that are done when the z12b config is used have a large impact on throughput on a server that uses an SSD with <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>, which causes perf regressions for all DBMS versions that used the z12b config &mdash; 10.11.16, 11.4, 11.8, 12.3 and 13.0.</p></div>
<div></div>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>&nbsp;and&nbsp;is run with 12 clients with a table per client. I repeated it with two workloads:</div>
<div>
<ul>
<li>CPU-bound</li>
<ul>
<li>the values for X, Y, Z are 10M, 16M, 4M</li>
</ul>
<li>IO-bound</li>
<ul>
<li>the values for X, Y, Z are 300M, 4M, 1M</li>
</ul>
</ul>
<div>The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.</div>
</div>
<div></div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.</li>
<li>Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div>
<div><b>Results: overview</b></div>
<div>
<div><b><br></b></div>
<div>The performance reports are here for the&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html">CPU-bound</a>&nbsp;and&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html">IO-bound</a>&nbsp;workloads.</div>
</div>
</div>
<div></div>
<div>The summary sections from&nbsp;the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.</div>
<div>
<div></div>
<div>Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is MariaDB 10.2.30.
<p>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</p></div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>This statement doesn&rsquo;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with&nbsp;<span>red</span>&nbsp;for &lt;= 0.95,&nbsp;<span>green</span>&nbsp;for &gt;= 1.05 and&nbsp;<span>grey</span>&nbsp;for values between 0.95 and 1.05.</div>
</div>
</div>
</div>
<div>
<div></div>
</div>
</div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The performance summary&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html#summary">is here</a>.
<p>The summary per benchmark step, where rQPS means relative QPS.</p></div>
</div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30, <span>rQPS is 1.47</span></li>
<li>CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html#l.i0.metrics">here</a>)</li>
</ul>
<li>l.x</li>
<ul>
<li>I will ignore this</li>
</ul>
<li>l.i1, l.i2</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30, <span>rQPS is 1.50 and 1.37</span></li>
<li><span>CPU per write (cpupq) is much smaller in 13.0.0 than 10.2.30 (see <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html#l.i1.metrics">here</a>)</span></li>
</ul>
<li>qr100, qr500, qr1000</li>
<ul>
<li>MariaDB 13.0.0 and 10.2.30 have similar QPS (<span>rQPS is close to 1.0</span>)</li>
<li>CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html#qr100.L1.metrics">here</a>)</li>
</ul>
<li>qp100, qp500, qp1000</li>
<ul>
<li>MariaDB 13.0.0 and 10.2.30 have similar QPS (<span>rQPS is close to 1.0</span>)</li>
<li>CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.mem.10m.20m.1800s.12u.maria/all.html#qp100.L2.metrics">here</a>)</li>
</ul>
</ul>
</div>
<div></div>
<div>
<div>
<div><b>Results: IO-bound</b></div>
<div></div>
<div>The performance summary&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#summary">is here</a>.
<p>The summary per benchmark step, where rQPS means relative QPS.</p></div>
</div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30,&nbsp;<span>rQPS is 1.25</span></li>
<li>CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#l.i0.metrics">here</a>)</li>
</ul>
<li>l.x</li>
<ul>
<li>I will ignore this</li>
</ul>
<li>l.i1, l.i2</li>
<ul>
<li>MariaDB 13.0.0 is slower than 10.2.30 for l.i1, <span>rQPS is 0.68</span></li>
<li>MariaDB 13.0.0 is faster than 10.2.30 for l.i2, rQPS is <span>1.31</span>. I suspect it is faster on l.i2 because it inherits less MVCC GC debt from l.i1 because it was slower on l.i1. So I won&rsquo;t celebrate this result and will focus on l.i1.</li>
<li>From the <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#l.i1.metrics">normalized vmstat and iostat metrics</a> I don&rsquo;t see anything obvious. But I do see a reduction in storage reads/s (rps) and storage read MB/s (rMBps). And this reduction starts in 10.11.16 with the z12b config and continues to 13.0.0. This does not occur on the earlier releases that are eable to use the z12a config. So I am curious if the extra fsyncs are the root cause.</li>
<li>From the <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#l.i1.graph">iostat summary for l.i1</a> that includes average values for all iostat columns, and these are not divided by QPS, what I see a much higher rate for fsyncs (f/s) as well as an increase in read latency. For MariaDB 10.11.16 the value for r_await is 0.640 with the z12a config vs 0.888 with the z12b config. I assume that more frequent fsync calls hurt read latency. The iostat results don&rsquo;t look great for either the z12a or z12b config and the real solution is to avoid using an SSD with high fsync latency, but that isn&rsquo;t always possible.</li>
</ul>
<li>qr100, qr500, qr1000</li>
<ul>
<li>no DBMS versions were able to sustain the target write rate for qr500 or qr1000 so I ignore them. This server needs more IOPs capacity &mdash; a second SSD, and both SSDs needs power loss protection to reduce fsync latency.</li>
<li>MariaDB 13.0.0 and 10.2.30 have similar performance, <span>rQPS is 0.96</span><span>.&nbsp;</span>The qr100 step for MariaDB 13.0.0 might not suffer from fsync latency like the qp100 step because it does less read IO per query than qp100 (see rpq&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#qr100.L1.metrics">here</a>).</li>
</ul>
<li>qp100, qp500, qp1000</li>
<ul>
<li>no DBMS versions were able to sustain the target write rate for qp500 or qp1000 so I ignore them. This server needs more IOPs capacity &mdash; a second SSD, and both SSDs needs power loss protection to reduce fsync latency.</li>
<li>MariaDB 13.0.0 is slower than 10.2.30, <span>rQPS is 0.62</span></li>
<li><span>From the <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#qp100.L2.metrics">normalized vmstat and iostat metrics</a>&nbsp;there are increases in CPU per query (cpupq) and storage reads per query (rpq) for all DBMS versions that use the z12b config (see <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#qp100.L2.metrics">here</a>).</span></li>
<li><span>From the <a href="https://mdcallag.github.io/reports/apr26.ib.dell32.io.300m.5m.1800s.12u.maria/all.html#qp100.L2.graph">iostat summary for qp100</a> that includes average values for all iostat columns the read latency increases for all DBMS versions that use the z12b config. I blame interference from the extra fsync calls.</span></li>
</ul>
</ul>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to_8.html">The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 32-core server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 24-core server</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html" />
      <id>https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html</id>
      <updated>2026-04-08T01:39:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for MariaDB versions 10.2 through 13.0 vs the Insert Benchmark on a 24-core server. The goal is to see how performance changes over time to find regressions or highlight improvements.MariaDB 13.0.0 is faster than 10.2.30 on most benchmark steps and otherwise as fast as 10.2.30. This is a great result.tl;drfor a CPU-bound workloadthe write-heavy steps are much faster in 13.0.0 than 10.2.30the read-heavy steps get similar QPS in 13.0.0 and 10.2.30for an IO-bound workloadmost of the write-heavy steps are much faster in 13.0.0 than 10.2.30the point-query heavy steps get similar QPS in 13.0.0 and 10.2.30the range-query heavy steps get more QPS in 13.0.0 than 10.2.30Builds, configuration and hardwareI compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.The server has 24-cores, 2-sockets and 64G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. Intel HT is disabled.The my.cnf files are here for: 10.2, 10.3, 10.4, 10.5, 10.6, 10.11, 11.4, 11.8, 12.3 and 13.0.The BenchmarkThe benchmark is explained here and is run with 8 clients with a table per client. I repeated it with two workloads:CPU-boundthe values for X, Y, Z are 10M, 16M, 4MIO-boundthe values for X, Y, Z are 250M, 4M, 1MThe point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.The benchmark steps are:l.i0insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: overviewThe performance reports are here for the CPU-bound and IO-bound workloads.The summary sections from the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is MariaDB 10.2.30.When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000This statement doesn&#039;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with red for = 1.05 and grey for values between 0.95 and 1.05.Results: CPU-boundThe performance summary is here.The summary per benchmark step, where rQPS means relative QPS.l.i0MariaDB 13.0.0 is faster than 10.2.30 (rQPS is 1.22)KB written to storage per insert (wKBpi) and CPU per insert (cpupq) are smaller in 13.0.0 than 10.2.30, see herel.xI will ignore thisl.i1, l.i2MariaDB 13.0.0 is faster than 10.2.30 (rQPS is 1.21 and 1.45)for l.i1, CPU per insert (cpupq) is smaller in 13.0.0 than 10.2.30 but KB written to storage per insert (wKBpi) and the context switch rate (cspq) are larger in 13.0.0 than 10.2.30, see herefor l.i2, CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are smaller in 13.0.0 than 10.2.30 but the context switch rate (cspq) is larger in 13.0.0 than 10.2.30, see hereqr100, qr500, qr1000MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)the results from vmstat and iostat are less useful here because the write rate in 10.2 to 10.4 was much larger than 10.5+. While the my.cnf settings are as close as possible across all versions, it looks like furious flushing was enabled in 10.2 to 10.4 and I need to figure out whether it is possible to disable that.qp100, qp500, qp1000MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)what I wrote above for vmstat and iostat with the qr* test also applies hereResults: IO-boundThe performance summary is here.The summary per benchmark step, where rQPS means relative QPS.l.i0MariaDB 13.0.0 is faster than 10.2.30 (rQPS is 1.16)KB written to storage per insert (wKBpi) and CPU per insert (cpupq) are smaller in 13.0.0 than 10.2.30, see herel.xI will ignore thisl.i1, l.i2MariaDB 13.0.0 and 10.2.30 have the same QPS for l.i1 while 13.0.0 is faster for l.i2 (rQPS is 1.03 and 3.70). It is odd that QPS drops from 12.3.1 to 13.0.0 on the l.i1 step.for l.i1, CPU per insert (cpupq) and the context switch rate (cspq) are larger in 13.0.0 than 12.3.1, see here. The flamegraphs, that I have not shared, look similar. From iostat results there is much more discard (TRIM, SSD GC) in progress with 13.0.0 than 12.3.1 and the overhead from that might explain the difference.for l.i2, almost everything looks better in 13.0.0 than 10.2.30. Unlike what occurs for the l.i1 step, the results for 13.0.0 are similar to 12.3.1, see here.qr100, qr500, qr1000no DBMS versions were able to sustain the target write rate for qr1000 so I ignore that stepMariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)the results from vmstat and iostat are less useful here because the write rate in 10.2 to 10.4 was much larger than 10.5+. While the my.cnf settings are as close as possible across all versions, it looks like furious flushing was enabled in 10.2 to 10.4 and I need to figure out whether it is possible to disable that.qp100, qp500, qp1000no DBMS versions were able to sustain the target write rate for qr1000 so I ignore that stepMariaDB 13.0.0 is faster than 10.2.30 (rQPS is 1.17 and 1.56)what I wrote above for vmstat and iostat with the qr* test also applies here</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html">The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 24-core server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for MariaDB versions 10.2 through 13.0 vs the&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">Insert Benchmark</a>&nbsp;on a 24-core server. The goal is to see how performance changes over time to find regressions or highlight improvements.</p>
<p>MariaDB 13.0.0 is faster than 10.2.30 on most benchmark steps and otherwise as fast as 10.2.30. This is a great result.</p>
<p>tl;dr</p>

<ul>
<li>for a CPU-bound workload</li>
<ul>
<li>the write-heavy steps are much faster in 13.0.0 than 10.2.30</li>
<li>the read-heavy steps get similar QPS in 13.0.0 and 10.2.30</li>
</ul>
<li>for an IO-bound workload</li>
<ul>
<li>most of the write-heavy steps are much faster in 13.0.0 than 10.2.30</li>
<li>the point-query heavy steps get similar QPS in 13.0.0 and 10.2.30</li>
<li>the range-query heavy steps get more QPS in 13.0.0 than 10.2.30</li>
</ul>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.</div>
<div></div>
<div>The server has 24-cores, 2-sockets and 64G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. Intel HT is disabled.</div>
</div>
<div></div>
<div>The my.cnf files are here for: <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma100244_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.2</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma100339_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.3</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma100434_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma100526_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.5</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma100619_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.6</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma101109_rel_withdbg/etc/my.cnf.cz12a_c24r64">10.11</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma110403_rel_withdbg/etc/my.cnf.cz12b_c24r64">11.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma110803_rel_withdbg/etc/my.cnf.cz12b_c24r64">11.8</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c24r64/ma1203/etc/my.cnf.cz12b_c24r64">12.3 and 13.0</a>.</div>
<div></div>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>&nbsp;and&nbsp;is run with 8 clients with a table per client. I repeated it with two workloads:</div>
<div>
<ul>
<li>CPU-bound</li>
<ul>
<li>the values for X, Y, Z are 10M, 16M, 4M</li>
</ul>
<li>IO-bound</li>
<ul>
<li>the values for X, Y, Z are 250M, 4M, 1M</li>
</ul>
</ul>
<div>The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.</div>
</div>
<div></div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.</li>
<li>Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div>
<div><b>Results: overview</b></div>
<div>
<div><b><br></b></div>
<div>The performance reports are here for the <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html">CPU-bound</a> and <a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html">IO-bound</a> workloads.</div>
</div>
</div>
<div></div>
<div>The summary sections from&nbsp;the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.</div>
<div>
<div></div>
<div>Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is MariaDB 10.2.30.
<p>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</p></div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>This statement doesn&rsquo;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with&nbsp;<span>red</span>&nbsp;for &lt;= 0.95,&nbsp;<span>green</span>&nbsp;for &gt;= 1.05 and&nbsp;<span>grey</span>&nbsp;for values between 0.95 and 1.05.</div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The performance summary <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html#summary">is here</a>.
<p>The summary per benchmark step, where rQPS means relative QPS.</p></div>
</div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30 (<span>rQPS is 1.22</span>)</li>
<li>KB written to storage per insert (wKBpi) and CPU per insert (cpupq) are smaller in 13.0.0 than 10.2.30, see <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html#l.i0.metrics">here</a></li>
</ul>
<li>l.x</li>
<ul>
<li>I will ignore this</li>
</ul>
<li>l.i1, l.i2</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30 (<span>rQPS is 1.21 and 1.45</span>)</li>
<li>for l.i1, CPU per insert (cpupq) is smaller in 13.0.0 than 10.2.30 but KB written to storage per insert (wKBpi) and the context switch rate (cspq) are larger in 13.0.0 than 10.2.30, see <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html#l.i1.metrics">here</a></li>
<li>for l.i2, CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are smaller in 13.0.0 than 10.2.30 but the context switch rate (cspq) is larger in 13.0.0 than 10.2.30, see <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html#l.i2.metrics">here</a></li>
</ul>
<li>qr100, qr500, qr1000</li>
<ul>
<li>MariaDB 13.0.0 and 10.2.30 have similar QPS (<span>rQPS is close to 1.0</span>)</li>
<li>the <a href="https://mdcallag.github.io/reports/apr26.ib.mem.10m.20m.1800s.8u.maria/all.html#qr100.L1.metrics">results from vmstat and iostat</a> are less useful here because the write rate in 10.2 to 10.4 was much larger than 10.5+. While the my.cnf settings are as close as possible across all versions, it looks like furious flushing was enabled in 10.2 to 10.4 and I need to figure out whether it is possible to disable that.</li>
</ul>
<li>qp100, qp500, qp1000</li>
<ul>
<li>MariaDB 13.0.0 and 10.2.30 have similar QPS (<span>rQPS is close to 1.0</span>)</li>
<li>what I wrote above for vmstat and iostat with the qr* test also applies here</li>
</ul>
</ul>
</div>
<div><b>Results: IO-bound</b></div>
<div>
<div></div>
<div>The performance summary <a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html#summary">is here</a>.</div>
</div>
<div></div>
<div>
<div>The summary per benchmark step, where rQPS means relative QPS.</div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>MariaDB 13.0.0 is faster than 10.2.30 (<span>rQPS is 1.16</span>)</li>
<li>KB written to storage per insert (wKBpi) and CPU per insert (cpupq) are smaller in 13.0.0 than 10.2.30, see&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html#l.i0.metrics">here</a></li>
</ul>
<li>l.x</li>
<ul>
<li>I will ignore this</li>
</ul>
<li>l.i1, l.i2</li>
<ul>
<li>MariaDB 13.0.0 and 10.2.30 have the same QPS for l.i1 while 13.0.0 is faster for l.i2 (rQPS is <span>1.03</span> and <span>3.70</span>). It is odd that QPS drops from 12.3.1 to 13.0.0 on the l.i1 step.</li>
</ul>
<ul>
<li>for l.i1, CPU per insert (cpupq) and the context switch rate (cspq) are larger in 13.0.0 than 12.3.1, see&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html#l.i1.metrics">here</a>. The flamegraphs, that I have not shared, look similar. From iostat results there is much more discard (TRIM, SSD GC) in progress with 13.0.0 than 12.3.1 and the overhead from that might explain the difference.</li>
<li>for l.i2, almost everything looks better in 13.0.0 than 10.2.30. Unlike what occurs for the l.i1 step, the results for 13.0.0 are similar to 12.3.1, see <a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html#l.i2.metrics">here</a>.</li>
</ul>
<li>qr100, qr500, qr1000</li>
<ul>
<li>no DBMS versions were able to sustain the target write rate for qr1000 so I ignore that step</li>
<li>MariaDB 13.0.0 and 10.2.30 have similar QPS (<span>rQPS is close to 1.0</span>)</li>
<li>the&nbsp;<a href="https://mdcallag.github.io/reports/apr26.ib.io.250m.5m.1800s.8u.maria/all.html#qr100.L1.metrics">results from vmstat and iostat</a>&nbsp;are less useful here because the write rate in 10.2 to 10.4 was much larger than 10.5+. While the my.cnf settings are as close as possible across all versions, it looks like furious flushing was enabled in 10.2 to 10.4 and I need to figure out whether it is possible to disable that.</li>
</ul>
<li>qp100, qp500, qp1000</li>
<ul>
<li>no DBMS versions were able to sustain the target write rate for qr1000 so I ignore that step</li>
<li>MariaDB 13.0.0 is faster than 10.2.30 (<span>rQPS is 1.17 and 1.56</span>)</li>
<li>what I wrote above for vmstat and iostat with the qr* test also applies here</li>
</ul>
</ul>
<div></div>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div>
<div></div>
</div>
</div>
</div>
</div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/04/the-insert-benchmark-vs-mariadb-102-to.html">The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 24-core server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Thanks AWS Open Source</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html" />
      <id>https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html</id>
      <updated>2026-04-07T20:11:00+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I would like to thank AWS Open Source for their support.</p>
<p>For some time, I am maintaining Planet for the MySQL Community, a blog / news aggregator for the MySQL Community/Ecosystem.  I am also maintaining a similar aggregator for the Valkey Community.</p>
<p>Maintaining blog / news aggregators is not free.  It incurs hosting, domain registration, and other costs (in addition to time,</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html">Thanks AWS Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I would like to thank AWS Open Source for their support.</p>
<p>For some time, I am maintaining Planet for the MySQL Community, a&nbsp;blog / news aggregator for the MySQL Community/Ecosystem.&nbsp; I am also maintaining a similar aggregator for the Valkey Community.</p>
<p>Maintaining blog / news aggregators is not free.&nbsp; It incurs hosting, domain registration, and other costs (in addition to time,</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html">Thanks AWS Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Thanks AWS Open Source</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html" />
      <id>https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html</id>
      <updated>2026-04-07T20:11:00+03:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I would like to thank AWS Open Source for their support.</p>
<p>For some time, I am maintaining Planet for the MySQL Community, a blog / news aggregator for the MySQL Community/Ecosystem.  I am also maintaining a similar aggregator for the Valkey Community.</p>
<p>Maintaining blog / news aggregators is not free.  It incurs hosting, domain registration, and other costs (in addition to time,</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html">Thanks AWS Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I would like to thank AWS Open Source for their support.</p>
<p>For some time, I am maintaining Planet for the MySQL Community, a&nbsp;blog / news aggregator for the MySQL Community/Ecosystem.&nbsp; I am also maintaining a similar aggregator for the Valkey Community.</p>
<p>Maintaining blog / news aggregators is not free.&nbsp; It incurs hosting, domain registration, and other costs (in addition to time,</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/04/thanks-aws-open-source.html">Thanks AWS Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The AWS Lambda ‘Kiss of Death’</title>
      <link rel="alternate" type="text/html" href="https://shatteredsilicon.net/aws-lambda-kiss-of-death/" />
      <id>https://shatteredsilicon.net/aws-lambda-kiss-of-death/</id>
      <updated>2026-04-07T18:15:28+03:00</updated>
      <author><name>Jonathan Levin</name></author>
      <summary type="html"><![CDATA[<p>Our story begins as most database issues start: with hands on foreheads, internally or externally, saying ‘WTF is going on?’. We observed a series of database freezes on our production environment. It was quite severe. Connections spiked, writes were stalled and at some point, a large database freeze and they cleared. Being a Galera environment, […]<br />
The post The AWS Lambda ‘Kiss of Death’ appeared first on Shattered Silicon.</p>
<p><a href="https://shatteredsilicon.net/aws-lambda-kiss-of-death/">The AWS Lambda ‘Kiss of Death’</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Our story begins as most database issues start: with hands on foreheads, internally or externally, saying &lsquo;WTF is going on?&rsquo;. We observed a series of database freezes on our production environment. It was quite severe. Connections spiked, writes were stalled and at some point, a large database freeze and they cleared. Being a Galera environment, [&hellip;]</p>
<p>The post <a rel="nofollow" href="https://shatteredsilicon.net/aws-lambda-kiss-of-death/">The AWS Lambda &lsquo;Kiss of Death&rsquo;</a> appeared first on <a rel="nofollow" href="https://shatteredsilicon.net/">Shattered Silicon</a>.</p>

<p><a href="https://shatteredsilicon.net/aws-lambda-kiss-of-death/">The AWS Lambda ‘Kiss of Death’</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Sysbench vs MariaDB on a small server: using the same charset for all versions</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mariadb-on-small-server.html" />
      <id>https://smalldatum.blogspot.com/2026/04/sysbench-vs-mariadb-on-small-server.html</id>
      <updated>2026-04-06T03:38:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for sysbench vs MariaDB on a small server. I repeated tests using the same charset (latin1) for all versions as explained here. In previous results I used a multi-byte charset for modern MariaDB (probably 11.4+) by mistake and that adds a 5% CPU overhead for many tests.tl;drMariaDB has done much better than MySQL at avoid regressions from code bloat.There are several performance improvements in MariaDB 12.3 and 13.0For reads there are small regressions and frequent improvements.For writes there are  regressions up to 10%, and the biggest contributor is MariaDB 11.4Builds, configuration and hardwareI compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it are here. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.The my.cnf files are here for 10.2, 10.3, 10.4, 10.5, 10.6, 10.11, 11.4, 11.8, 12.3 and 13.0.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide tables below with relative QPS. When the relative QPS is &#62; 1 then some version is faster than the base version. When it is &#60; 1 then there might be a regression.  The relative QPS is:(QPS for some version) / (QPS for MariaDB 10.2.30) Values from iostat and vmstat divided by QPS are here. These can help to explain why something is faster or slower because it shows how much HW is used per request.The spreadsheet with results and charts is here. Files with performance summaries are here.Results: point queriesSummaryThe y-axis starts at 0.8 to improve readability.Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 7 of 9 testsThere were regressions from 10.2 through 10.5Performance has been improving from 10.6 through 13.0Results: range queries without aggregationSummaryThe y-axis starts at 0.8 to improve readability.Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 2 of 5 testsThere were regressions from 10.2 through 10.5, then performance was stable from 10.6 though 11.8, and now performance has improved in 12.3 and 13.0.Results: range queries with aggregationSummaryThe y-axis starts at 0.8 to improve readability.Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 1 of 8 tests and within 2% in 6 testsResults: writesSummaryThe y-axis starts at 0.8 to improve readability.Modern MariaDB (13.0) is about 10% slower than old MariaDB (10.2) in 5 of 10 tests and the largest regressions arrive in 11.4.</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mariadb-on-small-server.html">Sysbench vs MariaDB on a small server: using the same charset for all versions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for sysbench vs MariaDB on a small server. I repeated tests using the same charset (latin1) for all versions as <a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">explained here</a>. In previous results I used a multi-byte charset for modern MariaDB (probably 11.4+) by mistake and that adds a 5% CPU overhead for many tests.</p>
<p>tl;dr</p>

<ul>
<li>MariaDB has done much better than MySQL at avoid regressions from code bloat.</li>
<li>There are several performance improvements in MariaDB 12.3 and 13.0</li>
<li>For reads there are small regressions and frequent improvements.</li>
<li>For writes there are&nbsp; regressions up to 10%, and the biggest contributor is MariaDB 11.4</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>

<div></div>

<div>I compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.</div>
</div>
<p>The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2022/10/small-servers-for-performance-testing-v4.html">are here</a>. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.</p>
<p>The my.cnf files are here for <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma100244_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.2</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma100339_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.3</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma100434_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma100527_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.5</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma100620_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.6</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma101110_rel_withdbg/etc/my.cnf.cz12a_c8r32">10.11</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma110404_rel_withdbg/etc/my.cnf.cz12b_c8r32">11.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma110803_rel_withdbg/etc/my.cnf.cz12b_c8r32">11.8</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12b_c8r32">12.3 and 13.0</a>.</p>
<p><b>Benchmark</b></p>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.</div>
<div>The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 1800 seconds.</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div>I provide tables below with relative QPS.&nbsp;<span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than the</span><span>&nbsp;</span><i>base version.</i><span>&nbsp;When it is &lt; 1 then there might be a regression.&nbsp;&nbsp;</span><span>The relative QPS is:</span></div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for MariaDB 10.2.30)<span>&nbsp;</span></p></blockquote>
</div>
<div><span>Values from iostat and vmstat divided by QPS <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.pn53.sb.ma.latin/o.met.latest">are here</a>.&nbsp;</span><span>These can help to explain why something is faster or slower because it shows how much HW is used per request.</span></div>
<div><span><br></span></div>
<div><span>The spreadsheet with results and charts <a href="https://docs.google.com/spreadsheets/d/1rG3YaQd4BNKsDQBqEikW_P5neGL6ELq5C3cCt3x63oc/edit?usp=sharing">is here</a>. Files with performance summaries <a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/apr26.pn53.sb.ma.latin">are here</a>.</span></div>
<div><span><br></span></div>
<div><span><b>Results: point queries</b></span></div>
<div><span><br></span></div>
<div>Summary</div>
<div>
<ul>
<li>The y-axis starts at 0.8 to improve readability.</li>
<li>Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 7 of 9 tests</li>
<ul>
<li>There were regressions from 10.2 through 10.5</li>
<li>Performance has been improving from 10.6 through 13.0</li>
</ul>
</ul>
</div>
<div><span><br></span></div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS-plejCaTpjbyOeMglmeK4wDrgJKu6oGkBGRySzpoczJGWcHzfOw07_brkq7CgQPJcJk6LFpkms5d5CVUUwewQXuzCHSdZ0G5CXVtn3Rfzeupno4Qj0PK2_cS3JNzEeQgXQvnTYy8VMxrjKmeiDheuPG1fyuZ-xNw51Vsj596Bhq6BLjHl92paO0W9kOq/s600/QPS%20relative%20to%2010.2.30_%20point%20queries.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhS-plejCaTpjbyOeMglmeK4wDrgJKu6oGkBGRySzpoczJGWcHzfOw07_brkq7CgQPJcJk6LFpkms5d5CVUUwewQXuzCHSdZ0G5CXVtn3Rfzeupno4Qj0PK2_cS3JNzEeQgXQvnTYy8VMxrjKmeiDheuPG1fyuZ-xNw51Vsj596Bhq6BLjHl92paO0W9kOq/w640-h396/QPS%20relative%20to%2010.2.30_%20point%20queries.png" width="640"></a></div>
<p><span><b>Results: range queries&nbsp;</b></span><b>without aggregation</b></p></div>
<div><span>
<div><span><br></span></div>
<div>Summary</div>
<div>
<ul>
<li>The y-axis starts at 0.8 to improve readability.</li>
<li>Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 2 of 5 tests</li>
<ul>
<li>There were regressions from 10.2 through 10.5, then performance was stable from 10.6 though 11.8, and now performance has improved in 12.3 and 13.0.</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuWI8AKtma5sPXcBYov7aSw_t4_a1sOoH_91TTHWA055KXuaE6CHbwTkE77q5iw86LNs38Wg-xwkqcVoGVpENLktRyYG-0nIvKnkc1vvWmZPSrR8eWG2DlUMTZEokyi7oZYSRYpWmsc8Z7orfL2ImO1jS_3ba2wT9STCLm3PYn6MoNfLi9JTwq1ndThEqO/s600/QPS%20relative%20to%2010.2.30_%20range%20queries%20without%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuWI8AKtma5sPXcBYov7aSw_t4_a1sOoH_91TTHWA055KXuaE6CHbwTkE77q5iw86LNs38Wg-xwkqcVoGVpENLktRyYG-0nIvKnkc1vvWmZPSrR8eWG2DlUMTZEokyi7oZYSRYpWmsc8Z7orfL2ImO1jS_3ba2wT9STCLm3PYn6MoNfLi9JTwq1ndThEqO/w640-h396/QPS%20relative%20to%2010.2.30_%20range%20queries%20without%20aggregation.png" width="640"></a></div>
<p><b>Results: range queries with aggregation</b></p></div>
<div><span>
<div><span><br></span></div>
<div>Summary</div>
<div>
<ul>
<li>The y-axis starts at 0.8 to improve readability.</li>
<li>Modern MariaDB (13.0) is faster than old MariaDB (10.2) in 1 of 8 tests and within 2% in 6 tests</li>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1EbOOoVHxU1ftQ0nLJIpkwfFx2nCxd7JKgphN10k850422eKfNwaaHVM9X8iSmHqDukfX6FF4Twb4Asqjch6qv4BSiZ5PvmZPe3abx4jEjmnZMk_GMmuf0QaI0AGR86ZXlW2GOcWL_C-hm8TgUHY6kg2CgkAdjt5BFXFKv4iWgC49rwUUllO0jroHknbm/s600/QPS%20relative%20to%2010.2.30_%20range%20queries%20with%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg1EbOOoVHxU1ftQ0nLJIpkwfFx2nCxd7JKgphN10k850422eKfNwaaHVM9X8iSmHqDukfX6FF4Twb4Asqjch6qv4BSiZ5PvmZPe3abx4jEjmnZMk_GMmuf0QaI0AGR86ZXlW2GOcWL_C-hm8TgUHY6kg2CgkAdjt5BFXFKv4iWgC49rwUUllO0jroHknbm/w640-h396/QPS%20relative%20to%2010.2.30_%20range%20queries%20with%20aggregation.png" width="640"></a></div>
<p><b>Results: writes</b></p></div>
<div><span>
<div><span><br></span></div>
<div>Summary</div>
<div>
<ul>
<li><span>The y-axis starts at 0.8 to improve readability.</span></li>
<li>Modern MariaDB (13.0) is about 10% slower than old MariaDB (10.2) in 5 of 10 tests and the largest regressions arrive in 11.4.</li>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX77y2o1vLXSI64YguijPy17prsrARgyAAj53ZlmrvyJ9bRv33fuNp7IK5k6OieNZcBVoya5s8Ueuw6DBYSpXOaWNKArHb4oA5g29OiR5fNyCrDFGoQ76TmjDNQzlRpTQUbVuUCSrC8tG4c3RDkV-2kpXAzmNAgqElez2cTtlTfHGtFQNG-frMrmMSe8Yk/s600/QPS%20relative%20to%2010.2.30_%20writes.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiX77y2o1vLXSI64YguijPy17prsrARgyAAj53ZlmrvyJ9bRv33fuNp7IK5k6OieNZcBVoya5s8Ueuw6DBYSpXOaWNKArHb4oA5g29OiR5fNyCrDFGoQ76TmjDNQzlRpTQUbVuUCSrC8tG4c3RDkV-2kpXAzmNAgqElez2cTtlTfHGtFQNG-frMrmMSe8Yk/w640-h396/QPS%20relative%20to%2010.2.30_%20writes.png" width="640"></a></div>
</div>
<p></p></span></div>
<p></p></span></div>
<p></p></span></div>
<p></p></span></div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/04/sysbench-vs-mariadb-on-small-server.html">Sysbench vs MariaDB on a small server: using the same charset for all versions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>CPU-bound sysbench on a large server: Postgres, MySQL and MariaDB</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/04/cpu-bound-sysbench-on-large-server.html" />
      <id>https://smalldatum.blogspot.com/2026/04/cpu-bound-sysbench-on-large-server.html</id>
      <updated>2026-04-04T02:30:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This post has results for CPU-bound sysbench vs Postgres, MySQL and MariaDB on a large server using older and newer releases. The goal is to measure:how performance changes over time from old versions to new versionsperformance between modern MySQL, MariaDB and PostgresThe context here is a collection of microbenchmarks using a large server with high concurrency. Results on other workloads might be different. But you might be able to predict performance for a more complex workload using the data I share here.tl;drfor point queriesPostgres is faster than MySQL, MySQL is faster than MariaDBmodern MariaDB suffers from huge regressions that arrived in 10.5 and remain in 12.xfor range queries without aggregationMySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)for range queries with aggregationMySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)for writesPostgres is much faster than MariaDB and MySQL (up to 4X faster)MariaDB is between 1.3X and 1.5X faster than MySQLon regressionsPostgres tends to be boring with few regressions from old to new versionsMySQL and MariaDB are exciting, with more regressions to debugHand-wavy summaryMy hand-wavy summary about performance over time has been the following. It needs a revision, but also needs to be concise. Modern Postgres is about as fast as old Postgres, with some improvements. It has done great at avoiding perf regressions.Modern MySQL at low concurrency has many performance regressions from new CPU overheads (code bloat). At high concurrency it is faster than old MySQL because the improvements for concurrency are larger than the regressions from code bloat.Modern MariaDB at low concurrency has similar perf as old MariaDB. But at high concurrency it has large regressions for point queries, small regressions for range queries and some large improvements for writes. Note that many things use point queries internally - range scan on non-covering index, updates, deletes. The regressions arrive in 10.5, 10.6, 10.11 and 11.4.For results on a small server with a low concurrency workload, I have many posts including:MySQL and MariaDB from 2024MySQL-only from 2026Postgres from 2025Builds, configuration and hardwareI compiled:Postgres from source for versions 12.22, 13.23, 14.21, 15.16, 16.12, 17.8 and 18.2.MySQL from source for versions 5.6.51, 5.7.44, 8.0.44, 8.4.7 and 9.5.0MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.15, 11.4.10, 11.8.6, 12.2.2 and 12.3.1I used a 48-core server from Hetzneran ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4128G RAMUbuntu 22.04 running the non-HWE kernel (5.5.0-118-generic). The server has since been updated to Ubuntu 24.04 and I am repeating tests.Configuration files for Postgres:the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions 12, 13, 14, 15, 16 and 17.for Postgres 18 I used conf.diff.cx10b_c32r128 (x10b_c32r128) which is as close as possible to the Postgres 17 config and uses io_method=syncThe my.cnf files for MySQL are here: 5.6.51, 5.7.44, 8.0.4x, 8.4.x, 9.x.0The my.cnf files for MariaDB are here: 10.2, 10.3, 10.4, 10.5, 10.6, 10.11, 11.4, 11.8, 12.2, 12.3.I thought I was using the latin1 charset for all versions of MariaDB and MySQL but I recently learned I was using somehting like utf8mb4 on recent versions (maybe MariaDB 11.4+ and MySQL 8.0+). See here for details. I will soon repeat tests using latin1 for all versions. For some tests, the use of a multi-byte charset increases CPU overhead by up to 5%, which reduces throughput by a similar amount.With Postgres I have been using a multi-byte charset for all versions.BenchmarkI used sysbench and my usage is explained here. I now run 32 of the 42 microbenchmarks listed in that blog post. Most test only one type of SQL statement. Benchmarks are run with the database cached by Postgres.The read-heavy microbenchmarks are run for 600 seconds and the write-heavy for 900 seconds. The benchmark is run with 40 clients and 8 tables with 10M rows per table. The database is cached.The purpose is to search for regressions from new CPU overhead and mutex contention. I use the small server with low concurrency to find regressions from new CPU overheads and then larger servers with high concurrency to find regressions from new CPU overheads and mutex contention.The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads -- both more and less synthetic.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries without aggregation while part 2 has queries with aggregation. I provide charts below with relative QPS. The relative QPS is the following:(QPS for some version) / (QPS for base version)When the relative QPS is &#62; 1 then some version is faster than base version.  When it is &#60; 1 then there might be a regression. When the relative QPS is 1.2 then some version is about 20% faster than base version.The per-test results from vmstat and iostat can help to explain why something is faster or slower because it shows how much HW is used per request, including CPU overhead per operation (cpu/o) and context switches per operation (cs/o) which are often a proxy for mutex contention.The spreadsheet with charts is here and in some cases is easier to read than the charts below. Files with performance summaries are archived here.The relative QPS numbers are also here for:MySQL vs MariaDB vs PostgresMySQLMariaDBPostgresFiles with HW efficiency numbers, average values from vmstat and iostat normalized by QPS, are here for:MySQL vs MariaDB vs PostgresMySQLMariaDBPostgresResults: MySQL vs MariaDB vs PostgresHW efficiency metrics are here. They have metrics from vmstat and iostat normalized by QPS.Point queriesPostgres is faster than MySQL is faster than MariaDBMySQL gets about 2X more QPS than MariaDB on 5 of the 9 testsa table for relative QPS by test is herefrom HW efficiency metrics for the random-points.range1000 test:Postgres is 1.35X faster than MySQL, MySQL is more than 2X faster than MariaDBMariaDB uses 2.28X more CPU and does 23.41X more context switches than MySQLPostgres uses less CPU but does ~1.93X more context switches than MySQLRange queries without aggregationMySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)MariaDB has lousy results on the range-notcovered-si test because it must do many point lookups to fetch columns not in the index and MariaDB has problems with point queries at high concurrencya table for relative QPS by test is herefrom HW efficiency metrics for the scan:MySQL is 1.2X faster than Postgres and 1.5X faster than MariaDBMariaDB uses 1.19X more CPU and does ~1000X more context switches than MySQLPostgres uses 1.55X more CPU but does few context switches than MySQLRange queries with aggregationMySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)a table for relative QPS by test is herefrom HW efficiency metrics for read-only-countMariaDB is 1.22X faster than MySQL, MySQL is 4.2X faster than PostgresMariaDB uses 1.22X more CPU than MySQL but does ~2X more context switchesPostgres uses 4.11X more CPU than MySQL and does 1.08X more context switchesQuery plans are here and MySQL + MariaDB benefit from the InnoDB clustered indexfrom HW efficiency metrics for read-only.range=10MariaDB is 1.22X faster than MySQL, MySQL is 4.2X fasterMySQL is 1.2X faster than Postgres and 1.5X faster than MariaDBMariaDB uses 1.19X more CPU and does ~1000X more context switches than MySQLPostgres uses 1.55X more CPU but does few context switches than MySQLWritesPostgres is much faster than MariaDB and MySQL (up to 4X faster)MariaDB is between 1.3X and 1.5X faster than MySQLa table for relative QPS by test is herefrom HW efficiency metrics for insertPostgres is 3.03X faster than MySQL, MariaDB is 1.32X faster than MySQLMySQL uses ~1.5X more CPU than MariaDB and ~2X more CPU than PostgresMySQL does ~1.3X more context switches than MariaDB and ~2.9X more than PostgresResults: MySQLHW efficiency metrics are here. They have metrics from vmstat and iostat normalized by QPS.Point queriesFor 7 of 9 tests QPS is ~1.8X larger or more in 5.7.44 than in 5.6.51For 2 tests there are small regressions after 5.6.51 -- points-covered-si &#38; points-notcovered-sia table for relative QPS by test is herefrom HW efficiency metrics for points-covered-si:the regression is explained by an increase in CPURange queries without aggregationthere is a small regression from 5.6 to 5.7 and a larger one from 5.7 to 8.0a table for relative QPS by test is herefrom HW efficiency metrics for range-covered-pk:CPU overhead grows by up to 1.4X after 5.6.51, this is true for all of the testsRange queries with aggregationregressions after 5.6.51 here are smaller than in the other groups, but 5.7 tends to do better than 8.0, 8.4 and 9.5a table for relative QPS by test is hereHW efficiency metrics are here for read-only_range=100QPS changes because CPU/query changesWritesQPS improves after 5.6 by up to ~7Xa table for relative QPS by test is hereHW efficiency metrics are here insertQPS improves after 5.6.51 because CPU per statement dropsResults: MariaDBHW efficiency metrics are here. The have metrics from vmstat and iostat normalized by QPS.Point queriesQPS for 6 of 9 tests drops in half (or more) from 10.2 to 12.3a table for relative QPS is heremost of the regressions arrive in 10.5 and the root cause might be remove support for innodb_buffer_pool_intances and only support one buffer pool instanceHW efficiency metrics are here for points-covered-pkthere are large increases in CPU overhead and the context switch rate starting in 10.5Range queries without aggregationfor range-covered-* and range-notcovered-pk there is a small regression in 10.4for range-not-covered-si there is a large regression in 10.5 because this query does frequent point lookups on the PK to get missing columnsfor scan there is a regression in 10.5 that goes away, but the regressions return in 10.11 and 11.4 a table for relative QPS by test is hereHW efficiency metrics are hereRange queries with aggregationfor most tests there are small regressions in 10.4 and 10.5a table for relative QPS by test is hereHW efficiency metrics are hereWritesfor most tests modern MariaDB is faster than 10.2table for relative QPS by test is hereHW efficiency metrics are hereResults: PostgresHW efficiency metrics are here. They have metrics from vmstat and iostat normalized by QPS.Point queriesQPS for hot-points increased by ~2.5X starting in Postgres 17.xotherwise QPS is stable from 12.22 through 18.2a table for relative QPS by test is hereHW efficiency metrics for the hot-points test are hereCPU drops by more than half starting in 17.xRange queries without aggregationQPS is stable for the range-not-covered-* and scan testsQPS drops almost in half for the range-covered-* testsa table for relative QPS by test is hereall versions use the same query plan for the range-covered-pk testHW efficiency metrics are here for range-covered-pk and for range-covered-siAn increase in CPU overhead explains the regressions for range-covered-*I hope to get flamegraphs and thread stacks for these tests to explain what happensRange queries with aggregationQPS is stable from 12.22 through 18.2a table for relative QPS by test is hereHW efficiency metrics are hereWritesQPS is stable for 5 of 10 testsQPS improves by up to 1.7X for the other 5 tests, most of that arrives in 17.xa table for relative QPS by test is hereHW efficiency metrics are here for update-indexCPU overhead and context switch rates drop almost in half starting in 17.x</p>
<p><a href="https://smalldatum.blogspot.com/2026/04/cpu-bound-sysbench-on-large-server.html">CPU-bound sysbench on a large server: Postgres, MySQL and MariaDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This post has results for CPU-bound sysbench vs Postgres, MySQL and MariaDB on a large server using older and newer releases.&nbsp;</p>
<p>The goal is to measure:</p>
<ul>
<li>how performance changes over time from old versions to new versions</li>
<li>performance between modern MySQL, MariaDB and Postgres</li>
</ul>
<p>The context here is a collection of microbenchmarks using a large server with high concurrency. Results on other workloads might be different. But you might be able to predict performance for a more complex workload using the data I share here.</p>
<p>tl;dr</p>
<div>
<ul>
<li>for point queries</li>
<ul>
<li>Postgres is faster than MySQL, MySQL is faster than MariaDB</li>
<li>modern MariaDB suffers from huge regressions that arrived in 10.5 and remain in 12.x</li>
</ul>
<li>for range queries without aggregation</li>
<ul>
<li>MySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)</li>
</ul>
<li>for range queries with aggregation</li>
<ul>
<li>MySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)</li>
</ul>
<li>for writes</li>
<ul>
<li>Postgres is much faster than MariaDB and MySQL (up to 4X faster)</li>
<li>MariaDB is between 1.3X and 1.5X faster than MySQL</li>
</ul>
<li>on regressions</li>
<ul>
<li>Postgres tends to be boring with few regressions from old to new versions</li>
<li>MySQL and MariaDB are exciting, with more regressions to debug</li>
</ul>
</ul>
</div>
<div><b>Hand-wavy summary</b></div>
<div></div>
<div>My hand-wavy summary about performance over time has been the following. It needs a revision, but also needs to be concise.&nbsp;<br><span><br><span>Modern Postgres is about as fast as old Postgres, with some improvements. It has done great at avoiding perf regressions.</span><span><br></span><span><br></span><span>Modern MySQL at low concurrency has many performance regressions from new CPU overheads (code bloat). At high concurrency it is faster than old MySQL because the improvements for concurrency are larger than the regressions from code bloat.</span><span><br></span><span><br></span><span>Modern MariaDB at low concurrency has similar perf as old MariaDB. But at high concurrency it has large regressions for point queries, small regressions for range queries and some large improvements for writes. Note that many things use point queries internally &ndash; range scan on non-covering index, updates, deletes. The regressions arrive in 10.5, 10.6, 10.11 and 11.4.</span></span></div>
<div><span><span><br>For results on a small server with a low concurrency workload, I have many posts including:
<ul>
<li><a href="https://smalldatum.blogspot.com/2024/04/sysbench-on-small-server-mariadb-and.html">MySQL and MariaDB</a> from 2024</li>
<li><a href="https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html">MySQL-only</a> from 2026</li>
<li><a href="https://smalldatum.blogspot.com/2025/09/postgres-180-vs-sysbench-on-small-server.html">Postgres</a> from 2025</li>
</ul>
<div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>I compiled:</div>
<div>
<ul>
<li>Postgres from source for versions 12.22, 13.23, 14.21, 15.16, 16.12, 17.8 and 18.2.</li>
<li>MySQL from source for versions 5.6.51, 5.7.44, 8.0.44, 8.4.7 and 9.5.0</li>
<li>MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.15, 11.4.10, 11.8.6, 12.2.2 and 12.3.1</li>
</ul>
</div>
<div><span>I used a 48-core server from Hetzner</span></div>
<div>
<ul>
<li>an ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled</li>
<li>2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4</li>
<li>128G RAM</li>
<li>Ubuntu 22.04 running the non-HWE kernel (5.5.0-118-generic). The server has since been updated to Ubuntu 24.04 and I am repeating tests.</li>
</ul>
<div>
<div><span>Configuration files for Postgres:</span></div>
<div>
<ul>
<li><span>the config file is named conf.diff.cx10a_c32r128 (x10a_c32r128) and is here for versions&nbsp;</span><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1219_o2nofp/conf.diff.cx10a_c32r128">12</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1315_o2nofp/conf.diff.cx10a_c32r128">13</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1412_o2nofp/conf.diff.cx10a_c32r128">14</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg157_o2nofp/conf.diff.cx10a_c32r128">15</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg163_o2nofp/conf.diff.cx10a_c32r128">16</a>&nbsp;and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg17beta1_o2nofp/conf.diff.cx10a_c32r128">17</a>.</li>
<li>for Postgres 18 I used&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10b_c32r128">conf.diff.cx10b_c32r128</a><span>&nbsp;</span><span>(x10b_c32r128) which is as close as possible to the Postgres 17 config and&nbsp;</span>uses io_method=sync</li>
</ul>
<div>The my.cnf files for MySQL are here:&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my5651_rel_o2nofp/etc/my.cnf.cz12a_c32r128">5.6.51</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my5744_rel_o2nofp/etc/my.cnf.cz12a_c32r128">5.7.44</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my8043_rel_o2nofp/etc/my.cnf.cz12a_c32r128">8.0.4x</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my8406_rel_o2nofp/etc/my.cnf.cz12a_c32r128">8.4.x</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my9400_rel_o2nofp/etc/my.cnf.cz12a_c32r128">9.x.0</a></div>
<div></div>
<div>The my.cnf files for MariaDB are here: <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100244_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.2</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100339_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.3</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100433_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100524_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.5</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma100617_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.6</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma101107_rel_withdbg/etc/my.cnf.cz12a_c32r128">10.11</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma110401_rel_withdbg/etc/my.cnf.cz12b_c32r128">11.4</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma110803_rel_withdbg/etc/my.cnf.cz12b_c32r128">11.8</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma120101_rel_withdbg/etc/my.cnf.cz12b_c32r128">12.2</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_c32r128">12.3</a>.
<p>I thought I was using the latin1 charset for all versions of MariaDB and MySQL but I recently learned I was using somehting like utf8mb4 on recent versions (maybe MariaDB 11.4+ and MySQL 8.0+). <a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">See here</a> for details. I will soon repeat tests using latin1 for all versions. For some tests, the use of a multi-byte charset increases CPU overhead by up to 5%, which reduces throughput by a similar amount.</p></div>
<div></div>
<div>With Postgres I have been using a multi-byte charset for all versions.</div>
<div></div>
<div>
<div>
<div><b>Benchmark</b></div>
<div>
<div></div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. I now run 32 of the 42 microbenchmarks listed in that blog post. Most test only one type of SQL statement. Benchmarks are run with the database cached by Postgres.</div>
<div>The read-heavy microbenchmarks are run for 600 seconds and the write-heavy for 900 seconds. The benchmark is run with 40 clients and 8 tables with 10M rows per table. The database is cached.</div>
</div>
</div>
<div></div>
<div>The purpose is to search for regressions from new CPU overhead and mutex contention. I use the small server with low concurrency to find regressions from new CPU overheads and then larger servers with high concurrency to find regressions from new CPU overheads and mutex contention.</div>
</div>
<div></div>
<div>The tests can be called microbenchmarks. They are very synthetic. But microbenchmarks also make it easy to understand which types of SQL statements have great or lousy performance. Performance testing benefits from a variety of workloads &mdash; both more and less synthetic.</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries without aggregation while part 2 has queries with aggregation.&nbsp;</span></div>
<div>I provide charts below with relative QPS. The relative QPS is the following:</div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for base version)</p></blockquote>
</div>
<div><span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than&nbsp;<i>base version</i></span><span>.&nbsp; When it is &lt; 1 then there might be a regression. When the relative QPS is 1.2 then&nbsp;<i>some version</i>&nbsp;is about 20% faster than&nbsp;</span><i>base version</i><span>.</span></div>
<div><span><br></span></div>
<div><span>The per-test results from vmstat and iostat&nbsp;</span><span>can help to explain why something is faster or slower because it shows how much HW is used per request, including CPU overhead per operation (cpu/o) and context switches per operation (cs/o) which are often a proxy for mutex contention.</span></div>
<div><span><span><br></span></span></div>
<div><span><span>The spreadsheet with charts <a href="https://docs.google.com/spreadsheets/d/1tDLbrRQuw0dSTzb-m1gSxtVCzuqLLQVeXAGmBWU3QOU/edit?usp=sharing">is here</a> and in some cases is easier to read than the charts below. Files with performance summaries are <a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/apr26.sb.hetz">archived here</a>.</span></span></div>
<div><span><span><br></span></span></div>
<div><span><span>The relative QPS numbers are also here for:
<ul>
<li><a href="https://gist.github.com/mdcallag/b07c1f4ee95619b8e047129fdbeb3431">MySQL vs MariaDB vs Postgres</a></li>
<li><a href="https://gist.github.com/mdcallag/b743672d5a7df142a34488040d7bed54">MySQL</a></li>
<li><a href="https://gist.github.com/mdcallag/b2ce4329503feeb6b4f93ca1c416e61f">MariaDB</a></li>
<li><a href="https://gist.github.com/mdcallag/34402f015bf976d2452b871a1b0ace84">Postgres</a></li>
</ul>
<div>Files with HW efficiency numbers, average values from vmstat and iostat normalized by QPS, are here for:</div>
<div>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest">MySQL vs MariaDB vs Postgres</a></li>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest">MySQL</a></li>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest">MariaDB</a></li>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest">Postgres</a></li>
</ul>
</div>
<p></p></span></span></div>
<div><span><span><b>Results: MySQL vs MariaDB vs Postgres</b></span></span></div>
<div></div>
<div>HW efficiency metrics <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest">are here</a>. They have metrics from vmstat and iostat normalized by QPS.</div>
<div></div>
<div>Point queries</div>
<div>
<ul>
<li>Postgres is faster than MySQL is faster than MariaDB</li>
<li>MySQL gets about 2X more QPS than MariaDB on 5 of the 9 tests</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/b07c1f4ee95619b8e047129fdbeb3431#file-gistfile1-txt-L5-L15">is here</a></li>
<li>from HW efficiency metrics for the <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest#L221-L229">random-points.range1000 test</a>:</li>
<ul>
<li>Postgres is 1.35X faster than MySQL, MySQL is more than 2X faster than MariaDB</li>
<li>MariaDB uses 2.28X more CPU and does 23.41X more context switches than MySQL</li>
<li>Postgres uses less CPU but does ~1.93X more context switches than MySQL</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjPc2bM3MFO7wwdlYA8lBj_gJNDzhY7AgoHEwqQ_Ae8blJ0bSjAhUiVc1ryXZmanm7MQjr5j5hR09bhCvgl5vQKxR3I5Ji7j5FBG8BKJTyYrb2lm5_lRDiQtzEoYP_Ac54LINw-R2FgdTIiHf_vJ6rPqZqfPXRcrsRhJrLiDrbt_C-oy0BfYNJW93i5dLL/s600/QPS%20relative%20to%20MySQL%208.4.7_%20point%20queries.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgjPc2bM3MFO7wwdlYA8lBj_gJNDzhY7AgoHEwqQ_Ae8blJ0bSjAhUiVc1ryXZmanm7MQjr5j5hR09bhCvgl5vQKxR3I5Ji7j5FBG8BKJTyYrb2lm5_lRDiQtzEoYP_Ac54LINw-R2FgdTIiHf_vJ6rPqZqfPXRcrsRhJrLiDrbt_C-oy0BfYNJW93i5dLL/w640-h396/QPS%20relative%20to%20MySQL%208.4.7_%20point%20queries.png" width="640"></a></div>
<p><span>Range queries without aggregation</span></p></div>
<div>
<ul>
<li>MySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)</li>
<li>MariaDB has lousy results on the range-notcovered-si test because it must do many point lookups to fetch columns not in the index and MariaDB has problems with point queries at high concurrency</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/b07c1f4ee95619b8e047129fdbeb3431#file-gistfile1-txt-L17-L23">is here</a></li>
<li>from HW efficiency metrics for the&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest#L371-L379">scan</a>:</li>
<ul>
<li>MySQL is 1.2X faster than Postgres and 1.5X faster than MariaDB</li>
<li>MariaDB uses 1.19X more CPU and does ~1000X more context switches than MySQL</li>
<li>Postgres uses 1.55X more CPU but does few context switches than MySQL</li>
</ul>
</ul>
</div>
<div><span>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj36Sbjy4tV3opusAs5WkGUoW1O0g10K6IEXW_D90cGiqlRb-0VnFcTqjma_ezEzoozJFGTt_rTl8XxDk9soNaen-pirWN60bIXbCkHzFIb8PGpBJbfLD8CW03BWokN5vbTYX4y2aqlDNikOd2pmuLdqmU0rHAb2AZOBfQnHaLXS061zDemYrfiFo1hIeAl/s600/QPS%20relative%20to%20MySQL%208.4.7_%20range%20queries%20without%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj36Sbjy4tV3opusAs5WkGUoW1O0g10K6IEXW_D90cGiqlRb-0VnFcTqjma_ezEzoozJFGTt_rTl8XxDk9soNaen-pirWN60bIXbCkHzFIb8PGpBJbfLD8CW03BWokN5vbTYX4y2aqlDNikOd2pmuLdqmU0rHAb2AZOBfQnHaLXS061zDemYrfiFo1hIeAl/w640-h396/QPS%20relative%20to%20MySQL%208.4.7_%20range%20queries%20without%20aggregation.png" width="640"></a></div>
<p><span>Range queries with aggregation</span></p></span></div>
<div>
<ul>
<li>MySQL is about as fast as MariaDB, both are faster than Postgres (often 2X faster)</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/b07c1f4ee95619b8e047129fdbeb3431#file-gistfile1-txt-L25-L34">is here</a></li>
<li>from HW efficiency metrics for&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest#L181-L189">read-only-count</a></li>
<ul>
<li>MariaDB is 1.22X faster than MySQL, MySQL is 4.2X faster than Postgres</li>
<li>MariaDB uses 1.22X more CPU than MySQL but does ~2X more context switches</li>
<li>Postgres uses 4.11X more CPU than MySQL and does 1.08X more context switches</li>
<li>Query plans <a href="https://gist.github.com/mdcallag/42306744249b368d2a54b9d44d9b1def">are here</a> and MySQL + MariaDB benefit from the InnoDB clustered index</li>
</ul>
<li>from HW efficiency metrics for <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest#L111-L119">read-only.range=10</a></li>
<ul>
<li>MariaDB is 1.22X faster than MySQL, MySQL is 4.2X fasterMySQL is 1.2X faster than Postgres and 1.5X faster than MariaDB</li>
<li>MariaDB uses 1.19X more CPU and does ~1000X more context switches than MySQL</li>
<li>Postgres uses 1.55X more CPU but does few context switches than MySQL</li>
</ul>
</ul>
</div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm6dkOB_tSUPWWEfHDruFZZZNyS4_ROXA8INJWZwUsYPDmYIoP1NEWN8lqYlEa6T-F2XDmbJBTIxk82WEPKkHYeLtQ3HjE20lZetR4LxrpTL-eDhYYTH9iRu-ReqJH2Wri7HdDGQTvo1f1cClV0OksifLhOnmFsi_nRF8RyVVNt-OiPRY04k06gHhBy-Np/s600/QPS%20relative%20to%20MySQL%208.4.7_%20range%20queries%20with%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjm6dkOB_tSUPWWEfHDruFZZZNyS4_ROXA8INJWZwUsYPDmYIoP1NEWN8lqYlEa6T-F2XDmbJBTIxk82WEPKkHYeLtQ3HjE20lZetR4LxrpTL-eDhYYTH9iRu-ReqJH2Wri7HdDGQTvo1f1cClV0OksifLhOnmFsi_nRF8RyVVNt-OiPRY04k06gHhBy-Np/w640-h396/QPS%20relative%20to%20MySQL%208.4.7_%20range%20queries%20with%20aggregation.png" width="640"></a></div>
<div><span>Writes</span></div>
<div>
<ul>
<li>Postgres is much faster than MariaDB and MySQL (up to 4X faster)</li>
<li>MariaDB is between 1.3X and 1.5X faster than MySQL</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/b07c1f4ee95619b8e047129fdbeb3431#file-gistfile1-txt-L36-L47">is here</a></li>
<li>from HW efficiency metrics for <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.ma.pg.latest#L341-L349">insert</a></li>
<ul>
<li>Postgres is 3.03X faster than MySQL, MariaDB is 1.32X faster than MySQL</li>
<li>MySQL uses ~1.5X more CPU than MariaDB and ~2X more CPU than Postgres</li>
<li>MySQL does ~1.3X more context switches than MariaDB and ~2.9X more than Postgres</li>
</ul>
</ul>
</div>
<p></p></span>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFQwpcPaXmp4RAQLHrjxueanJ9RlZdlFUj7UhhyQpWw-_K4Y1T-rOVmr22I_n8N6LR7L_ItB8hPRDCUiNsMgs7iXkSTXfX-CM-SI0C2RgAPCBnSHaxsOgkmgcES49_Gbzz3vsvt1Dau5A7awp-PgObBk_FgGwMiIZoafEgv1HlQxEGU9G1MXRns7HSDQvA/s600/QPS%20relative%20to%20MySQL%208.4.7_%20writes.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFQwpcPaXmp4RAQLHrjxueanJ9RlZdlFUj7UhhyQpWw-_K4Y1T-rOVmr22I_n8N6LR7L_ItB8hPRDCUiNsMgs7iXkSTXfX-CM-SI0C2RgAPCBnSHaxsOgkmgcES49_Gbzz3vsvt1Dau5A7awp-PgObBk_FgGwMiIZoafEgv1HlQxEGU9G1MXRns7HSDQvA/w640-h396/QPS%20relative%20to%20MySQL%208.4.7_%20writes.png" width="640"></a></div>
</div>
</div>
<div><b>Results: MySQL</b></div>
<div>
<div><span><br></span></div>
<div><span>HW efficiency metrics&nbsp;</span><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest">are here</a><span>. They have metrics from vmstat and iostat normalized by QPS.</span></div>
</div>
<div></div>
<div><span>
<div>Point queries</div>
<div>
<ul>
<li><span>For 7 of 9 tests QPS is ~1.8X larger or more in 5.7.44 than in 5.6.51</span></li>
<li><span>For 2 tests there are small regressions after 5.6.51 &mdash; points-covered-si &amp; points-notcovered-si</span></li>
<li>a table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/b743672d5a7df142a34488040d7bed54#file-gistfile1-txt-L7-L17">is here</a></li>
<li>from HW efficiency metrics for <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest#L407-L419">points-covered-si</a>:</li>
<ul>
<li>the regression is explained by an increase in CPU</li>
</ul>
</ul>
</div>
<p></p></span></div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgP4At7Kh-gvULwMVwRINHWt-KQ8rnMd0cRT-dYzOh0We9Q2d7ETfNDYb-ESNWZ5AU1JJhPHQyQuwsEtcqdxYuhFiXImhHV0mUOhsZUd9JtCHZhtursFNpIk_Btqwf-XBj76HoxAG-Uiv2VuOfHDZgZpyC-RIMbavDVhrNj32EK73D2cCrOIgdt5I-V1COg/s600/QPS%20relative%20to%20MySQL%205.6.51_%20point%20queries.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgP4At7Kh-gvULwMVwRINHWt-KQ8rnMd0cRT-dYzOh0We9Q2d7ETfNDYb-ESNWZ5AU1JJhPHQyQuwsEtcqdxYuhFiXImhHV0mUOhsZUd9JtCHZhtursFNpIk_Btqwf-XBj76HoxAG-Uiv2VuOfHDZgZpyC-RIMbavDVhrNj32EK73D2cCrOIgdt5I-V1COg/w640-h396/QPS%20relative%20to%20MySQL%205.6.51_%20point%20queries.png" width="640"></a></div>
<p><span>Range queries without aggregation</span></p></div>
<div>
<ul>
<li>there is a small regression from 5.6 to 5.7 and a larger one from 5.7 to 8.0</li>
<li>a table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/b743672d5a7df142a34488040d7bed54#file-gistfile1-txt-L19-L25">is here</a></li>
<li>from HW efficiency metrics for&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest#L407-L419">range-covered-pk</a>:</li>
<ul>
<li>CPU overhead grows by up to 1.4X after 5.6.51, this is true for all of the tests</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivH-1mjq0V6Qwlz8E37klAz0Gzsbaz5W9Kvlts1H2YxiJO73mFcuoovANETn6v8vfqcMjVUCOZNVCZ3COvBcbAUtChbxfaTuBN3kJ9dFiiy-sRQGoLKLgmmeeZBmC9kJgk3jNLKcsbYHPWqy7xvlvv9bD_zZSZB40xS85xcL6ZUVvWA0CbT4IO9-TimpUG/s600/QPS%20relative%20to%20MySQL%205.6.51_%20range%20queries%20without%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEivH-1mjq0V6Qwlz8E37klAz0Gzsbaz5W9Kvlts1H2YxiJO73mFcuoovANETn6v8vfqcMjVUCOZNVCZ3COvBcbAUtChbxfaTuBN3kJ9dFiiy-sRQGoLKLgmmeeZBmC9kJgk3jNLKcsbYHPWqy7xvlvv9bD_zZSZB40xS85xcL6ZUVvWA0CbT4IO9-TimpUG/w640-h396/QPS%20relative%20to%20MySQL%205.6.51_%20range%20queries%20without%20aggregation.png" width="640"></a></div>
<p><span>Range queries with aggregation</span></p></div>
<div>
<ul>
<li>regressions after 5.6.51 here are smaller than in the other groups, but 5.7 tends to do better than 8.0, 8.4 and 9.5</li>
<li><span>a table for relative QPS by test&nbsp;</span><a href="https://gist.github.com/mdcallag/b743672d5a7df142a34488040d7bed54#file-gistfile1-txt-L27-L36">is here</a></li>
<li><span>HW efficiency metrics </span><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest#L169-L181">are here</a><span> for read-only_range=100</span></li>
<ul>
<li>QPS changes because CPU/query changes</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5V6YBLhqX6yrHEcV_RvTpSomxm-GfwLqVkivGLuONr0dyWfsi9GRgyWrwf8VsG3F8UI0vhi7z6ay5Yy86jHoJlfqS3kmCwGoKWZJqxN28lwlg2hyphenhyphenjg8-_PhSELP68rgrK-UtFMx4_uljr0TV500cmlzlU5zRMrVh16CrHk0kiVE95OejIHPGv7n0_3BHY/s600/QPS%20relative%20to%20MySQL%205.6.51_%20range%20queries%20with%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5V6YBLhqX6yrHEcV_RvTpSomxm-GfwLqVkivGLuONr0dyWfsi9GRgyWrwf8VsG3F8UI0vhi7z6ay5Yy86jHoJlfqS3kmCwGoKWZJqxN28lwlg2hyphenhyphenjg8-_PhSELP68rgrK-UtFMx4_uljr0TV500cmlzlU5zRMrVh16CrHk0kiVE95OejIHPGv7n0_3BHY/w640-h396/QPS%20relative%20to%20MySQL%205.6.51_%20range%20queries%20with%20aggregation.png" width="640"></a></div>
<p><span>Writes</span></p></div>
<div>
<ul>
<li>QPS improves after 5.6 by up to ~7X</li>
<li><span>a table for relative QPS by test&nbsp;</span><a href="https://gist.github.com/mdcallag/b743672d5a7df142a34488040d7bed54#file-gistfile1-txt-L37-L49">is here</a></li>
<li><span>HW efficiency metrics&nbsp;</span><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.my.latest#L477-L489">are here</a><span>&nbsp;insert</span></li>
<ul>
<li>QPS improves after 5.6.51 because CPU per statement drops</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRDnSyz_I2LxAClMeXK6Fv7ThDk37OXYMsVVHrCk5ebWbKSjGqbm6SmNFDI3GpTcR-uN6iGZeyFxO1CKH0XgB7D1WHtmWsj4wVH0ZOQtZvDMtKs97uTKKUbM6_jQbcCg_stYEApr7EOVqCyUW03skJKIqWqsOW4YNMo1oVFs8YadO0rZKX-3b5NyRigVGX/s600/QPS%20relative%20to%20MySQL%205.6.51_%20writes.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhRDnSyz_I2LxAClMeXK6Fv7ThDk37OXYMsVVHrCk5ebWbKSjGqbm6SmNFDI3GpTcR-uN6iGZeyFxO1CKH0XgB7D1WHtmWsj4wVH0ZOQtZvDMtKs97uTKKUbM6_jQbcCg_stYEApr7EOVqCyUW03skJKIqWqsOW4YNMo1oVFs8YadO0rZKX-3b5NyRigVGX/w640-h396/QPS%20relative%20to%20MySQL%205.6.51_%20writes.png" width="640"></a></div>
<p><b>Results: MariaDB</b></p></div>
<div><span>
<div><span><span><br></span></span></div>
<div><span><span>HW efficiency metrics&nbsp;</span></span><a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest">are here</a><span>. The have metrics from vmstat and iostat normalized by QPS.</span></div>
<div><span><span><br></span></span></div>
<div><span><span>
<div><span>Point queries</span></div>
<div>
<ul>
<li>QPS for 6 of 9 tests drops in half (or more) from 10.2 to 12.3</li>
<li>a table for relative QPS <a href="https://gist.github.com/mdcallag/b2ce4329503feeb6b4f93ca1c416e61f#file-gistfile1-txt-L13-L23">is here</a></li>
<li>most of the regressions arrive in 10.5 and the root cause might be remove support for innodb_buffer_pool_intances and only support one buffer pool instance</li>
<li>HW efficiency metrics <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest#L625-L649">are here</a> for points-covered-pk</li>
<ul>
<li>there are large increases in CPU overhead and the context switch rate starting in 10.5</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7YV0KdAv1Z_Gx6w430JvOXzA1J6qtJjK2bkYV56jl_WDNX9Ka9Olh3nSBbWMJD3gazXl4JC_FtD-xs_V4drq2oulhC6HMI88269Msci_DgHb5yLQmiSLWIDnHGFKKV6oL_KQ8wPu8RmoDe_RrVqFvwgYe-ko_tH1WwRGjdF4QnMfOkf2kPcM24OZ5YjeN/s600/QPS%20relative%20to%20MariaDB%2010.2.30_%20point%20queries.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7YV0KdAv1Z_Gx6w430JvOXzA1J6qtJjK2bkYV56jl_WDNX9Ka9Olh3nSBbWMJD3gazXl4JC_FtD-xs_V4drq2oulhC6HMI88269Msci_DgHb5yLQmiSLWIDnHGFKKV6oL_KQ8wPu8RmoDe_RrVqFvwgYe-ko_tH1WwRGjdF4QnMfOkf2kPcM24OZ5YjeN/w640-h396/QPS%20relative%20to%20MariaDB%2010.2.30_%20point%20queries.png" width="640"></a></div>
<p><span>Range queries without aggregation</span></p></div>
<div>
<ul>
<li>for range-covered-* and range-notcovered-pk there is a small regression in 10.4</li>
<li>for range-not-covered-si there is a large regression in 10.5 because this query does frequent point lookups on the PK to get missing columns</li>
<li>for scan there is a regression in 10.5 that goes away, but the regressions return in 10.11 and 11.4&nbsp;</li>
<li>a table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/b2ce4329503feeb6b4f93ca1c416e61f#file-gistfile1-txt-L25-L31">is here</a></li>
<li>HW efficiency metrics <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest">are here</a></li>
</ul>
</div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKNDY_QTl7eiVteNy2HA9WHun4Y8IAwqpwo30AtxxDFEzDyrpUnaXhZm4O61U1SueQoLvO6tqWmVs3cKBM7XqM0zQrknqXAlLng75xTOuWq-_QPr73FHj4kywTrBpGq43ShUWUNAdOtdY5XLz1iTLW_qehRGbUTTABxk8oLCmsziWjuLeT3xLJ9N4xqg6y/s600/QPS%20relative%20to%20MariaDB%2010.2.30_%20range%20queries%20without%20aggregation%20(1).png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhKNDY_QTl7eiVteNy2HA9WHun4Y8IAwqpwo30AtxxDFEzDyrpUnaXhZm4O61U1SueQoLvO6tqWmVs3cKBM7XqM0zQrknqXAlLng75xTOuWq-_QPr73FHj4kywTrBpGq43ShUWUNAdOtdY5XLz1iTLW_qehRGbUTTABxk8oLCmsziWjuLeT3xLJ9N4xqg6y/w640-h396/QPS%20relative%20to%20MariaDB%2010.2.30_%20range%20queries%20without%20aggregation%20(1).png" width="640"></a></div>
<div><span>Range queries with aggregation</span></div>
<div>
<ul>
<li>for most tests there are small regressions in 10.4 and 10.5</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/b2ce4329503feeb6b4f93ca1c416e61f#file-gistfile1-txt-L33-L42">is here</a></li>
<li>HW efficiency metrics&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest">are here</a></li>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgD7iYP_ZRtGK3FJZU1ZVxqrqW2Mtj1lPhH_qnsZHo1N2H56weTl0i-4cYFuA5kriVJWi-rwJiCCEFVAozuYjq49lQ1g1DbfYhUp1noG7Qnh94Z5VTPGYf1rzi0NjBQUP-4Instv5KAUINMujXz7iBZn6YxjlkzehQo6Z-sRo8xBSvcjBFfYAquAR3FV1-W/s600/QPS%20relative%20to%20MariaDB%2010.2.30_%20range%20queries%20with%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgD7iYP_ZRtGK3FJZU1ZVxqrqW2Mtj1lPhH_qnsZHo1N2H56weTl0i-4cYFuA5kriVJWi-rwJiCCEFVAozuYjq49lQ1g1DbfYhUp1noG7Qnh94Z5VTPGYf1rzi0NjBQUP-4Instv5KAUINMujXz7iBZn6YxjlkzehQo6Z-sRo8xBSvcjBFfYAquAR3FV1-W/w640-h396/QPS%20relative%20to%20MariaDB%2010.2.30_%20range%20queries%20with%20aggregation.png" width="640"></a></div>
<p><span>Writes</span></p></div>
<div>
<ul>
<li>for most tests modern MariaDB is faster than 10.2</li>
<li>table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/b2ce4329503feeb6b4f93ca1c416e61f#file-gistfile1-txt-L44-L55">is here</a></li>
<li>HW efficiency metrics&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.ma.latest">are here</a></li>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIDUeUa6qlZg1vVirAqXU8PJKc0cLBbmu6HCe1fcJThzTot-smu5bEuv1Boon-0kxOKZ9Qu3pfFUvN_yikFW6WVzknonj3ob7JgC17SX7tLryNqOYbpQCCEO-YkmaWjJsvc9zZxj9zwTa4kZ9AmV-JKi_eeyhZ6uul1gND178hQgRmF3PdPVZndAzuyN26/s600/QPS%20relative%20to%20MariaDB%2010.2.30_%20writes.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhIDUeUa6qlZg1vVirAqXU8PJKc0cLBbmu6HCe1fcJThzTot-smu5bEuv1Boon-0kxOKZ9Qu3pfFUvN_yikFW6WVzknonj3ob7JgC17SX7tLryNqOYbpQCCEO-YkmaWjJsvc9zZxj9zwTa4kZ9AmV-JKi_eeyhZ6uul1gND178hQgRmF3PdPVZndAzuyN26/w640-h396/QPS%20relative%20to%20MariaDB%2010.2.30_%20writes.png" width="640"></a></div>
<p><b>Results: Postgres</b></p></div>
<p></p></span></span></div>
<div><span><span>
<div><span><span><br></span></span></div>
<div>HW efficiency metrics&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest">are here</a><span>. They have metrics from vmstat and iostat normalized by QPS.</span></div>
<p></p></span></span></div>
<p></p></span></div>
<div></div>
<div>
<div><span>Point queries</span></div>
<div>
<ul>
<li>QPS for hot-points increased by ~2.5X starting in Postgres 17.x</li>
<li>otherwise QPS is stable from 12.22 through 18.2</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/34402f015bf976d2452b871a1b0ace84#file-gistfile1-txt-L9-L19">is here</a></li>
<li>HW efficiency metrics for the hot-points test <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest#L415-L431">are here</a></li>
<ul>
<li>CPU drops by more than half starting in 17.x</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDR1TNU6JDQTI4Y-6rP7CplRrZ_bKi82LP1cn7-KmiPsAMVkT4zXvr0V0Van8tqkBJreC-JY3oQHijDCH-viO_nK0K8sF-tveeRw5ClTUFaDe233VxUu4SUyuroGkjEj-SxhxbUNapK9XhMMYrnulXkJTKojMi4-WTe7C_asP01by3mn1Kmf3s8EUf7Mck/s600/QPS%20relative%20to%20Postgres%2012.22_%20point%20queries.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDR1TNU6JDQTI4Y-6rP7CplRrZ_bKi82LP1cn7-KmiPsAMVkT4zXvr0V0Van8tqkBJreC-JY3oQHijDCH-viO_nK0K8sF-tveeRw5ClTUFaDe233VxUu4SUyuroGkjEj-SxhxbUNapK9XhMMYrnulXkJTKojMi4-WTe7C_asP01by3mn1Kmf3s8EUf7Mck/w640-h396/QPS%20relative%20to%20Postgres%2012.22_%20point%20queries.png" width="640"></a></div>
<p><span>Range queries without aggregation</span></p></div>
<div>
<ul>
<li>QPS is stable for the range-not-covered-* and scan tests</li>
<li>QPS drops almost in half for the range-covered-* tests</li>
<li>a table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/34402f015bf976d2452b871a1b0ace84#file-gistfile1-txt-L21-L27">is here</a></li>
<li>all versions use the same query plan for the range-covered-pk test</li>
<li>HW efficiency metrics are here <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest#L469-L485">for range-covered-pk</a> and for <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest#L559-L575">range-covered-si</a></li>
<ul>
<li>An increase in CPU overhead explains the regressions for range-covered-*</li>
<li>I hope to get flamegraphs and thread stacks for these tests to explain what happens</li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJNz0U4RR5pJIvkthpcLoL7mBpl-W_pnQcOesqGmzZHXxXol-u4I7VX9b1CGUG89JxNipOjlV80pJjVb8e8tZT_uFAaNpmSeskHM4dxUShUdY_us2FCV9Z8hRqZxKrOz-foa1tVn61AbGv92u6kI__9nqlGBOVSh5lhL5Bp-9DF0GBpAh6D6sLp7Cq2iIF/s600/QPS%20relative%20to%20Postgres%2012.22_%20range%20queries%20without%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJNz0U4RR5pJIvkthpcLoL7mBpl-W_pnQcOesqGmzZHXxXol-u4I7VX9b1CGUG89JxNipOjlV80pJjVb8e8tZT_uFAaNpmSeskHM4dxUShUdY_us2FCV9Z8hRqZxKrOz-foa1tVn61AbGv92u6kI__9nqlGBOVSh5lhL5Bp-9DF0GBpAh6D6sLp7Cq2iIF/w640-h396/QPS%20relative%20to%20Postgres%2012.22_%20range%20queries%20without%20aggregation.png" width="640"></a></div>
<p><span>Range queries with aggregation</span></p></div>
<div>
<ul>
<li>QPS is stable from 12.22 through 18.2</li>
<li>a table for relative QPS by test&nbsp;<a href="https://gist.github.com/mdcallag/34402f015bf976d2452b871a1b0ace84#file-gistfile1-txt-L29-L38">is here</a></li>
<li>HW efficiency metrics <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest">are here</a></li>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4UtG2u8m5vWLliRC-tXbp54BNtXuJ5ykqnsfsz5O8qXcHmfTBbe1635Jul6OfETnWSBUGnTd6Qz7dO3Yp3pipGQAdsXIygZSl3xWT5godbg_vpCkzkO24daRq_9gLWCWgA0IRO1hfJyHTNJSo-U2veMBIeKZENBya9wrqWAXHSbNvK4I49elaD1LibHct/s600/QPS%20relative%20to%20Postgres%2012.22_%20range%20queries%20with%20aggregation.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh4UtG2u8m5vWLliRC-tXbp54BNtXuJ5ykqnsfsz5O8qXcHmfTBbe1635Jul6OfETnWSBUGnTd6Qz7dO3Yp3pipGQAdsXIygZSl3xWT5godbg_vpCkzkO24daRq_9gLWCWgA0IRO1hfJyHTNJSo-U2veMBIeKZENBya9wrqWAXHSbNvK4I49elaD1LibHct/w640-h396/QPS%20relative%20to%20Postgres%2012.22_%20range%20queries%20with%20aggregation.png" width="640"></a></div>
<p><span>Writes</span></p></div>
</div>
<div>
<ul>
<li>QPS is stable for 5 of 10 tests</li>
<li>QPS improves by up to 1.7X for the other 5 tests, most of that arrives in 17.x</li>
<li>a table for relative QPS by test <a href="https://gist.github.com/mdcallag/34402f015bf976d2452b871a1b0ace84#file-gistfile1-txt-L40-L51">is here</a></li>
<li>HW efficiency metrics are here for <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest#L73-L89">update-index</a></li>
<ul>
<li>CPU overhead and context switch rates drop almost in half <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/apr26.sb.hetz/o.met.pg.latest#L73-L89">starting in 17.x</a></li>
</ul>
</ul>
</div>
<div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWd7lCq0eEu0kAIABRifbRkJfQU3syKs3dBCls-MjA1t770TbY_AXBBYdArEvwJY0hhBdMLpQrq4nPa5MZUo6rk0jE-Sw2QrJcUUAmuORLDlgBDQrhBBUr0PSnJd5x6J0DZD7z6EGTvryVS2qS3rfWo_QRLfUBvfajisATtUHZDa4c3bG91oUa8q0rVOjy/s600/QPS%20relative%20to%20Postgres%2012.22_%20writes.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWd7lCq0eEu0kAIABRifbRkJfQU3syKs3dBCls-MjA1t770TbY_AXBBYdArEvwJY0hhBdMLpQrq4nPa5MZUo6rk0jE-Sw2QrJcUUAmuORLDlgBDQrhBBUr0PSnJd5x6J0DZD7z6EGTvryVS2qS3rfWo_QRLfUBvfajisATtUHZDa4c3bG91oUa8q0rVOjy/w640-h396/QPS%20relative%20to%20Postgres%2012.22_%20writes.png" width="640"></a></div>
<p><span><br></span></p></div>
<div><span><br></span></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<ul></ul>
</div>
</div>
</div>
</div>
</div>
<p></p></span></span></div>

<p><a href="https://smalldatum.blogspot.com/2026/04/cpu-bound-sysbench-on-large-server.html">CPU-bound sysbench on a large server: Postgres, MySQL and MariaDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Bug Report: March 2026</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/03/percona-bug-report-march-2026/" />
      <id>https://percona.community/blog/2026/04/03/percona-bug-report-march-2026/</id>
      <updated>2026-04-03T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>At Percona, we operate on the premise that full transparency makes a product better. We strive to build the best open-source database products, but also to help you manage any issues that arise in any of the databases that we support. And, in true open-source form, report back on any issues or bugs you might encounter along the way.</p>
<p><a href="https://percona.community/blog/2026/04/03/percona-bug-report-march-2026/">Percona Bug Report: March 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>At Percona, we operate on the premise that full transparency makes a product better. We strive to build the best open-source database products, but also to help you manage any issues that arise in any of the databases that we support. And, in true open-source form, report back on any issues or bugs you might encounter along the way.</p>
<p>We constantly update our <a href="https://perconadev.atlassian.net/" target="_blank" rel="noopener noreferrer">bug reports</a> and monitor <a href="https://bugs.mysql.com/" target="_blank" rel="noopener noreferrer">other boards</a> to ensure we have the latest information, but we wanted to make it a little easier for you to keep track of the most critical ones. This post is a central place to get information on the most noteworthy open and recently resolved bugs.</p>
<p>In this edition of our bug report, we have the following list of bugs.</p>
<hr>
<h2>Percona Server/MySQL Bugs<a class="anchor-link" id="percona-server-mysql-bugs"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PS-10378" target="_blank" rel="noopener noreferrer">PS-10378</a>: In the MeCab plugin, BOOLEAN MODE full-text queries with a LIMIT clause do not behave as expected. Although the optimizer indicates that ranking should be skipped (Ft_hints: no_ranking), the query still performs full ranking and sorting before applying LIMIT, preventing the intended optimization and impacting performance.</p>
<p><strong>Reported Affected Version/s</strong>: 8.4.x<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46-37, 8.4.9-9, 9.7.0-0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PS-10448" target="_blank" rel="noopener noreferrer">PS-10448</a>: Insert prepared statements fail on partitioned tables with timestamp-based partitions when the partition key uses a non-constant default (e.g., <strong>CURRENT_TIMESTAMP</strong>). After initial execution, the statement remains bound to the original partition and fails with a partition mismatch error when data should go into a different partition.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.42-33, 8.0.43-34, 8.0.44-35, 8.4.7-7<br>
<strong>Upstream Bug</strong>: <a href="https://bugs.mysql.com/bug.php?id=119309" target="_blank" rel="noopener noreferrer">Bug #119309</a><br>
<strong>Workaround/Fix</strong>: Modify statements to explicitly use <strong>NOW()</strong> (requires updating procedures)<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46-37, 8.4.9-9, 9.7.0-0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PS-10481" target="_blank" rel="noopener noreferrer">PS-10481</a>: The range optimizer incorrectly falls back to a full table scan instead of using an index range scan for WHERE &hellip; IN() queries when values exceed column or prefix length on non-binary collations (e.g. utf8mb4_0900_ai_ci). A single truncated value in IN() can invalidate all valid ranges, forcing a full scan and degrading performance.</p>
<p><strong>Reported Affected Version/s</strong>: 8.4.x<br>
<strong>Upstream Bug</strong>: <a href="https://bugs.mysql.com/bug.php?id=118009" target="_blank" rel="noopener noreferrer">Bug #118009</a><br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: Not fixed yet</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PS-10593" target="_blank" rel="noopener noreferrer">PS-10593</a>: The audit_log plugin can crash (segfault) during memcpy operations when configured with audit_log_strategy=PERFORMANCE, audit_log_policy=ALL, and buffering enabled. The issue can be reproduced under specific memory allocator setups (e.g., jemalloc) and also occurs with standard libc malloc, indicating instability in the plugin&rsquo;s memory handling.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.34-26, 8.0.45-36, 8.4.7-7<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46-37, 8.4.9-9</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PS-10990" target="_blank" rel="noopener noreferrer">PS-10990</a>: Server crashes (signal 11) in Item_cache::walk when executing queries that use JOIN with a subquery in an IN clause inside stored procedures. The issue occurs during query execution/privilege checking and is reproducible across MySQL and Percona Server 8.0.x versions.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.45-36<br>
<strong>Upstream Bug</strong>: <a href="https://bugs.mysql.com/bug.php?id=115885" target="_blank" rel="noopener noreferrer">Bug #115885</a><br>
<strong>Workaround/Fix</strong>: Execute the query outside the stored procedure<br>
<strong>Fixed/Planned Version/s</strong>: Not specified</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PS-10578" target="_blank" rel="noopener noreferrer">PS-10578</a>: The legacy audit_log plugin does not populate the DB field in audit records unless the session is started with the &ndash;database option. Even when a database is selected later using USE or referenced explicitly in queries, the DB field may remain empty.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.43-34, 8.0.45-36<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Use Audit Log Filter component (8.4) or audit log filter (8.0), where this issue is not reproducible<br>
<strong>Fixed/Planned Version/s</strong>: Not planned to be fixed</p>
<hr>
<h2>Percona Xtradb Cluster<a class="anchor-link" id="percona-xtradb-cluster"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4844" target="_blank" rel="noopener noreferrer">PXC-4844</a>: In PXC clusters under high load, inconsistency voting during DDL or DCL operations can trigger an internal deadlock, causing standby nodes to get stuck applying transactions and continuously request FC pause. Although voting completes successfully and no node is expelled, writes remain blocked in wsrep: replicating and certifying write set, effectively stalling the cluster until the affected node is restarted.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.42<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Restart the blocked standby node to restore cluster activity<br>
<strong>Fixed/Planned Version/s</strong>: Not fixed yet</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4799" target="_blank" rel="noopener noreferrer">PXC-4799</a>: In PXC clusters, when a backup lock (<strong>LOCK INSTANCE FOR BACKUP</strong>) is active and a replicated DDL is pending, executing <strong>FLUSH TABLES WITH READ LOCK</strong> on the same node can trigger a deadlock. This results in an inconsistency vote and causes the node to leave the cluster, disrupting backup operations.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.42, 8.0.43, 8.4.6<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Avoid running DDL operations during backup or use a single backup instance instead of parallel runs<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46, 8.4.9, 9.7.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4814" target="_blank" rel="noopener noreferrer">PXC-4814</a>: In PXC with <strong>wsrep_OSU_method=&lsquo;RSU&rsquo;</strong>, a failed DDL due to table name case mismatch (e.g., <strong>OPTIMIZE TABLE</strong>) is incorrectly written to the binary log as a successful transaction (<strong>error_code=0</strong>). This results in a GTID being generated for a failed operation, causing GTID inconsistencies across cluster nodes and in replication setups.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.33-25, 8.0.44, 8.4.6<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Validate table name case sensitivity before executing DDL in RSU mode<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.45, 8.4.8, 9.6.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4845" target="_blank" rel="noopener noreferrer">PXC-4845</a>: After an IST failure (e.g., due to network issues), a PXC node may remain running in an inconsistent state instead of restarting, causing the donor and other nodes to become unresponsive. The joiner node gets stuck during state transfer instead of failing cleanly, impacting overall cluster availability.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.42<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.45, 8.4.8, 9.6.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4849" target="_blank" rel="noopener noreferrer">PXC-4849</a>: A PXC node fails to start after successful SST when <strong>read_only</strong> or <strong>super_read_only</strong> is enabled and event scheduler objects exist on the donor. During initialization, the event scheduler fails to load, causing the node to abort, making it impossible to run read-only nodes with events defined in the cluster.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.44, 8.4.7<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Start the node without read_only, then enable it manually later, or remove events<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46, 8.4.9, 9.7.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-4965" target="_blank" rel="noopener noreferrer">PXC-4965</a>: Passwords containing the <code>'</code> character are incorrectly handled, causing syntax errors during replication (e.g., <strong>SET PASSWORD</strong>) and triggering inconsistency voting that can force a node to leave the cluster.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.45, 8.4.7<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Avoid using <code>'</code> character in passwords<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46, 8.4.8, 9.6.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXC-5198" target="_blank" rel="noopener noreferrer">PXC-5198</a>: Executing <strong>SELECT &hellip; FOR UPDATE SKIP LOCKED</strong> can trigger InnoDB crashes with fatal errors (e.g., &ldquo;Unknown error code 21: Skip locked records&rdquo;) under concurrent transactional workloads. Instead of returning expected deadlock errors, the query causes mysqld to abort, impacting cluster stability.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.33-25, 8.0.35-27, 8.0.36-28<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Avoid using <strong>SKIP LOCKED</strong> in <strong>SELECT &hellip; FOR UPDATE</strong> queries<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.46, 8.4.8, 9.6.0</p>
<hr>
<h2>Percona XtraBackup<a class="anchor-link" id="percona-xtrabackup"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PXB-3543" target="_blank" rel="noopener noreferrer">PXB-3543</a>: Incremental backups in XtraBackup can become significantly slower than full backups on instances with a very large number of small tables, due to excessive CPU usage in memset during incremental processing. This leads to severe performance degradation, with incremental backups taking hours compared to minutes for full backups.</p>
<p><strong>Reported Affected Version/s</strong>: 8.0.35-33, 8.0.35-34<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Use full backups instead of incremental backups<br>
<strong>Fixed/Planned Version/s</strong>: 8.0.35-35, 8.4.0-6, 9.6.0-1</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PXB-3667" target="_blank" rel="noopener noreferrer">PXB-3667</a>: Installation of XtraBackup 8.4 fails on RHEL 9&ndash;based systems due to dependency conflicts between percona-xtrabackup-84, perl(DBD::mysql), and incompatible libmysqlclient versions. Percona Server 8.4 provides libmysqlclient.so.24, while required dependencies expect libmysqlclient.so.21, resulting in unresolved package installation errors.</p>
<p><strong>Reported Affected Version/s</strong>: 8.4.0-5<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: Not specified</p>
<hr>
<h2>Percona Toolkit<a class="anchor-link" id="percona-toolkit"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PT-2519" target="_blank" rel="noopener noreferrer">PT-2519</a>: pt-query-digest fails when processing large, slow query logs, repeatedly throwing &ldquo;Argument &ldquo;&rdquo; isn&rsquo;t numeric&rdquo; errors during the aggregate fingerprint stage. The tool retries multiple times but does not complete, resulting in stalled analysis and very slow progress.</p>
<p><strong>Reported Affected Version/s</strong>: 3.7.0, 3.7.1<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 3.7.3</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PT-2511" target="_blank" rel="noopener noreferrer">PT-2511</a>: pt-summary incorrectly reports that sshd is not running due to an invalid awk expression used to detect the process. The script checks the wrong field in ps output, causing false negatives even when sshd is active.</p>
<p><strong>Reported Affected Version/s</strong>: 3.7.1<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 3.7.3</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PT-2516" target="_blank" rel="noopener noreferrer">PT-2516</a>: pt-mongodb-index-check fails to detect duplicate indexes (e.g., <code>{a:1}</code> and <code>{a:1, b:1}</code>) and may produce no output, making it unclear whether the tool is functioning or connecting properly.</p>
<p><strong>Reported Affected Version/s</strong>: 3.7.1<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: Not specified</p>
<hr>
<h2>PMM [Percona Monitoring and Management]<a class="anchor-link" id="pmm-percona-monitoring-and-management"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14493" target="_blank" rel="noopener noreferrer">PMM-14493</a>: PMM fails to start when using Podman with the <strong>&ndash;log-driver passthrough</strong> option due to an error opening /dev/stderr during Nginx initialization. This causes the container to exit with configuration test failure, while other log drivers work as expected.</p>
<p><strong>Reported Affected Version/s</strong>: 3.4.0, 3.4.1<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Use a different <strong>&ndash;log-driver</strong> option such as none or journald<br>
<strong>Fixed/Planned Version/s</strong>: 3.8.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14576" target="_blank" rel="noopener noreferrer">PMM-14576</a>: PMM Client reports &ldquo;failed to get backup status&rdquo; errors during MongoDB backups, marking them as failed in the UI even though backups are successfully completed by PBM. This leads to incorrect backup status reporting and confusion for users.</p>
<p><strong>Reported Affected Version/s</strong>: 3.5.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Avoid using PMM Backup Management (not ideal)<br>
<strong>Fixed/Planned Version/s</strong>: 3.9.0, 3.X</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14594" target="_blank" rel="noopener noreferrer">PMM-14594</a>: PMM incorrectly reports compatible XtraBackup versions as incompatible with supported MySQL versions during backup validation. This causes backups to be blocked in PMM even when the installed XtraBackup version is the latest available and should be accepted.</p>
<p><strong>Reported Affected Version/s</strong>: 3.5.0, 3.6.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Use the xtrabackup command-line tool to take backups<br>
<strong>Fixed/Planned Version/s</strong>: 3.9.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14852" target="_blank" rel="noopener noreferrer">PMM-14852</a>: Some panels in the MongoDB InMemory dashboard show no data because they incorrectly use WiredTiger-specific metrics. As a result, dashboards for InMemory storage engine deployments can display empty or misleading panels instead of relevant metrics.</p>
<p><strong>Reported Affected Version/s</strong>: 3.2.0, 3.6.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 3.8.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14906" target="_blank" rel="noopener noreferrer">PMM-14906</a>: The postgres_exporter generates excessive <strong>SELECT version()</strong> queries (~4500/hour) after upgrading to PMM 3.6.0, flooding PostgreSQL logs and increasing unnecessary query load, causing log spam and disk growth.</p>
<p><strong>Reported Affected Version/s</strong>: 3.6.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 3.8.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PMM-14958" target="_blank" rel="noopener noreferrer">PMM-14958</a>: mysqld_exporter continues to generate duplicate metric collection errors with GTID and parallel replication enabled, even in PMM 3.6.0. These repeated errors (e.g., <strong>mysql_perf_schema_replication_group_worker_transport_time_seconds</strong>) lead to continuous log spam, causing rapid log growth (up to ~10GB/hour), disk space exhaustion, and increased noise that makes it difficult to identify real issues.</p>
<p><strong>Reported Affected Version/s</strong>: 3.6.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 3.7.1</p>
<hr>
<h2>Percona Kubernetes Operator<a class="anchor-link" id="percona-kubernetes-operator"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/K8SPG-737" target="_blank" rel="noopener noreferrer">K8SPG-737</a>: In PostgreSQL Kubernetes deployments, the node_exporter in the PMM client sidecar cannot access the datadir mountpoint because it is not exposed via /proc, preventing collection of datadir-related metrics. This results in incomplete monitoring data for PostgreSQL pods.</p>
<p><strong>Reported Affected Version/s</strong>: 2.9.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 2.10.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPXC-1737" target="_blank" rel="noopener noreferrer">K8SPXC-1737</a>: The PXC Operator crashes during reconciliation in CompareMySQLVersion when the cluster status lacks a MySQL version value. An empty version field causes a panic (&ldquo;Malformed version&rdquo;), preventing proper cluster reconciliation and replication setup.</p>
<p><strong>Reported Affected Version/s</strong>: 1.18.0, 1.19.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Create the cluster before configuring replication or manually patch the CR status to include the missing version value, for example:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">kubectl patch pxc  
</span></span><span class="line"><span class="cl"> --type=merge 
</span></span><span class="line"><span class="cl"> --subresource=status 
</span></span><span class="line"><span class="cl"> --patch '
</span></span><span class="line"><span class="cl">status:
</span></span><span class="line"><span class="cl"> pxc:
</span></span><span class="line"><span class="cl"> version: "8.0.42-33.1"'</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Fixed/Planned Version/s:</strong> 1.20.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPXC-1843" target="_blank" rel="noopener noreferrer">K8SPXC-1843</a>: Backups can get stuck in a Running state if the Joiner/Garbd disconnects from the Donor (e.g., due to sst-idle-timeout). Even after the SST process fails and the donor leaves the cluster, the backup process (e.g., xbcloud put) continues indefinitely without timing out, preventing backup completion.</p>
<p><strong>Reported Affected Version/s</strong>: 1.19.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 1.20.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPXC-1831" target="_blank" rel="noopener noreferrer">K8SPXC-1831</a>: When using mysqlAllocator=jemalloc on ARM images, the operator attempts to preload /usr/lib64/libjemalloc.so.1, but only libjemalloc.so.2 is available. This results in preload errors and prevents proper use of the jemalloc allocator.</p>
<p><strong>Reported Affected Version/s</strong>: 1.19.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 1.20.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPXC-1830" target="_blank" rel="noopener noreferrer">K8SPXC-1830</a>: ProxySQL monitoring fails in PMM when using caching_sha2_password, causing proxysql_exporter to fail authentication with errors like:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error opening connection to ProxySQL:
</span></span><span class="line"><span class="cl">unexpected resp from server for caching_sha2_password, perform full authentication</span></span></code></pre>
</div>
</div>
</div>
<p>This occurs because ProxySQL does not support the required RSA-based full authentication, breaking PMM monitoring integration.</p>
<p><strong>Reported Affected Version/s</strong>: 1.19.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Use <code>mysql_native_password</code><br>
<strong>Fixed/Planned Version/s</strong>: 1.20.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1617" target="_blank" rel="noopener noreferrer">K8SPSMDB-1617</a>: Scheduled backups can be triggered even when the MongoDB cluster is not ready (e.g., in initializing state) and without the required safety flags. This leads to failed backup attempts and inconsistent backup behaviour.</p>
<p><strong>Reported Affected Version/s</strong>: 1.22.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: Not specified</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPSMDB-1524" target="_blank" rel="noopener noreferrer">K8SPSMDB-1524</a>: The PBM agent continuously triggers resync storage operations, causing backup processes to stall or remain in pending/unknown states. Logs show repeated resync commands being executed without completion, leading to unstable backup behaviour.</p>
<p><strong>Reported Affected Version/s</strong>: 1.21.1<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 1.22.0</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/K8SPG-939" target="_blank" rel="noopener noreferrer">K8SPG-939</a>: Patroni does not propagate labels defined in the PostgreSQL Operator CR, causing failures in environments with strict label policies. As a result, Kubernetes rejects resource creation (e.g., Services) due to missing mandatory labels, preventing cluster reconciliation.</p>
<p><strong>Reported Affected Version/s</strong>: 2.8.2<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 2.9.0</p>
<hr>
<h2>PBM [Percona Backup for MongoDB]<a class="anchor-link" id="pbm-percona-backup-for-mongodb"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PBM-1683" target="_blank" rel="noopener noreferrer">PBM-1683</a>: The size_uncompressed_h field in pbm describe-backup reports incorrect (inflated) sizes for non-base incremental backups, showing significantly larger values than the actual data size and leading to misleading backup size reporting.</p>
<p><strong>Reported Affected Version/s</strong>: 2.10.0, 2.11.0, 2.12.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 2.14.0</p>
<hr>
<h2>PSMDB [Percona Server for MongoDB]<a class="anchor-link" id="psmdb-percona-server-for-mongodb"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PSMDB-1915" target="_blank" rel="noopener noreferrer">PSMDB-1915</a>: Newer PSMDB packages fail to install or upgrade on RHEL 9.4 due to a dependency on OpenSSL 3.4, which is not available in that OS version. This breaks upgrades (e.g., from 6.0.25 to 6.0.27) and affects multiple major versions.</p>
<p><strong>Reported Affected Version/s</strong>: 6.0.27-21, 7.0.28-15, 8.0.17-6<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 6.0.27-21, 7.0.28-15, 8.0.17-6</p>
<hr>
<p><a href="https://perconadev.atlassian.net/browse/PSMDB-1998" target="_blank" rel="noopener noreferrer">PSMDB-1998</a>: LDAP authentication can hang indefinitely when the LDAP server is unreachable due to missing timeout handling. This leads to continuously accumulating connections, eventually exhausting file descriptors and causing service disruption or crashes.</p>
<p><strong>Reported Affected Version/s</strong>: 7.0.16-10, 7.0.30-16<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: No workaround available<br>
<strong>Fixed/Planned Version/s</strong>: 7.0.31-17, 8.0.20-8</p>
<hr>
<h2>Percona Distribution for MySQL [Orchestrator]<a class="anchor-link" id="percona-distribution-for-mysql-orchestrator"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/DISTMYSQL-584" target="_blank" rel="noopener noreferrer">DISTMYSQL-584</a>: Orchestrator loses SSL-related settings such as SOURCE_SSL_CA and SOURCE_SSL_VERIFY_SERVER_CERT during failover when issuing CHANGE REPLICATION SOURCE, causing replication to run without required security configurations and potentially violating compliance requirements.</p>
<p><strong>Reported Affected Version/s</strong>: 8.4.7<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: Not specified</p>
<hr>
<h2>PCSM [Percona ClusterSync for MongoDB]<a class="anchor-link" id="pcsm-percona-clustersync-for-mongodb"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PCSM-294" target="_blank" rel="noopener noreferrer">PCSM-294</a>: PCSM replication can crash during change replication due to flawed conflict detection and unbatched pipeline generation. This results in oversized aggregation pipelines, memory exhaustion, or invalid $slice operations, causing replication to fail with errors such as stage limit exceeded, buffer limits, or invalid arguments.</p>
<p><strong>Reported Affected Version/s</strong>: 0.7.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: 0.8.0</p>
<hr>
<h2>PG_TDE [Percona Transparent Data Encryption for PostgreSQL]<a class="anchor-link" id="pg_tde-percona-transparent-data-encryption-for-postgresql"></a></h2>
<p><a href="https://perconadev.atlassian.net/browse/PG-2125" target="_blank" rel="noopener noreferrer">PG-2125</a>: pg_tde fails to create/register symmetric keys when using HashiCorp KMIP, returning errors from the KMIP server during key registration. This prevents key setup and blocks encryption workflows for users relying on KMIP integration.</p>
<p><strong>Reported Affected Version/s</strong>: pg_tde 2.1.0<br>
<strong>Upstream Bug</strong>: Not applicable<br>
<strong>Workaround/Fix</strong>: Not specified<br>
<strong>Fixed/Planned Version/s</strong>: pg_tde NEXT</p>
<hr>
<h2>Summary<a class="anchor-link" id="summary"></a></h2>
<p>We welcome community input and feedback on all our products. If you find a bug or would like to suggest an improvement or a feature, learn how in our post, <a href="https://www.percona.com/blog/2019/06/12/report-bugs-improvements-new-feature-requests-for-percona-products/" target="_blank" rel="noopener noreferrer">How to Report Bugs, Improvements, New Feature Requests for Percona Products</a>.</p>
<p>For the most up-to-date information, be sure to follow us on <a href="https://twitter.com/percona" target="_blank" rel="noopener noreferrer">Twitter</a>, <a href="https://www.linkedin.com/company/percona" target="_blank" rel="noopener noreferrer">LinkedIn</a>, and <a href="https://www.facebook.com/Percona?fref=ts" target="_blank" rel="noopener noreferrer">Facebook</a>.</p>
<p>Quick References:</p>
<p><a href="https://jira.percona.com/" target="_blank" rel="noopener noreferrer">Percona JIRA</a></p>
<p><a href="https://bugs.mysql.com/" target="_blank" rel="noopener noreferrer">MySQL Bug Report</a></p>
<p><a href="https://www.percona.com/blog/2019/06/12/report-bugs-improvements-new-feature-requests-for-percona-products/" target="_blank" rel="noopener noreferrer">Report a Bug in a Percona Product</a></p>

<p><a href="https://percona.community/blog/2026/04/03/percona-bug-report-march-2026/">Percona Bug Report: March 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>InnoDB Buffer Pool Tuning: From Rule-of-Thumb to Real Signals</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/04/02/innodb-buffer-pool-tuning-from-rule-of-thumb-to-real-signals/" />
      <id>https://percona.community/blog/2026/04/02/innodb-buffer-pool-tuning-from-rule-of-thumb-to-real-signals/</id>
      <updated>2026-04-02T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Introduction Many MySQL setups begin life with a familiar incantation:</p>
<p><a href="https://percona.community/blog/2026/04/02/innodb-buffer-pool-tuning-from-rule-of-thumb-to-real-signals/">InnoDB Buffer Pool Tuning: From Rule-of-Thumb to Real Signals</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Introduction<a class="anchor-link" id="introduction"></a></h2>
<p>Many MySQL setups begin life with a familiar incantation:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">innodb_buffer_pool_size = 70% of RAM</span></span></code></pre>
</div>
</div>
</div>
<p>&hellip;and then nothing changes.</p>
<p>That&rsquo;s not tuning. That&rsquo;s a starting guess.</p>
<p>Real tuning starts when the workload pushes back.</p>
<hr>
<h2>Visual Overview<a class="anchor-link" id="visual-overview"></a></h2>
<p><figure><img decoding="async" width="1024" height="1536" src="https://percona.community/blog/2026/04/innodb_buffer_pool_diagram_hu_fccfce324d38a928.webp" alt="InnoDB Buffer Pool Diagram" loading="lazy"></figure>
</p>
<hr>
<p>The InnoDB buffer pool is where database performance is quietly decided. It determines whether your workload hums along in memory or drags itself across disk. If you&rsquo;re not actively observing and tuning it, you&rsquo;re leaving performance on the table.</p>
<p>This guide walks through how to monitor, understand, and tune the buffer pool using real signals instead of guesswork.</p>
<hr>
<h2>What the Buffer Pool Really Is<a class="anchor-link" id="what-the-buffer-pool-really-is"></a></h2>
<p>The buffer pool isn&rsquo;t just &ldquo;memory for MySQL.&rdquo; It&rsquo;s a living system under constant pressure:</p>
<ul>
<li>A cache of data and indexes</li>
<li>A write staging area (dirty pages)</li>
<li>A contention zone between reads, writes, and eviction</li>
</ul>
<p>Think of it as your database&rsquo;s working memory. If your working set fits, queries glide. If it doesn&rsquo;t, pages are constantly evicted and reloaded, introducing latency that rarely announces itself clearly.</p>
<hr>
<h2>A Simple Mental Model<a class="anchor-link" id="a-simple-mental-model"></a></h2>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"> +---------------------------+
</span></span><span class="line"><span class="cl"> | Buffer Pool |
</span></span><span class="line"><span class="cl"> |---------------------------|
</span></span><span class="line"><span class="cl">Reads ---&gt; | Cached Pages |
</span></span><span class="line"><span class="cl"> | |
</span></span><span class="line"><span class="cl">Writes ---&gt; | Dirty Pages (pending IO) |
</span></span><span class="line"><span class="cl"> | |
</span></span><span class="line"><span class="cl">Eviction -&gt; | LRU / Free List |
</span></span><span class="line"><span class="cl"> +---------------------------+
</span></span><span class="line"><span class="cl"> |
</span></span><span class="line"><span class="cl"> v
</span></span><span class="line"><span class="cl"> Disk (slow)</span></span></code></pre>
</div>
</div>
</div>
<p>Three forces are always competing:</p>
<ul>
<li>Reads want hot data in memory</li>
<li>Writes generate dirty pages</li>
<li>Eviction makes room under pressure</li>
</ul>
<p>Your job is to keep this system balanced.</p>
<hr>
<h2>How to Monitor the Buffer Pool<a class="anchor-link" id="how-to-monitor-the-buffer-pool"></a></h2>
<h3>Option 1: Quick Snapshot<a class="anchor-link" id="option-1-quick-snapshot"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">ENGINE</span><span class="w"> </span><span class="n">INNODB</span><span class="w"> </span><span class="n">STATUS</span><span class="err"></span><span class="k">G</span></span></span></code></pre>
</div>
</div>
</div>
<p>Useful for human inspection. Look for:</p>
<ul>
<li>Buffer pool size</li>
<li>Free buffers</li>
<li>Database pages</li>
<li>Modified (dirty) pages</li>
<li>Page read/write rates</li>
</ul>
<p>Great for debugging. Not ideal for automation.</p>
<hr>
<h3>Option 2: Structured Metrics (Recommended)<a class="anchor-link" id="option-2-structured-metrics-recommended"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">pool_id</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">free_buffers</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">database_pages</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">modified_database_pages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Key fields:</strong></p>
<ul>
<li><code>free_buffers</code> &rarr; Available pages (breathing room)</li>
<li><code>database_pages</code> &rarr; Pages holding data</li>
<li><code>modified_database_pages</code> &rarr; Dirty pages waiting to flush</li>
</ul>
<p>Great for automation.</p>
<hr>
<h2>The 5 Signals That Actually Matter<a class="anchor-link" id="the-5-signals-that-actually-matter"></a></h2>
<h3>1. Buffer Pool Hit Ratio (Handle With Care)<a class="anchor-link" id="1-buffer-pool-hit-ratio-handle-with-care"></a></h3>
<p>Yes, it&rsquo;s widely used. No, it&rsquo;s not enough.</p>
<p>A high hit ratio does not mean your system is healthy. It does not capture:</p>
<ul>
<li>Page churn</li>
<li>Eviction pressure</li>
<li>Access patterns</li>
</ul>
<p>You can have a 99% hit ratio and still be IO-bound.</p>
<p>Use it as a sanity check, not a decision-maker.</p>
<hr>
<h3>2. Free Buffers<a class="anchor-link" id="2-free-buffers"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">free_buffers</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">free_buffers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Interpretation:</strong></p>
<ul>
<li>Near zero during steady load &rarr; normal</li>
<li>Near zero + rising disk reads &rarr; pressure</li>
<li>Near zero while mostly idle &rarr; suspicious (possible misread or config issue)</li>
</ul>
<hr>
<h3>3. Dirty Page Percentage<a class="anchor-link" id="3-dirty-page-percentage"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">(</span><span class="k">SUM</span><span class="p">(</span><span class="n">modified_database_pages</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">database_pages</span><span class="p">))</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">dirty_pct</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Interpretation (context matters):</strong></p>
<ul>
<li>0&ndash;5% &rarr; Very clean</li>
<li>5&ndash;20% &rarr; Typical</li>
<li>20&ndash;30%+ &rarr; Potential flushing lag</li>
</ul>
<hr>
<h3>4. Disk Read Pressure (Critical Signal)<a class="anchor-link" id="4-disk-read-pressure-critical-signal"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_buffer_pool_reads'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Take two samples 60s apart and compare</span></span></span></code></pre>
</div>
</div>
</div>
<p>Track the rate of change (reads/sec), not the absolute value.</p>
<p><strong>Interpretation:</strong></p>
<ul>
<li>Rising reads &rarr; Working set does not fit in memory</li>
<li>Flat reads &rarr; Memory is absorbing the workload</li>
</ul>
<hr>
<h3>5. Read Ahead / Eviction Pressure<a class="anchor-link" id="5-read-ahead-eviction-pressure"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_buffer_pool_read_ahead%'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_buffer_pool_pages_evicted'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_buffer_pool_reads'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Interpretation:</strong></p>
<ul>
<li>Efficient read-ahead:
<ul>
<li>read_ahead increases</li>
<li>read_ahead_evicted remains low</li>
</ul>
</li>
<li>Inefficient read-ahead (wasted IO):
<ul>
<li>High read_ahead_evicted / read_ahead</li>
<li>Indicates access patterns defeating prefetching</li>
</ul>
</li>
<li>Buffer pool churn:
<ul>
<li>pages_evicted rising</li>
<li>buffer_pool_reads rising</li>
<li>Indicates pages are evicted and re-read from disk</li>
</ul>
</li>
<li>Healthy vs unhealthy eviction:
<ul>
<li>High evictions + stable reads &rarr; normal turnover</li>
<li>High evictions + rising reads &rarr; memory pressure</li>
</ul>
</li>
</ul>
<p>Focus on rates of change over time, not absolute values.</p>
<hr>
<h2>Detecting Thrashing<a class="anchor-link" id="detecting-thrashing"></a></h2>
<p>Thrashing is when the buffer pool constantly evicts and reloads pages.</p>
<h3>Classic Symptoms<a class="anchor-link" id="classic-symptoms"></a></h3>
<ul>
<li>Low or zero free buffers</li>
<li>Increasing disk reads</li>
<li>Stable (but misleading) hit ratio</li>
<li>Spiky query latency</li>
</ul>
<h3>Visualizing Thrash<a class="anchor-link" id="visualizing-thrash"></a></h3>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Time ---&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Memory: [FULL][FULL][FULL][FULL]
</span></span><span class="line"><span class="cl">Reads: &uarr; &uarr;&uarr; &uarr;&uarr;&uarr; &uarr;&uarr;&uarr;&uarr;
</span></span><span class="line"><span class="cl">Latency: - ^ ^^ ^^^
</span></span><span class="line"><span class="cl">Evictions: &uarr; &uarr;&uarr; &uarr;&uarr;&uarr; &uarr;&uarr;&uarr;&uarr;</span></span></code></pre>
</div>
</div>
</div>
<p>If you see this pattern, your working set does not fit in memory.</p>
<hr>
<h2>Tuning the Buffer Pool<a class="anchor-link" id="tuning-the-buffer-pool"></a></h2>
<h3>Step 1: Size It Intentionally<a class="anchor-link" id="step-1-size-it-intentionally"></a></h3>
<p>Instead of blindly assigning 70% of RAM:</p>
<ul>
<li>Observe working set behavior</li>
<li>Monitor free buffers and reads</li>
<li>Increase gradually</li>
</ul>
<p>Avoid starving the OS or filesystem cache.</p>
<hr>
<h3>Step 2: Tune Flushing Behavior<a class="anchor-link" id="step-2-tune-flushing-behavior"></a></h3>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">innodb_max_dirty_pages_pct = 75
</span></span><span class="line"><span class="cl">innodb_io_capacity = 1000
</span></span><span class="line"><span class="cl">innodb_io_capacity_max = 2000</span></span></code></pre>
</div>
</div>
</div>
<ul>
<li>Sustained IO spikes &rarr; increase innodb_io_capacity</li>
<li>Dirty pages climbing &rarr; flushing lag</li>
<li>Sudden stalls &rarr; checkpoint pressure</li>
</ul>
<p><strong>What they control:</strong></p>
<ul>
<li><code>innodb_io_capacity</code> &rarr; Expected steady-state IO throughput</li>
<li><code>innodb_io_capacity_max</code> &rarr; Burst flushing capacity</li>
<li><code>innodb_max_dirty_pages_pct</code> &rarr; Threshold for aggressive flushing</li>
</ul>
<p>&#9888;&#65039; These values should reflect real hardware capability.</p>
<hr>
<h3>Step 3: Buffer Pool Instances:Reduce Contention<a class="anchor-link" id="step-3-buffer-pool-instancesreduce-contention"></a></h3>
<p>A practical, battle-tested guideline:</p>
<p>Use 1 instance per ~1GB of buffer pool, up to a reasonable limit.</p>
<p>Buffer Pool Instances: Reducing Contention</p>
<p>The buffer pool can be split into multiple instances, each managing its own internal structures. This helps reduce contention under high concurrency.</p>
<p>Without this, all threads compete for the same buffer pool internals. With multiple instances, that load is distributed.</p>
<hr>
<h3>When It Matters<a class="anchor-link" id="when-it-matters"></a></h3>
<p>Buffer pool instances only help when contention exists. You&rsquo;ll see benefits if your system has:</p>
<ul>
<li>High concurrency (many active threads)</li>
<li>CPU-bound workloads</li>
<li>Mutex contention in InnoDB</li>
</ul>
<p>If your workload is primarily IO-bound, this setting will have little impact.</p>
<hr>
<h3>Sizing Guidelines<a class="anchor-link" id="sizing-guidelines"></a></h3>
<p>General guidance:</p>
<ul>
<li>&lt; 1GB buffer pool &rarr; 1 instance</li>
<li>1GB&ndash;8GB &rarr; 2&ndash;4 instances</li>
<li>8GB&ndash;64GB &rarr; 4&ndash;8 instances</li>
<li>64GB+ &rarr; 8&ndash;16 instances</li>
</ul>
<hr>
<h3>Keep Instances Large Enough<a class="anchor-link" id="keep-instances-large-enough"></a></h3>
<p>Each instance needs enough memory to function efficiently.</p>
<p>Avoid going below ~1GB per instance.</p>
<p>If instances are too small:</p>
<ul>
<li>LRU efficiency drops</li>
<li>Eviction becomes more aggressive</li>
<li>Cache locality suffers</li>
</ul>
<p>Example</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">innodb_buffer_pool_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">32</span><span class="k">G</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">innodb_buffer_pool_instances</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">8</span></span></span></code></pre>
</div>
</div>
</div>
<p>This gives ~4GB per instance, which is well-balanced.</p>
<hr>
<h3>Common Mistakes<a class="anchor-link" id="common-mistakes"></a></h3>
<ul>
<li>Increasing instances without evidence of contention</li>
<li>Matching instance count to CPU cores</li>
<li>Using many instances with a small buffer pool</li>
<li>Expecting this to fix IO bottlenecks</li>
</ul>
<hr>
<h3>Step 4: Understand Resizing Behavior<a class="anchor-link" id="step-4-understand-resizing-behavior"></a></h3>
<p>Buffer pool resizing is online in modern MySQL versions, but:</p>
<ul>
<li>It happens in chunks</li>
<li>Controlled by <code>innodb_buffer_pool_chunk_size</code></li>
</ul>
<hr>
<h2>Real-World Scenarios<a class="anchor-link" id="real-world-scenarios"></a></h2>
<h3>Scenario 1: &ldquo;Everything Looks Fine&hellip; But It&rsquo;s Slow&rdquo;<a class="anchor-link" id="scenario-1-everything-looks-fine-but-its-slow"></a></h3>
<ul>
<li>High hit ratio</li>
<li>Low free buffers</li>
<li>Rising disk reads</li>
</ul>
<p><strong>Cause:</strong> Working set barely fits</p>
<p><strong>Fix:</strong> Increase buffer pool size gradually</p>
<p>If increasing the buffer pool size does not reduce disk reads, the problem is not memory.</p>
<hr>
<h3>Scenario 2: Write-Heavy Workload<a class="anchor-link" id="scenario-2-write-heavy-workload"></a></h3>
<ul>
<li>Dirty pages increasing</li>
<li>Periodic IO spikes</li>
</ul>
<p><strong>Cause:</strong> Flushing cannot keep up</p>
<p><strong>Fix:</strong></p>
<ul>
<li>Increase <code>innodb_io_capacity</code></li>
<li>Adjust dirty page thresholds</li>
</ul>
<hr>
<h3>Scenario 3: Sudden Latency Spikes<a class="anchor-link" id="scenario-3-sudden-latency-spikes"></a></h3>
<ul>
<li>Sharp performance drops</li>
<li>Disk activity surges</li>
</ul>
<p><strong>Cause:</strong> Checkpoint pressure</p>
<p><strong>Fix:</strong></p>
<ul>
<li>Improve IO capacity tuning</li>
<li>Reduce dirty page buildup</li>
</ul>
<hr>
<h2>Practical Monitoring Queries<a class="anchor-link" id="practical-monitoring-queries"></a></h2>
<h3>Buffer Pool Usage (MB)<a class="anchor-link" id="buffer-pool-usage-mb"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">(</span><span class="k">SUM</span><span class="p">(</span><span class="n">database_pages</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">16</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">1024</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">mb_used</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Assumes default 16KB page size (innodb_page_size).</p>
<h3>Dirty Page Percentage<a class="anchor-link" id="dirty-page-percentage"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="p">(</span><span class="n">modified_database_pages</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">database_pages</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">dirty_pct</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Free Buffer Check<a class="anchor-link" id="free-buffer-check"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">free_buffers</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">free_buffers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_BUFFER_POOL_STATS</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<hr>
<h2>Common Mistakes<a class="anchor-link" id="common-mistakes"></a></h2>
<ul>
<li>Treating 70% as a rule instead of a starting point</li>
<li>Blindly trusting hit ratio</li>
<li>Ignoring disk read trends</li>
<li>Oversizing and starving the OS</li>
<li>Not tuning IO capacity</li>
<li>Leaving defaults in write-heavy systems</li>
</ul>
<hr>
<h2>Quick Checklist<a class="anchor-link" id="quick-checklist"></a></h2>
<p>If you remember nothing else:</p>
<ul>
<li>Reads increasing? &rarr; working set too big</li>
<li>Free buffers always ~0? &rarr; pressure</li>
<li>Dirty pages high? &rarr; flushing lag</li>
<li>Latency spiking? &rarr; checkpoint or IO saturation</li>
</ul>
<hr>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>The InnoDB buffer pool doesn&rsquo;t fail loudly. It degrades quietly until your disk becomes the bottleneck.</p>
<p>By the time you notice, you&rsquo;re debugging latency instead of preventing it.</p>
<p>Monitor the right signals, and you&rsquo;ll see problems forming before users do.</p>
<p>That&rsquo;s the difference between reacting to performance&hellip; and controlling it.</p>

<p><a href="https://percona.community/blog/2026/04/02/innodb-buffer-pool-tuning-from-rule-of-thumb-to-real-signals/">InnoDB Buffer Pool Tuning: From Rule-of-Thumb to Real Signals</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>gcc vs clang for sysbench on a small server with Postgres, MySQL and MariaDB</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/gcc-vs-clang-for-sysbench-on-small.html" />
      <id>https://smalldatum.blogspot.com/2026/03/gcc-vs-clang-for-sysbench-on-small.html</id>
      <updated>2026-03-31T20:28:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for sysbench on a small server and compares performanc for Postgres, MySQL and MariaDB compiled using clang vs using gcc.tl;drThroughput with clang and gcc is similarBuilds, configuration and hardwareI compiled Postgres 18.3, MySQL 8.4.8 and MariaDB 11.8.6 from source. The server has 8 AMD cores with SMT disabled and 32G of RAM. The OS is Ubuntu 24.04, gcc is version 13.3.0 and clang is version 18.1.3. Storage is ext-4 with discard enabled and an NVMe SSD.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.The tests are run using 1 client and 1 table with 50M rows. The read-heavy microbenchmarks run for 630 seconds and the write-heavy for 930 seconds.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide tables below with relative QPS. When the relative QPS is &#62; 1 then some version is faster than the base version. When it is &#60; 1 then there might be a regression. The number below are the relative QPS computed as: (QPS with a gcc build / QPS with a clang build)Legend:* pg - for Postgres 18.3, (QPS with gcc / QPS with clang)* my - for MySQL 8.4.8, (QPS with gcc / QPS with clang)* ma - for MariaDB 11.8.6, (QPS with gcc / QPS with clang)-- point queriespg      my      ma1.02    1.00    0.99    hot-points1.02    0.98    1.02    point-query0.95    1.01    1.02    points-covered-pk0.96    1.02    1.02    points-covered-si0.97    1.00    1.02    points-notcovered-pk0.96    1.03    1.02    points-notcovered-si0.97    1.01    1.01    random-points_range=10000.98    1.01    1.01    random-points_range=1001.00    0.99    1.00    random-points_range=10-- range queries without aggregationpg      my      ma1.01    0.98    1.03    range-covered-pk1.00    0.98    1.05    range-covered-si0.99    0.98    1.04    range-notcovered-pk0.99    1.02    0.97    range-notcovered-si1.02    1.06    1.03    scan-- range queries with aggregationpg      my      ma1.01    0.96    1.05    read-only-count0.99    0.99    1.01    read-only-distinct0.99    1.00    1.00    read-only-order0.99    1.00    1.01    read-only_range=100001.00    0.98    1.00    read-only_range=1001.01    0.97    1.00    read-only_range=100.99    0.97    1.03    read-only-simple1.02    0.98    1.02    read-only-sum-- writespg      my      ma1.03    0.98    1.00    delete1.01    1.00    1.00    insert1.00    0.98    1.00    read-write_range=1001.00    0.98    1.00    read-write_range=100.99    1.01    0.97    update-index0.96    1.01    0.99    update-inlist0.99    0.99    0.99    update-nonindex1.02    0.98    0.99    update-one0.98    0.98    0.99    update-zipf1.00    0.99    0.99    write-only</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/gcc-vs-clang-for-sysbench-on-small.html">gcc vs clang for sysbench on a small server with Postgres, MySQL and MariaDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for sysbench on a small server and compares performanc for Postgres, MySQL and MariaDB compiled using clang vs using gcc.</p>
<p>tl;dr</p>

<ul>
<li>Throughput with clang and gcc is similar</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>

<div></div>

<div>I compiled Postgres 18.3, MySQL 8.4.8 and MariaDB 11.8.6 from source. The server has 8 AMD cores with SMT disabled and 32G of RAM. The OS is Ubuntu 24.04, gcc is version 13.3.0 and clang is version 18.1.3. Storage is ext-4 with discard enabled and an NVMe SSD.</div>
</div>
<div><b><br></b></div>
<div><b>Benchmark</b></div>
<div><b><br></b></div>
<div>
<div>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.</div>
<div>The tests are run using 1 client and 1 table with 50M rows. The read-heavy microbenchmarks run for 630 seconds and the write-heavy for 930 seconds.</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div>I provide tables below with relative QPS.&nbsp;<span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than the</span><span>&nbsp;</span><i>base version.</i><span>&nbsp;When it is &lt; 1 then there might be a regression.&nbsp;</span><span>The number below are the relative QPS computed as:</span>&nbsp;(QPS with a gcc build / QPS with a clang build)</div>
<div></div>
<p></p></span></div>
</div>
</div>
<div><span>Legend:<br></span><span>* pg &ndash; for Postgres 18.3, (QPS with gcc / QPS with clang)<br></span><span>* my &ndash; for MySQL 8.4.8, (QPS with gcc / QPS with clang)<br></span><span>* ma &ndash; for MariaDB 11.8.6, (QPS with gcc / QPS with clang)</span></div>
<div><span><br></span><span>&mdash; point queries<br></span><span>pg&nbsp; &nbsp; &nbsp; my&nbsp; &nbsp; &nbsp; ma<br></span><span>1.02&nbsp; &nbsp; 1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; hot-points<br></span><span>1.02&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.02&nbsp; &nbsp; point-query<br></span><span>0.95&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.02&nbsp; &nbsp; points-covered-pk<br></span><span>0.96&nbsp; &nbsp; 1.02&nbsp; &nbsp; 1.02&nbsp; &nbsp; points-covered-si<br></span><span>0.97&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.02&nbsp; &nbsp; points-notcovered-pk<br></span><span>0.96&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.02&nbsp; &nbsp; points-notcovered-si<br></span><span>0.97&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=1000<br></span><span>0.98&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; random-points_range=100<br></span><span>1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; random-points_range=10</span><span><br></span><span><br></span></div>
<div><span>&mdash; range queries without aggregation<br></span><span>pg&nbsp; &nbsp; &nbsp; my&nbsp; &nbsp; &nbsp; ma<br></span><span>1.01&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.03&nbsp; &nbsp; range-covered-pk<br></span><span>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.05&nbsp; &nbsp; range-covered-si<br></span><span>0.99&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.04&nbsp; &nbsp; range-notcovered-pk<br></span><span>0.99&nbsp; &nbsp; 1.02&nbsp; &nbsp; 0.97&nbsp; &nbsp; range-notcovered-si<br></span><span>1.02&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.03&nbsp; &nbsp; scan</span></div>
<div><span><br>&mdash; range queries with aggregation<br></span><span>pg&nbsp; &nbsp; &nbsp; my&nbsp; &nbsp; &nbsp; ma<br></span><span>1.01&nbsp; &nbsp; 0.96&nbsp; &nbsp; 1.05&nbsp; &nbsp; read-only-count<br></span><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; read-only-distinct<br></span><span>0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only-order<br></span><span>0.99&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; read-only_range=10000<br></span><span>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only_range=100<br></span><span>1.01&nbsp; &nbsp; 0.97&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-only_range=10<br></span><span>0.99&nbsp; &nbsp; 0.97&nbsp; &nbsp; 1.03&nbsp; &nbsp; read-only-simple<br></span><span>1.02&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.02&nbsp; &nbsp; read-only-sum</span><span><br></span><span><br>&mdash; writes<br></span><span>pg&nbsp; &nbsp; &nbsp; my&nbsp; &nbsp; &nbsp; ma<br></span><span>1.03&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; delete<br></span><span>1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.00&nbsp; &nbsp; insert<br></span><span>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-write_range=100<br></span><span>1.00&nbsp; &nbsp; 0.98&nbsp; &nbsp; 1.00&nbsp; &nbsp; read-write_range=10<br></span><span>0.99&nbsp; &nbsp; 1.01&nbsp; &nbsp; 0.97&nbsp; &nbsp; update-index<br></span><span>0.96&nbsp; &nbsp; 1.01&nbsp; &nbsp; 0.99&nbsp; &nbsp; update-inlist<br></span><span>0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; update-nonindex<br></span><span>1.02&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.99&nbsp; &nbsp; update-one<br></span><span>0.98&nbsp; &nbsp; 0.98&nbsp; &nbsp; 0.99&nbsp; &nbsp; update-zipf<br></span><span>1.00&nbsp; &nbsp; 0.99&nbsp; &nbsp; 0.99&nbsp; &nbsp; write-only</span></div>

<p><a href="https://smalldatum.blogspot.com/2026/03/gcc-vs-clang-for-sysbench-on-small.html">gcc vs clang for sysbench on a small server with Postgres, MySQL and MariaDB</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The insert benchmark on a small server : Postgres 12.22 through 18.3</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/the-insert-benchmark-on-small-server.html" />
      <id>https://smalldatum.blogspot.com/2026/03/the-insert-benchmark-on-small-server.html</id>
      <updated>2026-03-29T20:59:00+03:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for Postgres versions 12.22 through 18.3 with the Insert Benchmark on a small server. My previous post for the same hardware with results up to Postgres 18.1 is here. This post also has results for:all 17.x releases from 17.0 through 17.9 18.2 with and without full page writes enabledboth 1 and 4 usersPostgres continues to be boring in a good way. It is hard to find performance regressions. Performance wasn\'t always stable, but I am reluctant to expect it to show no changes because there are sources of variance beyond the DBMS, especially HW (a too-hot SSD or CPU will run slower). Sometimes perf changes because there are obvious perf bugs, sometimes it changes for other reasons. tl;dr for a  CPU-bound workloadperformance is stable from Postgres 12 through 18performance is stable from Postgres 17.0 through 17.9disabling full-page writes improves throughput on write-heavy benchmark stepstl;dr for an IO-bound workloadperformance is mostly stable from Postgres 12 through 18performance is stable from Postgres 17.0 through 17.9disabling full-page writes improves throughput on write-heavy benchmark stepsin a few cases there are large improvements to point-query throughput on the qp1000 benchmark step. I will try to explain that soon.Builds, configuration and hardwareI compiled Postgres from source using -O2 -fno-omit-frame-pointer for versions 12.22, 13.23, 14.22, 15.17, 16.13, 17.0 to 17.9, 18.2 and 18.3.The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.For versions prior to 18, the config file is named conf.diff.cx10a_c8r32 and they are as similar as possible and here for versions 12, 13, 14, 15, 16 and 17.For Postgres 18 in most cases I used a config named conf.diff.cx10b_c8r32 (aka cx10b) which is as similar as possible to the configs for versions 17 and earlier. But for tests with full-page writes disabled I used additional configs to compare with results from the cx10b config.cx10b_fpw0this adds full_page_writes=off while cx10b has =oncx10b_wallz4this adds wal_compression=lz4 while cx10b has =offcx10b_fpw0_wallz4this adds full_page_writes=off and wal_compression=lz4The BenchmarkThe benchmark is explained here and is run with 1 and 4 clients. In each case each client uses a separate table. I repeated it with two workloads:CPU-boundfor 1 user the values for X, Y, Z are 30M, 40M, 10Mfor 4 users the values for X, Y, Z are 10M, 16M, 4MIO-boundfor 1 user the values for X, Y, Z are 800M, 4M, 1Mfor 4 users the values for X, Y, Z are 200M, 4M, 1MThe point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.The benchmark steps are:l.i0insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: overviewThe performance reports are here for:CPU-boundLatest point releases: 1 user and 4 usersAll 17.x releases: 1 user and 4 usersFull-page writes enabled and disabled: 1 user and 4 usersIO-boundLatest point releases: 1 user and 4 usersAll 17.x releases: 1 user and 4 usersFull-page writes enabled and disabled: 1 user and 4 usersThe summary sections from the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is Postgres 12.22 for the latest point releases comparison, 17.0 for the 17.x releases comparison and 18.2 with the cx10b config for the full-page writes comparison. When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000This statement doesn&#039;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with red for = 1.05 and grey for values between 0.95 and 1.05.Results: CPU-boundThe performance summaries are here for:1 user: latest point releases, all 17.x releases and full-page writes4 users: latest point releases, all 17.x releases and full-page writesFor latest point releases at 1 userthere is either no change or a small improvement for l.i0 (load in PK order), l.x (create indexes) and the read-write tests (qr*, qp*).for l.i1 and l.i2 (random write-only) throughput drops by 5% to 10% from 12.22 to 13.23 and has been stable since then (throughput in 18.2 is similar to 13.23. The CPU per operation overhead (cpupq here) increases after 12.22 for the l.i2 step but there wasn&#039;t an obvious increase for the l.i1 step - but the way I measure this is far from perfect. The results I share here are worse than what I measured in December 2025.For latest point releases at 4 usersthere might be a small (3%) regression for l.i0 (load in PK order) in 18.2 vs 12.22. Perhaps this is noise. From vmstat and iostat metrics there aren&#039;t obvious changes.throughput in 18.2 is better than 12.22 for all other benchmark stepsFor all 17.x releases at 1 userthroughput is stable from 17.0 to 17.9 for all benchmark steps except l.i1 and l.i2 (random writes) where there might be a 5% regression late in 17.x. This might be from new CPU overhead - see cpupq here.For all 17.x releases at 4 usersthroughput is stable with small improvements from 17.0 to 17.9For full-page writes at 1 userthroughput improves by ~5% for l.i1 and l.i2 (random writes) when full-page writes are disabled  and KB written to storage per commit drops by ~20% -- see wkbpi here.enabling wal_compression=lz4 decreases write throughput for all write-heavy steps when full-page writes are enabled. The impact is smaller when full page writes are disabled.For full-page writes at 4 usersthroughput improves by</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/the-insert-benchmark-on-small-server.html">The insert benchmark on a small server : Postgres 12.22 through 18.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for Postgres versions 12.22 through 18.3 with the&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">Insert Benchmark</a>&nbsp;on a small server. My previous post for the same hardware with results up to Postgres 18.1 <a href="https://smalldatum.blogspot.com/2025/12/the-insert-benchmark-on-small-server.html">is here</a>. This post also has results for:</p>

<ul>
<li>all 17.x releases from 17.0 through 17.9&nbsp;</li>
<li>18.2 with and without full page writes enabled</li>
<li>both 1 and 4 users</li>
</ul>
<p>Postgres continues to be boring in a good way. It is hard to find performance regressions. Performance wasn&rsquo;t always stable, but I am reluctant to expect it to show no changes because there are sources of variance beyond the DBMS, especially HW (a too-hot SSD or CPU will run slower). Sometimes perf changes because there are obvious perf bugs, sometimes it changes for other reasons.</p>
<p>&nbsp;tl;dr for a&nbsp; CPU-bound workload</p>
<div>
<ul>
<li>performance is stable from Postgres 12 through 18</li>
<li>performance is stable from Postgres 17.0 through 17.9</li>
<li>disabling full-page writes improves throughput on write-heavy benchmark steps</li>
</ul>
<div>tl;dr for an IO-bound workload</div>
<div>
<ul>
<li>performance is mostly stable from Postgres 12 through 18</li>
<li>performance is stable from Postgres 17.0 through 17.9</li>
<li>disabling full-page writes improves throughput on write-heavy benchmark steps</li>
<li>in a few cases there are large improvements to point-query throughput on the qp1000 benchmark step. I will try to explain that soon.</li>
</ul>
</div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled Postgres from source using&nbsp;<i>-O2 -fno-omit-frame-pointer</i>&nbsp;for versions 12.22, 13.23, 14.22, 15.17, 16.13, 17.0 to 17.9, 18.2 and 18.3.</div>
<div>The server is an Beelink SER7 with a Ryzen 7 7840HS CPU with 8 cores and AMD SMT disabled, 32G of RAM. Storage is one SSD for the OS and an NVMe SSD for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04.</div>
<div></div>
<div>For versions prior to 18, the config file is named conf.diff.cx10a_c8r32 and they are as similar as possible and here for versions&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg1219_o2nofp/conf.diff.cx10a_c8r32">12</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg1315_o2nofp/conf.diff.cx10a_c8r32">13</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg1412_o2nofp/conf.diff.cx10a_c8r32">14</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg157_o2nofp/conf.diff.cx10a_c8r32">15</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg163_o2nofp/conf.diff.cx10a_c8r32">16</a>&nbsp;and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg172_o2nofp/conf.diff.cx10a_c8r32">17</a>.</div>

<div>For Postgres 18 in most cases I used a config named <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_c8r32">conf.diff.cx10b_c8r32</a>&nbsp;(aka cx10b) which is as similar as possible to the configs for versions 17 and earlier. But for tests with full-page writes disabled I used additional configs to compare with results from the cx10b config.</div>
<div>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_fpw0_c8r32">cx10b_fpw0</a></li>
<ul>
<li>this adds full_page_writes=off while cx10b has =on</li>
</ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_wallz4_c8r32">cx10b_wallz4</a></li>
<ul>
<li>this adds wal_compression=lz4&nbsp;while cx10b has =off</li>
</ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/pg18_o2nofp/conf.diff.cx10b_fpw0_wallz4_c8r32">cx10b_fpw0_wallz4</a></li>
<ul>
<li>this adds full_page_writes=off and wal_compression=lz4</li>
</ul>
</ul>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>&nbsp;and&nbsp;is run with 1 and 4 clients. In each case each client uses a separate table. I repeated it with two workloads:</div>
<div>
<ul>
<li>CPU-bound</li>
<ul>
<li>for 1 user the values for X, Y, Z are 30M, 40M, 10M</li>
<li>for 4 users the values for X, Y, Z are 10M, 16M, 4M</li>
</ul>
<li>IO-bound</li>
<ul>
<li>for 1 user the values for X, Y, Z are 800M, 4M, 1M</li>
<li>for 4 users the values for X, Y, Z are 200M, 4M, 1M</li>
</ul>
</ul>
<div>The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.</div>
</div>
<div></div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.</li>
<li>Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div>
<div><b>Results: overview</b></div>
<div>
<div><b><br></b></div>
<div>The performance reports are here for:</div>
</div>
</div>
<div>
<ul>
<li>CPU-bound</li>
<ul>
<li>Latest point releases: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pglatest/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pglatest/all.html">4 users</a></li>
<li>All 17.x releases: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg17/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pg17/all.html">4 users</a></li>
<li>Full-page writes enabled and disabled: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg.fpw/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pg.fpw/all.html">4 users</a></li>
</ul>
<li>IO-bound</li>
<ul>
<li>Latest point releases: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pglatest/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pglatest/all.html">4 users</a></li>
<li>All 17.x releases: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pg17/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pg17/all.html">4 users</a></li>
<li>Full-page writes enabled and disabled: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pg.fpw/all.html">1 user</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pg.fpw/all.html">4 users</a></li>
</ul>
</ul>
</div>
<div>The summary sections from&nbsp;the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.</div>
<div>
<div></div>
<div>Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is Postgres 12.22 for the latest point releases comparison, 17.0 for the 17.x releases comparison and 18.2 with the cx10b config for the full-page writes comparison.&nbsp;
<p>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</p></div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>This statement doesn&rsquo;t apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with&nbsp;<span>red</span>&nbsp;for &lt;= 0.95,&nbsp;<span>green</span>&nbsp;for &gt;= 1.05 and&nbsp;<span>grey</span>&nbsp;for values between 0.95 and 1.05.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div></div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The performance summaries are here for:</div>
<div>
<ul>
<li>1 user: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pglatest/all.html#summary">latest point releases</a>, <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg17/all.html#summary">all 17.x releases</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg.fpw/all.html#summary">full-page writes</a></li>
<li>4 users: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pglatest/all.html#summary">latest point releases</a>, <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pg17/all.html#summary">all 17.x releases</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pg.fpw/all.html#summary">full-page writes</a></li>
</ul>
<div>For latest point releases at 1 user</div>
<div>
<ul>
<li>there is either no change or a small improvement for l.i0 (load in PK order), l.x (create indexes) and the read-write tests (qr*, qp*).</li>
<li>for l.i1 and l.i2 (random write-only) throughput drops by 5% to 10% from 12.22 to 13.23 and has been stable since then (throughput in 18.2 is similar to 13.23. The CPU per operation overhead (<a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pglatest/all.html#l.i1.metrics">cpupq here</a>) increases after 12.22 for the l.i2 step but there wasn&rsquo;t an obvious increase for the l.i1 step &ndash; but the way I measure this is far from perfect. The results I share here are worse than what I <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.mem.30m.50m.1800s/all.html#summary">measured in December 2025</a>.</li>
</ul>
<div>For latest point releases at 4 users</div>
</div>
<div>
<ul>
<li>there might be a small (3%) regression for l.i0 (load in PK order) in 18.2 vs 12.22. Perhaps this is noise. From <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.10m.20m.1800s.4u.pglatest/all.html#l.i0.metrics">vmstat and iostat metrics</a> there aren&rsquo;t obvious changes.</li>
<li>throughput in 18.2 is better than 12.22 for all other benchmark steps</li>
</ul>
<div>For all 17.x releases at 1 user</div>
</div>
<div>
<ul>
<li>throughput is stable from 17.0 to 17.9 for all benchmark steps except l.i1 and l.i2 (random writes) where there might be a 5% regression late in 17.x. This might be from new CPU overhead &ndash; see <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg17/all.html#l.i1.metrics">cpupq here</a>.</li>
</ul>
<div>
<div>For all 17.x releases at 4 users</div>
<div>
<ul>
<li>throughput is stable with small improvements from 17.0 to 17.9</li>
</ul>
<div>
<div>For full-page writes at 1 user</div>
<div>
<ul>
<li>throughput improves by ~5% for l.i1 and l.i2 (random writes) when full-page writes are disabled&nbsp; and KB written to storage per commit drops by ~20% &mdash; see <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.mem.30m.50m.1800s.1u.pg.fpw/all.html#l.i1.metrics">wkbpi here</a>.</li>
<li>enabling wal_compression=lz4&nbsp;decreases write throughput for all write-heavy steps when full-page writes are enabled. The impact is smaller when full page writes are disabled.</li>
</ul>
<div>
<div>For full-page writes at 4 users</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<ul>
<li>throughput improves by &lt;= 5% for all write-heavy steps when full-page writes are disabled</li>
<li>the impact from wal_compression=lz4 isn&rsquo;t obvious</li>
</ul>
</div>
<div><b>Results: IO-bound</b></div>
<div>
<div></div>
<div>The performance summaries are here for:</div>
<div>
<ul>
<li>1 user:&nbsp;&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pglatest/all.html#summary">latest point releases</a>,&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pg17/all.html#summary">all 17.x releases</a>&nbsp;and&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pg.fpw/all.html#summary">full-page writes</a></li>
<li>4 users: <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pglatest/all.html#summary">latest point releases</a>, <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pg17/all.html#summary">all 17.x releases</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pg.fpw/all.html#summary">full-page writes</a></li>
</ul>
<div>
<div>For latest point releases at 1 user</div>
<div>
<ul>
<li>there are small (&lt;= 10%) improvements for l.i0 (load in PK order) and l.x (create index). I don't see anything obvious <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pglatest/all.html#l.i0.metrics">in vmstat and iostat metrics</a> to explain this.</li>
<li>there are small (&lt;= 10%) regressions for l.i1 and l.i2 (random writes) that might be from a sequence of small regressions from 13.x through 18.x. I don't see anything obvious&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pglatest/all.html#l.i0.metrics">in vmstat and iostat metrics</a>&nbsp;to explain this.</li>
<li>throughput is unchanged for the range-query read+write tests (qr*)</li>
<li>throughput improves by ~1.4X for the point-query read+write tests (qp*). This improvement arrived in 13.x. This can be explained by large drops in CPU overhead (cpupq) and context switch rates (cspq) &mdash; <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pglatest/all.html#qp500.L4.metrics">see here</a>.</li>
<li>the results here are similar to what I <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.io.800m.5m.1800s/all.html#summary">measured in December 2025</a></li>
</ul>
<div>For latest point releases at 4 users</div>
</div>
<div>
<ul>
<li>there are small (~10%) regressions for l.i0 (load in PK order) that arrived in 17.x. The context switch rate (cspq) <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pglatest/all.html#l.i0.metrics">increases in 17.x</a>.&nbsp;</li>
<li>there are small (&lt;= 20%) improvements for l.x (create index) that arrived in 13.x</li>
<li>there are large regressions for l.i1 and l.i2 (random writes) that arrive in 15.x through 18.x. There are large increases in CPU overhead (cpupq) &mdash; <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pglatest/all.html#l.i1.metrics">see here</a>.</li>
<li>throughput is unchanged for the range-query read+write tests (qr*)</li>
<li>throughput improves for the point-query read+write tests (qp*) at higher write rates (qp500, qp1000).</li>
</ul>
<div>For all 17.x releases at 1 user</div>
</div>
<div>
<ul>
<li>throughput is stable with a few exceptions</li>
<li>for qp1000 (point-query, read+write) it improves by ~5% in 17.1 and is then stable to 17.9</li>
<li>in 17.9 there are large (~1.4x) improvements for all of the point-query, read+write tests</li>
<li>the changes in throughput for qp1000 might be explained by a small drop in CPU overhead per query (cpupq) that arrived in 17.1 and a large drop that arrived in 17.9 &mdash; <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.800m.5m.1800s.1u.pg17/all.html#qp1000.L6.metrics">see here</a>.</li>
</ul>
<div>
<div>For all 17.x releases at 4 users</div>
<div>
<ul>
<li>throughput for most steps (l.i0, l.x, qr*, qp100, qp500) is stable</li>
<li>throughput for l.i1 and l.i2 (random writes) has more variance</li>
<li>throughput for qp1000 drops by up to 10% from 17.3 through 17.8 and in those cases the CPU overhead increased &mdash; see <a href="https://mdcallag.github.io/reports/mar26.ib.ser7.io.200m.5m.1800s.4u.pg17/all.html#qp1000.L6.metrics">cpupq here</a>.</li>
</ul>
<div>
<div>For full-page writes at 1 user</div>
<div>
<ul>
<li>throughput improves by 6% for l.i1 (random writes) when full-page writes are disabled</li>
<li>throughput improved for qp* tests when either full-page writes were disabled or lz4 was used for log_compression. That is harder to explain, perhaps it is noise.</li>
</ul>
<div>
<div>For full-page writes at 4 users</div>
<div>
<ul>
<li>throughput improves by 20% for l.i1 (random writes) when full-page writes are disabled</li>
<li>throughput improved for qp* tests when either full-page writes were disabled or lz4 was used for log_compression. That is harder to explain, perhaps it is noise.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div></div>
<p><b>n_dead_tup vs n_live_tup</b></p></div>
<div><b><br></b></div>
<div>The tables below show the ratio: n_dead_tup / (n_dead_tup + n_live_tup) for the CPU-bound and IO-bound workloads using 1 user (and one table). These were measured at the end of each benchmark step.
</div>
</div>
</div>
<div>
<div><span>CPU-bound</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; 12.22&nbsp; &nbsp;18.3</span></div>
<div><span>l.i0&nbsp; &nbsp; 0.000&nbsp; &nbsp;0.000</span></div>
<div><span>l.x&nbsp; &nbsp; &nbsp;0.000&nbsp; &nbsp;0.000</span></div>
<div><span>l.i1&nbsp; &nbsp; 0.065&nbsp; &nbsp;0.035</span></div>
<div><span>l.i2&nbsp; &nbsp; 0.045&nbsp; &nbsp;0.020</span></div>
<div><span>qr100&nbsp; &nbsp;0.006&nbsp; &nbsp;0.006</span></div>
<div><span>qp100&nbsp; &nbsp;0.012&nbsp; &nbsp;0.012</span></div>
<div><span>qr500&nbsp; &nbsp;0.040&nbsp; &nbsp;0.040</span></div>
<div><span>qp500&nbsp; &nbsp;0.021&nbsp; &nbsp;0.024</span></div>
<div><span>qr1000&nbsp; 0.031&nbsp; &nbsp;0.036</span></div>
<div><span>qp1000&nbsp; 0.040&nbsp; &nbsp;0.003</span></div>
<div><span><br></span></div>
<div><span>IO-bound</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; 12.22&nbsp; &nbsp;18.3</span></div>
<div><span>l.i0&nbsp; &nbsp; 0.000&nbsp; &nbsp;0.000</span></div>
<div><span>l.x&nbsp; &nbsp; &nbsp;0.000&nbsp; &nbsp;0.000</span></div>
<div><span>l.i1&nbsp; &nbsp; 0.005&nbsp; &nbsp;0.005</span></div>
<div><span>l.i2&nbsp; &nbsp; 0.006&nbsp; &nbsp;0.006</span></div>
<div><span>qr100&nbsp; &nbsp;0.000&nbsp; &nbsp;0.000</span></div>
<div><span>qp100&nbsp; &nbsp;0.000&nbsp; &nbsp;0.000</span></div>
<div><span>qr500&nbsp; &nbsp;0.002&nbsp; &nbsp;0.002</span></div>
<div><span>qp500&nbsp; &nbsp;0.003&nbsp; &nbsp;0.003</span></div>
<div><span>qr1000&nbsp; 0.005&nbsp; &nbsp;0.005</span></div>
<div><span>qp1000&nbsp; 0.007&nbsp; &nbsp;0.007</span></div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<p>&nbsp;</p>

<p><a href="https://smalldatum.blogspot.com/2026/03/the-insert-benchmark-on-small-server.html">The insert benchmark on a small server : Postgres 12.22 through 18.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Selecting a character set for MySQL and MariaDB clients</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html" />
      <id>https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html</id>
      <updated>2026-03-29T00:26:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p> MySQL and MariaDB have many character-set related options, perhaps too many:character_set_clientcharacter_set_connectioncharacter_set_databasecharacter_set_filesystemcharacter_set_resultscharacter_set_servercharacter_set_systemThis is a topic that I don\'t know much about and I am still far from an expert. My focus has been other DBMS topics. But I spent time recently on this topic while explaining what looked like a performance regression, but really was just a new release of MySQL using a charset that is less CPU-efficient than the previous charset that was used.DebuggingThe intial sequence to understand what was going on was:mysql -e \'SHOW GLOBAL VARIABLES like \"character_set_%\"mysql -e \'SHOW SESSION VARIABLES like \"character_set_%\"run \"SHOW SESSION VARIABLES\" from my benchmark clientNote:the output from steps 1 and 2 was differentwith SHOW GLOBAL VARIABLES I got character_set_client =latin1 but with SHOW SESSION VARIABLES I got character_set_client =utf8mb3. This happens. One reason is that some MySQL client binaries autodetect the charset based on the value of LANG or LC_TYPE from your Linux env. Another reason is that if autodetection isn\'t done then the clients can use the default charset that was set at compile time. That charset is then passed to the server during connection handshake (see thd_init_client_charset). So it is likely that character_set_client as displayed by SHOW GLOBAL VARIABLES isn\'t what your client will use.the output from steps 2 and 3 was differentautodetection is only done when mysql_options() is called with a certain flag (see below). And that is not done by the MySQL driver in sysbench, nor is it done by Python\'s MySQLdb. So my benchmark clients are likely selecting the default charset and don\'t do autodetection. And that default is determined by the version of the MySQL client library, meaning that default can change over the years. For the source that implements this, search for MYSQL_AUTODETECT_CHARSET_NAME and read sql-common/client.c.The following enables autodetection and should be called before calling mysql_real_connect():    mysql_options(...,                   MYSQL_SET_CHARSET_NAME,                  MYSQL_AUTODETECT_CHARSET_NAME);Note that adding the following into my.cnf isn\'t a workaround for clients that don\'t do autodetect.    [client]    default-character-set=...NotesThese are from my usage of MySQL 5.7.44, 8.0.45 and 8.4.8 along with MariaDB 10.6.25, 10.11.16 and 11.4.10. All were compiled from source as was sysbench. I installed MySQLdb and the MySQL client library via apt for Ubuntu 24.04.The values for character_set_client, character_set_results and character_set_connection were measured via the MySQL command-line client running SHOW GLOBAL VARIABLES and SHOW SESSION VARIABLES and then the benchmark clients running SHOW SESSION VARIABLES.The reason for sharing this is to explain the many possible values your session might use for character_set_client, character_set_results and character_set_connection. And using the wrong value might waste CPU.What per-session values are used for character_set_client&#124;results&#124;connection?* my.cnf has character_set_server=latin1* per SHOW GLOBAL VARIABLES each is set to =latin1* values below measured via SHOW SESSION VARIABLES<br />
Values for character_set_client&#124;results&#124;connection... with \"mysql\" command line client... this is easy to change with --default-character-set command line option or equivalent option in my.cnfdbms5.7.44     utf88.0.45     utf8mb48.4.8      utf8mb4<br />
10.6.25     utf8mb310.11.16    utf8mb311.4.10     utf8mb3<br />
Values for character_set_client&#124;results&#124;connection... with sysbench<br />
        client library versiondbms      5.7   8.0   8.4   10.6  10.11  11.45.7.44     latin1 latin1 latin1 NA   NA   NA8.0.45     latin1 utf8mb4 utf8mb4 NA   NA   NA8.4.8      latin1 utf8mb4 utf8mb4 NA   NA   NA<br />
10.6.25     latin1 latin1 latin1 utf8mb4 utf8mb4 utf8mb410.11.16    latin1 latin1 latin1 utf8mb4 utf8mb4 utf8mb411.4.10     latin1 utf8mb4 utf8mb4 utf8mb4 utf8mb4 utf8mb4<br />
Values for character_set_client&#124;results&#124;connection... with insert benchmark (Python MySQLdb and /lib/x86_64-linux-gnu/libmysqlclient.so.21... I am not what version is libmysqlclient.so.21, this is on Ubuntu 24.04<br />
dbms5.7.44     latin18.0.45     utf8mb48.4.8      utf8mb4<br />
10.6.25     latin110.11.16    latin111.4.10     utf8mb4</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">Selecting a character set for MySQL and MariaDB clients</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>&nbsp;MySQL and MariaDB have many character-set related options, perhaps too many:</p>

<ol>
<li>character_set_client</li>
<li>character_set_connection</li>
<li>character_set_database</li>
<li>character_set_filesystem</li>
<li>character_set_results</li>
<li>character_set_server</li>
<li>character_set_system</li>
</ol>
<div>This is a topic that I don&rsquo;t know much about and I am still far from an expert. My focus has been other DBMS topics. But I spent time recently on this topic while explaining what looked like a performance regression, but really was just a new release of MySQL using a charset that is less CPU-efficient than the previous charset that was used.</div>
<div></div>
<div><b>Debugging</b></div>
<div></div>
<div>The intial sequence to understand what was going on was:</div>
<div>
<ol>
<li>mysql -e &lsquo;SHOW GLOBAL VARIABLES like &ldquo;character_set_%&rdquo;</li>
<li>mysql -e &lsquo;SHOW SESSION VARIABLES like &ldquo;character_set_%&rdquo;</li>
<li>run &ldquo;SHOW SESSION VARIABLES&rdquo; from my benchmark client</li>
</ol>
<div>Note:</div>
</div>
<div>
<ul>
<li>the output from steps 1 and 2 was different</li>
<ul>
<li>with SHOW GLOBAL VARIABLES I got character_set_client =latin1 but with SHOW SESSION VARIABLES I got character_set_client =utf8mb3. This happens. One reason is that some MySQL client binaries autodetect the charset based on the value of LANG or LC_TYPE from your Linux env. Another reason is that if autodetection isn&rsquo;t done then the clients can use the default charset that was set at compile time. That charset is then passed to the server during connection handshake (see thd_init_client_charset). So it is likely that character_set_client as displayed by SHOW GLOBAL VARIABLES isn&rsquo;t what your client will use.</li>
</ul>
<li>the output from steps 2 and 3 was different</li>
<ul>
<li>autodetection is only done when mysql_options() is called with a certain flag (see below). And that is not done by the MySQL driver in sysbench, nor is it done by Python&rsquo;s MySQLdb. So my benchmark clients are likely selecting the default charset and don&rsquo;t do autodetection. And that default is determined by the version of the MySQL client library, meaning that default can change over the years. For the source that implements this, search for&nbsp;MYSQL_AUTODETECT_CHARSET_NAME and read sql-common/client.c.</li>
</ul>
</ul>
<div>The following enables autodetection and should be called before calling mysql_real_connect():</div>
<div><span><span>&nbsp; &nbsp; mysql_options(&hellip;,&nbsp;</span></span></div>
<div><span><span>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MYSQL_SET_CHARSET_NAME,</span></span></div>
<div><span><span>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MYSQL_AUTODETECT_CHARSET_NAME);</span></span></div>
<div><span><span><span><br></span></span></span></div>
<div><span><span>Note that adding the following into my.cnf isn&rsquo;t a workaround for clients that don&rsquo;t do autodetect.</span></span></div>
<div><span>&nbsp; &nbsp; [client]</span></div>
<div><span>&nbsp; &nbsp; default-character-set=&hellip;</span></div>
<div><span><br><span><b>Notes</b></span></span></div>
</div>
<div><span><span><br></span></span></div>
<div><span><span>These are from my usage of MySQL 5.7.44, 8.0.45 and 8.4.8 along with MariaDB 10.6.25, 10.11.16 and 11.4.10. All were compiled from source as was sysbench. I installed MySQLdb and the MySQL client library via apt for Ubuntu 24.04.
<p>The values for character_set_client, character_set_results and character_set_connection were measured via the MySQL command-line client running SHOW GLOBAL VARIABLES and SHOW SESSION VARIABLES and then the benchmark clients running SHOW SESSION VARIABLES.</p>
<p>The reason for sharing this is to explain the many possible values your session might use for character_set_client, character_set_results and character_set_connection. And using the wrong value might waste CPU.</p></span></span></div>
<div></div>

<table class="highlight tab-size js-file-line-container" data-hpc="" data-paste-markdown-skip="" data-tab-size="4" data-tagsearch-path="gistfile1.txt">
<tbody>
<tr>
<td class="blob-code blob-code-inner js-file-line">What per-session values are used for character_set_client|results|connection?</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">* my.cnf has character_set_server=latin1</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">* per SHOW GLOBAL VARIABLES each is set to =latin1</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">* values below measured via SHOW SESSION VARIABLES</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">Values for character_set_client|results|connection</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">&hellip; with &ldquo;mysql&rdquo; command line client</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">&hellip; this is easy to change with &ndash;default-character-set command line option or equivalent option in my.cnf</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">dbms</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">5.7.44          utf8</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.0.45          utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.4.8           utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.6.25         utf8mb3</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.11.16        utf8mb3</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">11.4.10         utf8mb3</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">Values for character_set_client|results|connection</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">&hellip; with sysbench</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line"></td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">                client library version</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">dbms            5.7     8.0     8.4     10.6    10.11   11.4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">5.7.44          latin1  latin1  latin1  NA      NA      NA</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.0.45          latin1  utf8mb4 utf8mb4 NA      NA      NA</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.4.8           latin1  utf8mb4 utf8mb4 NA      NA      NA</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.6.25         latin1  latin1  latin1  utf8mb4 utf8mb4 utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.11.16        latin1  latin1  latin1  utf8mb4 utf8mb4 utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">11.4.10         latin1  utf8mb4 utf8mb4 utf8mb4 utf8mb4 utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">Values for character_set_client|results|connection</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">&hellip; with insert benchmark (Python MySQLdb and /lib/x86_64-linux-gnu/libmysqlclient.so.21</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">&hellip; I am not what version is libmysqlclient.so.21, this is on Ubuntu 24.04</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line"></td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">dbms</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">5.7.44          latin1</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.0.45          utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">8.4.8           utf8mb4</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">
</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.6.25         latin1</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">10.11.16        latin1</td>
</tr>
<tr>
<td class="blob-code blob-code-inner js-file-line">11.4.10         utf8mb4</td>
</tr>
</tbody>
</table>

<p><a href="https://smalldatum.blogspot.com/2026/03/selecting-character-set-for-mysql-and.html">Selecting a character set for MySQL and MariaDB clients</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Binary Log Compression is Safe since MySQL 8.0.34</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html</id>
      <updated>2026-03-26T20:41:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This is a quick one.  My attention was recently brought (thanks Simon) on a relatively recent comment (25 Nov 2025) in Bug #103672 - Binlog compression transaction payload event exceeds max allowed packet :</p>
<p>The underlying server bug was fixed in 8.0.34 in BUG#33588473. The server now falls back to writing the transaction without compression, if the compressed size would</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html">Binary Log Compression is Safe since MySQL 8.0.34</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This is a quick one.&nbsp; My attention was recently brought (thanks Simon) on a relatively recent comment (25 Nov 2025) in Bug&nbsp;#103672 &ndash;&nbsp;Binlog compression transaction payload event exceeds max allowed packet&nbsp;:</p>
<p>The underlying server bug was fixed in 8.0.34 in BUG#33588473. The server now falls back to writing the transaction without compression, if the compressed size would</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html">Binary Log Compression is Safe since MySQL 8.0.34</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Binary Log Compression is Safe since MySQL 8.0.34</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html</id>
      <updated>2026-03-26T20:41:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This is a quick one.  My attention was recently brought (thanks Simon) on a relatively recent comment (25 Nov 2025) in Bug #103672 - Binlog compression transaction payload event exceeds max allowed packet :</p>
<p>The underlying server bug was fixed in 8.0.34 in BUG#33588473. The server now falls back to writing the transaction without compression, if the compressed size would</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html">Binary Log Compression is Safe since MySQL 8.0.34</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This is a quick one.&nbsp; My attention was recently brought (thanks Simon) on a relatively recent comment (25 Nov 2025) in Bug&nbsp;#103672 &ndash;&nbsp;Binlog compression transaction payload event exceeds max allowed packet&nbsp;:</p>
<p>The underlying server bug was fixed in 8.0.34 in BUG#33588473. The server now falls back to writing the transaction without compression, if the compressed size would</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/binlog-compression-now-safe.html">Binary Log Compression is Safe since MySQL 8.0.34</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Sysbench vs MySQL on a small server: no new regressions, many old ones</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html" />
      <id>https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html</id>
      <updated>2026-03-24T21:31:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has performance results for InnoDB from MySQL 5.6.51, 5.7.44, 8.0.X, 8.4.8 and 9.7.0 on a small server with sysbench microbenchmarks. The workload here is cached by InnoDB and my focus is on regressions from new CPU overheads. In many cases, MySQL 5.6.51 gets about 1.5X more QPS than modern MySQL (8.0.x thru 9.7). The root cause is new CPU overhead, possibly from code bloat.tl;drThere are too many performance regressions in MySQL 8.0.XThere are few performance regressions in MySQL 8.4 through 9.7.0In many cases MySQL 5.6.51 gets ~1.5X more QPS than 9.7.0 because 9.7.0 uses more CPULarge regressions arrived in MySQL 8.0.30 and 8.0.32, especiall for full-table scansBuilds, configuration and hardwareI compiled MySQL from source for versions 5.6.51, 5.7.44, 8.0.X, 8.4.8 and 9.7.0. For MySQL 8.0.X I used 8.0.28, 8.0.30, 8.0.31, 8.0.32, 8.0.33, 8.0.34, 8.0.35, 8.0.36 and 8.0.45.The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it are here. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.The my.cnf files are here for 5.6, 5.7, 8.4 and 9.7.The my.cnf files are here fo 8.0.28, 8.0.30, 8.0.31, 8.0.32, 8.0.33, 8.0.34, 8.0.35, 8.0.36 and 8.0.45.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 630 seconds and the write-heavy for 930 seconds.ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation. I provide tables below with relative QPS. When the relative QPS is &#62; 1 then some version is faster than the base version. When it is &#60; 1 then there might be a regression.  The relative QPS is below where the base version is either MySQL 5.6.51 or 8.0.28:(QPS for some version) / (QPS for base version) Values from iostat and vmstat divided by QPS are here for 5.6.51 as the base version and then here for 8.0.28 as the base version. These can help to explain why something is faster or slower because it shows how much HW is used per request.Results: point queriesSummary:there are large regressions from 5.6.51 to 5.7.44there are larger regressions from 5.7.44 to 8.0.45the regressions from 8.0.45 to 9.7.0 are smallthe regressions in the random-points tests are larger for range=10 than range=1000 (larger when the range is smaller). So the regressions are more likely to be in places other than InnoDB. The problem is new CPU overhead (see cpu/o here) which is 1.55X larger in 9.7.0 vs 5.6.51 for random-points_range=10 but only 1.19X larger in 9.7.0 for random-points_range=1000.Relative to: 5.6.51col-1 : 5.7.44col-2 : 8.0.45col-3 : 8.4.8col-4 : 9.7.0col-1   col-2   col-3   col-40.87    0.65    0.65    0.64    hot-points0.87    0.69    0.67    0.63    point-query0.87    0.72    0.72    0.71    points-covered-pk0.90    0.78    0.78    0.76    points-covered-si0.89    0.73    0.72    0.71    points-notcovered-pk0.89    0.77    0.76    0.75    points-notcovered-si1.00    0.84    0.83    0.83    random-points_range=10000.89    0.72    0.72    0.72    random-points_range=1000.87    0.69    0.68    0.66    random-points_range=10Summary:The large regressions in 8.0.x for point queries (see above) occur prior to 8.0.28Relative to: 8.0.28col-1 : 8.0.30col-2 : 8.0.31col-3 : 8.0.32col-4 : 8.0.33col-5 : 8.0.34col-6 : 8.0.35col-7 : 8.0.36col-8 : 8.0.45col-1   col-2   col-3   col-4   col-5   col-6   col-7   col-80.92    1.14    1.14    1.12    1.17    1.16    1.16    1.16    hot-points0.97    0.97    0.95    0.96    0.95    0.95    0.95    0.95    point-query0.94    1.09    1.09    1.08    1.12    1.12    1.11    1.15    points-covered-pk0.90    1.08    1.07    1.07    1.12    1.13    1.12    1.16    points-covered-si0.91    1.04    1.04    1.03    1.07    1.07    1.06    1.11    points-notcovered-pk0.88    0.96    0.96    0.95    1.00    1.01    1.00    1.06    points-notcovered-si0.79    2.35    2.42    2.37    2.45    2.45    2.47    2.56    random-points_range=10000.94    1.07    1.06    1.06    1.09    1.08    1.10    1.12    random-points_range=1000.93    0.94    0.93    0.93    0.94    0.94    0.93    0.95    random-points_range=10Results: range queries without aggregationSummary:there are large regressions from 5.6.51 to 5.7.44there are larger regressions from 5.7.44 to 8.0.45the regressions from 8.0.45 to 9.7.0 are smallthe problem is new CPU overhead and for the scan test the CPU overhead per query is about 1.5X larger in modern MySQL (8.0 thru 9.7) relative to MySQL 5.6.51 (see cpu/o here)Relative to: 5.6.51col-1 : 5.7.44col-2 : 8.0.45col-3 : 8.4.8col-4 : 9.7.0col-1   col-2   col-3   col-40.83    0.68    0.66    0.65    range-covered-pk0.83    0.70    0.69    0.67    range-covered-si0.84    0.66    0.65    0.64    range-notcovered-pk0.88    0.74    0.73    0.73    range-notcovered-si0.84    0.67    0.66    0.67    scanSummary:There is a large regression in 8.0.30 and a larger one in 8.0.32The scan test is the worst case for the regression.Relative to: 8.0.28col-1 : 8.0.30col-2 : 8.0.31col-3 : 8.0.32col-4 : 8.0.33col-5 : 8.0.34col-6 : 8.0.35col-7 : 8.0.36col-8 : 8.0.45col-1   col-2   col-3   col-4   col-5   col-6   col-7   col-80.95    0.94    0.92    0.92    0.92    0.93    0.93    0.96    range-covered-pk0.96    0.96    0.94    0.93    0.93    0.94    0.93    0.95    range-covered-si0.94    0.94    0.93    0.93    0.94    0.94    0.93    0.93    range-notcovered-pk0.89    0.87    0.87    0.86    0.89    0.91    0.89    0.95    range-notcovered-si0.93    0.92    0.79    0.82    0.83    0.77    0.82    0.80    scanResults: range queries with aggregationSummary:there are large regressions from 5.6.51 to 5.7.44there are larger regressions from 5.7.44 to 8.0.45the regressions from 8.0.45 to 9.7.0 are smallRelative to: 5.6.51col-1 : 5.7.44col-2 : 8.0.45col-3 : 8.4.8col-4 : 9.7.0col-1   col-2   col-3   col-40.86    0.70    0.69    0.68    read-only-count1.42    1.27    1.24    1.23    read-only-distinct0.91    0.75    0.74    0.73    read-only-order1.23    1.01    1.01    1.01    read-only_range=100000.93    0.77    0.76    0.74    read-only_range=1000.86    0.69    0.68    0.66    read-only_range=100.83    0.68    0.68    0.66    read-only-simple0.83    0.67    0.67    0.66    read-only-sumSummary:There are significant regressions in 8.0.30 and 8.0.32Relative to: 8.0.28col-1 : 8.0.30col-2 : 8.0.31col-3 : 8.0.32col-4 : 8.0.33col-5 : 8.0.34col-6 : 8.0.35col-7 : 8.0.36col-8 : 8.0.45col-1   col-2   col-3   col-4   col-5   col-6   col-7   col-80.95    0.94    0.87    0.87    0.88    0.87    0.89    0.91    read-only-count0.97    0.96    0.94    0.95    0.96    0.95    0.95    0.96    read-only-distinct0.97    0.96    0.93    0.95    0.95    0.94    0.95    0.95    read-only-order0.96    0.95    0.93    0.94    0.95    0.95    0.96    0.98    read-only_range=100000.96    0.96    0.94    0.95    0.95    0.94    0.95    0.94    read-only_range=1000.96    0.97    0.95    0.95    0.95    0.94    0.95    0.94    read-only_range=100.94    0.94    0.92    0.93    0.93    0.93    0.94    0.94    read-only-simple0.94    0.94    0.89    0.91    0.92    0.90    0.93    0.91    read-only-sumResults: writesSummary:there are large regressions from 5.6.51 to 5.7.44there are larger regressions from 5.7.44 to 8.0.45the regressions from 8.0.45 to 9.7.0 are smallthe insert test is the worst case and a big part of that is new CPU overhead, see cpu/o here, where it is 2.13X larger in 9.7.0 than 5.6.51. But for update-one the problem is writing more to storage per commit (see wkbpi here) rather than new CPU overhead.Relative to: 5.6.51col-1 : 5.7.44col-2 : 8.0.45col-3 : 8.4.8col-4 : 9.7.0col-1   col-2   col-3   col-40.85    0.60    0.59    0.55    delete0.81    0.55    0.54    0.52    insert0.93    0.75    0.74    0.71    read-write_range=1000.87    0.70    0.68    0.66    read-write_range=101.20    0.88    0.89    0.91    update-index1.04    0.74    0.73    0.71    update-inlist0.87    0.62    0.61    0.57    update-nonindex0.87    0.62    0.60    0.57    update-one0.87    0.63    0.61    0.58    update-zipf0.93    0.69    0.68    0.66    write-onlySummary:There are significant regressions in 8.0.30 and 8.0.32Relative to: 8.0.28col-1 : 8.0.30col-2 : 8.0.31col-3 : 8.0.32col-4 : 8.0.33col-5 : 8.0.34col-6 : 8.0.35col-7 : 8.0.36col-8 : 8.0.45col-1   col-2   col-3   col-4   col-5   col-6   col-7   col-80.96    0.95    0.92    0.92    0.92    0.91    0.91    0.91    delete0.94    0.93    0.91    0.91    0.91    0.90    0.90    0.90    insert0.96    0.96    0.94    0.94    0.94    0.94    0.94    0.93    read-write_range=1000.96    0.96    0.94    0.94    0.94    0.94    0.94    0.93    read-write_range=100.91    0.91    0.84    0.84    0.86    0.85    0.86    0.79    update-index0.94    0.95    0.92    0.91    0.92    0.91    0.91    0.91    update-inlist0.95    0.96    0.92    0.92    0.92    0.91    0.91    0.90    update-nonindex0.96    0.96    0.93    0.92    0.92    0.91    0.92    0.91    update-one0.96    0.96    0.92    0.92    0.92    0.91    0.91    0.90    update-zipf0.94    0.94    0.91    0.91    0.91    0.91    0.91    0.89    write-only</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html">Sysbench vs MySQL on a small server: no new regressions, many old ones</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has performance results for InnoDB from MySQL 5.6.51, 5.7.44, 8.0.X, 8.4.8 and 9.7.0 on a small server with sysbench microbenchmarks. The workload here is cached by InnoDB and my focus is on regressions from new CPU overheads.&nbsp;</p>
<p>In many cases, MySQL 5.6.51 gets about 1.5X more QPS than modern MySQL (8.0.x thru 9.7). The root cause is new CPU overhead, possibly from code bloat.</p>
<p>tl;dr</p>

<ul>
<li>There are too many performance regressions in MySQL 8.0.X</li>
<li>There are few performance regressions in MySQL 8.4 through 9.7.0</li>
<li>In many cases MySQL 5.6.51 gets ~1.5X more QPS than 9.7.0 because 9.7.0 uses more CPU</li>
<li>Large regressions arrived in MySQL 8.0.30 and 8.0.32, especiall for full-table scans</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>

<div></div>

<div>I compiled MySQL from source for versions 5.6.51, 5.7.44, 8.0.X, 8.4.8 and 9.7.0. For MySQL 8.0.X I used 8.0.28, 8.0.30, 8.0.31, 8.0.32, 8.0.33, 8.0.34, 8.0.35, 8.0.36 and 8.0.45.</div>
</div>
<p>The server is an ASUS ExpertCenter PN53 with AMD Ryzen 7 7735HS, 32G RAM and an m.2 device for the database. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2022/10/small-servers-for-performance-testing-v4.html">are here</a>. The OS is Ubuntu 24.04 and the database filesystem is ext4 with discard enabled.</p>
<p>The my.cnf files are here for <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my5651_rel_o2nofp/etc/my.cnf.cz12a_c8r32">5.6</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my5744_rel_o2nofp/etc/my.cnf.cz12a_c8r32">5.7</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8406_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.4</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my97/etc/my.cnf.cz12a_c8r32">9.7</a>.</p>
<p>The my.cnf files are here fo <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8028_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.28</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8030_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.30</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8031_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.31</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8032_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.32</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8033_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.33</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8034_rel/etc/my.cnf.cz12a_c8r32">8.0.34</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8035_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.35</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8036_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.36</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8040_rel_o2nofp/etc/my.cnf.cz12a_c8r32">8.0.45</a>.</p>
<p><b>Benchmark</b></p>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by InnoDB.</div>
<div>The tests are run using 1 table with 50M rows. The read-heavy microbenchmarks run for 630 seconds and the write-heavy for 930 seconds.</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;</span></div>
<div>I provide tables below with relative QPS.&nbsp;<span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than the</span><span>&nbsp;</span><i>base version.</i><span>&nbsp;When it is &lt; 1 then there might be a regression.&nbsp;&nbsp;</span><span>The relative QPS is below where the base version is either MySQL 5.6.51 or 8.0.28:</span></div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for base version)<span>&nbsp;</span></p></blockquote>
</div>
<div><span>Values from iostat and vmstat divided by QPS are <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.latest">here for 5.6.51</a> as the base version and then <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.my80v2">here for 8.0.28</a> as the base version</span><span>. These can help to explain why something is faster or slower because it shows how much HW is used per request.</span></div>
<div></div>
<div><b>Results: point queries</b></div>
<div></div>
<div>Summary:</div>
<div>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44</li>
<li>there are larger regressions from 5.7.44 to 8.0.45</li>
<li>the regressions from 8.0.45 to 9.7.0 are small</li>
<li>the regressions in the random-points tests are larger for range=10 than range=1000 (larger when the range is smaller). So the regressions are more likely to be in places other than InnoDB. The problem is new CPU overhead (<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.latest#L281-L321">see cpu/o here</a>) which is 1.55X larger in 9.7.0 vs 5.6.51 for random-points_range=10 but only 1.19X larger in 9.7.0 for random-points_range=1000.</li>
</ul>
</div>
<div><span>Relative to: 5.6.51</span></div>
<div>
<div><span>col-1 : 5.7.44</span></div>
<div><span>col-2 : 8.0.45</span></div>
<div><span>col-3 : 8.4.8</span></div>
<div><span>col-4 : 9.7.0</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4</span></div>
<div><span>0.87&nbsp; &nbsp; <span>0.65</span>&nbsp; &nbsp; 0.65&nbsp; &nbsp; 0.64&nbsp; &nbsp; hot-points</span></div>
<div><span>0.87&nbsp; &nbsp; 0.69&nbsp; &nbsp; 0.67&nbsp; &nbsp; 0.63&nbsp; &nbsp; point-query</span></div>
<div><span>0.87&nbsp; &nbsp; 0.72&nbsp; &nbsp; 0.72&nbsp; &nbsp; 0.71&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.90&nbsp; &nbsp; 0.78&nbsp; &nbsp; 0.78&nbsp; &nbsp; 0.76&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.89&nbsp; &nbsp; 0.73&nbsp; &nbsp; 0.72&nbsp; &nbsp; 0.71&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.89&nbsp; &nbsp; 0.77&nbsp; &nbsp; 0.76&nbsp; &nbsp; 0.75&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span>1.00&nbsp; &nbsp; <span>0.84</span>&nbsp; &nbsp; 0.83&nbsp; &nbsp; 0.83&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.89&nbsp; &nbsp; <span>0.72</span>&nbsp; &nbsp; 0.72&nbsp; &nbsp; 0.72&nbsp; &nbsp; random-points_range=100</span></div>
<div><span>0.87&nbsp; &nbsp; <span>0.69</span>&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
<div>Summary:</div>
<div>
<ul>
<li>The large regressions in 8.0.x for point queries (see above) occur prior to 8.0.28</li>
</ul>
</div>
<div><span>Relative to: 8.0.28</span></div>
<div>
<div><span>col-1 : 8.0.30</span></div>
<div><span>col-2 : 8.0.31</span></div>
<div><span>col-3 : 8.0.32</span></div>
<div><span>col-4 : 8.0.33</span></div>
<div><span>col-5 : 8.0.34</span></div>
<div><span>col-6 : 8.0.35</span></div>
<div><span>col-7 : 8.0.36</span></div>
<div><span>col-8 : 8.0.45</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7&nbsp; &nbsp;col-8</span></div>
<div><span>0.92&nbsp; &nbsp; 1.14&nbsp; &nbsp; 1.14&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.17&nbsp; &nbsp; 1.16&nbsp; &nbsp; 1.16&nbsp; &nbsp; 1.16&nbsp; &nbsp; hot-points</span></div>
<div><span>0.97&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; point-query</span></div>
<div><span>0.94&nbsp; &nbsp; 1.09&nbsp; &nbsp; 1.09&nbsp; &nbsp; 1.08&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.11&nbsp; &nbsp; 1.15&nbsp; &nbsp; points-covered-pk</span></div>
<div><span>0.90&nbsp; &nbsp; 1.08&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.13&nbsp; &nbsp; 1.12&nbsp; &nbsp; 1.16&nbsp; &nbsp; points-covered-si</span></div>
<div><span>0.91&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.04&nbsp; &nbsp; 1.03&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.11&nbsp; &nbsp; points-notcovered-pk</span></div>
<div><span>0.88&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.00&nbsp; &nbsp; 1.06&nbsp; &nbsp; points-notcovered-si</span></div>
<div><span>0.79&nbsp; &nbsp; 2.35&nbsp; &nbsp; 2.42&nbsp; &nbsp; 2.37&nbsp; &nbsp; 2.45&nbsp; &nbsp; 2.45&nbsp; &nbsp; 2.47&nbsp; &nbsp; 2.56&nbsp; &nbsp; random-points_range=1000</span></div>
<div><span>0.94&nbsp; &nbsp; 1.07&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.06&nbsp; &nbsp; 1.09&nbsp; &nbsp; 1.08&nbsp; &nbsp; 1.10&nbsp; &nbsp; 1.12&nbsp; &nbsp; random-points_range=100</span></div>
<div><span>0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.95&nbsp; &nbsp; random-points_range=10</span></div>
</div>
<div></div>
<div>
<div><b>Results: range queries without aggregation</b></div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44</li>
<li>there are larger regressions from 5.7.44 to 8.0.45</li>
<li>the regressions from 8.0.45 to 9.7.0 are small</li>
<li>the problem is new CPU overhead and for the scan test the CPU overhead per query is about 1.5X larger in modern MySQL (8.0 thru 9.7) relative to MySQL 5.6.51 (<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.latest#L519-L531">see cpu/o here</a>)</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 5.6.51</span></div>
<div><span>col-1 : 5.7.44</span></div>
<div><span>col-2 : 8.0.45</span></div>
<div><span>col-3 : 8.4.8</span></div>
<div><span>col-4 : 9.7.0</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4</span></div>
<div><span>0.83&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; 0.65&nbsp; &nbsp; range-covered-pk</span></div>
<div><span>0.83&nbsp; &nbsp; 0.70&nbsp; &nbsp; 0.69&nbsp; &nbsp; 0.67&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.84&nbsp; &nbsp; 0.66&nbsp; &nbsp; 0.65&nbsp; &nbsp; 0.64&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.88&nbsp; &nbsp; 0.74&nbsp; &nbsp; 0.73&nbsp; &nbsp; 0.73&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span><span>0.84</span>&nbsp; &nbsp; <span>0.67</span>&nbsp; &nbsp; 0.66&nbsp; &nbsp; 0.67&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>There is a large regression in 8.0.30 and a larger one in 8.0.32</li>
<li>The scan test is the worst case for the regression.</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 8.0.28</span></div>
<div><span>col-1 : 8.0.30</span></div>
<div><span>col-2 : 8.0.31</span></div>
<div><span>col-3 : 8.0.32</span></div>
<div><span>col-4 : 8.0.33</span></div>
<div><span>col-5 : 8.0.34</span></div>
<div><span>col-6 : 8.0.35</span></div>
<div><span>col-7 : 8.0.36</span></div>
<div><span>col-8 : 8.0.45</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7&nbsp; &nbsp;col-8</span></div>
<div><span>0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.96&nbsp; &nbsp; range-covered-pk</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.95&nbsp; &nbsp; range-covered-si</span></div>
<div><span>0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; range-notcovered-pk</span></div>
<div><span>0.89&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.86&nbsp; &nbsp; 0.89&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.89&nbsp; &nbsp; 0.95&nbsp; &nbsp; range-notcovered-si</span></div>
<div><span><span>0.93</span>&nbsp; &nbsp; 0.92&nbsp; &nbsp; <span>0.79</span>&nbsp; &nbsp; 0.82&nbsp; &nbsp; 0.83&nbsp; &nbsp; 0.77&nbsp; &nbsp; 0.82&nbsp; &nbsp; 0.80&nbsp; &nbsp; scan</span></div>
</div>
<div></div>
<div>
<div><b>Results: range queries with aggregation</b></div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44</li>
<li>there are larger regressions from 5.7.44 to 8.0.45</li>
<li>the regressions from 8.0.45 to 9.7.0 are small</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 5.6.51</span></div>
<div><span>col-1 : 5.7.44</span></div>
<div><span>col-2 : 8.0.45</span></div>
<div><span>col-3 : 8.4.8</span></div>
<div><span>col-4 : 9.7.0</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4</span></div>
<div><span>0.86&nbsp; &nbsp; 0.70&nbsp; &nbsp; 0.69&nbsp; &nbsp; 0.68&nbsp; &nbsp; read-only-count</span></div>
<div><span>1.42&nbsp; &nbsp; 1.27&nbsp; &nbsp; 1.24&nbsp; &nbsp; 1.23&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.91&nbsp; &nbsp; 0.75&nbsp; &nbsp; 0.74&nbsp; &nbsp; 0.73&nbsp; &nbsp; read-only-order</span></div>
<div><span>1.23&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; 1.01&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.93&nbsp; &nbsp; 0.77&nbsp; &nbsp; 0.76&nbsp; &nbsp; 0.74&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.86&nbsp; &nbsp; 0.69&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; read-only_range=10</span></div>
<div><span><span>0.83</span>&nbsp; &nbsp; <span>0.68</span>&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; read-only-simple</span></div>
<div><span>0.83&nbsp; &nbsp; 0.67&nbsp; &nbsp; 0.67&nbsp; &nbsp; 0.66&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>There are significant regressions in 8.0.30 and 8.0.32</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 8.0.28</span></div>
<div><span>col-1 : 8.0.30</span></div>
<div><span>col-2 : 8.0.31</span></div>
<div><span>col-3 : 8.0.32</span></div>
<div><span>col-4 : 8.0.33</span></div>
<div><span>col-5 : 8.0.34</span></div>
<div><span>col-6 : 8.0.35</span></div>
<div><span>col-7 : 8.0.36</span></div>
<div><span>col-8 : 8.0.45</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7&nbsp; &nbsp;col-8</span></div>
<div><span>0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.88&nbsp; &nbsp; 0.87&nbsp; &nbsp; 0.89&nbsp; &nbsp; 0.91&nbsp; &nbsp; read-only-count</span></div>
<div><span>0.97&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; read-only-distinct</span></div>
<div><span>0.97&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; read-only-order</span></div>
<div><span>0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.98&nbsp; &nbsp; read-only_range=10000</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; read-only_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; 0.97&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.94&nbsp; &nbsp; read-only_range=10</span></div>
<div><span>0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; read-only-simple</span></div>
<div><span><span>0.94</span>&nbsp; &nbsp; 0.94&nbsp; &nbsp; <span>0.89</span>&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.91&nbsp; &nbsp; read-only-sum</span></div>
</div>
<div></div>
<div>
<div><b>Results: writes</b></div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>there are large regressions from 5.6.51 to 5.7.44</li>
<li>there are larger regressions from 5.7.44 to 8.0.45</li>
<li>the regressions from 8.0.45 to 9.7.0 are small</li>
<li>the insert test is the worst case and a big part of that is new CPU overhead, <a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.latest#L477-L489">see cpu/o here</a>, where it is 2.13X larger in 9.7.0 than 5.6.51. But for update-one the problem is writing more to storage per commit (<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/mar26.pn53.sb.my/o.met.latest#L477-L489">see wkbpi here</a>) rather than new CPU overhead.</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 5.6.51</span></div>
<div><span>col-1 : 5.7.44</span></div>
<div><span>col-2 : 8.0.45</span></div>
<div><span>col-3 : 8.4.8</span></div>
<div><span>col-4 : 9.7.0</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4</span></div>
<div><span>0.85&nbsp; &nbsp; 0.60&nbsp; &nbsp; 0.59&nbsp; &nbsp; 0.55&nbsp; &nbsp; delete</span></div>
<div><span><span>0.81</span>&nbsp; &nbsp; <span>0.55</span>&nbsp; &nbsp; 0.54&nbsp; &nbsp; 0.52&nbsp; &nbsp; insert</span></div>
<div><span>0.93&nbsp; &nbsp; 0.75&nbsp; &nbsp; 0.74&nbsp; &nbsp; 0.71&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.87&nbsp; &nbsp; 0.70&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>1.20&nbsp; &nbsp; 0.88&nbsp; &nbsp; 0.89&nbsp; &nbsp; 0.91&nbsp; &nbsp; update-index</span></div>
<div><span>1.04&nbsp; &nbsp; 0.74&nbsp; &nbsp; 0.73&nbsp; &nbsp; 0.71&nbsp; &nbsp; update-inlist</span></div>
<div><span>0.87&nbsp; &nbsp; 0.62&nbsp; &nbsp; 0.61&nbsp; &nbsp; 0.57&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.87&nbsp; &nbsp; 0.62&nbsp; &nbsp; 0.60&nbsp; &nbsp; 0.57&nbsp; &nbsp; update-one</span></div>
<div><span>0.87&nbsp; &nbsp; 0.63&nbsp; &nbsp; 0.61&nbsp; &nbsp; 0.58&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.93&nbsp; &nbsp; 0.69&nbsp; &nbsp; 0.68&nbsp; &nbsp; 0.66&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>There are significant regressions in 8.0.30 and 8.0.32</li>
</ul>
</div>
</div>
<div>
<div><span>Relative to: 8.0.28</span></div>
<div><span>col-1 : 8.0.30</span></div>
<div><span>col-2 : 8.0.31</span></div>
<div><span>col-3 : 8.0.32</span></div>
<div><span>col-4 : 8.0.33</span></div>
<div><span>col-5 : 8.0.34</span></div>
<div><span>col-6 : 8.0.35</span></div>
<div><span>col-7 : 8.0.36</span></div>
<div><span>col-8 : 8.0.45</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3&nbsp; &nbsp;col-4&nbsp; &nbsp;col-5&nbsp; &nbsp;col-6&nbsp; &nbsp;col-7&nbsp; &nbsp;col-8</span></div>
<div><span>0.96&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; delete</span></div>
<div><span>0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.90&nbsp; &nbsp; 0.90&nbsp; &nbsp; insert</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.93&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.84&nbsp; &nbsp; 0.84&nbsp; &nbsp; 0.86&nbsp; &nbsp; 0.85&nbsp; &nbsp; 0.86&nbsp; &nbsp; 0.79&nbsp; &nbsp; update-index</span></div>
<div><span>0.94&nbsp; &nbsp; 0.95&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; update-inlist</span></div>
<div><span><span>0.95</span>&nbsp; &nbsp; 0.96&nbsp; &nbsp; <span>0.92</span>&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.90&nbsp; &nbsp; update-nonindex</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.93&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; update-one</span></div>
<div><span>0.96&nbsp; &nbsp; 0.96&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.92&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.90&nbsp; &nbsp; update-zipf</span></div>
<div><span>0.94&nbsp; &nbsp; 0.94&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.91&nbsp; &nbsp; 0.89&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>
</div>
</div>
</div>
<p></p></span></div>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/03/sysbench-vs-mysql-on-small-server-no.html">Sysbench vs MySQL on a small server: no new regressions, many old ones</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB/MySQL Environment MyEnv 3.0.0 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/" />
      <id>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/</id>
      <updated>2026-03-23T16:42:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.0 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment MyEnv.<br />
The new MyEnv can be downloaded here. How to install MyEnv is described in the MyEnv Installation Guide.<br />
In the inconceivable case that you find a bug in the MyEnv please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Upgrade from 2.x to 3.0<br />
Please check the MyEnv Installation Guide.<br />
Changes in MyEnv 3.0.0<br />
MyEnv</p>
<p>Template warning improved.<br />
Distro version in --version added.<br />
Check MyEnv configuration permissions.<br />
#fd increased for MyEnv.<br />
myenv.conf should have more secure permissions now.<br />
Situation caught when my.cnf is missing in myenv.conf.<br />
Directories home and run moved to dba and myenv.<br />
Unit file mariadb.service and mysql.service replaced by dba.service.<br />
User mysql replaced by dba in template.<br />
start_stop fixed warning in case argv[1] is missing.<br />
sys_uid filter fixed for Rocky Linux.<br />
dba unit file added to package.<br />
Nagios plugins detection removed from showMyEnvVersion.<br />
Old SysV init files removed and replaced by Systemd unit files.<br />
dba user was introduced and check for system user added.<br />
User dba changed an cosmetic fixes.</p>
<p>MyEnv Installer</p>
<p>2 concurrent installMyEnv versions cannot run any more.<br />
Directroy binlog, cgroups and angel removed from postgresql type installation.<br />
Wrapper script installMyEnv.sh removed.<br />
Cosmetics fixed in installer.<br />
Installation made more mysql friendly.<br />
libaio1t64 considered during installation recommendations on DEB systems.<br />
Next free port suggestion during installMyEnv improved. It will suggest the first free port now.<br />
apt-get and yum replaced by apt and dnf.</p>
<p>MyEnv Utilities</p>
<p>insert_test.sh made PostgreSQL ready.</p>
<p>PostgreSQL</p>
<p>PostgreSQL instance is stopped with fast instead of immediate now.<br />
show_create_table.sh for PostgreSQL made nicer.<br />
PostgreSQL status.sql added.<br />
Minor fixes for PostgreSQL.</p>
<p>General</p>
<p>CHANGELOG updated.<br />
rc made unique.<br />
Minor bugs fixed.<br />
Copyright year updated from 2024 to 2026.<br />
mkdir changed from /bin to /usr/bin which is the new/right standard on all our 3 supported distributions.</p>
<p>Documentation</p>
<p>README updated.<br />
Installation documentation improved, library related stuff documented.<br />
PostgreSQL added to documentation.<br />
lsb_release removed from documentation.<br />
Documentation restructured.<br />
Documentation made more resilient agaist errors.<br />
Documentation process improved.<br />
Documentation moved completely to asciidoc.</p>
<p>Packaging</p>
<p>Package list completed.<br />
Makefile fixed.<br />
Old distro stuff and initV stuff removed.<br />
Build scripts fixed.</p>
<p>For subscriptions of commercial use of MyEnv please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.0 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.0 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment <a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment">MyEnv</a>.</p>
<p>The new MyEnv can be downloaded <a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download">here</a>. How to install MyEnv is described in the <a href="https://support.fromdual.com/documentation/myenv/myenv.html#installation-guide" target="_blank">MyEnv Installation Guide</a>.</p>
<p>In the inconceivable case that you find a bug in the MyEnv please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20myenv">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">send them to us</a>.</p>
<h2>Upgrade from 2.x to 3.0<a class="anchor-link" id="upgrade-from-2-x-to-3-0"></a></h2>
<p>Please check the <a href="https://support.fromdual.com/documentation/myenv/myenv.html#upgrade" target="_blank" title="Upgrading MyEnv">MyEnv Installation Guide</a>.</p>
<h2>Changes in MyEnv 3.0.0<a class="anchor-link" id="changes-in-myenv-3-0-0"></a></h2>
<h3>MyEnv<a class="anchor-link" id="myenv"></a></h3>
<ul>
<li>Template warning improved.</li>
<li>Distro version in <code>--version</code> added.</li>
<li>Check MyEnv configuration permissions.</li>
<li>#fd increased for MyEnv.</li>
<li><code>myenv.conf</code> should have more secure permissions now.</li>
<li>Situation caught when <code>my.cnf</code> is missing in <code>myenv.conf</code>.</li>
<li>Directories <code>home</code> and <code>run</code> moved to <code>dba</code> and <code>myenv</code>.</li>
<li>Unit file <code>mariadb.service</code> and <code>mysql.service</code> replaced by <code>dba.service</code>.</li>
<li>User <code>mysql</code> replaced by <code>dba</code> in template.</li>
<li><code>start_stop</code> fixed warning in case <code>argv[1]</code> is missing.</li>
<li><code>sys_uid</code> filter fixed for Rocky Linux.</li>
<li><code>dba</code> unit file added to package.</li>
<li>Nagios plugins detection removed from <code>showMyEnvVersion</code>.</li>
<li>Old SysV init files removed and replaced by Systemd unit files.</li>
<li><code>dba</code> user was introduced and check for system user added.</li>
<li>User <code>dba</code> changed an cosmetic fixes.</li>
</ul>
<h3>MyEnv Installer<a class="anchor-link" id="myenv-installer"></a></h3>
<ul>
<li>2 concurrent <code>installMyEnv</code> versions cannot run any more.</li>
<li>Directroy <code>binlog</code>, <code>cgroups</code> and <code>angel</code> removed from <code>postgresql</code> <code>type</code> installation.</li>
<li>Wrapper script <code>installMyEnv.sh</code> removed.</li>
<li>Cosmetics fixed in installer.</li>
<li>Installation made more mysql friendly.</li>
<li><code>libaio1t64</code> considered during installation recommendations on DEB systems.</li>
<li>Next free port suggestion during <code>installMyEnv</code> improved. It will suggest the first free port now.</li>
<li><code>apt-get</code> and <code>yum</code> replaced by <code>apt</code> and <code>dnf</code>.</li>
</ul>
<h3>MyEnv Utilities<a class="anchor-link" id="myenv-utilities"></a></h3>
<ul>
<li><code>insert_test.sh</code> made PostgreSQL ready.</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li>PostgreSQL instance is stopped with fast instead of immediate now.</li>
<li><code>show_create_table.sh</code> for PostgreSQL made nicer.</li>
<li>PostgreSQL <code>status.sql</code> added.</li>
<li>Minor fixes for PostgreSQL.</li>
</ul>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li><code>CHANGELOG</code> updated.</li>
<li><code>rc</code> made unique.</li>
<li>Minor bugs fixed.</li>
<li>Copyright year updated from 2024 to 2026.</li>
<li><code>mkdir</code> changed from <code>/bin</code> to <code>/usr/bin</code> which is the new/right standard on all our 3 supported distributions.</li>
</ul>
<h3>Documentation<a class="anchor-link" id="documentation"></a></h3>
<ul>
<li>README updated.</li>
<li>Installation documentation improved, library related stuff documented.</li>
<li>PostgreSQL added to documentation.</li>
<li><code>lsb_release</code> removed from documentation.</li>
<li>Documentation restructured.</li>
<li>Documentation made more resilient agaist errors.</li>
<li>Documentation process improved.</li>
<li>Documentation moved completely to asciidoc.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>Package list completed.</li>
<li>Makefile fixed.</li>
<li>Old distro stuff and initV stuff removed.</li>
<li>Build scripts fixed.</li>
</ul>
<p>For subscriptions of commercial use of MyEnv please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20MyEnv">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.0 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB/MySQL Environment MyEnv 3.0.0 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/" />
      <id>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/</id>
      <updated>2026-03-23T16:42:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.0 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment MyEnv.<br />
The new MyEnv can be downloaded here. How to install MyEnv is described in the MyEnv Installation Guide.<br />
In the inconceivable case that you find a bug in the MyEnv please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Upgrade from 2.x to 3.0<br />
Please check the MyEnv Installation Guide.<br />
Changes in MyEnv 3.0.0<br />
MyEnv</p>
<p>Template warning improved.<br />
Distro version in --version added.<br />
Check MyEnv configuration permissions.<br />
#fd increased for MyEnv.<br />
myenv.conf should have more secure permissions now.<br />
Situation caught when my.cnf is missing in myenv.conf.<br />
Directories home and run moved to dba and myenv.<br />
Unit file mariadb.service and mysql.service replaced by dba.service.<br />
User mysql replaced by dba in template.<br />
start_stop fixed warning in case argv[1] is missing.<br />
sys_uid filter fixed for Rocky Linux.<br />
dba unit file added to package.<br />
Nagios plugins detection removed from showMyEnvVersion.<br />
Old SysV init files removed and replaced by Systemd unit files.<br />
dba user was introduced and check for system user added.<br />
User dba changed an cosmetic fixes.</p>
<p>MyEnv Installer</p>
<p>2 concurrent installMyEnv versions cannot run any more.<br />
Directroy binlog, cgroups and angel removed from postgresql type installation.<br />
Wrapper script installMyEnv.sh removed.<br />
Cosmetics fixed in installer.<br />
Installation made more mysql friendly.<br />
libaio1t64 considered during installation recommendations on DEB systems.<br />
Next free port suggestion during installMyEnv improved. It will suggest the first free port now.<br />
apt-get and yum replaced by apt and dnf.</p>
<p>MyEnv Utilities</p>
<p>insert_test.sh made PostgreSQL ready.</p>
<p>PostgreSQL</p>
<p>PostgreSQL instance is stopped with fast instead of immediate now.<br />
show_create_table.sh for PostgreSQL made nicer.<br />
PostgreSQL status.sql added.<br />
Minor fixes for PostgreSQL.</p>
<p>General</p>
<p>CHANGELOG updated.<br />
rc made unique.<br />
Minor bugs fixed.<br />
Copyright year updated from 2024 to 2026.<br />
mkdir changed from /bin to /usr/bin which is the new/right standard on all our 3 supported distributions.</p>
<p>Documentation</p>
<p>README updated.<br />
Installation documentation improved, library related stuff documented.<br />
PostgreSQL added to documentation.<br />
lsb_release removed from documentation.<br />
Documentation restructured.<br />
Documentation made more resilient agaist errors.<br />
Documentation process improved.<br />
Documentation moved completely to asciidoc.</p>
<p>Packaging</p>
<p>Package list completed.<br />
Makefile fixed.<br />
Old distro stuff and initV stuff removed.<br />
Build scripts fixed.</p>
<p>For subscriptions of commercial use of MyEnv please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.0 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 3.0.0 of its popular MariaDB, MySQL and PostgreSQL multi-instance environment <a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment">MyEnv</a>.</p>
<p>The new MyEnv can be downloaded <a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download">here</a>. How to install MyEnv is described in the <a href="https://support.fromdual.com/documentation/myenv/myenv.html#installation-guide" target="_blank">MyEnv Installation Guide</a>.</p>
<p>In the inconceivable case that you find a bug in the MyEnv please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20myenv">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">send them to us</a>.</p>
<h2>Upgrade from 2.x to 3.0<a class="anchor-link" id="upgrade-from-2-x-to-3-0"></a></h2>
<p>Please check the <a href="https://support.fromdual.com/documentation/myenv/myenv.html#upgrade" target="_blank" title="Upgrading MyEnv">MyEnv Installation Guide</a>.</p>
<h2>Changes in MyEnv 3.0.0<a class="anchor-link" id="changes-in-myenv-3-0-0"></a></h2>
<h3>MyEnv<a class="anchor-link" id="myenv"></a></h3>
<ul>
<li>Template warning improved.</li>
<li>Distro version in <code>--version</code> added.</li>
<li>Check MyEnv configuration permissions.</li>
<li>#fd increased for MyEnv.</li>
<li><code>myenv.conf</code> should have more secure permissions now.</li>
<li>Situation caught when <code>my.cnf</code> is missing in <code>myenv.conf</code>.</li>
<li>Directories <code>home</code> and <code>run</code> moved to <code>dba</code> and <code>myenv</code>.</li>
<li>Unit file <code>mariadb.service</code> and <code>mysql.service</code> replaced by <code>dba.service</code>.</li>
<li>User <code>mysql</code> replaced by <code>dba</code> in template.</li>
<li><code>start_stop</code> fixed warning in case <code>argv[1]</code> is missing.</li>
<li><code>sys_uid</code> filter fixed for Rocky Linux.</li>
<li><code>dba</code> unit file added to package.</li>
<li>Nagios plugins detection removed from <code>showMyEnvVersion</code>.</li>
<li>Old SysV init files removed and replaced by Systemd unit files.</li>
<li><code>dba</code> user was introduced and check for system user added.</li>
<li>User <code>dba</code> changed an cosmetic fixes.</li>
</ul>
<h3>MyEnv Installer<a class="anchor-link" id="myenv-installer"></a></h3>
<ul>
<li>2 concurrent <code>installMyEnv</code> versions cannot run any more.</li>
<li>Directroy <code>binlog</code>, <code>cgroups</code> and <code>angel</code> removed from <code>postgresql</code> <code>type</code> installation.</li>
<li>Wrapper script <code>installMyEnv.sh</code> removed.</li>
<li>Cosmetics fixed in installer.</li>
<li>Installation made more mysql friendly.</li>
<li><code>libaio1t64</code> considered during installation recommendations on DEB systems.</li>
<li>Next free port suggestion during <code>installMyEnv</code> improved. It will suggest the first free port now.</li>
<li><code>apt-get</code> and <code>yum</code> replaced by <code>apt</code> and <code>dnf</code>.</li>
</ul>
<h3>MyEnv Utilities<a class="anchor-link" id="myenv-utilities"></a></h3>
<ul>
<li><code>insert_test.sh</code> made PostgreSQL ready.</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li>PostgreSQL instance is stopped with fast instead of immediate now.</li>
<li><code>show_create_table.sh</code> for PostgreSQL made nicer.</li>
<li>PostgreSQL <code>status.sql</code> added.</li>
<li>Minor fixes for PostgreSQL.</li>
</ul>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li><code>CHANGELOG</code> updated.</li>
<li><code>rc</code> made unique.</li>
<li>Minor bugs fixed.</li>
<li>Copyright year updated from 2024 to 2026.</li>
<li><code>mkdir</code> changed from <code>/bin</code> to <code>/usr/bin</code> which is the new/right standard on all our 3 supported distributions.</li>
</ul>
<h3>Documentation<a class="anchor-link" id="documentation"></a></h3>
<ul>
<li>README updated.</li>
<li>Installation documentation improved, library related stuff documented.</li>
<li>PostgreSQL added to documentation.</li>
<li><code>lsb_release</code> removed from documentation.</li>
<li>Documentation restructured.</li>
<li>Documentation made more resilient agaist errors.</li>
<li>Documentation process improved.</li>
<li>Documentation moved completely to asciidoc.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>Package list completed.</li>
<li>Makefile fixed.</li>
<li>Old distro stuff and initV stuff removed.</li>
<li>Build scripts fixed.</li>
</ul>
<p>For subscriptions of commercial use of MyEnv please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20MyEnv">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-3.0.0-has-been-released/">MariaDB/MySQL Environment MyEnv 3.0.0 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/" />
      <id>https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/</id>
      <updated>2026-03-20T08:00:00+02:00</updated>
      <author><name>Sebastian Insausti</name></author>
      <summary type="html"><![CDATA[<p>Today, we’re asking PostgreSQL to do more and more, like handling both transactions and analytics, powering huge SaaS platforms, managing event data, and even dipping into AI-related tasks like vector search. This intense pressure highlights some pain points: slow, unpredictable reads, rigid indexing, complex setups, and risky major upgrades.  PostgreSQL 18 directly addresses these real-world […]<br />
The post PostgreSQL 18 Upgrades for AI-Era Workloads and Operations appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Today, we&rsquo;re asking PostgreSQL to do more and more, like handling both transactions and analytics, powering huge SaaS platforms, managing event data, and even dipping into AI-related tasks like vector search. This intense pressure highlights some pain points: slow, unpredictable reads, rigid indexing, complex setups, and risky major upgrades.&nbsp;</p>
<p>PostgreSQL 18 directly addresses these real-world operational challenges, skipping flashy features for practical improvements like asynchronous I/O to speed up reads, safer upgrades from keeping optimizer stats, better multi-column indexes, UUIDv7 support, and useful enhancements to logical replication.</p>
<p>I&rsquo;ll dive into these key operational changes, show how they fit your modern workflows, and explain how tools like ClusterControl can make adopting them smooth and painless.</p>
<h2 class="wp-block-heading"><strong>PostgreSQL 18&rsquo;s key features that improve core workload performance</strong><a class="anchor-link" id="postgresql-18s-key-features-that-improve-core-workload-performance"></a></h2>
<p>PostgreSQL 18 isn&rsquo;t about one big, new thing. Instead, it offers fixes for common headaches: slow storage, reads competing with new transactions, inconsistent performance post-upgrade, indexes that don&rsquo;t quite hit the mark, messy authentication, and confusing replication errors &mdash; these fixes smooth out the rough operational edges. Let&rsquo;s look at two key features that will have the greatest effect on production work.</p>
<h3 class="wp-block-heading"><strong>Async I/O subsystem (AIO)</strong><a class="anchor-link" id="async-i-o-subsystem-aio"></a></h3>
<p>Slow storage often causes problems, not the computer itself. Older PostgreSQL waited for each storage request, which is safe but can slow things down, especially when your data is too big for the cache or when running big scans alongside regular transactions.</p>
<p>PostgreSQL 18 fixes this with Asynchronous I/O (AIO). Now, the system can ask for multiple data blocks and keep working instead of waiting for each one. This can significantly improve read-heavy scans and some maintenance operations under the right I/O conditions. New settings like <code>io_method</code> help you tune this &mdash; <strong>don&rsquo;t just test AIO with a single query.</strong></p>
<p>Its real power shows up under heavy, simultaneous load. To see the benefit, test your actual, read-heavy workload, focusing on P95/P99 latency improvements. Pay special attention to maintenance, since PG 18 specifically aims to reduce I/O slowdown during VACUUM.</p>
<p><strong>TIP:</strong> A good test is to compare latency under pressure with and without AIO while background maintenance is running.</p>
<h3 class="wp-block-heading"><strong>Skip-scan on B-tree indexes</strong><a class="anchor-link" id="skip-scan-on-b-tree-indexes"></a></h3>
<p>In large systems, especially SaaS, we often use multi-column indexes. The problem is that queries don&rsquo;t always filter by the index&rsquo;s first column. For example, an index on (tenant_id, created_at) won&rsquo;t help a query just filtering by created_at. This usually means creating extra, redundant indexes &mdash; <strong>PostgreSQL 18 adds skip-scan for multi-column B-tree indexes.</strong></p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="979" src="https://severalnines.com/wp-content/uploads/2026/03/diagram-pg18-skip_vs_classic_scans-1024x979.png" alt="" class="wp-image-42910"></figure>
<p>When the planner estimates it&rsquo;s cheaper than scanning the table, it can iterate over the leading column&rsquo;s distinct values and reuse the same index to satisfy predicates on the later columns, even if the leading column isn&rsquo;t constrained. This is a big win because it means fewer extra indexes, making things cleaner, helping performance by reducing write overhead, and keeping VACUUM happy as your data inevitably scales.</p>
<h2 class="wp-block-heading"><strong>How Postgres 18 features practically support modern workload patterns</strong><a class="anchor-link" id="how-postgres-18-features-practically-support-modern-workload-patterns"></a></h2>
<p>PostgreSQL 18&rsquo;s new stuff isn&rsquo;t just random. It&rsquo;s built around how people are actually using Postgres right now. Most setups mix transactions, reporting, search, and more AI stuff, with evolving replication.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="660" src="https://severalnines.com/wp-content/uploads/2026/03/diagram-pg18-modern_workload_patterns-1024x660.png" alt="" class="wp-image-42913"></figure>
<p>Looking at what users are doing makes the improvements way easier to understand how they benefit day-to-day operations than just seeing a list of new features.</p>
<h3 class="wp-block-heading"><strong>AI &amp; vector search</strong><a class="anchor-link" id="ai-vector-search"></a></h3>
<p>When people use AI with PostgreSQL, they usually don&rsquo;t replace the database with a specialized vector engine. Instead, they keep PostgreSQL as the main system for transactional data and put AI-related data, like embeddings, right alongside it.</p>
<p>When running vector search and transactional workloads together, it&rsquo;s best to keep those data embeddings right next to the data they describe. This means you need reliable performance: steady write speeds, predictable read times even when searches spike, and enough replicas to scale reads without bogging down the main database.</p>
<p>Query complexity grows, mixing things like finding similar items with standard filters on who, when, permissions, or categories. This often leads to heavy database scans, unpredictable read bursts, and competition between search / reporting tasks and regular writes &mdash; PostgreSQL 18&rsquo;s features help smooth all of this out.</p>
<p>Asynchronous I/O helps with storage slowdowns during heavy reads, and skip-scan makes filtering around your similarity searches much faster by improving multicolumn indexes. You still need a smart strategy for your AI indexes, e.g. when to use HNSW, how to organize data, etc., but PG helps the whole system handle the pressure better. Adding ClusterControl to the mix creates a winning combination, as managing replicas, backups, and monitoring performance as you grow becomes much easier.</p>
<h3 class="wp-block-heading"><strong>Serverless ingestion + BI</strong><a class="anchor-link" id="serverless-ingestion-bi"></a></h3>
<p>Many teams want applications to feel serverless and handle real-time data analysis, even when self-managing PostgreSQL or in a hybrid setup. Raw speed isn&rsquo;t the priority; it&rsquo;s how the system handles sudden spikes and recovers quickly, posing two hurdles:</p>
<p><strong>First,</strong> when lots of people use it at once, things can get slow. <strong>Second,</strong> we have less and less time for maintenance and upgrades, and we need stability right away after an update.</p>
<p>PostgreSQL 18 fixes both. Better I/O helps with slow reads during busy times, and keeping performance stats after an upgrade means less post-update drama. Basically, PostgreSQL is becoming stronger for unpredictable loads, and when you can&rsquo;t afford any downtime. ClusterControl makes upgrades and backups consistent across all your setups; that&rsquo;s what makes the difference between a normal maintenance window and a full-blown incident.</p>
<h3 class="wp-block-heading"><strong>Isolated replication for multi-tenant workloads</strong><a class="anchor-link" id="isolated-replication-for-multi-tenant-workloads"></a></h3>
<p>Multi-tenant systems often use replication to handle more reads, separate workloads, or serve different regions. This usually means having read replicas for regions, consumers for analytics or search, and moving tenants around as the system grows.</p>
<p>PostgreSQL 18 improves replication by better supporting generated columns and making conflicts much easier to see. This makes tenant-specific replication safer to run and easier to fix when problems occur. ClusterControl helps by setting up and monitoring these setups consistently, preventing hard-to-maintain, one-off replication configurations.</p>
<p>The result is fewer unexpected issues during replication, clearer insight when conflicts happen, and more confidence when changing how your replication is set up.</p>
<h2 class="wp-block-heading"><strong>Postgres 18 features that reduce upgrade &amp; migration workflow risks</strong><a class="anchor-link" id="postgres-18-features-that-reduce-upgrade-migration-workflow-risks"></a></h2>
<p>Upgrading PostgreSQL is mostly about managing risk, not the specific steps. What teams truly care about is getting the system back to normal, predictable performance fast.</p>
<p>Tight maintenance windows, huge databases, and low tolerance for issues mean the period after the upgrade can be brutal. Slow query plans, unexpected slowdowns, or emergency tuning can quickly turn a successful upgrade into an on-call nightmare. Here are the design changes and features PostgreSQL 18 implements to tackle these specific problems.</p>
<h3 class="wp-block-heading"><strong>Faster upgrades with retained statistics</strong><a class="anchor-link" id="faster-upgrades-with-retained-statistics"></a></h3>
<p>Historically, PostgreSQL upgrades caused frustrating performance degradation because the query planner had to relearn all data statistics. PG 18&rsquo;s pg_upgrade utility now transfers most optimizer statistics. This feature dramatically stabilizes performance much faster post-upgrade by immediately providing current data knowledge to the planner, eliminating the stressful, lengthy process of relearning statistics, especially for large databases.</p>
<h3 class="wp-block-heading"><strong>Checksums enabled by default in initdb</strong><a class="anchor-link" id="checksums-enabled-by-default-in-initdb"></a></h3>
<p>PostgreSQL 18 changes a big default: new clusters now turn on data checksums automatically when you run <code>initdb</code>. Checksums are great for catching sneaky data corruption, though they use a tiny bit more CPU. Most teams already use them for better durability or compliance &mdash; you can still opt out with <code>--no-data-checksums</code>.</p>
<p><strong>However,</strong> <strong>checksum settings must match exactly when you upgrade</strong>. If your old cluster didn&rsquo;t have checksums, you can&rsquo;t magically turn them on during the upgrade.</p>
<p>Think of the checksum setting as a contract for your cluster. Document it, keep it consistent everywhere, and test it during your upgrade dry runs. Don&rsquo;t leave it as a last-minute decision, or you&rsquo;ll find problems during the final cutover instead of in testing.</p>
<h2 class="wp-block-heading"><strong>PG 18 enhancements that improve developer &amp; SQL quality of life</strong><a class="anchor-link" id="pg-18-enhancements-that-improve-developer-sql-quality-of-life"></a></h2>
<p>Even though some PostgreSQL features seem like they are just for developers, they often impact how things run behind the scenes. Schema and SQL choices can unexpectedly influence storage, how much data is written, replication size, and index performance over time. Postgres 18 brings changes in this area that operators should really pay attention to.</p>
<h3 class="wp-block-heading"><strong>Virtual generated columns (default)</strong><a class="anchor-link" id="virtual-generated-columns-default"></a></h3>
<p>PostgreSQL 18&rsquo;s generated columns are usually virtual, meaning the value is calculated when you read the row, not saved on disk, unless you choose to store it.</p>
<p>Operationally, this is key. Virtual columns cut down on writes and storage, which is great for busy tables. But if you read the derived value a lot or need to index it predictably, stored columns might be better, as the calculation is done once on write, not on read; for example,</p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint)
);</code></pre>
<p>In PostgreSQL 18, amount_cents is virtual by default. If you want the value precomputed and stored, because it&rsquo;s heavily queried or indexed, you can still do so explicitly:</p>
<pre class="wp-block-code"><code>amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint) STORED</code></pre>
<p><strong>N.B. This decision is crucial for replication.</strong> PG 18&rsquo;s logical replication is better at publishing stored generated values.</p>
<h3 class="wp-block-heading"><strong>UUIDv7 for time-ordered IDs</strong><a class="anchor-link" id="uuidv7-for-time-ordered-ids"></a></h3>
<p>UUIDs are great because they&rsquo;re unique everywhere and easy to make across different systems. The problem has been how they mess up B-tree indexes. Random UUIDs scatter new entries, causing slow index bloat and cache issues, especially on busy tables.</p>
<p>Recent PostgreSQL releases fixed this natively with uuidv7(), making it the operational standard for PostgreSQL 18 architectures. It generates UUIDs that are mostly time-ordered. This keeps your indexes much tidier while keeping the benefits of using UUIDs. For example:</p>
<pre class="wp-block-code"><code>CREATE TABLE sessions (
  id uuid PRIMARY KEY DEFAULT uuidv7(),
  created_at timestamptz NOT NULL DEFAULT now()
);</code></pre>
<p>If you&rsquo;ve been hesitant to use UUID primary keys on high-ingest tables because of index behavior, UUIDv7 makes that trade-off far more reasonable.</p>
<h3 class="wp-block-heading"><strong>Temporal constraints for time-varying facts</strong><a class="anchor-link" id="temporal-constraints-for-time-varying-facts"></a></h3>
<p>Dealing with time-sensitive data, like pricing or subscriptions, usually means complex application code and tricky locking to avoid mistakes. Postgres 18 simplifies this with temporal constraints.</p>
<p>These let the database enforce rules, like primary and foreign keys, over time ranges. This moves the headache of correctness from your application logic into the database, making enforcement instant and reliable.</p>
<p>For operations teams, this means fewer weird errors, less data cleanup, and fewer 2 a.m. alerts caused by subtle concurrency issues.</p>
<h3 class="wp-block-heading"><strong>OAuth authentication</strong><a class="anchor-link" id="oauth-authentication"></a></h3>
<p>PostgreSQL 18 now supports OAuth, which is a big deal for security. It gives you a path to reduce long-lived DB passwords by using short-lived tokens where it fits your identity stack. It won&rsquo;t fix bad internal role design, but it massively cuts down on the headache of credential sprawl, especially where infrastructure is constantly spinning up and down. OAuth is just way easier to manage than traditional passwords in those dynamic setups.</p>
<h2 class="wp-block-heading"><strong>How PostgreSQL 18 improves logical &amp; streaming replication efficiency</strong><a class="anchor-link" id="how-postgresql-18-improves-logical-streaming-replication-efficiency"></a></h2>
<p>Replication gets complicated fast. One replica is simple, but the more lag, conflicts, and strange failures you open yourself up to. PostgreSQL 18 doesn&rsquo;t magically automate logical replication or make it DDL aware. What it does instead is provide practical improvements that make operating, monitoring, and managing your replicas much easier.</p>
<h3 class="wp-block-heading"><strong>Generated column replication</strong><a class="anchor-link" id="generated-column-replication"></a></h3>
<p>Building on recent improvements, modern PostgreSQL lets you publish stored generated columns using the <code>publish_generated_columns</code> option. This is great for downstream systems that need the calculated value right away instead of having to recompute it. PostgreSQL sends the generated value and replicates it into a normal column on the subscriber.</p>
<p><strong>N.B. You cannot replicate it into another generated column; that will fail.</strong></p>
<p>Basically, this feature ships the finished, calculated results, not the formula or the generated column definition. Its best use is to simplify your consumers and avoid repeating work, without getting into complicated DDL replication. Let&rsquo;s look at a simple logical replication setup to illustrate how Postgres 18 handles generated columns.</p>
<p><strong>On the publisher:</strong></p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint) STORED
);

CREATE PUBLICATION orders_pub
FOR TABLE orders
WITH (publish_generated_columns = 'stored');</code></pre>
<p><strong>On the subscriber</strong>, the generated value is replicated into a regular column:</p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint NOT NULL
);

CREATE SUBSCRIPTION orders_sub
  CONNECTION 'host= port=5432 dbname= user= password='
  PUBLICATION orders_pub;</code></pre>
<p>This setup reflects how PostgreSQL 18 actually handles generated column replication: you publish the stored generated value and apply it to a normal column on the subscriber. It&rsquo;s a practical, explicit approach that avoids surprises and stays within the supported model.</p>
<h3 class="wp-block-heading"><strong>Streaming defaults and conflict logging</strong><a class="anchor-link" id="streaming-defaults-and-conflict-logging"></a></h3>
<p>Logical subscriptions now use parallel streaming by default. This means faster throughput and less lag right out of the box, especially when things are busy or transactions are large.</p>
<p>Conflict handling is also much better. Modern PostgreSQL logs conflicts and shows conflict details in <code>pg_stat_subscription_stats</code>. While not brand new to 18, utilizing this view is a massive upgrade if you are coming from older major versions. Replication conflicts are usually a nightmare because you can&rsquo;t see them as they happen. Better visibility means you can spot trends, link issues to workload changes, and write reliable troubleshooting guides without relying on guesswork.</p>
<h3 class="wp-block-heading"><strong>Hygiene improvements for larger estates</strong><a class="anchor-link" id="hygiene-improvements-for-larger-estates"></a></h3>
<p>When you have a lot of replication going on, keeping things tidy is as crucial as keeping them fast. PostgreSQL 18 adds a few safeguards to head off slow, hidden problems:</p>
<ul class="wp-block-list">
<li><code>idle_replication_slot_timeout</code>: Automatically invalidates idle replication slots that have been inactive for too long.</li>
<li><code>max_active_replication_origins</code>: Lets you limit the number of active replication origins, regardless of the number of existing slots.</li>
</ul>
<p>If you&rsquo;ve ever had an old, forgotten logical slot quietly hogging Write-Ahead Log (WAL) space for weeks, you&rsquo;ll appreciate these. They don&rsquo;t replace good monitoring, but they make it much harder for tiny mistakes to turn into massive cleanup projects later.</p>
<h2 class="wp-block-heading"><strong>Manual vs. ClusterControl PostgreSQL 18 operations</strong><a class="anchor-link" id="manual-vs-clustercontrol-postgresql-18-operations"></a></h2>
<p>Managing PostgreSQL by hand is fine until it isn&rsquo;t. When you have just a couple of clusters, doing it yourself is easy. But once you have more than a few, those manual steps start wasting time and attention. That&rsquo;s when the downsides really hit.</p>
<h3 class="wp-block-heading"><strong>Manual PG 18 operations</strong><a class="anchor-link" id="manual-pg-18-operations"></a></h3>
<p>Running PostgreSQL manually gives you total freedom, which is great for small setups. But not everything is good. Let&rsquo;s see what we&rsquo;re talking about.</p>
<p><strong>Pros:</strong></p>
<ul class="wp-block-list">
<li>You control everything about the setup.</li>
<li>You can perfectly tune it for each task.</li>
<li>Trying new things is easy in one environment.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul class="wp-block-list">
<li>Big upgrades are messy. Checksums, making sure plans stay stable, and planning for rollbacks are a headache.</li>
<li>Replication slot cleanup is a long-term chore, especially as your setup changes.</li>
<li>Backup plans often differ between clusters as you add or rebuild them.</li>
<li>Monitoring is usually a bunch of tools cobbled together, leading to confusing alerts and nobody knowing who&rsquo;s on point during a problem.</li>
</ul>
<p>Trivial alone, critical cumulatively, these will chew up a ton of your team&rsquo;s time as you grow.</p>
<h3 class="wp-block-heading"><strong>Automated PG 18 ops with ClusterControl</strong><a class="anchor-link" id="automated-pg-18-ops-with-clustercontrol"></a></h3>
<p>ClusterControl really shines when you&rsquo;re rolling PostgreSQL 18 because it saves you from having to figure out the same operational steps over and over again for every new cluster.</p>
<p><strong>Pros:</strong></p>
<ul class="wp-block-list">
<li>Centralized hybrid setup and management of all PostgreSQL clusters.</li>
<li>Guided major version updates, ensuring you don&rsquo;t miss crucial pre-checks.</li>
<li>Easily and safely applied parameter changes, like AIO tuning, across the board.</li>
<li>Turnkey streaming replication, including built-in HAProxy and PgBouncer support.</li>
<li>Single view alerting and health checks, e.g. replication lag, node status, backups, etc.</li>
<li>Backup policy enforcement using common tools, e.g. pgBackRest, pg_basebackup, etc.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul class="wp-block-list">
<li>It&rsquo;s a platform, implying its own learning curve.</li>
<li>You need to check that your rollout timing aligns with ClusterControl&rsquo;s support for the PostgreSQL version you want to use.</li>
</ul>
<p>For teams managing PostgreSQL at scale across many environments, this consistency is often more valuable than having total control over every single command, and easier.</p>
<h2 class="wp-block-heading"><strong>Installing &amp; setting up Postgres 18</strong><a class="anchor-link" id="installing-setting-up-postgres-18"></a></h2>
<p>When piloting PostgreSQL 18, set up your test environment to mimic your real deployment exactly. Use the same storage, replication setup, extensions, like <code>pgvector</code>, and a realistic, large enough dataset. Small, fake tests will just hide the real problems you need to find.</p>
<h3 class="wp-block-heading"><strong>Installing PostgreSQL 18</strong><a class="anchor-link" id="installing-postgresql-18"></a></h3>
<p>The specific package names and repos differ based on your system, but here&rsquo;s an example of a typical installation:</p>
<h4 class="wp-block-heading"><strong>For Debian-Based OS:</strong></h4>
<pre class="wp-block-code"><code>sudo apt update
sudo apt install postgresql-18</code></pre>
<p><strong>For RedHat-Based OS:</strong></p>
<pre class="wp-block-code"><code>sudo dnf install postgresql18-server</code></pre>
<h3 class="wp-block-heading"><strong>Checksums at initdb</strong><a class="anchor-link" id="checksums-at-initdb"></a></h3>
<p>Remember, PostgreSQL 18 enables checksums by default at initialization.</p>
<pre class="wp-block-code"><code>sudo /usr/pgsql-18/bin/postgresql-18-setup initdb</code></pre>
<p>But, you can opt out:</p>
<pre class="wp-block-code"><code>sudo -u postgres /usr/pgsql-18/bin/initdb
--no-data-checksums -D /var/lib/pgsql/18/data</code></pre>
<p><strong>Don&rsquo;t forget</strong> that <code>pg_upgrade</code> requires checksum settings to match between the source and target clusters. Treat checksum posture as an upgrade design decision and validate it during rehearsals, not during the cutover window.</p>
<h3 class="wp-block-heading"><strong>Confirm AIO-related settings:&nbsp;</strong><a class="anchor-link" id="confirm-aio-related-settings"></a></h3>
<p>Once the cluster is up, confirm the effective I/O-related settings you&rsquo;re running with:</p>
<pre class="wp-block-code"><code>SHOW io_method;
SHOW effective_io_concurrency;
SHOW maintenance_io_concurrency;</code></pre>
<p>Exact behavior depends on platform and build options, but this gives you a baseline before you start tuning or running load tests.</p>
<h3 class="wp-block-heading"><strong>Adding the PG 18 cluster to ClusterControl and initializing HA / replication</strong><a class="anchor-link" id="adding-the-pg-18-cluster-to-clustercontrol-and-initializing-ha-replication"></a></h3>
<p>Once running, the next step is to bring the cluster into ClusterControl and establish a sane baseline topology. For many teams, a solid default looks like:</p>
<ul class="wp-block-list">
<li>1 primary</li>
<li>1&ndash;2 replicas</li>
<li>HAProxy for routing and HA</li>
<li>PgBouncer for connection pooling (especially with spiky workloads)</li>
</ul>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="374" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_topology_viewer-pg18_pgbouncer_lb-1024x374.png" alt="" class="wp-image-42727"></figure>
<p>ClusterControl&rsquo;s guided workflows can deploy PostgreSQL streaming replication and integrate HAProxy and PgBouncer as part of the setup, reducing the amount of manual wiring needed to reach a production-ready state.</p>
<h2 class="wp-block-heading"><strong>PostgreSQL 18 operations &amp; monitoring</strong><a class="anchor-link" id="postgresql-18-operations-monitoring"></a></h2>
<p>PostgreSQL 18 offers better control and visibility. But that only matters if you use it to create reliable runbooks for when things go sideways. Don&rsquo;t tweak every last setting; the real win is a predictable system under pressure, not the ability to see clearly when it isn&rsquo;t.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="516" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_cluster_overview_dashboard-pg18-1024x516.png" alt="" class="wp-image-42728"></figure>
<h3 class="wp-block-heading"><strong>Tune AIO safely</strong><a class="anchor-link" id="tune-aio-safely"></a></h3>
<p>Asynchronous I/O (AIO) in PostgreSQL 18 is a big deal, especially for handling many tasks at once, but don&rsquo;t rush it. It shines under heavy load, not in simple tests.</p>
<p><strong>Start Simple:</strong></p>
<ul class="wp-block-list">
<li>Pick the right io_method for your system.</li>
<li>Test it with your actual, busy application, not just single queries.</li>
<li>Then try adjusting <code>io_combine_limit</code> and <code>io_max_combine_limit</code>.</li>
</ul>
<p><strong>What to Watch For:</strong></p>
<ul class="wp-block-list">
<li>How long scans take, especially big ones.</li>
<li>Storage delays when the system is busy.</li>
<li>Your worst-case waiting times (P95/P99), not just the average.</li>
<li>How VACUUM behaves while everything else is running.</li>
</ul>
<p><strong>How to tell if it is working: </strong>You&rsquo;ll see fewer unexpected slowdowns when reading a lot of data and less fighting between maintenance tasks and user traffic.</p>
<h3 class="wp-block-heading"><strong>Vacuum/Analyze delay reporting for SLOs</strong><a class="anchor-link" id="vacuum-analyze-delay-reporting-for-slos"></a></h3>
<p>PostgreSQL 18 has better insight into maintenance throttling. If you enable <code>track_cost_delay_timing</code>, VACUUM and ANALYZE will tell you exactly how long they waited because of cost-based delays.</p>
<p>This is huge for troubleshooting. Are you falling behind on maintenance because you told the system to slow down, or because it&rsquo;s genuinely struggling? Knowing the difference is key when figuring out why you missed an SLO or when planning your next capacity upgrade.</p>
<h3 class="wp-block-heading"><strong>Logical replication visibility</strong><a class="anchor-link" id="logical-replication-visibility"></a></h3>
<p>PostgreSQL 18 makes using logical replication much easier by giving you better tools to see what&rsquo;s happening.</p>
<p>Make <code>pg_stat_subscription_stats</code> a regular check-in:</p>
<ul class="wp-block-list">
<li>See conflict counts and when they happened.</li>
<li>Monitor lag and how applies are working over time.</li>
<li>Check that the default parallel streaming is what you want.</li>
</ul>
<p>While better visibility doesn&rsquo;t stop replication problems, it lets you move past the guesswork so you can actually understand the issues and automate fixes.</p>
<h3 class="wp-block-heading"><strong>ClusterControl dashboards</strong><a class="anchor-link" id="clustercontrol-dashboards"></a></h3>
<p>ClusterControl simplifies managing your PostgreSQL clusters by giving you a clear, consistent view of the important stuff:</p>
<ul class="wp-block-list">
<li>Node health and resources</li>
<li>Replication status and lag</li>
<li>High availability and auto-recovery</li>
<li>Backup posture, state, and compliance</li>
</ul>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="516" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_host_overview_dashboard-pg18-1024x516.png" alt="" class="wp-image-42729"></figure>
<h2 class="wp-block-heading"><strong>Ready to move? PostgreSQL 18 upgrade planning checklist</strong><a class="anchor-link" id="ready-to-move-postgresql-18-upgrade-planning-checklist"></a></h2>
<p>Before jumping to PostgreSQL 18, just check these basics first:</p>
<ul class="wp-block-list">
<li>Backups: Take a full backup and make sure you can restore it.</li>
<li>Checksums: See if your current cluster uses data checksums and plan for the new one.</li>
<li>Extensions: Check that all your extensions (like <code>pgvector</code> or <code>PostGIS</code>) are compatible with PostgreSQL 18.</li>
</ul>
<p>Getting these things squared away now makes the whole upgrade process much smoother.</p>
<h2 class="wp-block-heading"><strong>Conclusion</strong><a class="anchor-link" id="conclusion"></a></h2>
<p>PostgreSQL 18 is all about better operations. It fixes common headaches like slow I/O (thanks to async I/O), keeps upgrades predictable by saving optimizer stats, makes multicolumn indexes smarter with skip-scan, adds modern OAuth authentication, improves index use for busy tables with UUIDv7, and simplifies logical replication.</p>
<p>If you&rsquo;re thinking of upgrading, don&rsquo;t rush. Test PostgreSQL 18 in your lower environments with real data and load first. Check your extensions, especially pgvector. Decide on checksums early, as they&rsquo;re now on by default, and <code>pg_upgrade</code> needs them to match. And definitely test your replication setup, including conflict scenarios.</p>
<p>Taking the time for this careful rollout means fewer surprises and smoother changes in production. Ready to get started with PostgreSQL 18, regardless of where you run it?</p>
<h2 class="wp-block-heading"><strong>Install ClusterControl in 10-minutes. Free 30-day Enterprise trial included!</strong><a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading"><strong>Script Installation Instructions</strong><a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading">Other Installation Options</h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading">ClusterControl on Marketplaces</h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<ul class="wp-block-list">
<li><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></li>
<li><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></li>
<li><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></li>
<li><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></li>
<li><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></li>
</ul>
<p>The post <a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/" />
      <id>https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/</id>
      <updated>2026-03-20T08:00:00+02:00</updated>
      <author><name>Sebastian Insausti</name></author>
      <summary type="html"><![CDATA[<p>Today, we’re asking PostgreSQL to do more and more, like handling both transactions and analytics, powering huge SaaS platforms, managing event data, and even dipping into AI-related tasks like vector search. This intense pressure highlights some pain points: slow, unpredictable reads, rigid indexing, complex setups, and risky major upgrades.  PostgreSQL 18 directly addresses these real-world […]<br />
The post PostgreSQL 18 Upgrades for AI-Era Workloads and Operations appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Today, we&rsquo;re asking PostgreSQL to do more and more, like handling both transactions and analytics, powering huge SaaS platforms, managing event data, and even dipping into AI-related tasks like vector search. This intense pressure highlights some pain points: slow, unpredictable reads, rigid indexing, complex setups, and risky major upgrades.&nbsp;</p>
<p>PostgreSQL 18 directly addresses these real-world operational challenges, skipping flashy features for practical improvements like asynchronous I/O to speed up reads, safer upgrades from keeping optimizer stats, better multi-column indexes, UUIDv7 support, and useful enhancements to logical replication.</p>
<p>I&rsquo;ll dive into these key operational changes, show how they fit your modern workflows, and explain how tools like ClusterControl can make adopting them smooth and painless.</p>
<h2 class="wp-block-heading"><strong>PostgreSQL 18&rsquo;s key features that improve core workload performance</strong><a class="anchor-link" id="postgresql-18s-key-features-that-improve-core-workload-performance"></a></h2>
<p>PostgreSQL 18 isn&rsquo;t about one big, new thing. Instead, it offers fixes for common headaches: slow storage, reads competing with new transactions, inconsistent performance post-upgrade, indexes that don&rsquo;t quite hit the mark, messy authentication, and confusing replication errors &mdash; these fixes smooth out the rough operational edges. Let&rsquo;s look at two key features that will have the greatest effect on production work.</p>
<h3 class="wp-block-heading"><strong>Async I/O subsystem (AIO)</strong><a class="anchor-link" id="async-i-o-subsystem-aio"></a></h3>
<p>Slow storage often causes problems, not the computer itself. Older PostgreSQL waited for each storage request, which is safe but can slow things down, especially when your data is too big for the cache or when running big scans alongside regular transactions.</p>
<p>PostgreSQL 18 fixes this with Asynchronous I/O (AIO). Now, the system can ask for multiple data blocks and keep working instead of waiting for each one. This can significantly improve read-heavy scans and some maintenance operations under the right I/O conditions. New settings like <code>io_method</code> help you tune this &mdash; <strong>don&rsquo;t just test AIO with a single query.</strong></p>
<p>Its real power shows up under heavy, simultaneous load. To see the benefit, test your actual, read-heavy workload, focusing on P95/P99 latency improvements. Pay special attention to maintenance, since PG 18 specifically aims to reduce I/O slowdown during VACUUM.</p>
<p><strong>TIP:</strong> A good test is to compare latency under pressure with and without AIO while background maintenance is running.</p>
<h3 class="wp-block-heading"><strong>Skip-scan on B-tree indexes</strong><a class="anchor-link" id="skip-scan-on-b-tree-indexes"></a></h3>
<p>In large systems, especially SaaS, we often use multi-column indexes. The problem is that queries don&rsquo;t always filter by the index&rsquo;s first column. For example, an index on (tenant_id, created_at) won&rsquo;t help a query just filtering by created_at. This usually means creating extra, redundant indexes &mdash; <strong>PostgreSQL 18 adds skip-scan for multi-column B-tree indexes.</strong></p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="979" src="https://severalnines.com/wp-content/uploads/2026/03/diagram-pg18-skip_vs_classic_scans-1024x979.png" alt="" class="wp-image-42910"></figure>
<p>When the planner estimates it&rsquo;s cheaper than scanning the table, it can iterate over the leading column&rsquo;s distinct values and reuse the same index to satisfy predicates on the later columns, even if the leading column isn&rsquo;t constrained. This is a big win because it means fewer extra indexes, making things cleaner, helping performance by reducing write overhead, and keeping VACUUM happy as your data inevitably scales.</p>
<h2 class="wp-block-heading"><strong>How Postgres 18 features practically support modern workload patterns</strong><a class="anchor-link" id="how-postgres-18-features-practically-support-modern-workload-patterns"></a></h2>
<p>PostgreSQL 18&rsquo;s new stuff isn&rsquo;t just random. It&rsquo;s built around how people are actually using Postgres right now. Most setups mix transactions, reporting, search, and more AI stuff, with evolving replication.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="660" src="https://severalnines.com/wp-content/uploads/2026/03/diagram-pg18-modern_workload_patterns-1024x660.png" alt="" class="wp-image-42913"></figure>
<p>Looking at what users are doing makes the improvements way easier to understand how they benefit day-to-day operations than just seeing a list of new features.</p>
<h3 class="wp-block-heading"><strong>AI &amp; vector search</strong><a class="anchor-link" id="ai-vector-search"></a></h3>
<p>When people use AI with PostgreSQL, they usually don&rsquo;t replace the database with a specialized vector engine. Instead, they keep PostgreSQL as the main system for transactional data and put AI-related data, like embeddings, right alongside it.</p>
<p>When running vector search and transactional workloads together, it&rsquo;s best to keep those data embeddings right next to the data they describe. This means you need reliable performance: steady write speeds, predictable read times even when searches spike, and enough replicas to scale reads without bogging down the main database.</p>
<p>Query complexity grows, mixing things like finding similar items with standard filters on who, when, permissions, or categories. This often leads to heavy database scans, unpredictable read bursts, and competition between search / reporting tasks and regular writes &mdash; PostgreSQL 18&rsquo;s features help smooth all of this out.</p>
<p>Asynchronous I/O helps with storage slowdowns during heavy reads, and skip-scan makes filtering around your similarity searches much faster by improving multicolumn indexes. You still need a smart strategy for your AI indexes, e.g. when to use HNSW, how to organize data, etc., but PG helps the whole system handle the pressure better. Adding ClusterControl to the mix creates a winning combination, as managing replicas, backups, and monitoring performance as you grow becomes much easier.</p>
<h3 class="wp-block-heading"><strong>Serverless ingestion + BI</strong><a class="anchor-link" id="serverless-ingestion-bi"></a></h3>
<p>Many teams want applications to feel serverless and handle real-time data analysis, even when self-managing PostgreSQL or in a hybrid setup. Raw speed isn&rsquo;t the priority; it&rsquo;s how the system handles sudden spikes and recovers quickly, posing two hurdles:</p>
<p><strong>First,</strong> when lots of people use it at once, things can get slow. <strong>Second,</strong> we have less and less time for maintenance and upgrades, and we need stability right away after an update.</p>
<p>PostgreSQL 18 fixes both. Better I/O helps with slow reads during busy times, and keeping performance stats after an upgrade means less post-update drama. Basically, PostgreSQL is becoming stronger for unpredictable loads, and when you can&rsquo;t afford any downtime. ClusterControl makes upgrades and backups consistent across all your setups; that&rsquo;s what makes the difference between a normal maintenance window and a full-blown incident.</p>
<h3 class="wp-block-heading"><strong>Isolated replication for multi-tenant workloads</strong><a class="anchor-link" id="isolated-replication-for-multi-tenant-workloads"></a></h3>
<p>Multi-tenant systems often use replication to handle more reads, separate workloads, or serve different regions. This usually means having read replicas for regions, consumers for analytics or search, and moving tenants around as the system grows.</p>
<p>PostgreSQL 18 improves replication by better supporting generated columns and making conflicts much easier to see. This makes tenant-specific replication safer to run and easier to fix when problems occur. ClusterControl helps by setting up and monitoring these setups consistently, preventing hard-to-maintain, one-off replication configurations.</p>
<p>The result is fewer unexpected issues during replication, clearer insight when conflicts happen, and more confidence when changing how your replication is set up.</p>
<h2 class="wp-block-heading"><strong>Postgres 18 features that reduce upgrade &amp; migration workflow risks</strong><a class="anchor-link" id="postgres-18-features-that-reduce-upgrade-migration-workflow-risks"></a></h2>
<p>Upgrading PostgreSQL is mostly about managing risk, not the specific steps. What teams truly care about is getting the system back to normal, predictable performance fast.</p>
<p>Tight maintenance windows, huge databases, and low tolerance for issues mean the period after the upgrade can be brutal. Slow query plans, unexpected slowdowns, or emergency tuning can quickly turn a successful upgrade into an on-call nightmare. Here are the design changes and features PostgreSQL 18 implements to tackle these specific problems.</p>
<h3 class="wp-block-heading"><strong>Faster upgrades with retained statistics</strong><a class="anchor-link" id="faster-upgrades-with-retained-statistics"></a></h3>
<p>Historically, PostgreSQL upgrades caused frustrating performance degradation because the query planner had to relearn all data statistics. PG 18&rsquo;s pg_upgrade utility now transfers most optimizer statistics. This feature dramatically stabilizes performance much faster post-upgrade by immediately providing current data knowledge to the planner, eliminating the stressful, lengthy process of relearning statistics, especially for large databases.</p>
<h3 class="wp-block-heading"><strong>Checksums enabled by default in initdb</strong><a class="anchor-link" id="checksums-enabled-by-default-in-initdb"></a></h3>
<p>PostgreSQL 18 changes a big default: new clusters now turn on data checksums automatically when you run <code>initdb</code>. Checksums are great for catching sneaky data corruption, though they use a tiny bit more CPU. Most teams already use them for better durability or compliance &mdash; you can still opt out with <code>--no-data-checksums</code>.</p>
<p><strong>However,</strong> <strong>checksum settings must match exactly when you upgrade</strong>. If your old cluster didn&rsquo;t have checksums, you can&rsquo;t magically turn them on during the upgrade.</p>
<p>Think of the checksum setting as a contract for your cluster. Document it, keep it consistent everywhere, and test it during your upgrade dry runs. Don&rsquo;t leave it as a last-minute decision, or you&rsquo;ll find problems during the final cutover instead of in testing.</p>
<h2 class="wp-block-heading"><strong>PG 18 enhancements that improve developer &amp; SQL quality of life</strong><a class="anchor-link" id="pg-18-enhancements-that-improve-developer-sql-quality-of-life"></a></h2>
<p>Even though some PostgreSQL features seem like they are just for developers, they often impact how things run behind the scenes. Schema and SQL choices can unexpectedly influence storage, how much data is written, replication size, and index performance over time. Postgres 18 brings changes in this area that operators should really pay attention to.</p>
<h3 class="wp-block-heading"><strong>Virtual generated columns (default)</strong><a class="anchor-link" id="virtual-generated-columns-default"></a></h3>
<p>PostgreSQL 18&rsquo;s generated columns are usually virtual, meaning the value is calculated when you read the row, not saved on disk, unless you choose to store it.</p>
<p>Operationally, this is key. Virtual columns cut down on writes and storage, which is great for busy tables. But if you read the derived value a lot or need to index it predictably, stored columns might be better, as the calculation is done once on write, not on read; for example,</p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint)
);</code></pre>
<p>In PostgreSQL 18, amount_cents is virtual by default. If you want the value precomputed and stored, because it&rsquo;s heavily queried or indexed, you can still do so explicitly:</p>
<pre class="wp-block-code"><code>amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint) STORED</code></pre>
<p><strong>N.B. This decision is crucial for replication.</strong> PG 18&rsquo;s logical replication is better at publishing stored generated values.</p>
<h3 class="wp-block-heading"><strong>UUIDv7 for time-ordered IDs</strong><a class="anchor-link" id="uuidv7-for-time-ordered-ids"></a></h3>
<p>UUIDs are great because they&rsquo;re unique everywhere and easy to make across different systems. The problem has been how they mess up B-tree indexes. Random UUIDs scatter new entries, causing slow index bloat and cache issues, especially on busy tables.</p>
<p>Recent PostgreSQL releases fixed this natively with uuidv7(), making it the operational standard for PostgreSQL 18 architectures. It generates UUIDs that are mostly time-ordered. This keeps your indexes much tidier while keeping the benefits of using UUIDs. For example:</p>
<pre class="wp-block-code"><code>CREATE TABLE sessions (
  id uuid PRIMARY KEY DEFAULT uuidv7(),
  created_at timestamptz NOT NULL DEFAULT now()
);</code></pre>
<p>If you&rsquo;ve been hesitant to use UUID primary keys on high-ingest tables because of index behavior, UUIDv7 makes that trade-off far more reasonable.</p>
<h3 class="wp-block-heading"><strong>Temporal constraints for time-varying facts</strong><a class="anchor-link" id="temporal-constraints-for-time-varying-facts"></a></h3>
<p>Dealing with time-sensitive data, like pricing or subscriptions, usually means complex application code and tricky locking to avoid mistakes. Postgres 18 simplifies this with temporal constraints.</p>
<p>These let the database enforce rules, like primary and foreign keys, over time ranges. This moves the headache of correctness from your application logic into the database, making enforcement instant and reliable.</p>
<p>For operations teams, this means fewer weird errors, less data cleanup, and fewer 2 a.m. alerts caused by subtle concurrency issues.</p>
<h3 class="wp-block-heading"><strong>OAuth authentication</strong><a class="anchor-link" id="oauth-authentication"></a></h3>
<p>PostgreSQL 18 now supports OAuth, which is a big deal for security. It gives you a path to reduce long-lived DB passwords by using short-lived tokens where it fits your identity stack. It won&rsquo;t fix bad internal role design, but it massively cuts down on the headache of credential sprawl, especially where infrastructure is constantly spinning up and down. OAuth is just way easier to manage than traditional passwords in those dynamic setups.</p>
<h2 class="wp-block-heading"><strong>How PostgreSQL 18 improves logical &amp; streaming replication efficiency</strong><a class="anchor-link" id="how-postgresql-18-improves-logical-streaming-replication-efficiency"></a></h2>
<p>Replication gets complicated fast. One replica is simple, but the more lag, conflicts, and strange failures you open yourself up to. PostgreSQL 18 doesn&rsquo;t magically automate logical replication or make it DDL aware. What it does instead is provide practical improvements that make operating, monitoring, and managing your replicas much easier.</p>
<h3 class="wp-block-heading"><strong>Generated column replication</strong><a class="anchor-link" id="generated-column-replication"></a></h3>
<p>Building on recent improvements, modern PostgreSQL lets you publish stored generated columns using the <code>publish_generated_columns</code> option. This is great for downstream systems that need the calculated value right away instead of having to recompute it. PostgreSQL sends the generated value and replicates it into a normal column on the subscriber.</p>
<p><strong>N.B. You cannot replicate it into another generated column; that will fail.</strong></p>
<p>Basically, this feature ships the finished, calculated results, not the formula or the generated column definition. Its best use is to simplify your consumers and avoid repeating work, without getting into complicated DDL replication. Let&rsquo;s look at a simple logical replication setup to illustrate how Postgres 18 handles generated columns.</p>
<p><strong>On the publisher:</strong></p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint GENERATED ALWAYS AS ((amount * 100)::bigint) STORED
);

CREATE PUBLICATION orders_pub
FOR TABLE orders
WITH (publish_generated_columns = 'stored');</code></pre>
<p><strong>On the subscriber</strong>, the generated value is replicated into a regular column:</p>
<pre class="wp-block-code"><code>CREATE TABLE orders (
  id bigint PRIMARY KEY,
  amount numeric(12,2) NOT NULL,
  amount_cents bigint NOT NULL
);

CREATE SUBSCRIPTION orders_sub
  CONNECTION 'host= port=5432 dbname= user= password='
  PUBLICATION orders_pub;</code></pre>
<p>This setup reflects how PostgreSQL 18 actually handles generated column replication: you publish the stored generated value and apply it to a normal column on the subscriber. It&rsquo;s a practical, explicit approach that avoids surprises and stays within the supported model.</p>
<h3 class="wp-block-heading"><strong>Streaming defaults and conflict logging</strong><a class="anchor-link" id="streaming-defaults-and-conflict-logging"></a></h3>
<p>Logical subscriptions now use parallel streaming by default. This means faster throughput and less lag right out of the box, especially when things are busy or transactions are large.</p>
<p>Conflict handling is also much better. Modern PostgreSQL logs conflicts and shows conflict details in <code>pg_stat_subscription_stats</code>. While not brand new to 18, utilizing this view is a massive upgrade if you are coming from older major versions. Replication conflicts are usually a nightmare because you can&rsquo;t see them as they happen. Better visibility means you can spot trends, link issues to workload changes, and write reliable troubleshooting guides without relying on guesswork.</p>
<h3 class="wp-block-heading"><strong>Hygiene improvements for larger estates</strong><a class="anchor-link" id="hygiene-improvements-for-larger-estates"></a></h3>
<p>When you have a lot of replication going on, keeping things tidy is as crucial as keeping them fast. PostgreSQL 18 adds a few safeguards to head off slow, hidden problems:</p>
<ul class="wp-block-list">
<li><code>idle_replication_slot_timeout</code>: Automatically invalidates idle replication slots that have been inactive for too long.</li>
<li><code>max_active_replication_origins</code>: Lets you limit the number of active replication origins, regardless of the number of existing slots.</li>
</ul>
<p>If you&rsquo;ve ever had an old, forgotten logical slot quietly hogging Write-Ahead Log (WAL) space for weeks, you&rsquo;ll appreciate these. They don&rsquo;t replace good monitoring, but they make it much harder for tiny mistakes to turn into massive cleanup projects later.</p>
<h2 class="wp-block-heading"><strong>Manual vs. ClusterControl PostgreSQL 18 operations</strong><a class="anchor-link" id="manual-vs-clustercontrol-postgresql-18-operations"></a></h2>
<p>Managing PostgreSQL by hand is fine until it isn&rsquo;t. When you have just a couple of clusters, doing it yourself is easy. But once you have more than a few, those manual steps start wasting time and attention. That&rsquo;s when the downsides really hit.</p>
<h3 class="wp-block-heading"><strong>Manual PG 18 operations</strong><a class="anchor-link" id="manual-pg-18-operations"></a></h3>
<p>Running PostgreSQL manually gives you total freedom, which is great for small setups. But not everything is good. Let&rsquo;s see what we&rsquo;re talking about.</p>
<p><strong>Pros:</strong></p>
<ul class="wp-block-list">
<li>You control everything about the setup.</li>
<li>You can perfectly tune it for each task.</li>
<li>Trying new things is easy in one environment.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul class="wp-block-list">
<li>Big upgrades are messy. Checksums, making sure plans stay stable, and planning for rollbacks are a headache.</li>
<li>Replication slot cleanup is a long-term chore, especially as your setup changes.</li>
<li>Backup plans often differ between clusters as you add or rebuild them.</li>
<li>Monitoring is usually a bunch of tools cobbled together, leading to confusing alerts and nobody knowing who&rsquo;s on point during a problem.</li>
</ul>
<p>Trivial alone, critical cumulatively, these will chew up a ton of your team&rsquo;s time as you grow.</p>
<h3 class="wp-block-heading"><strong>Automated PG 18 ops with ClusterControl</strong><a class="anchor-link" id="automated-pg-18-ops-with-clustercontrol"></a></h3>
<p>ClusterControl really shines when you&rsquo;re rolling PostgreSQL 18 because it saves you from having to figure out the same operational steps over and over again for every new cluster.</p>
<p><strong>Pros:</strong></p>
<ul class="wp-block-list">
<li>Centralized hybrid setup and management of all PostgreSQL clusters.</li>
<li>Guided major version updates, ensuring you don&rsquo;t miss crucial pre-checks.</li>
<li>Easily and safely applied parameter changes, like AIO tuning, across the board.</li>
<li>Turnkey streaming replication, including built-in HAProxy and PgBouncer support.</li>
<li>Single view alerting and health checks, e.g. replication lag, node status, backups, etc.</li>
<li>Backup policy enforcement using common tools, e.g. pgBackRest, pg_basebackup, etc.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul class="wp-block-list">
<li>It&rsquo;s a platform, implying its own learning curve.</li>
<li>You need to check that your rollout timing aligns with ClusterControl&rsquo;s support for the PostgreSQL version you want to use.</li>
</ul>
<p>For teams managing PostgreSQL at scale across many environments, this consistency is often more valuable than having total control over every single command, and easier.</p>
<h2 class="wp-block-heading"><strong>Installing &amp; setting up Postgres 18</strong><a class="anchor-link" id="installing-setting-up-postgres-18"></a></h2>
<p>When piloting PostgreSQL 18, set up your test environment to mimic your real deployment exactly. Use the same storage, replication setup, extensions, like <code>pgvector</code>, and a realistic, large enough dataset. Small, fake tests will just hide the real problems you need to find.</p>
<h3 class="wp-block-heading"><strong>Installing PostgreSQL 18</strong><a class="anchor-link" id="installing-postgresql-18"></a></h3>
<p>The specific package names and repos differ based on your system, but here&rsquo;s an example of a typical installation:</p>
<h4 class="wp-block-heading"><strong>For Debian-Based OS:</strong></h4>
<pre class="wp-block-code"><code>sudo apt update
sudo apt install postgresql-18</code></pre>
<p><strong>For RedHat-Based OS:</strong></p>
<pre class="wp-block-code"><code>sudo dnf install postgresql18-server</code></pre>
<h3 class="wp-block-heading"><strong>Checksums at initdb</strong><a class="anchor-link" id="checksums-at-initdb"></a></h3>
<p>Remember, PostgreSQL 18 enables checksums by default at initialization.</p>
<pre class="wp-block-code"><code>sudo /usr/pgsql-18/bin/postgresql-18-setup initdb</code></pre>
<p>But, you can opt out:</p>
<pre class="wp-block-code"><code>sudo -u postgres /usr/pgsql-18/bin/initdb
--no-data-checksums -D /var/lib/pgsql/18/data</code></pre>
<p><strong>Don&rsquo;t forget</strong> that <code>pg_upgrade</code> requires checksum settings to match between the source and target clusters. Treat checksum posture as an upgrade design decision and validate it during rehearsals, not during the cutover window.</p>
<h3 class="wp-block-heading"><strong>Confirm AIO-related settings:&nbsp;</strong><a class="anchor-link" id="confirm-aio-related-settings"></a></h3>
<p>Once the cluster is up, confirm the effective I/O-related settings you&rsquo;re running with:</p>
<pre class="wp-block-code"><code>SHOW io_method;
SHOW effective_io_concurrency;
SHOW maintenance_io_concurrency;</code></pre>
<p>Exact behavior depends on platform and build options, but this gives you a baseline before you start tuning or running load tests.</p>
<h3 class="wp-block-heading"><strong>Adding the PG 18 cluster to ClusterControl and initializing HA / replication</strong><a class="anchor-link" id="adding-the-pg-18-cluster-to-clustercontrol-and-initializing-ha-replication"></a></h3>
<p>Once running, the next step is to bring the cluster into ClusterControl and establish a sane baseline topology. For many teams, a solid default looks like:</p>
<ul class="wp-block-list">
<li>1 primary</li>
<li>1&ndash;2 replicas</li>
<li>HAProxy for routing and HA</li>
<li>PgBouncer for connection pooling (especially with spiky workloads)</li>
</ul>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="374" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_topology_viewer-pg18_pgbouncer_lb-1024x374.png" alt="" class="wp-image-42727"></figure>
<p>ClusterControl&rsquo;s guided workflows can deploy PostgreSQL streaming replication and integrate HAProxy and PgBouncer as part of the setup, reducing the amount of manual wiring needed to reach a production-ready state.</p>
<h2 class="wp-block-heading"><strong>PostgreSQL 18 operations &amp; monitoring</strong><a class="anchor-link" id="postgresql-18-operations-monitoring"></a></h2>
<p>PostgreSQL 18 offers better control and visibility. But that only matters if you use it to create reliable runbooks for when things go sideways. Don&rsquo;t tweak every last setting; the real win is a predictable system under pressure, not the ability to see clearly when it isn&rsquo;t.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="516" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_cluster_overview_dashboard-pg18-1024x516.png" alt="" class="wp-image-42728"></figure>
<h3 class="wp-block-heading"><strong>Tune AIO safely</strong><a class="anchor-link" id="tune-aio-safely"></a></h3>
<p>Asynchronous I/O (AIO) in PostgreSQL 18 is a big deal, especially for handling many tasks at once, but don&rsquo;t rush it. It shines under heavy load, not in simple tests.</p>
<p><strong>Start Simple:</strong></p>
<ul class="wp-block-list">
<li>Pick the right io_method for your system.</li>
<li>Test it with your actual, busy application, not just single queries.</li>
<li>Then try adjusting <code>io_combine_limit</code> and <code>io_max_combine_limit</code>.</li>
</ul>
<p><strong>What to Watch For:</strong></p>
<ul class="wp-block-list">
<li>How long scans take, especially big ones.</li>
<li>Storage delays when the system is busy.</li>
<li>Your worst-case waiting times (P95/P99), not just the average.</li>
<li>How VACUUM behaves while everything else is running.</li>
</ul>
<p><strong>How to tell if it is working: </strong>You&rsquo;ll see fewer unexpected slowdowns when reading a lot of data and less fighting between maintenance tasks and user traffic.</p>
<h3 class="wp-block-heading"><strong>Vacuum/Analyze delay reporting for SLOs</strong><a class="anchor-link" id="vacuum-analyze-delay-reporting-for-slos"></a></h3>
<p>PostgreSQL 18 has better insight into maintenance throttling. If you enable <code>track_cost_delay_timing</code>, VACUUM and ANALYZE will tell you exactly how long they waited because of cost-based delays.</p>
<p>This is huge for troubleshooting. Are you falling behind on maintenance because you told the system to slow down, or because it&rsquo;s genuinely struggling? Knowing the difference is key when figuring out why you missed an SLO or when planning your next capacity upgrade.</p>
<h3 class="wp-block-heading"><strong>Logical replication visibility</strong><a class="anchor-link" id="logical-replication-visibility"></a></h3>
<p>PostgreSQL 18 makes using logical replication much easier by giving you better tools to see what&rsquo;s happening.</p>
<p>Make <code>pg_stat_subscription_stats</code> a regular check-in:</p>
<ul class="wp-block-list">
<li>See conflict counts and when they happened.</li>
<li>Monitor lag and how applies are working over time.</li>
<li>Check that the default parallel streaming is what you want.</li>
</ul>
<p>While better visibility doesn&rsquo;t stop replication problems, it lets you move past the guesswork so you can actually understand the issues and automate fixes.</p>
<h3 class="wp-block-heading"><strong>ClusterControl dashboards</strong><a class="anchor-link" id="clustercontrol-dashboards"></a></h3>
<p>ClusterControl simplifies managing your PostgreSQL clusters by giving you a clear, consistent view of the important stuff:</p>
<ul class="wp-block-list">
<li>Node health and resources</li>
<li>Replication status and lag</li>
<li>High availability and auto-recovery</li>
<li>Backup posture, state, and compliance</li>
</ul>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="516" src="https://severalnines.com/wp-content/uploads/2026/02/cc_database_host_overview_dashboard-pg18-1024x516.png" alt="" class="wp-image-42729"></figure>
<h2 class="wp-block-heading"><strong>Ready to move? PostgreSQL 18 upgrade planning checklist</strong><a class="anchor-link" id="ready-to-move-postgresql-18-upgrade-planning-checklist"></a></h2>
<p>Before jumping to PostgreSQL 18, just check these basics first:</p>
<ul class="wp-block-list">
<li>Backups: Take a full backup and make sure you can restore it.</li>
<li>Checksums: See if your current cluster uses data checksums and plan for the new one.</li>
<li>Extensions: Check that all your extensions (like <code>pgvector</code> or <code>PostGIS</code>) are compatible with PostgreSQL 18.</li>
</ul>
<p>Getting these things squared away now makes the whole upgrade process much smoother.</p>
<h2 class="wp-block-heading"><strong>Conclusion</strong><a class="anchor-link" id="conclusion"></a></h2>
<p>PostgreSQL 18 is all about better operations. It fixes common headaches like slow I/O (thanks to async I/O), keeps upgrades predictable by saving optimizer stats, makes multicolumn indexes smarter with skip-scan, adds modern OAuth authentication, improves index use for busy tables with UUIDv7, and simplifies logical replication.</p>
<p>If you&rsquo;re thinking of upgrading, don&rsquo;t rush. Test PostgreSQL 18 in your lower environments with real data and load first. Check your extensions, especially pgvector. Decide on checksums early, as they&rsquo;re now on by default, and <code>pg_upgrade</code> needs them to match. And definitely test your replication setup, including conflict scenarios.</p>
<p>Taking the time for this careful rollout means fewer surprises and smoother changes in production. Ready to get started with PostgreSQL 18, regardless of where you run it?</p>
<h2 class="wp-block-heading"><strong>Install ClusterControl in 10-minutes. Free 30-day Enterprise trial included!</strong><a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading"><strong>Script Installation Instructions</strong><a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading">Other Installation Options</h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading">ClusterControl on Marketplaces</h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<ul class="wp-block-list">
<li><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></li>
<li><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></li>
<li><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></li>
<li><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></li>
<li><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></li>
</ul>
<p>The post <a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/postgresql-18-upgrades-for-ai-era-workloads-and-operations/">PostgreSQL 18 Upgrades for AI-Era Workloads and Operations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB innovation: binlog_storage_engine, 32-core server, Insert Benchmark</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine_0126897374.html" />
      <id>https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine_0126897374.html</id>
      <updated>2026-03-19T02:57:08+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option binlog_storage_engine. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB). This work was done by Small Datum LLC and sponsored by the MariaDB Foundation.My previous post had results for sysbench with a small server. This post has results for the Insert Benchmark with a large (32-core) server. Both servers use an SSD that has has high fsync latency. This is probably a best-case comparison for the feature. If you really care, then get enterprise SSDs with power loss protection. But you might encounter high fsync latency on public cloud servers.While throughput improves with the InnoDB doublewrite buffer disabled, I am not suggesting people do that for production workloads without understanding the risks it creates.tl;dr for a CPU-bound workloadthroughput for write-heavy steps is larger with the InnoDB doublewrite buffer disabledthroughput for write-heavy steps is much larger with the binlog storage engine enabledthroughput for write-heavy steps is largest with both the binlog storage engine enabled and the InnoDB doublewrite buffer disabled. In this case it was up to 8.9X larger.tl;dr for an IO-bound workloadsee the tl;dr abovethe best throughput comes from enabling the binlog storage engine and disabling the InnoDB doublewrite buffer and was 3.26X.Builds, configuration and hardwareI compiled MariaDB 12.3.1 from source.The server has 32-cores and 128G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. AMD SMT is disabled. The SSD has high fsync latency.I tried 4 my.cnf files:z12b_syncmy.cnf.cz12b_sync_c32r128 (z12b_sync) uses sync-on-commit for the binlog and InnoDBz12c_syncmy.cnf.cz12c_sync_c32r128 (z12c_sync) is like z12b_sync and then enables the binlog storage enginez12b_sync_dw0my.cnf.cz12b_sync_dw0_c32r128 (z12b_sync_dw0) is like z12b_sync and then disables the InnoDB doublewrite bufferz12c_sync_dw0my.cnf.cz12c_sync_dw0_c32r128 (z12c_sync_dw0) is like z12c_sync and then disables the InnoDB doublewrite bufferThe BenchmarkThe benchmark is explained here. It was run with 12 clients for two workloads:CPU-bound - the database is cached by InnoDB, but there is still much write IOIO-bound - most, but not all, benchmark steps are IO-boundThe benchmark steps are:l.i0insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 10M for CPU-bound and 300M for IO-bound.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 16M for CPU-bound and 4M for IO-bound.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 4M for CPU-bound and 1M for IO-bound.Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 1800 seconds.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: summaryThe performance reports are here for CPU-bound and IO-bound.The summary sections from the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA. And from the third table for the IO-bound workload I see that there were failures to meet the SLA for qp500, qr500, qp1000 and qr1000.I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version.When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.I often use context switch rates as a proxy for mutex contention.Results: CPU-boundThe summary is here.Some of the improvements here are huge courtesy of storage with high fsync latency.Throughput is much better with the binlog storage engine enabled when the InnoDB doublewrite buffer is also enabled. Comparing z12b_sync and z12c_sync (z12c_sync uses the binlog storage engine):throughput for l.i0 (load in PK order) is 3.63X larger for z12c_syncthroughput for l.i1 (write-only, larger transactions) is 2.80X larger for z12c_syncthroughput for l.i2 (write-only, smaller transactions) is 8.13X larger for z12c_syncThere is a smaller benefit from only disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12b_sync_dw0:throughput for l.i0 (load in PK order) is the same for z12b_sync and z12b_sync_dw0throughput for l.i1 (write-only, larger transactions) is 1.14X larger for z12b_sync_dw0throughput for l.i2 (write-only, smaller transactions) is 1.93X larger for z12b_sync_dw0The largest benefits come from using the binlog storage engine and disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12c_sync_dw0:throughput for l.i0 (load in PK order) is 3.61X larger for z12c_sync_dw0throughput for l.i1 (write-only, larger transactions) is 3.03X larger for z12b_sync_dw0throughput for l.i2 (write-only, smaller transactions) is 8.90X larger for z12b_sync_dw0Results: IO-boundThe summary is here.For the read-write steps the insert SLA was not met for qr500, qp500, qr1000 and qp1000 as those steps needed more IOPs than the storage devices can provide. So I ignore those steps.Some of the improvements here are huge courtesy of storage with high fsync latency.Throughput is much better with the binlog storage engine enabled when the InnoDB doublewrite buffer is also enabled. Comparing z12b_sync and z12c_sync (z12c_sync uses the binlog storage engine):throughput for l.i0 (load in PK order) is 3.05X larger for z12c_syncthroughput for l.i1 (write-only, larger transactions) is 1.22X larger for z12c_syncthroughput for l.i2 (write-only, smaller transactions) is 1.58X larger for z12c_syncThere is a smaller benefit from only disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12b_sync_dw0:throughput for l.i0 (load in PK order) is the same for z12b_sync and z12b_sync_dw0throughput for l.i1 (write-only, larger transactions) is 2.06X larger for z12b_sync_dw0throughput for l.i2 (write-only, smaller transactions) is 1.59X larger for z12b_sync_dw0The largest benefits come from using the binlog storage engine and disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12c_sync_dw0:throughput for l.i0 (load in PK order) is 3.01X larger for z12c_sync_dw0throughput for l.i1 (write-only, larger transactions) is 3.26X larger for z12b_sync_dw0throughput for l.i2 (write-only, smaller transactions) is 2.78X larger for z12b_sync_dw0</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine_0126897374.html">MariaDB innovation: binlog_storage_engine, 32-core server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option&nbsp;<a href="https://mariadb.org/new-binlog-implementation-in-mariadb-12-3/">binlog_storage_engine</a>. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB). This work was done by Small Datum LLC and sponsored by the MariaDB Foundation.</p>
<p>My&nbsp;<a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html">previous post</a>&nbsp;had results for sysbench with a small server. This post has results for the Insert Benchmark with a large (32-core) server. Both servers use an SSD that has has&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>. This is probably a best-case comparison for the feature. If you really care, then get enterprise SSDs with power loss protection. But you might encounter high fsync latency on public cloud servers.</p>
<p>While throughput improves with the InnoDB doublewrite buffer disabled, I am not suggesting people do that for production workloads without understanding the risks it creates.</p>
<p>tl;dr for a CPU-bound workload</p>

<ul>
<li>throughput for write-heavy steps is larger with the InnoDB doublewrite buffer disabled</li>
<li>throughput for write-heavy steps is much larger with the binlog storage engine enabled</li>
<li>throughput for write-heavy steps is largest with both the binlog storage engine enabled and the InnoDB doublewrite buffer disabled. In this case it was up to 8.9X larger.</li>
</ul>
<div>tl;dr for an IO-bound workload</div>
<div>
<ul>
<li>see the tl;dr above</li>
<li>the best throughput comes from enabling the binlog storage engine and disabling the InnoDB doublewrite buffer and was 3.26X.</li>
</ul>
<div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>
<div></div>
<div>I compiled MariaDB 12.3.1 from source.</div>
<div></div>
<div>The server has 32-cores and 128G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. AMD SMT is disabled. The SSD has&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.h">high fsync latency</a>.</div>
<div></div>
</div>
<div>I tried 4 my.cnf files:</div>
<div>
<ul>
<li>z12b_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_sync_c32r128">my.cnf.cz12b_sync_c32r128</a>&nbsp;(z12b_sync) uses sync-on-commit for the binlog and InnoDB</li>
</ul>
<li>z12c_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12c_sync_c32r128">my.cnf.cz12c_sync_c32r128</a>&nbsp;(z12c_sync) is like z12b_sync and then enables the binlog storage engine</li>
</ul>
<li>z12b_sync_dw0</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_sync_dw0_c32r128">my.cnf.cz12b_sync_dw0_c32r128</a>&nbsp;(z12b_sync_dw0) is like z12b_sync and then disables the InnoDB doublewrite buffer</li>
</ul>
<li>z12c_sync_dw0</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12c_sync_dw0_c32r128">my.cnf.cz12c_sync_dw0_c32r128</a>&nbsp;(z12c_sync_dw0) is like z12c_sync and then disables the InnoDB doublewrite buffer</li>
</ul>
</ul>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>. It was run with 12 clients for two workloads:</div>
<div>
<ul>
<li>CPU-bound &ndash; the database is cached by InnoDB, but there is still much write IO</li>
<li>IO-bound &ndash; most, but not all, benchmark steps are IO-bound</li>
</ul>
</div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 10M for CPU-bound and 300M for IO-bound.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 16M for CPU-bound and 4M for IO-bound.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 4M for CPU-bound and 1M for IO-bound.</li>
<li>Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 1800 seconds.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div><b>Results: summary</b></div>
<div>
<div>
<div></div>
<div>The performance reports are here for <a href="https://mdcallag.github.io/reports/mar26.ib.mem.dell32.ma1203.sync.10m.20m.1800s/all.html">CPU-bound</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.io.dell32.ma1203.sync.300m.5m.1800s/all.html">IO-bound</a>.</div>
</div>
<div></div>
<div>The summary sections from&nbsp;the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA. And from the third table for the&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.io.dell32.ma1203.sync.300m.5m.1800s/all.html#summary">IO-bound workload</a>&nbsp;I see that there were failures to meet the SLA for qp500, qr500, qp1000 and qr1000.</div>
<div>
<div></div>
<div>I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version.
<p>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</p></div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.</div>
</div>
</div>
<div></div>
<div>I often use context switch rates as a proxy for mutex contention.</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The summary&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.mem.dell32.ma1203.sync.10m.20m.1800s/all.html#summary">is here</a>.</div>
<div></div>
<div>Some of the improvements here are huge courtesy of storage with high fsync latency.</div>
<div></div>
<div>Throughput is much better with the binlog storage engine enabled when the InnoDB doublewrite buffer is also enabled. Comparing z12b_sync and z12c_sync (z12c_sync uses the binlog storage engine):
<ul>
<li>throughput for l.i0 (load in PK order) is 3.63X larger for z12c_sync</li>
<li>throughput for l.i1 (write-only, larger transactions) is 2.80X larger for z12c_sync</li>
<li>throughput for l.i2 (write-only, smaller transactions) is&nbsp;8.13X larger for z12c_sync</li>
</ul>
<div>There is a smaller benefit from only disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12b_sync_dw0:</div>
<ul>
<li>throughput for l.i0 (load in PK order) is the same for z12b_sync and z12b_sync_dw0</li>
<li>throughput for l.i1 (write-only, larger transactions) is 1.14X larger for z12b_sync_dw0</li>
<li>throughput for l.i2 (write-only, smaller transactions) is&nbsp;1.93X larger for z12b_sync_dw0</li>
</ul>
<div>
<div>The largest benefits come from using the binlog storage engine and disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12c_sync_dw0:</div>
<ul>
<li>throughput for l.i0 (load in PK order) is 3.61X larger for z12c_sync_dw0</li>
<li>throughput for l.i1 (write-only, larger transactions) is 3.03X larger for z12b_sync_dw0</li>
<li>throughput for l.i2 (write-only, smaller transactions) is 8.90X larger for z12b_sync_dw0</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div><b>Results: IO-bound</b></div>
<div>
<div></div>
<div>The summary <a href="https://mdcallag.github.io/reports/mar26.ib.io.dell32.ma1203.sync.300m.5m.1800s/all.html#summary">is here</a>.</div>
<div></div>
<div>For the read-write steps the insert SLA was not met for qr500, qp500, qr1000 and qp1000 as those steps needed more IOPs than the storage devices can provide. So I ignore those steps.
</div>
<div>Some of the improvements here are huge courtesy of storage with high fsync latency.</div>
<div></div>
<div>Throughput is much better with the binlog storage engine enabled when the InnoDB doublewrite buffer is also enabled. Comparing z12b_sync and z12c_sync (z12c_sync uses the binlog storage engine):
<ul>
<li>throughput for l.i0 (load in PK order) is 3.05X larger for z12c_sync</li>
<li>throughput for l.i1 (write-only, larger transactions) is 1.22X larger for z12c_sync</li>
<li>throughput for l.i2 (write-only, smaller transactions) is 1.58X larger for z12c_sync</li>
</ul>
<div>There is a smaller benefit from only disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12b_sync_dw0:</div>
<ul>
<li>throughput for l.i0 (load in PK order) is the same for z12b_sync and z12b_sync_dw0</li>
<li>throughput for l.i1 (write-only, larger transactions) is 2.06X larger for z12b_sync_dw0</li>
<li>throughput for l.i2 (write-only, smaller transactions) is 1.59X larger for z12b_sync_dw0</li>
</ul>
<div>
<div>The largest benefits come from using the binlog storage engine and disabling the InnoDB doublewrite buffer. Comparing z12b_sync and z12c_sync_dw0:</div>
<ul>
<li>throughput for l.i0 (load in PK order) is 3.01X larger for z12c_sync_dw0</li>
<li>throughput for l.i1 (write-only, larger transactions) is 3.26X larger for z12b_sync_dw0</li>
<li>throughput for l.i2 (write-only, smaller transactions) is 2.78X larger for z12b_sync_dw0</li>
</ul>
</div>
</div>
</div>
<div>
<div></div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine_0126897374.html">MariaDB innovation: binlog_storage_engine, 32-core server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Automated security validation: How 7,000+ tests shaped MariaDB&#8217;s new AppArmor profile</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/" />
      <id>https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/</id>
      <updated>2026-03-19T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Linux kernel security modules provide a good additional layer of security around individual programs by restricting what they are allowed to do, and at best block and detect zero-day security vulnerabilities as soon as anyone tries to exploit them, long before they are widely known and reported. However, the challenge is how to create these security profiles without accidentally also blocking legitimate actions. For MariaDB in Debian and Ubuntu, a new AppArmor profile was recently created by leveraging the extensive test suite with 7000+ tests, giving good confidence that AppArmor is unlikely to yield false positive alerts with it.<br />
AppArmor is a Mandatory Access Control (MAC) system, meaning that each process controlled by AppArmor has a sort of an “allowlist” called profile that defines all capabilities and file paths a program can access. If a program tries to do something not covered by the rules in its AppArmor profile, the action will be denied on the Linux kernel level and a warning logged in the system journal. This additional security layer is valuable because even if a malicious user found a security vulnerability some day in the future, the AppArmor profile severely restricts the ability to exploit it and gain access to the operating system.<br />
AppArmor was originally developed by Novell for use in SUSE Linux, but nowadays the main driver is Canonical and AppArmor is extensively used in Ubuntu and Debian, and many of their derivatives (e.g. Linux Mint, Pop!_OS, Zorin OS) and in Arch. AppArmor’s benefit compared to the main alternative SELinux (used mainly in the RedHat/Fedora ecosystem) is that AppArmor is easier to manage. AppArmor continues to be actively developed, with new major version 5.0 expected to arrive soon.<br />
I also have some personal history contributing some notification handler scripts in Python and I also created the website that AppArmor.net still runs.<br />
Regular review of denials in the system log required<br />
Any system administrator using Debian/Ubuntu needs to know how to check for AppArmor denials. The point of using AppArmor is kind of moot if nobody is checking the denials. When AppArmor blocks an action, it logs the event to the system audit or kernel logs. Understanding these logs is crucial for troubleshooting custom configurations or identifying potential security incidents.<br />
To view recent denials, check /var/log/audit/audit.log or run journalctl -ke --grep=apparmor.<br />
A typical denial entry for MariaDB will look like this (split across multiple lines for legibility):</p>
<p>Copy</p>
<p>msg=audit(1700000000.123:456): apparmor=\"DENIED\" operation=\"open\"<br />
profile=\"/usr/sbin/mariadbd\" name=\"/custom/data/path/test.ibd\" pid=1234<br />
comm=\"mariadbd\" requested_mask=\"r\" denied_mask=\"r\" fsuid=1000 ouid=0msg=audit(1700000000.123:456): apparmor=\"DENIED\" operation=\"open\"<br />
profile=\"/usr/sbin/mariadbd\" name=\"/custom/data/path/test.ibd\" pid=1234<br />
comm=\"mariadbd\" requested_mask=\"r\" denied_mask=\"r\" fsuid=1000 ouid=0<br />
How to interpret this output:</p>
<p>msg=audit(…): The audit timestamp and event serial number.<br />
apparmor=“DENIED”: Indicates AppArmor blocked the action.<br />
operation: The action being attempted (e.g., open, mknod, file_mmap, file_perm).<br />
profile: The specific AppArmor profile that triggered the denial (in this case the /usr/sbin/mariadbd profile).<br />
name: The file path or resource that was blocked. In the example above, a custom data path was denied access because it wasn’t defined in the profile’s allowed abstractions.<br />
comm: The command name that triggered the denial (here mariadbd).<br />
requested_mask / denied_mask: Shows the permissions requested (e.g., r for read, w for write).<br />
pid: The process ID.<br />
fsuid: The user ID of the process attempting the action.<br />
ouid: The owner user ID of the target file.</p>
<p>If an action seems legit and should not be denied, the sysadmin needs to update the existing rules at /etc/apparmor.d/ or drop a local customization file in at /etc/apparmor.d/local/. If the denied action looks malicious, the sysadmin should start a security investigation and if needed report a suspected zero-day vulnerability to the upstream software vendor (e.g. Ubuntu customers to Canonical, or MariaDB customers to MariaDB).<br />
AppArmor in MariaDB - not a novel thing, and not easy to implement well<br />
Based on old bug reports, there was an AppArmor profile already back in 2011, but it was removed in MariaDB 5.1.56 due to backlash from users running into various issues. A new profile was created in 2015, but kept opt-in only due to the risk of side effects. It likely had very few users and saw minimal maintenance, getting only a handful of updates in the past 10 years.<br />
The primary challenge in using mandatory access control systems with MariaDB lies in the sheer breadth of MariaDB’s operational footprint with diverse storage engines and plugins. Also the code base in MariaDB assumes that system calls to Linux always work – which they do under normal circumstances – and do not handle errors well if AppArmor suddenly denies a system call. MariaDB is also a large and complex piece of software to run and operate, and it can be very challenging for system administrators to root-cause that a misbehavior in their system was due to AppArmor blocking a single syscall.<br />
Ironically, AppArmor is most beneficial exactly due to the same reasons for MariaDB. The larger and more complex a software is, the larger are the odds of a security vulnerability arising between the various components. And AppArmor profile helps reduce this complexity down to a single access list.<br />
Over the years there has been users requesting to get the AppArmor profile back, such as Debian Bug#875890 since 2017. The need was raised recently again by the Ubuntu security team during the MariaDB Ubuntu ‘main’ inclusion review in 2025, which prompted a renewed effort by Debian/Ubuntu developers, mainly myself and Aquila Macedo, with upstream MariaDB assistance from Daniel Black.<br />
A fresh approach: leverage the MariaDB test suite for automated testing and the open source community for reviews<br />
The key to creating a robust AppArmor profile is the ability to know in detail what is expected and normal behavior of the system. One could in theory read all of the source code in MariaDB, but with over two million lines, it is of course not feasible in practice. However, MariaDB does have a very extensive 7000+ test suite, and running it should trigger most code paths in MariaDB. Utilizing the test suite was key in creating the new AppArmor profile for MariaDB: we installed MariaDB on a Ubuntu system, enabled AppArmor in complain mode and iterated on the allowlist by running the full mariadb-test-run with all MariaDB plugins and features enabled until we had a comprehensive yet clean list of rules.<br />
To be extra diligent, we also reworked the autopkgtest for MariaDB in Debian and Ubuntu CI systems to run with the AppArmor profile enabled and to print all AppArmor notices at the end of the run, making it easy to detect now and in the future if the MariaDB test suite triggers any AppArmor denials. If any test fails, the release would not get promoted further, protecting users from regressions.<br />
While developing and triggering manual test runs we used the maximal achievable test suite with 7177 tests. The test is however so extensive it takes over two hours to run, and it also has some brittle tests, so the standard test run in Debian and Ubuntu autopkgtest is limited just to MariaDB’s main suite with about 1000 tests. Having some tests fail while testing the AppArmor profile was not a problem, because we didn’t need all the tests to pass – we merely needed them to run as many code paths as possible to see if they run any system calls not accounted for in the AppArmor profile.<br />
Note that extending the profile was not just mechanical copying of log messages to the profile. For example, even though a couple of tests involve running the dash shell, we decided to not allow it, as it opens too much of a path for a potential exploit to access the operating system.<br />
The result of this effort is a modernized, robust profile that is now production-ready. Those interested in the exact technical details can read the Debian Bug#1130272 and the Merge Request discussions at salsa.debian.org, which hosts the Debian packaging source code.<br />
Now available in Debian unstable, soon Ubuntu – feedback welcome!<br />
Even though the file is just 200 lines long, the work to craft it spanned several weeks. To minimize risk we also did a gradual rollout by releasing the first new profile version in complain mode, so AppArmor only logs would-be-denials without blocking anything. The AppArmor profile was switched to enforce mode only in the very latest MariaDB revision 1:11.8.6-4 in Debian, and a NEWS item issued to help increase user awareness of this change. It is also slated for the upcoming Ubuntu 26.04 “Resolute Raccoon” release next month, providing out-of-the-box hardening for the wider ecosystem.<br />
While automated testing is extensive, it cannot simulate everything. Most notably various complicated replication topologies and all Galera setups are likely not covered. Thus, I am calling on the community to deploy this profile and monitor for any audit denials in the kernel logs. If you encounter unexpected behavior or legitimate denials, please submit a bug report via the Debian Bug Tracking System.<br />
To ensure you are running the latest MariaDB version, run apt install --update --yes mariadb-server. To view the latest profile rules, run cat /etc/apparmor.d/mariadbd and to see if it is enforced review the output of aa-status. To quickly check if there were any AppArmor denials, simply run journalctl -k &#124; grep -i apparmor &#124; grep -i mariadb.<br />
Systemd hardening also adopted as security features keep evolving<br />
For those interested in MariaDB security hardening, note that also new systemd hardening options were rolled out in Debian/Ubuntu recently. Note that Debian and Ubuntu are mainly volunteer-driven open source developer communities, and if you find this topic interesting and you think you have the necessary skills, feel free to submit your improvement ideas as Merge Requests at salsa.debian.org/mariadb-team. If your improvement suggestions are not Debian/Ubuntu specific, please submit them directly to upstream at GitHub.com/MariaDB.</p>
<p><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/">Automated security validation: How 7,000+ tests shaped MariaDB&#8217;s new AppArmor profile</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/mariadb-apparmor-profile-debian-ubuntu.jpg" alt="Featured image of post Automated security validation: How 7,000+ tests shaped MariaDB's new AppArmor profile"></p>
<p>Linux kernel security modules provide a good additional layer of security around individual programs by restricting what they are allowed to do, and at best block and detect zero-day security vulnerabilities as soon as anyone tries to exploit them, long before they are widely known and reported. However, the challenge is <strong>how to create these security profiles without accidentally also blocking legitimate actions</strong>. For MariaDB in Debian and Ubuntu, a new AppArmor profile was recently created by leveraging the extensive test suite with 7000+ tests, giving good confidence that AppArmor is unlikely to yield false positive alerts with it.</p>
<p><a class="link" href="https://en.wikipedia.org/wiki/AppArmor" target="_blank" rel="noopener">AppArmor</a> is a Mandatory Access Control (MAC) system, meaning that each process controlled by AppArmor has a sort of an &ldquo;allowlist&rdquo; called <em>profile</em> that defines all capabilities and file paths a program can access. If a program tries to do something not covered by the rules in its AppArmor profile, the action will be denied on the Linux kernel level and a warning logged in the system journal. This additional security layer is valuable because even if a malicious user found a security vulnerability some day in the future, the AppArmor profile severely restricts the ability to exploit it and gain access to the operating system.</p>
<p>AppArmor was originally developed by Novell for use in <a class="link" href="https://en.wikipedia.org/wiki/SUSE_Linux_Enterprise" target="_blank" rel="noopener">SUSE Linux</a>, but nowadays the main driver is Canonical and AppArmor is extensively used in <a class="link" href="https://ubuntu.com/" target="_blank" rel="noopener">Ubuntu</a> and <a class="link" href="https://www.debian.org/" target="_blank" rel="noopener">Debian</a>, and many of their derivatives (e.g. Linux Mint, Pop!_OS, Zorin OS) and in <a class="link" href="https://wiki.archlinux.org/title/AppArmor" target="_blank" rel="noopener">Arch</a>. AppArmor&rsquo;s benefit compared to the main alternative SELinux (used mainly in the RedHat/Fedora ecosystem) is that AppArmor is easier to manage. AppArmor continues to be actively developed, with new major version 5.0 expected to arrive soon.</p>
<p>I also have some personal history contributing some notification handler scripts in Python and I also created the website that <a class="link" href="https://apparmor.net/" target="_blank" rel="noopener">AppArmor.net</a> still runs.</p>
<h2><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/#regular-review-of-denials-in-the-system-log-required" class="header-anchor"></a>Regular review of denials in the system log required<br>
<a class="anchor-link" id="regular-review-of-denials-in-the-system-log-required"></a></h2>
<p>Any system administrator using Debian/Ubuntu needs to know <a class="link" href="https://manpages.ubuntu.com/manpages/resolute/en/man7/apparmor.7.html" target="_blank" rel="noopener">how to check for AppArmor denials</a>. <strong>The point of using AppArmor is kind of moot if nobody is checking the denials.</strong> When AppArmor blocks an action, it logs the event to the system audit or kernel logs. Understanding these logs is crucial for troubleshooting custom configurations or identifying potential security incidents.</p>
<p>To view recent denials, check <code>/var/log/audit/audit.log</code> or run <code>journalctl -ke --grep=apparmor</code>.</p>
<p>A typical denial entry for MariaDB will look like this (split across multiple lines for legibility):</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang"></span><br>
<button class="codeblock-copy" data-id="codeblock-id-0" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>msg=audit(1700000000.123:456): apparmor="DENIED" operation="open"<br>
profile="/usr/sbin/mariadbd" name="/custom/data/path/test.ibd" pid=1234<br>
comm="mariadbd" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0</code></p>
<pre><code>msg=audit(1700000000.123:456): apparmor="DENIED" operation="open"
profile="/usr/sbin/mariadbd" name="/custom/data/path/test.ibd" pid=1234
comm="mariadbd" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0</code></pre>
</div>
<p>How to interpret this output:</p>
<ul>
<li>msg=audit(&hellip;): The audit timestamp and event serial number.</li>
<li>apparmor=&ldquo;DENIED&rdquo;: Indicates AppArmor blocked the action.</li>
<li>operation: The action being attempted (e.g., <code>open</code>, <code>mknod</code>, <code>file_mmap</code>, <code>file_perm</code>).</li>
<li>profile: The specific AppArmor profile that triggered the denial (in this case the <code>/usr/sbin/mariadbd</code> profile).</li>
<li>name: The file path or resource that was blocked. In the example above, a custom data path was denied access because it wasn&rsquo;t defined in the profile&rsquo;s allowed abstractions.</li>
<li>comm: The command name that triggered the denial (here <code>mariadbd</code>).</li>
<li>requested_mask / denied_mask: Shows the permissions requested (e.g., <code>r</code> for read, <code>w</code> for write).</li>
<li>pid: The process ID.</li>
<li>fsuid: The user ID of the process attempting the action.</li>
<li>ouid: The owner user ID of the target file.</li>
</ul>
<p>If an action seems legit and should not be denied, the sysadmin needs to update the existing rules at <code>/etc/apparmor.d/</code> or drop a local customization file in at <code>/etc/apparmor.d/local/</code>. If the denied action looks malicious, the sysadmin should start a security investigation and if needed report a suspected zero-day vulnerability to the upstream software vendor (e.g. Ubuntu customers to Canonical, or MariaDB customers to MariaDB).</p>
<h2><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/#apparmor-in-mariadb---not-a-novel-thing-and-not-easy-to-implement-well" class="header-anchor"></a>AppArmor in MariaDB &ndash; not a novel thing, and not easy to implement well<br>
<a class="anchor-link" id="apparmor-in-mariadb-not-a-novel-thing-and-not-easy-to-implement-well"></a></h2>
<p>Based on <a class="link" href="https://jira.mariadb.org/issues/?jql=text%20~%20apparmor%20ORDER%20BY%20updated%20ASC" target="_blank" rel="noopener">old bug reports</a>, there was an AppArmor profile already back in 2011, but it was removed in MariaDB 5.1.56 due to backlash from users running into various issues. A new profile was created <a class="link" href="https://github.com/MariaDB/server/commit/6050ab658696925f2a031b901eb398fff65fa92a" target="_blank" rel="noopener">in 2015</a>, but kept opt-in only due to the risk of side effects. It likely had very few users and saw minimal maintenance, getting only a handful of updates in the past 10 years.</p>
<p><strong>The primary challenge in</strong> using mandatory access control systems with MariaDB lies in <strong>the sheer breadth of MariaDB&rsquo;s operational footprint</strong> with diverse storage engines and plugins. Also the code base in MariaDB assumes that system calls to Linux always work &ndash; which they do under normal circumstances &ndash; and do not handle errors well if AppArmor suddenly denies a system call. MariaDB is also a large and complex piece of software to run and operate, and it can be very challenging for system administrators to root-cause that a misbehavior in their system was due to AppArmor blocking a single syscall.</p>
<p>Ironically, AppArmor is most beneficial exactly due to the same reasons for MariaDB. The larger and more complex a software is, the larger are the odds of a security vulnerability arising between the various components. <strong>And AppArmor profile helps reduce this complexity down to a single access list.</strong></p>
<p>Over the years there has been users requesting to get the AppArmor profile back, such as <a class="link" href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=875890" target="_blank" rel="noopener">Debian Bug#875890</a> since 2017. The need was raised recently again by the Ubuntu security team during the <a class="link" href="https://bugs.launchpad.net/ubuntu/+source/mariadb/+bug/2122095" target="_blank" rel="noopener">MariaDB Ubuntu &lsquo;main&rsquo; inclusion review</a> in 2025, which prompted a renewed effort by Debian/Ubuntu developers, mainly <a class="link" href="https://salsa.debian.org/otto" target="_blank" rel="noopener">myself</a> and <a class="link" href="https://salsa.debian.org/aquila" target="_blank" rel="noopener">Aquila Macedo</a>, with upstream MariaDB assistance from <a class="link" href="https://salsa.debian.org/grooverdan" target="_blank" rel="noopener">Daniel Black</a>.</p>
<h2><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/#a-fresh-approach-leverage-the-mariadb-test-suite-for-automated-testing-and-the-open-source-community-for-reviews" class="header-anchor"></a>A fresh approach: leverage the MariaDB test suite for automated testing and the open source community for reviews<br>
<a class="anchor-link" id="a-fresh-approach-leverage-the-mariadb-test-suite-for-automated-testing-and-the-open-source-community-for-reviews"></a></h2>
<p>The key to creating a robust AppArmor profile is the ability to know in detail what is expected and <em>normal</em> behavior of the system. One could in theory read all of the source code in MariaDB, but with over two million lines, it is of course not feasible in practice. However, MariaDB does have a very extensive <a class="link" href="https://optimizedbyotto.com/post/grokking-mariadb-test-run-mtr/">7000+ test suite</a>, and running it should trigger most code paths in MariaDB. Utilizing the <strong>test suite was key in creating the new AppArmor profile for MariaDB</strong>: we installed MariaDB on a Ubuntu system, enabled AppArmor in <code>complain</code> mode and iterated on the <em>allowlist</em> by running the full <a class="link" href="https://mariadb.com/docs/server/clients-and-utilities/testing-tools/mariadb-test/mariadb-test-run-pl-options" target="_blank" rel="noopener"><code>mariadb-test-run</code></a> with all MariaDB plugins and features enabled until we had a comprehensive yet clean list of rules.</p>
<p>To be extra diligent, we also reworked the <a class="link" href="https://documentation.ubuntu.com/project/how-ubuntu-is-made/processes/automatic-package-testing-autopkgtest/" target="_blank" rel="noopener">autopkgtest</a> for MariaDB in Debian and Ubuntu CI systems to run with the AppArmor profile enabled and to print all AppArmor notices at the end of the run, making it easy to detect now and in the future if the MariaDB test suite triggers any AppArmor denials. If any test fails, the release would not get promoted further, protecting users from regressions.</p>
<p>While developing and triggering manual test runs we used the maximal achievable test suite with 7177 tests. The test is however so extensive it takes over two hours to run, and it also has some brittle tests, so the standard test run in Debian and Ubuntu autopkgtest is limited just to MariaDB&rsquo;s main suite with about 1000 tests. Having some tests fail while testing the AppArmor profile was not a problem, because we didn&rsquo;t need all the tests to pass &ndash; we merely needed them to run as many code paths as possible to see if they run any system calls not accounted for in the AppArmor profile.</p>
<p>Note that extending the profile was not just mechanical copying of log messages to the profile. For example, even though a couple of tests involve running the <a class="link" href="https://manpages.debian.org/unstable/dash/dash.1.en.html" target="_blank" rel="noopener">dash shell</a>, we decided to not allow it, as it opens too much of a path for a potential exploit to access the operating system.</p>
<p>The result of this effort is a modernized, robust profile that is now production-ready. Those interested in the exact technical details can read the <a class="link" href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1130272" target="_blank" rel="noopener">Debian Bug#1130272</a> and the <a class="link" href="https://salsa.debian.org/mariadb-team/mariadb-server/-/merge_requests/" target="_blank" rel="noopener">Merge Request discussions at salsa.debian.org</a>, which hosts the Debian packaging source code.</p>
<h2><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/#now-available-in-debian-unstable-soon-ubuntu--feedback-welcome" class="header-anchor"></a>Now available in Debian unstable, soon Ubuntu &ndash; feedback welcome!<br>
<a class="anchor-link" id="now-available-in-debian-unstable-soon-ubuntu-feedback-welcome"></a></h2>
<p>Even though the <a class="link" href="https://salsa.debian.org/mariadb-team/mariadb-server/-/blob/e32e833e7b505f1f65a6666e5499d9ea5843698c/debian/apparmor/mariadbd" target="_blank" rel="noopener">file is just 200 lines long</a>, the work to craft it spanned several weeks. To minimize risk we also did a gradual rollout by releasing the first new profile version in <code>complain</code> mode, so AppArmor only logs would-be-denials without blocking anything. The AppArmor profile was switched to <code>enforce</code> mode only in the very latest MariaDB revision 1:11.8.6-4 in Debian, and a NEWS item issued to help increase user awareness of this change. It is also slated for the upcoming Ubuntu 26.04 &ldquo;Resolute Raccoon&rdquo; release next month, providing out-of-the-box hardening for the wider ecosystem.</p>
<p>While automated testing is extensive, it cannot simulate everything. Most notably various complicated replication topologies and all Galera setups are likely not covered. Thus, I am calling on the community to deploy this profile and monitor for any audit denials in the kernel logs. <strong>If you encounter unexpected behavior or legitimate denials, please submit a bug report via the <a class="link" href="https://bugs.debian.org/" target="_blank" rel="noopener">Debian Bug Tracking System</a>.</strong></p>
<p>To ensure you are running the latest MariaDB version, run <code>apt install --update --yes mariadb-server</code>. To view the latest profile rules, run <code>cat /etc/apparmor.d/mariadbd</code> and to see if it is enforced review the output of <code>aa-status</code>. To quickly check if there were any AppArmor denials, simply run <code>journalctl -k | grep -i apparmor | grep -i mariadb</code>.</p>
<h2><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/#systemd-hardening-also-adopted-as-security-features-keep-evolving" class="header-anchor"></a>Systemd hardening also adopted as security features keep evolving<br>
<a class="anchor-link" id="systemd-hardening-also-adopted-as-security-features-keep-evolving"></a></h2>
<p>For those interested in <a class="link" href="https://optimizedbyotto.com/post/zero-configuration-tls-mariadb-11.8/">MariaDB security hardening</a>, note that also <a class="link" href="https://salsa.debian.org/mariadb-team/mariadb-server/-/merge_requests/152" target="_blank" rel="noopener">new systemd hardening options</a> were rolled out in Debian/Ubuntu recently. Note that Debian and Ubuntu are mainly volunteer-driven open source developer communities, and if you find this topic interesting and you think you have the necessary skills, feel free to submit your improvement ideas as Merge Requests at <a class="link" href="https://salsa.debian.org/mariadb-team/mariadb-server/-/merge_requests/" target="_blank" rel="noopener">salsa.debian.org/mariadb-team</a>. If your improvement suggestions are not Debian/Ubuntu specific, please submit them directly to upstream at <a class="link" href="https://github.com/mariadb/server/" target="_blank" rel="noopener">GitHub.com/MariaDB</a>.</p>

<p><a href="https://optimizedbyotto.com/post/new-apparmor-profile-for-mariadb/">Automated security validation: How 7,000+ tests shaped MariaDB&#8217;s new AppArmor profile</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB innovation: binlog_storage_engine, 48-core server, Insert Benchmark</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine.html" />
      <id>https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine.html</id>
      <updated>2026-03-18T19:46:56+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option binlog_storage_engine. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB). See this blog post for more details on the new feature. This work was done by Small Datum LLC and sponsored by the MariaDB Foundation.My previous post had results for sysbench with a small server. This post has results for the Insert Benchmark with a large (48-core) server. Storage on this server has a low fsync latency while the small server has high fsync latency.In this test throughput doesn\'t improve with the InnoDB doublewrite buffer disabled. Even if it did I am not suggesting people do that for production workloads without understanding the risks it creates.tl;drbinlog storage engine makes some things better without making other things worsebinlog storage engine doesn\'t make all write-heavy steps faster because the commit path isn\'t the bottleneck in all cases on a server with storage that has low fsync latencytl;dr for a CPU-bound workloadthe l.i0 step (load in PK order) is ~1.3X faster with binlog storage enginethe l.i2 step (write-only with smaller transactions) is ~1.5X faster with binlog storage enginetl;dr for an IO-bound workloadthe l.i0 step (load in PK order) is ~1.08X faster with binlog storage engineBuilds, configuration and hardwareI compiled MariaDB 12.3.1 from source.The server has 48-cores and 128G of RAM. Storage is 2 NVMe device with ext-4, discard enabled and RAID. The OS is Ubuntu 22.04. AMD SMT is disabled. The SSD has low fsync latency.I tried 4 my.cnf files:z12b_syncmy.cnf.cz12b_sync_c32r128 (z12b_sync) is like z12b except it enables sync-on-commit for the binlog and InnoDBz12c_syncmy.cnf.cz12c_sync_c32r128 (z12c_sync) is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.z12b_sync_dw0my.cnf.cz12b_sync_dw0_c32r128 (z12b_sync_dw0) is like z12b_sync but disables the InnoDB doublewrite bufferz12c_sync_dw0my.cnf.cz12c_sync_dw0_c32r128 (z12c_sync_dw0) is like cz12c_sync but disables the InnoDB doublewrite bufferThe BenchmarkThe benchmark is explained here. It was run with 20 clients for two workloads:CPU-bound - the database is cached by InnoDB, but there is still much write IOIO-bound - most, but not all, benchmark steps are IO-boundThe benchmark steps are:l.i0insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 10M for CPU-bound and 200M for IO-bound.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 40M for CPU-bound and 4M for IO-bound.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 10M for CPU-bound and 1M for IO-bound.Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 3600 seconds.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: summaryThe performance reports are here for CPU-bound and IO-bound.The summary sections from the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA. And from the third table for the IO-bound workload I see that there were failures to meet the SLA for qp500, qr500, qp1000 and qr1000.I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version.When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.I often use context switch rates as a proxy for mutex contention.Results: CPU-boundThe summary is here. Disabling the InnoDB doublewrite buffer doesn&#039;t improve performance.With and without the InnoDB doublewrite buffer enabled, enabling the binlog storage engine improves throughput a lot for two of the write-heavy steps while there are only small changes on the other two write-heavy steps:l.i0, load in PK order, gets ~1.3X more throughputwhen the binlog storage engine is enabled (see here)storage writes per insert (wpi) are reduced by about 1/2KB written to storage per insert (wkbpi) is a bit smallercontext switches per insert (cspq) are reduced by about 1/3l.x, create secondary indexes, is unchangedwhen the binlog storage engine is enabled (see here)storage writes per insert (wpi) are reduced by about 4/5KB written to storage per insert (wkbpi) are reduced almost in halfcontext switches per insert (cspq) are reduced by about 1/4l.i1, write-only with larger tranactions, is unchangedl.i2, write-only with smaller transactions, gets ~1.5X more throughputResults: IO-boundThe summary is here.Disabling the InnoDB doublewrite buffer doesn&#039;t improve performance.For the read-write steps the insert SLA was not met for qr500, qp500, qr1000 and qp1000 as those steps needed more IOPs than the storage devices can provide. So I ignore those steps.Enabling the InnoDB doublewrite buffer improves throughput by ~1.25X on the l.i2 step (write-only with smaller transactions) but doesn&#039;t change performance on the other steps.as expected there is a large reduction in KB written to storage (see wkbpi here)Enabling the binlog storage engine improves throughput by 9% and 8% on the l.i0 step (load in PK order) but doesn&#039;t have a significant impact on other steps.with the binlog storage engine there is a large reduction in storage writes per insert (wpi), a small reduction in KB written to storage per insert (wkbpi) and small increases in CPU per insert (cpupq) and contex switches per insert (cspq) -- see here</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine.html">MariaDB innovation: binlog_storage_engine, 48-core server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option&nbsp;<a href="https://mariadb.org/new-binlog-implementation-in-mariadb-12-3/">binlog_storage_engine</a>. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB). See this <a href="https://mariadb.org/mariadb-innovation-innodb-based-binary-log/">blog post</a> for more details on the new feature. This work was done by Small Datum LLC and sponsored by the MariaDB Foundation.</p>
<p>My&nbsp;<a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html">previous post</a>&nbsp;had results for sysbench with a small server. This post has results for the Insert Benchmark with a large (48-core) server. Storage on this server has a low fsync latency while the small server has&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>.</p>
<p>In this test throughput doesn&rsquo;t improve with the InnoDB doublewrite buffer disabled. Even if it did I am not suggesting people do that for production workloads without understanding the risks it creates.</p>
<p>tl;dr</p>

<ul>
<li>binlog storage engine makes some things better without making other things worse</li>
<li>binlog storage engine doesn&rsquo;t make all write-heavy steps faster because the commit path isn&rsquo;t the bottleneck in all cases on a server with storage that has low fsync latency</li>
</ul>
<p>tl;dr for a CPU-bound workload</p>

<ul>
<li>the l.i0 step (load in PK order) is ~1.3X faster with binlog storage engine</li>
<li>the l.i2 step (write-only with smaller transactions) is ~1.5X faster with binlog storage engine</li>
</ul>
<div>tl;dr for an IO-bound workload</div>
<div>
<ul>
<li>the l.i0 step (load in PK order) is ~1.08X faster with binlog storage engine</li>
</ul>
<div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>
<div></div>
<div>I compiled MariaDB 12.3.1 from source.</div>
<div></div>
<div>The server has 48-cores and 128G of RAM. Storage is 2 NVMe device with ext-4, discard enabled and RAID. The OS is Ubuntu 22.04. AMD SMT is disabled. The SSD has&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.h">low fsync latency</a>.</div>
<div></div>
</div>
<div>I tried 4 my.cnf files:</div>
<div>
<ul>
<li>z12b_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_sync_c32r128">my.cnf.cz12b_sync_c32r128</a>&nbsp;(z12b_sync) is like z12b except it enables sync-on-commit for the binlog and InnoDB</li>
</ul>
<li>z12c_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12c_sync_c32r128">my.cnf.cz12c_sync_c32r128</a>&nbsp;(z12c_sync) is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.</li>
</ul>
<li>z12b_sync_dw0</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_sync_dw0_c32r128">my.cnf.cz12b_sync_dw0_c32r128</a>&nbsp;(z12b_sync_dw0) is like z12b_sync but disables the InnoDB doublewrite buffer</li>
</ul>
<li>z12c_sync_dw0</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12c_sync_dw0_c32r128">my.cnf.cz12c_sync_dw0_c32r128</a>&nbsp;(z12c_sync_dw0) is like cz12c_sync but disables the InnoDB doublewrite buffer</li>
</ul>
</ul>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>. It was run with 20 clients for two workloads:</div>
<div>
<ul>
<li>CPU-bound &ndash; the database is cached by InnoDB, but there is still much write IO</li>
<li>IO-bound &ndash; most, but not all, benchmark steps are IO-bound</li>
</ul>
</div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 10M for CPU-bound and 200M for IO-bound.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 40M for CPU-bound and 4M for IO-bound.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 10M for CPU-bound and 1M for IO-bound.</li>
<li>Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 3600 seconds.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div>
<div><b>Results: summary</b></div>
<div>
<div>
<div></div>
<div>The performance reports are here for <a href="https://mdcallag.github.io/reports/mar26.ib.mem.hetz.ma1203synconly.10m.50m.3600s/all.html">CPU-bound</a> and <a href="https://mdcallag.github.io/reports/mar26.ib.io.hetz.ma1203synconly.200m.5m.3600s/all.html">IO-bound</a>.</div>
</div>
<div></div>
<div>The summary sections from&nbsp;the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA. And from the third table for the <a href="https://mdcallag.github.io/reports/mar26.ib.io.hetz.ma1203synconly.200m.5m.3600s/all.html#summary">IO-bound workload</a> I see that there were failures to meet the SLA for qp500, qr500, qp1000 and qr1000.</div>
<div>
<div></div>
<div>I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version.
<p>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</p></div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.</div>
</div>
</div>
<div></div>
<div>I often use context switch rates as a proxy for mutex contention.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The summary&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.mem.hetz.ma1203synconly.10m.50m.3600s/all.html#summary">is here</a>.&nbsp;</div>
<div>
<ul>
<li>Disabling the InnoDB doublewrite buffer doesn&rsquo;t improve performance.</li>
</ul>
</div>
<div>With and without the InnoDB doublewrite buffer enabled, enabling the binlog storage engine improves throughput a lot for two of the write-heavy steps while there are only small changes on the other two write-heavy steps:</div>
<div>
<ul>
<li>l.i0, load in PK order, gets ~1.3X more throughput</li>
<ul>
<li>when the binlog storage engine is enabled (see&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.mem.hetz.ma1203synconly.10m.50m.3600s/all.html#l.i0.metrics">here</a>)</li>
<ul>
<li>storage writes per insert (wpi) are reduced by about 1/2</li>
<li>KB written to storage per insert (wkbpi) is a bit smaller</li>
<li>context switches per insert (cspq) are reduced by about 1/3</li>
</ul>
</ul>
<li>l.x, create secondary indexes, is unchanged</li>
<ul>
<li>when the binlog storage engine is enabled (see <a href="https://mdcallag.github.io/reports/mar26.ib.mem.hetz.ma1203synconly.10m.50m.3600s/all.html#l.i2.metrics">here</a>)</li>
<ul>
<li>storage writes per insert (wpi) are reduced by about 4/5</li>
<li>KB written to storage per insert (wkbpi) are reduced almost in half</li>
<li>context switches per insert (cspq) are reduced by about 1/4</li>
</ul>
</ul>
<li>l.i1, write-only with larger tranactions, is unchanged</li>
<li>l.i2, write-only with smaller transactions, gets ~1.5X more throughput</li>
</ul>
</div>
</div>
<div></div>
<div><b>Results: IO-bound</b></div>
<div>
<div></div>
<div>The summary&nbsp;<a href="https://mdcallag.github.io/reports/mar26.ib.io.hetz.ma1203synconly.200m.5m.3600s/all.html#summary">is here</a>.</div>
<div>
<ul>
<li>Disabling the InnoDB doublewrite buffer doesn&rsquo;t improve performance.</li>
<li>For the read-write steps the insert SLA was not met for qr500, qp500, qr1000 and qp1000 as those steps needed more IOPs than the storage devices can provide. So I ignore those steps.</li>
<li>Enabling the InnoDB doublewrite buffer improves throughput by ~1.25X on the l.i2 step (write-only with smaller transactions) but doesn&rsquo;t change performance on the other steps.</li>
<ul>
<li>as expected there is a large reduction in KB written to storage (see wkbpi <a href="https://mdcallag.github.io/reports/mar26.ib.io.hetz.ma1203synconly.200m.5m.3600s/all.html#l.i2.metrics">here</a>)</li>
</ul>
<li>Enabling the binlog storage engine improves throughput by 9% and 8% on the l.i0 step (load in PK order) but doesn&rsquo;t have a significant impact on other steps.</li>
<ul>
<li>with the binlog storage engine there is a large reduction in storage writes per insert (wpi), a small reduction in KB written to storage per insert (wkbpi) and small increases in CPU per insert (cpupq) and contex switches per insert (cspq) &mdash; see <a href="https://mdcallag.github.io/reports/mar26.ib.io.hetz.ma1203synconly.200m.5m.3600s/all.html#l.i0.metrics">here</a></li>
</ul>
</ul>
</div>
</div>
<div></div>
<div></div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/03/mariadb-innovation-binlogstorageengine.html">MariaDB innovation: binlog_storage_engine, 48-core server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>CPU efficiency for MariaDB, MySQL and Postgres on TPROC-C with a small server</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/cpu-efficiency-for-mariadb-mysql-and.html" />
      <id>https://smalldatum.blogspot.com/2026/03/cpu-efficiency-for-mariadb-mysql-and.html</id>
      <updated>2026-03-16T19:35:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>I started to use TPROC-C from HammerDB to test MariaDB, MySQL and Postgres and published results for MySQL and Postgres on small and large servers. This post provides more detail on CPU overheads for MariaDB, MySQL and Postgres on a small server.tl;drPostgres get the most throughput and the difference is large.MariaDB gets more throughput than MySQLThroughput improves for MariaDB and MySQL but not for Postgres when stored procedures are enabled. It is possible that the stored procedure support in MariaDB and MySQL is more CPU efficient than in Postgres. The HammerDB author explained that HammerDB uses server-side functions with Postgres when stored procs are disabled. That might explain why there isn\'t much of a benefit.Postgres uses ~2X to ~4X more CPU for background tasks than InnoDB but it is doing between 1.5X and 3X more writes so were I to normalize that CPU overhead (from vacuum) it might be similar to MySQL and MariaDB. Regardless, the total amount of CPU for background tasks is not significant relative to other CPU consumers.Builds, configuration and hardwareI compiled everything from source: MariaDB 11.8.6, MySQL 8.4.8 and Postgres 18.2.The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it are here.For Postgres 18 the config file is named conf.diff.cx10b_c8r32 and adds io_mod=\'sync\' which matches behavior in earlier Postgres versions.For MySQL the config file is named my.cnf.cz12a_c8r32.For MariaDB the config file is named my.cnf.cz12b_c8r32.For all DBMS fsync on commit is disabled to avoid turning this into an fsync benchmark. The server has an SSD with high fsync latency.BenchmarkThe benchmark is tproc-c from HammerDB. The tproc-c benchmark is derived from TPC-C.The benchmark was run for one workload, the working set is cached and there is only one user:vu=1, w=100 - 1 virtual user, 100 warehousesThe test was repeated with stored procedure support in HammerDB enabled and then disabled. For my previous results it was always enabled. I did this to understand the impact of stored procedures. While they are great for workloads with much concurrency because they reduce lock-hold durations, the workload here did not have much concurrency. That helps me understand the CPU efficiency of stored procedures.The benchmark for Postgres is run by this script which depends on scripts here. The MySQL scripts are similar.stored procedures are enabledpartitioning is used for when the warehouse count is &#62;= 1000a 5 minute rampup is usedthen performance is measured for 120 minutesResults: NOPMThe numbers in the table below are the NOPM (throughput) for TPROC-C.SummaryPostgres sustains the most throughput with and without stored proceduresMariaDB sustains more throughput than MySQLStored procedures help MariaDB and MySQL, but do not improve Postgres throughputLegend:* sp0 - stored procedures disabled* sp1 - stored procedures enabledsp0     sp111975   19281   MariaDB 11.8.6 9400   16874   MySQL 8.4.833261   33679   Postgres 18.2Results: vmstatThe following is computed from a sample of ~1000 lines of vmstat output collected from the middle of the benchmark run. The ratio of us to sy is almost 2X larger in Postgres than in MariaDB and MySQL with stored procedures disabled. But the ratios are similar with stored procedures enabled.The context switch rate is about 5X larger in MariaDB and MySQL vs Postgres with stored procedures disabled before normalizing by thoughput, with normalization the difference would be even larger. But the difference is smaller with stored procedures enabled.Postgres has better throughput because MariaDB and MySQL use more CPU per NOPM. The diference is larger with stored procedures disabled. Perhaps the stored prcoedure evaluator in MariaDB and MySQL is more efficient than in Postgres.Legend:* r - average value for the r column, runnable tasks* cs - average value for the cs column, context switches/s* us, sy - average value for the us and sy columns, user and system CPU utilization/s* us+sy - average value for the sum of us and sy* cpuPer - ((us+sy) / NOPM) * 1000, smaller is better--- sp0r       cs      us      sy      us+sy   cpuPer1.112   54786   10.0    3.1     13.2    1.102   MariaDB 11.8.61.130   65413   10.8    2.9     13.7    1.457   MySQL 8.4.81.206   11266   12.2    1.9     14.1    0.423   Postgres 18.2--- sp1r       cs      us      sy      us+sy   cpuPer1.079   11739   12.0    1.2     13.1    0.679   MariaDB 11.8.61.043   14698   12.0    1.0     13.0    0.770   MySQL 8.4.81.107    9776   12.4    1.4     13.8    0.409   Postgres 18.2Results: flamegraphs with stored proceduresThe flamegraphs are here.\'The following tables summarize CPU time based on the percentage of samples that can be mapped to various tasks and processes. Note that these are absolute values. So both MySQL and Postgres have similar distributions of CPU time per area even when Postgres gets 2X or 3X more throughput.Summarythe CPU distributions by area are mostly similar for MariaDB, MySQL and PostgresPostgres uses 2X to 4X more CPU for background work (vacuum)Legend* client - time in the HammerDB benchmark client* swap - time in kernel swap code* db-fg - time running statements in the DBMS for the client* db-bg - time doing background work in the DBMS- Total        MariaDB MySQL   Postgresclient   4.62    5.70    6.82swap     5.15    7.09    5.55db-fg   86.83   83.89   79.43db-bg    1.43    3.00   ~6.x- Limited to db-fg, excludes Postgres because the data is messy        MariaDB MySQLupdate  22.39   21.49insert   6.17    5.82select  23.43   18.04commit   ~4.0    ~5.0parse   ~10.0   ~10.0Results: flamegraphs without stored proceduresThe flamegraphs are here.Summarythe CPU distributions by area are mostly similar for MariaDB and MySQLPostgres uses 2X to 4X more CPU for background work (vacuum)Legend* client - time in the HammerDB benchmark client* swap - time in kernel swap code* db-fg - time running statements in the DBMS for the client* db-bg - time doing background work in the DBMS- Total        MariaDB MySQL   Postgresclient  14.29   11.92    7.52swap    13.18   15.58    5.80db-fg   70.39   70.14   77.24db-bg   ~1.0    ~2.0     6.55- Limited to db-fg, excludes Postgres because the data is messy        MariaDB MySQLupdate  14.19   12.04insert   4.29    3.57select  18.14   11.96prepare   NA     6.73commit  ~2.0     2.64parse    8.86    9.73network 15.47   13.73For MySQL parse, 2.5% was from pfs_digest_end_vc and children.</p>
<p><a href="https://smalldatum.blogspot.com/2026/03/cpu-efficiency-for-mariadb-mysql-and.html">CPU efficiency for MariaDB, MySQL and Postgres on TPROC-C with a small server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I started to use TPROC-C from <a href="https://www.hammerdb.com/">HammerDB</a> to test MariaDB, MySQL and Postgres and published results for MySQL and Postgres on <a href="https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-small-server.html">small</a> and <a href="https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-large-server.html">large</a> servers. This post provides more detail on CPU overheads for MariaDB, MySQL and Postgres on a small server.</p>
<p>tl;dr</p>

<ul>
<li>Postgres get the most throughput and the difference is large.</li>
<li>MariaDB gets more throughput than MySQL</li>
<li>Throughput improves for MariaDB and MySQL but not for Postgres when stored procedures are enabled. It is possible that the stored procedure support in MariaDB and MySQL is more CPU efficient than in Postgres. The HammerDB author explained that HammerDB uses server-side functions with Postgres when stored procs are disabled. That might explain why there isn&rsquo;t much of a benefit.</li>
<li>Postgres uses ~2X to ~4X more CPU for background tasks than InnoDB but it is doing between 1.5X and 3X more writes so were I to normalize that CPU overhead (from vacuum) it might be similar to MySQL and MariaDB. Regardless, the total amount of CPU for background tasks is not significant relative to other CPU consumers.</li>
</ul>
<div><b>Builds, configuration and hardware</b></div>

<div>
<div>
<div>I compiled everything from source: MariaDB 11.8.6, MySQL 8.4.8 and Postgres 18.2.</div>
<div></div>
<div>The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2026/03/ASUS%20ExpertCenter%20PN53%20with%20AMD%20Ryzen%207%207735HS,%2032G%20RAM%20and%202%20m.2%20slots%20(one%20for%20OS%20install,%20one%20for%20DB%20perf%20tests)">are here</a>.</div>
<div></div>
<div>
<div>For Postgres 18 the config file is named&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/may25.pg18/pn53/conf.diff.cx10b_c8r32">conf.diff.cx10b_c8r32</a>&nbsp;and adds io_mod=&rsquo;sync&rsquo; which matches behavior in earlier Postgres versions.</div>
</div>
</div>
<div>For MySQL the config file is named&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/my8406_rel_o2nofp/etc/my.cnf.cz12a_c8r32">my.cnf.cz12a_c8r32</a>.</div>
<div></div>
<div>For MariaDB the config file is named <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma110803_rel_withdbg/etc/my.cnf.cz12b_c8r32">my.cnf.cz12b_c8r32</a>.
</div>
<div>For all DBMS fsync on commit is disabled to avoid turning this into an fsync benchmark. The server has an SSD with&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>.</div>
<div></div>
<div>
<div><b>Benchmark</b>
<div></div>
</div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://www.hammerdb.com/docs/ch03.html">tproc-c</a>&nbsp;from&nbsp;<a href="https://www.hammerdb.com/">HammerDB</a>. The tproc-c benchmark is derived from TPC-C.
<p>The benchmark was run for one workload, the working set is cached and there is only one user:</p></div>
<div>
<ul>
<li>vu=1, w=100 &ndash; 1 virtual user, 100 warehouses</li>
</ul>
<div>The test was repeated with stored procedure support in HammerDB enabled and then disabled. For my previous results it was always enabled. I did this to understand the impact of stored procedures. While they are great for workloads with much concurrency because they reduce lock-hold durations, the workload here did not have much concurrency. That helps me understand the CPU efficiency of stored procedures.</div>
<div></div>
<div>The benchmark for Postgres is run by&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/jan26.tprocc.pn53.pg/allpg.N.sh">this script</a>&nbsp;which depends on&nbsp;<a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/jan26.tprocc.pn53.pg/testscripts">scripts here</a>. The MySQL scripts are similar.</div>
<div>
<ul>
<li>stored procedures are enabled</li>
<li>partitioning is used for when the warehouse count is &gt;= 1000</li>
<li>a 5 minute rampup is used</li>
<li>then performance is measured for 120 minutes</li>
</ul>
<div><b>Results: NOPM</b></div>
</div>
</div>
</div>
<div></div>
<div>The numbers in the table below are the NOPM (throughput) for TPROC-C.</div>
<div></div>
<div>Summary</div>
<div>
<ul>
<li>Postgres sustains the most throughput with and without stored procedures</li>
<li>MariaDB sustains more throughput than MySQL</li>
<li>Stored procedures help MariaDB and MySQL, but do not improve Postgres throughput</li>
</ul>
</div>
<div><span>Legend:<br>* sp0 &ndash; stored procedures disabled</span></div>
</div>
<div><span>* sp1 &ndash; stored procedures enabled</span></div>
<div><span><br></span></div>
<div>
<div><span>sp0&nbsp; &nbsp; &nbsp;sp1</span></div>
<div><span>11975&nbsp; &nbsp;19281&nbsp; &nbsp;MariaDB 11.8.6</span></div>
<div><span>&nbsp;<span>9400</span>&nbsp; &nbsp;<span>16874</span>&nbsp; &nbsp;MySQL 8.4.8</span></div>
<div><span><span>33261</span>&nbsp; &nbsp;<span>33679</span>&nbsp; &nbsp;Postgres 18.2</span></div>
</div>
<div></div>
<div><b>Results: vmstat</b></div>
<div></div>
<div>The following is computed from a sample of ~1000 lines of vmstat output collected from the middle of the benchmark run.&nbsp;</div>
<div>
<ul>
<li>The ratio of us to sy is almost 2X larger in Postgres than in MariaDB and MySQL with stored procedures disabled. But the ratios are similar with stored procedures enabled.</li>
<li>The context switch rate is about 5X larger in MariaDB and MySQL vs Postgres with stored procedures disabled before normalizing by thoughput, with normalization the difference would be even larger. But the difference is smaller with stored procedures enabled.</li>
<li>Postgres has better throughput because MariaDB and MySQL use more CPU per NOPM. The diference is larger with stored procedures disabled. Perhaps the stored prcoedure evaluator in MariaDB and MySQL is more efficient than in Postgres.</li>
</ul>
</div>
<div><span>Legend:</span></div>
<div><span>* r &ndash; average value for the r column, runnable tasks</span></div>
<div><span>* cs &ndash; average value for the cs column, context switches/s</span></div>
<div><span>* us, sy &ndash; average value for the us and sy columns, user and system CPU utilization/s</span></div>
<div><span>* us+sy &ndash; average value for the sum of us and sy</span></div>
<div><span>* cpuPer &ndash; ((us+sy) / NOPM) * 1000, smaller is better</span></div>
<div><span><br></span></div>
<div><span>
<div>&mdash; sp0</div>
<div>r&nbsp; &nbsp; &nbsp; &nbsp;cs&nbsp; &nbsp; &nbsp; us&nbsp; &nbsp; &nbsp; sy&nbsp; &nbsp; &nbsp; us+sy&nbsp; &nbsp;cpuPer</div>
<div>1.112&nbsp; &nbsp;<span>54786</span>&nbsp; &nbsp;10.0&nbsp; &nbsp; 3.1&nbsp; &nbsp; &nbsp;13.2&nbsp; &nbsp; 1.102&nbsp; &nbsp;MariaDB 11.8.6</div>
<div>1.130&nbsp; &nbsp;<span>65413</span>&nbsp; &nbsp;10.8&nbsp; &nbsp; 2.9&nbsp; &nbsp; &nbsp;13.7&nbsp; &nbsp; <span>1.457</span>&nbsp; &nbsp;MySQL 8.4.8</div>
<div>1.206&nbsp; &nbsp;<span>11266</span>&nbsp; &nbsp;12.2&nbsp; &nbsp; 1.9&nbsp; &nbsp; &nbsp;14.1&nbsp; &nbsp; <span>0.423</span>&nbsp; &nbsp;Postgres 18.2</div>
<div></div>
<div>&mdash; sp1</div>
<div>r&nbsp; &nbsp; &nbsp; &nbsp;cs&nbsp; &nbsp; &nbsp; us&nbsp; &nbsp; &nbsp; sy&nbsp; &nbsp; &nbsp; us+sy&nbsp; &nbsp;cpuPer</div>
<div>1.079&nbsp; &nbsp;11739&nbsp; &nbsp;12.0&nbsp; &nbsp; 1.2&nbsp; &nbsp; &nbsp;13.1&nbsp; &nbsp; 0.679&nbsp; &nbsp;MariaDB 11.8.6</div>
<div>1.043&nbsp; &nbsp;<span>14698</span>&nbsp; &nbsp;12.0&nbsp; &nbsp; 1.0&nbsp; &nbsp; &nbsp;13.0&nbsp; &nbsp; <span>0.770</span>&nbsp; &nbsp;MySQL 8.4.8</div>
<div>1.107&nbsp; &nbsp; <span>9776</span>&nbsp; &nbsp;12.4&nbsp; &nbsp; 1.4&nbsp; &nbsp; &nbsp;13.8&nbsp; &nbsp; <span>0.409</span>&nbsp; &nbsp;Postgres 18.2</div>
<p></p></span></div>
<div></div>
<div><b>Results: flamegraphs with stored procedures</b></div>
<div></div>
<div>The flamegraphs <a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/mar26.sp.pn53.vu1.w100/sp1">are here</a>.&rsquo;</div>
<div></div>
<div>The following tables summarize CPU time based on the percentage of samples that can be mapped to various tasks and processes. Note that these are absolute values. So both MySQL and Postgres have similar distributions of CPU time per area even when Postgres gets 2X or 3X more throughput.</div>
<div></div>
<div>Summary</div>
<div>
<ul>
<li>the CPU distributions by area are mostly similar for MariaDB, MySQL and Postgres</li>
<li>Postgres uses 2X to 4X more CPU for background work (vacuum)</li>
</ul>
</div>
<div><span>Legend</span></div>
<div>
<div><span>* client &ndash; time in the HammerDB benchmark client</span></div>
<div><span>* swap &ndash; time in kernel swap code</span></div>
<div><span>* db-fg &ndash; time running statements in the DBMS for the client</span></div>
<div><span>* db-bg &ndash; time doing background work in the DBMS</span></div>
<div><span><br></span></div>
<div><span>&ndash; Total</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; MariaDB MySQL&nbsp; &nbsp;Postgres</span></div>
<div><span>client&nbsp; &nbsp;4.62&nbsp; &nbsp; 5.70&nbsp; &nbsp; 6.82</span></div>
<div><span>swap&nbsp; &nbsp; &nbsp;5.15&nbsp; &nbsp; 7.09&nbsp; &nbsp; 5.55</span></div>
<div><span>db-fg&nbsp; &nbsp;86.83&nbsp; &nbsp;83.89&nbsp; &nbsp;79.43</span></div>
<div><span>db-bg&nbsp; &nbsp; 1.43&nbsp; &nbsp; 3.00&nbsp; &nbsp;~6.x</span></div>
<div><span><br></span></div>
<div><span>&ndash; Limited to db-fg, excludes Postgres because the data is messy</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; MariaDB MySQL</span></div>
<div><span>update&nbsp; 22.39&nbsp; &nbsp;21.49</span></div>
<div><span>insert&nbsp; &nbsp;6.17&nbsp; &nbsp; 5.82</span></div>
<div><span>select&nbsp; 23.43&nbsp; &nbsp;18.04</span></div>
<div><span>commit&nbsp; &nbsp;~4.0&nbsp; &nbsp; ~5.0</span></div>
<div><span>parse&nbsp; &nbsp;~10.0&nbsp; &nbsp;~10.0</span></div>
</div>
<div></div>
<div>
<div><b>Results: flamegraphs without stored procedures</b></div>
<div></div>
<div>The flamegraphs&nbsp;<a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/mar26.sp.pn53.vu1.w100/sp0">are here</a>.</div>
<div></div>
<div>
<div>Summary</div>
<div>
<ul>
<li>the CPU distributions by area are mostly similar for MariaDB and MySQL</li>
<li>Postgres uses 2X to 4X more CPU for background work (vacuum)</li>
</ul>
</div>
</div>
<div>
<div><span>Legend</span></div>
<div>
<div><span>* client &ndash; time in the HammerDB benchmark client</span></div>
<div><span>* swap &ndash; time in kernel swap code</span></div>
<div><span>* db-fg &ndash; time running statements in the DBMS for the client</span></div>
<div><span>* db-bg &ndash; time doing background work in the DBMS</span></div>
</div>
</div>
<div><span><br></span></div>
<div>
<div><span>&ndash; Total</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; MariaDB MySQL&nbsp; &nbsp;Postgres</span></div>
<div><span>client&nbsp; 14.29&nbsp; &nbsp;11.92&nbsp; &nbsp; 7.52</span></div>
<div><span>swap&nbsp; &nbsp; 13.18&nbsp; &nbsp;15.58&nbsp; &nbsp; 5.80</span></div>
<div><span>db-fg&nbsp; &nbsp;70.39&nbsp; &nbsp;70.14&nbsp; &nbsp;77.24</span></div>
<div><span>db-bg&nbsp; &nbsp;~1.0&nbsp; &nbsp; ~2.0&nbsp; &nbsp; &nbsp;6.55</span></div>
<div><span><br></span></div>
<div><span>&ndash; Limited to db-fg, excludes Postgres because the data is messy</span></div>
<div><span>&nbsp; &nbsp; &nbsp; &nbsp; MariaDB MySQL</span></div>
<div><span>update&nbsp; 14.19&nbsp; &nbsp;12.04</span></div>
<div><span>insert&nbsp; &nbsp;4.29&nbsp; &nbsp; 3.57</span></div>
<div><span>select&nbsp; 18.14&nbsp; &nbsp;11.96</span></div>
<div><span>prepare&nbsp; &nbsp;NA&nbsp; &nbsp; &nbsp;6.73</span></div>
<div><span>commit&nbsp; ~2.0&nbsp; &nbsp; &nbsp;2.64</span></div>
<div><span>parse&nbsp; &nbsp; 8.86&nbsp; &nbsp; 9.73</span></div>
<div><span>network 15.47&nbsp; &nbsp;13.73</span></div>
<div></div>
<div>For MySQL parse, 2.5% was from pfs_digest_end_vc and children.</div>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/03/cpu-efficiency-for-mariadb-mysql-and.html">CPU efficiency for MariaDB, MySQL and Postgres on TPROC-C with a small server</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Active-Active MySQL Group Replication Best Practices</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/" />
      <id>https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/</id>
      <updated>2026-03-11T12:54:52+02:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>In MySQL Group Replication (MGR) or Group Replication, an active-active configuration allows multiple group members to accept concurrent write transactions. These writes are coordinated through a consensus-based group communication system (GCS) and validated via write-set certification to preserve global transactional consistency as defined by the Group Replication protocol. active-active mode is designed for write scalability. […]<br />
The post Active-Active MySQL Group Replication Best Practices appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In MySQL Group Replication (MGR) or Group Replication, an active-active configuration allows multiple group members to accept concurrent write transactions. These writes are coordinated through a consensus-based group communication system (GCS) and validated via write-set certification to preserve global transactional consistency as defined by the Group Replication protocol. active-active mode is designed for write scalability.</p>
<p>In this mode, all Group Replication nodes are in PRIMARY state, allowing them to accept read and write traffic. Data modifications or changes will be written to the intended node and replicated across the cluster. Transactions are replicated synchronously. When a transaction conflict is detected, by using certification for those conflicting transactions, it will be resolved automatically. Although powerful, this mode is not intended for <strong>unlimited write scalability</strong>.</p>
<p>Group Replication&rsquo;s scalability is partially dependent on low write conflicts. High contention leads to rollback surge or saturation, ultimately leading to temporary system unavailability due to lock up. Fundamentally, this mode means no passive standby nodes, not;</p>
<ul class="wp-block-list">
<li>Unlimited write scalability</li>
<li>Lock-free writes</li>
<li>No coordination overhead</li>
<li>No single-writer behavior internally</li>
</ul>
<p>This blog post will explore what it means to run a Group Replication setup in production and the operational best practices that you need to follow to ensure its performance and stability.</p>
<h2 class="wp-block-heading"><strong>Note on how MySQL Group Replication manages communication</strong><a class="anchor-link" id="note-on-how-mysql-group-replication-manages-communication"></a></h2>
<p>Let&rsquo;s first take a moment to understand how communication actually works within MySQL Group Replication. The communication layer is the Group Communication System (GCS), which serves as a high-level abstraction responsible for group membership management and total-order message delivery. Its engine is XCom, which implements a Paxos-based consensus protocol to ensure that all members of the group receive the same transactions in the same order. This separation of concerns allows MySQL Group Replication to evolve the underlying communication engine without changing the replication semantics exposed to the server layer.</p>
<p>Understanding how MySQL Group Replication enforces consistency, coordinates writes, and handles failures across the group, it&rsquo;s easier to reason which use cases it is ideal for.</p>
<h2 class="wp-block-heading"><strong>Ideal use cases for active-active MySQL Group Replication</strong><a class="anchor-link" id="ideal-use-cases-for-active-active-mysql-group-replication"></a></h2>
<ul class="wp-block-list">
<li>High Availability (HA) Systems: Critical applications that cannot afford the 10&ndash;30 seconds of downtime typically required for a leader election in single-primary modes.</li>
<li>Low-Conflict Workloads: Applications that write to different parts of the database, especially those architectures that use shards. This performs best, as they avoid the &ldquo;certification&rdquo; failures that occur when two nodes try to update the same record.</li>
<li>Geographically Distributed Reads: While writes are synchronous and can be slow over long distances, having multiple &ldquo;active&rdquo; nodes allows local users to perform reads and writes with lower initial connection latency.</li>
</ul>
<p>Let&rsquo;s now go over the best practices for a MySQL Group Replication deployment&nbsp; setup.</p>
<h2 class="wp-block-heading"><strong>Active-active cluster</strong> <strong>best practices</strong><a class="anchor-link" id="active-active-cluster-best-practices"></a></h2>
<h3 class="wp-block-heading"><strong>Determining the number of database member nodes in the cluster</strong><a class="anchor-link" id="determining-the-number-of-database-member-nodes-in-the-cluster"></a></h3>
<p>The number of instances should be determined by expected failure scenarios rather than capacity requirements. According to MySQL documentation, Group Replication requires a majority of members to commit transactions, making quorum size critical to availability.</p>
<p>Network partitions and node failures are inevitable. Using an odd number of instances ensures that a majority can still be formed during partial outages. Choosing an odd number of members improves write availability and aligns with MySQL Group Replication&rsquo;s quorum model. For example, a five-node cluster can continue processing writes even if two nodes become unreachable, while an evenly sized cluster risks entering a write-blocked state during a symmetric split.</p>
<h3 class="wp-block-heading"><strong>Splitting read and write traffic</strong><a class="anchor-link" id="splitting-read-and-write-traffic"></a></h3>
<p>In an active-active MySQL Group Replication setup, write operations place significantly more stress on the system than reads, especially when they result in heavy disk activity. Writes consume CPU, generate redo and binary logs, trigger replication traffic, and must be applied consistently across the group, amplifying their impact across the cluster.</p>
<p>Separating write traffic helps prevent sustained write workloads from overwhelming the cluster. By directing writes to nodes with sufficient capacity and lower contention while allowing others to focus on serving reads, the cluster maintains more stable performance, reduces write related bottlenecks, and scales read workloads more effectively.</p>
<h3 class="wp-block-heading"><strong>Connection pooling</strong><a class="anchor-link" id="connection-pooling"></a></h3>
<p>Frequent connection creation and teardown can quickly become a bottleneck and place unnecessary stress on the cluster. Maintaining a pool of reusable connections helps stabilize workload distribution and prevents spikes in CPU and memory usage caused by excessive connection overhead.</p>
<p>Using proxies such as MySQL Router or ProxySQL allows applications to reuse existing connections while intelligently routing traffic across group members, reducing connection overhead, improving response times, and helping the cluster handle fluctuating workloads more predictably.<br>Error handling</p>
<p>When using active-active MySQL Group Replication, applications must be prepared to handle transient failures. Network interruptions, node restarts, or membership changes can temporarily cause connection errors or transaction failures, even when the cluster itself is healthy.</p>
<p>Implementing controlled failback handling at the application layer allows temporary disruptions to resolve naturally before user impact. Likewise, <strong>well designed</strong> error handling improves application reliability, protects data consistency, and ensures that temporary replication events have the opportunity to resolve naturally before escalating to visible outages. Short-lived connection or transaction failures can often succeed once the system stabilizes, especially during membership changes or brief network interruptions.</p>
<h3 class="wp-block-heading"><strong>Transaction size</strong><a class="anchor-link" id="transaction-size"></a></h3>
<p>Before deploying MySQL Group Replication in production, understand the transaction sizes generated by your application. MGR enforces a maximum transaction size using the <a href="blank">group_replication_transaction_size_limit</a> parameter, directly affecting replication latency and stability.</p>
<p>If you set a small transaction, then it&rsquo;ll replicate efficiently making it suitable for OLTP workloads. Large batch operations usually increase memory usage, network traffic, and pressure on the replica when applying the transactions. However, setting the limit too low can cause valid transactions to fail, while setting it too high can lead to replication lag or resource exhaustion.&nbsp;</p>
<p>Instead of raising the limit on memory constrained instances, large data changes should be split into smaller batches. <strong>Take note</strong>, this setting must be consistent across all group members.</p>
<h3 class="wp-block-heading"><strong>Strict consistency checks</strong><a class="anchor-link" id="strict-consistency-checks"></a></h3>
<p>MySQL Group Replication provides the <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_enforce_update_everywhere_checks">group_replication_enforce_update_everywhere_checks</a> variable, which is disabled by default. This system variable is a group-wide configuration setting. It must have the same value on all group members, cannot be changed while Group Replication is running, and requires a full reboot of the group (a bootstrap by a server with <code>group_replication_bootstrap_group=ON</code>) in order for the value change to take effect.</p>
<p>When disabled, applications must ensure that conflicting transactions are not executed concurrently on different nodes, which requires thorough testing and strict control over write paths, especially in schemas involving foreign keys, cascading operations, or concurrent modifications across tables. When enabled, statements are checked as follows to ensure their compatibility with multi-primary mode:</p>
<ol class="wp-block-list">
<li>If a transaction is executed under the SERIALIZABLE isolation level, then its commit fails when synchronizing itself with the group.</li>
<li>If a transaction executes against a table that has foreign keys with cascading constraints, then the transaction fails to commit when synchronizing itself with the group.</li>
</ol>
<p>In short, enable it when:</p>
<ul class="wp-block-list">
<li>You want strict consistency checks, AND
<ul class="wp-block-list">
<li>You don&rsquo;t rely on SERIALIZABLE isolation level</li>
<li>Your tables do not rely on foreign key checks with cascading constraints</li>
<li>You prefer failed transactions over application-managed conflict handling</li>
<li>Correctness is more important than write flexibility</li>
</ul>
</li>
</ul>
<p>Disable it when:</p>
<ul class="wp-block-list">
<li>Your schema relies on cascading foreign key constraints</li>
<li>Cascades are part of normal application behavior</li>
<li>You accept responsibility for preventing conflicting writes</li>
<li>Write paths are tightly controlled or serialized at the application level</li>
</ul>
<h3 class="wp-block-heading"><strong>Failure detection parameters</strong><a class="anchor-link" id="failure-detection-parameters"></a></h3>
<p>There are three failure detection parameters that you must adjust to define your application&rsquo;s failure tolerance:</p>
<ul class="wp-block-list">
<li><code>group_replication_member_expel_timeout</code>: This variable controls how long a member is audited before it is expelled from the group. The latest version&rsquo;s (8.4 as of this writing) default is 5 seconds. Therefore, Group Replication first detects a suspected failure after ~5 seconds (no messages), waiting x <code>group_replication_member_expel_timeout</code> seconds more before expelling the member.</li>
</ul>
<p>During the waiting period, the suspected node is listed as UNREACHABLE, but still part of the group view. If it resumes communication before expulsion, it rejoins without operator intervention. When you need to tune this variable depends on your needs.&nbsp;</p>
<p><strong>When to tune: </strong>If you have intermittent network blips or slow links, it makes sense to increase the default value to avoid unnecessary expulsions. Decrease only if you prefer faster failure detection, e.g. frequent real outages and stable networks, setting it to 0 only for immediate post-detection expulsion.</p>
<ul class="wp-block-list">
<li><code>group_replication_autorejoin_tries</code>: Set this to the desired number of automatic rejoin attempts a member makes after being expelled or losing quorum. Set to 3 attempts (with roughly 5 minutes of waiting time between attempts) by default, the member will try to rejoin the group automatically after expulsion or network isolation. If all attempts fail, it stops and follows the exit action. During and between auto-rejoin attempts, a DB instance remains in read-only mode and does not accept writes, thereby increasing the likelihood of stale reads over time.</li>
</ul>
<p><strong>When to tune:</strong> Increase this variable for environments with frequently long, but temporary network partitions. Decrease if you want to restrict rejoin attempts, or expedite identifying DB instances that require manual intervention. Otherwise, set it to 0 if you want to handle rejoining manually or your applications cannot tolerate the possibility of stale reads for any period of time. <strong>Common practice in highly available clusters:</strong> keep several tries to allow network recovery without admin intervention.</p>
<ul class="wp-block-list">
<li><code>group_replication_unreachable_majority_timeout</code>: Timeout used when a member is in the minority (cannot reach a majority or establish a quorum) before&nbsp; declaring a lost quorum. Set to 0 by default, a member node enters a special unreachable majority state if it cannot contact a majority of the group. After this timeout expires, actions such as expulsion or exit actions are applied. Setting this value higher than 0 helps to prevent a minority partition from running indefinitely or making unsafe decisions.</li>
</ul>
<p>Otherwise, when the defined timeout is reached, all pending transactions on the minority are rolled back, and the DB instances in the minority partition are moved to the ERROR state. From there, your application can perform error-handling as needed.</p>
<p><strong>When to tune: </strong>Increase it for deployments where temporary connectivity loss to a majority is expected but resolves soon. Otherwise, decrease for faster detection of actual partition and to avoid split-brain risk.</p>
<ul class="wp-block-list">
<li><code>group_replication_exit_state_action</code>: Defines how a member behaves when the server leaves the group unintentionally, for example, after encountering an applier error, or in the case of a majority loss, or when another member expels it due to a suspicion time out. Note that an expelled group member does not know that it was expelled until it reconnects to the group, so the specified action is only taken if the member manages to reconnect, or if it raises a suspicion on itself and self-expels. Current values available for you to set are ABORT_SERVER, OFFLINE_MODE, and READ_ONLY.&nbsp;</li>
</ul>
<p><strong>When to tune: </strong>Tune if you need strict consistency, then choose shutdown on failure to avoid stale reads or split-brain risk. For read-heavy clusters where full availability matters, prefer read-only or offline modes.</p>
<h3 class="wp-block-heading"><strong>Flow control</strong><a class="anchor-link" id="flow-control"></a></h3>
<p>In MGR, a transaction is only finalized once a majority of members agree on its global order. In an active-active setup, this coordination is sensitive to uneven performance: fast writers can easily outrun slower members, causing replication lag and in-memory backlog.</p>
<p>Flow control is the mechanism that keeps the group balanced. It monitors how far members fall behind in both transaction certification and apply phases, and temporarily throttles write throughput across all primaries when predefined limits are exceeded. The key controls are <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_certifier_threshold">group_replication_flow_control_certifier_threshold</a> and <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_applier_threshold">group_replication_flow_control_applier_threshold,</a> which define how much backlog the group is willing to tolerate before throttling.</p>
<p>Throughput recovery is governed by quota-based settings such as <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_max_quota">group_replication_flow_control_max_quota</a>, which caps how quickly write capacity is restored once lag subsides. The overall behavior is enabled or disabled via <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_mode">group_replication_flow_control_mode</a>, though disabling flow control is generally discouraged in production due to the increased risk of memory exhaustion and instability.</p>
<p>Rather than turning flow control off, a better strategy is to tune these thresholds alongside sufficient parallel applier capacity, so throttling only occurs under real pressure. Continuous monitoring of replication lag and backlog is essential, as optimal values depend heavily on transaction size, write bursts, and workload patterns in active-active environments.</p>
<h3 class="wp-block-heading"><strong>Transaction consistency</strong><a class="anchor-link" id="transaction-consistency"></a></h3>
<p>MySQL Group Replication provides a parameter <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_consistency">group_replication_consistency</a> on which you can set values of the following options: EVENTUAL, BEFORE, AFTER, and BEFORE_AND_AFTER.</p>
<p>BEFORE leads to a sweet spot which offers strong enough consistency for correctness and light enough synchronization for performance and is usually the best balance for production environments. Before executing a transaction, every member has to wait until it has applied all transactions that were already globally ordered, ensuring reads and writes are based on a reasonably current view of the group, which then helps prevent obvious stale reads, reduces write-write conflicts, avoids unnecessary waiting on future transactions, and keeps latency acceptable under normal load.</p>
<p>It is also highly recommended to monitor the replication status of your MGR cluster nodes. You might use <code>performance_schema</code> tables to monitor the health of your active-active cluster.</p>
<ul class="wp-block-list">
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-replication-group-members.html">performance_schema.replication_group_members</a>: Provides the status of each DB instance that is part of the cluster.</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-replication-group-member-stats.html">performance_schema.replication_group_member_stats</a>: Provides cluster-level information related to certification, as well as statistics for the transactions received and originated by each DB instance in the cluster.</li>
<li><a href="https://dev.mysql.com/doc/mysql-perfschema-excerpt/5.7/en/performance-schema-replication-connection-status-table.html">performance_schema.replication_connection_status</a>: Provides the current status of the replication I/O thread that handles the replica&rsquo;s connection to the source DB instance.</li>
</ul>
<h2 class="wp-block-heading"><strong>Deploying a MySQL Group Replication using ClusterControl</strong><a class="anchor-link" id="deploying-a-mysql-group-replication-using-clustercontrol"></a></h2>
<p>By default, deploying through ClusterControl will setup an active MySQL Group Replication cluster. This means that all nodes in the cluster are available to receive write requests. Below are the steps to deploy using ClusterControl.</p>
<p>First, login to your ClusterControl with your administrator username/password credentials. Once you are able to login, click the <em>Deploy a cluster </em>button in the right corner of the UI. This will launch the deployment wizard, starting with a create / import cluster prompt.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="840" src="https://severalnines.com/wp-content/uploads/2025/09/CC_db_deployment_wizard-creation_type_selection-1024x840.png" alt="" class="wp-image-41547"></figure>
<p>Choose <em>Create a database cluster</em>. Next, you will choose which cluster to deploy. Select <em>MySQL Group Replication</em> in the database drop-down section. As of this writing, Oracle is the only supported vendor and versions 8.0 and 8.4 &mdash; see the screenshot below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="835" src="https://severalnines.com/wp-content/uploads/2026/02/cc_db_deployment_wizard-database_selection-mysql_group_replication-1024x835.png" alt="" class="wp-image-42713"></figure>
<p>After you hit the <em>Continue</em> button, you will start the straightforward deployment workflow in earnest. You can also follow our user guide based on our documentation by clicking <a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/#mysql-group-replication">here</a>.</p>
<p>The final step is to review your deployment configuration as illustrated below; if all looks good, hit the <em>Finish</em><strong> </strong>button to initiate the deployment and ClusterControl will do the rest. </p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="979" height="1024" src="https://severalnines.com/wp-content/uploads/2026/02/cc_db_deployment_wizard-deployment_preview-mysql_group_replication-979x1024.png" alt="" class="wp-image-42716"></figure>
<p>Once started, you can view the stepwise deployment process logs as illustrated below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="930" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-job_progress_detail-mgr_deployment-1024x930.png" alt="" class="wp-image-42714"></figure>
<p>Otherwise, you can view the deployment process&rsquo;s progress bar in the UI&rsquo;s foreground:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="179" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-db_deployment_progress-mgr-1024x179.png" alt="" class="wp-image-42715"></figure>
<p>When the deployment workflow finishes, the cluster will be shown within your <em>Clusters</em> tab &mdash; clicking the cluster&rsquo;s name, you can view its dashboards, topology, node list, performance graphs, backups, logs, and more. Monitoring your cluster using dashboards and other ClusterControl features will render analyzing and inspecting your cluster&rsquo;s health trivial.</p>
<h2 class="wp-block-heading"><strong>Conclusion</strong><a class="anchor-link" id="conclusion"></a></h2>
<p>MySQL Group Replication is a strong choice for high availability and multi-writer flexibility, but <strong>it is not</strong> a path to unlimited write scalability. It replaces the simplicity of a single-writer model with the complexity of distributed coordination.</p>
<p>A successful active/active deployment depends on deliberate design, how familiar you are with Group Replication, as well as your application&rsquo;s behavior, data access patterns, and operational practices and underlying distributed systems.</p>
<p>It may be best to start with Single-Primary Group Replication then move to active-active once you have a full understanding of the write patterns, the application&rsquo;s setup and design for conflict handling, and have tested failure and conflict scenarios extensively. When applied to the right use cases and deliberately run, active-active MySQL Group Replication delivers resilient, predictable production performance.</p>
<p>Ready to implement an active-active MGR cluster efficiently and reliably in any environment?</p>
<h2 class="wp-block-heading"><strong>Install ClusterControl in 10-minutes.&nbsp;Free 30-day&nbsp;Enterprise trial included!</strong><a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading"><strong>Script installation instructions</strong><a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading"><strong>Other installation options</strong></h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading"><strong>ClusterControl on marketplaces</strong></h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<ul class="wp-block-list">
<li><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></li>
<li><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></li>
<li><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></li>
<li><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></li>
<li><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></li>
</ul>
<p>The post <a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Active-Active MySQL Group Replication Best Practices</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/" />
      <id>https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/</id>
      <updated>2026-03-11T12:54:52+02:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>In MySQL Group Replication (MGR) or Group Replication, an active-active configuration allows multiple group members to accept concurrent write transactions. These writes are coordinated through a consensus-based group communication system (GCS) and validated via write-set certification to preserve global transactional consistency as defined by the Group Replication protocol. active-active mode is designed for write scalability. […]<br />
The post Active-Active MySQL Group Replication Best Practices appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In MySQL Group Replication (MGR) or Group Replication, an active-active configuration allows multiple group members to accept concurrent write transactions. These writes are coordinated through a consensus-based group communication system (GCS) and validated via write-set certification to preserve global transactional consistency as defined by the Group Replication protocol. active-active mode is designed for write scalability.</p>
<p>In this mode, all Group Replication nodes are in PRIMARY state, allowing them to accept read and write traffic. Data modifications or changes will be written to the intended node and replicated across the cluster. Transactions are replicated synchronously. When a transaction conflict is detected, by using certification for those conflicting transactions, it will be resolved automatically. Although powerful, this mode is not intended for <strong>unlimited write scalability</strong>.</p>
<p>Group Replication&rsquo;s scalability is partially dependent on low write conflicts. High contention leads to rollback surge or saturation, ultimately leading to temporary system unavailability due to lock up. Fundamentally, this mode means no passive standby nodes, not;</p>
<ul class="wp-block-list">
<li>Unlimited write scalability</li>
<li>Lock-free writes</li>
<li>No coordination overhead</li>
<li>No single-writer behavior internally</li>
</ul>
<p>This blog post will explore what it means to run a Group Replication setup in production and the operational best practices that you need to follow to ensure its performance and stability.</p>
<h2 class="wp-block-heading"><strong>Note on how MySQL Group Replication manages communication</strong><a class="anchor-link" id="note-on-how-mysql-group-replication-manages-communication"></a></h2>
<p>Let&rsquo;s first take a moment to understand how communication actually works within MySQL Group Replication. The communication layer is the Group Communication System (GCS), which serves as a high-level abstraction responsible for group membership management and total-order message delivery. Its engine is XCom, which implements a Paxos-based consensus protocol to ensure that all members of the group receive the same transactions in the same order. This separation of concerns allows MySQL Group Replication to evolve the underlying communication engine without changing the replication semantics exposed to the server layer.</p>
<p>Understanding how MySQL Group Replication enforces consistency, coordinates writes, and handles failures across the group, it&rsquo;s easier to reason which use cases it is ideal for.</p>
<h2 class="wp-block-heading"><strong>Ideal use cases for active-active MySQL Group Replication</strong><a class="anchor-link" id="ideal-use-cases-for-active-active-mysql-group-replication"></a></h2>
<ul class="wp-block-list">
<li>High Availability (HA) Systems: Critical applications that cannot afford the 10&ndash;30 seconds of downtime typically required for a leader election in single-primary modes.</li>
<li>Low-Conflict Workloads: Applications that write to different parts of the database, especially those architectures that use shards. This performs best, as they avoid the &ldquo;certification&rdquo; failures that occur when two nodes try to update the same record.</li>
<li>Geographically Distributed Reads: While writes are synchronous and can be slow over long distances, having multiple &ldquo;active&rdquo; nodes allows local users to perform reads and writes with lower initial connection latency.</li>
</ul>
<p>Let&rsquo;s now go over the best practices for a MySQL Group Replication deployment&nbsp; setup.</p>
<h2 class="wp-block-heading"><strong>Active-active cluster</strong> <strong>best practices</strong><a class="anchor-link" id="active-active-cluster-best-practices"></a></h2>
<h3 class="wp-block-heading"><strong>Determining the number of database member nodes in the cluster</strong><a class="anchor-link" id="determining-the-number-of-database-member-nodes-in-the-cluster"></a></h3>
<p>The number of instances should be determined by expected failure scenarios rather than capacity requirements. According to MySQL documentation, Group Replication requires a majority of members to commit transactions, making quorum size critical to availability.</p>
<p>Network partitions and node failures are inevitable. Using an odd number of instances ensures that a majority can still be formed during partial outages. Choosing an odd number of members improves write availability and aligns with MySQL Group Replication&rsquo;s quorum model. For example, a five-node cluster can continue processing writes even if two nodes become unreachable, while an evenly sized cluster risks entering a write-blocked state during a symmetric split.</p>
<h3 class="wp-block-heading"><strong>Splitting read and write traffic</strong><a class="anchor-link" id="splitting-read-and-write-traffic"></a></h3>
<p>In an active-active MySQL Group Replication setup, write operations place significantly more stress on the system than reads, especially when they result in heavy disk activity. Writes consume CPU, generate redo and binary logs, trigger replication traffic, and must be applied consistently across the group, amplifying their impact across the cluster.</p>
<p>Separating write traffic helps prevent sustained write workloads from overwhelming the cluster. By directing writes to nodes with sufficient capacity and lower contention while allowing others to focus on serving reads, the cluster maintains more stable performance, reduces write related bottlenecks, and scales read workloads more effectively.</p>
<h3 class="wp-block-heading"><strong>Connection pooling</strong><a class="anchor-link" id="connection-pooling"></a></h3>
<p>Frequent connection creation and teardown can quickly become a bottleneck and place unnecessary stress on the cluster. Maintaining a pool of reusable connections helps stabilize workload distribution and prevents spikes in CPU and memory usage caused by excessive connection overhead.</p>
<p>Using proxies such as MySQL Router or ProxySQL allows applications to reuse existing connections while intelligently routing traffic across group members, reducing connection overhead, improving response times, and helping the cluster handle fluctuating workloads more predictably.<br>Error handling</p>
<p>When using active-active MySQL Group Replication, applications must be prepared to handle transient failures. Network interruptions, node restarts, or membership changes can temporarily cause connection errors or transaction failures, even when the cluster itself is healthy.</p>
<p>Implementing controlled failback handling at the application layer allows temporary disruptions to resolve naturally before user impact. Likewise, <strong>well designed</strong> error handling improves application reliability, protects data consistency, and ensures that temporary replication events have the opportunity to resolve naturally before escalating to visible outages. Short-lived connection or transaction failures can often succeed once the system stabilizes, especially during membership changes or brief network interruptions.</p>
<h3 class="wp-block-heading"><strong>Transaction size</strong><a class="anchor-link" id="transaction-size"></a></h3>
<p>Before deploying MySQL Group Replication in production, understand the transaction sizes generated by your application. MGR enforces a maximum transaction size using the <a href="blank">group_replication_transaction_size_limit</a> parameter, directly affecting replication latency and stability.</p>
<p>If you set a small transaction, then it&rsquo;ll replicate efficiently making it suitable for OLTP workloads. Large batch operations usually increase memory usage, network traffic, and pressure on the replica when applying the transactions. However, setting the limit too low can cause valid transactions to fail, while setting it too high can lead to replication lag or resource exhaustion.&nbsp;</p>
<p>Instead of raising the limit on memory constrained instances, large data changes should be split into smaller batches. <strong>Take note</strong>, this setting must be consistent across all group members.</p>
<h3 class="wp-block-heading"><strong>Strict consistency checks</strong><a class="anchor-link" id="strict-consistency-checks"></a></h3>
<p>MySQL Group Replication provides the <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_enforce_update_everywhere_checks">group_replication_enforce_update_everywhere_checks</a> variable, which is disabled by default. This system variable is a group-wide configuration setting. It must have the same value on all group members, cannot be changed while Group Replication is running, and requires a full reboot of the group (a bootstrap by a server with <code>group_replication_bootstrap_group=ON</code>) in order for the value change to take effect.</p>
<p>When disabled, applications must ensure that conflicting transactions are not executed concurrently on different nodes, which requires thorough testing and strict control over write paths, especially in schemas involving foreign keys, cascading operations, or concurrent modifications across tables. When enabled, statements are checked as follows to ensure their compatibility with multi-primary mode:</p>
<ol class="wp-block-list">
<li>If a transaction is executed under the SERIALIZABLE isolation level, then its commit fails when synchronizing itself with the group.</li>
<li>If a transaction executes against a table that has foreign keys with cascading constraints, then the transaction fails to commit when synchronizing itself with the group.</li>
</ol>
<p>In short, enable it when:</p>
<ul class="wp-block-list">
<li>You want strict consistency checks, AND
<ul class="wp-block-list">
<li>You don&rsquo;t rely on SERIALIZABLE isolation level</li>
<li>Your tables do not rely on foreign key checks with cascading constraints</li>
<li>You prefer failed transactions over application-managed conflict handling</li>
<li>Correctness is more important than write flexibility</li>
</ul>
</li>
</ul>
<p>Disable it when:</p>
<ul class="wp-block-list">
<li>Your schema relies on cascading foreign key constraints</li>
<li>Cascades are part of normal application behavior</li>
<li>You accept responsibility for preventing conflicting writes</li>
<li>Write paths are tightly controlled or serialized at the application level</li>
</ul>
<h3 class="wp-block-heading"><strong>Failure detection parameters</strong><a class="anchor-link" id="failure-detection-parameters"></a></h3>
<p>There are three failure detection parameters that you must adjust to define your application&rsquo;s failure tolerance:</p>
<ul class="wp-block-list">
<li><code>group_replication_member_expel_timeout</code>: This variable controls how long a member is audited before it is expelled from the group. The latest version&rsquo;s (8.4 as of this writing) default is 5 seconds. Therefore, Group Replication first detects a suspected failure after ~5 seconds (no messages), waiting x <code>group_replication_member_expel_timeout</code> seconds more before expelling the member.</li>
</ul>
<p>During the waiting period, the suspected node is listed as UNREACHABLE, but still part of the group view. If it resumes communication before expulsion, it rejoins without operator intervention. When you need to tune this variable depends on your needs.&nbsp;</p>
<p><strong>When to tune: </strong>If you have intermittent network blips or slow links, it makes sense to increase the default value to avoid unnecessary expulsions. Decrease only if you prefer faster failure detection, e.g. frequent real outages and stable networks, setting it to 0 only for immediate post-detection expulsion.</p>
<ul class="wp-block-list">
<li><code>group_replication_autorejoin_tries</code>: Set this to the desired number of automatic rejoin attempts a member makes after being expelled or losing quorum. Set to 3 attempts (with roughly 5 minutes of waiting time between attempts) by default, the member will try to rejoin the group automatically after expulsion or network isolation. If all attempts fail, it stops and follows the exit action. During and between auto-rejoin attempts, a DB instance remains in read-only mode and does not accept writes, thereby increasing the likelihood of stale reads over time.</li>
</ul>
<p><strong>When to tune:</strong> Increase this variable for environments with frequently long, but temporary network partitions. Decrease if you want to restrict rejoin attempts, or expedite identifying DB instances that require manual intervention. Otherwise, set it to 0 if you want to handle rejoining manually or your applications cannot tolerate the possibility of stale reads for any period of time. <strong>Common practice in highly available clusters:</strong> keep several tries to allow network recovery without admin intervention.</p>
<ul class="wp-block-list">
<li><code>group_replication_unreachable_majority_timeout</code>: Timeout used when a member is in the minority (cannot reach a majority or establish a quorum) before&nbsp; declaring a lost quorum. Set to 0 by default, a member node enters a special unreachable majority state if it cannot contact a majority of the group. After this timeout expires, actions such as expulsion or exit actions are applied. Setting this value higher than 0 helps to prevent a minority partition from running indefinitely or making unsafe decisions.</li>
</ul>
<p>Otherwise, when the defined timeout is reached, all pending transactions on the minority are rolled back, and the DB instances in the minority partition are moved to the ERROR state. From there, your application can perform error-handling as needed.</p>
<p><strong>When to tune: </strong>Increase it for deployments where temporary connectivity loss to a majority is expected but resolves soon. Otherwise, decrease for faster detection of actual partition and to avoid split-brain risk.</p>
<ul class="wp-block-list">
<li><code>group_replication_exit_state_action</code>: Defines how a member behaves when the server leaves the group unintentionally, for example, after encountering an applier error, or in the case of a majority loss, or when another member expels it due to a suspicion time out. Note that an expelled group member does not know that it was expelled until it reconnects to the group, so the specified action is only taken if the member manages to reconnect, or if it raises a suspicion on itself and self-expels. Current values available for you to set are ABORT_SERVER, OFFLINE_MODE, and READ_ONLY.&nbsp;</li>
</ul>
<p><strong>When to tune: </strong>Tune if you need strict consistency, then choose shutdown on failure to avoid stale reads or split-brain risk. For read-heavy clusters where full availability matters, prefer read-only or offline modes.</p>
<h3 class="wp-block-heading"><strong>Flow control</strong><a class="anchor-link" id="flow-control"></a></h3>
<p>In MGR, a transaction is only finalized once a majority of members agree on its global order. In an active-active setup, this coordination is sensitive to uneven performance: fast writers can easily outrun slower members, causing replication lag and in-memory backlog.</p>
<p>Flow control is the mechanism that keeps the group balanced. It monitors how far members fall behind in both transaction certification and apply phases, and temporarily throttles write throughput across all primaries when predefined limits are exceeded. The key controls are <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_certifier_threshold">group_replication_flow_control_certifier_threshold</a> and <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_applier_threshold">group_replication_flow_control_applier_threshold,</a> which define how much backlog the group is willing to tolerate before throttling.</p>
<p>Throughput recovery is governed by quota-based settings such as <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_max_quota">group_replication_flow_control_max_quota</a>, which caps how quickly write capacity is restored once lag subsides. The overall behavior is enabled or disabled via <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_flow_control_mode">group_replication_flow_control_mode</a>, though disabling flow control is generally discouraged in production due to the increased risk of memory exhaustion and instability.</p>
<p>Rather than turning flow control off, a better strategy is to tune these thresholds alongside sufficient parallel applier capacity, so throttling only occurs under real pressure. Continuous monitoring of replication lag and backlog is essential, as optimal values depend heavily on transaction size, write bursts, and workload patterns in active-active environments.</p>
<h3 class="wp-block-heading"><strong>Transaction consistency</strong><a class="anchor-link" id="transaction-consistency"></a></h3>
<p>MySQL Group Replication provides a parameter <a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-system-variables.html#sysvar_group_replication_consistency">group_replication_consistency</a> on which you can set values of the following options: EVENTUAL, BEFORE, AFTER, and BEFORE_AND_AFTER.</p>
<p>BEFORE leads to a sweet spot which offers strong enough consistency for correctness and light enough synchronization for performance and is usually the best balance for production environments. Before executing a transaction, every member has to wait until it has applied all transactions that were already globally ordered, ensuring reads and writes are based on a reasonably current view of the group, which then helps prevent obvious stale reads, reduces write-write conflicts, avoids unnecessary waiting on future transactions, and keeps latency acceptable under normal load.</p>
<p>It is also highly recommended to monitor the replication status of your MGR cluster nodes. You might use <code>performance_schema</code> tables to monitor the health of your active-active cluster.</p>
<ul class="wp-block-list">
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-replication-group-members.html">performance_schema.replication_group_members</a>: Provides the status of each DB instance that is part of the cluster.</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/group-replication-replication-group-member-stats.html">performance_schema.replication_group_member_stats</a>: Provides cluster-level information related to certification, as well as statistics for the transactions received and originated by each DB instance in the cluster.</li>
<li><a href="https://dev.mysql.com/doc/mysql-perfschema-excerpt/5.7/en/performance-schema-replication-connection-status-table.html">performance_schema.replication_connection_status</a>: Provides the current status of the replication I/O thread that handles the replica&rsquo;s connection to the source DB instance.</li>
</ul>
<h2 class="wp-block-heading"><strong>Deploying a MySQL Group Replication using ClusterControl</strong><a class="anchor-link" id="deploying-a-mysql-group-replication-using-clustercontrol"></a></h2>
<p>By default, deploying through ClusterControl will setup an active MySQL Group Replication cluster. This means that all nodes in the cluster are available to receive write requests. Below are the steps to deploy using ClusterControl.</p>
<p>First, login to your ClusterControl with your administrator username/password credentials. Once you are able to login, click the <em>Deploy a cluster </em>button in the right corner of the UI. This will launch the deployment wizard, starting with a create / import cluster prompt.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="840" src="https://severalnines.com/wp-content/uploads/2025/09/CC_db_deployment_wizard-creation_type_selection-1024x840.png" alt="" class="wp-image-41547"></figure>
<p>Choose <em>Create a database cluster</em>. Next, you will choose which cluster to deploy. Select <em>MySQL Group Replication</em> in the database drop-down section. As of this writing, Oracle is the only supported vendor and versions 8.0 and 8.4 &mdash; see the screenshot below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="835" src="https://severalnines.com/wp-content/uploads/2026/02/cc_db_deployment_wizard-database_selection-mysql_group_replication-1024x835.png" alt="" class="wp-image-42713"></figure>
<p>After you hit the <em>Continue</em> button, you will start the straightforward deployment workflow in earnest. You can also follow our user guide based on our documentation by clicking <a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/#mysql-group-replication">here</a>.</p>
<p>The final step is to review your deployment configuration as illustrated below; if all looks good, hit the <em>Finish</em><strong> </strong>button to initiate the deployment and ClusterControl will do the rest. </p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="979" height="1024" src="https://severalnines.com/wp-content/uploads/2026/02/cc_db_deployment_wizard-deployment_preview-mysql_group_replication-979x1024.png" alt="" class="wp-image-42716"></figure>
<p>Once started, you can view the stepwise deployment process logs as illustrated below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="930" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-job_progress_detail-mgr_deployment-1024x930.png" alt="" class="wp-image-42714"></figure>
<p>Otherwise, you can view the deployment process&rsquo;s progress bar in the UI&rsquo;s foreground:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="179" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-db_deployment_progress-mgr-1024x179.png" alt="" class="wp-image-42715"></figure>
<p>When the deployment workflow finishes, the cluster will be shown within your <em>Clusters</em> tab &mdash; clicking the cluster&rsquo;s name, you can view its dashboards, topology, node list, performance graphs, backups, logs, and more. Monitoring your cluster using dashboards and other ClusterControl features will render analyzing and inspecting your cluster&rsquo;s health trivial.</p>
<h2 class="wp-block-heading"><strong>Conclusion</strong><a class="anchor-link" id="conclusion"></a></h2>
<p>MySQL Group Replication is a strong choice for high availability and multi-writer flexibility, but <strong>it is not</strong> a path to unlimited write scalability. It replaces the simplicity of a single-writer model with the complexity of distributed coordination.</p>
<p>A successful active/active deployment depends on deliberate design, how familiar you are with Group Replication, as well as your application&rsquo;s behavior, data access patterns, and operational practices and underlying distributed systems.</p>
<p>It may be best to start with Single-Primary Group Replication then move to active-active once you have a full understanding of the write patterns, the application&rsquo;s setup and design for conflict handling, and have tested failure and conflict scenarios extensively. When applied to the right use cases and deliberately run, active-active MySQL Group Replication delivers resilient, predictable production performance.</p>
<p>Ready to implement an active-active MGR cluster efficiently and reliably in any environment?</p>
<h2 class="wp-block-heading"><strong>Install ClusterControl in 10-minutes.&nbsp;Free 30-day&nbsp;Enterprise trial included!</strong><a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading"><strong>Script installation instructions</strong><a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading"><strong>Other installation options</strong></h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading"><strong>ClusterControl on marketplaces</strong></h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<ul class="wp-block-list">
<li><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></li>
<li><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></li>
<li><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></li>
<li><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></li>
<li><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></li>
</ul>
<p>The post <a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/active-active-mysql-group-replication-best-practices/">Active-Active MySQL Group Replication Best Practices</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Running pgBackRest with pg_tde: A Practical Percona Walkthrough</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/03/10/running-pgbackrest-with-pg_tde-a-practical-percona-walkthrough/" />
      <id>https://percona.community/blog/2026/03/10/running-pgbackrest-with-pg_tde-a-practical-percona-walkthrough/</id>
      <updated>2026-03-10T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Not every PostgreSQL installation requires encryption at rest. However, for organizations mandating strict data protection and privacy standards, it is often non-negotiable. When security policies are this rigorous, you need a strategy that protects your data without sacrificing recoverability.</p>
<p><a href="https://percona.community/blog/2026/03/10/running-pgbackrest-with-pg_tde-a-practical-percona-walkthrough/">Running pgBackRest with pg_tde: A Practical Percona Walkthrough</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Not every PostgreSQL installation requires encryption at rest. However, for organizations mandating strict data protection and privacy standards, it is often non-negotiable. When security policies are this rigorous, you need a strategy that protects your data without sacrificing recoverability.</p>
<p>While Transparent Data Encryption (TDE) successfully locks down your data at rest, it raises a critical operational question: will your backup and restore workflow continue to work correctly with encrypted data? When deploying Transparent Data Encryption, it is essential to validate that pgBackRest can reliably back up and restore encrypted clusters.</p>
<p>This post walks through a complete, step-by-step configuration of pg_tde with pgBackRest on Debian/Ubuntu. We will explore how to optimize your backup performance, secure your repository, and most importantly verify that your encrypted backup restores work exactly as expected.</p>
<h2>What is pg_tde?<a class="anchor-link" id="what-is-pg_tde"></a></h2>
<p>Percona&rsquo;s solution for transparent data encryption (<a href="https://docs.percona.com/pg-tde/index.html" target="_blank" rel="noopener noreferrer">pg_tde</a>) is an open source, community driven extension that provides Transparent Data Encryption (TDE) for PostgreSQL. This extension allows data to be encrypted at the storage level without affecting application behavior.</p>
<p>Unlike full disk encryption, which exposes data once the system boots, TDE ensures that the actual database files remain encrypted at the file system level. This protects your data, dumps, and backups even if the operating system is compromised. Currently, it is bundled with Percona Server for PostgreSQL and available in Percona Distribution for PostgreSQL 17+.</p>
<p>Recent Percona releases make this combination more practical than ever. With WAL encryption now ready for production use, we need a backup strategy that respects data security. In this walkthrough, we will demonstrate how to pair it with pgBackRest to ensure fully recoverable, encrypted backups.</p>
<h2>The Use Case<a class="anchor-link" id="the-use-case"></a></h2>
<p>Imagine a team that wants strong security controls without changing application code. They need:</p>
<ul>
<li>Data files encrypted at rest (tables and WAL)</li>
<li>Backups that are consistent, verifiable, and restorable</li>
<li>A setup that is easy to automate and explain</li>
</ul>
<p>Percona Distribution for PostgreSQL plus pg_tde and pgBackRest closes the gaps where needed: pg_tde takes care of encryption, pgBackRest provides flexible backup/restore capabilities.</p>
<h2>A Quick Note on Compatibility<a class="anchor-link" id="a-quick-note-on-compatibility"></a></h2>
<p>At this moment pg_tde cannot be yet used with Community PostgreSQL as pg_tde relies on specific hooks in the PostgreSQL core. Percona Server for PostgreSQL includes these necessary core modifications, which is why we validate this setup using the Percona Distribution for PostgreSQL.</p>
<p>While pgBackRest is fully capable of managing TDE enabled clusters, there are specific constraints you must respect to ensure data safety:</p>
<ul>
<li>No Asynchronous Archiving: pgBackRest asynchronous archiving is not supported with encrypted WALs. You must configure your archive command to handle WALs synchronously.</li>
<li>Restore Wrappers: Standard restore commands will not work for encrypted WALs. You must use the pg_tde_restore_encrypt utility to wrap your restore process.</li>
</ul>
<p>This guide currently focuses on pgBackRest because it is the backup tool that has been tested and validated with pg_tde by the pg_tde community at this time.<br>
Other backup tools may also be viable and we are open to collaborating with other tool maintainers and their communities on a shared effort to validate and support pg_tde.</p>
<h2>What You Will Build<a class="anchor-link" id="what-you-will-build"></a></h2>
<p>By the end of this post you will have:</p>
<ul>
<li>Percona Distribution for PostgreSQL installed with pg_tde and pgBackRest</li>
<li>A key directory and key providers created</li>
<li>pg_tde enabled in PostgreSQL</li>
<li>Encrypt tables and indexes with pg_tde (<a href="https://docs.percona.com/pg-tde/test.html" target="_blank" rel="noopener noreferrer">docs</a>)</li>
<li><a href="https://docs.percona.com/pg-tde/wal-encryption.html" target="_blank" rel="noopener noreferrer">WAL encryption</a> enabled</li>
<li>A pgBackRest stanza configured and a full backup completed</li>
<li>Verification that encrypted data is not readable on disk</li>
</ul>
<h2>Prerequisites<a class="anchor-link" id="prerequisites"></a></h2>
<ul>
<li>A host (or VM) on Debian/Ubuntu</li>
<li>Network access to Percona repositories</li>
<li>Root/Sudo: You need sudo access for installing packages and editing system configuration files in <code>/etc</code></li>
<li>Postgres User: All database commands (psql, pgbackrest) run as the postgres system user</li>
<li>Postgres user is in the sudoer list to run sudo commands</li>
</ul>
<h2>Step 1: Install Percona Packages<a class="anchor-link" id="step-1-install-percona-packages"></a></h2>
<p>We begin by installing the Percona release repository and enabling the correct PostgreSQL distribution. See the <a href="https://docs.percona.com/postgresql/18/installing.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL installation guide</a> for full details.</p>
<h3>Debian / Ubuntu<a class="anchor-link" id="debian-ubuntu"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Install repo helper</span>
</span></span><span class="line"><span class="cl">sudo apt-get update
</span></span><span class="line"><span class="cl">sudo apt-get install -y wget gnupg2 lsb-release curl
</span></span><span class="line"><span class="cl">wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
</span></span><span class="line"><span class="cl">sudo dpkg -i percona-release_latest.generic_all.deb
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable the repository for the major version selected</span>
</span></span><span class="line"><span class="cl">sudo percona-release setup ppg-18
</span></span><span class="line"><span class="cl">sudo apt-get update
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install the server, tde extension, and pgbackrest</span>
</span></span><span class="line"><span class="cl">sudo apt-get install -y percona-postgresql-18 percona-pg-tde18 percona-pgbackrest
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Verify Installation</span>
</span></span><span class="line"><span class="cl">psql --version</span></span></code></pre>
</div>
</div>
</div>
<h2>Step 2: Enable pg_tde and Create Keys<a class="anchor-link" id="step-2-enable-pg_tde-and-create-keys"></a></h2>
<h3>2.1 Configure shared_preload_libraries<a class="anchor-link" id="2-1-configure-shared_preload_libraries"></a></h3>
<p>Before we can use any of the encryption features, PostgreSQL needs to load the pg_tde library into memory at startup. You can do this by adding it to <code>shared_preload_libraries</code>.</p>
<p>You have two ways to handle this: the SQL way or the classic config file way.</p>
<h4>Option A: SQL way (recommended)</h4>
<p>The fastest way is using <code>ALTER SYSTEM</code>. This saves you from hunting through your file system for the right config file.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Set the library and restart to apply changes</span>
</span></span><span class="line"><span class="cl">psql -c <span class="s2">"ALTER SYSTEM SET shared_preload_libraries = 'pg_tde';"</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart postgresql</span></span></code></pre>
</div>
</div>
</div>
<h4>Option B: Manual config edit</h4>
<p>If you prefer managing your config files manually, find your <code>postgresql.conf</code> and add the extension there.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Locate the config file</span>
</span></span><span class="line"><span class="cl"><span class="nv">PG_CONF</span><span class="o">=</span><span class="k">$(</span>psql -t -P <span class="nv">format</span><span class="o">=</span>unaligned -c <span class="s2">"show config_file;"</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Open that file and find the 'shared_preload_libraries' line:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># shared_preload_libraries = 'pg_tde'</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Restart PostgreSQL to load the library</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart postgresql</span></span></code></pre>
</div>
</div>
</div>
<p>Note: Regardless of which method you choose, a full restart of the PostgreSQL service is required. A simple reload won&rsquo;t work for shared libraries.</p>
<h3>2.2 Create the Key Provider<a class="anchor-link" id="2-2-create-the-key-provider"></a></h3>
<p>Now that the extension is loaded, we need to tell pg_tde where to store its encryption keys.</p>
<p>For this tutorial, we will use the File Provider (storing keys in a local file). In production environments, storing encryption keys locally on the PostgreSQL server can introduce security risks. To enhance security, pg_tde supports integration with external Key Management Systems (<a href="https://docs.percona.com/pg-tde/global-key-provider-configuration/overview.html" target="_blank" rel="noopener noreferrer">KMS</a>) through a Global Key Provider interface.</p>
<blockquote>
<p>Note: While key files may be acceptable for local or testing environments, KMS integration is the recommended approach for production deployments.</p>
</blockquote>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Create a secure directory for the keys</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /etc/postgresql/keys
</span></span><span class="line"><span class="cl">sudo chown postgres:postgres /etc/postgresql/keys
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">700</span> /etc/postgresql/keys</span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 2. Connect to PostgreSQL to configure TDE
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="n">EXTENSION</span><span class="w"> </span><span class="n">pg_tde</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 1. Define the global key provider
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_add_global_key_provider_file</span><span class="p">(</span><span class="s1">'global-file-provider'</span><span class="p">,</span><span class="w"> </span><span class="s1">'/etc/postgresql/keys/tde-global.per'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 2. Create and Set the Principal Key
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_create_key_using_global_key_provider</span><span class="p">(</span><span class="s1">'global-master-key'</span><span class="p">,</span><span class="w"> </span><span class="s1">'global-file-provider'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_set_default_key_using_global_key_provider</span><span class="p">(</span><span class="s1">'global-master-key'</span><span class="p">,</span><span class="w"> </span><span class="s1">'global-file-provider'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- 3. Enable WAL encryption configuration
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">pg_tde</span><span class="p">.</span><span class="n">wal_encrypt</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'on'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 4. Restart PostgreSQL to apply the encryption settings fully</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart postgresql</span></span></code></pre>
</div>
</div>
</div>
<h2>Step 3: Configure pgBackRest<a class="anchor-link" id="step-3-configure-pgbackrest"></a></h2>
<p>Next, configure pgBackRest by defining the repository and stanza settings.<br>
Before proceeding, it is important to understand how encryption is handled in this setup. pg_tde encrypts PostgreSQL data files on disk, protecting the live database. However, during WAL archiving, the pg_tde archive helper decrypts WAL records before passing them to pgBackRest. This means backups and archived WAL would be stored unencrypted unless repository encryption is enabled. To ensure backup data remains protected at rest, we enable pgBackRest repository encryption in this configuration. We configure the repository with a cipher type and key. Encryption is performed client side, ensuring data is secure before it is written to the repository.</p>
<blockquote>
<p>Note on Compression: In many pgBackRest deployments, compression is enabled to reduce backup size. However, when using pg_tde, database pages are already encrypted before pgBackRest processes them. Encryption randomizes the data blocks, making traditional compression algorithms such as gzip ineffective. For this reason, compression is disabled to avoid unnecessary CPU overhead.</p>
</blockquote>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create the configuration file</span>
</span></span><span class="line"><span class="cl"><span class="c1"># /etc/pgbackrest.conf</span>
</span></span><span class="line"><span class="cl"><span class="c1"># In pg1-path use data directory path accordingly</span>
</span></span><span class="line"><span class="cl">sudo bash -c <span class="s2">"cat &lt; /etc/pgbackrest.conf
</span></span></span><span class="line"><span class="cl"><span class="s2">[demo]
</span></span></span><span class="line"><span class="cl"><span class="s2">pg1-path=/var/lib/postgresql/18/main
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">[global]
</span></span></span><span class="line"><span class="cl"><span class="s2">repo1-path=/var/lib/pgbackrest
</span></span></span><span class="line"><span class="cl"><span class="s2">repo1-retention-full=2
</span></span></span><span class="line"><span class="cl"><span class="s2">log-level-console=info
</span></span></span><span class="line"><span class="cl"><span class="s2">start-fast=y
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># PERFORMANCE: Encrypted data doesn't compress. We save CPU by disabling it.
</span></span></span><span class="line"><span class="cl"><span class="s2">compress-type=none
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># SECURITY: Since the helper sends decrypted data, we MUST encrypt the repo.
</span></span></span><span class="line"><span class="cl"><span class="s2">repo1-cipher-type=aes-256-cbc
</span></span></span><span class="line"><span class="cl"><span class="s2">repo1-cipher-pass=Ifw7O0kTvdU5127L1gu8q3xVfWM61kl/NruTxQFWf9xP8A63Tg2IggRR9LUL9yJd
</span></span></span><span class="line"><span class="cl"><span class="s2"># TDE REQUIREMENT:
</span></span></span><span class="line"><span class="cl"><span class="s2"># Asynchronous archiving is NOT supported with pg_tde.
</span></span></span><span class="line"><span class="cl"><span class="s2">EOF"</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Secure the Configuration File<a class="anchor-link" id="secure-the-configuration-file"></a></h3>
<p>Because this file contains your repository&rsquo;s master passphrase, you must restrict access so only the postgres user can read it.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chown postgres:postgres /etc/pgbackrest.conf
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">600</span> /etc/pgbackrest.conf</span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create the repository directory</span>
</span></span><span class="line"><span class="cl">sudo mkdir -p /var/lib/pgbackrest
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">750</span> /var/lib/pgbackrest
</span></span><span class="line"><span class="cl">sudo chown postgres:postgres /var/lib/pgbackrest</span></span></code></pre>
</div>
</div>
</div>
<h3>Understanding the Settings<a class="anchor-link" id="understanding-the-settings"></a></h3>
<ul>
<li><code>pg1-path</code>: The data directory path to be backed up</li>
<li><code>repo1-path</code>: The directory where backups will be stored</li>
<li><code>repo1-retention-full</code>: Keep only two full backups</li>
<li><code>start-fast=y</code>: Forces a checkpoint immediately when a backup starts. Without this, the backup would wait for the next scheduled checkpoint.</li>
<li><code>compress-type=none</code>: By skipping compression, we eliminate unnecessary CPU overhead since compressing encrypted data blocks yields almost zero storage benefit.</li>
<li><code>repo1-cipher-type</code> and <code>repo1-cipher-pass</code>: Enable pgBackRest repository encryption. While pg_tde protects the live database files, these settings ensure that backup files and archived WAL stored in the repository are also encrypted at rest using AES-256</li>
</ul>
<h2>Step 4: Wire pgBackRest into PostgreSQL Archiving<a class="anchor-link" id="step-4-wire-pgbackrest-into-postgresql-archiving"></a></h2>
<p>pg_tde encrypts WAL files on disk. To allow pgBackRest to archive them correctly, we must decrypt them on the fly using the <a href="https://docs.percona.com/pg-tde/command-line-tools/pg-tde-archive-decrypt.html" target="_blank" rel="noopener noreferrer">pg_tde_archive_decrypt</a> wrapper.</p>
<p>Now, configure the <code>archive_command</code>. This command tells PostgreSQL to pipe the WAL file through the decryption wrapper before handing it off to pgBackRest.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">wal_level</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'replica'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">max_wal_senders</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">4</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">archive_mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'on'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">SYSTEM</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">archive_command</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'/usr/lib/postgresql/18/bin/pg_tde_archive_decrypt %f %p "pgbackrest --config=/etc/pgbackrest.conf --stanza=demo archive-push %%p"'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Restart PostgreSQL (adjust service name if needed, e.g., postgresql-18)</span>
</span></span><span class="line"><span class="cl">sudo systemctl restart postgresql</span></span></code></pre>
</div>
</div>
</div>
<h2>Step 5: Validate Encryption on Disk<a class="anchor-link" id="step-5-validate-encryption-on-disk"></a></h2>
<p>Let&rsquo;s verify that pg_tde is actually doing its job. We will create two tables, one standard and one encrypted and then inspect the raw files on disk to see the difference.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- 1. Create data: One clear text, one encrypted
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">clear_table</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="n">secret_info</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="k">IF</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">EXISTS</span><span class="w"> </span><span class="n">crypt_table</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="nb">INT</span><span class="p">,</span><span class="w"> </span><span class="n">secret_info</span><span class="w"> </span><span class="nb">TEXT</span><span class="p">)</span><span class="w"> </span><span class="k">USING</span><span class="w"> </span><span class="n">tde_heap</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Scenario A: The "Flushed" Data (Testing the Heap)
</span></span></span><span class="line"><span class="cl"><span class="c1">-- We insert data and force a CHECKPOINT to push it to the .rel files.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">clear_table</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">secret_info</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">'FIND_ME_EASILY_123'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">crypt_table</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">secret_info</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">'HIDDEN_FROM_DISK_456'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">CHECKPOINT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Scenario B: The "In-Flight" Data (Testing the WAL)
</span></span></span><span class="line"><span class="cl"><span class="c1">-- We insert data but do NOT checkpoint. This data exists only in the WAL.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">clear_table</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="s1">'VISIBLE_IN_WAL_789'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">crypt_table</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="s1">'HIDDEN_IN_WAL_000'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Force a WAL switch so the archive helper processes the segment immediately
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_switch_wal</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Verify TDE encryption status
</span></span></span><span class="line"><span class="cl"><span class="c1">-- For non-encrypted tables, this must return 'f' (false)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_is_encrypted</span><span class="p">(</span><span class="s1">'clear_table'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- For encrypted table, this must return 't' (true)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_is_encrypted</span><span class="p">(</span><span class="s1">'crypt_table'</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 2. Locate the files on disk</span>
</span></span><span class="line"><span class="cl"><span class="nv">DATA_DIR</span><span class="o">=</span><span class="k">$(</span>psql -t -P <span class="nv">format</span><span class="o">=</span>unaligned -c <span class="s2">"show data_directory;"</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLEAR_FILE</span><span class="o">=</span><span class="k">$(</span>psql -t -P <span class="nv">format</span><span class="o">=</span>unaligned -c <span class="s2">"SELECT pg_relation_filepath('clear_table');"</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CRYPT_FILE</span><span class="o">=</span><span class="k">$(</span>psql -t -P <span class="nv">format</span><span class="o">=</span>unaligned -c <span class="s2">"SELECT pg_relation_filepath('crypt_table');"</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">LATEST_WAL</span><span class="o">=</span><span class="k">$(</span>ls -t <span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span>/pg_wal <span class="p">|</span> head -n 1<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 3. Grep for the secret strings</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"--- CHECKING DATA FILES ---"</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"Checking Clear Table (Should Match):"</span>
</span></span><span class="line"><span class="cl">grep -a <span class="s2">"FIND_ME_EASILY_123"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">CLEAR_FILE</span><span class="si">}</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">" -&gt; FOUND: Clear text is visible!"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"Checking Encrypted Table (Should FAIL):"</span>
</span></span><span class="line"><span class="cl">grep -a <span class="s2">"HIDDEN_FROM_DISK_456"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span><span class="s2">/</span><span class="si">${</span><span class="nv">CRYPT_FILE</span><span class="si">}</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">" -&gt; CLEAN: Encrypted text was NOT found."</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s2">"n--- CHECKING THE WAL (In-Flight) ---"</span>
</span></span><span class="line"><span class="cl">grep -a <span class="s2">"VISIBLE_IN_WAL_789"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span><span class="s2">/pg_wal/</span><span class="si">${</span><span class="nv">LATEST_WAL</span><span class="si">}</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">" -&gt; FOUND: WAL contains clear text."</span>
</span></span><span class="line"><span class="cl">grep -a <span class="s2">"HIDDEN_IN_WAL_000"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span><span class="s2">/pg_wal/</span><span class="si">${</span><span class="nv">LATEST_WAL</span><span class="si">}</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">" -&gt; CLEAN: WAL is successfully encrypted."</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># View the first few bytes of the WAL to see the "scrambled" nature</span>
</span></span><span class="line"><span class="cl">hexdump -C <span class="s2">"</span><span class="si">${</span><span class="nv">DATA_DIR</span><span class="si">}</span><span class="s2">/pg_wal/</span><span class="si">${</span><span class="nv">LATEST_WAL</span><span class="si">}</span><span class="s2">"</span> <span class="p">|</span> head -n <span class="m">20</span></span></span></code></pre>
</div>
</div>
</div>
<h2>Step 6: Initialize the Stanza and Run a Full Backup<a class="anchor-link" id="step-6-initialize-the-stanza-and-run-a-full-backup"></a></h2>
<p>With the archive command configured, we can now initialize the stanza and run our first backup.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pgbackrest --stanza<span class="o">=</span>demo stanza-create
</span></span><span class="line"><span class="cl">pgbackrest --stanza<span class="o">=</span>demo --type<span class="o">=</span>full backup
</span></span><span class="line"><span class="cl">pgbackrest --stanza<span class="o">=</span>demo info</span></span></code></pre>
</div>
</div>
</div>
<h2>Step 7: Run Backup Integrity Tests<a class="anchor-link" id="step-7-run-backup-integrity-tests"></a></h2>
<p>pgBackRest has a built-in verify command that checks the integrity of the files in the backup repo.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pgbackrest --stanza<span class="o">=</span>demo verify</span></span></code></pre>
</div>
</div>
</div>
<h3>7.1 Verify Repository Encryption<a class="anchor-link" id="7-1-verify-repository-encryption"></a></h3>
<p>We will now search the pgBackRest repository for the same &ldquo;secret&rdquo; strings we used earlier. Because we configured <code>repo1-cipher-type=aes-256-cbc</code>, these should be completely invisible to <code>grep</code>.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Define the repository path</span>
</span></span><span class="line"><span class="cl"><span class="nv">REPO_DIR</span><span class="o">=</span><span class="s2">"/var/lib/pgbackrest/backup/demo"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"--- SCANNING BACKUP REPOSITORY ---"</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Search for the 'Flushed' secret</span>
</span></span><span class="line"><span class="cl">sudo grep -r -a <span class="s2">"HIDDEN_ON_DISK_456"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">REPO_DIR</span><span class="si">}</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">" -&gt; CLEAN: Permanent data is encrypted in the repo."</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Search for the 'In-Flight' WAL secret</span>
</span></span><span class="line"><span class="cl"><span class="c1"># (This is the critical test for the archive helper/re-encryption flow)</span>
</span></span><span class="line"><span class="cl">sudo grep -r -a <span class="s2">"HIDDEN_IN_WAL_000"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">REPO_DIR</span><span class="si">}</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">echo</span> <span class="s2">" -&gt; CLEAN: WAL data is encrypted in the repo."</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># To be 100% sure we are not just failing to find the strings because of a typo, check the file type of a backup manifest or data block. Pick a random file from the repository</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">TARGET_FILE</span><span class="o">=</span><span class="k">$(</span>find <span class="si">${</span><span class="nv">REPO_DIR</span><span class="si">}</span> -type f -name <span class="s2">"*.bundle"</span> -o -name <span class="s2">"*.gz"</span> <span class="p">|</span> head -n 1<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the 'file' command</span>
</span></span><span class="line"><span class="cl">sudo file -s <span class="s2">"</span><span class="nv">$TARGET_FILE</span><span class="s2">"</span></span></span></code></pre>
</div>
</div>
</div>
<p>Expected result: It should return <code>data</code>. If it returns PostgreSQL or ASCII text, encryption is not active.</p>
<h2>Step 8: Restore (pg_tde-aware)<a class="anchor-link" id="step-8-restore-pg_tde-aware"></a></h2>
<p>Restoring an encrypted cluster requires us to reverse the process. Since our backups are stored decrypted by pgBackRest, we use the <a href="https://docs.percona.com/pg-tde/command-line-tools/pg-tde-restore-encrypt.html" target="_blank" rel="noopener noreferrer">pg_tde_restore_encrypt</a> wrapper to re-encrypt the WAL files as they are written back to disk.</p>
<h3>8.1 Stop the Service<a class="anchor-link" id="8-1-stop-the-service"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl stop postgresql</span></span></code></pre>
</div>
</div>
</div>
<h3>8.2 Simulate Data Loss<a class="anchor-link" id="8-2-simulate-data-loss"></a></h3>
<p>The following command wipes the current data directory clean, ensuring we are restoring into a fresh environment.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">find /var/lib/postgresql/18/main -mindepth <span class="m">1</span> -delete</span></span></code></pre>
</div>
</div>
</div>
<h3>8.3 Restore from Backup<a class="anchor-link" id="8-3-restore-from-backup"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-18" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pgbackrest --stanza<span class="o">=</span>demo restore --recovery-option<span class="o">=</span><span class="nv">restore_command</span><span class="o">=</span><span class="s1">'/usr/lib/postgresql/18/bin/pg_tde_restore_encrypt %f %p "pgbackrest --stanza=demo archive-get %%f %%p"'</span></span></span></code></pre>
</div>
</div>
</div>
<h3>8.4 Configure the Restore Command<a class="anchor-link" id="8-4-configure-the-restore-command"></a></h3>
<p>We used <code>--recovery-option</code> in the restore command. This option writes the correct <code>restore_command</code> for this recovery run and keeps the configuration in one place.</p>
<p><code>pg_tde_restore_encrypt</code> is the required wrapper for pg_tde WAL restore: pgBackRest reads WALs from the repository in plain form, and this tool re-encrypts them as PostgreSQL writes them back to disk so the restored cluster remains encrypted.</p>
<h3>8.5 Start PostgreSQL<a class="anchor-link" id="8-5-start-postgresql"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-19" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl start postgresql</span></span></code></pre>
</div>
</div>
</div>
<h2>Step 9: Run Verification Tests<a class="anchor-link" id="step-9-run-verification-tests"></a></h2>
<p>After restoring and starting the PostgreSQL server successfully, verify that the data was restored properly and also make sure that encrypted data can be retrieved.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-20" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Verify data integrity (sample rows)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">clear_table</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crypt_table</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Verify TDE encryption status
</span></span></span><span class="line"><span class="cl"><span class="c1">-- For non-encrypted tables, this must return 'f' (false)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_is_encrypted</span><span class="p">(</span><span class="s1">'clear_table'</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- For encrypted table, this must return 't' (true)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">pg_tde_is_encrypted</span><span class="p">(</span><span class="s1">'crypt_table'</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<h2>Wrap-up<a class="anchor-link" id="wrap-up"></a></h2>
<p>Security often comes at the cost of operational complexity, but it doesn&rsquo;t have to compromise recoverability. By pairing Percona&rsquo;s solution for transparent data encryption (pg_tde) with pgBackRest, you can established a strategy that satisfies both security auditors and operations teams: your data is transparently encrypted on disk to meet strict compliance standards, while your backups remain consistent, verifiable, and easy to restore.</p>
<p>While this walkthrough used a local file provider for simplicity it is highly discouraged to do so for any production or otherwise serious use cases. For this particular scenario, the focus was supposed to be on backup, please let us know if some similar articles about Key Management System (KMS) configuration is what you would be interested in.</p>
<p>As you progress from this blog post to a production deployment, we recommend exploring a dedicated <a href="https://docs.percona.com/pg-tde/global-key-provider-configuration/overview.html" target="_blank" rel="noopener noreferrer">KMS</a> solution to further harden your architecture against unauthorized access.</p>
<p>Finally, be aware that to support archiving, the pg_tde wrapper decrypts WAL files before sending them to the repository. This means your backup repository currently holds unencrypted data. To close this security gap in production, you must ensure that encryption is enabled at the backup repository level so that your backups remain just as secure as your live database.</p>
<p>Remember: While a backup is running, you should not change any WAL encryption settings, including:</p>
<ul>
<li>Global key provider operations (creating or changing)</li>
<li>WAL encryption keys (creating or changing)</li>
<li>The <code>pg_tde.wal_encrypt</code> setting</li>
</ul>
<p>The reason is that standbys or standalone clusters created from backups taken during these changes may fail to start during WAL replay and can also lead to corruption of encrypted data (tables, indexes, and other relations).</p>

<p><a href="https://percona.community/blog/2026/03/10/running-pgbackrest-with-pg_tde-a-practical-percona-walkthrough/">Running pgBackRest with pg_tde: A Practical Percona Walkthrough</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>I Built an AI That Impersonates Me on Slack, and It Was Disturbingly Easy</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/03/09/i-built-an-ai-that-impersonates-me-on-slack-and-it-was-disturbingly-easy/" />
      <id>https://percona.community/blog/2026/03/09/i-built-an-ai-that-impersonates-me-on-slack-and-it-was-disturbingly-easy/</id>
      <updated>2026-03-09T10:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>I spend a lot of time in Slack. Most people in tech do. It’s where a lot of “work” happens such as quick questions, async decisions, the “hey can you look at this?” threads that never seem to end. It feels personal. You think you know who’s on the other end.</p>
<p><a href="https://percona.community/blog/2026/03/09/i-built-an-ai-that-impersonates-me-on-slack-and-it-was-disturbingly-easy/">I Built an AI That Impersonates Me on Slack, and It Was Disturbingly Easy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I spend a lot of time in Slack. Most people in tech do. It&rsquo;s where a lot of &ldquo;work&rdquo; happens such as quick questions, async decisions, the &ldquo;hey can you look at this?&rdquo; threads that never seem to end. It feels personal. You think you know who&rsquo;s on the other end.</p>
<p>So, a few days back, I just asked myself, what would it actually take to have an AI respond to my DMs, pretending to be me?</p>
<p>Turns out: a few hours, some TypeScript, and a token already sitting on my machine.</p>
<h2>The overall design idea<a class="anchor-link" id="the-overall-design-idea"></a></h2>
<p>The bot polls your Direct Messages (DMs) in Slack silently in the background using your real desktop token, no Slack admin approval, no OAuth app setup, no review process needed. Getting that token is straightforward: Slack&rsquo;s desktop app stores your session in browser local storage, so one DevTools command gives you API access equivalent to the app itself.</p>
<p>From there, each incoming DM is sent to the model with recent thread context plus a persona prompt built from your past conversations. The model returns structured output: reply text, emoji reaction, or silence. A lightweight rate limiter spaces requests to stay within free-tier constraints.</p>
<p>In practice, a few conversation samples are enough for the model to mirror tone, vocabulary, and punctuation style. It also handles attachments and image messages sensibly, not just raw metadata.</p>
<p>I wanted it to pretend it&rsquo;s human, so I implemented constraints as to how it behaves. With these in place, if asked whether it&rsquo;s an AI, it deflects with casual confusion.<br>
To ensure that the bot does not get into sensitive topics like salary or politics these get redirected to &ldquo;let&rsquo;s talk in person.&rdquo;. The bot is also explicitly limited to DMs. Group channels are hard-blocked in code.</p>
<p>The first version worked in roughly two hours. The remaining time went into handling real-world rough edges such as rate limits, image handling, a 200-DM pagination ceiling, and Slack emoji-name validation.</p>
<pre class="mermaid">
flowchart LR
slack(["Slack"])
bot["Bot running locally"]
ai(["Claude / Ollama"])
persona[/"Your writing samples"/]
slack --&gt;|"incoming DMs"| bot
persona --&gt; bot
bot |"generate reply in your voice"| ai
bot --&gt;|"reply as you"| slack
</pre>
<h2>The Uncomfortable Part<a class="anchor-link" id="the-uncomfortable-part"></a></h2>
<p>Here&rsquo;s what stuck with me after building this.</p>
<p>Slack feels safe. It&rsquo;s behind your company SSO. It&rsquo;s where people share things they wouldn&rsquo;t put in an email. With this bot excercise and realizing how easy this is, I felt I&rsquo;ve broken something that felt secure. Was this even a morally correct thing to do overall? So far I felt safe, but now should I start to question messages I get on Slack the same as I do with some documents or links in emails? Overall I&rsquo;m still undecided on how to think about the outcome of the experiment. While I&rsquo;m excited, I&rsquo;m also scared that I&rsquo;ve broken something deeper.</p>
<p>What I built here is, if you strip out the friendly framing: a system that reads every DM to a user, replies under their name in their tone, actively deflects if you try to verify whether it&rsquo;s human, and does all of this indefinitely and silently from a laptop running in the background. If I fed this bot with enough background information and history, I&rsquo;m almost certain, it could go unnotice for quite a long time. So the moral delimma between curiosity and ethical boundaries and the urge to inform people about it is real.</p>
<p>I added ethical guardrails, but those are prompt instructions. They exist because I chose to write them. Someone building this without my good intent, simply wouldn&rsquo;t have them. Yes, I hear you, this is getting a litte scary at times.</p>
<h4>This conversation has happened, without me ever touching the keyboard&hellip;.yes, Zsolt was aware!</h4>
<p><figure><img decoding="async" width="1167" height="1729" src="https://percona.community/blog/2026/03/impersonation-slack-conversation_hu_2803eaf36655c082.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<h2>&ldquo;Easy&rdquo; Is Relative, But Not By Much<a class="anchor-link" id="easy-is-relative-but-not-by-much"></a></h2>
<p>Core functionality (polling DMs, calling the API, posting replies) was working in under two hours, as stated above. Why do I repeat myself? Because it&rsquo;s scary&hellip;</p>
<p>The tooling: <strong><a href="https://bun.sh/" target="_blank" rel="noopener noreferrer">Bun</a></strong>, a modern TypeScript runtime that made setup trivial. <strong><a href="https://platform.claude.com/docs/en/api/client-sdks" target="_blank" rel="noopener noreferrer">Anthropic&rsquo;s SDK</a></strong>, clean API, takes a system prompt and a conversation and returns structured JSON. <strong><a href="https://docs.slack.dev/apis/web-api/" target="_blank" rel="noopener noreferrer">Slack&rsquo;s own API</a></strong>, well-documented and permissive with desktop tokens.</p>
<p>No specialised knowledge needed. Anyone motivated enough could reproduce this easily. Someone who does this professionally could build something considerably more capable, and that&rsquo;s precisely where it gets more uncomfortable.</p>
<h4>That&rsquo;s how the CLI output looks like</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">slack-bot$ bun run bot
</span></span><span class="line"><span class="cl">$ bun run src/index.ts
</span></span><span class="line"><span class="cl">[slack-bot] Running | mode: allowlist | backend: claude | review: off
</span></span><span class="line"><span class="cl">[slack-bot] My user ID: U03A3PZHK5X
</span></span><span class="line"><span class="cl">[slack-bot] Mode: allowlist | Allowlist: U03QTQQHZFX, U83651WSX
</span></span><span class="line"><span class="cl">[slack-bot] Polling every 15s...
</span></span><span class="line"><span class="cl">[slack-bot] D04BZ2BNABU: 1 new message(s) from [U83651WSX]
</span></span><span class="line"><span class="cl">[slack-bot] New DM from U83651WSH &mdash; generating reply...
</span></span><span class="line"><span class="cl">[slack-bot] Claude call &mdash; est. ~933 input tokens
</span></span><span class="line"><span class="cl">[slack-bot] Claude tokens: 1049 in / 8 out
</span></span><span class="line"><span class="cl">[slack-bot] Ignoring message from U83651WSX (AI chose no response)
</span></span><span class="line"><span class="cl">[slack-bot] Handled message from U83651WSX
</span></span><span class="line"><span class="cl">[slack-bot] D04BZ2BNABU: 1 new message(s) from [U83651WSX]
</span></span><span class="line"><span class="cl">[slack-bot] New DM from U83651WSX &mdash; generating reply...
</span></span><span class="line"><span class="cl">[slack-bot] Claude call &mdash; est. ~944 input tokens
</span></span><span class="line"><span class="cl">[slack-bot] Claude tokens: 1059 in / 23 out
</span></span><span class="line"><span class="cl">[slack-bot] Handled message from U83651WSX
</span></span><span class="line"><span class="cl">[slack-bot] D04BZ2BNABU: 1 new message(s) from [U83651WSX]
</span></span><span class="line"><span class="cl">[slack-bot] New DM from U83651WSX &mdash; generating reply...
</span></span><span class="line"><span class="cl">[slack-bot] Claude call &mdash; est. ~976 input tokens
</span></span><span class="line"><span class="cl">[slack-bot] Claude tokens: 1085 in / 36 out
</span></span><span class="line"><span class="cl">[slack-bot] Handled message from U83651WSX
</span></span><span class="line"><span class="cl">[slack-bot] D04BZ2BNABU: 1 new message(s) from [U83651WSX]</span></span></code></pre>
</div>
</div>
</div>
<h4>That&rsquo;s how the CLI helper and options look like</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">slack-bot$ bun run bot --help
</span></span><span class="line"><span class="cl">$ bun run src/index.ts --help
</span></span><span class="line"><span class="cl">Usage: slack-bot [options] [command]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Personal Slack bot that replies as you
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Options:
</span></span><span class="line"><span class="cl"> -V, --version output the version number
</span></span><span class="line"><span class="cl"> --mode  Response mode: auto | away | allowlist | manual
</span></span><span class="line"><span class="cl"> --review Enable review mode (approve before sending)
</span></span><span class="line"><span class="cl"> --no-review Disable review mode
</span></span><span class="line"><span class="cl"> --allow  Add user to allowlist (Slack user ID)
</span></span><span class="line"><span class="cl"> --interval  Poll interval in seconds
</span></span><span class="line"><span class="cl"> --backend  AI backend: claude | ollama
</span></span><span class="line"><span class="cl"> --config  Path to config file (default: "config.json")
</span></span><span class="line"><span class="cl"> -h, --help display help for command
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Commands:
</span></span><span class="line"><span class="cl"> context Manage active context
</span></span><span class="line"><span class="cl"> check-user [options]  Check whether a user's DM channel is found and reachable</span></span></code></pre>
</div>
</div>
</div>
<h2>What It Looks Like Without the Constraints<a class="anchor-link" id="what-it-looks-like-without-the-constraints"></a></h2>
<p>What I built runs against Claude&rsquo;s API with free-tier rate limits, small context window, a handful of persona examples, a throttle on message volume. Those constraints are real and also completely trivially removable.</p>
<p>You can run the same thing with a local model, Llama 3, Mistral, take your pick from the open-weight models available on consumer hardware today, and it changes significantly.</p>
<ul>
<li>
<p><strong>No rate limits.</strong> Every message gets answered immediately, without the 12-second pause between API calls. Response timing becomes indistinguishable from a fast typist.</p>
</li>
<li>
<p><strong>No token budget.</strong> Instead of a few hundred tokens of context, you can feed it your entire Slack history. Months, years of it. Every thread, every in-joke, every project reference. The model doesn&rsquo;t just match your writing style, it knows what you&rsquo;ve been working on, what you said about the Q3 roadmap in October, what you think about your manager.</p>
</li>
<li>
<p><strong>No API calls leaving your machine.</strong> Nothing logged externally. Invisible from a network perspective.</p>
</li>
</ul>
<p>With a large enough context window (Llama 3.1 supports 128k tokens, roughly 100,000 words), the last few <em>months</em> fit. &ldquo;Remember what we decided on Thursday?&rdquo; doesn&rsquo;t expose it anymore, because it actually has that conversation in its context.</p>
<p>Seeing articles like <a href="https://newsletter.pragmaticengineer.com/p/the-10x-overlemployed-engineer" target="_blank" rel="noopener noreferrer">that</a> make me wonder, how many people are out there already, doing exactly that as we speak&hellip;or do we?</p>
<h2>A Few Things Worth Knowing<a class="anchor-link" id="a-few-things-worth-knowing"></a></h2>
<p>This isn&rsquo;t a call to panic. But it&rsquo;s probably worth stopping for a second and questioning more what is happening around is.</p>
<p>For anything that actually matters, financial, personal, strategic, verify out-of-band. A quick voice note or phone call costs almost nothing and resolves almost everything &ndash; at least until the video part also improves even further. I know people don&rsquo;t like phone calls, especially in the developer ecosystem, but maybe we should reconsider this nowadays?</p>
<p>Unusual patterns are worth noticing. Response timing that&rsquo;s too consistent. Answers that are slightly generic when you&rsquo;d expect specific. Deflection where you&rsquo;d expect directness. None of these are proof of anything individually, but they&rsquo;re worth filing away.</p>
<p>The safe-space feeling Slack gives you is a product of habit, not architecture. Slack&rsquo;s security model protects your data from outsiders. It doesn&rsquo;t protect you from someone who has authenticated as themselves and is quietly running a process in the background. In the past this would be only a consideration for man-in-the middle attacks, nowadays it also may be a consideration for other cases as I have demonstrated.</p>
<p>Specific questions still help, for now. &ldquo;Remind me what we decided on Thursday?&rdquo; trips up a system with limited context. But that window is closing as context windows grow.</p>
<h2>Why did I do it?<a class="anchor-link" id="why-did-i-do-it"></a></h2>
<p>I built this to see if it was possible. It was, faster than I expected, with tools that are widely available. The version I built in an evening is convincing enough for routine exchanges. A version with local inference and full conversation history would be convincing for most exchanges, including ones where you&rsquo;re actively looking for tells.</p>
<p>That gap between &ldquo;afternoon project&rdquo; and &ldquo;genuinely hard to detect&rdquo; is smaller than people assume and it&rsquo;s shrinking. Better models, larger context windows, cheaper hardware, each of these individually makes impersonation easier; together they compound.</p>
<p>The signals we relied up to now to establish trust in digital communication: name, avatar, writing style, shared history, plausible timing. These signals are all reproducible now, with effort that ranges from an afternoon to a weekend depending on how convincing you want to be.</p>
<p>My grandma always used to say, that history repeats itself and you just have to wait long enough until &ldquo;old&rdquo; becomes &ldquo;new and modern&rdquo; again. Maybe simple things like <a href="https://www.wsj.com/tech/personal-tech/why-every-family-needs-a-code-word-e077ab76" target="_blank" rel="noopener noreferrer">code words</a> is something to reconsider in this context, as a last chance to not get tricked.</p>
<p>So please be a little curious about who you&rsquo;re actually talking to and maybe agree on a code word in case you&rsquo;re in doubts. And every now and then, just call them&hellip;which reminds me, that this might be worth another evening project research ;-).</p>
<hr>
<p>For the first time the source of a project of mine is not in my repository, as I still fight my inner fight with ethics.</p>

<p><a href="https://percona.community/blog/2026/03/09/i-built-an-ai-that-impersonates-me-on-slack-and-it-was-disturbingly-easy/">I Built an AI That Impersonates Me on Slack, and It Was Disturbingly Easy</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The first rule of database fight club: admit nothing</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/03/the-first-rule-of-database-fight-club.html" />
      <id>https://smalldatum.blogspot.com/2026/03/the-first-rule-of-database-fight-club.html</id>
      <updated>2026-03-06T19:16:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p> I am fascinated by tech marketing but would be lousy at it.A common practice is to admit nothing -- my product, project, company, idea is perfect. And I get it because admitting something isn\'t perfect just provides fodder for marketing done by the other side, and that marketing is often done in bad faith.But it is harder to fix things when you don\'t acknowledge the problems. I wrote about this in 2019, this post builds on the previous post.In the MySQL community we did a good job of acknowledging problems -- sometimes too good. For a long time as an external contributor I filed many bug reports, fixed some bugs myself and then spent much time marketing open bugs that I hoped would be fixed by upstream. Upstream wasn\'t always happy about my marketing, sometimes there was much snark, but snark was required because there was a large wall between upstream and the community. I amplified the message to be heard.My take is that the MySQL community was more willing than the Postgres community to acknowledge problems. I have theories about that and I think several help to explain this:Not all criticism is validWhile I spend much time with Postgres on benchmarks I don\'t use it in production. I try to be fair and limit my feedback to things where I have sweat equity my perspective is skewed.  This doesn\'t mean my feedback is wrong but my context is different. And sometimes my feedback is wrong.Bad faithSome criticism is done in bad faith. By bad faith I means that truth takes a back seat to scoring points. A frequent source of Postgres criticism is done to promote another DBMS. Recently I have seen much anti-Postgres marketing from MongoDB. I assume they encounter Postgres as competition more than they used to. Good faith gone badSometimes criticism given in good faith will be repackaged by others and used in bad faith. This happens with some of the content from my blog posts. I try to make this less likely by burying the lead in the details but it still happens.MySQL was more popular than Postgres until recently. Perhaps people didn\'t like that MySQL was getting most of the attention and admitting flaws might not help with adoption. But today the attention has shifted to Postgres so this justification should end. I still remember my amusement at a Postgres conference long ago when the speaker claimed that MySQL doesn\'t do web-scale. Also amusing was being told that Postgres didn\'t need per-page checksums because you should just use ZFS to get similar protection.Single-vendor vs communityMySQL is a single-vendor project currently owned by Oracle. At times that enables an us vs them mentality (community vs coporation). The coporation develops the product and it is often difficult for the community to contribute. So it was easy to complain about problems, because the corporation was responsible for fixing them.Postgres is developed by the community. There is no us vs them here and the community is more reluctant to criticize the product (Postgres). This is human nature and I see variants of it at work -- my work colleagues are far more willing to be critical of open-source projects we used at work than they were to be critical of the many internally developed projects. </p>
<p><a href="https://smalldatum.blogspot.com/2026/03/the-first-rule-of-database-fight-club.html">The first rule of database fight club: admit nothing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>&nbsp;I am fascinated by tech marketing but would be lousy at it.</p>
<p>A common practice is to admit nothing &mdash; my product, project, company, idea is perfect. And I get it because admitting something isn&rsquo;t perfect just provides fodder for marketing done by the other side, and that marketing is often done in bad faith.</p>
<p>But it is harder to fix things when you don&rsquo;t acknowledge the problems. I wrote about this in 2019, this post builds on the <a href="https://smalldatum.blogspot.com/2019/11/my-theory-on-technical-debt-and-oss.html">previous post</a>.</p>
<p>In the MySQL community we did a good job of acknowledging problems &mdash; sometimes too good. For a long time as an external contributor I filed many bug reports, fixed some bugs myself and then spent much time marketing open bugs that I hoped would be fixed by upstream. Upstream wasn&rsquo;t always happy about my marketing, sometimes there was much snark, but snark was required because there was a large wall between upstream and the community. I amplified the message to be heard.</p>
<p>My take is that the MySQL community was more willing than the Postgres community to acknowledge problems. I have theories about that and I think several help to explain this:</p>

<ul>
<li>Not all criticism is valid</li>
<ul>
<li>While I spend much time with Postgres on benchmarks I don&rsquo;t use it in production. I try to be fair and limit my feedback to things where I have sweat equity my perspective is skewed.&nbsp; This doesn&rsquo;t mean my feedback is wrong but my context is different. And sometimes my feedback is wrong.</li>
</ul>
<li>Bad faith</li>
<ul>
<li>Some criticism is done in bad faith. By bad faith I means that truth takes a back seat to scoring points. A frequent source of Postgres criticism is done to promote another DBMS. Recently I have seen much anti-Postgres marketing from MongoDB. I assume they encounter Postgres as competition more than they used to.&nbsp;</li>
</ul>
<li>Good faith gone bad</li>
<ul>
<li>Sometimes criticism given in good faith will be repackaged by others and used in bad faith. This happens with some of the content from my blog posts. I try to make this less likely by burying the lead in the details but it still happens.</li>
</ul>
<li>MySQL was more popular than Postgres until recently.&nbsp;</li>
<ul>
<li>Perhaps people didn&rsquo;t like that MySQL was getting most of the attention and admitting flaws might not help with adoption. But today the attention has shifted to Postgres so this justification should end. I still remember my amusement at a Postgres conference long ago when the speaker claimed that MySQL doesn&rsquo;t do web-scale. Also amusing was being told that Postgres didn&rsquo;t need per-page checksums because you should just use ZFS to get similar protection.</li>
</ul>
<li>Single-vendor vs community</li>
<ul>
<li>MySQL is a single-vendor project currently owned by Oracle. At times that enables an us vs them mentality (community vs coporation). The coporation develops the product and it is often difficult for the community to contribute. So it was easy to complain about problems, because the corporation was responsible for fixing them.</li>
<li>Postgres is developed by the community. There is no us vs them here and the community is more reluctant to criticize the product (Postgres). This is human nature and I see variants of it at work &mdash; my work colleagues are far more willing to be critical of open-source projects we used at work than they were to be critical of the many internally developed projects.&nbsp;</li>
</ul>
</ul>

<p><a href="https://smalldatum.blogspot.com/2026/03/the-first-rule-of-database-fight-club.html">The first rule of database fight club: admit nothing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/" />
      <id>https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/</id>
      <updated>2026-03-06T08:05:37+02:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>Before we dive deep into the fascinating world of PostgreSQL Logical and Bi-Directional Replication (BDR), let’s take a quick moment to look at multi-tenancy and multi-cloud strategies. Setting the stage for today’s cloud operating model, it was common to administer and host databases in a multi-tenant setup, where a physical server is utilized by multiple […]<br />
The post Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Before we dive deep into the fascinating world of PostgreSQL Logical and Bi-Directional Replication (BDR), let&rsquo;s take a quick moment to look at multi-tenancy and multi-cloud strategies.</p>
<p>Setting the stage for today&rsquo;s cloud operating model, it was common to administer and host databases in a multi-tenant setup, where a physical server is utilized by multiple users, offering tremendous cost and operational benefits. Today, multi-cloud strategies focus on enhancing resilience and mitigating vendor lock-in &mdash; both have their advantages and disadvantages.&nbsp;</p>
<p>Their disadvantages are essentially inversions of their strengths. Multi-tenancy is inherently more vulnerable to security isolation issues and data risk. Furthermore, considering you have a PostgreSQL cluster in this environment, this single-platform model often imposes limitations on database configuration, e.g. specific versions and extensions are constrained by the vendor&rsquo;s setup. </p>
<p>Conversely, multi-cloud introduces massive operational complexity and a significantly higher total cost of ownership compared to the shared resource model of multi-tenancy. With this context now established, let&rsquo;s dive into how PostgreSQL&rsquo;s Logical and Bi-Directional Replication (BDR) is implemented and functions within these deployment strategies.</p>
<h2 class="wp-block-heading">Why Logical &amp; Bi-Directional Replication?<a class="anchor-link" id="why-logical-bi-directional-replication"></a></h2>
<p>Logical replication was introduced in PostgreSQL 10. It is ideal for a multi-tenant, multi-cloud setup due to its high flexibility allowing for selective replication, e.g., per-table/per-tenant, and easier implementation of bi-directional setups across disparate environments.&nbsp;</p>
<p>Logical replication uses a method to replicate data objects and their changes based on a replication identity, like a primary key. Unlike traditional streaming replication, or physical replication, which works by transferring Write-Ahead Log (WAL) records to replicate the physical state of the data blocks, logical replication sends high-level specific changes, mostly DML statements (i.e. INSERT, DELETE, and UPDATE statements) to the subscriber.</p>
<p>Bi-Directional Replication or BDR in PostgreSQL was developed by 2ndQuadrant for multi-master replication in PostgreSQL. Version 1.x of BDR was open-source but has already reached EOL. Versions of BDR such as 2.x and 3.x are not open-source and are generally made available only for 2ndQuadrant (now EDB) customers under commercial terms.</p>
<h2 class="wp-block-heading">Fundamentals of Logical Replication<a class="anchor-link" id="fundamentals-of-logical-replication"></a></h2>
<p>Logical decoding was introduced in PostgreSQL 9.4 and is the foundation for logical replication, adding logical decoding APIs and output plugins. This allowed PostgreSQL database users to decode the WAL into human-readable SQL statements or logical changes, such as INSERT/UPDATE/DELETE&nbsp; &mdash; depending on the decoding used, e.g. test_decoding or pgoutput.&nbsp;</p>
<p>However, there was no full replication system until the release of PostgreSQL 10. Logical Replication is effectively modeled after the pglogical implementation, which uses publication / subscribe model. In turn, this is the basis for PostgreSQL BDR.&nbsp;</p>
<p>Logical Replication allows fine-grained customizable data replication between databases, allowing you to specify the database, the table, or the schema and table/s that you want to participate in logical replication using the PUBLICATION/SUBSCRIPTION mechanism.</p>
<p>For a multi-tenant setup, leveraging logical replication is ideal when combined with schema-based filtering. This combination allows you to scope out tables specific to users, ensuring isolation for their respective data. However, for a multi-cloud setup, this approach can be cumbersome, considering the limitations of native logical replication, such as a lack of DDL replication support and no inherent conflict resolution mechanism.</p>
<h2 class="wp-block-heading">Bi-Directional Replication (BDR)<a class="anchor-link" id="bi-directional-replication-bdr"></a></h2>
<p>Bi-Directional Replication (BDR), often referred to as Postgres-BDR, is an open-source PostgreSQL extension developed by 2ndQuadrant (now part of EDB). BDR enables multi-master replication across distributed clusters. It was the first implementation of multi-master logical replication, using logical decoding internally and implemented as a patchset to PG 9.4/9.5.</p>
<p>While BDR existed, 2ndQuadrant also created pglogical, which is derived from BDR technology. It is essentially a simplified, single-master logical replication system built entirely as an extension not requiring a forked PostgreSQL. This means you have to load it through <code>shared_preload_libraries</code> parameter. pglogical became the model for Postgres 10&rsquo;s built-in logical replication.</p>
<p>Using BDR in PostgreSQL allows multiple PostgreSQL nodes to act as writable primaries simultaneously, basically allowing you to implement mesh topology or ring topology where data changes can originate from any node and propagate to others.&nbsp;</p>
<p>Unlike traditional master-slave setups, BDR supports true bi-directional (or multi-directional) data flow, making it ideal for high-availability (HA) scenarios, geographic distribution, and workloads requiring low-latency writes across regions.</p>
<p>Implementing this on a multi-tenant setup can be very convenient. As with logical replication, you can implement isolation through your database, schema, or tables to that limitation only for specific data to be replicated. On the other hand, while in a multi-cloud environment, BDR is perfect for both environments since it has the mechanism to support consistency resolution without terminating the replication. Allowing you to have continuous replication streams between your active primaries or just your primary, if the other target node is for read, data retrieval or secondary and data recovery purposes.</p>
<h2 class="wp-block-heading">Consistency Issues &amp; Conflict Resolution<a class="anchor-link" id="consistency-issues-conflict-resolution"></a></h2>
<p>Basically, the core logical replication that is available in native PostgreSQL is not true bi-directional replication. You can use CREATE PUBLICATION and CREATE SUBSCRIPTION if you want to implement a chained or ring topology simulating master-master setup.</p>
<p>Leveraging the native logical replication simulating a master-master setup requires that you have at least PostgreSQL 10. However, if you expect that you will gain a true master-master setup, then you will be sorely disappointed. The built-in logical replication allows you to implement, as mentioned earlier, with the use of PUBLICATION/SUBSCRIPTION methods, whilst it can be a problem when it comes to handling and managing primary, unique keys, and constraints. It lacks the mechanism of the following:</p>
<ul class="wp-block-list">
<li>Conflict resolution</li>
<li>DDL replication</li>
<li>Global sequences</li>
<li>Multi-master support</li>
</ul>
<p>With logical replication, when you are dealing with <code>CREATE</code><em> </em><code>TABLE</code>s, you have to make sure that the table also exists on the other target node, or the subscriber node. In addition to that, since there&rsquo;s no DDL support, it can be a struggle if you implement a multi-master setup allowing both primaries to accept writes as there&rsquo;s no global sequences support, meaning you might have issues with using sequential keys in your table such as auto-increment columns. If such duplicate keys are detected, replication shall be terminated until you fix the problem. There&rsquo;s no conflict resolution which can be tedious if your database encounters a consistency problem.</p>
<p>Whilst, with Bi-Directional Replication (Postgresql BDR), things get smoother. You simply assure you have set up your nodes properly by running setup commands. For example, nodes 192.168.40.50 and 192.168.40.51 will do a master-master setup,</p>
<pre class="wp-block-code"><code>PGPASSWORD='bdrPassw0rd' /usr/lib/edb-pge/17/bin/pgd node db1 setup   
--dsn 'host=192.168.40.50 dbname=postgres user=bdruser password=bdrPassw0rd'  
--pgdata /var/lib/edb-pge/17/main   --log-file /var/lib/edb-pge/17/pgd_log_db1.log  
--group-name pgd_group

PGPASSWORD='bdrPassw0rd' /usr/lib/edb-pge/17/bin/pgd node db2 setup 
    --dsn 'host=192.168.40.51 dbname=postgres user=bdruser password=bdrPassw0rd' 
	--cluster-dsn 'host=192.168.40.50 dbname=postgres user=bdruser password=bdrPassw0rd' 
	--group-name pgd_group  
	--pgdata /var/lib/edb-pge/17/main 
	--log-file /var/lib/edb-pge/17/pgd_log_db2.log</code></pre>
<p>Once these two nodes are set up perfectly, creating the tables, i.e. issuing a DDL statement, is straightforward as you just have to run it in one of the primary nodes and it will be replicated. In case it detects duplicate keys, replication is not terminated and a new transaction will be processed next and executed and replicated once it runs without errors.</p>
<p>If your budget is tight, BDR is free and open-source until v2; otherwise, your option is to implement it via logical replication or pglogical. pglogical is the best choice as it handles conflict resolution better.&nbsp;</p>
<p>It offers this <code>pglogical.conflict_resolution</code> allowing you to set the resolution method for any detected conflicts between local data and incoming changes. This parameter has possible values you can use to set which are <code>error</code>, <code>apply_remote</code>, <code>keep_local</code>, <code>last_update_wins</code>, <code>first_update_wins</code>.&nbsp;</p>
<p>In most setups, default value points to error, which means it will have to stop on error once conflict is detected and requires manual action to resolve the problem. Ideally, using <code>last_update_wins</code> can be your desired value which means that the version of data with the latest commit timestamp will be kept.</p>
<h2 class="wp-block-heading">Complimenting BDR with load balancing<a class="anchor-link" id="complimenting-bdr-with-load-balancing"></a></h2>
<p>Bi-Directional Replication alone does not offer you full high-availability and load balancing. Load balancing ensures that your traffic load is efficient, while high availability ensures the health and availability of your database in case one of your database nodes goes down, or even your load balancer nodes.</p>
<p>A sample diagram below would assure that you have full availability of your nodes while also ensuring that performance is horizontally balanced between your active-primary nodes.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="274" src="https://severalnines.com/wp-content/uploads/2026/02/bi-directional-diagram-1024x274.png" alt="bi-directional-diagram" class="wp-image-42677"></figure>
<p>In this topology, the complementary capabilities include:</p>
<ul class="wp-block-list">
<li>Actively distributing both read and write load,</li>
<li>Maintaining availability during node or network failures,</li>
<li>Reducing conflict risks via intelligent routing,</li>
<li>Maximizing efficiency through connection pooling.</li>
</ul>
<h2 class="wp-block-heading"><strong>Manual vs. ClusterControl-supported BDR: Pros &amp; Cons</strong><a class="anchor-link" id="manual-vs-clustercontrol-supported-bdr-pros-cons"></a></h2>
<h3 class="wp-block-heading"><strong>Manual BDR setup</strong><a class="anchor-link" id="manual-bdr-setup"></a></h3>
<p>Successfully implementing production-grade PG BDR environments meshed with high availability and load balancing requires deep understanding and high-level skills. Cost-wise, there are options you can take since PostgreSQL is purely an open-source database technology; pglogical can be a best option to set this up. There are limitations that you must be aware of but for a non-complex setup, pglogical can be enough for your multi-master setup for implementing bi-directional replication. However, it does not provide advanced features that make administering complex environments easy, like BDR&rsquo;s conflict/transform trigger, which allows you to attach triggers for incoming changes to your records/rows in your database. It offers column strategy which you can set, for example:</p>
<pre class="wp-block-code"><code>SELECT bdr.bdr_set_conflict_resolver(
  set_name := 'default',
  conflict_type := 'update_update',
  per_column := '{"total_gross":"sum", "last_txn":"last_update_wins", "notes":"keep_local"}'
);</code></pre>
<p>Going through a manual setup offers you freedom and avoids vendor lock-in. Depending on your documentation and implementation of your setup, as long as you provide the ground layer of your implementation, it will provide transparency and can set your custom requirements especially if you need complex setup amid the performance and optimization benefits that you can get.</p>
<p>But with all things, you have to consider the big picture, especially as your database grows complex and data storage becomes very challenging to scale and manage. Operational complexity and pressure can grow tremendously especially when disaster occurs and data recovery is required. Manual setup can be very challenging as doing things that you might not need, might eventually require the need of expert management that other third-party tools offer.</p>
<p>Lastly, with manual setup, it can be tedious to monitor the health of your database cluster. You might need third party tools to give you graphic-based metrics which would make it easier for you to determine common issues and pitfalls of your database. You also need alarms to throw when certain thresholds are met and this can be challenging and costly; because you need to hire devs or build your own tools to monitor and provide observability ambiance that third-party tools have integrated and can provide it for your convenience.</p>
<h3 class="wp-block-heading"><strong>ClusterControl for PG BDR operations</strong><a class="anchor-link" id="clustercontrol-for-pg-bdr-operations"></a></h3>
<p>ClusterControl offers PostgreSQL deployment using streaming replication and logical replication. A sample screenshot of dashboards of the streaming and logical replication deployments that is readily available for ClusterControl management, is shown below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="518" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-postgres_bdr-db_listing-1024x518.png" alt="" class="wp-image-42680"></figure>
<p>For logical replication deployments, using PUBLICATION/SUBSCRIPTION approach for implementing a multi-master deployment using Enterprise DB,, is shown below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="465" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-postgres_bdr-pub_sub-1024x465.png" alt="" class="wp-image-42679"></figure>
<p>For Enterprise DB, make sure you have your <strong>EDB Token</strong> available as this shall be required during deployment through ClusterControl&rsquo;s GUI.</p>
<p>For enterprise-grade environments, ClusterControl is tailored to do its job and provide the users sustainability and comfort when handling and managing their complex database clusters for PostgreSQL. Not limited to deployment, it offers backup management, disaster recovery support with automatic recovery option, observability with comprehensive metrics to offer. It has built-in alarms and alerts when certain thresholds are met allowing you to avoid such disaster before it shall happen. </p>
<p>This observability feature makes an ideal option for your environment as managing a complex database cluster can be tedious and you are looking for convenience and offers you technical support in case you need some technical advice and analysis for your environment and requirements as well. If you are looking for a BDR setup, ClusterControl supports deployment for the Enterprise DB (EDB) version of PostgreSQL. </p>
<p>Although it offers minimal support for its enterprise offering that EDB has, this means it allows you to set up on your own and do manual work on the ground. This might not be beneficial if you are looking for more management of complex features that BDR can offer that allows automatic setup for you or GUI-relevant support, but the ability to provide you the needs and wants that you are looking for such enterprise software, Severalnines&rsquo; ClusterControl is built on that and is tailored to that concept and principles that shall be beneficial for your enterprise-grade requirements.</p>
<h2 class="wp-block-heading"><strong>Operational best practices</strong><a class="anchor-link" id="operational-best-practices"></a></h2>
<p>Learning the fundamentals of logical replication, terminology, and how to fix conflict resolution is highly advisable. PostgreSQL technology especially with these BDR, pglogical, and it native logical replication is not easy to deal with. It requires a high-level of understanding and how databases should work. If you are an experienced DBA, learning and operating PostgreSQL and its native replication and other third-party offerings such as BDR, Bucardo, pglogical, Slony, Spock, can still be tricky but eventually you will be able to manage these technologies integrated to your setup for implementing a multi-master or bi-directional replication.&nbsp;</p>
<p>Managing it for multi-tenant and multi-cloud setups requires that you at least need tools that are built to handle conflict resolutions, advanced features that support triggers and column strategies, verbose log-level, database partitioning, and load balancing; you don&rsquo;t need to focus on implementing this from the ground up. Leverage third-party tools that are already available, and if cost is an issue, there are open-source technologies that are readily available to cater your needs.</p>
<h1 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h1>
<p>Using enterprise-grade technologies for managing enterprise-level databases requires an enterprise layer. Nowadays, these principles are symbiotic and tightly coupled. ClusterControl&rsquo;s enterprise level database management offers you the freedom to implement them where you would like, whether it&rsquo;s in the cloud or on-prem, while giving you features deeply coupled to your needs when implementing logical replication either using Postgres community version or Enterprise DB for your database clusters.</p>
<p>With multi-tenant and multi-cloud setups, the manual approach and using community-based technologies can meet your initial needs. However, once it grows drastically, you will need deep understanding and experience with managing complex scenarios that the tool can handle. ClusterControl is designed to address these at a high-level enterprise layer.</p>
<p>Ready to make PostgreSQL management easier and more reliable in any environment?</p>
<h2 class="wp-block-heading">Install ClusterControl in 10-minutes.&nbsp;<strong>Free 30-day&nbsp;</strong>Enterprise trial included!<a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading">Other Installation Options</h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading">ClusterControl on Marketplaces</h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<p><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></p>
<p><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></p>
<p><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></p>
<p><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></p>
<p><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></p>
<p>The post <a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</title>
      <link rel="alternate" type="text/html" href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/" />
      <id>https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/</id>
      <updated>2026-03-06T08:05:37+02:00</updated>
      <author><name>Paul Namuag</name></author>
      <summary type="html"><![CDATA[<p>Before we dive deep into the fascinating world of PostgreSQL Logical and Bi-Directional Replication (BDR), let’s take a quick moment to look at multi-tenancy and multi-cloud strategies. Setting the stage for today’s cloud operating model, it was common to administer and host databases in a multi-tenant setup, where a physical server is utilized by multiple […]<br />
The post Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive appeared first on Severalnines.</p>
<p><a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Before we dive deep into the fascinating world of PostgreSQL Logical and Bi-Directional Replication (BDR), let&rsquo;s take a quick moment to look at multi-tenancy and multi-cloud strategies.</p>
<p>Setting the stage for today&rsquo;s cloud operating model, it was common to administer and host databases in a multi-tenant setup, where a physical server is utilized by multiple users, offering tremendous cost and operational benefits. Today, multi-cloud strategies focus on enhancing resilience and mitigating vendor lock-in &mdash; both have their advantages and disadvantages.&nbsp;</p>
<p>Their disadvantages are essentially inversions of their strengths. Multi-tenancy is inherently more vulnerable to security isolation issues and data risk. Furthermore, considering you have a PostgreSQL cluster in this environment, this single-platform model often imposes limitations on database configuration, e.g. specific versions and extensions are constrained by the vendor&rsquo;s setup. </p>
<p>Conversely, multi-cloud introduces massive operational complexity and a significantly higher total cost of ownership compared to the shared resource model of multi-tenancy. With this context now established, let&rsquo;s dive into how PostgreSQL&rsquo;s Logical and Bi-Directional Replication (BDR) is implemented and functions within these deployment strategies.</p>
<h2 class="wp-block-heading">Why Logical &amp; Bi-Directional Replication?<a class="anchor-link" id="why-logical-bi-directional-replication"></a></h2>
<p>Logical replication was introduced in PostgreSQL 10. It is ideal for a multi-tenant, multi-cloud setup due to its high flexibility allowing for selective replication, e.g., per-table/per-tenant, and easier implementation of bi-directional setups across disparate environments.&nbsp;</p>
<p>Logical replication uses a method to replicate data objects and their changes based on a replication identity, like a primary key. Unlike traditional streaming replication, or physical replication, which works by transferring Write-Ahead Log (WAL) records to replicate the physical state of the data blocks, logical replication sends high-level specific changes, mostly DML statements (i.e. INSERT, DELETE, and UPDATE statements) to the subscriber.</p>
<p>Bi-Directional Replication or BDR in PostgreSQL was developed by 2ndQuadrant for multi-master replication in PostgreSQL. Version 1.x of BDR was open-source but has already reached EOL. Versions of BDR such as 2.x and 3.x are not open-source and are generally made available only for 2ndQuadrant (now EDB) customers under commercial terms.</p>
<h2 class="wp-block-heading">Fundamentals of Logical Replication<a class="anchor-link" id="fundamentals-of-logical-replication"></a></h2>
<p>Logical decoding was introduced in PostgreSQL 9.4 and is the foundation for logical replication, adding logical decoding APIs and output plugins. This allowed PostgreSQL database users to decode the WAL into human-readable SQL statements or logical changes, such as INSERT/UPDATE/DELETE&nbsp; &mdash; depending on the decoding used, e.g. test_decoding or pgoutput.&nbsp;</p>
<p>However, there was no full replication system until the release of PostgreSQL 10. Logical Replication is effectively modeled after the pglogical implementation, which uses publication / subscribe model. In turn, this is the basis for PostgreSQL BDR.&nbsp;</p>
<p>Logical Replication allows fine-grained customizable data replication between databases, allowing you to specify the database, the table, or the schema and table/s that you want to participate in logical replication using the PUBLICATION/SUBSCRIPTION mechanism.</p>
<p>For a multi-tenant setup, leveraging logical replication is ideal when combined with schema-based filtering. This combination allows you to scope out tables specific to users, ensuring isolation for their respective data. However, for a multi-cloud setup, this approach can be cumbersome, considering the limitations of native logical replication, such as a lack of DDL replication support and no inherent conflict resolution mechanism.</p>
<h2 class="wp-block-heading">Bi-Directional Replication (BDR)<a class="anchor-link" id="bi-directional-replication-bdr"></a></h2>
<p>Bi-Directional Replication (BDR), often referred to as Postgres-BDR, is an open-source PostgreSQL extension developed by 2ndQuadrant (now part of EDB). BDR enables multi-master replication across distributed clusters. It was the first implementation of multi-master logical replication, using logical decoding internally and implemented as a patchset to PG 9.4/9.5.</p>
<p>While BDR existed, 2ndQuadrant also created pglogical, which is derived from BDR technology. It is essentially a simplified, single-master logical replication system built entirely as an extension not requiring a forked PostgreSQL. This means you have to load it through <code>shared_preload_libraries</code> parameter. pglogical became the model for Postgres 10&rsquo;s built-in logical replication.</p>
<p>Using BDR in PostgreSQL allows multiple PostgreSQL nodes to act as writable primaries simultaneously, basically allowing you to implement mesh topology or ring topology where data changes can originate from any node and propagate to others.&nbsp;</p>
<p>Unlike traditional master-slave setups, BDR supports true bi-directional (or multi-directional) data flow, making it ideal for high-availability (HA) scenarios, geographic distribution, and workloads requiring low-latency writes across regions.</p>
<p>Implementing this on a multi-tenant setup can be very convenient. As with logical replication, you can implement isolation through your database, schema, or tables to that limitation only for specific data to be replicated. On the other hand, while in a multi-cloud environment, BDR is perfect for both environments since it has the mechanism to support consistency resolution without terminating the replication. Allowing you to have continuous replication streams between your active primaries or just your primary, if the other target node is for read, data retrieval or secondary and data recovery purposes.</p>
<h2 class="wp-block-heading">Consistency Issues &amp; Conflict Resolution<a class="anchor-link" id="consistency-issues-conflict-resolution"></a></h2>
<p>Basically, the core logical replication that is available in native PostgreSQL is not true bi-directional replication. You can use CREATE PUBLICATION and CREATE SUBSCRIPTION if you want to implement a chained or ring topology simulating master-master setup.</p>
<p>Leveraging the native logical replication simulating a master-master setup requires that you have at least PostgreSQL 10. However, if you expect that you will gain a true master-master setup, then you will be sorely disappointed. The built-in logical replication allows you to implement, as mentioned earlier, with the use of PUBLICATION/SUBSCRIPTION methods, whilst it can be a problem when it comes to handling and managing primary, unique keys, and constraints. It lacks the mechanism of the following:</p>
<ul class="wp-block-list">
<li>Conflict resolution</li>
<li>DDL replication</li>
<li>Global sequences</li>
<li>Multi-master support</li>
</ul>
<p>With logical replication, when you are dealing with <code>CREATE</code><em> </em><code>TABLE</code>s, you have to make sure that the table also exists on the other target node, or the subscriber node. In addition to that, since there&rsquo;s no DDL support, it can be a struggle if you implement a multi-master setup allowing both primaries to accept writes as there&rsquo;s no global sequences support, meaning you might have issues with using sequential keys in your table such as auto-increment columns. If such duplicate keys are detected, replication shall be terminated until you fix the problem. There&rsquo;s no conflict resolution which can be tedious if your database encounters a consistency problem.</p>
<p>Whilst, with Bi-Directional Replication (Postgresql BDR), things get smoother. You simply assure you have set up your nodes properly by running setup commands. For example, nodes 192.168.40.50 and 192.168.40.51 will do a master-master setup,</p>
<pre class="wp-block-code"><code>PGPASSWORD='bdrPassw0rd' /usr/lib/edb-pge/17/bin/pgd node db1 setup   
--dsn 'host=192.168.40.50 dbname=postgres user=bdruser password=bdrPassw0rd'  
--pgdata /var/lib/edb-pge/17/main   --log-file /var/lib/edb-pge/17/pgd_log_db1.log  
--group-name pgd_group

PGPASSWORD='bdrPassw0rd' /usr/lib/edb-pge/17/bin/pgd node db2 setup 
    --dsn 'host=192.168.40.51 dbname=postgres user=bdruser password=bdrPassw0rd' 
	--cluster-dsn 'host=192.168.40.50 dbname=postgres user=bdruser password=bdrPassw0rd' 
	--group-name pgd_group  
	--pgdata /var/lib/edb-pge/17/main 
	--log-file /var/lib/edb-pge/17/pgd_log_db2.log</code></pre>
<p>Once these two nodes are set up perfectly, creating the tables, i.e. issuing a DDL statement, is straightforward as you just have to run it in one of the primary nodes and it will be replicated. In case it detects duplicate keys, replication is not terminated and a new transaction will be processed next and executed and replicated once it runs without errors.</p>
<p>If your budget is tight, BDR is free and open-source until v2; otherwise, your option is to implement it via logical replication or pglogical. pglogical is the best choice as it handles conflict resolution better.&nbsp;</p>
<p>It offers this <code>pglogical.conflict_resolution</code> allowing you to set the resolution method for any detected conflicts between local data and incoming changes. This parameter has possible values you can use to set which are <code>error</code>, <code>apply_remote</code>, <code>keep_local</code>, <code>last_update_wins</code>, <code>first_update_wins</code>.&nbsp;</p>
<p>In most setups, default value points to error, which means it will have to stop on error once conflict is detected and requires manual action to resolve the problem. Ideally, using <code>last_update_wins</code> can be your desired value which means that the version of data with the latest commit timestamp will be kept.</p>
<h2 class="wp-block-heading">Complimenting BDR with load balancing<a class="anchor-link" id="complimenting-bdr-with-load-balancing"></a></h2>
<p>Bi-Directional Replication alone does not offer you full high-availability and load balancing. Load balancing ensures that your traffic load is efficient, while high availability ensures the health and availability of your database in case one of your database nodes goes down, or even your load balancer nodes.</p>
<p>A sample diagram below would assure that you have full availability of your nodes while also ensuring that performance is horizontally balanced between your active-primary nodes.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="274" src="https://severalnines.com/wp-content/uploads/2026/02/bi-directional-diagram-1024x274.png" alt="bi-directional-diagram" class="wp-image-42677"></figure>
<p>In this topology, the complementary capabilities include:</p>
<ul class="wp-block-list">
<li>Actively distributing both read and write load,</li>
<li>Maintaining availability during node or network failures,</li>
<li>Reducing conflict risks via intelligent routing,</li>
<li>Maximizing efficiency through connection pooling.</li>
</ul>
<h2 class="wp-block-heading"><strong>Manual vs. ClusterControl-supported BDR: Pros &amp; Cons</strong><a class="anchor-link" id="manual-vs-clustercontrol-supported-bdr-pros-cons"></a></h2>
<h3 class="wp-block-heading"><strong>Manual BDR setup</strong><a class="anchor-link" id="manual-bdr-setup"></a></h3>
<p>Successfully implementing production-grade PG BDR environments meshed with high availability and load balancing requires deep understanding and high-level skills. Cost-wise, there are options you can take since PostgreSQL is purely an open-source database technology; pglogical can be a best option to set this up. There are limitations that you must be aware of but for a non-complex setup, pglogical can be enough for your multi-master setup for implementing bi-directional replication. However, it does not provide advanced features that make administering complex environments easy, like BDR&rsquo;s conflict/transform trigger, which allows you to attach triggers for incoming changes to your records/rows in your database. It offers column strategy which you can set, for example:</p>
<pre class="wp-block-code"><code>SELECT bdr.bdr_set_conflict_resolver(
  set_name := 'default',
  conflict_type := 'update_update',
  per_column := '{"total_gross":"sum", "last_txn":"last_update_wins", "notes":"keep_local"}'
);</code></pre>
<p>Going through a manual setup offers you freedom and avoids vendor lock-in. Depending on your documentation and implementation of your setup, as long as you provide the ground layer of your implementation, it will provide transparency and can set your custom requirements especially if you need complex setup amid the performance and optimization benefits that you can get.</p>
<p>But with all things, you have to consider the big picture, especially as your database grows complex and data storage becomes very challenging to scale and manage. Operational complexity and pressure can grow tremendously especially when disaster occurs and data recovery is required. Manual setup can be very challenging as doing things that you might not need, might eventually require the need of expert management that other third-party tools offer.</p>
<p>Lastly, with manual setup, it can be tedious to monitor the health of your database cluster. You might need third party tools to give you graphic-based metrics which would make it easier for you to determine common issues and pitfalls of your database. You also need alarms to throw when certain thresholds are met and this can be challenging and costly; because you need to hire devs or build your own tools to monitor and provide observability ambiance that third-party tools have integrated and can provide it for your convenience.</p>
<h3 class="wp-block-heading"><strong>ClusterControl for PG BDR operations</strong><a class="anchor-link" id="clustercontrol-for-pg-bdr-operations"></a></h3>
<p>ClusterControl offers PostgreSQL deployment using streaming replication and logical replication. A sample screenshot of dashboards of the streaming and logical replication deployments that is readily available for ClusterControl management, is shown below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="518" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-postgres_bdr-db_listing-1024x518.png" alt="" class="wp-image-42680"></figure>
<p>For logical replication deployments, using PUBLICATION/SUBSCRIPTION approach for implementing a multi-master deployment using Enterprise DB,, is shown below:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="465" src="https://severalnines.com/wp-content/uploads/2026/02/cc_ui-postgres_bdr-pub_sub-1024x465.png" alt="" class="wp-image-42679"></figure>
<p>For Enterprise DB, make sure you have your <strong>EDB Token</strong> available as this shall be required during deployment through ClusterControl&rsquo;s GUI.</p>
<p>For enterprise-grade environments, ClusterControl is tailored to do its job and provide the users sustainability and comfort when handling and managing their complex database clusters for PostgreSQL. Not limited to deployment, it offers backup management, disaster recovery support with automatic recovery option, observability with comprehensive metrics to offer. It has built-in alarms and alerts when certain thresholds are met allowing you to avoid such disaster before it shall happen. </p>
<p>This observability feature makes an ideal option for your environment as managing a complex database cluster can be tedious and you are looking for convenience and offers you technical support in case you need some technical advice and analysis for your environment and requirements as well. If you are looking for a BDR setup, ClusterControl supports deployment for the Enterprise DB (EDB) version of PostgreSQL. </p>
<p>Although it offers minimal support for its enterprise offering that EDB has, this means it allows you to set up on your own and do manual work on the ground. This might not be beneficial if you are looking for more management of complex features that BDR can offer that allows automatic setup for you or GUI-relevant support, but the ability to provide you the needs and wants that you are looking for such enterprise software, Severalnines&rsquo; ClusterControl is built on that and is tailored to that concept and principles that shall be beneficial for your enterprise-grade requirements.</p>
<h2 class="wp-block-heading"><strong>Operational best practices</strong><a class="anchor-link" id="operational-best-practices"></a></h2>
<p>Learning the fundamentals of logical replication, terminology, and how to fix conflict resolution is highly advisable. PostgreSQL technology especially with these BDR, pglogical, and it native logical replication is not easy to deal with. It requires a high-level of understanding and how databases should work. If you are an experienced DBA, learning and operating PostgreSQL and its native replication and other third-party offerings such as BDR, Bucardo, pglogical, Slony, Spock, can still be tricky but eventually you will be able to manage these technologies integrated to your setup for implementing a multi-master or bi-directional replication.&nbsp;</p>
<p>Managing it for multi-tenant and multi-cloud setups requires that you at least need tools that are built to handle conflict resolutions, advanced features that support triggers and column strategies, verbose log-level, database partitioning, and load balancing; you don&rsquo;t need to focus on implementing this from the ground up. Leverage third-party tools that are already available, and if cost is an issue, there are open-source technologies that are readily available to cater your needs.</p>
<h1 class="wp-block-heading">Conclusion<a class="anchor-link" id="conclusion"></a></h1>
<p>Using enterprise-grade technologies for managing enterprise-level databases requires an enterprise layer. Nowadays, these principles are symbiotic and tightly coupled. ClusterControl&rsquo;s enterprise level database management offers you the freedom to implement them where you would like, whether it&rsquo;s in the cloud or on-prem, while giving you features deeply coupled to your needs when implementing logical replication either using Postgres community version or Enterprise DB for your database clusters.</p>
<p>With multi-tenant and multi-cloud setups, the manual approach and using community-based technologies can meet your initial needs. However, once it grows drastically, you will need deep understanding and experience with managing complex scenarios that the tool can handle. ClusterControl is designed to address these at a high-level enterprise layer.</p>
<p>Ready to make PostgreSQL management easier and more reliable in any environment?</p>
<h2 class="wp-block-heading">Install ClusterControl in 10-minutes.&nbsp;<strong>Free 30-day&nbsp;</strong>Enterprise trial included!<a class="anchor-link" id="install-clustercontrol-in-10-minutes-free-30-day-enterprise-trial-included"></a></h2>
<h3 class="wp-block-heading">Script Installation Instructions<a class="anchor-link" id="script-installation-instructions"></a></h3>
<p>The installer script is the simplest way to get ClusterControl up and running. Run it on your chosen host, and it will take care of installing all required packages and dependencies.</p>
<p>Offline environments are supported as well. See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/offline-installation/">Offline Installation</a>&nbsp;guide for more details.</p>
<p>On the ClusterControl server, run the following commands:</p>
<pre class="wp-block-code"><code>wget https://severalnines.com/downloads/cmon/install-cc
chmod +x install-cc</code></pre>
<p>With your install script ready, run the command below. Replace&nbsp;<code>S9S_CMON_PASSWORD</code>&nbsp;and&nbsp;<code>S9S_ROOT_PASSWORD</code>&nbsp;placeholders with your choice password, or remove the environment variables from the command to interactively set the passwords. If you have multiple network interface cards, assign one IP address for the&nbsp;<code>HOST</code>&nbsp;variable in the command using&nbsp;<code>HOST=</code>.</p>
<pre class="wp-block-code"><code>S9S_CMON_PASSWORD= S9S_ROOT_PASSWORD= HOST= ./install-cc # as root or sudo user</code></pre>
<p>After the installation is complete, open a web browser, navigate to&nbsp;<code>https:///</code>, and create the first admin user by entering a username (note that &ldquo;admin&rdquo; is reserved) and a password on the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/quickstart/#step-2-create-the-first-admin-user">welcome page</a>. Once you&rsquo;re in, you can&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/create-database-cluster/">deploy</a>&nbsp;a new database cluster or&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/user-guide/deployment/import-database-cluster/">import</a>&nbsp;an existing one.</p>
<p>The installer script supports a range of environment variables for advanced setup. You can define them using export or by prefixing the install command.</p>
<p>See the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#environment-variables">list of supported variables</a>&nbsp;and&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#example-use-cases">example use cases</a>&nbsp;to tailor your installation.</p>
<h4 class="wp-block-heading">Other Installation Options</h4>
<p><strong>Helm Chart</strong></p>
<p>Deploy ClusterControl on Kubernetes using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#helm-chart">official Helm chart</a>.</p>
<p><strong>Ansible Role</strong></p>
<p>Automate installation and configuration using our&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#ansible-role">Ansible playbooks</a>.</p>
<p><strong>Puppet Module</strong></p>
<p>Manage your ClusterControl deployment with the&nbsp;<a href="https://docs.severalnines.com/clustercontrol/latest/getting-started/installation/online-installation/#puppet-module">Puppet module</a>.</p>
<h4 class="wp-block-heading">ClusterControl on Marketplaces</h4>
<p>Prefer to launch ClusterControl directly from the cloud? It&rsquo;s available on these platforms:</p>
<p><a href="https://console.cloud.google.com/marketplace/product/severalnines-public/clustercontrol">Google Cloud Platform</a></p>
<p><a href="https://marketplace.digitalocean.com/apps/clustercontrol">DigitalOcean Marketplace</a></p>
<p><a href="https://gridscale.io/en/marketplace">gridscale.io Marketplace</a></p>
<p><a href="https://www.vultr.com/marketplace/apps/clustercontrol/">Vultr Marketplace</a></p>
<p><a href="https://www.linode.com/marketplace/apps/severalnines/clustercontrol/">Linode Marketplace</a></p>
<p>The post <a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://severalnines.com/">Severalnines</a>.</p>

<p><a href="https://severalnines.com/blog/multi-tenant-multi-cloud-logical-and-bi-directional-replication-deep-dive/">Multi-Tenant, Multi-Cloud Logical and Bi-Directional Replication Deep Dive</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Row Deletion Jobs Done Right</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html</id>
      <updated>2026-03-05T13:39:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am continuing my blog post series on using indexes — or tables — as queues.  In this post, I cover Row Deletion Jobs (I do not call these purge jobs, to avoid confusion with the InnoDB Purge).  Such jobs are tempting to implement using an index, but this might be a wrong / suboptimal way.  I write about the right / better / cheaper way</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html">Row Deletion Jobs Done Right</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am continuing my blog post series on using indexes&nbsp;&mdash;&nbsp;or tables&nbsp;&mdash;&nbsp;as queues.&nbsp; In this post, I cover Row Deletion Jobs (I do not call these purge jobs, to avoid confusion with the InnoDB Purge).&nbsp; Such jobs are tempting to implement using an index, but this might be a wrong&nbsp;/&nbsp;suboptimal way.&nbsp; I write about the right&nbsp;/ better&nbsp;/ cheaper way</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html">Row Deletion Jobs Done Right</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Row Deletion Jobs Done Right</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html</id>
      <updated>2026-03-05T13:39:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am continuing my blog post series on using indexes — or tables — as queues.  In this post, I cover Row Deletion Jobs (I do not call these purge jobs, to avoid confusion with the InnoDB Purge).  Such jobs are tempting to implement using an index, but this might be a wrong / suboptimal way.  I write about the right / better / cheaper way</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html">Row Deletion Jobs Done Right</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am continuing my blog post series on using indexes&nbsp;&mdash;&nbsp;or tables&nbsp;&mdash;&nbsp;as queues.&nbsp; In this post, I cover Row Deletion Jobs (I do not call these purge jobs, to avoid confusion with the InnoDB Purge).&nbsp; Such jobs are tempting to implement using an index, but this might be a wrong&nbsp;/&nbsp;suboptimal way.&nbsp; I write about the right&nbsp;/ better&nbsp;/ cheaper way</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/row-deletion-jobs-done-right.html">Row Deletion Jobs Done Right</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL 18 OIDC Authentication with Ping Identity using pg_oidc_validator</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/03/04/postgresql-18-oidc-authentication-with-ping-identity-using-pg_oidc_validator/" />
      <id>https://percona.community/blog/2026/03/04/postgresql-18-oidc-authentication-with-ping-identity-using-pg_oidc_validator/</id>
      <updated>2026-03-04T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>PostgreSQL 18 introduced native OAuth 2.0 authentication support, marking an important step towards modern, centralized identity-based access control. However, since every identity provider implements OpenID Connect (OIDC) slightly differently, PostgreSQL delegates token validation to external validator libraries. This is where Percona’s pg_oidc_validator extension comes in - it bridges PostgreSQL with any OIDC-compliant Identity Provider.</p>
<p><a href="https://percona.community/blog/2026/03/04/postgresql-18-oidc-authentication-with-ping-identity-using-pg_oidc_validator/">PostgreSQL 18 OIDC Authentication with Ping Identity using pg_oidc_validator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>PostgreSQL 18 introduced native OAuth 2.0 authentication support, marking an important step towards modern, centralized identity-based access control. However, since every identity provider implements OpenID Connect (OIDC) slightly differently, PostgreSQL delegates token validation to external validator libraries. This is where Percona&rsquo;s <a href="https://github.com/Percona-Lab/pg_oidc_validator" target="_blank" rel="noopener noreferrer">pg_oidc_validator</a> extension comes in &ndash; it bridges PostgreSQL with any OIDC-compliant Identity Provider.</p>
<p>There are several identity and access management (IAM) solutions available today that enable Single Sign-On (SSO) using OAuth 2.0 and OpenID Connect. In an earlier blog by my colleague Zsolt, <a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/" target="_blank" rel="noopener noreferrer">OIDC in PostgreSQL: With Keycloak</a>, he demonstrated how PostgreSQL 18 can be integrated with Keycloak using pg_oidc_validator. In this post, we explore the same concept using <a href="https://www.pingidentity.com/en/platform.html" target="_blank" rel="noopener noreferrer">Ping Identity</a> (PingOne).</p>
<p>Ping Identity is widely used in enterprise environments for identity and access management. If you are in such an environment, integrating PostgreSQL directly with Ping Identity can provide access control.</p>
<p>This blog is intended for PostgreSQL users, DBAs and customers who want to evaluate or deploy OIDC authentication using pg_oidc_validator. The goal is to provide a practical step-by-step walk-through to get a working setup.</p>
<p>We will cover the following topics as part of this blog:</p>
<ul>
<li>Setting up PingOne environment</li>
<li>Install PostgreSQL 18 from Packages</li>
<li>Configure PostgreSQL for OAuth/OIDC authentication</li>
<li>Test login using an OIDC flow</li>
</ul>
<h1>Setting up PingOne environment<a class="anchor-link" id="setting-up-pingone-environment"></a></h1>
<ol>
<li>
<p>Register a new <a href="https://www.pingidentity.com/en/account/register.html" target="_blank" rel="noopener noreferrer">account</a></p>
<figure><img decoding="async" width="2048" height="1222" src="https://percona.community/blog/2026/03/ping-account-register_hu_9055dd798d72a910.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Fill in the required details to complete the profile</p>
<figure><img decoding="async" width="2048" height="1124" src="https://percona.community/blog/2026/03/ping-profile_hu_c049b5662674813b.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>The next step is to sign in to your Ping Identity <a href="https://www.pingidentity.com/en/account/sign-on.html" target="_blank" rel="noopener noreferrer">account</a></p>
<figure><img decoding="async" width="2048" height="1222" src="https://percona.community/blog/2026/03/ping-sign-on_hu_f28ddbf463ef421c.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Upon successful Sign-in, we will see Ping Identity Administrator Console. In the left navigation panel, click on Environments -&gt; <strong>Environments +</strong> (marked in red).</p>
<figure><img decoding="async" width="2048" height="1006" src="https://percona.community/blog/2026/03/ping-admin-console_hu_e0c245faa5b60dcd.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Provide an environment name and click on Finish</p>
<figure><img decoding="async" width="1999" height="1101" src="https://percona.community/blog/2026/03/ping-environment_hu_1de0ea2de65f2cd9.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Once the environment is created, click on Manage environment.</p>
<figure><img decoding="async" width="2048" height="1124" src="https://percona.community/blog/2026/03/ping-manage-environment_hu_3b5626b48f80dcd3.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>In the left navigation panel, click on Applications -&gt; Applications -&gt; click on <strong>Applications +</strong>. Fill the application name, select the application type as <strong>Device Authorization</strong> and click on Save.</p>
<figure><img decoding="async" width="2048" height="1131" src="https://percona.community/blog/2026/03/ping-new-application_hu_aae3c2c62119b6f4.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Upon successful creation, we will see generated <strong>Client ID</strong> and <strong>Issuer ID</strong>. The Issuer ID can be copied from under the <em>Connection Details</em> section. Enable the toggle so that the application is Active.</p>
<figure><img decoding="async" width="2048" height="1127" src="https://percona.community/blog/2026/03/ping-created-application_hu_ef3be57d9c95f3d1.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Once the application is successfully created, we need to add a client scope. In the left navigation panel, click on Applications -&gt; Resources -&gt; OpenID Connect. You will see a section called <em>Scopes</em> under which there is a <strong>+ Add Scope</strong> button.</p>
<figure><img decoding="async" width="3018" height="1544" src="https://percona.community/blog/2026/03/ping-click-add-scope_hu_a9b2f79e0db03afb.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Upon clicking the Add scope button, we need to fill the <em>Scope name</em> and click on Save. In our example, we are creating a scope called <em>pgscope</em></p>
<figure><img decoding="async" width="3018" height="1644" src="https://percona.community/blog/2026/03/ping-add-scope-name_hu_958186a62af4db1b.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Now, let&rsquo;s assign the custom scope we created to our client application. In the left navigation panel, click on Applications -&gt; Applications. Select the application <em>postgres</em> which we created previously and click on <em>Resource Access</em></p>
<figure><img decoding="async" width="3018" height="1644" src="https://percona.community/blog/2026/03/ping-application-config_hu_e869ced0fa01a34f.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>From the list of available scopes, select the custom scope we created and click on Save. This will assign the scope to our application.</p>
<figure><img decoding="async" width="3018" height="1644" src="https://percona.community/blog/2026/03/ping-application-add-scope_hu_abe259fdf76f4369.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>The next step is to add a new user. In the left navigation panel, click on Directory -&gt; Users and click on <strong>Users +</strong> sign.</p>
<p>Fill the username field. For our exercise, we are creating a user called <strong>employees.</strong> In some identity providers (IdPs), it is possible to customize tokens and control how certain claims (including the <strong>sub</strong> &ndash; subject claim) are generated or mapped. However, it is important to note that while Ping Identity allows customization of the ID token, the access token claims (including sub) for the default OpenID Connect resource cannot be customized. The value of sub in the access token is generated and managed internally by PingOne and cannot be altered, mapped, or derived from another attribute (such as email or username). As a result, PostgreSQL must be configured to work with the sub value exactly as issued in the access token by PingOne.</p>
<figure><img decoding="async" width="2048" height="1127" src="https://percona.community/blog/2026/03/ping-create-user_hu_63ab10a5807db6e1.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
<li>
<p>Enable the user by turning the toggle &ldquo;ON&rdquo; and set a password.</p>
<figure><img decoding="async" width="2048" height="1124" src="https://percona.community/blog/2026/03/ping-enable-user_hu_fef5b33888a4960.webp" alt="&nbsp;" loading="lazy"></figure>

</li>
</ol>
<h1>Install PostgreSQL 18 from Packages<a class="anchor-link" id="install-postgresql-18-from-packages"></a></h1>
<p>Since OAuth support is only available starting with PostgreSQL 18, we need a PostgreSQL server of at least this version.In the guide, we will install PostgreSQL 18 using Percona&rsquo;s official packages.</p>
<p><strong>Note:</strong></p>
<ol>
<li>
<p>Ensure that the <a href="https://docs.percona.com/percona-software-repositories/installing.html" target="_blank" rel="noopener noreferrer">percona-release</a> is already installed and configured on your system. You can refer to the official Percona documentation for setup instructions.</p>
</li>
<li>
<p>For this exercise, the steps are demonstrated on Ubuntu 24.04</p>
</li>
</ol>
<h2>Enable the PostgreSQL 18 repository<a class="anchor-link" id="enable-the-postgresql-18-repository"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo percona-release enable-only ppg-18.2 release
</span></span><span class="line"><span class="cl">sudo apt update</span></span></code></pre>
</div>
</div>
</div>
<h2>Install PostgreSQL 18<a class="anchor-link" id="install-postgresql-18"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install -y percona-postgresql-18</span></span></code></pre>
</div>
</div>
</div>
<h2>Install OAuth Support for libpq<a class="anchor-link" id="install-oauth-support-for-libpq"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install libpq-oauth</span></span></code></pre>
</div>
</div>
</div>
<h1>Configure PostgreSQL for OAuth/OIDC authentication<a class="anchor-link" id="configure-postgresql-for-oauth-oidc-authentication"></a></h1>
<h2>Install pg_oidc_validator<a class="anchor-link" id="install-pg_oidc_validator"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt install percona-pg-oidc-validator18</span></span></code></pre>
</div>
</div>
</div>
<h2>Setting Up a Sample Use Case for OIDC-Based Access<a class="anchor-link" id="setting-up-a-sample-use-case-for-oidc-based-access"></a></h2>
<p>Imagine the following use case:</p>
<ul>
<li>A company regularly generates promotional discount codes and stores them in a table called dcode inside a database named promo</li>
<li>A new discount code is generated and added to this table every day.</li>
<li>The company wants all employees to be able to access the latest code whenever needed.</li>
<li>For simplicity in this demonstration, employees retrieve the code by connecting to the database and querying the table directly.</li>
<li>To avoid managing individual database accounts for every employee, access is not tied to separate user credentials.</li>
<li>Instead, authentication to the database is handled through the company&rsquo;s SSO system, allowing employees to connect using their existing corporate identity.</li>
</ul>
<pre class="mermaid">
---
config:
theme: neutral
---
architecture-beta
group company_network(cloud)[Company Network]
service employee(user)[Employee] in company_network
service sso(server)[Company SSO PingIdentity] in company_network
service promo_db(database)[Promo Database] in company_network
service code_gen(server)[Daily Code Generator] in company_network
code_gen:B --&gt; T:promo_db
employee:R --&gt; L:sso
sso:R --&gt; L:promo_db
</pre>
<p><strong>Let&rsquo;s connect to PostgreSQL:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -u postgres psql</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Create a database:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">DATABASE</span><span class="w"> </span><span class="n">promo</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="err"></span><span class="k">c</span><span class="w"> </span><span class="n">promo</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Add a table to store discount codes:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">dcode</span><span class="w"> </span><span class="p">(</span><span class="n">code</span><span class="w"> </span><span class="nb">varchar</span><span class="p">(</span><span class="mi">10</span><span class="p">),</span><span class="w"> </span><span class="n">GENERATED_AT</span><span class="w"> </span><span class="k">TIMESTAMP</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="n">now</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">dcode</span><span class="w"> </span><span class="p">(</span><span class="n">code</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">'SAVENOW'</span><span class="p">);</span></span></span></code></pre>
</div>
</div>
</div>
<p><strong>Create the access user:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="n">employees</span><span class="w"> </span><span class="k">WITH</span><span class="w"> </span><span class="n">LOGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">GRANT</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">dcode</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">employees</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h2>Configure OAuth access in PostgreSQL:<a class="anchor-link" id="configure-oauth-access-in-postgresql"></a></h2>
<p>In order for users to connect using OAuth and authenticate through the Ping Identity server, we need to create an identity map and add an entry for such access on PostgreSQL&rsquo;s authentication configuration file.</p>
<p>Edit the identity mapping configuration file and add an entry, which we will call <em>oidc</em>, mapping connections originated by a system user identified with a sub ID. For this exercise, we allow any string to match</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vim /etc/postgresql/18/main/pg_ident.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># MAPNAME SYSTEM-USERNAME DATABASE-USERNAME</span>
</span></span><span class="line"><span class="cl">oidc /^<span class="o">(</span>.*<span class="o">)</span>$ employees</span></span></code></pre>
</div>
</div>
</div>
<p>Next, edit the authentication configuration file. The configuration file <em>pg_hba.conf</em> acts as a sort of firewall for connections. With the below line, we are instructing PostgreSQL to allow all connections coming from any network that attempt to access the database promo as user employees using the new authentication method <em>oauth</em>. We are also indicating that the authentication provider is Ping Identity and the scope is <em>openid</em>.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vim /etc/postgresql/18/main/pg_hba.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># TYPE DATABASE USER ADDRESS METHOD</span>
</span></span><span class="line"><span class="cl">host promo employees 0.0.0.0/0 oauth <span class="nv">issuer</span><span class="o">=</span>https://auth.pingone.com.au/64935f69-5a0a-4b69-a8bd-46967d218303/as <span class="nv">scope</span><span class="o">=</span>pgscope <span class="nv">map</span><span class="o">=</span>oidc</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Note:</strong></p>
<ol>
<li>
<p>Place this entry after the existing local rules and just before the replication rules in pg_hba.conf. PostgreSQL evaluates pg_hba.conf from top to bottom, and the first matching rule is applied. Putting the OAuth rule earlier ensures that connections to database promo as user employees use OAuth instead of falling back to password authentication.</p>
</li>
<li>
<p>In production environments, restrict the IP range instead of using <em>0.0.0.0/0</em></p>
</li>
<li>
<p>The <code>oauth_issuer</code> must exactly match the <strong>Issuer ID</strong> from your PingOne environment. The Issuer URL is unique to each PingOne environment and contains your environment UUID. It typically follows this format: <code>https://auth.pingone.com.au//as</code>. Replace <code></code> with the actual value from your PingOne environment. Do not copy the placeholder value directly.</p>
</li>
</ol>
<h2>Enabling the pg_oidc_validation extension<a class="anchor-link" id="enabling-the-pg_oidc_validation-extension"></a></h2>
<p>Edit the postgresql.conf and add below lines</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vim /etc/postgresql/18/main/postgresql.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">oauth_validator_libraries</span> <span class="o">=</span> <span class="s1">'pg_oidc_validator'</span></span></span></code></pre>
</div>
</div>
</div>
<p>The validator uses the <code>sub</code> claim from the access token by default. Hence, we need not explicitly set <code>pg_oidc_validator.authn_field=sub</code>. The sub claim is defined by the OpenID Connect specification as a stable and unique identifier for a user within an identity provider (IdP). It is intended to uniquely represent a user and remain consistent across authentication sessions.</p>
<p>PostgreSQL does not interpret or transform this value. The validator extracts the configured claim and PostgreSQL compares it against a database role or an entry in pg_ident.conf. If the value does not match the expected role or mapping, authentication will fail, even if the token itself is valid.</p>
<p>In this setup with Ping Identity, only the sub claim can be used for authentication with the default OpenID Connect resource.</p>
<h2>Reload the configuration<a class="anchor-link" id="reload-the-configuration"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo -u postgres psql
</span></span><span class="line"><span class="cl">SELECT pg_reload_conf<span class="o">()</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h2>Monitor the server logs<a class="anchor-link" id="monitor-the-server-logs"></a></h2>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tail -f /var/log/postgresql/postgresql-18-main.log</span></span></code></pre>
</div>
</div>
</div>
<h1>Test login using an OIDC flow<a class="anchor-link" id="test-login-using-an-oidc-flow"></a></h1>
<p><strong>Connecting to the database:</strong></p>
<p>For this quick connection test, we use psql to connect to the promo database as the employees user, explicitly specifying the host IP along with the <strong>oauth_issuer</strong> and <strong>client_id</strong>.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">psql <span class="s1">'host=127.0.0.1 user=employees dbname=promo oauth_issuer=https://auth.pingone.com.au/64935f69-5a0a-4b69-a8bd-46967d218303/as oauth_client_id=1e892f71-09d7-4ed6-a534-0dc888d39c7c'</span></span></span></code></pre>
</div>
</div>
</div>
<p>By connecting via the host IP address rather than the local socket, PostgreSQL treats this as a host-based connection, ensuring that the OAuth configuration in pg_hba.conf is applied. The authentication is handled by PostgreSQL&rsquo;s authentication framework, which uses OIDC with Ping Identity as the identity provider to validate the token.</p>
<p>We will see a prompt on the console with the URL and activation code.You will notice an activation code <strong>XXXX-XXX.</strong> Example shown below:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">text</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Visit https://auth.pingone.com.au/64935f69-5a0a-4b69-a8bd-46967d218303/device and enter the code: 7KK7-88DK</span></span></code></pre>
</div>
</div>
</div>
<p>Upon clicking the URL, it will prompt you to log in with the <strong>employee&rsquo;s</strong> user, which we created during the PingOne environment setup.</p>
<figure><img decoding="async" width="2048" height="1126" src="https://percona.community/blog/2026/03/ping-user-login_hu_fb39239858342406.webp" alt="&nbsp;" loading="lazy"></figure>

<p>Next, it will prompt you to enter the activation code.</p>
<figure><img decoding="async" width="2048" height="1126" src="https://percona.community/blog/2026/03/ping-activation-code_hu_8648a4b24a2bfa39.webp" alt="&nbsp;" loading="lazy"></figure>

<p>Approve access for the application, and that&rsquo;s it! The user has now been successfully authenticated via OIDC.</p>
<figure><img decoding="async" width="2048" height="1126" src="https://percona.community/blog/2026/03/ping-approve-user_hu_29ace334305789da.webp" alt="&nbsp;" loading="lazy"></figure>

<p>Return to the PostgreSQL prompt and you should see that the login to the promo database is successful. You can now query the <em>dcode</em> table to fetch the discount code.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">psql <span class="s1">'host=127.0.0.1 user=employees dbname=promo oauth_issuer=https://auth.pingone.com.au/64935f69-5a0a-4b69-a8bd-46967d218303/as oauth_client_id=1e892f71-09d7-4ed6-a534-0dc888d39c7c'</span>
</span></span><span class="line"><span class="cl">Visit https://auth.pingone.com.au/64935f69-5a0a-4b69-a8bd-46967d218303/device and enter the code: 7KK7-88DK
</span></span><span class="line"><span class="cl">psql <span class="o">(</span>18.2 - Percona Server <span class="k">for</span> PostgreSQL 18.2.1<span class="o">)</span>
</span></span><span class="line"><span class="cl">SSL connection <span class="o">(</span>protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql<span class="o">)</span>
</span></span><span class="line"><span class="cl">Type <span class="s2">"help"</span> <span class="k">for</span> help.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">promo</span><span class="o">=</span>&gt; <span class="k">select</span> * from dcode<span class="p">;</span>
</span></span><span class="line"><span class="cl"> code <span class="p">|</span> generated_at
</span></span><span class="line"><span class="cl">---------+----------------------------
</span></span><span class="line"><span class="cl"> SAVENOW <span class="p">|</span> 2026-02-13 09:40:13.109801
</span></span><span class="line"><span class="cl"><span class="o">(</span><span class="m">1</span> row<span class="o">)</span></span></span></code></pre>
</div>
</div>
</div>
<p>With the steps shown in this guide, we now have a working end-to-end setup using OIDC authentication and device flow login. From here, the same model can be extended to real-world enterprise environments with tighter network restrictions and role mapping.</p>
<p>If you run into issues while setting up pg_oidc_validator or integrating PostgreSQL with Ping Identity, check the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">community forums</a> first, chances are someone in the community may already have encountered a similar issue. If not, feel free to open a <a href="https://github.com/Percona-Lab/pg_oidc_validator/issues" target="_blank" rel="noopener noreferrer">discussion</a> or raise a request for help.</p>

<p><a href="https://percona.community/blog/2026/03/04/postgresql-18-oidc-authentication-with-ping-identity-using-pg_oidc_validator/">PostgreSQL 18 OIDC Authentication with Ping Identity using pg_oidc_validator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The &#8220;bus factor&#8221; risk in MongoDB, MariaDB, Redis, MySQL, PostgreSQL, and SQLite</title>
      <link rel="alternate" type="text/html" href="https://programmingbrain.com/2025/03/bus-factor-risk-in-open-source-databases.html" />
      <id>https://programmingbrain.com/2025/03/bus-factor-risk-in-open-source-databases.html</id>
      <updated>2026-03-03T14:01:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Features and performance are important when choosing databases, but so it is the “bus factor” risk</p>
<p><a href="https://programmingbrain.com/2025/03/bus-factor-risk-in-open-source-databases.html">The &#8220;bus factor&#8221; risk in MongoDB, MariaDB, Redis, MySQL, PostgreSQL, and SQLite</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Features and performance are important when choosing databases, but so it is the &ldquo;bus factor&rdquo; risk</p>

<p><a href="https://programmingbrain.com/2025/03/bus-factor-risk-in-open-source-databases.html">The &#8220;bus factor&#8221; risk in MongoDB, MariaDB, Redis, MySQL, PostgreSQL, and SQLite</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html</id>
      <updated>2026-03-02T19:47:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am starting a blog post series on using indexes — or tables — as queues.  I had this series in the back of my mind for some time.  This started a few years back when I worked on optimizing a row deletion job (I do not call this a purge job, to avoid confusion with the InnoDB Purge).  Such jobs can be generalized to using indexes (or tables) as queues (this is</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html">Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am starting a blog post series on using indexes&nbsp;&mdash;&nbsp;or tables&nbsp;&mdash;&nbsp;as queues.&nbsp; I had this series in the back of my mind for some time.&nbsp; This started a few years back when I worked on optimizing a row deletion job (I do not call this a purge job, to avoid confusion with the InnoDB Purge).&nbsp; Such jobs can be generalized to using indexes (or tables) as queues (this is</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html">Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html" />
      <id>https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html</id>
      <updated>2026-03-02T19:47:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am starting a blog post series on using indexes — or tables — as queues.  I had this series in the back of my mind for some time.  This started a few years back when I worked on optimizing a row deletion job (I do not call this a purge job, to avoid confusion with the InnoDB Purge).  Such jobs can be generalized to using indexes (or tables) as queues (this is</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html">Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am starting a blog post series on using indexes&nbsp;&mdash;&nbsp;or tables&nbsp;&mdash;&nbsp;as queues.&nbsp; I had this series in the back of my mind for some time.&nbsp; This started a few years back when I worked on optimizing a row deletion job (I do not call this a purge job, to avoid confusion with the InnoDB Purge).&nbsp; Such jobs can be generalized to using indexes (or tables) as queues (this is</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/03/mind-the-innodb-purge-on-queue-or-row-deletion-job-else-slow-queries.html">Mind the InnoDB Purge on Queue / Row Deletion Job (else slow queries)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Hardening MySQL: Practical Security Strategies for DBAs</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/03/02/hardening-mysql-practical-security-strategies-for-dbas/" />
      <id>https://percona.community/blog/2026/03/02/hardening-mysql-practical-security-strategies-for-dbas/</id>
      <updated>2026-03-02T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>MySQL Security Best Practices: A Practical Guide for Locking Down Your Database Introduction MySQL runs just about everywhere. I’ve seen it behind small personal projects, internal tools, SaaS platforms, and large enterprise systems handling serious transaction volume. When your database sits at the center of everything, it becomes part of your security perimeter whether you planned it that way or not. And that makes it a target.</p>
<p><a href="https://percona.community/blog/2026/03/02/hardening-mysql-practical-security-strategies-for-dbas/">Hardening MySQL: Practical Security Strategies for DBAs</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1>MySQL Security Best Practices: A Practical Guide for Locking Down Your Database<a class="anchor-link" id="mysql-security-best-practices-a-practical-guide-for-locking-down-your-database"></a></h1>
<h2>Introduction<a class="anchor-link" id="introduction"></a></h2>
<p>MySQL runs just about everywhere. I&rsquo;ve seen it behind small personal projects, internal tools, SaaS platforms, and large enterprise systems handling serious transaction volume. When your database sits at the center of everything, it becomes part of your security perimeter whether you planned it that way or not. And that makes it a target.</p>
<p>Securing MySQL isn&rsquo;t about flipping one magical setting and calling it done. It&rsquo;s about layers. Tight access control. Encrypted connections. Clear visibility into what&rsquo;s happening on the server. And operational discipline that doesn&rsquo;t drift over time.</p>
<p>In this guide, I&rsquo;m going to walk through practical MySQL security best practices that you can apply right away. These are the kinds of checks and hardening steps that reduce real risk in real environments, and help build a database platform that stays resilient under pressure.</p>
<hr>
<h2>1. Principle of Least Privilege<a class="anchor-link" id="1-principle-of-least-privilege"></a></h2>
<p>One of the most common security mistakes is over-granting privileges.<br>
Applications and users should have only the permissions they absolutely<br>
need.</p>
<h3>Bad Practice<a class="anchor-link" id="bad-practice"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">ALL</span><span class="w"> </span><span class="k">PRIVILEGES</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="o">*</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">'appuser'</span><span class="o">@</span><span class="s1">'10.%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Better Approach<a class="anchor-link" id="better-approach"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="k">SELECT</span><span class="p">,</span><span class="w"> </span><span class="k">INSERT</span><span class="p">,</span><span class="w"> </span><span class="k">UPDATE</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">appdb</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">'appuser'</span><span class="o">@</span><span class="s1">'10.%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Recommendations<a class="anchor-link" id="recommendations"></a></h3>
<ul>
<li>Avoid global privileges unless absolutely required</li>
<li>Restrict users by host whenever possible</li>
<li>Separate admin accounts from application accounts</li>
<li>Use different credentials for read-only vs write operations</li>
</ul>
<h3>Audit Existing Privileges<a class="anchor-link" id="audit-existing-privileges"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">user</span><span class="p">,</span><span class="w"> </span><span class="k">host</span><span class="p">,</span><span class="w"> </span><span class="n">Select_priv</span><span class="p">,</span><span class="w"> </span><span class="n">Insert_priv</span><span class="p">,</span><span class="w"> </span><span class="n">Update_priv</span><span class="p">,</span><span class="w"> </span><span class="n">Delete_priv</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<hr>
<h2>2. Strong Authentication &amp; Password Policies<a class="anchor-link" id="2-strong-authentication-password-policies"></a></h2>
<p>Weak credentials remain one of the easiest attack vectors.</p>
<h3>Enable Password Validation<a class="anchor-link" id="enable-password-validation"></a></h3>
<p>component_validate_password is MySQL&rsquo;s modern password policy engine. Think of it as a gatekeeper for credential quality. Every time someone tries to set or change a password, it checks whether that password meets your defined security standards before letting it in.</p>
<p>It replaces the older validate_password plugin with a component-based architecture that is more flexible and better aligned with MySQL 8.x design.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">COMPONENT</span><span class="w"> </span><span class="s1">'file://component_validate_password'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>What It Does<a class="anchor-link" id="what-it-does"></a></h3>
<p>When enabled, it enforces rules such as:</p>
<ul>
<li>Minimum password length</li>
<li>Required mix of character types</li>
<li>Dictionary file checks</li>
<li>Strength scoring</li>
</ul>
<p>If a password fails policy, the statement is rejected before the credential is stored.</p>
<h3>Why It Matters<a class="anchor-link" id="why-it-matters"></a></h3>
<p>Weak passwords remain one of the most common entry points in database breaches. This component reduces risk by enforcing baseline credential hygiene automatically, instead of relying on developer discipline.</p>
<h3>Recommended Policies<a class="anchor-link" id="recommended-policies"></a></h3>
<ul>
<li>Minimum length: 14+ characters</li>
<li>Require mixed case, numbers, and symbols</li>
<li>Enable dictionary checks</li>
<li>Enable username checks</li>
</ul>
<h3>Remove Anonymous Accounts<a class="anchor-link" id="remove-anonymous-accounts"></a></h3>
<h4>Find Anonymous Users</h4>
<p>Anonymous users have an empty User field.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">user</span><span class="p">,</span><span class="w"> </span><span class="k">host</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">mysql</span><span class="p">.</span><span class="k">user</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="k">user</span><span class="o">=</span><span class="s1">''</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>If you see rows returned, those are anonymous accounts.</p>
<h3>Drop Anonymous Users<a class="anchor-link" id="drop-anonymous-users"></a></h3>
<p>In modern MySQL versions:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">DROP</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">''</span><span class="o">@</span><span class="s1">'localhost'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">''</span><span class="o">@</span><span class="s1">'%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Adjust the Host value based on what your query returned.</p>
<h3>Why This Matters<a class="anchor-link" id="why-this-matters"></a></h3>
<p>Anonymous users:</p>
<ul>
<li>Allow login without credentials</li>
<li>May have default privileges in some distributions</li>
<li>Increase the attack surface unnecessarily</li>
</ul>
<p>In hardened environments, there should be zero accounts with an empty username. Every identity should be explicit, accountable, and least-privileged.</p>
<h2>3. Encryption Everywhere<a class="anchor-link" id="3-encryption-everywhere"></a></h2>
<p>Encryption protects data both in transit and at rest.</p>
<h3>Enable Transparent Data Encryption (TDE)<a class="anchor-link" id="enable-transparent-data-encryption-tde"></a></h3>
<p>See my January 13 post for a deep dive into Transparent Data Encryption:<br>
<a href="https://percona.community/blog/2026/01/13/configuring-the-component-keyring-in-percona-server-and-pxc-8.4/" target="_blank" rel="noopener noreferrer">Configuring the Component Keyring in Percona Server and PXC 8.4</a></p>
<h3>Enable TLS for Connections<a class="anchor-link" id="enable-tls-for-connections"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">require_secure_transport</span><span class="o">=</span><span class="k">ON</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Verify SSL Usage<a class="anchor-link" id="verify-ssl-usage"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Ssl_cipher'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Encryption Areas to Consider<a class="anchor-link" id="encryption-areas-to-consider"></a></h3>
<ul>
<li>Client-server connections</li>
<li>Replication channels</li>
<li>Backups and snapshot storage</li>
<li>Disk-level encryption</li>
</ul>
<h2>4. Patch Management &amp; Version Hygiene<a class="anchor-link" id="4-patch-management-version-hygiene"></a></h2>
<p>Running outdated MySQL versions is equivalent to leaving known<br>
vulnerabilities exposed.</p>
<h3>Maintenance Strategy<a class="anchor-link" id="maintenance-strategy"></a></h3>
<ul>
<li>Track vendor security advisories</li>
<li>Apply minor updates regularly</li>
<li>Test patches in staging before production rollout</li>
<li>Avoid unsupported MySQL versions</li>
</ul>
<h3>Check Version<a class="anchor-link" id="check-version"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">VERSION</span><span class="p">();</span></span></span></code></pre>
</div>
</div>
</div>
<h2>5. Logging, Auditing, and Monitoring<a class="anchor-link" id="5-logging-auditing-and-monitoring"></a></h2>
<p>Security without visibility is blind defense, enable Audit Logging.</p>
<h3>1. audit_log Plugin (Legacy Model)<a class="anchor-link" id="1-audit_log-plugin-legacy-model"></a></h3>
<h4>Installation</h4>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">PLUGIN</span><span class="w"> </span><span class="n">audit_log</span><span class="w"> </span><span class="n">SONAME</span><span class="w"> </span><span class="s1">'audit_log.so'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h4>Verify</h4>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">PLUGINS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'audit%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>2. audit_log_filter Component (Modern Model)<a class="anchor-link" id="2-audit_log_filter-component-modern-model"></a></h3>
<p>Introduced in MySQL 8 to provide a more flexible and granular alternative to the older plugin model.</p>
<h4>Installation</h4>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">INSTALL</span><span class="w"> </span><span class="n">COMPONENT</span><span class="w"> </span><span class="s1">'file://component_audit_log_filter'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h4>Verify</h4>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">mysql</span><span class="p">.</span><span class="n">component</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h4>Architecture Difference</h4>
<p>Instead of a single global policy, you create:</p>
<ul>
<li>Filters (define what to log)</li>
<li>Users assigned to filters</li>
</ul>
<p>It&rsquo;s granular and rule-driven.</p>
<h3>Auditing Key Events<a class="anchor-link" id="auditing-key-events"></a></h3>
<ul>
<li>Failed logins</li>
<li>Privilege changes</li>
<li>Schema modifications</li>
<li>Unusual query activity</li>
</ul>
<h3>References:<a class="anchor-link" id="references"></a></h3>
<ol>
<li><a href="https://percona.community/blog/2025/09/18/audit-log-filter-component/" target="_blank" rel="noopener noreferrer">Audit Log Filter Component<br>
</a></li>
<li><a href="https://percona.community/blog/2025/10/08/audit-log-filters-part-ii/" target="_blank" rel="noopener noreferrer">Audit Log Filters Part II<br>
</a></li>
</ol>
<h3>Useful Metrics<a class="anchor-link" id="useful-metrics"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Aborted_connects'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Connections'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h2>6. Secure Configuration Hardening<a class="anchor-link" id="6-secure-configuration-hardening"></a></h2>
<p>A secure baseline configuration reduces risk from common attack<br>
patterns.</p>
<h3>Recommended Settings<a class="anchor-link" id="recommended-settings"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">ini</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">local_infile</span><span class="o">=</span><span class="s">OFF</span>
</span></span><span class="line"><span class="cl"><span class="na">secure_file_priv</span><span class="o">=</span><span class="s">/var/lib/mysql-files</span>
</span></span><span class="line"><span class="cl"><span class="na">sql_mode</span><span class="o">=</span><span class="s">"STRICT_ALL_TABLES"</span>
</span></span><span class="line"><span class="cl"><span class="na">secure-log-path</span><span class="o">=</span><span class="s">/var/log/mysql</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Why These Matter<a class="anchor-link" id="why-these-matter"></a></h3>
<ul>
<li>Prevent arbitrary file imports</li>
<li>Reduce filesystem abuse</li>
<li>Restrict data export/import locations</li>
</ul>
<h2>7. Backup Security<a class="anchor-link" id="7-backup-security"></a></h2>
<p>Backups often contain everything an attacker wants.</p>
<h3>Backup Best Practices<a class="anchor-link" id="backup-best-practices"></a></h3>
<ul>
<li>Encrypt backups</li>
<li>Restrict filesystem permissions</li>
<li>Store offsite copies securely</li>
<li>Rotate backup credentials</li>
<li>Verify restore procedures regularly</li>
</ul>
<h3>Example Permission Check<a class="anchor-link" id="example-permission-check"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ls -l /backup/mysql</span></span></code></pre>
</div>
</div>
</div>
<h2>8. Replication &amp; Cluster Security<a class="anchor-link" id="8-replication-cluster-security"></a></h2>
<p>Replication is not just a data distribution feature. It is a persistent, privileged communication channel between servers. If misconfigured, it can become a lateral movement pathway inside your infrastructure. Treat every replication link as a trusted but tightly controlled corridor.</p>
<p>Principle: Replication Is a Privileged Service Account</p>
<p>Replication users require elevated capabilities. They must be isolated, tightly scoped, and monitored like any other service identity.</p>
<h3>Secure Replication Users<a class="anchor-link" id="secure-replication-users"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">USER</span><span class="w"> </span><span class="s1">'repl'</span><span class="o">@</span><span class="s1">'10.%'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">IDENTIFIED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="s1">'strongpassword'</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">REQUIRE</span><span class="w"> </span><span class="n">SSL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">GRANT</span><span class="w"> </span><span class="n">REPLICATION</span><span class="w"> </span><span class="n">REPLICA</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="o">*</span><span class="p">.</span><span class="o">*</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="s1">'repl'</span><span class="o">@</span><span class="s1">'10.%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Hardening considerations:</p>
<ul>
<li>Restrict host patterns as narrowly as possible. Avoid % whenever feasible.</li>
<li>Require SSL or X.509 certificate authentication.</li>
<li>Enforce strong password policies or use a secrets manager.</li>
<li>Disable interactive login capability if applicable.</li>
</ul>
<h3>Encrypt Replication Traffic<a class="anchor-link" id="encrypt-replication-traffic"></a></h3>
<p>Replication traffic may include sensitive row data, DDL statements, and metadata. Always encrypt it.</p>
<p>At minimum:</p>
<ul>
<li>Enable require_secure_transport=ON</li>
<li>Configure TLS certificates on source and replica</li>
<li>Set replication channel to use SSL:</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">CHANGE</span><span class="w"> </span><span class="n">REPLICATION</span><span class="w"> </span><span class="k">SOURCE</span><span class="w"> </span><span class="k">TO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">SOURCE_SSL</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">SOURCE_SSL_CA</span><span class="o">=</span><span class="s1">'/path/ca.pem'</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">SOURCE_SSL_CERT</span><span class="o">=</span><span class="s1">'/path/client-cert.pem'</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">SOURCE_SSL_KEY</span><span class="o">=</span><span class="s1">'/path/client-key.pem'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>For MySQL Group Replication or InnoDB Cluster:</p>
<ul>
<li>Enable group communication SSL</li>
<li>Validate certificate identity</li>
<li>Use dedicated replication networks</li>
</ul>
<h3>Binary Log and Relay Log Protection<a class="anchor-link" id="binary-log-and-relay-log-protection"></a></h3>
<p>Replication relies on binary logs. Protect them.</p>
<ul>
<li>Set binlog_encryption=ON</li>
<li>Set relay_log_info_repository=TABLE</li>
<li>Restrict filesystem access to log directories</li>
<li>Monitor log retention policies</li>
</ul>
<p>Compromised binary logs can reveal historical data changes.</p>
<h2>9. Continuous Security Reviews<a class="anchor-link" id="9-continuous-security-reviews"></a></h2>
<p>Security is not a one-time checklist. Regular audits help catch<br>
configuration drift and evolving threats.</p>
<h3>Suggested Review Cadence<a class="anchor-link" id="suggested-review-cadence"></a></h3>
<ul>
<li>Weekly: failed login review</li>
<li>Monthly: privilege audits</li>
<li>Quarterly: configuration review</li>
<li>Semiannually: full security assessment</li>
</ul>
<h2>Security Checklist Summary<a class="anchor-link" id="security-checklist-summary"></a></h2>
<table>
<thead>
<tr>
<th>Area</th>
<th>Key Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Access Control</td>
<td>Least privilege grants</td>
</tr>
<tr>
<td>Authentication</td>
<td>Strong password policies</td>
</tr>
<tr>
<td>Encryption</td>
<td>TLS + encrypted storage</td>
</tr>
<tr>
<td>Updates</td>
<td>Regular patching</td>
</tr>
<tr>
<td>Monitoring</td>
<td>Audit logging enabled</td>
</tr>
<tr>
<td>Configuration</td>
<td>Harden defaults</td>
</tr>
<tr>
<td>Backups</td>
<td>Encrypt and protect</td>
</tr>
<tr>
<td>Replication</td>
<td>Secure replication users</td>
</tr>
</tbody>
</table>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>Strong MySQL security doesn&rsquo;t come from one feature or one tool. It comes from layers working together. Hardened configuration. Tight, intentional privilege design. Encryption everywhere it makes sense. And monitoring that actually gets reviewed instead of just written to disk.</p>
<p>In my experience, the strongest environments aren&rsquo;t the ones trying to be unbreakable. They&rsquo;re the ones built to detect, contain, and respond. Every layer should either reduce blast radius or increase visibility. If an attacker gets through one control, the next one slows them down. And while they&rsquo;re slowing down, your logging and monitoring should already be telling you something isn&rsquo;t right.</p>
<p>That&rsquo;s what a mature security posture looks like in practice.</p>

<p><a href="https://percona.community/blog/2026/03/02/hardening-mysql-practical-security-strategies-for-dbas/">Hardening MySQL: Practical Security Strategies for DBAs</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Does Every PXC Node Need XtraBackup Installed?</title>
      <link rel="alternate" type="text/html" href="https://anothermysqldba.blogspot.com/2026/03/does-every-pxc-node-need-xtrabackup.html" />
      <id>https://anothermysqldba.blogspot.com/2026/03/does-every-pxc-node-need-xtrabackup.html</id>
      <updated>2026-03-01T19:11:00+02:00</updated>
      <author><name>Keith Larson ( anothermysqldba )</name></author>
      <summary type="html"><![CDATA[<p>One question that surfaces regularly in the Percona forums: Does every node in a Percona XtraDB Cluster (PXC) need to have XtraBackup installed? It\'s a fair question, especially when managing a mixed environment or trying to minimize the software footprint on certain nodes. Here is what the actual mechanics and testing confirm.</p>
<p>The Short Answer (But Read On)</p>
<p>It depends on what you want that node to do. The nuance matters quite a bit here, so it is worth walking through how State Snapshot Transfer (SST) works in PXC and why XtraBackup\'s presence — or absence — on a given node is significant.</p>
<p>A Quick Refresher on SST in PXC</p>
<p>When a new node joins a Percona XtraDB Cluster, or when an existing node has been down long enough that Incremental State Transfer (IST) is no longer possible, the cluster performs a State Snapshot Transfer (SST). This is essentially a full data copy from a donor node to the joiner node.</p>
<p>PXC supports multiple SST methods, configured in my.cnf:</p>
<p>[mysqld]<br />
wsrep_sst_method = xtrabackup-v2</p>
<p>The available SST methods include:</p>
<p> xtrabackup-v2 — The recommended method for PXC, using Percona XtraBackup; performs SST without locking the donor for extended periods<br />
 clone — Available in PXC 8.0.22+ using MySQL\'s built-in Clone Plugin; removes the XtraBackup dependency for SST<br />
 mysqldump — Slower and locks the donor during transfer; not recommended for production<br />
 rsync — Requires the donor to be read-only during transfer, blocking writes; also not recommended for live clusters</p>
<p>The xtrabackup-v2 method has historically been the default approach and remains widely used in existing deployments precisely because it keeps the donor node available for writes during the transfer. The other legacy methods can block writes on the donor, which is generally unacceptable in a production cluster. Note that Percona has been increasingly recommending the clone method for new installations on PXC 8.0.22 and later, as it removes the external tool dependency at the SST layer.</p>
<p>Where Does XtraBackup Need to Be Installed?</p>
<p>When an SST using xtrabackup-v2 is triggered, both the donor and the joiner need XtraBackup installed and accessible. Here is why both sides are involved:</p>
<p> The donor runs XtraBackup to stream the snapshot data outbound<br />
 The joiner runs XtraBackup — specifically the xbstream and xbcrypt utilities — to receive and apply that streamed data</p>
<p>If the joiner node does not have XtraBackup installed and you attempt to bring it into the cluster using xtrabackup-v2, the SST will fail. The error log on the joiner will typically show something like this:</p>
<p>[ERROR] WSREP: Failed to read \'ready \' from: wsrep_sst_xtrabackup-v2<br />
...<br />
wsrep_sst_xtrabackup-v2: line 522: xbstream: command not found<br />
[ERROR] WSREP: SST failed: 2 (No such file or directory)</p>
<p>That is a clear and unambiguous failure mode. If xbstream is not present on the joiner, the SST will not complete.</p>
<p>What About Nodes That Will Never Be a Joiner?</p>
<p>Technically, if a node will always act as a donor and never needs to rejoin the cluster from scratch, you could argue it only needs XtraBackup in its donor capacity. In practice, however, any node can become a joiner — after a crash, after planned maintenance, or after recovering from a network partition. There is no reliable way to guarantee a node will never need to receive an SST.</p>
<p>The practical guidance here is straightforward: install XtraBackup on every PXC node, without exception. The overhead of having it installed is negligible. The cost of a failed SST during an unplanned outage is not.</p>
<p>The Clone Plugin Alternative (PXC 8.0.22+)</p>
<p>Starting with PXC 8.0.22, Percona added support for the MySQL Clone Plugin as an SST method. This is worth knowing about because it removes the XtraBackup dependency for SST purposes entirely:</p>
<p>[mysqld]<br />
wsrep_sst_method = clone</p>
<p>With the clone method, the Clone Plugin must be loaded on all nodes:</p>
<p>INSTALL PLUGIN clone SONAME \'mysql_clone.so\';<br />
SHOW PLUGINS WHERE Name = \'clone\';</p>
<p>+-------+--------+-------+----------------+---------+<br />
&#124; Name &#124; Status &#124; Type &#124; Library    &#124; License &#124;<br />
+-------+--------+-------+----------------+---------+<br />
&#124; clone &#124; ACTIVE &#124; CLONE &#124; mysql_clone.so &#124; GPL   &#124;<br />
+-------+--------+-------+----------------+---------+</p>
<p>The clone method is a solid option for standardizing without XtraBackup as an SST dependency. That said, XtraBackup still has real value for your external backup strategy regardless of which SST method you choose. SST is a cluster synchronization mechanism — it is not a backup, and it should never be treated as one.</p>
<p>Checking Your Current SST Configuration</p>
<p>You can verify your current SST method and Galera-related settings with:</p>
<p>SHOW VARIABLES LIKE \'wsrep_sst_method\';</p>
<p>+------------------+---------------+<br />
&#124; Variable_name  &#124; Value     &#124;<br />
+------------------+---------------+<br />
&#124; wsrep_sst_method &#124; xtrabackup-v2 &#124;<br />
+------------------+---------------+</p>
<p>To check cluster state and confirm which node may be acting as donor:</p>
<p>SHOW STATUS LIKE \'wsrep_local_state_comment\';</p>
<p>+---------------------------+--------+<br />
&#124; Variable_name       &#124; Value &#124;<br />
+---------------------------+--------+<br />
&#124; wsrep_local_state_comment &#124; Synced &#124;<br />
+---------------------------+--------+</p>
<p>SHOW STATUS LIKE \'wsrep_connected\';</p>
<p>+-----------------+-------+<br />
&#124; Variable_name  &#124; Value &#124;<br />
+-----------------+-------+<br />
&#124; wsrep_connected &#124; ON  &#124;<br />
+-----------------+-------+</p>
<p>SHOW STATUS LIKE \'wsrep_cluster_size\';</p>
<p>+--------------------+-------+<br />
&#124; Variable_name   &#124; Value &#124;<br />
+--------------------+-------+<br />
&#124; wsrep_cluster_size &#124; 3   &#124;<br />
+--------------------+-------+</p>
<p>Practical Observations</p>
<p>A few things worth noting from working with PXC environments directly:</p>
<p> The version of XtraBackup must match your PXC version. Using XtraBackup 2.x with PXC 8.0 will cause SST failures. Use Percona XtraBackup 8.0 with PXC 8.0, and confirm version alignment after any upgrade.<br />
 Even if you switch to the clone SST method, keep XtraBackup installed for scheduled backups. Your backup strategy and your SST method are separate concerns and should be treated as such.<br />
 The wsrep_sst_donor variable lets you specify a preferred donor node, which is useful for directing SST away from your busiest or most latency-sensitive member.<br />
 If you are running Percona Toolkit alongside PXC, be aware of how DDL replication works in your specific PXC version — Total Order Isolation (TOI) versus Rolling Schema Upgrade (RSU) behavior differs and is worth a dedicated look before running schema changes in production.</p>
<p>Summary</p>
<p>To answer the question directly: if you are using xtrabackup-v2 as your SST method — which remains the default in many existing PXC deployments — then yes, XtraBackup needs to be installed on every cluster member. Any node can be either a donor or a joiner depending on circumstances, and both roles require XtraBackup to be present when using this method.</p>
<p>If you are on PXC 8.0.22 or later and want to eliminate that dependency at the SST layer, the Clone Plugin method is a viable alternative and is increasingly Percona\'s recommended choice for new deployments. If you are starting fresh, PXC 8.4 LTS is the current long-term support release and the recommended target for new installations. Even when using clone for SST, XtraBackup remains the right tool for your actual backup jobs.</p>
<p>Do not try to save a few megabytes of disk space by skipping XtraBackup on select nodes. The SST failure that eventually results from that decision is not a trade-off worth making.</p>
<p>Resources</p>
<p> Percona XtraDB Cluster SST Documentation<br />
 wsrep_sst_method System Variable Reference<br />
 Percona XtraBackup 8.0 Documentation<br />
 Percona XtraDB Cluster 8.4 LTS Documentation<br />
 Percona Community Forums</p>
<p><a href="https://anothermysqldba.blogspot.com/2026/03/does-every-pxc-node-need-xtrabackup.html">Does Every PXC Node Need XtraBackup Installed?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>One question that surfaces regularly in the Percona forums: <em>Does every node in a Percona XtraDB Cluster (PXC) need to have XtraBackup installed?</em> It&rsquo;s a fair question, especially when managing a mixed environment or trying to minimize the software footprint on certain nodes. Here is what the actual mechanics and testing confirm.</p>
<h3>The Short Answer (But Read On)<a class="anchor-link" id="the-short-answer-but-read-on"></a></h3>
<p><strong>It depends on what you want that node to do.</strong> The nuance matters quite a bit here, so it is worth walking through how State Snapshot Transfer (SST) works in PXC and why XtraBackup&rsquo;s presence &mdash; or absence &mdash; on a given node is significant.</p>
<h3>A Quick Refresher on SST in PXC<a class="anchor-link" id="a-quick-refresher-on-sst-in-pxc"></a></h3>
<p>When a new node joins a Percona XtraDB Cluster, or when an existing node has been down long enough that Incremental State Transfer (IST) is no longer possible, the cluster performs a <strong>State Snapshot Transfer (SST)</strong>. This is essentially a full data copy from a donor node to the joiner node.</p>
<p>PXC supports multiple SST methods, configured in <code>my.cnf</code>:</p>
<pre><code>[mysqld]
wsrep_sst_method = xtrabackup-v2
</code></pre>
<p>The available SST methods include:</p>
<ul>
<li><strong>xtrabackup-v2</strong> &mdash; The recommended method for PXC, using Percona XtraBackup; performs SST without locking the donor for extended periods</li>
<li><strong>clone</strong> &mdash; Available in PXC 8.0.22+ using MySQL&rsquo;s built-in Clone Plugin; removes the XtraBackup dependency for SST</li>
<li><strong>mysqldump</strong> &mdash; Slower and locks the donor during transfer; not recommended for production</li>
<li><strong>rsync</strong> &mdash; Requires the donor to be read-only during transfer, blocking writes; also not recommended for live clusters</li>
</ul>
<p>The <code>xtrabackup-v2</code> method has historically been the default approach and remains widely used in existing deployments precisely because it keeps the donor node available for writes during the transfer. The other legacy methods can block writes on the donor, which is generally unacceptable in a production cluster. Note that Percona has been increasingly recommending the <code>clone</code> method for new installations on PXC 8.0.22 and later, as it removes the external tool dependency at the SST layer.</p>
<h3>Where Does XtraBackup Need to Be Installed?<a class="anchor-link" id="where-does-xtrabackup-need-to-be-installed"></a></h3>
<p>When an SST using <code>xtrabackup-v2</code> is triggered, <strong>both the donor and the joiner</strong> need XtraBackup installed and accessible. Here is why both sides are involved:</p>
<ul>
<li>The <strong>donor</strong> runs XtraBackup to stream the snapshot data outbound</li>
<li>The <strong>joiner</strong> runs XtraBackup &mdash; specifically the <code>xbstream</code> and <code>xbcrypt</code> utilities &mdash; to receive and apply that streamed data</li>
</ul>
<p>If the joiner node does not have XtraBackup installed and you attempt to bring it into the cluster using <code>xtrabackup-v2</code>, the SST will fail. The error log on the joiner will typically show something like this:</p>
<pre><code>[ERROR] WSREP: Failed to read 'ready ' from: wsrep_sst_xtrabackup-v2
...
wsrep_sst_xtrabackup-v2: line 522: xbstream: command not found
[ERROR] WSREP: SST failed: 2 (No such file or directory)
</code></pre>
<p>That is a clear and unambiguous failure mode. If <code>xbstream</code> is not present on the joiner, the SST will not complete.</p>
<h3>What About Nodes That Will Never Be a Joiner?<a class="anchor-link" id="what-about-nodes-that-will-never-be-a-joiner"></a></h3>
<p>Technically, if a node will always act as a donor and never needs to rejoin the cluster from scratch, you could argue it only needs XtraBackup in its donor capacity. In practice, however, any node can become a joiner &mdash; after a crash, after planned maintenance, or after recovering from a network partition. There is no reliable way to guarantee a node will never need to receive an SST.</p>
<p>The practical guidance here is straightforward: <strong>install XtraBackup on every PXC node, without exception.</strong> The overhead of having it installed is negligible. The cost of a failed SST during an unplanned outage is not.</p>
<h3>The Clone Plugin Alternative (PXC 8.0.22+)<a class="anchor-link" id="the-clone-plugin-alternative-pxc-8-0-22"></a></h3>
<p>Starting with PXC 8.0.22, Percona added support for the <strong>MySQL Clone Plugin</strong> as an SST method. This is worth knowing about because it removes the XtraBackup dependency for SST purposes entirely:</p>
<pre><code>[mysqld]
wsrep_sst_method = clone
</code></pre>
<p>With the clone method, the Clone Plugin must be loaded on all nodes:</p>
<pre><code>INSTALL PLUGIN clone SONAME 'mysql_clone.so';
SHOW PLUGINS WHERE Name = 'clone';
</code></pre>
<pre><code>+-------+--------+-------+----------------+---------+
| Name  | Status | Type  | Library        | License |
+-------+--------+-------+----------------+---------+
| clone | ACTIVE | CLONE | mysql_clone.so | GPL     |
+-------+--------+-------+----------------+---------+
</code></pre>
<p>The clone method is a solid option for standardizing without XtraBackup as an SST dependency. That said, XtraBackup still has real value for your <strong>external backup strategy</strong> regardless of which SST method you choose. SST is a cluster synchronization mechanism &mdash; it is not a backup, and it should never be treated as one.</p>
<h3>Checking Your Current SST Configuration<a class="anchor-link" id="checking-your-current-sst-configuration"></a></h3>
<p>You can verify your current SST method and Galera-related settings with:</p>
<pre><code>SHOW VARIABLES LIKE 'wsrep_sst_method';
</code></pre>
<pre><code>+------------------+---------------+
| Variable_name    | Value         |
+------------------+---------------+
| wsrep_sst_method | xtrabackup-v2 |
+------------------+---------------+
</code></pre>
<p>To check cluster state and confirm which node may be acting as donor:</p>
<pre><code>SHOW STATUS LIKE 'wsrep_local_state_comment';
</code></pre>
<pre><code>+---------------------------+--------+
| Variable_name             | Value  |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+
</code></pre>
<pre><code>SHOW STATUS LIKE 'wsrep_connected';
</code></pre>
<pre><code>+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| wsrep_connected | ON    |
+-----------------+-------+
</code></pre>
<pre><code>SHOW STATUS LIKE 'wsrep_cluster_size';
</code></pre>
<pre><code>+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+
</code></pre>
<h3>Practical Observations<a class="anchor-link" id="practical-observations"></a></h3>
<p>A few things worth noting from working with PXC environments directly:</p>
<ul>
<li>The <strong>version of XtraBackup must match your PXC version</strong>. Using XtraBackup 2.x with PXC 8.0 will cause SST failures. Use Percona XtraBackup 8.0 with PXC 8.0, and confirm version alignment after any upgrade.</li>
<li>Even if you switch to the <code>clone</code> SST method, <strong>keep XtraBackup installed</strong> for scheduled backups. Your backup strategy and your SST method are separate concerns and should be treated as such.</li>
<li>The <code>wsrep_sst_donor</code> variable lets you specify a preferred donor node, which is useful for directing SST away from your busiest or most latency-sensitive member.</li>
<li>If you are running Percona Toolkit alongside PXC, be aware of how DDL replication works in your specific PXC version &mdash; Total Order Isolation (TOI) versus Rolling Schema Upgrade (RSU) behavior differs and is worth a dedicated look before running schema changes in production.</li>
</ul>
<h3>Summary<a class="anchor-link" id="summary"></a></h3>
<p>To answer the question directly: <strong>if you are using <code>xtrabackup-v2</code> as your SST method &mdash; which remains the default in many existing PXC deployments &mdash; then yes, XtraBackup needs to be installed on every cluster member.</strong> Any node can be either a donor or a joiner depending on circumstances, and both roles require XtraBackup to be present when using this method.</p>
<p>If you are on PXC 8.0.22 or later and want to eliminate that dependency at the SST layer, the Clone Plugin method is a viable alternative and is increasingly Percona&rsquo;s recommended choice for new deployments. If you are starting fresh, PXC 8.4 LTS is the current long-term support release and the recommended target for new installations. Even when using clone for SST, XtraBackup remains the right tool for your actual backup jobs.</p>
<p>Do not try to save a few megabytes of disk space by skipping XtraBackup on select nodes. The SST failure that eventually results from that decision is not a trade-off worth making.</p>
<h3>Resources<a class="anchor-link" id="resources"></a></h3>
<ul>
<li><a href="https://docs.percona.com/percona-xtradb-cluster/8.0/state-snapshot-transfer.html">Percona XtraDB Cluster SST Documentation</a></li>
<li><a href="https://docs.percona.com/percona-xtradb-cluster/8.0/wsrep-system-index.html#wsrep_sst_method">wsrep_sst_method System Variable Reference</a></li>
<li><a href="https://docs.percona.com/percona-xtrabackup/8.0/">Percona XtraBackup 8.0 Documentation</a></li>
<li><a href="https://docs.percona.com/percona-xtradb-cluster/8.4/">Percona XtraDB Cluster 8.4 LTS Documentation</a></li>
<li><a href="https://forums.percona.com/">Percona Community Forums</a></li>
</ul>

<p><a href="https://anothermysqldba.blogspot.com/2026/03/does-every-pxc-node-need-xtrabackup.html">Does Every PXC Node Need XtraBackup Installed?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>SQLSTATE [HY000] [2006] Galera has gone away</title>
      <link rel="alternate" type="text/html" href="https://medium.com/@arbaudie.it/sqlstate-hy000-2006-galera-has-gone-away-656f3b40ef8c?source=rss-c779d007e7fe------2" />
      <id>https://medium.com/@arbaudie.it/sqlstate-hy000-2006-galera-has-gone-away-656f3b40ef8c?source=rss-c779d007e7fe------2</id>
      <updated>2026-02-27T13:28:55+02:00</updated>
      <author><name>ArBauDie.IT</name></author>
      <summary type="html"><![CDATA[<p>TL:DR : i find this move detrimental to MariaDB as a wholeMariaDB plc’s decision to pull Galera, its synchronous multi-master replication solution, out of the open-source ecosystem in MariaDB 12.3 LTS has been quietly shaking the extended MariaDB community lately.Galera has been a critical component for high-availability MariaDB deployments, enabling robust scaling and fault tolerance. But with MariaDB’s acquisition of Codership and the sudden shift of Galera to a commercial license, the message to the community is clear: the rules have changed. For organizations that migrated from MySQL to MariaDB in search of stability, openness, and innovation, this move raises serious questions.MariaDB plc frames this decision as a necessary step to accelerate innovation and deliver greater value to customers. By integrating Galera directly into its Enterprise Platform, the company aims to streamline development, reduce feature delays, and offer a more cohesive high-availability solution. This aligns with MariaDB’s broader push to strengthen its enterprise offerings, particularly following its transition to private ownership and the arrival of a new CEO focused on profitability.However, the acquisition also serves a more strategic purpose: preventing future forks of Galera by third parties. By owning the codebase, MariaDB now controls the technology’s evolution and distribution, effectively eliminating competition from community-driven alternatives. While this may secure revenue streams, it risks alienating the very community that has been MariaDB’s strongest advocate.The parallels with HashiCorp’s controversial shift to a Business Source License for Vault are striking. Both companies have taken widely used open-source tools and placed them behind commercial barriers. Yet while HashiCorp provided clear migration paths, MariaDB’s approach feels abrupt.Also the timing particularly damaging. MariaDB Foundation is making pushes to present MariaDB as the natural continuation of MySQL amidst growing concerns about Oracle’s neglect of MySQL. And in parallel we have MariaDB plc behaving just like Oracle with a very popular piece of tech. This comes in direct conflict with the Foundation claims giving MariaDB’s detractors just the perfect ammunition to kill the narrative even before it can spread out.Beside this already disastrous timing, it can only erode the trust in MariaDB’s future. To put it bluntly, stating that open source does not equate to “free for everyone in perpetuity” as the CEO of MariaDB plc did during a MariaDB Foudnation board meeting is mostly telling about an absence of commitment to openness. Furthermore this argument used to explain close-sourcing Galera and Maxscale could very well be also applied the server itself. C*O won’t like this potential for instability. I dont like it either. And i really think this will drive many potential and actual clients away from MariaDB. It’s litteraly a godsend for the likes of Percona, VillageSQL and mostly PostgreSQL. As if the latter needed any help in this very moment …One a personal level, it seems i underestimated the greed of the corporation when i expressed myself about the Codership acquisition. Maybe i was too naïve indeed despite having a usually rather cynical nature when it comes down to business.Now the question is : how will the Foundation handle this ? We have elements of answers in the minutes of their board meeting 1/2026. I totally concur with option 2 as well. Will it take the form of a formal community-driven fork of galera 4 ? Would it take a port of Galera to VillageSQL ? Will it be Percona driven ? I can’t tell yet, but actions will speak louder than words as always.EDIT : a few hours after my publication, the plc issued a public statement on its blog : https://mariadb.com/resources/blog/mariadb-community-server-12-3-will-include-galera-cluster/</p>
<p><a href="https://medium.com/@arbaudie.it/sqlstate-hy000-2006-galera-has-gone-away-656f3b40ef8c?source=rss-c779d007e7fe------2">SQLSTATE [HY000] [2006] Galera has gone away</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>TL:DR&nbsp;: i find this move detrimental to MariaDB as a&nbsp;whole</p>
<p>MariaDB plc&rsquo;s decision to pull Galera, its synchronous multi-master replication solution, <a href="https://jira.mariadb.org/browse/MDEV-38744">out of the open-source ecosystem in MariaDB 12.3 LTS</a> has been <a href="https://www.linkedin.com/posts/federicorazzoli_dear-mariadb-foundation-weve-been-friends-share-7432242107899310081-chm1?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAGaVBsBb-oIeGyeZBxLG0008Lo2noBE7bk">quietly shaking the extended MariaDB community</a> lately.</p>
<p>Galera has been a critical component for high-availability MariaDB deployments, enabling robust scaling and fault tolerance. But with MariaDB&rsquo;s acquisition of Codership and the sudden shift of Galera to a commercial license, the message to the community is clear: the rules have changed. For organizations that migrated from MySQL to MariaDB in search of stability, openness, and innovation, this move raises serious questions.</p>
<p>MariaDB plc frames this decision as a necessary step to accelerate innovation and deliver greater value to customers. By integrating Galera directly into its Enterprise Platform, the company aims to streamline development, reduce feature delays, and offer a more cohesive high-availability solution. This aligns with MariaDB&rsquo;s broader push to strengthen its enterprise offerings, particularly following its transition to private ownership and the arrival of a new CEO focused on profitability.</p>
<p>However, the acquisition also serves a more strategic purpose: preventing future forks of Galera by third parties. By owning the codebase, MariaDB now controls the technology&rsquo;s evolution and distribution, effectively eliminating competition from community-driven alternatives. While this may secure revenue streams, it risks alienating the very community that has been MariaDB&rsquo;s strongest advocate.</p>
<p>The parallels with HashiCorp&rsquo;s controversial shift to a Business Source License for Vault are striking. Both companies have taken widely used open-source tools and placed them behind commercial barriers. Yet while HashiCorp provided clear migration paths, MariaDB&rsquo;s approach feels&nbsp;abrupt.</p>
<p>Also the timing particularly damaging. MariaDB Foundation is making pushes to present <a href="https://mariadb.org/is-mariadb-part-of-the-mysql-ecosystem/">MariaDB as the natural continuation of MySQL</a> amidst <a href="https://www.infoworld.com/article/4134394/community-push-intensifies-to-free-mysql-from-oracles-control-amid-stagnation-fears.html">growing concerns about Oracle&rsquo;s neglect of MySQL</a>. And in parallel we have MariaDB plc behaving just like Oracle with a very popular piece of tech. This comes in direct conflict with the Foundation claims giving MariaDB&rsquo;s detractors just the perfect ammunition to kill the narrative even before it can spread out.<br>Beside this already disastrous timing, it can only erode the trust in MariaDB&rsquo;s future. To put it bluntly, stating that open source does not equate to &ldquo;free for everyone in perpetuity&rdquo; as the CEO of MariaDB plc did during a MariaDB Foudnation board meeting is mostly telling about an absence of commitment to openness. Furthermore this argument used to explain close-sourcing Galera and Maxscale could very well be also applied the server itself. C*O won&rsquo;t like this potential for instability. I dont like it either. And i really think this will drive many potential and actual clients away from MariaDB. It&rsquo;s litteraly a godsend for the likes of Percona, VillageSQL and mostly PostgreSQL. As if the latter needed any help in this very moment&nbsp;&hellip;</p>
<p>One a personal level, it seems i underestimated the greed of the corporation when <a href="https://medium.com/@arbaudie.it/personal-opinion-the-future-of-galera-cluster-13827b522387">i expressed myself about the Codership acquisition</a>. Maybe i was too na&iuml;ve indeed despite having a usually rather cynical nature when it comes down to business.</p>
<p>Now the question is&nbsp;: how will the Foundation handle this&nbsp;? We have elements of answers in <a href="https://mariadb.org/bodminutes/2026-02-25/#4-decision-mariadb-foundations-stance-about-mariadb-plcs-galera-sunset-decision">the minutes of their board meeting 1/2026</a>. I totally concur with option 2 as well. Will it take the form of a formal community-driven fork of galera 4&nbsp;? Would it take a port of Galera to VillageSQL&nbsp;? Will it be Percona driven&nbsp;? I can&rsquo;t tell yet, but actions will speak louder than words as&nbsp;always.</p>
<p>EDIT&nbsp;: a few hours after my publication, the plc issued a public statement on its blog&nbsp;: <a href="https://mariadb.com/resources/blog/mariadb-community-server-12-3-will-include-galera-cluster/">https://mariadb.com/resources/blog/mariadb-community-server-12-3-will-include-galera-cluster/</a></p>
<p><img loading="lazy" decoding="async" src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=656f3b40ef8c" width="1" height="1" alt=""></p>

<p><a href="https://medium.com/@arbaudie.it/sqlstate-hy000-2006-galera-has-gone-away-656f3b40ef8c?source=rss-c779d007e7fe------2">SQLSTATE [HY000] [2006] Galera has gone away</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html" />
      <id>https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html</id>
      <updated>2026-02-25T20:57:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>Something changed in MySQL 8.4 related to caching, and it is easy to miss, so it deserves a post.  And a subject adjacent to this is the missing Release Candidate for MySQL 8.4 LTS, with my hope that the next LTS will have a Release Candidate, so I also cover this topic below.</p>
<p>(if you are not interested in Caching and Flushing, you can jump directly to the section about Release Candidate)</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html">More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Something changed in MySQL 8.4 related to caching, and it is easy to miss, so it deserves a post.&nbsp; And a subject adjacent to this is the missing Release Candidate for MySQL 8.4 LTS, with my hope that the next LTS will have a Release Candidate, so I also cover this topic below.</p>
<p>(if you are not interested in Caching and Flushing, you can jump directly to the section about Release Candidate)</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html">More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html" />
      <id>https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html</id>
      <updated>2026-02-25T20:57:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>Something changed in MySQL 8.4 related to caching, and it is easy to miss, so it deserves a post.  And a subject adjacent to this is the missing Release Candidate for MySQL 8.4 LTS, with my hope that the next LTS will have a Release Candidate, so I also cover this topic below.</p>
<p>(if you are not interested in Caching and Flushing, you can jump directly to the section about Release Candidate)</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html">More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Something changed in MySQL 8.4 related to caching, and it is easy to miss, so it deserves a post.&nbsp; And a subject adjacent to this is the missing Release Candidate for MySQL 8.4 LTS, with my hope that the next LTS will have a Release Candidate, so I also cover this topic below.</p>
<p>(if you are not interested in Caching and Flushing, you can jump directly to the section about Release Candidate)</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/02/more-than-flushing-also-caching-for-innodb-flush-method-and-missing-release-candidate.html">More than Flushing (also Caching) for innodb_flush_method, and Missing Release Candidate</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Meet Percona at KubeCon + CloudNativeCon Europe 2026</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/25/meet-percona-at-kubecon--cloudnativecon-europe-2026/" />
      <id>https://percona.community/blog/2026/02/25/meet-percona-at-kubecon--cloudnativecon-europe-2026/</id>
      <updated>2026-02-25T12:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The Percona team is heading to KubeCon + CloudNativeCon Europe in Amsterdam, and we’d love to meet you in person!</p>
<p><a href="https://percona.community/blog/2026/02/25/meet-percona-at-kubecon--cloudnativecon-europe-2026/">Meet Percona at KubeCon + CloudNativeCon Europe 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The Percona team is heading to KubeCon + CloudNativeCon Europe in Amsterdam, and we&rsquo;d love to meet you in person!</p>
<p>You can find us at <strong>Booth 790</strong>. This is a great chance to talk with engineers working on Percona Operators.</p>
<p>We will be there to discuss:</p>
<ul>
<li>Running MySQL, PostgreSQL, and MongoDB on Kubernetes</li>
<li>Production-ready HA setups</li>
<li>Backup and PITR strategies</li>
<li>Multi-cluster and multi-region deployments</li>
<li>Operators roadmap and upcoming features</li>
<li>Real-world troubleshooting stories</li>
</ul>
<p>If you&rsquo;re running Percona Operators in production (or just getting started), we&rsquo;d love to hear your feedback and learn about your challenges.</p>
<p>If you&rsquo;re just curious (or even suspicious) about running databases on Kubernetes, we&rsquo;d love to talk and answer your questions.</p>
<h3>Admission Tickets &ndash; 20% Off for Our Community<a class="anchor-link" id="admission-tickets-20-off-for-our-community"></a></h3>
<p>We have a 20% discount code available for Percona community members.<br>
If you&rsquo;re planning to attend and don&rsquo;t have a ticket yet, drop a comment or message us and we&rsquo;ll share the details.</p>
<h3>Schedule a Meeting<a class="anchor-link" id="schedule-a-meeting"></a></h3>
<p>Want dedicated time with our engineers?<br>
Drop a comment here or reach out directly. We&rsquo;re happy to schedule a meeting during the event.</p>
<p>See you in Amsterdam!</p>

<p><a href="https://percona.community/blog/2026/02/25/meet-percona-at-kubecon--cloudnativecon-europe-2026/">Meet Percona at KubeCon + CloudNativeCon Europe 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL coffee break: version upgrade related reindexing &#8211; reasons</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/25/postgresql-coffee-break-version-upgrade-related-reindexing-reasons/" />
      <id>https://percona.community/blog/2026/02/25/postgresql-coffee-break-version-upgrade-related-reindexing-reasons/</id>
      <updated>2026-02-25T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>During FOSDEM I had a chance to join a presentation by Alexander Sosna of GitLab on the backup procedure he has followed with his team to decrease the downtime during major upgrades. I highly recommend the talk, definitely worth watching!</p>
<p><a href="https://percona.community/blog/2026/02/25/postgresql-coffee-break-version-upgrade-related-reindexing-reasons/">PostgreSQL coffee break: version upgrade related reindexing &#8211; reasons</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>During FOSDEM I had a chance to join a <a href="https://fosdem.org/2026/schedule/event/ZF8ZLX-zero-downtime-postgresql-upgrades/" target="_blank" rel="noopener noreferrer">presentation</a> by <a href="https://www.linkedin.com/in/alexander-sosna-7193688b/" target="_blank" rel="noopener noreferrer">Alexander Sosna</a> of GitLab on the backup procedure he has followed with his team to decrease the downtime during major upgrades. I highly recommend the talk, definitely worth watching!</p>
<p>I loved the presentation, especially since a similar procedure is what we recommend our users as well. Unfortunately it&rsquo;s not for everyone: it&rsquo;s applicable ONLY when you can stop DDL operations on your database cluster for some time. As you can imagine that&rsquo;s not a case for every deployment.</p>
<p>This limitation got me into some very engaging discussions during my fav part of any conference, the so called &ldquo;hallway track&rdquo;. Networking and discussions after the talks and on the conference corridors are why I like going to such events. I&rsquo;ve heard some stories of nasty surprises coming out of the unexpected re-indexing after an upgrade.</p>
<p>It made me wonder, how many professionals have moved to PostgreSQL from other areas of expertise and are lacking information about the index rebuilds after upgrades may be necessary. How often are DevOps engineers or SREs, neither fluent in PostgreSQL nor experienced with databases, tasked with maintaining database infrastructure?</p>
<p>In the meantime I figured out that a short coffee time read is what I want to aim at. No super deep dives, rather food for thought and inspiring more of those engaging discussions I like so much about the conferences.</p>
<p>So let&rsquo;s get to it. This week I want to go through some basic facts before diving into more challenging topics in the coming weeks.</p>
<h3>What are collations<a class="anchor-link" id="what-are-collations"></a></h3>
<p>In general, a collation defines how values are ordered and compared. In databases, it most commonly applies to text. It&rsquo;s often not a trivial thing to determine how strings are sorted, whether two values are considered equal, and how things like upper vs lower case or special characters are treated. Just like alphabetical order helps us organize words in everyday life, collations define the rules the database uses when comparing and sorting character data. This allows us to sort structures like strings. Numbers are even simpler.</p>
<p>A good visualization of how significant collations are is when you try to imagine determining whether one item is before another if they come from different alphabets. Where do you position country specific characters in such a sorting order?</p>
<p>Lets look at a set of example data:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Bob
</span></span><span class="line"><span class="cl">Anna
</span></span><span class="line"><span class="cl">Zo&euml;
</span></span><span class="line"><span class="cl">&Aacute;lvaro</span></span></code></pre>
</div>
</div>
</div>
<p>When using an English-like collation the sorting would look like this as <code>&Aacute;</code> is treated like <code>A</code> . This is visible in Collation 1:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Anna
</span></span><span class="line"><span class="cl">&Aacute;lvaro
</span></span><span class="line"><span class="cl">Bob
</span></span><span class="line"><span class="cl">Zo&euml;</span></span></code></pre>
</div>
</div>
</div>
<p>Though with a collation changed so that <code>&Aacute;</code> is sorted separately before <code>A</code> . This is visible in Collation 2:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&Aacute;lvaro
</span></span><span class="line"><span class="cl">Anna
</span></span><span class="line"><span class="cl">Bob
</span></span><span class="line"><span class="cl">Zo&euml;</span></span></code></pre>
</div>
</div>
</div>
<p>It&rsquo;s clearly visible that collation defines how text is sorted and compared.</p>
<h3>What are indexes<a class="anchor-link" id="what-are-indexes"></a></h3>
<p>If you&rsquo;re not a database professional, you may not be familiar with what an index is. Think of it as of a structure that speeds up the search. In an old school library you had to check the index of authors to find the book you&rsquo;ve been looking for. Similarly in databases when knowing what the data is going to be searched for, we introduce indexes.</p>
<p>A common use of indexes is to enforce uniqueness of values. Primary keys and unique constraints automatically create unique indexes to ensure that no duplicate values are inserted. In addition, users can create their own indexes to support specific query patterns. Indexes can be single or multi column, depending on the particular use cases and to help speed up specific queries.<br>
Let&rsquo;s look at an example of physically unsorted data stored in heap</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Users (unsorted data)
</span></span><span class="line"><span class="cl">--------------------
</span></span><span class="line"><span class="cl">[1] Zo&euml;
</span></span><span class="line"><span class="cl">[2] &Aacute;lvaro
</span></span><span class="line"><span class="cl">[3] Anna
</span></span><span class="line"><span class="cl">[4] Bob</span></span></code></pre>
</div>
</div>
</div>
<p>with an Index on name</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Index (B-tree on name)
</span></span><span class="line"><span class="cl">----------------------
</span></span><span class="line"><span class="cl">Anna -&gt; [3]
</span></span><span class="line"><span class="cl">Bob -&gt; [4]
</span></span><span class="line"><span class="cl">Zo&euml; -&gt; [1]
</span></span><span class="line"><span class="cl">&Aacute;lvaro -&gt; [2]</span></span></code></pre>
</div>
</div>
</div>
<p>The index stores values in sorted order and points to their location in the table. Scanning through the list in order is fine for small number of items, as indexes get larger, there are opportunities to optimize this process. The most commonly used optimisation, very simplified, is to split the list and create a pointer which stores the maximum value in the left half of the list and the minimum value in the right half. As the lists get too large again, they are split again, creating a tree of these pointers.</p>
<h3>Why re-indexing is needed<a class="anchor-link" id="why-re-indexing-is-needed"></a></h3>
<p>If the rules for comparing the strings are different at query time from what they were when the index was created, the search may fail in various ways. If the scan encounters a value which, by the rules at query time, is higher in the sort order than the value being searched for, it will conclude that the value being searched for is not in the list. Similarly, the search may follow a pointer to the wrong list of values. To make this more concrete, let&rsquo;s look at a <a href="https://lists.debian.org/debian-glibc/2019/03/msg00030.html" target="_blank" rel="noopener noreferrer">real world example</a> from a <code>glibc</code> change in 2019. Before the change, a list of our values was sorted as follows</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">aa
</span></span><span class="line"><span class="cl">a a
</span></span><span class="line"><span class="cl">a-a
</span></span><span class="line"><span class="cl">a+a</span></span></code></pre>
</div>
</div>
</div>
<p>after the change, the list was sorted as follows:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">a a
</span></span><span class="line"><span class="cl">a+a
</span></span><span class="line"><span class="cl">a-a
</span></span><span class="line"><span class="cl">aa</span></span></code></pre>
</div>
</div>
</div>
<p>If an index was built under the first set of rules and searched under the second, a search for the value &lsquo;a a&rsquo; will fail, because the first item in the index (&lsquo;aa&rsquo;) is higher in the sort order than the value being searched for. Failing to find a value which is in the list can cause issues with application behavior &ndash; like missing records or records which appear in some queries (when the index is not used) but not in others (where the index is used). It may allow the insertion of values that should now be considered equal under the new collation rules, effectively violating uniqueness expectations.</p>
<p>Since PostgreSQL 10, the server tracks the collation version used when an index was built and can detect when it no longer matches the system&rsquo;s current collation version, which is why reindexing may suddenly become mandatory after an upgrade.</p>
<p>However, PostgreSQL server does not analyze your actual data to determine whether the collation change affects your stored values. It only detects that the collation provider version has changed. In some cases, this means reindexing is required even though the effective sort order of your specific data has not changed. Because PostgreSQL cannot reliably determine whether your specific dataset is affected, it treats the situation as potentially unsafe.</p>
<h3>When do collations change?<a class="anchor-link" id="when-do-collations-change"></a></h3>
<p><figure><img decoding="async" width="1536" height="1024" src="https://percona.community/blog/2026/02/Jan-glibc-confusion_hu_8bf34db7fd20efb5.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>There&rsquo;s a number of scenarios when collations change:</p>
<ul>
<li>OS / <code>glibc</code> upgrade (Linux) &ndash; PostgreSQL relies on the system <code>glibc</code> provided collations so if a version changes, the collation rules may change as well. The example above is <a href="https://lists.debian.org/debian-glibc/2019/03/msg00030.html" target="_blank" rel="noopener noreferrer">taken from the upgrade to glibc 2.28</a> which is one <a href="https://wiki.postgresql.org/wiki/Locale_data_changes" target="_blank" rel="noopener noreferrer">PostgreSQL Community remembers</a> due to the ripple effect it caused.</li>
<li>ICU library upgrade (for ICU collations) &ndash; if PostgreSQL deployment uses ICU collations, upgrading ICU library will affect these. While they are not tied to <code>glibc</code> these changes also happen.</li>
<li>Major PostgreSQL upgrade (in some cases) &ndash; if the new version uses a different collation provider behavior or updated ICU integration</li>
<li>Database restored on a system with different collation versions &ndash; logical dump/restore onto a host with different <code>glibc</code> / ICU versions can invalidate indexes.</li>
</ul>
<h3>When re-indexing is necessary<a class="anchor-link" id="when-re-indexing-is-necessary"></a></h3>
<p>Looking at the above provided list an observation is quite immediate, that not every collation type is affected:</p>
<ul>
<li>if the collation is not dependent on <code>glibc</code> / ICU then it won&rsquo;t be affected. As such C/POSIX collations are immune to such issues.</li>
<li>Same truth sticks to the data types. The collation change problem affects only those indexes which are the text or character based. All other datatypes like all types of integers and floating points, date, timestamp,&nbsp; geometric data types or even vector data remain unaffected.</li>
</ul>
<p>What&rsquo;s also important is that even if a collation changed it&rsquo;s not necessary it will affect a given index. Think of it this way, if your database does not use any language specific characters, chances are that collation change will not require re-index. Unfortunately you don&rsquo;t know that until you look inside your data.<br>
In controlled environments, teams may assess the risk before scheduling reindexing, but from PostgreSQL&rsquo;s perspective the index must be considered potentially inconsistent until rebuilt.</p>
<h3>What&rsquo;s next?<a class="anchor-link" id="whats-next"></a></h3>
<p>Next week we&rsquo;ll look at what is the reality of the DBA team regarding upgrades</p>

<p><a href="https://percona.community/blog/2026/02/25/postgresql-coffee-break-version-upgrade-related-reindexing-reasons/">PostgreSQL coffee break: version upgrade related reindexing &#8211; reasons</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB innovation: vector index performance</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-vector-index.html" />
      <id>https://smalldatum.blogspot.com/2026/02/mariadb-innovation-vector-index.html</id>
      <updated>2026-02-23T16:54:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>Last year I shared many posts documenting MariaDB performance for vector search using ann-benchmarks. Performance was great in MariaDB 11 and this blog post explains that it is even better in MariaDB 12. This work was done by Small Datum LLC and sponsored by the MariaDB Foundation. My previous posts were published in January and February 2025.tl;drVector search recall vs precision in MariaDB 12.3 is better than in MariaDB 11.8Vector search recall vs precision in Maria 11.8 is better than in Postgres 18.2 with pgvector 0.8.1The improvements in MariaDB 12.3 are more significant for larger datasetsMariaDB 12.3 has the best results because it use less CPU per query, This is confirmed by running vmstat in the background.BenchmarkThis post has much more detail about my approach. I ran the benchmark for 1 session. I use ann-benchmarks via my fork of a fork of a fork at this commit.  The ann-benchmarks config files are here for MariaDB and for Postgres.This time I used the dbpedia-openai-X-angular tests for X in 100k, 500k and 1000k.For hardware I used a larger server (Hetzner ax162-s) with 48 cores, 128G of RAM, Ubuntu 22.04 and HW RAID 10 using 2 NVMe devices. For databases I used:MariaDB versions 11.8.5 and 12.3.0 with this config file. Both were compiled from source. Postgres 18.2 with pgvector 0.8.1 with this config file. These were compiled from source. For Postgres tests were run with and without halfvec (float16).I had ps and vmstat running during the benchmark and confirmed there weren\'t storage reads as the table and index were cached by MariaDB and Postgres.The command lines to run the benchmark using my helper scripts are:    bash rall.batch.sh v1 dbpedia-openai-100k-angular c32r128    bash rall.batch.sh v1 dbpedia-openai-500k-angular c32r128    bash rall.batch.sh v1 dbpedia-openai-1000k-angular c32r128Results: dbpedia-openai-100k-angularSummaryMariaDB 12.3 has the best resultsthe difference between MariaDB 12.3 and 11.8 is smaller here than it is below for 500k and 1000kResults: dbpedia-openai-500k-angularSummaryMariaDB 12.3 has the best resultsthe difference between MariaDB 12.3 and 11.8 is larger here than above for 100kResults: dbpedia-openai-1000k-angularSummaryMariaDB 12.3 has the best resultsthe difference between MariaDB 12.3 and 11.8 is larger here than it is above for 100k and 500k</p>
<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-vector-index.html">MariaDB innovation: vector index performance</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Last year I shared many posts documenting MariaDB performance for vector search using <a href="https://github.com/erikbern/ann-benchmarks">ann-benchmarks</a>. Performance was great in MariaDB 11 and this blog post explains that it is even better in MariaDB 12. This work was done by&nbsp;<a href="https://smalldatum.github.io/">Small Datum LLC</a>&nbsp;and sponsored by the MariaDB Foundation. My previous posts were published in <a href="https://smalldatum.blogspot.com/2025/01/">January</a> and <a href="https://smalldatum.blogspot.com/2025/02/">February</a> 2025.</p>
<p>tl;dr</p>

<ul>
<li>Vector search recall vs precision in MariaDB 12.3 is better than in MariaDB 11.8</li>
<li>Vector search recall vs precision in Maria 11.8 is better than in Postgres 18.2 with pgvector 0.8.1</li>
<li>The improvements in MariaDB 12.3 are more significant for larger datasets</li>
<li>MariaDB 12.3 has the best results because it use less CPU per query, This is confirmed by running vmstat in the background.</li>
</ul>
<p><b>Benchmark</b></p>
<div>
<div><a href="https://smalldatum.blogspot.com/2025/01/evaluating-vector-indexes-in-mariadb.html">This post</a>&nbsp;has much more detail about my approach. I ran the benchmark for 1 session. I use&nbsp;<a href="https://github.com/erikbern/ann-benchmarks/">ann-benchmarks</a>&nbsp;via my&nbsp;<a href="https://github.com/mdcallag/ann-benchmarks-from-vuvova">fork of a fork of a fork</a>&nbsp;at&nbsp;<a href="https://github.com/mdcallag/ann-benchmarks-from-vuvova/commit/f0c0d0ccbbe765c6d758239eb95406b5dd07845d">this commit</a>.&nbsp; The ann-benchmarks config files are here&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/jan25.ann.gist-960-euclidean.v1/dop_1/pg172/config.yml.mariadb">for MariaDB</a>&nbsp;and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/jan25.ann.gist-960-euclidean.v1/dop_1/pg172/config.yml.pgvector">for Postgres</a>.</div>
<div></div>
<div>This time I used the dbpedia-openai-X-angular tests for X in 100k, 500k and 1000k.</div>
<div></div>
<div>For hardware I used a larger server (Hetzner ax162-s) with 48 cores, 128G of RAM, Ubuntu 22.04 and HW RAID 10 using 2 NVMe devices.&nbsp;</div>
<div></div>
<div>For databases I used:</div>
<div>
<ul>
<li>MariaDB versions 11.8.5 and 12.3.0 with&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/ma1203/etc/my.cnf.cz12b_vector_c32r128">this config file</a>. Both were compiled from source.&nbsp;</li>
<li>Postgres 18.2 with pgvector 0.8.1 with&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10a_vector_c32r128">this config file</a>. These were compiled from source. For Postgres tests were run with and without halfvec (float16).</li>
</ul>
</div>
<div>I had ps and vmstat running during the benchmark and confirmed there weren&rsquo;t storage reads as the table and index were cached by MariaDB and Postgres.</div>
<div>The command lines to run the benchmark using my&nbsp;<a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/jan25.ann.gist-960-euclidean.v1/helper_scripts">helper scripts</a>&nbsp;are:<br><span>&nbsp; &nbsp; bash rall.batch.sh v1&nbsp;</span><span>dbpedia-openai-100k-angular c32r128</span></div>
<div><span>&nbsp; &nbsp; bash rall.batch.sh v1&nbsp;</span><span>dbpedia-openai-500k-angular c32r128</span></div>
<div>
<div><span>&nbsp; &nbsp; bash rall.batch.sh v1&nbsp;</span><span>dbpedia-openai-1000k-angular c32r128</span></div>
</div>
</div>
<div><span><br></span></div>
<div><span><b>Results: dbpedia-openai-100k-angular</b></span></div>
<div></div>
<div>
<div>Summary</div>
<div>
<ul>
<li>MariaDB 12.3 has the best results</li>
<li>the difference between MariaDB 12.3 and 11.8 is smaller here than it is below for 500k and 1000k</li>
</ul>
</div>
</div>
<div><span>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghoAH4mzg4DbO9v-w27cVrEMvi6urszwdzCMxe4oeR8uCtd_oQLIBeM4-cqGIhNXPWKTZRpuSBQ5kXHb0uQvydPor3iSnFNvQgyEqwm6wf158-LYDb7Aaq6ggXdbOuUeg5Rvjgq5sYKlQLQvyI6Hl1Q8-QQrFTC8x2OgRSpAaK6O75IoktJRlGwjJ-baeF/s1173/dbpedia-openai-100k-angular.png"><img loading="lazy" decoding="async" border="0" data-original-height="778" data-original-width="1173" height="424" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghoAH4mzg4DbO9v-w27cVrEMvi6urszwdzCMxe4oeR8uCtd_oQLIBeM4-cqGIhNXPWKTZRpuSBQ5kXHb0uQvydPor3iSnFNvQgyEqwm6wf158-LYDb7Aaq6ggXdbOuUeg5Rvjgq5sYKlQLQvyI6Hl1Q8-QQrFTC8x2OgRSpAaK6O75IoktJRlGwjJ-baeF/w640-h424/dbpedia-openai-100k-angular.png" width="640"></a></div>
<p><b>Results: dbpedia-openai-500k-angular</b></p></span></div>
<div><span>
<div></div>
<div>Summary</div>
<div>
<ul>
<li>MariaDB 12.3 has the best results</li>
<li>the difference between MariaDB 12.3 and 11.8 is larger here than above for 100k</li>
</ul>
</div>
<div><span>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-NyNnnIHAY68jdbydeP6fPt5UIBj605fT1U_9QsE752XlGmjrr_GoxrJcF13iSsb7vXMtdo_R7t74qEoEy5Ton9-5g6J3ZsU1dsXc1OJOKeyODmpJcyn1pvta7NTr-RB0HAQfT6ok-DZF0Y_l5PaQsYFqJ0PVQo8rN34hK1BAmO1daC77rhEipWC7hxDL/s1173/dbpedia-openai-500k-angular.png"><img loading="lazy" decoding="async" border="0" data-original-height="778" data-original-width="1173" height="424" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-NyNnnIHAY68jdbydeP6fPt5UIBj605fT1U_9QsE752XlGmjrr_GoxrJcF13iSsb7vXMtdo_R7t74qEoEy5Ton9-5g6J3ZsU1dsXc1OJOKeyODmpJcyn1pvta7NTr-RB0HAQfT6ok-DZF0Y_l5PaQsYFqJ0PVQo8rN34hK1BAmO1daC77rhEipWC7hxDL/w640-h424/dbpedia-openai-500k-angular.png" width="640"></a></div>
<p><b>Results: dbpedia-openai-1000k-angular</b></p></span></div>
<div><span>
<div></div>
<div>Summary</div>
<div>
<ul>
<li>MariaDB 12.3 has the best results</li>
<li>the difference between MariaDB 12.3 and 11.8 is larger here than it is above for 100k and 500k</li>
</ul>
</div>
<div><span>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNnnpzCeUzX3yDzd_WybVB8o826nBL5XLt4OQTHz2Hrt2zR2UdQf-7uC_k1dIzZMYph15ZKzKWsF4N_rIaYzhyphenhyphenYgFGMcGXJufkVUNfAyacKDLqpqbDwy84KtxQpScLax6tulPCWfC2NQ_t7VRZ46g8ugCEpyX93h36WJZq46YZB7G2aYG2krQregdIf7wZ/s1173/dbpedia-openai-1000k-angular.png"><img loading="lazy" decoding="async" border="0" data-original-height="778" data-original-width="1173" height="424" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjNnnpzCeUzX3yDzd_WybVB8o826nBL5XLt4OQTHz2Hrt2zR2UdQf-7uC_k1dIzZMYph15ZKzKWsF4N_rIaYzhyphenhyphenYgFGMcGXJufkVUNfAyacKDLqpqbDwy84KtxQpScLax6tulPCWfC2NQ_t7VRZ46g8ugCEpyX93h36WJZq46YZB7G2aYG2krQregdIf7wZ/w640-h424/dbpedia-openai-1000k-angular.png" width="640"></a></div>
<p><span><br></span></p></span></div>
<p></p></span></div>
<p></p></span></div>

<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-vector-index.html">MariaDB innovation: vector index performance</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL + Neo4j for AI Workloads: Why Relational Databases Still Matter</title>
      <link rel="alternate" type="text/html" href="https://anothermysqldba.blogspot.com/2026/02/mysql-neo4j-for-ai-workloads-why.html" />
      <id>https://anothermysqldba.blogspot.com/2026/02/mysql-neo4j-for-ai-workloads-why.html</id>
      <updated>2026-02-22T00:01:00+02:00</updated>
      <author><name>Keith Larson ( anothermysqldba )</name></author>
      <summary type="html"><![CDATA[<p>So I figured it was about time I documented how to build persistent memory for AI agents using the databases you already know. Not vector databases - MySQL and Neo4j.</p>
<p>This isn\'t theoretical. I use this architecture daily, handling AI agent memory across multiple projects. Here\'s the schema and query patterns that actually work.</p>
<p>The Architecture</p>
<p>AI agents need two types of memory:</p>
<p> Structured memory - What happened, when, why (MySQL)<br />
 Pattern memory - What connects to what (Neo4j)</p>
<p>Vector databases are for similarity search. They\'re not for tracking workflow state or decision history. For that, you need ACID transactions and proper relationships.</p>
<p>The MySQL Schema</p>
<p>Here\'s the actual schema for AI agent persistent memory:</p>
<p>-- Architecture decisions the AI made<br />
CREATE TABLE architecture_decisions (<br />
  id INT AUTO_INCREMENT PRIMARY KEY,<br />
  project_id INT NOT NULL,<br />
  title VARCHAR(255) NOT NULL,<br />
  decision TEXT NOT NULL,<br />
  rationale TEXT,<br />
  alternatives_considered TEXT,<br />
  status ENUM(\'accepted\', \'rejected\', \'pending\') DEFAULT \'accepted\',<br />
  decided_at DATETIME DEFAULT CURRENT_TIMESTAMP,<br />
  tags JSON,<br />
  INDEX idx_project_date (project_id, decided_at),<br />
  INDEX idx_status (status)<br />
) ENGINE=InnoDB;</p>
<p>-- Code patterns the AI learned<br />
CREATE TABLE code_patterns (<br />
  id INT AUTO_INCREMENT PRIMARY KEY,<br />
  project_id INT NOT NULL,<br />
  category VARCHAR(50) NOT NULL,<br />
  name VARCHAR(255) NOT NULL,<br />
  description TEXT,<br />
  code_example TEXT,<br />
  language VARCHAR(50),<br />
  confidence_score FLOAT DEFAULT 0.5,<br />
  usage_count INT DEFAULT 0,<br />
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,<br />
  updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,<br />
  INDEX idx_project_category (project_id, category),<br />
  INDEX idx_confidence (confidence_score)<br />
) ENGINE=InnoDB;</p>
<p>-- Work session tracking<br />
CREATE TABLE work_sessions (<br />
  id INT AUTO_INCREMENT PRIMARY KEY,<br />
  session_id VARCHAR(255) UNIQUE NOT NULL,<br />
  project_id INT NOT NULL,<br />
  started_at DATETIME DEFAULT CURRENT_TIMESTAMP,<br />
  ended_at DATETIME,<br />
  summary TEXT,<br />
  context JSON,<br />
  INDEX idx_project_session (project_id, started_at)<br />
) ENGINE=InnoDB;</p>
<p>-- Pitfalls to avoid (learned from mistakes)<br />
CREATE TABLE pitfalls (<br />
  id INT AUTO_INCREMENT PRIMARY KEY,<br />
  project_id INT NOT NULL,<br />
  category VARCHAR(50),<br />
  title VARCHAR(255) NOT NULL,<br />
  description TEXT,<br />
  how_to_avoid TEXT,<br />
  severity ENUM(\'critical\', \'high\', \'medium\', \'low\'),<br />
  encountered_count INT DEFAULT 1,<br />
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,<br />
  INDEX idx_project_severity (project_id, severity)<br />
) ENGINE=InnoDB;</p>
<p>Foreign keys. Check constraints. Proper indexing. This is what relational databases are good at.</p>
<p>Query Patterns</p>
<p>Here\'s how you actually query this for AI agent memory:</p>
<p>-- Get recent decisions for context<br />
SELECT title, decision, rationale, decided_at<br />
FROM architecture_decisions<br />
WHERE project_id = ?<br />
 AND decided_at &#62; DATE_SUB(NOW(), INTERVAL 30 DAY)<br />
ORDER BY decided_at DESC<br />
LIMIT 10;</p>
<p>-- Find high-confidence patterns<br />
SELECT category, name, description, code_example<br />
FROM code_patterns<br />
WHERE project_id = ?<br />
 AND confidence_score &#62;= 0.80<br />
ORDER BY usage_count DESC, confidence_score DESC<br />
LIMIT 20;</p>
<p>-- Check for known pitfalls before implementing<br />
SELECT title, description, how_to_avoid<br />
FROM pitfalls<br />
WHERE project_id = ?<br />
 AND category = ?<br />
 AND severity IN (\'critical\', \'high\')<br />
ORDER BY encountered_count DESC;</p>
<p>-- Track session context across interactions<br />
SELECT context<br />
FROM work_sessions<br />
WHERE session_id = ?<br />
ORDER BY started_at DESC<br />
LIMIT 1;</p>
<p>These are straightforward SQL queries. EXPLAIN shows index usage exactly where expected. No surprises.</p>
<p>The Neo4j Layer</p>
<p>MySQL handles the structured data. Neo4j handles the relationships:</p>
<p>// Create nodes for decisions<br />
CREATE (d:Decision {<br />
 id: \'dec_123\',<br />
 title: \'Use FastAPI\',<br />
 project_id: 1,<br />
 embedding: [0.23, -0.45, ...] // Vector for similarity<br />
})</p>
<p>// Create relationships<br />
CREATE (d1:Decision {id: \'dec_123\', title: \'Use FastAPI\'})<br />
CREATE (d2:Decision {id: \'dec_45\', title: \'Used Flask before\'})<br />
CREATE (d1)-[:SIMILAR_TO {score: 0.85}]- &#62;(d2)<br />
CREATE (d1)-[:CONTRADICTS]- &#62;(d3:Decision {title: \'Avoid frameworks\'})</p>
<p>// Query: Find similar past decisions<br />
MATCH (current:Decision {id: $decision_id})<br />
MATCH (current)-[r:SIMILAR_TO]-(similar:Decision)<br />
WHERE r.score &#62; 0.80<br />
RETURN similar.title, r.score<br />
ORDER BY r.score DESC</p>
<p>// Query: What outcomes followed this pattern?<br />
MATCH (d:Decision)-[:LEADS_TO]- &#62;(o:Outcome)<br />
WHERE d.title CONTAINS \'Redis\'<br />
RETURN d.title, o.type, o.success_rate</p>
<p>How They Work Together</p>
<p>The flow looks like this:</p>
<p> AI agent generates content or makes a decision<br />
 Store structured data in MySQL (what, when, why, full context)<br />
 Generate embedding, store in Neo4j with relationships to similar items<br />
 Next session: Neo4j finds relevant similar decisions<br />
 MySQL provides the full details of those decisions</p>
<p>MySQL is the source of truth. Neo4j is the pattern finder.</p>
<p>Why Not Just Vector Databases?</p>
<p>I\'ve seen teams try to build AI agent memory with just Pinecone or Weaviate. It doesn\'t work well because:</p>
<p>Vector DBs are good for:</p>
<p> Finding documents similar to a query<br />
 Semantic search (RAG)<br />
 \"Things like this\"</p>
<p>Vector DBs are bad for:</p>
<p> \"What did we decide on March 15th?\"<br />
 \"Show me decisions that led to outages\"<br />
 \"What\'s the current status of this workflow?\"<br />
 \"Which patterns have confidence &#62; 0.8 AND usage_count &#62; 10?\"</p>
<p>Those queries need structured filtering, joins, and transactions. That\'s relational database territory.</p>
<p>MCP and the Future</p>
<p>The Model Context Protocol (MCP) is standardizing how AI systems handle context. Early MCP implementations are discovering what we already knew: you need both structured storage and graph relationships.</p>
<p>MySQL handles the MCP \"resources\" and \"tools\" catalog. Neo4j handles the \"relationships\" between context items. Vector embeddings are just one piece of the puzzle.</p>
<p>Production Notes</p>
<p>Current system running this architecture:</p>
<p> MySQL 8.0, 48 tables, ~2GB data<br />
 Neo4j Community, ~50k nodes, ~200k relationships<br />
 Query latency: MySQL</p>
<p><a href="https://anothermysqldba.blogspot.com/2026/02/mysql-neo4j-for-ai-workloads-why.html">MySQL + Neo4j for AI Workloads: Why Relational Databases Still Matter</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>So I figured it was about time I documented how to build persistent memory for AI agents using the databases you already know. Not vector databases &ndash; MySQL and Neo4j.</p>
<p>This isn&rsquo;t theoretical. I use this architecture daily, handling AI agent memory across multiple projects. Here&rsquo;s the schema and query patterns that actually work.</p>
<h3>The Architecture<a class="anchor-link" id="the-architecture"></a></h3>
<p>AI agents need two types of memory:</p>
<ul>
<li><strong>Structured memory</strong> &ndash; What happened, when, why (MySQL)</li>
<li><strong>Pattern memory</strong> &ndash; What connects to what (Neo4j)</li>
</ul>
<p>Vector databases are for similarity search. They&rsquo;re not for tracking workflow state or decision history. For that, you need ACID transactions and proper relationships.</p>
<h3>The MySQL Schema<a class="anchor-link" id="the-mysql-schema"></a></h3>
<p>Here&rsquo;s the actual schema for AI agent persistent memory:</p>
<pre><code>-- Architecture decisions the AI made
CREATE TABLE architecture_decisions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    decision TEXT NOT NULL,
    rationale TEXT,
    alternatives_considered TEXT,
    status ENUM('accepted', 'rejected', 'pending') DEFAULT 'accepted',
    decided_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    tags JSON,
    INDEX idx_project_date (project_id, decided_at),
    INDEX idx_status (status)
) ENGINE=InnoDB;

-- Code patterns the AI learned
CREATE TABLE code_patterns (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    code_example TEXT,
    language VARCHAR(50),
    confidence_score FLOAT DEFAULT 0.5,
    usage_count INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_project_category (project_id, category),
    INDEX idx_confidence (confidence_score)
) ENGINE=InnoDB;

-- Work session tracking
CREATE TABLE work_sessions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    session_id VARCHAR(255) UNIQUE NOT NULL,
    project_id INT NOT NULL,
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    ended_at DATETIME,
    summary TEXT,
    context JSON,
    INDEX idx_project_session (project_id, started_at)
) ENGINE=InnoDB;

-- Pitfalls to avoid (learned from mistakes)
CREATE TABLE pitfalls (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    category VARCHAR(50),
    title VARCHAR(255) NOT NULL,
    description TEXT,
    how_to_avoid TEXT,
    severity ENUM('critical', 'high', 'medium', 'low'),
    encountered_count INT DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_project_severity (project_id, severity)
) ENGINE=InnoDB;</code></pre>
<p>Foreign keys. Check constraints. Proper indexing. This is what relational databases are good at.</p>
<h3>Query Patterns<a class="anchor-link" id="query-patterns"></a></h3>
<p>Here&rsquo;s how you actually query this for AI agent memory:</p>
<pre><code>-- Get recent decisions for context
SELECT title, decision, rationale, decided_at
FROM architecture_decisions
WHERE project_id = ?
  AND decided_at &gt; DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY decided_at DESC
LIMIT 10;

-- Find high-confidence patterns
SELECT category, name, description, code_example
FROM code_patterns
WHERE project_id = ?
  AND confidence_score &gt;= 0.80
ORDER BY usage_count DESC, confidence_score DESC
LIMIT 20;

-- Check for known pitfalls before implementing
SELECT title, description, how_to_avoid
FROM pitfalls
WHERE project_id = ?
  AND category = ?
  AND severity IN ('critical', 'high')
ORDER BY encountered_count DESC;

-- Track session context across interactions
SELECT context
FROM work_sessions
WHERE session_id = ?
ORDER BY started_at DESC
LIMIT 1;</code></pre>
<p>These are straightforward SQL queries. EXPLAIN shows index usage exactly where expected. No surprises.</p>
<h3>The Neo4j Layer<a class="anchor-link" id="the-neo4j-layer"></a></h3>
<p>MySQL handles the structured data. Neo4j handles the relationships:</p>
<pre><code>// Create nodes for decisions
CREATE (d:Decision {
  id: 'dec_123',
  title: 'Use FastAPI',
  project_id: 1,
  embedding: [0.23, -0.45, ...]  // Vector for similarity
})

// Create relationships
CREATE (d1:Decision {id: 'dec_123', title: 'Use FastAPI'})
CREATE (d2:Decision {id: 'dec_45', title: 'Used Flask before'})
CREATE (d1)-[:SIMILAR_TO {score: 0.85}]-&gt;(d2)
CREATE (d1)-[:CONTRADICTS]-&gt;(d3:Decision {title: 'Avoid frameworks'})

// Query: Find similar past decisions
MATCH (current:Decision {id: $decision_id})
MATCH (current)-[r:SIMILAR_TO]-(similar:Decision)
WHERE r.score &gt; 0.80
RETURN similar.title, r.score
ORDER BY r.score DESC

// Query: What outcomes followed this pattern?
MATCH (d:Decision)-[:LEADS_TO]-&gt;(o:Outcome)
WHERE d.title CONTAINS 'Redis'
RETURN d.title, o.type, o.success_rate</code></pre>
<h3>How They Work Together<a class="anchor-link" id="how-they-work-together"></a></h3>
<p>The flow looks like this:</p>
<ol>
<li>AI agent generates content or makes a decision</li>
<li>Store structured data in MySQL (what, when, why, full context)</li>
<li>Generate embedding, store in Neo4j with relationships to similar items</li>
<li>Next session: Neo4j finds relevant similar decisions</li>
<li>MySQL provides the full details of those decisions</li>
</ol>
<p>MySQL is the source of truth. Neo4j is the pattern finder.</p>
<h3>Why Not Just Vector Databases?<a class="anchor-link" id="why-not-just-vector-databases"></a></h3>
<p>I&rsquo;ve seen teams try to build AI agent memory with just Pinecone or Weaviate. It doesn&rsquo;t work well because:</p>
<p><strong>Vector DBs are good for:</strong></p>
<ul>
<li>Finding documents similar to a query</li>
<li>Semantic search (RAG)</li>
<li>&ldquo;Things like this&rdquo;</li>
</ul>
<p><strong>Vector DBs are bad for:</strong></p>
<ul>
<li>&ldquo;What did we decide on March 15th?&rdquo;</li>
<li>&ldquo;Show me decisions that led to outages&rdquo;</li>
<li>&ldquo;What&rsquo;s the current status of this workflow?&rdquo;</li>
<li>&ldquo;Which patterns have confidence &gt; 0.8 AND usage_count &gt; 10?&rdquo;</li>
</ul>
<p>Those queries need structured filtering, joins, and transactions. That&rsquo;s relational database territory.</p>
<h3>MCP and the Future<a class="anchor-link" id="mcp-and-the-future"></a></h3>
<p>The Model Context Protocol (MCP) is standardizing how AI systems handle context. Early MCP implementations are discovering what we already knew: you need both structured storage and graph relationships.</p>
<p>MySQL handles the MCP &ldquo;resources&rdquo; and &ldquo;tools&rdquo; catalog. Neo4j handles the &ldquo;relationships&rdquo; between context items. Vector embeddings are just one piece of the puzzle.</p>
<h3>Production Notes<a class="anchor-link" id="production-notes"></a></h3>
<p>Current system running this architecture:</p>
<ul>
<li>MySQL 8.0, 48 tables, ~2GB data</li>
<li>Neo4j Community, ~50k nodes, ~200k relationships</li>
<li>Query latency: MySQL &lt;10ms, Neo4j &lt;50ms</li>
<li>Backup: Standard mysqldump + neo4j-admin dump</li>
<li>Monitoring: Same Percona tools I&rsquo;ve used for years</li>
</ul>
<p>The operational complexity is low because these are mature databases with well-understood operational patterns.</p>
<h3>Too Much Work? Let AI Build It For You<a class="anchor-link" id="too-much-work-let-ai-build-it-for-you"></a></h3>
<p>Look, I get it. This is a lot of schema to set up, a lot of queries to write, a lot of moving parts.</p>
<p>Here&rsquo;s the thing: you don&rsquo;t have to type it all yourself. Copy the schema above, paste it into Claude Code or Kimi CLI, and tell it what you want to build. The AI will generate the Python code, the connection handling, the query patterns &ndash; all of it.</p>
<p>If you want to understand what&rsquo;s happening under the hood, start here:</p>
<p><a href="https://machinelearningmastery.com/building-a-simple-mcp-server-in-python/">Building a Simple MCP Server in Python</a></p>
<p>Then let your AI tool do the heavy lifting. That&rsquo;s literally what I did. The schema is mine, the architecture decisions are mine, but the implementation?<br>
  Claude wrote most of it while I watched and corrected.</p>
<p>Use the tools. That&rsquo;s what they&rsquo;re for.</p>
<h3>When to Use What<a class="anchor-link" id="when-to-use-what"></a></h3>
<table>
<tr>
<th>Use Case</th>
<th>Database</th>
</tr>
<tr>
<td>Workflow state, decisions, audit trail</td>
<td>MySQL/PostgreSQL</td>
</tr>
<tr>
<td>Pattern detection, similarity, relationships</td>
<td>Neo4j</td>
</tr>
<tr>
<td>Semantic document search (RAG)</td>
<td>Vector DB (optional)</td>
</tr>
</table>
<p>Start with MySQL for state. Add Neo4j when you need pattern recognition. Only add vector DBs if you&rsquo;re actually doing semantic document retrieval.</p>
<h3>Summary<a class="anchor-link" id="summary"></a></h3>
<p>AI agents need persistent memory. Not just embeddings in a vector database &ndash; structured, relational, temporal memory with pattern recognition.</p>
<p>MySQL handles the structured state. Neo4j handles the graph relationships. Together they provide what vector databases alone cannot.</p>
<p>Don&rsquo;t abandon relational databases for AI workloads. Use the right tool for each job, which is using both together.</p>
<p><strong>For more on the AI agent perspective on this architecture, see the companion post on <a href="https://3k1o.blogspot.com/2026/02/beyond-vector-databases-how-ai-actually.html">3k1o</a>.</strong></p>

<p><a href="https://anothermysqldba.blogspot.com/2026/02/mysql-neo4j-for-ai-workloads-why.html">MySQL + Neo4j for AI Workloads: Why Relational Databases Still Matter</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Do AI models still keep getting better, or have they plateaued?</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/" />
      <id>https://optimizedbyotto.com/post/ai-models-plateaued-or-not/</id>
      <updated>2026-02-22T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The AI hype is based on the assumption that the frontier AI labs are producing better and better foundational models at an accelerating pace. Is that really true, or are people just in sort of a mass psychosis because AI models have become so good at mimicking human behavior that we unconsciously attribute increasing intelligence to them? I decided to conduct a mini-benchmark of my own to find out if the latest and greatest AI models are actually really good or not.<br />
The problem with benchmarks<br />
Every time any team releases a new LLM, they boast how well it performs on various industry benchmarks such as Humanity’s Last Exam, SWE-Bench and Ai2 ARC or ARC-AGI. An overall leaderboard can be viewed at LLM-stats. This incentivizes teams to optimize for specific benchmarks, which might make them excel on specific tasks while general abilities degrade. Also, the older a benchmark dataset is, the more online material there is discussing the questions and best answers, which in turn increases the chances of newer models trained on more recent web content scoring better.<br />
Thus I prefer looking at real-time leaderboards such as the LM Arena leaderboard (or OpenCompass for Chinese models that might be missing from LM Arena). However, even though the LM Arena Elo score is rated by humans in real-time, the benchmark can still be played. For example, Meta reportedly used a special chat-optimized model instead of the actual Llama 4 model when getting scored on the LM Arena.<br />
Therefore I trust my own first-hand experience more than the benchmarks for gaining intuition. Intuition however is not a compelling argument in discussions on whether or not new flagship AI models have plateaued. Thus, I decided to devise my own mini-benchmark so that no model could have possibly seen it in its training data or be specifically optimized for it in any way.<br />
My mini-benchmark<br />
I crafted 6 questions based on my own experience using various LLMs for several years and having developed some intuition about what kinds of questions LLMs typically struggle with.<br />
I conducted the benchmark using the OpenRouter.ai chat playroom with the following state-of-the-art models:</p>
<p>Claude Opus 4.6 (Anthropic)<br />
GPT-5.2 (OpenAI)<br />
Grok 4.1 (xAI)<br />
Gemini 3.1 Pro Preview (Google)<br />
GLM 5 (Z.ai)<br />
MinMax M2.5 (MinMax)<br />
Qwen3.5 Plus 2026-02-15 (Alibaba)<br />
Kimi K2.5 (Moonshot.ai)</p>
<p>OpenRouter.ai is great as it very easy to get responses from multiple models in parallel to a single question. Also it allows to turn off web search to force the models to answer purely based on their embedded knowledge.</p>
<p>Common for all the test questions is that they are fairly straightforward and have a clear answer, yet the answer isn’t common knowledge or statistically the most obvious one, and instead requires a bit of reasoning to get correct.<br />
Some of these questions are also based on myself witnessing a flagship model failing miserably to answer it.<br />
1. Which cities have hosted the Olympics more than just once?<br />
This question requires accounting for both summer and winter Olympics, and for Olympics hosted across multiple cities.<br />
The variance in responses comes from if the model understands that Beijing should be counted as it has hosted both summer and winter Olympics. Interestingly GPT was the only model to not mention Beijing at all. Some variance also comes from how models account for co-hosted Olympics. For example Cortina should be counted as having hosted the Olympics twice, in 1956 and 2026, but only Claude, Gemini and Kimi pointed this out. Stockholm’s 1956 hosting of the equestrian games during the Melbourne Olympics is a special case, which GPT, Gemini and Kimi pointed out in a side note. Some models seem to have old training material, and for example Grok assumes the current year is 2024. All models that accounted for awarded future Olympics (e.g. Los Angeles 2028) marked them clearly as upcoming.<br />
Overall I would judge that only GPT and MinMax gave incomplete answers, while all other models replied as the best humans could reasonably have.<br />
2. If EUR/USD continues to slide to 1.5 by mid-2026, what is the likely effect on BMW’s stock price by end of 2026?<br />
This question requires mapping the currency exchange rate to historic value, dodging the misleading word “slide”, and reasoning on where the revenue of a company comes from and how a weaker US dollar affects it in multiple ways. I’ve frequently witnessed flagship models get it wrong how interest rates and exchange rates work. Apparently the binary choice between “up” or “down” is somehow challenging to the internal statistical model in the LLMs on a topic where there are a lot of training material that talk about both things being likely to happen, and choosing between them requires specifically reasoning about the scenario at hand and disregarding general knowledge of the situation.<br />
However, this time all the models concluded correctly that a weak dollar would have a negative overall effect on the BMW stock price. Gemini, GLM, Qwen and Kimi also mention the potential hedging effect of BMW’s X-series production in South Carolina for worldwide export.<br />
3. What is the Unicode code point for the traffic cone emoji?<br />
This was the first question where the the flagship models clearly still struggle in 2026. The trap here is that there is no traffic cone emoji, so an advanced model should simply refuse to give any Unicode numbers at all. Most LLMs however have an urge to give some answer, leading to hallucinations. Also, as the answer has a graphical element to it, the LLM might not understand how the emoji “looks” in ways that would be obvious to a human, and thus many models claim the construction sign emoji is a traffic cone, which it is not.<br />
By far the worst response was from GPT, that simply hallucinates and stops there:</p>
<p>While Gemini and Grok were among the three models not falling into this trap, the response from Claude was exemplary good:</p>
<p>4. Which languages are the 10 most commonly spoken in the world? For each language, count from 1 to 5.<br />
This seemingly simple question can become quite complex when you account for the fact that other languages might also have their own character system, both for words and the numerals (e.g. Arabic واحد/one vs ١/1), and that numbers might have variations depending on what the thing being counted is (e.g in French object gender un/une), or whether plain numbers and occurrences have their own words (e.g. Russian один/odin vs раз/raz). I’ve also witnessed older models frequently mix up on what parts of the reply should be using what language.<br />
For a human this question is trivial, assuming they know the language. For LLMs it is surprisingly challenging. Grok and MinMax only gave a list of the most popular languages and stopped there without showing any words in the languages. Qwen got a bit further by trying to list numbers in each language, but the lists were only the numbers 1 to 5:</p>
<p>I have seen Grok do something similar before, which in fact inspired me to include this question in my mini-benchmark.<br />
GPT got a bit further, but for Hindi, Arabic and Bengali it listed the numerals in local script, not the number words. Gemini, GLM and Kimi gave a complete and correct answer as a list, while the absolute best answer and presentation was by Claude, that gave the table below:</p>
<p>Claude Opus 4.6<br />
10 Most Commonly Spoken Languages (by Total Speakers)</p>
<p>#<br />
Language<br />
1<br />
2<br />
3<br />
4<br />
5</p>
<p>1<br />
English<br />
One<br />
Two<br />
Three<br />
Four<br />
Five</p>
<p>2<br />
Mandarin Chinese<br />
一 (yī)<br />
二 (èr)<br />
三 (sān)<br />
四 (sì)<br />
五 (wǔ)</p>
<p>3<br />
Hindi<br />
एक (ek)<br />
दो (do)<br />
तीन (tīn)<br />
चार (chār)<br />
पाँच (pā̃ch)</p>
<p>4<br />
Spanish<br />
Uno<br />
Dos<br />
Tres<br />
Cuatro<br />
Cinco</p>
<p>5<br />
French<br />
Un<br />
Deux<br />
Trois<br />
Quatre<br />
Cinq</p>
<p>6<br />
Arabic<br />
واحد (wāḥid)<br />
اثنان (ithnān)<br />
ثلاثة (thalātha)<br />
أربعة (arbaʿa)<br />
خمسة (khamsa)</p>
<p>7<br />
Bengali<br />
এক (ek)<br />
দুই (dui)<br />
তিন (tin)<br />
চার (chār)<br />
পাঁচ (pā̃ch)</p>
<p>8<br />
Portuguese<br />
Um<br />
Dois<br />
Três<br />
Quatro<br />
Cinco</p>
<p>9<br />
Russian<br />
Один (odin)<br />
Два (dva)<br />
Три (tri)<br />
Четыре (chetyre)<br />
Пять (pyat\')</p>
<p>10<br />
Japanese<br />
一 (ichi)<br />
二 (ni)<br />
三 (san)<br />
四 (shi)<br />
五 (go)</p>
<p>Note: Rankings are based on estimated total speakers (native + non-native). Rankings can vary depending on the source and methodology used.</p>
<p>5. Count the number of drone launch pads in the picture.<br />
Together with the question, I uploaded this picture:</p>
<p>A human can easily count that there are 10 rows and 30+ columns in the grid, but because the picture resolution isn’t good enough, the exact number of columns can’t be counted, and the answer should be that there are at least 300 launch pads in the picture.<br />
GPT and Grok both guessed the count is zero. Instead of hallucinating some number they say zero, but it would have been better to not give any number at all, and just state that they are unable to perform the task. Gemini gave as its answer “101”, which is quite odd, but reading the reasoning section, it seems to have tried counting items in the image without reasoning much about what it is actually counting and that there is clearly a grid that can make the counting much easier. Both Qwen and Kimi state they can see four parallel structures, but are unable to count drone launch pads.<br />
The absolutely best answer was given by Claude, which counted 10-12 rows and 30-40+ columns, and concluded that there must be 300-500 drone launch pads. Very close to best human level - impressive!<br />
This question applied only to multi-modal models that can see images, so GLM and MinMax could not give any response.<br />
6. Explain why I am getting the error below, and what is the best way to fix it?<br />
Together with the question above, I gave this code block:</p>
<p>Copy</p>
<p>$ SH_SCRIPTS=\"$(mktemp; grep -Irnw debian/ -e \'^#!.*/sh\' &#124; sort -u &#124; cut -d \':\' -f 1 &#124;&#124; true)\"<br />
$ shellcheck -x --enable=all --shell=sh \"$SH_SCRIPTS\"<br />
/tmp/tmp.xQOpI5Nljx<br />
debian/tests/integration-tests: /tmp/tmp.xQOpI5Nljx<br />
debian/tests/integration-tests: openBinaryFile: does not exist (No such file or directory)$ SH_SCRIPTS=\"$(mktemp; grep -Irnw debian/ -e \'^#!.*/sh\' &#124; sort -u &#124; cut -d \':\' -f 1 &#124;&#124; true)\"<br />
$ shellcheck -x --enable=all --shell=sh \"$SH_SCRIPTS\"<br />
/tmp/tmp.xQOpI5Nljx<br />
debian/tests/integration-tests: /tmp/tmp.xQOpI5Nljx<br />
debian/tests/integration-tests: openBinaryFile: does not exist (No such file or directory)<br />
Older models would easily be misled by the last error message thinking that a file went missing, and focus on suggesting changes to the complex-looking first line. In reality the error is simply caused by having the quotes around the $SH_SCRIPTS, resulting in the entire multi-line string being passed as a single argument to shellcheck. So instead of receiving two separate file paths, shellcheck tries to open one file literally named /tmp/tmp.xQOpI5Nljxndebian/tests/integration-tests.<br />
Incorrect argument expansion is fairly easy for an experienced human programmer to notice, but tricky for an LLM. Indeed, Grok, MinMax, and Qwen fell for this trap and focused on the mktemp, assuming it somehow fails to create a file. Interestingly GLM fails to produce an answer at all, as the reasoning step seems to be looping, thinking too much about the missing file, but not understanding why it would be missing when there is nothing wrong with how mktemp is executed.<br />
Claude, Gemini, and Kimi immediately spot the real root cause of passing the variable quoted and suggested correct fixes that involve either removing the quotes, or using Bash arrays or xargs in a way that makes the whole command also handle correctly filenames with spaces in them.<br />
Conclusion</p>
<p>Model<br />
Sports<br />
Economics<br />
Emoji<br />
Languages<br />
Visual<br />
Shell<br />
Score</p>
<p>Claude Opus 4.6<br />
✓<br />
✓<br />
✓<br />
✓<br />
✓<br />
✓<br />
6/6</p>
<p>GPT-5.2<br />
✗<br />
✓<br />
✗<br />
~<br />
✗<br />
✓<br />
2.5/6</p>
<p>Grok 4.1<br />
✓<br />
✓<br />
✓<br />
✗<br />
✗<br />
✗<br />
3/6</p>
<p>Gemini 3.1 Pro<br />
✓<br />
✓<br />
✓<br />
✓<br />
✗<br />
✓<br />
5/6</p>
<p>GLM 5<br />
✓<br />
✓<br />
?<br />
✓<br />
N/A<br />
✗<br />
3/5</p>
<p>MinMax M2.5<br />
✗<br />
✓<br />
✗<br />
✗<br />
N/A<br />
✗<br />
1/5</p>
<p>Qwen3.5 Plus<br />
✓<br />
✓<br />
✗<br />
~<br />
✗<br />
✗<br />
2.5/6</p>
<p>Kimi K2.5<br />
✓<br />
✓<br />
✗<br />
✓<br />
✗<br />
✓<br />
4/6</p>
<p>Obviously, my mini-benchmark only had 6 questions, and I ran it only once. This was obviously not scientifically rigorous. However it was systematic enough to trump just a mere feeling.<br />
The main finding for me personally is that Claude Opus 4.6, the flagship model by Anthropic, seems to give great answers consistently. The answers are not only correct, but also well scoped giving enough information to cover everything that seems relevant, without blurping unnecessary filler.<br />
I used Claude extensively in 2023-2024 when it was the main model available at my day work, but for the past year I had been using other models that I felt were better at the time. Now Claude seems to be the best-of-the-best again, with Gemini and Kimi as close follow-ups. Comparing their pricing at OpenRouter.ai the Kimi K2.5 price of $0.6 / million tokens is almost 90% cheaper than the Claude Opus 4.6’s $5.0 / million tokens suggests that Kimi K2.5 offers the best price-per-performance ratio. Claude might be cheaper with a monthly subscription directly from Anthropic, potentially narrowing the price gap.<br />
Overall I do feel that Anthropic, Google and Moonshot.ai have been pushing the envelope with their latest models in a way that one can’t really claim that AI models have plateaued. In fact, one could claim that at least Claude has now climbed over the hill of “AI slop” and consistently produces valuable results. If and when AI usage expands from here, we might actually not drown in AI slop as chances of accidentally crappy results decrease. This makes me positive about the future.<br />
I am also really happy to see that there wasn’t just one model crushing everybody else, but that there are at least three models doing very well. As an open source enthusiast I am particularly glad to see that Moonshot.ai’s Kimi K2.5 is published with an open license. Given the hardware, anyone can run it on their own. OpenRouter.ai currently lists 9 independent providers alongside Moonshot.ai itself, showcasing the potential of open-weight models in practice.<br />
If the pattern holds and flagship models continue improving at this pace we might look back at 2026 as the year AI stopped feeling like a call center associate and started to resemble a scientific researcher. While new models become available we need to keep testing, keep questioning, and keep our expectations grounded in actual performance rather than press releases.<br />
Thanks to OpenRouter.ai for providing a great service that makes testing various models incredibly easy!</p>
<p><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/">Do AI models still keep getting better, or have they plateaued?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/flagship-ai-mini-benchmark.png" alt="Featured image of post Do AI models still keep getting better, or have they plateaued?"></p>
<p>The AI hype is based on the assumption that the frontier AI labs are producing better and better foundational models <em>at an accelerating pace</em>. Is that really true, or are people just in sort of a mass psychosis because AI models have become so good at mimicking human behavior that we unconsciously attribute increasing intelligence to them? I decided to conduct a mini-benchmark of my own to find out if the latest and greatest AI models are actually really good or not.</p>
<h2><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#the-problem-with-benchmarks" class="header-anchor"></a>The problem with benchmarks<br>
<a class="anchor-link" id="the-problem-with-benchmarks"></a></h2>
<p>Every time any team releases a new LLM, they boast how well it performs on various industry benchmarks such as <a class="link" href="https://agi.safe.ai/" target="_blank" rel="noopener">Humanity&rsquo;s Last Exam</a>, <a class="link" href="https://www.swebench.com/" target="_blank" rel="noopener">SWE-Bench</a> and <a class="link" href="https://allenai.org/data/arc" target="_blank" rel="noopener">Ai2 ARC</a> or <a class="link" href="https://arcprize.org/leaderboard" target="_blank" rel="noopener">ARC-AGI</a>. An overall leaderboard can be viewed at <a class="link" href="https://llm-stats.com/" target="_blank" rel="noopener">LLM-stats</a>. This incentivizes teams to optimize for specific benchmarks, which might make them excel on specific tasks while general abilities degrade. <strong>Also, the older a benchmark dataset is, the more online material there is discussing the questions and best answers,</strong> which in turn increases the chances of newer models trained on more recent web content scoring better.</p>
<p>Thus I prefer looking at real-time leaderboards such as the <a class="link" href="https://arena.ai/leaderboard" target="_blank" rel="noopener">LM Arena leaderboard</a> (or <a class="link" href="https://rank.opencompass.org.cn/leaderboard-llm" target="_blank" rel="noopener">OpenCompass</a> for Chinese models that might be missing from LM Arena). However, even though the LM Arena Elo score is rated by humans in real-time, the benchmark can still be played. For example, <a class="link" href="https://www.heise.de/en/news/Meta-cheats-on-Llama-4-benchmark-10344087.html" target="_blank" rel="noopener">Meta reportedly</a> used a special chat-optimized model instead of the actual Llama 4 model when getting scored on the LM Arena.</p>
<p>Therefore I trust my own first-hand experience more than the benchmarks for gaining intuition. Intuition however is not a compelling argument in discussions on whether or not new flagship AI models have plateaued. Thus, I decided to devise my own mini-benchmark so that no model could have possibly seen it in its training data or be specifically optimized for it in any way.</p>
<h2><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#my-mini-benchmark" class="header-anchor"></a>My mini-benchmark<br>
<a class="anchor-link" id="my-mini-benchmark"></a></h2>
<p>I crafted 6 questions based on my own experience using various LLMs for several years and having developed some intuition about what kinds of questions LLMs typically struggle with.</p>
<p>I conducted the benchmark using the <a class="link" href="https://openrouter.ai/chat?models=anthropic%2Fclaude-opus-4.6%2Copenai%2Fgpt-5.2%2Cx-ai%2Fgrok-4.1-fast%2Cgoogle%2Fgemini-3.1-pro-preview%2Cz-ai%2Fglm-5%2Cminimax%2Fminimax-m2.5%2Cqwen%2Fqwen3.5-plus-02-15%2Cmoonshotai%2Fkimi-k2.5" target="_blank" rel="noopener">OpenRouter.ai chat playroom</a> with the following state-of-the-art models:</p>
<ul>
<li><a class="link" href="https://openrouter.ai/anthropic/claude-opus-4.6" target="_blank" rel="noopener">Claude Opus 4.6 (Anthropic)</a></li>
<li><a class="link" href="https://openrouter.ai/openai/gpt-5.2" target="_blank" rel="noopener">GPT-5.2 (OpenAI)</a></li>
<li><a class="link" href="https://openrouter.ai/x-ai/grok-4.1-fast" target="_blank" rel="noopener">Grok 4.1 (xAI)</a></li>
<li><a class="link" href="https://openrouter.ai/google/gemini-3.1-pro-preview" target="_blank" rel="noopener">Gemini 3.1 Pro Preview (Google)</a></li>
<li><a class="link" href="https://openrouter.ai/z-ai/glm-5" target="_blank" rel="noopener">GLM 5 (Z.ai)</a></li>
<li><a class="link" href="https://openrouter.ai/minimax/minimax-m2.5" target="_blank" rel="noopener">MinMax M2.5 (MinMax)</a></li>
<li><a class="link" href="https://openrouter.ai/qwen/qwen3.5-plus-02-15" target="_blank" rel="noopener">Qwen3.5 Plus 2026-02-15 (Alibaba)</a></li>
<li><a class="link" href="https://openrouter.ai/moonshotai/kimi-k2.5" target="_blank" rel="noopener">Kimi K2.5 (Moonshot.ai)</a></li>
</ul>
<p>OpenRouter.ai is great as it very easy to get responses from multiple models in parallel to a single question. Also it allows to turn off web search to force the models to answer purely based on their embedded knowledge.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/flagship-ai-mini-benchmark.gif" width="800" height="679" loading="lazy" alt="OpenRouter.ai Chat playroom" class="gallery-image" data-flex-grow="117" data-flex-basis="282px">
</p>
<p>Common for all the test questions is that they are fairly straightforward and have a clear answer, yet the answer isn&rsquo;t common knowledge or statistically the most obvious one, and instead requires a bit of reasoning to get correct.</p>
<p>Some of these questions are also based on myself witnessing a flagship model failing miserably to answer it.</p>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#1-which-cities-have-hosted-the-olympics-more-than-just-once" class="header-anchor"></a>1. Which cities have hosted the Olympics more than just once?<br>
<a class="anchor-link" id="1-which-cities-have-hosted-the-olympics-more-than-just-once"></a></h3>
<p>This question requires accounting for both summer and winter Olympics, and for Olympics hosted across multiple cities.</p>
<p>The variance in responses comes from if the model understands that Beijing should be counted as it has hosted both summer and winter Olympics. Interestingly GPT was the only model to not mention Beijing at all. Some variance also comes from how models account for co-hosted Olympics. For example Cortina should be counted as having hosted the Olympics twice, in 1956 and 2026, but only Claude, Gemini and Kimi pointed this out. Stockholm&rsquo;s 1956 hosting of the equestrian games during the Melbourne Olympics is a special case, which GPT, Gemini and Kimi pointed out in a side note. Some models seem to have old training material, and for example Grok assumes the current year is 2024. All models that accounted for awarded future Olympics (e.g. Los Angeles 2028) marked them clearly as upcoming.</p>
<p>Overall I would judge that only GPT and MinMax gave incomplete answers, while all other models replied as the best humans could reasonably have.</p>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#2-if-eurusd-continues-to-slide-to-15-by-mid-2026-what-is-the-likely-effect-on-bmws-stock-price-by-end-of-2026" class="header-anchor"></a>2. If EUR/USD continues to slide to 1.5 by mid-2026, what is the likely effect on BMW&rsquo;s stock price by end of 2026?<br>
<a class="anchor-link" id="2-if-eur-usd-continues-to-slide-to-1-5-by-mid-2026-what-is-the-likely-effect-on-bmws-stock-price-by-end-of-2026"></a></h3>
<p>This question requires mapping the currency exchange rate to historic value, dodging the misleading word &ldquo;slide&rdquo;, and reasoning on where the revenue of a company comes from and how a weaker US dollar affects it in multiple ways. I&rsquo;ve frequently witnessed flagship models get it wrong how interest rates and exchange rates work. Apparently the binary choice between &ldquo;up&rdquo; or &ldquo;down&rdquo; is somehow challenging to the internal statistical model in the LLMs on a topic where there are a lot of training material that talk about both things being likely to happen, and choosing between them requires specifically reasoning about the scenario at hand and disregarding general knowledge of the situation.</p>
<p>However, this time all the models concluded correctly that a weak dollar would have a negative overall effect on the BMW stock price. Gemini, GLM, Qwen and Kimi also mention the potential hedging effect of BMW&rsquo;s X-series production in South Carolina for worldwide export.</p>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#3-what-is-the-unicode-code-point-for-the-traffic-cone-emoji" class="header-anchor"></a>3. What is the Unicode code point for the traffic cone emoji?<br>
<a class="anchor-link" id="3-what-is-the-unicode-code-point-for-the-traffic-cone-emoji"></a></h3>
<p>This was the first question where the the flagship models clearly still struggle in 2026. The trap here is that there is no traffic cone emoji, so an advanced model should simply refuse to give any Unicode numbers at all. Most LLMs however have an urge to give some answer, leading to hallucinations. Also, as the answer has a graphical element to it, the LLM might not understand how the emoji &ldquo;looks&rdquo; in ways that would be obvious to a human, and thus many models claim the construction sign emoji is a traffic cone, which it is not.</p>
<p>By far the worst response was from GPT, that simply hallucinates and stops there:</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/gpt-5.2-traffic-cone-emoji.png" width="899" height="117" loading="lazy" alt="OpenAIs GPT-5.2 completely wrong answer to traffic cone emoji question" class="gallery-image" data-flex-grow="768" data-flex-basis="1844px">
</p>
<p>While Gemini and Grok were among the three models not falling into this trap, the response from Claude was exemplary good:</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/claude-opus-4.6-traffic-cone-emoji.png" width="899" height="387" loading="lazy" alt="Claude Opus 4.6 exemplary good answer to traffic cone emoji question" class="gallery-image" data-flex-grow="232" data-flex-basis="557px">
</p>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#4-which-languages-are-the-10-most-commonly-spoken-in-the-world-for-each-language-count-from-1-to-5" class="header-anchor"></a>4. Which languages are the 10 most commonly spoken in the world? For each language, count from 1 to 5.<br>
<a class="anchor-link" id="4-which-languages-are-the-10-most-commonly-spoken-in-the-world-for-each-language-count-from-1-to-5"></a></h3>
<p>This seemingly simple question can become quite complex when you account for the fact that other languages might also have their own character system, both for words and the numerals (e.g. Arabic &#1608;&#1575;&#1581;&#1583;/one vs &#1633;/1), and that numbers might have variations depending on what the thing being counted is (e.g in French object gender un/une), or whether plain numbers and occurrences have their own words (e.g. Russian &#1086;&#1076;&#1080;&#1085;/odin vs &#1088;&#1072;&#1079;/raz). I&rsquo;ve also witnessed older models frequently mix up on what parts of the reply should be using what language.</p>
<p>For a human this question is trivial, assuming they know the language. For LLMs it is surprisingly challenging. Grok and MinMax only gave a list of the most popular languages and stopped there without showing any words in the languages. Qwen got a bit further by trying to list numbers in each language, but the lists were only the numbers 1 to 5:</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/qwen3.5-plus-count-to-five.png" width="899" height="562" loading="lazy" alt="Qwen3.5 Plus count to five in top 10 languages" class="gallery-image" data-flex-grow="159" data-flex-basis="383px">
</p>
<p>I have seen Grok do something similar before, which in fact inspired me to include this question in my mini-benchmark.</p>
<p>GPT got a bit further, but for Hindi, Arabic and Bengali it listed the numerals in local script, not the number words. Gemini, GLM and Kimi gave a complete and correct answer as a list, while the absolute best answer and presentation was by Claude, that gave the table below:</p>
<blockquote>
<p><strong>Claude Opus 4.6</strong></p>
<p>10 Most Commonly Spoken Languages (by Total Speakers)</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Language</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td><strong>English</strong></td>
<td>One</td>
<td>Two</td>
<td>Three</td>
<td>Four</td>
<td>Five</td>
</tr>
<tr>
<td>2</td>
<td><strong>Mandarin Chinese</strong></td>
<td>&#19968; (y&#299;)</td>
<td>&#20108; (&egrave;r)</td>
<td>&#19977; (s&#257;n)</td>
<td>&#22235; (s&igrave;)</td>
<td>&#20116; (w&#468;)</td>
</tr>
<tr>
<td>3</td>
<td><strong>Hindi</strong></td>
<td>&#2319;&#2325; (ek)</td>
<td>&#2342;&#2379; (do)</td>
<td>&#2340;&#2368;&#2344; (t&#299;n)</td>
<td>&#2330;&#2366;&#2352; (ch&#257;r)</td>
<td>&#2346;&#2366;&#2305;&#2330; (p&#257;&#771;ch)</td>
</tr>
<tr>
<td>4</td>
<td><strong>Spanish</strong></td>
<td>Uno</td>
<td>Dos</td>
<td>Tres</td>
<td>Cuatro</td>
<td>Cinco</td>
</tr>
<tr>
<td>5</td>
<td><strong>French</strong></td>
<td>Un</td>
<td>Deux</td>
<td>Trois</td>
<td>Quatre</td>
<td>Cinq</td>
</tr>
<tr>
<td>6</td>
<td><strong>Arabic</strong></td>
<td>&#1608;&#1575;&#1581;&#1583; (w&#257;&#7717;id)</td>
<td>&#1575;&#1579;&#1606;&#1575;&#1606; (ithn&#257;n)</td>
<td>&#1579;&#1604;&#1575;&#1579;&#1577; (thal&#257;tha)</td>
<td>&#1571;&#1585;&#1576;&#1593;&#1577; (arba&#703;a)</td>
<td>&#1582;&#1605;&#1587;&#1577; (khamsa)</td>
</tr>
<tr>
<td>7</td>
<td><strong>Bengali</strong></td>
<td>&#2447;&#2453; (ek)</td>
<td>&#2470;&#2497;&#2439; (dui)</td>
<td>&#2468;&#2495;&#2472; (tin)</td>
<td>&#2458;&#2494;&#2480; (ch&#257;r)</td>
<td>&#2474;&#2494;&#2433;&#2458; (p&#257;&#771;ch)</td>
</tr>
<tr>
<td>8</td>
<td><strong>Portuguese</strong></td>
<td>Um</td>
<td>Dois</td>
<td>Tr&ecirc;s</td>
<td>Quatro</td>
<td>Cinco</td>
</tr>
<tr>
<td>9</td>
<td><strong>Russian</strong></td>
<td>&#1054;&#1076;&#1080;&#1085; (odin)</td>
<td>&#1044;&#1074;&#1072; (dva)</td>
<td>&#1058;&#1088;&#1080; (tri)</td>
<td>&#1063;&#1077;&#1090;&#1099;&#1088;&#1077; (chetyre)</td>
<td>&#1055;&#1103;&#1090;&#1100; (pyat&rsquo;)</td>
</tr>
<tr>
<td>10</td>
<td><strong>Japanese</strong></td>
<td>&#19968; (ichi)</td>
<td>&#20108; (ni)</td>
<td>&#19977; (san)</td>
<td>&#22235; (shi)</td>
<td>&#20116; (go)</td>
</tr>
</tbody>
</table>
<p><em><strong>Note:</strong> Rankings are based on estimated <em>total</em> speakers (native + non-native). Rankings can vary depending on the source and methodology used.</em></p>
</blockquote>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#5-count-the-number-of-drone-launch-pads-in-the-picture" class="header-anchor"></a>5. Count the number of drone launch pads in the picture.<br>
<a class="anchor-link" id="5-count-the-number-of-drone-launch-pads-in-the-picture"></a></h3>
<p>Together with the question, I uploaded this picture:</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/drone-pad-counting-task.jpg" width="2196" height="1584" loading="lazy" alt="Drone pad picture attached to question above" class="gallery-image" data-flex-grow="138" data-flex-basis="332px">
</p>
<p>A human can easily count that there are 10 rows and 30+ columns in the grid, but because the picture resolution isn&rsquo;t good enough, the exact number of columns can&rsquo;t be counted, and the answer should be that there are at least 300 launch pads in the picture.</p>
<p>GPT and Grok both guessed the count is zero. Instead of hallucinating some number they say zero, but it would have been better to not give any number at all, and just state that they are unable to perform the task. Gemini gave as its answer &ldquo;101&rdquo;, which is quite odd, but reading the reasoning section, it seems to have tried counting items in the image without reasoning much about what it is actually counting and that there is clearly a grid that can make the counting much easier. Both Qwen and Kimi state they can see four parallel structures, but are unable to count drone launch pads.</p>
<p>The absolutely best answer was given by Claude, which counted 10-12 rows and 30-40+ columns, and concluded that there must be 300-500 drone launch pads. Very close to best human level &ndash; impressive!</p>
<p>This question applied only to multi-modal models that can see images, so GLM and MinMax could not give any response.</p>
<h3><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#6-explain-why-i-am-getting-the-error-below-and-what-is-the-best-way-to-fix-it" class="header-anchor"></a>6. Explain why I am getting the error below, and what is the best way to fix it?<br>
<a class="anchor-link" id="6-explain-why-i-am-getting-the-error-below-and-what-is-the-best-way-to-fix-it"></a></h3>
<p>Together with the question above, I gave this code block:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang"></span><br>
<button class="codeblock-copy" data-id="codeblock-id-0" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>$ SH_SCRIPTS="$(mktemp; grep -Irnw debian/ -e '^#!.*/sh' | sort -u | cut -d ':' -f 1 || true)"<br>
$ shellcheck -x --enable=all --shell=sh "$SH_SCRIPTS"<br>
/tmp/tmp.xQOpI5Nljx<br>
debian/tests/integration-tests: /tmp/tmp.xQOpI5Nljx<br>
debian/tests/integration-tests: openBinaryFile: does not exist (No such file or directory)</code></p>
<pre><code>$ SH_SCRIPTS="$(mktemp; grep -Irnw debian/ -e '^#!.*/sh' | sort -u | cut -d ':' -f 1 || true)"
$ shellcheck -x --enable=all --shell=sh "$SH_SCRIPTS"
/tmp/tmp.xQOpI5Nljx
debian/tests/integration-tests: /tmp/tmp.xQOpI5Nljx
debian/tests/integration-tests: openBinaryFile: does not exist (No such file or directory)</code></pre>
</div>
<p>Older models would easily be misled by the last error message thinking that a file went missing, and focus on suggesting changes to the complex-looking first line. In reality the error is simply caused by having the quotes around the <code>$SH_SCRIPTS</code>, resulting in the entire multi-line string being passed as a single argument to <code>shellcheck</code>. So instead of receiving two separate file paths, <code>shellcheck</code> tries to open one file literally named <code>/tmp/tmp.xQOpI5Nljxndebian/tests/integration-tests</code>.</p>
<p>Incorrect argument expansion is fairly easy for an experienced human programmer to notice, but tricky for an LLM. Indeed, Grok, MinMax, and Qwen fell for this trap and focused on the <code>mktemp</code>, assuming it somehow fails to create a file. Interestingly GLM fails to produce an answer at all, as the reasoning step seems to be looping, thinking too much about the missing file, but not understanding why it would be missing when there is nothing wrong with how <code>mktemp</code> is executed.</p>
<p>Claude, Gemini, and Kimi immediately spot the real root cause of passing the variable quoted and suggested correct fixes that involve either removing the quotes, or using Bash arrays or <code>xargs</code> in a way that makes the whole command also handle correctly filenames with spaces in them.</p>
<h2><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/#conclusion" class="header-anchor"></a>Conclusion<br>
<a class="anchor-link" id="conclusion"></a></h2>
<table>
<thead>
<tr>
<th>Model</th>
<th>Sports</th>
<th>Economics</th>
<th>Emoji</th>
<th>Languages</th>
<th>Visual</th>
<th>Shell</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Opus 4.6</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>6/6</td>
</tr>
<tr>
<td>GPT-5.2</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>~</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>2.5/6</td>
</tr>
<tr>
<td>Grok 4.1</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>&#10007;</td>
<td>&#10007;</td>
<td>3/6</td>
</tr>
<tr>
<td>Gemini 3.1 Pro</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>5/6</td>
</tr>
<tr>
<td>GLM 5</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>?</td>
<td>&#10003;</td>
<td>N/A</td>
<td>&#10007;</td>
<td>3/5</td>
</tr>
<tr>
<td>MinMax M2.5</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>&#10007;</td>
<td>N/A</td>
<td>&#10007;</td>
<td>1/5</td>
</tr>
<tr>
<td>Qwen3.5 Plus</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>~</td>
<td>&#10007;</td>
<td>&#10007;</td>
<td>2.5/6</td>
</tr>
<tr>
<td>Kimi K2.5</td>
<td>&#10003;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>&#10007;</td>
<td>&#10003;</td>
<td>4/6</td>
</tr>
</tbody>
</table>
<p>Obviously, my mini-benchmark only had 6 questions, and I ran it only once. This was obviously not scientifically rigorous. However it was systematic enough to trump just a <em>mere feeling</em>.</p>
<p>The main finding for me personally is that Claude Opus 4.6, the flagship model by <a class="link" href="https://www.anthropic.com/" target="_blank" rel="noopener">Anthropic</a>, seems to give great answers consistently. The answers are not only correct, but also well scoped giving enough information to cover everything that seems relevant, without blurping unnecessary filler.</p>
<p>I used Claude extensively in 2023-2024 when it was the main model available at my day work, but for the past year I had been using other models that I felt were better at the time. Now Claude seems to be the best-of-the-best again, with Gemini and Kimi as close follow-ups. <a class="link" href="https://openrouter.ai/compare/anthropic/claude-opus-4.6/google/gemini-3.1-pro-preview/moonshotai/kimi-k2.5" target="_blank" rel="noopener">Comparing their pricing at OpenRouter.ai</a> the Kimi K2.5 price of $0.6 / million tokens is almost 90% cheaper than the Claude Opus 4.6&rsquo;s $5.0 / million tokens suggests that Kimi K2.5 offers the best <strong>price-per-performance ratio</strong>. Claude might be cheaper with a monthly subscription directly from Anthropic, potentially narrowing the price gap.</p>
<p>Overall I do feel that Anthropic, Google and Moonshot.ai have been pushing the envelope with their latest models in a way that <strong>one can&rsquo;t really claim that AI models have plateaued</strong>. In fact, one could claim that at least Claude has now climbed over the hill of <a class="link" href="https://en.wikipedia.org/wiki/AI_slop" target="_blank" rel="noopener">&ldquo;AI slop&rdquo;</a> and consistently produces valuable results. If and when AI usage expands from here, <strong>we might actually not drown in AI slop</strong> as chances of accidentally crappy results decrease. This makes me positive about the future.</p>
<p>I am also really happy to see that there wasn&rsquo;t just one model crushing everybody else, but that there are <strong>at least three models doing very well</strong>. As an open source enthusiast I am particularly glad to see that <a class="link" href="https://www.moonshot.ai/" target="_blank" rel="noopener">Moonshot.ai&rsquo;s</a> Kimi K2.5 is published with an open license. Given the hardware, anyone can run it on their own. OpenRouter.ai currently lists <a class="link" href="https://openrouter.ai/moonshotai/kimi-k2.5/providers" target="_blank" rel="noopener">9 independent providers</a> alongside Moonshot.ai itself, showcasing the potential of open-weight models in practice.</p>
<p>If the pattern holds and flagship models continue improving at this pace we might look back at 2026 as the year AI stopped feeling like a call center associate and started to resemble a scientific researcher. While new models become available we need to keep testing, keep questioning, and keep our expectations grounded in actual performance rather than press releases.</p>
<p>Thanks to OpenRouter.ai for providing a great service that makes testing various models incredibly easy!</p>

<p><a href="https://optimizedbyotto.com/post/ai-models-plateaued-or-not/">Do AI models still keep getting better, or have they plateaued?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL 8.0 JSON Functions: Practical Examples and Indexing</title>
      <link rel="alternate" type="text/html" href="https://anothermysqldba.blogspot.com/2026/02/mysql-80-json-functions-practical.html" />
      <id>https://anothermysqldba.blogspot.com/2026/02/mysql-80-json-functions-practical.html</id>
      <updated>2026-02-21T22:39:00+02:00</updated>
      <author><name>Keith Larson ( anothermysqldba )</name></author>
      <summary type="html"><![CDATA[<p>This post covers a hands-on walkthrough of MySQL 8.0\'s JSON functions. JSON support has been in MySQL since 5.7, but 8.0 added a meaningful set of improvements — better indexing strategies, new functions, and multi-valued indexes — that make working with JSON data considerably more practical. The following documents several of the most commonly needed patterns, including EXPLAIN output and performance observations worth knowing about.</p>
<p>This isn\'t a \"JSON vs. relational\" debate post. If you\'re storing JSON in MySQL, you probably already have your reasons. The goal here is to make sure you\'re using the available tooling effectively.</p>
<p>Environment</p>
<p>mysql &#62; SELECT @@version, @@version_commentG<br />
*************************** 1. row ***************************<br />
    @@version: 8.0.36<br />
@@version_comment: MySQL Community Server - GPL</p>
<p>Testing was done on a VM with 8GB RAM and innodb_buffer_pool_size set to 4G. One housekeeping note worth mentioning: query_cache_type is irrelevant in 8.0 since the query cache was removed entirely. If you migrated a 5.7 instance and still have that variable in your my.cnf, remove it — MySQL 8.0 will throw a startup error.</p>
<p>Setting Up a Test Table</p>
<p>The test table simulates a fairly common pattern — an application storing user profile data and event metadata as JSON blobs:</p>
<p>CREATE TABLE user_events (<br />
 id     INT UNSIGNED NOT NULL AUTO_INCREMENT,<br />
 user_id   INT UNSIGNED NOT NULL,<br />
 event_data JSON NOT NULL,<br />
 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,<br />
 PRIMARY KEY (id),<br />
 INDEX idx_user (user_id)<br />
) ENGINE=InnoDB;</p>
<p>INSERT INTO user_events (user_id, event_data) VALUES<br />
(1, \'{\"action\":\"login\",\"ip\":\"192.168.1.10\",\"tags\":[\"mobile\",\"vpn\"],\"score\":88}\'),<br />
(1, \'{\"action\":\"purchase\",\"ip\":\"192.168.1.10\",\"tags\":[\"desktop\"],\"score\":72,\"amount\":49.99}\'),<br />
(2, \'{\"action\":\"login\",\"ip\":\"10.0.0.5\",\"tags\":[\"mobile\"],\"score\":91}\'),<br />
(3, \'{\"action\":\"logout\",\"ip\":\"10.0.0.9\",\"tags\":[\"desktop\",\"vpn\"],\"score\":65}\'),<br />
(2, \'{\"action\":\"purchase\",\"ip\":\"10.0.0.5\",\"tags\":[\"mobile\"],\"score\":84,\"amount\":129.00}\');</p>
<p>Basic Extraction: JSON_VALUE vs. JSON_EXTRACT</p>
<p>JSON_VALUE() was introduced in MySQL 8.0.21 and is the cleaner way to extract scalar values with built-in type casting. Before that, you were using JSON_EXTRACT() (or the - &#62; shorthand) and casting manually, which works but adds noise to your queries.</p>
<p>-- Pre-8.0.21 approach<br />
SELECT user_id,<br />
    JSON_EXTRACT(event_data, \'$.action\') AS action,<br />
    CAST(JSON_EXTRACT(event_data, \'$.score\') AS UNSIGNED) AS score<br />
FROM user_events;</p>
<p>-- Cleaner 8.0.21+ approach<br />
SELECT user_id,<br />
    JSON_VALUE(event_data, \'$.action\') AS action,<br />
    JSON_VALUE(event_data, \'$.score\' RETURNING UNSIGNED) AS score<br />
FROM user_events;</p>
<p>Output from the second query:</p>
<p>+---------+----------+-------+<br />
&#124; user_id &#124; action  &#124; score &#124;<br />
+---------+----------+-------+<br />
&#124;    1 &#124; login  &#124;  88 &#124;<br />
&#124;    1 &#124; purchase &#124;  72 &#124;<br />
&#124;    2 &#124; login  &#124;  91 &#124;<br />
&#124;    3 &#124; logout  &#124;  65 &#124;<br />
&#124;    2 &#124; purchase &#124;  84 &#124;<br />
+---------+----------+-------+<br />
5 rows in set (0.00 sec)</p>
<p>The RETURNING clause is genuinely useful. It eliminates the awkward double-cast pattern and makes intent clearer when reading query code later.</p>
<p>Multi-Valued Indexes: The Real Game Changer</p>
<p>This is where 8.0 actually moved the needle for JSON workloads. Multi-valued indexes, available since MySQL 8.0.17, let you index array elements inside a JSON column directly. Here\'s what that looks like in practice:</p>
<p>ALTER TABLE user_events<br />
 ADD INDEX idx_tags ((CAST(event_data- &#62;\'$.tags\' AS CHAR(64) ARRAY)));</p>
<p>Here is what EXPLAIN shows before and after on a query filtering by tag value:</p>
<p>-- Without the multi-valued index:<br />
EXPLAIN SELECT * FROM user_events<br />
WHERE JSON_CONTAINS(event_data- &#62;\'$.tags\', \'\"vpn\"\')G</p>
<p>*************************** 1. row ***************************<br />
      id: 1<br />
 select_type: SIMPLE<br />
    table: user_events<br />
  partitions: NULL<br />
     type: ALL<br />
possible_keys: NULL<br />
     key: NULL<br />
   key_len: NULL<br />
     ref: NULL<br />
     rows: 5<br />
   filtered: 100.00<br />
    Extra: Using where</p>
<p>-- After adding the multi-valued index:<br />
EXPLAIN SELECT * FROM user_events<br />
WHERE JSON_CONTAINS(event_data- &#62;\'$.tags\', \'\"vpn\"\')G</p>
<p>*************************** 1. row ***************************<br />
      id: 1<br />
 select_type: SIMPLE<br />
    table: user_events<br />
  partitions: NULL<br />
     type: range<br />
possible_keys: idx_tags<br />
     key: idx_tags<br />
   key_len: 67<br />
     ref: NULL<br />
     rows: 2<br />
   filtered: 100.00<br />
    Extra: Using where</p>
<p>Full table scan down to a range scan. On 5 rows this is trivial, but on a table with millions of rows and frequent tag-based filtering, that difference is significant. The improvement scales directly with table size and query frequency.</p>
<p>One important gotcha: MEMBER OF() and JSON_OVERLAPS() also benefit from multi-valued indexes, but JSON_SEARCH() does not. This matters when choosing your query pattern at design time:</p>
<p>-- This WILL use the multi-valued index:<br />
SELECT * FROM user_events<br />
WHERE \'vpn\' MEMBER OF (event_data- &#62;\'$.tags\');</p>
<p>-- This will NOT use it:<br />
SELECT * FROM user_events<br />
WHERE JSON_SEARCH(event_data- &#62;\'$.tags\', \'one\', \'vpn\') IS NOT NULL;</p>
<p>Aggregating and Transforming JSON</p>
<p>A few aggregation functions worth knowing well:</p>
<p>-- Build a JSON array of actions per user<br />
SELECT user_id,<br />
    JSON_ARRAYAGG(JSON_VALUE(event_data, \'$.action\')) AS actions<br />
FROM user_events<br />
GROUP BY user_id;</p>
<p>+---------+----------------------+<br />
&#124; user_id &#124; actions       &#124;<br />
+---------+----------------------+<br />
&#124;    1 &#124; [\"login\",\"purchase\"] &#124;<br />
&#124;    2 &#124; [\"login\",\"purchase\"] &#124;<br />
&#124;    3 &#124; [\"logout\"]      &#124;<br />
+---------+----------------------+<br />
3 rows in set (0.01 sec)</p>
<p>-- Summarize into a JSON object keyed by action<br />
SELECT user_id,<br />
    JSON_OBJECTAGG(<br />
     JSON_VALUE(event_data, \'$.action\'),<br />
     JSON_VALUE(event_data, \'$.score\' RETURNING UNSIGNED)<br />
    ) AS score_by_action<br />
FROM user_events<br />
GROUP BY user_id;</p>
<p>+---------+--------------------------------+<br />
&#124; user_id &#124; score_by_action        &#124;<br />
+---------+--------------------------------+<br />
&#124;    1 &#124; {\"login\": 88, \"purchase\": 72} &#124;<br />
&#124;    2 &#124; {\"login\": 91, \"purchase\": 84} &#124;<br />
&#124;    3 &#124; {\"logout\": 65}         &#124;<br />
+---------+--------------------------------+<br />
3 rows in set (0.00 sec)</p>
<p>JSON_OBJECTAGG() will throw an error if there are duplicate keys within a group. This is worth knowing before you encounter it in a production ETL pipeline. In that case, you\'ll need to deduplicate upstream or handle it in application logic before the data reaches this aggregation step.</p>
<p>Checking SHOW STATUS After JSON-Heavy Queries</p>
<p>When evaluating query patterns, checking handler metrics is a useful habit:</p>
<p>FLUSH STATUS;</p>
<p>SELECT * FROM user_events<br />
WHERE JSON_VALUE(event_data, \'$.score\' RETURNING UNSIGNED) &#62; 80;</p>
<p>SHOW STATUS LIKE \'Handler_read%\';</p>
<p>+----------------------------+-------+<br />
&#124; Variable_name       &#124; Value &#124;<br />
+----------------------------+-------+<br />
&#124; Handler_read_first     &#124; 1   &#124;<br />
&#124; Handler_read_key      &#124; 0   &#124;<br />
&#124; Handler_read_last     &#124; 0   &#124;<br />
&#124; Handler_read_next     &#124; 4   &#124;<br />
&#124; Handler_read_prev     &#124; 0   &#124;<br />
&#124; Handler_read_rnd      &#124; 0   &#124;<br />
&#124; Handler_read_rnd_next   &#124; 6   &#124;<br />
+----------------------------+-------+<br />
7 rows in set (0.00 sec)</p>
<p>The Handler_read_rnd_next value confirms a full scan — no surprise since there\'s no functional index on the score value. For score-based filtering at scale, a generated column with an index is the right answer:</p>
<p>ALTER TABLE user_events<br />
 ADD COLUMN score_val TINYINT UNSIGNED<br />
  GENERATED ALWAYS AS (JSON_VALUE(event_data, \'$.score\' RETURNING UNSIGNED)) VIRTUAL,<br />
 ADD INDEX idx_score (score_val);</p>
<p>After adding that, the same query drops to a proper index range scan. Generated columns on JSON fields are available in both MySQL 8.0 and Percona Server 8.0, and they remain the most reliable path for scalar JSON field filtering at any meaningful scale.</p>
<p>If you\'re running Percona Server, pt-query-digest from the Percona Toolkit is still the most practical way to identify which JSON-heavy queries are actually causing pain in production before you start adding indexes speculatively.</p>
<p>Practical Observations</p>
<p> Multi-valued indexes (8.0.17+) are a long overdue improvement and work well when your query patterns align with JSON_CONTAINS() or MEMBER OF()<br />
 JSON_VALUE() with RETURNING (8.0.21+) is cleaner than the old cast-after-extract pattern and worth adopting consistently<br />
 Generated columns plus indexes remain the most reliable path for scalar JSON field filtering at scale<br />
 Watch for JSON_OBJECTAGG() duplicate key errors in grouped data — it surfaces as a hard error in ETL pipelines and can be easy to miss in testing if your sample data happens to be clean<br />
 Always verify index usage with EXPLAIN — the optimizer doesn\'t always pick up multi-valued indexes in complex WHERE clauses, and it\'s worth confirming rather than assuming</p>
<p>Summary</p>
<p>MySQL 8.0\'s JSON improvements are genuinely useful, particularly multi-valued indexes and JSON_VALUE() with type casting. They don\'t replace good schema design, but for cases where JSON storage is appropriate or inherited, you now have real tools to work with rather than just hoping the optimizer figures it out. The generated column pattern in particular is worth evaluating early if you know certain JSON fields will be used in WHERE clauses regularly.</p>
<p>Useful references:</p>
<p> MySQL 8.0 JSON Function Reference<br />
 Multi-Valued Indexes Documentation<br />
 JSON_VALUE() Function Reference<br />
 Percona Toolkit</p>
<p><a href="https://anothermysqldba.blogspot.com/2026/02/mysql-80-json-functions-practical.html">MySQL 8.0 JSON Functions: Practical Examples and Indexing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This post covers a hands-on walkthrough of MySQL 8.0&rsquo;s JSON functions. JSON support has been in MySQL since 5.7, but 8.0 added a meaningful set of improvements &mdash; better indexing strategies, new functions, and multi-valued indexes &mdash; that make working with JSON data considerably more practical. The following documents several of the most commonly needed patterns, including EXPLAIN output and performance observations worth knowing about.</p>
<p>This isn&rsquo;t a &ldquo;JSON vs. relational&rdquo; debate post. If you&rsquo;re storing JSON in MySQL, you probably already have your reasons. The goal here is to make sure you&rsquo;re using the available tooling effectively.</p>
<h3>Environment<a class="anchor-link" id="environment"></a></h3>
<pre><code>mysql&gt; SELECT @@version, @@version_commentG
*************************** 1. row ***************************
        @@version: 8.0.36
@@version_comment: MySQL Community Server - GPL
</code></pre>
<p>Testing was done on a VM with 8GB RAM and <strong>innodb_buffer_pool_size</strong> set to 4G. One housekeeping note worth mentioning: <strong>query_cache_type</strong> is irrelevant in 8.0 since the query cache was removed entirely. If you migrated a 5.7 instance and still have that variable in your my.cnf, remove it &mdash; MySQL 8.0 will throw a startup error.</p>
<h3>Setting Up a Test Table<a class="anchor-link" id="setting-up-a-test-table"></a></h3>
<p>The test table simulates a fairly common pattern &mdash; an application storing user profile data and event metadata as JSON blobs:</p>
<pre><code>CREATE TABLE user_events (
  id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id     INT UNSIGNED NOT NULL,
  event_data  JSON NOT NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  INDEX idx_user (user_id)
) ENGINE=InnoDB;

INSERT INTO user_events (user_id, event_data) VALUES
(1, '{"action":"login","ip":"192.168.1.10","tags":["mobile","vpn"],"score":88}'),
(1, '{"action":"purchase","ip":"192.168.1.10","tags":["desktop"],"score":72,"amount":49.99}'),
(2, '{"action":"login","ip":"10.0.0.5","tags":["mobile"],"score":91}'),
(3, '{"action":"logout","ip":"10.0.0.9","tags":["desktop","vpn"],"score":65}'),
(2, '{"action":"purchase","ip":"10.0.0.5","tags":["mobile"],"score":84,"amount":129.00}');
</code></pre>
<h3>Basic Extraction: JSON_VALUE vs. JSON_EXTRACT<a class="anchor-link" id="basic-extraction-json_value-vs-json_extract"></a></h3>
<p><strong>JSON_VALUE()</strong> was introduced in MySQL 8.0.21 and is the cleaner way to extract scalar values with built-in type casting. Before that, you were using <strong>JSON_EXTRACT()</strong> (or the <strong>-&gt;</strong> shorthand) and casting manually, which works but adds noise to your queries.</p>
<pre><code>-- Pre-8.0.21 approach
SELECT user_id,
       JSON_EXTRACT(event_data, '$.action') AS action,
       CAST(JSON_EXTRACT(event_data, '$.score') AS UNSIGNED) AS score
FROM user_events;

-- Cleaner 8.0.21+ approach
SELECT user_id,
       JSON_VALUE(event_data, '$.action') AS action,
       JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) AS score
FROM user_events;
</code></pre>
<p>Output from the second query:</p>
<pre><code>+---------+----------+-------+
| user_id | action   | score |
+---------+----------+-------+
|       1 | login    |    88 |
|       1 | purchase |    72 |
|       2 | login    |    91 |
|       3 | logout   |    65 |
|       2 | purchase |    84 |
+---------+----------+-------+
5 rows in set (0.00 sec)
</code></pre>
<p>The <strong>RETURNING</strong> clause is genuinely useful. It eliminates the awkward double-cast pattern and makes intent clearer when reading query code later.</p>
<h3>Multi-Valued Indexes: The Real Game Changer<a class="anchor-link" id="multi-valued-indexes-the-real-game-changer"></a></h3>
<p>This is where 8.0 actually moved the needle for JSON workloads. Multi-valued indexes, available since MySQL 8.0.17, let you index array elements inside a JSON column directly. Here&rsquo;s what that looks like in practice:</p>
<pre><code>ALTER TABLE user_events
  ADD INDEX idx_tags ((CAST(event_data-&gt;'$.tags' AS CHAR(64) ARRAY)));
</code></pre>
<p>Here is what EXPLAIN shows before and after on a query filtering by tag value:</p>
<pre><code>-- Without the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data-&gt;'$.tags', '"vpn"')G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 100.00
        Extra: Using where

-- After adding the multi-valued index:
EXPLAIN SELECT * FROM user_events
WHERE JSON_CONTAINS(event_data-&gt;'$.tags', '"vpn"')G

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: user_events
   partitions: NULL
         type: range
possible_keys: idx_tags
          key: idx_tags
      key_len: 67
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using where
</code></pre>
<p>Full table scan down to a range scan. On 5 rows this is trivial, but on a table with millions of rows and frequent tag-based filtering, that difference is significant. The improvement scales directly with table size and query frequency.</p>
<p>One important gotcha: <strong>MEMBER OF()</strong> and <strong>JSON_OVERLAPS()</strong> also benefit from multi-valued indexes, but <strong>JSON_SEARCH()</strong> does not. This matters when choosing your query pattern at design time:</p>
<pre><code>-- This WILL use the multi-valued index:
SELECT * FROM user_events
WHERE 'vpn' MEMBER OF (event_data-&gt;'$.tags');

-- This will NOT use it:
SELECT * FROM user_events
WHERE JSON_SEARCH(event_data-&gt;'$.tags', 'one', 'vpn') IS NOT NULL;
</code></pre>
<h3>Aggregating and Transforming JSON<a class="anchor-link" id="aggregating-and-transforming-json"></a></h3>
<p>A few aggregation functions worth knowing well:</p>
<pre><code>-- Build a JSON array of actions per user
SELECT user_id,
       JSON_ARRAYAGG(JSON_VALUE(event_data, '$.action')) AS actions
FROM user_events
GROUP BY user_id;

+---------+----------------------+
| user_id | actions              |
+---------+----------------------+
|       1 | ["login","purchase"] |
|       2 | ["login","purchase"] |
|       3 | ["logout"]           |
+---------+----------------------+
3 rows in set (0.01 sec)

-- Summarize into a JSON object keyed by action
SELECT user_id,
       JSON_OBJECTAGG(
         JSON_VALUE(event_data, '$.action'),
         JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)
       ) AS score_by_action
FROM user_events
GROUP BY user_id;

+---------+--------------------------------+
| user_id | score_by_action                |
+---------+--------------------------------+
|       1 | {"login": 88, "purchase": 72}  |
|       2 | {"login": 91, "purchase": 84}  |
|       3 | {"logout": 65}                 |
+---------+--------------------------------+
3 rows in set (0.00 sec)
</code></pre>
<p><strong>JSON_OBJECTAGG()</strong> will throw an error if there are duplicate keys within a group. This is worth knowing before you encounter it in a production ETL pipeline. In that case, you&rsquo;ll need to deduplicate upstream or handle it in application logic before the data reaches this aggregation step.</p>
<h3>Checking SHOW STATUS After JSON-Heavy Queries<a class="anchor-link" id="checking-show-status-after-json-heavy-queries"></a></h3>
<p>When evaluating query patterns, checking handler metrics is a useful habit:</p>
<pre><code>FLUSH STATUS;

SELECT * FROM user_events
WHERE JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED) &gt; 80;

SHOW STATUS LIKE 'Handler_read%';

+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Handler_read_first         | 1     |
| Handler_read_key           | 0     |
| Handler_read_last          | 0     |
| Handler_read_next          | 4     |
| Handler_read_prev          | 0     |
| Handler_read_rnd           | 0     |
| Handler_read_rnd_next      | 6     |
+----------------------------+-------+
7 rows in set (0.00 sec)
</code></pre>
<p>The <strong>Handler_read_rnd_next</strong> value confirms a full scan &mdash; no surprise since there&rsquo;s no functional index on the score value. For score-based filtering at scale, a generated column with an index is the right answer:</p>
<pre><code>ALTER TABLE user_events
  ADD COLUMN score_val TINYINT UNSIGNED
    GENERATED ALWAYS AS (JSON_VALUE(event_data, '$.score' RETURNING UNSIGNED)) VIRTUAL,
  ADD INDEX idx_score (score_val);
</code></pre>
<p>After adding that, the same query drops to a proper index range scan. Generated columns on JSON fields are available in both MySQL 8.0 and Percona Server 8.0, and they remain the most reliable path for scalar JSON field filtering at any meaningful scale.</p>
<p>If you&rsquo;re running Percona Server, <strong>pt-query-digest</strong> from the <a href="https://www.percona.com/software/database-tools/percona-toolkit">Percona Toolkit</a> is still the most practical way to identify which JSON-heavy queries are actually causing pain in production before you start adding indexes speculatively.</p>
<h3>Practical Observations<a class="anchor-link" id="practical-observations"></a></h3>
<ul>
<li>Multi-valued indexes (8.0.17+) are a long overdue improvement and work well when your query patterns align with <strong>JSON_CONTAINS()</strong> or <strong>MEMBER OF()</strong></li>
<li><strong>JSON_VALUE() with RETURNING</strong> (8.0.21+) is cleaner than the old cast-after-extract pattern and worth adopting consistently</li>
<li>Generated columns plus indexes remain the most reliable path for scalar JSON field filtering at scale</li>
<li>Watch for <strong>JSON_OBJECTAGG()</strong> duplicate key errors in grouped data &mdash; it surfaces as a hard error in ETL pipelines and can be easy to miss in testing if your sample data happens to be clean</li>
<li>Always verify index usage with EXPLAIN &mdash; the optimizer doesn&rsquo;t always pick up multi-valued indexes in complex WHERE clauses, and it&rsquo;s worth confirming rather than assuming</li>
</ul>
<h3>Summary<a class="anchor-link" id="summary"></a></h3>
<p>MySQL 8.0&rsquo;s JSON improvements are genuinely useful, particularly multi-valued indexes and <strong>JSON_VALUE()</strong> with type casting. They don&rsquo;t replace good schema design, but for cases where JSON storage is appropriate or inherited, you now have real tools to work with rather than just hoping the optimizer figures it out. The generated column pattern in particular is worth evaluating early if you know certain JSON fields will be used in WHERE clauses regularly.</p>
<p>Useful references:</p>
<ul>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/json-function-reference.html">MySQL 8.0 JSON Function Reference</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/create-index.html#create-index-multi-valued">Multi-Valued Indexes Documentation</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-value">JSON_VALUE() Function Reference</a></li>
<li><a href="https://www.percona.com/software/database-tools/percona-toolkit">Percona Toolkit</a></li>
</ul>

<p><a href="https://anothermysqldba.blogspot.com/2026/02/mysql-80-json-functions-practical.html">MySQL 8.0 JSON Functions: Practical Examples and Indexing</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>FromDual Performance Monitor 2.2.1 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/" />
      <id>https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/</id>
      <updated>2026-02-19T17:18:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.2.1 of its popular Database Performance Monitor for MariaDB, Galera Cluster, MySQL and PostgreSQL fpmmm.<br />
The FromDual Performance Monitor enables Database and System Administrators to monitor and understand what is going on inside their databases and on the machines where the databases reside.<br />
More information you can find here: FromDual Performance Monitor.<br />
Download<br />
The new FromDual Performance Monitor can be downloaded from our Sofware Download page or you can use our repositories. How to install and use the FromDual Performance Monitor is documented in the Documentation.<br />
In the inconceivable case that you find a bug in the FromDual Performance Monitor please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Monitoring as a Service (MaaS)<br />
You do not want to set-up your database monitoring yourself? No problem: Choose our Monitoring as a Service (MaaS) to safe time and costs!<br />
Installation of Performance Monitor 2.2.1<br />
How to install the FromDual Performance Monitor you can find in the Installation Guide.<br />
Upgrade of fpmmm tar ball from 1.x to 2.2.1<br />
There are some changes in the configuration file (fpmmm.conf):</p>
<p>The access rights should be change as follows: chmod 600 /etc/fpmmm.conf<br />
The key Methode was spelled wrong in the configuration file. It was renamed to Method.<br />
The key PidFile is ambiguous which could lead to problems and bugs. Thus it was changed to either MyPidFile for fpmmm and DbPidFile for the database.</p>
<p>Upgrade with DEB/RPM packages should happen automatically. For tar balls follow this instruction:<br />
$ cd /opt<br />
$ tar xf /download/fpmmm-2.2.1.tar.gz<br />
$ rm -f fpmmm<br />
$ ln -s fpmmm-2.2.1 fpmmm</p>
<p>Changes in FromDual Performance Monitor 2.2.1<br />
These release notes include both the changes that came with version 2.2.0 and version 2.2.1.<br />
This release contains new features and various bug fixes.<br />
You can verify your current FromDual Performance Monitor version with the following command:<br />
$ /opt/fpmmm/bin/fpmmm --version</p>
<p>General</p>
<p>Updated to latest myEnv library.<br />
PHP 8.5 incompatibilities fixed.<br />
Typos fixed.<br />
Error messages improved.<br />
Function real_connect warnings send to console are suppressed now.<br />
Connection problems timeout reduced so in case of troubles we should see more and earlier…<br />
Other cosmetic errors and debugging information fixed.<br />
Data are gathered and set to zero even thought database is not reachable.<br />
Indention of logged messages fixed.<br />
Function exit is logged now as well.<br />
SSL connection handling added.<br />
Fix of error: array_sum(): Addition is not supported on type string in warning after upgrade to Ubuntu 24.04/PHP 8.3.<br />
Error log parsing had problems with huge error logs. Now we have added a size barrier.<br />
Function getDistributions updated/cleaned-up.<br />
Command lsb_release removed.<br />
Documentation added.<br />
Nagios: Tests fixed for MariaDB 11.8.</p>
<p>Templates</p>
<p>Server: Available I/O system information added to each I/O system on top, pages named.<br />
InnoDB: Pages named, row write operations graph added.<br />
MySQL: Some graphs and query dashboard made nicer.</p>
<p>Agent</p>
<p>none</p>
<p>Server</p>
<p>Items FromDual.MySQL.server.disk.avg_io_read_wait and FromDual.MySQL.server.disk.avg_io_write_wait removed because they are showing completely wrong values. Use FromDual.MySQL.server.disk.r_await and FromDual.MySQL.server.disk.w_await instead.<br />
Workaround for missing cpuinfo old cachefile implemented.</p>
<p>Galera</p>
<p>Old style variable fixed which causes problems with newer version.<br />
Default values on database stop added.<br />
Workaround for cut wsrep_provider_options bug in MySQL Galera Cluster added.</p>
<p>InnoDB</p>
<p>Variable innodb_log_file_size made consistent for MariaDB and MySQL.<br />
Deprecated and removed variable innodb_log_files_in_group removed.<br />
Fix for innodb_log_file_size in MySQL 9.4.<br />
Log occupancy graph added and graph added to dashboard.<br />
Variable tx_isolation replaced by transaction isolation which is deprecated in MariaDB 11.2 and MySQL 5.7.</p>
<p>MySQL</p>
<p>Variable vendor_versions_behind special case caught.<br />
Connection charset changed from utf8 to utf8mb4 due to errors in MariaDB 11.8.<br />
Template pages named.</p>
<p>Process</p>
<p>none</p>
<p>Security</p>
<p>Module improved for new behaviour in MariaDB 11.8.</p>
<p>Master</p>
<p>Wrong version check for master fixed.</p>
<p>Slave</p>
<p>Slave lagging problem fixed.<br />
Wrong version check for slave fixed.<br />
MySQL 8.4 commands added for replication monitoring.</p>
<p>Backup</p>
<p>none</p>
<p>PostgreSQL</p>
<p>Rudimentary PostgreSQL monitoring added.</p>
<p>Packaging</p>
<p>RHEL 8 added again.<br />
RPM spec adapted for RHEL 10.<br />
SNMP library updated.<br />
Debian 10 and RHEL 7 removed.<br />
DEB sign stuff added.</p>
<p>For subscriptions of commercial use of fpmmm please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/">FromDual Performance Monitor 2.2.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.2.1 of its popular Database Performance Monitor for MariaDB, Galera Cluster, MySQL and PostgreSQL <a href="https://www.fromdual.com/software/fromdual-performance-monitor/"><code>fpmmm</code></a>.</p>
<p>The FromDual Performance Monitor enables Database and System Administrators to monitor and understand what is going on inside their databases and on the machines where the databases reside.</p>
<p>More information you can find here: <a href="https://www.fromdual.com/software/fromdual-performance-monitor/">FromDual Performance Monitor</a>.</p>
<h2>Download<a class="anchor-link" id="download"></a></h2>
<p>The new FromDual Performance Monitor can be downloaded from our <a href="https://support.fromdual.com/admin/public/download.php" target="_blank">Sofware Download</a> page or you can use our <a href="https://www.fromdual.com/repositories/">repositories</a>. How to install and use the FromDual Performance Monitor is documented in the <a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html" target="_blank">Documentation</a>.</p>
<p>In the inconceivable case that you find a bug in the FromDual Performance Monitor please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20fpmmm">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please send them <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">to us</a>.</p>
<h2>Monitoring as a Service (MaaS)<a class="anchor-link" id="monitoring-as-a-service-maas"></a></h2>
<p>You do not want to set-up your database monitoring yourself? No problem: Choose our <a href="https://www.fromdual.com/services/monitoring-as-a-service-maas/">Monitoring as a Service</a> (MaaS) to safe time and costs!</p>
<h2>Installation of Performance Monitor 2.2.1<a class="anchor-link" id="installation-of-performance-monitor-2-2-1"></a></h2>
<p>How to install the FromDual Performance Monitor you can find in the <a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html#installation-guide" target="_blank">Installation Guide</a>.</p>
<h2>Upgrade of fpmmm tar ball from 1.x to 2.2.1<a class="anchor-link" id="upgrade-of-fpmmm-tar-ball-from-1-x-to-2-2-1"></a></h2>
<p>There are some changes in the configuration file (<code>fpmmm.conf</code>):</p>
<ul>
<li>The access rights should be change as follows: <code>chmod 600 /etc/fpmmm.conf</code></li>
<li>The key <code>Methode</code> was spelled wrong in the configuration file. It was renamed to <code>Method</code>.</li>
<li>The key <code>PidFile</code> is ambiguous which could lead to problems and bugs. Thus it was changed to either <code>MyPidFile</code> for fpmmm and <code>DbPidFile</code> for the database.</li>
</ul>
<p>Upgrade with DEB/RPM packages should happen automatically. For tar balls follow this instruction:</p>
<pre><code>$ cd /opt
$ tar xf /download/fpmmm-2.2.1.tar.gz
$ rm -f fpmmm
$ ln -s fpmmm-2.2.1 fpmmm
</code></pre>
<h2>Changes in FromDual Performance Monitor 2.2.1<a class="anchor-link" id="changes-in-fromdual-performance-monitor-2-2-1"></a></h2>
<p>These release notes include both the changes that came with version 2.2.0 and version 2.2.1.</p>
<p>This release contains new features and various bug fixes.</p>
<p>You can verify your current FromDual Performance Monitor version with the following command:</p>
<pre><code>$ /opt/fpmmm/bin/fpmmm --version
</code></pre>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Updated to latest myEnv library.</li>
<li>PHP 8.5 incompatibilities fixed.</li>
<li>Typos fixed.</li>
<li>Error messages improved.</li>
<li>Function <code>real_connect</code> warnings send to console are suppressed now.</li>
<li>Connection problems timeout reduced so in case of troubles we should see more and earlier&hellip;</li>
<li>Other cosmetic errors and debugging information fixed.</li>
<li>Data are gathered and set to zero even thought database is not reachable.</li>
<li>Indention of logged messages fixed.</li>
<li>Function exit is logged now as well.</li>
<li>SSL connection handling added.</li>
<li>Fix of error: array_sum(): Addition is not supported on type string in warning after upgrade to Ubuntu 24.04/PHP 8.3.</li>
<li>Error log parsing had problems with huge error logs. Now we have added a size barrier.</li>
<li>Function <code>getDistributions</code> updated/cleaned-up.</li>
<li>Command lsb_release removed.</li>
<li>Documentation added.</li>
<li>Nagios: Tests fixed for MariaDB 11.8.</li>
</ul>
<h3>Templates<a class="anchor-link" id="templates"></a></h3>
<ul>
<li>Server: Available I/O system information added to each I/O system on top, pages named.</li>
<li>InnoDB: Pages named, row write operations graph added.</li>
<li>MySQL: Some graphs and query dashboard made nicer.</li>
</ul>
<h3>Agent<a class="anchor-link" id="agent"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>Server<a class="anchor-link" id="server"></a></h3>
<ul>
<li>Items <code>FromDual.MySQL.server.disk.avg_io_read_wait</code> and <code>FromDual.MySQL.server.disk.avg_io_write_wait</code> removed because they are showing completely wrong values. Use <code>FromDual.MySQL.server.disk.r_await</code> and <code>FromDual.MySQL.server.disk.w_await</code> instead.</li>
<li>Workaround for missing cpuinfo old cachefile implemented.</li>
</ul>
<h3>Galera<a class="anchor-link" id="galera"></a></h3>
<ul>
<li>Old style variable fixed which causes problems with newer version.</li>
<li>Default values on database stop added.</li>
<li>Workaround for cut <code>wsrep_provider_options</code> bug in MySQL Galera Cluster added.</li>
</ul>
<h3>InnoDB<a class="anchor-link" id="innodb"></a></h3>
<ul>
<li>Variable <code>innodb_log_file_size</code> made consistent for MariaDB and MySQL.</li>
<li>Deprecated and removed variable <code>innodb_log_files_in_group</code> removed.</li>
<li>Fix for <code>innodb_log_file_size</code> in MySQL 9.4.</li>
<li>Log occupancy graph added and graph added to dashboard.</li>
<li>Variable <code>tx_isolation</code> replaced by transaction isolation which is deprecated in MariaDB 11.2 and MySQL 5.7.</li>
</ul>
<h3>MySQL<a class="anchor-link" id="mysql"></a></h3>
<ul>
<li>Variable <code>vendor_versions_behind</code> special case caught.</li>
<li>Connection charset changed from <code>utf8</code> to <code>utf8mb4</code> due to errors in MariaDB 11.8.</li>
<li>Template pages named.</li>
</ul>
<h3>Process<a class="anchor-link" id="process"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>Security<a class="anchor-link" id="security"></a></h3>
<ul>
<li>Module improved for new behaviour in MariaDB 11.8.</li>
</ul>
<h3>Master<a class="anchor-link" id="master"></a></h3>
<ul>
<li>Wrong version check for master fixed.</li>
</ul>
<h3>Slave<a class="anchor-link" id="slave"></a></h3>
<ul>
<li>Slave lagging problem fixed.</li>
<li>Wrong version check for slave fixed.</li>
<li>MySQL 8.4 commands added for replication monitoring.</li>
</ul>
<h3>Backup<a class="anchor-link" id="backup"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li>Rudimentary PostgreSQL monitoring added.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>RHEL 8 added again.</li>
<li>RPM spec adapted for RHEL 10.</li>
<li>SNMP library updated.</li>
<li>Debian 10 and RHEL 7 removed.</li>
<li>DEB sign stuff added.</li>
</ul>
<p>For subscriptions of commercial use of <code>fpmmm</code> please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20fpmmm">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/">FromDual Performance Monitor 2.2.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>FromDual Performance Monitor 2.2.1 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/" />
      <id>https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/</id>
      <updated>2026-02-19T17:18:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.2.1 of its popular Database Performance Monitor for MariaDB, Galera Cluster, MySQL and PostgreSQL fpmmm.<br />
The FromDual Performance Monitor enables Database and System Administrators to monitor and understand what is going on inside their databases and on the machines where the databases reside.<br />
More information you can find here: FromDual Performance Monitor.<br />
Download<br />
The new FromDual Performance Monitor can be downloaded from our Sofware Download page or you can use our repositories. How to install and use the FromDual Performance Monitor is documented in the Documentation.<br />
In the inconceivable case that you find a bug in the FromDual Performance Monitor please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Monitoring as a Service (MaaS)<br />
You do not want to set-up your database monitoring yourself? No problem: Choose our Monitoring as a Service (MaaS) to safe time and costs!<br />
Installation of Performance Monitor 2.2.1<br />
How to install the FromDual Performance Monitor you can find in the Installation Guide.<br />
Upgrade of fpmmm tar ball from 1.x to 2.2.1<br />
There are some changes in the configuration file (fpmmm.conf):</p>
<p>The access rights should be change as follows: chmod 600 /etc/fpmmm.conf<br />
The key Methode was spelled wrong in the configuration file. It was renamed to Method.<br />
The key PidFile is ambiguous which could lead to problems and bugs. Thus it was changed to either MyPidFile for fpmmm and DbPidFile for the database.</p>
<p>Upgrade with DEB/RPM packages should happen automatically. For tar balls follow this instruction:<br />
$ cd /opt<br />
$ tar xf /download/fpmmm-2.2.1.tar.gz<br />
$ rm -f fpmmm<br />
$ ln -s fpmmm-2.2.1 fpmmm</p>
<p>Changes in FromDual Performance Monitor 2.2.1<br />
These release notes include both the changes that came with version 2.2.0 and version 2.2.1.<br />
This release contains new features and various bug fixes.<br />
You can verify your current FromDual Performance Monitor version with the following command:<br />
$ /opt/fpmmm/bin/fpmmm --version</p>
<p>General</p>
<p>Updated to latest myEnv library.<br />
PHP 8.5 incompatibilities fixed.<br />
Typos fixed.<br />
Error messages improved.<br />
Function real_connect warnings send to console are suppressed now.<br />
Connection problems timeout reduced so in case of troubles we should see more and earlier…<br />
Other cosmetic errors and debugging information fixed.<br />
Data are gathered and set to zero even thought database is not reachable.<br />
Indention of logged messages fixed.<br />
Function exit is logged now as well.<br />
SSL connection handling added.<br />
Fix of error: array_sum(): Addition is not supported on type string in warning after upgrade to Ubuntu 24.04/PHP 8.3.<br />
Error log parsing had problems with huge error logs. Now we have added a size barrier.<br />
Function getDistributions updated/cleaned-up.<br />
Command lsb_release removed.<br />
Documentation added.<br />
Nagios: Tests fixed for MariaDB 11.8.</p>
<p>Templates</p>
<p>Server: Available I/O system information added to each I/O system on top, pages named.<br />
InnoDB: Pages named, row write operations graph added.<br />
MySQL: Some graphs and query dashboard made nicer.</p>
<p>Agent</p>
<p>none</p>
<p>Server</p>
<p>Items FromDual.MySQL.server.disk.avg_io_read_wait and FromDual.MySQL.server.disk.avg_io_write_wait removed because they are showing completely wrong values. Use FromDual.MySQL.server.disk.r_await and FromDual.MySQL.server.disk.w_await instead.<br />
Workaround for missing cpuinfo old cachefile implemented.</p>
<p>Galera</p>
<p>Old style variable fixed which causes problems with newer version.<br />
Default values on database stop added.<br />
Workaround for cut wsrep_provider_options bug in MySQL Galera Cluster added.</p>
<p>InnoDB</p>
<p>Variable innodb_log_file_size made consistent for MariaDB and MySQL.<br />
Deprecated and removed variable innodb_log_files_in_group removed.<br />
Fix for innodb_log_file_size in MySQL 9.4.<br />
Log occupancy graph added and graph added to dashboard.<br />
Variable tx_isolation replaced by transaction isolation which is deprecated in MariaDB 11.2 and MySQL 5.7.</p>
<p>MySQL</p>
<p>Variable vendor_versions_behind special case caught.<br />
Connection charset changed from utf8 to utf8mb4 due to errors in MariaDB 11.8.<br />
Template pages named.</p>
<p>Process</p>
<p>none</p>
<p>Security</p>
<p>Module improved for new behaviour in MariaDB 11.8.</p>
<p>Master</p>
<p>Wrong version check for master fixed.</p>
<p>Slave</p>
<p>Slave lagging problem fixed.<br />
Wrong version check for slave fixed.<br />
MySQL 8.4 commands added for replication monitoring.</p>
<p>Backup</p>
<p>none</p>
<p>PostgreSQL</p>
<p>Rudimentary PostgreSQL monitoring added.</p>
<p>Packaging</p>
<p>RHEL 8 added again.<br />
RPM spec adapted for RHEL 10.<br />
SNMP library updated.<br />
Debian 10 and RHEL 7 removed.<br />
DEB sign stuff added.</p>
<p>For subscriptions of commercial use of fpmmm please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/">FromDual Performance Monitor 2.2.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.2.1 of its popular Database Performance Monitor for MariaDB, Galera Cluster, MySQL and PostgreSQL <a href="https://www.fromdual.com/software/fromdual-performance-monitor/"><code>fpmmm</code></a>.</p>
<p>The FromDual Performance Monitor enables Database and System Administrators to monitor and understand what is going on inside their databases and on the machines where the databases reside.</p>
<p>More information you can find here: <a href="https://www.fromdual.com/software/fromdual-performance-monitor/">FromDual Performance Monitor</a>.</p>
<h2>Download<a class="anchor-link" id="download"></a></h2>
<p>The new FromDual Performance Monitor can be downloaded from our <a href="https://support.fromdual.com/admin/public/download.php" target="_blank">Sofware Download</a> page or you can use our <a href="https://www.fromdual.com/repositories/">repositories</a>. How to install and use the FromDual Performance Monitor is documented in the <a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html" target="_blank">Documentation</a>.</p>
<p>In the inconceivable case that you find a bug in the FromDual Performance Monitor please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20fpmmm">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please send them <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">to us</a>.</p>
<h2>Monitoring as a Service (MaaS)<a class="anchor-link" id="monitoring-as-a-service-maas"></a></h2>
<p>You do not want to set-up your database monitoring yourself? No problem: Choose our <a href="https://www.fromdual.com/services/monitoring-as-a-service-maas/">Monitoring as a Service</a> (MaaS) to safe time and costs!</p>
<h2>Installation of Performance Monitor 2.2.1<a class="anchor-link" id="installation-of-performance-monitor-2-2-1"></a></h2>
<p>How to install the FromDual Performance Monitor you can find in the <a href="https://support.fromdual.com/documentation/fpmmm/fpmmm.html#installation-guide" target="_blank">Installation Guide</a>.</p>
<h2>Upgrade of fpmmm tar ball from 1.x to 2.2.1<a class="anchor-link" id="upgrade-of-fpmmm-tar-ball-from-1-x-to-2-2-1"></a></h2>
<p>There are some changes in the configuration file (<code>fpmmm.conf</code>):</p>
<ul>
<li>The access rights should be change as follows: <code>chmod 600 /etc/fpmmm.conf</code></li>
<li>The key <code>Methode</code> was spelled wrong in the configuration file. It was renamed to <code>Method</code>.</li>
<li>The key <code>PidFile</code> is ambiguous which could lead to problems and bugs. Thus it was changed to either <code>MyPidFile</code> for fpmmm and <code>DbPidFile</code> for the database.</li>
</ul>
<p>Upgrade with DEB/RPM packages should happen automatically. For tar balls follow this instruction:</p>
<pre><code>$ cd /opt
$ tar xf /download/fpmmm-2.2.1.tar.gz
$ rm -f fpmmm
$ ln -s fpmmm-2.2.1 fpmmm
</code></pre>
<h2>Changes in FromDual Performance Monitor 2.2.1<a class="anchor-link" id="changes-in-fromdual-performance-monitor-2-2-1"></a></h2>
<p>These release notes include both the changes that came with version 2.2.0 and version 2.2.1.</p>
<p>This release contains new features and various bug fixes.</p>
<p>You can verify your current FromDual Performance Monitor version with the following command:</p>
<pre><code>$ /opt/fpmmm/bin/fpmmm --version
</code></pre>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Updated to latest myEnv library.</li>
<li>PHP 8.5 incompatibilities fixed.</li>
<li>Typos fixed.</li>
<li>Error messages improved.</li>
<li>Function <code>real_connect</code> warnings send to console are suppressed now.</li>
<li>Connection problems timeout reduced so in case of troubles we should see more and earlier&hellip;</li>
<li>Other cosmetic errors and debugging information fixed.</li>
<li>Data are gathered and set to zero even thought database is not reachable.</li>
<li>Indention of logged messages fixed.</li>
<li>Function exit is logged now as well.</li>
<li>SSL connection handling added.</li>
<li>Fix of error: array_sum(): Addition is not supported on type string in warning after upgrade to Ubuntu 24.04/PHP 8.3.</li>
<li>Error log parsing had problems with huge error logs. Now we have added a size barrier.</li>
<li>Function <code>getDistributions</code> updated/cleaned-up.</li>
<li>Command lsb_release removed.</li>
<li>Documentation added.</li>
<li>Nagios: Tests fixed for MariaDB 11.8.</li>
</ul>
<h3>Templates<a class="anchor-link" id="templates"></a></h3>
<ul>
<li>Server: Available I/O system information added to each I/O system on top, pages named.</li>
<li>InnoDB: Pages named, row write operations graph added.</li>
<li>MySQL: Some graphs and query dashboard made nicer.</li>
</ul>
<h3>Agent<a class="anchor-link" id="agent"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>Server<a class="anchor-link" id="server"></a></h3>
<ul>
<li>Items <code>FromDual.MySQL.server.disk.avg_io_read_wait</code> and <code>FromDual.MySQL.server.disk.avg_io_write_wait</code> removed because they are showing completely wrong values. Use <code>FromDual.MySQL.server.disk.r_await</code> and <code>FromDual.MySQL.server.disk.w_await</code> instead.</li>
<li>Workaround for missing cpuinfo old cachefile implemented.</li>
</ul>
<h3>Galera<a class="anchor-link" id="galera"></a></h3>
<ul>
<li>Old style variable fixed which causes problems with newer version.</li>
<li>Default values on database stop added.</li>
<li>Workaround for cut <code>wsrep_provider_options</code> bug in MySQL Galera Cluster added.</li>
</ul>
<h3>InnoDB<a class="anchor-link" id="innodb"></a></h3>
<ul>
<li>Variable <code>innodb_log_file_size</code> made consistent for MariaDB and MySQL.</li>
<li>Deprecated and removed variable <code>innodb_log_files_in_group</code> removed.</li>
<li>Fix for <code>innodb_log_file_size</code> in MySQL 9.4.</li>
<li>Log occupancy graph added and graph added to dashboard.</li>
<li>Variable <code>tx_isolation</code> replaced by transaction isolation which is deprecated in MariaDB 11.2 and MySQL 5.7.</li>
</ul>
<h3>MySQL<a class="anchor-link" id="mysql"></a></h3>
<ul>
<li>Variable <code>vendor_versions_behind</code> special case caught.</li>
<li>Connection charset changed from <code>utf8</code> to <code>utf8mb4</code> due to errors in MariaDB 11.8.</li>
<li>Template pages named.</li>
</ul>
<h3>Process<a class="anchor-link" id="process"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>Security<a class="anchor-link" id="security"></a></h3>
<ul>
<li>Module improved for new behaviour in MariaDB 11.8.</li>
</ul>
<h3>Master<a class="anchor-link" id="master"></a></h3>
<ul>
<li>Wrong version check for master fixed.</li>
</ul>
<h3>Slave<a class="anchor-link" id="slave"></a></h3>
<ul>
<li>Slave lagging problem fixed.</li>
<li>Wrong version check for slave fixed.</li>
<li>MySQL 8.4 commands added for replication monitoring.</li>
</ul>
<h3>Backup<a class="anchor-link" id="backup"></a></h3>
<ul>
<li>none</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li>Rudimentary PostgreSQL monitoring added.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>RHEL 8 added again.</li>
<li>RPM spec adapted for RHEL 10.</li>
<li>SNMP library updated.</li>
<li>Debian 10 and RHEL 7 removed.</li>
<li>DEB sign stuff added.</li>
</ul>
<p>For subscriptions of commercial use of <code>fpmmm</code> please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20fpmmm">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/fpmmm-release-notes/fromdual-performance-monitor-2.2.1-has-been-released/">FromDual Performance Monitor 2.2.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Explaining why throughput varies for Postgres with a CPU-bound Insert Benchmark</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/02/explaining-why-throughput-varies-for.html" />
      <id>https://smalldatum.blogspot.com/2026/02/explaining-why-throughput-varies-for.html</id>
      <updated>2026-02-18T20:38:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>Throughput for the write-heavy steps of the Insert Benchmark look like a distorted sine wave with Postgres on CPU-bound workloads but not on IO-bound workloads. For the CPU-bound workloads the chart for max response time at N-second intervals for inserts is flat but for deletes it looks like the distorted sine wave. To see the chart for deletes, scroll down from here. So this looks like a problem for deletes and this post starts to explain that.tl;drOnce again, blame vacuumHistory of the Insert BenchmarkLong ago (prior to 2010) the Insert Benchmark was published by Tokutek to highlight things that the TokuDB storage engine was great at. I was working on MySQL at Google at the time and the benchmark was useful to me, however it was written in C++. While the Insert Benchmark is great at showing the benefits of an LSM storage engine, this was years before MyRocks and I was only doing InnoDB at the time, on spinning disks. So I rewrote it in Python to make it easier to modify, and then the Tokutek team improved a few things about my rewrite, and I have been enhancing it slowly since then.Until a few years ago the steps of the benchmark were:load - insert in PK ordercreate 3 secondary indexesdo more inserts as fast as possibledo rate-limited inserts concurrent with range and point queriesThe problem with this approach is that the database size grows forever and that limited for how long I could run the benchmark before running out of storage. So I changed it and the new approach keeps the database at a fixed size after the load. The new workflow is:load - insert in PK ordercreate 3 secondary indexesdo inserts+deletes at the same rate, as fast as possibledo rate-limited inserts+deletes at the same rate concurrent with range and point queriesThe insert and delete statements run at the same rate to keep the table from changing size. The Insert Benchmark client uses Python multiprocessing, there is one process doing Insert statements, another doing Delete statements and both get their work from queues. Another process populates those queues and that other process controlling what is put on the queue is what keeps them running at the same rate.The benchmark treats the table like a queue, and when ordered by PK (transactionid) there are inserts at the high end and deletes at the low end. The delete statement currently looks like:    delete from %s where transactionid in        (select transactionid from %s where transactionid &#62;= %d order by transactionid asc limit %d)The delete statement is written like that because it must delete the oldest rows -- the ones that have the smallest value for transactionid. While the process that does deletes has some idea of what that smallest value is, it doesn\'t know it for sure, thus the query. To improve performance it maintains a guess for the value that will be</p>
<p><a href="https://smalldatum.blogspot.com/2026/02/explaining-why-throughput-varies-for.html">Explaining why throughput varies for Postgres with a CPU-bound Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Throughput for the write-heavy steps of the Insert Benchmark look like a distorted sine wave with Postgres <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.mem.30m.50m.1800s/tput.l.i1.html#pg181_o2nofp.cx10b_c8r32.ips">on CPU-bound</a>&nbsp;workloads but not <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.io.800m.5m.1800s/tput.l.i1.html#pg181_o2nofp.cx10b_c8r32.ips">on IO-bound</a> workloads. For the CPU-bound workloads the chart for <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.mem.30m.50m.1800s/tput.l.i1.html#pg181_o2nofp.cx10b_c8r32.imax">max response time at N-second intervals</a> for inserts is flat but for deletes it looks like the distorted sine wave. To see the chart for deletes, scroll down <a href="https://mdcallag.github.io/reports/dec25.ib.pn53.pg.latest.mem.30m.50m.1800s/tput.l.i1.html#pg181_o2nofp.cx10b_c8r32.imax">from here</a>. So this looks like a problem for deletes and this post starts to explain that.</p>
<p>tl;dr</p>

<ul>
<li>Once again, blame vacuum</li>
</ul>
<p><b>History of the Insert Benchmark</b></p>
<p>Long ago (prior to 2010) the <a href="https://smalldatum.blogspot.com/2017/06/the-insert-benchmark.html">Insert Benchmark</a> was published by Tokutek to highlight things that the TokuDB storage engine was great at. I was working on MySQL at Google at the time and the benchmark was useful to me, however it was written in C++. While the Insert Benchmark is great at showing the benefits of an LSM storage engine, this was years before MyRocks and I was only doing InnoDB at the time, on spinning disks. So I rewrote it in Python to make it easier to modify, and then the Tokutek team improved a few things about my rewrite, and I have been enhancing it slowly since then.</p>
<p>Until a few years ago the steps of the benchmark were:</p>

<ul>
<li>load &ndash; insert in PK order</li>
<li>create 3 secondary indexes</li>
<li>do more inserts as fast as possible</li>
<li>do rate-limited inserts concurrent with range and point queries</li>
</ul>
<div>The problem with this approach is that the database size grows forever and that limited for how long I could run the benchmark before running out of storage. So I changed it and the new approach keeps the database at a fixed size after the load. The new workflow is:</div>
<div>
<ul>
<li>load &ndash; insert in PK order</li>
<li>create 3 secondary indexes</li>
<li>do inserts+deletes at the same rate, as fast as possible</li>
<li>do rate-limited inserts+deletes at the same rate concurrent with range and point queries</li>
</ul>
<div>The insert and delete statements run at the same rate to keep the table from changing size. The Insert Benchmark client uses Python multiprocessing, there is one process doing Insert statements, another doing Delete statements and both get their work from queues. Another process populates those queues and that other process controlling what is put on the queue is what keeps them running at the same rate.</div>
<div></div>
<div>The benchmark treats the table like a queue, and when ordered by PK (transactionid) there are inserts at the high end and deletes at the low end. The delete statement currently looks like:<br><i>&nbsp; &nbsp; delete from %s where transactionid in</i></div>
<div><i>&nbsp; &nbsp; &nbsp; &nbsp; (select transactionid from %s where transactionid &gt;= %d order by transactionid asc limit %d)</i></div>
</div>
<div></div>
<div>The delete statement is written like that because it must delete the oldest rows &mdash; the ones that have the smallest value for transactionid. While the process that does deletes has some idea of what that smallest value is, it doesn&rsquo;t know it for sure, thus the query. To improve performance it maintains a guess for the value that will be &lt;= the real minimum and it updates that guess over time.</div>
<div></div>
<div>I encountered other performance problems with Postgres while figuring out how to maintain that guess and <a href="https://www.google.com/search?q=site%3Asmalldatum.blogspot.com+get_actual_variable_range">get_actual_variable_range() in Postgres</a> was the problem. Maintaining that guess requires a resync query every N seconds where the resync query is:&nbsp;<i>select min(transactionid) from %s</i>. The problem for this query in general is that is scans the low end of the PK index on transactionid and when vacuum hasn&rsquo;t been done recently, then it will scan and skip many entries that aren&rsquo;t visible (wasting much CPU and some IO) before finding visible rows. Unfortunately, there will be some time between consecutive vacuums to the same table and this problem can&rsquo;t be avoided. The result is that the response time for the query increases a lot in between vacuums. For more on how get_actual_variable_range() contributes to this problem, see <a href="https://smalldatum.blogspot.com/2024/01/explaining-performance-regression-in.html">this post</a>.
<p>I assume the sine wave for delete response time is caused by one or both of:</p>
<ul>
<li>get_actual_varable_range() CPU overhead while planning the delete statement</li>
<li>CPU overhead from scanning and skipping tombstones while executing the select subquery</li>
</ul>
</div>
<div>The structure of the delete statement above reduces the number of tombstones that the select subquery might encounter by specifying where <i>transactionid &gt;= %d</i>. Perhaps that isn&rsquo;t sufficient. Perhaps the Postgres query planner still has too much CPU overhead from get_actual_variable_range() while planning that delete statement. I have yet to figure that out. But I have figured out that vacuum is a frequent source of problems.</div>

<div></div>

<div>
<ul></ul>
</div>

<p><a href="https://smalldatum.blogspot.com/2026/02/explaining-why-throughput-varies-for.html">Explaining why throughput varies for Postgres with a CPU-bound Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PostgreSQL minor release postponed in Q1’ 2026</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/18/postgresql-minor-release-postponed-in-q1-2026/" />
      <id>https://percona.community/blog/2026/02/18/postgresql-minor-release-postponed-in-q1-2026/</id>
      <updated>2026-02-18T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>In case you are awaiting the February PostgreSQL Community minor update released on plan on February 12 we want to make sure that our users and customers are up to date and aware of what to expect.</p>
<p><a href="https://percona.community/blog/2026/02/18/postgresql-minor-release-postponed-in-q1-2026/">PostgreSQL minor release postponed in Q1’ 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In case you are awaiting the February PostgreSQL Community minor update <a href="https://www.postgresql.org/about/news/postgresql-182-178-1612-1516-and-1421-released-3235/" target="_blank" rel="noopener noreferrer">released on plan on February 12</a> we want to make sure that our users and customers are up to date and aware of what to expect.</p>
<p>This scheduled PostgreSQL release was delivered by the PostgreSQL Community on time and came carrying 5 CVE fixes and over 65 bugs bug fixes.</p>
<p>Unfortunately shortly after, the <a href="https://www.postgresql.org/about/news/out-of-cycle-release-scheduled-for-february-26-2026-3241/" target="_blank" rel="noopener noreferrer">release team announced that an additional out of cycle release</a> is planned for February 26. This follow up release addresses two regressions identified in the February 12 update.</p>
<p>Because of this, we have decided not to ship a <a href="https://docs.percona.com/postgresql/18/" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL</a> build based on the February 12 release. Instead, we will wait for the February 26 Community update and base our release on that version once it becomes available from PGDG. This means also a delay in the release of Percona Operator for PostgreSQL that uses images based on our PostgreSQL releases.</p>
<h3>Always look on the bright side<a class="anchor-link" id="always-look-on-the-bright-side"></a></h3>
<p><figure><img decoding="async" width="1536" height="1024" src="https://percona.community/blog/2026/02/Jan-always-Feb17_hu_cee9f5c7742b62d4.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>While this is a delay in release it comes with some benefits. For our users and customers, this means a cleaner upgrade path. Rather than releasing February 12 now and asking you to update again shortly after, we prefer to wait and deliver a single update that includes the fixes. Our goal is to make updates predictable and smooth for users of Percona Distribution for PostgreSQL, as well as extensions such as <a href="https://github.com/percona/pg_tde" target="_blank" rel="noopener noreferrer">pg_tde</a> and <a href="https://github.com/percona/pg_stat_monitor" target="_blank" rel="noopener noreferrer">pg_stat_monitor</a>. It should also allow you to carry less operational burden with the added maintenance that an extra update would require.</p>
<p>We appreciate how quickly the PostgreSQL Community identified and addressed the regressions. Open collaboration across the ecosystem, including reports and testing from many contributors, helps ensure PostgreSQL continues to improve for everyone.</p>
<h3>Path forward<a class="anchor-link" id="path-forward"></a></h3>
<p>This is the third out of cycle release in the past year, following similar updates in <a href="https://www.postgresql.org/about/news/out-of-cycle-release-scheduled-for-november-21-2024-2958/" target="_blank" rel="noopener noreferrer">November 2024</a> and <a href="https://www.postgresql.org/about/news/out-of-cycle-release-scheduled-for-february-20-2025-3016/" target="_blank" rel="noopener noreferrer">February 2025</a>. It highlights how responsive and diligent the PostgreSQL Community is when issues are identified. At the same time, it reminds us all how important continuous testing and collaboration are as PostgreSQL adoption continues to grow. Contributing to PostgreSQL, whether through testing, reporting, or development, is one of the best ways to help strengthen quality across the ecosystem.</p>
<h3>Elephants keep ears open<a class="anchor-link" id="elephants-keep-ears-open"></a></h3>
<p>As soon as the February 26 release is available and our builds are ready, we will share the update.</p>
<p>If you need to move forward with the February 12 version in the meantime, please reach out. We are happy to talk through your situation and help you assess what makes the most sense for your environment.</p>
<p>Our customers can contact us through Percona Support Services to receive the high quality assistance we are known for. We also encourage community users to reach out via the <a href="https://forums.percona.com/" target="_blank" rel="noopener noreferrer">Percona Community Forums</a>, where we will do our best to provide guidance based on the information you share.</p>
<p>Thank you for your trust and for being part of the Percona community.</p>

<p><a href="https://percona.community/blog/2026/02/18/postgresql-minor-release-postponed-in-q1-2026/">PostgreSQL minor release postponed in Q1’ 2026</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Let’s go physical(ly separated)</title>
      <link rel="alternate" type="text/html" href="https://medium.com/@arbaudie.it/lets-go-physical-ly-separated-4053ae5be14c?source=rss-c779d007e7fe------2" />
      <id>https://medium.com/@arbaudie.it/lets-go-physical-ly-separated-4053ae5be14c?source=rss-c779d007e7fe------2</id>
      <updated>2026-02-18T09:44:18+02:00</updated>
      <author><name>ArBauDie.IT</name></author>
      <summary type="html"><![CDATA[<p>In my previous article , we discussed best practices for securing and monitoring admin access, such as role-based access controls (RBAC). It’s a good time to have a quick reminder about AAA aka “ Authentication, Authorization and Accounting” , which is a framework designed to manage access to networked resources and ensure secure, controlled interactions :Authentication : Verifies the identity of a user or system attempting to access a resource. This step involves credentials to confirm legitimacy before granting access.Authorization : Determines what a user or system is allowed to do after authentication. It sets permissions, defining the actions or resources that can be accessed based on the user’s role, ensuring they only interact with the data or systems for which they have clearance.Accounting : Tracks and logs user activities within a system, providing a record of what actions were taken, when, and by whom. This audit trail helps in monitoring, troubleshooting, and detecting unauthorized or malicious behavior.Altogether, AAA enhances security by ensuring that only authenticated users can access specific resources, with their actions monitored and recorded for accountability. And that’s exactly what we did with use of personal logins, role based access control and using the audit log to track admin activity.We know that admins aren’t the only one being able to temper with or gaining unwanted access to datas : any user is a potential risk for our database. Obviously, the previous advices still applies but role based access can prove to be inefficient in case of fine grained access control.Let’s imagine we are in a high security line of business and users should have very strict and tight access control. One way to prevent malicous users to circumvent the filtering si to physically isolate the user from the data. But can we dot it with MariaDB ?First step, we create a user as mentionned in my previous article.CREATE USER \'IT-O\'@\'Coruscant\' IDENTIFIED via ed25519 REQUIRE SSL WITH MAX_USER_CONNECTIONS 1 PASSWORD EXPIRE INTERVAL xx DAYS;Second step, we create a dedicated schema using his user token.Third step, we are gonna use to match the tables. We are now leveraging 3 parameters of said views : , DEFINER and SQL SECURITY :ALGORITHM allows us to tell how to execute the view. Here we will be choosing to make use of a temporary table to store the result of the view. While not the fastest, it has the good taste to make the view unwritable, on top of not having granted INSERT,UPDATE/DELETE privileges to the user,DEFINER allows us to give “ownership” of the view to a specific account,SQL SECURITY allows us to have the view executed with the DEFINER set of privileges instead of the INVOKER (aka the user) one.CREATE VIEW `IT-O`.BoobyTable ALGORITHM=temptable DEFINER=`locked.admin.account`@`localhost` SQL SECURITY=DEFINER AS SELECT necessary,columns,only FROM RealSchema.RealTable WHERE RestrictionClauses=values;Fourth thing, said user needs to manipulate the underlying table datas. We could theoritically allow the user to write through the view . But the control over its action is then limited, hence i prefer creating ad hoc that will emulate the desired actions. With those objects we can leverage the same last 2 parameters( stored routines ) as with views to ensure a good isolation. DEFINER &#38; SQL SECURITYDELIMITER // CREATE OR REPLACE DEFINER=`locked.admin.account`@`localhost` PROCEDURE `IT-O`.Action_BoobyTable (IN param_name type , OUT param_name type) SQL SECURITY=DEFINER BEGIN ACTIONS in SQL/PSM or PL/SQL END// DELIMITER ;Fifth step, we give the permissions to said user over the objects it needs.CREATE ROLE `IT-O`; GRANT SELECT on `IT-O`.* to `IT-O`; GRANT EXECUTE on `IT-O`.necesary_procs to `IT-O`; GRANT `IT-O` to `IT-O`@`Coruscant`; SET DEFAULT ROLE `IT-O` for `IT-O`@`Coruscant`;And now we have a user which acces is tightly controled and monitored even tho he has direct access to the database. Of course this could also be upgraded making him flow through on a dedicated service with , general login , query throttling , and possibly also resultset size limitation . inserting connexion IP in the statement data maskingOf course, deployment of such an internal architecture can be partially automated since it is mostly linked to parametrizing.Special thanks to Federico Razzoli for mentionning the use of a locked admin account as DEFINER.Originally published at https://www.linkedin.com.</p>
<p><a href="https://medium.com/@arbaudie.it/lets-go-physical-ly-separated-4053ae5be14c?source=rss-c779d007e7fe------2">Let’s go physical(ly separated)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<figure><img decoding="async" alt="" src="https://cdn-images-1.medium.com/max/1024/0*Ox7hBbPR7lTe6cyq"></figure>
<p>In my <a href="https://medium.com/@arbaudie.it/cave-adminem-7c97503f289c">previous article</a>&nbsp;, we discussed best practices for securing and monitoring admin access, such as role-based access controls (RBAC). It&rsquo;s a good time to have a quick reminder about AAA aka &ldquo; Authentication, Authorization and Accounting&rdquo;&nbsp;, which is a framework designed to manage access to networked resources and ensure secure, controlled interactions&nbsp;:</p>
<ol>
<li>Authentication&nbsp;: Verifies the identity of a user or system attempting to access a resource. This step involves credentials to confirm legitimacy before granting&nbsp;access.</li>
<li>Authorization&nbsp;: Determines what a user or system is allowed to do after authentication. It sets permissions, defining the actions or resources that can be accessed based on the user&rsquo;s role, ensuring they only interact with the data or systems for which they have clearance.</li>
<li>Accounting&nbsp;: Tracks and logs user activities within a system, providing a record of what actions were taken, when, and by whom. This audit trail helps in monitoring, troubleshooting, and detecting unauthorized or malicious behavior.</li>
</ol>
<p>Altogether, AAA enhances security by ensuring that only authenticated users can access specific resources, with their actions monitored and recorded for accountability. And that&rsquo;s exactly what we did with use of personal logins, role based access control and using the audit log to track admin activity.</p>
<p>We know that admins aren&rsquo;t the only one being able to temper with or gaining unwanted access to datas&nbsp;: any user is a potential risk for our database. Obviously, the previous advices still applies but role based access can prove to be inefficient in case of fine grained access&nbsp;control.</p>
<p>Let&rsquo;s imagine we are in a high security line of business and users should have very strict and tight access control. One way to prevent malicous users to circumvent the filtering si to physically isolate the user from the data. But can we dot it with MariaDB&nbsp;?</p>
<p>First step, we create a user as mentionned in my previous&nbsp;article.</p>
<pre>CREATE USER 'IT-O'@'Coruscant' <br>IDENTIFIED via ed25519 REQUIRE SSL <br>WITH MAX_USER_CONNECTIONS 1 <br>PASSWORD EXPIRE INTERVAL xx DAYS;</pre>
<p>Second step, we create a dedicated schema using his user&nbsp;token.</p>
<p>Third step, we are gonna use to match the tables. We are now leveraging 3 parameters of said views&nbsp;:&nbsp;, DEFINER and SQL SECURITY&nbsp;:</p>
<ul>
<li>ALGORITHM allows us to tell how to execute the view. Here we will be choosing to make use of a temporary table to store the result of the view. While not the fastest, it has the good taste to make the view unwritable, on top of not having granted INSERT,UPDATE/DELETE privileges to the&nbsp;user,</li>
<li>DEFINER allows us to give &ldquo;ownership&rdquo; of the view to a specific&nbsp;account,</li>
<li>SQL SECURITY allows us to have the view executed with the DEFINER set of privileges instead of the INVOKER (aka the user)&nbsp;one.</li>
</ul>
<pre>CREATE VIEW `IT-O`.BoobyTable ALGORITHM=temptable <br>DEFINER=`locked.admin.account`@`localhost` <br>SQL SECURITY=DEFINER AS <br>SELECT necessary,columns,only <br>FROM RealSchema.RealTable <br>WHERE RestrictionClauses=values;</pre>
<p>Fourth thing, said user needs to manipulate the underlying table datas. We could theoritically allow the user to <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Finserting-and-updating-with-views%2F&amp;urlhash=228t&amp;trk=article-ssr-frontend-pulse_little-text-block">write through the view</a>&nbsp;. But the control over its action is then limited, hence i prefer creating ad hoc that will emulate the desired actions. With those objects we can leverage the same last 2 parameters( <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fstored-procedures%2F&amp;urlhash=zaDr&amp;trk=article-ssr-frontend-pulse_little-text-block">stored routines</a> ) as with views to ensure a good isolation. <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fstored-routine-privileges%2F%23definer-clause&amp;urlhash=nYug&amp;trk=article-ssr-frontend-pulse_little-text-block">DEFINER &amp; SQL&nbsp;SECURITY</a></p>
<pre>DELIMITER // CREATE OR REPLACE DEFINER=`locked.admin.account`@`localhost` <br>PROCEDURE `IT-O`.Action_BoobyTable (IN param_name type , OUT param_name type) <br>SQL SECURITY=DEFINER <br>BEGIN ACTIONS in SQL/PSM or PL/SQL <br>END// <br>DELIMITER ;</pre>
<p>Fifth step, we give the permissions to said user over the objects it&nbsp;needs.</p>
<pre>CREATE ROLE `IT-O`; <br>GRANT SELECT on `IT-O`.* to `IT-O`; <br>GRANT EXECUTE on `IT-O`.necesary_procs to `IT-O`; <br>GRANT `IT-O` to `IT-O`@`Coruscant`; <br>SET DEFAULT ROLE `IT-O` for `IT-O`@`Coruscant`;</pre>
<p>And now we have a user which acces is tightly controled and monitored even tho he has direct access to the database. Of course this could also be upgraded making him flow through on a dedicated service with&nbsp;, <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fmariadb-maxscale-2402-maxscale-2402-query-log-all-filter%2F&amp;urlhash=WLkQ&amp;trk=article-ssr-frontend-pulse_little-text-block">general login</a>&nbsp;, <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fmariadb-maxscale-2402-maxscale-2402-throttle%2F&amp;urlhash=l87J&amp;trk=article-ssr-frontend-pulse_little-text-block">query throttling</a>&nbsp;, and possibly also <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fmariadb-maxscale-2402-maxscale-2402-maxrows%2F&amp;urlhash=wlll&amp;trk=article-ssr-frontend-pulse_little-text-block">resultset size limitation</a>&nbsp;. <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fmariadb-maxscale-2402-maxscale-2402-comment-filter%2F%23example-1-inject-ip-address-of-the-connected-client-into-statements&amp;urlhash=YSCO&amp;trk=article-ssr-frontend-pulse_little-text-block">inserting connexion IP in the statement</a> <a href="https://www.linkedin.com/redir/redirect?url=https%3A%2F%2Fmariadb.com%2Fkb%2Fen%2Fmariadb-maxscale-2402-maxscale-2402-masking%2F&amp;urlhash=Nr7I&amp;trk=article-ssr-frontend-pulse_little-text-block">data&nbsp;masking</a></p>
<p>Of course, deployment of such an internal architecture can be partially automated since it is mostly linked to parametrizing.</p>
<p>Special thanks to <a href="https://uk.linkedin.com/in/federicorazzoli?trk=article-ssr-frontend-pulse_little-mention">Federico Razzoli </a>for mentionning the use of a locked admin account as&nbsp;DEFINER.</p>
<p><em>Originally published at </em><a href="https://www.linkedin.com/pulse/lets-go-physically-separated-sylvain-arbaudie-yzu3f/"><em>https://www.linkedin.com</em></a><em>.</em></p>
<p><img loading="lazy" decoding="async" src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=4053ae5be14c" width="1" height="1" alt=""></p>

<p><a href="https://medium.com/@arbaudie.it/lets-go-physical-ly-separated-4053ae5be14c?source=rss-c779d007e7fe------2">Let’s go physical(ly separated)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB innovation: binlog_storage_engine, small server, Insert Benchmark</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html" />
      <id>https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html</id>
      <updated>2026-02-18T04:20:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option binlog_storage_engine. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB).My previous post had results for sysbench with a small server. This post has results for the Insert Benchmark with a similar small server. Both servers use an SSD that has has high fsync latency. This is probably a best-case comparison for the feature. If you really care, then get enterprise SSDs with power loss protection. But you might encounter high fsync latency on public cloud servers.tl;dr for a CPU-bound workloadEnabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps -- l.i0, l.i1 and l.i2.When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 1.75X larger for l.i0 (load) and 4X or more larger on the random write steps (l.i1, l.i2)tl;dr for an IO-bound workloadEnabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps -- l.i0, l.i1 and l.i2. It also has a large impact on qp1000, which is the most write-heavy of the query+write steps.When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 4.74X larger for l.i0 (load), 1.50X larger for l.i1 (random writes) and 2.99X larger for l.i2 (random writes)Builds, configuration and hardwareI compiled MariaDB 12.3.0 from source.The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it are here. The storage device has high fsync latency.I used 4 my.cnf files:z12bmy.cnf.cz12b_c8r32 (z12b) is my default configuration. Sync-on-commit is disabled for both the binlog and InnoDB so that write-heavy benchmarks create more stress.z12cmy.cnf.cz12c_c8r32 (z12c) is like z12b except it enables binlog_storage_enginez12b_syncmy.cnf.cz12b_sync_c8r32 (z12b_sync) is like z12b except it enables sync-on-commit for the binlog and InnoDBz12c_syncmy.cnf.cz12c_sync_c8r32 (z12c_sync) is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.The BenchmarkThe benchmark is explained here. It was run with 1 client for two workloads:CPU-bound - the database is cached by InnoDB, but there is still much write IOIO-bound - most, but not all, benchmark steps are IO-boundThe benchmark steps are:l.i0insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 30M for CPU-bound and 800M for IO-bound.l.xcreate 3 secondary indexes per table. There is one connection per client.l.i1use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 40M for CPU-bound and 4M for IO-bound.l.i2like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 10M for CPU-bound and 1M for IO-bound.Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.qr100use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 1800 seconds.qp100like qr100 except uses point queries on the PK indexqr500like qr100 but the insert and delete rates are increased from 100/s to 500/sqp500like qp100 but the insert and delete rates are increased from 100/s to 500/sqr1000like qr100 but the insert and delete rates are increased from 100/s to 1000/sqp1000like qp100 but the insert and delete rates are increased from 100/s to 1000/sResults: summaryThe performance reports are here for:CPU-boundall-versions - results for z12b, z12c, z12b_sync and z12c_syncsync-only - results for z12b_sync vs 12c_syncIO-boundall-versions - results for z12b, z12c, z12b_sync and z12c_syncsync-only - results for z12b_sync vs 12c_syncThe summary sections from the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version. When relative QPS is &#62; 1.0 then performance improved over time. When it is &#60; 1.0 then there are regressions. The Q in relative QPS measures: insert/s for l.i0, l.i1, l.i2indexed rows/s for l.xrange queries/s for qr100, qr500, qr1000point queries/s for qp100, qp500, qp1000Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.I often use context switch rates as a proxy for mutex contention.Results: CPU-boundThe summaries are here for all-versions and sync-only.Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps -- l.i0, l.i1 and l.i2.When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 1.75X larger for l.i0 (load) and 4X or more larger on the random write steps (l.i1, l.i2)The second table from the summary section has been inlined below. That table shows relative throughput which is:all-versions: (QPS for my config / QPS for z12b)sync-only: (QPS for my config / QPS for z12b)For all-versionsdbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp1000ma120300_rel_withdbg.cz12b_c8r321.001.001.001.001.001.001.001.001.001.00ma120300_rel_withdbg.cz12c_c8r321.031.011.001.031.000.991.001.001.011.00ma120300_rel_withdbg.cz12b_sync_c8r320.041.020.070.011.011.011.001.011.001.00ma120300_rel_withdbg.cz12c_sync_c8r320.081.030.280.061.021.011.011.021.021.01And for sync-only the relative QPS is:all-versions: (QPS for my config / QPS for z12b_sync)sync-only: (QPS for my config / QPS for z12b_sync)dbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp1000ma120300_rel_withdbg.cz12b_sync_c8r321.001.001.001.001.001.001.001.001.001.00ma120300_rel_withdbg.cz12c_sync_c8r321.751.013.996.831.011.011.011.011.031.01Results: IO-boundThe summaries are here for all-versions and sync-only.Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps -- l.i0, l.i1 and l.i2. It also has a large impact on qp1000, which is the most write-heavy of the query+write steps.When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 4.74X larger for l.i0 (load), 1.50X larger for l.i1 (random writes) and 2.99X larger for l.i2 (random writes)The second table from the summary section has been inlined below. That table shows relative throughput which is:all-versions: (QPS for my config / QPS for z12b)sync-only: (QPS for my config / QPS for z12b)For all-versionsdbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp1000ma120300_rel_withdbg.cz12b_c8r321.001.001.001.001.001.001.001.001.001.00ma120300_rel_withdbg.cz12c_c8r321.010.990.991.011.011.011.011.071.011.04ma120300_rel_withdbg.cz12b_sync_c8r320.041.000.550.101.020.971.000.800.950.55ma120300_rel_withdbg.cz12c_sync_c8r320.181.000.830.311.021.011.020.961.020.86And for sync-only the relative QPS is:all-versions: (QPS for my config / QPS for z12b_sync)sync-only: (QPS for my config / QPS for z12b_sync)dbmsl.i0l.xl.i1l.i2qr100qp100qr500qp500qr1000qp1000ma120300_rel_withdbg.cz12b_sync_c8r321.001.001.001.001.001.001.001.001.001.00ma120300_rel_withdbg.cz12c_sync_c8r324.741.001.502.991.001.041.021.201.081.57</p>
<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html">MariaDB innovation: binlog_storage_engine, small server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option&nbsp;<a href="https://mariadb.org/new-binlog-implementation-in-mariadb-12-3/">binlog_storage_engine</a>. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB).</p>
<p>My <a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine.html">previous post</a> had results for sysbench with a small server. This post has results for the Insert Benchmark with a similar small server. Both servers use an SSD that has has <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>. This is probably a best-case comparison for the feature. If you really care, then get enterprise SSDs with power loss protection. But you might encounter high fsync latency on public cloud servers.</p>
<p>tl;dr for a CPU-bound workload</p>

<ul>
<li>Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps &mdash; l.i0, l.i1 and l.i2.</li>
<li>When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 1.75X larger for l.i0 (load) and 4X or more larger on the random write steps (l.i1, l.i2)</li>
</ul>
<div>tl;dr for an IO-bound workload</div>
<div>
<ul>
<li>Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps &mdash; l.i0, l.i1 and l.i2. It also has a large impact on qp1000, which is the most write-heavy of the query+write steps.</li>
<li>When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 4.74X larger for l.i0 (load), 1.50X larger for l.i1 (random writes) and 2.99X larger for l.i2 (random writes)</li>
</ul>
</div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div>
<div></div>
<div>I compiled MariaDB 12.3.0 from source.</div>
<div>The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2026/02/ASUS%20ExpertCenter%20PN53%20with%20AMD%20Ryzen%207%207735HS,%2032G%20RAM%20and%202%20m.2%20slots%20(one%20for%20OS%20install,%20one%20for%20DB%20perf%20tests)">are here</a>. The storage device has&nbsp;<a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.h">high fsync latency</a>.</div>
</div>
<div></div>
<div>I used 4 my.cnf files:</div>
<div>
<ul>
<li>z12b</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12b_c8r32">my.cnf.cz12b_c8r32</a>&nbsp;(z12b) is my default configuration. Sync-on-commit is disabled for both the binlog and InnoDB so that write-heavy benchmarks create more stress.</li>
</ul>
<li>z12c</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12c_c8r32">my.cnf.cz12c_c8r32</a>&nbsp;(z12c) is like z12b except it enables binlog_storage_engine</li>
</ul>
<li>z12b_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12b_sync_c8r32">my.cnf.cz12b_sync_c8r32</a>&nbsp;(z12b_sync) is like z12b except it enables sync-on-commit for the binlog and InnoDB</li>
</ul>
<li>z12c_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12c_sync_c8r32">my.cnf.cz12c_sync_c8r32</a>&nbsp;(z12c_sync) is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.</li>
</ul>
</ul>
<div>
<div><b>The Benchmark</b></div>
<div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://smalldatum.blogspot.com/2023/12/updates-for-insert-benchmark-december.html">explained here</a>. It was run with 1 client for two workloads:</div>
<div>
<ul>
<li>CPU-bound &ndash; the database is cached by InnoDB, but there is still much write IO</li>
<li>IO-bound &ndash; most, but not all, benchmark steps are IO-bound</li>
</ul>
</div>
<div>The benchmark steps are:</div>
<div>
<div>
<ul>
<li>l.i0</li>
<ul>
<li>insert XM rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client. X is 30M for CPU-bound and 800M for IO-bound.</li>
</ul>
<li>l.x</li>
<ul>
<li>create 3 secondary indexes per table. There is one connection per client.</li>
</ul>
<li>l.i1</li>
<ul>
<li>use 2 connections/client. One inserts XM rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate. X is 40M for CPU-bound and 4M for IO-bound.</li>
</ul>
<li>l.i2</li>
<ul>
<li>like l.i1 but each transaction modifies 5 rows (small transactions) and YM rows are inserted and deleted per table. Y is 10M for CPU-bound and 1M for IO-bound.</li>
<li>Wait for S seconds after the step finishes to reduce MVCC GC debt and perf variance during the read-write benchmark steps that follow. The value of S is a function of the table size.</li>
</ul>
<li>qr100</li>
<ul>
<li>use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload. This step runs for 1800 seconds.</li>
</ul>
<li>qp100</li>
<ul>
<li>like qr100 except uses point queries on the PK index</li>
</ul>
<li>qr500</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qp500</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 500/s</li>
</ul>
<li>qr1000</li>
<ul>
<li>like qr100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
<li>qp1000</li>
<ul>
<li>like qp100 but the insert and delete rates are increased from 100/s to 1000/s</li>
</ul>
</ul>
<div><b>Results: summary</b></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div>
<div></div>
<div>The performance reports are here for:</div>
</div>
<div>
<ul>
<li>CPU-bound</li>
<ul>
<li><a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.syncall.30m.50m.1800s/all.html">all-versions</a> &ndash; results for z12b, z12c, z12b_sync and z12c_sync</li>
<li><a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.synconly.30m.50m.1800s/all.html">sync-only</a> &ndash; results for z12b_sync vs 12c_sync</li>
</ul>
<li>IO-bound</li>
<ul>
<li><a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.syncall.800m.5m.1800s/all.html">all-versions</a> &ndash; results for z12b, z12c, z12b_sync and z12c_sync</li>
<li><a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.synconly.800m.5m.1800s/all.html">sync-only</a> &ndash; results for z12b_sync vs 12c_sync</li>
</ul>
</ul>
</div>
<div>The summary sections from&nbsp;the performance reports have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.</div>
<div>
<div></div>
<div>I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version $base is the result from the base version.&nbsp;</div>
<div>When relative QPS is &gt; 1.0 then performance improved over time. When it is &lt; 1.0 then there are regressions. The Q in relative QPS measures:&nbsp;</div>
<div>
<ul>
<li>insert/s for l.i0, l.i1, l.i2</li>
<li>indexed rows/s for l.x</li>
<li>range queries/s for qr100, qr500, qr1000</li>
<li>point queries/s for qp100, qp500, qp1000</li>
</ul>
<div>Below I use colors to highlight the relative QPS values with yellow for regressions and blue for improvements.</div>
</div>
</div>
<div></div>
<div>I often use context switch rates as a proxy for mutex contention.</div>
</div>
<div></div>
<div><b>Results: CPU-bound</b></div>
<div></div>
<div>The summaries are here for <a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.syncall.30m.50m.1800s/all.html#summary">all-versions</a> and <a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.synconly.30m.50m.1800s/all.html#summary">sync-only</a>.</div>
<div>
<ul>
<li>Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps &mdash; l.i0, l.i1 and l.i2.</li>
<li>When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 1.75X larger for l.i0 (load) and 4X or more larger on the random write steps (l.i1, l.i2)</li>
</ul>
</div>
<div>The second table from the summary section has been inlined below. That table shows relative throughput which is:</div>
<div>
<ul>
<li>all-versions: (QPS for my config / QPS for z12b)</li>
<li>sync-only: (QPS for my config / QPS for z12b)</li>
</ul>
<div>For all-versions</div>
<div>
<table border="1" cellpadding="8">
<tbody>
<tr>
<th><span>dbms</span></th>
<th><span>l.i0</span></th>
<th><span>l.x</span></th>
<th><span>l.i1</span></th>
<th><span>l.i2</span></th>
<th><span>qr100</span></th>
<th><span>qp100</span></th>
<th><span>qr500</span></th>
<th><span>qp500</span></th>
<th><span>qr1000</span></th>
<th><span>qp1000</span></th>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_c8r32</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_c8r32</span></td>
<td><span>1.03</span></td>
<td><span>1.01</span></td>
<td><span>1.00</span></td>
<td><span>1.03</span></td>
<td><span>1.00</span></td>
<td><span>0.99</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.01</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_sync_c8r32</span></td>
<td><span>0.04</span></td>
<td><span>1.02</span></td>
<td><span>0.07</span></td>
<td><span>0.01</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.00</span></td>
<td><span>1.01</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_sync_c8r32</span></td>
<td><span>0.08</span></td>
<td><span>1.03</span></td>
<td><span>0.28</span></td>
<td><span>0.06</span></td>
<td><span>1.02</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.02</span></td>
<td><span>1.02</span></td>
<td><span>1.01</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div></div>
<div>And for sync-only the relative QPS is:</div>
<div>
<ul>
<li>all-versions: (QPS for my config / QPS for z12b_sync)</li>
<li>sync-only: (QPS for my config / QPS for z12b_sync)</li>
</ul>
</div>
<div>
<table border="1" cellpadding="8">
<tbody>
<tr>
<th><span>dbms</span></th>
<th><span>l.i0</span></th>
<th><span>l.x</span></th>
<th><span>l.i1</span></th>
<th><span>l.i2</span></th>
<th><span>qr100</span></th>
<th><span>qp100</span></th>
<th><span>qr500</span></th>
<th><span>qp500</span></th>
<th><span>qr1000</span></th>
<th><span>qp1000</span></th>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_sync_c8r32</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_sync_c8r32</span></td>
<td><span>1.75</span></td>
<td><span>1.01</span></td>
<td><span>3.99</span></td>
<td><span>6.83</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.03</span></td>
<td><span>1.01</span></td>
</tr>
</tbody>
</table>
</div>
<div></div>
<div><b>Results: IO-bound</b></div>
<div></div>
<div>The summaries are here for <a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.syncall.800m.5m.1800s/all.html">all-versions</a> and <a href="https://mdcallag.github.io/reports/feb26.ib.mem.pn53.ma1203.synconly.800m.5m.1800s/all.html">sync-only</a>.</div>
<div>
<ul>
<li>Enabling sync on commit for InnoDB and the binlog has a large impact on throughput for the write-heavy steps &mdash; l.i0, l.i1 and l.i2. It also has a large impact on qp1000, which is the most write-heavy of the query+write steps.</li>
<li>When sync on commit is enabled, then also enabling the binlog_storage_engine is great for performance as throughput on the write-heavy steps is 4.74X larger for l.i0 (load), 1.50X larger for l.i1 (random writes) and 2.99X larger for l.i2 (random writes)</li>
</ul>
</div>
<div>The second table from the summary section has been inlined below. That table shows relative throughput which is:</div>
<div>
<div>
<ul>
<li>all-versions: (QPS for my config / QPS for z12b)</li>
<li>sync-only: (QPS for my config / QPS for z12b)</li>
</ul>
<div>For all-versions</div>
</div>
</div>
<div>
<table border="1" cellpadding="8">
<tbody>
<tr>
<th><span>dbms</span></th>
<th><span>l.i0</span></th>
<th><span>l.x</span></th>
<th><span>l.i1</span></th>
<th><span>l.i2</span></th>
<th><span>qr100</span></th>
<th><span>qp100</span></th>
<th><span>qr500</span></th>
<th><span>qp500</span></th>
<th><span>qr1000</span></th>
<th><span>qp1000</span></th>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_c8r32</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_c8r32</span></td>
<td><span>1.01</span></td>
<td><span>0.99</span></td>
<td><span>0.99</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.01</span></td>
<td><span>1.07</span></td>
<td><span>1.01</span></td>
<td><span>1.04</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_sync_c8r32</span></td>
<td><span>0.04</span></td>
<td><span>1.00</span></td>
<td><span>0.55</span></td>
<td><span>0.10</span></td>
<td><span>1.02</span></td>
<td><span>0.97</span></td>
<td><span>1.00</span></td>
<td><span>0.80</span></td>
<td><span>0.95</span></td>
<td><span>0.55</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_sync_c8r32</span></td>
<td><span>0.18</span></td>
<td><span>1.00</span></td>
<td><span>0.83</span></td>
<td><span>0.31</span></td>
<td><span>1.02</span></td>
<td><span>1.01</span></td>
<td><span>1.02</span></td>
<td><span>0.96</span></td>
<td><span>1.02</span></td>
<td><span>0.86</span></td>
</tr>
</tbody>
</table>
</div>
<div></div>
<div>And for sync-only the relative QPS is:</div>
<div>
<div>
<ul>
<li>all-versions: (QPS for my config / QPS for z12b_sync)</li>
<li>sync-only: (QPS for my config / QPS for z12b_sync)</li>
</ul>
</div>
</div>
<div>
<table border="1" cellpadding="8">
<tbody>
<tr>
<th><span>dbms</span></th>
<th><span>l.i0</span></th>
<th><span>l.x</span></th>
<th><span>l.i1</span></th>
<th><span>l.i2</span></th>
<th><span>qr100</span></th>
<th><span>qp100</span></th>
<th><span>qr500</span></th>
<th><span>qp500</span></th>
<th><span>qr1000</span></th>
<th><span>qp1000</span></th>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12b_sync_c8r32</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
<td><span>1.00</span></td>
</tr>
<tr>
<td><span>ma120300_rel_withdbg.cz12c_sync_c8r32</span></td>
<td><span>4.74</span></td>
<td><span>1.00</span></td>
<td><span>1.50</span></td>
<td><span>2.99</span></td>
<td><span>1.00</span></td>
<td><span>1.04</span></td>
<td><span>1.02</span></td>
<td><span>1.20</span></td>
<td><span>1.08</span></td>
<td><span>1.57<br></span></td>
</tr>
</tbody>
</table>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine_17.html">MariaDB innovation: binlog_storage_engine, small server, Insert Benchmark</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB innovation: binlog_storage_engine</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine.html" />
      <id>https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine.html</id>
      <updated>2026-02-16T19:06:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option binlog_storage_engine. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB).In this post I have results for the performance benefit from this when using storage that has a high fsync latency. This is probably a best-case comparison for the feature. A future post will cover the benefit on servers that don\'t have high fsync latency.tl;drthe performance benefit from this is excellent when storage has a high fsync latencythere is a small improvement (up to 6%) for write throughput when binlog_storage_engine is enabled but sync-on-commit is not enabledmy mental performance model needs to be improved. I gussed that throughput would increase by ~2X when using binlog_storage_engine relative to not using it but using sync_binlog=1 and innodb_flush_log_at_trx_commit=1. However the improvement is larger than 4X.Some historyMongoDB has done this for years -- the replication log is stored in WiredTiger. Long ago there were requests for this feature from the Galera team, and I wonder if they will benefit from this now. I have been curious about the benefit of the feature, but long ago I was also wary of it because it can increase stress on InnoDB and back in the day InnoDB already struggled with high-concurrency workloads.Long ago group commit didn\'t work for the binlog. The Facebook MySQL team did some work to fix that, and eventually. A Google search describes our work as the first and I found an old Facebook note that I probably wrote about the effort.Builds, configuration and hardwareI compiled MariaDB 12.3.0 from source.The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it are here. The storage device has high fsync latency.I used 4 my.cnf files:z12bmy.cnf.cz12b_c8r32 is my default configuration. Sync-on-commit is disabled for both the binlog and InnoDB so that write-heavy benchmarks create more stress.z12cmy.cnf.cz12c_c8r32 is like z12b except it enables binlog_storage_enginez12b_syncmy.cnf.cz12b_sync_c8r32 is like z12b except it enables sync-on-commit for the binlog and InnoDBz12c_syncmy.cnf.cz12c_sync_c8r32 is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.BenchmarkI used sysbench and my usage is explained here. To save time I only run 32 of the 42 microbenchmarks and most test only 1 type of SQL statement. Benchmarks are run with the database cached by Postgres.The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 900 seconds.The benchmark is run with 1 client, 1 table and 50M rows. ResultsThe microbenchmarks are split into 4 groups -- 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don\'t do aggregation while part 2 has queries that do aggregation.  But here I only report results for the write-heavy tests.I provide charts below with relative QPS. The relative QPS is the following:(QPS for some version) / (QPS for base version)When the relative QPS is &#62; 1 then some version is faster than base version.  When it is &#60; 1 then there might be a regression. I present results for:z12b, z12c, z12b_sync and z12c_sync with z12b as the base version z12b_sync and z12c_sync with z12b_sync as the base versionResults: z12b, z12c, z12b_sync, z12c_syncSummary:z12c gets up to 6% more throughput than z12b but the CPU overhead per operation are similar for z12b and z12cz12b_sync has the worst performance thanks to 2 fsyncs per commitz12c_sync gets more than 4X the throughput vs z12b_sync. If fsync latency were the only thing that determined performance then I would expect the difference to be ~2X. There is more going on here and in the next section I mention that enabling binlog_storage_engine also reduces the CPU overhead.some per-test data from iostat and vmstat is herea representative sample of iostat collected at 1-second intervals during the update-inlist test is here. When comparing z12b_sync with z12c_syncthe fsync rate (f/s) is ~2.5X larger for z12c_sync vs z12b_sync (~690/s vs ~275/s) but fsync latency (f_await) is similar. So with binlog_storage_engine enabled MySQL is more efficient, and perhaps thanks to a lower CPU overhead, there is less work to do in between calls to fsyncRelative to: z12bcol-1 : z12ccol-2 : z12b_synccol-3 : z12c_synccol-1   col-2   col-31.06    0.01    0.05    delete1.05    0.01    0.05    insert1.01    0.12    0.47    read-write_range=1001.01    0.10    0.44    read-write_range=101.03    0.01    0.11    update-index1.02    0.02    0.12    update-inlist1.05    0.01    0.06    update-nonindex1.05    0.01    0.06    update-one1.05    0.01    0.06    update-zipf1.01    0.03    0.20    write-onlyResults: z12b_sync, z12c_syncSummary:z12c_sync gets more than 4X the throughput vs z12b_sync. If fsync latency were the only thing that determined performance then I would expect the difference to be ~2X. There is more going on here and below I mention that enabling binlog_storage_engine also reduces the CPU overhead.some per-test data from iostat and vmstat is here and the CPU overhead per operation is much smaller with binlog_storage_engine -- see here for the update-inlist test. In general, when sync-on-commit is enabled then the CPU overhead with binlog_storage_engine enabled is between 1/3 and 2/3 of the overhead without it enabled.Relative to: z12b_synccol-1 : z12c_synccol-16.40    delete5.64    insert4.06    read-write_range=1004.40    read-write_range=107.64    update-index7.17    update-inlist5.73    update-nonindex5.82    update-one5.80    update-zipf6.61    write-only</p>
<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine.html">MariaDB innovation: binlog_storage_engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MariaDB 12.3 has a new feature enabled by the option <a href="https://mariadb.org/new-binlog-implementation-in-mariadb-12-3/">binlog_storage_engine</a>. When enabled it uses InnoDB instead of raw files to store the binlog. A big benefit from this is reducing the number of fsync calls per commit from 2 to 1 because it reduces the number of resource managers from 2 (binlog, InnoDB) to 1 (InnoDB).</p>
<p>In this post I have results for the performance benefit from this when using storage that has a <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">high fsync latency</a>. This is probably a best-case comparison for the feature. A future post will cover the benefit on servers that don&rsquo;t have high fsync latency.</p>
<p>tl;dr</p>

<ul>
<li>the performance benefit from this is excellent when storage has a high fsync latency</li>
<li>there is a small improvement (up to 6%) for write throughput when binlog_storage_engine is enabled but sync-on-commit is not enabled</li>
<li>my mental performance model needs to be improved. I gussed that throughput would increase by ~2X when using binlog_storage_engine relative to not using it but using sync_binlog=1 and innodb_flush_log_at_trx_commit=1. However the improvement is larger than 4X.</li>
</ul>
<div><b>Some history</b></div>
<div></div>
<div>MongoDB has done this for years &mdash; the replication log is stored in WiredTiger.&nbsp;</div>
<div></div>
<div>Long ago there were requests for this feature from the Galera team, and I wonder if they will benefit from this now. I have been curious about the benefit of the feature, but long ago I was also wary of it because it can increase stress on InnoDB and back in the day InnoDB already struggled with high-concurrency workloads.
<p>Long ago group commit didn&rsquo;t work for the binlog. The Facebook MySQL team did some work to fix that, and eventually. A Google search&nbsp;<a href="https://www.google.com/search?q=mysql+binlog+%22group+commit%22+facebook">describes our work</a>&nbsp;as the first and I found an <a href="https://m.facebook.com/nt/screen/?params=%7B%22note_id%22%3A10157508562376696%7D&amp;path=%2Fnotes%2Fnote%2F">old Facebook note</a> that I probably wrote about the effort.</p></div>
<div><b><br></b></div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled MariaDB 12.3.0 from source.</div>
<div>The server is an ASUS ExpertCenter PN53 with an AMD Ryzen 7 7735HS CPU, 8 cores, SMT disabled, and 32G of RAM. Storage is one NVMe device for the database using ext-4 with discard enabled. The OS is Ubuntu 24.04. More details on it&nbsp;<a href="https://smalldatum.blogspot.com/2026/02/ASUS%20ExpertCenter%20PN53%20with%20AMD%20Ryzen%207%207735HS,%2032G%20RAM%20and%202%20m.2%20slots%20(one%20for%20OS%20install,%20one%20for%20DB%20perf%20tests)">are here</a>. The storage device has <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.h">high fsync latency</a>.</div>
</div>
<div></div>
<div>I used 4 my.cnf files:</div>
<div>
<ul>
<li>z12b</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12b_c8r32">my.cnf.cz12b_c8r32</a>&nbsp;is my default configuration. Sync-on-commit is disabled for both the binlog and InnoDB so that write-heavy benchmarks create more stress.</li>
</ul>
<li>z12c</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12c_c8r32">my.cnf.cz12c_c8r32</a> is like z12b except it enables binlog_storage_engine</li>
</ul>
<li>z12b_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12b_sync_c8r32">my.cnf.cz12b_sync_c8r32</a> is like z12b except it enables sync-on-commit for the binlog and InnoDB</li>
</ul>
<li>z12c_sync</li>
<ul>
<li><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c8r32/ma1203/etc/my.cnf.cz12c_sync_c8r32">my.cnf.cz12c_sync_c8r32</a> is like cz12c except it enables sync-on-commit for InnoDB. Note that InnoDB is used to store the binlog so there is nothing else to sync on commit.</li>
</ul>
</ul>
<div>
<div>
<p><b>Benchmark</b></p>
<div>
<div>I used sysbench and my usage is&nbsp;<a href="http://smalldatum.blogspot.com/2017/02/using-modern-sysbench-to-compare.html">explained here</a>. To save time I only run 32 of the 42 microbenchmarks&nbsp;</div>
<div>and most test only 1 type of SQL statement. Benchmarks are run with the database cached by Postgres.</div>
<div>The read-heavy microbenchmarks run for 600 seconds and the write-heavy for 900 seconds.
<p>The benchmark is run with 1 client, 1 table and 50M rows.&nbsp;</p></div>
</div>
</div>
</div>
<div></div>
<div>
<div><b>Results</b></div>
<div><span>
<div></div>
<div><span>The microbenchmarks are split into 4 groups &mdash; 1 for point queries, 2 for range queries, 1 for writes. For the range query microbenchmarks, part 1 has queries that don&rsquo;t do aggregation while part 2 has queries that do aggregation.&nbsp;&nbsp;
<p>But here I only report results for the write-heavy tests.</p></span></div>
<div>I provide charts below with relative QPS. The relative QPS is the following:</div>
<div>
<div></div>
<blockquote><p>(QPS for some version) / (QPS for base version)</p></blockquote>
</div>
<div><span>When the relative QPS is &gt; 1 then&nbsp;</span><i>some version</i><span>&nbsp;is faster than&nbsp;</span><i>base version</i><span>.&nbsp; When it is &lt; 1 then there might be a regression.&nbsp;</span><span>
<p><span>I present results for:</span></p>
<ul>
<li><span>z12b, z12c, z12b_sync and z12c_sync with z12b as the base version</span></li>
<li>&nbsp;z12b_sync and z12c_sync with z12b_sync as the base version</li>
</ul>
<div><b>Results: z12b, z12c, z12b_sync, z12c_sync</b></div>
<div></div>
<div>Summary:</div>
<div>
<ul>
<li>z12c gets up to 6% more throughput than z12b but the CPU overhead per operation are similar for z12b and z12c</li>
<li>z12b_sync has the worst performance thanks to 2 fsyncs per commit</li>
<li>z12c_sync gets more than 4X the throughput vs z12b_sync. If fsync latency were the only thing that determined performance then I would expect the difference to be ~2X. There is more going on here and in the next section I mention that enabling binlog_storage_engine also reduces the CPU overhead.</li>
<li>some per-test data from iostat and vmstat <a href="https://gist.github.com/mdcallag/ec11b28478551d5fbe69ec52ef9faf2c#file-gistfile1-txt-L6-L7">is here</a></li>
<li>a representative sample of iostat collected at 1-second intervals during the update-inlist test <a href="https://gist.github.com/mdcallag/a8e8055f6f290982f1c6594657cacd21">is here</a>. When comparing <a href="https://gist.github.com/mdcallag/a8e8055f6f290982f1c6594657cacd21#file-gistfile1-txt-L27-L38">z12b_sync</a> with <a href="https://gist.github.com/mdcallag/a8e8055f6f290982f1c6594657cacd21#file-gistfile1-txt-L40-L51">z12c_sync</a></li>
<ul>
<li>the fsync rate (f/s) is ~2.5X larger for z12c_sync vs z12b_sync (~690/s vs ~275/s) but fsync latency (f_await) is similar. So with binlog_storage_engine enabled MySQL is more efficient, and perhaps thanks to a lower CPU overhead, there is less work to do in between calls to fsync</li>
</ul>
</ul>
</div>
<div><span>Relative to: z12b</span></div>
<div>
<div><span>col-1 : z12c</span></div>
<div><span>col-2 : z12b_sync</span></div>
<div><span>col-3 : z12c_sync</span></div>
<div><span><br></span></div>
<div><span>col-1&nbsp; &nbsp;col-2&nbsp; &nbsp;col-3</span></div>
<div><span>1.06&nbsp; &nbsp; <span>0.01</span>&nbsp; &nbsp; <span>0.05</span>&nbsp; &nbsp; delete</span></div>
<div><span>1.05&nbsp; &nbsp; <span>0.01</span>&nbsp; &nbsp; <span>0.05</span>&nbsp; &nbsp; insert</span></div>
<div><span>1.01&nbsp; &nbsp; <span>0.12</span>&nbsp; &nbsp; <span>0.47</span>&nbsp; &nbsp; read-write_range=100</span></div>
<div><span>1.01&nbsp; &nbsp; <span>0.10</span>&nbsp; &nbsp; <span>0.44</span>&nbsp; &nbsp; read-write_range=10</span></div>
<div><span>1.03&nbsp; &nbsp; <span>0.01</span><span>&nbsp; &nbsp; </span><span>0.11</span>&nbsp; &nbsp; update-index</span></div>
<div><span>1.02&nbsp; &nbsp; <span>0.02</span><span>&nbsp; &nbsp; </span><span>0.12</span>&nbsp; &nbsp; update-inlist</span></div>
<div><span>1.05&nbsp; &nbsp; <span>0.01</span><span>&nbsp; &nbsp; </span><span>0.06</span>&nbsp; &nbsp; update-nonindex</span></div>
<div><span>1.05&nbsp; &nbsp; <span>0.01</span><span>&nbsp; &nbsp; </span><span>0.06</span>&nbsp; &nbsp; update-one</span></div>
<div><span>1.05&nbsp; &nbsp; <span>0.01</span><span>&nbsp; &nbsp; </span><span>0.06</span>&nbsp; &nbsp; update-zipf</span></div>
<div><span>1.01&nbsp; &nbsp; <span>0.03</span><span>&nbsp; &nbsp; </span><span>0.20</span>&nbsp; &nbsp; write-only</span></div>
</div>
<p></p></span></div>
<p></p></span></div>
</div>
<div></div>
</div>
<div>
<div><b>Results: z12b_sync, z12c_sync</b></div>
<div></div>
</div>
<div>
<div>Summary:</div>
<div>
<ul>
<li>z12c_sync gets more than 4X the throughput vs z12b_sync. If fsync latency were the only thing that determined performance then I would expect the difference to be ~2X. There is more going on here and below I mention that enabling binlog_storage_engine also reduces the CPU overhead.</li>
<li>some per-test data from iostat and vmstat&nbsp;<a href="https://gist.github.com/mdcallag/8a02ec11fa4c8a04d9430cbb463c20d3">is here</a>&nbsp;and the CPU overhead per operation is much smaller with binlog_storage_engine &mdash; <a href="https://gist.github.com/mdcallag/8a02ec11fa4c8a04d9430cbb463c20d3#file-gistfile1-txt-L4-L5">see here</a> for the update-inlist test. In general, when sync-on-commit is enabled then the CPU overhead with binlog_storage_engine enabled is between 1/3 and 2/3 of the overhead without it enabled.</li>
</ul>
</div>
</div>
<div><span>Relative to: z12b_sync</span></div>
<div>
<div><span>col-1 : z12c_sync</span></div>
<div><span><br></span></div>
<div><span>col-1</span></div>
<div><span><span>6.40</span>&nbsp; &nbsp; delete</span></div>
<div><span><span>5.64</span>&nbsp; &nbsp; insert</span></div>
<div><span><span>4.06</span>&nbsp; &nbsp; read-write_range=100</span></div>
<div><span><span>4.40</span>&nbsp; &nbsp; read-write_range=10</span></div>
<div><span><span>7.64</span>&nbsp; &nbsp; update-index</span></div>
<div><span><span>7.17</span>&nbsp; &nbsp; update-inlist</span></div>
<div><span><span>5.73</span>&nbsp; &nbsp; update-nonindex</span></div>
<div><span><span>5.82</span>&nbsp; &nbsp; update-one</span></div>
<div><span><span>5.80</span>&nbsp; &nbsp; update-zipf</span></div>
<div><span><span>6.61</span>&nbsp; &nbsp; write-only</span></div>
</div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/02/mariadb-innovation-binlogstorageengine.html">MariaDB innovation: binlog_storage_engine</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>HammerDB tproc-c on a large server, Postgres and MySQL</title>
      <link rel="alternate" type="text/html" href="https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-large-server.html" />
      <id>https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-large-server.html</id>
      <updated>2026-02-15T20:42:00+02:00</updated>
      <author><name>Mark Callaghan</name></author>
      <summary type="html"><![CDATA[<p>This has results for HammerDB tproc-c on a small server using MySQL and Postgres. I am new to HammerDB and still figuring out how to explain and present results so I will keep this simple and just share graphs without explaining the results.The comparison might favor Postgres for the IO-bound workloads because I used smaller buffer pools than normal to avoid OOM. I have to do this because RSS for the HammerDB client grows over time as it buffers more response time stats. And while I used buffered IO for Postgres, I use O_DIRECT for InnoDB. So Postgres might have avoided some read IO thanks to the OS page cache while InnoDB did not.tl;dr for MySQLWith vu=40 MySQL 8.4.8 uses about 2X more CPU per transaction and does more than 2X more context switches per transaction compared to Postgres 18.1. I will get CPU profiles soon.Modern MySQL brings us great improvements to concurrency and too many new CPU overheadsMySQL 5.6 and 8.4 have similar throughput at the lowest concurrency (vu=10)MySQl 8.4 is a lot faster than 5.6 at the highest concurrency (vu=40)tl;dr for PostgresModern Postgres has regressions relative to old PostgresThe regressions increase with the warehouse count, at wh=4000 the NOPM drops between 3% and 13% depending on the virtual user count (vu).tl;dr for Postgres vs MySQLPostgres and MySQL have similar throughput for the largest warehouse count (wh=4000)Otherwise Postgres gets between 1.4X and 2X more throughput (NOPM)Builds, configuration and hardwareI compiled Postgres versions from source: 12.22, 13.23, 14.20, 15.15, 16.11, 17.7 and 18.1.I compiled MySQL versions from source: 5.6.51, 5.7.44, 8.0.45, 8.4.8, 9.4.0 and 9.6.0.I used a 48-core server from Hetzneran ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4128G RAMUbuntu 22.04 running the non-HWE kernel (5.5.0-118-generic)Postgres configuration files:prior to v18 the config file is named conf.diff.cx10a50g_c32r128 (x10a_c32r128) and is here for versions 12, 13, 14, 15, 16 and 17.for Postgres 18 I used conf.diff.cx10b_c32r128 (x10b_c32r128) with io_method=sync to be similar to the config used for versions 12 through 17.MySQL configuration filesprior to 9.6 the config file is named my.cnf.cz12a50g_c32r128 (z12a50g_c32r128 or z12a50g) and is here for versions 5.6, 5.7, 8.0 and 8.4for 9.6 it is named my.cnf.cz13a50g_c32r128 (z13a50g_c32r128 or z13a50g) and is hereFor both Postgres and MySQL fsync on commit is disabled to avoid turning this into an fsync benchmark. The server has 2 SSDs with SW RAID and low fsync latency.BenchmarkThe benchmark is tproc-c from HammerDB. The tproc-c benchmark is derived from TPC-C.The benchmark was run for several workloads:vu=10, wh=1000 - 10 virtual users, 1000 warehousesvu=20, wh=1000 - 20 virtual users, 1000 warehousesvu=40, wh=1000 - 40 virtual users, 1000 warehousesvu=10, wh=2000 - 10 virtual users, 2000 warehousesvu=20, wh=2000 - 20 virtual users, 2000 warehousesvu=40, wh=2000 - 40 virtual users, 2000 warehousesvu=10, wh=4000 - 10 virtual users, 4000 warehousesvu=20, wh=4000 - 20 virtual users, 4000 warehousesvu=40, wh=4000 - 40 virtual users, 4000 warehousesThe wh=1000 workloads are less heavy on IO. The wh=4000 workloads are more heavy on IO.The benchmark for Postgres is run by a variant of this script which depends on scripts here. The MySQL scripts are similar.stored procedures are enabledpartitioning is used because the warehouse count is &#62;= 1000a 5 minute rampup is usedthen performance is measured for 60 minutesBasic metrics: iostatI am still improving my helper scripts to report various performance metrics. The table here has average values from iostat during the benchmark run phase for MySQL 8.4.8 and Postgres 18.1. For these configurations the NOPM values for Postgres and MySQL were similar so I won\'t present normalized values (average value / NOPM) and NOPM is throughput.average wMB/s increases with the warehouse count for Postgres but not for MySQLr/s increases with the warehouse count for Postgres and MySQLiostat metrics* r/s = average rate of reads/s from storage* wMB/s = average MB/s written to storagemy8408r/s     wMB/s22833.0 906.2   vu=40, wh=100063079.8 1428.5  vu=40, wh=200082282.3 1398.2  vu=40, wh=4000pg181r/s     wMB/s30394.9 1261.9  vu=40, wh=100059770.4 1267.8  vu=40, wh=200078052.3 1272.9  vu=40, wh=4000Basic metrics: vmstatI am still improving my helper scripts to report various performance metrics. The table here has average values from vmstat during the benchmark run phase for MySQL 8.4.8 and Postgres 18.1. For these configurations the NOPM values for Postgres and MySQL were similar so I won\'t present normalized values (average value / NOPM).CPU utilization is almost 2X larger for MySQLContext switch rates are more than 2X larger for MySQLIn the future I hope to learn why MySQL uses almost 2X more CPU per transaction and has more than 2X more context switches per transaction relative to Postgresvmstat metrics* cs - average value for cs (context switches/s)* us - average value for us (user CPU)* sy - average value for sy (system CPU)* id - average value for id (idle)* wa - average value for wa (waiting for IO)* us+sy - sum of us and symy8408cs      us      sy      id      wa      us+sy455648  61.9    8.2     24.2    5.7     70.1    vu=40, wh=1000484955  50.4    9.2     19.5    21.0    59.6    vu=40, wh=2000487410  39.5    8.4     19.4    32.6    48.0    vu=40, wh=4000pg181cs      us      sy      id      wa      us+sy127486  23.5    10.1    63.3    3.0     33.6    vu=40, wh=1000166257  17.2    11.1    62.5    9.1     28.3    vu=40, wh=2000203578  13.9    11.3    59.2    15.6    25.2    vu=40, wh=4000ResultsMy analysis at this point is simple -- I only consider average throughput. Eventually I will examine throughput over time and efficiency (CPU and IO).On the charts that follow y-axis does not start at 0 to improve readability at the risk of overstating the differences. The y-axis shows relative throughput. There might be a regression when the relative throughput is less than 1.0. There might be an improvement when it is &#62; 1.0. The relative throughput is:(NOPM for some-version / NOPM for base-version)I provide three charts below:only MySQL - base-version is MySQL 5.6.51only Postgres - base-version is Postgres 12.22Postgres vs MySQL - base-version is Postgres 18.1, some-version is MySQL 8.4.8Results: MySQL 5.6 to 9.6Legend:my5651.z12a is MySQL 5.6.51 with the z12a50g configmy5744.z12a is MySQL 5.7.44 with the z12a50g configmy8045.z12a is MySQL 8.0.45 with the z12a50g configmy8408.z12a is MySQL 8.4.8 with the z12a50g configmy9500.z13a is MySQL 9.6.0 with the z13a50g configSummaryAt the lowest concurrency (vu=10) MySQL 8.4.8 has similar throughput as 5.6.51 because CPU regressions in modern MySQL offset the concurrency improvements.At the highest concurrency (vu=40) MySQL 8.4.8 is much faster than 5.6.51 and the regressions after 5.7 are small. This matches what I have seen elsewhere -- while modern MySQL suffers from CPU regressions it benefits from concurrency improvements. Imagine if we could get those concurrency improvements without the CPU regressions.And the absolute NOPM values are here:my5651my5744my8045my8408my9600vu=10, wh=1000163059183268156039155194151748vu=20, wh=1000210506321670283282281038279269vu=40, wh=1000216677454743439589435095433618vu=10, wh=2000107492130229111798110161108386vu=20, wh=2000155398225068193658190717189847vu=40, wh=2000178278302723297236307504293217vu=10, wh=400081242103406894148931688458vu=20, wh=4000131241179112155134152998152301vu=40, wh=4000146809228554234922229511230557Results: Postgres 12 to 18Legend:pg1222 is Postgres 12.22 with the x10a50g configpg1323 is Postgres 13.23 with the x10a50g configpg1420 is Postgres 14.20 with the x10a50g configpg1515 is Postgres 15.15 with the x10a50g configpg1611 is Postgres 16.11 with the x10a50g configpg177 is Postgres 17.7 with the x10a50g configpg181 is Postgres 18.1 with the x10b50g configSummaryModern Postgres has regressions relative to old PostgresThe regressions increase with the warehouse count, at wh=4000 the NOPM drops between 3% and 13% depending on the virtual user count (vu).The relative NOPM values are here:pg1222pg1323pg1420pg1515pg1611pg177pg181vu=10, wh=10001.0001.0001.0541.0421.0041.0100.968vu=20, wh=10001.0001.0351.0371.0281.0281.0010.997vu=40, wh=10001.0001.0400.9881.0001.0270.9980.970vu=10, wh=20001.0001.0261.0591.0751.0681.0811.029vu=20, wh=20001.0001.0221.0461.0430.9790.9720.934vu=40, wh=20001.0001.0141.0321.0360.9791.0100.947vu=10, wh=40001.0001.0271.0321.0350.9930.9980.974vu=20, wh=40001.0001.0051.0491.0480.9400.9270.876vu=40, wh=40001.0000.9911.0190.9831.0010.9790.937The absolute NOPM values are here:pg1222pg1323pg1420pg1515pg1611pg177pg181vu=10, wh=1000353077353048372015367933354513356469341688vu=20, wh=1000423565438456439398435454435288423986422397vu=40, wh=1000445114462851439728445144457110444364431648vu=10, wh=2000223048228914236231239868238117241185229549vu=20, wh=2000314380321380328688328044307728305452293627vu=40, wh=2000320347324769330444331896313553323454303403vu=10, wh=4000162054166461167320167761160962161716157872vu=20, wh=4000244598245804256593256231230037226844214309vu=40, wh=4000252931250634257820248584253059247610236986Results: MySQL vs PostgresLegend:pg181 is Postgres 18.1 with the x10b50g configmy8408 is MySQL 8.4.8 with the z12a50g configSummaryPostgres and MySQL have similar throughput for the largest warehouse count (wh=4000)Otherwise Postgres gets between 1.4X and 2X more throughput (NOPM)The absolute NOPM values are here:pg181my8408vu=10, wh=1000341688155194vu=20, wh=1000422397281038vu=40, wh=1000431648435095vu=10, wh=2000229549110161vu=20, wh=2000293627190717vu=40, wh=2000303403307504vu=10, wh=400015787289316vu=20, wh=4000214309152998vu=40, wh=4000236986229511</p>
<p><a href="https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-large-server.html">HammerDB tproc-c on a large server, Postgres and MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This has results for&nbsp;<a href="https://www.hammerdb.com/">HammerDB</a>&nbsp;tproc-c on a small server using MySQL and Postgres. I am new to HammerDB and still figuring out how to explain and present results so I will keep this simple and just share graphs without explaining the results.</p>
<p>The comparison might favor Postgres for the IO-bound workloads because I used smaller buffer pools than normal to avoid OOM. I have to do this because RSS for the HammerDB client grows over time as it buffers more response time stats. And while I used buffered IO for Postgres, I use O_DIRECT for InnoDB. So Postgres might have avoided some read IO thanks to the OS page cache while InnoDB did not.</p>
<p>tl;dr for MySQL</p>

<ul>
<li>With vu=40 MySQL 8.4.8 uses about 2X more CPU per transaction and does more than 2X more context switches per transaction compared to Postgres 18.1. I will get CPU profiles soon.</li>
<li>Modern MySQL brings us great improvements to concurrency and too many new CPU overheads</li>
<ul>
<li>MySQL 5.6 and 8.4 have similar throughput at the lowest concurrency (vu=10)</li>
<li>MySQl 8.4 is a lot faster than 5.6 at the highest concurrency (vu=40)</li>
</ul>
</ul>
<div>tl;dr for Postgres</div>
<div>
<ul>
<li>Modern Postgres has regressions relative to old Postgres</li>
<li>The regressions increase with the warehouse count, at wh=4000 the NOPM drops between 3% and 13% depending on the virtual user count (vu).</li>
</ul>
<div>tl;dr for Postgres vs MySQL</div>
</div>
<div>
<ul>
<li>Postgres and MySQL have similar throughput for the largest warehouse count (wh=4000)</li>
<li>Otherwise Postgres gets between 1.4X and 2X more throughput (NOPM)</li>
</ul>
</div>
<div><b>Builds, configuration and hardware</b></div>
<div>
<div></div>
<div>I compiled Postgres versions from source: 12.22, 13.23, 14.20, 15.15, 16.11, 17.7 and 18.1.</div>
<div></div>
<div>I compiled MySQL versions from source: 5.6.51, 5.7.44, 8.0.45, 8.4.8, 9.4.0 and 9.6.0.</div>
</div>
<div></div>
<div>
<div>I used a 48-core server from Hetzner</div>
<div>
<ul>
<li>an ax162s with an AMD EPYC 9454P 48-Core Processor with SMT disabled</li>
<li>2 Intel D7-P5520 NVMe storage devices with RAID 1 (3.8T each) using ext4</li>
<li>128G RAM</li>
<li>Ubuntu 22.04 running the non-HWE kernel (5.5.0-118-generic)</li>
</ul>
<div>
<div><span>Postgres configuration files:</span></div>
<div>
<ul>
<li><span>prior to v18 the config file is named conf.diff.cx10a50g_c32r128 (x10a_c32r128) and is here for versions&nbsp;</span><a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1219_o2nofp/conf.diff.cx10a50g_c32r128">12</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1315_o2nofp/conf.diff.cx10a50g_c32r128">13</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg1412_o2nofp/conf.diff.cx10a50g_c32r128">14</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg157_o2nofp/conf.diff.cx10a50g_c32r128">15</a>,&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg163_o2nofp/conf.diff.cx10a50g_c32r128">16</a>&nbsp;and&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg17beta1_o2nofp/conf.diff.cx10a50g_c32r128">17</a>.</li>
<li>for Postgres 18 I used&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/pg18beta3_o2nofp/conf.diff.cx10b50g_c32r128">conf.diff.cx10b_c32r128</a>&nbsp;(x10b_c32r128) with io_method=sync to be similar to the config used for versions 12 through 17.</li>
</ul>
<div>MySQL configuration files</div>
</div>
</div>
</div>
</div>
<div>
<ul>
<li>prior to 9.6 the config file is named my.cnf.cz12a50g_c32r128 (z12a50g_c32r128 or z12a50g) and is here for versions <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my5651_rel_o2nofp/etc/my.cnf.cz12a50g_c32r128">5.6</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my5744_rel_o2nofp/etc/my.cnf.cz12a50g_c32r128">5.7</a>, <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my8043_rel_o2nofp/etc/my.cnf.cz12a50g_c32r128">8.0</a> and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my8406_rel_o2nofp/etc/my.cnf.cz12a50g_c32r128">8.4</a></li>
<li>for 9.6 it is named my.cnf.cz13a50g_c32r128 (z13a50g_c32r128 or z13a50g) and <a href="https://github.com/mdcallag/mytools/blob/master/bench/conf/arc/oct24/c32r128/my9500/etc/my.cnf.cz13a50g_c32r128">is here</a></li>
</ul>
<div>For both Postgres and MySQL fsync on commit is disabled to avoid turning this into an fsync benchmark. The server has 2 SSDs with SW RAID and <a href="https://smalldatum.blogspot.com/2026/01/ssds-power-loss-protection-and-fsync.html">low fsync latency</a>.</div>
<div></div>
<div>
<div><b>Benchmark</b>
<div></div>
</div>
<div></div>
<div>The benchmark is&nbsp;<a href="https://www.hammerdb.com/docs/ch03.html">tproc-c</a>&nbsp;from&nbsp;<a href="https://www.hammerdb.com/">HammerDB</a>. The tproc-c benchmark is derived from TPC-C.
<p>The benchmark was run for several workloads:</p></div>
<div>
<ul>
<li>vu=10, wh=1000 &ndash; 10 virtual users, 1000 warehouses</li>
<li>vu=20, wh=1000 &ndash; 20 virtual users, 1000 warehouses</li>
<li>vu=40, wh=1000 &ndash; 40 virtual users, 1000 warehouses</li>
<li>vu=10, wh=2000 &ndash; 10 virtual users, 2000 warehouses</li>
<li>vu=20, wh=2000 &ndash; 20 virtual users, 2000 warehouses</li>
<li>vu=40, wh=2000 &ndash; 40 virtual users, 2000 warehouses</li>
<li>vu=10, wh=4000 &ndash; 10 virtual users, 4000 warehouses</li>
<li>vu=20, wh=4000 &ndash; 20 virtual users, 4000 warehouses</li>
<li>vu=40, wh=4000 &ndash; 40 virtual users, 4000 warehouses</li>
</ul>
<div>The wh=1000 workloads are less heavy on IO. The wh=4000 workloads are more heavy on IO.</div>
<div></div>
<div>The benchmark for Postgres is run by a variant of&nbsp;<a href="https://github.com/mdcallag/mytools/blob/master/bench/arc/jan26.tprocc.pn53.pg/allpg.N.sh">this script</a>&nbsp;which depends on&nbsp;<a href="https://github.com/mdcallag/mytools/tree/master/bench/arc/jan26.tprocc.pn53.pg/testscripts">scripts here</a>. The MySQL scripts are similar.</div>
<div>
<ul>
<li>stored procedures are enabled</li>
<li>partitioning is used because the warehouse count is &gt;= 1000</li>
<li>a 5 minute rampup is used</li>
<li>then performance is measured for 60 minutes</li>
</ul>
<div><b>Basic metrics: iostat</b></div>
<div></div>
<div>I am still improving my helper scripts to report various performance metrics. The table here has average values from iostat during the benchmark run phase for MySQL 8.4.8 and Postgres 18.1. For these configurations the NOPM values for Postgres and MySQL were similar so I won&rsquo;t present normalized values (average value / NOPM) and NOPM is throughput.</div>
<div>
<ul>
<li>average wMB/s increases with the warehouse count for Postgres but not for MySQL</li>
<li>r/s increases with the warehouse count for Postgres and MySQL</li>
</ul>
</div>
<div><span>iostat metrics</span></div>
<div>
<div><span>* r/s = average rate of reads/s from storage</span></div>
<div><span>* wMB/s = average MB/s written to storage</span></div>
<div><span><br></span></div>
<div><span>my8408</span></div>
<div><span>r/s&nbsp; &nbsp; &nbsp;wMB/s</span></div>
<div><span>22833.0 906.2&nbsp; &nbsp;vu=40, wh=1000</span></div>
<div><span>63079.8 1428.5&nbsp; vu=40, wh=2000</span></div>
<div><span>82282.3 1398.2&nbsp; vu=40, wh=4000</span></div>
<div><span><br></span></div>
<div><span>pg181</span></div>
<div><span>r/s&nbsp; &nbsp; &nbsp;wMB/s</span></div>
<div><span>30394.9 1261.9&nbsp; vu=40, wh=1000</span></div>
<div><span>59770.4 1267.8&nbsp; vu=40, wh=2000</span></div>
<div><span>78052.3 1272.9&nbsp; vu=40, wh=4000</span></div>
</div>
<div></div>
<div><b>Basic metrics: vmstat</b></div>
<div></div>
<div>I am still improving my helper scripts to report various performance metrics. The table here has average values from vmstat during the benchmark run phase for MySQL 8.4.8 and Postgres 18.1. For these configurations the NOPM values for Postgres and MySQL were similar so I won&rsquo;t present normalized values (average value / NOPM).</div>
<div>
<ul>
<li>CPU utilization is almost 2X larger for MySQL</li>
<li>Context switch rates are more than 2X larger for MySQL</li>
<li>In the future I hope to learn why MySQL uses almost 2X more CPU per transaction and has more than 2X more context switches per transaction relative to Postgres</li>
</ul>
</div>
<div><span>vmstat metrics</span></div>
<div>
<div><span>* cs &ndash; average value for cs (context switches/s)</span></div>
<div><span>* us &ndash; average value for us (user CPU)</span></div>
<div><span>* sy &ndash; average value for sy (system CPU)</span></div>
<div><span>* id &ndash; average value for id (idle)</span></div>
<div><span>* wa &ndash; average value for wa (waiting for IO)</span></div>
<div><span>* us+sy &ndash; sum of us and sy</span></div>
<div><span><br></span></div>
<div><span>my8408</span></div>
<div><span>cs&nbsp; &nbsp; &nbsp; us&nbsp; &nbsp; &nbsp; sy&nbsp; &nbsp; &nbsp; id&nbsp; &nbsp; &nbsp; wa&nbsp; &nbsp; &nbsp; us+sy</span></div>
<div><span>455648&nbsp; 61.9&nbsp; &nbsp; 8.2&nbsp; &nbsp; &nbsp;24.2&nbsp; &nbsp; 5.7&nbsp; &nbsp; &nbsp;70.1&nbsp; &nbsp; vu=40, wh=1000</span></div>
<div><span>484955&nbsp; 50.4&nbsp; &nbsp; 9.2&nbsp; &nbsp; &nbsp;19.5&nbsp; &nbsp; 21.0&nbsp; &nbsp; 59.6&nbsp; &nbsp; vu=40, wh=2000</span></div>
<div><span>487410&nbsp; 39.5&nbsp; &nbsp; 8.4&nbsp; &nbsp; &nbsp;19.4&nbsp; &nbsp; 32.6&nbsp; &nbsp; 48.0&nbsp; &nbsp; vu=40, wh=4000</span></div>
<div><span><br></span></div>
<div><span>pg181</span></div>
<div><span>cs&nbsp; &nbsp; &nbsp; us&nbsp; &nbsp; &nbsp; sy&nbsp; &nbsp; &nbsp; id&nbsp; &nbsp; &nbsp; wa&nbsp; &nbsp; &nbsp; us+sy</span></div>
<div><span>127486&nbsp; 23.5&nbsp; &nbsp; 10.1&nbsp; &nbsp; 63.3&nbsp; &nbsp; 3.0&nbsp; &nbsp; &nbsp;33.6&nbsp; &nbsp; vu=40, wh=1000</span></div>
<div><span>166257&nbsp; 17.2&nbsp; &nbsp; 11.1&nbsp; &nbsp; 62.5&nbsp; &nbsp; 9.1&nbsp; &nbsp; &nbsp;28.3&nbsp; &nbsp; vu=40, wh=2000</span></div>
<div><span>203578&nbsp; 13.9&nbsp; &nbsp; 11.3&nbsp; &nbsp; 59.2&nbsp; &nbsp; 15.6&nbsp; &nbsp; 25.2&nbsp; &nbsp; vu=40, wh=4000</span></div>
</div>
<div></div>
<div>
<div>
<div><b>Results</b></div>
<div><b><br></b></div>
<div>My analysis at this point is simple &mdash; I only consider average throughput. Eventually I will examine throughput over time and efficiency (CPU and IO).</div>
<div></div>
<div>On the charts that follow y-axis does not start at 0 to improve readability <b>at the risk of overstating the differences</b>. The y-axis shows relative throughput. There might be a regression when the relative throughput is less than 1.0. There might be an improvement when it is &gt; 1.0. The relative throughput is:</div>
</div>
<blockquote><p>(NOPM for&nbsp;<i>some-version</i>&nbsp;/ NOPM for&nbsp;<i>base-version</i>)</p></blockquote>
<p>I provide three charts below:</p>

<ul>
<li>only MySQL &ndash;&nbsp;<i>base-version</i>&nbsp;is MySQL 5.6.51</li>
<li>only Postgres &ndash;&nbsp;<i>base-version</i>&nbsp;is Postgres 12.22</li>
<li>Postgres vs MySQL &ndash;&nbsp;<i>base-version</i>&nbsp;is Postgres 18.1,&nbsp;<i>some-version</i>&nbsp;is MySQL 8.4.8</li>
</ul>
</div>
<div>
<div></div>
</div>
</div>
</div>
</div>
<div><b>Results: MySQL 5.6 to 9.6</b></div>
</div>
<div>
<p>Legend:</p>
<ul>
<li>my5651.z12a is MySQL 5.6.51 with the z12a50g config</li>
<li>my5744.z12a is MySQL 5.7.44 with the z12a50g config</li>
<li>my8045.z12a is MySQL 8.0.45 with the z12a50g config</li>
<li>my8408.z12a is MySQL 8.4.8 with the z12a50g config</li>
<li>my9500.z13a is MySQL 9.6.0 with the z13a50g config</li>
</ul>
<p>Summary</p>

<ul>
<li>At the lowest concurrency (vu=10) MySQL 8.4.8 has similar throughput as 5.6.51 because CPU regressions in modern MySQL offset the concurrency improvements.</li>
<li>At the highest concurrency (vu=40) MySQL 8.4.8 is much faster than 5.6.51 and the regressions after 5.7 are small. This matches what I have seen elsewhere &mdash; while modern MySQL suffers from CPU regressions it benefits from concurrency improvements. Imagine if we could get those concurrency improvements without the CPU regressions.</li>
</ul>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-juZIFwWtxtBi0SQ7Whhv6G0nLgtkFKSNGivr7_4Fh-Cmx8mq9zy8L_JTbvE9iZiYluDQmZWqd5p2J2CDLAXGI0T61rC_CQS1DVMiHoarSmH2BPh40mvxFyBskM-8IajNoHZfczNKPO_dDSuKxrFynR9H1n9mlsxhBsHVZw8Zeo3hrdYyziRc9gvxl5MG/s600/Relative%20NOPM%20on%20a%20large%20server_%20MySQL.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg-juZIFwWtxtBi0SQ7Whhv6G0nLgtkFKSNGivr7_4Fh-Cmx8mq9zy8L_JTbvE9iZiYluDQmZWqd5p2J2CDLAXGI0T61rC_CQS1DVMiHoarSmH2BPh40mvxFyBskM-8IajNoHZfczNKPO_dDSuKxrFynR9H1n9mlsxhBsHVZw8Zeo3hrdYyziRc9gvxl5MG/w640-h396/Relative%20NOPM%20on%20a%20large%20server_%20MySQL.png" width="640"></a></div>
<p>And the absolute NOPM values are here:</p>

<table border="1" cellpadding="0" cellspacing="0" data-sheets-baot="1" data-sheets-root="1" dir="ltr">
<colgroup>
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100"></colgroup>
<tbody>
<tr>
<td></td>
<td>my5651</td>
<td>my5744</td>
<td>my8045</td>
<td>my8408</td>
<td>my9600</td>
</tr>
<tr>
<td>vu=10, wh=1000</td>
<td>163059</td>
<td>183268</td>
<td>156039</td>
<td>155194</td>
<td>151748</td>
</tr>
<tr>
<td>vu=20, wh=1000</td>
<td>210506</td>
<td>321670</td>
<td>283282</td>
<td>281038</td>
<td>279269</td>
</tr>
<tr>
<td>vu=40, wh=1000</td>
<td>216677</td>
<td>454743</td>
<td>439589</td>
<td>435095</td>
<td>433618</td>
</tr>
<tr>
<td>vu=10, wh=2000</td>
<td>107492</td>
<td>130229</td>
<td>111798</td>
<td>110161</td>
<td>108386</td>
</tr>
<tr>
<td>vu=20, wh=2000</td>
<td>155398</td>
<td>225068</td>
<td>193658</td>
<td>190717</td>
<td>189847</td>
</tr>
<tr>
<td>vu=40, wh=2000</td>
<td>178278</td>
<td>302723</td>
<td>297236</td>
<td>307504</td>
<td>293217</td>
</tr>
<tr>
<td>vu=10, wh=4000</td>
<td>81242</td>
<td>103406</td>
<td>89414</td>
<td>89316</td>
<td>88458</td>
</tr>
<tr>
<td>vu=20, wh=4000</td>
<td>131241</td>
<td>179112</td>
<td>155134</td>
<td>152998</td>
<td>152301</td>
</tr>
<tr>
<td>vu=40, wh=4000</td>
<td>146809</td>
<td>228554</td>
<td>234922</td>
<td>229511</td>
<td>230557</td>
</tr>
</tbody>
</table>
<p><b>Results: Postgres 12 to 18</b></p>
<p>Legend:</p>
<ul>
<li>pg1222 is Postgres 12.22 with the x10a50g config</li>
<li>pg1323 is Postgres 13.23 with the x10a50g config</li>
<li>pg1420 is Postgres 14.20 with the x10a50g config</li>
<li>pg1515 is Postgres 15.15 with the x10a50g config</li>
<li>pg1611 is Postgres 16.11 with the x10a50g config</li>
<li>pg177 is Postgres 17.7 with the x10a50g config</li>
<li>pg181 is Postgres 18.1 with the x10b50g config</li>
</ul>
<p>Summary</p>

<ul>
<li>Modern Postgres has regressions relative to old Postgres</li>
<li>The regressions increase with the warehouse count, at wh=4000 the NOPM drops between 3% and 13% depending on the virtual user count (vu).</li>
</ul>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9bKWABILkGwJHvK9zBvnoWwpw6vbI6_A8OTEglhbPt7BjBEVVVT1KpC6NUQVlvpK3JbiP7y6Rnei7kTCKyPxLsnwqTzAs0Aa0Q5kHylICO2IuKMiadIY_o8w_qWG-s8HavljpBT6aA6GBEkNnyVoHciGH70-IwYQ0aF3hjRyuW1HLodMGnm3hgPoV7bt0/s600/Relative%20NOPM%20on%20a%20large%20server_%20Postgres.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh9bKWABILkGwJHvK9zBvnoWwpw6vbI6_A8OTEglhbPt7BjBEVVVT1KpC6NUQVlvpK3JbiP7y6Rnei7kTCKyPxLsnwqTzAs0Aa0Q5kHylICO2IuKMiadIY_o8w_qWG-s8HavljpBT6aA6GBEkNnyVoHciGH70-IwYQ0aF3hjRyuW1HLodMGnm3hgPoV7bt0/w640-h396/Relative%20NOPM%20on%20a%20large%20server_%20Postgres.png" width="640"></a></div>
<p></p>
<div>The relative NOPM values are here:</div>
<div></div>

<table border="1" cellpadding="0" cellspacing="0" data-sheets-baot="1" data-sheets-root="1" dir="ltr">
<colgroup>
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100"></colgroup>
<tbody>
<tr>
<td></td>
<td>pg1222</td>
<td>pg1323</td>
<td>pg1420</td>
<td>pg1515</td>
<td>pg1611</td>
<td>pg177</td>
<td>pg181</td>
</tr>
<tr>
<td><span>vu=10, wh=1000</span></td>
<td><span>1.000</span></td>
<td><span>1.000</span></td>
<td><span>1.054</span></td>
<td><span>1.042</span></td>
<td><span>1.004</span></td>
<td><span>1.010</span></td>
<td><span>0.968</span></td>
</tr>
<tr>
<td><span>vu=20, wh=1000</span></td>
<td><span>1.000</span></td>
<td><span>1.035</span></td>
<td><span>1.037</span></td>
<td><span>1.028</span></td>
<td><span>1.028</span></td>
<td><span>1.001</span></td>
<td><span>0.997</span></td>
</tr>
<tr>
<td><span>vu=40, wh=1000</span></td>
<td><span>1.000</span></td>
<td><span>1.040</span></td>
<td><span>0.988</span></td>
<td><span>1.000</span></td>
<td><span>1.027</span></td>
<td><span>0.998</span></td>
<td><span>0.970</span></td>
</tr>
<tr>
<td><span>vu=10, wh=2000</span></td>
<td><span>1.000</span></td>
<td><span>1.026</span></td>
<td><span>1.059</span></td>
<td><span>1.075</span></td>
<td><span>1.068</span></td>
<td><span>1.081</span></td>
<td><span>1.029</span></td>
</tr>
<tr>
<td><span>vu=20, wh=2000</span></td>
<td><span>1.000</span></td>
<td><span>1.022</span></td>
<td><span>1.046</span></td>
<td><span>1.043</span></td>
<td><span>0.979</span></td>
<td><span>0.972</span></td>
<td><span>0.934</span></td>
</tr>
<tr>
<td><span>vu=40, wh=2000</span></td>
<td><span>1.000</span></td>
<td><span>1.014</span></td>
<td><span>1.032</span></td>
<td><span>1.036</span></td>
<td><span>0.979</span></td>
<td><span>1.010</span></td>
<td><span>0.947</span></td>
</tr>
<tr>
<td><span>vu=10, wh=4000</span></td>
<td><span>1.000</span></td>
<td><span>1.027</span></td>
<td><span>1.032</span></td>
<td><span>1.035</span></td>
<td><span>0.993</span></td>
<td><span>0.998</span></td>
<td><span>0.974</span></td>
</tr>
<tr>
<td><span>vu=20, wh=4000</span></td>
<td><span>1.000</span></td>
<td><span>1.005</span></td>
<td><span>1.049</span></td>
<td><span>1.048</span></td>
<td><span>0.940</span></td>
<td><span>0.927</span></td>
<td><span>0.876</span></td>
</tr>
<tr>
<td><span>vu=40, wh=4000</span></td>
<td><span>1.000</span></td>
<td><span>0.991</span></td>
<td><span>1.019</span></td>
<td><span>0.983</span></td>
<td><span>1.001</span></td>
<td><span>0.979</span></td>
<td><span>0.937</span></td>
</tr>
</tbody>
</table>
<div></div>

<div>The absolute NOPM values are here:</div>
<div></div>

<table border="1" cellpadding="0" cellspacing="0" data-sheets-baot="1" data-sheets-root="1" dir="ltr">
<colgroup>
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100">
<col width="100"></colgroup>
<tbody>
<tr>
<td></td>
<td><span>pg1222</span></td>
<td><span>pg1323</span></td>
<td><span>pg1420</span></td>
<td><span>pg1515</span></td>
<td><span>pg1611</span></td>
<td><span>pg177</span></td>
<td><span>pg181</span></td>
</tr>
<tr>
<td><span>vu=10, wh=1000</span></td>
<td><span>353077</span></td>
<td><span>353048</span></td>
<td><span>372015</span></td>
<td><span>367933</span></td>
<td><span>354513</span></td>
<td><span>356469</span></td>
<td><span>341688</span></td>
</tr>
<tr>
<td><span>vu=20, wh=1000</span></td>
<td><span>423565</span></td>
<td><span>438456</span></td>
<td><span>439398</span></td>
<td><span>435454</span></td>
<td><span>435288</span></td>
<td><span>423986</span></td>
<td><span>422397</span></td>
</tr>
<tr>
<td><span>vu=40, wh=1000</span></td>
<td><span>445114</span></td>
<td><span>462851</span></td>
<td><span>439728</span></td>
<td><span>445144</span></td>
<td><span>457110</span></td>
<td><span>444364</span></td>
<td><span>431648</span></td>
</tr>
<tr>
<td><span>vu=10, wh=2000</span></td>
<td><span>223048</span></td>
<td><span>228914</span></td>
<td><span>236231</span></td>
<td><span>239868</span></td>
<td><span>238117</span></td>
<td><span>241185</span></td>
<td><span>229549</span></td>
</tr>
<tr>
<td><span>vu=20, wh=2000</span></td>
<td><span>314380</span></td>
<td><span>321380</span></td>
<td><span>328688</span></td>
<td><span>328044</span></td>
<td><span>307728</span></td>
<td><span>305452</span></td>
<td><span>293627</span></td>
</tr>
<tr>
<td><span>vu=40, wh=2000</span></td>
<td><span>320347</span></td>
<td><span>324769</span></td>
<td><span>330444</span></td>
<td><span>331896</span></td>
<td><span>313553</span></td>
<td><span>323454</span></td>
<td><span>303403</span></td>
</tr>
<tr>
<td><span>vu=10, wh=4000</span></td>
<td><span>162054</span></td>
<td><span>166461</span></td>
<td><span>167320</span></td>
<td><span>167761</span></td>
<td><span>160962</span></td>
<td><span>161716</span></td>
<td><span>157872</span></td>
</tr>
<tr>
<td><span>vu=20, wh=4000</span></td>
<td><span>244598</span></td>
<td><span>245804</span></td>
<td><span>256593</span></td>
<td><span>256231</span></td>
<td><span>230037</span></td>
<td><span>226844</span></td>
<td><span>214309</span></td>
</tr>
<tr>
<td><span>vu=40, wh=4000</span></td>
<td><span>252931</span></td>
<td><span>250634</span></td>
<td><span>257820</span></td>
<td><span>248584</span></td>
<td><span>253059</span></td>
<td><span>247610</span></td>
<td><span>236986</span></td>
</tr>
</tbody>
</table>
<div></div>

<div><b>Results: MySQL vs Postgres</b></div>
<div>
<p>Legend:</p>
<ul>
<li>pg181 is Postgres 18.1 with the x10b50g config</li>
<li>my8408 is MySQL 8.4.8 with the z12a50g config</li>
</ul>
<p>Summary</p>

<ul>
<li>Postgres and MySQL have similar throughput for the largest warehouse count (wh=4000)</li>
<li>Otherwise Postgres gets between 1.4X and 2X more throughput (NOPM)</li>
</ul>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNjiZaCuVAOks1rZk-KPyNStyYZD2aDw-hIz1FTFy98BpwArrlQ-ngWnzEvwEnqAL44DgcmH1eNTcGePbxf2UctfP2rzpz8SGRv8Byd9d2da4mD1TXpqD2uqLHcOJN2B84iKq0sJuVD8kRUp_D69YlVq8tPPMJrlFXK2UEywx3MIDLJDxasmEgGwy8eNYz/s600/Relative%20NOPM%20on%20a%20large%20server_%20Postgres%2018.1%20vs%20MySQL%208.4.8.png"><img loading="lazy" decoding="async" border="0" data-original-height="371" data-original-width="600" height="396" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNjiZaCuVAOks1rZk-KPyNStyYZD2aDw-hIz1FTFy98BpwArrlQ-ngWnzEvwEnqAL44DgcmH1eNTcGePbxf2UctfP2rzpz8SGRv8Byd9d2da4mD1TXpqD2uqLHcOJN2B84iKq0sJuVD8kRUp_D69YlVq8tPPMJrlFXK2UEywx3MIDLJDxasmEgGwy8eNYz/w640-h396/Relative%20NOPM%20on%20a%20large%20server_%20Postgres%2018.1%20vs%20MySQL%208.4.8.png" width="640"></a></div>
<div>The absolute NOPM values are here:</div>
</div>
</div>
<div></div>
<div>
<table border="1" cellpadding="0" cellspacing="0" data-sheets-baot="1" data-sheets-root="1" dir="ltr">
<colgroup>
<col width="100">
<col width="100">
<col width="100"></colgroup>
<tbody>
<tr>
<td></td>
<td>pg181</td>
<td>my8408</td>
</tr>
<tr>
<td>vu=10, wh=1000</td>
<td>341688</td>
<td>155194</td>
</tr>
<tr>
<td>vu=20, wh=1000</td>
<td>422397</td>
<td>281038</td>
</tr>
<tr>
<td>vu=40, wh=1000</td>
<td>431648</td>
<td>435095</td>
</tr>
<tr>
<td>vu=10, wh=2000</td>
<td>229549</td>
<td>110161</td>
</tr>
<tr>
<td>vu=20, wh=2000</td>
<td>293627</td>
<td>190717</td>
</tr>
<tr>
<td>vu=40, wh=2000</td>
<td>303403</td>
<td>307504</td>
</tr>
<tr>
<td>vu=10, wh=4000</td>
<td>157872</td>
<td>89316</td>
</tr>
<tr>
<td>vu=20, wh=4000</td>
<td>214309</td>
<td>152998</td>
</tr>
<tr>
<td>vu=40, wh=4000</td>
<td>236986</td>
<td>229511</td>
</tr>
</tbody>
</table>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>

<p><a href="https://smalldatum.blogspot.com/2026/02/hammerdb-tproc-c-on-large-server.html">HammerDB tproc-c on a large server, Postgres and MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>What is the quickest way to load data into the database?</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/load-data-quick-into-the-database/" />
      <id>https://www.fromdual.com/blog/load-data-quick-into-the-database/</id>
      <updated>2026-02-11T09:04:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We had some really exciting problems to solve for the last customer! Especially because the database wasn’t exactly small.<br />
Here are some key data: CPU: 2 sockets x 24 cores x 2 threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD in RAID-10 and 7 Tbyte data, several thousand clients, rapidly growing.<br />
The current throughput: 1 M SELECT/min, 56 k INSERT/min, 44 k UPDATE/min, 7 k DELETE/min averaged over 30 days. With a strong upward trend. Application and queries not consistently optimised. Database configuration: ‘state of the art’ not verified with benchmarks. CPU utilisation approx. 50% on average, more at peak times. I/O system still has available resources.<br />
The customer collects position and other device data and stores it in the database. In other words, a classic IoT problem (with time series, index clustered table, etc.).<br />
The question he has asked is: What is the fastest way to copy data from one table (pending data, a kind of queue) to another table (final data, per client)?<br />
The data flow looks something like this:<br />
+------------+<br />
&#124; IoT Device &#124;--+<br />
+------------+<br />
  +-----+<br />
+------------+  &#124; AS &#124; +--------------+ Processing +------------+<br />
&#124; IoT Device &#124;------+-- &#62;&#124; &#124;-- &#62;&#124; Pending data &#124;------------- &#62;&#124; Final data &#124;<br />
+------------+ / &#124; 400 &#124; +--------------+ of data +------------+<br />
 / +-----+<br />
+------------+ /<br />
&#124; IoT Device &#124;--+<br />
+------------+<br />
3 different variants to copy the data were available for selection.<br />
Variant 1: INSERT and DELETE (simplest form)<br />
The simplest variant is a simple INSERT and DELETE. This variant is particularly problematic because MariaDB/MySQL and PostgreSQL have AUTOCOMMIT enabled by default (here, here, here and here).<br />
To help you visualise this a little better, here is some pseudocode:<br />
// 20k rows<br />
for (i = 1; i GRANT ALL ON *.* TO \'app\'@\'127.0.0.1\';</p>
<p>$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --prepare</p>
<p>$ for i in $(seq 5) ; do<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=1<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=2<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=3<br />
done</p>
<p>$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --clean-up<br />
Everyone can work out the measured values themselves with the corresponding test script.<br />
Preparation and execution with PostgreSQL<br />
You can execute these tests yourself with the following commands:<br />
postgres# CREATE DATABASE test;<br />
postgres# CREATE USER app PASSWORD \'secret\';<br />
postgres# GRANT ALL ON DATABASE test TO app;<br />
postgres# GRANT ALL ON SCHEMA public TO app;</p>
<p>$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --prepare</p>
<p>$ for i in $(seq 5) ; do<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=1<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=2<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=3<br />
done</p>
<p>$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --clean-up<br />
Anyone can work out the measured values themselves with the corresponding test script.<br />
Results<br />
To avoid unnecessary discussions, we have ‘only’ listed the relative performance (runtime) here, as MarkC has been doing recently. We are happy to provide our measured values bilaterally. However, they can be easily reproduced with the test script itself.<br />
Less is better:</p>
<p>					Variant 1<br />
					Variant 2<br />
					Variant 3</p>
<p>					MariaDB 11.8, avg(5)<br />
					100.0%<br />
					7.5%<br />
					6.2%</p>
<p>					PostgreSQL 19dev, avg(5)<br />
					100.0%<br />
					11.2%<br />
					7.0%</p>
<p>Attention: The values of MariaDB/MySQL and PostgreSQL can NOT be compared directly!</p>
<p>And here is the graphical evaluation:</p>
<p> </p>
<p>Remarks<br />
With faster discs, the difference between 1 and 2/3 would probably not have been quite so significant. Customer tests have shown a difference of ‘only’ about a factor of 5 (instead of a factor of 9 to 16).<br />
There are certainly other ways in which this loading process can be optimised. Here are a few that come to mind:</p>
<p>INSERT INTO ... SELECT * FROM<br />
LOAD DATA INFILE/COPY, if possible<br />
Prepared statements<br />
Server side Stored Language (SQL/PSM, PL/pgSQL, …) :-(<br />
PDO-Fetch of the results?<br />
etc.</p>
<p>Maybe I should look for a profiler (Xdebug or xhprof)?<br />
Further contributions</p>
<p>Load CSV files into the database<br />
MariaDB Prepared Statements, Transactions and Multi-Row Inserts<br />
How good is MySQL INSERT TRIGGER performance</p>
<p>This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/load-data-quick-into-the-database/">What is the quickest way to load data into the database?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We had some really exciting problems to solve for the last customer! Especially because the database wasn&rsquo;t exactly small.</p>
<p>Here are some key data: CPU: 2 sockets x 24 cores x 2 threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD in RAID-10 and 7 Tbyte data, several thousand clients, rapidly growing.</p>
<p>The current throughput: 1 M <code>SELECT</code>/min, 56 k <code>INSERT</code>/min, 44 k <code>UPDATE</code>/min, 7 k <code>DELETE</code>/min averaged over 30 days. With a strong upward trend. Application and queries not consistently optimised. Database configuration: &lsquo;state of the art&rsquo; not verified with benchmarks. CPU utilisation approx. 50% on average, more at peak times. I/O system still has available resources.</p>
<p>The customer collects position and other device data and stores it in the database. In other words, a classic IoT problem (with time series, index clustered table, etc.).</p>
<p>The question he has asked is: What is the fastest way to copy data from one table (pending data, a kind of queue) to another table (final data, per client)?</p>
<p>The data flow looks something like this:</p>
<pre><code>+------------+
| IoT Device |--+
+------------+ 
  +-----+
+------------+  | AS | +--------------+ Processing +------------+
| IoT Device |------+--&gt;| |--&gt;| Pending data |-------------&gt;| Final data |
+------------+ / | 400 | +--------------+ of data +------------+
 / +-----+
+------------+ /
| IoT Device |--+
+------------+
</code></pre>
<p>3 different variants to copy the data were available for selection.</p>
<h2>Variant 1: <code>INSERT</code> and <code>DELETE</code> (simplest form)<a class="anchor-link" id="variant-1-insert-and-delete-simplest-form"></a></h2>
<p>The simplest variant is a simple <code>INSERT</code> and <code>DELETE</code>. This variant is particularly problematic because MariaDB/MySQL and PostgreSQL have <code>AUTOCOMMIT</code> enabled by default (<a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-autocommit-commit-rollback.html" target="_blank">here</a>, <a href="https://mariadb.com/docs/server/reference/sql-statements/transactions/start-transaction" target="_blank">here</a>, <a href="https://www.postgresql.org/docs/current/ecpg-sql-set-autocommit.html" target="_blank">here</a> and <a href="https://www.cybertec-postgresql.com/en/disabling-autocommit-in-postgresql-can-damage-your-health/" target="_blank">here</a>).</p>
<p>To help you visualise this a little better, here is some pseudocode:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 foreach ( row ) {
 INSERT INTO final;
 -- implicit COMMIT
 DELETE FROM pending WHERE id = row[id];
 -- implicit COMMIT
 }
}
</code></pre>
<p>So if we want to copy 20 k rows, this variant causes: 40 k <code>COMMIT</code>s (<code>fsync</code>) and 42 k network round trips!</p>
<h2>Variant 2: <code>START TRANSACTION</code> and <code>INSERT</code> ad <code>DELETE</code><a class="anchor-link" id="variant-2-start-transaction-and-insert-ad-delete"></a></h2>
<p>This variant is used by more experienced database developers. Here is the corresponding pseudocode:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 foreach ( row ) {
 INSERT INTO final;
 DELETE FROM pending WHERE id = row[id];
 }
 COMMIT;
}
</code></pre>
<p>If we want to copy 20 k rows in this example, this variant only causes 2 k <code>COMMIT</code>s (<code>fsync</code>)! So 20 times less! But 46 k network round trips (10% more).</p>
<h2>Variant 3: <code>START TRANSACTION</code> and optimised <code>INSERT</code> and <code>DELETE</code><a class="anchor-link" id="variant-3-start-transaction-and-optimised-insert-and-delete"></a></h2>
<p>This variant is a little more demanding in terms of programming. It is used if you want to get a little closer to the limits of what is possible. Here is the pseudo code:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 INSERT INTO final (), (), (), (), (), (), (), (), (), ();
 DELETE FROM pending WHERE id = IN (...);
 COMMIT;
}
</code></pre>
<p>And this 3rd variant also only causes 2 k <code>COMMIT</code>&rsquo;s (<code>fsync</code>) with 20 k rows, but saves the loop via the <code>INSERT</code> and <code>DELETE</code> statements in the database. So on the one hand we save network round trips (only 10 k) and CPU cycles on the database (which are difficult to scale) for parsing the queries.</p>
<h2>Test set-up<a class="anchor-link" id="test-set-up"></a></h2>
<p>To test the whole thing, we have prepared a small script: <a href="https://www.fromdual.com/code-examples/load_data.php.txt">load_data.php</a></p>
<h3>Preparation and execution with MariaDB/MySQL<a class="anchor-link" id="preparation-and-execution-with-mariadb-mysql"></a></h3>
<p>You can execute these tests yourself with the following commands:</p>
<pre><code>SQL&gt; CREATE DATABASE test;
SQL&gt; CREATE USER 'app'@'127.0.0.1' IDENTIFIED BY 'secret';
SQL&gt; GRANT ALL ON *.* TO 'app'@'127.0.0.1';

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --clean-up
</code></pre>
<p>Everyone can work out the measured values themselves with the corresponding test script.</p>
<h3>Preparation and execution with PostgreSQL<a class="anchor-link" id="preparation-and-execution-with-postgresql"></a></h3>
<p>You can execute these tests yourself with the following commands:</p>
<pre><code>postgres# CREATE DATABASE test;
postgres# CREATE USER app PASSWORD 'secret';
postgres# GRANT ALL ON DATABASE test TO app;
postgres# GRANT ALL ON SCHEMA public TO app;

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --clean-up
</code></pre>
<p>Anyone can work out the measured values themselves with the corresponding test script.</p>
<h2>Results<a class="anchor-link" id="results"></a></h2>
<p>To avoid unnecessary discussions, we have &lsquo;only&rsquo; listed the relative performance (runtime) here, as MarkC has been doing recently. We are happy to provide our measured values bilaterally. However, they can be easily reproduced with the test script itself.</p>
<p>Less is better:</p>
<table>
<thead>
<tr>
<th></th>
<th>Variant 1</th>
<th>Variant 2</th>
<th>Variant 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>MariaDB 11.8, avg(5)</td>
<td>100.0%</td>
<td>7.5%</td>
<td>6.2%</td>
</tr>
<tr>
<td>PostgreSQL 19dev, avg(5)</td>
<td>100.0%</td>
<td>11.2%</td>
<td>7.0%</td>
</tr>
</tbody>
</table>
<p><strong>Attention</strong>: The values of MariaDB/MySQL and PostgreSQL can NOT be compared directly!</p>
<p></p>
<p>And here is the graphical evaluation:</p>
<p><img decoding="async" src="https://www.fromdual.com/images/mariadb-data-load.png" alt="mariadb"></p>
<p>&nbsp;</p>
<p><img decoding="async" src="https://www.fromdual.com/images/postgresql-data-load.png" alt="postgresql"></p>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>With faster discs, the difference between 1 and 2/3 would probably not have been quite so significant. Customer tests have shown a difference of &lsquo;only&rsquo; about a factor of 5 (instead of a factor of 9 to 16).</p>
<p>There are certainly other ways in which this loading process can be optimised. Here are a few that come to mind:</p>
<ul>
<li><code>INSERT INTO ... SELECT * FROM</code></li>
<li><code>LOAD DATA INFILE</code>/<code>COPY</code>, if possible</li>
<li>Prepared statements</li>
<li>Server side Stored Language (SQL/PSM, PL/pgSQL, &hellip;) &#128577;</li>
<li>PDO-Fetch of the results?</li>
<li>etc.</li>
</ul>
<p>Maybe I should look for a profiler (<a href="https://xdebug.org/" target="_blank">Xdebug</a> or <a href="https://www.php.net/manual/en/book.xhprof.php" target="_blank">xhprof</a>)?</p>
<h2>Further contributions<a class="anchor-link" id="further-contributions"></a></h2>
<ul>
<li><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a></li>
<li><a href="https://www.fromdual.com/blog/mariadb-prepared-statements-transactions-and-multi-row-inserts/">MariaDB Prepared Statements, Transactions and Multi-Row Inserts</a></li>
<li><a href="https://www.fromdual.com/blog/how-good-is-mysql-insert-trigger-performance/">How good is MySQL INSERT TRIGGER performance</a></li>
</ul>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/load-data-quick-into-the-database/">What is the quickest way to load data into the database?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>What is the quickest way to load data into the database?</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/load-data-quick-into-the-database/" />
      <id>https://www.fromdual.com/blog/load-data-quick-into-the-database/</id>
      <updated>2026-02-11T09:04:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We had some really exciting problems to solve for the last customer! Especially because the database wasn’t exactly small.<br />
Here are some key data: CPU: 2 sockets x 24 cores x 2 threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD in RAID-10 and 7 Tbyte data, several thousand clients, rapidly growing.<br />
The current throughput: 1 M SELECT/min, 56 k INSERT/min, 44 k UPDATE/min, 7 k DELETE/min averaged over 30 days. With a strong upward trend. Application and queries not consistently optimised. Database configuration: ‘state of the art’ not verified with benchmarks. CPU utilisation approx. 50% on average, more at peak times. I/O system still has available resources.<br />
The customer collects position and other device data and stores it in the database. In other words, a classic IoT problem (with time series, index clustered table, etc.).<br />
The question he has asked is: What is the fastest way to copy data from one table (pending data, a kind of queue) to another table (final data, per client)?<br />
The data flow looks something like this:<br />
+------------+<br />
&#124; IoT Device &#124;--+<br />
+------------+<br />
  +-----+<br />
+------------+  &#124; AS &#124; +--------------+ Processing +------------+<br />
&#124; IoT Device &#124;------+-- &#62;&#124; &#124;-- &#62;&#124; Pending data &#124;------------- &#62;&#124; Final data &#124;<br />
+------------+ / &#124; 400 &#124; +--------------+ of data +------------+<br />
 / +-----+<br />
+------------+ /<br />
&#124; IoT Device &#124;--+<br />
+------------+<br />
3 different variants to copy the data were available for selection.<br />
Variant 1: INSERT and DELETE (simplest form)<br />
The simplest variant is a simple INSERT and DELETE. This variant is particularly problematic because MariaDB/MySQL and PostgreSQL have AUTOCOMMIT enabled by default (here, here, here and here).<br />
To help you visualise this a little better, here is some pseudocode:<br />
// 20k rows<br />
for (i = 1; i GRANT ALL ON *.* TO \'app\'@\'127.0.0.1\';</p>
<p>$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --prepare</p>
<p>$ for i in $(seq 5) ; do<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=1<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=2<br />
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=3<br />
done</p>
<p>$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --clean-up<br />
Everyone can work out the measured values themselves with the corresponding test script.<br />
Preparation and execution with PostgreSQL<br />
You can execute these tests yourself with the following commands:<br />
postgres# CREATE DATABASE test;<br />
postgres# CREATE USER app PASSWORD \'secret\';<br />
postgres# GRANT ALL ON DATABASE test TO app;<br />
postgres# GRANT ALL ON SCHEMA public TO app;</p>
<p>$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --prepare</p>
<p>$ for i in $(seq 5) ; do<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=1<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=2<br />
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=3<br />
done</p>
<p>$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --clean-up<br />
Anyone can work out the measured values themselves with the corresponding test script.<br />
Results<br />
To avoid unnecessary discussions, we have ‘only’ listed the relative performance (runtime) here, as MarkC has been doing recently. We are happy to provide our measured values bilaterally. However, they can be easily reproduced with the test script itself.<br />
Less is better:</p>
<p>					Variant 1<br />
					Variant 2<br />
					Variant 3</p>
<p>					MariaDB 11.8, avg(5)<br />
					100.0%<br />
					7.5%<br />
					6.2%</p>
<p>					PostgreSQL 19dev, avg(5)<br />
					100.0%<br />
					11.2%<br />
					7.0%</p>
<p>Attention: The values of MariaDB/MySQL and PostgreSQL can NOT be compared directly!</p>
<p>And here is the graphical evaluation:</p>
<p> </p>
<p>Remarks<br />
With faster discs, the difference between 1 and 2/3 would probably not have been quite so significant. Customer tests have shown a difference of ‘only’ about a factor of 5 (instead of a factor of 9 to 16).<br />
There are certainly other ways in which this loading process can be optimised. Here are a few that come to mind:</p>
<p>INSERT INTO ... SELECT * FROM<br />
LOAD DATA INFILE/COPY, if possible<br />
Prepared statements<br />
Server side Stored Language (SQL/PSM, PL/pgSQL, …) :-(<br />
PDO-Fetch of the results?<br />
etc.</p>
<p>Maybe I should look for a profiler (Xdebug or xhprof)?<br />
Further contributions</p>
<p>Load CSV files into the database<br />
MariaDB Prepared Statements, Transactions and Multi-Row Inserts<br />
How good is MySQL INSERT TRIGGER performance</p>
<p>This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/load-data-quick-into-the-database/">What is the quickest way to load data into the database?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We had some really exciting problems to solve for the last customer! Especially because the database wasn&rsquo;t exactly small.</p>
<p>Here are some key data: CPU: 2 sockets x 24 cores x 2 threads = 96 vCores, 756 G RAM, 2 x 10 Tbyte PCIe SSD in RAID-10 and 7 Tbyte data, several thousand clients, rapidly growing.</p>
<p>The current throughput: 1 M <code>SELECT</code>/min, 56 k <code>INSERT</code>/min, 44 k <code>UPDATE</code>/min, 7 k <code>DELETE</code>/min averaged over 30 days. With a strong upward trend. Application and queries not consistently optimised. Database configuration: &lsquo;state of the art&rsquo; not verified with benchmarks. CPU utilisation approx. 50% on average, more at peak times. I/O system still has available resources.</p>
<p>The customer collects position and other device data and stores it in the database. In other words, a classic IoT problem (with time series, index clustered table, etc.).</p>
<p>The question he has asked is: What is the fastest way to copy data from one table (pending data, a kind of queue) to another table (final data, per client)?</p>
<p>The data flow looks something like this:</p>
<pre><code>+------------+
| IoT Device |--+
+------------+ 
  +-----+
+------------+  | AS | +--------------+ Processing +------------+
| IoT Device |------+--&gt;| |--&gt;| Pending data |-------------&gt;| Final data |
+------------+ / | 400 | +--------------+ of data +------------+
 / +-----+
+------------+ /
| IoT Device |--+
+------------+
</code></pre>
<p>3 different variants to copy the data were available for selection.</p>
<h2>Variant 1: <code>INSERT</code> and <code>DELETE</code> (simplest form)<a class="anchor-link" id="variant-1-insert-and-delete-simplest-form"></a></h2>
<p>The simplest variant is a simple <code>INSERT</code> and <code>DELETE</code>. This variant is particularly problematic because MariaDB/MySQL and PostgreSQL have <code>AUTOCOMMIT</code> enabled by default (<a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-autocommit-commit-rollback.html" target="_blank">here</a>, <a href="https://mariadb.com/docs/server/reference/sql-statements/transactions/start-transaction" target="_blank">here</a>, <a href="https://www.postgresql.org/docs/current/ecpg-sql-set-autocommit.html" target="_blank">here</a> and <a href="https://www.cybertec-postgresql.com/en/disabling-autocommit-in-postgresql-can-damage-your-health/" target="_blank">here</a>).</p>
<p>To help you visualise this a little better, here is some pseudocode:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 foreach ( row ) {
 INSERT INTO final;
 -- implicit COMMIT
 DELETE FROM pending WHERE id = row[id];
 -- implicit COMMIT
 }
}
</code></pre>
<p>So if we want to copy 20 k rows, this variant causes: 40 k <code>COMMIT</code>s (<code>fsync</code>) and 42 k network round trips!</p>
<h2>Variant 2: <code>START TRANSACTION</code> and <code>INSERT</code> ad <code>DELETE</code><a class="anchor-link" id="variant-2-start-transaction-and-insert-ad-delete"></a></h2>
<p>This variant is used by more experienced database developers. Here is the corresponding pseudocode:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 foreach ( row ) {
 INSERT INTO final;
 DELETE FROM pending WHERE id = row[id];
 }
 COMMIT;
}
</code></pre>
<p>If we want to copy 20 k rows in this example, this variant only causes 2 k <code>COMMIT</code>s (<code>fsync</code>)! So 20 times less! But 46 k network round trips (10% more).</p>
<h2>Variant 3: <code>START TRANSACTION</code> and optimised <code>INSERT</code> and <code>DELETE</code><a class="anchor-link" id="variant-3-start-transaction-and-optimised-insert-and-delete"></a></h2>
<p>This variant is a little more demanding in terms of programming. It is used if you want to get a little closer to the limits of what is possible. Here is the pseudo code:</p>
<pre><code>// 20k rows
for (i = 1; i &lt;= 2000; i++) {

 SELECT * FROM pending LIMIT 10;
 START TRANSACTION;
 INSERT INTO final (), (), (), (), (), (), (), (), (), ();
 DELETE FROM pending WHERE id = IN (...);
 COMMIT;
}
</code></pre>
<p>And this 3rd variant also only causes 2 k <code>COMMIT</code>&rsquo;s (<code>fsync</code>) with 20 k rows, but saves the loop via the <code>INSERT</code> and <code>DELETE</code> statements in the database. So on the one hand we save network round trips (only 10 k) and CPU cycles on the database (which are difficult to scale) for parsing the queries.</p>
<h2>Test set-up<a class="anchor-link" id="test-set-up"></a></h2>
<p>To test the whole thing, we have prepared a small script: <a href="https://www.fromdual.com/code-examples/load_data.php.txt">load_data.php</a></p>
<h3>Preparation and execution with MariaDB/MySQL<a class="anchor-link" id="preparation-and-execution-with-mariadb-mysql"></a></h3>
<p>You can execute these tests yourself with the following commands:</p>
<pre><code>SQL&gt; CREATE DATABASE test;
SQL&gt; CREATE USER 'app'@'127.0.0.1' IDENTIFIED BY 'secret';
SQL&gt; GRANT ALL ON *.* TO 'app'@'127.0.0.1';

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=mysql --database=test --host=127.0.0.1 --port=3306 --user=app --password=secret --clean-up
</code></pre>
<p>Everyone can work out the measured values themselves with the corresponding test script.</p>
<h3>Preparation and execution with PostgreSQL<a class="anchor-link" id="preparation-and-execution-with-postgresql"></a></h3>
<p>You can execute these tests yourself with the following commands:</p>
<pre><code>postgres# CREATE DATABASE test;
postgres# CREATE USER app PASSWORD 'secret';
postgres# GRANT ALL ON DATABASE test TO app;
postgres# GRANT ALL ON SCHEMA public TO app;

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --prepare

$ for i in $(seq 5) ; do
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=1
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=2
 ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --run --variant=3
done

$ ./load_data.php --database-type=postgresql --database=test --host=127.0.0.1 --port=5432 --user=app --password=secret --clean-up
</code></pre>
<p>Anyone can work out the measured values themselves with the corresponding test script.</p>
<h2>Results<a class="anchor-link" id="results"></a></h2>
<p>To avoid unnecessary discussions, we have &lsquo;only&rsquo; listed the relative performance (runtime) here, as MarkC has been doing recently. We are happy to provide our measured values bilaterally. However, they can be easily reproduced with the test script itself.</p>
<p>Less is better:</p>
<table>
<thead>
<tr>
<th></th>
<th>Variant 1</th>
<th>Variant 2</th>
<th>Variant 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>MariaDB 11.8, avg(5)</td>
<td>100.0%</td>
<td>7.5%</td>
<td>6.2%</td>
</tr>
<tr>
<td>PostgreSQL 19dev, avg(5)</td>
<td>100.0%</td>
<td>11.2%</td>
<td>7.0%</td>
</tr>
</tbody>
</table>
<p><strong>Attention</strong>: The values of MariaDB/MySQL and PostgreSQL can NOT be compared directly!</p>
<p></p>
<p>And here is the graphical evaluation:</p>
<p><img decoding="async" src="https://www.fromdual.com/images/mariadb-data-load.png" alt="mariadb"></p>
<p>&nbsp;</p>
<p><img decoding="async" src="https://www.fromdual.com/images/postgresql-data-load.png" alt="postgresql"></p>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>With faster discs, the difference between 1 and 2/3 would probably not have been quite so significant. Customer tests have shown a difference of &lsquo;only&rsquo; about a factor of 5 (instead of a factor of 9 to 16).</p>
<p>There are certainly other ways in which this loading process can be optimised. Here are a few that come to mind:</p>
<ul>
<li><code>INSERT INTO ... SELECT * FROM</code></li>
<li><code>LOAD DATA INFILE</code>/<code>COPY</code>, if possible</li>
<li>Prepared statements</li>
<li>Server side Stored Language (SQL/PSM, PL/pgSQL, &hellip;) &#128577;</li>
<li>PDO-Fetch of the results?</li>
<li>etc.</li>
</ul>
<p>Maybe I should look for a profiler (<a href="https://xdebug.org/" target="_blank">Xdebug</a> or <a href="https://www.php.net/manual/en/book.xhprof.php" target="_blank">xhprof</a>)?</p>
<h2>Further contributions<a class="anchor-link" id="further-contributions"></a></h2>
<ul>
<li><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a></li>
<li><a href="https://www.fromdual.com/blog/mariadb-prepared-statements-transactions-and-multi-row-inserts/">MariaDB Prepared Statements, Transactions and Multi-Row Inserts</a></li>
<li><a href="https://www.fromdual.com/blog/how-good-is-mysql-insert-trigger-performance/">How good is MySQL INSERT TRIGGER performance</a></li>
</ul>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/load-data-quick-into-the-database/">What is the quickest way to load data into the database?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB has broken the concept of dynamically configurable buffer pools!</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/" />
      <id>https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/</id>
      <updated>2026-02-09T18:14:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Problem description<br />
MySQL introduced the dynamically configurable InnoDB buffer pool with 5.7.5 in September 2014 (here and here):</p>
<p>The innodb_buffer_pool_size configuration option can be set dynamically using a SET statement, allowing you to resize the buffer pool without restarting the server. For example:<br />
mysql &#62; SET GLOBAL innodb_buffer_pool_size=402653184;</p>
<p>MariaDB 10.2.2 adopted this feature in September 2016 (source):</p>
<p>InnoDB was merged from MySQL-5.7.14 (XtraDB is disabled in MariaDB-10.2.2 pending a similar merge)</p>
<p>The problematic thing is, on the one hand, that this feature now no longer works as it did before and no longer works as expected. On the other hand, they changed the behaviour in spring 2025 within a major release series (LTS), which in my opinion is an absolute no-go (source):</p>
<p>From MariaDB 10.11.12 / 11.4.6 / 11.8.2, there are significant changes to the InnoDB buffer pool behavior.</p>
<p>And what is more, the description of this is quite poor (source):</p>
<p>decreasing innodb_buffer_pool_size at runtime does not release memory (MDEV-32339)<br />
reorganise innodb buffer pool (and remove buffer pool chunks) (MDEV-29445)<br />
The Linux memory pressure interface, which could previously not be disabled and could cause performance anomalies, was rewritten and is disabled by default. (MDEV-34863)<br />
Server crashes when resizing default innodb buffer pool after setting innodb-buffer-pool-chunk-size to 1M (MDEV-34677)</p>
<p>In the corresponding worklog (MDEV-36197) MarkoM also describes a different behaviour:</p>
<p>innodb_buffer_pool_size_auto_max (my proposal for this task) would set the maximum for the automation (default: 0 to disable the logic).</p>
<p>which would have made much more sense in my opinion.<br />
How did it work before?<br />
How did it work in the past with MariaDB and still today with MySQL:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size\';<br />
+-------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+-------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 2097152 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
+-------------------------------+-----------+</p>
<p>SQL &#62; SET GLOBAL innodb_buffer_pool_size = @@innodb_buffer_pool_chunk_size * 128;</p>
<p>SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size\';<br />
+-------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+-------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 2097152 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 268435456 &#124;<br />
+-------------------------------+-----------+<br />
Thus everything OK. Works as expected and as usual.<br />
What does MariaDB do today?<br />
What happens today with MariaDB:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size%\';<br />
+----------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+----------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 0 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_auto_min &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_max &#124; 134217728 &#124;<br />
+----------------------------------+-----------+</p>
<p>SQL &#62; SET GLOBAL innodb_buffer_pool_size = 256*1024*1024;<br />
Query OK, 0 rows affected, 1 warning (0.000 sec)</p>
<p>SQL &#62; show warnings;<br />
+---------+------+----------------------------------------------------------------+<br />
&#124; Level &#124; Code &#124; Message &#124;<br />
+---------+------+----------------------------------------------------------------+<br />
&#124; Warning &#124; 1292 &#124; Truncated incorrect innodb_buffer_pool_size value: \'268435456\' &#124;<br />
+---------+------+----------------------------------------------------------------+</p>
<p>SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size%\';<br />
+----------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+----------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 0 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_auto_min &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_max &#124; 134217728 &#124;<br />
+----------------------------------+-----------+<br />
So nothing at all! Not even an error, just a warning. And if I do not look closely, I do not even realise that something did not work.<br />
The MariaDB error log says:<br />
[Note] InnoDB: Memory pressure event disregarded; innodb_buffer_pool_size=128m, innodb_buffer_pool_size_auto_min=128m<br />
If you then do some searching, you find out that something has changed here: Buffer Pool Changes and try intuitively:<br />
SQL &#62; SET GLOBAL innodb_buffer_pool_size_max = 256*1024*1024;<br />
ERROR 1238 (HY000): Variable \'innodb_buffer_pool_size_max\' is a read only variable<br />
But that does not work either.<br />
This means you have to restart the database! And possibly at the very moment when you do not actually want to restart the database and need this feature…<br />
The documentation also states ((source):</p>
<p>Default Value: specified by the initial value of innodb_buffer_pool_size, rounded up to the block size of that variable. See the section about buffer pool changes in MariaDB 10.11.12, 11.4.6, and 11.8.2.</p>
<p>and (source):</p>
<p>If innodb_buffer_pool_size_max is 0 or not specified, it defaults to the innodb_buffer_pool_size value.</p>
<p>This means that I have to think again beforehand about how big I should make innodb_buffer_pool_size_max and can only correct it afterwards during operation, should I have forgotten or misjudged it.<br />
In my opinion, this is a complete step backwards from an operational point of view. This is probably another implementation for some cloud-only as a service solution (enterprise?).<br />
My suggestion is: Either, as suggested in the MDEV: 0 should switch off this feature and the behaviour should be as before or the default value should be set to 75% of the RAM size, as innodb_dedicated_server does with MySQL.<br />
I had the audacity to open a bug here: New InnoDB Buffer Pool autosize feature not so optimal implemented.<br />
FedericoR has thankfully recommended the following link: Issues with new buffer pool configuration in MariaDB Minors (10.11.12/13/14, 11.4.6/7/8, 11.8.2/3). I do not seem to be the only one who was annoyed by this change…<br />
How does PostgreSQL do this?<br />
PostgreSQL is currently not (yet) able to change shared_buffers dynamically. The default is usually 128M. The rule of thumb here, similar to MyISAM, is 25 - 40% of RAM. The lack of this feature is probably not as serious with PostgreSQL, however, as PostgreSQL relies heavily on the file system cache, similar to MyISAM.<br />
Source: Resource Consumption<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/">MariaDB has broken the concept of dynamically configurable buffer pools!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Problem description<a class="anchor-link" id="problem-description"></a></h2>
<p>MySQL introduced the dynamically configurable InnoDB buffer pool with 5.7.5 in September 2014 (<a href="https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-5.html" target="_blank">here</a> and <a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool-resize.html#innodb-buffer-pool-online-resize" target="_blank">here</a>):</p>
<blockquote>
<p>The innodb_buffer_pool_size configuration option can be set dynamically using a SET statement, allowing you to resize the buffer pool without restarting the server. For example:</p>
<p>mysql&gt; SET GLOBAL innodb_buffer_pool_size=402653184;</p>
</blockquote>
<p>MariaDB 10.2.2 adopted this feature in September 2016 (<a href="https://mariadb.com/docs/release-notes/community-server/old-releases/10.2/10.2.2#notable-changes" target="_blank">source</a>):</p>
<blockquote>
<p>InnoDB was merged from MySQL-5.7.14 (XtraDB is disabled in MariaDB-10.2.2 pending a similar merge)</p>
</blockquote>
<p>The problematic thing is, on the one hand, that this feature now no longer works as it did before and no longer works as expected. On the other hand, they changed the behaviour in spring 2025 within a major release series (LTS), which in my opinion is an absolute no-go (<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank">source</a>):</p>
<blockquote>
<p>From MariaDB 10.11.12 / 11.4.6 / 11.8.2, there are significant changes to the InnoDB buffer pool behavior.</p>
</blockquote>
<p>And what is more, the description of this is quite poor (<a href="https://mariadb.com/docs/release-notes/community-server/10.11/10.11.12#innodb" target="_blank">source</a>):</p>
<blockquote>
<ul>
<li>decreasing innodb_buffer_pool_size at runtime does not release memory (MDEV-32339)</li>
<li>reorganise innodb buffer pool (and remove buffer pool chunks) (MDEV-29445)</li>
<li>The Linux memory pressure interface, which could previously not be disabled and could cause performance anomalies, was rewritten and is disabled by default. (MDEV-34863)</li>
<li>Server crashes when resizing default innodb buffer pool after setting innodb-buffer-pool-chunk-size to 1M (MDEV-34677)</li>
</ul>
</blockquote>
<p>In the corresponding worklog (<a href="https://jira.mariadb.org/browse/MDEV-36197" target="_blank">MDEV-36197</a>) MarkoM also describes a different behaviour:</p>
<blockquote>
<p>innodb_buffer_pool_size_auto_max (my proposal for this task) would set the maximum for the automation (default: 0 to disable the logic).</p>
</blockquote>
<p>which would have made much more sense in my opinion.</p>
<h2>How did it work before?<a class="anchor-link" id="how-did-it-work-before"></a></h2>
<p>How did it work in the past with MariaDB and still today with MySQL:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size';
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 2097152 |
| innodb_buffer_pool_size | 134217728 |
+-------------------------------+-----------+

SQL&gt; SET GLOBAL innodb_buffer_pool_size = @@innodb_buffer_pool_chunk_size * 128;

SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size';
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 2097152 |
| innodb_buffer_pool_size | 268435456 |
+-------------------------------+-----------+
</code></pre>
<p>Thus everything OK. Works as expected and as usual.</p>
<h2>What does MariaDB do today?<a class="anchor-link" id="what-does-mariadb-do-today"></a></h2>
<p>What happens today with MariaDB:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size%';
+----------------------------------+-----------+
| Variable_name | Value |
+----------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 0 |
| innodb_buffer_pool_size | 134217728 |
| innodb_buffer_pool_size_auto_min | 134217728 |
| innodb_buffer_pool_size_max | 134217728 |
+----------------------------------+-----------+

SQL&gt; SET GLOBAL innodb_buffer_pool_size = 256*1024*1024;
Query OK, 0 rows affected, 1 warning (0.000 sec)

SQL&gt; show warnings;
+---------+------+----------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------+
| Warning | 1292 | Truncated incorrect innodb_buffer_pool_size value: '268435456' |
+---------+------+----------------------------------------------------------------+

SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size%';
+----------------------------------+-----------+
| Variable_name | Value |
+----------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 0 |
| innodb_buffer_pool_size | 134217728 |
| innodb_buffer_pool_size_auto_min | 134217728 |
| innodb_buffer_pool_size_max | 134217728 |
+----------------------------------+-----------+
</code></pre>
<p>So nothing at all! Not even an error, just a warning. And if I do not look closely, I do not even realise that something did not work.</p>
<p>The MariaDB error log says:</p>
<pre><code>[Note] InnoDB: Memory pressure event disregarded; innodb_buffer_pool_size=128m, innodb_buffer_pool_size_auto_min=128m
</code></pre>
<p>If you then do some searching, you find out that something has changed here: <a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank">Buffer Pool Changes</a> and try intuitively:</p>
<pre><code>SQL&gt; SET GLOBAL innodb_buffer_pool_size_max = 256*1024*1024;
ERROR 1238 (HY000): Variable 'innodb_buffer_pool_size_max' is a read only variable
</code></pre>
<p>But that does not work either.</p>
<p>This means you have to restart the database! And possibly at the very moment when you do not actually want to restart the database and need this feature&hellip;</p>
<p>The documentation also states ((<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-system-variables#innodb_buffer_pool_size_max" target="_blank" title="innodb_buffer_pool_size_max">source</a>):</p>
<blockquote>
<p>Default Value: specified by the initial value of innodb_buffer_pool_size, rounded up to the block size of that variable. See the section about buffer pool changes in MariaDB 10.11.12, 11.4.6, and 11.8.2.</p>
</blockquote>
<p>and (<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank" title="Buffer Pool Changes">source</a>):</p>
<blockquote>
<p>If innodb_buffer_pool_size_max is 0 or not specified, it defaults to the innodb_buffer_pool_size value.</p>
</blockquote>
<p>This means that I have to think again beforehand about how big I should make <code>innodb_buffer_pool_size_max</code> and can only correct it afterwards during operation, should I have forgotten or misjudged it.</p>
<p>In my opinion, this is a complete step backwards from an operational point of view. This is probably another implementation for some cloud-only as a service solution (enterprise?).</p>
<p>My suggestion is: Either, as suggested in the MDEV: 0 should switch off this feature and the behaviour should be as before or the default value should be set to 75% of the RAM size, as <code>innodb_dedicated_server</code> does with MySQL.</p>
<p>I had the audacity to open a bug here: <a href="https://jira.mariadb.org/browse/MDEV-38779" target="_blank">New InnoDB Buffer Pool autosize feature not so optimal implemented</a>.</p>
<p>FedericoR has thankfully recommended the following link: <a href="https://www.mail-archive.com/developers@lists.mariadb.org/msg00822.html" target="_blank" title="MariaDB developers mailing list">Issues with new buffer pool configuration in MariaDB Minors (10.11.12/13/14, 11.4.6/7/8, 11.8.2/3)</a>. I do not seem to be the only one who was annoyed by this change&hellip;</p>
<h2>How does PostgreSQL do this?<a class="anchor-link" id="how-does-postgresql-do-this"></a></h2>
<p>PostgreSQL is currently not (yet) able to change <code>shared_buffers</code> dynamically. The default is usually 128M. The rule of thumb here, similar to MyISAM, is 25 &ndash; 40% of RAM. The lack of this feature is probably not as serious with PostgreSQL, however, as PostgreSQL relies heavily on the file system cache, similar to MyISAM.</p>
<p>Source: <a href="https://www.postgresql.org/docs/current/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY" target="_blank">Resource Consumption</a></p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/">MariaDB has broken the concept of dynamically configurable buffer pools!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB has broken the concept of dynamically configurable buffer pools!</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/" />
      <id>https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/</id>
      <updated>2026-02-09T18:14:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Problem description<br />
MySQL introduced the dynamically configurable InnoDB buffer pool with 5.7.5 in September 2014 (here and here):</p>
<p>The innodb_buffer_pool_size configuration option can be set dynamically using a SET statement, allowing you to resize the buffer pool without restarting the server. For example:<br />
mysql &#62; SET GLOBAL innodb_buffer_pool_size=402653184;</p>
<p>MariaDB 10.2.2 adopted this feature in September 2016 (source):</p>
<p>InnoDB was merged from MySQL-5.7.14 (XtraDB is disabled in MariaDB-10.2.2 pending a similar merge)</p>
<p>The problematic thing is, on the one hand, that this feature now no longer works as it did before and no longer works as expected. On the other hand, they changed the behaviour in spring 2025 within a major release series (LTS), which in my opinion is an absolute no-go (source):</p>
<p>From MariaDB 10.11.12 / 11.4.6 / 11.8.2, there are significant changes to the InnoDB buffer pool behavior.</p>
<p>And what is more, the description of this is quite poor (source):</p>
<p>decreasing innodb_buffer_pool_size at runtime does not release memory (MDEV-32339)<br />
reorganise innodb buffer pool (and remove buffer pool chunks) (MDEV-29445)<br />
The Linux memory pressure interface, which could previously not be disabled and could cause performance anomalies, was rewritten and is disabled by default. (MDEV-34863)<br />
Server crashes when resizing default innodb buffer pool after setting innodb-buffer-pool-chunk-size to 1M (MDEV-34677)</p>
<p>In the corresponding worklog (MDEV-36197) MarkoM also describes a different behaviour:</p>
<p>innodb_buffer_pool_size_auto_max (my proposal for this task) would set the maximum for the automation (default: 0 to disable the logic).</p>
<p>which would have made much more sense in my opinion.<br />
How did it work before?<br />
How did it work in the past with MariaDB and still today with MySQL:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size\';<br />
+-------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+-------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 2097152 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
+-------------------------------+-----------+</p>
<p>SQL &#62; SET GLOBAL innodb_buffer_pool_size = @@innodb_buffer_pool_chunk_size * 128;</p>
<p>SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size\';<br />
+-------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+-------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 2097152 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 268435456 &#124;<br />
+-------------------------------+-----------+<br />
Thus everything OK. Works as expected and as usual.<br />
What does MariaDB do today?<br />
What happens today with MariaDB:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size%\';<br />
+----------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+----------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 0 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_auto_min &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_max &#124; 134217728 &#124;<br />
+----------------------------------+-----------+</p>
<p>SQL &#62; SET GLOBAL innodb_buffer_pool_size = 256*1024*1024;<br />
Query OK, 0 rows affected, 1 warning (0.000 sec)</p>
<p>SQL &#62; show warnings;<br />
+---------+------+----------------------------------------------------------------+<br />
&#124; Level &#124; Code &#124; Message &#124;<br />
+---------+------+----------------------------------------------------------------+<br />
&#124; Warning &#124; 1292 &#124; Truncated incorrect innodb_buffer_pool_size value: \'268435456\' &#124;<br />
+---------+------+----------------------------------------------------------------+</p>
<p>SQL &#62; SHOW GLOBAL VARIABLES LIKE \'innodb_buffer_pool%size%\';<br />
+----------------------------------+-----------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+----------------------------------+-----------+<br />
&#124; innodb_buffer_pool_chunk_size &#124; 0 &#124;<br />
&#124; innodb_buffer_pool_size &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_auto_min &#124; 134217728 &#124;<br />
&#124; innodb_buffer_pool_size_max &#124; 134217728 &#124;<br />
+----------------------------------+-----------+<br />
So nothing at all! Not even an error, just a warning. And if I do not look closely, I do not even realise that something did not work.<br />
The MariaDB error log says:<br />
[Note] InnoDB: Memory pressure event disregarded; innodb_buffer_pool_size=128m, innodb_buffer_pool_size_auto_min=128m<br />
If you then do some searching, you find out that something has changed here: Buffer Pool Changes and try intuitively:<br />
SQL &#62; SET GLOBAL innodb_buffer_pool_size_max = 256*1024*1024;<br />
ERROR 1238 (HY000): Variable \'innodb_buffer_pool_size_max\' is a read only variable<br />
But that does not work either.<br />
This means you have to restart the database! And possibly at the very moment when you do not actually want to restart the database and need this feature…<br />
The documentation also states ((source):</p>
<p>Default Value: specified by the initial value of innodb_buffer_pool_size, rounded up to the block size of that variable. See the section about buffer pool changes in MariaDB 10.11.12, 11.4.6, and 11.8.2.</p>
<p>and (source):</p>
<p>If innodb_buffer_pool_size_max is 0 or not specified, it defaults to the innodb_buffer_pool_size value.</p>
<p>This means that I have to think again beforehand about how big I should make innodb_buffer_pool_size_max and can only correct it afterwards during operation, should I have forgotten or misjudged it.<br />
In my opinion, this is a complete step backwards from an operational point of view. This is probably another implementation for some cloud-only as a service solution (enterprise?).<br />
My suggestion is: Either, as suggested in the MDEV: 0 should switch off this feature and the behaviour should be as before or the default value should be set to 75% of the RAM size, as innodb_dedicated_server does with MySQL.<br />
I had the audacity to open a bug here: New InnoDB Buffer Pool autosize feature not so optimal implemented.<br />
FedericoR has thankfully recommended the following link: Issues with new buffer pool configuration in MariaDB Minors (10.11.12/13/14, 11.4.6/7/8, 11.8.2/3). I do not seem to be the only one who was annoyed by this change…<br />
How does PostgreSQL do this?<br />
PostgreSQL is currently not (yet) able to change shared_buffers dynamically. The default is usually 128M. The rule of thumb here, similar to MyISAM, is 25 - 40% of RAM. The lack of this feature is probably not as serious with PostgreSQL, however, as PostgreSQL relies heavily on the file system cache, similar to MyISAM.<br />
Source: Resource Consumption<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/">MariaDB has broken the concept of dynamically configurable buffer pools!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Problem description<a class="anchor-link" id="problem-description"></a></h2>
<p>MySQL introduced the dynamically configurable InnoDB buffer pool with 5.7.5 in September 2014 (<a href="https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-5.html" target="_blank">here</a> and <a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool-resize.html#innodb-buffer-pool-online-resize" target="_blank">here</a>):</p>
<blockquote>
<p>The innodb_buffer_pool_size configuration option can be set dynamically using a SET statement, allowing you to resize the buffer pool without restarting the server. For example:</p>
<p>mysql&gt; SET GLOBAL innodb_buffer_pool_size=402653184;</p>
</blockquote>
<p>MariaDB 10.2.2 adopted this feature in September 2016 (<a href="https://mariadb.com/docs/release-notes/community-server/old-releases/10.2/10.2.2#notable-changes" target="_blank">source</a>):</p>
<blockquote>
<p>InnoDB was merged from MySQL-5.7.14 (XtraDB is disabled in MariaDB-10.2.2 pending a similar merge)</p>
</blockquote>
<p>The problematic thing is, on the one hand, that this feature now no longer works as it did before and no longer works as expected. On the other hand, they changed the behaviour in spring 2025 within a major release series (LTS), which in my opinion is an absolute no-go (<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank">source</a>):</p>
<blockquote>
<p>From MariaDB 10.11.12 / 11.4.6 / 11.8.2, there are significant changes to the InnoDB buffer pool behavior.</p>
</blockquote>
<p>And what is more, the description of this is quite poor (<a href="https://mariadb.com/docs/release-notes/community-server/10.11/10.11.12#innodb" target="_blank">source</a>):</p>
<blockquote>
<ul>
<li>decreasing innodb_buffer_pool_size at runtime does not release memory (MDEV-32339)</li>
<li>reorganise innodb buffer pool (and remove buffer pool chunks) (MDEV-29445)</li>
<li>The Linux memory pressure interface, which could previously not be disabled and could cause performance anomalies, was rewritten and is disabled by default. (MDEV-34863)</li>
<li>Server crashes when resizing default innodb buffer pool after setting innodb-buffer-pool-chunk-size to 1M (MDEV-34677)</li>
</ul>
</blockquote>
<p>In the corresponding worklog (<a href="https://jira.mariadb.org/browse/MDEV-36197" target="_blank">MDEV-36197</a>) MarkoM also describes a different behaviour:</p>
<blockquote>
<p>innodb_buffer_pool_size_auto_max (my proposal for this task) would set the maximum for the automation (default: 0 to disable the logic).</p>
</blockquote>
<p>which would have made much more sense in my opinion.</p>
<h2>How did it work before?<a class="anchor-link" id="how-did-it-work-before"></a></h2>
<p>How did it work in the past with MariaDB and still today with MySQL:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size';
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 2097152 |
| innodb_buffer_pool_size | 134217728 |
+-------------------------------+-----------+

SQL&gt; SET GLOBAL innodb_buffer_pool_size = @@innodb_buffer_pool_chunk_size * 128;

SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size';
+-------------------------------+-----------+
| Variable_name | Value |
+-------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 2097152 |
| innodb_buffer_pool_size | 268435456 |
+-------------------------------+-----------+
</code></pre>
<p>Thus everything OK. Works as expected and as usual.</p>
<h2>What does MariaDB do today?<a class="anchor-link" id="what-does-mariadb-do-today"></a></h2>
<p>What happens today with MariaDB:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size%';
+----------------------------------+-----------+
| Variable_name | Value |
+----------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 0 |
| innodb_buffer_pool_size | 134217728 |
| innodb_buffer_pool_size_auto_min | 134217728 |
| innodb_buffer_pool_size_max | 134217728 |
+----------------------------------+-----------+

SQL&gt; SET GLOBAL innodb_buffer_pool_size = 256*1024*1024;
Query OK, 0 rows affected, 1 warning (0.000 sec)

SQL&gt; show warnings;
+---------+------+----------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------+
| Warning | 1292 | Truncated incorrect innodb_buffer_pool_size value: '268435456' |
+---------+------+----------------------------------------------------------------+

SQL&gt; SHOW GLOBAL VARIABLES LIKE 'innodb_buffer_pool%size%';
+----------------------------------+-----------+
| Variable_name | Value |
+----------------------------------+-----------+
| innodb_buffer_pool_chunk_size | 0 |
| innodb_buffer_pool_size | 134217728 |
| innodb_buffer_pool_size_auto_min | 134217728 |
| innodb_buffer_pool_size_max | 134217728 |
+----------------------------------+-----------+
</code></pre>
<p>So nothing at all! Not even an error, just a warning. And if I do not look closely, I do not even realise that something did not work.</p>
<p>The MariaDB error log says:</p>
<pre><code>[Note] InnoDB: Memory pressure event disregarded; innodb_buffer_pool_size=128m, innodb_buffer_pool_size_auto_min=128m
</code></pre>
<p>If you then do some searching, you find out that something has changed here: <a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank">Buffer Pool Changes</a> and try intuitively:</p>
<pre><code>SQL&gt; SET GLOBAL innodb_buffer_pool_size_max = 256*1024*1024;
ERROR 1238 (HY000): Variable 'innodb_buffer_pool_size_max' is a read only variable
</code></pre>
<p>But that does not work either.</p>
<p>This means you have to restart the database! And possibly at the very moment when you do not actually want to restart the database and need this feature&hellip;</p>
<p>The documentation also states ((<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-system-variables#innodb_buffer_pool_size_max" target="_blank" title="innodb_buffer_pool_size_max">source</a>):</p>
<blockquote>
<p>Default Value: specified by the initial value of innodb_buffer_pool_size, rounded up to the block size of that variable. See the section about buffer pool changes in MariaDB 10.11.12, 11.4.6, and 11.8.2.</p>
</blockquote>
<p>and (<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-buffer-pool#buffer-pool-changes" target="_blank" title="Buffer Pool Changes">source</a>):</p>
<blockquote>
<p>If innodb_buffer_pool_size_max is 0 or not specified, it defaults to the innodb_buffer_pool_size value.</p>
</blockquote>
<p>This means that I have to think again beforehand about how big I should make <code>innodb_buffer_pool_size_max</code> and can only correct it afterwards during operation, should I have forgotten or misjudged it.</p>
<p>In my opinion, this is a complete step backwards from an operational point of view. This is probably another implementation for some cloud-only as a service solution (enterprise?).</p>
<p>My suggestion is: Either, as suggested in the MDEV: 0 should switch off this feature and the behaviour should be as before or the default value should be set to 75% of the RAM size, as <code>innodb_dedicated_server</code> does with MySQL.</p>
<p>I had the audacity to open a bug here: <a href="https://jira.mariadb.org/browse/MDEV-38779" target="_blank">New InnoDB Buffer Pool autosize feature not so optimal implemented</a>.</p>
<p>FedericoR has thankfully recommended the following link: <a href="https://www.mail-archive.com/developers@lists.mariadb.org/msg00822.html" target="_blank" title="MariaDB developers mailing list">Issues with new buffer pool configuration in MariaDB Minors (10.11.12/13/14, 11.4.6/7/8, 11.8.2/3)</a>. I do not seem to be the only one who was annoyed by this change&hellip;</p>
<h2>How does PostgreSQL do this?<a class="anchor-link" id="how-does-postgresql-do-this"></a></h2>
<p>PostgreSQL is currently not (yet) able to change <code>shared_buffers</code> dynamically. The default is usually 128M. The rule of thumb here, similar to MyISAM, is 25 &ndash; 40% of RAM. The lack of this feature is probably not as serious with PostgreSQL, however, as PostgreSQL relies heavily on the file system cache, similar to MyISAM.</p>
<p>Source: <a href="https://www.postgresql.org/docs/current/runtime-config-resource.html#RUNTIME-CONFIG-RESOURCE-MEMORY" target="_blank">Resource Consumption</a></p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/mariadb-dynamically-configurable-buffer-pool-broken/">MariaDB has broken the concept of dynamically configurable buffer pools!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Pre-FOSDEM &#038; FOSDEM 2026, Community, Databases, and Open Source</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/09/pre-fosdem-fosdem-2026-community-databases-and-open-source/" />
      <id>https://percona.community/blog/2026/02/09/pre-fosdem-fosdem-2026-community-databases-and-open-source/</id>
      <updated>2026-02-09T10:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>This is a recap of Percona at preFosdem and Fosdem!</p>
<p><a href="https://percona.community/blog/2026/02/09/pre-fosdem-fosdem-2026-community-databases-and-open-source/">Pre-FOSDEM &amp; FOSDEM 2026, Community, Databases, and Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This is a recap of Percona at preFosdem and Fosdem!</p>
<p><figure><img decoding="async" width="1920" height="1080" src="https://percona.community/blog/2026/02/fosdem-all_hu_7d06c4b9eb5e1305.webp" alt="Fosdem intro" loading="lazy"></figure>
</p>
<p>Before FOSDEM officially started, the database community gathered for MySQL Belgium Days (Pre-FOSDEM), a two-day event bringing together MySQL developers, DBAs, engineers, tool builders, and open-source enthusiasts. It was an excellent space for deep technical discussions, knowledge sharing, and reconnecting with the community, hosted by the amazing <strong>Frederic Descamps</strong>.<br>
The event featured strong participation from <strong>Percona</strong> and the wider MySQL ecosystem, with talks led by <strong>Peter Zaitsev, Marco Tusa, Fernando Laudares Camargos, Arunjith Aravindan, Vinicius Grippa, Pep Pla, and Yura Sorokin</strong>.</p>
<p><figure><img decoding="async" width="1317" height="616" src="https://percona.community/blog/2026/02/fosdem-speakers_hu_923d235a9c77b1.webp" alt="Fosdem speakers" loading="lazy"></figure>
</p>
<p>Find the recordings of the talks <a href="https://www.youtube.com/playlist?list=PL6tzEWmw-bpxe0k5Xrk09N-m6q5rGTy_l" target="_blank" rel="noopener noreferrer">here</a>.</p>
<p>Also, in Belgium same week, several other events took place, including <strong>PGDay-FOSDEM</strong>, <strong>MariaDB Day</strong>, and the <strong>MySQL Summit</strong>.</p>
<p>At <strong>PGDay</strong>, it was a pleasure to see the PostgreSQL community together; we had several participants representing us.</p>
<p><figure><img decoding="async" width="1172" height="449" src="https://percona.community/blog/2026/02/fosdem-pg_hu_f591cc8b94186875.webp" alt="Fosdem speakers" loading="lazy"></figure>
</p>
<p><strong>MariaDB Day</strong>. We had Peter Zeitsev presenting a talk titled &ldquo;What MariaDB Community can learn from PostgreSQL?&rdquo;</p>
<figure><img decoding="async" width="4032" height="2268" src="https://percona.community/blog/2026/02/fosdem-peter_hu_d39c99ad9e52489c.webp" alt="Fosdem speakers" loading="lazy"></figure>

<h2>MySQL summit<a class="anchor-link" id="mysql-summit"></a></h2>
<p>During MySQL Days in Brussels, the community gathered for an in-person MySQL Summit focused on collaboration and strengthening the MySQL ecosystem, with open discussions around its present and future driven by community involvement.</p>
<p><figure><img decoding="async" width="4032" height="2268" src="https://percona.community/blog/2026/02/fosdem-mysql-summit_hu_e6fbcbced6cd724e.webp" alt="Fosdem MySQL Summit" loading="lazy"></figure>
</p>
<h2>MySQL RockStars 2026<a class="anchor-link" id="mysql-rockstars-2026"></a></h2>
<p>The MySQL Rockstar Award is a recognition given by the MySQL Community Team at Oracle, together with previous award winners, to members of the MySQL community who have actively contributed to promoting MySQL during the past year.<br>
MySQL Legends are long-standing community members who have made a significant and lasting impact on the adoption, development, and evolution of MySQL over many years.</p>
<p>This year, the MySQL RockStars selected were:</p>
<ul>
<li>Matthias Crauwels</li>
<li>Marco Tusa</li>
<li>Umesh Shastry</li>
<li>Ronald Bradford</li>
<li>Marcelo Altmann</li>
</ul>
<p><figure><img decoding="async" width="1600" height="1200" src="https://percona.community/blog/2026/02/fosdem-rockstars_hu_8433bff1f6445f59.webp" alt="Fosdem postgressql" loading="lazy"></figure>
</p>
<p>Congratulations to all of them! You can find previous <a href="https://www.mysqlandfriends.eu/mysql-rockstars-hall-of-fame/" target="_blank" rel="noopener noreferrer">MySQL RockStars in this list</a></p>
<h2>FOSDEM 2026, Percona booth<a class="anchor-link" id="fosdem-2026-percona-booth"></a></h2>
<p>At the Percona booth, conversations focused on open-source databases and Kubernetes, including <a href="https://github.com/openeverest/openeverest" target="_blank" rel="noopener noreferrer">OpenEverest&rsquo;s</a> first-ever presence at FOSDEM. Around 40 Perconians were present, a great chance to finally meet many colleagues in person. As always, we had many visitors, and it was great to see that many already knew about Percona, while others were eager to learn more and explore what we do in the world of Open Source!</p>
<p><figure><img decoding="async" width="1090" height="473" src="https://percona.community/blog/2026/02/fosdem-booth_hu_5f5d91e5bc3909da.webp" alt="Fosdem postgressql" loading="lazy"></figure>
</p>
<h2>FOSDEM 2026, Databases DevRoom<a class="anchor-link" id="fosdem-2026-databases-devroom"></a></h2>
<p>FOSDEM 2026 officially kicked off at the ULB Solbosch Campus, bringing together thousands of open-source contributors from around the world.<br>
The Database DevRoom (UB2.252A) was packed with high-quality talks and discussions, co-led with <strong>Matthias Crauwels</strong> and <strong>Ray Paik</strong>, and</p>
<p><figure><img decoding="async" width="2048" height="1536" src="https://percona.community/blog/2026/02/fosdem-database-01_hu_e80567875d768fa8.webp" alt="Fosdem postgressql" loading="lazy"></figure>
<figure><img decoding="async" width="2048" height="1536" src="https://percona.community/blog/2026/02/fosdem-database-02_hu_9a03338ad8fd9329.webp" alt="Fosdem postgressql" loading="lazy"></figure>
</p>
<p>Find more details and some of the recordings <a href="https://fosdem.org/2026/schedule/track/databases/" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2>Celebrating 20 Years of Percona<a class="anchor-link" id="celebrating-20-years-of-percona"></a></h2>
<p>This year, <strong>Percona</strong> celebrates its 20th anniversary. Throughout FOSDEM and Pre-FOSDEM events, it was inspiring to meet long-time users who have relied on Percona&rsquo;s open-source solutions for years and shared their positive experiences.<br>
You can explore Percona&rsquo;s 20-year journey here:<br>
&#128073; <a href="https://percona20.com/" target="_blank" rel="noopener noreferrer">https://percona20.com/</a><br>
If you&rsquo;ve had a great experience with Percona, you&rsquo;re invited to share your story via the community survey.</p>
<p><figure><img decoding="async" width="1167" height="825" src="https://percona.community/blog/2026/02/fosdem-percona-20_hu_7131f2ee164644a.webp" alt="Fosdem postgressql" loading="lazy"></figure>
</p>
<p>See you at FOSDEM 2027!</p>

<p><a href="https://percona.community/blog/2026/02/09/pre-fosdem-fosdem-2026-community-databases-and-open-source/">Pre-FOSDEM &amp; FOSDEM 2026, Community, Databases, and Open Source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Wireshark now can decode MySQL X Protocol</title>
      <link rel="alternate" type="text/html" href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html" />
      <id>https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html</id>
      <updated>2026-02-08T17:40:00+02:00</updated>
      <author><name>Daniël van Eeden</name></author>
      <summary type="html"><![CDATA[<p>The new protocol dissector for X Protocol in MySQL was just merged to the master branch in Wireshark. To get it build Wireshark from the master branch or wait for the next release.This protocol is using Google Protobuf, which makes it much easier to work with than the regular MySQL protocol.See also: https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html If you like what Wireshark does, consider donating on https://wiresharkfoundation.org/donate/   </p>
<p><a href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html">Wireshark now can decode MySQL X Protocol</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The new protocol dissector for X Protocol in MySQL was just merged to the master branch in Wireshark. To get it build Wireshark from the master branch or wait for the next release.</p>
<p>This protocol is using Google Protobuf, which makes it much easier to work with than the regular MySQL protocol.</p>
<p>See also: <a href="https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html" target="_blank">https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html</a>&nbsp;</p>
<p>If you like what Wireshark does, consider donating on <a href="https://wiresharkfoundation.org/donate/" target="_blank">https://wiresharkfoundation.org/donate/&nbsp;</a></p>
<p>&nbsp;</p>
<p>&nbsp;<img loading="lazy" decoding="async" alt="" height="499" src="https://blogger.googleusercontent.com/img/a/AVvXsEh6-KGp7Fn5XAAplfFATldBEeh6xJo01teGG5MqeB74FzBBgsWAsZDhg_J0oLZeZP64nYyTEbbw4W0cWyiUBrw4X07rrETwAdCQqSsopYGZYn2TjZBcC5FKxu59S6jbW2R-X7ft649YqVLJvLg4HV_-UnqgvluhtXkf2-4efjPtB1B6mhsUF1_3vcEX_Lah=w640-h499" width="640"></p>

<p><a href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html">Wireshark now can decode MySQL X Protocol</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Wireshark now can decode MySQL X Protocol</title>
      <link rel="alternate" type="text/html" href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html" />
      <id>https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html</id>
      <updated>2026-02-08T17:40:00+02:00</updated>
      <author><name>Daniël van Eeden</name></author>
      <summary type="html"><![CDATA[<p>The new protocol dissector for X Protocol in MySQL was just merged to the master branch in Wireshark. To get it build Wireshark from the master branch or wait for the next release.This protocol is using Google Protobuf, which makes it much easier to work with than the regular MySQL protocol.See also: https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html If you like what Wireshark does, consider donating on https://wiresharkfoundation.org/donate/   </p>
<p><a href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html">Wireshark now can decode MySQL X Protocol</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The new protocol dissector for X Protocol in MySQL was just merged to the master branch in Wireshark. To get it build Wireshark from the master branch or wait for the next release.</p>
<p>This protocol is using Google Protobuf, which makes it much easier to work with than the regular MySQL protocol.</p>
<p>See also: <a href="https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html" target="_blank">https://dev.mysql.com/doc/dev/mysql-server/latest/page_mysqlx_protocol.html</a>&nbsp;</p>
<p>If you like what Wireshark does, consider donating on <a href="https://wiresharkfoundation.org/donate/" target="_blank">https://wiresharkfoundation.org/donate/&nbsp;</a></p>
<p>&nbsp;</p>
<p>&nbsp;<img loading="lazy" decoding="async" alt="" height="499" src="https://blogger.googleusercontent.com/img/a/AVvXsEh6-KGp7Fn5XAAplfFATldBEeh6xJo01teGG5MqeB74FzBBgsWAsZDhg_J0oLZeZP64nYyTEbbw4W0cWyiUBrw4X07rrETwAdCQqSsopYGZYn2TjZBcC5FKxu59S6jbW2R-X7ft649YqVLJvLg4HV_-UnqgvluhtXkf2-4efjPtB1B6mhsUF1_3vcEX_Lah=w640-h499" width="640"></p>

<p><a href="https://databaseblog.myname.nl/2026/02/wireshark-now-can-decode-mysql-x.html">Wireshark now can decode MySQL X Protocol</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>How much space does NULL need?</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/how-much-space-does-null-need/" />
      <id>https://www.fromdual.com/blog/how-much-space-does-null-need/</id>
      <updated>2026-02-08T15:15:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The last time I consulted a customer, he came up to me beaming with joy and said that he had taken my advice and changed all the primary key columns from BIGINT (8 bytes) to INT (4 bytes) and that had made a big difference! His MySQL 8.4 database is now 750 Gbyte smaller (from 5.5 Tbyte). Nice!<br />
And yes, I know that contradicts the recommendations of some of my PostgreSQL colleagues (here and here). In the MySQL world, more emphasis is placed on such things (source):</p>
<p>Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables</p>
<p>Also, InnoDB works a wee bit differently (index clustered table and primary key in all secondary keys) than PostgreSQL (heap table, indices with row pointer (ctid)).<br />
But that’s not really the issue. Immediately afterwards, he asked me whether the deletion of columns of type DOUBLE (8 bytes, in PostgreSQL-speak DOUBLE PRECISION) would also save space or whether he should rather drop the columns straight away. My first reflex response to DOUBLE was: NULL is good, followed by OPTIMIZE TABLE (VACUUM FULL in PostgreSQL parlance). But the second thought was, DOUBLE is a data type of fixed length, does NULL also apply there or only for data types with variable length? Caution is the mother of the porcelain box! Love to consult the manual first…<br />
And there it says (source):</p>
<p>Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.</p>
<p>and (source):</p>
<p>The variable-length part of the record header contains a bit vector for indicating NULL columns. … Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.</p>
<p>Experiment with MariaDB/MySQL<br />
Test setup<br />
Somehow the description is a bit too complicated for me. Perhaps a small sketch would help? So let’s give it a try:<br />
SQL &#62; -- DROP TABLE IF EXISTS tracking;</p>
<p>SQL &#62; CREATE TABLE tracking (<br />
 id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT<br />
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE<br />
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE<br />
);</p>
<p>SQL &#62; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;<br />
SQL &#62; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;<br />
... bis 16 M rows<br />
The table is approx. 1.8 Gbyte in size for both MariaDB and MySQL with 16 M rows. Since this information is only given very imprecisely in INFORMATION_SCHEMA, let’s take a look at the file system:<br />
MariaDB 11.8:<br />
SQL &#62; system ls -l tracking.ibd<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd<br />
MySQL 8.4:<br />
SQL &#62; system ls -l tracking.ibd<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd<br />
Defragment the table<br />
Then we ‘defragment’ the table with the OPTIMIZE TABLE command:<br />
SQL &#62; OPTIMIZE TABLE tracking;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
&#124; Table &#124; Op &#124; Msg_type &#124; Msg_text &#124;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
&#124; test.tracking &#124; optimize &#124; note &#124; Table does not support optimize, doing recreate + analyze instead &#124;<br />
&#124; test.tracking &#124; optimize &#124; status &#124; OK &#124;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
Attention: The table is copied once! It therefore needs twice the amount of disc space for a short time! This can be observed while the OPTIMIZE TABLE command is running:<br />
MariaDB:<br />
$ watch -d -n 1 \'ls -l trac* #*\'<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 \'#sql-alter-d57-8c.frm\'<br />
-rw-rw---- 1 mysql mysql 968884224 Feb 7 10:39 \'#sql-alter-d57-8c.ibd\'<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd<br />
MySQL:<br />
$ watch -d -n 1 \'ls -l trac* #*\'<br />
-rw-r----- 1 mysql mysql 369098752 Feb 7 10:40 #sql-ib1594-4164062678.ibd<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd<br />
The result is amazing! With MariaDB, the table has remained somewhat the same size:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:39 tracking.ibd<br />
With MySQL, on the other hand, the table has actually grown after the ‘defragmentation’, namely by approx. 14%:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:41 tracking.ibd<br />
If we execute the OPTIMIZE TABLE command again, the size remains constant for both MariaDB and MySQL:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:46 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:48 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:48 tracking.ibd<br />
Attempt 1: NULL out<br />
Now we NULL out the values:<br />
SQL &#62; UPDATE tracking<br />
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL<br />
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL<br />
;<br />
After this step, the sizes of the files have even grown slightly:<br />
MariaDB (+1.3%):<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:49 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1937768448 Feb 7 11:04 tracking.ibd<br />
MySQL (+0.2%):<br />
-rw-r----- 1 mysql mysql 2202009600 Feb 7 11:04 tracking.ibd<br />
We then defragment the table again with the OPTIMIZE TABLE command. The tables shrink as expected.<br />
MariaDB (to 23%):<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:09 tracking.frm<br />
-rw-rw---- 1 mysql mysql 448790528 Feb 7 11:10 tracking.ibd<br />
MySQL (to 24%):<br />
-rw-r----- 1 mysql mysql 520093696 Feb 7 11:10 tracking.ibd<br />
OPTIMIZE TABLE again does NOT change the file size any more…<br />
Attempt 2: Deleting the columns<br />
Now we try the whole thing again with the DROP COLUMN command. The starting position is again the same as described above:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:15 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 11:18 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 11:19 tracking.ibd<br />
After the OPTIMIZE TABLE command, the values look similar to the first attempt:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:20 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:21 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:21 tracking.ibd<br />
OPTIMIZE TABLE again also brings no further changes, as above:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:22 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:23 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:24 tracking.ibd<br />
And now the actual second attempt with dropping the columns:<br />
SQL &#62; ALTER TABLE tracking<br />
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4<br />
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9<br />
;<br />
The first thing we notice is that the command is INSTANTANEOUS, i.e. it does not make any changes to the data but only changes the metadata. On the one hand, this is good, as it minimises the impact on the application. On the other hand, it also means that no space is saved.<br />
So let’s get to grips with the whole thing again with the OPTIMIZE TABLE command:<br />
MariaDB (to 93%):<br />
-rw-rw---- 1 mysql mysql 925 Feb 7 11:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 415236096 Feb 7 11:29 tracking.ibd<br />
MySQL (to 92%):<br />
-rw-r----- 1 mysql mysql 478150656 Feb 7 11:28 tracking.ibd<br />
Conclusion<br />
Both, dropping the columns and the NULL out of columns save a significant amount of space. Dropping the columns saves about 7% more space than NULL them out. If it is possible from an application point of view, you should therefore drop columns that are no longer required, or if not possible, at least NULL them out.<br />
Experiment with PostgreSQL<br />
And now let’s take a look at the whole thing with PostgreSQL 19devel.<br />
Test setup<br />
The test setup is analogous to MariaDB/MySQL:<br />
postgres=# -- DROP TABLE IF EXISTS tracking;</p>
<p>postgres=# CREATE TABLE tracking (<br />
 id SERIAL PRIMARY KEY<br />
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION<br />
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION<br />
);</p>
<p>postgres=# timing</p>
<p>postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)<br />
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;<br />
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)<br />
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;<br />
... bis 16 M rows<br />
Firstly, we want to know how big the table has actually become. PostgreSQL seems to know this information very precisely:<br />
postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 1963417600 &#124; 1872 MB &#124; 376856576 &#124; 359 MB &#124; 2340274176 &#124; 2232 MB &#124; 2340798464 &#124; 2232 MB<br />
Then we want to know where these files can be found in the file system:<br />
postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();<br />
 db_oid<br />
--------<br />
 5</p>
<p>postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode<br />
 FROM pg_class WHERE relname = \'tracking\';<br />
 table_oid &#124; relname &#124; relnamespace &#124; relfilenode<br />
-----------+----------+--------------+-------------<br />
 40965 &#124; tracking &#124; 2200 &#124; 40965</p>
<p>postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid<br />
 FROM pg_index i<br />
 JOIN pg_class c ON i.indrelid = c.oid<br />
 WHERE c.relname = \'tracking\';<br />
 index_name &#124; index_oid<br />
---------------+-----------<br />
 tracking_pkey &#124; 40970</p>
<p>postgres=# SELECT pg_relation_filepath(\'tracking\');<br />
 pg_relation_filepath<br />
----------------------<br />
 base/5/40965<br />
Table and index size in the file system:<br />
$ ls -ltr 40965* 40970*<br />
-rw------- 1 mysql mysql 40960 Feb 7 18:33 40965_vm<br />
-rw------- 1 mysql mysql 499712 Feb 7 18:33 40965_fsm<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:34 40965.1<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:34 40965<br />
-rw------- 1 mysql mysql 376856576 Feb 7 18:35 40970</p>
<p>*_fsm means “free space map”<br />
*_vm means “visibility map”<br />
*.1 means 2nd segment of the object (table or index)</p>
<p>PostgreSQL seems to work with segments of 1 Gbyte by default and, unlike MariaDB/MySQL (INFORMATION_SCHEMA), knows exactly how large its files are. And the discrepancy from above (between tot_rel_siz and tab_and_idx_siz) can be explained by the fsm and vm files.<br />
The PostgreSQL equivalent of the MariaDB/MySQL OPTIMIZE TABLE is the VACUUM FULL command:<br />
postgres=# VACUUM FULL tracking;</p>
<p>$ ls -ltr<br />
-rw------- 1 mysql mysql 40960 Feb 7 18:39 40965_vm<br />
-rw------- 1 mysql mysql 499712 Feb 7 18:39 40965_fsm<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40965<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40965.1<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40975</p>
<p>...</p>
<p>-rw------- 1 mysql mysql 49152 Feb 7 18:39 2704<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2703<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2696<br />
-rw------- 1 mysql mysql 65536 Feb 7 18:39 2674<br />
-rw------- 1 mysql mysql 81920 Feb 7 18:39 2673<br />
-rw------- 1 mysql mysql 98304 Feb 7 18:39 2659<br />
-rw------- 1 mysql mysql 139264 Feb 7 18:39 2658<br />
-rw------- 1 mysql mysql 24576 Feb 7 18:39 2619_fsm<br />
-rw------- 1 mysql mysql 163840 Feb 7 18:39 2619<br />
-rw------- 1 mysql mysql 106496 Feb 7 18:39 2608<br />
-rw------- 1 mysql mysql 491520 Feb 7 18:39 1249<br />
-rw------- 1 mysql mysql 122880 Feb 7 18:39 1247<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2662<br />
-rw------- 1 mysql mysql 114688 Feb 7 18:39 1259<br />
-rw------- 1 mysql mysql 16384 Feb 7 18:39 3455<br />
-rw------- 1 mysql mysql 49152 Feb 7 18:39 2663<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1<br />
-rw------- 1 mysql mysql 376864768 Feb 7 18:39 40975<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40965<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40970<br />
The first thing you notice is that PostgreSQL touches quite a few files and the ‘free space map’ file has disappeared. In contrast to MariaDB/MySQL, the table segments have remained the same size. You can also see that the old table has ‘disappeared’ (40965, 40970) and a new one has been created (40972 and 40975). The VACUUM FULL command in PostgreSQL also creates a copy of the data, as in MariaDB/MySQL.<br />
postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 1963417600 &#124; 1872 MB &#124; 376864768 &#124; 359 MB &#124; 2340282368 &#124; 2232 MB &#124; 2340282368 &#124; 2232 MB<br />
The following query helps to understand which other files/objects have been created:<br />
postgres=# SELECT c.oid, c.relname, ns.nspname<br />
FROM pg_class AS c<br />
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace<br />
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)<br />
;<br />
 oid &#124; relname &#124; nspname<br />
-------+-----------------------------------+------------<br />
 40965 &#124; tracking &#124; public<br />
 40970 &#124; tracking_pkey &#124; public<br />
 2619 &#124; pg_statistic &#124; pg_catalog<br />
 1247 &#124; pg_type &#124; pg_catalog<br />
 2703 &#124; pg_type_oid_index &#124; pg_catalog<br />
 2704 &#124; pg_type_typname_nsp_index &#124; pg_catalog<br />
 2658 &#124; pg_attribute_relid_attnam_index &#124; pg_catalog<br />
 2659 &#124; pg_attribute_relid_attnum_index &#124; pg_catalog<br />
 2662 &#124; pg_class_oid_index &#124; pg_catalog<br />
 2663 &#124; pg_class_relname_nsp_index &#124; pg_catalog<br />
 3455 &#124; pg_class_tblspc_relfilenode_index &#124; pg_catalog<br />
 2696 &#124; pg_statistic_relid_att_inh_index &#124; pg_catalog<br />
 2673 &#124; pg_depend_depender_index &#124; pg_catalog<br />
 2674 &#124; pg_depend_reference_index &#124; pg_catalog<br />
 1249 &#124; pg_attribute &#124; pg_catalog<br />
 1259 &#124; pg_class &#124; pg_catalog<br />
 2608 &#124; pg_depend &#124; pg_catalog</p>
<p>postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname<br />
 FROM pg_index i<br />
 JOIN pg_class c ON i.indrelid = c.oid<br />
 JOIN pg_namespace AS ns ON ns.oid = c.relnamespace<br />
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)<br />
;<br />
 index_name &#124; index_oid &#124; nspname<br />
-----------------------------------+-----------+------------<br />
 pg_type_typname_nsp_index &#124; 2704 &#124; pg_catalog<br />
 pg_attribute_relid_attnam_index &#124; 2658 &#124; pg_catalog<br />
 tracking_pkey &#124; 40970 &#124; public<br />
 pg_class_relname_nsp_index &#124; 2663 &#124; pg_catalog<br />
 pg_class_tblspc_relfilenode_index &#124; 3455 &#124; pg_catalog<br />
 pg_type_oid_index &#124; 2703 &#124; pg_catalog<br />
 pg_attribute_relid_attnum_index &#124; 2659 &#124; pg_catalog<br />
 pg_statistic_relid_att_inh_index &#124; 2696 &#124; pg_catalog<br />
 pg_depend_depender_index &#124; 2673 &#124; pg_catalog<br />
 pg_depend_reference_index &#124; 2674 &#124; pg_catalog<br />
 pg_class_oid_index &#124; 2662 &#124; pg_catalog<br />
Attempt 1: NULL out<br />
Then we also NULL the columns in PostgreSQL. From here on, we save the view of the file system, as PostgreSQL seems to know the file sizes exactly, as we have seen above:<br />
postgres=# UPDATE tracking<br />
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL<br />
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL<br />
;</p>
<p>postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 2695716864 &#124; 2571 MB &#124; 753696768 &#124; 719 MB &#124; 3449413632 &#124; 3290 MB &#124; 3450101760 &#124; 3290 MB<br />
Here we see that the table segments grow massively (+37%), which is called ‘bloat’ in PostgreSQL terminology. The MVCC implementation of PostgreSQL stores both the old and never new version of the row ‘in-place’ directly in the table, in contrast to MariaDB/MySQL which stores the old version in UNDO space and the new row ‘in-place’. The index file also increases significantly (+100%). We need to do more research to find out why this is the case. In addition, a ‘free space map’ is created again (difference between tot_rel_siz and tab_and_idx_siz).<br />
A subsequent VACUUM FULL reduces the table (to 28%) and the index (to 50%) again in relation to the previous size:<br />
postgres=# VACUUM FULL tracking;</p>
<p>postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1119780864 &#124; 1068 MB<br />
and also in relation to the original size, the table (to 38%) and the index (to 100%) become smaller again. Why the index has remained the same size and only the table has shrunk remains to be investigated…<br />
Experiment 2: Deleting the columns<br />
The columns are then dropped with DROP COLUMN.<br />
postgres=# ALTER TABLE tracking<br />
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4<br />
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9<br />
;<br />
As the response was immediate, it can be assumed that this operation is also instantaneous. Unfortunately, I couldn’t find anything about this in the PostgreSQL documentation.<br />
Nothing has changed significantly in terms of size, which is actually to be expected with an instant operation. However, the fact that the size did not change after the VACUUM FULL command was a little surprising:<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1120010240 &#124; 1068 MB</p>
<p>postgres=# VACUUM FULL tracking;</p>
<p> tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1119780864 &#124; 1068 MB<br />
Remarks<br />
Locking in PostgreSQL works as follows:</p>
<p>VACUUM Concurrent DML commands are possible similar to the MariaDB/MySQL OPTIMIZE TABLE command. However, the result is not quite the same.<br />
VACUUM FULL causes an ACCESS EXCLUSIVE lock. Similar to the MariaDB/MySQL 5.5 and older OPTIMIZE TABLE command. DML and SELECT commands are NOT permitted.</p>
<p>Sources</p>
<p>How to Get Sizes of Database Objects in PostgreSQL<br />
Database File Layout<br />
System Administration Functions<br />
CLUSTER<br />
VACUUM<br />
Explicit Locking</p>
<p>Additional attempts</p>
<p>Instead of 0.0, NULL was filled into the columns d0 - d9. The table remained small (tot_rel_siz_prtty = 1068 MB). It is therefore also worth saving NULL instead of dummy values with PostgreSQL.<br />
The columns d0 - d9 were created with DOUBLE PRECISION NOT NULL and the values 0.0 were filled. No effect: The table remained large (tot_rel_siz_prtty = 2232 MB).</p>
<p>This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/how-much-space-does-null-need/">How much space does NULL need?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The last time I consulted a customer, he came up to me beaming with joy and said that he had taken my advice and changed all the primary key columns from <code>BIGINT</code> (8 bytes) to <code>INT</code> (4 bytes) and that had made a big difference! His MySQL 8.4 database is now 750 Gbyte smaller (from 5.5 Tbyte). Nice!</p>
<p>And yes, I know that contradicts the recommendations of some of my PostgreSQL colleagues (<a href="https://www.crunchydata.com/blog/postgres-serials-should-be-bigint-and-how-to-migrate" target="_blank">here</a> and <a href="https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/#should-i-use-integerserial-or-bigintbigserial-for-my-auto-generated-primary-key" target="_blank">here</a>). In the MySQL world, more emphasis is placed on such things (<a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank">source</a>):</p>
<blockquote>
<p>Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables</p>
</blockquote>
<p>Also, InnoDB works a wee bit differently (index clustered table and primary key in all secondary keys) than PostgreSQL (heap table, indices with row pointer (<code>ctid</code>)).</p>
<p>But that&rsquo;s not really the issue. Immediately afterwards, he asked me whether the deletion of columns of type <code>DOUBLE</code> (8 bytes, in PostgreSQL-speak <code>DOUBLE PRECISION</code>) would also save space or whether he should rather drop the columns straight away. My first reflex response to <code>DOUBLE</code> was: <code>NULL</code> is good, followed by <code>OPTIMIZE TABLE</code> (<code>VACUUM FULL</code> in PostgreSQL parlance). But the second thought was, <code>DOUBLE</code> is a data type of fixed length, does <code>NULL</code> also apply there or only for data types with variable length? Caution is the mother of the porcelain box! Love to consult the manual first&hellip;</p>
<p>And there it says (<a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank">source</a>):</p>
<blockquote>
<p>Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.</p>
</blockquote>
<p>and (<a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html" target="_blank">source</a>):</p>
<blockquote>
<p>The variable-length part of the record header contains a bit vector for indicating NULL columns. &hellip; Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.</p>
</blockquote>
<h2>Experiment with MariaDB/MySQL<a class="anchor-link" id="experiment-with-mariadb-mysql"></a></h2>
<h3>Test setup<a class="anchor-link" id="test-setup"></a></h3>
<p>Somehow the description is a bit too complicated for me. Perhaps a small sketch would help? So let&rsquo;s give it a try:</p>
<pre><code>SQL&gt; -- DROP TABLE IF EXISTS tracking;

SQL&gt; CREATE TABLE tracking (
 id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE
);

SQL&gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
SQL&gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
</code></pre>
<p>The table is approx. 1.8 Gbyte in size for both MariaDB and MySQL with 16 M rows. Since this information is only given very imprecisely in <code>INFORMATION_SCHEMA</code>, let&rsquo;s take a look at the file system:</p>
<p>MariaDB 11.8:</p>
<pre><code>SQL&gt; system ls -l tracking.ibd
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
</code></pre>
<p>MySQL 8.4:</p>
<pre><code>SQL&gt; system ls -l tracking.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
</code></pre>
<h3>Defragment the table<a class="anchor-link" id="defragment-the-table"></a></h3>
<p>Then we &lsquo;defragment&rsquo; the table with the <code>OPTIMIZE TABLE</code> command:</p>
<pre><code>SQL&gt; OPTIMIZE TABLE tracking;
+---------------+----------+----------+-------------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+---------------+----------+----------+-------------------------------------------------------------------+
| test.tracking | optimize | note | Table does not support optimize, doing recreate + analyze instead |
| test.tracking | optimize | status | OK |
+---------------+----------+----------+-------------------------------------------------------------------+
</code></pre>
<p><strong>Attention</strong>: The table is copied once! It therefore needs twice the amount of disc space for a short time! This can be observed while the <code>OPTIMIZE TABLE</code> command is running:</p>
<p>MariaDB:</p>
<pre><code>$ watch -d -n 1 'ls -l trac* #*'
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 '#sql-alter-d57-8c.frm'
-rw-rw---- 1 mysql mysql 968884224 Feb 7 10:39 '#sql-alter-d57-8c.ibd'
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>$ watch -d -n 1 'ls -l trac* #*'
-rw-r----- 1 mysql mysql 369098752 Feb 7 10:40 #sql-ib1594-4164062678.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
</code></pre>
<p>The result is amazing! With MariaDB, the table has remained somewhat the same size:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:39 tracking.ibd
</code></pre>
<p>With MySQL, on the other hand, the table has actually grown after the &lsquo;defragmentation&rsquo;, namely by approx. 14%:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:41 tracking.ibd
</code></pre>
<p>If we execute the <code>OPTIMIZE TABLE</code> command again, the size remains constant for both MariaDB and MySQL:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:46 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:48 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:48 tracking.ibd
</code></pre>
<h3>Attempt 1: <code>NULL</code> out<a class="anchor-link" id="attempt-1-null-out"></a></h3>
<p>Now we <code>NULL</code> out the values:</p>
<pre><code>SQL&gt; UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;
</code></pre>
<p>After this step, the sizes of the files have even grown slightly:</p>
<p>MariaDB (+1.3%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:49 tracking.frm
-rw-rw---- 1 mysql mysql 1937768448 Feb 7 11:04 tracking.ibd
</code></pre>
<p>MySQL (+0.2%):</p>
<pre><code>-rw-r----- 1 mysql mysql 2202009600 Feb 7 11:04 tracking.ibd
</code></pre>
<p>We then defragment the table again with the <code>OPTIMIZE TABLE</code> command. The tables shrink as expected.</p>
<p>MariaDB (to 23%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:09 tracking.frm
-rw-rw---- 1 mysql mysql 448790528 Feb 7 11:10 tracking.ibd
</code></pre>
<p>MySQL (to 24%):</p>
<pre><code>-rw-r----- 1 mysql mysql 520093696 Feb 7 11:10 tracking.ibd
</code></pre>
<p><code>OPTIMIZE TABLE</code> again does NOT change the file size any more&hellip;</p>
<h3>Attempt 2: Deleting the columns<a class="anchor-link" id="attempt-2-deleting-the-columns"></a></h3>
<p>Now we try the whole thing again with the <code>DROP COLUMN</code> command. The starting position is again the same as described above:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:15 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 11:18 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 1929379840 Feb 7 11:19 tracking.ibd
</code></pre>
<p>After the <code>OPTIMIZE TABLE</code> command, the values look similar to the first attempt:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:20 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:21 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:21 tracking.ibd
</code></pre>
<p><code>OPTIMIZE TABLE</code> again also brings no further changes, as above:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:22 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:23 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:24 tracking.ibd
</code></pre>
<p>And now the actual second attempt with dropping the columns:</p>
<pre><code>SQL&gt; ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
</code></pre>
<p>The first thing we notice is that the command is <code>INSTANTANEOUS</code>, i.e. it does not make any changes to the data but only changes the metadata. On the one hand, this is good, as it minimises the impact on the application. On the other hand, it also means that no space is saved.</p>
<p>So let&rsquo;s get to grips with the whole thing again with the <code>OPTIMIZE TABLE</code> command:</p>
<p>MariaDB (to 93%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 925 Feb 7 11:28 tracking.frm
-rw-rw---- 1 mysql mysql 415236096 Feb 7 11:29 tracking.ibd
</code></pre>
<p>MySQL (to 92%):</p>
<pre><code>-rw-r----- 1 mysql mysql 478150656 Feb 7 11:28 tracking.ibd
</code></pre>
<h3>Conclusion<a class="anchor-link" id="conclusion"></a></h3>
<p>Both, dropping the columns and the <code>NULL</code> out of columns save a significant amount of space. Dropping the columns saves about 7% more space than <code>NULL</code> them out. If it is possible from an application point of view, you should therefore drop columns that are no longer required, or if not possible, at least <code>NULL</code> them out.</p>
<h2>Experiment with PostgreSQL<a class="anchor-link" id="experiment-with-postgresql"></a></h2>
<p>And now let&rsquo;s take a look at the whole thing with PostgreSQL 19devel.</p>
<h3>Test setup<a class="anchor-link" id="test-setup"></a></h3>
<p>The test setup is analogous to MariaDB/MySQL:</p>
<pre><code>postgres=# -- DROP TABLE IF EXISTS tracking;

postgres=# CREATE TABLE tracking (
 id SERIAL PRIMARY KEY
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION
);

postgres=# timing

postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
</code></pre>
<p>Firstly, we want to know how big the table has actually become. PostgreSQL seems to know this information very precisely:</p>
<pre><code>postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376856576 | 359 MB | 2340274176 | 2232 MB | 2340798464 | 2232 MB
</code></pre>
<p>Then we want to know where these files can be found in the file system:</p>
<pre><code>postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();
 db_oid
--------
 5

postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode
 FROM pg_class WHERE relname = 'tracking';
 table_oid | relname | relnamespace | relfilenode
-----------+----------+--------------+-------------
 40965 | tracking | 2200 | 40965

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 WHERE c.relname = 'tracking';
 index_name | index_oid
---------------+-----------
 tracking_pkey | 40970

postgres=# SELECT pg_relation_filepath('tracking');
 pg_relation_filepath
----------------------
 base/5/40965
</code></pre>
<p>Table and index size in the file system:</p>
<pre><code>$ ls -ltr 40965* 40970*
-rw------- 1 mysql mysql 40960 Feb 7 18:33 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:33 40965_fsm
-rw------- 1 mysql mysql 889675776 Feb 7 18:34 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:34 40965
-rw------- 1 mysql mysql 376856576 Feb 7 18:35 40970
</code></pre>
<ul>
<li><code>*_fsm</code> means &ldquo;free space map&rdquo;</li>
<li><code>*_vm</code> means &ldquo;visibility map&rdquo;</li>
<li><code>*.1</code> means 2nd segment of the object (table or index)</li>
</ul>
<p>PostgreSQL seems to work with segments of 1 Gbyte by default and, unlike MariaDB/MySQL (<code>INFORMATION_SCHEMA</code>), knows exactly how large its files are. And the discrepancy from above (between <code>tot_rel_siz</code> and <code>tab_and_idx_siz</code>) can be explained by the <code>fsm</code> and <code>vm</code> files.</p>
<p>The PostgreSQL equivalent of the MariaDB/MySQL <code>OPTIMIZE TABLE</code> is the <code>VACUUM FULL</code> command:</p>
<pre><code>postgres=# VACUUM FULL tracking;

$ ls -ltr
-rw------- 1 mysql mysql 40960 Feb 7 18:39 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:39 40965_fsm
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40965
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 0 Feb 7 18:39 40975

...

-rw------- 1 mysql mysql 49152 Feb 7 18:39 2704
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2703
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2696
-rw------- 1 mysql mysql 65536 Feb 7 18:39 2674
-rw------- 1 mysql mysql 81920 Feb 7 18:39 2673
-rw------- 1 mysql mysql 98304 Feb 7 18:39 2659
-rw------- 1 mysql mysql 139264 Feb 7 18:39 2658
-rw------- 1 mysql mysql 24576 Feb 7 18:39 2619_fsm
-rw------- 1 mysql mysql 163840 Feb 7 18:39 2619
-rw------- 1 mysql mysql 106496 Feb 7 18:39 2608
-rw------- 1 mysql mysql 491520 Feb 7 18:39 1249
-rw------- 1 mysql mysql 122880 Feb 7 18:39 1247
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2662
-rw------- 1 mysql mysql 114688 Feb 7 18:39 1259
-rw------- 1 mysql mysql 16384 Feb 7 18:39 3455
-rw------- 1 mysql mysql 49152 Feb 7 18:39 2663
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 376864768 Feb 7 18:39 40975
-rw------- 1 mysql mysql 0 Feb 7 18:39 40965
-rw------- 1 mysql mysql 0 Feb 7 18:39 40970
</code></pre>
<p>The first thing you notice is that PostgreSQL touches quite a few files and the &lsquo;free space map&rsquo; file has disappeared. In contrast to MariaDB/MySQL, the table segments have remained the same size. You can also see that the old table has &lsquo;disappeared&rsquo; (40965, 40970) and a new one has been created (40972 and 40975). The <code>VACUUM FULL</code> command in PostgreSQL also creates a copy of the data, as in MariaDB/MySQL.</p>
<pre><code>postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376864768 | 359 MB | 2340282368 | 2232 MB | 2340282368 | 2232 MB
</code></pre>
<p>The following query helps to understand which other files/objects have been created:</p>
<pre><code>postgres=# SELECT c.oid, c.relname, ns.nspname
FROM pg_class AS c
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 oid | relname | nspname
-------+-----------------------------------+------------
 40965 | tracking | public
 40970 | tracking_pkey | public
 2619 | pg_statistic | pg_catalog
 1247 | pg_type | pg_catalog
 2703 | pg_type_oid_index | pg_catalog
 2704 | pg_type_typname_nsp_index | pg_catalog
 2658 | pg_attribute_relid_attnam_index | pg_catalog
 2659 | pg_attribute_relid_attnum_index | pg_catalog
 2662 | pg_class_oid_index | pg_catalog
 2663 | pg_class_relname_nsp_index | pg_catalog
 3455 | pg_class_tblspc_relfilenode_index | pg_catalog
 2696 | pg_statistic_relid_att_inh_index | pg_catalog
 2673 | pg_depend_depender_index | pg_catalog
 2674 | pg_depend_reference_index | pg_catalog
 1249 | pg_attribute | pg_catalog
 1259 | pg_class | pg_catalog
 2608 | pg_depend | pg_catalog

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 index_name | index_oid | nspname
-----------------------------------+-----------+------------
 pg_type_typname_nsp_index | 2704 | pg_catalog
 pg_attribute_relid_attnam_index | 2658 | pg_catalog
 tracking_pkey | 40970 | public
 pg_class_relname_nsp_index | 2663 | pg_catalog
 pg_class_tblspc_relfilenode_index | 3455 | pg_catalog
 pg_type_oid_index | 2703 | pg_catalog
 pg_attribute_relid_attnum_index | 2659 | pg_catalog
 pg_statistic_relid_att_inh_index | 2696 | pg_catalog
 pg_depend_depender_index | 2673 | pg_catalog
 pg_depend_reference_index | 2674 | pg_catalog
 pg_class_oid_index | 2662 | pg_catalog
</code></pre>
<h3>Attempt 1: <code>NULL</code> out<a class="anchor-link" id="attempt-1-null-out"></a></h3>
<p>Then we also <code>NULL</code> the columns in PostgreSQL. From here on, we save the view of the file system, as PostgreSQL seems to know the file sizes exactly, as we have seen above:</p>
<pre><code>postgres=# UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 2695716864 | 2571 MB | 753696768 | 719 MB | 3449413632 | 3290 MB | 3450101760 | 3290 MB
</code></pre>
<p>Here we see that the table segments grow massively (+37%), which is called &lsquo;bloat&rsquo; in PostgreSQL terminology. The MVCC implementation of PostgreSQL stores both the old and never new version of the row &lsquo;in-place&rsquo; directly in the table, in contrast to MariaDB/MySQL which stores the old version in UNDO space and the new row &lsquo;in-place&rsquo;. The index file also increases significantly (+100%). We need to do more research to find out why this is the case. In addition, a &lsquo;free space map&rsquo; is created again (difference between <code>tot_rel_siz</code> and <code>tab_and_idx_siz</code>).</p>
<p>A subsequent <code>VACUUM FULL</code> reduces the table (to 28%) and the index (to 50%) again in relation to the previous size:</p>
<pre><code>postgres=# VACUUM FULL tracking;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
</code></pre>
<p>and also in relation to the original size, the table (to 38%) and the index (to 100%) become smaller again. Why the index has remained the same size and only the table has shrunk remains to be investigated&hellip;</p>
<h3>Experiment 2: Deleting the columns<a class="anchor-link" id="experiment-2-deleting-the-columns"></a></h3>
<p>The columns are then dropped with <code>DROP COLUMN</code>.</p>
<pre><code>postgres=# ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
</code></pre>
<p>As the response was immediate, it can be assumed that this operation is also instantaneous. Unfortunately, I couldn&rsquo;t find anything about this in the PostgreSQL documentation.</p>
<p>Nothing has changed significantly in terms of size, which is actually to be expected with an instant operation. However, the fact that the size did not change after the <code>VACUUM FULL</code> command was a little surprising:</p>
<pre><code> tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1120010240 | 1068 MB

postgres=# VACUUM FULL tracking;

 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
</code></pre>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>Locking in PostgreSQL works as follows:</p>
<ul>
<li><code>VACUUM</code> Concurrent DML commands are possible similar to the MariaDB/MySQL <code>OPTIMIZE TABLE</code> command. However, the result is not quite the same.</li>
<li><code>VACUUM FULL</code> causes an <code>ACCESS EXCLUSIVE</code> lock. Similar to the MariaDB/MySQL 5.5 and older <code>OPTIMIZE TABLE</code> command. DML and <code>SELECT</code> commands are NOT permitted.</li>
</ul>
<h2>Sources<a class="anchor-link" id="sources"></a></h2>
<ul>
<li><a href="https://neon.com/postgresql/postgresql-administration/postgresql-database-indexes-table-size" target="_blank">How to Get Sizes of Database Objects in PostgreSQL</a></li>
<li><a href="https://www.postgresql.org/docs/current/storage-file-layout.html" target="_blank">Database File Layout</a></li>
<li><a href="https://www.postgresql.org/docs/current/functions-admin.html" target="_blank">System Administration Functions</a></li>
<li><a href="https://www.postgresql.org/docs/current/sql-cluster.html" target="_blank">CLUSTER</a></li>
<li><a href="https://www.postgresql.org/docs/current/sql-vacuum.html" target="_blank">VACUUM</a></li>
<li><a href="https://www.postgresql.org/docs/current/explicit-locking.html" target="_blank">Explicit Locking</a></li>
</ul>
<h2>Additional attempts<a class="anchor-link" id="additional-attempts"></a></h2>
<ol>
<li>Instead of <code>0.0</code>, <code>NULL</code> was filled into the columns <code>d0</code> &ndash; <code>d9</code>. The table remained small (<code>tot_rel_siz_prtty = 1068 MB</code>). It is therefore also worth saving <code>NULL</code> instead of dummy values with PostgreSQL.</li>
<li>The columns <code>d0</code> &ndash; <code>d9</code> were created with <code>DOUBLE PRECISION NOT NULL</code> and the values <code>0.0</code> were filled. No effect: The table remained large (<code>tot_rel_siz_prtty = 2232 MB</code>).</li>
</ol>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/how-much-space-does-null-need/">How much space does NULL need?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>How much space does NULL need?</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/how-much-space-does-null-need/" />
      <id>https://www.fromdual.com/blog/how-much-space-does-null-need/</id>
      <updated>2026-02-08T15:15:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The last time I consulted a customer, he came up to me beaming with joy and said that he had taken my advice and changed all the primary key columns from BIGINT (8 bytes) to INT (4 bytes) and that had made a big difference! His MySQL 8.4 database is now 750 Gbyte smaller (from 5.5 Tbyte). Nice!<br />
And yes, I know that contradicts the recommendations of some of my PostgreSQL colleagues (here and here). In the MySQL world, more emphasis is placed on such things (source):</p>
<p>Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables</p>
<p>Also, InnoDB works a wee bit differently (index clustered table and primary key in all secondary keys) than PostgreSQL (heap table, indices with row pointer (ctid)).<br />
But that’s not really the issue. Immediately afterwards, he asked me whether the deletion of columns of type DOUBLE (8 bytes, in PostgreSQL-speak DOUBLE PRECISION) would also save space or whether he should rather drop the columns straight away. My first reflex response to DOUBLE was: NULL is good, followed by OPTIMIZE TABLE (VACUUM FULL in PostgreSQL parlance). But the second thought was, DOUBLE is a data type of fixed length, does NULL also apply there or only for data types with variable length? Caution is the mother of the porcelain box! Love to consult the manual first…<br />
And there it says (source):</p>
<p>Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.</p>
<p>and (source):</p>
<p>The variable-length part of the record header contains a bit vector for indicating NULL columns. … Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.</p>
<p>Experiment with MariaDB/MySQL<br />
Test setup<br />
Somehow the description is a bit too complicated for me. Perhaps a small sketch would help? So let’s give it a try:<br />
SQL &#62; -- DROP TABLE IF EXISTS tracking;</p>
<p>SQL &#62; CREATE TABLE tracking (<br />
 id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT<br />
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE<br />
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE<br />
);</p>
<p>SQL &#62; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;<br />
SQL &#62; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;<br />
... bis 16 M rows<br />
The table is approx. 1.8 Gbyte in size for both MariaDB and MySQL with 16 M rows. Since this information is only given very imprecisely in INFORMATION_SCHEMA, let’s take a look at the file system:<br />
MariaDB 11.8:<br />
SQL &#62; system ls -l tracking.ibd<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd<br />
MySQL 8.4:<br />
SQL &#62; system ls -l tracking.ibd<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd<br />
Defragment the table<br />
Then we ‘defragment’ the table with the OPTIMIZE TABLE command:<br />
SQL &#62; OPTIMIZE TABLE tracking;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
&#124; Table &#124; Op &#124; Msg_type &#124; Msg_text &#124;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
&#124; test.tracking &#124; optimize &#124; note &#124; Table does not support optimize, doing recreate + analyze instead &#124;<br />
&#124; test.tracking &#124; optimize &#124; status &#124; OK &#124;<br />
+---------------+----------+----------+-------------------------------------------------------------------+<br />
Attention: The table is copied once! It therefore needs twice the amount of disc space for a short time! This can be observed while the OPTIMIZE TABLE command is running:<br />
MariaDB:<br />
$ watch -d -n 1 \'ls -l trac* #*\'<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 \'#sql-alter-d57-8c.frm\'<br />
-rw-rw---- 1 mysql mysql 968884224 Feb 7 10:39 \'#sql-alter-d57-8c.ibd\'<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd<br />
MySQL:<br />
$ watch -d -n 1 \'ls -l trac* #*\'<br />
-rw-r----- 1 mysql mysql 369098752 Feb 7 10:40 #sql-ib1594-4164062678.ibd<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd<br />
The result is amazing! With MariaDB, the table has remained somewhat the same size:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:39 tracking.ibd<br />
With MySQL, on the other hand, the table has actually grown after the ‘defragmentation’, namely by approx. 14%:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:41 tracking.ibd<br />
If we execute the OPTIMIZE TABLE command again, the size remains constant for both MariaDB and MySQL:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:46 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:48 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:48 tracking.ibd<br />
Attempt 1: NULL out<br />
Now we NULL out the values:<br />
SQL &#62; UPDATE tracking<br />
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL<br />
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL<br />
;<br />
After this step, the sizes of the files have even grown slightly:<br />
MariaDB (+1.3%):<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:49 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1937768448 Feb 7 11:04 tracking.ibd<br />
MySQL (+0.2%):<br />
-rw-r----- 1 mysql mysql 2202009600 Feb 7 11:04 tracking.ibd<br />
We then defragment the table again with the OPTIMIZE TABLE command. The tables shrink as expected.<br />
MariaDB (to 23%):<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:09 tracking.frm<br />
-rw-rw---- 1 mysql mysql 448790528 Feb 7 11:10 tracking.ibd<br />
MySQL (to 24%):<br />
-rw-r----- 1 mysql mysql 520093696 Feb 7 11:10 tracking.ibd<br />
OPTIMIZE TABLE again does NOT change the file size any more…<br />
Attempt 2: Deleting the columns<br />
Now we try the whole thing again with the DROP COLUMN command. The starting position is again the same as described above:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:15 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 11:18 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 1929379840 Feb 7 11:19 tracking.ibd<br />
After the OPTIMIZE TABLE command, the values look similar to the first attempt:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:20 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:21 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:21 tracking.ibd<br />
OPTIMIZE TABLE again also brings no further changes, as above:<br />
MariaDB:<br />
-rw-rw---- 1 mysql mysql 1206 Feb 7 11:22 tracking.frm<br />
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:23 tracking.ibd<br />
MySQL:<br />
-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:24 tracking.ibd<br />
And now the actual second attempt with dropping the columns:<br />
SQL &#62; ALTER TABLE tracking<br />
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4<br />
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9<br />
;<br />
The first thing we notice is that the command is INSTANTANEOUS, i.e. it does not make any changes to the data but only changes the metadata. On the one hand, this is good, as it minimises the impact on the application. On the other hand, it also means that no space is saved.<br />
So let’s get to grips with the whole thing again with the OPTIMIZE TABLE command:<br />
MariaDB (to 93%):<br />
-rw-rw---- 1 mysql mysql 925 Feb 7 11:28 tracking.frm<br />
-rw-rw---- 1 mysql mysql 415236096 Feb 7 11:29 tracking.ibd<br />
MySQL (to 92%):<br />
-rw-r----- 1 mysql mysql 478150656 Feb 7 11:28 tracking.ibd<br />
Conclusion<br />
Both, dropping the columns and the NULL out of columns save a significant amount of space. Dropping the columns saves about 7% more space than NULL them out. If it is possible from an application point of view, you should therefore drop columns that are no longer required, or if not possible, at least NULL them out.<br />
Experiment with PostgreSQL<br />
And now let’s take a look at the whole thing with PostgreSQL 19devel.<br />
Test setup<br />
The test setup is analogous to MariaDB/MySQL:<br />
postgres=# -- DROP TABLE IF EXISTS tracking;</p>
<p>postgres=# CREATE TABLE tracking (<br />
 id SERIAL PRIMARY KEY<br />
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION<br />
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION<br />
);</p>
<p>postgres=# timing</p>
<p>postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)<br />
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;<br />
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)<br />
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;<br />
... bis 16 M rows<br />
Firstly, we want to know how big the table has actually become. PostgreSQL seems to know this information very precisely:<br />
postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 1963417600 &#124; 1872 MB &#124; 376856576 &#124; 359 MB &#124; 2340274176 &#124; 2232 MB &#124; 2340798464 &#124; 2232 MB<br />
Then we want to know where these files can be found in the file system:<br />
postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();<br />
 db_oid<br />
--------<br />
 5</p>
<p>postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode<br />
 FROM pg_class WHERE relname = \'tracking\';<br />
 table_oid &#124; relname &#124; relnamespace &#124; relfilenode<br />
-----------+----------+--------------+-------------<br />
 40965 &#124; tracking &#124; 2200 &#124; 40965</p>
<p>postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid<br />
 FROM pg_index i<br />
 JOIN pg_class c ON i.indrelid = c.oid<br />
 WHERE c.relname = \'tracking\';<br />
 index_name &#124; index_oid<br />
---------------+-----------<br />
 tracking_pkey &#124; 40970</p>
<p>postgres=# SELECT pg_relation_filepath(\'tracking\');<br />
 pg_relation_filepath<br />
----------------------<br />
 base/5/40965<br />
Table and index size in the file system:<br />
$ ls -ltr 40965* 40970*<br />
-rw------- 1 mysql mysql 40960 Feb 7 18:33 40965_vm<br />
-rw------- 1 mysql mysql 499712 Feb 7 18:33 40965_fsm<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:34 40965.1<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:34 40965<br />
-rw------- 1 mysql mysql 376856576 Feb 7 18:35 40970</p>
<p>*_fsm means “free space map”<br />
*_vm means “visibility map”<br />
*.1 means 2nd segment of the object (table or index)</p>
<p>PostgreSQL seems to work with segments of 1 Gbyte by default and, unlike MariaDB/MySQL (INFORMATION_SCHEMA), knows exactly how large its files are. And the discrepancy from above (between tot_rel_siz and tab_and_idx_siz) can be explained by the fsm and vm files.<br />
The PostgreSQL equivalent of the MariaDB/MySQL OPTIMIZE TABLE is the VACUUM FULL command:<br />
postgres=# VACUUM FULL tracking;</p>
<p>$ ls -ltr<br />
-rw------- 1 mysql mysql 40960 Feb 7 18:39 40965_vm<br />
-rw------- 1 mysql mysql 499712 Feb 7 18:39 40965_fsm<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40965<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40965.1<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40975</p>
<p>...</p>
<p>-rw------- 1 mysql mysql 49152 Feb 7 18:39 2704<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2703<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2696<br />
-rw------- 1 mysql mysql 65536 Feb 7 18:39 2674<br />
-rw------- 1 mysql mysql 81920 Feb 7 18:39 2673<br />
-rw------- 1 mysql mysql 98304 Feb 7 18:39 2659<br />
-rw------- 1 mysql mysql 139264 Feb 7 18:39 2658<br />
-rw------- 1 mysql mysql 24576 Feb 7 18:39 2619_fsm<br />
-rw------- 1 mysql mysql 163840 Feb 7 18:39 2619<br />
-rw------- 1 mysql mysql 106496 Feb 7 18:39 2608<br />
-rw------- 1 mysql mysql 491520 Feb 7 18:39 1249<br />
-rw------- 1 mysql mysql 122880 Feb 7 18:39 1247<br />
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2662<br />
-rw------- 1 mysql mysql 114688 Feb 7 18:39 1259<br />
-rw------- 1 mysql mysql 16384 Feb 7 18:39 3455<br />
-rw------- 1 mysql mysql 49152 Feb 7 18:39 2663<br />
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972<br />
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1<br />
-rw------- 1 mysql mysql 376864768 Feb 7 18:39 40975<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40965<br />
-rw------- 1 mysql mysql 0 Feb 7 18:39 40970<br />
The first thing you notice is that PostgreSQL touches quite a few files and the ‘free space map’ file has disappeared. In contrast to MariaDB/MySQL, the table segments have remained the same size. You can also see that the old table has ‘disappeared’ (40965, 40970) and a new one has been created (40972 and 40975). The VACUUM FULL command in PostgreSQL also creates a copy of the data, as in MariaDB/MySQL.<br />
postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 1963417600 &#124; 1872 MB &#124; 376864768 &#124; 359 MB &#124; 2340282368 &#124; 2232 MB &#124; 2340282368 &#124; 2232 MB<br />
The following query helps to understand which other files/objects have been created:<br />
postgres=# SELECT c.oid, c.relname, ns.nspname<br />
FROM pg_class AS c<br />
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace<br />
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)<br />
;<br />
 oid &#124; relname &#124; nspname<br />
-------+-----------------------------------+------------<br />
 40965 &#124; tracking &#124; public<br />
 40970 &#124; tracking_pkey &#124; public<br />
 2619 &#124; pg_statistic &#124; pg_catalog<br />
 1247 &#124; pg_type &#124; pg_catalog<br />
 2703 &#124; pg_type_oid_index &#124; pg_catalog<br />
 2704 &#124; pg_type_typname_nsp_index &#124; pg_catalog<br />
 2658 &#124; pg_attribute_relid_attnam_index &#124; pg_catalog<br />
 2659 &#124; pg_attribute_relid_attnum_index &#124; pg_catalog<br />
 2662 &#124; pg_class_oid_index &#124; pg_catalog<br />
 2663 &#124; pg_class_relname_nsp_index &#124; pg_catalog<br />
 3455 &#124; pg_class_tblspc_relfilenode_index &#124; pg_catalog<br />
 2696 &#124; pg_statistic_relid_att_inh_index &#124; pg_catalog<br />
 2673 &#124; pg_depend_depender_index &#124; pg_catalog<br />
 2674 &#124; pg_depend_reference_index &#124; pg_catalog<br />
 1249 &#124; pg_attribute &#124; pg_catalog<br />
 1259 &#124; pg_class &#124; pg_catalog<br />
 2608 &#124; pg_depend &#124; pg_catalog</p>
<p>postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname<br />
 FROM pg_index i<br />
 JOIN pg_class c ON i.indrelid = c.oid<br />
 JOIN pg_namespace AS ns ON ns.oid = c.relnamespace<br />
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)<br />
;<br />
 index_name &#124; index_oid &#124; nspname<br />
-----------------------------------+-----------+------------<br />
 pg_type_typname_nsp_index &#124; 2704 &#124; pg_catalog<br />
 pg_attribute_relid_attnam_index &#124; 2658 &#124; pg_catalog<br />
 tracking_pkey &#124; 40970 &#124; public<br />
 pg_class_relname_nsp_index &#124; 2663 &#124; pg_catalog<br />
 pg_class_tblspc_relfilenode_index &#124; 3455 &#124; pg_catalog<br />
 pg_type_oid_index &#124; 2703 &#124; pg_catalog<br />
 pg_attribute_relid_attnum_index &#124; 2659 &#124; pg_catalog<br />
 pg_statistic_relid_att_inh_index &#124; 2696 &#124; pg_catalog<br />
 pg_depend_depender_index &#124; 2673 &#124; pg_catalog<br />
 pg_depend_reference_index &#124; 2674 &#124; pg_catalog<br />
 pg_class_oid_index &#124; 2662 &#124; pg_catalog<br />
Attempt 1: NULL out<br />
Then we also NULL the columns in PostgreSQL. From here on, we save the view of the file system, as PostgreSQL seems to know the file sizes exactly, as we have seen above:<br />
postgres=# UPDATE tracking<br />
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL<br />
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL<br />
;</p>
<p>postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 2695716864 &#124; 2571 MB &#124; 753696768 &#124; 719 MB &#124; 3449413632 &#124; 3290 MB &#124; 3450101760 &#124; 3290 MB<br />
Here we see that the table segments grow massively (+37%), which is called ‘bloat’ in PostgreSQL terminology. The MVCC implementation of PostgreSQL stores both the old and never new version of the row ‘in-place’ directly in the table, in contrast to MariaDB/MySQL which stores the old version in UNDO space and the new row ‘in-place’. The index file also increases significantly (+100%). We need to do more research to find out why this is the case. In addition, a ‘free space map’ is created again (difference between tot_rel_siz and tab_and_idx_siz).<br />
A subsequent VACUUM FULL reduces the table (to 28%) and the index (to 50%) again in relation to the previous size:<br />
postgres=# VACUUM FULL tracking;</p>
<p>postgres=# SELECT pg_relation_size(\'tracking\') AS tab_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\')) AS tab_siz_prtty<br />
 , pg_indexes_size(\'tracking\') AS idx_siz<br />
 , pg_size_pretty(pg_indexes_size(\'tracking\')) AS idx_siz_prtty<br />
 , pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\') AS tab_and_idx_siz<br />
 , pg_size_pretty(pg_relation_size(\'tracking\') + pg_indexes_size(\'tracking\')) AS tab_and_idx_siz_prtty<br />
 , pg_total_relation_size(\'tracking\') AS tot_rel_siz<br />
 , pg_size_pretty(pg_total_relation_size(\'tracking\')) AS tot_rel_siz_prtty<br />
;<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1119780864 &#124; 1068 MB<br />
and also in relation to the original size, the table (to 38%) and the index (to 100%) become smaller again. Why the index has remained the same size and only the table has shrunk remains to be investigated…<br />
Experiment 2: Deleting the columns<br />
The columns are then dropped with DROP COLUMN.<br />
postgres=# ALTER TABLE tracking<br />
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4<br />
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9<br />
;<br />
As the response was immediate, it can be assumed that this operation is also instantaneous. Unfortunately, I couldn’t find anything about this in the PostgreSQL documentation.<br />
Nothing has changed significantly in terms of size, which is actually to be expected with an instant operation. However, the fact that the size did not change after the VACUUM FULL command was a little surprising:<br />
 tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1120010240 &#124; 1068 MB</p>
<p>postgres=# VACUUM FULL tracking;</p>
<p> tab_siz &#124; tab_siz_prtty &#124; idx_siz &#124; idx_siz_prtty &#124; tab_and_idx_siz &#124; tab_and_idx_siz_prtty &#124; tot_rel_siz &#124; tot_rel_siz_prtty<br />
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------<br />
 742916096 &#124; 709 MB &#124; 376864768 &#124; 359 MB &#124; 1119780864 &#124; 1068 MB &#124; 1119780864 &#124; 1068 MB<br />
Remarks<br />
Locking in PostgreSQL works as follows:</p>
<p>VACUUM Concurrent DML commands are possible similar to the MariaDB/MySQL OPTIMIZE TABLE command. However, the result is not quite the same.<br />
VACUUM FULL causes an ACCESS EXCLUSIVE lock. Similar to the MariaDB/MySQL 5.5 and older OPTIMIZE TABLE command. DML and SELECT commands are NOT permitted.</p>
<p>Sources</p>
<p>How to Get Sizes of Database Objects in PostgreSQL<br />
Database File Layout<br />
System Administration Functions<br />
CLUSTER<br />
VACUUM<br />
Explicit Locking</p>
<p>Additional attempts</p>
<p>Instead of 0.0, NULL was filled into the columns d0 - d9. The table remained small (tot_rel_siz_prtty = 1068 MB). It is therefore also worth saving NULL instead of dummy values with PostgreSQL.<br />
The columns d0 - d9 were created with DOUBLE PRECISION NOT NULL and the values 0.0 were filled. No effect: The table remained large (tot_rel_siz_prtty = 2232 MB).</p>
<p>This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/how-much-space-does-null-need/">How much space does NULL need?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The last time I consulted a customer, he came up to me beaming with joy and said that he had taken my advice and changed all the primary key columns from <code>BIGINT</code> (8 bytes) to <code>INT</code> (4 bytes) and that had made a big difference! His MySQL 8.4 database is now 750 Gbyte smaller (from 5.5 Tbyte). Nice!</p>
<p>And yes, I know that contradicts the recommendations of some of my PostgreSQL colleagues (<a href="https://www.crunchydata.com/blog/postgres-serials-should-be-bigint-and-how-to-migrate" target="_blank">here</a> and <a href="https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/#should-i-use-integerserial-or-bigintbigserial-for-my-auto-generated-primary-key" target="_blank">here</a>). In the MySQL world, more emphasis is placed on such things (<a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank">source</a>):</p>
<blockquote>
<p>Use the most efficient (smallest) data types possible. MySQL has many specialized types that save disk space and memory. For example, use the smaller integer types if possible to get smaller tables</p>
</blockquote>
<p>Also, InnoDB works a wee bit differently (index clustered table and primary key in all secondary keys) than PostgreSQL (heap table, indices with row pointer (<code>ctid</code>)).</p>
<p>But that&rsquo;s not really the issue. Immediately afterwards, he asked me whether the deletion of columns of type <code>DOUBLE</code> (8 bytes, in PostgreSQL-speak <code>DOUBLE PRECISION</code>) would also save space or whether he should rather drop the columns straight away. My first reflex response to <code>DOUBLE</code> was: <code>NULL</code> is good, followed by <code>OPTIMIZE TABLE</code> (<code>VACUUM FULL</code> in PostgreSQL parlance). But the second thought was, <code>DOUBLE</code> is a data type of fixed length, does <code>NULL</code> also apply there or only for data types with variable length? Caution is the mother of the porcelain box! Love to consult the manual first&hellip;</p>
<p>And there it says (<a href="https://dev.mysql.com/doc/refman/8.4/en/data-size.html" target="_blank">source</a>):</p>
<blockquote>
<p>Declare columns to be NOT NULL if possible. It makes SQL operations faster, by enabling better use of indexes and eliminating overhead for testing whether each value is NULL. You also save some storage space, one bit per column. If you really need NULL values in your tables, use them. Just avoid the default setting that allows NULL values in every column.</p>
</blockquote>
<p>and (<a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-row-format.html" target="_blank">source</a>):</p>
<blockquote>
<p>The variable-length part of the record header contains a bit vector for indicating NULL columns. &hellip; Columns that are NULL do not occupy space other than the bit in this vector. The variable-length part of the header also contains the lengths of variable-length columns. Each length takes one or two bytes, depending on the maximum length of the column. If all columns in the index are NOT NULL and have a fixed length, the record header has no variable-length part.</p>
</blockquote>
<h2>Experiment with MariaDB/MySQL<a class="anchor-link" id="experiment-with-mariadb-mysql"></a></h2>
<h3>Test setup<a class="anchor-link" id="test-setup"></a></h3>
<p>Somehow the description is a bit too complicated for me. Perhaps a small sketch would help? So let&rsquo;s give it a try:</p>
<pre><code>SQL&gt; -- DROP TABLE IF EXISTS tracking;

SQL&gt; CREATE TABLE tracking (
 id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT
, d0 DOUBLE, d1 DOUBLE, d2 DOUBLE, d3 DOUBLE, d4 DOUBLE
, d5 DOUBLE, d6 DOUBLE, d7 DOUBLE, d8 DOUBLE, d9 DOUBLE
);

SQL&gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
SQL&gt; INSERT INTO tracking SELECT NULL, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
</code></pre>
<p>The table is approx. 1.8 Gbyte in size for both MariaDB and MySQL with 16 M rows. Since this information is only given very imprecisely in <code>INFORMATION_SCHEMA</code>, let&rsquo;s take a look at the file system:</p>
<p>MariaDB 11.8:</p>
<pre><code>SQL&gt; system ls -l tracking.ibd
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
</code></pre>
<p>MySQL 8.4:</p>
<pre><code>SQL&gt; system ls -l tracking.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
</code></pre>
<h3>Defragment the table<a class="anchor-link" id="defragment-the-table"></a></h3>
<p>Then we &lsquo;defragment&rsquo; the table with the <code>OPTIMIZE TABLE</code> command:</p>
<pre><code>SQL&gt; OPTIMIZE TABLE tracking;
+---------------+----------+----------+-------------------------------------------------------------------+
| Table | Op | Msg_type | Msg_text |
+---------------+----------+----------+-------------------------------------------------------------------+
| test.tracking | optimize | note | Table does not support optimize, doing recreate + analyze instead |
| test.tracking | optimize | status | OK |
+---------------+----------+----------+-------------------------------------------------------------------+
</code></pre>
<p><strong>Attention</strong>: The table is copied once! It therefore needs twice the amount of disc space for a short time! This can be observed while the <code>OPTIMIZE TABLE</code> command is running:</p>
<p>MariaDB:</p>
<pre><code>$ watch -d -n 1 'ls -l trac* #*'
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 '#sql-alter-d57-8c.frm'
-rw-rw---- 1 mysql mysql 968884224 Feb 7 10:39 '#sql-alter-d57-8c.ibd'
-rw-rw---- 1 mysql mysql 1206 Feb 7 10:28 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 10:32 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>$ watch -d -n 1 'ls -l trac* #*'
-rw-r----- 1 mysql mysql 369098752 Feb 7 10:40 #sql-ib1594-4164062678.ibd
-rw-r----- 1 mysql mysql 1929379840 Feb 7 10:33 tracking.ibd
</code></pre>
<p>The result is amazing! With MariaDB, the table has remained somewhat the same size:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:39 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:39 tracking.ibd
</code></pre>
<p>With MySQL, on the other hand, the table has actually grown after the &lsquo;defragmentation&rsquo;, namely by approx. 14%:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:41 tracking.ibd
</code></pre>
<p>If we execute the <code>OPTIMIZE TABLE</code> command again, the size remains constant for both MariaDB and MySQL:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:46 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 10:48 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 10:48 tracking.ibd
</code></pre>
<h3>Attempt 1: <code>NULL</code> out<a class="anchor-link" id="attempt-1-null-out"></a></h3>
<p>Now we <code>NULL</code> out the values:</p>
<pre><code>SQL&gt; UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;
</code></pre>
<p>After this step, the sizes of the files have even grown slightly:</p>
<p>MariaDB (+1.3%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 10:49 tracking.frm
-rw-rw---- 1 mysql mysql 1937768448 Feb 7 11:04 tracking.ibd
</code></pre>
<p>MySQL (+0.2%):</p>
<pre><code>-rw-r----- 1 mysql mysql 2202009600 Feb 7 11:04 tracking.ibd
</code></pre>
<p>We then defragment the table again with the <code>OPTIMIZE TABLE</code> command. The tables shrink as expected.</p>
<p>MariaDB (to 23%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:09 tracking.frm
-rw-rw---- 1 mysql mysql 448790528 Feb 7 11:10 tracking.ibd
</code></pre>
<p>MySQL (to 24%):</p>
<pre><code>-rw-r----- 1 mysql mysql 520093696 Feb 7 11:10 tracking.ibd
</code></pre>
<p><code>OPTIMIZE TABLE</code> again does NOT change the file size any more&hellip;</p>
<h3>Attempt 2: Deleting the columns<a class="anchor-link" id="attempt-2-deleting-the-columns"></a></h3>
<p>Now we try the whole thing again with the <code>DROP COLUMN</code> command. The starting position is again the same as described above:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:15 tracking.frm
-rw-rw---- 1 mysql mysql 1933574144 Feb 7 11:18 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 1929379840 Feb 7 11:19 tracking.ibd
</code></pre>
<p>After the <code>OPTIMIZE TABLE</code> command, the values look similar to the first attempt:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:20 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:21 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:21 tracking.ibd
</code></pre>
<p><code>OPTIMIZE TABLE</code> again also brings no further changes, as above:</p>
<p>MariaDB:</p>
<pre><code>-rw-rw---- 1 mysql mysql 1206 Feb 7 11:22 tracking.frm
-rw-rw---- 1 mysql mysql 1912602624 Feb 7 11:23 tracking.ibd
</code></pre>
<p>MySQL:</p>
<pre><code>-rw-r----- 1 mysql mysql 2197815296 Feb 7 11:24 tracking.ibd
</code></pre>
<p>And now the actual second attempt with dropping the columns:</p>
<pre><code>SQL&gt; ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
</code></pre>
<p>The first thing we notice is that the command is <code>INSTANTANEOUS</code>, i.e. it does not make any changes to the data but only changes the metadata. On the one hand, this is good, as it minimises the impact on the application. On the other hand, it also means that no space is saved.</p>
<p>So let&rsquo;s get to grips with the whole thing again with the <code>OPTIMIZE TABLE</code> command:</p>
<p>MariaDB (to 93%):</p>
<pre><code>-rw-rw---- 1 mysql mysql 925 Feb 7 11:28 tracking.frm
-rw-rw---- 1 mysql mysql 415236096 Feb 7 11:29 tracking.ibd
</code></pre>
<p>MySQL (to 92%):</p>
<pre><code>-rw-r----- 1 mysql mysql 478150656 Feb 7 11:28 tracking.ibd
</code></pre>
<h3>Conclusion<a class="anchor-link" id="conclusion"></a></h3>
<p>Both, dropping the columns and the <code>NULL</code> out of columns save a significant amount of space. Dropping the columns saves about 7% more space than <code>NULL</code> them out. If it is possible from an application point of view, you should therefore drop columns that are no longer required, or if not possible, at least <code>NULL</code> them out.</p>
<h2>Experiment with PostgreSQL<a class="anchor-link" id="experiment-with-postgresql"></a></h2>
<p>And now let&rsquo;s take a look at the whole thing with PostgreSQL 19devel.</p>
<h3>Test setup<a class="anchor-link" id="test-setup"></a></h3>
<p>The test setup is analogous to MariaDB/MySQL:</p>
<pre><code>postgres=# -- DROP TABLE IF EXISTS tracking;

postgres=# CREATE TABLE tracking (
 id SERIAL PRIMARY KEY
, d0 DOUBLE PRECISION, d1 DOUBLE PRECISION, d2 DOUBLE PRECISION, d3 DOUBLE PRECISION, d4 DOUBLE PRECISION
, d5 DOUBLE PRECISION, d6 DOUBLE PRECISION, d7 DOUBLE PRECISION, d8 DOUBLE PRECISION, d9 DOUBLE PRECISION
);

postgres=# timing

postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
postgres=# INSERT INTO tracking (d0, d1, d2, d3, d4, d5, d6, d7, d8, d9)
 SELECT 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 FROM tracking;
... bis 16 M rows
</code></pre>
<p>Firstly, we want to know how big the table has actually become. PostgreSQL seems to know this information very precisely:</p>
<pre><code>postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376856576 | 359 MB | 2340274176 | 2232 MB | 2340798464 | 2232 MB
</code></pre>
<p>Then we want to know where these files can be found in the file system:</p>
<pre><code>postgres=# SELECT oid AS db_oid FROM pg_database WHERE datname = current_database();
 db_oid
--------
 5

postgres=# SELECT oid AS table_oid, relname, relnamespace, relfilenode
 FROM pg_class WHERE relname = 'tracking';
 table_oid | relname | relnamespace | relfilenode
-----------+----------+--------------+-------------
 40965 | tracking | 2200 | 40965

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 WHERE c.relname = 'tracking';
 index_name | index_oid
---------------+-----------
 tracking_pkey | 40970

postgres=# SELECT pg_relation_filepath('tracking');
 pg_relation_filepath
----------------------
 base/5/40965
</code></pre>
<p>Table and index size in the file system:</p>
<pre><code>$ ls -ltr 40965* 40970*
-rw------- 1 mysql mysql 40960 Feb 7 18:33 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:33 40965_fsm
-rw------- 1 mysql mysql 889675776 Feb 7 18:34 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:34 40965
-rw------- 1 mysql mysql 376856576 Feb 7 18:35 40970
</code></pre>
<ul>
<li><code>*_fsm</code> means &ldquo;free space map&rdquo;</li>
<li><code>*_vm</code> means &ldquo;visibility map&rdquo;</li>
<li><code>*.1</code> means 2nd segment of the object (table or index)</li>
</ul>
<p>PostgreSQL seems to work with segments of 1 Gbyte by default and, unlike MariaDB/MySQL (<code>INFORMATION_SCHEMA</code>), knows exactly how large its files are. And the discrepancy from above (between <code>tot_rel_siz</code> and <code>tab_and_idx_siz</code>) can be explained by the <code>fsm</code> and <code>vm</code> files.</p>
<p>The PostgreSQL equivalent of the MariaDB/MySQL <code>OPTIMIZE TABLE</code> is the <code>VACUUM FULL</code> command:</p>
<pre><code>postgres=# VACUUM FULL tracking;

$ ls -ltr
-rw------- 1 mysql mysql 40960 Feb 7 18:39 40965_vm
-rw------- 1 mysql mysql 499712 Feb 7 18:39 40965_fsm
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40965
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40965.1
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 0 Feb 7 18:39 40975

...

-rw------- 1 mysql mysql 49152 Feb 7 18:39 2704
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2703
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2696
-rw------- 1 mysql mysql 65536 Feb 7 18:39 2674
-rw------- 1 mysql mysql 81920 Feb 7 18:39 2673
-rw------- 1 mysql mysql 98304 Feb 7 18:39 2659
-rw------- 1 mysql mysql 139264 Feb 7 18:39 2658
-rw------- 1 mysql mysql 24576 Feb 7 18:39 2619_fsm
-rw------- 1 mysql mysql 163840 Feb 7 18:39 2619
-rw------- 1 mysql mysql 106496 Feb 7 18:39 2608
-rw------- 1 mysql mysql 491520 Feb 7 18:39 1249
-rw------- 1 mysql mysql 122880 Feb 7 18:39 1247
-rw------- 1 mysql mysql 32768 Feb 7 18:39 2662
-rw------- 1 mysql mysql 114688 Feb 7 18:39 1259
-rw------- 1 mysql mysql 16384 Feb 7 18:39 3455
-rw------- 1 mysql mysql 49152 Feb 7 18:39 2663
-rw------- 1 mysql mysql 1073741824 Feb 7 18:39 40972
-rw------- 1 mysql mysql 889675776 Feb 7 18:39 40972.1
-rw------- 1 mysql mysql 376864768 Feb 7 18:39 40975
-rw------- 1 mysql mysql 0 Feb 7 18:39 40965
-rw------- 1 mysql mysql 0 Feb 7 18:39 40970
</code></pre>
<p>The first thing you notice is that PostgreSQL touches quite a few files and the &lsquo;free space map&rsquo; file has disappeared. In contrast to MariaDB/MySQL, the table segments have remained the same size. You can also see that the old table has &lsquo;disappeared&rsquo; (40965, 40970) and a new one has been created (40972 and 40975). The <code>VACUUM FULL</code> command in PostgreSQL also creates a copy of the data, as in MariaDB/MySQL.</p>
<pre><code>postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 1963417600 | 1872 MB | 376864768 | 359 MB | 2340282368 | 2232 MB | 2340282368 | 2232 MB
</code></pre>
<p>The following query helps to understand which other files/objects have been created:</p>
<pre><code>postgres=# SELECT c.oid, c.relname, ns.nspname
FROM pg_class AS c
JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 oid | relname | nspname
-------+-----------------------------------+------------
 40965 | tracking | public
 40970 | tracking_pkey | public
 2619 | pg_statistic | pg_catalog
 1247 | pg_type | pg_catalog
 2703 | pg_type_oid_index | pg_catalog
 2704 | pg_type_typname_nsp_index | pg_catalog
 2658 | pg_attribute_relid_attnam_index | pg_catalog
 2659 | pg_attribute_relid_attnum_index | pg_catalog
 2662 | pg_class_oid_index | pg_catalog
 2663 | pg_class_relname_nsp_index | pg_catalog
 3455 | pg_class_tblspc_relfilenode_index | pg_catalog
 2696 | pg_statistic_relid_att_inh_index | pg_catalog
 2673 | pg_depend_depender_index | pg_catalog
 2674 | pg_depend_reference_index | pg_catalog
 1249 | pg_attribute | pg_catalog
 1259 | pg_class | pg_catalog
 2608 | pg_depend | pg_catalog

postgres=# SELECT i.indexrelid::regclass as index_name, i.indexrelid as index_oid, ns.nspname
 FROM pg_index i
 JOIN pg_class c ON i.indrelid = c.oid
 JOIN pg_namespace AS ns ON ns.oid = c.relnamespace
 WHERE c.oid IN (2704, 2703, 2696, 2674, 2673, 2659, 2658, 2619, 2608, 1249, 1247, 40972, 2662, 1259, 3455, 2663, 40975, 40965, 40970)
;
 index_name | index_oid | nspname
-----------------------------------+-----------+------------
 pg_type_typname_nsp_index | 2704 | pg_catalog
 pg_attribute_relid_attnam_index | 2658 | pg_catalog
 tracking_pkey | 40970 | public
 pg_class_relname_nsp_index | 2663 | pg_catalog
 pg_class_tblspc_relfilenode_index | 3455 | pg_catalog
 pg_type_oid_index | 2703 | pg_catalog
 pg_attribute_relid_attnum_index | 2659 | pg_catalog
 pg_statistic_relid_att_inh_index | 2696 | pg_catalog
 pg_depend_depender_index | 2673 | pg_catalog
 pg_depend_reference_index | 2674 | pg_catalog
 pg_class_oid_index | 2662 | pg_catalog
</code></pre>
<h3>Attempt 1: <code>NULL</code> out<a class="anchor-link" id="attempt-1-null-out"></a></h3>
<p>Then we also <code>NULL</code> the columns in PostgreSQL. From here on, we save the view of the file system, as PostgreSQL seems to know the file sizes exactly, as we have seen above:</p>
<pre><code>postgres=# UPDATE tracking
SET d0 = NULL, d1 = NULL, d2 = NULL, d3 = NULL, d4 = NULL
 , d5 = NULL, d6 = NULL, d7 = NULL, d8 = NULL, d9 = NULL
;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
------------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 2695716864 | 2571 MB | 753696768 | 719 MB | 3449413632 | 3290 MB | 3450101760 | 3290 MB
</code></pre>
<p>Here we see that the table segments grow massively (+37%), which is called &lsquo;bloat&rsquo; in PostgreSQL terminology. The MVCC implementation of PostgreSQL stores both the old and never new version of the row &lsquo;in-place&rsquo; directly in the table, in contrast to MariaDB/MySQL which stores the old version in UNDO space and the new row &lsquo;in-place&rsquo;. The index file also increases significantly (+100%). We need to do more research to find out why this is the case. In addition, a &lsquo;free space map&rsquo; is created again (difference between <code>tot_rel_siz</code> and <code>tab_and_idx_siz</code>).</p>
<p>A subsequent <code>VACUUM FULL</code> reduces the table (to 28%) and the index (to 50%) again in relation to the previous size:</p>
<pre><code>postgres=# VACUUM FULL tracking;

postgres=# SELECT pg_relation_size('tracking') AS tab_siz
 , pg_size_pretty(pg_relation_size('tracking')) AS tab_siz_prtty
 , pg_indexes_size('tracking') AS idx_siz
 , pg_size_pretty(pg_indexes_size('tracking')) AS idx_siz_prtty
 , pg_relation_size('tracking') + pg_indexes_size('tracking') AS tab_and_idx_siz
 , pg_size_pretty(pg_relation_size('tracking') + pg_indexes_size('tracking')) AS tab_and_idx_siz_prtty
 , pg_total_relation_size('tracking') AS tot_rel_siz
 , pg_size_pretty(pg_total_relation_size('tracking')) AS tot_rel_siz_prtty
;
 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
</code></pre>
<p>and also in relation to the original size, the table (to 38%) and the index (to 100%) become smaller again. Why the index has remained the same size and only the table has shrunk remains to be investigated&hellip;</p>
<h3>Experiment 2: Deleting the columns<a class="anchor-link" id="experiment-2-deleting-the-columns"></a></h3>
<p>The columns are then dropped with <code>DROP COLUMN</code>.</p>
<pre><code>postgres=# ALTER TABLE tracking
 DROP COLUMN d0, DROP COLUMN d1, DROP COLUMN d2, DROP COLUMN d3, DROP COLUMN d4
, DROP COLUMN d5, DROP COLUMN d6, DROP COLUMN d7, DROP COLUMN d8, DROP COLUMN d9
;
</code></pre>
<p>As the response was immediate, it can be assumed that this operation is also instantaneous. Unfortunately, I couldn&rsquo;t find anything about this in the PostgreSQL documentation.</p>
<p>Nothing has changed significantly in terms of size, which is actually to be expected with an instant operation. However, the fact that the size did not change after the <code>VACUUM FULL</code> command was a little surprising:</p>
<pre><code> tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1120010240 | 1068 MB

postgres=# VACUUM FULL tracking;

 tab_siz | tab_siz_prtty | idx_siz | idx_siz_prtty | tab_and_idx_siz | tab_and_idx_siz_prtty | tot_rel_siz | tot_rel_siz_prtty
-----------+---------------+-----------+---------------+-----------------+-----------------------+-------------+-------------------
 742916096 | 709 MB | 376864768 | 359 MB | 1119780864 | 1068 MB | 1119780864 | 1068 MB
</code></pre>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>Locking in PostgreSQL works as follows:</p>
<ul>
<li><code>VACUUM</code> Concurrent DML commands are possible similar to the MariaDB/MySQL <code>OPTIMIZE TABLE</code> command. However, the result is not quite the same.</li>
<li><code>VACUUM FULL</code> causes an <code>ACCESS EXCLUSIVE</code> lock. Similar to the MariaDB/MySQL 5.5 and older <code>OPTIMIZE TABLE</code> command. DML and <code>SELECT</code> commands are NOT permitted.</li>
</ul>
<h2>Sources<a class="anchor-link" id="sources"></a></h2>
<ul>
<li><a href="https://neon.com/postgresql/postgresql-administration/postgresql-database-indexes-table-size" target="_blank">How to Get Sizes of Database Objects in PostgreSQL</a></li>
<li><a href="https://www.postgresql.org/docs/current/storage-file-layout.html" target="_blank">Database File Layout</a></li>
<li><a href="https://www.postgresql.org/docs/current/functions-admin.html" target="_blank">System Administration Functions</a></li>
<li><a href="https://www.postgresql.org/docs/current/sql-cluster.html" target="_blank">CLUSTER</a></li>
<li><a href="https://www.postgresql.org/docs/current/sql-vacuum.html" target="_blank">VACUUM</a></li>
<li><a href="https://www.postgresql.org/docs/current/explicit-locking.html" target="_blank">Explicit Locking</a></li>
</ul>
<h2>Additional attempts<a class="anchor-link" id="additional-attempts"></a></h2>
<ol>
<li>Instead of <code>0.0</code>, <code>NULL</code> was filled into the columns <code>d0</code> &ndash; <code>d9</code>. The table remained small (<code>tot_rel_siz_prtty = 1068 MB</code>). It is therefore also worth saving <code>NULL</code> instead of dummy values with PostgreSQL.</li>
<li>The columns <code>d0</code> &ndash; <code>d9</code> were created with <code>DOUBLE PRECISION NOT NULL</code> and the values <code>0.0</code> were filled. No effect: The table remained large (<code>tot_rel_siz_prtty = 2232 MB</code>).</li>
</ol>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/how-much-space-does-null-need/">How much space does NULL need?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Someone is deleting my shared memory segments!</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/" />
      <id>https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/</id>
      <updated>2026-02-08T05:27:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When we work with PostgreSQL under our myEnv, we regularly get shared memory segment errors. Example:<br />
psql: error: connection to server on socket \"/tmp/.s.PGSQL.5433\" failed:<br />
FATAL: could not open shared memory segment \"/PostgreSQL.4220847662\":<br />
No such file or directory<br />
or we see similar messages in the PostgreSQL error log:<br />
ERROR: could not open shared memory segment \"/PostgreSQL.4220847662\":<br />
No such file or directory<br />
Because I am a MariaDB/MySQL admin, I am not very familiar with shared memory problems (MariaDB/MySQL does not work with shared memory). Fortunately, a search on the Internet led us on the right track (source). It is noted there:</p>
<p>The documentation of systemd states that this only happens for<br />
non-system users. Can you check whether your “postgres” user (or<br />
whatever you are using) is a system user?</p>
<p>Linux System User<br />
First I had to find out what a system user under Linux actually is. I found an answer here: What’s the difference between a normal user and a system user?.</p>
<p>That is not a technical difference but an organizational decision. E.g. it makes sense to show normal users in a login dialog (so that you can click them instead of having to type the user name) but it wouldn’t to show system accounts (the UIDs under which daemons and other automatic processes run) there.</p>
<p>The LSB standard says: User ID Ranges:</p>
<p>The system User IDs from 0 to 99 should be statically allocated by the system, and shall not be created by applications.<br />
The system User IDs from 100 to 499 should be reserved for dynamic allocation by system administrators and post install scripts using useradd.</p>
<p>On my Ubuntu system it looks like this:<br />
$ grep SYS_ /etc/login.defs<br />
#SYS_UID_MIN 100<br />
#SYS_UID_MAX 999<br />
#SYS_GID_MIN 100<br />
#SYS_GID_MAX 999<br />
This would be correct for the PostgreSQL user:<br />
$ id postgres<br />
uid=130(postgres) gid=142(postgres) groups=142(postgres),116(ssl-cert)<br />
But since the PostgreSQL instance in question runs under our myEnv, that’s different:<br />
$ id dba<br />
uid=1001(dba) gid=1001(dba) groups=1001(dba)<br />
So now we have two options:</p>
<p>We change SYS_UID_MAX and SYS_GID_MAX to 1001 (simple variant).<br />
Or we change the UID and the GID of our user dba to less than 1000.</p>
<p>Simple variant: Change SYS_UID_MAX and SYS_GID_MAX to 1001<br />
# /etc/login.defs<br />
SYS_UID_MAX 1001<br />
SYS_GID_MAX 1001<br />
To be on the safe side, the machine was rebooted. But that did not help: After a short time, the same errors occur again.<br />
More complicated variant: Changing the UID from 1001 to 990<br />
$ cat /etc/passwd<br />
...<br />
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin<br />
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin<br />
tomcat:x:999:999:Apache Tomcat:/:/sbin/nologin<br />
oli:x:1000:1000:Oli Sennhauser,,,:/home/oli:/bin/bash<br />
dba:x:1001:1001:DBA user:/home/dba:/bin/bash<br />
...</p>
<p>$ id dba<br />
uid=1001(dba) gid=1001(dba) groups=1001(dba)<br />
To do this, all processes of this user must be stopped!<br />
$ usermod --uid 990 dba<br />
$ groupmod --gid 990 dba</p>
<p>$ find / -user 1001 -exec chown --no-dereference dba {} ;<br />
$ find / -group 1001 -exec chgrp --no-dereference dba {} ;<br />
This seems to have solved the problem!<br />
Additional information<br />
A source mentioned above also recommended setting the following parameters in systemd-logind:<br />
# /etc/systemd/system/systemd-logind.service.d/override.conf<br />
RemoveIPC=no<br />
RuntimeDirectorySize=1%<br />
However, this measure is no longer necessary as it works without this change.<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/">Someone is deleting my shared memory segments!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When we work with PostgreSQL under our <a href="https://www.fromdual.com/myenv/">myEnv</a>, we regularly get shared memory segment errors. Example:</p>
<pre><code>psql: error: connection to server on socket "/tmp/.s.PGSQL.5433" failed:
FATAL: could not open shared memory segment "/PostgreSQL.4220847662":
No such file or directory
</code></pre>
<p>or we see similar messages in the PostgreSQL error log:</p>
<pre><code>ERROR: could not open shared memory segment "/PostgreSQL.4220847662":
No such file or directory
</code></pre>
<p>Because I am a MariaDB/MySQL admin, I am not very familiar with shared memory problems (MariaDB/MySQL does not work with shared memory). Fortunately, a search on the Internet led us on the right track (<a href="https://www.postgresql.org/message-id/56A52018.1030001%40gmx.net" target="_blank" title="Re: systemd deletes shared memory segment in /dev/shm/Postgresql.NNNNNN">source</a>). It is noted there:</p>
<blockquote>
<p>The documentation of systemd states that this only happens for<br>
non-system users. Can you check whether your &ldquo;postgres&rdquo; user (or<br>
whatever you are using) is a system user?</p>
</blockquote>
<h2>Linux System User<a class="anchor-link" id="linux-system-user"></a></h2>
<p>First I had to find out what a system user under Linux actually is. I found an answer here: <a href="https://unix.stackexchange.com/questions/80277/whats-the-difference-between-a-normal-user-and-a-system-user" target="_blank">What&rsquo;s the difference between a normal user and a system user?</a>.</p>
<blockquote>
<p>That is not a technical difference but an organizational decision. E.g. it makes sense to show normal users in a login dialog (so that you can click them instead of having to type the user name) but it wouldn&rsquo;t to show system accounts (the UIDs under which daemons and other automatic processes run) there.</p>
</blockquote>
<p>The LSB standard says: <a href="https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/uidrange.html" target="_blank">User ID Ranges</a>:</p>
<blockquote>
<p>The system User IDs from 0 to 99 should be statically allocated by the system, and shall not be created by applications.<br>
The system User IDs from 100 to 499 should be reserved for dynamic allocation by system administrators and post install scripts using useradd.</p>
</blockquote>
<p>On my Ubuntu system it looks like this:</p>
<pre><code>$ grep SYS_ /etc/login.defs
#SYS_UID_MIN 100
#SYS_UID_MAX 999
#SYS_GID_MIN 100
#SYS_GID_MAX 999
</code></pre>
<p>This would be correct for the PostgreSQL user:</p>
<pre><code>$ id postgres
uid=130(postgres) gid=142(postgres) groups=142(postgres),116(ssl-cert)
</code></pre>
<p>But since the PostgreSQL instance in question runs under our <a href="https://www.fromdual.com/myenv/">myEnv</a>, that&rsquo;s different:</p>
<pre><code>$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
</code></pre>
<p>So now we have two options:</p>
<ol>
<li>We change <code>SYS_UID_MAX</code> and <code>SYS_GID_MAX</code> to 1001 (simple variant).</li>
<li>Or we change the <code>UID</code> and the <code>GID</code> of our user <code>dba</code> to less than 1000.</li>
</ol>
<h2>Simple variant: Change <code>SYS_UID_MAX</code> and <code>SYS_GID_MAX</code> to 1001<a class="anchor-link" id="simple-variant-change-sys_uid_max-and-sys_gid_max-to-1001"></a></h2>
<pre><code># /etc/login.defs
SYS_UID_MAX 1001
SYS_GID_MAX 1001
</code></pre>
<p>To be on the safe side, the machine was rebooted. But that did not help: After a short time, the same errors occur again.</p>
<h2>More complicated variant: Changing the <code>UID</code> from 1001 to 990<a class="anchor-link" id="more-complicated-variant-changing-the-uid-from-1001-to-990"></a></h2>
<pre><code>$ cat /etc/passwd
...
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
tomcat:x:999:999:Apache Tomcat:/:/sbin/nologin
oli:x:1000:1000:Oli Sennhauser,,,:/home/oli:/bin/bash
dba:x:1001:1001:DBA user:/home/dba:/bin/bash
...

$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
</code></pre>
<p>To do this, all processes of this user must be stopped!</p>
<pre><code>$ usermod --uid 990 dba
$ groupmod --gid 990 dba

$ find / -user 1001 -exec chown --no-dereference dba {} ;
$ find / -group 1001 -exec chgrp --no-dereference dba {} ;
</code></pre>
<p>This seems to have solved the problem!</p>
<h2>Additional information<a class="anchor-link" id="additional-information"></a></h2>
<p>A source mentioned above also recommended setting the following parameters in <code>systemd-logind</code>:</p>
<pre><code># /etc/systemd/system/systemd-logind.service.d/override.conf
RemoveIPC=no
RuntimeDirectorySize=1%
</code></pre>
<p>However, this measure is no longer necessary as it works without this change.</p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/">Someone is deleting my shared memory segments!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Someone is deleting my shared memory segments!</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/" />
      <id>https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/</id>
      <updated>2026-02-08T05:27:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When we work with PostgreSQL under our myEnv, we regularly get shared memory segment errors. Example:<br />
psql: error: connection to server on socket \"/tmp/.s.PGSQL.5433\" failed:<br />
FATAL: could not open shared memory segment \"/PostgreSQL.4220847662\":<br />
No such file or directory<br />
or we see similar messages in the PostgreSQL error log:<br />
ERROR: could not open shared memory segment \"/PostgreSQL.4220847662\":<br />
No such file or directory<br />
Because I am a MariaDB/MySQL admin, I am not very familiar with shared memory problems (MariaDB/MySQL does not work with shared memory). Fortunately, a search on the Internet led us on the right track (source). It is noted there:</p>
<p>The documentation of systemd states that this only happens for<br />
non-system users. Can you check whether your “postgres” user (or<br />
whatever you are using) is a system user?</p>
<p>Linux System User<br />
First I had to find out what a system user under Linux actually is. I found an answer here: What’s the difference between a normal user and a system user?.</p>
<p>That is not a technical difference but an organizational decision. E.g. it makes sense to show normal users in a login dialog (so that you can click them instead of having to type the user name) but it wouldn’t to show system accounts (the UIDs under which daemons and other automatic processes run) there.</p>
<p>The LSB standard says: User ID Ranges:</p>
<p>The system User IDs from 0 to 99 should be statically allocated by the system, and shall not be created by applications.<br />
The system User IDs from 100 to 499 should be reserved for dynamic allocation by system administrators and post install scripts using useradd.</p>
<p>On my Ubuntu system it looks like this:<br />
$ grep SYS_ /etc/login.defs<br />
#SYS_UID_MIN 100<br />
#SYS_UID_MAX 999<br />
#SYS_GID_MIN 100<br />
#SYS_GID_MAX 999<br />
This would be correct for the PostgreSQL user:<br />
$ id postgres<br />
uid=130(postgres) gid=142(postgres) groups=142(postgres),116(ssl-cert)<br />
But since the PostgreSQL instance in question runs under our myEnv, that’s different:<br />
$ id dba<br />
uid=1001(dba) gid=1001(dba) groups=1001(dba)<br />
So now we have two options:</p>
<p>We change SYS_UID_MAX and SYS_GID_MAX to 1001 (simple variant).<br />
Or we change the UID and the GID of our user dba to less than 1000.</p>
<p>Simple variant: Change SYS_UID_MAX and SYS_GID_MAX to 1001<br />
# /etc/login.defs<br />
SYS_UID_MAX 1001<br />
SYS_GID_MAX 1001<br />
To be on the safe side, the machine was rebooted. But that did not help: After a short time, the same errors occur again.<br />
More complicated variant: Changing the UID from 1001 to 990<br />
$ cat /etc/passwd<br />
...<br />
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin<br />
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin<br />
tomcat:x:999:999:Apache Tomcat:/:/sbin/nologin<br />
oli:x:1000:1000:Oli Sennhauser,,,:/home/oli:/bin/bash<br />
dba:x:1001:1001:DBA user:/home/dba:/bin/bash<br />
...</p>
<p>$ id dba<br />
uid=1001(dba) gid=1001(dba) groups=1001(dba)<br />
To do this, all processes of this user must be stopped!<br />
$ usermod --uid 990 dba<br />
$ groupmod --gid 990 dba</p>
<p>$ find / -user 1001 -exec chown --no-dereference dba {} ;<br />
$ find / -group 1001 -exec chgrp --no-dereference dba {} ;<br />
This seems to have solved the problem!<br />
Additional information<br />
A source mentioned above also recommended setting the following parameters in systemd-logind:<br />
# /etc/systemd/system/systemd-logind.service.d/override.conf<br />
RemoveIPC=no<br />
RuntimeDirectorySize=1%<br />
However, this measure is no longer necessary as it works without this change.<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/">Someone is deleting my shared memory segments!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When we work with PostgreSQL under our <a href="https://www.fromdual.com/myenv/">myEnv</a>, we regularly get shared memory segment errors. Example:</p>
<pre><code>psql: error: connection to server on socket "/tmp/.s.PGSQL.5433" failed:
FATAL: could not open shared memory segment "/PostgreSQL.4220847662":
No such file or directory
</code></pre>
<p>or we see similar messages in the PostgreSQL error log:</p>
<pre><code>ERROR: could not open shared memory segment "/PostgreSQL.4220847662":
No such file or directory
</code></pre>
<p>Because I am a MariaDB/MySQL admin, I am not very familiar with shared memory problems (MariaDB/MySQL does not work with shared memory). Fortunately, a search on the Internet led us on the right track (<a href="https://www.postgresql.org/message-id/56A52018.1030001%40gmx.net" target="_blank" title="Re: systemd deletes shared memory segment in /dev/shm/Postgresql.NNNNNN">source</a>). It is noted there:</p>
<blockquote>
<p>The documentation of systemd states that this only happens for<br>
non-system users. Can you check whether your &ldquo;postgres&rdquo; user (or<br>
whatever you are using) is a system user?</p>
</blockquote>
<h2>Linux System User<a class="anchor-link" id="linux-system-user"></a></h2>
<p>First I had to find out what a system user under Linux actually is. I found an answer here: <a href="https://unix.stackexchange.com/questions/80277/whats-the-difference-between-a-normal-user-and-a-system-user" target="_blank">What&rsquo;s the difference between a normal user and a system user?</a>.</p>
<blockquote>
<p>That is not a technical difference but an organizational decision. E.g. it makes sense to show normal users in a login dialog (so that you can click them instead of having to type the user name) but it wouldn&rsquo;t to show system accounts (the UIDs under which daemons and other automatic processes run) there.</p>
</blockquote>
<p>The LSB standard says: <a href="https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/uidrange.html" target="_blank">User ID Ranges</a>:</p>
<blockquote>
<p>The system User IDs from 0 to 99 should be statically allocated by the system, and shall not be created by applications.<br>
The system User IDs from 100 to 499 should be reserved for dynamic allocation by system administrators and post install scripts using useradd.</p>
</blockquote>
<p>On my Ubuntu system it looks like this:</p>
<pre><code>$ grep SYS_ /etc/login.defs
#SYS_UID_MIN 100
#SYS_UID_MAX 999
#SYS_GID_MIN 100
#SYS_GID_MAX 999
</code></pre>
<p>This would be correct for the PostgreSQL user:</p>
<pre><code>$ id postgres
uid=130(postgres) gid=142(postgres) groups=142(postgres),116(ssl-cert)
</code></pre>
<p>But since the PostgreSQL instance in question runs under our <a href="https://www.fromdual.com/myenv/">myEnv</a>, that&rsquo;s different:</p>
<pre><code>$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
</code></pre>
<p>So now we have two options:</p>
<ol>
<li>We change <code>SYS_UID_MAX</code> and <code>SYS_GID_MAX</code> to 1001 (simple variant).</li>
<li>Or we change the <code>UID</code> and the <code>GID</code> of our user <code>dba</code> to less than 1000.</li>
</ol>
<h2>Simple variant: Change <code>SYS_UID_MAX</code> and <code>SYS_GID_MAX</code> to 1001<a class="anchor-link" id="simple-variant-change-sys_uid_max-and-sys_gid_max-to-1001"></a></h2>
<pre><code># /etc/login.defs
SYS_UID_MAX 1001
SYS_GID_MAX 1001
</code></pre>
<p>To be on the safe side, the machine was rebooted. But that did not help: After a short time, the same errors occur again.</p>
<h2>More complicated variant: Changing the <code>UID</code> from 1001 to 990<a class="anchor-link" id="more-complicated-variant-changing-the-uid-from-1001-to-990"></a></h2>
<pre><code>$ cat /etc/passwd
...
polkitd:x:997:997:User for polkitd:/:/usr/sbin/nologin
systemd-coredump:x:998:998:systemd Core Dumper:/:/usr/sbin/nologin
tomcat:x:999:999:Apache Tomcat:/:/sbin/nologin
oli:x:1000:1000:Oli Sennhauser,,,:/home/oli:/bin/bash
dba:x:1001:1001:DBA user:/home/dba:/bin/bash
...

$ id dba
uid=1001(dba) gid=1001(dba) groups=1001(dba)
</code></pre>
<p>To do this, all processes of this user must be stopped!</p>
<pre><code>$ usermod --uid 990 dba
$ groupmod --gid 990 dba

$ find / -user 1001 -exec chown --no-dereference dba {} ;
$ find / -group 1001 -exec chgrp --no-dereference dba {} ;
</code></pre>
<p>This seems to have solved the problem!</p>
<h2>Additional information<a class="anchor-link" id="additional-information"></a></h2>
<p>A source mentioned above also recommended setting the following parameters in <code>systemd-logind</code>:</p>
<pre><code># /etc/systemd/system/systemd-logind.service.d/override.conf
RemoveIPC=no
RuntimeDirectorySize=1%
</code></pre>
<p>However, this measure is no longer necessary as it works without this change.</p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/postgresql/deleted-postgresql-shared-memory-segments/">Someone is deleting my shared memory segments!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Load CSV files into the database</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/load-csv-files-into-the-database/" />
      <id>https://www.fromdual.com/blog/load-csv-files-into-the-database/</id>
      <updated>2026-02-06T17:04:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Recently, I wanted to display the places of residence of the members of my club on a map for a personal gimmick (IGOC members). I knew the addresses of the club members. But not the coordinates of their places of residence.<br />
So I went in search of the coordinates and found what I was looking for at the Federal Office of Topography (swisstopo).<br />
The data is available there as a CSV file. Details here: Swiss town coordinates.<br />
How do I load this data into a database?<br />
Loading the data with MariaDB/MySQL<br />
MariaDB and MySQL have the LOAD DATA INFILE command:<br />
SQL &#62; DROP TABLE IF EXISTS wgs84;</p>
<p>SQL &#62; -- SET GLOBAL local_infile = ON; -- Only needed with MySQL</p>
<p>SQL &#62; CREATE TABLE wgs84 (<br />
 ortschaftsname VARCHAR(32)<br />
, plz4 SMALLINT<br />
, zusatzziffer SMALLINT<br />
, zip_id SMALLINT UNSIGNED<br />
, gemeindename VARCHAR(32)<br />
, bfs_nr SMALLINT<br />
, kantonskuerzel CHAR(2)<br />
, adressenanteil varchar(8)<br />
, e DOUBLE<br />
, n DOUBLE<br />
, sprache VARCHAR(8)<br />
, validity VARCHAR(12)<br />
);</p>
<p>SQL &#62; -- TRUNCATE TABLE wgs84;</p>
<p>SQL &#62; LOAD DATA LOCAL INFILE \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
INTO TABLE wgs84<br />
FIELDS TERMINATED BY \';\'<br />
LINES TERMINATED BY \'rn\'<br />
IGNORE 1 LINES<br />
;<br />
Query OK, 5713 rows affected<br />
Records: 5713 Deleted: 0 Skipped: 0 Warnings: 0<br />
You can then query the data in the database:<br />
SQL &#62; SELECT * FROM wgs84 ORDER BY ortschaftsname LIMIT 5;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
&#124; ortschaftsname &#124; plz4 &#124; zusatzziffer &#124; zip_id &#124; gemeindename &#124; bfs_nr &#124; kantonskuerzel &#124; adressenanteil &#124; e &#124; n &#124; sprache &#124; validity &#124;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
&#124; Aadorf &#124; 8355 &#124; 0 &#124; 4672 &#124; Aadorf &#124; 4551 &#124; TG &#124; 96.802 % &#124; 8.903193007810433 &#124; 47.491079014637265 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aadorf &#124; 8355 &#124; 0 &#124; 4672 &#124; Elgg &#124; 294 &#124; ZH &#124; 3.198 % &#124; 8.89206766645808 &#124; 47.4933781685032 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5000 &#124; 0 &#124; 2913 &#124; Aarau &#124; 4001 &#124; AG &#124; 99.713 % &#124; 8.048148371736266 &#124; 47.38973523857376 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5000 &#124; 0 &#124; 2913 &#124; Suhr &#124; 4012 &#124; AG &#124; 0.287 % &#124; 8.059410934099922 &#124; 47.383298214804334 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5004 &#124; 0 &#124; 2932 &#124; Aarau &#124; 4001 &#124; AG &#124; 100 % &#124; 8.060698546432551 &#124; 47.400587704180744 &#124; de &#124; 2008-07-01 &#124;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
5 rows in set<br />
Or something more precise:<br />
SQL &#62; SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat<br />
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);<br />
+-------------+-----------+-------------------+--------------------+<br />
&#124; city &#124; city_code &#124; lon &#124; lat &#124;<br />
+-------------+-----------+-------------------+--------------------+<br />
&#124; Aarburg &#124; 4663 &#124; 7.904271716719409 &#124; 47.321443418782955 &#124;<br />
&#124; Aarburg &#124; 4663 &#124; 7.889249714098425 &#124; 47.313536073562474 &#124;<br />
&#124; Aarburg &#124; 4663 &#124; 7.880309179095798 &#124; 47.31255194439023 &#124;<br />
&#124; Adligenswil &#124; 6043 &#124; 8.364849060491428 &#124; 47.07037816052481 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.173740257895282 &#124; 47.64491046067056 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.159171428030783 &#124; 47.654149879509134 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.204470741840725 &#124; 47.639949130372145 &#124;<br />
+-------------+-----------+-------------------+--------------------+<br />
7 rows in set (0.003 sec)<br />
I will leave it to the reader to clean out the duplicates… :-)<br />
So far so good, now to the finer points::<br />
Differences between MariaDB and MySQL<br />
The procedure described above works perfectly with MariaDB 11.4 and 11.8. There are small differences with MySQL 8.4:<br />
The first error message that prevents loading is this one:<br />
ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides<br />
It can be bypassed relatively easily with the command:<br />
SQL &#62; SET GLOBAL local_infile = ON;<br />
The next attempt will fail as follows:<br />
ERROR 2068 (HY000): LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.<br />
This problem can be solved by starting the MySQL client as follows:<br />
$ mysql --local-infile=1 --user=root test<br />
Sources</p>
<p>MariaDB: LOAD DATA INFILE<br />
MySQL: LOAD DATA Statement</p>
<p>Loading the data with PostgreSQL<br />
PostgreSQL has the command COPY ... FROM:<br />
postgres=# DROP TABLE IF EXISTS wgs84;</p>
<p>postgres=# CREATE TABLE wgs84 (<br />
 ortschaftsname VARCHAR(32)<br />
, plz4 SMALLINT<br />
, zusatzziffer SMALLINT<br />
, zip_id INT<br />
, gemeindename VARCHAR(32)<br />
, bfs_nr SMALLINT<br />
, kantonskuerzel CHAR(2)<br />
, adressenanteil varchar(8)<br />
, e DOUBLE PRECISION<br />
, n DOUBLE PRECISION<br />
, sprache VARCHAR(8)<br />
, validity VARCHAR(12)<br />
);</p>
<p>postgres=# -- TRUNCATE TABLE wgs84;</p>
<p>postgres=# COPY wgs84<br />
FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
DELIMITER \';\'<br />
CSV HEADER<br />
;<br />
COPY 5713<br />
Here, too, we receive the result as expected in the usual PostgreSQL form:<br />
postgres=# SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat<br />
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);<br />
 city &#124; city_code &#124; lon &#124; lat<br />
-------------+-----------+-------------------+--------------------<br />
 Aarburg &#124; 4663 &#124; 7.904271716719409 &#124; 47.321443418782955<br />
 Aarburg &#124; 4663 &#124; 7.889249714098425 &#124; 47.313536073562474<br />
 Aarburg &#124; 4663 &#124; 7.880309179095798 &#124; 47.31255194439023<br />
 Adligenswil &#124; 6043 &#124; 8.36487538940682 &#124; 47.07037794822416<br />
 Kreuzlingen &#124; 8280 &#124; 9.173740257895282 &#124; 47.64491046067056<br />
 Kreuzlingen &#124; 8280 &#124; 9.159171428030783 &#124; 47.654149879509134<br />
 Kreuzlingen &#124; 8280 &#124; 9.204470741840725 &#124; 47.639949130372145<br />
(7 rows)<br />
Sources</p>
<p>PostgreSQL: COPY</p>
<p>Small differences between MariaDB/MySQL and PostgreSQL<br />
Basically, the load command is completely different in the two database worlds.<br />
With MariaDB and PostgreSQL, the commands run “out-of-the-box”. MySQL has two additional security hurdles built in here.<br />
PostgreSQL does not recognise UNSIGNED integer data types, so the next largest data type (INT) must be used, which is a little less space-saving than with MariaDB/MySQL.<br />
Remarks<br />
When we did the same test a few days ago, there was still a loading error. So it seems that the data source has also changed slightly…<br />
I have not found out quickly whether there is an SQL standard for these load commands and if so, whether MariaDB/MySQL or PostgreSQL are standard-compliant here.<br />
And of course there are other ways to get your CSV data into the database…<br />
The tools mariadb-import/mysqlimport are used if you want to do this from the command line. The CSV Storage Engine can also be misused for this purpose (see here for details). An officially supported variant is the MariaDB CONNECT Storage Engine with the CSV type (see here):<br />
SQL &#62; INSTALL SONAME \'ha_connect\';</p>
<p>SQL &#62; CREATE TABLE wgs84_fdw<br />
ENGINE = CONNECT<br />
table_type = CSV<br />
file_name=\'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
header = 1<br />
sep_char = \';\'<br />
quoted = 0;</p>
<p>SQL &#62; INSERT INTO wgs84 SELECT * FROM wgs84_fdw;<br />
Unfortunately, it looks like the CONNECT Storage Engine will no longer be supported by MariaDB! And the mydumper/myloader tool also seems to be able to handle CSV files.<br />
And of course the whole thing can also be solved using applications…<br />
With PostgreSQL there are the following options:<br />
postgres=# copy wgs84 FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\' DELIMITER \';\' CSV HEADER<br />
then from the shell:<br />
$ psql --user=dba -c \"copy wgs84 FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\' DELIMITER \';\' CSV HEADER\"<br />
And the variant via the Foreign Data Wrapper (FWD). But I have not tried this:<br />
postgres=# CREATE EXTENSION postgres_fdw;</p>
<p>postgres=# CREATE SERVER foreign_server<br />
 FOREIGN DATA WRAPPER postgres_fdw<br />
 OPTIONS (<br />
 datasource \'CSV:/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\',<br />
 format \'CSV\'<br />
 )<br />
;</p>
<p>postgres=# CREATE USER MAPPING FOR local_user<br />
 SERVER foreign_server<br />
 OPTIONS (user \'foreign_user\', password \'password\')<br />
;</p>
<p>postgres=# CREATE FOREIGN TABLE foreign_table (<br />
 id integer NOT NULL,<br />
 data text<br />
)<br />
 SERVER foreign_server<br />
 OPTIONS (schema_name \'some_schema\', table_name \'some_table\')<br />
;<br />
Addendum<br />
The MariaDB/MySQL data type DOUBLE is called DOUBLE PRECISION in ProsgreSQL.<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Recently, I wanted to display the places of residence of the members of my club on a map for a personal gimmick (<a href="https://www.shinguz.ch/computer/gis/igoc-mitglieder/" target="_blank">IGOC members</a>). I knew the addresses of the club members. But not the coordinates of their places of residence.</p>
<p>So I went in search of the coordinates and found what I was looking for at the Federal Office of Topography (<a href="https://www.swisstopo.admin.ch/en" target="_blank">swisstopo</a>).</p>
<p>The data is available there as a CSV file. Details here: <a href="https://www.shinguz.ch/computer/gis/schweizer-ortschafts-koordinaten/" target="_blank">Swiss town coordinates</a>.</p>
<p>How do I load this data into a database?</p>
<h2>Loading the data with MariaDB/MySQL<a class="anchor-link" id="loading-the-data-with-mariadb-mysql"></a></h2>
<p>MariaDB and MySQL have the <code>LOAD DATA INFILE</code> command:</p>
<pre><code>SQL&gt; DROP TABLE IF EXISTS wgs84;

SQL&gt; -- SET GLOBAL local_infile = ON; -- Only needed with MySQL

SQL&gt; CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id SMALLINT UNSIGNED
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE
, n DOUBLE
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

SQL&gt; -- TRUNCATE TABLE wgs84;

SQL&gt; LOAD DATA LOCAL INFILE '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
INTO TABLE wgs84
FIELDS TERMINATED BY ';'
LINES TERMINATED BY 'rn'
IGNORE 1 LINES
;
Query OK, 5713 rows affected
Records: 5713 Deleted: 0 Skipped: 0 Warnings: 0
</code></pre>
<p>You can then query the data in the database:</p>
<pre><code>SQL&gt; SELECT * FROM wgs84 ORDER BY ortschaftsname LIMIT 5;
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| ortschaftsname | plz4 | zusatzziffer | zip_id | gemeindename | bfs_nr | kantonskuerzel | adressenanteil | e | n | sprache | validity |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| Aadorf | 8355 | 0 | 4672 | Aadorf | 4551 | TG | 96.802 % | 8.903193007810433 | 47.491079014637265 | de | 2008-07-01 |
| Aadorf | 8355 | 0 | 4672 | Elgg | 294 | ZH | 3.198 % | 8.89206766645808 | 47.4933781685032 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Aarau | 4001 | AG | 99.713 % | 8.048148371736266 | 47.38973523857376 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Suhr | 4012 | AG | 0.287 % | 8.059410934099922 | 47.383298214804334 | de | 2008-07-01 |
| Aarau | 5004 | 0 | 2932 | Aarau | 4001 | AG | 100 % | 8.060698546432551 | 47.400587704180744 | de | 2008-07-01 |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
5 rows in set
</code></pre>
<p>Or something more precise:</p>
<pre><code>SQL&gt; SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
+-------------+-----------+-------------------+--------------------+
| city | city_code | lon | lat |
+-------------+-----------+-------------------+--------------------+
| Aarburg | 4663 | 7.904271716719409 | 47.321443418782955 |
| Aarburg | 4663 | 7.889249714098425 | 47.313536073562474 |
| Aarburg | 4663 | 7.880309179095798 | 47.31255194439023 |
| Adligenswil | 6043 | 8.364849060491428 | 47.07037816052481 |
| Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056 |
| Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134 |
| Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145 |
+-------------+-----------+-------------------+--------------------+
7 rows in set (0.003 sec)
</code></pre>
<p>I will leave it to the reader to clean out the duplicates&hellip; &#128578;</p>
<p>So far so good, now to the finer points::</p>
<h3>Differences between MariaDB and MySQL<a class="anchor-link" id="differences-between-mariadb-and-mysql"></a></h3>
<p>The procedure described above works perfectly with MariaDB 11.4 and 11.8. There are small differences with MySQL 8.4:</p>
<p>The first error message that prevents loading is this one:</p>
<pre><code>ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides
</code></pre>
<p>It can be bypassed relatively easily with the command:</p>
<pre><code>SQL&gt; SET GLOBAL local_infile = ON;
</code></pre>
<p>The next attempt will fail as follows:</p>
<pre><code>ERROR 2068 (HY000): LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.
</code></pre>
<p>This problem can be solved by starting the MySQL client as follows:</p>
<pre><code>$ mysql --local-infile=1 --user=root test
</code></pre>
<h3>Sources<a class="anchor-link" id="sources"></a></h3>
<ul>
<li>MariaDB: <a href="https://mariadb.com/docs/server/reference/sql-statements/data-manipulation/inserting-loading-data/load-data-into-tables-or-index/load-data-infile" target="_blank">LOAD DATA INFILE</a></li>
<li>MySQL: <a href="https://dev.mysql.com/doc/refman/8.4/en/load-data.html" target="_blank">LOAD DATA Statement</a></li>
</ul>
<h2>Loading the data with PostgreSQL<a class="anchor-link" id="loading-the-data-with-postgresql"></a></h2>
<p>PostgreSQL has the command <code>COPY ... FROM</code>:</p>
<pre><code>postgres=# DROP TABLE IF EXISTS wgs84;

postgres=# CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id INT
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE PRECISION
, n DOUBLE PRECISION
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

postgres=# -- TRUNCATE TABLE wgs84;

postgres=# COPY wgs84
FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
DELIMITER ';'
CSV HEADER
;
COPY 5713
</code></pre>
<p>Here, too, we receive the result as expected in the usual PostgreSQL form:</p>
<pre><code>postgres=# SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
 city | city_code | lon | lat
-------------+-----------+-------------------+--------------------
 Aarburg | 4663 | 7.904271716719409 | 47.321443418782955
 Aarburg | 4663 | 7.889249714098425 | 47.313536073562474
 Aarburg | 4663 | 7.880309179095798 | 47.31255194439023
 Adligenswil | 6043 | 8.36487538940682 | 47.07037794822416
 Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056
 Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134
 Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145
(7 rows)
</code></pre>
<h3>Sources<a class="anchor-link" id="sources"></a></h3>
<ul>
<li>PostgreSQL: <a href="https://www.postgresql.org/docs/current/sql-copy.html" target="_blank">COPY</a></li>
</ul>
<h2>Small differences between MariaDB/MySQL and PostgreSQL<a class="anchor-link" id="small-differences-between-mariadb-mysql-and-postgresql"></a></h2>
<p>Basically, the load command is completely different in the two database worlds.</p>
<p>With MariaDB and PostgreSQL, the commands run &ldquo;out-of-the-box&rdquo;. MySQL has two additional security hurdles built in here.</p>
<p>PostgreSQL does not recognise <code>UNSIGNED</code> integer data types, so the next largest data type (<code>INT</code>) must be used, which is a little less space-saving than with MariaDB/MySQL.</p>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>When we did the same test a few days ago, there was still a loading error. So it seems that the data source has also changed slightly&hellip;</p>
<p>I have not found out quickly whether there is an SQL standard for these load commands and if so, whether MariaDB/MySQL or PostgreSQL are standard-compliant here.</p>
<p>And of course there are other ways to get your CSV data into the database&hellip;</p>
<p>The tools <code>mariadb-import</code>/<code>mysqlimport</code> are used if you want to do this from the command line. The CSV Storage Engine can also be misused for this purpose (see <a href="https://www.fromdual.com/blog/csv-storage-engine/">here</a> for details). An officially supported variant is the MariaDB CONNECT Storage Engine with the CSV type (see <a href="https://mariadb.com/docs/server/server-usage/storage-engines/connect/connect-table-types/connect-csv-and-fmt-table-types" target="_blank">here</a>):</p>
<pre><code>SQL&gt; INSTALL SONAME 'ha_connect';

SQL&gt; CREATE TABLE wgs84_fdw
ENGINE = CONNECT
table_type = CSV
file_name='/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
header = 1
sep_char = ';'
quoted = 0;

SQL&gt; INSERT INTO wgs84 SELECT * FROM wgs84_fdw;
</code></pre>
<p>Unfortunately, it looks like the CONNECT Storage Engine will no longer be supported by MariaDB! And the <code>mydumper</code>/<code>myloader</code> tool also seems to be able to handle CSV files.</p>
<p>And of course the whole thing can also be solved using applications&hellip;</p>
<p>With PostgreSQL there are the following options:</p>
<pre><code>postgres=# copy wgs84 FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv' DELIMITER ';' CSV HEADER
</code></pre>
<p>then from the shell:</p>
<pre><code>$ psql --user=dba -c "copy wgs84 FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv' DELIMITER ';' CSV HEADER"
</code></pre>
<p>And the variant via the Foreign Data Wrapper (FWD). But I have not tried this:</p>
<pre><code>postgres=# CREATE EXTENSION postgres_fdw;

postgres=# CREATE SERVER foreign_server
 FOREIGN DATA WRAPPER postgres_fdw
 OPTIONS (
 datasource 'CSV:/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv',
 format 'CSV'
 )
;

postgres=# CREATE USER MAPPING FOR local_user
 SERVER foreign_server
 OPTIONS (user 'foreign_user', password 'password')
;

postgres=# CREATE FOREIGN TABLE foreign_table (
 id integer NOT NULL,
 data text
)
 SERVER foreign_server
 OPTIONS (schema_name 'some_schema', table_name 'some_table')
;
</code></pre>
<h2>Addendum<a class="anchor-link" id="addendum"></a></h2>
<p>The MariaDB/MySQL data type <code>DOUBLE</code> is called <code>DOUBLE PRECISION</code> in ProsgreSQL.</p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Load CSV files into the database</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/load-csv-files-into-the-database/" />
      <id>https://www.fromdual.com/blog/load-csv-files-into-the-database/</id>
      <updated>2026-02-06T17:04:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Recently, I wanted to display the places of residence of the members of my club on a map for a personal gimmick (IGOC members). I knew the addresses of the club members. But not the coordinates of their places of residence.<br />
So I went in search of the coordinates and found what I was looking for at the Federal Office of Topography (swisstopo).<br />
The data is available there as a CSV file. Details here: Swiss town coordinates.<br />
How do I load this data into a database?<br />
Loading the data with MariaDB/MySQL<br />
MariaDB and MySQL have the LOAD DATA INFILE command:<br />
SQL &#62; DROP TABLE IF EXISTS wgs84;</p>
<p>SQL &#62; -- SET GLOBAL local_infile = ON; -- Only needed with MySQL</p>
<p>SQL &#62; CREATE TABLE wgs84 (<br />
 ortschaftsname VARCHAR(32)<br />
, plz4 SMALLINT<br />
, zusatzziffer SMALLINT<br />
, zip_id SMALLINT UNSIGNED<br />
, gemeindename VARCHAR(32)<br />
, bfs_nr SMALLINT<br />
, kantonskuerzel CHAR(2)<br />
, adressenanteil varchar(8)<br />
, e DOUBLE<br />
, n DOUBLE<br />
, sprache VARCHAR(8)<br />
, validity VARCHAR(12)<br />
);</p>
<p>SQL &#62; -- TRUNCATE TABLE wgs84;</p>
<p>SQL &#62; LOAD DATA LOCAL INFILE \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
INTO TABLE wgs84<br />
FIELDS TERMINATED BY \';\'<br />
LINES TERMINATED BY \'rn\'<br />
IGNORE 1 LINES<br />
;<br />
Query OK, 5713 rows affected<br />
Records: 5713 Deleted: 0 Skipped: 0 Warnings: 0<br />
You can then query the data in the database:<br />
SQL &#62; SELECT * FROM wgs84 ORDER BY ortschaftsname LIMIT 5;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
&#124; ortschaftsname &#124; plz4 &#124; zusatzziffer &#124; zip_id &#124; gemeindename &#124; bfs_nr &#124; kantonskuerzel &#124; adressenanteil &#124; e &#124; n &#124; sprache &#124; validity &#124;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
&#124; Aadorf &#124; 8355 &#124; 0 &#124; 4672 &#124; Aadorf &#124; 4551 &#124; TG &#124; 96.802 % &#124; 8.903193007810433 &#124; 47.491079014637265 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aadorf &#124; 8355 &#124; 0 &#124; 4672 &#124; Elgg &#124; 294 &#124; ZH &#124; 3.198 % &#124; 8.89206766645808 &#124; 47.4933781685032 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5000 &#124; 0 &#124; 2913 &#124; Aarau &#124; 4001 &#124; AG &#124; 99.713 % &#124; 8.048148371736266 &#124; 47.38973523857376 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5000 &#124; 0 &#124; 2913 &#124; Suhr &#124; 4012 &#124; AG &#124; 0.287 % &#124; 8.059410934099922 &#124; 47.383298214804334 &#124; de &#124; 2008-07-01 &#124;<br />
&#124; Aarau &#124; 5004 &#124; 0 &#124; 2932 &#124; Aarau &#124; 4001 &#124; AG &#124; 100 % &#124; 8.060698546432551 &#124; 47.400587704180744 &#124; de &#124; 2008-07-01 &#124;<br />
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+<br />
5 rows in set<br />
Or something more precise:<br />
SQL &#62; SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat<br />
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);<br />
+-------------+-----------+-------------------+--------------------+<br />
&#124; city &#124; city_code &#124; lon &#124; lat &#124;<br />
+-------------+-----------+-------------------+--------------------+<br />
&#124; Aarburg &#124; 4663 &#124; 7.904271716719409 &#124; 47.321443418782955 &#124;<br />
&#124; Aarburg &#124; 4663 &#124; 7.889249714098425 &#124; 47.313536073562474 &#124;<br />
&#124; Aarburg &#124; 4663 &#124; 7.880309179095798 &#124; 47.31255194439023 &#124;<br />
&#124; Adligenswil &#124; 6043 &#124; 8.364849060491428 &#124; 47.07037816052481 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.173740257895282 &#124; 47.64491046067056 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.159171428030783 &#124; 47.654149879509134 &#124;<br />
&#124; Kreuzlingen &#124; 8280 &#124; 9.204470741840725 &#124; 47.639949130372145 &#124;<br />
+-------------+-----------+-------------------+--------------------+<br />
7 rows in set (0.003 sec)<br />
I will leave it to the reader to clean out the duplicates… :-)<br />
So far so good, now to the finer points::<br />
Differences between MariaDB and MySQL<br />
The procedure described above works perfectly with MariaDB 11.4 and 11.8. There are small differences with MySQL 8.4:<br />
The first error message that prevents loading is this one:<br />
ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides<br />
It can be bypassed relatively easily with the command:<br />
SQL &#62; SET GLOBAL local_infile = ON;<br />
The next attempt will fail as follows:<br />
ERROR 2068 (HY000): LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.<br />
This problem can be solved by starting the MySQL client as follows:<br />
$ mysql --local-infile=1 --user=root test<br />
Sources</p>
<p>MariaDB: LOAD DATA INFILE<br />
MySQL: LOAD DATA Statement</p>
<p>Loading the data with PostgreSQL<br />
PostgreSQL has the command COPY ... FROM:<br />
postgres=# DROP TABLE IF EXISTS wgs84;</p>
<p>postgres=# CREATE TABLE wgs84 (<br />
 ortschaftsname VARCHAR(32)<br />
, plz4 SMALLINT<br />
, zusatzziffer SMALLINT<br />
, zip_id INT<br />
, gemeindename VARCHAR(32)<br />
, bfs_nr SMALLINT<br />
, kantonskuerzel CHAR(2)<br />
, adressenanteil varchar(8)<br />
, e DOUBLE PRECISION<br />
, n DOUBLE PRECISION<br />
, sprache VARCHAR(8)<br />
, validity VARCHAR(12)<br />
);</p>
<p>postgres=# -- TRUNCATE TABLE wgs84;</p>
<p>postgres=# COPY wgs84<br />
FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
DELIMITER \';\'<br />
CSV HEADER<br />
;<br />
COPY 5713<br />
Here, too, we receive the result as expected in the usual PostgreSQL form:<br />
postgres=# SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat<br />
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);<br />
 city &#124; city_code &#124; lon &#124; lat<br />
-------------+-----------+-------------------+--------------------<br />
 Aarburg &#124; 4663 &#124; 7.904271716719409 &#124; 47.321443418782955<br />
 Aarburg &#124; 4663 &#124; 7.889249714098425 &#124; 47.313536073562474<br />
 Aarburg &#124; 4663 &#124; 7.880309179095798 &#124; 47.31255194439023<br />
 Adligenswil &#124; 6043 &#124; 8.36487538940682 &#124; 47.07037794822416<br />
 Kreuzlingen &#124; 8280 &#124; 9.173740257895282 &#124; 47.64491046067056<br />
 Kreuzlingen &#124; 8280 &#124; 9.159171428030783 &#124; 47.654149879509134<br />
 Kreuzlingen &#124; 8280 &#124; 9.204470741840725 &#124; 47.639949130372145<br />
(7 rows)<br />
Sources</p>
<p>PostgreSQL: COPY</p>
<p>Small differences between MariaDB/MySQL and PostgreSQL<br />
Basically, the load command is completely different in the two database worlds.<br />
With MariaDB and PostgreSQL, the commands run “out-of-the-box”. MySQL has two additional security hurdles built in here.<br />
PostgreSQL does not recognise UNSIGNED integer data types, so the next largest data type (INT) must be used, which is a little less space-saving than with MariaDB/MySQL.<br />
Remarks<br />
When we did the same test a few days ago, there was still a loading error. So it seems that the data source has also changed slightly…<br />
I have not found out quickly whether there is an SQL standard for these load commands and if so, whether MariaDB/MySQL or PostgreSQL are standard-compliant here.<br />
And of course there are other ways to get your CSV data into the database…<br />
The tools mariadb-import/mysqlimport are used if you want to do this from the command line. The CSV Storage Engine can also be misused for this purpose (see here for details). An officially supported variant is the MariaDB CONNECT Storage Engine with the CSV type (see here):<br />
SQL &#62; INSTALL SONAME \'ha_connect\';</p>
<p>SQL &#62; CREATE TABLE wgs84_fdw<br />
ENGINE = CONNECT<br />
table_type = CSV<br />
file_name=\'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\'<br />
header = 1<br />
sep_char = \';\'<br />
quoted = 0;</p>
<p>SQL &#62; INSERT INTO wgs84 SELECT * FROM wgs84_fdw;<br />
Unfortunately, it looks like the CONNECT Storage Engine will no longer be supported by MariaDB! And the mydumper/myloader tool also seems to be able to handle CSV files.<br />
And of course the whole thing can also be solved using applications…<br />
With PostgreSQL there are the following options:<br />
postgres=# copy wgs84 FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\' DELIMITER \';\' CSV HEADER<br />
then from the shell:<br />
$ psql --user=dba -c \"copy wgs84 FROM \'/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\' DELIMITER \';\' CSV HEADER\"<br />
And the variant via the Foreign Data Wrapper (FWD). But I have not tried this:<br />
postgres=# CREATE EXTENSION postgres_fdw;</p>
<p>postgres=# CREATE SERVER foreign_server<br />
 FOREIGN DATA WRAPPER postgres_fdw<br />
 OPTIONS (<br />
 datasource \'CSV:/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv\',<br />
 format \'CSV\'<br />
 )<br />
;</p>
<p>postgres=# CREATE USER MAPPING FOR local_user<br />
 SERVER foreign_server<br />
 OPTIONS (user \'foreign_user\', password \'password\')<br />
;</p>
<p>postgres=# CREATE FOREIGN TABLE foreign_table (<br />
 id integer NOT NULL,<br />
 data text<br />
)<br />
 SERVER foreign_server<br />
 OPTIONS (schema_name \'some_schema\', table_name \'some_table\')<br />
;<br />
Addendum<br />
The MariaDB/MySQL data type DOUBLE is called DOUBLE PRECISION in ProsgreSQL.<br />
This page was translated using deepl.com.</p>
<p><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Recently, I wanted to display the places of residence of the members of my club on a map for a personal gimmick (<a href="https://www.shinguz.ch/computer/gis/igoc-mitglieder/" target="_blank">IGOC members</a>). I knew the addresses of the club members. But not the coordinates of their places of residence.</p>
<p>So I went in search of the coordinates and found what I was looking for at the Federal Office of Topography (<a href="https://www.swisstopo.admin.ch/en" target="_blank">swisstopo</a>).</p>
<p>The data is available there as a CSV file. Details here: <a href="https://www.shinguz.ch/computer/gis/schweizer-ortschafts-koordinaten/" target="_blank">Swiss town coordinates</a>.</p>
<p>How do I load this data into a database?</p>
<h2>Loading the data with MariaDB/MySQL<a class="anchor-link" id="loading-the-data-with-mariadb-mysql"></a></h2>
<p>MariaDB and MySQL have the <code>LOAD DATA INFILE</code> command:</p>
<pre><code>SQL&gt; DROP TABLE IF EXISTS wgs84;

SQL&gt; -- SET GLOBAL local_infile = ON; -- Only needed with MySQL

SQL&gt; CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id SMALLINT UNSIGNED
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE
, n DOUBLE
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

SQL&gt; -- TRUNCATE TABLE wgs84;

SQL&gt; LOAD DATA LOCAL INFILE '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
INTO TABLE wgs84
FIELDS TERMINATED BY ';'
LINES TERMINATED BY 'rn'
IGNORE 1 LINES
;
Query OK, 5713 rows affected
Records: 5713 Deleted: 0 Skipped: 0 Warnings: 0
</code></pre>
<p>You can then query the data in the database:</p>
<pre><code>SQL&gt; SELECT * FROM wgs84 ORDER BY ortschaftsname LIMIT 5;
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| ortschaftsname | plz4 | zusatzziffer | zip_id | gemeindename | bfs_nr | kantonskuerzel | adressenanteil | e | n | sprache | validity |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
| Aadorf | 8355 | 0 | 4672 | Aadorf | 4551 | TG | 96.802 % | 8.903193007810433 | 47.491079014637265 | de | 2008-07-01 |
| Aadorf | 8355 | 0 | 4672 | Elgg | 294 | ZH | 3.198 % | 8.89206766645808 | 47.4933781685032 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Aarau | 4001 | AG | 99.713 % | 8.048148371736266 | 47.38973523857376 | de | 2008-07-01 |
| Aarau | 5000 | 0 | 2913 | Suhr | 4012 | AG | 0.287 % | 8.059410934099922 | 47.383298214804334 | de | 2008-07-01 |
| Aarau | 5004 | 0 | 2932 | Aarau | 4001 | AG | 100 % | 8.060698546432551 | 47.400587704180744 | de | 2008-07-01 |
+----------------+------+--------------+--------+--------------+--------+----------------+----------------+-------------------+--------------------+---------+------------+
5 rows in set
</code></pre>
<p>Or something more precise:</p>
<pre><code>SQL&gt; SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
+-------------+-----------+-------------------+--------------------+
| city | city_code | lon | lat |
+-------------+-----------+-------------------+--------------------+
| Aarburg | 4663 | 7.904271716719409 | 47.321443418782955 |
| Aarburg | 4663 | 7.889249714098425 | 47.313536073562474 |
| Aarburg | 4663 | 7.880309179095798 | 47.31255194439023 |
| Adligenswil | 6043 | 8.364849060491428 | 47.07037816052481 |
| Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056 |
| Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134 |
| Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145 |
+-------------+-----------+-------------------+--------------------+
7 rows in set (0.003 sec)
</code></pre>
<p>I will leave it to the reader to clean out the duplicates&hellip; &#128578;</p>
<p>So far so good, now to the finer points::</p>
<h3>Differences between MariaDB and MySQL<a class="anchor-link" id="differences-between-mariadb-and-mysql"></a></h3>
<p>The procedure described above works perfectly with MariaDB 11.4 and 11.8. There are small differences with MySQL 8.4:</p>
<p>The first error message that prevents loading is this one:</p>
<pre><code>ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides
</code></pre>
<p>It can be bypassed relatively easily with the command:</p>
<pre><code>SQL&gt; SET GLOBAL local_infile = ON;
</code></pre>
<p>The next attempt will fail as follows:</p>
<pre><code>ERROR 2068 (HY000): LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.
</code></pre>
<p>This problem can be solved by starting the MySQL client as follows:</p>
<pre><code>$ mysql --local-infile=1 --user=root test
</code></pre>
<h3>Sources<a class="anchor-link" id="sources"></a></h3>
<ul>
<li>MariaDB: <a href="https://mariadb.com/docs/server/reference/sql-statements/data-manipulation/inserting-loading-data/load-data-into-tables-or-index/load-data-infile" target="_blank">LOAD DATA INFILE</a></li>
<li>MySQL: <a href="https://dev.mysql.com/doc/refman/8.4/en/load-data.html" target="_blank">LOAD DATA Statement</a></li>
</ul>
<h2>Loading the data with PostgreSQL<a class="anchor-link" id="loading-the-data-with-postgresql"></a></h2>
<p>PostgreSQL has the command <code>COPY ... FROM</code>:</p>
<pre><code>postgres=# DROP TABLE IF EXISTS wgs84;

postgres=# CREATE TABLE wgs84 (
 ortschaftsname VARCHAR(32)
, plz4 SMALLINT
, zusatzziffer SMALLINT
, zip_id INT
, gemeindename VARCHAR(32)
, bfs_nr SMALLINT
, kantonskuerzel CHAR(2)
, adressenanteil varchar(8)
, e DOUBLE PRECISION
, n DOUBLE PRECISION
, sprache VARCHAR(8)
, validity VARCHAR(12)
);

postgres=# -- TRUNCATE TABLE wgs84;

postgres=# COPY wgs84
FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
DELIMITER ';'
CSV HEADER
;
COPY 5713
</code></pre>
<p>Here, too, we receive the result as expected in the usual PostgreSQL form:</p>
<pre><code>postgres=# SELECT ortschaftsname AS city, plz4 AS city_code, e AS lon, n AS lat
 FROM wgs84 WHERE plz4 IN (8280, 4663, 6043);
 city | city_code | lon | lat
-------------+-----------+-------------------+--------------------
 Aarburg | 4663 | 7.904271716719409 | 47.321443418782955
 Aarburg | 4663 | 7.889249714098425 | 47.313536073562474
 Aarburg | 4663 | 7.880309179095798 | 47.31255194439023
 Adligenswil | 6043 | 8.36487538940682 | 47.07037794822416
 Kreuzlingen | 8280 | 9.173740257895282 | 47.64491046067056
 Kreuzlingen | 8280 | 9.159171428030783 | 47.654149879509134
 Kreuzlingen | 8280 | 9.204470741840725 | 47.639949130372145
(7 rows)
</code></pre>
<h3>Sources<a class="anchor-link" id="sources"></a></h3>
<ul>
<li>PostgreSQL: <a href="https://www.postgresql.org/docs/current/sql-copy.html" target="_blank">COPY</a></li>
</ul>
<h2>Small differences between MariaDB/MySQL and PostgreSQL<a class="anchor-link" id="small-differences-between-mariadb-mysql-and-postgresql"></a></h2>
<p>Basically, the load command is completely different in the two database worlds.</p>
<p>With MariaDB and PostgreSQL, the commands run &ldquo;out-of-the-box&rdquo;. MySQL has two additional security hurdles built in here.</p>
<p>PostgreSQL does not recognise <code>UNSIGNED</code> integer data types, so the next largest data type (<code>INT</code>) must be used, which is a little less space-saving than with MariaDB/MySQL.</p>
<h2>Remarks<a class="anchor-link" id="remarks"></a></h2>
<p>When we did the same test a few days ago, there was still a loading error. So it seems that the data source has also changed slightly&hellip;</p>
<p>I have not found out quickly whether there is an SQL standard for these load commands and if so, whether MariaDB/MySQL or PostgreSQL are standard-compliant here.</p>
<p>And of course there are other ways to get your CSV data into the database&hellip;</p>
<p>The tools <code>mariadb-import</code>/<code>mysqlimport</code> are used if you want to do this from the command line. The CSV Storage Engine can also be misused for this purpose (see <a href="https://www.fromdual.com/blog/csv-storage-engine/">here</a> for details). An officially supported variant is the MariaDB CONNECT Storage Engine with the CSV type (see <a href="https://mariadb.com/docs/server/server-usage/storage-engines/connect/connect-table-types/connect-csv-and-fmt-table-types" target="_blank">here</a>):</p>
<pre><code>SQL&gt; INSTALL SONAME 'ha_connect';

SQL&gt; CREATE TABLE wgs84_fdw
ENGINE = CONNECT
table_type = CSV
file_name='/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv'
header = 1
sep_char = ';'
quoted = 0;

SQL&gt; INSERT INTO wgs84 SELECT * FROM wgs84_fdw;
</code></pre>
<p>Unfortunately, it looks like the CONNECT Storage Engine will no longer be supported by MariaDB! And the <code>mydumper</code>/<code>myloader</code> tool also seems to be able to handle CSV files.</p>
<p>And of course the whole thing can also be solved using applications&hellip;</p>
<p>With PostgreSQL there are the following options:</p>
<pre><code>postgres=# copy wgs84 FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv' DELIMITER ';' CSV HEADER
</code></pre>
<p>then from the shell:</p>
<pre><code>$ psql --user=dba -c "copy wgs84 FROM '/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv' DELIMITER ';' CSV HEADER"
</code></pre>
<p>And the variant via the Foreign Data Wrapper (FWD). But I have not tried this:</p>
<pre><code>postgres=# CREATE EXTENSION postgres_fdw;

postgres=# CREATE SERVER foreign_server
 FOREIGN DATA WRAPPER postgres_fdw
 OPTIONS (
 datasource 'CSV:/tmp/AMTOVZ_CSV_WGS84/AMTOVZ_CSV_WGS84.csv',
 format 'CSV'
 )
;

postgres=# CREATE USER MAPPING FOR local_user
 SERVER foreign_server
 OPTIONS (user 'foreign_user', password 'password')
;

postgres=# CREATE FOREIGN TABLE foreign_table (
 id integer NOT NULL,
 data text
)
 SERVER foreign_server
 OPTIONS (schema_name 'some_schema', table_name 'some_table')
;
</code></pre>
<h2>Addendum<a class="anchor-link" id="addendum"></a></h2>
<p>The MariaDB/MySQL data type <code>DOUBLE</code> is called <code>DOUBLE PRECISION</code> in ProsgreSQL.</p>
<p>This page was translated using <a href="https://www.deepl.com/en/translator" target="_blank">deepl.com</a>.</p>

<p><a href="https://www.fromdual.com/blog/load-csv-files-into-the-database/">Load CSV files into the database</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PGDay and FOSDEM Report from Kai</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/04/pgday-and-fosdem-report-from-kai/" />
      <id>https://percona.community/blog/2026/02/04/pgday-and-fosdem-report-from-kai/</id>
      <updated>2026-02-04T10:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The following thoughts and comments are completely my personal opinion and do not reflect my employers thoughts or beliefs. If you don’t like anything in this post, reach out to me directly, so I can ignore it ;-).</p>
<p><a href="https://percona.community/blog/2026/02/04/pgday-and-fosdem-report-from-kai/">PGDay and FOSDEM Report from Kai</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The following thoughts and comments are completely my personal opinion and do not reflect my employers thoughts or beliefs. If you don&rsquo;t like anything in this post, reach out to me directly, so I can ignore it ;-).</p>
<p>I&rsquo;m currently on the train on my way back home from FOSDEM this year and man, I&rsquo;m exhausted but also happy. Why? Because the PG and FOSDEM community is just crazily awesome. While it&rsquo;s always too much of everything, it&rsquo;s at the same time inspiring to see so many enthusiastic IT nerds in one place, discussing and working on what they love &ndash; technology and engineering challenges.</p>
<h2>PGDay FOSDEM<a class="anchor-link" id="pgday-fosdem"></a></h2>
<p>It all started with the usual PGDay FOSDEM the day before FOSDEM. Just in case &ndash; this has been happening for over 15 years and if you read this as a little blame that you didn&rsquo;t know about it, that&rsquo;s absolutely correct, as you should. It&rsquo;s been a great event as usual: around 150 Postgres enthusiasts collaborating with each other. There was a great set of talks (no recording available, so yes, just join next year to not miss anything), as well as the hallway track conversations.</p>
<p><figure><img decoding="async" width="1542" height="2048" src="https://percona.community/blog/2026/02/pgday-slonik_hu_f2e4f0e89ddaa055.webp" alt="PGDay Kai and Slonik" loading="lazy"></figure>
</p>
<p>I was able and accepted again as a volunteer helping to make the event happen. While you might think, what&rsquo;s special about it, I cannot express my gratitude for being able to help in any way. I simply love it. I&rsquo;m not a great coder and I&rsquo;ve never been one. I&rsquo;m the one that looks at his code from a year ago and questions his technical existence and overall abilities if I should rather do something without touching a keyboard. What I am very well capable of is helping and supporting events. So it was my pleasure and I hope you do feel inspired to do the same next year or at any future event, not only in the Postgres ecosystem but in general. I strongly believe in this: doing good things will get you good things back.</p>
<p>After the wrap up to the PGDay and a great community dinner to collaborate and discuss further, I simply fell completely tired asleep, as the next day and FOSDEM was already waiting.</p>
<h2>FOSDEM Day 1<a class="anchor-link" id="fosdem-day-1"></a></h2>
<p><figure><img decoding="async" width="2268" height="4032" src="https://percona.community/blog/2026/02/fosdem-pgbooth-volunteering_hu_b97f333c090b19b7.webp" alt="PGDay Kai PG Booth Volunteering" loading="lazy"></figure>
</p>
<p>The next day started with volunteering at the Postgres booth. As usual, Saturday was simply crazy. The Postgres swag like hoodies, caps, mugs, shirts, etc. was almost ripped out of our living hands. We had people waiting in line just to be able to get some swag. That fact alone shows how Postgres is viewed outside of the internal PG ecosystem community. How many times I heard the sentence &ldquo;Thanks a lot for the great work you do&rdquo; or &ldquo;Postgres just works.&rdquo; Yeah, we can all argue about the details and scenarios, but what this is about is the overall ease of use. Not everyone has terabytes of data or the most complex HA and replication scenarios on this planet. Some just need a functional and boring database and, in the best case, open source &ndash; and we all know, looking at real open source, not single-vendor owned, Postgres is the king and here to stay.</p>
<p>After all of this, I switched clothes and helped at the Percona booth. This wasn&rsquo;t any less interesting in comparison to the PG booth. How many people stepped by, asking about what we do or thanking us for our projects and that we remained open source even after all these years and so many other companies not withstanding the quick and easy money to go with open-core or closed offerings. That&rsquo;s the reason I&rsquo;m proud to be part of this company. We walk the talk, since 20y and we have no incentive ever changing it. Thanks to Peter Zaitsev and Peter Farkas aka P&sup2; &ndash; for those who know, just know.</p>
<p>Following that I had the pleassure of being the Slonik guide again. What is a Slonik guide you might ask? Slonik, the mascot of Postgres (big blue elephant), needs some help and guidance while walking throught the crowd, as you can barely see anything while inside the costome. As usual, Slonik is a celebraty. Everyone wants a picture and taking their chance to photograph Slonik in the &ldquo;wild&rdquo;. As you can see, even MySQL&rsquo;s Sakila couldn&rsquo;t resist and had to take a picture with Slonik.</p>
<p><figure><img decoding="async" width="1542" height="2048" src="https://percona.community/blog/2026/02/fosdem-slonik_hu_42e8ca0dcb376339.webp" alt="FOSDEM Sakila and Slonik" loading="lazy"></figure>
</p>
<p>If you&rsquo;re wondering, like many others, why Slonik and why an Elephant? <a href="https://learnsql.com/blog/the-history-of-slonik-the-postgresql-elephant-logo/" target="_blank" rel="noopener noreferrer">Click here for some nice written down history lesson</a></p>
<p>After an exciting but also energy-draining day, I enjoyed a Percona crew/team dinner at BrewDog, with some great conversations and good food. <a href="https://www.reddit.com/r/Homebrewing/comments/47icau/brewdog_just_open_sourced_all_their_recipes/" target="_blank" rel="noopener noreferrer">Fun Fact: Did you know that BrewDog is also open source?</a>. I couldn&rsquo;t stay too long &ndash; sorry about that &ndash; but I had another date. The famous Floor Drees organized in tradition another karaoke event that I couldn&rsquo;t miss. As I couldn&rsquo;t make it to earlier versions of it, I definitely wanted to join. What should I say apart from thanks, Floor, for this great tradition. Yes, I had a hard time talking the next day, but damn I had fun singing Swedish, Polish, German, and English songs &ndash; and yes, I most likely misunderstood all of them as usual.</p>
<p>Too many songs for my voice and maybe a &ldquo;soft drink or two&rdquo; later, I felt in my bed like a stone, and couldn&rsquo;t really accept the fact that my alarm clock went off almost five minutes later (at least that&rsquo;s how it felt to me).</p>
<h2>FOSDEM Day 2<a class="anchor-link" id="fosdem-day-2"></a></h2>
<p><figure><img decoding="async" width="1536" height="2048" src="https://percona.community/blog/2026/02/fosdem-perconabooth-volunteering_hu_7d4c304f24a0936a.webp" alt="PGDay Kai PG Booth Volunteering" loading="lazy"></figure>
</p>
<p>No whining helped, just getting up and making myself ready for Day 2 of FOSDEM, which started with another round of volunteering at the Postgres and Percona booths. Both basically matched the previous feedback, apart from a definitely dropped and less crowded space &ndash; seems I wasn&rsquo;t the only one singing last night ;-).</p>
<p>With that, thanks a lot to everyone making this great FOSDEM happen. I&rsquo;ll try now if the Deutsche Bahn restaurant actually works this time, as I need coffee, a big one, maybe two&hellip; See all of you next year again or at another event this year.</p>
<blockquote>
<p>Stay on top of Postgres development without the inbox overwhelm. Explore <a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">hackorum.dev</a> today and share your feedback with us.</p>
</blockquote>

<p><a href="https://percona.community/blog/2026/02/04/pgday-and-fosdem-report-from-kai/">PGDay and FOSDEM Report from Kai</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>New binlog implementation in MariaDB 12.3</title>
      <link rel="alternate" type="text/html" href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/" />
      <id>https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/</id>
      <updated>2026-02-03T19:08:37+02:00</updated>
      <author><name>knielsen</name></author>
      <summary type="html"><![CDATA[<p>I have recently completed a large project to implement a new improved binlog format for MariaDB. The result will be available shortly in the upcoming MariaDB 12.3.1 release. In this article, I will give a short overview of the new binlog implementation. For more details, check the documentation which is in the source tree as… Continue reading New binlog implementation in MariaDB 12.3</p>
<p><a href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/">New binlog implementation in MariaDB 12.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I have recently completed a large project to implement a new improved binlog format for MariaDB. The result will be available shortly in the upcoming MariaDB 12.3.1 release.</p>
<p>In this article, I will give a short overview of the new binlog implementation. For more details, check the documentation which is in the source tree as the file <code>Docs/replication/binlog.md</code>, or here: <a href="https://github.com/MariaDB/server/blob/knielsen_binlog_in_engine/Docs/replication/binlog.md">https://github.com/MariaDB/server/blob/knielsen_binlog_in_engine/Docs/replication/binlog.md</a></p>
<h2>Using the new binlog<a class="anchor-link" id="using-the-new-binlog"></a></h2>
<p>To enable the new binlog, configure the MariaDB server with <code>binlog_storage_engine=innodb</code>.</p>
<p>Additionally, the binlog must itself be enabled as usual using the option <code>log_bin</code>. Note that no argument can be given to the <code>log_bin</code> option (this is to avoid confusion with the meaning of such argument as the name to use for the old binlog format, as the new binlog file names are fixed).</p>
<pre class="wp-block-preformatted">    binlog_storage_engine=innodb
    log_bin</pre>
<p>When the new binlog file is enabled and the server restarted, any old binlog files are no longer available. See the above-referenced documentation for options on how to migrate old binlogs of an existing server.</p>
<h2>Benefits of the new binlog<a class="anchor-link" id="benefits-of-the-new-binlog"></a></h2>
<p>For the user, the new binlog format brings two main benefits.</p>
<p>First, for users that are running with<code> --innodb-flush-log-at-trx-commit</code> set to 2 or 0 for performance reasons, the new binlog will make the binlog crash-safe (when used with InnoDB tables). This means that if the server crashes or the machine loses power, the restarted server will recover itself into a consistent state, including the state of replication and consistency between the binlog and the InnoDB table contents. With the old binlog format, such a crash could easily leave the binlog in a different state than the InnoDB table data, which then causes replication slaves to diverge from the master. To have the old binlog be crash-safe required setting both <code>--sync-binlog=1</code> and <code>--innodb-flush-log-at-trx-commit=1</code>.</p>
<p>Second, for users that are running with <code>--innodb-flush-log-at-trx-commit</code> set to 1 because they need durability of commits, the new binlog will provide a large speedup of the time taken to commit. Because the new binlog is integrated with InnoDB, only half as many flushes to disk of buffers are needed per commit as with the old binlog.</p>
<p>Thus, the primary user-visible benefits of the new binlog is greatly improved speedup of transaction commits.</p>
<p>The speedup that will be obtained will be completely dependent on the actual workload of the application and on the hardware used for running the database. The speedup will be greater when transactions are small; when the transaction parallelism is modest; and when disk writes have a higher latency (like consumer-grade SSDs or network-attached storage). This is because the new binlog particularly reduces the amount of disk writes that have to happen during commit of a batch of parallel transactions. So if there are many small individual transactions and writes are expensive, the speedup can be huge. If there are few individual transactions, most transactions run in parallel and batch up in a single group commit, and/or disk writes are fast, the speedup will be smaller (but can still be significant).</p>
<h2>Technical background<a class="anchor-link" id="technical-background"></a></h2>
<p>The core of a transactional system like a database &ndash; but also for example a file-system &ndash; is its transactional log, also referred to as the write-ahead log or redo log, amongst others:</p>
<p><a href="https://en.wikipedia.org/wiki/Transaction_log">https://en.wikipedia.org/wiki/Transaction_log</a></p>
<p>This log is the core of how the database achieves a high throughput of updates to data stored on its disks, while simultaneously being able to gracefully recover into a consistent state if the system crashes during operation.</p>
<p>Unfortunately MariaDB does not have a central implementation of its Transaction Log. The main storage engine, InnoDB, has its own implementation, which is separate from the log used by other parts of the server; in particular the (old) binlog is a separate &ldquo;transaction log&rdquo;, and there are other logs used by the Aria storage engine, by DDL operations, etc. Some parts do not even have any transaction log backing them, and are thus not crash-safe. Arguably, this lack of a central transaction log is the biggest architectural limitation of MariaDB currently.</p>
<p>For the binlog in particular, having the binlog separate from the InnoDB write-ahead log causes not just a lot of code complexity, but also a huge performance cost. Because of the two separate logs, it is necessary to use a two-phase commit protocol between the two. This requires two separate synchronous disk writes per (group) commit, otherwise a crash would leave the date in one inconsistent with the other, and replication would break. The need to have these two disk flushes is a <em>huge</em> overhead.</p>
<p>The new binlog implementation fixes this, by re-implementing the binlog data format inside of InnoDB. Similar to InnoDB tablespace files, the new binlog files are now being handled through the InnoDB write-ahead log. This means that when a transaction commit happens, both the table data <em>and</em> the binlog data get written through the InnoDB write-ahead log. The write of data to binlog files can happen later, asynchronously and in an efficient manner. The InnoDB write-ahead log will be used to recover both table data and binlog data into a consistent state, and the overhead of being able to do so is being re-used for the binlog part. Thus, the overhead of two-phase commit and binlog disk flushes is gone, which is a major contribution to the performance improvements of the new binlog.</p>
<p>More subtle, but at least as important, is the improvements under the hood of the code implementing the new binlog.</p>
<p>The new binlog is implemented in InnoDB through an extension of the storage engine API. This means that another storage engine could in principle implement its own version of the binlog, which would be beneficial for users that were mainly using that storage engine for their data. But perhaps more importantly, it means that there is now a well-defined API for <em>how</em> the binlog writes work and what operations are possible on it. This gives a much cleaner separation between the file format and operations used to store the binlog on disk and read it back, as opposed to the actual contents of the binlog in the form of replication events used by slaves to replicate the master&rsquo;s data.</p>
<p>And the actual file format of the new binlog is also greatly improved.</p>
<p>The old binlog is a very naive implementation, it is just a flat file with each individual binlog event written as just a raw sequence of bytes one after the other. This is inefficient for the underlying file system, as each write has to update in two places on disk: the actual data written to the end of the file; and the metadata recording the increase in file length. It also makes it impossible to start reading the binlog file from an arbitrary place, since the start of a new event cannot be distinguished from arbitrary data contained inside an event.</p>
<p>The new binlog has a proper page-based file, which can be pre-allocated efficiently on the file system using eg. <code>posix_fallocate()</code>, and written efficiently page-by-page to the disk. And the binlog data records have proper framing within pages, so that it is possible to look at an arbitrary page in the file and understand what kind of data is there and where one record ends and the next one begins. Having a good page-based file format for the binlog is a great improvement, and something that I have desired for many years.</p>
<p>In many ways, the main benefits to me of the new binlog format is not so much the immediate performance gains, though these are quite substantial already. The really important benefits are the possibilities that are now open for future development and improvements of the binlog and replication, many things that were previously impossible to achieve due to the limitations and convoluted code and design.</p>
<p>For example, with the new binlog, large transactions are now no longer constrained to be written into the binlog as a single block at commit time; they can be written in pieces spread out over the binlog files as the transaction executes. This opens the possibility for having the slaves replicate these pieces optimistically in parallel with the transaction running on the master. This has the potential to greatly reduce the replication lag caused by long-running transactions.</p>
<p>Another example is if and when InnoDB is extended with an option for log archiving, so that the InnoDB write-ahead log is not overwritten cyclicly, but written as a sequence of files containing the complete redo data. Then the new binlog API could be used to implement the binlog data completely inside the InnoDB write-ahead log, so that replication could simply read the binlog data out of the archived log files, and the overhead of having separate binlog files could be eliminated completely.</p>
<p>And there are many other improvements, small and large, that will now be possible to do going forward, based on the improvements done in this project.</p>
<h2>Final words<a class="anchor-link" id="final-words"></a></h2>
<p>Thanks for reading this far! I encourage you to try out the new binlog and see how it works. Any questions or reports of problems are welcome, please direct all queries to the developers@ or discuss@ mailing lists:</p>
<ul>
<li><a href="https://lists.mariadb.org/hyperkitty/list/developers@lists.mariadb.org/">https://lists.mariadb.org/hyperkitty/list/developers@lists.mariadb.org/</a></li>
<li><a href="https://lists.mariadb.org/hyperkitty/list/discuss@lists.mariadb.org/">https://lists.mariadb.org/hyperkitty/list/discuss@lists.mariadb.org/</a></li>
</ul>

<p><a href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/">New binlog implementation in MariaDB 12.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>New binlog implementation in MariaDB 12.3</title>
      <link rel="alternate" type="text/html" href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/" />
      <id>https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/</id>
      <updated>2026-02-03T19:08:37+02:00</updated>
      <author><name>knielsen</name></author>
      <summary type="html"><![CDATA[<p>I have recently completed a large project to implement a new improved binlog format for MariaDB. The result will be available shortly in the upcoming MariaDB 12.3.1 release. In this article, I will give a short overview of the new binlog implementation. For more details, check the documentation which is in the source tree as… Continue reading New binlog implementation in MariaDB 12.3</p>
<p><a href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/">New binlog implementation in MariaDB 12.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I have recently completed a large project to implement a new improved binlog format for MariaDB. The result will be available shortly in the upcoming MariaDB 12.3.1 release.</p>
<p>In this article, I will give a short overview of the new binlog implementation. For more details, check the documentation which is in the source tree as the file <code>Docs/replication/binlog.md</code>, or here: <a href="https://github.com/MariaDB/server/blob/knielsen_binlog_in_engine/Docs/replication/binlog.md">https://github.com/MariaDB/server/blob/knielsen_binlog_in_engine/Docs/replication/binlog.md</a></p>
<h2>Using the new binlog<a class="anchor-link" id="using-the-new-binlog"></a></h2>
<p>To enable the new binlog, configure the MariaDB server with <code>binlog_storage_engine=innodb</code>.</p>
<p>Additionally, the binlog must itself be enabled as usual using the option <code>log_bin</code>. Note that no argument can be given to the <code>log_bin</code> option (this is to avoid confusion with the meaning of such argument as the name to use for the old binlog format, as the new binlog file names are fixed).</p>
<pre class="wp-block-preformatted">    binlog_storage_engine=innodb
    log_bin</pre>
<p>When the new binlog file is enabled and the server restarted, any old binlog files are no longer available. See the above-referenced documentation for options on how to migrate old binlogs of an existing server.</p>
<h2>Benefits of the new binlog<a class="anchor-link" id="benefits-of-the-new-binlog"></a></h2>
<p>For the user, the new binlog format brings two main benefits.</p>
<p>First, for users that are running with<code> --innodb-flush-log-at-trx-commit</code> set to 2 or 0 for performance reasons, the new binlog will make the binlog crash-safe (when used with InnoDB tables). This means that if the server crashes or the machine loses power, the restarted server will recover itself into a consistent state, including the state of replication and consistency between the binlog and the InnoDB table contents. With the old binlog format, such a crash could easily leave the binlog in a different state than the InnoDB table data, which then causes replication slaves to diverge from the master. To have the old binlog be crash-safe required setting both <code>--sync-binlog=1</code> and <code>--innodb-flush-log-at-trx-commit=1</code>.</p>
<p>Second, for users that are running with <code>--innodb-flush-log-at-trx-commit</code> set to 1 because they need durability of commits, the new binlog will provide a large speedup of the time taken to commit. Because the new binlog is integrated with InnoDB, only half as many flushes to disk of buffers are needed per commit as with the old binlog.</p>
<p>Thus, the primary user-visible benefits of the new binlog is greatly improved speedup of transaction commits.</p>
<p>The speedup that will be obtained will be completely dependent on the actual workload of the application and on the hardware used for running the database. The speedup will be greater when transactions are small; when the transaction parallelism is modest; and when disk writes have a higher latency (like consumer-grade SSDs or network-attached storage). This is because the new binlog particularly reduces the amount of disk writes that have to happen during commit of a batch of parallel transactions. So if there are many small individual transactions and writes are expensive, the speedup can be huge. If there are few individual transactions, most transactions run in parallel and batch up in a single group commit, and/or disk writes are fast, the speedup will be smaller (but can still be significant).</p>
<h2>Technical background<a class="anchor-link" id="technical-background"></a></h2>
<p>The core of a transactional system like a database &ndash; but also for example a file-system &ndash; is its transactional log, also referred to as the write-ahead log or redo log, amongst others:</p>
<p><a href="https://en.wikipedia.org/wiki/Transaction_log">https://en.wikipedia.org/wiki/Transaction_log</a></p>
<p>This log is the core of how the database achieves a high throughput of updates to data stored on its disks, while simultaneously being able to gracefully recover into a consistent state if the system crashes during operation.</p>
<p>Unfortunately MariaDB does not have a central implementation of its Transaction Log. The main storage engine, InnoDB, has its own implementation, which is separate from the log used by other parts of the server; in particular the (old) binlog is a separate &ldquo;transaction log&rdquo;, and there are other logs used by the Aria storage engine, by DDL operations, etc. Some parts do not even have any transaction log backing them, and are thus not crash-safe. Arguably, this lack of a central transaction log is the biggest architectural limitation of MariaDB currently.</p>
<p>For the binlog in particular, having the binlog separate from the InnoDB write-ahead log causes not just a lot of code complexity, but also a huge performance cost. Because of the two separate logs, it is necessary to use a two-phase commit protocol between the two. This requires two separate synchronous disk writes per (group) commit, otherwise a crash would leave the date in one inconsistent with the other, and replication would break. The need to have these two disk flushes is a <em>huge</em> overhead.</p>
<p>The new binlog implementation fixes this, by re-implementing the binlog data format inside of InnoDB. Similar to InnoDB tablespace files, the new binlog files are now being handled through the InnoDB write-ahead log. This means that when a transaction commit happens, both the table data <em>and</em> the binlog data get written through the InnoDB write-ahead log. The write of data to binlog files can happen later, asynchronously and in an efficient manner. The InnoDB write-ahead log will be used to recover both table data and binlog data into a consistent state, and the overhead of being able to do so is being re-used for the binlog part. Thus, the overhead of two-phase commit and binlog disk flushes is gone, which is a major contribution to the performance improvements of the new binlog.</p>
<p>More subtle, but at least as important, is the improvements under the hood of the code implementing the new binlog.</p>
<p>The new binlog is implemented in InnoDB through an extension of the storage engine API. This means that another storage engine could in principle implement its own version of the binlog, which would be beneficial for users that were mainly using that storage engine for their data. But perhaps more importantly, it means that there is now a well-defined API for <em>how</em> the binlog writes work and what operations are possible on it. This gives a much cleaner separation between the file format and operations used to store the binlog on disk and read it back, as opposed to the actual contents of the binlog in the form of replication events used by slaves to replicate the master&rsquo;s data.</p>
<p>And the actual file format of the new binlog is also greatly improved.</p>
<p>The old binlog is a very naive implementation, it is just a flat file with each individual binlog event written as just a raw sequence of bytes one after the other. This is inefficient for the underlying file system, as each write has to update in two places on disk: the actual data written to the end of the file; and the metadata recording the increase in file length. It also makes it impossible to start reading the binlog file from an arbitrary place, since the start of a new event cannot be distinguished from arbitrary data contained inside an event.</p>
<p>The new binlog has a proper page-based file, which can be pre-allocated efficiently on the file system using eg. <code>posix_fallocate()</code>, and written efficiently page-by-page to the disk. And the binlog data records have proper framing within pages, so that it is possible to look at an arbitrary page in the file and understand what kind of data is there and where one record ends and the next one begins. Having a good page-based file format for the binlog is a great improvement, and something that I have desired for many years.</p>
<p>In many ways, the main benefits to me of the new binlog format is not so much the immediate performance gains, though these are quite substantial already. The really important benefits are the possibilities that are now open for future development and improvements of the binlog and replication, many things that were previously impossible to achieve due to the limitations and convoluted code and design.</p>
<p>For example, with the new binlog, large transactions are now no longer constrained to be written into the binlog as a single block at commit time; they can be written in pieces spread out over the binlog files as the transaction executes. This opens the possibility for having the slaves replicate these pieces optimistically in parallel with the transaction running on the master. This has the potential to greatly reduce the replication lag caused by long-running transactions.</p>
<p>Another example is if and when InnoDB is extended with an option for log archiving, so that the InnoDB write-ahead log is not overwritten cyclicly, but written as a sequence of files containing the complete redo data. Then the new binlog API could be used to implement the binlog data completely inside the InnoDB write-ahead log, so that replication could simply read the binlog data out of the archived log files, and the overhead of having separate binlog files could be eliminated completely.</p>
<p>And there are many other improvements, small and large, that will now be possible to do going forward, based on the improvements done in this project.</p>
<h2>Final words<a class="anchor-link" id="final-words"></a></h2>
<p>Thanks for reading this far! I encourage you to try out the new binlog and see how it works. Any questions or reports of problems are welcome, please direct all queries to the developers@ or discuss@ mailing lists:</p>
<ul>
<li><a href="https://lists.mariadb.org/hyperkitty/list/developers@lists.mariadb.org/">https://lists.mariadb.org/hyperkitty/list/developers@lists.mariadb.org/</a></li>
<li><a href="https://lists.mariadb.org/hyperkitty/list/discuss@lists.mariadb.org/">https://lists.mariadb.org/hyperkitty/list/discuss@lists.mariadb.org/</a></li>
</ul>

<p><a href="https://knielsen-hq.org/w/new-binlog-implementation-in-mariadb-12-3/">New binlog implementation in MariaDB 12.3</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Hackorum &#8211; A Forum-Style View of pg-hackers</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/02/hackorum-a-forum-style-view-of-pg-hackers/" />
      <id>https://percona.community/blog/2026/02/02/hackorum-a-forum-style-view-of-pg-hackers/</id>
      <updated>2026-02-02T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Last year at pgconf.dev, there was a discussion about improving the user interface for the PostgreSQL hackers mailing list, which is the main communication channel for PostgreSQL core development. Based on that discussion, I want to share a small project we have been working on:</p>
<p><a href="https://percona.community/blog/2026/02/02/hackorum-a-forum-style-view-of-pg-hackers/">Hackorum &#8211; A Forum-Style View of pg-hackers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Last year at pgconf.dev, there was a discussion about improving the user interface for the PostgreSQL hackers mailing list, which is the main communication channel for PostgreSQL core development. Based on that discussion, I want to share a small project we have been working on:</p>
<p><a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">https://hackorum.dev/</a></p>
<p>Hackorum provides a <strong>read-only (for now)</strong> web view of the mailing list with a more forum-like presentation. It is a <strong>work-in-progress proof of concept</strong>, and we are primarily looking for feedback on whether this approach is useful and what we should improve next.</p>
<h2>What Hackorum already does<a class="anchor-link" id="what-hackorum-already-does"></a></h2>
<p>Hackorum focuses on readability, navigation, and workflow improvements for people who follow pg-hackers. Some highlights:</p>
<ul>
<li><strong>Continuous mailing list synchronization</strong>: The site is subscribed to the list</li>
<li><strong>Commitfest integration</strong>: See commitfest context next to threads &ndash; you directly know what&rsquo;s the state of this commit/thread.</li>
<li><strong>User profiles</strong>: Contributor/committer status from the main website</li>
<li><strong>Statistics</strong>: Per-user and mailing lists insights</li>
<li><strong>Easy download of attached patches</strong>: Including helper script for easy rebase and merge</li>
<li><strong>Additional logged-in user features</strong>: Per-message read status, starring threads, tags, notes, mentions on messages and threads</li>
<li><strong>Basic team support</strong>: Shared reading status, shared mentioned tags and notes &ndash; mention someone underneath an email an the person gets notified</li>
<li><strong>Resend email</strong>: Integration from the official archive</li>
<li><strong>Importing read status / tags via CSV files</strong>: To help migration from email-based workflows</li>
</ul>
<p><figure><img decoding="async" width="2728" height="1216" src="https://percona.community/blog/2026/02/hackorum-topics_hu_d07aed84ec43688a.webp" alt="Hackorum topics overview" loading="lazy"></figure>
</p>
<h2>What we plan next<a class="anchor-link" id="what-we-plan-next"></a></h2>
<ul>
<li><strong>Sending emails from the web UI</strong>: Initially via Gmail API for Google-authenticated users who authorize sending</li>
<li><strong>Advanced search functionality</strong></li>
<li><strong>Integrating other mailing lists</strong></li>
</ul>
<h2>Try it and share feedback<a class="anchor-link" id="try-it-and-share-feedback"></a></h2>
<p>If you want to take a look, just got to <a href="https://hackorum.dev/" target="_blank" rel="noopener noreferrer">https://hackorum.dev/</a></p>
<p>The repository, including a simple dev setup, can be found here: <a href="https://github.com/hackorum-dev/hackorum" target="_blank" rel="noopener noreferrer">https://github.com/hackorum-dev/hackorum</a></p>
<p>Is this useful? What is missing? What would you change? Bug reports, feature requests, and contributions are all welcome. <a href="https://github.com/hackorum-dev/hackorum/issues" target="_blank" rel="noopener noreferrer">https://github.com/hackorum-dev/hackorum/issues</a></p>
<p>Thanks for taking a look, and we appreciate any feedback.</p>

<p><a href="https://percona.community/blog/2026/02/02/hackorum-a-forum-style-view-of-pg-hackers/">Hackorum &#8211; A Forum-Style View of pg-hackers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Tuning MySQL for Performance: The Variables That Actually Matter</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/02/01/tuning-mysql-for-performance-the-variables-that-actually-matter/" />
      <id>https://percona.community/blog/2026/02/01/tuning-mysql-for-performance-the-variables-that-actually-matter/</id>
      <updated>2026-02-01T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>There is a special kind of boredom that only database people know. The kind where you stare at a server humming along and think, surely there is something here I can tune. Good news: there is.</p>
<p><a href="https://percona.community/blog/2026/02/01/tuning-mysql-for-performance-the-variables-that-actually-matter/">Tuning MySQL for Performance: The Variables That Actually Matter</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>There is a special kind of boredom that only database people know. The kind where you stare at a server humming along and think, <em>surely there is something here I can tune</em>. Good news: there is.</p>
<p>This post walks through the <strong>most important MySQL variables to tune for performance</strong>, why they matter, and when touching them helps versus when it quietly makes things worse. This is written with <strong>InnoDB-first workloads</strong> in mind, because let&rsquo;s be honest, that&rsquo;s almost everyone.</p>
<hr>
<h2>1. <code>innodb_buffer_pool_size</code><a class="anchor-link" id="1-innodb_buffer_pool_size"></a></h2>
<h3>Real metrics to watch<a class="anchor-link" id="real-metrics-to-watch"></a></h3>
<p>Before touching this variable, look at these:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_buffer_pool_read%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Key fields:</p>
<ul>
<li><code>Innodb_buffer_pool_reads</code> &ndash; physical reads from disk</li>
<li><code>Innodb_buffer_pool_read_requests</code> &ndash; logical reads</li>
</ul>
<p><strong>Rule of thumb:</strong><br>
If <code>reads / read_requests</code> &gt; 1&ndash;2%, your buffer pool is too small.</p>
<h3>Example graph<a class="anchor-link" id="example-graph"></a></h3>
<p>Plot <code>Innodb_buffer_pool_reads</code> over time. A healthy system shows a flat or gently rising line. Spikes that look like a city skyline usually mean memory pressure or a cold cache.</p>
<p>If MySQL performance had a crown jewel, this would be it.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>The InnoDB buffer pool caches table data and indexes in memory. Reads served from RAM are fast. Reads from disk are&hellip; character building.</p>
<h3>How to tune it<a class="anchor-link" id="how-to-tune-it"></a></h3>
<ul>
<li>Dedicated DB server: <strong>60&ndash;75% of system RAM</strong></li>
<li>Shared server: be conservative and leave memory for the OS and other services</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'innodb_buffer_pool_size'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Pro tip<a class="anchor-link" id="pro-tip"></a></h3>
<p>If your working set fits in the buffer pool, MySQL feels magical. If it doesn&rsquo;t, no amount of query tuning will save you.</p>
<hr>
<h2>2. <code>innodb_buffer_pool_instances</code><a class="anchor-link" id="2-innodb_buffer_pool_instances"></a></h2>
<p>This one matters once memory gets big.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Splits the buffer pool into multiple instances to reduce internal mutex contention.</p>
<h3>How to tune it<a class="anchor-link" id="how-to-tune-it"></a></h3>
<ul>
<li>Only relevant if buffer pool is <strong>&ge; 1GB</strong></li>
<li>Rule of thumb: <strong>1 instance per 1&ndash;2GB</strong>, max 8</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'innodb_buffer_pool_instances'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Gotcha<a class="anchor-link" id="gotcha"></a></h3>
<p>More is not always better. Too many instances wastes memory and can hurt performance.</p>
<hr>
<h2>3. <code>innodb_log_file_size</code><a class="anchor-link" id="3-innodb_log_file_size"></a></h2>
<h3>Real metrics to watch<a class="anchor-link" id="real-metrics-to-watch"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_log%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Pay attention to:</p>
<ul>
<li><code>Innodb_log_waits</code></li>
<li><code>Innodb_log_write_requests</code></li>
</ul>
<p><strong>If <code>Innodb_log_waits</code> is non-zero</strong>, redo logs are too small for your write rate.</p>
<h3>Example graph<a class="anchor-link" id="example-graph"></a></h3>
<p>Graph <code>Innodb_log_waits</code> as a rate per second. Ideally, this line hugs zero like it&rsquo;s afraid of heights.</p>
<p>This variable controls how calmly MySQL handles write-heavy workloads.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Defines the size of redo logs. Larger logs mean fewer checkpoints and smoother writes.</p>
<h3>How to tune it<a class="anchor-link" id="how-to-tune-it"></a></h3>
<ul>
<li>OLTP workloads: <strong>1&ndash;4GB total redo log</strong> is common</li>
<li>Large transactions benefit from larger logs</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'innodb_log_file_size'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Warning<a class="anchor-link" id="warning"></a></h3>
<p>Changing this requires a restart. Plan accordingly or accept the wrath of your on-call future self.</p>
<hr>
<h2>4. <code>innodb_flush_log_at_trx_commit</code><a class="anchor-link" id="4-innodb_flush_log_at_trx_commit"></a></h2>
<h3>Real metrics to watch<a class="anchor-link" id="real-metrics-to-watch"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Innodb_os_log_fsyncs'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Switching from <code>1</code> to <code>2</code> often reduces fsyncs by <strong>orders of magnitude</strong>.</p>
<h3>Example graph<a class="anchor-link" id="example-graph"></a></h3>
<p>Overlay two lines:</p>
<ul>
<li><code>Transactions per second</code></li>
<li><code>Innodb_os_log_fsyncs per second</code></li>
</ul>
<p>On busy systems, this graph alone can justify the change to skeptical auditors.</p>
<p>Performance versus durability, the eternal duel.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Controls how often redo logs are flushed to disk.</p>
<h3>Common values<a class="anchor-link" id="common-values"></a></h3>
<ul>
<li><code>1</code> &ndash; Safest, slowest (flush every commit)</li>
<li><code>2</code> &ndash; Very popular compromise</li>
<li><code>0</code> &ndash; Fast, risky</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'innodb_flush_log_at_trx_commit'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Reality check<a class="anchor-link" id="reality-check"></a></h3>
<p>For many production systems, <strong><code>2</code> delivers massive performance gains</strong> with acceptable risk, especially with reliable storage.</p>
<hr>
<h2>5. <code>innodb_flush_method</code><a class="anchor-link" id="5-innodb_flush_method"></a></h2>
<p>This decides how MySQL talks to your disks.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Controls whether MySQL uses OS cache or bypasses it.</p>
<h3>Recommended<a class="anchor-link" id="recommended"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">ini</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">innodb_flush_method</span><span class="o">=</span><span class="s">O_DIRECT</span></span></span></code></pre>
</div>
</div>
</div>
<p>This avoids double-buffering between MySQL and the OS page cache.</p>
<h3>Caveat<a class="anchor-link" id="caveat"></a></h3>
<p>Some filesystems and older kernels behave differently. Always test.</p>
<hr>
<h2>6. <code>max_connections</code><a class="anchor-link" id="6-max_connections"></a></h2>
<p>This is not a performance knob. It is a <strong>damage limiter</strong>.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Caps the number of concurrent client connections.</p>
<h3>Why it matters<a class="anchor-link" id="why-it-matters"></a></h3>
<p>Each connection consumes memory. Too many and MySQL dies spectacularly.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'max_connections'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Advice<a class="anchor-link" id="advice"></a></h3>
<ul>
<li>Set it realistically</li>
<li>Use connection pooling</li>
<li>Monitor <code>Threads_connected</code></li>
</ul>
<hr>
<h2>7. <code>thread_cache_size</code><a class="anchor-link" id="7-thread_cache_size"></a></h2>
<h3>Real metrics to watch<a class="anchor-link" id="real-metrics-to-watch"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Threads%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Key fields:</p>
<ul>
<li><code>Threads_created</code></li>
<li><code>Connections</code></li>
</ul>
<p>If <code>Threads_created / Connections</code> stays above a few percent, your cache is undersized.</p>
<h3>Example graph<a class="anchor-link" id="example-graph"></a></h3>
<p>Graph <code>Threads_created</code> as a counter. A healthy system shows a curve that flattens over time, not a staircase.</p>
<p>Small change, measurable win.</p>
<h3>What it does<a class="anchor-link" id="what-it-does"></a></h3>
<p>Caches threads so MySQL doesn&rsquo;t constantly create and destroy them.</p>
<h3>How to tune<a class="anchor-link" id="how-to-tune"></a></h3>
<p>Watch:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Threads_created'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>If it keeps climbing, increase <code>thread_cache_size</code>.</p>
<hr>
<h2>8. <code>table_open_cache</code> and <code>table_definition_cache</code><a class="anchor-link" id="8-table_open_cache-and-table_definition_cache"></a></h2>
<p>Metadata matters more than people expect.</p>
<h3>What they do<a class="anchor-link" id="what-they-do"></a></h3>
<p>Cache open tables and table definitions to avoid repeated filesystem access.</p>
<h3>Symptoms of being too low<a class="anchor-link" id="symptoms-of-being-too-low"></a></h3>
<ul>
<li>High <code>Opened_tables</code></li>
<li>Metadata lock waits</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'table_open_cache'</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SHOW</span><span class="w"> </span><span class="n">VARIABLES</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'table_definition_cache'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<hr>
<h2>9. <code>tmp_table_size</code> and <code>max_heap_table_size</code><a class="anchor-link" id="9-tmp_table_size-and-max_heap_table_size"></a></h2>
<h3>Real metrics to watch<a class="anchor-link" id="real-metrics-to-watch"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SHOW</span><span class="w"> </span><span class="k">GLOBAL</span><span class="w"> </span><span class="n">STATUS</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">'Created_tmp%'</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Watch:</p>
<ul>
<li><code>Created_tmp_tables</code></li>
<li><code>Created_tmp_disk_tables</code></li>
</ul>
<p>If disk temp tables exceed <strong>5&ndash;10%</strong> of total temp tables, queries are spilling to disk.</p>
<h3>Example graph<a class="anchor-link" id="example-graph"></a></h3>
<p>Stacked area chart:</p>
<ul>
<li>In-memory temp tables</li>
<li>Disk-based temp tables</li>
</ul>
<p>Disk usage creeping upward usually points to reporting queries pretending to be OLTP.</p>
<p>Disk-based temp tables are silent performance killers.</p>
<h3>What they do<a class="anchor-link" id="what-they-do"></a></h3>
<p>Limit how large in-memory temp tables can grow.</p>
<h3>How to tune<a class="anchor-link" id="how-to-tune"></a></h3>
<p>Set both to the same value:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">ini</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">tmp_table_size</span><span class="o">=</span><span class="s">256M</span>
</span></span><span class="line"><span class="cl"><span class="na">max_heap_table_size</span><span class="o">=</span><span class="s">256M</span></span></span></code></pre>
</div>
</div>
</div>
<h3>Reality<a class="anchor-link" id="reality"></a></h3>
<p>This helps complex queries, but bad queries still need fixing.</p>
<hr>
<h2>10. <code>slow_query_log</code> and <code>long_query_time</code><a class="anchor-link" id="10-slow_query_log-and-long_query_time"></a></h2>
<p>Not a performance variable, but a performance <em>revelation</em>.</p>
<h3>Why it matters<a class="anchor-link" id="why-it-matters"></a></h3>
<p>You cannot tune what you cannot see.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">ini</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">slow_query_log</span><span class="o">=</span><span class="s">ON</span>
</span></span><span class="line"><span class="cl"><span class="na">long_query_time</span><span class="o">=</span><span class="s">1</span></span></span></code></pre>
</div>
</div>
</div>
<p>This turns guesswork into evidence.</p>
<hr>
<h2>A Note on Graphing These Metrics<a class="anchor-link" id="a-note-on-graphing-these-metrics"></a></h2>
<p>You don&rsquo;t need exotic tools. These work well:</p>
<ul>
<li><code>performance_schema</code></li>
<li><code>sys</code> schema views</li>
<li>Prometheus + mysqld_exporter</li>
<li>Percona Monitoring and Management (PMM)</li>
</ul>
<p><strong>Golden rule:</strong> Always graph rates, not raw counters.</p>
<hr>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>Tuning MySQL is less about endless knobs and more about <strong>understanding pressure points</strong>:</p>
<ul>
<li>Memory first</li>
<li>I/O second</li>
<li>Concurrency third</li>
</ul>
<p>Most performance wins come from <strong>a handful of variables</strong>, not heroic config files full of folklore.</p>
<p>If you tune one thing today, make it the buffer pool. If you tune two, add redo logs. Everything else is refinement.</p>
<p>And if you&rsquo;re bored again tomorrow, congratulations. You&rsquo;re officially a database person.</p>

<p><a href="https://percona.community/blog/2026/02/01/tuning-mysql-for-performance-the-variables-that-actually-matter/">Tuning MySQL for Performance: The Variables That Actually Matter</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The concepts of forking</title>
      <link rel="alternate" type="text/html" href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html" />
      <id>http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html</id>
      <updated>2026-01-29T09:46:00+02:00</updated>
      <author><name>Michael &quot;Monty&quot; Widenius</name></author>
      <summary type="html"><![CDATA[<p>Lately there has been a lot of discussion about “hard” or “soft” forks related to MySQL. As someone who has done a successful fork of MySQL, I think this is both confusing and trivialising the concept of forking.In my previous blog,  I did touch a bit on this topic, but it looks like some more clarifications are needed.When we did the initial fork of MariaDB from MySQL, we tried our best to keep things 100% user compatible while still adding new features and fixing issues in MySQL. For MariaDB 5.1 - &#62; MariaDB 5.5, we merged all relevant changes from MySQL into MariaDB.This did not mean that MariaDB was 100% compatible with MySQL, as any change in a fork makes things incompatible in some manner. For example, the enhanced optimiser in MariaDB 5.5 did work slightly differently (better) than MySQL, and if one used any of the new features in MariaDB, one could not trivially go back to MySQL anymore. However, for most users these changes were not notable and allowed most Linux distributions to automatically move MySQL users to MariaDB without any disturbance.Over time, the merging of MySQL code became harder and gave us less benefit compared to the effort of doing the merges. The new MySQL developers had started to move source code around (which made merges harder), and we, the MariaDB developers, were not happy with the quality of the code related to bug fixes or some of the new features. It was easier to write the new feature from scratch than to use the MySQL code. However, for each feature we did our best to ensure that the syntax and behaviour were identical to MySQL.Another big problem was that MySQL started to copy features (not code) from MariaDB, but used a different SQL syntax than what MariaDB was using. One example is the usage of CHANNEL in multi-source replication. It did not make any sense for MariaDB to copy the multi-source code from MySQL, as we already had a working, stable implementation we were happy with.With MariaDB 10.0, we decided to stop merges from MySQL and instead monitor new features and implement those that we thought made sense for MariaDB.Moving to MariaDB 10.0 allowed us more flexibility in adding more features to MariaDB without being constrained by the MySQL code, like Galera, Oracle compatibility, and a lot of other things listed here.Nowadays, most of the MariaDB development work is adding features customers and MariaDB users are missing (link to MariaDB 13.0 roadmap will shortly be added here). A lot of this work is related to new Oracle compatibility required by new customers, like FULL OUTER JOIN. There are still a few notable features in MySQL that we have not had time to re-implement, like multi-value indexing (for indexing JSON), JSON operators, and LATERAL tables. All of the mentioned ones are on the MariaDB 13.0 roadmap.We, the MariaDB developers, are still working on keeping MariaDB compatible with MySQL (and Percona Server). In MariaDB 10.11, we added support for the popular extensions from Percona Server. In the latest MariaDB versions we have ensured that one can replicate from MySQL to MariaDB and back.   We have also added support for the caching_sha2_password plugin, to allow MySQL users to switch to MariaDB without changing their passwords, support of the default MySQL character collation set, utf8mb4_0900_* and multiple JSON functions.We also listen to MySQL users moving to MariaDB and do our best to implement the features they need to be able to move to MariaDB. The MariaDB Foundation is there for those who want to be part of this effort!The above hopefully gives the needed background to discuss different kinds of forks (just kidding) in more detail.Internal forkFork where the company/original development team forks the product for political, redesign, or development reasons. The fork may be more or less, or not at all, compatible with the predecessor.Examples:MySQL 8.0 (someone could call this a “hard” fork as it was hard to move to it and very hard to go backwards )OpenOffice → Apache OpenOffice (after Oracle acquisition; internal governance shift)Sun Solaris → Oracle Solaris (post-acquisition direction change)KDE 3 → KDE 4 (often cited as an internal “hard” break due to massive architectural changes)Python 2 → Python 3 (not a fork in licence terms, but functionally an internal compatibility break)Drizzle (https://en.wikipedia.org/wiki/Drizzle_(database_server)External forkWhen an external group or company forks a project for various reasons. The most common reasons are creational differences in how to take the project forward or distrust in the original project owners.The external fork has a lot of subcategories:Downstream \"no-changes\" forkThe fork is based on the original project with a small, limited subset of changes to get the project to work within an ecosystem or with an external/internal project that requires some minor changes.The code is basically a rebase plus patches on top of the original code.No user-visible changes from the original project.Examples:Packages in Linux and other OS distributionsUbuntu kernel (downstream of Linux with minimal, policy-driven patches)Homebrew / MacPorts packagesDebian-patched GNU toolsAndroid Linux kernel (arguably borderline, but many devices are close to upstream + patches)Downstream forkThe fork is based on a rebase of the original code, but with user-visible changes that bring a different user experience while keeping the base 100% compatible with the original project. It is reasonably easy to move to the fork, but harder for users of this fork to move back to the original.The forks usually have the problem that newer major versions have to drop options or features when the original project adds them, which makes upgrades to the next version a bit harder.Examples:Red Hat Enterprise Linux (downstream of Fedora)Ubuntu (downstream of Debian)Amazon Linux (downstream of RHEL/CentOS lineage)PostgreSQL distributions (EDB Postgres, Amazon Aurora PostgreSQL-compatible)Percona ServerMariaDB 5.1 - &#62; 5.4 (these MariaDB versions never had to drop a feature)Compatibility forkThe fork was originally a \'Downstream fork\' but moved to, instead of using rebases, only merging selected patches from the original project and rewriting things the developers disliked. The goal is still to have high compatibility with the original project.Examples:LibreOffice (from OpenOffice.org)Jenkins (from Hudson, especially post-Oracle divergence)Percona XtraDB ClusterMariaDB 5.5Independent fork (or \"branch\")The fork is no longer dependent on the original project. It may still take selected patches or ideas from the original project.It usually tries to keep things compatible to make it easy for original project users to move to the new project, but the main focus is solving new problems for its growing user base.Examples:GhostBSDOpenBSD (from NetBSD)Illumos (from OpenSolaris)systemd (initially replacing sysvinit, now fully independent ecosystem)Neo4j Community vs Enterprise split (conceptual fit)Firefox (historically from Mozilla Suite)MariaDB 10+Some people have recently expressed that they are afraid that MySQL development is stopping or slowing down, and others have started to talk about the need to do a “soft” fork of MySQL.The point I am trying to make is that if these worries are real, then any fork will sooner or later have to become an independent fork/branch or die together with MySQL (as there will be no new features in the fork).One of the mantras in open source is that it is better to join an existing project than to create a new one! Instead of talking about creating yet another fork of MySQL, it would be better if everyone gathered around MariaDB! MariaDB development is not dependent on Oracle for its future. This is assured by the MariaDB Foundation, which was created to make it easy for anyone to participate in the development of the MariaDB server. MariaDB plc is working together with the MariaDB Foundation to make this possible.MariaDB is, after all, created by the same people who created MySQL and is developed in the way it would have been if Oracle had not bought MySQL. The rapid adoption of MariaDB (350+ million database installations and rapidly increasing) shows that MariaDB is truly the future of MySQL.PS:Please leave a comment if you have a better name for any of the fork categories, another fork category that should be added, or more examples for the categories.</p>
<p><a href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html">The concepts of forking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<div>Lately there has been a lot of discussion about &ldquo;hard&rdquo; or &ldquo;soft&rdquo; forks related to MySQL. As someone who has done a successful fork of MySQL, I think this is both confusing and trivialising the concept of forking.</div>
<div></div>
<div>In my <a href="https://monty-says.blogspot.com/2024/10/celebrating-15-years-of-mariadb.html">previous blog</a>,&nbsp; I did touch a bit on this topic, but it looks like some more clarifications are needed.</div>
<div></div>
<div>When we did the initial fork of MariaDB from MySQL, we tried our best to keep things 100% user compatible while still adding new features and fixing issues in MySQL. For MariaDB 5.1 -&gt; MariaDB 5.5, we merged all relevant changes from MySQL into MariaDB.</div>
<div></div>
<div>This did not mean that MariaDB was 100% compatible with MySQL, as any change in a fork makes things incompatible in some manner. For example, the enhanced optimiser in MariaDB 5.5 did work slightly differently (better) than MySQL, and if one used any of the new features in MariaDB, one could not trivially go back to MySQL anymore. However, for most users these changes were not notable and allowed most Linux distributions to automatically move MySQL users to MariaDB without any disturbance.</div>
<div></div>
<div>Over time, the merging of MySQL code became harder and gave us less benefit compared to the effort of doing the merges. The new MySQL developers had started to move source code around (which made merges harder), and we, the MariaDB developers, were not happy with the quality of the code related to bug fixes or some of the new features. It was easier to write the new feature from scratch than to use the MySQL code. However, for each feature we did our best to ensure that the syntax and behaviour were identical to MySQL.</div>
<div></div>
<div>Another big problem was that MySQL started to copy features (not code) from MariaDB, but used a different SQL syntax than what MariaDB was using. One example is the usage of CHANNEL in multi-source replication. It did not make any sense for MariaDB to copy the multi-source code from MySQL, as we already had a working, stable implementation we were happy with.</div>
<div></div>
<div>With MariaDB 10.0, we decided to stop merges from MySQL and instead monitor new features and implement those that we thought made sense for MariaDB.</div>
<div></div>
<div>Moving to MariaDB 10.0 allowed us more flexibility in adding more features to MariaDB without being constrained by the MySQL code, like Galera, Oracle compatibility, and a lot of other things listed <a href="https://monty-says.blogspot.com/2024/10/celebrating-15-years-of-mariadb.html">here</a>.</div>
<div></div>
<div>Nowadays, most of the MariaDB development work is adding features customers and MariaDB users are missing (link to MariaDB 13.0 roadmap will shortly be added here). A lot of this work is related to new Oracle compatibility required by new customers, like FULL OUTER JOIN. There are still a few notable features in MySQL that we have not had time to re-implement, like multi-value indexing (for indexing JSON), JSON operators, and LATERAL tables. All of the mentioned ones are on the MariaDB 13.0 roadmap.</div>
<div></div>
<div>
<div>We, the MariaDB developers, are still working on keeping MariaDB compatible with MySQL (and Percona Server). In MariaDB 10.11, we added support for the popular extensions from Percona Server. In the latest MariaDB versions we have ensured that one can r<a href="https://mariadb.com/docs/release-notes/community-server/about/compatibility-and-differences/replication-compatibility-between-mariadb-and-mysql">eplicate from MySQL to MariaDB and back</a>.&nbsp; &nbsp;We have also added support for the&nbsp;<a href="https://mariadb.com/docs/server/reference/plugins/authentication-plugins/authentication-plugin-caching_sha2_password">caching_sha2_password plugin</a>,&nbsp;to allow MySQL users to switch to MariaDB without changing their passwords, support of the default MySQL character collation set,&nbsp;<a href="https://mariadb.com/docs/release-notes/community-server/11.4/11.4.5#character-sets-and-collations">utf8mb4_0900_*</a>&nbsp;and&nbsp;<a href="https://mariadb.com/docs/server/reference/sql-functions/special-functions/json-functions">multiple JSON functions</a>.</div>
<div></div>
<div>We also listen to MySQL users moving to MariaDB and do our best to implement the features they need to be able to move to MariaDB. The&nbsp;<a href="https://mariadb.org/">MariaDB Foundation</a>&nbsp;is there for those who want to be part of this effort!</div>
</div>
<div></div>
<div>The above hopefully gives the needed background to discuss different kinds of <a href="https://www.sambonet-shop.com/en-us/types-of-forks.html" rel="nofollow">forks</a>&nbsp;(just kidding) in more detail.</div>
<div></div>
<div>Internal fork</div>
<div>
<ul>
<li>Fork where the company/original development team forks the product for political, redesign, or development reasons. The fork may be more or less, or not at all, compatible with the predecessor.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>MySQL 8.0 (someone could call this a &ldquo;hard&rdquo; fork as it was hard to move to it and very hard to go backwards )</li>
<li>OpenOffice &rarr; Apache OpenOffice (after Oracle acquisition; internal governance shift)</li>
<li>Sun Solaris &rarr; Oracle Solaris (post-acquisition direction change)</li>
<li>KDE 3 &rarr; KDE 4 (often cited as an internal &ldquo;hard&rdquo; break due to massive architectural changes)</li>
<li>Python 2 &rarr; Python 3 (not a fork in licence terms, but functionally an internal compatibility break)</li>
<li>Drizzle (https://en.wikipedia.org/wiki/Drizzle_(database_server)</li>
</ul>
</div>
<div>External fork</div>
<div>
<ul>
<li>When an external group or company forks a project for various reasons. The most common reasons are creational differences in how to take the project forward or distrust in the original project owners.</li>
</ul>
</div>
<div>The external fork has a lot of subcategories:</div>
<div></div>
<div>Downstream &ldquo;no-changes&rdquo; fork</div>
<div>
<ul>
<li>The fork is based on the original project with a small, limited subset of changes to get the project to work within an ecosystem or with an external/internal project that requires some minor changes.</li>
<li>The code is basically a rebase plus patches on top of the original code.</li>
<li>No user-visible changes from the original project.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>Packages in Linux and other OS distributions</li>
<li>Ubuntu kernel (downstream of Linux with minimal, policy-driven patches)</li>
<li>Homebrew / MacPorts packages</li>
<li>Debian-patched GNU tools</li>
<li>Android Linux kernel (arguably borderline, but many devices are close to upstream + patches)</li>
</ul>
</div>
<div>Downstream fork</div>
<div>
<ul>
<li>The fork is based on a rebase of the original code, but with user-visible changes that bring a different user experience while keeping the base 100% compatible with the original project. It is reasonably easy to move to the fork, but harder for users of this fork to move back to the original.</li>
<li>The forks usually have the problem that newer major versions have to drop options or features when the original project adds them, which makes upgrades to the next version a bit harder.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>Red Hat Enterprise Linux (downstream of Fedora)</li>
<li>Ubuntu (downstream of Debian)</li>
<li>Amazon Linux (downstream of RHEL/CentOS lineage)</li>
<li>PostgreSQL distributions (EDB Postgres, Amazon Aurora PostgreSQL-compatible)</li>
<li>Percona Server</li>
<li>MariaDB 5.1 -&gt; 5.4 (these MariaDB versions never had to drop a feature)</li>
</ul>
</div>
<div>Compatibility fork</div>
<div>
<ul>
<li>The fork was originally a &lsquo;Downstream fork&rsquo; but moved to, instead of using rebases, only merging selected patches from the original project and rewriting things the developers disliked. The goal is still to have high compatibility with the original project.</li>
</ul>
</div>
<div>
<ul>
<li>Examples:</li>
<li>LibreOffice (from OpenOffice.org)</li>
<li>Jenkins (from Hudson, especially post-Oracle divergence)</li>
<li>Percona XtraDB Cluster</li>
<li>MariaDB 5.5</li>
</ul>
</div>
<div>Independent fork (or &ldquo;branch&rdquo;)</div>
<div>
<ul>
<li>The fork is no longer dependent on the original project. It may still take selected patches or ideas from the original project.</li>
<li>It usually tries to keep things compatible to make it easy for original project users to move to the new project, but the main focus is solving new problems for its growing user base.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>GhostBSD</li>
<li>OpenBSD (from NetBSD)</li>
<li>Illumos (from OpenSolaris)</li>
<li>systemd (initially replacing sysvinit, now fully independent ecosystem)</li>
<li>Neo4j Community vs Enterprise split (conceptual fit)</li>
<li>Firefox (historically from Mozilla Suite)</li>
<li>MariaDB 10+</li>
</ul>
</div>
<div>Some people have recently expressed that they are afraid that <a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/">MySQL development is stopping or slowing down</a>, and others have started to talk about the need to do a &ldquo;soft&rdquo; fork of MySQL.</div>
<div></div>
<div>The point I am trying to make is that if these worries are real, then any fork will sooner or later have to become an independent fork/branch or die together with MySQL (as there will be no new features in the fork).</div>
<div></div>
<div>One of the mantras in open source is that it is better to join an existing project than to create a new one! Instead of talking about creating yet another fork of MySQL, it would be better if everyone gathered around MariaDB! MariaDB development is not dependent on Oracle for its future. This is assured by the <a href="http://mariadb.org/">MariaDB Foundation</a>, which was created to make it easy for anyone to participate in the development of the MariaDB server. <a href="http://mariadb.com/">MariaDB plc</a> is working together with the <a href="http://mariadb.org/">MariaDB Foundation</a> to make this possible.</div>
<div></div>
<div>MariaDB is, after all, created by the same people who created MySQL and is developed in the way it would have been if Oracle had not bought MySQL. The rapid adoption of MariaDB (350+ million database installations and rapidly increasing) shows that MariaDB is truly the future of MySQL.</div>
<div></div>
<div>PS:</div>
<div>Please leave a comment if you have a better name for any of the fork categories, another fork category that should be added, or more examples for the categories.</div>
<div></div>

<p><a href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html">The concepts of forking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The concepts of forking</title>
      <link rel="alternate" type="text/html" href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html" />
      <id>http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html</id>
      <updated>2026-01-29T09:46:00+02:00</updated>
      <author><name>Michael &quot;Monty&quot; Widenius</name></author>
      <summary type="html"><![CDATA[<p>Lately there has been a lot of discussion about “hard” or “soft” forks related to MySQL. As someone who has done a successful fork of MySQL, I think this is both confusing and trivialising the concept of forking.In my previous blog,  I did touch a bit on this topic, but it looks like some more clarifications are needed.When we did the initial fork of MariaDB from MySQL, we tried our best to keep things 100% user compatible while still adding new features and fixing issues in MySQL. For MariaDB 5.1 - &#62; MariaDB 5.5, we merged all relevant changes from MySQL into MariaDB.This did not mean that MariaDB was 100% compatible with MySQL, as any change in a fork makes things incompatible in some manner. For example, the enhanced optimiser in MariaDB 5.5 did work slightly differently (better) than MySQL, and if one used any of the new features in MariaDB, one could not trivially go back to MySQL anymore. However, for most users these changes were not notable and allowed most Linux distributions to automatically move MySQL users to MariaDB without any disturbance.Over time, the merging of MySQL code became harder and gave us less benefit compared to the effort of doing the merges. The new MySQL developers had started to move source code around (which made merges harder), and we, the MariaDB developers, were not happy with the quality of the code related to bug fixes or some of the new features. It was easier to write the new feature from scratch than to use the MySQL code. However, for each feature we did our best to ensure that the syntax and behaviour were identical to MySQL.Another big problem was that MySQL started to copy features (not code) from MariaDB, but used a different SQL syntax than what MariaDB was using. One example is the usage of CHANNEL in multi-source replication. It did not make any sense for MariaDB to copy the multi-source code from MySQL, as we already had a working, stable implementation we were happy with.With MariaDB 10.0, we decided to stop merges from MySQL and instead monitor new features and implement those that we thought made sense for MariaDB.Moving to MariaDB 10.0 allowed us more flexibility in adding more features to MariaDB without being constrained by the MySQL code, like Galera, Oracle compatibility, and a lot of other things listed here.Nowadays, most of the MariaDB development work is adding features customers and MariaDB users are missing (link to MariaDB 13.0 roadmap will shortly be added here). A lot of this work is related to new Oracle compatibility required by new customers, like FULL OUTER JOIN. There are still a few notable features in MySQL that we have not had time to re-implement, like multi-value indexing (for indexing JSON), JSON operators, and LATERAL tables. All of the mentioned ones are on the MariaDB 13.0 roadmap.We, the MariaDB developers, are still working on keeping MariaDB compatible with MySQL (and Percona Server). In MariaDB 10.11, we added support for the popular extensions from Percona Server. In the latest MariaDB versions we have ensured that one can replicate from MySQL to MariaDB and back.   We have also added support for the caching_sha2_password plugin, to allow MySQL users to switch to MariaDB without changing their passwords, support of the default MySQL character collation set, utf8mb4_0900_* and multiple JSON functions.We also listen to MySQL users moving to MariaDB and do our best to implement the features they need to be able to move to MariaDB. The MariaDB Foundation is there for those who want to be part of this effort!The above hopefully gives the needed background to discuss different kinds of forks (just kidding) in more detail.Internal forkFork where the company/original development team forks the product for political, redesign, or development reasons. The fork may be more or less, or not at all, compatible with the predecessor.Examples:MySQL 8.0 (someone could call this a “hard” fork as it was hard to move to it and very hard to go backwards )OpenOffice → Apache OpenOffice (after Oracle acquisition; internal governance shift)Sun Solaris → Oracle Solaris (post-acquisition direction change)KDE 3 → KDE 4 (often cited as an internal “hard” break due to massive architectural changes)Python 2 → Python 3 (not a fork in licence terms, but functionally an internal compatibility break)Drizzle (https://en.wikipedia.org/wiki/Drizzle_(database_server)External forkWhen an external group or company forks a project for various reasons. The most common reasons are creational differences in how to take the project forward or distrust in the original project owners.The external fork has a lot of subcategories:Downstream \"no-changes\" forkThe fork is based on the original project with a small, limited subset of changes to get the project to work within an ecosystem or with an external/internal project that requires some minor changes.The code is basically a rebase plus patches on top of the original code.No user-visible changes from the original project.Examples:Packages in Linux and other OS distributionsUbuntu kernel (downstream of Linux with minimal, policy-driven patches)Homebrew / MacPorts packagesDebian-patched GNU toolsAndroid Linux kernel (arguably borderline, but many devices are close to upstream + patches)Downstream forkThe fork is based on a rebase of the original code, but with user-visible changes that bring a different user experience while keeping the base 100% compatible with the original project. It is reasonably easy to move to the fork, but harder for users of this fork to move back to the original.The forks usually have the problem that newer major versions have to drop options or features when the original project adds them, which makes upgrades to the next version a bit harder.Examples:Red Hat Enterprise Linux (downstream of Fedora)Ubuntu (downstream of Debian)Amazon Linux (downstream of RHEL/CentOS lineage)PostgreSQL distributions (EDB Postgres, Amazon Aurora PostgreSQL-compatible)Percona ServerMariaDB 5.1 - &#62; 5.4 (these MariaDB versions never had to drop a feature)Compatibility forkThe fork was originally a \'Downstream fork\' but moved to, instead of using rebases, only merging selected patches from the original project and rewriting things the developers disliked. The goal is still to have high compatibility with the original project.Examples:LibreOffice (from OpenOffice.org)Jenkins (from Hudson, especially post-Oracle divergence)Percona XtraDB ClusterMariaDB 5.5Independent fork (or \"branch\")The fork is no longer dependent on the original project. It may still take selected patches or ideas from the original project.It usually tries to keep things compatible to make it easy for original project users to move to the new project, but the main focus is solving new problems for its growing user base.Examples:GhostBSDOpenBSD (from NetBSD)Illumos (from OpenSolaris)systemd (initially replacing sysvinit, now fully independent ecosystem)Neo4j Community vs Enterprise split (conceptual fit)Firefox (historically from Mozilla Suite)MariaDB 10+Some people have recently expressed that they are afraid that MySQL development is stopping or slowing down, and others have started to talk about the need to do a “soft” fork of MySQL.The point I am trying to make is that if these worries are real, then any fork will sooner or later have to become an independent fork/branch or die together with MySQL (as there will be no new features in the fork).One of the mantras in open source is that it is better to join an existing project than to create a new one! Instead of talking about creating yet another fork of MySQL, it would be better if everyone gathered around MariaDB! MariaDB development is not dependent on Oracle for its future. This is assured by the MariaDB Foundation, which was created to make it easy for anyone to participate in the development of the MariaDB server. MariaDB plc is working together with the MariaDB Foundation to make this possible.MariaDB is, after all, created by the same people who created MySQL and is developed in the way it would have been if Oracle had not bought MySQL. The rapid adoption of MariaDB (350+ million database installations and rapidly increasing) shows that MariaDB is truly the future of MySQL.PS:Please leave a comment if you have a better name for any of the fork categories, another fork category that should be added, or more examples for the categories.</p>
<p><a href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html">The concepts of forking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<div>Lately there has been a lot of discussion about &ldquo;hard&rdquo; or &ldquo;soft&rdquo; forks related to MySQL. As someone who has done a successful fork of MySQL, I think this is both confusing and trivialising the concept of forking.</div>
<div></div>
<div>In my <a href="https://monty-says.blogspot.com/2024/10/celebrating-15-years-of-mariadb.html">previous blog</a>,&nbsp; I did touch a bit on this topic, but it looks like some more clarifications are needed.</div>
<div></div>
<div>When we did the initial fork of MariaDB from MySQL, we tried our best to keep things 100% user compatible while still adding new features and fixing issues in MySQL. For MariaDB 5.1 -&gt; MariaDB 5.5, we merged all relevant changes from MySQL into MariaDB.</div>
<div></div>
<div>This did not mean that MariaDB was 100% compatible with MySQL, as any change in a fork makes things incompatible in some manner. For example, the enhanced optimiser in MariaDB 5.5 did work slightly differently (better) than MySQL, and if one used any of the new features in MariaDB, one could not trivially go back to MySQL anymore. However, for most users these changes were not notable and allowed most Linux distributions to automatically move MySQL users to MariaDB without any disturbance.</div>
<div></div>
<div>Over time, the merging of MySQL code became harder and gave us less benefit compared to the effort of doing the merges. The new MySQL developers had started to move source code around (which made merges harder), and we, the MariaDB developers, were not happy with the quality of the code related to bug fixes or some of the new features. It was easier to write the new feature from scratch than to use the MySQL code. However, for each feature we did our best to ensure that the syntax and behaviour were identical to MySQL.</div>
<div></div>
<div>Another big problem was that MySQL started to copy features (not code) from MariaDB, but used a different SQL syntax than what MariaDB was using. One example is the usage of CHANNEL in multi-source replication. It did not make any sense for MariaDB to copy the multi-source code from MySQL, as we already had a working, stable implementation we were happy with.</div>
<div></div>
<div>With MariaDB 10.0, we decided to stop merges from MySQL and instead monitor new features and implement those that we thought made sense for MariaDB.</div>
<div></div>
<div>Moving to MariaDB 10.0 allowed us more flexibility in adding more features to MariaDB without being constrained by the MySQL code, like Galera, Oracle compatibility, and a lot of other things listed <a href="https://monty-says.blogspot.com/2024/10/celebrating-15-years-of-mariadb.html">here</a>.</div>
<div></div>
<div>Nowadays, most of the MariaDB development work is adding features customers and MariaDB users are missing (link to MariaDB 13.0 roadmap will shortly be added here). A lot of this work is related to new Oracle compatibility required by new customers, like FULL OUTER JOIN. There are still a few notable features in MySQL that we have not had time to re-implement, like multi-value indexing (for indexing JSON), JSON operators, and LATERAL tables. All of the mentioned ones are on the MariaDB 13.0 roadmap.</div>
<div></div>
<div>
<div>We, the MariaDB developers, are still working on keeping MariaDB compatible with MySQL (and Percona Server). In MariaDB 10.11, we added support for the popular extensions from Percona Server. In the latest MariaDB versions we have ensured that one can r<a href="https://mariadb.com/docs/release-notes/community-server/about/compatibility-and-differences/replication-compatibility-between-mariadb-and-mysql">eplicate from MySQL to MariaDB and back</a>.&nbsp; &nbsp;We have also added support for the&nbsp;<a href="https://mariadb.com/docs/server/reference/plugins/authentication-plugins/authentication-plugin-caching_sha2_password">caching_sha2_password plugin</a>,&nbsp;to allow MySQL users to switch to MariaDB without changing their passwords, support of the default MySQL character collation set,&nbsp;<a href="https://mariadb.com/docs/release-notes/community-server/11.4/11.4.5#character-sets-and-collations">utf8mb4_0900_*</a>&nbsp;and&nbsp;<a href="https://mariadb.com/docs/server/reference/sql-functions/special-functions/json-functions">multiple JSON functions</a>.</div>
<div></div>
<div>We also listen to MySQL users moving to MariaDB and do our best to implement the features they need to be able to move to MariaDB. The&nbsp;<a href="https://mariadb.org/">MariaDB Foundation</a>&nbsp;is there for those who want to be part of this effort!</div>
</div>
<div></div>
<div>The above hopefully gives the needed background to discuss different kinds of <a href="https://www.sambonet-shop.com/en-us/types-of-forks.html" rel="nofollow">forks</a>&nbsp;(just kidding) in more detail.</div>
<div></div>
<div>Internal fork</div>
<div>
<ul>
<li>Fork where the company/original development team forks the product for political, redesign, or development reasons. The fork may be more or less, or not at all, compatible with the predecessor.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>MySQL 8.0 (someone could call this a &ldquo;hard&rdquo; fork as it was hard to move to it and very hard to go backwards )</li>
<li>OpenOffice &rarr; Apache OpenOffice (after Oracle acquisition; internal governance shift)</li>
<li>Sun Solaris &rarr; Oracle Solaris (post-acquisition direction change)</li>
<li>KDE 3 &rarr; KDE 4 (often cited as an internal &ldquo;hard&rdquo; break due to massive architectural changes)</li>
<li>Python 2 &rarr; Python 3 (not a fork in licence terms, but functionally an internal compatibility break)</li>
<li>Drizzle (https://en.wikipedia.org/wiki/Drizzle_(database_server)</li>
</ul>
</div>
<div>External fork</div>
<div>
<ul>
<li>When an external group or company forks a project for various reasons. The most common reasons are creational differences in how to take the project forward or distrust in the original project owners.</li>
</ul>
</div>
<div>The external fork has a lot of subcategories:</div>
<div></div>
<div>Downstream &ldquo;no-changes&rdquo; fork</div>
<div>
<ul>
<li>The fork is based on the original project with a small, limited subset of changes to get the project to work within an ecosystem or with an external/internal project that requires some minor changes.</li>
<li>The code is basically a rebase plus patches on top of the original code.</li>
<li>No user-visible changes from the original project.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>Packages in Linux and other OS distributions</li>
<li>Ubuntu kernel (downstream of Linux with minimal, policy-driven patches)</li>
<li>Homebrew / MacPorts packages</li>
<li>Debian-patched GNU tools</li>
<li>Android Linux kernel (arguably borderline, but many devices are close to upstream + patches)</li>
</ul>
</div>
<div>Downstream fork</div>
<div>
<ul>
<li>The fork is based on a rebase of the original code, but with user-visible changes that bring a different user experience while keeping the base 100% compatible with the original project. It is reasonably easy to move to the fork, but harder for users of this fork to move back to the original.</li>
<li>The forks usually have the problem that newer major versions have to drop options or features when the original project adds them, which makes upgrades to the next version a bit harder.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>Red Hat Enterprise Linux (downstream of Fedora)</li>
<li>Ubuntu (downstream of Debian)</li>
<li>Amazon Linux (downstream of RHEL/CentOS lineage)</li>
<li>PostgreSQL distributions (EDB Postgres, Amazon Aurora PostgreSQL-compatible)</li>
<li>Percona Server</li>
<li>MariaDB 5.1 -&gt; 5.4 (these MariaDB versions never had to drop a feature)</li>
</ul>
</div>
<div>Compatibility fork</div>
<div>
<ul>
<li>The fork was originally a &lsquo;Downstream fork&rsquo; but moved to, instead of using rebases, only merging selected patches from the original project and rewriting things the developers disliked. The goal is still to have high compatibility with the original project.</li>
</ul>
</div>
<div>
<ul>
<li>Examples:</li>
<li>LibreOffice (from OpenOffice.org)</li>
<li>Jenkins (from Hudson, especially post-Oracle divergence)</li>
<li>Percona XtraDB Cluster</li>
<li>MariaDB 5.5</li>
</ul>
</div>
<div>Independent fork (or &ldquo;branch&rdquo;)</div>
<div>
<ul>
<li>The fork is no longer dependent on the original project. It may still take selected patches or ideas from the original project.</li>
<li>It usually tries to keep things compatible to make it easy for original project users to move to the new project, but the main focus is solving new problems for its growing user base.</li>
</ul>
</div>
<div>Examples:</div>
<div>
<ul>
<li>GhostBSD</li>
<li>OpenBSD (from NetBSD)</li>
<li>Illumos (from OpenSolaris)</li>
<li>systemd (initially replacing sysvinit, now fully independent ecosystem)</li>
<li>Neo4j Community vs Enterprise split (conceptual fit)</li>
<li>Firefox (historically from Mozilla Suite)</li>
<li>MariaDB 10+</li>
</ul>
</div>
<div>Some people have recently expressed that they are afraid that <a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/">MySQL development is stopping or slowing down</a>, and others have started to talk about the need to do a &ldquo;soft&rdquo; fork of MySQL.</div>
<div></div>
<div>The point I am trying to make is that if these worries are real, then any fork will sooner or later have to become an independent fork/branch or die together with MySQL (as there will be no new features in the fork).</div>
<div></div>
<div>One of the mantras in open source is that it is better to join an existing project than to create a new one! Instead of talking about creating yet another fork of MySQL, it would be better if everyone gathered around MariaDB! MariaDB development is not dependent on Oracle for its future. This is assured by the <a href="http://mariadb.org/">MariaDB Foundation</a>, which was created to make it easy for anyone to participate in the development of the MariaDB server. <a href="http://mariadb.com/">MariaDB plc</a> is working together with the <a href="http://mariadb.org/">MariaDB Foundation</a> to make this possible.</div>
<div></div>
<div>MariaDB is, after all, created by the same people who created MySQL and is developed in the way it would have been if Oracle had not bought MySQL. The rapid adoption of MariaDB (350+ million database installations and rapidly increasing) shows that MariaDB is truly the future of MySQL.</div>
<div></div>
<div>PS:</div>
<div>Please leave a comment if you have a better name for any of the fork categories, another fork category that should be added, or more examples for the categories.</div>
<div></div>

<p><a href="http://monty-says.blogspot.com/2026/01/the-concepts-of-forking.html">The concepts of forking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>SQL Savepoints and When to Use Them</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/sql-savepoints-and-when-to-use-them/" />
      <id>https://vettabase.com/sql-savepoints-and-when-to-use-them/</id>
      <updated>2026-01-22T10:03:08+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>Not many developers know about savepoints in relational databases. Even less of them know when to use them. It’s not their fault: I can’t remember seeing a good explanation of this feature. Let’s try to clarify this lesser-known functionality. In this article I’m using MariaDB syntax. But the concepts are very similar for other transactional databases. To know the exact syntax you should use on a particular DBMS, please check its documentation. Transactions: A Brief Tutorial As you probably know, in the context of databases, a transaction is a series of instructions that will completely succeed or completely fail. Any data change that happens inside a transaction is only visible to other connections when the transaction succeeds or fails. The transaction succeeds when the COMMIT statement is succefully issued. And it fails when an error occurs (though there can be exceptions, depending on which database you use) or the ROLLBACK command is issued. The SQL statements of a transaction usually look like this: Or: This can be confusing, at the beginning: why would one run ROLLBACK and make a transaction fail? Shouldn’t it fail automatically when an error occurs? The reasons are: There is much more to say about transactions, […]</p>
<p><a href="https://vettabase.com/sql-savepoints-and-when-to-use-them/">SQL Savepoints and When to Use Them</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">Not many developers know about savepoints in relational databases. Even less of them know when to use them. It&rsquo;s not their fault: I can&rsquo;t remember seeing a good explanation of this feature. Let&rsquo;s try to clarify this lesser-known functionality.</p>
<p class="wp-block-paragraph">In this article I&rsquo;m using MariaDB syntax. But the concepts are very similar for other transactional databases. To know the exact syntax you should use on a particular DBMS, please check its documentation.</p>
<h2 class="wp-block-heading">Transactions: A Brief Tutorial<a class="anchor-link" id="transactions-a-brief-tutorial"></a></h2>
<p class="wp-block-paragraph">As you probably know, in the context of databases, a transaction is a series of instructions that will completely succeed or completely fail. Any data change that happens inside a transaction is only visible to other connections when the transaction succeeds or fails. The transaction succeeds when the <code>COMMIT</code> statement is succefully issued. And it fails when an error occurs (though there can be exceptions, depending on which database you use) or the <code>ROLLBACK</code> command is issued.</p>
<p class="wp-block-paragraph">The SQL statements of a transaction usually look like this:</p>
<pre class="wp-block-code"><code>START TRANSACTION;
-- read or write some data
COMMIT;</code></pre>
<p class="wp-block-paragraph">Or:</p>
<pre class="wp-block-code"><code>START TRANSACTION;
-- read or write some data
ROLLBACK;</code></pre>
<p class="wp-block-paragraph">This can be confusing, at the beginning: why would one run <code>ROLLBACK</code> and make a transaction fail? Shouldn&rsquo;t it fail automatically when an error occurs?</p>
<p class="wp-block-paragraph">The reasons are:</p>
<ul class="wp-block-list">
<li><strong>Some errors do not make the transaction fail automatically</strong>. While the last statement has failed, the transaction might still be open so that you can retry or ignore the failed operation. The details vary depending on the DBMS you&rsquo;re using. On some DBMSs, this is the case for syntax errors. But your application &ldquo;knows&rdquo; that an SQL syntax error can only be caused by a bug, and the only safe thing to do is rolling back.</li>
<li><strong>You might detect a logical inconsistency</strong>. Sure, databases can have constraints that prevent some types of inconsistencies to be introduced in the data, like foreign keys or <a href="https://vettabase.com/validating-rows-with-check-constraints-in-mariadb/" data-type="post" data-id="337540"><code>CHECK</code> constraints</a>. But it&rsquo;s impossible to prevent all possible inconsistencies. When you read rows you might find an inconsistency and you might want to rollback. For example, you might find that a number is higher than its logical maximum, or a string is unexpectedly empty.</li>
<li><strong>Cancelling operations</strong>. The user might stop the operation by pressing CTRL-C or some button on your website. Or you might read rows, and find some reason why an operation must be cancelled. For example, the products that need to be sold aren&rsquo;t in stack.</li>
</ul>
<p class="wp-block-paragraph">There is much more to say about transactions, but we can dig into this topic in another article. Let&rsquo;s move on, to savepoints.</p>
<h2 class="wp-block-heading">Using Savepoints<a class="anchor-link" id="using-savepoints"></a></h2>
<p class="wp-block-paragraph">It&rsquo;s worth stressing that a transaction must be atomic: it will entirely succeed or entirely fail. Still, it can contain savepoints. When the first part of a transaction succeed but then something failed, you might not want to rollback the entire transaction. Maybe you want to rollback to a savepoint (hopefully the last successful query) and try again the rest. Or maybe, just acknowledge that the rest of the transaction can&rsquo;t succeed and give it up &ndash; there are cases when this makes sense.</p>
<p class="wp-block-paragraph">Let&rsquo;s see how it works. using once again MariaDB syntax:</p>
<pre class="wp-block-code"><code>START TRANSACTION;
-- write something successfully
SAVEPOINT orders_updated;
-- write something else successfully
SAVEPOINT inventory_updates;
-- try something that fails
ROLLBACK TO inventory_updates;
-- try again
COMMIT;</code></pre>
<p class="wp-block-paragraph">In this example we&rsquo;re rolling back to the latest savepoint, <code>inventory_updates</code>. We can also rollback to previous one, or we can rollback the transaction completely.</p>
<h2 class="wp-block-heading">When to Use Savepoints in Real Life<a class="anchor-link" id="when-to-use-savepoints-in-real-life"></a></h2>
<p class="wp-block-paragraph">Now you know how to use savepoints. But when and why would you do that? It&rsquo;s not obvious, and I know that it&rsquo;s hard to find a good explanation.</p>
<p class="wp-block-paragraph">You should think the instructions after a savepoint as an <strong>optional sub-transaction</strong>. The subtransaction will:</p>
<ul class="wp-block-list">
<li>Completely succeed or completely rollback.</li>
<li>Rollback if the main transaction rolls back.</li>
<li>See the same data as the same transaction. If you use the <code>REPEATABLE READ</code> isolation level, which is the default in MariaDB but not in most other databases.</li>
<li>Produce changes that become visible to other connections only when the whole global transaction commits.</li>
</ul>
<p class="wp-block-paragraph">That said, here are some scenarios where savepoints (sub-transactions) will prove useful.</p>
<p class="wp-block-paragraph"><strong>Optional or Experimental Features</strong></p>
<p class="wp-block-paragraph">The subtransaction is about an optional or experimental feature of the application. Maybe the user created a web page that is written into the database. Some other information might be written in the same transaction: a tag creation, a category creation, whatever.</p>
<p class="wp-block-paragraph">Some SEO-related information should also be written. It&rsquo;s useful to make this happen in the same transaction, so that the final result will become visible altogether. But the SEO-related information is written into two tables. One always exists, but the other only exist if a certain plugin was loaded.</p>
<p class="wp-block-paragraph">Or maybe it&rsquo;s still an experimental feature. It usually works, but you know that it may fail under certain circumstances. If the SEO information can&rsquo;t be entirely written, you don&rsquo;t want any of it to be written. But you still want the web page to be created.</p>
<p class="wp-block-paragraph"><strong>Retries</strong></p>
<p class="wp-block-paragraph">Some applications have to do operations that fail relatively often, but they need to retry until successful. The reasons are usually a non-optimal way to handle concurrency, or dependence from an external technology that is not as reliable as it should be (external services or non-optimal microservices).</p>
<p class="wp-block-paragraph">Some transactions include many statement, or slow statements. If you need to retry them many times, this can easily result in frequent row locks that reduce the application&rsquo;s scalability.</p>
<p class="wp-block-paragraph">This can be partly avoided if the transaction starts with some statements that usually succeed. After there &ldquo;safe&rdquo; statements, a savepoint should be set. Then the statement is likely to fail (or not produce the expected results) should follow. If more than one statement is likely to fail, you can set a savepoint just before each of them. In this way, in case of a failure (whether it is an error or a logical failure) you can rollback to the last savepoint and only retry the last statement. Your DBAs will thank you.</p>
<p class="wp-block-paragraph"><strong>Releasing Locks</strong></p>
<p class="wp-block-paragraph">Maybe you&rsquo;re updating big portions of rows from multiple tables. It is probably a batch operation run by a job. For example, maybe you&rsquo;re updating the inventory. Occasionally there are Inventory discrepancies sometimes occur between your warehouse and database. They are caused by events that your software doesn&rsquo;t know about, like damages, incorrect deliveries, thefts, and so on.</p>
<p class="wp-block-paragraph">Your application receives a JSON file containing inventory data, then it selects the rows in your tables, and then it corrects the differences. This happens in a single transaction. You need to be sure that no one changes the products quantities while you&rsquo;re performing these reconciliations, so you use <code>SELECT ... FOR UPDATE</code> to acquire exclusive locks on the rows.</p>
<p class="wp-block-paragraph">But the reconciliation might take a long time, and locking all rows for the whole duration would cause too many problems. So, before reading each table, you should set a savepoint. If the table doesn&rsquo;t need to be modified, you can rollback to the last savepoint, to allow the database to release locks acquired after the savepoint.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">We discussed savepoints, a lesser-known SQL feature. Savepoints can greatly improve the handling of transactions that are likely to fail, especially the longer transactions.</p>
<p class="wp-block-paragraph">If you have opinions or some experience with savepoints that you&rsquo;d like to share, please comment! Your comments are valuable to us.</p>
<p class="wp-block-paragraph">If you&rsquo;d like to know more about optimising translactions and SQL queries, consider our <a href="https://vettabase.com/services/database-training/" data-type="page" data-id="38">training courses</a>.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/sql-savepoints-and-when-to-use-them/">SQL Savepoints and When to Use Them</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB doesn&#8217;t depend on MySQL</title>
      <link rel="alternate" type="text/html" href="https://programmingbrain.com/2025/01/mariadb-does-not-depend-on-mysql.html" />
      <id>https://programmingbrain.com/2025/01/mariadb-does-not-depend-on-mysql.html</id>
      <updated>2026-01-21T17:07:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Thoughts on how MariaDB is incorrectly perceived merely as a fork of MySQL and how MariaDB is independent from MySQL yet highly compatible</p>
<p><a href="https://programmingbrain.com/2025/01/mariadb-does-not-depend-on-mysql.html">MariaDB doesn&#8217;t depend on MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Thoughts on how MariaDB is incorrectly perceived merely as a fork of MySQL and how MariaDB is independent from MySQL yet highly compatible</p>

<p><a href="https://programmingbrain.com/2025/01/mariadb-does-not-depend-on-mysql.html">MariaDB doesn&#8217;t depend on MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Vettabase and HammerDB Partner to de-Risk Database Migrations</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/vettabase-and-hammerdb-partner-to-de-risk-database-migrations/" />
      <id>https://vettabase.com/vettabase-and-hammerdb-partner-to-de-risk-database-migrations/</id>
      <updated>2026-01-20T09:14:10+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>Vettabase and HammerDB are announcing a partnership to de-risk and assist database migrations. As vendor-independent companies that offer services for multiple database technologies, Vettabase and HammerDB intend to help organisations in the delicate move of changing their database systems. For many teams, a migration to another database is a logical option — but it comes with real concerns: Our partnership is designed specifically to address these challenges. A database migration is a complex, delicate process. We can assist clients in all the stages of this process, including: A special case: migrating from MySQL Organizations that rely on MySQL are increasingly facing a difficult question: how to ensure long-term stability as MySQL development slows down. Recent MySQL developments have led to uncertainty about its long-term direction. Vettabase and HammerDB’s role, in this scenario, is simple: enable companies to move from MySQL safely, predictably, and without performance surprises. We will help companies to identify the best database technology. As vendor-independent service providers, we don’t have bias dictated by commercial interests. MariaDB is often the natural choice because of its high level of compatibility with MySQL, but we are open to evaluate PostgreSQL, and potentially other solutions. Vettabase brings deep, hands-on experience in […]</p>
<p><a href="https://vettabase.com/vettabase-and-hammerdb-partner-to-de-risk-database-migrations/">Vettabase and HammerDB Partner to de-Risk Database Migrations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph"><strong>Vettabase</strong> and <strong>HammerDB</strong> are announcing a partnership to de-risk and assist database migrations. As vendor-independent companies that offer services for multiple database technologies, Vettabase and HammerDB intend to help organisations in the delicate move of changing their database systems.</p>
<p class="wp-block-paragraph">For many teams, a migration to another database is a logical option &mdash; but it comes with real concerns:</p>
<ul class="wp-block-list">
<li>How to avoid production incidents;</li>
<li>How to avoid regressions in performance or scalability;</li>
<li>How to validate that the new system meets or exceeds existing workloads;</li>
<li>How to plan and execute the migration without disrupting production;</li>
<li>To which extent existing skills and tooling can be adapted to the new technology.</li>
</ul>
<p class="wp-block-paragraph">Our partnership is designed specifically to address these challenges.</p>
<p class="wp-block-paragraph">A database migration is a complex, delicate process. We can assist clients in all the stages of this process, including:</p>
<ul class="wp-block-list">
<li>Identify the best technology for a specific workload;</li>
<li>Testing and benchmarking a specific workload with the designed technology;</li>
<li>Assessing the risks;</li>
<li>Setting up staging and production environments;</li>
<li>Performance tuning, configuration and schema optimisation;</li>
<li>Training for Database Administrators and Developers;</li>
<li>Post-deployment 24/7 support.</li>
</ul>
<p class="wp-block-paragraph"><strong>A special case: migrating from MySQL</strong></p>
<p class="wp-block-paragraph">Organizations that rely on MySQL are increasingly facing a difficult question: how to ensure long-term stability as MySQL development slows down. Recent MySQL developments have led to uncertainty about its long-term direction.</p>
<p class="wp-block-paragraph">Vettabase and HammerDB&rsquo;s role, in this scenario, is simple: <strong>enable companies to move from MySQL safely, predictably, and without performance surprises</strong>. We will help companies to identify the best database technology. As vendor-independent service providers, we don&rsquo;t have bias dictated by commercial interests. MariaDB is often the natural choice because of its high level of compatibility with MySQL, but we are open to evaluate PostgreSQL, and potentially other solutions.</p>
<p class="wp-block-paragraph"><strong>Vettabase</strong> brings deep, hands-on experience in MariaDB, MySQL and PostgreSQL critical setups, as well as its 24/7 support, disaster recovery planning, performance tuning, and targeted DBA training.</p>
<p class="wp-block-paragraph"><strong>HammerDB</strong> provides objective performance validation throughout the process. By applying repeatable, industry-standard workloads before and after migration, and during the optimisation stage, HammerDB helps ensure that performance characteristics are understood, measurable, and preserved &mdash; or improved.</p>
<p class="wp-block-paragraph">This collaboration builds on Vettabase and HammerDB&rsquo;s established role in the MariaDB ecosystem, and we&rsquo;d like to highlight HammerDB&rsquo;s work with the MariaDB Foundation to identify and resolve significant performance bottlenecks. Together, Vettabase and HammerDB offer a migration approach grounded in evidence rather than assumptions.</p>
<p class="wp-block-paragraph">The partnership is aimed at organizations that depend on MySQL today but need a database platform with an actively maintained core, a clear future, and transparent governance &mdash; without accepting migration risk as a necessary cost.</p>
<p class="wp-block-paragraph">For more information about joint migration and validation services, refer to <a href="https://www.hammerdb.com/services.html" rel="noopener">HammerDB&rsquo;s website</a>.</p>
<p class="wp-block-paragraph"><strong>About Vettabase</strong></p>
<p class="wp-block-paragraph">Vettabase is a database consulting company specializing in MariaDB, MySQL, PostgreSQL, Cassandra, and related ecosystems. It helps organizations address scalability, performance, and high-availability challenges through consulting, 24/7 support, and training.</p>
<p class="wp-block-paragraph"><strong>About HammerDB</strong></p>
<p class="wp-block-paragraph">HammerDB is the company behind the open-source HammerDB database benchmarking tool, widely used to evaluate and compare database performance using industry-standard workloads.</p>
<p class="wp-block-paragraph"><strong><a href="https://www.hammerdb.com/services.html" rel="noopener">Contact us to discuss Database Migrations</a></strong></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/vettabase-and-hammerdb-partner-to-de-risk-database-migrations/">Vettabase and HammerDB Partner to de-Risk Database Migrations</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Distributed, Multi-Database Transactions Involving MariaDB and PostgreSQL</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/distributed-multi-database-transactions-involving-mariadb-and-postgresql/" />
      <id>https://vettabase.com/distributed-multi-database-transactions-involving-mariadb-and-postgresql/</id>
      <updated>2026-01-19T12:12:43+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>In some situations, an application needs to run a single logical transaction that involves multiple database technologies: in our example, they’ll be MariaDB and PostgreSQL. This is not an optimal scenario and I’m not recommending to design systems in this way. But it’s simply a situation that you might have to deal with in real life, for various reasons that are outside of the scope of this article. Multi-database work implies several problems, because a transaction must be atomic, but in a distributed architecture this is very hard to guarantee. This article explores how to run distributed transactions using similar built-in features of MariaDB and PostgreSQL: two-phase commit transactions (2pc transactions). This is not the only solution and it’s not always the best. There are different patterns to deal with this situation, and there is software that implement these patterns for you. But 2pc transactions are relatively simple to use for developers, and they only imply sending some special SQL commands to the database. We’ll also discuss which problems you will solve using 2pc transactions, and which problems will arise because of this solution. See also the xa-utils repository that contains bonus material for this article. Atomicity and Durability Important […]</p>
<p><a href="https://vettabase.com/distributed-multi-database-transactions-involving-mariadb-and-postgresql/">Distributed, Multi-Database Transactions Involving MariaDB and PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">In some situations, an application needs to run a single logical transaction that involves multiple database technologies: in our example, they&rsquo;ll be MariaDB and PostgreSQL. This is not an optimal scenario and I&rsquo;m not recommending to design systems in this way. But it&rsquo;s simply a situation that you might have to deal with in real life, for various reasons that are outside of the scope of this article.</p>
<p class="wp-block-paragraph">Multi-database work implies several problems, because a transaction must be atomic, but in a distributed architecture this is very hard to guarantee. This article explores how to run distributed transactions using similar built-in features of MariaDB and PostgreSQL: <strong>two-phase commit transactions</strong> (2pc transactions).</p>
<p class="wp-block-paragraph">This is not the only solution and it&rsquo;s not always the best. There are different patterns to deal with this situation, and there is software that implement these patterns for you. But 2pc transactions are relatively simple to use for developers, and they only imply sending some special SQL commands to the database.</p>
<p class="wp-block-paragraph">We&rsquo;ll also discuss which problems you will solve using 2pc transactions, and which problems will arise because of this solution.</p>
<p class="wp-block-paragraph">See also the <a href="https://github.com/Vettabase/xa-utils/" rel="noopener"><strong>xa-utils</strong> repository</a> that contains bonus material for this article.</p>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>A note about terminology. The reader might be confused by the terms <strong>2pc transactions</strong> and <strong>XA transactions</strong>, used in an apparently interchangeable way in this article. 2pc transactions is a generic term describing a concept, and it applies to both PostgreSQL and MariaDB. XA is a standard that MariaDB and many other databases follow, but Postgres does not.</p>
</div>
</fieldset>
</div>
<h2 class="wp-block-heading">Atomicity and Durability<a class="anchor-link" id="atomicity-and-durability"></a></h2>
<p class="wp-block-paragraph">Important characteristics of transactions are atomicity and durability. Atomicity means that multiple writes to multiple tables can be enclosed in a single transaction, and yet that transaction can only completely succeed or completely fail. You can issue a <code>ROLLBACK</code> command to revert the changes, or the changes might be reverted automatically because an error of some kind causes the transaction to fail. But in no case will the changes be partially reverted.</p>
<p class="wp-block-paragraph">This includes the cases when the database crashes. Transactions that are not complete will never be applied. Transactions that ended with a successful <code>COMMIT</code> command and made some changes won&rsquo;t be lost. This is called durability.</p>
<p class="wp-block-paragraph">Single-database transactions are held by a single technology, that implements atomicity and durability entirely. MariaDB implements transactions in a classic way, by using transaction logs that can always be used (in conjunction with lazily updated tablespaces) to reconstruct an exact version of the data. PostgreSQL does this in a more simplistic way, by storing each physical version of each row in the files, periodically removing old unused versions, and trying to keep an index visibility map that prevents too many accesses to obsolete versions. But the way the DBMSs implement durability is important for the DBAs, not for developers: from an end-user perspective, all databases behave more or less in the same way.</p>
<p class="wp-block-paragraph">But how to guarantee atomicity and durability when multiple technologies are involved? The solution is conceptually simple: the transaction has two commits, or if you prefer, it has a preparation phase that needs to precede the commit.</p>
<h2 class="wp-block-heading">A Single-Database 2pc Transaction Workflow<a class="anchor-link" id="a-single-database-2pc-transaction-workflow"></a></h2>
<p class="wp-block-paragraph">For each involved database, the workflow will be the following:</p>
<ol class="wp-block-list">
<li>The transaction starts.</li>
<li>Data changes are sent to the database.</li>
<li>The transaction is prepared. This means that the transaction itself is persisted, and will survive a connection drop or a database crash. But the data changes are not applied yet, and are not visible to other transactions. Importantly, rollback is still allowed.</li>
<li>The transaction is committed. The changes are applied to the data, and become visible to new transactions.</li>
</ol>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>One step is missing: the transaction&rsquo;s end. This is step is only relevant when your application needs to do something more complex. But MariaDB and PostgreSQL don&rsquo;t support this. See the limitations below.</p>
</div>
</fieldset>
</div>
<h2 class="wp-block-heading">A Multi-Database Workflow<a class="anchor-link" id="a-multi-database-workflow"></a></h2>
<p class="wp-block-paragraph">It&rsquo;s important to understand the above workflow for a single-database 2pc transaction. But now, let&rsquo;s see the overall workflow for a transaction that involves several databases.</p>
<ol class="wp-block-list">
<li>The application starts to write data into the databases involved in the transaction.</li>
<li>As soon as possible, the application will run a <em>prepare</em> command. It&rsquo;s entirely possible to prepare a transaction on a database, and then continue to write into other databases.</li>
<li>Did any prepare command fail? If so, the application will run a rollback command against all the involved databases.</li>
<li>Did all prepare commands succeed? If so, the application will run a commit against all the database.</li>
</ol>
<h2 class="wp-block-heading">Which Problems Still Hold<a class="anchor-link" id="which-problems-still-hold"></a></h2>
<p class="wp-block-paragraph">Database servers may crash or become unreachable. Application servers might crash too. In theory 2pc transactions solve these problems, because the preparation phase persists the transaction, but if something fails elsewhere, the application can still rollback the prepared transaction. In practice, though, a database crash at the wrong time can still cause headaches, or worse.</p>
<p class="wp-block-paragraph">Let&rsquo;s see what can go wrong.</p>
<p class="wp-block-paragraph"><strong>A database crashes and won&rsquo;t come back in a reasonable time</strong></p>
<p class="wp-block-paragraph">A prepared transaction still has active locks. This is by design. Row locks and metadata locks can&rsquo;t be released before commit, because the application might never decide to apply those changes. But what if one of the involved databases has crashed and its data is corrupted? Bringing it up again might take a long time.</p>
<p class="wp-block-paragraph">You&rsquo;ll have to rollback the transaction immediately on the databases that are up and running. Once the failed database restarts, you must make sure that the transaction is rolled back.</p>
<p class="wp-block-paragraph"><strong>A database permanently dies after other databases performed a commit</strong></p>
<p class="wp-block-paragraph">Your application verifies that all preparations succeeded. It starts to send commits to all the involved databases. But&hellip; after at least one commit succeeded, one commit fails because a server is gone for good. Maybe the data centre is on fire, maybe SSH accesses are gone and beyond repair. You&rsquo;ll be able to restore data in the end, but you&rsquo;ll have to use a backup, and this will take hours.</p>
<p class="wp-block-paragraph">You might have to redo the transaction. In the best case, the data you need to write are the same data written in one of the other databases involved in the transaction. If some data is missing from the other databases, considering writing it to allow this type of recovery. For example, if only a <code>state</code> column is present in database <code>B</code> but absent from database <code>A</code>, consider adding it and keeping it updated. This won&rsquo;t make recovery easy, but at least it should be possible.</p>
<p class="wp-block-paragraph">An alternative is running a <em>compensatory transaction</em> in the databases that are still up and running. The application should have the logic to do this. Sometimes it&rsquo;s relatively easy: if the application <code>INSERT</code>ed a row, all it has to do is to <code>DELETE</code> it. But if the application <code>DELETE</code>d or <code>UPDATE</code>d rows, the application needs to somehow remember the old values, to be able to rewrite them.</p>
<p class="wp-block-paragraph">With MariaDB, you can use system-verioned tables. The older versions of a <code>DELETE</code>d or <code>UPDATE</code>d row still exist. You can explicitly query old rows, and retrieve the values to restore. Unfortunately, PostgreSQL doesn&rsquo;t have this feature.</p>
<p class="wp-block-paragraph">Clearly, I&rsquo;m simplifying things. When you decide to <em>compensate</em> a transaction that shouldn&rsquo;t have been committed, the rows might have been further modified by other transactions that were correctly committed. This case is complex to handle, and the right thing to do depends on too many factors &ndash; I can&rsquo;t provide a generic guidance on this.</p>
<p class="wp-block-paragraph"><strong>The application server crashes and restarts while the DBA is taking action</strong></p>
<p class="wp-block-paragraph">To run 2pc transactions, you might want to use a transaction <em>orchestrator</em>, or <em>transaction manager</em>, like <a href="https://shardingsphere.apache.org/" rel="noopener">Apache ShardingSphere</a> or <a href="https://www.atomikos.com/Main/WebHome" rel="noopener">Atomikos</a>. They automatically handle preparations, commits and rollbacks for a multi-database transaction. Even if the application server crashes, they will take care of prepared transactions on restart.</p>
<p class="wp-block-paragraph">But in the meanwhile, a DBA or some other automation take care of the prepared transacctions. And these actors (the transaction manager and the DBA) might disagree on what to do: the transaction manager might know that some transactions can be committed on all involved databases, but a DBA might decide to rollback everything everywhere &ldquo;just to be sure&rdquo;. This might lead to have a partially committed and partially rolled back transaction.</p>
<p class="wp-block-paragraph">How to prevent this issue? Configure the transaction manager to always rollback all transactions on restart, if this is possible. If it&rsquo;s not possible, the DBA (or any automation script) must somehow make sure that the transaction manager won&rsquo;t resurrect at the wrong time. Or they might temporarily revoke all their permissions on the database.</p>
<h2 class="wp-block-heading">MariaDB Syntax<a class="anchor-link" id="mariadb-syntax"></a></h2>
<p class="wp-block-paragraph">MariaDB syntax for 2pc transactions adheres to the XA Open/X standard:</p>
<pre class="wp-block-code"><code>XA START '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;
-- read or write some data here ...
XA END '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;
XA PREPARE '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;
XA COMMIT '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;</code></pre>
<p class="wp-block-paragraph"><code>'019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1</code> is the transaction id, which consists of three components. Only the first is mandatory. The second component is the branch id, and it could be a generical <code>'.mariadb'</code> or an id for the current MariaDB server or cluster. If the component has a meaning, it&rsquo;s a good idea to start it with a separator, because when we list the prepared transactions it will appear concatenated to the first component. The last component is meant to be a version number of the format, usable to know how the first components should be interpreted. It&rsquo;s 1 by default. MariaDB doesn&rsquo;t interpret these components, but if you use a transaction manager, it might interpret them. The id of a committed or rollbacked transaction can be reused.</p>
<p class="wp-block-paragraph">During the transaction, you might realise that you don&rsquo;t need to write to other databases, so you might want to skip <code>XA PREPARE</code> and use a one-phase connection instead:</p>
<pre class="wp-block-code"><code>XA START '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;
-- read or write some data here ...
XA END '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;
XA COMMIT '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1 ONE PHASE;</code></pre>
<p class="wp-block-paragraph">To rollback:</p>
<pre class="wp-block-code"><code>XA ROLLBACK '019bad8c-2ea2-7080-9332-3274861c1969', '.maria', 1;</code></pre>
<p class="wp-block-paragraph">To list prepared transactions, and then decide what to do with them:</p>
<pre class="wp-block-code"><code>MariaDB [(none)]&gt; XA RECOVER;
+----------+--------------+--------------+---------------+
| formatID | gtrid_length | bqual_length | data          |
+----------+--------------+--------------+---------------+
|        1 |           13 |            0 | Transaction 1 |
|        3 |            3 |            6 | t31.maria     |
|        1 |           11 |            0 | xxx-xxx-xxx   |
+----------+--------------+--------------+---------------+

MariaDB [(none)]&gt; XA RECOVER FORMAT = 'SQL';
+----------+--------------+--------------+-----------------------------+
| formatID | gtrid_length | bqual_length | data                        |
+----------+--------------+--------------+-----------------------------+
|        1 |           13 |            0 | 'Transaction 1'             |
|        3 |            3 |            6 | X'743331',X'2e6d61726961',3 |
|        1 |           11 |            0 | 'xxx-xxx-xxx'               |
+----------+--------------+--------------+-----------------------------+

MariaDB [(none)]&gt; XA ROLLBACK X'743331',X'2e6d61726961',3;
ERROR 1402 (XA100): XA_RBROLLBACK: Transaction branch was rolled back

MariaDB [(none)]&gt; XA RECOVER FORMAT = 'SQL';
+----------+--------------+--------------+-----------------+
| formatID | gtrid_length | bqual_length | data            |
+----------+--------------+--------------+-----------------+
|        1 |           13 |            0 | 'Transaction 1' |
|        1 |           11 |            0 | 'xxx-xxx-xxx'   |
+----------+--------------+--------------+-----------------+</code></pre>
<p class="wp-block-paragraph"><code>FORMAT = 'SQL'</code> is very convenient, because it shows the transaction id exactly as it should appear in <code>XA COMMIT</code> or <code>XA ROLLBACK</code>.</p>
<h2 class="wp-block-heading">PostgreSQL Syntax<a class="anchor-link" id="postgresql-syntax"></a></h2>
<p class="wp-block-paragraph">PostgreSQL syntax is not based on a particular standard, so it might not work with some transaction managers.</p>
<p class="wp-block-paragraph">In PostgreSQL you start a transaction normally. If you decide to commit in one phase, you will use no special syntax. If you decide to use a two-phase commit, you&rsquo;ll need to use <code>PREPARE TRANSACTION</code> and <code>COMMIT PREPARED</code>.</p>
<pre class="wp-block-code"><code>START TRANSACTION;
PREPARE TRANSACTION '019badbc-bb6b-7eb0-b5ac-439ade362710';
COMMIT PREPARED '019badbc-bb6b-7eb0-b5ac-439ade362710';</code></pre>
<p class="wp-block-paragraph">To rollback:</p>
<pre class="wp-block-code"><code>ROLLBACK PREPARED '019badbc-bb6b-7eb0-b5ac-439ade362710';</code></pre>
<p class="wp-block-paragraph">To list the prepared transactions:</p>
<pre class="wp-block-code"><code>postgres=# SELECT * FROM pg_catalog.pg_prepared_xacts;
 transaction |                 gid                  |           prepared            |  owner   | database 
-------------+--------------------------------------+-------------------------------+----------+----------
         769 | 019badbc-bb6b-7eb0-b5ac-439ade362710 | 2026-01-15 22:50:16.098882+00 | postgres | postgres
         771 | trx2                                 | 2026-01-15 22:51:27.578579+00 | postgres | postgres
         772 | trx3                                 | 2026-01-15 22:51:48.320954+00 | postgres | postgres

postgres=# ROLLBACK PREPARED '019badbc-bb6b-7eb0-b5ac-439ade362710';
ROLLBACK PREPARED

postgres=# SELECT * FROM pg_catalog.pg_prepared_xacts;
 transaction | gid  |           prepared            |  owner   | database 
-------------+------+-------------------------------+----------+----------
         772 | trx3 | 2026-01-15 22:51:48.320954+00 | postgres | postgres
         771 | trx2 | 2026-01-15 22:51:27.578579+00 | postgres | postgres</code></pre>
<h2 class="wp-block-heading">A Complete Scenario<a class="anchor-link" id="a-complete-scenario"></a></h2>
<p class="wp-block-paragraph">Now that we discussed how XA transactions work and how to use them, let&rsquo;s see a complete example in a realistic scenario.</p>
<p class="wp-block-paragraph">The use case is the following: we have two applications working with inventory data. The ecommerce platform uses MariaDB and the procurement application uses PostgreSQL. Both read and write data. Importantly for this example, both can decrease a product&rsquo;s availability (we&rsquo;re a reseller, and the produrement app handles returns of flawed products to the vendor). This is hopefully a temporary situation, as one of the applications will be abandoned or ported to the other database. But in the meanwhile, the company must be able to operate.</p>
<p class="wp-block-paragraph">Here&rsquo;s what should happen when a customer buys a product:</p>
<ol class="wp-block-list">
<li>We check for product availability in MariaDB. If it&rsquo;s equal or higher than the desired quantity, the purchase can take place. But we need to make sure that concurrent transactions don&rsquo;t read or modify the quantity before the purchase is completed or cancelled.</li>
<li>We check for product availability in Postgres, too. And again, we need to make sure that no one can read or modify the quantity before the purchase has completed. If we can&rsquo;t acquire the lock immediately or if the quantities don&rsquo;t match, we don&rsquo;t have a reasonable guarantee of consistency. So the purchase will fail &ndash; but it may retry immediately, or after a short timeout.
<ul class="wp-block-list">
<li>Special case: if we don&rsquo;t find the desided product in Postgres, the product is not handled by the procurement app. In this case, we&rsquo;ll complete the MariaDB transaction with a one-phase commit. This is not strictly necessary, but it&rsquo;s a performance optimisation we can use when we realise that a 2pc is not needed.</li>
</ul>
</li>
<li>We modify the quantity in MariaDB (<code>UPDATE</code>)..</li>
<li>We <code>PREPARE</code> the XA transaction in MariaDB.</li>
<li>We modify the quantity in PostgreSQL (<code>UPDATE</code>).</li>
<li>We <code>PREPARE</code> the 2pc transaction in PostgreSQL.</li>
<li>If every former step succceeded, we <code>COMMIT</code> the XA transaction in MariaDB. Otherwise, we <code>ROLLBACK</code>.</li>
<li>If every former step succceeded, at least up to point 6, we also <code>COMMIT</code> the XA transaction in PostgreSQL. Otherwise, we <code>ROLLBACK</code>.</li>
<li>If a database crashed before the final <code>COMMIT</code>, we will <code>COMMIT</code> the transaction at restart.</li>
</ol>
<p class="wp-block-paragraph">The SQL syntax was explained above, so I&rsquo;m not including the commands here for the sake of brevity.</p>
<p class="wp-block-paragraph">It is important to have a log of these operations at application level, to be able to debug all sorts of problems &ndash; including applicaiton bugs or DBMSs bugs. To make a logical distributed transaction easier to follow across the log, it would be useful to use the same transaction id on MariaDB and PostgreSQL.</p>
<h2 class="wp-block-heading">Locks and Performance Considerations<a class="anchor-link" id="locks-and-performance-considerations"></a></h2>
<p class="wp-block-paragraph">To achieve true global consistency, XA transactions should theoretically use the <code>SERIALIZABLE</code> isolation level. This is the same as <code>REPEATABLE READ</code>, except that reads acquire shared locks on the rows they examine. In this way, writes requested by other transactions will be delayed until commit or rollback. This is important because transactions are not committed at the same time in every database. So, without locks, data would be globally inconsistent.</p>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>Note that <code>SERIALIZABLE</code> acquires shared locks, not exclusive locks. This means that, as a general rule, consurrent reads are allowed. Acquiring exclusive locks is possible, but normally it&rsquo;s not necessary.</p>
</div>
</fieldset>
</div>
<p class="wp-block-paragraph"><code>SERIALIZABLE</code> can come with serious scalability issues. It might result into transactions constantly waiting for each other. For this reason, <code>REPEATABLE READ</code> is normally the right choice. It is the default in MariaDB, but it should be set manually in PostgreSQL (the default is <code>READ COMMITTED</code>).</p>
<p class="wp-block-paragraph">Note that 2pc transactions are not lightweight. Generally speaking, it is recommended that an application doesn&rsquo;t do anything else in the middle of a transaction, to end the transaction as soon as possible. The reason is that long transactions tend to damage a database performance and they keep locks alive for more time. This recommendation can&rsquo;t be applied to 2pc transactions because they&rsquo;re meant exactly to allow you to run other transactions on other databases, while at least database is waiting. You should still try not to make 2pc transactions longer than necessary.</p>
<p class="wp-block-paragraph">2pc transactions don&rsquo;t release locks on preparation. They can&rsquo;t, because the database doesn&rsquo;t know yet if the transaction will be committed or not. So the applications should be prepared to wait for longer lock times. Deadlocks are also possible. While deadlocks that involve a single database can still be detected, deadlocks that involve multiple databases cannot. So the timeout should be long enough to allow the application to tolerate frequent normal locks, but short enough to avoid incidents in case of distributed deadlocks.</p>
<h2 class="wp-block-heading">Limitations<a class="anchor-link" id="limitations"></a></h2>
<p class="wp-block-paragraph">MariaDB and PostgreSQL have similar limitations concerning 2pc transactions.</p>
<h3 class="wp-block-heading">Common Limitations<a class="anchor-link" id="common-limitations"></a></h3>
<p class="wp-block-paragraph">The XA standard separates database connections from transaction. A connection should be able to detach from a transaction, and another connection should be able to take control of that connection and continue its work. This is supported by Oracle, DB2 and SQL Server. But MariaDB and PostgreSQL don&rsquo;t support this feature. MariaDB supports the syntax to do this, but it will return an error, except for the case when a transaction is suspended by a connection, and the <em>same connection</em> later resumes the <em>same transaction</em>.</p>
<p class="wp-block-paragraph">In both MariaDB and PostgreSQL, observability is limited. You can monitor transactions, regardless they have 1-phase or 2-phase commits. But in the case of 2-phase commits, it would be useful to know which transactions have been prepared. If nothing else, because you can&rsquo;t kill them unless you&rsquo;re sure that they are rolled back in other databases.</p>
<h3 class="wp-block-heading">MariaDB Limitations<a class="anchor-link" id="mariadb-limitations"></a></h3>
<p class="wp-block-paragraph">In MariaDB XA transactions are only supported by the InnoDB, MyRocks, and SPIDER storage engines. For InnoDB, before MariaDB 10.3, XA transactions could be disabled via the <code>innodb_support_xa</code> variable, but they were enabled by default and can&rsquo;t be disabled anymore. Disabling them was a bad idea even when users weren&rsquo;t supposed to use XA transactions, because MariaDB used them internally. Disabling them meant having no guarantee that the transactions appeared in the binary log in the correct order, which would have led to replication inconsistencies or failures.</p>
<h3 class="wp-block-heading">PostgreSQL limitations<a class="anchor-link" id="postgresql-limitations"></a></h3>
<p class="wp-block-paragraph">In PostgreSQL, 2pc transactions are disabled by default. To enable them, set <code>max_prepared_transactions</code> to a number &gt; 0 in the configuration file.</p>
<p class="wp-block-paragraph">PostgreSQL doesn&rsquo;t follow a standard. As a consequence, some transaction coordinators might not support it.</p>
<p class="wp-block-paragraph">In XA, transaction id&rsquo;s have three components. In PostgreSQL, an id is just a string. While you can logically split this string into three substrings, most probably transaction managers that support PostgreSQL won&rsquo;t support any particular logic involving these components.</p>
<p class="wp-block-paragraph">PostgreSQL&rsquo;s logic is more error-prone than the XA standard. With the standard, you start an XA transaction using a special syntax. You&rsquo;ll still be able to commit in one phase if you realise that you don&rsquo;t need to start transactions on other databases, but you&rsquo;ll need to specify <code>COMMIT ... ONE PHASE</code> explicitly. With PostgreSQL, you start transactions without specifying if you wish a one-phase or a two-phase commit. As a consequence, if your code has a bug, you might improperly run a single commit when a two-phase commit is needed.</p>
<p class="wp-block-paragraph">For XA transactions, MariaDB requires an isolation level of <code>REPEATABLE READ</code> (MariaDB&rsquo;s default) or <code>SERIALIZABLE</code>. This is correct: even REPEATABLE READ doesn&rsquo;t offer sufficient consistency guarantees for distributed transactions, but requiring locking reads might lead to big performance problems. PostgreSQL accepts <code>READ COMMITTED</code> (PostgreSQL&rsquo;s default), which could improve scalability but at the expence of consistency. It also accepts <code>READ UNCOMMITTED</code>, but in PostgreSQL that is a synonym for <code>READ COMMITTED</code>.</p>
<h2 class="wp-block-heading">Bonus Material<a class="anchor-link" id="bonus-material"></a></h2>
<p class="wp-block-paragraph">Some extra material can be found in the <a href="https://github.com/Vettabase/xa-utils/" rel="noopener"><strong>xa-utils</strong> repository</a>. This includes a cheatsheet for MariaDB and PostgreSQL, and stored procedures to rollback all prepared transactions after a server restart (but before accepting client connections).</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">Two-phase transactions allow us to coordinate transactions across multiple databases, making sure that every transaction will completely succeed or completely fail.</p>
<p class="wp-block-paragraph">Both MariaDB and PostgreSQL support two-phase commit transactions. MariaDB does it by following the XA standard, PostgreSQL implements non-standard statements. Their implementation is limited compared to XA support in databases like Oracle or DB2, but it&rsquo;s sufficient for handling practical scenarios.</p>
<p class="wp-block-paragraph">We discussed 2pc transactions, as well as the problems that are not resolved by 2pc transactions, or are caused by them. Some of these problems don&rsquo;t have an easy solution in complex cases. These are edge cases that rarely occur, so they shouldn&rsquo;t discourage the use of XA. But DBAs should be aware of these problems, and have plans to follow if things go wrong.</p>
<p class="wp-block-paragraph">If you need to understand better which problems might affect your particular scenario, or if you need help in setting up recovery procedures, <a href="https://vettabase.com/contact/" data-type="page" data-id="11">contact us</a> for a consultation.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/distributed-multi-database-transactions-involving-mariadb-and-postgresql/">Distributed, Multi-Database Transactions Involving MariaDB and PostgreSQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>OIDC in PostgreSQL: With Keycloak</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/" />
      <id>https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/</id>
      <updated>2026-01-19T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We spent a long time, two blog posts to be specific, talking about OAuth/OIDC in theory. Now we’ll take a more practical look at the topic: how can we configure PostgreSQL with a popular open source identity provider, Keycloak, and our pg_oidc_validator plugin?</p>
<p><a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/">OIDC in PostgreSQL: With Keycloak</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We spent a long time, <a href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/">two</a> blog <a href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/">posts</a> to be specific, talking about OAuth/OIDC in theory.<br>
Now we&rsquo;ll take a more practical look at the topic:<br>
how can we configure PostgreSQL with a popular open source identity provider, <a href="https://www.keycloak.org/" target="_blank" rel="noopener noreferrer">Keycloak</a>, and our <a href="https://github.com/percona/pg_oidc_validator" target="_blank" rel="noopener noreferrer">pg_oidc_validator</a> plugin?</p>
<p>We&rsquo;ll not only look at the PostgreSQL configuration part, but also discuss the environment requirements and setting up Keycloak.</p>
<h3>Docker containers<a class="anchor-link" id="docker-containers"></a></h3>
<p>If you are only interested in trying out a working demo installation, we have a ready-to-use Docker Compose configuration available <a href="https://github.com/Percona-Lab/pg_oidc_validator/tree/main/examples/keycloak" target="_blank" rel="noopener noreferrer">in our GitHub repo</a>.<br>
This setup includes a Keycloak instance, a PostgreSQL server, and a utility container that runs <code>psql</code>, all running in different containers, simulating different machines.</p>
<pre class="mermaid">
graph TB
subgraph Host["Host Machine"]
User["&#128100; User"]
Browser["&#127760; Browser"]
end
subgraph DockerNetwork["Docker Network"]
Keycloak["&#128272; Keycloak Container"]
PG["&#128452;&#65039; PostgreSQL Container"]
PSQLClient["&#128187; psql Container"]
end
User --- Browser
User --- PSQLClient
Browser --- Keycloak
PSQLClient --- Keycloak
PSQLClient --- PG
PG --- Keycloak
style User fill:#e1f5ff
style Browser fill:#fff4e6
style Keycloak fill:#ffe6e6
style PG fill:#e6ffe6
style PSQLClient fill:#f0e6ff
style Host fill:#f5f5f5
style DockerNetwork fill:#e8f4f8
</pre>
<p><strong>Warning:</strong><br>
This is a demo environment, intended only for testing purposes.<br>
Do not use it in production.</p>
<p>Alternatively, if you are only interested in configuring PostgreSQL, you can use this setup to start up Keycloak, and only focus on the <a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/#configuring-postgresql">PostgreSQL related sections of this post</a>.</p>
<p>Also note:<br>
while it is a ready-to-use configuration, with everything set up&hellip; it&rsquo;s missing one bit:<br>
because it tries to do everything correctly, including running every service in a different container, we have to use hostnames; we can&rsquo;t just use <code>localhost</code> everywhere.<br>
This means that the Keycloak service uses the <code>keycloak</code> hostname as its name, and the host OS needs to be able to resolve this to use the device authorization flow.<br>
In most operating systems, this requires editing the hosts file &ndash; detailed instructions are shown later.</p>
<h3>Running Keycloak (with HTTPS)<a class="anchor-link" id="running-keycloak-with-https"></a></h3>
<p>Keycloak itself has a ready-to-use Docker image for trying it out.<br>
Executing it is quite simple, but this default setup results in an unsecure setup, which is not enough for us:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev</span></span></code></pre>
</div>
</div>
</div>
<p>The above command starts up a container with a freshly initialized provider, with an admin user and admin password, and exposes it on port 8080 (on every interface).<br>
This is a nice way to try out the UI and start discovering Keycloak, but it has some limitations:</p>
<p>Authentication/authorization has to be secure, and that means it has to use secure transport layers.<br>
While the OAuth standard doesn&rsquo;t specify an explicit protocol, <a href="https://www.rfc-editor.org/rfc/rfc9700" target="_blank" rel="noopener noreferrer">RFC 9700</a>, which defines best practices, clearly showcases HTTPS everywhere.</p>
<p>But to use that, we first need certificates for the encrypted connection.</p>
<h3>How do I get certificates?<a class="anchor-link" id="how-do-i-get-certificates"></a></h3>
<p>Depending on the exact demo environment, we have two choices:</p>
<ul>
<li>If the demo environment uses a public domain, the proper approach is to use a certificate signed by a trusted third party.<br>
There are free authorities like <a href="https://letsencrypt.org/" target="_blank" rel="noopener noreferrer">Let&rsquo;s Encrypt</a> or <a href="https://zerossl.com/" target="_blank" rel="noopener noreferrer">ZeroSSL</a> for quick setups.</li>
<li>In the more likely case where the demo environment is private, we have to use self-signed certificates.<br>
The rest of the blog post will discuss this approach.</li>
</ul>
<p>Generating a simple self-signed certificate is easy with OpenSSL.<br>
The following is a sample command that can run non-interactively, without asking additional questions &ndash; but again it has some environment dependency, the hostname, which we first have to figure out:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">openssl req -x509 -newkey rsa:4096 -keyout key.pem -out crt.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN="</span></span></code></pre>
</div>
</div>
</div>
<p>It generates a certificate that is valid for 10 years, for <code>hostname</code>.<br>
The <code>hostname</code> part is important:<br>
to enforce security, PostgreSQL validates the TLS certificate&rsquo;s hostname against the issuer URL.<br>
If the hostname in the OAuth issuer URL and the certificate&rsquo;s Common Name (or Subject Alternative Name) don&rsquo;t match, it won&rsquo;t proceed with the login.</p>
<p>If you plan to run Keycloak in a Docker container but run PostgreSQL directly on the host machine, Docker exposes the 8443 port used by Keycloak on localhost, both the browser used to complete the login process and PostgreSQL can refer to the issuer as <code>https://localhost:8443/...</code>, which means the hostname can be <code>localhost</code>:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">openssl req -x509 -newkey rsa:4096 -keyout key.pem -out crt.pem -sha256 -days 3650 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=localhost"</span></span></code></pre>
</div>
</div>
</div>
<p>But if you intend to follow the Docker compose setup, where Keycloak and PostgreSQL are two separate containers, this no longer works:<br>
the port mapping only exposes the Keycloak service for the host, not for the PostgreSQL container.<br>
That container has to refer to it as &ldquo;https://keycloak:8443/&hellip;&rdquo;</p>
<p>Which means that in this scenario, we have to use <code>CN=keycloak</code> instead.<br>
Or alternatively, you can generate a certificate that includes both hostnames, this is what the docker compose example configuration does:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">openssl req -x509 -newkey rsa:4096 -keyout key.pem -out crt.pem -sha256 -days 3650 -nodes 
</span></span><span class="line"><span class="cl"> -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=keycloak" 
</span></span><span class="line"><span class="cl"> -addext "subjectAltName=DNS:keycloak,DNS:localhost,IP:127.0.0.1"</span></span></code></pre>
</div>
</div>
</div>
<h3>Back to Keycloak<a class="anchor-link" id="back-to-keycloak"></a></h3>
<p>To run Keycloak with HTTPS, we have to use a different port, and specify the certificates</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">docker run -p 127.0.0.1:8443:8443 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -e KC_HTTPS_CERTIFICATE_FILE=/keys/crt.pem -e KC_HTTPS_CERTIFICATE_KEY_FILE=/keys/key.pem -v /path/to/the/keys:/keys/ quay.io/keycloak/keycloak:latest start-dev</span></span></code></pre>
</div>
</div>
</div>
<p>This command specifies two more environment variables, the filenames of the certificate and the private key, and mounts the directory containing them.</p>
<p><strong>Note:</strong><br>
Please note that we used <code>127.0.0.1:8443:8443</code> instead of simply <code>8443:8443</code><br>
It is a good practice to not expose admin interfaces with default passwords publicly.</p>
<h4>Trusting the certificates</h4>
<p>Now that we have a running Keycloak instance with HTTPS certificates, we need to make sure our systems trust them.</p>
<p>When you open a browser and navigate to a website with a self-signed certificate, you&rsquo;ll get a warning.<br>
After acknowledging the warning you can proceed and use the website normally.</p>
<p>Similarly, software using HTTPS for communications usually defaults to proper certificate verification, but often also allows administrators to either disable the certificate check &ndash; not safe in production, but useful for quick demos like this &ndash; or to manually specify a certificate authority used for verification.</p>
<p>Unfortunately at this point this isn&rsquo;t the case for PostgreSQL, it doesn&rsquo;t provide such options.<br>
The operating system has to trust the certificates on both the server and client host, otherwise it will refuse to complete the OIDC authentication flow.</p>
<p>If you used the approach with a public domain and generally trusted authority, this is not an issue.<br>
But if you generated a self-signed certificate instead, you won&rsquo;t be able to authenticate unless you make the systems running PostgreSQL trust this certificate.</p>
<p>On most Linux systems, this is as simple as copying the certificate (<code>crt.pem</code>) to a specific directory, and running a system script that updates the trusted certificates.<br>
For example, on Ubuntu:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">sudo cp crt.pem /usr/local/share/ca-certificates/keycloak-test.crt
</span></span><span class="line"><span class="cl">sudo update-ca-certificates</span></span></code></pre>
</div>
</div>
</div>
<p>Also, we shouldn&rsquo;t forget that we can have up to 4 different systems:</p>
<ul>
<li>the Keycloak server</li>
<li>the PostgreSQL Server</li>
<li>the system running the psql client</li>
<li>and another system running the browser which completes the device flow</li>
</ul>
<p>This is the case with our Docker Compose example &ndash; 3 of these are containers, and the browser runs on the host machine.<br>
Browsers usually ignore certificates placed in the above folder.<br>
But that&rsquo;s not an issue, you can acknowledge the warning and still use the website.<br>
The only part where trust matters is the <code>libcurl</code> library used by PostgreSQL, and that uses the certificates trusted by the system.</p>
<p>The host system only has to trust the certificate if it is also used to run PostgreSQL.</p>
<p><strong>Note:</strong><br>
Please do not add random certificates to your everyday OS, or at least remember to delete them later.</p>
<h4>Recognizing the Keycloak host</h4>
<p>Besides trusting the certificates, there&rsquo;s another hostname-related configuration we need to address.</p>
<p>Even if you run PostgreSQL directly on the host machine, it is still possible to access Keycloak using the &lsquo;https://keycloak&rsquo; URL instead of localhost &ndash; and if you do run the PostgreSQL server in a container, you have to use this form.</p>
<p>For this to work, all 3 systems that need to connect to Keycloak (the PostgreSQL server, the psql client, and the browser) have to recognize this hostname.<br>
When using <code>docker compose</code>, this hostname resolution will work directly in the other containers, but not on the host itself.</p>
<p>To make this work on the host, or on any other machine that requires it, you have to edit the hosts file:</p>
<p><strong>On Linux/Mac:</strong></p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">"127.0.0.1 keycloak"</span> <span class="p">|</span> sudo tee -a /etc/hosts</span></span></code></pre>
</div>
</div>
</div>
<p><strong>On Windows:</strong> Edit <code>C:WindowsSystem32driversetchosts</code> as Administrator and add:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">127.0.0.1 keycloak</span></span></code></pre>
</div>
</div>
</div>
<h4>Can&rsquo;t I just use localhost in the browser instead?</h4>
<p>You might be wondering if there&rsquo;s a shortcut here.</p>
<p>Even if the PostgreSQL container has to reference Keycloak as <code>keycloak</code>, your host still sees the exposed port as <code>localhost:8443</code>.<br>
So can you just use this in the browser instead, and complete the authentication that way, without editing the hosts file?</p>
<p>The answer is unfortunately no.<br>
There are two possible scenarios:</p>
<ul>
<li>If Keycloak is configured with strict hostname, it will try to redirect the browser to &ldquo;https://keycloak&hellip;&rdquo; during the authorization process</li>
<li>If Keycloak is configured with dynamic hostname, it will complete the process with &ldquo;https://localhost&rdquo;, but it will also use &ldquo;localhost&rdquo; in the generated access tokens instead of &ldquo;keycloak&rdquo;.<br>
When our validator checks the token, it will notice this discrepancy and reject the login attempt.</li>
</ul>
<h3>Configuring your realm<a class="anchor-link" id="configuring-your-realm"></a></h3>
<p>With the Keycloak infrastructure setup out of the way, we can now focus on configuring Keycloak itself.</p>
<p>After you have your Keycloak instance up and running, it is time to open a browser and navigate to <code>https://keycloak:8443</code>.<br>
A quick login with &ldquo;admin&rdquo; and &ldquo;admin&rdquo;, and the browser already displays the admin UI with the default master realm.</p>
<p>A complete detailed introduction is out of scope for this blog post &ndash; the <a href="https://www.keycloak.org/documentation" target="_blank" rel="noopener noreferrer">Keycloak documentation</a> is much better for that &ndash; we will only try to explain the minimum required to set up a relatively simple, but secure configuration for our PostgreSQL instance.<br>
The steps we show here will be similar to what our demo setup also uses.</p>
<p>Let&rsquo;s start by creating a new realm, under the &ldquo;Manage realms&rdquo; menu:<br>
realms are the main building blocks of isolation in Keycloak, storing users, clients, and everything, so it&rsquo;s a good practice not to use the default one.<br>
In our example, we named our realm <code>pgrealm</code>.<br>
This name is important, as it will be included in the OAuth issuer URL.</p>
<p><figure><img decoding="async" width="1603" height="905" src="https://percona.community/blog/2026/01/keycloak_step1_realm_hu_c9a9e6dd2adc1729.webp" alt="Creating a new realm in Keycloak" loading="lazy"></figure>
</p>
<p>After hitting &ldquo;Create&rdquo;, the new realm is automatically set as current, and we can continue configuring it.</p>
<p>Let&rsquo;s continue by creating our &ldquo;testuser&rdquo; under &ldquo;Users&rdquo;.<br>
Select that the email is verified, fill the requested email, first name and last name fields, and hit create.<br>
If you miss some of these fields, Keycloak will ask you to complete them during the first login.</p>
<p><figure><img decoding="async" width="1109" height="845" src="https://percona.community/blog/2026/01/keycloak_step2_user_hu_eda78e640da9a043.webp" alt="Creating a user in Keycloak" loading="lazy"></figure>
</p>
<p>After the user is created, navigate to the &ldquo;Credentials&rdquo; tab on the displayed user admin page, and set a password.<br>
Also remove the checkbox from &ldquo;Temporary&rdquo;, unless you want to change it during the first login.<br>
In our example setup, we used &ldquo;asdfasdf&rdquo;.<br>
This is of course only appropriate for a quick demo setup, use a better one for anything else.</p>
<p><figure><img decoding="async" width="1005" height="580" src="https://percona.community/blog/2026/01/keycloak_step3_user_hu_398f737b88f86129.webp" alt="Setting user credentials in Keycloak" loading="lazy"></figure>
</p>
<p>After creating our user, let&rsquo;s create a client under &ldquo;Clients&rdquo; with the &ldquo;Create Client&rdquo; button.<br>
The first screen asks for a client ID &ndash; this will be required for the <code>psql</code> command &ndash; and a name and description &ndash; these will be displayed on the authorization page by the browser.</p>
<p><figure><img decoding="async" width="1842" height="744" src="https://percona.community/blog/2026/01/keycloak_step4_client_hu_af6c6feecff39bec.webp" alt="Creating a client in Keycloak" loading="lazy"></figure>
</p>
<p>After clicking next, the next screen configures how OIDC should work exactly.</p>
<p>The first toggle, &ldquo;Client authentication&rdquo; can be both on and off &ndash; this controls if the client requires a secret, or only an ID.<br>
As we discussed earlier, <code>psql</code> is a public client, and while it can use a secret, it&rsquo;s not really a secret.<br>
Adding a secret only adds complexity while not providing more security, as we also have to specify that during the connection call, but it is supported.</p>
<p>For the &ldquo;authentication flow&rdquo; we have to check &ldquo;OAuth 2.0 Device Authorization Grant&rdquo; to enable the device flow, and we can leave everything else on default.</p>
<p><figure><img decoding="async" width="1373" height="817" src="https://percona.community/blog/2026/01/keycloak_step5_client_hu_e97a53955453b011.webp" alt="Configuring client authentication flow in Keycloak" loading="lazy"></figure>
</p>
<p>The final third screen doesn&rsquo;t require any changes &ndash; those are settings for HTTP-based flows, but we are using the device flow.<br>
If you are configuring PostgreSQL OIDC with a web application, of course you should fill these properly.</p>
<p>After creating our client, it&rsquo;s also a good practice to make our user consent more explicit:<br>
as we tried to make this clear in earlier blog posts, this is the only line of defense with OAuth-based logins:<br>
administrators have to be very clear about displaying where the user logs in.</p>
<p>First, if you scroll down on the Client administration page displayed after creation, there&rsquo;s a section about the consent screen.<br>
It&rsquo;s a good practice to make this as explicit as possible, with a nice custom message for the users.</p>
<p><figure><img decoding="async" width="1236" height="578" src="https://percona.community/blog/2026/01/keycloak_step6_client_hu_1e8d4188a95371f.webp" alt="Configuring consent screen in Keycloak" loading="lazy"></figure>
</p>
<p>After saving this, we also should add a client scope using the menu item below &ldquo;Clients&rdquo;.<br>
The name attribute is what we&rsquo;ll have to specify in our PostgreSQL configuration.<br>
The two toggles below are both required: &ldquo;include in token scope&rdquo; means that it will be included in the JWT; without that, the validator won&rsquo;t be able to verify the presence of the scope.<br>
&ldquo;Display on consent screen&rdquo; means that this is a scope that should be explicitly displayed to the user during login.</p>
<p>(We&rsquo;ll discuss what scopes are and why they matter in the next section.)</p>
<p><figure><img decoding="async" width="1870" height="834" src="https://percona.community/blog/2026/01/keycloak_step7_scope_hu_94bd02d074f6502a.webp" alt="Creating a client scope in Keycloak" loading="lazy"></figure>
</p>
<p>And with this, our basic Keycloak configuration is ready!</p>
<h3>What is a scope?<a class="anchor-link" id="what-is-a-scope"></a></h3>
<p>The scope configuration in the previous section might seem confusing:<br>
why did we configure consent screen settings at multiple locations?<br>
What is this scope concept actually about?</p>
<p>To answer this question, we have to remember our earlier examples, where we discussed that PostgreSQL itself is not a client (application), but it is just something used by possibly multiple clients.<br>
In the real world, that client could be &ldquo;psql&rdquo;, or &ldquo;EditorApp&rdquo;, or anything else that uses PostgreSQL while not providing more security.</p>
<p>PostgreSQL could use a list of allowed clients for authorization &ndash; but it doesn&rsquo;t do that.<br>
Instead it relies on another OAuth concept, scopes.</p>
<p>The OAuth scope is a mechanism intended to limit what a token is allowed to access.<br>
There are some generic scopes, such as &ldquo;email&rdquo; or &ldquo;profile&rdquo;.<br>
And there are many application specific scopes:<br>
cloud providers like Google or Azure use them to control which services a token is allowed to access.<br>
For example, if you grant something the ability to add entries to your calendar, it won&rsquo;t be allowed to read your emails or location history.</p>
<p>The same way, you can think of scopes with PostgreSQL as &ldquo;which database the user can access&rdquo;.<br>
Database here could mean both a PostgreSQL instance, or just a single database in it &ndash; as OAuth is configured in pg_hba, it is possible to configure different required scopes for different databases (more about this later).</p>
<p>During the Keycloak configuration we recommended configuring an explicit consent screen for the client, but technically that part is outside of the database configuration.<br>
A database can be accessed by multiple clients, some of those might be created way later in time.</p>
<p>Even if somebody doesn&rsquo;t configure that part, a scope with a required consent screen will be displayed, if the client requests it.<br>
And that is part of the database configuration &ndash; as we can configure PostgreSQL explicitly to require specific scopes in its configuration.</p>
<h3>Configuring PostgreSQL<a class="anchor-link" id="configuring-postgresql"></a></h3>
<p>Now that we have a working, properly configured Keycloak server, it&rsquo;s time to configure the PostgreSQL side of the equation.<br>
While we do have a working Docker example as part of the Docker Compose configuration, this is currently tricky to set up:<br>
there is no Docker image with our pg_oidc_validator.</p>
<p>You can check the <a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/link">relevant section of the compose configuration</a>, but it requires a custom entry point and a short bash script.</p>
<p>In this blog post we&rsquo;ll focus on the actual manual steps instead.</p>
<h4>Required packages</h4>
<p>On the server, we need the PostgreSQL 18 server packages installed, and also the pg_oidc_validator package.<br>
For the validator, we currently have downloadable deb and rpm packages on our <a href="https://github.com/percona/pg_oidc_validator/releases" target="_blank" rel="noopener noreferrer">GitHub releases page</a>, and packages for SUSE can be found in the <a href="https://software.opensuse.org/package/pg_oidc_validator" target="_blank" rel="noopener noreferrer">official SUSE packages</a>.</p>
<p>On the client side, we need the PostgreSQL 18 client along with the OAuth client package &ndash; this is usually a separate package named libpq-oauth on most distributions and requires separate installation</p>
<h4>Setting up a data directory</h4>
<p>Let&rsquo;s just assume that we have a data directory initialized somewhere:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">initdb -D datadir</span></span></code></pre>
</div>
</div>
</div>
<p>We will have to modify a few configuration files for OIDC to work.</p>
<p>Let&rsquo;s start by enabling the validator in <code>datadir/postgresql.conf</code>:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">oauth_validator_libraries = pg_oidc_validator
</span></span><span class="line"><span class="cl">pg_oidc_validator.authn_field = email</span></span></code></pre>
</div>
</div>
</div>
<p>The first line tells PostgreSQL to load the Percona pg_oidc_validator, and the second line is a specific configuration parameter for our validator &ndash; it tells it that we want to map Keycloak users to PostgreSQL users based on the email claim in the access token.<br>
This second line is optional; it defaults to the &ldquo;sub&rdquo; (subject) field, which identifies the user in most OIDC providers.<br>
However, Keycloak doesn&rsquo;t allow us to customize the value of this field, and it returns a non-user-friendly identifier in it.<br>
In practice, it is clearer to use a more verbose field for mapping, such as email, so we&rsquo;ll use that in our example.</p>
<p>After modifying this file, we can try to start the server.<br>
If all required packages are installed correctly, and there isn&rsquo;t another PostgreSQL instance running on the default port, it should start without issues:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pg_ctl -D datadir start</span></span></code></pre>
</div>
</div>
</div>
<p>With the server running, let&rsquo;s create a matching PostgreSQL user for our testuser:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">createuser testuser</span></span></code></pre>
</div>
</div>
</div>
<p>Next, we have to tell PostgreSQL that we want to use the Keycloak instance we set up previously.<br>
Let&rsquo;s add an entry to <code>datadir/pg_hba.conf</code> to reference the OIDC provider.<br>
Add this line to the beginning of the file, before the <code>trust</code> sections &ndash; since pg_hba is executed line by line, if the <code>trust</code> entries are before the OAuth entry, authentication will never reach that line:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">host all all 0.0.0.0/0 oauth scope="email pgscope",issuer=https://keycloak:8443/realms/pgrealm,map=kcmap</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Warning:</strong><br>
In a real-world setup, you would have to remove all trust entries.<br>
The <code>trust</code> authentication method allows connections without any password verification and should never be used in production.</p>
<p>This entry adds the OAuth option for connections using IPv4.<br>
We specify that we require &ldquo;pgscope&rdquo;, which is the example scope with the custom consent screen we created earlier in the Keycloak configuration, and the &ldquo;email&rdquo; scope, which instructs Keycloak to include the user&rsquo;s email address in the JWT (this will also be presented on the consent screen).</p>
<p>The latter is required because in this example we map our database users to the users on the identity provider using their email address &ndash; this will be configured in more detail in the &ldquo;kcmap&rdquo; referenced in the configuration line.</p>
<p>The only required parameter to OAuth is the &ldquo;issuer&rdquo;; everything else is optional.<br>
But providing required scopes is a good practice, and it is required for proper security, as we explained earlier.<br>
There are also other parameters not mentioned here, a full list is available in the <a href="https://www.postgresql.org/docs/current/auth-oauth.html" target="_blank" rel="noopener noreferrer">PostgreSQL documentation</a>.</p>
<p>One potentially important parameter is the &ldquo;validator&rdquo;.<br>
PostgreSQL allows multiple different pg_hba OAuth entries, and it also allows multiple different validators:<br>
the configuration parameter in <code>postgresql.conf</code> is called <code>oauth_validator_libraries</code>.<br>
If that parameter actually contains multiple libraries, the &ldquo;validator&rdquo; parameter becomes required for OAuth entries in pg_hba conf, and has to match an entry in the validator list.<br>
Otherwise, it is assumed that all OAuth entries use the one available validator.</p>
<p>The above means that the map setting is also optional.<br>
There are scenarios where it&rsquo;s not needed, such as if the identity provider has a claim that directly contains user names matching PostgreSQL usernames, we could use that field instead and skip the manual mapping.</p>
<p>Following our example, let&rsquo;s define an entry in <code>datadir/pg_ident.conf</code> for the <code>kcmap</code>.<br>
We only need a single entry for our testuser:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># MAPNAME SYSTEM-USERNAME DATABASE-USERNAME
</span></span><span class="line"><span class="cl">kcmap testuser@example.com testuser</span></span></code></pre>
</div>
</div>
</div>
<p>All that&rsquo;s left is to reload our configuration:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pg_ctl -D datadir reload</span></span></code></pre>
</div>
</div>
</div>
<h4>Connecting to the database</h4>
<p>With all the configuration in place, it&rsquo;s time for the moment of truth:<br>
actually connecting to PostgreSQL using OIDC!</p>
<p>With the server properly configured, we are ready to connect to it using psql.<br>
To do so, we have to use a command similar to the following:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">bin/psql -h 127.0.0.1 'dbname=postgres oauth_issuer=https://keycloak:8443/realms/pgrealm oauth_client_id=pgclient'</span></span></code></pre>
</div>
</div>
</div>
<p>Note that we have to repeat the issuer URL here.<br>
It is always required, even if the PostgreSQL configuration only contains one OAuth issuer.<br>
This URL has to match <strong>exactly</strong> the issuer URL in a pg_hba line, and both should <strong>exactly</strong> match the issuer URL in the JWT.<br>
If there&rsquo;s a mismatch anywhere, authentication will fail.<br>
This is why we had to take extra steps previously to make the Keycloak hostname available everywhere.</p>
<p>This is also the only place where we have to mention a client id &ndash; and if we configured an authenticated client in Keycloak, we also have to specify <code>oauth_client_secret</code>.<br>
The server can work with multiple clients, but now that we are actually starting up a PostgreSQL client, we can specify which OAuth client will we use to complete the authentication flow.</p>
<p>After we execute this command, psql will display the device authorization instructions, showing us a URL and a device code.</p>
<p>All we have to do is follow the instructions:</p>
<ol>
<li>navigate to the specified URL</li>
<li>enter the device code</li>
<li>login with our testuser</li>
<li>confirm the consent screen</li>
</ol>
<p>If you are doing all of this in a single session, don&rsquo;t forget to log out of the admin user session before executing these steps, or use a different browser for it.<br>
We want to log in with the testuser, not with the admin &ndash; the admin user doesn&rsquo;t have a mapping in our PostgreSQL configuration.</p>
<p><!-- TODO: Add consent screen screenshot when available --></p>
<p>While we go through these steps, the <code>psql</code> client periodically polls Keycloak to see if the flow was completed on the OIDC provider side.<br>
Since this is a periodic polling, done every few seconds, we might have to wait a few seconds before we are logged in to an SQL session.</p>
<h3>That&rsquo;s all!<a class="anchor-link" id="thats-all"></a></h3>
<p>And just like that, we&rsquo;ve successfully authenticated to PostgreSQL using OIDC!</p>
<p>With the <code>psql</code> command logged in, we&rsquo;ve completed the full circle:<br>
from setting up Keycloak with proper certificates, through configuring realms and clients, to establishing a secure OIDC-authenticated PostgreSQL connection.</p>
<p>Hopefully these instructions were clear and everything worked on the first try.<br>
If you encountered issues along the way, don&rsquo;t be discouraged:<br>
OAuth/OIDC is complex, there are many things that could go wrong.<br>
Since this is an important security feature, it has to fail if anything is even slightly wrong.</p>
<p>In our next post, we&rsquo;ll focus on errors and failures:<br>
both to help diagnose possible errors with the OAuth flow in PostgreSQL, but also for reassurance:<br>
in an authentication setup not letting unauthorized people log in is just as important as successfully logging in somebody with the proper permissions.<br>
Stay tuned for our examples showcasing how pg_oidc_validator and PostgreSQL&rsquo;s OAuth support can keep your server safe!</p>
<p>If you find any issues with our validator, or have comments / feature requests, please reach out to us in our <a href="https://github.com/percona-lab/pg_oidc_validator" target="_blank" rel="noopener noreferrer">Github page</a>!</p>

<p><a href="https://percona.community/blog/2026/01/19/oidc-in-postgresql-with-keycloak/">OIDC in PostgreSQL: With Keycloak</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Configuring the Component Keyring in Percona Server and PXC 8.4</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2026/01/13/configuring-the-component-keyring-in-percona-server-and-pxc-8.4/" />
      <id>https://percona.community/blog/2026/01/13/configuring-the-component-keyring-in-percona-server-and-pxc-8.4/</id>
      <updated>2026-01-13T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Configuring the Component Keyring in Percona Server and PXC 8.4 (Or: how to make MySQL encryption boring, which is the goal)</p>
<p><a href="https://percona.community/blog/2026/01/13/configuring-the-component-keyring-in-percona-server-and-pxc-8.4/">Configuring the Component Keyring in Percona Server and PXC 8.4</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1>Configuring the Component Keyring in Percona Server and PXC 8.4<a class="anchor-link" id="configuring-the-component-keyring-in-percona-server-and-pxc-8-4"></a></h1>
<p><em>(Or: how to make MySQL encryption boring, which is the goal)</em></p>
<p>Encryption is one of those things everyone agrees is important, right up until MySQL refuses to start and you&rsquo;re staring at a JSON file wondering which brace ruined your evening.</p>
<p>With <strong>MySQL 8.4</strong>, encryption has firmly moved into the <strong>component world</strong>, and if you&rsquo;re running <strong>Percona Server 8.4</strong> or <strong>Percona XtraDB Cluster (PXC) 8.4</strong>, the supported path forward is the <code>component_keyring_file</code> component.</p>
<p>The good news: the setup is mostly identical for Percona Server and PXC.<br>
The bad news: PXC 8.4.4 and 8.4.5 shipped with a bug that makes this less fun than it should be.</p>
<p>Let&rsquo;s walk through a setup that works, keeps your keys locked down, and avoids the usual landmines.</p>
<hr>
<h2>Step 1: Tell MySQL Which Component to Load<a class="anchor-link" id="step-1-tell-mysql-which-component-to-load"></a></h2>
<p>Components are registered using <strong>JSON</strong>, not traditional MySQL configuration syntax. This is important, because MySQL will not politely warn you if you get it wrong. It will simply refuse to start.</p>
<p>Create the file:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vi /usr/sbin/mysqld.my</span></span></code></pre>
</div>
</div>
</div>
<p>Add:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">json</span><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"components"</span><span class="p">:</span> <span class="s2">"file://component_keyring_file"</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p>Take a second to double-check the formatting. One missing quote here will cost you more time than you want to admit.</p>
<p>Now lock it down:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chown root:root /usr/sbin/mysqld.my
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">644</span> /usr/sbin/mysqld.my</span></span></code></pre>
</div>
</div>
</div>
<p>This is configuration, not data. MySQL only needs to read it.</p>
<hr>
<h2>Step 2: Prepare the Keyring Directory (Handle With Care)<a class="anchor-link" id="step-2-prepare-the-keyring-directory-handle-with-care"></a></h2>
<p>This directory will hold encryption keys. Treat it accordingly.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /var/lib
</span></span><span class="line"><span class="cl">sudo mkdir mysql-keyring
</span></span><span class="line"><span class="cl">sudo chown mysql:mysql mysql-keyring
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">750</span> mysql-keyring</span></span></code></pre>
</div>
</div>
</div>
<p>A simple rule that saves headaches:</p>
<ul>
<li><strong>mysql owns the keys</strong></li>
<li><strong>MySQL is allowed to access them</strong></li>
<li><strong>Nobody else gets any ideas</strong></li>
</ul>
<hr>
<h2>Step 3: Configure the Keyring Component Itself<a class="anchor-link" id="step-3-configure-the-keyring-component-itself"></a></h2>
<p>Next, move to the plugin directory:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /usr/lib64/mysql/plugin</span></span></code></pre>
</div>
</div>
</div>
<p>Create the component configuration file:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo vi component_keyring_file.cnf</span></span></code></pre>
</div>
</div>
</div>
<p>Add:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">json</span><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"path"</span><span class="p">:</span> <span class="s2">"/var/lib/mysql-keyring/component_keyring_file"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nt">"read_only"</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre>
</div>
</div>
</div>
<p>This file tells MySQL where the keyring lives and ensures it can&rsquo;t be casually modified at runtime.</p>
<p>Set ownership and permissions:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo chown root:root component_keyring_file.cnf
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">640</span> component_keyring_file.cnf</span></span></code></pre>
</div>
</div>
</div>
<p>Again: configuration belongs to root. MySQL just reads it.</p>
<hr>
<h2>Step 4: The PXC 8.4.4 / 8.4.5 Bug (Yes, There&rsquo;s One)<a class="anchor-link" id="step-4-the-pxc-8-4-4-8-4-5-bug-yes-theres-one"></a></h2>
<p>If you&rsquo;re running <strong>Percona Server</strong>, you can skip this entire section and enjoy your day.</p>
<p>If you&rsquo;re running <strong>Percona XtraDB Cluster 8.4.4 or 8.4.5</strong>, there is a known issue with plugin paths that prevents the component keyring from loading correctly. This was fixed in <strong>PXC 8.4.6</strong>.</p>
<p>If upgrading isn&rsquo;t an option yet, you&rsquo;ll need one of the following workarounds.</p>
<h3>Option A: Create a Symlink (Preferred)<a class="anchor-link" id="option-a-create-a-symlink-preferred"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ln -s <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span>/usr/bin/pxc_extra/pxb-8.4/lib/lib64/xtrabackup/plugin <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span>/usr/bin/pxc_extra/pxb-8.4/lib/plugin</span></span></code></pre>
</div>
</div>
</div>
<h3>Option B: Copy the Plugin Directory<a class="anchor-link" id="option-b-copy-the-plugin-directory"></a></h3>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo cp -ar <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span>/usr/bin/pxc_extra/pxb-8.4/lib/lib64/xtrabackup/plugin <span class="se">
</span></span></span><span class="line"><span class="cl"><span class="se"></span>/usr/bin/pxc_extra/pxb-8.4/lib</span></span></code></pre>
</div>
</div>
</div>
<p>If you&rsquo;re on <strong>PXC 8.4.6 or newer</strong>, this problem is already behind you and you can safely pretend it never existed.</p>
<hr>
<h2>Step 5: Restart MySQL<a class="anchor-link" id="step-5-restart-mysql"></a></h2>
<p>Time for the moment of truth:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl restart mysql</span></span></code></pre>
</div>
</div>
</div>
<p>Or <code>mysqld</code>, depending on your system.</p>
<p>If MySQL starts cleanly, you&rsquo;re doing well. If not, go back and check your JSON files. It&rsquo;s almost always the JSON.</p>
<hr>
<h2>Step 6: Verify the Keyring Is Actually Loaded<a class="anchor-link" id="step-6-verify-the-keyring-is-actually-loaded"></a></h2>
<p>Never assume. Always verify.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">keyring_component_status</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>You should see the <code>component_keyring_file</code> listed and active. If it&rsquo;s there, the keyring is live.</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="o">+</span><span class="c1">---------------------+-----------------------------------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">STATUS_KEY</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">STATUS_VALUE</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+-----------------------------------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">Component_name</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">component_keyring_file</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">Author</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Oracle</span><span class="w"> </span><span class="n">Corporation</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">License</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">GPL</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">Implementation_name</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">component_keyring_file</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="k">Version</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">Component_status</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Active</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">Data_file</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="o">/</span><span class="n">var</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">mysql</span><span class="o">-</span><span class="n">keyring</span><span class="o">/</span><span class="n">component_keyring_file</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="n">Read_only</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Yes</span><span class="w"> </span><span class="o">|</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+-----------------------------------------------+
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="mi">8</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">set</span><span class="w"> </span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">00</span><span class="w"> </span><span class="n">sec</span><span class="p">)</span></span></span></code></pre>
</div>
</div>
</div>
<hr>
<h2>A Note for Percona Server Users<a class="anchor-link" id="a-note-for-percona-server-users"></a></h2>
<p>Percona Server may still include <strong>legacy keyring plugins</strong> such as:</p>
<ul>
<li><code>keyring_file</code></li>
<li><code>keyring_vault</code></li>
</ul>
<p>Do not mix legacy keyring plugins with component keyrings. They come from different eras of MySQL design and do not coexist peacefully.</p>
<p>Choose one model. For MySQL 8.4 and forward, <strong>components are the future</strong>.</p>
<h2>Additional Steps for Percona XtraDB Cluster (PXC)<a class="anchor-link" id="additional-steps-for-percona-xtradb-cluster-pxc"></a></h2>
<p>Percona XtraDB Cluster introduces one critical difference compared to standalone Percona Server: the keyring file itself is not replicated by Galera. Only metadata and transactional state are replicated. The encryption keys remain node-local filesystem artifacts and must be handled deliberately.</p>
<h3>Node 1: Establish the Authoritative Keyring<a class="anchor-link" id="node-1-establish-the-authoritative-keyring"></a></h3>
<p>Choose a single node to initialize the keyring. This is typically Node1, but the choice itself is not important as long as you are consistent.</p>
<p>On this node:</p>
<ul>
<li>Complete all previous steps in this document</li>
<li>Start MySQL successfully</li>
<li>Verify the keyring component is loaded:</li>
</ul>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">keyring_component_status</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Once this node is running, the file below will be created and populated:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">swift</span><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="o">/</span><span class="kd">var</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">mysql</span><span class="o">-</span><span class="n">keyring</span><span class="o">/</span><span class="n">component_keyring_file</span></span></span></code></pre>
</div>
</div>
</div>
<p>This file becomes the authoritative source of encryption keys for the entire cluster.</p>
<h3>Why the Keyring File Must Be Copied<a class="anchor-link" id="why-the-keyring-file-must-be-copied"></a></h3>
<p>PXC ensures that encrypted data remains readable on all nodes, but it does not distribute encryption keys themselves. Each node must have access to the same key material, or encrypted tablespaces will fail to open.</p>
<p>If a node starts without the correct keyring file, you may see:</p>
<ul>
<li>Tablespace open failures</li>
<li>Startup errors related to encryption</li>
<li>Inconsistent behavior during SST or IST</li>
</ul>
<p>This is expected behavior and not a bug.</p>
<h3>Distribute the Keyring File to Other Nodes<a class="anchor-link" id="distribute-the-keyring-file-to-other-nodes"></a></h3>
<p>On each remaining PXC node:</p>
<ol>
<li>Ensure MySQL is stopped</li>
<li>Create the keyring directory if it does not exist:</li>
</ol>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo mkdir -p /var/lib/mysql-keyring
</span></span><span class="line"><span class="cl">sudo chown mysql:mysql /var/lib/mysql-keyring
</span></span><span class="line"><span class="cl">sudo chmod <span class="m">750</span> /var/lib/mysql-keyring</span></span></code></pre>
</div>
</div>
</div>
<ol>
<li>Securely copy the keyring file from Node1:</li>
</ol>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">scp /var/lib/mysql-keyring/component_keyring_file node2:/var/lib/mysql-keyring/component_keyring_file</span></span></code></pre>
</div>
</div>
</div>
<p><strong>Important:</strong><br>
Do not modify the file. Do not recreate it. Do not allow MySQL to generate a new one on secondary nodes.</p>
<h3>Start MySQL on Each Node and Verify<a class="anchor-link" id="start-mysql-on-each-node-and-verify"></a></h3>
<p>After the keyring file is in place:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">bash</span><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl start mysqld</span></span></code></pre>
</div>
</div>
</div>
<p>Verify the component is active:</p>
<div class="code-block">
<div class="code-block__header"><span class="code-block__lang">sql</span><button class="code-block__copy" type="button" data-copy-target="codeblock-18" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">performance_schema</span><span class="p">.</span><span class="n">keyring_component_status</span><span class="p">;</span></span></span></code></pre>
</div>
</div>
</div>
<p>Each node should report the component_keyring_file as loaded and active.</p>
<p>At this point:</p>
<ul>
<li>Encrypted tablespaces will open correctly</li>
<li>SST and IST operations will succeed</li>
<li>The cluster will behave consistently during restarts</li>
</ul>
<h2>Operational Notes and Best Practices<a class="anchor-link" id="operational-notes-and-best-practices"></a></h2>
<ul>
<li>Treat the keyring file like a secret, not configuration</li>
<li>Restrict access to root only</li>
<li>Include the keyring file in your secure backup strategy</li>
<li>When provisioning new nodes, copy the keyring file before first startup</li>
<li>Never rotate or regenerate the keyring independently on individual nodes</li>
</ul>
<p>If the keyring is lost and encrypted data exists, recovery is not possible.</p>
<hr>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>This setup works reliably for:</p>
<ul>
<li>Percona Server 8.4</li>
<li>Percona XtraDB Cluster 8.4<br>
(with the known exception of 8.4.4&ndash;8.4.5)</li>
</ul>
<p>Most failures come down to:</p>
<ul>
<li>Treating JSON like a <code>.cnf</code> file</li>
<li>Loose ownership on sensitive files</li>
<li>Forgetting the PXC-specific workaround</li>
</ul>
<p>Once those are handled, the component keyring fades into the background where it belongs. And when it comes to encryption, boring, quiet, and uneventful is exactly the outcome you want.</p>

<p><a href="https://percona.community/blog/2026/01/13/configuring-the-component-keyring-in-percona-server-and-pxc-8.4/">Configuring the Component Keyring in Percona Server and PXC 8.4</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Stop using MySQL in 2026, it is not true open source</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/" />
      <id>https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/</id>
      <updated>2026-01-11T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>If you care about supporting open source software, and still use MySQL in 2026, you should switch to MariaDB like so many others have already done.<br />
The number of git commits on github.com/mysql/mysql-server has been significantly declining in 2025. The screenshot below shows the state of git commits as of writing this in January 2026, and the picture should be alarming to anyone who cares about software being open source.</p>
<p>This is not surprising – Oracle should not be trusted as the steward for open source projects<br />
When Oracle acquired Sun Microsystems and MySQL along with it back in 2009, the European Commission almost blocked the deal due to concerns that Oracle’s goal was just to stifle competition. The deal went through as Oracle made a commitment to keep MySQL going and not kill it, but (to nobody’s surprise) Oracle has not been a good steward of MySQL as an open source project and the community around it has been withering away for years now. All development is done behind closed doors. The publicly visible bug tracker is not the real one Oracle staff actually uses for MySQL development, and the few people who try to contribute to MySQL just see their Pull Requests and patch submissions marked as received with mostly no feedback and then those changes may or may not be in the next MySQL release, often rewritten, and with only Oracle staff in the git author/committer fields. The real author only gets a small mention in a blog post. When I was the engineering manager for the core team working on RDS MySQL and RDS MariaDB at Amazon Web Services, I oversaw my engineers’ contributions to both MySQL and MariaDB (the latter being a fork of MySQL by the original MySQL author, Michael Widenius). All the software developers in my org disliked submitting code to MySQL due to how bad the reception by Oracle was to their contributions.<br />
MariaDB is the stark opposite with all development taking place in real-time on github.com/mariadb/server, anyone being able to submit a Pull Request and get a review, all bugs being openly discussed at jira.mariadb.org and so forth, just like one would expect from a true open source project. MySQL is open source only by license (GPL v2), but not as a project.<br />
MySQL’s technical decline in recent years<br />
Despite not being a good open source steward, Oracle should be given credit that it did keep the MySQL organization alive and allowed it to exist fairly independently and continue developing and releasing new MySQL versions well over a decade after the acquisition. I have no insight into how many customers they had, but I assume the MySQL business was fairly profitable and financially useful to Oracle, at least as long as it didn’t gain too many features to threaten Oracle’s own main database business.<br />
I don’t know why, perhaps because too many talented people had left the organization, but it seems that from a technical point of view MySQL clearly started to deteriorate from 2022 onward.<br />
When MySQL 8.0.29 was released with the default ALTER TABLE method switched to run in-place, it had a lot of corner cases that didn’t work, causing the database to crash and data to corrupt for many users. The issue wasn’t fully fixed until a year later in MySQL 8.0.32. To many users annoyance Oracle announced the 8.0 series as “evergreen” and introduced features and changes in the minor releases, instead of just doing bugfixes and security fixes like users historically had learnt to expect from these x.y.Z maintenance releases.<br />
There was no new major MySQL version for six years. After MySQL 8.0 in 2018 it wasn’t until 2023 when MySQL 8.1 was released, and it was just a short-term preview release. The first actual new major release MySQL 8.4 LTS was released in 2024. Even though it was a new major release, many users got disappointed as it had barely any new features.<br />
Many also reported degraded performance with newer MySQL versions, for example the benchmark by famous MySQL performance expert Mark Callaghan below shows that on write-heavy workloads MySQL 9.5 throughput is typically 15% less than in 8.0.</p>
<p>Due to newer MySQL versions deprecating many features, a lot of users also complained about significant struggles regarding both MySQL 5.7- &#62;8.0 and 8.0- &#62;8.4 upgrades. With few new features and heavy focus on code base cleanup and feature deprecation, it became obvious to many that Oracle had decided to just keep MySQL barely alive, and put all new relevant features (e.g. vector search) into Heatwave, Oracle’s closed-source and cloud-only service for MySQL customers.<br />
As it was evident that Oracle isn’t investing in MySQL, Percona’s Peter Zaitsev wrote Is Oracle Finally Killing MySQL in June 2024. At this time MySQL’s popularity as ranked by DB-Engines had also started to tank hard, a trend that likely accelerates in 2026.</p>
<p>In September 2025 news reported that Oracle was reducing its workforce and that the MySQL staff was getting heavily reduced. Obviously this does not bode well for MySQL’s future, and Peter Zaitsev posted already in November stats showing that the latest MySQL maintenance release contained fewer bug fixes than before.<br />
Open source is more than ideology: it has very real effects on software security and sovereignty<br />
Some say they don’t care if MySQL is truly open source or not, or that they don’t care if it has a future in coming years, as long as it still works now. I am afraid people thinking so are taking a huge risk. The database is often the most critical part of a software application stack, and any flaw or problem in operations, let alone a security issue, will have immediate consequences, and “not caring” will eventually get people fired or sued.<br />
In open source problems are discussed openly, and the bigger the problem, the more people and companies will contribute to fixing it. Open source as a development methodology is similar to the scientific method with free flow of ideas that are constantly contested and only the ones with the most compelling evidence win. Not being open means more obscurity, more risk and more “just trust us bro” attitude.<br />
This open vs. closed is very visible for example in how Oracle handles security issues. We can see that in 2025 alone MySQL published 123 CVEs about security issues, while MariaDB had 8. There were 117 CVEs that only affected MySQL and not MariaDB in 2025. I haven’t read them all, but typically the CVEs hardly contain any real details. As an example, the most recent one CVE-2025-53067 states “Easily exploitable vulnerability allows high privileged attacker with network access via multiple protocols to compromise MySQL Server.” There is no information a security researcher or auditor could use to verify if any original issue actually existed, or if it was fixed, or if the fix was sufficient and fully mitigating the issue or not. MySQL users just have to take the word of Oracle that it is all good now. Handling security issues like this is in stark contrast to other open source projects, where all security issues and their code fixes are open for full scrutiny after the initial embargo is over and CVE made public.<br />
There is also various forms of enshittification going on one would not see in a true open source project, and everything about MySQL as a software, documentation and website is pushing users to stop using the open source version and move to the closed MySQL versions, and in particular to Heatwave, which is not only closed-source but also results in Oracle fully controlling customer’s databases contents.<br />
Of course, some could say this is how Oracle makes money and is able to provide a better product. But stories on Reddit and elsewhere suggest that what is going on is more like Oracle milking hard the last remaining MySQL customers who are forced to pay more and more for getting less and less.<br />
There are options and migrating is easy, just do it<br />
A large part of MySQL users switched to MariaDB already in the mid-2010s, in particular everyone who had cared deeply about their database software staying truly open source. That included large installations such as Wikipedia, and Linux distributions such as Fedora and Debian. Because it’s open source and there is no centralized machine collecting statistics, nobody knows what the exact market shares look like. There are however some application specific stats, such as that 57% of WordPress sites around the world run MariaDB, while the share for MySQL is 42%.<br />
For anyone running a classic LAMP stack application such as WordPress, Drupal, Mediawiki, Nextcloud, or Magento, switching the old MySQL database to MariaDB is be straightforward. As MariaDB is a fork of MySQL and mostly backwards compatible with it, swapping out MySQL for MariaDB can be done without changing any of the existing connectors or database clients, as they will continue to work with MariaDB as if it was MySQL.<br />
For those running custom applications and who have the freedom to make changes to how and what database is used, there are tens of mature and well-functioning open source databases to choose from, with PostgreSQL being the most popular general database. If your application was built from the start for MySQL, switching to PostgreSQL may however require a lot of work, and the MySQL/MariaDB architecture and storage engine InnoDB may still offer an edge in e.g. online services where high performance, scalability and solid replication features are of highest priority. For a quick and easy migration MariaDB is probably the best option.<br />
Switching from MySQL to the Percona Server is also very easy, as it closely tracks all changes in MySQL and deviates from it only by a small number of improvements done by Percona. However, also precisely because of it being basically just a customized version of the MySQL Server, it’s not a viable long-term solution for those trying to fully ditch the dependency on Oracle.<br />
There are also several open source databases that have no common ancestry with MySQL, but strive to be MySQL-compatible. Thus most apps built for MySQL can simply switch to using them without needing SQL statements to be rewritten. One such database is TiDB, which has been designed from scratch specifically for highly scalable and large systems, and is so good that even Amazon’s latest database solution DSQL was built borrowing many ideas from TiDB. However, TiDB only really shines with larger distributed setups, so for the vast majority of regular small- and mid-scale applications currently using MySQL, the most practical solution is probably to just switch to MariaDB, which on most Linux distributions can simply be installed by running apt/dnf/brew install mariadb-server.<br />
Whatever you end up choosing, as long as it is not Oracle, you will be better off.</p>
<p><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/">Stop using MySQL in 2026, it is not true open source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/featured-image.jpg" alt="Featured image of post Stop using MySQL in 2026, it is not true open source"></p>
<p><strong>If you care about supporting open source software, and still use MySQL in 2026, you should switch to MariaDB like so many others have already done.</strong></p>
<p>The number of git commits on <a class="link" href="https://github.com/mysql/mysql-server/graphs/commit-activity" target="_blank" rel="noopener">github.com/mysql/mysql-server</a> has been significantly declining in 2025. The screenshot below shows the state of git commits as of writing this in January 2026, and the picture should be alarming to anyone who cares about software being open source.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/mysql-github-commits-decreasing-2025.png" width="927" height="605" loading="lazy" alt="MySQL GitHub commit activity decreasing drastically" class="gallery-image" data-flex-grow="153" data-flex-basis="367px">
</p>
<h2><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/#this-is-not-surprising--oracle-should-not-be-trusted-as-the-steward-for-open-source-projects" class="header-anchor"></a>This is not surprising &ndash; Oracle should not be trusted as the steward for open source projects<br>
<a class="anchor-link" id="this-is-not-surprising-oracle-should-not-be-trusted-as-the-steward-for-open-source-projects"></a></h2>
<p>When Oracle acquired Sun Microsystems and MySQL along with it back in 2009, the European Commission almost blocked the deal due to concerns that Oracle&rsquo;s goal was just to stifle competition. The deal went through as Oracle made a commitment to keep MySQL going and not kill it, but (to nobody&rsquo;s surprise) Oracle has not been a good steward of MySQL as an open source project and the community around it has been withering away for years now. <strong>All development is done behind closed doors.</strong> The publicly visible bug tracker is not the real one Oracle staff actually uses for MySQL development, and the few people who try to contribute to MySQL just see their Pull Requests and patch submissions marked as received with mostly no feedback and then those changes may or may not be in the next MySQL release, often rewritten, and with only Oracle staff in the git author/committer fields. The real author only gets a small mention in a blog post. When I was the engineering manager for the core team working on RDS MySQL and RDS MariaDB at Amazon Web Services, I oversaw my engineers&rsquo; contributions to both <a class="link" href="https://en.wikipedia.org/wiki/MySQL" target="_blank" rel="noopener">MySQL</a> and <a class="link" href="https://en.wikipedia.org/wiki/MariaDB" target="_blank" rel="noopener">MariaDB</a> (the latter being a fork of MySQL by the original MySQL author, <a class="link" href="https://en.wikipedia.org/wiki/Michael_Widenius" target="_blank" rel="noopener">Michael Widenius</a>). All the software developers in my org disliked submitting code to MySQL due to how bad the reception by Oracle was to their contributions.</p>
<p>MariaDB is the stark opposite with all development taking place in real-time on <a class="link" href="http://github.com/mariadb/server" target="_blank" rel="noopener">github.com/mariadb/server</a>, anyone being able to submit a Pull Request and get a review, all bugs being openly discussed at <a class="link" href="http://jira.mariadb.org/" target="_blank" rel="noopener">jira.mariadb.org</a> and so forth, just like one would expect from a true open source project. <em>MySQL is open source only by license</em> (<a class="link" href="https://github.com/mysql/mysql-server/blob/trunk/LICENSE" target="_blank" rel="noopener">GPL v2</a>), but not as a project.</p>
<h2><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/#mysqls-technical-decline-in-recent-years" class="header-anchor"></a>MySQL&rsquo;s technical decline in recent years<br>
<a class="anchor-link" id="mysqls-technical-decline-in-recent-years"></a></h2>
<p>Despite not being a good open source steward, Oracle should be given credit that it did keep the MySQL organization alive and allowed it to exist fairly independently and continue developing and releasing new MySQL versions well over a decade after the acquisition. I have no insight into how many customers they had, but I assume the MySQL business was fairly profitable and financially useful to Oracle, at least as long as it didn&rsquo;t gain too many features to threaten Oracle&rsquo;s own main database business.</p>
<p>I don&rsquo;t know why, perhaps because too many talented people had left the organization, but it seems that from a technical point of view MySQL clearly started to deteriorate from 2022 onward.</p>
<p>When MySQL 8.0.29 was released with the default ALTER TABLE method switched to run <em>in-place</em>, it had a lot of corner cases that didn&rsquo;t work, causing the database to crash and data to corrupt for many users. The issue wasn&rsquo;t fully fixed until a year later in MySQL 8.0.32. To many users annoyance Oracle announced the 8.0 series as &ldquo;evergreen&rdquo; and introduced features and changes in the minor releases, instead of just doing bugfixes and security fixes like users historically had learnt to expect from these x.y.Z maintenance releases.</p>
<p><strong>There was no new major MySQL version for six years.</strong> After MySQL 8.0 in 2018 it wasn&rsquo;t until 2023 when MySQL 8.1 was released, and it was just a short-term preview release. The first actual new major release MySQL 8.4 LTS was released in 2024. Even though it was a new major release, many users got disappointed as it had barely any new features.</p>
<p>Many also reported degraded performance with newer MySQL versions, for example the benchmark by famous MySQL performance expert <a class="link" href="https://smalldatum.blogspot.com/" target="_blank" rel="noopener">Mark Callaghan</a> below shows that on write-heavy workloads <a class="link" href="https://smalldatum.blogspot.com/2025/12/performance-regressions-in-mysql-84-and.html" target="_blank" rel="noopener">MySQL 9.5 throughput is typically 15% less than in 8.0</a>.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/smalldatum-benchmark-mysql-new-versions-regressed.png" width="640" height="396" loading="lazy" alt="Benchmark showing new MySQL versions being slower than the old" class="gallery-image" data-flex-grow="161" data-flex-basis="387px">
</p>
<p>Due to newer MySQL versions deprecating many features, a lot of users also complained about <strong>significant struggles regarding both MySQL 5.7-&gt;8.0 and 8.0-&gt;8.4 upgrades</strong>. With few new features and heavy focus on code base cleanup and feature deprecation, it became obvious to many that Oracle had decided to just keep MySQL barely alive, and put all new relevant features (e.g. vector search) into Heatwave, Oracle&rsquo;s closed-source and cloud-only service for MySQL customers.</p>
<p>As it was evident that Oracle isn&rsquo;t investing in MySQL, Percona&rsquo;s Peter Zaitsev wrote <a class="link" href="https://www.percona.com/blog/is-oracle-finally-killing-mysql/" target="_blank" rel="noopener">Is Oracle Finally Killing MySQL</a> in June 2024. At this time MySQL&rsquo;s popularity as ranked by <a class="link" href="https://db-engines.com/en/ranking_trend" target="_blank" rel="noopener">DB-Engines</a> had also started to tank hard, a trend that likely accelerates in 2026.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/db-engines-ranking-mysql-going-down.png" width="925" height="541" loading="lazy" alt="MySQL dropping significantly in DB-Engines ranking" class="gallery-image" data-flex-grow="170" data-flex-basis="410px">
</p>
<p>In September 2025 <a class="link" href="https://www.theregister.com/2025/09/11/oracle_slammed_for_mysql_job/" target="_blank" rel="noopener">news reported</a> that Oracle was reducing its workforce and that the <em>MySQL staff was getting heavily reduced</em>. Obviously this does not bode well for MySQL&rsquo;s future, and Peter Zaitsev posted already in November stats showing that the <a class="link" href="https://www.linkedin.com/posts/peterzaitsev_opensource-mysql-activity-7386744600893501440-lZ7I/" target="_blank" rel="noopener">latest MySQL maintenance release contained fewer bug fixes</a> than before.</p>
<h2><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/#open-source-is-more-than-ideology-it-has-very-real-effects-on-software-security-and-sovereignty" class="header-anchor"></a>Open source is more than ideology: it has very real effects on software security and sovereignty<br>
<a class="anchor-link" id="open-source-is-more-than-ideology-it-has-very-real-effects-on-software-security-and-sovereignty"></a></h2>
<p>Some say they don&rsquo;t care if MySQL is truly open source or not, or that they don&rsquo;t care if it has a future in coming years, as long as it still works now. I am afraid people thinking so are taking a huge risk. The database is often the most critical part of a software application stack, and any flaw or problem in operations, let alone a security issue, will have immediate consequences, and <em>&ldquo;not caring&rdquo; will eventually get people fired or sued</em>.</p>
<p>In open source problems are discussed openly, and the bigger the problem, the more people and companies will contribute to fixing it. Open source as a development methodology is similar to the scientific method with free flow of ideas that are constantly contested and only the ones with the most compelling evidence win. <em>Not being open means more obscurity, more risk and more &ldquo;just trust us bro&rdquo; attitude.</em></p>
<p>This open vs. closed is very visible for example in how Oracle handles security issues. We can see that in 2025 alone <strong>MySQL published 123 CVEs</strong> about security issues, while <a class="link" href="https://mariadb.com/docs/server/security/securing-mariadb/security" target="_blank" rel="noopener">MariaDB had 8</a>. There were 117 CVEs that only affected MySQL and not MariaDB in 2025. I haven&rsquo;t read them all, but typically the CVEs hardly contain any real details. As an example, the most recent one <a class="link" href="https://www.cve.org/cverecord?id=cve-2025-53067" target="_blank" rel="noopener">CVE-2025-53067</a> states <em>&ldquo;Easily exploitable vulnerability allows high privileged attacker with network access via multiple protocols to compromise MySQL Server.&rdquo;</em> There is <strong>no</strong> information a security researcher or auditor could use to verify if any original issue actually existed, or if it was fixed, or if the fix was sufficient and fully mitigating the issue or not. MySQL users just have to take the word of Oracle that it is all good now. Handling security issues like this is in stark contrast to other open source projects, where all security issues and their code fixes are open for full scrutiny after the initial embargo is over and CVE made public.</p>
<p>There is also various forms of <a class="link" href="https://en.wikipedia.org/wiki/Enshittification" target="_blank" rel="noopener">enshittification</a> going on one would not see in a true open source project, and everything about MySQL as a software, documentation and website is pushing users to stop using the open source version and move to the closed MySQL versions, and in particular to Heatwave, which is not only closed-source but also results in Oracle fully controlling customer&rsquo;s databases contents.</p>
<p>Of course, some could say this is how Oracle makes money and is able to provide a better product. But stories on <a class="link" href="https://www.reddit.com/r/mysql/comments/1o298er/oracle_rif_effects_on_mysql/" target="_blank" rel="noopener">Reddit</a> and elsewhere suggest that what is going on is more like Oracle milking hard the last remaining MySQL customers who are <em>forced to pay more and more for getting less and less</em>.</p>
<h2><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/#there-are-options-and-migrating-is-easy-just-do-it" class="header-anchor"></a>There are options and migrating is easy, just do it<br>
<a class="anchor-link" id="there-are-options-and-migrating-is-easy-just-do-it"></a></h2>
<p>A large part of MySQL users switched to MariaDB already in the mid-2010s, in particular everyone who had cared deeply about their database software staying truly open source. That included large installations such as Wikipedia, and Linux distributions such as Fedora and Debian. Because it&rsquo;s open source and there is no centralized machine collecting statistics, nobody knows what the exact market shares look like. There are however some application specific stats, such as that <a class="link" href="https://wordpress.org/about/stats/#mysql_version" target="_blank" rel="noopener">57% of WordPress sites around the world run MariaDB</a>, while the share for MySQL is 42%.</p>
<p>For anyone running a classic <a class="link" href="https://en.wikipedia.org/wiki/LAMP_%28software_bundle%29" target="_blank" rel="noopener">LAMP stack</a> application such as WordPress, Drupal, Mediawiki, Nextcloud, or Magento, switching the old MySQL database to MariaDB is be straightforward. As MariaDB is a <a class="link" href="https://en.wikipedia.org/wiki/Fork_%28software_development%29" target="_blank" rel="noopener">fork</a> of MySQL and mostly backwards compatible with it, swapping out MySQL for MariaDB can be done without changing any of the existing connectors or database clients, as they will continue to work with MariaDB as if it was MySQL.</p>
<p>For those running custom applications and who have the freedom to make changes to how and what database is used, there are tens of mature and well-functioning open source databases to choose from, with PostgreSQL being the most popular general database. If your application was built from the start for MySQL, switching to PostgreSQL may however require a lot of work, and the MySQL/MariaDB architecture and storage engine InnoDB <a class="link" href="https://www.uber.com/en-ca/blog/postgres-to-mysql-migration/" target="_blank" rel="noopener">may still offer an edge</a> in e.g. online services where high performance, scalability and solid replication features are of highest priority. For a quick and easy migration MariaDB is probably the best option.</p>
<p>Switching from MySQL to the Percona Server is also very easy, as it closely tracks all changes in MySQL and deviates from it only by a small number of improvements done by Percona. However, also precisely because of it being basically just a customized version of the MySQL Server, it&rsquo;s not a viable long-term solution for those trying to fully ditch the dependency on Oracle.</p>
<p>There are also several open source databases that have no common ancestry with MySQL, but strive to be MySQL-compatible. Thus most apps built for MySQL can simply switch to using them without needing SQL statements to be rewritten. One such database is <a class="link" href="https://en.wikipedia.org/wiki/TiDB" target="_blank" rel="noopener">TiDB</a>, which has been designed from scratch specifically for highly scalable and large systems, and is so good that even Amazon&rsquo;s latest database solution DSQL was built borrowing many ideas from TiDB. However, TiDB only really shines with larger distributed setups, so for the vast majority of regular small- and mid-scale applications currently using MySQL, the most practical solution is probably to just switch to MariaDB, which on most Linux distributions can simply be installed by running <code>apt/dnf/brew install mariadb-server</code>.</p>
<p>Whatever you end up choosing, as long as it is <strong>not Oracle</strong>, you will be better off.</p>

<p><a href="https://optimizedbyotto.com/post/reasons-to-stop-using-mysql/">Stop using MySQL in 2026, it is not true open source</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Undo Log Truncation Bug in 8.0 leads to Data Corruption</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html" />
      <id>https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html</id>
      <updated>2026-01-05T22:23:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am upset about this one : I have a hard time not seeing this as negligence, and it starts to become a pattern...  So please forgive me if this post is not my most diplomatic, because I really think someone deserves a kick in the butt !  But what is all this about...</p>
<p>There is a MySQL bug, which can lead to data corruption, opened for 8.0 in September 2023, fixed in MySQL 8.4.0 (</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html">Undo Log Truncation Bug in 8.0 leads to Data Corruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am upset about this one : I have a hard time not seeing this as negligence, and it starts to become a pattern&hellip;&nbsp; So please forgive me if this post is not my most diplomatic, because I really think someone deserves a kick in the butt&nbsp;!&nbsp; But what is all this about&hellip;</p>
<p>There is a MySQL bug, which can lead to data corruption, opened for 8.0 in September 2023, fixed in MySQL 8.4.0 (</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html">Undo Log Truncation Bug in 8.0 leads to Data Corruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Undo Log Truncation Bug in 8.0 leads to Data Corruption</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html" />
      <id>https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html</id>
      <updated>2026-01-05T22:23:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>I am upset about this one : I have a hard time not seeing this as negligence, and it starts to become a pattern...  So please forgive me if this post is not my most diplomatic, because I really think someone deserves a kick in the butt !  But what is all this about...</p>
<p>There is a MySQL bug, which can lead to data corruption, opened for 8.0 in September 2023, fixed in MySQL 8.4.0 (</p>
<p><a href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html">Undo Log Truncation Bug in 8.0 leads to Data Corruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I am upset about this one : I have a hard time not seeing this as negligence, and it starts to become a pattern&hellip;&nbsp; So please forgive me if this post is not my most diplomatic, because I really think someone deserves a kick in the butt&nbsp;!&nbsp; But what is all this about&hellip;</p>
<p>There is a MySQL bug, which can lead to data corruption, opened for 8.0 in September 2023, fixed in MySQL 8.4.0 (</p>

<p><a href="https://jfg-mysql.blogspot.com/2026/01/undo-log-truncation-bug-in-80-leads-to-data-corruption.html.html">Undo Log Truncation Bug in 8.0 leads to Data Corruption</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB Underrated Features: Zero Dates and Partial Dates</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/mariadb-underrated-features-zero-dates-and-partial-dates/" />
      <id>https://vettabase.com/mariadb-underrated-features-zero-dates-and-partial-dates/</id>
      <updated>2025-12-30T09:43:45+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>How do you represent information like this in a database? There are many ways to do that. The most common is to split dates into three different columns, each of which will be NULL when it doesn’t have a specific value.But this makes dates harder to validate for the database, it’s inpractical because it complicates SQL queries, and NULL is error-prone. A more practical and efficient way to store partial dates is to use zero-dates, and date with zero-components (partial dates). In this article I’ll show you how to use them. sql_mode and date validity MariaDB has an sql_mode variable that affects the way SQL queries are interpreted. It’s useful to: The sql_mode is a comma-separated list of flags. In this article, we’re interested in the following flags: NO_ZERO_DATE allows the special date 0000-00-00. NO_ZERO_IN_DATE allows the year, month, or day component to be zero, even if the rest of the date is not necessarily zero. For example: 0000-09-30. ALLOW_INVALID_DATES has no effects on zero dates or partial dates. It just makes the validity check more trivial: instead of checking the length of the month, keeping into account leap years, it will simply consider valid the day part when it […]</p>
<p><a href="https://vettabase.com/mariadb-underrated-features-zero-dates-and-partial-dates/">MariaDB Underrated Features: Zero Dates and Partial Dates</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">How do you represent information like this in a database?</p>
<ul class="wp-block-list">
<li>This event happened in 2015/06, but we don&rsquo;t know in which day.</li>
<li>This job is scheduled to happen on the first day of the month at 00:00:00, every month and every year.</li>
<li>This never happened.</li>
</ul>
<p class="wp-block-paragraph">There are many ways to do that. The most common is to split dates into three different columns, each of which will be <code>NULL</code> when it doesn&rsquo;t have a specific value.<br>But this makes dates harder to validate for the database, it&rsquo;s inpractical because it complicates SQL queries, and <code>NULL</code> is error-prone.</p>
<p class="wp-block-paragraph">A more practical and efficient way to store partial dates is to use zero-dates, and date with zero-components (partial dates). In this article I&rsquo;ll show you how to use them.</p>
<h2 class="wp-block-heading">sql_mode and date validity<a class="anchor-link" id="sql_mode-and-date-validity"></a></h2>
<p class="wp-block-paragraph">MariaDB has an <a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode" rel="noopener">sql_mode</a> variable that affects the way SQL queries are interpreted. It&rsquo;s useful to:</p>
<ul class="wp-block-list">
<li>Make error-checking looser, accepting values that are not entirely valid.</li>
<li>Making error-checking stricter, making sure that invalid values are rejected.</li>
<li>Increasing compatibility with some other DBMSs, at both syntax and semantic levels.</li>
</ul>
<p class="wp-block-paragraph">The sql_mode is a comma-separated list of flags. In this article, we&rsquo;re interested in the following flags:</p>
<ul class="wp-block-list">
<li><code><a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode#no_zero_date" rel="noopener">NO_ZERO_DATE</a></code>;</li>
<li><code><a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode#no_zero_in_date" rel="noopener">NO_ZERO_IN_DATE</a></code>;</li>
<li><code><a href="https://mariadb.com/docs/server/server-management/variables-and-modes/sql_mode#allow_invalid_dates" rel="noopener">ALLOW_INVALID_DATES</a></code>.</li>
</ul>
<p class="wp-block-paragraph"><code>NO_ZERO_DATE</code> allows the special date <code>0000-00-00</code>. </p>
<p class="wp-block-paragraph"><code>NO_ZERO_IN_DATE</code> allows the year, month, or day component to be zero, even if the rest of the date is not necessarily zero. For example: <code>0000-09-30</code>.</p>
<p class="wp-block-paragraph"><code>ALLOW_INVALID_DATES</code> has no effects on zero dates or partial dates. It just makes the validity check more trivial: instead of checking the length of the month, keeping into account leap years, it will simply consider valid the day part when it doesn&rsquo;t exceed 31.</p>
<p class="wp-block-paragraph">To see the current <code>sql_mode</code>, run this query:</p>
<pre class="wp-block-code"><code>SELECT @@sql_mode;</code></pre>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>All the above flags are off by default. If you plan to use zero/partial dates or if you&rsquo;re not sure, I recommend to leave it as-is. If you plan to never use these features, I recommend to set <code>NO_ZERO_DATE</code> and <code>NO_ZERO_IN_DATE</code> to make your MariaDB data more resilient to application bugs.</p>
</div>
</fieldset>
</div>
<p class="wp-block-paragraph">The above flag affects the <code>DATE</code> and <code>DATETIME</code> data types. Don&rsquo;t try to use this feature with <code>TIMESTAMP</code> columns, because only valid dates can reliably be converted to UNIX timestamps.</p>
<p class="wp-block-paragraph">For simplicity, in this article we&rsquo;ll only use the <code>DATE</code> type.</p>
<h2 class="wp-block-heading">Working with Zero Dates and Partial Dates<a class="anchor-link" id="working-with-zero-dates-and-partial-dates"></a></h2>
<p class="wp-block-paragraph">Let&rsquo;s start by creating a normal table with a <code>DATE</code> column with a <code>UNIQUE</code> index on it:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE TABLE schedule (
    id INT UNSIGNED AUTO_INCREMENT,
    date TIMESTAMP NULL,
    PRIMARY KEY (id),
    UNIQUE unq_date (date)
);</code></pre>
<p class="wp-block-paragraph">Now let&rsquo;s insert some values. As you can see, they include a zero date and somew partial dates of all types:</p>
<pre class="wp-block-code"><code>INSERT INTO schedule (date) VALUES
      ('0000-00-00')
    , ('0000-00-01')
    , ('0000-00-31')
    , ('0000-12-01')
    , ('0000-11-00')
    , ('0000-12-00')
    , ('0000-12-10')
    , ('0000-12-21')
    , ('2000-00-01')
    , ('2000-00-11')
    , ('2000-01-00')
    , ('2000-01-01')
    , ('2000-12-21')
    , ('2001-00-00')
    , ('2001-01-00')
    , ('2001-02-02')
    , ('2001-02-03')
;</code></pre>
<p class="wp-block-paragraph">We can now test the <code>UNIQUE</code> index. We know that <code>UNIQUE</code> indexes accept multiple <code>NULL</code>s, but we don&rsquo;t expect zero dates or zero date components to be treated as <code>NULL</code>s:</p>
<pre class="wp-block-code"><code>&gt; INSERT INTO schedule (date) VALUES ('0000-00-00');
ERROR 1062 (23000): Duplicate entry '0000-00-00' for key 'unq_date'
&gt; INSERT INTO schedule (date) VALUES ('2001-01-00');
ERROR 1062 (23000): Duplicate entry '2001-01-00' for key 'unq_date'</code></pre>
<p class="wp-block-paragraph">Dates are ordered as expected, with the zeroes preceding other numbers:</p>
<pre class="wp-block-code"><code>&gt; SELECT date FROM schedule ORDER BY 1;
+------------+
| date       |
+------------+
| 0000-00-00 |
| 0000-00-01 |
| 0000-00-31 |
| 0000-11-00 |
| 0000-12-00 |
| 0000-12-01 |
| 0000-12-10 |
| 0000-12-21 |
| 2000-00-01 |
| 2000-00-11 |
| 2000-01-00 |
| 2000-01-01 |
| 2000-12-21 |
| 2001-00-00 |
| 2001-01-00 |
| 2001-02-02 |
| 2001-02-03 |
+------------+</code></pre>
<p class="wp-block-paragraph">We can also find zero years, zero months and zero dates by using the <code>YEAR()</code>, <code>MONTH()</code>, and <code>DAYOFMONTH()</code> SQL functions:</p>
<pre class="wp-block-code"><code>&gt; SELECT date FROM schedule WHERE DAYOFMONTH(date) = 0;
+------------+
| date       |
+------------+
| 0000-00-00 |
| 0000-11-00 |
| 0000-12-00 |
| 2000-01-00 |
| 2001-00-00 |
| 2001-01-00 |
+------------+</code></pre>
<p class="wp-block-paragraph">What happens if we ask for information that won&rsquo;t make sense without a complete valid date, like the day of the week? In this case, we&rsquo;ll obtain <code>NULL</code>:</p>
<pre class="wp-block-code"><code>&gt; SELECT date, DAYOFWEEK(date) FROM schedule LIMIT 8;
+------------+-----------------+
| date       | DAYOFWEEK(date) |
+------------+-----------------+
| 0000-00-00 |            NULL |
| 0000-00-01 |            NULL |
| 0000-00-31 |            NULL |
| 0000-11-00 |            NULL |
| 0000-12-00 |            NULL |
| 0000-12-01 |               6 |
| 0000-12-10 |               1 |
| 0000-12-21 |               5 |
+------------+-----------------+</code></pre>
<h2 class="wp-block-heading">Partial Dates Validation<a class="anchor-link" id="partial-dates-validation"></a></h2>
<p class="wp-block-paragraph">When the month is zero, MariaDB can&rsquo;t decide the maximum day, but it still assumes that it can&rsquo;t be more than 31:</p>
<pre class="wp-block-code"><code>&gt; SELECT DATE '2000-00-31'; 
+-------------------+
| DATE '2000-00-31' |
+-------------------+
| 2000-00-31        |
+-------------------+
1 row in set (0.000 sec)

MariaDB [(none)]&gt; SELECT DATE '2000-00-32';
ERROR 1525 (HY000): Incorrect DATE value: '2000-00-32'</code></pre>
<p class="wp-block-paragraph">Similarly, I&rsquo;d expect MariaDB to accept <code>0000-02-29</code>, because of leap years. This is not the case, so I reported the bug <a href="https://jira.mariadb.org/browse/MDEV-38455" rel="noopener">MDEV-38455</a>.</p>
<p class="wp-block-paragraph">That said, you can force MariaDB to eeject days <code>&gt; 28</code>, to only accept dates that are surely valid:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE TABLE schedule (
    id INT UNSIGNED AUTO_INCREMENT,
    date DATE NULL CHECK (DAYOFMONTH(date) &lt;= 28),
    PRIMARY KEY (id),
    UNIQUE unq_date (date)
);</code></pre>
<h2 class="wp-block-heading">Zeroes versus NULL<a class="anchor-link" id="zeroes-versus-null"></a></h2>
<p class="wp-block-paragraph">As mentioned before, instead of <code>'0000-00-00'</code>, you might use <code>NULL</code>. And instead of <code>'2026-01-00'</code> you might use three different columns, and set <code>day</code> to <code>NULL</code>.</p>
<p class="wp-block-paragraph">I discourage the use of <code>NULL</code> when possible, for many reasons. For example, it is error-prone and <a href="https://vettabase.com/what-does-null-mean-in-sql/" data-type="post" data-id="38282">semantically inconsistent</a>: depending on the situation, it could mean <em>non-applicable</em> or <em>unknown value</em>. So <code>0+NULL=NULL</code>, which makes sense if it&rsquo;s an unknown value. But <code>MAX(col)</code> ignores <code>NULL</code>s, which only makes sense if it&rsquo;s an absent value. However, in some situations it&rsquo;s so much more convenient than the alternatives that, in practice, we don&rsquo;t have alternatives.</p>
<p class="wp-block-paragraph">In the case of dates, if you only need a value that means unknown <strong>or</strong> (XOR) a value that means not-applicable, my recommendation is to only use zero dates or partial dates.</p>
<p class="wp-block-paragraph">But if you need both, it might be a good idea to use <code>NULL</code> <strong>and</strong> zero dates or partial dates. In this case, I&rsquo;d use <code>NULL</code> as an unknown value, because its behaviour tends to be a bit more consistent with this interpretation.</p>
<h2 class="wp-block-heading">MySQL Compatibility<a class="anchor-link" id="mysql-compatibility"></a></h2>
<p class="wp-block-paragraph">The features described in this article were implemented in MySQL long before MariaDB existed. They should work without changes in all present and past versions (which currently means, up to 9.5). But the <code>NO_ZERO_DATE</code> and <code>NO_ZERO_IN_DATE</code> <code>sql_mode</code> flags have been deprecated for some years, and will be removed at some point. When it happens, zero dates and partial dates won&rsquo;t be accepted anymore.</p>
<p class="wp-block-paragraph">If you use these features but you use MySQL, consider migrating to MariaDB.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">Not many people know that MariaDB supports zero dates and partial dates. And I&rsquo;d be surprised to find out that some ORMs support it.</p>
<p class="wp-block-paragraph">Nevertheless, this is a convenient feature and it has many practical uses. The most common alternative is using three different columns, which is less logical, more error-prone, and less efficient. Additional virtual columns can be created to build appropriate indexes, if needed. But this is usually unnecessary. You should be able to use a single date as a single column, even if some or all its components are set to zero.</p>
<p class="wp-block-paragraph">If you want to know more about MariaDB specific features for Developers or for Database Administrators, consider Vettabase <a href="https://vettabase.com/services/database-training/mariadb-training/" data-type="page" data-id="42">training courses</a>.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/mariadb-underrated-features-zero-dates-and-partial-dates/">MariaDB Underrated Features: Zero Dates and Partial Dates</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Navigating Tree and Graph Data with Recursive SQL</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/navigating-tree-and-graph-data-with-recursive-sql/" />
      <id>https://vettabase.com/navigating-tree-and-graph-data-with-recursive-sql/</id>
      <updated>2025-12-27T09:24:39+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>Hierarchical and networked data appears everywhere in modern databases: organisational charts, product category trees, dependency graphs, and even transport networks. Applications need to retrieve this data to draw a chart, find out whom a certain employee reports to, or find the routes that connect two train stops. Storing and querying this kind of data in a relational database is not trivial. If it’s modelled poorly, you might easily end up running a query for each node: an example of the infamous N+1 issue that doesn’t scale, and usually represents a performance bottleneck. This article shows you how to design tables to store tree or graph data, and how to navigate these structures efficiently with a single SQL query. We’ll use example written for MariaDB, but the SELECT queries work equally well with PostgreSQL. Querying Trees A tree is a data structure where nodes have exactly one parent node, except root nodes, which have none. A node can have any number of children. Here’s an example from the Encyclopédie, ou dictionnaire raisonné des sciences, des arts et des métiers (1752), source: Wikipedia. Variants There are variants. For example, you might require that a node has exactly 0 or 2 children. You […]</p>
<p><a href="https://vettabase.com/navigating-tree-and-graph-data-with-recursive-sql/">Navigating Tree and Graph Data with Recursive SQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">Hierarchical and networked data appears everywhere in modern databases: organisational charts, product category trees, dependency graphs, and even transport networks. Applications need to retrieve this data to draw a chart, find out whom a certain employee reports to, or find the routes that connect two train stops.</p>
<p class="wp-block-paragraph">Storing and querying this kind of data in a relational database is not trivial. If it&rsquo;s modelled poorly, you might easily end up running a query for each node: an example of the <a href="https://stackoverflow.com/a/97253/9445059" rel="noopener">infamous N+1 issue</a> that doesn&rsquo;t scale, and usually represents a performance bottleneck. This article shows you how to design tables to store tree or graph data, and how to navigate these structures efficiently with a single SQL query.</p>
<p class="wp-block-paragraph">We&rsquo;ll use example written for MariaDB, but the <code>SELECT</code> queries work equally well with PostgreSQL.</p>
<h2 class="wp-block-heading">Querying Trees<a class="anchor-link" id="querying-trees"></a></h2>
<p class="wp-block-paragraph">A tree is a data structure where nodes have exactly one <strong>parent</strong> node, except root nodes, which have none. A node can have any number of <strong>children</strong>. Here&rsquo;s an example from the Encyclop&eacute;die, ou dictionnaire raisonn&eacute; des sciences, des arts et des m&eacute;tiers (1752), source: Wikipedia.</p>
<figure class="wp-block-image aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="584" height="797" src="https://vettabase.com/wp-content/uploads/2025/12/Screenshot-2025-12-21-10.48.50-PM.png" alt="" class="wp-image-383537"></figure>
<p class="wp-block-paragraph"><strong>Variants</strong></p>
<p class="wp-block-paragraph">There are variants. For example, you might require that a node has exactly 0 or 2 children. You can still use the above table, but you&rsquo;ll have to enforce this constraint on the application level, which requires additional queries. Or you can enforce it with stored procedures to add 2 children and delete 2 children, and make sure that the application user doesn&rsquo;t have permissions to directly write to the table. But, in this case, it might be easier to invert the relationship&rsquo;s direction: you can remove <code>parent_id</code>, and add <code>child1</code> and <code>child2</code> columns.</p>
<p class="wp-block-paragraph">We can&rsquo;t cover all variants of a tree in this article. By reading the rest of the text you&rsquo;ll find out the principles, and you&rsquo;ll have to figure out by yourself how to apply them to any variant you might need to implement.</p>
<h3 class="wp-block-heading">Tree Tables<a class="anchor-link" id="tree-tables"></a></h3>
<p class="wp-block-paragraph">Let&rsquo;s see how to represent a tree in a database with an example. The following table represents product categories, where a category can contain other categories. You can create the following table with MariaDB:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE TABLE category (
    id INT UNSIGNED AUTO_INCREMENT,
    parent_id INT UNSIGNED,
    name VARCHAR(100) NOT NULL
        CHECK (name &gt; ''),
    PRIMARY KEY (id),
    UNIQUE unq_id_parent_id (id, parent_id),
    FOREIGN KEY fk_parent_id_to_id (parent_id)
        REFERENCES category (id)
        ON DELETE CASCADE
        ON UPDATE RESTRICT,
    UNIQUE unq_name (name)
);</code></pre>
<p class="wp-block-paragraph">The key parts are:</p>
<ul class="wp-block-list">
<li><code>id</code>: Normally I recommend to use <a href="https://vettabase.com/the-uuid-data-type-in-mariadb/" data-type="post" data-id="290242">the <code>UUID</code> data type</a> for primary keys, but we&rsquo;re going to use <code>INT</code> to make the results more reasable.</li>
<li><code>parent_id</code>: For root nodes it&rsquo;s <code>NULL</code>, for other nodes it points to <code>id</code>. This allows self-joins or, in simple words, connecting a row from this table to another row in the same table.</li>
<li>A foreign key called <code>fk_parent_id_to_id</code> officialises this relationship.</li>
</ul>
<h3 class="wp-block-heading">Step-By-Step Operations<a class="anchor-link" id="step-by-step-operations"></a></h3>
<p class="wp-block-paragraph">Let&rsquo;s start easy. The following are simple queries that can be used to navigate the tree step by step, using the <code>id</code> and <code>parent_id</code> fields. Let&rsquo;s assume that we are navigating statrting from the row with <code>id=11</code>.</p>
<p class="wp-block-paragraph"><strong>Find the immediate parent</strong></p>
<pre class="wp-block-code"><code>SELECT id, parent_id, name FROM category WHERE id =
    (SELECT parent_id FROM category WHERE id = 11);</code></pre>
<p class="wp-block-paragraph"><strong>Find the immediate children</strong></p>
<pre class="wp-block-code"><code>SELECT id, parent_id, name FROM category WHERE parent_id = 11;</code></pre>
<p class="wp-block-paragraph"><strong>Find all siblings</strong></p>
<pre class="wp-block-code"><code>SELECT id, parent_id, name FROM category WHERE parent_id =
    (SELECT parent_id FROM category WHERE id = 11);</code></pre>
<p class="wp-block-paragraph"><strong>Find next sibling</strong></p>
<pre class="wp-block-code"><code>SELECT id, parent_id, name FROM category
    WHERE
        parent_id =
            (SELECT parent_id FROM category WHERE id = 11)
        AND id &gt; 11
    ORDER BY id
    LIMIT 1
;</code></pre>
<p class="wp-block-paragraph"><strong>Find previous sibling</strong></p>
<pre class="wp-block-code"><code>SELECT id, parent_id, name FROM category
    WHERE
        parent_id =
            (SELECT parent_id FROM category WHERE id = 11)
        AND id &lt; 11
    ORDER BY id DESC
    LIMIT 1
;</code></pre>
<h3 class="wp-block-heading">Obtaining a Tree From a Query<a class="anchor-link" id="obtaining-a-tree-from-a-query"></a></h3>
<p class="wp-block-paragraph">The following query returns trees starting from root nodes:</p>
<pre class="wp-block-code"><code>WITH RECURSIVE category_tree AS (
    SELECT
            id, parent_id, name,
            id AS root_id, 1 AS level, TRUE AS is_root
        FROM category
        WHERE parent_id IS NULL
    UNION ALL
    SELECT
            c.id, c.parent_id, c.name,
            ct.root_id, ct.level + 1, FALSE AS is_root
        FROM category c
        INNER JOIN category_tree ct
            ON c.parent_id = ct.id
)
SELECT id, parent_id, root_id, level, is_root, name
    FROM category_tree
    ORDER BY root_id, level, id
;
+------+-----------+---------+-------+---------+------------------+
| id   | parent_id | root_id | level | is_root | name             |
+------+-----------+---------+-------+---------+------------------+
|    1 |      NULL |       1 |     1 |       1 | Home &amp; Garden    |
|   11 |         1 |       1 |     2 |       0 | Kitchen          |
|   12 |         1 |       1 |     2 |       0 | Bedroom          |
|  111 |        11 |       1 |     3 |       0 | Ovens            |
|  112 |        11 |       1 |     3 |       0 | Cookers          |
|  113 |        11 |       1 |     3 |       0 | Fridges          |
|  121 |        12 |       1 |     3 |       0 | Beds             |
|  122 |        12 |       1 |     3 |       0 | Wardrobes        |
|  123 |        12 |       1 |     3 |       0 | Night Tables     |
|    2 |      NULL |       2 |     1 |       1 | Electronics      |
|   21 |         2 |       2 |     2 |       0 | Computers        |
|   22 |         2 |       2 |     2 |       0 | Audio &amp; Video    |
|  211 |        21 |       2 |     3 |       0 | Laptops          |
|  212 |        21 |       2 |     3 |       0 | Gaming Computers |
|  221 |        22 |       2 |     3 |       0 | TV               |
|  222 |        22 |       2 |     3 |       0 | Wi-Fi            |
+------+-----------+---------+-------+---------+------------------+</code></pre>
<p class="wp-block-paragraph">Le&rsquo;ts analyse this query.</p>
<p class="wp-block-paragraph">We have a recursive <strong>Common Table Expression (CTE)</strong> called <code>category_tree</code>.</p>
<p class="wp-block-paragraph">The CTE starts with an <strong>anchor part</strong>, which obtains the root nodes and runs only once:</p>
<pre class="wp-block-code"><code>SELECT
        id, parent_id, name,
        id AS root_id, 1 AS level, TRUE AS is_root
    FROM category
    WHERE parent_id IS NULL</code></pre>
<p class="wp-block-paragraph">Then we have an <code>INNER JOIN</code> that recursively joins the CTE&rsquo;s results with the <code>category</code> table:</p>
<pre class="wp-block-code"><code>SELECT
        c.id, c.parent_id, c.name,
        ct.root_id, ct.level + 1, FALSE AS is_root
    FROM category c
    INNER JOIN category_tree ct
        ON c.parent_id = ct.id</code></pre>
<p class="wp-block-paragraph">The first time, it will only joins the root nodes (the anchor part&rsquo;s results) with their immediate children. Then, it will join these two levels with their immediate childre. And so on.</p>
<p class="wp-block-paragraph">The anchor part and the recursive part are merged using <code>UNION ALL</code>. The reason is that there can&rsquo;t be any duplicate rows, so there is no need to do any additional work to remove them.</p>
<p class="wp-block-paragraph">Finally, we have an <strong>outer query</strong> that orders and returns all the results from the CTE&rsquo;s last execution.</p>
<p class="wp-block-paragraph">Note that we have some additional columns. They aren&rsquo;t required, but you might find the, useful in some cases. These columns are:</p>
<ul class="wp-block-list">
<li><code>root_id</code> is the root node&rsquo;s id. It&rsquo;s useful if the root level is particularly important for you. <code>root_id</code> is assigned in the anchor, and is copied as-is in the next levels.</li>
<li><code>level</code> is the row&rsquo;s tree level. It starts as 1 in the anchor, and is incremented at every CTE execution.</li>
<li><code>is_root</code> isn&rsquo;t necessary in this case, because you might check whether <code>parent_id</code> is <code>NULL</code>. But a root node returned by your query might be any node at any tree level. For example, you might arbitrarily decide that it&rsquo;s the node with <code>id=12</code>. In this case, <code>is_root</code> might be useful. t&rsquo;s assigned to <code>TRUE</code> in the anchor and to <code>FALSE</code> for the next level.</li>
</ul>
<h2 class="wp-block-heading">Querying Graphs<a class="anchor-link" id="querying-graphs"></a></h2>
<p class="wp-block-paragraph">Graphs can be considered similar to trees. If you see it in this way, the main difference is that nodes can have more than one parent.</p>
<p class="wp-block-paragraph">But actually, in most graphs it doesn&rsquo;t make sense to distinguish between parents and children, or talk about siblings. There are only peer <strong>nodes</strong>, and links between them called <strong>arcs</strong>. These links have a direction.</p>
<p class="wp-block-paragraph">In the following examples, however, we&rsquo;ll use a graph that is actually a small variation of the category tree we used earlier. Is a graph, and no a tree, because a category can have multiple parents: trees don&rsquo;t allow this.</p>
<p class="wp-block-paragraph">A graph example is a public transport map:</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="719" src="https://vettabase.com/wp-content/uploads/2025/12/Screenshot-2025-12-22-3.51.32-AM-1024x719.png" alt="" class="wp-image-383726"></figure>
<p class="wp-block-paragraph"><strong>Variations</strong></p>
<p class="wp-block-paragraph">An arc may carry additional metadata, such as creation and deletion timestamps, tags, or the reason why the arc exists.</p>
<p class="wp-block-paragraph">Additionally, arcs might have a weight. If the graph represents a transport map, the weight is probably the dinstance that separates the nodes linked by an arc. This might be taken into account when we need to find the shortest path between two nodes. But this is beyond the scope of this article. If you need to use weights in this way, I recommend using the <a href="https://mariadb.com/docs/server/server-usage/storage-engines/oqgraph-storage-engine" rel="noopener">MariaDB OQGRAPH engine</a>. If there is interest, it might be a good topic for a future article.</p>
<h3 class="wp-block-heading">Graph Tables<a class="anchor-link" id="graph-tables"></a></h3>
<p class="wp-block-paragraph">A graph table is a <em>many to many relationship</em> between a table and itself. Let&rsquo;s see a practical example:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE TABLE category (
    id INT UNSIGNED AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL
        CHECK (name &gt; ''),
    PRIMARY KEY (id),
    INDEX idx_name (name)
);

CREATE OR REPLACE TABLE category_arc (
    id INT UNSIGNED AUTO_INCREMENT,
    from_category INT UNSIGNED NOT NULL,
    to_category INT UNSIGNED NOT NULL,
    PRIMARY KEY (id),
    UNIQUE unq_from_category_to_category (from_category, to_category),
    FOREIGN KEY fk_from_category_to_id (from_category)
        REFERENCES category (id)
        ON DELETE CASCADE
        ON UPDATE RESTRICT,
    FOREIGN KEY fk_to_category_to_id (to_category)
        REFERENCES category (id)
        ON DELETE CASCADE
        ON UPDATE RESTRICT
);</code></pre>
<p class="wp-block-paragraph">We modified the <code>category</code> table by removing the <code>parent_id</code> column.</p>
<p class="wp-block-paragraph">The many to many relationship is represented by <code>category_arc</code>. As anticipated, this relationship has a direction: <code>from_category</code> points to a parent category, and <code>to_category</code> points to a child category. This design allows us to have multiple children for the same parent, and multiple parents for the same child.</p>
<h3 class="wp-block-heading">Step-By-Step Operations<a class="anchor-link" id="step-by-step-operations"></a></h3>
<p class="wp-block-paragraph">Since arcs have a direction, we&rsquo;ll need to run queries that keep into account this direction. We&rsquo;ll assume that we&rsquo;re statrting from the row with <code>id=21</code>.</p>
<p class="wp-block-paragraph"><strong>Find current node&rsquo;s parents and children</strong></p>
<pre class="wp-block-code"><code>SELECT
        c.id, c.name,
        JSON_ARRAYAGG(DISTINCT c_parent.from_category ORDER BY 1) AS parents,
        JSON_ARRAYAGG(DISTINCT c_child.to_category ORDER BY 1) AS children
    FROM category c
    INNER JOIN category_arc c_parent
        ON c.id = c_parent.to_category
    INNER JOIN category_arc c_child
        ON c.id = c_child.from_category
    WHERE c.id = 21
    GROUP BY c.id, c.name
;</code></pre>
<p class="wp-block-paragraph"><strong>Find all adjacent nodes (no distinction between parents and children)</strong></p>
<pre class="wp-block-code"><code>SELECT
        c.id, c.name
    FROM (
        (SELECT from_category AS id FROM category_arc WHERE to_category = 21)
        UNION ALL
        (SELECT to_category AS id FROM category_arc WHERE from_category = 21)
    ) a
    INNER JOIN category c
        ON a.id = c.id
;</code></pre>
<h3 class="wp-block-heading">Finding All Reachable Nodes, Recursively<a class="anchor-link" id="finding-all-reachable-nodes-recursively"></a></h3>
<p class="wp-block-paragraph">The following query recursively finds all nodes that are directly or indirectly linked to the starting node. It treats parents and children in the same way.</p>
<pre class="wp-block-code"><code>WITH RECURSIVE category_network AS (
    (
        SELECT
                id, name,
                1 AS distance
            FROM category
            WHERE id = 21
    )
    UNION ALL
    (
        (
            SELECT
                    c.id, c.name,
                    cn.distance + 1 AS distance
                FROM category_network cn
                INNER JOIN category_arc ca 
                    ON cn.id = ca.from_category
                INNER JOIN category c 
                    ON c.id = ca.to_category
                WHERE c.id  cn.id
        ) UNION ALL (
            SELECT
                    c.id, c.name,
                    cn.distance + 1 AS distance
                FROM category_network cn
                INNER JOIN category_arc ca 
                    ON cn.id = ca.to_category
                INNER JOIN category c 
                    ON c.id = ca.from_category
                WHERE c.id  cn.id
        )
    )
) CYCLE id RESTRICT
SELECT DISTINCT
        id, name,
        distance
    FROM category_network
    WHERE id  21
    ORDER BY id
;</code></pre>
<p class="wp-block-paragraph">Again, we start with a <strong>CTE</strong> called <code>category_network</code>, and the first part is the <strong>anchor</strong>. It selects the <strong>starting node</strong>, which doesn&rsquo;t have to be an edge node.</p>
<p class="wp-block-paragraph">The anchor is the first part of a <code>UNION</code>. Second part is another <code>UNION</code>. This is the case because, with our table design, parents and children are linked using different columns: <code>from_category</code> and <code>to_category</code>, but our query shouldn&rsquo;t distinguish between parents and children.</p>
<p class="wp-block-paragraph">Another way to do this would be to write <code>ON</code> clauses with an <code>OR</code> logical operator or an <code>IN</code> comparison operator:</p>
<pre class="wp-block-code"><code>ON t1.id = t2.from_category OR t1.id = t2.to_category
ON t1.id IN (t2.from_category, t2.to_category)</code></pre>
<p class="wp-block-paragraph">But this wouldn&rsquo;t be index-friendly. So we prefer to read parents and children in the two branches of a <code>UNION</code>, which should use indexes properly. If the rows returned by the <code>UNION</code> are not too many, this query will be fast.</p>
<p class="wp-block-paragraph">Note that all <code>UNION</code>s are of type <code>UNION ALL</code>. This is because they can&rsquo;t possibly return duplicate rows, so we don&rsquo;t need the database to do extra work to remove duplicates.</p>
<p class="wp-block-paragraph">Then we have <strong>cycle detection</strong> syntax: <code>CYCLE id RESTRICT</code>. We have to do this because, in a graph, two nodes can be connected by more than one path. In this case, we&rsquo;ll have an infinite loop. To avoid this, we need to use the <code>CYCLE ... RESTRICT</code> clause. This query was tested on MariaDB. PostgreSQL requires a slightly different syntax for cycle detection, the the logic is exactly the same.</p>
<p class="wp-block-paragraph">Finally, we have an <strong>outer query</strong> that excludes the starting node (you might want to do so or not, depending on your application logic) and sorts the rows by <code>id</code>.</p>
<p class="wp-block-paragraph">In this query, I add an additional columns: <code>distance</code>. You might need it or not, The logic is the same that I used for the <code>level</code> column of the tree query.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">We discussed how to store tree and graph data structures in a relational database. We discussed how to run simple next-node queries, and how to run queries that return all the nodes reachable form the starting point. For graph examples, we actually used a variant of the tree table design, which is still a logical tree but allows a node to have multiple parents. In a training, I wouldn&rsquo;t do such a thing. But if you managed to follow my explanation, you learnt how to handle graphs the harder way, and shouldn&rsquo;t have any problems working with a proper graph, which doesn&rsquo;t have a distinction between parents and children.</p>
<p class="wp-block-paragraph">We used MariaDB for the examples. Almost all the queries will work on PostgreSQL, too. But, as mentioned, MariaDB offers another interesting feature that I didn&rsquo;t discuss in this article: the OQGRAPH storage engine. This engine allows us to work with weighted graphs and easily find the shortest path between two nodes. If there is interest, I can illustrate OQGRAPH in a dedicate article.</p>
<p class="wp-block-paragraph">If you want to know more about trees, graphs, CTEs, or other advanced aspects of SQL, consider our <a href="https://vettabase.com/contact/">SQL training courses</a>.</p>
<p class="wp-block-paragraph">Federico Razzoli</p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/navigating-tree-and-graph-data-with-recursive-sql/">Navigating Tree and Graph Data with Recursive SQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Open source, PostgreSQL, and risk mitigation in an era of acquisitions</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/12/19/open-source-postgresql-and-risk-mitigation-in-an-era-of-acquisitions/" />
      <id>https://percona.community/blog/2025/12/19/open-source-postgresql-and-risk-mitigation-in-an-era-of-acquisitions/</id>
      <updated>2025-12-19T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>As the year comes to a close and many of us start slowing down before the winter holidays, I find myself reflecting on patterns I’ve seen repeat, both as a customer and as someone working closely with the PostgreSQL ecosystem.</p>
<p><a href="https://percona.community/blog/2025/12/19/open-source-postgresql-and-risk-mitigation-in-an-era-of-acquisitions/">Open source, PostgreSQL, and risk mitigation in an era of acquisitions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>As the year comes to a close and many of us start slowing down before the winter holidays, I find myself reflecting on patterns I&rsquo;ve seen repeat, both as a customer and as someone working closely with the PostgreSQL ecosystem.</p>
<p>Looking back at software acquisitions over the past year, one might assume they only change logos. In reality, they often change roadmaps, priorities, and unfortunately all too often also the promises customers originally bought into.</p>
<p>I have experienced this more than once as a customer. Most recently, my team evaluated a product management tool that, shortly after being acquired, informed us we had a single month to migrate away. While I personally had not invested much time yet, colleagues had already moved documentation and workflows only to see that work become wasted effort. For those not familiar with product management, migrating all the product planning documentation is a huge effort and having it wasted is like erasing a month of your life.</p>
<p>One might say &ldquo;business is business&rdquo; and that customers can always take their money elsewhere. That may be true, but it does little to make customers feel safe or protected. In practice, it often leaves them confused about where accountability lies. Who should we blame when the original vendor no longer exists and the acquiring company is acting in its own strategic interest?</p>
<p>What struck me this year is how often these experiences echoed each other: across different tools, teams, and ecosystems. Unfortunately, I am now seeing similar patterns emerge in my professional focus area: PostgreSQL.</p>
<p><figure><img decoding="async" width="2048" height="2048" src="https://percona.community/blog/2025/12/Jan-Hippo-PG_hu_c29670fb6d2d2b95.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>To be very clear, the PostgreSQL Community itself is driving an outstanding database, one that has been, and I am confident will remain, truly open source and community-developed. PostgreSQL as a project is healthy, stable, and thriving, and I hope it will thrive even more in the coming year.</p>
<p>That said, PostgreSQL is not just the database engine itself, but an entire ecosystem of vendors, extensions, and services built around it.</p>
<p>Over the past year, several important companies in the PostgreSQL ecosystem have been acquired and in conversations throughout the year this topic kept resurfacing. In recent prospective customer conversations, we increasingly hear from PostgreSQL users running mission-critical workloads on-premises who feel abandoned by their vendor. These organizations are being quietly nudged toward &ldquo;modernization&rdquo; paths that, in practice, resemble mandatory SaaS migration.</p>
<p>Having worked closely with MongoDB users for years, this pattern feels familiar. Since MongoDB Atlas became the primary strategic focus, many customers experienced similar pressure. What feels different, and what stood out to me most this year, in the PostgreSQL world is timing.</p>
<p>Teams often discover late in the renewal cycle that:</p>
<ul>
<li>Their on-prem PostgreSQL deployment is no longer strategic for the vendor</li>
<li>Key components they depend on, which were never fully open source, are no longer available for renewal</li>
<li>The implied options are to &ldquo;move to the cloud&rdquo; or rapidly find an alternative, even when migration planning is complicated by lack of source availability</li>
</ul>
<p>This puts PostgreSQL customers in a difficult position:<br>
accept architectural change under pressure, or scramble to replace a trusted vendor while the clock is already ticking.</p>
<p>While recent conversations often reference Crunchy Data following its acquisition by Snowflake, this is not about a single company. The broader pattern has repeated across the industry, including infrastructure significant projects involving MinIO, Bitnami, HashiCorp, and Redis.</p>
<p>This raises a fundamental question for the PostgreSQL ecosystem and infrastructure software in general:</p>
<blockquote>
<p>When a critical infrastructure vendor is acquired or changes licensing, who advocates for the customers that cannot move?</p>
</blockquote>
<p>Open source is not just a licensing model or philosophical ideal. It provides freedom of choice, deployment flexibility, and risk mitigation. Recent community responses such as OpenTofu, OpenBao, and Valkey demonstrate a growing maturity and ability of open source communities to organize when freedoms erode.</p>
<p>It is disappointing to see these dynamics emerge around PostgreSQL, even though the database itself remains one of the most open source and community governed projects in the industry.</p>
<p>It is disappointing to see similar dynamics affect PostgreSQL, a database often considered one of the most open source driven projects in the industry. When PostgreSQL derivatives are impacted, the shadow inevitably reaches upstream as well.</p>
<p>If this post reaches teams who were not yet aware of these shifts and gives them more time to plan going into the new year, it serves its purpose. If you are running PostgreSQL in environments where SaaS is not an option, now is the time to ask difficult questions before renewal conversations start, not after options disappear.</p>
<p>As we head into a new year, I expect these conversations to become even more common. Acquisitions, licensing changes and cloud first strategies are not slowing down, but neither is the need for predictability, transparency and choice in how PostgreSQL is deployed and supported.</p>
<p><figure><img decoding="async" width="2058" height="2048" src="https://percona.community/blog/2025/12/Jan-2026_hu_96fc147aa9841603.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>Looking ahead to 2026, my hope is simple: more open source products, more stable and predictable service offerings around them, and fewer last-minute surprises for the teams who depend on them every day.</p>
<h1>We Want to Hear from You<a class="anchor-link" id="we-want-to-hear-from-you"></a></h1>
<p>I am curious:</p>
<ul>
<li>Have you seen similar shifts in PostgreSQL vendors post-acquisition?</li>
<li>What is preventing you from moving to PostgreSQL community builds or alternative support models?</li>
</ul>
<p>Let&rsquo;s talk.<br>
You can reach me via:</p>
<ul>
<li>LinkedIn: <a href="https://www.linkedin.com/in/janwie/" target="_blank" rel="noopener noreferrer">https://www.linkedin.com/in/janwie/</a></li>
<li>Email: jan(dot)wieremjewicz(at)percona(dot)com</li>
</ul>
<h1>Open Source isn&rsquo;t a strategy, it&rsquo;s who we are!<a class="anchor-link" id="open-source-isnt-a-strategy-its-who-we-are"></a></h1>
<p>We are always open to your feedback. You can reach us at:</p>
<ul>
<li>Percona Community Forums&#8232;<a href="https://forums.percona.com/c/postgresql/25" target="_blank" rel="noopener noreferrer">https://forums.percona.com/c/postgresql/25</a></li>
<li>Via issues and discussions on <a href="https://github.com/percona/" target="_blank" rel="noopener noreferrer">Percona GitHub repositories</a></li>
</ul>
<p>If there is an event we attend, focused on open source or not, don&rsquo;t be a stranger, come chat with us in person!</p>

<p><a href="https://percona.community/blog/2025/12/19/open-source-postgresql-and-risk-mitigation-in-an-era-of-acquisitions/">Open source, PostgreSQL, and risk mitigation in an era of acquisitions</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Backtesting trailing stop-loss strategies with Python and market data</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/" />
      <id>https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/</id>
      <updated>2025-12-19T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>In January 2024 I wrote about the insanity of the Magnificent Seven dominating the MSCI World Index, and I wondered how long the number can continue to go up? It has continued to surge upward at an accelerating pace, which makes me worry that a crash is likely closer. As a software professional, I decided to analyze whether using stop-loss orders could reliably automate avoiding deep drawdowns.<br />
As everyone with some savings in the stock market (hopefully) knows, the stock market eventually experiences crashes. It is just a matter of when and how deep the crash will be. Staying on the sidelines for years is not a good investment strategy, as inflation will erode the value of your savings. Assuming the current true inflation rate is around 7%, a restaurant dinner that costs 20 euros today will cost 24.50 euros in three years. Savings of 1000 euros today would drop in purchasing power from 50 dinners to only 40 dinners in three years.<br />
Hence, if you intend to retain the value of your hard-earned savings, they need to be invested in something that grows in value. Most people try to beat inflation by buying shares in stable companies, directly or via broad market ETFs. These historically grow faster than inflation during normal years, but likely drop in value during recessions.<br />
What is a trailing stop-loss order?<br />
What if you could buy stocks to benefit from their value increasing without having to worry about a potential crash? All modern online stock brokers have a feature called stop-loss, where you can enter a price at which your stocks automatically get sold if they drop down to that price. A trailing stop-loss order is similar, but instead of a fixed price, you enter a margin (e.g. 10%). If the stock price rises, the stop-loss price will trail upwards by that margin.<br />
For example, if you buy a share at 100 euros and it has risen to 110 euros, you can set a 10% trailing stop-loss order which automatically sells it if the price drops 10% from the peak of 110 euros, at 99 euros. Thus, no matter what happens, you only lost 1 euro. And if the stock price continues to rise to 150 euros, the trailing stop-loss would automatically readjust to 150 euros minus 10%, which is 135 euros (150-15=135). If the price dropped to 135 euros, you would lock in a gain of 35 euros, which is not the peak price of 150 euros, but still better than whatever the price fell down to as a result of a large crash.<br />
In the simple case above, it obviously makes sense in theory, but it might not make sense in practice. Prices constantly oscillate, so you don’t want a margin that is too small, otherwise you exit too early. Conversely, having a large margin may result in too large a drawdown before exiting. If markets crash rapidly, it might be that nobody buys your stocks at the stop-loss price, and shares have to be sold at an even lower price. Also, what will you do once the position is sold? The reason you invested in the stock market was to avoid holding cash, so would you buy the same stock back when the crash bottoms? But how will you know when the bottom has been reached?<br />
Backtesting stock market strategies with Python, YFinance, Pandas and Lightweight Charts<br />
I am not a professional investor, and nobody should take investment advice from me. However, I know what backtesting is and how to leverage open source software. So, I wrote a Python script to test if the trading strategy of using trailing stop-loss orders with specific margin values would have worked for a particular stock.<br />
First you need to have data. YFinance is a handy Python library that can be used to download the historic price data for any stock ticker on Yahoo.com. Then you need to manipulate the data. Pandas is the Python data analysis library with advanced data structures for working with relational or labeled data. Finally, to visualize the results, I used Lightweight Charts, which is a fast, interactive library for rendering financial charts, allowing you to plot the stock price, the trailing stop-loss line, and the points where trades would have occurred. I really like how the zoom is implemented in Lightweight Charts, which makes drilling into the data points feel effortless.<br />
The full solution is not polished enough to be published for others to use, but you can piece together your own by reusing some of the key snippets. To avoid re-downloading the same data repeatedly, I implemented a small caching wrapper that saves the data locally (as Parquet files):</p>
<p>python</p>
<p>Copy</p>
<p>CACHE_DIR.mkdir(parents=True, exist_ok=True)<br />
end_date = datetime.today().strftime(\"%Y-%m-%d\")<br />
cache_file = CACHE_DIR / f\"{TICKER}-{START_DATE}--{end_date}.parquet\"<br />
if cache_file.is_file():<br />
dataframe = pandas.read_parquet(cache_file)<br />
print(f\"Loaded price data from cache: {cache_file}\")<br />
else:<br />
dataframe = yfinance.download(<br />
TICKER,<br />
start=START_DATE,<br />
end=end_date,<br />
progress=False,<br />
auto_adjust=False<br />
)<br />
dataframe.to_parquet(cache_file)<br />
print(f\"Fetched new price data from Yahoo Finance and cached to: {cache_file}\")CACHE_DIR.mkdir(parents=True, exist_ok=True)<br />
end_date = datetime.today().strftime(\"%Y-%m-%d\")<br />
cache_file = CACHE_DIR / f\"{TICKER}-{START_DATE}--{end_date}.parquet\"</p>
<p>if cache_file.is_file():<br />
 dataframe = pandas.read_parquet(cache_file)<br />
 print(f\"Loaded price data from cache: {cache_file}\")<br />
else:<br />
 dataframe = yfinance.download(<br />
 TICKER,<br />
 start=START_DATE,<br />
 end=end_date,<br />
 progress=False,<br />
 auto_adjust=False<br />
 )</p>
<p> dataframe.to_parquet(cache_file)<br />
 print(f\"Fetched new price data from Yahoo Finance and cached to: {cache_file}\")<br />
The dataframe is a Pandas object with a powerful API. For example, to print a snippet from the beginning and the end of the dataframe to see what the data looks like, you can use:</p>
<p>python</p>
<p>Copy</p>
<p>print(\"First 5 rows of the raw data:\")<br />
print(df.head())<br />
print(\"Last 5 rows of the raw data:\")<br />
print(df.tail())print(\"First 5 rows of the raw data:\")<br />
print(df.head())<br />
print(\"Last 5 rows of the raw data:\")<br />
print(df.tail())<br />
Example output:</p>
<p>Copy</p>
<p>First 5 rows of the raw data<br />
Price Adj Close Close High Low Open Volume<br />
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br />
Date<br />
2014-01-02 29.956285 55.540001 56.910000 55.349998 56.700001 316552<br />
2014-01-03 30.031801 55.680000 55.990002 55.290001 55.580002 210044<br />
2014-01-06 30.080338 55.770000 56.230000 55.529999 55.560001 185142<br />
2014-01-07 30.943321 57.369999 57.619999 55.790001 55.880001 370397<br />
2014-01-08 31.385597 58.189999 59.209999 57.750000 57.790001 489940<br />
Last 5 rows of the raw data<br />
Price Adj Close Close High Low Open Volume<br />
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br />
Date<br />
2025-12-11 78.669998 78.669998 78.919998 76.900002 76.919998 357918<br />
2025-12-12 78.089996 78.089996 80.269997 78.089996 79.470001 280477<br />
2025-12-15 79.080002 79.080002 79.449997 78.559998 78.559998 233852<br />
2025-12-16 78.860001 78.860001 79.980003 78.809998 79.430000 283057<br />
2025-12-17 80.080002 80.080002 80.150002 79.080002 79.199997 262818First 5 rows of the raw data<br />
Price Adj Close Close High Low Open Volume<br />
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br />
Date<br />
2014-01-02 29.956285 55.540001 56.910000 55.349998 56.700001 316552<br />
2014-01-03 30.031801 55.680000 55.990002 55.290001 55.580002 210044<br />
2014-01-06 30.080338 55.770000 56.230000 55.529999 55.560001 185142<br />
2014-01-07 30.943321 57.369999 57.619999 55.790001 55.880001 370397<br />
2014-01-08 31.385597 58.189999 59.209999 57.750000 57.790001 489940<br />
Last 5 rows of the raw data<br />
Price Adj Close Close High Low Open Volume<br />
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br />
Date<br />
2025-12-11 78.669998 78.669998 78.919998 76.900002 76.919998 357918<br />
2025-12-12 78.089996 78.089996 80.269997 78.089996 79.470001 280477<br />
2025-12-15 79.080002 79.080002 79.449997 78.559998 78.559998 233852<br />
2025-12-16 78.860001 78.860001 79.980003 78.809998 79.430000 283057<br />
2025-12-17 80.080002 80.080002 80.150002 79.080002 79.199997 262818<br />
Adding new columns to the dataframe is easy. For example, I used a custom function to calculate the Relative Strength Index (RSI). To add a new column “RSI” with a value for every row based on the price from that row, only one line of code is needed, without custom loops:</p>
<p>python</p>
<p>Copy</p>
<p>df[\"RSI\"] = compute_rsi(df[\"price\"], period=14)df[\"RSI\"] = compute_rsi(df[\"price\"], period=14)<br />
After manipulating the data, the series can be converted into an array structure and printed as JSON into a placeholder in an HTML template:</p>
<p>python</p>
<p>Copy</p>
<p> baseline_series = [<br />
{\"time\": ts, \"value\": val}<br />
for ts, val in df_plot[[\"timestamp\", BASELINE_LABEL]].itertuples(index=False)<br />
]<br />
baseline_json = json.dumps(baseline_series)<br />
template = jinja2.Template(\"template.html\")<br />
rendered_html = template.render(<br />
title=title,<br />
heading=heading,<br />
description=description_html,<br />
...<br />
baseline_json=baseline_json,<br />
...<br />
)<br />
with open(\"report.html\", \"w\", encoding=\"utf-8\") as f:<br />
f.write(rendered_html)<br />
print(\"Report generated!\") baseline_series = [<br />
 {\"time\": ts, \"value\": val}<br />
 for ts, val in df_plot[[\"timestamp\", BASELINE_LABEL]].itertuples(index=False)<br />
 ]</p>
<p> baseline_json = json.dumps(baseline_series)<br />
 template = jinja2.Template(\"template.html\")<br />
 rendered_html = template.render(<br />
 title=title,<br />
 heading=heading,<br />
 description=description_html,<br />
 ...<br />
 baseline_json=baseline_json,<br />
 ...<br />
 )</p>
<p> with open(\"report.html\", \"w\", encoding=\"utf-8\") as f:<br />
 f.write(rendered_html)<br />
 print(\"Report generated!\")<br />
In the HTML template, the marker {{ variable }} in Jinja syntax gets replaced with the actual JSON:</p>
<p>html</p>
<p>Copy</p>
<p>{{ title }}<br />
...</p>
<p>{{ heading }}</p>
<p>// Ensure the DOM is ready before we initialise the chart<br />
document.addEventListener(\'DOMContentLoaded\', () = &#62; {<br />
// Parse the JSON data passed from Python<br />
const baselineData = {{ baseline_json &#124; safe }};<br />
const strategyData = {{ strategy_json &#124; safe }};<br />
const markersData = {{ markers_json &#124; safe }};<br />
// Create the chart<br />
const chart = LightweightCharts.createChart(document.getElementById(\'chart\'), {<br />
width: document.getElementById(\'chart\').clientWidth,<br />
height: 500,<br />
layout: {<br />
background: { color: \"#222\" },<br />
textColor: \"#ccc\"<br />
},<br />
grid: {<br />
vertLines: { color: \"#555\" },<br />
horzLines: { color: \"#555\" }<br />
}<br />
});<br />
// Add baseline series<br />
const baselineSeries = chart.addLineSeries({<br />
title: \'{{ baseline_label }}\',<br />
lastValueVisible: false,<br />
priceLineVisible: false,<br />
priceLineWidth: 1<br />
});<br />
baselineSeries.setData(baselineData);<br />
baselineSeries.priceScale().applyOptions({<br />
entireTextOnly: true<br />
});<br />
// Add strategy series<br />
const strategySeries = chart.addLineSeries({<br />
title: \'{{ strategy_label }}\',<br />
lastValueVisible: false,<br />
priceLineVisible: false,<br />
color: \'#FF6D00\'<br />
});<br />
strategySeries.setData(strategyData);<br />
// Add buy/sell markers to the strategy series<br />
strategySeries.setMarkers(markersData);<br />
// Fit the chart to show the full data range (full zoom)<br />
chart.timeScale().fitContent();<br />
})</p>
<p> {{ title }}<br />
 ...</p>
<p> {{ heading }}</p>
<p> // Ensure the DOM is ready before we initialise the chart<br />
 document.addEventListener(\'DOMContentLoaded\', () = &#62; {<br />
 // Parse the JSON data passed from Python<br />
 const baselineData = {{ baseline_json &#124; safe }};<br />
 const strategyData = {{ strategy_json &#124; safe }};<br />
 const markersData = {{ markers_json &#124; safe }};</p>
<p> // Create the chart<br />
 const chart = LightweightCharts.createChart(document.getElementById(\'chart\'), {<br />
 width: document.getElementById(\'chart\').clientWidth,<br />
 height: 500,<br />
 layout: {<br />
 background: { color: \"#222\" },<br />
 textColor: \"#ccc\"<br />
 },<br />
 grid: {<br />
 vertLines: { color: \"#555\" },<br />
 horzLines: { color: \"#555\" }<br />
 }<br />
 });</p>
<p> // Add baseline series<br />
 const baselineSeries = chart.addLineSeries({<br />
 title: \'{{ baseline_label }}\',<br />
 lastValueVisible: false,<br />
 priceLineVisible: false,<br />
 priceLineWidth: 1<br />
 });<br />
 baselineSeries.setData(baselineData);</p>
<p> baselineSeries.priceScale().applyOptions({<br />
 entireTextOnly: true<br />
 });</p>
<p> // Add strategy series<br />
 const strategySeries = chart.addLineSeries({<br />
 title: \'{{ strategy_label }}\',<br />
 lastValueVisible: false,<br />
 priceLineVisible: false,<br />
 color: \'#FF6D00\'<br />
 });<br />
 strategySeries.setData(strategyData);</p>
<p> // Add buy/sell markers to the strategy series<br />
 strategySeries.setMarkers(markersData);</p>
<p> // Fit the chart to show the full data range (full zoom)<br />
 chart.timeScale().fitContent();<br />
 })</p>
<p>There are also Python libraries built specifically for backtesting investment strategies, such as Backtrader and Zipline, but they do not seem to be actively maintained, and probably have too many features and complexity compared to what I needed for doing this simple test.<br />
The screenshot below shows an example of backtesting a strategy on the Waste Management Inc stock from January 2015 to December 2025. The baseline “Buy and hold” scenario is shown as the blue line and it fully tracks the stock price, while the orange line shows how the strategy would have performed, with markers for the sells and buys along the way.</p>
<p>Results<br />
I experimented with multiple strategies and tested them with various parameters, but I don’t think I found a strategy that was consistently and clearly better than just buy-and-hold.<br />
It basically boils down to the fact that I was not able to find any way to calculate when the crash has bottomed based on historical data. You can only know in hindsight that the price has stopped dropping and is on a steady path to recovery, but at that point it is already too late to buy in. In my testing, most strategies underperformed buy-and-hold because they sold when the crash started, but bought back after it recovered at a slightly higher price.<br />
In particular when using narrow margins and selling on a 3-6% drawdown the strategy performed very badly, as those small dips tend to recover in a few days. Essentially, the strategy was repeating the pattern of selling 100 stocks at a 6% discount, then being able to buy back only 94 shares the next day, then again selling 94 shares at a 6% discount, and only being able to buy back maybe 90 shares after recovery, and so forth, never catching up to the buy-and-hold.<br />
The strategy worked better in large market crashes as they tended to last longer, and there were higher chances of buying back the shares while the price was still low. For example, in the 2020 crash selling at a 20% drawdown was a good strategy, as the stock I tested dropped nearly 50% and remained low for several weeks; thus, the strategy bought back the stocks while the price was still low and had not yet started to climb significantly. But that was just a lucky incident, as the delta between the trailing stop-loss margin of 20% and total crash of 50% was large enough. If the crash had been only 25%, the strategy would have missed the rebound and ended up buying back the stocks at a slightly higher price.<br />
Also, note that the simulation assumes that the trade itself is too small to affect the price formation. We should keep in mind that in reality, if many people have stop-loss orders in place, a large price drop would trigger all of them, creating a flood of sell orders, which in turn would affect the price and drive it lower even faster and deeper. Luckily, it seems that stop-loss orders are generally not a good strategy, and we don’t need to fear that too many people will be using them.<br />
Conclusion<br />
Even though using a trailing stop-loss strategy does not seem to help in getting consistently higher returns based on my backtesting, I would still say it is useful in protecting from the downside of stock investing. It can act as a kind of “insurance policy” to considerably decrease the chances of losing big while increasing the chances of losing a little bit. If you are risk-averse, which I think I probably am, this tradeoff can make sense. I’d rather miss out on an initial 50% loss and an overall 3% gain on recovery than have to sit through weeks or months with a 50% loss before the price recovers to prior levels.<br />
Most notably, the trailing stop-loss strategy works best if used only once. If it is repeated multiple times, the small losses in gains will compound into big losses overall.<br />
Thus, I think I might actually put this automation in place at least on the stocks in my portfolio that have had the highest gains. If they keep going up, I will ride along, but once the crash happens, I will be out of those particular stocks permanently.<br />
Do you have a favorite open source investment tool or are you aware of any strategy that actually works? Comment below!</p>
<p><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/">Backtesting trailing stop-loss strategies with Python and market data</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/featured-image.png" alt="Featured image of post Backtesting trailing stop-loss strategies with Python and market data"></p>
<p>In <a class="link" href="https://optimizedbyotto.com/post/when-everyone-else-is-wrong/">January 2024 I wrote</a> about the insanity of the <em>Magnificent Seven</em> dominating the MSCI World Index, and I wondered how long the number can continue to go up? It has continued to surge upward at an accelerating pace, which makes me worry that a crash is likely closer. As a software professional, I decided to analyze <strong>whether using stop-loss orders could reliably automate avoiding deep drawdowns</strong>.</p>
<p>As everyone with some savings in the stock market (hopefully) knows, the stock market eventually experiences crashes. It is just a matter of <em>when</em> and <em>how deep</em> the crash will be. Staying on the sidelines for years is not a good investment strategy, as inflation will erode the value of your savings. Assuming the current true inflation rate is around 7%, a restaurant dinner that costs 20 euros today will cost 24.50 euros in three years. Savings of 1000 euros today would drop in purchasing power from 50 dinners to only 40 dinners in three years.</p>
<p>Hence, if you intend to retain the value of your hard-earned savings, they need to be invested in something that grows in value. Most people try to beat inflation by buying shares in stable companies, directly or via broad market ETFs. These historically <strong>grow faster than inflation</strong> during normal years, <strong>but likely drop in value during recessions</strong>.</p>
<h2><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/#what-is-a-trailing-stop-loss-order" class="header-anchor"></a>What is a trailing stop-loss order?<br>
<a class="anchor-link" id="what-is-a-trailing-stop-loss-order"></a></h2>
<p>What if you could buy stocks to benefit from their value increasing without having to worry about a potential crash? All modern online stock brokers have a feature called stop-loss, where you can enter a price at which your stocks automatically get sold if they drop down to that price. A trailing stop-loss order is similar, but instead of a fixed price, you enter a margin (e.g. 10%). If the stock price rises, the stop-loss price will trail upwards by that margin.</p>
<p>For example, if you buy a share at 100 euros and it has risen to 110 euros, you can set a 10% trailing stop-loss order which automatically sells it if the price drops 10% from the peak of 110 euros, at 99 euros. Thus, no matter what happens, you only lost 1 euro. And if the stock price continues to rise to 150 euros, the trailing stop-loss would automatically readjust to 150 euros minus 10%, which is 135 euros (150-15=135). If the price dropped to 135 euros, you would lock in a gain of 35 euros, which is not the peak price of 150 euros, but still better than whatever the price fell down to as a result of a large crash.</p>
<p>In the simple case above, it obviously makes sense in <em>theory</em>, but it might not make sense in <em>practice</em>. Prices constantly oscillate, so you don&rsquo;t want a margin that is too small, otherwise you exit too early. Conversely, having a large margin may result in too large a drawdown before exiting. If markets crash rapidly, it might be that nobody buys your stocks at the stop-loss price, and shares have to be sold at an even lower price. Also, what will you do once the position is sold? The reason you invested in the stock market was to avoid holding cash, so would you buy the same stock back when the crash bottoms? But how will you know when the bottom has been reached?</p>
<h2><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/#backtesting-stock-market-strategies-with-python-yfinance-pandas-and-lightweight-charts" class="header-anchor"></a>Backtesting stock market strategies with Python, YFinance, Pandas and Lightweight Charts<br>
<a class="anchor-link" id="backtesting-stock-market-strategies-with-python-yfinance-pandas-and-lightweight-charts"></a></h2>
<p>I am not a professional investor, and nobody should take investment advice from me. However, I know what <a class="link" href="https://en.wikipedia.org/wiki/Backtesting" target="_blank" rel="noopener">backtesting</a> is and how to leverage open source software. So, I wrote a Python script to test if the <a class="link" href="https://en.wikipedia.org/wiki/Trading_strategy" target="_blank" rel="noopener">trading strategy</a> of using trailing stop-loss orders with specific margin values would have worked for a particular stock.</p>
<p><strong>First you need to have data.</strong> <a class="link" href="https://github.com/ranaroussi/yfinance" target="_blank" rel="noopener">YFinance</a> is a handy Python library that can be used to download the historic price data for any stock ticker on <a class="link" href="http://yahoo.com/" target="_blank" rel="noopener">Yahoo.com</a>. <strong>Then you need to manipulate the data.</strong> <a class="link" href="https://github.com/pandas-dev/pandas" target="_blank" rel="noopener">Pandas</a> is <em>the</em> Python data analysis library with advanced data structures for working with relational or labeled data. <strong>Finally, to visualize the results</strong>, I used <a class="link" href="https://github.com/tradingview/lightweight-charts" target="_blank" rel="noopener">Lightweight Charts</a>, which is a fast, interactive library for rendering financial charts, allowing you to plot the stock price, the trailing stop-loss line, and the points where trades would have occurred. I really like how the zoom is implemented in Lightweight Charts, which makes drilling into the data points feel effortless.</p>
<p>The full solution is not polished enough to be published for others to use, but you can piece together your own by reusing some of the key snippets. To avoid re-downloading the same data repeatedly, I implemented a small caching wrapper that saves the data locally (as <a class="link" href="https://en.wikipedia.org/wiki/Apache_Parquet" target="_blank" rel="noopener">Parquet</a> files):</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang">python</span><br>
<button class="codeblock-copy" data-id="codeblock-id-0" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>CACHE_DIR.mkdir(parents=True, exist_ok=True)<br>
end_date = datetime.today().strftime("%Y-%m-%d")<br>
cache_file = CACHE_DIR / f"{TICKER}-{START_DATE}--{end_date}.parquet"<br>
if cache_file.is_file():<br>
dataframe = pandas.read_parquet(cache_file)<br>
print(f"Loaded price data from cache: {cache_file}")<br>
else:<br>
dataframe = yfinance.download(<br>
TICKER,<br>
start=START_DATE,<br>
end=end_date,<br>
progress=False,<br>
auto_adjust=False<br>
)<br>
dataframe.to_parquet(cache_file)<br>
print(f"Fetched new price data from Yahoo Finance and cached to: {cache_file}")</code></p>
<div>
<div class="highlight">
<pre><code class="language-python" data-lang="python"><span><span>CACHE_DIR<span>.</span>mkdir(parents<span>=</span><span>True</span>, exist_ok<span>=</span><span>True</span>)
</span></span><span><span>end_date <span>=</span> datetime<span>.</span>today()<span>.</span>strftime(<span>"%Y-%m-</span><span>%d</span><span>"</span>)
</span></span><span><span>cache_file <span>=</span> CACHE_DIR <span>/</span> <span>f</span><span>"</span><span>{</span>TICKER<span>}</span><span>-</span><span>{</span>START_DATE<span>}</span><span>--</span><span>{</span>end_date<span>}</span><span>.parquet"</span>
</span></span><span><span>
</span></span><span><span><span>if</span> cache_file<span>.</span>is_file():
</span></span><span><span> dataframe <span>=</span> pandas<span>.</span>read_parquet(cache_file)
</span></span><span><span> print(<span>f</span><span>"Loaded price data from cache: </span><span>{</span>cache_file<span>}</span><span>"</span>)
</span></span><span><span><span>else</span>:
</span></span><span><span> dataframe <span>=</span> yfinance<span>.</span>download(
</span></span><span><span> TICKER,
</span></span><span><span> start<span>=</span>START_DATE,
</span></span><span><span> end<span>=</span>end_date,
</span></span><span><span> progress<span>=</span><span>False</span>,
</span></span><span><span> auto_adjust<span>=</span><span>False</span>
</span></span><span><span> )
</span></span><span><span>
</span></span><span><span> dataframe<span>.</span>to_parquet(cache_file)
</span></span><span><span> print(<span>f</span><span>"Fetched new price data from Yahoo Finance and cached to: </span><span>{</span>cache_file<span>}</span><span>"</span>)</span></span></code></pre>
</div>
</div>
</div>
<p>The <strong>dataframe</strong> is a Pandas object with a <a class="link" href="https://pandas.pydata.org/docs/reference" target="_blank" rel="noopener">powerful API</a>. For example, to print a snippet from the beginning and the end of the dataframe to see what the data looks like, you can use:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang">python</span><br>
<button class="codeblock-copy" data-id="codeblock-id-1" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>print("First 5 rows of the raw data:")<br>
print(df.head())<br>
print("Last 5 rows of the raw data:")<br>
print(df.tail())</code></p>
<div>
<div class="highlight">
<pre><code class="language-python" data-lang="python"><span><span>print(<span>"First 5 rows of the raw data:"</span>)
</span></span><span><span>print(df<span>.</span>head())
</span></span><span><span>print(<span>"Last 5 rows of the raw data:"</span>)
</span></span><span><span>print(df<span>.</span>tail())</span></span></code></pre>
</div>
</div>
</div>
<p>Example output:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang"></span><br>
<button class="codeblock-copy" data-id="codeblock-id-2" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>First 5 rows of the raw data<br>
Price Adj Close Close High Low Open Volume<br>
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br>
Date<br>
2014-01-02 29.956285 55.540001 56.910000 55.349998 56.700001 316552<br>
2014-01-03 30.031801 55.680000 55.990002 55.290001 55.580002 210044<br>
2014-01-06 30.080338 55.770000 56.230000 55.529999 55.560001 185142<br>
2014-01-07 30.943321 57.369999 57.619999 55.790001 55.880001 370397<br>
2014-01-08 31.385597 58.189999 59.209999 57.750000 57.790001 489940<br>
Last 5 rows of the raw data<br>
Price Adj Close Close High Low Open Volume<br>
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA<br>
Date<br>
2025-12-11 78.669998 78.669998 78.919998 76.900002 76.919998 357918<br>
2025-12-12 78.089996 78.089996 80.269997 78.089996 79.470001 280477<br>
2025-12-15 79.080002 79.080002 79.449997 78.559998 78.559998 233852<br>
2025-12-16 78.860001 78.860001 79.980003 78.809998 79.430000 283057<br>
2025-12-17 80.080002 80.080002 80.150002 79.080002 79.199997 262818</code></p>
<pre><code>First 5 rows of the raw data
Price Adj Close Close High Low Open Volume
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA
Date
2014-01-02 29.956285 55.540001 56.910000 55.349998 56.700001 316552
2014-01-03 30.031801 55.680000 55.990002 55.290001 55.580002 210044
2014-01-06 30.080338 55.770000 56.230000 55.529999 55.560001 185142
2014-01-07 30.943321 57.369999 57.619999 55.790001 55.880001 370397
2014-01-08 31.385597 58.189999 59.209999 57.750000 57.790001 489940
Last 5 rows of the raw data
Price Adj Close Close High Low Open Volume
Ticker BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA BNP.PA
Date
2025-12-11 78.669998 78.669998 78.919998 76.900002 76.919998 357918
2025-12-12 78.089996 78.089996 80.269997 78.089996 79.470001 280477
2025-12-15 79.080002 79.080002 79.449997 78.559998 78.559998 233852
2025-12-16 78.860001 78.860001 79.980003 78.809998 79.430000 283057
2025-12-17 80.080002 80.080002 80.150002 79.080002 79.199997 262818</code></pre>
</div>
<p>Adding new columns to the dataframe is easy. For example, I used a custom function to calculate the Relative Strength Index (RSI). To add a new column &ldquo;RSI&rdquo; with a value for every row based on the price from that row, only one line of code is needed, without custom loops:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang">python</span><br>
<button class="codeblock-copy" data-id="codeblock-id-3" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code>df["RSI"] = compute_rsi(df["price"], period=14)</code></p>
<div>
<div class="highlight">
<pre><code class="language-python" data-lang="python"><span><span>df[<span>"RSI"</span>] <span>=</span> compute_rsi(df[<span>"price"</span>], period<span>=</span><span>14</span>)</span></span></code></pre>
</div>
</div>
</div>
<p>After manipulating the data, the series can be converted into an array structure and printed as JSON into a placeholder in an HTML template:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang">python</span><br>
<button class="codeblock-copy" data-id="codeblock-id-4" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code> baseline_series = [<br>
{"time": ts, "value": val}<br>
for ts, val in df_plot[["timestamp", BASELINE_LABEL]].itertuples(index=False)<br>
]<br>
baseline_json = json.dumps(baseline_series)<br>
template = jinja2.Template("template.html")<br>
rendered_html = template.render(<br>
title=title,<br>
heading=heading,<br>
description=description_html,<br>
...<br>
baseline_json=baseline_json,<br>
...<br>
)<br>
with open("report.html", "w", encoding="utf-8") as f:<br>
f.write(rendered_html)<br>
print("Report generated!")</code></p>
<div>
<div class="highlight">
<pre><code class="language-python" data-lang="python"><span><span> baseline_series <span>=</span> [
</span></span><span><span> {<span>"time"</span>: ts, <span>"value"</span>: val}
</span></span><span><span> <span>for</span> ts, val <span>in</span> df_plot[[<span>"timestamp"</span>, BASELINE_LABEL]]<span>.</span>itertuples(index<span>=</span><span>False</span>)
</span></span><span><span> ]
</span></span><span><span>
</span></span><span><span> baseline_json <span>=</span> json<span>.</span>dumps(baseline_series)
</span></span><span><span> template <span>=</span> jinja2<span>.</span>Template(<span>"template.html"</span>)
</span></span><span><span> rendered_html <span>=</span> template<span>.</span>render(
</span></span><span><span> title<span>=</span>title,
</span></span><span><span> heading<span>=</span>heading,
</span></span><span><span> description<span>=</span>description_html,
</span></span><span><span> <span>...</span>
</span></span><span><span> baseline_json<span>=</span>baseline_json,
</span></span><span><span> <span>...</span>
</span></span><span><span> )
</span></span><span><span>
</span></span><span><span> <span>with</span> open(<span>"report.html"</span>, <span>"w"</span>, encoding<span>=</span><span>"utf-8"</span>) <span>as</span> f:
</span></span><span><span> f<span>.</span>write(rendered_html)
</span></span><span><span> print(<span>"Report generated!"</span>)</span></span></code></pre>
</div>
</div>
</div>
<p>In the HTML template, the marker <code>{{ variable }}</code> in Jinja syntax gets replaced with the actual JSON:</p>
<div class="codeblock ">
<header>
<span class="codeblock-lang">html</span><br>
<button class="codeblock-copy" data-id="codeblock-id-5" data-copied-text="Copied!"><br>
Copy<br>
</button><br>
</header>
<p><code></code></p>
<p></p><title>{{ title }}</title><br>
...
<h1>{{ heading }}<a class="anchor-link" id="heading"></a></h1>
<div id="chart"></div>
<p>// Ensure the DOM is ready before we initialise the chart<br>
document.addEventListener('DOMContentLoaded', () =&gt; {<br>
// Parse the JSON data passed from Python<br>
const baselineData = {{ baseline_json | safe }};<br>
const strategyData = {{ strategy_json | safe }};<br>
const markersData = {{ markers_json | safe }};<br>
// Create the chart<br>
const chart = LightweightCharts.createChart(document.getElementById('chart'), {<br>
width: document.getElementById('chart').clientWidth,<br>
height: 500,<br>
layout: {<br>
background: { color: "#222" },<br>
textColor: "#ccc"<br>
},<br>
grid: {<br>
vertLines: { color: "#555" },<br>
horzLines: { color: "#555" }<br>
}<br>
});<br>
// Add baseline series<br>
const baselineSeries = chart.addLineSeries({<br>
title: '{{ baseline_label }}',<br>
lastValueVisible: false,<br>
priceLineVisible: false,<br>
priceLineWidth: 1<br>
});<br>
baselineSeries.setData(baselineData);<br>
baselineSeries.priceScale().applyOptions({<br>
entireTextOnly: true<br>
});<br>
// Add strategy series<br>
const strategySeries = chart.addLineSeries({<br>
title: '{{ strategy_label }}',<br>
lastValueVisible: false,<br>
priceLineVisible: false,<br>
color: '#FF6D00'<br>
});<br>
strategySeries.setData(strategyData);<br>
// Add buy/sell markers to the strategy series<br>
strategySeries.setMarkers(markersData);<br>
// Fit the chart to show the full data range (full zoom)<br>
chart.timeScale().fitContent();<br>
})</p>
<p></p>
<div>
<div class="highlight">
<pre><code class="language-html" data-lang="html"><span><span><span></span>
</span></span><span><span>&lt;<span>html</span> <span>lang</span><span>=</span><span>"en"</span>&gt;
</span></span><span><span>&lt;<span>head</span>&gt;
</span></span><span><span> &lt;<span>meta</span> <span>charset</span><span>=</span><span>"UTF-8"</span>&gt;
</span></span><span><span> &lt;<span>title</span>&gt;{{ title }}&lt;/<span>title</span>&gt;
</span></span><span><span> ...
</span></span><span><span>&lt;/<span>head</span>&gt;
</span></span><span><span>&lt;<span>body</span>&gt;
</span></span><span><span> &lt;<span>h1</span>&gt;{{ heading }}&lt;/<span>h1</span>&gt;
</span></span><span><span> &lt;<span>div</span> <span>id</span><span>=</span><span>"chart"</span>&gt;&lt;/<span>div</span>&gt;
</span></span><span><span> &lt;<span>script</span>&gt;
</span></span><span><span> <span>// Ensure the DOM is ready before we initialise the chart
</span></span></span><span><span><span></span> document.<span>addEventListener</span>(<span>'DOMContentLoaded'</span>, () =&gt; {
</span></span><span><span> <span>// Parse the JSON data passed from Python
</span></span></span><span><span><span></span> <span>const</span> <span>baselineData</span> <span>=</span> {{ <span>baseline_json</span> <span>|</span> <span>safe</span> }};
</span></span><span><span> <span>const</span> <span>strategyData</span> <span>=</span> {{ <span>strategy_json</span> <span>|</span> <span>safe</span> }};
</span></span><span><span> <span>const</span> <span>markersData</span> <span>=</span> {{ <span>markers_json</span> <span>|</span> <span>safe</span> }};
</span></span><span><span>
</span></span><span><span> <span>// Create the chart
</span></span></span><span><span><span></span> <span>const</span> <span>chart</span> <span>=</span> <span>LightweightCharts</span>.<span>createChart</span>(document.<span>getElementById</span>(<span>'chart'</span>), {
</span></span><span><span> <span>width</span><span>:</span> document.<span>getElementById</span>(<span>'chart'</span>).<span>clientWidth</span>,
</span></span><span><span> <span>height</span><span>:</span> <span>500</span>,
</span></span><span><span> <span>layout</span><span>:</span> {
</span></span><span><span> <span>background</span><span>:</span> { <span>color</span><span>:</span> <span>"#222"</span> },
</span></span><span><span> <span>textColor</span><span>:</span> <span>"#ccc"</span>
</span></span><span><span> },
</span></span><span><span> <span>grid</span><span>:</span> {
</span></span><span><span> <span>vertLines</span><span>:</span> { <span>color</span><span>:</span> <span>"#555"</span> },
</span></span><span><span> <span>horzLines</span><span>:</span> { <span>color</span><span>:</span> <span>"#555"</span> }
</span></span><span><span> }
</span></span><span><span> });
</span></span><span><span>
</span></span><span><span> <span>// Add baseline series
</span></span></span><span><span><span></span> <span>const</span> <span>baselineSeries</span> <span>=</span> <span>chart</span>.<span>addLineSeries</span>({
</span></span><span><span> <span>title</span><span>:</span> <span>'{{ baseline_label }}'</span>,
</span></span><span><span> <span>lastValueVisible</span><span>:</span> <span>false</span>,
</span></span><span><span> <span>priceLineVisible</span><span>:</span> <span>false</span>,
</span></span><span><span> <span>priceLineWidth</span><span>:</span> <span>1</span>
</span></span><span><span> });
</span></span><span><span> <span>baselineSeries</span>.<span>setData</span>(<span>baselineData</span>);
</span></span><span><span>
</span></span><span><span> <span>baselineSeries</span>.<span>priceScale</span>().<span>applyOptions</span>({
</span></span><span><span> <span>entireTextOnly</span><span>:</span> <span>true</span>
</span></span><span><span> });
</span></span><span><span>
</span></span><span><span> <span>// Add strategy series
</span></span></span><span><span><span></span> <span>const</span> <span>strategySeries</span> <span>=</span> <span>chart</span>.<span>addLineSeries</span>({
</span></span><span><span> <span>title</span><span>:</span> <span>'{{ strategy_label }}'</span>,
</span></span><span><span> <span>lastValueVisible</span><span>:</span> <span>false</span>,
</span></span><span><span> <span>priceLineVisible</span><span>:</span> <span>false</span>,
</span></span><span><span> <span>color</span><span>:</span> <span>'#FF6D00'</span>
</span></span><span><span> });
</span></span><span><span> <span>strategySeries</span>.<span>setData</span>(<span>strategyData</span>);
</span></span><span><span>
</span></span><span><span> <span>// Add buy/sell markers to the strategy series
</span></span></span><span><span><span></span> <span>strategySeries</span>.<span>setMarkers</span>(<span>markersData</span>);
</span></span><span><span>
</span></span><span><span> <span>// Fit the chart to show the full data range (full zoom)
</span></span></span><span><span><span></span> <span>chart</span>.<span>timeScale</span>().<span>fitContent</span>();
</span></span><span><span> })
</span></span><span><span> &lt;/<span>script</span>&gt;
</span></span><span><span>&lt;/<span>body</span>&gt;
</span></span><span><span>&lt;/<span>html</span>&gt;</span></span></code></pre>
</div>
</div>
</div>
<p>There are also Python libraries built specifically for backtesting investment strategies, such as <a class="link" href="https://github.com/mementum/backtrader" target="_blank" rel="noopener">Backtrader</a> and <a class="link" href="https://github.com/quantopian/zipline" target="_blank" rel="noopener">Zipline</a>, but they do not seem to be actively maintained, and probably have too many features and complexity compared to what I needed for doing this simple test.</p>
<p>The screenshot below shows an example of backtesting a strategy on the Waste Management Inc stock from January 2015 to December 2025. The baseline &ldquo;Buy and hold&rdquo; scenario is shown as the blue line and it fully tracks the stock price, while the orange line shows how the strategy would have performed, with markers for the sells and buys along the way.</p>
<p><img decoding="async" src="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/backtest-waste-management.png" width="1657" height="1243" loading="lazy" alt="Backtest run example" class="gallery-image" data-flex-grow="133" data-flex-basis="319px">
</p>
<h2><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/#results" class="header-anchor"></a>Results<br>
<a class="anchor-link" id="results"></a></h2>
<p>I experimented with multiple strategies and tested them with various parameters, but I don&rsquo;t think I found a strategy that was consistently and clearly better than just buy-and-hold.</p>
<p>It basically boils down to the fact that I was <strong>not able to find any way to calculate when the crash has bottomed</strong> based on historical data. You can only know in hindsight that the price has stopped dropping and is on a steady path to recovery, but at that point it is already too late to buy in. In my testing, <strong>most strategies underperformed buy-and-hold</strong> because they sold when the crash started, but bought back after it recovered at a slightly higher price.</p>
<p>In particular when using narrow margins and selling on a 3-6% drawdown the strategy performed very badly, as those small dips tend to recover in a few days. Essentially, the strategy was repeating the pattern of selling 100 stocks at a 6% discount, then being able to buy back only 94 shares the next day, then again selling 94 shares at a 6% discount, and only being able to buy back maybe 90 shares after recovery, and so forth, never catching up to the buy-and-hold.</p>
<p>The <strong>strategy worked better in large market crashes</strong> as they tended to last longer, and there were higher chances of buying back the shares while the price was still low. For example, in the 2020 crash selling at a 20% drawdown was a good strategy, as the stock I tested dropped nearly 50% and remained low for several weeks; thus, the strategy bought back the stocks while the price was still low and had not yet started to climb significantly. But that was just a lucky incident, as the delta between the trailing stop-loss margin of 20% and total crash of 50% was large enough. If the crash had been only 25%, the strategy would have missed the rebound and ended up buying back the stocks at a slightly higher price.</p>
<p>Also, note that the simulation assumes that the trade itself is too small to affect the price formation. We should keep in mind that in reality, if many people have stop-loss orders in place, a large price drop would trigger all of them, creating a flood of sell orders, which in turn would affect the price and drive it lower even faster and deeper. Luckily, it seems that stop-loss orders are generally not a good strategy, and we don&rsquo;t need to fear that too many people will be using them.</p>
<h2><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/#conclusion" class="header-anchor"></a>Conclusion<br>
<a class="anchor-link" id="conclusion"></a></h2>
<p>Even though using a trailing stop-loss strategy does not seem to help in getting consistently higher returns based on my backtesting, I would still say it is <strong>useful in protecting from the downside</strong> of stock investing. It can act as a kind of <em>&ldquo;insurance policy&rdquo;</em> to considerably decrease the chances of losing <em>big</em> while increasing the chances of losing <em>a little bit</em>. If you are risk-averse, which I think I probably am, this tradeoff can make sense. I&rsquo;d rather miss out on an initial 50% loss <em>and</em> an overall 3% gain on recovery than have to sit through weeks or months with a 50% loss before the price recovers to prior levels.</p>
<p>Most notably, the <strong>trailing stop-loss strategy works best if used only once</strong>. If it is repeated multiple times, the small losses in gains will compound into big losses overall.</p>
<p>Thus, I think I might actually put this automation in place at least on the stocks in my portfolio that have had the highest gains. If they keep going up, I will ride along, but once the crash happens, I will be out of those particular stocks permanently.</p>
<p>Do you have a favorite open source investment tool or are you aware of any strategy that actually works? Comment below!</p>

<p><a href="https://optimizedbyotto.com/post/backtest-stop-loss-strategy-python/">Backtesting trailing stop-loss strategies with Python and market data</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Enhancing PostgreSQL OIDC with pg_oidc_validator</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/12/17/enhancing-postgresql-oidc-with-pg_oidc_validator/" />
      <id>https://percona.community/blog/2025/12/17/enhancing-postgresql-oidc-with-pg_oidc_validator/</id>
      <updated>2025-12-17T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>With PostgreSQL 18 introducing built-in OAuth 2.0 and OpenID Connect (OIDC) authentication, tools like pg_oidc_validator have become an essential part of the ecosystem by enabling server-side verification of OIDC tokens directly inside PostgreSQL. If you’re new to the topic, make sure to read our earlier posts explaining the underlying concepts and the need for external validators:</p>
<p><a href="https://percona.community/blog/2025/12/17/enhancing-postgresql-oidc-with-pg_oidc_validator/">Enhancing PostgreSQL OIDC with pg_oidc_validator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>With PostgreSQL 18 introducing built-in OAuth 2.0 and OpenID Connect (OIDC) authentication, tools like <a href="https://github.com/Percona-Lab/pg_oidc_validator" target="_blank" rel="noopener noreferrer">pg_oidc_validator</a> have become an essential part of the ecosystem by enabling server-side verification of OIDC tokens directly inside PostgreSQL. If you&rsquo;re new to the topic, make sure to read our earlier posts explaining the underlying concepts and the need for external validators:</p>
<ul>
<li><a href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/" target="_blank" rel="noopener noreferrer">Why PostgreSQL needs external token validators</a></li>
<li><a href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/" target="_blank" rel="noopener noreferrer">Security aspects of OIDC validation in PostgreSQL</a></li>
<li><a href="https://www.percona.com/blog/postgresql-oidc-authentication-with-pg_oidc_validator/" target="_blank" rel="noopener noreferrer">Deploying pg_oidc_validator v0.1 &ndash; a DBA&rsquo;s perspective</a></li>
</ul>
<p>This release builds on the initial version <a href="https://percona.community/blog/2025/10/22/say-hello-to-oidc-in-postgresql-18/" target="_blank" rel="noopener noreferrer">announced in October</a> and continues our mission to make OIDC adoption in PostgreSQL reliable, fast, and accessible for all users.</p>
<h2><strong>What&rsquo;s New in This Release</strong><a class="anchor-link" id="whats-new-in-this-release"></a></h2>
<p>This new iteration of pg_oidc_validator (v0.2) introduces two major improvements:</p>
<ul>
<li>initial caching support, and</li>
<li>Debian/Ubuntu and RPM packages to simplify installation.</li>
</ul>
<p>Most importantly, these improvements come directly from community feedback, ****whether during conversations at PGConf.EU in Riga, KubeCon US in Atlanta, or through GitHub and forums. Thank you for helping us shape this project!</p>
<h2><strong>Caching Support in pg_oidc_validator</strong><a class="anchor-link" id="caching-support-in-pg_oidc_validator"></a></h2>
<p>OIDC token verification requires fetching issuer metadata and JWKS keyset<strong>s</strong> from an external identity provider (IdP). Without caching, every PostgreSQL backend performing validation must re-fetch this data, increasing latency and putting unnecessary load on the IdP.</p>
<p><figure><img decoding="async" width="1024" height="1024" src="https://percona.community/blog/2025/12/pg_oidc_cache_hu_a315a4a8cf167da1.webp" alt="&nbsp;" loading="lazy"></figure>
</p>
<p>pg_oidc_validator v0.2 introduces a lightweight caching layer. This allows the validator to:</p>
<ul>
<li>cache OIDC discovery documents and JWKS responses when permitted by the IdP,</li>
<li>use cached responses across PostgreSQL backends,</li>
<li>reduce outbound HTTP calls,</li>
<li>validate tokens at in-memory speeds, and</li>
<li>integrate cleanly with IdP key rotation.</li>
</ul>
<p>This results in improved performance, reduced IdP load, and better scalability for deployments using Keycloak, Okta, Microsoft Entra ID, Ping Identity, or other OIDC providers.</p>
<h3><strong>A Note on Testing</strong><a class="anchor-link" id="a-note-on-testing"></a></h3>
<p>The caching layer currently lacks full automated test coverage. This is because Keycloak does not allow caching for issuer or JWKS endpoints (<a href="https://github.com/keycloak/keycloak/issues/15216" target="_blank" rel="noopener noreferrer">Keycloak issue #15216</a>), preventing us from validating caching behavior.</p>
<p>To address this, we plan to extend the test setup by placing an nginx proxy between PostgreSQL and Keycloak to simulate IdP responses that include cache-friendly headers.</p>
<h2><strong>Pre-Built Packages Now Available</strong><a class="anchor-link" id="pre-built-packages-now-available"></a></h2>
<p>Installing pg_oidc_validator is now easier than ever. We provide builds at the <a href="https://github.com/Percona-Lab/pg_oidc_validator/releases/tag/latest" target="_blank" rel="noopener noreferrer">latest release page</a>, where nightly builds are available for:</p>
<ul>
<li><strong>Debian / Ubuntu</strong> &ndash; tested on Ubuntu 24.04</li>
<li><strong>RHEL / Oracle Linux / Rocky Linux</strong> &ndash; tested on OL8 and OL9</li>
</ul>
<p>If you prefer building from source, instructions are available directly in the project&rsquo;s <a href="https://github.com/Percona-Lab/pg_oidc_validator" target="_blank" rel="noopener noreferrer">README</a>.</p>
<h2><strong>Try it, test it, tell us all about it!</strong><a class="anchor-link" id="try-it-test-it-tell-us-all-about-it"></a></h2>
<p>As an open source project, pg_oidc_validator grows with your feedback. We want to hear about:</p>
<ul>
<li>your deployment use cases,</li>
<li>performance characteristics,</li>
<li>integration challenges,</li>
<li>and features you&rsquo;d like to see next.</li>
</ul>
<p>You can reach us here:</p>
<ul>
<li>
<p><strong>Percona Community Forums:</strong></p>
<p><a href="https://forums.percona.com/c/postgresql/25" target="_blank" rel="noopener noreferrer">https://forums.percona.com/c/postgresql/25</a></p>
</li>
<li>
<p><strong>GitHub:</strong></p>
<ul>
<li>Issues: <a href="https://github.com/Percona-Lab/pg_oidc_validator/issues" target="_blank" rel="noopener noreferrer">https://github.com/Percona-Lab/pg_oidc_validator/issues</a></li>
<li>Discussions: <a href="https://github.com/Percona-Lab/pg_oidc_validator/discussions" target="_blank" rel="noopener noreferrer">https://github.com/Percona-Lab/pg_oidc_validator/discussions</a></li>
</ul>
</li>
</ul>
<p>And of course, if you see Percona at an event, come talk to us at the booth!</p>

<p><a href="https://percona.community/blog/2025/12/17/enhancing-postgresql-oidc-with-pg_oidc_validator/">Enhancing PostgreSQL OIDC with pg_oidc_validator</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>What is New in Percona Toolkit 3.7.1</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/12/17/what-is-new-in-percona-toolkit-3.7.1/" />
      <id>https://percona.community/blog/2025/12/17/what-is-new-in-percona-toolkit-3.7.1/</id>
      <updated>2025-12-17T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Percona Toolkit 3.7.1 has been released on Dec 17, 2025. The most important updates in this version are:</p>
<p><a href="https://percona.community/blog/2025/12/17/what-is-new-in-percona-toolkit-3.7.1/">What is New in Percona Toolkit 3.7.1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Percona Toolkit 3.7.1 has been released on <strong>Dec 17, 2025</strong>. The most important updates in this version are:</p>
<ul>
<li>Finalized SSL/TLS support for MySQL</li>
<li>Added support for Debian 13 and Amazon Linux 2023</li>
<li>Fixed MariaDB support broken in version 3.7.0</li>
<li>Added options to skip certain collections in <code>pt-k8s-debug-collector</code> and <code>pt-stalk</code></li>
<li>Documentation improvements</li>
<li>Other performance improvements</li>
</ul>
<p>In this blog, I will outline the most significant changes. A full list of improvements and bug fixes can be found in the <a href="https://docs.percona.com/percona-toolkit/release_notes.html" target="_blank" rel="noopener noreferrer">release notes</a>.</p>
<h1>SSL/TLS support for MySQL<a class="anchor-link" id="ssl-tls-support-for-mysql"></a></h1>
<p>Percona Toolkit historically did not have consistent SSL support. This was reported at <a href="https://perconadev.atlassian.net/browse/PT-191" target="_blank" rel="noopener noreferrer">https://perconadev.atlassian.net/browse/PT-191</a>. In version 3.7.0, option <code>s</code> for <code>DSN</code> was introduced. This option instructs <code>DBD::mysql</code> to open a secure connection with the database. This version also adds command-line option <code>--mysql-ssl</code> and its short form <code>-s</code> to all tools. All other SSL/TLS-related options, such as <code>ssl-ca</code>, <code>ssl-cert</code>, <code>ssl-cipher</code>, etc, could be specified in the configuration file if necessary. This completes SSL/TLS support for MySQL. For more details and information check <a href="https://www.percona.com/blog/unlocking-secure-connections-ssl-tls-support-in-percona-toolkit/" target="_blank" rel="noopener noreferrer">this blog post</a>.</p>
<h1>Supported Platforms Update<a class="anchor-link" id="supported-platforms-update"></a></h1>
<p>Percona repositories now have Percona Toolkit packages for Debian 13 and Amazon Linux 2023. To install them enable repository <code>pt</code> with the <code>percona-release</code> utility. Ubuntu Focal reached its EOL and support for this platform has been removed. More information on Percona repositories is available in the <a href="https://docs.percona.com/percona-software-repositories/index.html" target="_blank" rel="noopener noreferrer">User Reference Manual</a>.</p>
<h1>Regression Bug Fixes<a class="anchor-link" id="regression-bug-fixes"></a></h1>
<p>Recent major changes introducing MySQL 8.4 support missed ignore case modificator for the regular expression that checks if MySQL flavor is MariaDB. As a result, tools executed replication statements not compatible with MariaDB. Version 3.7.1 fixes the regular expression and re-adds MariaDB support back (<a href="https://perconadev.atlassian.net/browse/PT-2451" target="_blank" rel="noopener noreferrer">PT-2451</a>). Future versions of Percona Toolkit will have better MariaDB support, including MariaDB-specific versions of non-offensive replication commands.</p>
<p>Utility <code>pt-sift</code> stopped working, because dependent library <code>alt_cmds.sh</code> was not included (<a href="https://perconadev.atlassian.net/browse/PT-2498" target="_blank" rel="noopener noreferrer">PT-2498</a>). This was not found during previous release testing, because regression test for the tool was not run. Now this miss is fixed and the utility works properly again. Additionally, regression test is updated.</p>
<p>Helper utility <code>version_cmp</code> was written in some compiled language and source code for it was not available (<a href="https://perconadev.atlassian.net/browse/PT-2469" target="_blank" rel="noopener noreferrer">PT-2469</a>). This broke version checking on platforms not compatible with the unknown platform where the binary was originally compiled. Now this utility rewritten as a Bourne-Again shell script.</p>
<h1>Modern MySQL Support<a class="anchor-link" id="modern-mysql-support"></a></h1>
<p>Percona Toolkit uses legacy MySQL syntax in many places to be compatible with older versions of MySQL. In other places, it misses modern MySQL diagnostic additions. This version makes first steps to improve this situation by adding such features as invisible index support in <code>pt-duplicate-key-checker</code> (<a href="https://percona.community/blog/2025/12/17/what-is-new-in-percona-toolkit-3.7.1/github.com/percona/percona-toolkit/pull/996">PR-996</a>) and <code>performance_schema.threads</code> collecton in <code>pt-stalk</code> (<a href="https://perconadev.atlassian.net/browse/PT-1718" target="_blank" rel="noopener noreferrer">PT-1718</a>). Currently, data from <code>performance_schema.threads</code> is collected along with the deprecated <code>information_schema.processlist</code>. In the future, support for <code>information_schema.processlist</code> will be deprecated, then removed.</p>
<p>Future versions of Percona Toolkit will have more modern MySQL diagnostic support.</p>
<h1>Performance Improvements<a class="anchor-link" id="performance-improvements"></a></h1>
<p><code>pt-stalk</code> now has new option, <code>--skip-collection</code>, that allows to skip one or more collections. Supported values for this option are: <code>ps-locks-transactions</code>, <code>thread-variables</code>, <code>innodbstatus</code>, <code>lock-waits</code>, <code>mysqladmin</code>, <code>processlist</code>, <code>rocksdbstatus</code>, <code>transactions</code>. To skip two or more collections, separate them with a comma. E.g., <code>--skip-collection=processlist,innodbstatus</code>. You will find more information at <a href="https://perconadev.atlassian.net/browse/PT-2289" target="_blank" rel="noopener noreferrer">PT-2289</a> and in the <a href="https://docs.percona.com/percona-toolkit/pt-stalk.html" target="_blank" rel="noopener noreferrer">User Reference Manual for <code>pt-stalk</code></a>.</p>
<p><code>pt-k8s-debug-collector</code> introduces option <code>-skip-pod-summary</code> allowing to skip pod summary collections, such as <code>pt-mysql-summary</code>, <code>pt-mongodb-summary</code>, or <code>pg_gather</code>. Check <a href="https://perconadev.atlassian.net/browse/PT-2453" target="_blank" rel="noopener noreferrer">PT-2453</a> and the <a href="https://docs.percona.com/percona-toolkit/pt-k8s-debug-collector.html" target="_blank" rel="noopener noreferrer">User Reference Manual for <code>pt-k8s-debug-collector</code></a>.</p>
<p>Originally, tools output was always buffered. This is usually good for performance but you may want to disable this feature when need to see output of the tools faster. For example, if you run <code>pt-archiver</code> or <code>pt-table-checksum</code> on large table in Kubernetes, you won&rsquo;t see progress (<a href="https://perconadev.atlassian.net/browse/PT-2052" target="_blank" rel="noopener noreferrer">PT-2052</a>) until the tool finishes. New option, <code>--[no]buffer-stdout</code>, allows to disable buffering when needed.</p>
<h2>Incompatilbe change<a class="anchor-link" id="incompatilbe-change"></a></h2>
<p>Earlier, if <code>--chunk-size</code> was enabled for <code>pt-online-schema-change</code>, option <code>--chunk-time</code> was ignored. This caused situations when a user has to start with default automatic chunk size even if it was not effective for some tables, and wait when chunk size is adjusted in subsequent iterations. Alternatively, they had to guess fixed chunk size that implies time consuming <a href="https://en.wikipedia.org/wiki/Trial_and_error" target="_blank" rel="noopener noreferrer">try and error</a> approach (<a href="https://perconadev.atlassian.net/browse/PT-1423" target="_blank" rel="noopener noreferrer">PT-1423</a>).</p>
<p>Starting from version 3.7.1, if both options <code>--chunk-size</code> and <code>--chunk-time</code> are specified, initial chunk size will be as specified by the option <code>--chunk-size</code>, but later it will be adjusted, so that the next query takes specified amount of time (in seconds) to execute.</p>
<h1>Documentation Improvements<a class="anchor-link" id="documentation-improvements"></a></h1>
<p>While working on this release we found undocumented featues such as <code>--recursion-method=dsn</code> support in <code>pt-table-sync</code> (<a href="https://perconadev.atlassian.net/browse/PT-2470" target="_blank" rel="noopener noreferrer">PT-2470</a>), broken man page for <code>pt-secure-collect</code> and other tools written in Go language (<a href="https://perconadev.atlassian.net/browse/PT-1564" target="_blank" rel="noopener noreferrer">PT-1564</a>), as well as minor documentation issues. Now all of them are fixed.</p>
<h1>Community contributions<a class="anchor-link" id="community-contributions"></a></h1>
<p>This release includes contributions from Community and Percona Engineers who do not actively work on the project. We want to thank:</p>
<ul>
<li>Iwo Panowicz for option <code>-skip-pod-summary</code> in <code>pt-k8s-debug-collector</code> (<a href="https://perconadev.atlassian.net/browse/PT-2453" target="_blank" rel="noopener noreferrer">PT-2453</a>)</li>
<li>Matthew Boehm for invisible indexes support in <code>pt-duplicate-key-checker</code> (<a href="https://github.com/percona/percona-toolkit/pull/996" target="_blank" rel="noopener noreferrer">PR-996</a>)</li>
<li>Nilnandan Joshi for collecting <code>performance_schema.threads</code> along with <code>information_schema.processlist</code> in <code>pt-stalk</code> (<a href="https://perconadev.atlassian.net/browse/PT-1718" target="_blank" rel="noopener noreferrer">PT-1718</a>) and fix for <a href="https://perconadev.atlassian.net/browse/PT-2014" target="_blank" rel="noopener noreferrer">PT-2014 &ndash; pt-config-diff does not honor case insensitivity flag</a></li>
<li>Pawe&#322; Kudzia for the updated documentation of pt-query-digest (<a href="https://github.com/percona/percona-toolkit/pull/953" target="_blank" rel="noopener noreferrer">PR-953</a>)</li>
<li>Maciej Dobrzanski for fixing <a href="https://github.com/percona/percona-toolkit/pull/890" target="_blank" rel="noopener noreferrer">PR-890 &ndash; pt-config-diff: MySQL truncates run-time variable values longer than 1024 characters</a></li>
<li>Marek Knappe for fixing <a href="https://perconadev.atlassian.net/browse/PT-2418" target="_blank" rel="noopener noreferrer">PT-2418 &ndash; pt-online-schema-change 3.7.0 lost data when exe alter xxx rename column xxx</a> and <a href="https://perconadev.atlassian.net/browse/PT-2458" target="_blank" rel="noopener noreferrer">PT-2458 &ndash; remove-data-dir defaults to True</a></li>
<li>Yoann La Cancellera for his work on <code>pt-galera-log-explainer</code></li>
<li>Nyele for restoring MariaDB support (<a href="https://perconadev.atlassian.net/browse/PT-2465" target="_blank" rel="noopener noreferrer">PT-2465</a>)</li>
<li>Taehyung Lim for fixing <a href="https://perconadev.atlassian.net/browse/PT-2401" target="_blank" rel="noopener noreferrer">PT-2401 &ndash; pt-online-schema-change &rsquo;table does not exist&rsquo; on macos</a></li>
<li>Viktoras Agejevas for fixing <a href="https://github.com/percona/percona-toolkit/pull/989" target="_blank" rel="noopener noreferrer">PR-989 &ndash; Fix script crashing with precedence error</a> in <code>pt-online-schema-change</code></li>
<li>Hartley McGuire for fixing <a href="https://perconadev.atlassian.net/browse/PT-2015" target="_blank" rel="noopener noreferrer">PT-2015 &ndash; pt-config-diff does not sort variable flags</a></li>
</ul>

<p><a href="https://percona.community/blog/2025/12/17/what-is-new-in-percona-toolkit-3.7.1/">What is New in Percona Toolkit 3.7.1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>mlrd: DynamoDB-Compatible API on MySQL</title>
      <link rel="alternate" type="text/html" href="https://hackmysql.com/mlrd-dynamodb-compatible-api-on-mysql/" />
      <id>https://hackmysql.com/mlrd-dynamodb-compatible-api-on-mysql/</id>
      <updated>2025-12-12T21:39:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Introducing mlrd (“mallard”) to the world: a DynamoDB-compatible API on MySQL.<br />
Crazy, but it works really well and I’m confident it will help a lot of businesses save a lot of money.<br />
Here’s why.</p>
<p><a href="https://hackmysql.com/mlrd-dynamodb-compatible-api-on-mysql/">mlrd: DynamoDB-Compatible API on MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Introducing <a href="https://mlrd.tech/"><code>mlrd</code></a> (&ldquo;mallard&rdquo;) to the world: a DynamoDB-compatible API on MySQL.<br>
Crazy, but it works really well and I&rsquo;m confident it will help a lot of businesses save a lot of money.<br>
Here&rsquo;s why.</p>

<p><a href="https://hackmysql.com/mlrd-dynamodb-compatible-api-on-mysql/">mlrd: DynamoDB-Compatible API on MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB/MySQL Environment MyEnv 2.1.1 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/" />
      <id>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/</id>
      <updated>2025-12-12T16:43:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.1.1 of its popular MariaDB, Galera Cluster and MySQL multi-instance environment MyEnv.<br />
The new MyEnv can be downloaded here. How to install MyEnv is described in the MyEnv Installation Guide.<br />
In the inconceivable case that you find a bug in MyEnv please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Upgrade from 1.1.x to 2.0<br />
Please look at the MyEnv 2.0.0 Release Notes.<br />
Upgrade from 2.0.x to 2.1.1<br />
$ sudo -i -u mysql<br />
$ cd ${HOME}/product<br />
$ tar xf /download/myenv-2.1.1.tar.gz<br />
$ rm -f myenv<br />
$ ln -s myenv-2.1.1 myenv</p>
<p>Plug-ins<br />
If you are using plug-ins for showMyEnvStatus create all the links in the new directory structure:<br />
$ cd ${HOME}/product/myenv<br />
$ ln -s ../../utl/oem_agent.php plg/showMyEnvStatus/</p>
<p>Upgrade of the instance directory structure<br />
From MyEnv 1.0 to 2.0 the directory structure of instances has fundamentally changed. Nevertheless MyEnv 2.0 works fine with MyEnv 1.0 directory structures.<br />
Changes in MyEnv 2.1.1<br />
MyEnv</p>
<p>addInstance for PostgreSQL added.<br />
Basic PostgreSQL functionality implemented.<br />
up working now.<br />
stop, start, restart and status implemented for PostgreSQL.<br />
Version extraction for PostgreSQL added.<br />
Function extractVersion rewritten so section config is passed and not basedir any more.<br />
Configuration type mysqld changed to mysql and now also usable for mariadb and postgresql.<br />
New variant of version comment added for MySQL 8.0.<br />
Comment added to my_exec().</p>
<p>MyEnv Installer</p>
<p>mysql/mariadb fork installation implemented and tested.<br />
Example in installMyEnv corrected/improved.<br />
Made nasty warning during installation of new instance go away.</p>
<p>MyEnv Utilities</p>
<p>New script added.<br />
SSL added to monitor.<br />
Slave monitor made MySQL 8+ ready.<br />
checksum_table works now also with tables consisting of protected keywords.<br />
More debugging info added.<br />
All scripts checked by shellcheck.<br />
Missing runtime directory is caught and fixed now.<br />
shellcheck suggestions applied.<br />
checksum_table.sh added.<br />
Utility scripts brought to new state.<br />
Build slave script added.<br />
table diff refactored.<br />
Chunking added.<br />
table_diff is now ready for chunking.<br />
Faster checksum implemented and output shortened.<br />
Row by row crc32 checksum.<br />
Moved the checksum table in its own function.<br />
table_diff.php added.</p>
<p>PostgreSQL</p>
<p>show_create_table.sh for PostgreSQL added.<br />
PostgreSQL files added here until we have a better location.</p>
<p>General</p>
<p>Code clean-up and configuration check added.<br />
Recursive directory removal improved.<br />
rc made unique.<br />
Minor typos fixed.<br />
my.cnf.template cleaned-up and synced with website.<br />
2 bugs with PHP 8.5 fixed.<br />
Libraries updated.<br />
Some return codes can be ignored because Redhat tools return error codes &#62; 100 as OK.</p>
<p>Documentation</p>
<p>Documentation added and ready to start.</p>
<p>Packaging</p>
<p>Distro versions centralized in one place.<br />
Zabbix repo added for Ubuntu 24.04.<br />
Rocky 10 and Debian 11 added.<br />
lxc replaced by incus, distros cleaned-up.<br />
Ubuntu 24.04 and MariaDB 10.11 and 11.4 enabled.<br />
brman as build project added.<br />
glb included in build scripts.<br />
restartContainer implemented.<br />
gid and uid added to pushFileToContainer.<br />
Build cleaned-up.<br />
Build scrips improved.<br />
Container file push and pull added.<br />
Build infrastructure reorganized.<br />
stopContainer and startContainer added.<br />
Container library started.<br />
Debian 10 removed.<br />
Update container script is waiting to avoid infrastructure build failure.<br />
Old package for RPM changed.<br />
install_base separted from mysql_home.<br />
Installation moved from /home/mysql to /opt.<br />
Fix DEB package.<br />
Ubuntu 22.04 added for package build.<br />
Bug in Debian build script fixed.<br />
Ubuntu 24.04 added to build infrastructure.<br />
recreate_build_infrastructure.sh rewritten in PHP.<br />
update_container_templates.sh rewritten in PHP.<br />
Made package build infrastructure more generic.</p>
<p>For subscriptions of commercial use of MyEnv please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/">MariaDB/MySQL Environment MyEnv 2.1.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.1.1 of its popular MariaDB, Galera Cluster and MySQL multi-instance environment <a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment">MyEnv</a>.</p>
<p>The new MyEnv can be downloaded <a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download">here</a>. How to install MyEnv is described in the <a href="https://support.fromdual.com/documentation/myenv/myenv.html" target="_blank">MyEnv Installation Guide</a>.</p>
<p>In the inconceivable case that you find a bug in MyEnv please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20myenv">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">send them to us</a>.</p>
<h2>Upgrade from 1.1.x to 2.0<a class="anchor-link" id="upgrade-from-1-1-x-to-2-0"></a></h2>
<p>Please look at the <a href="https://www.fromdual.com/mysql-mariadb-environment-myenv-2.0.0-has-been-released" title="MySQL Environment MyEnv 2.0.0 has been released">MyEnv 2.0.0 Release Notes</a>.</p>
<h2>Upgrade from 2.0.x to 2.1.1<a class="anchor-link" id="upgrade-from-2-0-x-to-2-1-1"></a></h2>
<pre><code>$ sudo -i -u mysql
$ cd ${HOME}/product
$ tar xf /download/myenv-2.1.1.tar.gz
$ rm -f myenv
$ ln -s myenv-2.1.1 myenv
</code></pre>
<h3>Plug-ins<a class="anchor-link" id="plug-ins"></a></h3>
<p>If you are using plug-ins for <code>showMyEnvStatus</code> create all the links in the new directory structure:</p>
<pre><code>$ cd ${HOME}/product/myenv
$ ln -s ../../utl/oem_agent.php plg/showMyEnvStatus/
</code></pre>
<h3>Upgrade of the instance directory structure<a class="anchor-link" id="upgrade-of-the-instance-directory-structure"></a></h3>
<p>From MyEnv 1.0 to 2.0 the directory structure of instances has fundamentally changed. Nevertheless MyEnv 2.0 works fine with MyEnv 1.0 directory structures.</p>
<h2>Changes in MyEnv 2.1.1<a class="anchor-link" id="changes-in-myenv-2-1-1"></a></h2>
<h3>MyEnv<a class="anchor-link" id="myenv"></a></h3>
<ul>
<li><code>addInstance</code> for PostgreSQL added.</li>
<li>Basic PostgreSQL functionality implemented.</li>
<li><code>up</code> working now.</li>
<li><code>stop</code>, <code>start</code>, <code>restart</code> and <code>status</code> implemented for PostgreSQL.</li>
<li>Version extraction for PostgreSQL added.</li>
<li>Function <code>extractVersion</code> rewritten so section config is passed and not <code>basedir</code> any more.</li>
<li>Configuration type <code>mysqld</code> changed to <code>mysql</code> and now also usable for <code>mariadb</code> and <code>postgresql</code>.</li>
<li>New variant of version comment added for MySQL 8.0.</li>
<li>Comment added to <code>my_exec()</code>.</li>
</ul>
<h3>MyEnv Installer<a class="anchor-link" id="myenv-installer"></a></h3>
<ul>
<li>mysql/mariadb fork installation implemented and tested.</li>
<li>Example in <code>installMyEnv</code> corrected/improved.</li>
<li>Made nasty warning during installation of new instance go away.</li>
</ul>
<h3>MyEnv Utilities<a class="anchor-link" id="myenv-utilities"></a></h3>
<ul>
<li>New script added.</li>
<li>SSL added to monitor.</li>
<li>Slave monitor made MySQL 8+ ready.</li>
<li><code>checksum_table</code> works now also with tables consisting of protected keywords.</li>
<li>More debugging info added.</li>
<li>All scripts checked by <code>shellcheck</code>.</li>
<li>Missing runtime directory is caught and fixed now.</li>
<li><code>shellcheck</code> suggestions applied.</li>
<li><code>checksum_table.sh</code> added.</li>
<li>Utility scripts brought to new state.</li>
<li>Build slave script added.</li>
<li>table diff refactored.</li>
<li>Chunking added.</li>
<li><code>table_diff</code> is now ready for chunking.</li>
<li>Faster checksum implemented and output shortened.</li>
<li>Row by row crc32 checksum.</li>
<li>Moved the checksum table in its own function.</li>
<li><code>table_diff.php</code> added.</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li><code>show_create_table.sh</code> for PostgreSQL added.</li>
<li>PostgreSQL files added here until we have a better location.</li>
</ul>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Code clean-up and configuration check added.</li>
<li>Recursive directory removal improved.</li>
<li>rc made unique.</li>
<li>Minor typos fixed.</li>
<li><code>my.cnf.template</code> cleaned-up and synced with website.</li>
<li>2 bugs with PHP 8.5 fixed.</li>
<li>Libraries updated.</li>
<li>Some return codes can be ignored because Redhat tools return error codes &gt; 100 as OK.</li>
</ul>
<h3>Documentation<a class="anchor-link" id="documentation"></a></h3>
<ul>
<li>Documentation added and ready to start.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>Distro versions centralized in one place.</li>
<li>Zabbix repo added for Ubuntu 24.04.</li>
<li>Rocky 10 and Debian 11 added.</li>
<li><code>lxc</code> replaced by <code>incus</code>, distros cleaned-up.</li>
<li>Ubuntu 24.04 and MariaDB 10.11 and 11.4 enabled.</li>
<li><code>brman</code> as build project added.</li>
<li><code>glb</code> included in build scripts.</li>
<li><code>restartContainer</code> implemented.</li>
<li><code>gid</code> and <code>uid</code> added to <code>pushFileToContainer</code>.</li>
<li>Build cleaned-up.</li>
<li>Build scrips improved.</li>
<li>Container file push and pull added.</li>
<li>Build infrastructure reorganized.</li>
<li><code>stopContainer</code> and <code>startContainer</code> added.</li>
<li>Container library started.</li>
<li>Debian 10 removed.</li>
<li>Update container script is waiting to avoid infrastructure build failure.</li>
<li>Old package for RPM changed.</li>
<li><code>install_base</code> separted from <code>mysql_home</code>.</li>
<li>Installation moved from <code>/home/mysql</code> to <code>/opt</code>.</li>
<li>Fix DEB package.</li>
<li>Ubuntu 22.04 added for package build.</li>
<li>Bug in Debian build script fixed.</li>
<li>Ubuntu 24.04 added to build infrastructure.</li>
<li><code>recreate_build_infrastructure.sh</code> rewritten in PHP.</li>
<li><code>update_container_templates.sh</code> rewritten in PHP.</li>
<li>Made package build infrastructure more generic.</li>
</ul>
<p>For subscriptions of commercial use of MyEnv please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20MyEnv">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/">MariaDB/MySQL Environment MyEnv 2.1.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB/MySQL Environment MyEnv 2.1.1 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/" />
      <id>https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/</id>
      <updated>2025-12-12T16:43:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.1.1 of its popular MariaDB, Galera Cluster and MySQL multi-instance environment MyEnv.<br />
The new MyEnv can be downloaded here. How to install MyEnv is described in the MyEnv Installation Guide.<br />
In the inconceivable case that you find a bug in MyEnv please report it to us by sending an email.<br />
Any feedback, statements and testimonials are welcome as well! Please send them to us.<br />
Upgrade from 1.1.x to 2.0<br />
Please look at the MyEnv 2.0.0 Release Notes.<br />
Upgrade from 2.0.x to 2.1.1<br />
$ sudo -i -u mysql<br />
$ cd ${HOME}/product<br />
$ tar xf /download/myenv-2.1.1.tar.gz<br />
$ rm -f myenv<br />
$ ln -s myenv-2.1.1 myenv</p>
<p>Plug-ins<br />
If you are using plug-ins for showMyEnvStatus create all the links in the new directory structure:<br />
$ cd ${HOME}/product/myenv<br />
$ ln -s ../../utl/oem_agent.php plg/showMyEnvStatus/</p>
<p>Upgrade of the instance directory structure<br />
From MyEnv 1.0 to 2.0 the directory structure of instances has fundamentally changed. Nevertheless MyEnv 2.0 works fine with MyEnv 1.0 directory structures.<br />
Changes in MyEnv 2.1.1<br />
MyEnv</p>
<p>addInstance for PostgreSQL added.<br />
Basic PostgreSQL functionality implemented.<br />
up working now.<br />
stop, start, restart and status implemented for PostgreSQL.<br />
Version extraction for PostgreSQL added.<br />
Function extractVersion rewritten so section config is passed and not basedir any more.<br />
Configuration type mysqld changed to mysql and now also usable for mariadb and postgresql.<br />
New variant of version comment added for MySQL 8.0.<br />
Comment added to my_exec().</p>
<p>MyEnv Installer</p>
<p>mysql/mariadb fork installation implemented and tested.<br />
Example in installMyEnv corrected/improved.<br />
Made nasty warning during installation of new instance go away.</p>
<p>MyEnv Utilities</p>
<p>New script added.<br />
SSL added to monitor.<br />
Slave monitor made MySQL 8+ ready.<br />
checksum_table works now also with tables consisting of protected keywords.<br />
More debugging info added.<br />
All scripts checked by shellcheck.<br />
Missing runtime directory is caught and fixed now.<br />
shellcheck suggestions applied.<br />
checksum_table.sh added.<br />
Utility scripts brought to new state.<br />
Build slave script added.<br />
table diff refactored.<br />
Chunking added.<br />
table_diff is now ready for chunking.<br />
Faster checksum implemented and output shortened.<br />
Row by row crc32 checksum.<br />
Moved the checksum table in its own function.<br />
table_diff.php added.</p>
<p>PostgreSQL</p>
<p>show_create_table.sh for PostgreSQL added.<br />
PostgreSQL files added here until we have a better location.</p>
<p>General</p>
<p>Code clean-up and configuration check added.<br />
Recursive directory removal improved.<br />
rc made unique.<br />
Minor typos fixed.<br />
my.cnf.template cleaned-up and synced with website.<br />
2 bugs with PHP 8.5 fixed.<br />
Libraries updated.<br />
Some return codes can be ignored because Redhat tools return error codes &#62; 100 as OK.</p>
<p>Documentation</p>
<p>Documentation added and ready to start.</p>
<p>Packaging</p>
<p>Distro versions centralized in one place.<br />
Zabbix repo added for Ubuntu 24.04.<br />
Rocky 10 and Debian 11 added.<br />
lxc replaced by incus, distros cleaned-up.<br />
Ubuntu 24.04 and MariaDB 10.11 and 11.4 enabled.<br />
brman as build project added.<br />
glb included in build scripts.<br />
restartContainer implemented.<br />
gid and uid added to pushFileToContainer.<br />
Build cleaned-up.<br />
Build scrips improved.<br />
Container file push and pull added.<br />
Build infrastructure reorganized.<br />
stopContainer and startContainer added.<br />
Container library started.<br />
Debian 10 removed.<br />
Update container script is waiting to avoid infrastructure build failure.<br />
Old package for RPM changed.<br />
install_base separted from mysql_home.<br />
Installation moved from /home/mysql to /opt.<br />
Fix DEB package.<br />
Ubuntu 22.04 added for package build.<br />
Bug in Debian build script fixed.<br />
Ubuntu 24.04 added to build infrastructure.<br />
recreate_build_infrastructure.sh rewritten in PHP.<br />
update_container_templates.sh rewritten in PHP.<br />
Made package build infrastructure more generic.</p>
<p>For subscriptions of commercial use of MyEnv please get in contact with us.</p>
<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/">MariaDB/MySQL Environment MyEnv 2.1.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.1.1 of its popular MariaDB, Galera Cluster and MySQL multi-instance environment <a href="https://www.fromdual.com/software/fromdual-myenv/" title="MariaDB, MySQL and PostgreSQL multi-instance environment">MyEnv</a>.</p>
<p>The new MyEnv can be downloaded <a href="https://support.fromdual.com/admin/public/download.php" target="_blank" title="FromDual download">here</a>. How to install MyEnv is described in the <a href="https://support.fromdual.com/documentation/myenv/myenv.html" target="_blank">MyEnv Installation Guide</a>.</p>
<p>In the inconceivable case that you find a bug in MyEnv please report it to us by sending an <a href="mailto:contact@fromdual.com?Subject=Bug%20report%20for%20myenv">email</a>.</p>
<p>Any feedback, statements and testimonials are welcome as well! Please <a href="mailto:feedback@fromdual.com?Subject=Feedback%20for%20fpmmm">send them to us</a>.</p>
<h2>Upgrade from 1.1.x to 2.0<a class="anchor-link" id="upgrade-from-1-1-x-to-2-0"></a></h2>
<p>Please look at the <a href="https://www.fromdual.com/mysql-mariadb-environment-myenv-2.0.0-has-been-released" title="MySQL Environment MyEnv 2.0.0 has been released">MyEnv 2.0.0 Release Notes</a>.</p>
<h2>Upgrade from 2.0.x to 2.1.1<a class="anchor-link" id="upgrade-from-2-0-x-to-2-1-1"></a></h2>
<pre><code>$ sudo -i -u mysql
$ cd ${HOME}/product
$ tar xf /download/myenv-2.1.1.tar.gz
$ rm -f myenv
$ ln -s myenv-2.1.1 myenv
</code></pre>
<h3>Plug-ins<a class="anchor-link" id="plug-ins"></a></h3>
<p>If you are using plug-ins for <code>showMyEnvStatus</code> create all the links in the new directory structure:</p>
<pre><code>$ cd ${HOME}/product/myenv
$ ln -s ../../utl/oem_agent.php plg/showMyEnvStatus/
</code></pre>
<h3>Upgrade of the instance directory structure<a class="anchor-link" id="upgrade-of-the-instance-directory-structure"></a></h3>
<p>From MyEnv 1.0 to 2.0 the directory structure of instances has fundamentally changed. Nevertheless MyEnv 2.0 works fine with MyEnv 1.0 directory structures.</p>
<h2>Changes in MyEnv 2.1.1<a class="anchor-link" id="changes-in-myenv-2-1-1"></a></h2>
<h3>MyEnv<a class="anchor-link" id="myenv"></a></h3>
<ul>
<li><code>addInstance</code> for PostgreSQL added.</li>
<li>Basic PostgreSQL functionality implemented.</li>
<li><code>up</code> working now.</li>
<li><code>stop</code>, <code>start</code>, <code>restart</code> and <code>status</code> implemented for PostgreSQL.</li>
<li>Version extraction for PostgreSQL added.</li>
<li>Function <code>extractVersion</code> rewritten so section config is passed and not <code>basedir</code> any more.</li>
<li>Configuration type <code>mysqld</code> changed to <code>mysql</code> and now also usable for <code>mariadb</code> and <code>postgresql</code>.</li>
<li>New variant of version comment added for MySQL 8.0.</li>
<li>Comment added to <code>my_exec()</code>.</li>
</ul>
<h3>MyEnv Installer<a class="anchor-link" id="myenv-installer"></a></h3>
<ul>
<li>mysql/mariadb fork installation implemented and tested.</li>
<li>Example in <code>installMyEnv</code> corrected/improved.</li>
<li>Made nasty warning during installation of new instance go away.</li>
</ul>
<h3>MyEnv Utilities<a class="anchor-link" id="myenv-utilities"></a></h3>
<ul>
<li>New script added.</li>
<li>SSL added to monitor.</li>
<li>Slave monitor made MySQL 8+ ready.</li>
<li><code>checksum_table</code> works now also with tables consisting of protected keywords.</li>
<li>More debugging info added.</li>
<li>All scripts checked by <code>shellcheck</code>.</li>
<li>Missing runtime directory is caught and fixed now.</li>
<li><code>shellcheck</code> suggestions applied.</li>
<li><code>checksum_table.sh</code> added.</li>
<li>Utility scripts brought to new state.</li>
<li>Build slave script added.</li>
<li>table diff refactored.</li>
<li>Chunking added.</li>
<li><code>table_diff</code> is now ready for chunking.</li>
<li>Faster checksum implemented and output shortened.</li>
<li>Row by row crc32 checksum.</li>
<li>Moved the checksum table in its own function.</li>
<li><code>table_diff.php</code> added.</li>
</ul>
<h3>PostgreSQL<a class="anchor-link" id="postgresql"></a></h3>
<ul>
<li><code>show_create_table.sh</code> for PostgreSQL added.</li>
<li>PostgreSQL files added here until we have a better location.</li>
</ul>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Code clean-up and configuration check added.</li>
<li>Recursive directory removal improved.</li>
<li>rc made unique.</li>
<li>Minor typos fixed.</li>
<li><code>my.cnf.template</code> cleaned-up and synced with website.</li>
<li>2 bugs with PHP 8.5 fixed.</li>
<li>Libraries updated.</li>
<li>Some return codes can be ignored because Redhat tools return error codes &gt; 100 as OK.</li>
</ul>
<h3>Documentation<a class="anchor-link" id="documentation"></a></h3>
<ul>
<li>Documentation added and ready to start.</li>
</ul>
<h3>Packaging<a class="anchor-link" id="packaging"></a></h3>
<ul>
<li>Distro versions centralized in one place.</li>
<li>Zabbix repo added for Ubuntu 24.04.</li>
<li>Rocky 10 and Debian 11 added.</li>
<li><code>lxc</code> replaced by <code>incus</code>, distros cleaned-up.</li>
<li>Ubuntu 24.04 and MariaDB 10.11 and 11.4 enabled.</li>
<li><code>brman</code> as build project added.</li>
<li><code>glb</code> included in build scripts.</li>
<li><code>restartContainer</code> implemented.</li>
<li><code>gid</code> and <code>uid</code> added to <code>pushFileToContainer</code>.</li>
<li>Build cleaned-up.</li>
<li>Build scrips improved.</li>
<li>Container file push and pull added.</li>
<li>Build infrastructure reorganized.</li>
<li><code>stopContainer</code> and <code>startContainer</code> added.</li>
<li>Container library started.</li>
<li>Debian 10 removed.</li>
<li>Update container script is waiting to avoid infrastructure build failure.</li>
<li>Old package for RPM changed.</li>
<li><code>install_base</code> separted from <code>mysql_home</code>.</li>
<li>Installation moved from <code>/home/mysql</code> to <code>/opt</code>.</li>
<li>Fix DEB package.</li>
<li>Ubuntu 22.04 added for package build.</li>
<li>Bug in Debian build script fixed.</li>
<li>Ubuntu 24.04 added to build infrastructure.</li>
<li><code>recreate_build_infrastructure.sh</code> rewritten in PHP.</li>
<li><code>update_container_templates.sh</code> rewritten in PHP.</li>
<li>Made package build infrastructure more generic.</li>
</ul>
<p>For subscriptions of commercial use of MyEnv please <a href="mailto:contact@fromdual.com?Subject=Commercial%20use%20of%20MyEnv">get in contact</a> with us.</p>

<p><a href="https://www.fromdual.com/blog/myenv-release-notes/fromdual-environment-myenv-2.1.1-has-been-released/">MariaDB/MySQL Environment MyEnv 2.1.1 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Under Construction: Building the MariaDB Benchmarking Test Automation Framework (TAF)</title>
      <link rel="alternate" type="text/html" href="https://mysql-qa.blogspot.com/2025/12/under-construction-building-mariadb.html" />
      <id>https://mysql-qa.blogspot.com/2025/12/under-construction-building-mariadb.html</id>
      <updated>2025-12-12T13:48:00+02:00</updated>
      <author><name>jbm</name></author>
      <summary type="html"><![CDATA[<p>Building a New Test Automation Framework for MariaDB</p>
<p>MariaDB has given me the chance to pursue a lifelong dream: creating a new Test Automation Framework (TAF) — an improved, expanded evolution of the Autobench3 framework I originally built for MySQL.</p>
<p>Autobench3 was never just a benchmark API. It was a framework that wrapped benchmark APIs to provide a consistent platform for configuration and abstraction. Instead of requiring deep knowledge of each benchmark tool, Autobench3 allowed developers to work with simple command‑line options and property files. The framework made clear what suite was being run, what test case was in play, which database software was targeted, and what overrides were applied.</p>
<p>When running, the framework would load the chosen test suite into itself — becoming Sysbench, becoming DBT2 — and then drive the workload through a consistent set of lifecycle stages.</p>
<p>Autobench3: Still in Service, But Limited</p>
<p>Autobench3 continues to serve MySQL well in worklog development, commit monitoring, and release performance testing. Yet it has important limitations:</p>
<p> Hardwired to MySQL only — no MariaDB, PostgreSQL, or other database support. XML results only — output tied directly to the Automated Test Results (ATR) parser, database, and web stack. Single‑install limitation — only one MySQL install could be active at a time, with no concept of an active install marker. Large driver size — even with helper libraries, the driver was nearly 9,000 lines of code. </p>
<p>When Oracle ended my role, I was crushed. I had just finished coding developer compare functionality through Gerrit/Jenkins/AB3 and was dreaming of a lights‑out solution: automated detection of performance regressions, commit hunting, compare runs, profiling, and automatic outreach to developers.</p>
<p>A New Beginning with MariaDB</p>
<p>Then came an unbelievable opportunity — from the very family that started MySQL and MariaDB. The CEO reached out and asked if I wanted to rebuild a framework, perform benchmarking, and blog about improving MariaDB’s performance.</p>
<p>My answer was immediate: Absolutely.</p>
<p>Since then, I’ve been heads‑down building. November was spent creating the driver and the first beta test suites:</p>
<p> Sysbench‑Lua with BMK abilities (BMK) HammerDB for TPROC-C and TPROC-H (HammerDB) </p>
<p>With the driver running and suites loading correctly, I put them to use.</p>
<p>Why Profile Guided Optimization (PGO) Matters</p>
<p>One thing many don’t realize: Oracle’s Enterprise Editions of MySQL are PGO (Profile Guided Optimized) builds. That optimization is locked behind a paywall. The biggest gains from PGO show up in cached database workloads — point‑selects and read‑heavy cases in particular.</p>
<p>When I built MariaDB 11.8.3 both with and without PGO, the difference was undeniable: throughput improvements of up to +30% in many test cases, with nearly every run showing measurable gains. And here’s the key point — MariaDB can deliver those optimizations for free.</p>
<p>Just as thread pool was once a feature you had to pay Oracle for in MySQL but is freely available in MariaDB, my hope is that PGO builds will become another example of MariaDB giving the community what Oracle keeps gated.</p>
<p>Framework Progress</p>
<p>Today, the framework is under heavy construction:</p>
<p> Core driver and test suites (basic beta form, still need more testing) </p>
<p>This is not a finished product. The current pieces are proof‑of‑concept only, showing that the driver can load and run suites correctly. Much more work remains before it’s ready for general use. The next stages include:</p>
<p> Expanding database plugin logic (MariaDB, MySQL, and a generic layout for PostgreSQL, MSSQL, etc.) Adding support for multiple unpacked installs with easy switching Continuing to refine plugin‑based reporters </p>
<p>Call to Action</p>
<p>My hope is that when the framework is released — as a free product under the MariaDB Foundation, just as the Foundation always does — others will download it, use it, and contribute. Not only by improving the framework itself, but also by reporting any performance issues they encounter with MariaDB while using it.</p>
<p>Together, we can make MariaDB better for everyone.</p>
<p>When beta is released it will just be the beginning. There’s plenty of growing up to do, and I welcome feedback — good, bad, even brutal. I won’t take it personally; I’ll take it as proof that you care.</p>
<p>Stay tuned!</p>
<p>#MariaDB #MariaDBFoundation #MySQL #PerformanceTesting</p>
<p><a href="https://mysql-qa.blogspot.com/2025/12/under-construction-building-mariadb.html">Under Construction: Building the MariaDB Benchmarking Test Automation Framework (TAF)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h1>Building a New Test Automation Framework for MariaDB<a class="anchor-link" id="building-a-new-test-automation-framework-for-mariadb"></a></h1>
<p>MariaDB has given me the chance to pursue a lifelong dream: creating a new Test Automation Framework (TAF) &mdash; an improved, expanded evolution of the Autobench3 framework I originally built for MySQL.</p>
<p>Autobench3 was never just a benchmark API. It was a framework that wrapped benchmark APIs to provide a consistent platform for configuration and abstraction. Instead of requiring deep knowledge of each benchmark tool, Autobench3 allowed developers to work with simple command&#8209;line options and property files. The framework made clear what suite was being run, what test case was in play, which database software was targeted, and what overrides were applied.</p>
<p>When running, the framework would load the chosen test suite into itself &mdash; becoming Sysbench, becoming DBT2 &mdash; and then drive the workload through a consistent set of lifecycle stages.</p>
<h2>Autobench3: Still in Service, But Limited<a class="anchor-link" id="autobench3-still-in-service-but-limited"></a></h2>
<p>Autobench3 continues to serve MySQL well in worklog development, commit monitoring, and release performance testing. Yet it has important limitations:</p>
<ul>
<li>Hardwired to MySQL only &mdash; no MariaDB, PostgreSQL, or other database support.</li>
<li>XML results only &mdash; output tied directly to the Automated Test Results (ATR) parser, database, and web stack.</li>
<li>Single&#8209;install limitation &mdash; only one MySQL install could be active at a time, with no concept of an active install marker.</li>
<li>Large driver size &mdash; even with helper libraries, the driver was nearly 9,000 lines of code.</li>
</ul>
<p>When Oracle ended my role, I was crushed. I had just finished coding developer compare functionality through Gerrit/Jenkins/AB3 and was dreaming of a lights&#8209;out solution: automated detection of performance regressions, commit hunting, compare runs, profiling, and automatic outreach to developers.</p>
<h2>A New Beginning with MariaDB<a class="anchor-link" id="a-new-beginning-with-mariadb"></a></h2>
<p>Then came an unbelievable opportunity &mdash; from the very family that started MySQL and MariaDB. The CEO reached out and asked if I wanted to rebuild a framework, perform benchmarking, and blog about improving MariaDB&rsquo;s performance.</p>
<p>My answer was immediate: Absolutely.</p>
<p>Since then, I&rsquo;ve been heads&#8209;down building. November was spent creating the driver and the first beta test suites:</p>
<ul>
<li><a href="https://github.com/akopytov/sysbench" target="_blank" rel="noopener noreferrer">Sysbench&#8209;Lua</a> with BMK abilities (<a href="http://dimitrik.free.fr/blog/posts/mysql-perf-bmk-kit.html" target="_blank" rel="noopener noreferrer">BMK</a>)</li>
<li>HammerDB for TPROC-C and TPROC-H (<a href="https://www.hammerdb.com/" target="_blank" rel="noopener noreferrer">HammerDB</a>)</li>
</ul>
<p>With the driver running and suites loading correctly, I put them to use.</p>
<h2>Why Profile Guided Optimization (PGO) Matters<a class="anchor-link" id="why-profile-guided-optimization-pgo-matters"></a></h2>
<p>One thing many don&rsquo;t realize: Oracle&rsquo;s Enterprise Editions of MySQL are PGO (Profile Guided Optimized) builds. That optimization is locked behind a paywall. The biggest gains from PGO show up in cached database workloads &mdash; point&#8209;selects and read&#8209;heavy cases in particular.</p>
<p>When I built MariaDB 11.8.3 both with and without PGO, the difference was undeniable: throughput improvements of up to +30% in many test cases, with nearly every run showing measurable gains. And here&rsquo;s the key point &mdash; MariaDB can deliver those optimizations for free.</p>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6_HD7Omfetm6c-yMNTu2xGD8lW80_ijPot9Vicj8nayiYdMGuuv_Xtg8bayjEAcP6LcqzqmPRahEmwJre-CAtlYt32WLXNPhuLguksPePOuwTv6YH__w0dNceojnl9EvADAvKNz1yMRhP3RpCBierSqV4lGAep2oaeV4GKJsEG5qZquUDS5hxCw/s931/OLTP_RO_NON_VS_PGO.png"><img decoding="async" alt="" border="0" width="320" data-original-height="579" data-original-width="931" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj6_HD7Omfetm6c-yMNTu2xGD8lW80_ijPot9Vicj8nayiYdMGuuv_Xtg8bayjEAcP6LcqzqmPRahEmwJre-CAtlYt32WLXNPhuLguksPePOuwTv6YH__w0dNceojnl9EvADAvKNz1yMRhP3RpCBierSqV4lGAep2oaeV4GKJsEG5qZquUDS5hxCw/s320/OLTP_RO_NON_VS_PGO.png"></a></div>
<div class="separator"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijp9TUnq-0rqtCsvU7JltJxs-N24Egq4SNp6ZdX8toVxbnKmVAodGJHN5tBTe4pEIHl3Sji6tGUxEjk9zjKrok_vLkOm4yjmOIzinsAZHcPNAkHUwcgpDlq_7VBiTOINKWIrL-Iw3qJjI9ShnRgxKk-c59kdlg4TgJ9TMLc6UNkg_V-gpyRYm_Bg/s1011/TPCC_NON_VS_PGO.png"><img decoding="async" alt="" border="0" width="320" data-original-height="748" data-original-width="1011" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEijp9TUnq-0rqtCsvU7JltJxs-N24Egq4SNp6ZdX8toVxbnKmVAodGJHN5tBTe4pEIHl3Sji6tGUxEjk9zjKrok_vLkOm4yjmOIzinsAZHcPNAkHUwcgpDlq_7VBiTOINKWIrL-Iw3qJjI9ShnRgxKk-c59kdlg4TgJ9TMLc6UNkg_V-gpyRYm_Bg/s320/TPCC_NON_VS_PGO.png"></a></div>
<p>Just as thread pool was once a feature you had to pay Oracle for in MySQL but is freely available in MariaDB, my hope is that PGO builds will become another example of MariaDB giving the community what Oracle keeps gated.</p>
<h2>Framework Progress<a class="anchor-link" id="framework-progress"></a></h2>
<p>Today, the framework is under heavy construction:</p>
<ul>
<li>Core driver and test suites (basic beta form, still need more testing)</li>
</ul>
<p>This is not a finished product. The current pieces are proof&#8209;of&#8209;concept only, showing that the driver can load and run suites correctly. Much more work remains before it&rsquo;s ready for general use. The next stages include:</p>
<ul>
<li>Expanding database plugin logic (MariaDB, MySQL, and a generic layout for PostgreSQL, MSSQL, etc.)</li>
<li>Adding support for multiple unpacked installs with easy switching</li>
<li>Continuing to refine plugin&#8209;based reporters</li>
</ul>
<h2>Call to Action<a class="anchor-link" id="call-to-action"></a></h2>
<p>My hope is that when the framework is released &mdash; as a free product under the MariaDB Foundation, just as the Foundation always does &mdash; others will download it, use it, and contribute. Not only by improving the framework itself, but also by reporting any performance issues they encounter with MariaDB while using it.</p>
<p>Together, we can make MariaDB better for everyone.</p>
<p>When&nbsp;beta is released&nbsp;it will just&nbsp;be the beginning. There&rsquo;s plenty of growing up to do, and I welcome feedback &mdash; good, bad, even brutal. I won&rsquo;t take it personally; I&rsquo;ll take it as proof that you care.</p>
<p>Stay tuned!</p>
<p>#MariaDB #MariaDBFoundation #MySQL #PerformanceTesting</p>

<p><a href="https://mysql-qa.blogspot.com/2025/12/under-construction-building-mariadb.html">Under Construction: Building the MariaDB Benchmarking Test Automation Framework (TAF)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Deploying garbd (Galera Arbitrator Daemon) &#124; MariaDB Galera pt 2</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/deploying-garbd-galera-arbitrator-daemon-mariadb-galera-pt-2/" />
      <id>https://vettabase.com/deploying-garbd-galera-arbitrator-daemon-mariadb-galera-pt-2/</id>
      <updated>2025-12-11T11:08:36+02:00</updated>
      <author><name>Mike Rykmas</name></author>
      <summary type="html"><![CDATA[<p>In the first part of this series, we deployed a 3-node MariaDB Galera Cluster on Ubuntu 24.04. While a 3-node topology provides the best fault tolerance, sometimes you need a simpler setup – for example, a two-node cluster with a lightweight arbitrator to maintain quorum without running a full third MariaDB instance. At Vettabase, we often use this pattern in small or resource-limited environments, where running three full database nodes would be overkill. In this guide, we’ll convert the third database node (Galera3) into a Garbd (Galera Arbitrator Daemon) instance – a tiny yet essential component that helps maintain cluster quorum efficiently. Installing garbd sudo apt update sudo apt install galera-arbitrator-4 -y Configure garbd Create /etc/default/garb GALERA_NODES=\"172.31.2.197:4567,172.31.3.237:4567\" GALERA_GROUP=\"vettabase_galera\" Stop MariaDB and Start garbd sudo systemctl stop mariadb sudo systemctl start garb Verify garbd’ Cluster Membership Once you start the arbitrator service, check its status: sudo systemctl status garb You should see output similar to: INFO: 1.0 (galera1): State transfer to 0.0 (garb) complete. INFO: Member 1.0 (galera1) synced with group. This confirms that garb has successfully joined the cluster and synchronized with the group. Check from a Galera Node On either database node, verify that the cluster size increased to […]</p>
<p><a href="https://vettabase.com/deploying-garbd-galera-arbitrator-daemon-mariadb-galera-pt-2/">Deploying garbd (Galera Arbitrator Daemon) | MariaDB Galera pt 2</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<div>In the <a href="https://vettabase.com/installing-a-3-node-mariadb-galera-cluster-on-ubuntu-24-04">first part</a> of this series, we deployed a 3-node MariaDB Galera Cluster on Ubuntu 24.04.</div>
<div>While a 3-node topology provides the best fault tolerance, sometimes you need a simpler setup &ndash; for example, a two-node cluster with a lightweight arbitrator to maintain quorum without running a full third MariaDB instance.</div>
<div>At Vettabase, we often use this pattern in small or resource-limited environments, where running three full database nodes would be overkill.</div>
<div>In this guide, we&rsquo;ll convert the third database node (Galera3) into a Garbd (Galera Arbitrator Daemon) instance &ndash; a tiny yet essential component that helps maintain cluster quorum efficiently.</div>
<h1>Installing garbd<a class="anchor-link" id="installing-garbd"></a></h1>
<pre>sudo apt update
sudo apt install galera-arbitrator-4 -y</pre>
<h1>Configure garbd<a class="anchor-link" id="configure-garbd"></a></h1>
<div>Create <strong>/etc/default/garb</strong></div>
<pre>GALERA_NODES="172.31.2.197:4567,172.31.3.237:4567"
GALERA_GROUP="vettabase_galera"</pre>
<h1>Stop MariaDB and Start garbd<a class="anchor-link" id="stop-mariadb-and-start-garbd"></a></h1>
<pre>sudo systemctl stop mariadb
sudo systemctl start garb</pre>
<h1>Verify garbd&rsquo; Cluster Membership<a class="anchor-link" id="verify-garbd-cluster-membership"></a></h1>
<div>Once you start the arbitrator service, check its status:</div>
<pre>sudo systemctl status garb</pre>
<div>You should see output similar to:</div>
<pre>INFO: 1.0 (galera1): State transfer to 0.0 (garb) complete.
INFO: Member 1.0 (galera1) synced with group.</pre>
<div>This confirms that <strong>garb</strong> has successfully joined the cluster and synchronized with the group.</div>
<h1>Check from a Galera Node<a class="anchor-link" id="check-from-a-galera-node"></a></h1>
<div>On either database node, verify that the cluster size increased to 3:</div>
<pre>mariadb -u root -p -S /run/mysqld/mysqld.sock 
&nbsp; -e "SHOW GLOBAL STATUS LIKE 'wsrep_cluster_size';"</pre>
<div>Expected output:</div>
<pre>+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wsrep_cluster_size | 3     |
+--------------------+-------+</pre>
<div>You can also check the cluster view in the logs &ndash; it should show garb alongside your data nodes:</div>
<pre>[Note] WSREP: ================================================
View:
  id:8bb99c56-be58-11f0-b57c-7e793e9a515a:518
  status:primary
  protocol_version:4
  capabilities:MULTI-MASTER,CERTIFICATION,PARALLEL_APPLYING,REPLAY,ISOLATION,PAUSE,CAUSAL_READ,INCREMENTAL_WS,UNORDERED,PREORDERED,STREAMING,NBO
  final:no
  own_index:1
  members(3):
        0:370d8f1e-be6c-11f0-81ee-6ad9baf05d85,garb
        1:52a0c379-be67-11f0-b8ae-3201eb641669,galera1
        2:c66106d9-be64-11f0-a06a-33bd45771681,galera2
=================================================</pre>
<div>The arbitrator node (garb) is now part of the Galera group and maintains quorum without storing any data or running a full MariaDB instance.</div>
<h1>Summary<a class="anchor-link" id="summary"></a></h1>
<div>The Galera cluster now runs with two full data nodes and one lightweight arbitrator. The <strong>garbd</strong>&nbsp;daemon ensures quorum and availability while consuming minimal resources &ndash; ideal for cost-sensitive or resource-constrained environments.</div>
<p><em>Mykhaylo Rykmas</em></p>
<p><strong><a href="https://vettabase.com/vettabase-is-a-mariadb-foundation-sponsor/">Vettabase is a MariaDB Foundation sponsor!</a></strong></p>
<p>&nbsp;</p>

<p><a href="https://vettabase.com/deploying-garbd-galera-arbitrator-daemon-mariadb-galera-pt-2/">Deploying garbd (Galera Arbitrator Daemon) | MariaDB Galera pt 2</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Installing a MariaDB Galera Cluster on Ubuntu 24.04 &#124; MariaDB Galera pt 1</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/installing-a-3-node-mariadb-galera-cluster-on-ubuntu-24-04-mariadb-galera-pt-1/" />
      <id>https://vettabase.com/installing-a-3-node-mariadb-galera-cluster-on-ubuntu-24-04-mariadb-galera-pt-1/</id>
      <updated>2025-12-04T10:37:35+02:00</updated>
      <author><name>Mike Rykmas</name></author>
      <summary type="html"><![CDATA[<p>At Vettabase, we’re starting a new blog series on High Availability (HA) with focus on MariaDB Galera Cluster. This series will be a collection of hands-on guides, each tackling one practical topic: from installation, configuration, and adding or removing nodes, to backups, upgrades, and schema changes. Our goal is simple: create a complete, practical reference that anyone can follow to deploy and maintain a resilient MariaDB Galera cluster. Each article will be concise, command-driven, and easy to reproduce on your own servers and this first post covers the foundation: installing a 3-node MariaDB Galera Cluster on Ubuntu 24.04 LTS. Environment Setup For this setup, we used three AWS EC2 instances (each t3.micro, Free Tier) running Ubuntu 24.04 LTS. Each host is configured for SSH key–based access and passwordless sudo privileges: ssh -i ubuntu@ List of nodes: Galera1: 172.31.2.197 Galera2: 172.31.3.237 Galera3: 172.31.0.181 Galera Architecture Overview MariaDB Galera Cluster is a multi-primary virtually synchronous replication system. That means all nodes (called Galera nodes) can accept both reads and writes, and every transaction is replicated to all others in real time. Key concepts to understand before setup: Cluster: a group of nodes communicating via the gcomm:// protocol. Primary Component: […]</p>
<p><a href="https://vettabase.com/installing-a-3-node-mariadb-galera-cluster-on-ubuntu-24-04-mariadb-galera-pt-1/">Installing a MariaDB Galera Cluster on Ubuntu 24.04 | MariaDB Galera pt 1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>At Vettabase, we&rsquo;re starting a new blog series on High Availability (HA) with focus on MariaDB Galera Cluster. This series will be a collection of hands-on guides, each tackling one practical topic: from installation, configuration, and adding or removing nodes, to backups, upgrades, and schema changes.</p>
<p>Our goal is simple: create a complete, practical reference that anyone can follow to deploy and maintain a resilient MariaDB Galera cluster.</p>
<p>Each article will be concise, command-driven, and easy to reproduce on your own servers and this first post covers the foundation: installing a 3-node MariaDB Galera Cluster on Ubuntu 24.04 LTS.</p>
<p><span></span></p>

<h1>Environment Setup<a class="anchor-link" id="environment-setup"></a></h1>
<p>For this setup, we used three <a target="_blank" rel="noopener">AWS EC2 instances</a> (each t3.micro, Free Tier) running Ubuntu 24.04 LTS. Each host is configured for SSH key&ndash;based access and passwordless sudo privileges:</p>
<pre>ssh -i  ubuntu@</pre>
<p>List of nodes:</p>
<ul>
<li>Galera1: 172.31.2.197</li>
<li>Galera2: 172.31.3.237</li>
<li>Galera3: 172.31.0.181</li>
</ul>
<h1>Galera Architecture Overview<a class="anchor-link" id="galera-architecture-overview"></a></h1>
<p>MariaDB Galera Cluster is a multi-primary virtually synchronous replication system. That means all nodes (called Galera nodes) can accept both reads and writes, and every transaction is replicated to all others in real time.</p>
<p>Key concepts to understand before setup:</p>
<ul>
<li><strong>Cluster</strong>: a group of nodes communicating via the <em>gcomm://</em> protocol.</li>
<li><strong>Primary Component</strong>: the active group of nodes that can process writes.</li>
<li><strong>SST</strong> (State Snapshot Transfer): a full data copy from one node to another when a new node joins the cluster.</li>
<li><strong>IST</strong> (Incremental State Transfer): a sync of only recent changes.</li>
<li><strong>Bootstrap</strong>: the initial action of starting the first node in a Galera cluster. It creates the primary component and defines the cluster&rsquo;s initial state. Only one node should ever be bootstrapped. All other nodes must join it.</li>
</ul>
<h2>Galera&rsquo;s Quorum<a class="anchor-link" id="galeras-quorum"></a></h2>
<div>
<div>Galera Cluster relies on a quorum-based decision system to maintain data consistency and prevent split-brain situations. Quorum means that <strong>more than half of the nodes</strong> must be online for the cluster to remain operational (Primary Component). If the quorum is lost &ndash; for example, if two of three nodes suddenly crash &ndash; the remaining node automatically switches to a non-primary state and stops accepting writes to prevent data divergence.</div>
<div></div>
<div>That&rsquo;s why a <strong>3-node setup</strong> is recommended: it guarantees that even if one node fails, the remaining two can still reach quorum and continue processing writes safely. If 3 nodes are not sufficient (which isn&rsquo;t common), 5 nodes are recommended, so the quorum will consist in 3 nodes.</div>
</div>
<h2>Installation<a class="anchor-link" id="installation"></a></h2>
<div>
<div>
<p>The installation process is well documented on the <a href="https://mariadb.com/docs/server/server-management/install-and-upgrade-mariadb/installing-mariadb/binary-packages/mariadb-package-repository-setup-and-usage" rel="noopener">official MariaDB documentation</a>, and we&rsquo;ll be following those steps here.</p>
<p>Run the following commands on each node:</p>
</div>
<div>
<pre>curl -LsSO https://r.mariadb.com/downloads/mariadb_repo_setup</pre>
<div>
<div>Before executing it, verify the checksum to make sure you downloaded the original MariaDB script. Check the current checksum on the MariaDB page above, then run:</div>
</div>
<div>
<pre>checksum=923eea378be2c129adb4d191f01162c1fe5473f1114d7586f096b5f6b9874efe
echo "${checksum} mariadb_repo_setup" | sha256sum -c -</pre>
<div>
<div>Expected output:</div>
<div>
<pre>mariadb_repo_setup: OK</pre>
<div>Make the script executable:</div>
<pre>chmod +x mariadb_repo_setup</pre>
<h2>Adding the MariaDB 11.8 Repository<a class="anchor-link" id="adding-the-mariadb-11-8-repository"></a></h2>
<div>
<div>At the time of this writing, the latest LTS (Long-Term Support) release of MariaDB at the time of writing is MariaDB 11.8.</div>
<div>Run the following command to add the repository:</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<pre>sudo ./mariadb_repo_setup --mariadb-server-version="mariadb-11.8"</pre>
<div>
<div>Expected output:</div>
<div>
<pre>[info] Checking for script prerequisites.
[info] MariaDB Server version 11.8 is valid
[info] Repository file successfully written to /etc/apt/sources.list.d/mariadb.list
[info] Adding trusted package signing keys...
[info] Running apt-get update...
[info] Done adding trusted package signing keys</pre>
<div>
<h2>Install the Required Packages<a class="anchor-link" id="install-the-required-packages"></a></h2>
<div>
<div>Finally, install MariaDB and Galera components:</div>
<div>
<pre>sudo apt install mariadb-server mariadb-client mariadb-backup galera-4 -y</pre>
<div>
<div>This installs:</div>
<ul>
<li><strong>mariadb-server</strong>: main database engine</li>
<li><strong>mariadb-client</strong>: client tools (mysql, mariadb, etc.)</li>
<li><strong>mariadb-backup</strong>: backup utility</li>
<li><strong>galera-4</strong>: synchronous replication provider for the Galera cluster</li>
</ul>
<div>
<h2>Secure the MariaDB Installation<a class="anchor-link" id="secure-the-mariadb-installation"></a></h2>
<div>
<div>After installing MariaDB and Galera components, it&rsquo;s recommended to run the built-in hardening script. This will remove anonymous users, disable remote root login, and secure your installation.</div>
<pre>sudo mariadb-secure-installation</pre>
<div>
<div>You&rsquo;ll be prompted to:</div>
<ul>
<li>Switch to unix_socket authentication &ndash; (recommended: Yes)</li>
<li>Remove anonymous users &ndash; (Yes)</li>
<li>Disallow root login remotely &ndash; (Yes)</li>
<li>Remove test database &ndash; (Yes)</li>
<li>Reload privilege tables &ndash; (Yes)</li>
</ul>
<div>
<div>Once completed, your MariaDB instance will be more secure and ready for Galera configuration.</div>
</div>
<div>
<h1>Configure the First (Bootstrap) Node<a class="anchor-link" id="configure-the-first-bootstrap-node"></a></h1>
<div>
<div>We&rsquo;ll start by configuring the first node. This is the one that will bootstrap the cluster and initialize the primary component. After that, the remaining nodes will simply join it and synchronize automatically.</div>
<div>For reference, our cluster nodes are:</div>
<ul>
<li>Galera1: 172.31.2.197 (bootstrap node)</li>
<li>Galera2: 172.31.3.237</li>
<li>Galera3: 172.31.0.181</li>
</ul>
<div>
<h2>Minimal Configuration<a class="anchor-link" id="minimal-configuration"></a></h2>
<div>
<div>Instead of modifying the default configuration files, it&rsquo;s a good practice to split the configuration into two separate files:</div>
<div>one for core MariaDB settings, and one for Galera-specific options.</div>
<div>This keeps things clean and easier to manage.</div>
<ul>
<li><strong>/etc/mysql/my.cnf</strong></li>
</ul>
<div>
<pre>[mariadbd]
# Basic settings
binlog_format=ROW
default_storage_engine=InnoDB
innodb_autoinc_lock_mode=2
bind-address=0.0.0.0
log_error=/var/log/mysql/mariadb.errsocket=/run/mysqld/mysqld.sock

# Innodb
innodb_force_primary_key=1

# Galera settings wsrep_on=ON
</pre>
</div>
<div>
<div>
<pre>wsrep_provider=/usr/lib/galera/libgalera_smm.so
wsrep_cluster_name=""
wsrep_cluster_address="gcomm://"

# Node identity (change per node)
wsrep_node_name=
wsrep_node_address=""

# SST configuration
wsrep_sst_method=mariabackup
wsrep_sst_auth="sstuser:sstpassword"</pre>
</div>
</div>
<div>
<div>This layout separates the core database configuration from the cluster logic, making it easier to upgrade MariaDB, manage changes, or temporarily disable Galera (for example, during maintenance).</div>
<div>Galera Parameters Explanation:</div>
<ul>
<li><strong>wsrep_on</strong> &ndash; enables Galera replication.</li>
<li><strong>wsrep_provider</strong> &ndash; path to the Galera library (libgalera_smm.so), required for replication to function.</li>
<li><strong>wsrep_cluster_name</strong> &ndash; logical name of the cluster; all nodes must use the same name.</li>
<li><strong>wsrep_cluster_address</strong> &ndash; list of all cluster node IPs separated by commas in the format `gcomm://IP1,IP2,IP3`. During bootstrap, this list tells Galera which nodes to contact.</li>
<li><strong>wsrep_node_name</strong> &ndash; a unique name for the node within the cluster.</li>
<li><strong>wsrep_node_address</strong> &ndash; the IP address used for replication traffic.</li>
<li><strong>wsrep_sst_method</strong> &ndash; defines the method for State Snapshot Transfer (SST) &ndash; the process of copying full data from one node to another.
<ul>
<li><strong>rsync</strong> &ndash; simple and easy to configure.</li>
<li><strong>mariabackup</strong> &ndash; preferred for large datasets (non-blocking, hot backup).</li>
</ul>
</li>
<li><strong>wsrep_sst_auth</strong> &ndash; credentials used by the donor node during SST.
<ul>
<li>Format: <strong>&ldquo;username:password&rdquo;</strong>.</li>
<li>This user must have privileges: <strong>RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT</strong>.</li>
</ul>
</li>
</ul>
<p class="p1">Add <strong><span class="s1">innodb_force_primary_key=1</span></strong> to ensure all InnoDB tables have a primary key, as Galera requires PKs for consistent row replication and to prevent write conflicts or data divergence.</p>
<div>
<div></div>
<div>Example for galera1:</div>
<ul>
<li><strong>/etc/mysql/my.cnf</strong></li>
</ul>
<pre>[mariadbd]
# Basic settings
binlog_format=ROW
default_storage_engine=InnoDB
innodb_autoinc_lock_mode=2
bind-address=0.0.0.0
log_error=/var/log/mysql/mariadb.err
socket=/run/mysqld/mysqld.sock

# Innodb
innodb_force_primary_key=1

# Galera settings
wsrep_on=ON
wsrep_provider=/usr/lib/galera/libgalera_smm.so
wsrep_cluster_name="vettabase_galera"
wsrep_cluster_address="gcomm://172.31.2.197,172.31.3.237,172.31.0.181"

# Node identity
wsrep_node_name=galera1
wsrep_node_address="172.31.2.197"

# SST configuration
wsrep_sst_method=mariabackup
wsrep_sst_auth="sst_user:sst_password"</pre>
</div>
</div>
<div>
<h2>Bootstrap the First Node<a class="anchor-link" id="bootstrap-the-first-node"></a></h2>
<div>
<div>Once the configuration files are in place on all nodes, we can bootstrap the cluster &ndash; this step initializes the very first node and creates the Primary Component of the Galera cluster.</div>
<div>Only <strong>one node</strong> should ever be bootstrapped. All other nodes will join it automatically.</div>
<div>Run the following command on Galera1:</div>
<pre>sudo systemctl stop mariadb
sudo galera_new_cluster</pre>
<div>Check that MariaDB is running:</div>
<pre>sudo systemctl status mariadb</pre>
<div>Then verify the Galera cluster status by running:</div>
<pre>mariadb -u root -p -S /run/mysqld/mysqld.sock 
  -e "SHOW GLOBAL STATUS LIKE 'wsrep%'" 
  | grep -E "^wsrep_(cluster_size|cluster_status|local_state_comment|ready)"</pre>
<div>Expected output:</div>
<pre>wsrep_local_state_comment Synced
wsrep_cluster_size 1
wsrep_cluster_status Primary
wsrep_ready ON</pre>
<div>Explanation:</div>
<ul>
<li><strong>wsrep_local_state_comment = Synced</strong>: the node is operational and ready.</li>
<li><strong>wsrep_cluster_size = 1</strong>: the node has formed a cluster.</li>
<li><strong>wsrep_cluster_status = Primary</strong>: the cluster has quorum.</li>
<li><strong>wsrep_ready = ON</strong>: the node can accept queries.</li>
</ul>
<div>If all these values are correct, <strong>Galera1 node has been successfully bootstrapped</strong> and is ready for other nodes to join.</div>
<h2>Create the SST User<a class="anchor-link" id="create-the-sst-user"></a></h2>
<div>Before adding the remaining nodes, we need to create a dedicated user SST. This user will allow the donor node (currently Galera1) to authenticate and send data during SST.</div>
<div>Connect to MariaDB on Galera1:</div>
<pre>mariadb -u root -p -S /run/mysqld/mysqld.sock</pre>
<div>Then execute the following SQL commands:</div>
<pre>CREATE USER 'sst_user'@'%' IDENTIFIED BY 'sst_password';
GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO 'sst_user'@'%';
FLUSH PRIVILEGES;</pre>
<div>
<p>Once created, the node is fully ready to act as an SST donor and replicate data to other cluster members.</p>
</div>
</div>
</div>
</div>
<div>
<h1>Joining the Remaining Nodes<a class="anchor-link" id="joining-the-remaining-nodes"></a></h1>
<div>With the first node bootstrapped and the SST user created, we can now add the remaining nodes (<strong>Galera2</strong> and <strong>Galera3</strong>) to the cluster. These nodes will automatically synchronize with <strong>Galera1</strong> using the configured <strong>SST method</strong>.</div>
<h2>Verify Configuration on Each Node<a class="anchor-link" id="verify-configuration-on-each-node"></a></h2>
<div>Make sure the configuration file <strong>/etc/mysql/my.cnf</strong> on <strong>Galera2</strong> and <strong>Galera3</strong> is correct:</div>
<ul>
<li>The IP list in <strong>wsrep_cluster_address</strong> contains all three nodes.</li>
<li>Each node has its own unique <strong>wsrep_node_name</strong> and <strong>wsrep_node_address</strong>.</li>
<li>The same <strong>wsrep_cluster_name </strong>and <strong>wsrep_sst_auth</strong> credentials are used as on <strong>Galera1</strong>.</li>
</ul>
<div>Example for <strong>Galera2</strong> (172.31.3.237):</div>
<pre>wsrep_node_name=galera2
wsrep_node_address="172.31.3.237"</pre>
<div>Example for <strong>Galera3</strong>&nbsp;(172.31.0.181):</div>
<pre>wsrep_node_name=galera2
wsrep_node_address="172.31.0.181"</pre>
<h2>Start MariaDB on Each Node<a class="anchor-link" id="start-mariadb-on-each-node"></a></h2>
<div>Now start MariaDB one node at a time (first <strong>Galera2</strong>, then <strong>Galera3</strong>):</div>
<pre>sudo systemctl restart mariadb</pre>
<div>When restarting or adding new nodes, you can follow the logs in <strong>/var/log/mysql/mariadb.err</strong>&nbsp;to monitor the synchronization process. A healthy cluster should show messages similar to these:</div>
<pre>WSREP: Server galera1 synced with group
WSREP: Server status change joined -&gt; synced
WSREP: Synchronized with group, ready for connections
WSREP: New COMPONENT: primary = yes, bootstrap = no, my_idx = 0, memb_num = 2
WSREP: IST request ... tcp://172.31.3.237:4568
WSREP: 1.0 (galera2): State transfer from 0.0 (galera1) complete.
WSREP: Member 1.0 (galera2) synced with group.</pre>
<div>The latest line means your cluster is fully operational. All nodes are synchronized and ready to accept client connections.</div>
<h1>Galera Health Check<a class="anchor-link" id="galera-health-check"></a></h1>
<div>To confirm that your Galera Cluster is healthy and synchronized across all nodes, run the following command <strong>on all nodes</strong>:</div>
<pre>mariadb -u root -p -S /run/mysqld/mysqld.sock 
  -e "SHOW GLOBAL STATUS LIKE 'wsrep%'" 
  | grep -E "^wsrep_(cluster_size|cluster_status|local_state_comment|ready)"</pre>
<div>Expected output:</div>
<pre>wsrep_local_state_comment Synced
wsrep_cluster_size 3
wsrep_cluster_status Primary
wsrep_ready ON</pre>
<div><strong>wsrep_cluster_size = 3 </strong>means all three nodes are connected to the cluster.</div>
<h1>Summary<a class="anchor-link" id="summary"></a></h1>
<div>In this first post of our Vettabase High Availability series, we built a fully functional 3-node MariaDB Galera Cluster on Ubuntu 24.04.</div>
<div></div>
<div>We covered everything from installing MariaDB and configuring Galera parameters to bootstrapping the first node and verifying cluster health. At this point you should have a stable, synchronized cluster.</div>
<div></div>
<div>In the next post, we&rsquo;ll cover optional deployment of garbd (Galera Arbitrator Daemon). While a 3-node cluster is the recommended and most resilient topology, allowing the cluster to remain operational even if one node is down, garbd can be useful in scenarios where you temporarily need quorum support without running a full additional database node.</div>
<div></div>
</div>
<div>See: <a href="https://vettabase.com/deploying-garbd-galera-arbitrator-daemon-mariadb-galera-pt-2/">Deploying garbd (Galera Arbitrator Daemon) | MariaDB Galera pt 2</a></div>
<div>
<div></div>
<div><em>Mykhaylo Rykmas</em></div>
</div>
</div>
<div></div>
<div><strong><a href="https://vettabase.com/vettabase-is-a-mariadb-foundation-sponsor/">Vettabase is a MariaDB Foundation sponsor!</a></strong></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

<p><a href="https://vettabase.com/installing-a-3-node-mariadb-galera-cluster-on-ubuntu-24-04-mariadb-galera-pt-1/">Installing a MariaDB Galera Cluster on Ubuntu 24.04 | MariaDB Galera pt 1</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL Replication Best Practices: How to Keep Your Replicas Sane (and Your Nights Quiet)</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/12/03/mysql-replication-best-practices-how-to-keep-your-replicas-sane-and-your-nights-quiet/" />
      <id>https://percona.community/blog/2025/12/03/mysql-replication-best-practices-how-to-keep-your-replicas-sane-and-your-nights-quiet/</id>
      <updated>2025-12-03T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>MySQL replication has been around forever, and yet… people still manage to set it up in ways that break at the worst possible moment. Even in 2025, you can get burned by tiny schema differences, missing primary keys, or one forgotten config flag. I’ve seen replicas drift so far out of sync they might as well live in a different universe.</p>
<p><a href="https://percona.community/blog/2025/12/03/mysql-replication-best-practices-how-to-keep-your-replicas-sane-and-your-nights-quiet/">MySQL Replication Best Practices: How to Keep Your Replicas Sane (and Your Nights Quiet)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>MySQL replication has been around forever, and yet&hellip; people still manage to set it up in ways that break at the worst possible moment. Even in 2025, you can get burned by tiny schema differences, missing primary keys, or one forgotten config flag. I&rsquo;ve seen replicas drift so far out of sync they might as well live in a different universe.</p>
<p>This guide covers the practical best practices&mdash;the stuff real DBAs use every day to keep replication stable, predictable, and boring. (Boring is a compliment in database land.)</p>
<h3>Always Use GTIDs. Yes, Always.<a class="anchor-link" id="always-use-gtids-yes-always"></a></h3>
<p>GTID-based replication is one of those features that people resist turning on, and then once they do, they never want to go back.</p>
<p>Why GTIDs?</p>
<ul>
<li>Failover become sane</li>
<li>Reparenting replicas stops being a headache</li>
<li>Missing transactions are easy to detect</li>
</ul>
<p>Your my.cnf should absolutely include:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">gtid_mode=ON
</span></span><span class="line"><span class="cl">enforce_gtid_consistency=ON
</span></span><span class="line"><span class="cl">log_replica_updates=ON</span></span></code></pre>
</div>
</div>
</div>
<p>Once GTIDs are enabled, do not mix in old-style replication. That path leads straight to confusion.</p>
<h3>Use Row-Based Replication (RBR)<a class="anchor-link" id="use-row-based-replication-rbr"></a></h3>
<p>Statement-based replication is a nostalgia trip that nobody asked for. It breaks on:</p>
<ul>
<li>NOW(), UUID(), and similar functions</li>
<li>Floating point differences</li>
<li>Collation mismatches</li>
<li>Triggers behaving differently</li>
</ul>
<p>Just skip the pain and use:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">binlog_format=ROW</span></span></code></pre>
</div>
</div>
</div>
<p>RBR is slightly more verbose, but 100&times; more predictable. When something breaks, it&rsquo;s never because you chose ROW.</p>
<h3>Every Table Needs a Primary Key. No Exceptions.<a class="anchor-link" id="every-table-needs-a-primary-key-no-exceptions"></a></h3>
<p>If you take nothing else from this guide, take this:</p>
<p><strong>Replication without primary keys is a bad time.</strong></p>
<p>Row-based replication needs a way to find the row that changed. Without a PK (or at least a UNIQUE index), the server has to use every column as a lookup. That&rsquo;s slow, error-prone, and sometimes impossible.</p>
<p>The usual symptoms:</p>
<ul>
<li>Replication lag slowly creeping up</li>
<li>Replica doing full table scans on updates</li>
<li>Rows failing to apply</li>
<li>Errors like:</li>
</ul>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error 1032: Can't find record in table</span></span></code></pre>
</div>
</div>
</div>
<p>Save yourself hours of debugging and just make sure every table has a primary key.</p>
<h3>Keep the Schema Identical Everywhere<a class="anchor-link" id="keep-the-schema-identical-everywhere"></a></h3>
<p>Replication assumes that everyone&rsquo;s using the same schema. MySQL will happily keep going even if your schemas don&rsquo;t match&mdash;and then quietly drift out of sync.</p>
<p>Here are the practical ways to keep schemas aligned:</p>
<h4>Approach A &mdash; mysqldump (most common)</h4>
<p>Export schemas only:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">mysqldump --no-data mydb &gt; schema.sql</span></span></code></pre>
</div>
</div>
</div>
<p>From both servers, then:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">diff source-schema.sql replica-schema.sql</span></span></code></pre>
</div>
</div>
</div>
<h4>Approach B &mdash; information_schema metadata</h4>
<p>This approach is great for automaton:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">SELECT table_name, column_name, column_type, is_nullable, column_default
</span></span><span class="line"><span class="cl">FROM information_schema.columns
</span></span><span class="line"><span class="cl">WHERE table_schema = 'mydb'
</span></span><span class="line"><span class="cl">ORDER BY table_name, ordinal_position;</span></span></code></pre>
</div>
</div>
</div>
<p>Execute this query on each server and diff the results. Update mydb to match the database whose schema metadata you want to examine.</p>
<h4>Approach C &mdash; pt-table-checksum (data only)</h4>
<p>This doesn&rsquo;t compare schemas &mdash; it catches data drift.<br>
You should consider running it on a schedule such as:</p>
<ul>
<li>high-change OLTP DBs run weekly or even daily</li>
<li>huge multi-TB DBs run quarterly</li>
<li>some sensitive systems avoid running it during peak hours</li>
</ul>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-table-checksum --replicate=percona.checksums</span></span></code></pre>
</div>
</div>
</div>
<p>You can fix drift with:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-table-sync --execute --replicate=percona.checksums</span></span></code></pre>
</div>
</div>
</div>
<p>Schema checks + data checks = safe replication.</p>
<h3>Harden Your Binary Log Settings<a class="anchor-link" id="harden-your-binary-log-settings"></a></h3>
<p>Your binlogs are the backbone of replication. Treat them carefully.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">sync_binlog=1
</span></span><span class="line"><span class="cl">binlog_row_image=FULL
</span></span><span class="line"><span class="cl">binlog_expire_logs_seconds=604800 # 7 days</span></span></code></pre>
</div>
</div>
</div>
<p>sync_binlog=1 is the big one&mdash;without it, a crash can corrupt binlogs or the GTID position, and that leads to a very bad day.</p>
<h3>Protect Your Replicas with super_read_only<a class="anchor-link" id="protect-your-replicas-with-super_read_only"></a></h3>
<p>Never allow accidental writes to replicas, in your my.cnf set:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">read_only=ON
</span></span><span class="line"><span class="cl">super_read_only=ON</span></span></code></pre>
</div>
</div>
</div>
<p><strong>super_read_only</strong> closes the loophole that even SUPER users could previously use to write to replicas.</p>
<h3>Use a Dedicated Replication User<a class="anchor-link" id="use-a-dedicated-replication-user"></a></h3>
<p>Give the minimal permissions:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">CREATE USER 'repl'@'%' IDENTIFIED BY 'strong_password';
</span></span><span class="line"><span class="cl">GRANT REPLICATION REPLICA ON *.* TO 'repl'@'%';</span></span></code></pre>
</div>
</div>
</div>
<p>This user should do exactly one thing: replicate.<br>
Don&rsquo;t reuse app users&mdash;you&rsquo;re just begging for trouble.</p>
<h3>Replication Lag: Watch It Like a Hawk<a class="anchor-link" id="replication-lag-watch-it-like-a-hawk"></a></h3>
<p>Seconds_Behind_Source lies more often than you&rsquo;d expect. It&rsquo;s okay for a quick glance but don&rsquo;t rely on it.</p>
<p>Better options:</p>
<ul>
<li>Performance Schema: replication_applier_status_by_worker</li>
<li>Percona Monitoring and Management (PMM)</li>
<li>Custom heartbeat tables</li>
<li>pt-heartbeat</li>
</ul>
<p>Lag is one of the biggest causes of outages&mdash;monitor it continuously. Lag is usually the first sign something is wrong&mdash;catch it early.</p>
<h3>Use Parallel Replication (But Don&rsquo;t Overdo It)<a class="anchor-link" id="use-parallel-replication-but-dont-overdo-it"></a></h3>
<p>If your primary has multiple writers or many concurrent transactions, in your my.cnf enable parallel workers:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">replica_parallel_type=LOGICAL_CLOCK
</span></span><span class="line"><span class="cl">replica_parallel_workers=4</span></span></code></pre>
</div>
</div>
</div>
<p>4&ndash;8 workers is a sweet spot for most systems. More workers &ne; more speed; after a point it just increases memory footprint without real benefit.</p>
<p>But when it helps, it really helps&mdash;like cutting lag by 80&ndash;90%.</p>
<h3>Use SSL Anywhere Outside the LAN<a class="anchor-link" id="use-ssl-anywhere-outside-the-lan"></a></h3>
<p>Replication traffic isn&rsquo;t something you want exposed.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">source_ssl=1
</span></span><span class="line"><span class="cl">source_ssl_ca=/path/ca.pem</span></span></code></pre>
</div>
</div>
</div>
<p>Earlier versions used the master_ssl_* variables, but the idea is the same: encrypt the connection when it leaves your trusted network.</p>
<h2>Final Thoughts<a class="anchor-link" id="final-thoughts"></a></h2>
<p>MySQL replication can be rock-solid, but only if you follow a handful of rules that experienced DBAs know by heart:</p>
<ul>
<li>Use GTIDs</li>
<li>Use RBR</li>
<li>Always have primary keys</li>
<li>Keep schemas aligned</li>
<li>Check for data drift</li>
<li>Harden binlog settings</li>
<li>Protect replicas from accidental writes</li>
<li>Monitor lag properly</li>
<li>Use parallel workers when appropriate</li>
<li>Encrypt connections over untrusted networks</li>
</ul>
<p>Follow these, and your replicas will stay healthy, consistent, and (mostly) invisible&mdash;which is exactly how you want them.</p>

<p><a href="https://percona.community/blog/2025/12/03/mysql-replication-best-practices-how-to-keep-your-replicas-sane-and-your-nights-quiet/">MySQL Replication Best Practices: How to Keep Your Replicas Sane (and Your Nights Quiet)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Open Source AI Models Building a Development Team</title>
      <link rel="alternate" type="text/html" href="https://anothermysqldba.blogspot.com/2025/12/open-source-ai-models-building.html" />
      <id>https://anothermysqldba.blogspot.com/2025/12/open-source-ai-models-building.html</id>
      <updated>2025-12-02T23:50:00+02:00</updated>
      <author><name>Keith Larson ( anothermysqldba )</name></author>
      <summary type="html"><![CDATA[<p>The Question We\'re Finally AskingFor years we\'ve debated: Can AI replace software engineers? The question was always a bit theatrical. The real question—the one that actually matters—is a different one entirely: Can AI augment the engineering process in ways that make better code happen faster?I think we\'re closer to a practical answer than we realize.There\'s a concept that\'s been brewing in the open source and commercial AI spaces, one that mirrors something we\'ve known in software engineering for decades: diverse perspectives catch what homogeneous ones miss. Single engineers make mistakes. Teams catch them. The question becomes: can we build a team out of AI models, each with distinct expertise, and orchestrate them to produce better outcomes?I\'ve been working on a proof of concept with this team-based approach. I started back in Aug 2025 and picked it up again recently. It\'s a component of my broader ApocryiaAI framework (apocryia.com will be the public facing frontend). For this POC, what I\'ve built is a set of Python scripts that integrate with our private backend infrastructure and Percona database, orchestrating local open source models to work as a unified team. They collaborate to solve whatever task is requested, each bringing specialized perspective and expertise. All of this team communication is visible in real-time via a private IRC server—allowing me to observe the interactions, understand their reasoning process, and even interject during the workflow when needed. What I\'m describing here is specifically that IRC-based autonomous development team. It\'s running, it\'s working, and the results are worth thinking about. Yes, it\'s still a proof of concept, and I\'m the first to admit that. But this concept supports what I try to do for my team and myself: work smarter, not harder. This isn\'t AI replacing developers. It\'s AI working alongside developers, providing diverse opinions across different points of view and models.The Architecture: A Team, Not a ModelThe architecture is deceptively simple but conceptually important. Instead of throwing a single large language model at a development task and hoping it produces good code, we\'ve created four specialized roles:ProjectManager (qwen2.5-coder) - Creates detailed implementation plansDeveloper (deepseek-coder-v2) - Generates complete, runnable codeReviewer (apocryiaai-unified - A private model I trained with MIT business and computer science grad-level data) - Evaluates code for correctness and business logicLucifer (mistral) - Challenges everything, finds edge cases and flawsEach model is chosen specifically for that role. The ProjectManager isn\'t trying to write code—it\'s thinking about requirements, dependencies, and success criteria. The Developer is optimized for code generation. The Reviewer has business-level understanding. And Lucifer\'s job is to be difficult.This is not novel in human teams. It\'s basic team dynamics. What\'s novel is making it work with open source models that fit in a local machine.The Workflow: Iteration With PurposeHere\'s where it gets interesting. The workflow doesn\'t end with code generation. It\'s iterative:Cycle 1:Developer writes initial codeReviewer evaluates it (rating it 1-10)Lucifer challenges the code with critical analysisDecision Logic:If Reviewer gives it 8+/10 AND Lucifer says it\'s acceptable → ApproveIf either agent rejects it → Trigger refinementCycle 2 (if needed):Developer refines code based on specific feedback from both agentsReviewer re-evaluates the refined codeLucifer reviews the improvementsReal Example: Hello World TaskI tested this with a specific task: \"Create a Python script that prints \'Hello World\' a configurable number of times based on passed input (like: hello_world.py -X 3 would print it 3 times).\"Cycle 1 - Initial Development:The Developer generated: import sysdef hello(times):    for i in range(times):        print(\"Hello World\")if __name__ == \"__main__\":    if len(sys.argv) &#62; 1:        try:            times = int(sys.argv[1])            hello(times)        except ValueError:            print(\"Please enter a valid integer.\")    else:        print(\"Usage: python hello_world.py \")The Reviewer evaluated it: Rating: 7/10 with feedback:\"The code works but is missing proper argument handling. It uses positional arguments instead of flags. Should support -X or --number flag format. Error handling could be more robust.\"The Lucifer agent challenged it:\"This implementation is naive. What happens if someone passes -5 or 1000000? No bounds checking. The positional argument approach is error-prone. Consider using argparse for professional flag handling.\"Decision: Not approved. 7/10  str:    \"\"\"    Explain the reason behind the sky appearing blue due to Rayleigh scattering and    other related phenomena.    Returns:        str: A detailed explanation of why the sky appears blue, including information             about Rayleigh scattering and its role in color dispersion.    \"\"\"    return (        \"The sky appears blue because of a phenomenon called Rayleigh scattering. \"        \"When sunlight enters Earth\'s atmosphere, shorter wavelength (blue) light is \"        \"scattered more by air molecules than longer wavelength (red/yellow) light. \"        \"This causes the blue color we see in the sky.\"    )if __name__ == \"__main__\":    print(why_is_the_sky_blue())The Reviewer re-evaluated: Rating: 8/10\"Improved significantly. Type hints added, docstring is comprehensive, explanation is clear and scientifically accurate.\"The Lucifer agent approved:\"Much better. The technical details about wavelength are now clear. Code follows Python best practices. This is a solid implementation.\"Decision: Both agents approve. Task completed.Verified Output:The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlightenters Earth\'s atmosphere, shorter wavelength (blue) light is scattered more by airmolecules than longer wavelength (red/yellow) light. This causes the blue color we seein the sky.What This Example Shows:The system handles diverse task types (not just utilities)Even a \"perfect\" 10/10 from Reviewer doesn\'t bypass the approval gateLucifer\'s critical eye catches improvements that pure quality metrics missType hints, docstrings, and clarity matter to the teamCode goes through refinement even when it works, pushing toward excellenceWhy This Matters: The Approval ProblemHere\'s something most AI code generation tools gloss over: How do you know when code is actually ready?Most systems have a single decision gate: \"Is this acceptable yes/no?\" That\'s the wrong question. The better question is: \"Have multiple perspectives—operating from different priorities and expertise—agreed this is good?\"The approval logic in ApocryiaAI requires both the Reviewer and Lucifer to explicitly approve. Not a loose \"looks fine\" but explicit agreement:Reviewer must give it a rating of 8/10 or higher, OR explicitly say \"approved/looks good\"Lucifer must explicitly say \"no issues/acceptable/approved\"This creates a natural tension. The Reviewer wants the code to work correctly and follow best practices. Lucifer wants to find what\'s wrong. Code that satisfies both perspectives has genuinely passed multiple tests.Why Explicit Approval Matters: A Cautionary TaleThis is harder than you\'d think. We initially had a system that used loose keyword matching for approval. Words like \"looks good\" would trigger approval even when the model was just introducing its analysis. Here\'s an example of what went wrong:Initial (Broken) System:Lucifer: \"In order to provide a comprehensive review, I\'ll delve deeper intothe edge cases. The input validation looks good in principle...\"System detected: \"looks good\" → APPROVED ✅ (WRONG!)Lucifer was about to identify critical issues, but the system approved the code prematurely because it detected the phrase \"looks good\" mid-sentence as the model was introducing its analysis.Fixed System: Now we require explicit approval phrases only when they appear as standalone conclusions:Lucifer: \"After thorough analysis, no issues found. This implementationis acceptable and ready for deployment.\"System detected: \"no issues found\" + \"acceptable\" → APPROVED ✅ (CORRECT!)The difference? We distinguish between:Positive mentions in analysis: \"This approach looks good, but...\" (not approval)Explicit approval conclusions: \"No issues. This is approved.\" (approval)This seemingly small change prevents false positives where models talk about good code while actually criticizing it.The Practical Side: GPU Memory and Open Source RealitiesHere\'s something I haven\'t seen discussed enough: Open source models sitting in GPU memory between tasks is wasteful.We added model unloading via Ollama API calls. After each agent completes its task, we explicitly unload its model from GPU memory. This keeps the system usable on real hardware, not just theoretical deployments.This is a small detail but reveals something important: we\'re not building a research project. We\'re trying to make something that actually runs on machines people have.Model Selection: Why Each Role Gets Its Specific ModelThe models we\'re using:ProjectManager: qwen2.5-coder:7bLightweight (7B parameters) so planning doesn\'t bottleneck the workflowExcels at breaking tasks into structured plans with dependenciesWhen asked to plan the \"Hello World\" task, it produced:Clear understanding of requirements (handle variable counts, validate input)Step-by-step plan (arg parsing → validation → output loop)Potential issues (negative numbers, bounds checking)Success criteria (clean exit codes, proper error messages)Not wasted generating code—just strategic thinking.Developer: deepseek-coder-v2:latestLargest and most specialized for code generation in our lineupProduces complete, runnable code blocks on first passHandles complex scaffolding (argparse setup, error handling, proper exit codes)When asked to refine based on feedback, actually understands what \"add bounds checking\" means and implements it correctlyReviewer: apocryiaai-unified:latestRare combination: technical correctness evaluation + business logic understandingDoesn\'t just say \"this code works\" but thinks about use cases and edge casesExample feedback on our script: \"Professional argument handling. Only minor suggestion: consider logging instead of print for errors.\"That\'s not just technical critique—that\'s production thinkingLucifer: mistral:latestSharp critical analysis without being a code expertAsks hard questions: \"What happens if someone passes -5 or 1000000?\"Thinks about failure modes and abuse casesDoesn\'t get lost in syntax—focuses on fundamental flawsAll open source. All fit on consumer hardware. None require cloud APIs.Why This Mix Works Better Than a Single ModelA single large model trying all four roles would either:Excel at one role, mediocre at othersProduce bloated, slow responses trying to cover everythingApprove its own code (alignment problem—it defends its earlier decisions)With specialized models:Planning is fast and focusedCode generation leverages the best tool availableReview is genuinely independent critiqueLucifer isn\'t trying to write code—just finding problemsWhat Works. What Doesn\'t. Honest Assessment.What Actually Works:The iterative refinement genuinely improves code. 7→9 isn\'t a coincidence.Diverse perspectives catch real issues. When Lucifer finds edge cases, they\'re usually valid.The approval mechanism creates a quality gate that\'s harder to game than single-model evaluation.Locally-run models mean no API costs, no privacy concerns, no rate limiting.What\'s Still Hard:Computational cost: 4+ LLM calls per task. For trivial tasks, this is overkill.Model reliability: The system depends on models actually being critical and honest. If a model learns to approve things to move forward, the whole thing breaks.Specification problems remain. If the initial requirement is fundamentally wrong, refinement helps but doesn\'t fix it.Scaling: One successful task doesn\'t prove it scales across diverse problem types.What Needs More Data:Does 2 cycles converge on actually better code, or is that specific to this task?What\'s the failure rate on production deployments?At what complexity level does the overhead justify the quality improvement?How do these systems perform on different categories of problems (utility scripts, system programming, web backends)?The Bigger Question: What\'s This For?If you\'re thinking \"this seems like a lot of machinery for hello_world.py,\" you\'re right.The value emerges at scale and complexity. Consider:Team Augmentation - Your actual team has a senior engineer, a junior, and a critical reviewer. Adding an automated adversarial agent (Lucifer) that catches what you\'d miss? That scales.Knowledge Preservation - When the critical feedback is logged, you can learn why code was rejected. Over time, you understand the approval patterns. That\'s institutional knowledge.Specification Evolution - The PM learning mechanism captures when critical issues would have been caught by better specifications. Feed that back to requirements.Local Autonomy - No cloud dependency. No API costs. You control your development pipeline.The right comparison isn\'t \"can this replace engineers\" but \"can this augment the engineering process in ways that produce better outcomes per unit of human effort?\"On that question, the early data looks promising.The Open Source AngleHere\'s why open source models matter for this:You\'re not dependent on a commercial company\'s moods about pricing, availability, or model changes. You\'re not sending your code to external APIs. You\'re not at risk of waking up to a terms-of-service change that affects your workflow.The community around Ollama, the models themselves (qwen, deepseek, mistral), and the frameworks we\'re using are all genuinely open. You can inspect them. You can run them on your hardware. You can contribute back.That\'s different from cloud-based AI. It\'s also different from the single-model approach most people take. It\'s team-based thinking applied to open source infrastructure.Where This GoesThe next phase is validation. More diverse tasks. Different problem types. Real production code, not just examples.We need to understand:Does the approval mechanism hold up when models encounter truly novel situations?How does cost-per-task scale as complexity increases?Can the PM learning feedback actually improve specification quality over time?What happens when the team disagrees and can\'t converge?I have hypotheses on these. But hypotheses aren\'t evidence. Evidence comes from running it.What I like the most about this: YOU can do it also. You can apply the same concepts to whatever architecture and infrastructure you want. Don\'t want an IRC server, ok, no problem, I wanted insights into what the team was doing, but you don\'t have to. Do you want more team members, ok sure... The concept is based on you using AI to help you work smarter, not harder. The PhilosophyWhat we\'re experimenting with here is: building development automation with tools you control, from models you understand, running on hardware you own.That matters more than people realize.My AI team concept isn\'t trying to replace developers or even me. It\'s trying to be the kind of colleague that works with me and who catches bugs, asks hard questions, and pushes back on mediocre code. That colleague exists in every good team. Automation is making it possible to have that colleague always present.Whether this specific approach is the right one, I\'m not sure yet. But the direction—toward distributed expertise, adversarial review, and local autonomy—that direction feels right.The code is working. The team is functional. The quality improvements are measurable.Now we find out if it scales, the real work now begins....</p>
<p><a href="https://anothermysqldba.blogspot.com/2025/12/open-source-ai-models-building.html">Open Source AI Models Building a Development Team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><b>The Question We&rsquo;re Finally Asking</b></p>
<p>For years we&rsquo;ve debated: Can AI replace software engineers? The question was always a bit theatrical. The real question&mdash;the one that actually matters&mdash;is a different one entirely: Can AI augment the engineering process in ways that make better code happen faster?</p>
<p>I think we&rsquo;re closer to a practical answer than we realize.</p>
<p>There&rsquo;s a concept that&rsquo;s been brewing in the open source and commercial AI spaces, one that mirrors something we&rsquo;ve known in software engineering for decades: diverse perspectives catch what homogeneous ones miss. Single engineers make mistakes. Teams catch them. The question becomes: can we build a team out of AI models, each with distinct expertise, and orchestrate them to produce better outcomes?</p>
<p>I&rsquo;ve been working on a proof of concept with this team-based approach. I started back in Aug 2025 and picked it up again recently. It&rsquo;s a component of my broader ApocryiaAI framework (apocryia.com will be the public facing frontend). For this POC, what I&rsquo;ve built is a set of Python scripts that integrate with our private backend infrastructure and Percona database, orchestrating local open source models to work as a unified team. They collaborate to solve whatever task is requested, each bringing specialized perspective and expertise. All of this team communication is visible in real-time via a private IRC server&mdash;allowing me to observe the interactions, understand their reasoning process, and even interject during the workflow when needed. What I&rsquo;m describing here is specifically that IRC-based autonomous development team. It&rsquo;s running, it&rsquo;s working, and the results are worth thinking about. Yes, it&rsquo;s still a proof of concept, and I&rsquo;m the first to admit that. But this concept supports what I try to do for my team and myself: work smarter, not harder. This isn&rsquo;t AI replacing developers. It&rsquo;s AI working alongside developers, providing diverse opinions across different points of view and models.</p>
<p><b>The Architecture: A Team, Not a Model</b></p>
<p>The architecture is deceptively simple but conceptually important. Instead of throwing a single large language model at a development task and hoping it produces good code, we&rsquo;ve created four specialized roles:</p>

<ol>
<li>ProjectManager (qwen2.5-coder) &ndash; Creates detailed implementation plans</li>
<li>Developer (deepseek-coder-v2) &ndash; Generates complete, runnable code</li>
<li>Reviewer (apocryiaai-unified &ndash; A private model I trained with MIT business and computer science grad-level data) &ndash; Evaluates code for correctness and business logic</li>
<li>Lucifer (mistral) &ndash; Challenges everything, finds edge cases and flaws</li>
</ol>
<p>Each model is chosen specifically for that role. The ProjectManager isn&rsquo;t trying to write code&mdash;it&rsquo;s thinking about requirements, dependencies, and success criteria. The Developer is optimized for code generation. The Reviewer has business-level understanding. And Lucifer&rsquo;s job is to be difficult.</p>
<p>This is not novel in human teams. It&rsquo;s basic team dynamics. What&rsquo;s novel is making it work with open source models that fit in a local machine.</p>
<p></p>
<p><b>The Workflow: Iteration With Purpose</b></p>
<p>Here&rsquo;s where it gets interesting. The workflow doesn&rsquo;t end with code generation. It&rsquo;s iterative:</p>
<p>Cycle 1:</p>

<ul>
<li>Developer writes initial code</li>
<li>Reviewer evaluates it (rating it 1-10)</li>
<li>Lucifer challenges the code with critical analysis</li>
</ul>
<p>Decision Logic:</p>

<ul>
<li>If Reviewer gives it 8+/10 AND Lucifer says it&rsquo;s acceptable &rarr; Approve</li>
<li>If either agent rejects it &rarr; Trigger refinement</li>
</ul>
<p>Cycle 2 (if needed):</p>

<ul>
<li>Developer refines code based on specific feedback from both agents</li>
<li>Reviewer re-evaluates the refined code</li>
<li>Lucifer reviews the improvements</li>
</ul>
<p><b>Real Example: Hello World Task</b></p>
<p>I tested this with a specific task: &ldquo;Create a Python script that prints &lsquo;Hello World&rsquo; a configurable number of times based on passed input (like: hello_world.py -X 3 would print it 3 times).&rdquo;</p>
<p></p>
<p>Cycle 1 &ndash; Initial Development:</p>
<p>The Developer generated:</p>
<div>
<pre> import sys</pre>
</div>
<p></p>
<p>def hello(times):</p>
<p>&nbsp; &nbsp; for i in range(times):</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Hello World&rdquo;)</p>
<p></p>
<p>if __name__ == &ldquo;__main__&rdquo;:</p>
<p>&nbsp; &nbsp; if len(sys.argv) &gt; 1:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; try:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; times = int(sys.argv[1])</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; hello(times)</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; except ValueError:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Please enter a valid integer.&rdquo;)</p>
<p>&nbsp; &nbsp; else:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Usage: python hello_world.py &ldquo;)</p>
<p>The Reviewer evaluated it: Rating: 7/10 with feedback:</p>
<p>&ldquo;The code works but is missing proper argument handling. It uses positional arguments instead of flags. Should support -X or &ndash;number flag format. Error handling could be more robust.&rdquo;</p>
<p></p>
<p>The Lucifer agent challenged it:</p>
<p>&ldquo;This implementation is naive. What happens if someone passes -5 or 1000000? No bounds checking. The positional argument approach is error-prone. Consider using argparse for professional flag handling.&rdquo;</p>
<p></p>
<p>Decision: Not approved. 7/10 &lt; 8/10 threshold. Trigger refinement.</p>
<p></p>
<p>Cycle 2 &ndash; Code Refinement:</p>
<p></p>
<p>Based on both feedbacks, the Developer refined the code:</p>
<div>
<pre>import sys</pre>
</div>
<p>import argparse</p>
<p></p>
<p>def hello(times):</p>
<p>&nbsp; &nbsp; if times &lt;= 0:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Error: Number must be positive&rdquo;)</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; return False</p>
<p>&nbsp; &nbsp; if times &gt; 1000:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Error: Maximum 1000 iterations allowed&rdquo;)</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; return False</p>
<p></p>
<p>&nbsp; &nbsp; for i in range(times):</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(&ldquo;Hello World&rdquo;)</p>
<p>&nbsp; &nbsp; return True</p>
<p></p>
<p>if __name__ == &ldquo;__main__&rdquo;:</p>
<p>&nbsp; &nbsp; parser = argparse.ArgumentParser(description=&rsquo;Print Hello World N times&rsquo;)</p>
<p>&nbsp; &nbsp; parser.add_argument(&lsquo;-X&rsquo;, &lsquo;&ndash;number&rsquo;, type=int, required=True,</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;help=&rsquo;Number of times to print (1-1000)&rsquo;)</p>
<p></p>
<p>&nbsp; &nbsp; try:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; args = parser.parse_args()</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; success = hello(args.number)</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(0 if success else 1)</p>
<p>&nbsp; &nbsp; except SystemExit:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; raise</p>
<p>&nbsp; &nbsp; except Exception as e:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; print(f&rdquo;Error: {e}&rdquo;)</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; sys.exit(1)</p>
<p>The Reviewer re-evaluated: Rating: 9/10</p>
<p>&ldquo;Significant improvement. Now uses proper argparse, validates inputs, has bounds checking, explicit error messages. Professional argument handling. Only minor suggestion: consider logging instead of print for errors.&rdquo;</p>
<p></p>
<p>The Lucifer agent reviewed: Approved</p>
<p>&ldquo;Much better. Edge cases are handled. Bounds checking prevents abuse. Error messages are clear. No issues with this implementation.&rdquo;</p>
<p>Decision: Approved. Both agents agree. Deploy.</p>
<p></p>
<p>Real iteration. Real improvement. From 7&rarr;9 rating. Visible in the logs.</p>
<p></p>
<p>Another Example: &ldquo;Why is the Sky Blue?&rdquo;</p>
<p>This example shows the system handling non-coding tasks&mdash;explanatory content rather than utility code.</p>
<p>Task: &ldquo;Write a 50 word sentence about why the sky is blue&rdquo;</p>
<p></p>
<p>Cycle 1 &ndash; Initial Generation:</p>
<p>The Developer generated:</p>
<div>
<pre>def explain_sky_blue():</pre>
</div>
<p>&nbsp; &nbsp; &ldquo;&rdquo;&rdquo;</p>
<p>&nbsp; &nbsp; Returns an explanation of why the sky appears blue.</p>
<p>&nbsp; &nbsp; &ldquo;&rdquo;&rdquo;</p>
<p>&nbsp; &nbsp; return &ldquo;The sky appears blue because of a phenomenon called Rayleigh scattering&hellip;&rdquo;</p>
<p></p>
<p>if __name__ == &ldquo;__main__&rdquo;:</p>
<p>&nbsp; &nbsp; print(explain_sky_blue())</p>
<p>The Reviewer evaluated: Rating: 10/10</p>
<p>&ldquo;Well-structured code with clear documentation and correct scientific explanation. Excellent.&rdquo;</p>
<p></p>
<p>The Lucifer agent challenged:</p>
<p>&ldquo;As Lucifer, here are my findings: While functionally correct, the explanation lacks depth about wavelength specifics. The code structure is good but could benefit from type hints for better Python practices.&rdquo;</p>
<p>Decision: Despite Reviewer&rsquo;s perfect score, Lucifer insisted on refinement. System triggered improvement.</p>
<p></p>
<p>Cycle 2 &ndash; Code Refinement:</p>
<p>Based on Lucifer&rsquo;s feedback, Developer improved:</p>
<div>
<pre>def why_is_the_sky_blue() -&gt; str:</pre>
</div>
<p>&nbsp; &nbsp; &ldquo;&rdquo;&rdquo;</p>
<p>&nbsp; &nbsp; Explain the reason behind the sky appearing blue due to Rayleigh scattering and</p>
<p>&nbsp; &nbsp; other related phenomena.</p>
<p></p>
<p>&nbsp; &nbsp; Returns:</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; str: A detailed explanation of why the sky appears blue, including information</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;about Rayleigh scattering and its role in color dispersion.</p>
<p>&nbsp; &nbsp; &ldquo;&rdquo;&rdquo;</p>
<p>&nbsp; &nbsp; return (</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &ldquo;The sky appears blue because of a phenomenon called Rayleigh scattering. &ldquo;</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &ldquo;When sunlight enters Earth&rsquo;s atmosphere, shorter wavelength (blue) light is &ldquo;</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &ldquo;scattered more by air molecules than longer wavelength (red/yellow) light. &ldquo;</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &ldquo;This causes the blue color we see in the sky.&rdquo;</p>
<p>&nbsp; &nbsp; )</p>
<p></p>
<p>if __name__ == &ldquo;__main__&rdquo;:</p>
<p>&nbsp; &nbsp; print(why_is_the_sky_blue())</p>
<p>The Reviewer re-evaluated: Rating: 8/10</p>
<p>&ldquo;Improved significantly. Type hints added, docstring is comprehensive, explanation is clear and scientifically accurate.&rdquo;</p>
<p></p>
<p>The Lucifer agent approved:</p>
<p>&ldquo;Much better. The technical details about wavelength are now clear. Code follows Python best practices. This is a solid implementation.&rdquo;</p>
<p></p>
<p>Decision: Both agents approve. Task completed.</p>
<p>Verified Output:</p>
<div>
<pre>The sky appears blue because of a phenomenon called Rayleigh scattering. When sunlight</pre>
</div>
<p>enters Earth&rsquo;s atmosphere, shorter wavelength (blue) light is scattered more by air</p>
<p>molecules than longer wavelength (red/yellow) light. This causes the blue color we see</p>
<p>in the sky.</p>
<p>What This Example Shows:</p>

<ul>
<li>The system handles diverse task types (not just utilities)</li>
<li>Even a &ldquo;perfect&rdquo; 10/10 from Reviewer doesn&rsquo;t bypass the approval gate</li>
<li>Lucifer&rsquo;s critical eye catches improvements that pure quality metrics miss</li>
<li>Type hints, docstrings, and clarity matter to the team</li>
<li>Code goes through refinement even when it works, pushing toward excellence</li>
</ul>
<p>Why This Matters: The Approval Problem</p>
<p>Here&rsquo;s something most AI code generation tools gloss over: How do you know when code is actually ready?</p>
<p>Most systems have a single decision gate: &ldquo;Is this acceptable yes/no?&rdquo; That&rsquo;s the wrong question. The better question is: &ldquo;Have multiple perspectives&mdash;operating from different priorities and expertise&mdash;agreed this is good?&rdquo;</p>
<p>The approval logic in ApocryiaAI requires both the Reviewer and Lucifer to explicitly approve. Not a loose &ldquo;looks fine&rdquo; but explicit agreement:</p>

<ul>
<li>Reviewer must give it a rating of 8/10 or higher, OR explicitly say &ldquo;approved/looks good&rdquo;</li>
<li>Lucifer must explicitly say &ldquo;no issues/acceptable/approved&rdquo;</li>
</ul>
<p>This creates a natural tension. The Reviewer wants the code to work correctly and follow best practices. Lucifer wants to find what&rsquo;s wrong. Code that satisfies both perspectives has genuinely passed multiple tests.</p>
<p></p>
<p>Why Explicit Approval Matters: A Cautionary Tale</p>
<p>This is harder than you&rsquo;d think. We initially had a system that used loose keyword matching for approval. Words like &ldquo;looks good&rdquo; would trigger approval even when the model was just introducing its analysis. Here&rsquo;s an example of what went wrong:</p>
<p></p>
<p>Initial (Broken) System:</p>
<div>
<pre>Lucifer: "In order to provide a comprehensive review, I'll delve deeper into</pre>
</div>
<p>the edge cases. The input validation looks good in principle&hellip;&rdquo;</p>
<p>System detected: &ldquo;looks good&rdquo; &rarr; APPROVED &#9989; (WRONG!)</p>
<p>Lucifer was about to identify critical issues, but the system approved the code prematurely because it detected the phrase &ldquo;looks good&rdquo; mid-sentence as the model was introducing its analysis.</p>
<p></p>
<p>Fixed System: Now we require explicit approval phrases only when they appear as standalone conclusions:</p>
<div>
<pre>Lucifer: "After thorough analysis, no issues found. This implementation</pre>
</div>
<p>is acceptable and ready for deployment.&rdquo;</p>
<p>System detected: &ldquo;no issues found&rdquo; + &ldquo;acceptable&rdquo; &rarr; APPROVED &#9989; (CORRECT!)</p>
<p>The difference? We distinguish between:</p>

<ul>
<li>Positive mentions in analysis: &ldquo;This approach looks good, but&hellip;&rdquo; (not approval)</li>
<li>Explicit approval conclusions: &ldquo;No issues. This is approved.&rdquo; (approval)</li>
</ul>
<p>This seemingly small change prevents false positives where models talk about good code while actually criticizing it.</p>
<p></p>
<p><b>The Practical Side: GPU Memory and Open Source Realities</b></p>
<p>Here&rsquo;s something I haven&rsquo;t seen discussed enough: Open source models sitting in GPU memory between tasks is wasteful.</p>
<p>We added model unloading via Ollama API calls. After each agent completes its task, we explicitly unload its model from GPU memory. This keeps the system usable on real hardware, not just theoretical deployments.</p>
<p>This is a small detail but reveals something important: we&rsquo;re not building a research project. We&rsquo;re trying to make something that actually runs on machines people have.</p>
<p></p>
<p>Model Selection: Why Each Role Gets Its Specific Model</p>
<p>The models we&rsquo;re using:</p>
<p><b>ProjectManager: qwen2.5-coder:7b</b></p>

<ul>
<li>Lightweight (7B parameters) so planning doesn&rsquo;t bottleneck the workflow</li>
<li>Excels at breaking tasks into structured plans with dependencies</li>
<li>When asked to plan the &ldquo;Hello World&rdquo; task, it produced:</li>
<li>Clear understanding of requirements (handle variable counts, validate input)</li>
<li>Step-by-step plan (arg parsing &rarr; validation &rarr; output loop)</li>
<li>Potential issues (negative numbers, bounds checking)</li>
<li>Success criteria (clean exit codes, proper error messages)</li>
<li>Not wasted generating code&mdash;just strategic thinking.</li>
</ul>
<p><b>Developer: deepseek-coder-v2:latest</b></p>

<ul>
<li>Largest and most specialized for code generation in our lineup</li>
<li>Produces complete, runnable code blocks on first pass</li>
<li>Handles complex scaffolding (argparse setup, error handling, proper exit codes)</li>
<li>When asked to refine based on feedback, actually understands what &ldquo;add bounds checking&rdquo; means and implements it correctly</li>
</ul>
<p><b>Reviewer: apocryiaai-unified:latest</b></p>

<ul>
<li>Rare combination: technical correctness evaluation + business logic understanding</li>
<li>Doesn&rsquo;t just say &ldquo;this code works&rdquo; but thinks about use cases and edge cases</li>
<li>Example feedback on our script: &ldquo;Professional argument handling. Only minor suggestion: consider logging instead of print for errors.&rdquo;</li>
<li>That&rsquo;s not just technical critique&mdash;that&rsquo;s production thinking</li>
</ul>
<p><b>Lucifer: mistral:latest</b></p>

<ul>
<li>Sharp critical analysis without being a code expert</li>
<li>Asks hard questions: &ldquo;What happens if someone passes -5 or 1000000?&rdquo;</li>
<li>Thinks about failure modes and abuse cases</li>
<li>Doesn&rsquo;t get lost in syntax&mdash;focuses on fundamental flaws</li>
</ul>
<p>All open source. All fit on consumer hardware. None require cloud APIs.</p>
<p></p>
<p>Why This Mix Works Better Than a Single Model</p>
<p>A single large model trying all four roles would either:</p>

<ol>
<li>Excel at one role, mediocre at others</li>
<li>Produce bloated, slow responses trying to cover everything</li>
<li>Approve its own code (alignment problem&mdash;it defends its earlier decisions)</li>
</ol>
<p>With specialized models:</p>

<ul>
<li>Planning is fast and focused</li>
<li>Code generation leverages the best tool available</li>
<li>Review is genuinely independent critique</li>
<li>Lucifer isn&rsquo;t trying to write code&mdash;just finding problems</li>
</ul>
<p><b>What Works. What Doesn&rsquo;t. Honest Assessment.</b></p>
<p>What Actually Works:</p>

<ul>
<li>The iterative refinement genuinely improves code. 7&rarr;9 isn&rsquo;t a coincidence.</li>
<li>Diverse perspectives catch real issues. When Lucifer finds edge cases, they&rsquo;re usually valid.</li>
<li>The approval mechanism creates a quality gate that&rsquo;s harder to game than single-model evaluation.</li>
<li>Locally-run models mean no API costs, no privacy concerns, no rate limiting.</li>
</ul>
<p>What&rsquo;s Still Hard:</p>

<ul>
<li>Computational cost: 4+ LLM calls per task. For trivial tasks, this is overkill.</li>
<li>Model reliability: The system depends on models actually being critical and honest. If a model learns to approve things to move forward, the whole thing breaks.</li>
<li>Specification problems remain. If the initial requirement is fundamentally wrong, refinement helps but doesn&rsquo;t fix it.</li>
<li>Scaling: One successful task doesn&rsquo;t prove it scales across diverse problem types.</li>
</ul>
<p>What Needs More Data:</p>

<ul>
<li>Does 2 cycles converge on actually better code, or is that specific to this task?</li>
<li>What&rsquo;s the failure rate on production deployments?</li>
<li>At what complexity level does the overhead justify the quality improvement?</li>
<li>How do these systems perform on different categories of problems (utility scripts, system programming, web backends)?</li>
</ul>
<p>The Bigger Question: What&rsquo;s This For?</p>
<p>If you&rsquo;re thinking &ldquo;this seems like a lot of machinery for hello_world.py,&rdquo; you&rsquo;re right.</p>
<p>The value emerges at scale and complexity. Consider:</p>

<ol>
<li>Team Augmentation &ndash; Your actual team has a senior engineer, a junior, and a critical reviewer. Adding an automated adversarial agent (Lucifer) that catches what you&rsquo;d miss? That scales.</li>
<li>Knowledge Preservation &ndash; When the critical feedback is logged, you can learn why code was rejected. Over time, you understand the approval patterns. That&rsquo;s institutional knowledge.</li>
<li>Specification Evolution &ndash; The PM learning mechanism captures when critical issues would have been caught by better specifications. Feed that back to requirements.</li>
<li>Local Autonomy &ndash; No cloud dependency. No API costs. You control your development pipeline.</li>
</ol>
<p>The right comparison isn&rsquo;t &ldquo;can this replace engineers&rdquo; but &ldquo;can this augment the engineering process in ways that produce better outcomes per unit of human effort?&rdquo;</p>
<p>On that question, the early data looks promising.</p>
<p></p>
<p><b>The Open Source Angle</b></p>
<p>Here&rsquo;s why open source models matter for this:</p>
<p>You&rsquo;re not dependent on a commercial company&rsquo;s moods about pricing, availability, or model changes. You&rsquo;re not sending your code to external APIs. You&rsquo;re not at risk of waking up to a terms-of-service change that affects your workflow.</p>
<p>The community around Ollama, the models themselves (qwen, deepseek, mistral), and the frameworks we&rsquo;re using are all genuinely open. You can inspect them. You can run them on your hardware. You can contribute back.</p>
<p>That&rsquo;s different from cloud-based AI. It&rsquo;s also different from the single-model approach most people take. It&rsquo;s team-based thinking applied to open source infrastructure.</p>
<p></p>
<p>Where This Goes</p>
<p>The next phase is validation. More diverse tasks. Different problem types. Real production code, not just examples.</p>
<p>We need to understand:</p>

<ul>
<li>Does the approval mechanism hold up when models encounter truly novel situations?</li>
<li>How does cost-per-task scale as complexity increases?</li>
<li>Can the PM learning feedback actually improve specification quality over time?</li>
<li>What happens when the team disagrees and can&rsquo;t converge?</li>
<li>I have hypotheses on these. But hypotheses aren&rsquo;t evidence. Evidence comes from running it.</li>
</ul>
<p>What I like the most about this:&nbsp;</p>
<p>YOU can do it also. You can apply the same concepts to whatever architecture and infrastructure you want. Don&rsquo;t want an IRC server, ok, no problem, I wanted insights into what the team was doing, but you don&rsquo;t have to. Do you want more team members, ok sure&hellip; The concept is based on you using AI to help you work smarter, not harder.&nbsp;</p>
<p>The Philosophy</p>
<p>What we&rsquo;re experimenting with here is: building development automation with tools you control, from models you understand, running on hardware you own.</p>
<p>That matters more than people realize.</p>
<p>My AI team concept isn&rsquo;t trying to replace developers or even me. It&rsquo;s trying to be the kind of colleague that works with me and who catches bugs, asks hard questions, and pushes back on mediocre code. That colleague exists in every good team. Automation is making it possible to have that colleague always present.</p>
<p>Whether this specific approach is the right one, I&rsquo;m not sure yet. But the direction&mdash;toward distributed expertise, adversarial review, and local autonomy&mdash;that direction feels right.</p>
<p>The code is working. The team is functional. The quality improvements are measurable.</p>
<p>Now we find out if it scales, the real work now begins&hellip;.</p>
<p></p>

<p><a href="https://anothermysqldba.blogspot.com/2025/12/open-source-ai-models-building.html">Open Source AI Models Building a Development Team</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Benefits of Importing Data with MariaDB CONNECT and PostgreSQL Data Wrappers</title>
      <link rel="alternate" type="text/html" href="https://vettabase.com/benefits-of-importing-data-with-mariadb-connect-and-postgresql-data-wrappers/" />
      <id>https://vettabase.com/benefits-of-importing-data-with-mariadb-connect-and-postgresql-data-wrappers/</id>
      <updated>2025-12-02T10:44:29+02:00</updated>
      <author><name>Federico Razzoli</name></author>
      <summary type="html"><![CDATA[<p>There are many ways to import data from external sources into a database. MariaDB and PostgreSQL offer native solutions: the MariaDB CONNECT storage engine and PostgreSQL Foreign Data Wrappers. Unfortunately, these options are often overlooked, in favour of more expensive, more fragile and slower solutions. Let’s see what CONNECT and FDWs are, and why they are often the best choice for importing data. MariaDB CONNECT MariaDB knows nothing about how to read or write data, indexes, caches, or running transactions. The MariaDB server sees these as abstract operations, and delegate them to a special type of plugins called storage engines. Storage engines have absolute freedom on how to read and write data, as long as they return the type of variables that MariaDB expects. CONNECT is a storage engine that works with heterogeneous data sources. Here we’ll ignore data files in various formats and other special data source types, and we’ll focus on remote databases. As long as a DBMS supports ODBC, JDBC, the native MySQL protocol or MongoDB protocol, CONNECT should be able to work with it. From a user perspective, it will be exactly working with a local table. Except that connecting to remote sources is slower. But […]</p>
<p><a href="https://vettabase.com/benefits-of-importing-data-with-mariadb-connect-and-postgresql-data-wrappers/">Benefits of Importing Data with MariaDB CONNECT and PostgreSQL Data Wrappers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p class="wp-block-paragraph">There are many ways to import data from external sources into a database. MariaDB and PostgreSQL offer native solutions: the MariaDB CONNECT storage engine and PostgreSQL Foreign Data Wrappers. Unfortunately, these options are often overlooked, in favour of more expensive, more fragile and slower solutions. Let&rsquo;s see what CONNECT and FDWs are, and why they are often the best choice for importing data.</p>
<h2 class="wp-block-heading">MariaDB CONNECT<a class="anchor-link" id="mariadb-connect"></a></h2>
<p class="wp-block-paragraph">MariaDB knows nothing about how to read or write data, indexes, caches, or running transactions. The MariaDB server sees these as abstract operations, and delegate them to a special type of plugins called <a href="https://vettabase.com/category/mariadb/mariadb-storage-engines/" data-type="category" data-id="81">storage engines</a>. Storage engines have absolute freedom on how to read and write data, as long as they return the type of variables that MariaDB expects.</p>
<p class="wp-block-paragraph">CONNECT is a storage engine that works with heterogeneous data sources. Here we&rsquo;ll ignore data files in various formats and other special data source types, and we&rsquo;ll focus on remote databases. As long as a DBMS supports ODBC, JDBC, the native MySQL protocol or MongoDB protocol, CONNECT should be able to work with it. From a user perspective, it will be exactly working with a local table. Except that connecting to remote sources is slower. But this is not a problem for many use cases.</p>
<p class="wp-block-paragraph">Copilot with GPT-4.1 created the following diagram of MariaDB CONNECT architecture:</p>
<pre class="wp-block-code"><code>+---------------------+
|   MariaDB Server    |
+---------------------+
          |
          v
+---------------------------+
|  CONNECT Storage Engine   |
+---------------------------+
    |           |         |
    v           v         v
+--------+  +--------+  +--------+
|  FILE  |  | ODBC   |  | JDBC   |
| Table  |  | Table  |  | Table  |
+--------+  +--------+  +--------+
    |          |          |
    v          v          v
External  Remote DBs   Remote DBs
 Files    (ODBC)       (JDBC)</code></pre>
<h2 class="wp-block-heading">PostgreSQL Foreign Data Wrappers<a class="anchor-link" id="postgresql-foreign-data-wrappers"></a></h2>
<p class="wp-block-paragraph">PostgreSQL has a brilliant extensions system. There are extension types for a number of different goals. One of them if <code>postgresql_fdw</code>, that allows users to develop Foreign Data Wrappers (FDWs). A FDW is a program that allows PostgreSQL to run SQL queries on remote data sources. A huge number of FDWs have been developed, see the <a href="https://wiki.postgresql.org/wiki/Foreign_data_wrappers" rel="noopener">list</a> on PostgreSQL Wiki.</p>
<div class="awgt-alert-content-wrap">
<fieldset class="awgt-alert-box awgt-lay-one">
<legend class="awgt-alert-icon"></legend>
<div class="awgt-alert-content">
<p>Before using one of them, you should check if it&rsquo;s still maintained, if it supports your PostgreSQL version, and if it supports the latest version.</p>
</div>
</fieldset>
</div>
<p class="wp-block-paragraph">Originally, all PostgreSQL FDWs were written in C. Later, some frameworks emerged that allows us to use other languages:</p>
<ul class="wp-block-list">
<li><a href="https://multicorn.org/" rel="noopener">Multicorn</a> for Python;</li>
<li><a href="https://github.com/franckverrot/holycorn" rel="noopener">Holycorn</a> for Ruby;</li>
<li><a href="https://github.com/supabase/wrappers" rel="noopener">Wrappers</a> for Rust.</li>
</ul>
<p class="wp-block-paragraph">Copilot with GPT-4.1 created this diagram for us:</p>
<pre class="wp-block-code"><code>+---------------------------+
|    PostgreSQL Client      |
|      (SQL Query)          |
+---------------------------+
              |
              v
+-------------------------------------+
|      PostgreSQL Database Server     |
| (FDW Extension Installed &amp; Config'd)|
+-------------------------------------+
              |
              v
+-----------------------------+
|  Foreign Data Wrapper (FDW) |
+-----------------------------+
      |            |            |
      v            v            v
+----------+  +----------+  +-------------+
| Remote   |  | Remote   |  | Remote      |
| Postgres |  | MySQL    |  | File/Csv    |
| Database |  | Database |  | or API      |
+----------+  +----------+  +-------------+</code></pre>
<h2 class="wp-block-heading">A CONNECT Table Example<a class="anchor-link" id="a-connect-table-example"></a></h2>
<p class="wp-block-paragraph">There are many ways to create a CONNECT table, depending on which remote technology we are connecting to, whether we want to map all the columns, whether we need to make some transformation, and so on.</p>
<p class="wp-block-paragraph">Here is a trivial example:</p>
<pre class="wp-block-code"><code>CREATE OR REPLACE TABLE world.country
    ENGINE = CONNECT
    TABLE_TYPE = MYSQL
    CONNECTION = 'mysql://connect_se:secret@mariadb-source/world/country'
;</code></pre>
<p class="wp-block-paragraph">In this example, we map the <code>country</code> local table to a remote <code>country</code> table, located on a MariaDB or MySQL server. We are mapping all the columns, so we don&rsquo;t need to specify them. If the remote technology was, for example, SQL Server, we might had needed to specify the columns anyway to define the type mapping.</p>
<h2 class="wp-block-heading">A Foreign Data Wrapper Table Example<a class="anchor-link" id="a-foreign-data-wrapper-table-example"></a></h2>
<p class="wp-block-paragraph">In this simple example, we&rsquo;ll link a local PostgreSQL table to a remote PostgreSQL table.</p>
<p class="wp-block-paragraph">First, we create a FDW to the server:</p>
<pre class="wp-block-code"><code>CREATE SERVER pg2
    FOREIGN DATA WRAPPER postgres_fdw
    OPTIONS (host 'pg2.vettabase.com', dbname 'db', port '5432')
;</code></pre>
<p class="wp-block-paragraph">Then we create a user for the FDW:</p>
<pre class="wp-block-code"><code>CREATE USER MAPPING FOR importer
    SERVER pg2
    OPTIONS (user 'app', password 'Secr3t')
;</code></pre>
<p class="wp-block-paragraph">Finally, we create the table itself:</p>
<pre class="wp-block-code"><code>CREATE FOREIGN TABLE employee (
    id INT,
    first_name TEXT,
    last_name TEXT
)
    SERVER pg2
    OPTIONS (schema_name 'db', table_name 'employee')
;</code></pre>
<h2 class="wp-block-heading">Common Features and Drawbacks<a class="anchor-link" id="common-features-and-drawbacks"></a></h2>
<p class="wp-block-paragraph">Both MariaDB CONNECT and PostgreSQL FDWs allow us to run SQL queries over heterogenous data sources. We can create a table that is linked to a remote database, a <a href="https://vettabase.com/how-to-query-a-rest-api-with-mariadb-connect-engine/" data-type="post" data-id="313059">REST API</a>, a local CSV files, and more. We can both read and write data (though some CONNECT types and some FDWs might be read-only). We can even use JOIN, subqueries, or other SQL constructs that read from multiple tables, combining multiple remote sources and local tables.</p>
<p class="wp-block-paragraph">This approach is great, but it has has some drawbacks. Let&rsquo;s discuss them briefly.</p>
<p class="wp-block-paragraph"><strong>Security implications</strong></p>
<p class="wp-block-paragraph">The server that runs MariaDB or PostgreSQL needs access to the remote data. The database needs to store any necessary credentials.</p>
<p class="wp-block-paragraph">This might look like a serious risk. However, note that many databases contain equally sensitive and valuable, such as user secrets, personal data, and financial data.</p>
<p class="wp-block-paragraph"><strong>Skill mismatch</strong></p>
<p class="wp-block-paragraph">Developers know how to retrieve data from an API. But they&rsquo;re usually not familiar with the CONNECT engine or FDWs. In many cases, building a proper table for importing data is trivial. In complex cases, they might need help from a DBA or a database engineer. This might be a problem for small teams, where these skills are absent.</p>
<p class="wp-block-paragraph"><strong>Slow queries</strong></p>
<p class="wp-block-paragraph">Queries that need remote data, or join remote and local data, are inevitably slower than regular queries. However, this is unlikely to be a problem for scheduled data import processes, or for one-off queries.</p>
<h2 class="wp-block-heading">Comparing CONNECT or FDWs to the alternatives<a class="anchor-link" id="comparing-connect-or-fdws-to-the-alternatives"></a></h2>
<p class="wp-block-paragraph">Let&rsquo;s discuss the other methods to import data into MariaDB or PostgreSQL, and why CONNECT and FDWs are usually better options.</p>
<h3 class="wp-block-heading">REST APIs<a class="anchor-link" id="rest-apis"></a></h3>
<p class="wp-block-paragraph">For application that are maintained by your organisation, an option to export data is to put an API in front of it, and implement calls for data export. Let&rsquo;s see why this is usually not the best idea, when you can use CONNECT or FDWs:</p>
<ul class="wp-block-list">
<li>Developing this feature is expensive, especially if a suitable API doesn&rsquo;t exist at all.</li>
<li>The API also needs to be maintained, just like any script that calls the API. Data structures change, libraries need be upgraded, and so on. And this happens on both sides.</li>
<li>Moving data can take time. If the export is made by a script located on a third host,, the data has to make two trips.</li>
<li>For the same reason, the procedure is less reliable and security risks are higher.</li>
</ul>
<h3 class="wp-block-heading">Generated Data Files<a class="anchor-link" id="generated-data-files"></a></h3>
<p class="wp-block-paragraph">It&rsquo;s possible to have a job that generates data files from the source and sends them to the target. This might be done using CSV or JSON. Or it&rsquo;s possible to generate a logical backup (a dump).</p>
<p class="wp-block-paragraph">This is the simplest way to handle data transfers, provided that only one or just a few tables are copied. Also, if the source and target table structures change over time, the process will need be fixed. Also, this might result into a nightmare if the source and the target technologies are different.</p>
<h3 class="wp-block-heading">Import Queries<a class="anchor-link" id="import-queries"></a></h3>
<p class="wp-block-paragraph">A script can simply run queries against the source, obtain data, and write them to the server. This allows to handle differences between the source and the target in more flexible ways, import data more selectively, apply transormations, and so on. However, at the end of the day, you&rsquo;ll simply have a script that simulates the job typically made by CONNECT or FDWs.</p>
<p class="wp-block-paragraph">On top on that, the script will typically run on a third host, making the process slower, less reliable, and less secure.</p>
<h2 class="wp-block-heading">Conclusions<a class="anchor-link" id="conclusions"></a></h2>
<p class="wp-block-paragraph">We discussed two great way to import data into MariaDB and PostgreSQL: CONNECT and Foreign Data Wrappers. We also highlighted why, while these solutions aren&rsquo;t very common, they are usually faster, more reliable, and more secure than alternatives.</p>
<p class="wp-block-paragraph">Take a look at the <a href="https://vettabase.com/tag/mariadb-connect/">mariadb-connect</a> tag to see our articles on MariaDB CONNECT.</p>
<p class="wp-block-paragraph"><em>Federico Razzoli</em></p>
<p class="wp-block-paragraph">
</p>
<p><a href="https://vettabase.com/benefits-of-importing-data-with-mariadb-connect-and-postgresql-data-wrappers/">Benefits of Importing Data with MariaDB CONNECT and PostgreSQL Data Wrappers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Community Recap: Percona.Connect London 2025, Building the Future of Open Source Together</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/12/02/community-recap-percona.connect-london-2025-building-the-future-of-open-source-together/" />
      <id>https://percona.community/blog/2025/12/02/community-recap-percona.connect-london-2025-building-the-future-of-open-source-together/</id>
      <updated>2025-12-02T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Percona.Connect London 2025 brought the open-source database community together for a half-day of learning and collaboration. The event focused on providing practical, technical insights for DBAs, DevOps engineers, and developers. The main takeaway was clear: Stability, Openness, and Automation are essential for modern, large-scale data infrastructure.</p>
<p><a href="https://percona.community/blog/2025/12/02/community-recap-percona.connect-london-2025-building-the-future-of-open-source-together/">Community Recap: Percona.Connect London 2025, Building the Future of Open Source Together</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://connect.percona.com/london/" target="_blank" rel="noopener noreferrer">Percona.Connect London 2025</a> brought the open-source database community together for a half-day of learning and collaboration. The event focused on providing practical, technical insights for DBAs, DevOps engineers, and developers. The main takeaway was clear: Stability, Openness, and Automation are essential for modern, large-scale data infrastructure.</p>
<h2>Top Discussions &amp; Key Takeaways<a class="anchor-link" id="top-discussions-key-takeaways"></a></h2>
<h2>1. The Rise of Valkey: A Truly Open Caching Alternative<a class="anchor-link" id="1-the-rise-of-valkey-a-truly-open-caching-alternative"></a></h2>
<p><strong>Martin Visser</strong>, Valkey Technical Lead, explained the changes to the Redis license, the community needs a trusted, open-source replacement. Valkey was highlighted as the leading solution.</p>
<ul>
<li><a href="https://github.com/valkey-io/valkey" target="_blank" rel="noopener noreferrer">Valkey</a> was started by former Redis contributors quickly after Redis removed its open source license in 2024.</li>
<li>It is a true open-source project governed under the Linux Foundation.</li>
<li>It offers enhancements like better memory efficiency, performance, and scalability.</li>
<li>In a recent Percona survey of 200 DBAs, <strong>Valkey was the most preferred alternative to Redis</strong>.</li>
</ul>
<p><figure><img decoding="async" width="1249" height="707" src="https://percona.community/blog/2025/12/img1_hu_de67e465bf009964.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<h2>2. Running PostgreSQL in a Cloud Native context<a class="anchor-link" id="2-running-postgresql-in-a-cloud-native-context"></a></h2>
<p><strong>Takis Stathopoulos</strong>, Enterprise Architect, presented on running PostgreSQL in a Cloud Native context, explaining how Kubernetes Operators simplify complex deployments.</p>
<ul>
<li><strong>Cloud Native vs. Cloud First</strong>: Cloud Native (Kubernetes) offers Portability and No vendor lock-in, allowing you to run the database consistently across different clouds and on-premise infrastructure.</li>
<li><strong>Percona Operator for PostgreSQL</strong>: This tool automates crucial operations like setting up high availability (using Patroni), backups (using pgBackrest), and scaling.</li>
<li><strong>When to use Cloud Native</strong>: It&rsquo;s ideal for large, microservice-based applications and teams prioritizing portability and avoiding vendor lock-in.</li>
</ul>
<p><figure><img decoding="async" width="1273" height="709" src="https://percona.community/blog/2025/12/img2_hu_234370e37e7b7e5a.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<p><figure><img decoding="async" width="1268" height="714" src="https://percona.community/blog/2025/12/img3_hu_4ed76eaedcdfccab.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<h2>3. Native PostgreSQL TDE is Here: Securing Data Simply<a class="anchor-link" id="3-native-postgresql-tde-is-here-securing-data-simply"></a></h2>
<p><strong>Alastair Turner</strong>, Postgres Community Advocate, introduced the new Native Transparent Data Encryption (TDE) for PostgreSQL.</p>
<p><figure><img decoding="async" width="2890" height="1600" src="https://percona.community/blog/2025/12/extra_hu_80a596e8b140f820.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
<figure><img decoding="async" width="1441" height="807" src="https://percona.community/blog/2025/12/img4_hu_93f8d9b3dd6d8b94.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<h2>4. The Future of MySQL: Vector Search &amp; Binlog Server<a class="anchor-link" id="4-the-future-of-mysql-vector-search-binlog-server"></a></h2>
<p><strong>Dennis Kittrell</strong>, MySQL Product Manager, discussed two key features planned for MySQL that address major operational and feature challenges.</p>
<ul>
<li><strong>MySQL Binlog Server MVP</strong>: This component aims to solve the problem of quick disaster recovery by acting as a stable, reliable replication source. It enables Precise Point-in-Time Recovery (PITR) using simple time or GTID coordinates.</li>
<li><strong>Native Vector Support MVP</strong>: This feature allows users to eliminate the complexity of using a separate vector database. You can store, index, and search vector embeddings directly in MySQL, allowing you to combine vector searches with standard business logic in a single, transactional query</li>
</ul>
<h3>Our Community Focus<a class="anchor-link" id="our-community-focus"></a></h3>
<p>A common theme from the use cases was that while open source adoption is high, operational teams often lack the proper support and visibility.</p>
<p>Percona&rsquo;s goal is to support the community by providing:</p>
<ul>
<li>Stability when under heavy load or during maintenance.</li>
<li>Faster Troubleshooting with better monitoring and observability.</li>
<li>Safer Deployments through expert configuration and security support.</li>
</ul>
<p><figure><img decoding="async" width="4032" height="3024" src="https://percona.community/blog/2025/12/img5_hu_aac22caf4b12df56.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<p>Thank you to everyone who joined us in London for a dynamic event. We hope the insights gained will help you with your open source database deployments.</p>
<p>The conversations continue in the Percona Community! You can reach out directly to the speakers:</p>
<ul>
<li>Martin Visser (Valkey Technical Lead) <a href="https://www.linkedin.com/in/martinrvisser/" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
<li>Dennis Kittrell (MySQL Product Manager) <a href="https://www.linkedin.com/in/kittrell/" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
<li>Alastair Turner (Postgres Community Advocate) <a href="https://www.linkedin.com/in/decodableminion/" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
<li>Takis Stathopoulos (Enterprise Architect) <a href="https://www.linkedin.com/in/pgstathopoulos/" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
<li>Andre Pons (Enterprise Sales Manager) <a href="https://www.linkedin.com/in/andre-pons-8b4a1013/" target="_blank" rel="noopener noreferrer">LinkedIn</a></li>
</ul>
<p><figure><img decoding="async" width="4019" height="2179" src="https://percona.community/blog/2025/12/img7_hu_887a94578c4f2bfc.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
<figure><img decoding="async" width="4284" height="4010" src="https://percona.community/blog/2025/12/img6_hu_df50e23b208c1ff7.webp" alt="Percona Connect London 2025" loading="lazy"></figure>
</p>
<p>Join the Percona Community Conversation!</p>
<ul>
<li><a href="https://forum.percona.com/" target="_blank" rel="noopener noreferrer">Percona Forum</a></li>
<li><a href="https://www.linkedin.com/company/percona/" target="_blank" rel="noopener noreferrer">Percona on LinkedIn</a></li>
</ul>

<p><a href="https://percona.community/blog/2025/12/02/community-recap-percona.connect-london-2025-building-the-future-of-open-source-together/">Community Recap: Percona.Connect London 2025, Building the Future of Open Source Together</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>DEP-18: A proposal for Git-based collaboration in Debian</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/debian-collaboration-on-git/" />
      <id>https://optimizedbyotto.com/post/debian-collaboration-on-git/</id>
      <updated>2025-11-30T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>I am a huge fan of Git, as I have witnessed how it has made software development so much more productive compared to the pre-2010s era. I wish all Debian source code were in Git to reap the full benefits.<br />
Git is not perfect, as it requires significant effort to learn properly, and the ecosystem is complex with even more things to learn ranging from cryptographic signatures and commit hooks to Git-assisted code review best practices, ‘forge’ websites, and CI systems.<br />
Sure, there is still room to optimize its use, but Git certainly has proven itself and is now the industry standard. Thus, some readers might be surprised to learn that Debian development in 2025 is not actually based on Git. In Debian, the version control is done by the Debian archive itself. Each ‘commit’ is a new upload to the archive, and the ‘commit message’ is the debian/changelog entry. The ‘commit log’ is available at snapshots.debian.org.<br />
In practice, most Debian Developers (people who have the credentials to upload to the Debian archive) do use Git and host their packaging source code on salsa.debian.org – the GitLab instance of Debian. This is, however, based on each DD’s personal preferences. The Debian project does not have any policy requiring that packages be hosted on salsa.debian.org or be in version control at all.<br />
Is collaborative software development possible without git and version control software?<br />
Debian, however, has some peculiarities that may be surprising to people who have grown accustomed to GitHub, GitLab or various company-internal code review systems.<br />
In Debian:</p>
<p>The source code of the next upload is not public but resides only on the developer’s laptop.<br />
Code contributions are plain patch files, based on the latest revision released in the Debian archive (where the unstable area is equivalent to the main development branch).<br />
These patches are submitted by email to a bug tracker that does no validation or testing whatsoever.<br />
Developers applying these patches typically have elaborate Mutt or Emacs setups to facilitate fetching patches from email.<br />
There is no public staging area, no concept of rebasing patches or withdrawing a patch and replacing it with a better version.<br />
The submitter won’t see any progress information until a notification email arrives after a new version has been uploaded to the Debian archive.</p>
<p>This system has served Debian for three decades. It is not broken, but using the package archive just feels… well, archaic.<br />
There is a more efficient way, and indeed the majority of Debian packages have a metadata field Vcs-Git that advertises which version control repository the maintainer uses. However, newcomers to Debian are surprised to notice that not all packages are hosted on salsa.debian.org but at various random places with their own account and code submission systems, and there is nothing enforcing or even warning if the code there is out of sync with what was uploaded to Debian. Any Debian Developer can at any time upload a new package with whatever changes, bypassing the Git repository, even when the package advertised a Git repository. All PGP signed commits, Git tags and other information in the Git repository are just extras currently, as the Debian archive does not enforce or validate anything about them.<br />
This also makes contributing to multiple packages in parallel hard. One can’t just go on salsa.debian.org and fork a bunch of repositories and submit Merge Requests. Currently, the only reliable way is to download source packages from Debian unstable, develop patches on top of them, and send the final version as a plain patch file by email to the Debian bug tracker. To my knowledge, no system exists to facilitate working with the patches in the bug tracker, such as rebasing patches 6 months later to detect if they or equivalent changes were applied or if sending refreshed versions is needed.<br />
To newcomers in Debian, it is even more surprising that there are packages that are on salsa.debian.org but have the Merge Requests feature disabled. This is often because the maintainer does not want to receive notification emails about new Merge Requests, but rather just emails from bugs.debian.org. This may sound arrogant, but keep in mind that these developers put in the effort to set up their Mutt/Emacs workflow for the existing Debian process, and extending it to work with GitLab notifications is not trivial. There are also purists who want to do everything via the command-line (without having to open a browser, run JavaScript and maintain a live Internet connection), and tools like glab are not convenient enough for the full workflow.<br />
Inefficient ways of working prevent Debian from flourishing<br />
I would claim, based on my personal experiences from the past 10+ years as a Debian Developer, that the lack of high-quality and productive tooling is seriously harming Debian. The current methods of collaboration are cumbersome for aspiring contributors to learn and suboptimal to use for both new and seasoned contributors.<br />
There are no exit interviews for contributors who left Debian, no comprehensive data on reasons to contribute or stop contributing, nor are there any metrics tracking how many people tried but failed to contribute to Debian. Some data points to support my concerns do exist:</p>
<p>The contributor database shows that the number of contributors is growing slower than Debian’s popularity.<br />
Most packages are maintained by one person working alone (just pick any package at random and look at the upload history).</p>
<p>Debian should embrace git, but decision-making is slow<br />
Debian is all about community and collaboration. One would assume that Debian prioritized above all making collaboration tools and processes simpler, faster and less error-prone, as it would help both current and future package maintainers. Yet, it isn’t so, due to some reasons unique to Debian.<br />
There is no single company or entity running Debian, and it has managed to operate as a pure meritocracy and do-cracy for over 30 years. This is impressive and admirable. Unfortunately, some of the infrastructure and technical processes are also nearly 30 years old and very difficult to change for the same reason: the nature of Debian’s distributed decision-making process.<br />
As a software developer and manager with 25+ years of experience, I strongly feel that developing software collaboratively using Git is a major step forward that Debian needs to take, in one form or another, and I hope to see other DDs voice their support if they agree.<br />
Debian Enhancement Proposal 18<br />
Following how consensus is achieved in Debian, I started drafting DEP-18 in 2024, and it is currently awaiting enough thumbs up at https://salsa.debian.org/dep-team/deps/-/merge_requests/21 to get into CANDIDATE status next.<br />
In summary, the DEP-18 proposes that everyone keen on collaborating should:</p>
<p>Maintain Debian packaging sources in Git on Salsa.<br />
Use Merge Requests to show your work and to get reviews.<br />
Run Salsa CI before upload.</p>
<p>The principles above are not novel. According to stats at e.g. trends.debian.net, and UDD, ~93% of all Debian source packages are already hosted on salsa.debian.org. As of June 1st, 2025, only 1640 source packages remain that are not hosted on Salsa. The purpose of DEP-18 is to state in writing what Debian is currently doing for most packages, and thus express what among others new contributors should be learning and doing, so basic collaboration is smooth and free from structural obstacles.<br />
Most packages are also already allowing Merge Requests and using Salsa CI, but there hasn’t been any written recommendation anywhere in Debian to do so. The Debian Policy (v.4.7.2) does not even mention the word “Salsa” a single time. The current process documentation on how to do non-maintainer uploads or salvaging packages are all based on uploading packages to the archive, without any consideration of using git-based collaboration such as posting a Merge Request first. Personally I feel posting a Merge Request would be a better approach, as it would invite collaborators to discuss and provide code reviews. If there are no responses, the submitter can proceed to merge, but compared to direct uploads to the Debian archive, the Merge Request practice at least tries to offer a time and place for discussions and reviews to happen.<br />
It could very well be that in the future somebody comes up with a new packaging format that makes upstream source package management easier, or a monorepo with all packages, or some other future structures or processes. Having a DEP to state how to do things now does not prevent people from experimenting and innovating if they intentionally want to do that. The DEP is merely an expression of the minimal common denominators in the packaging workflow that maintainers and contributors should follow, unless they know better.<br />
Transparency and collaboration<br />
Among the DEP-18 recommendations is:</p>
<p>The recommended first step in contributing to a package is to use the built-in “Fork” feature on Salsa. This serves two purposes. Primarily, it allows any contributor to publish their Git branches and submit them as Merge Requests. Additionally, the mere existence of a list of “Forks” enables contributors to discover each other, and in rare cases when the original package is not accepting improvements, collaboration could arise among the contributors and potentially lead to permanent forks in the general meaning. Forking is a fundamental part of the dynamics in open source that helps drive quality and agreement. The ability to fork ultimately serves as the last line of defense of users’ rights. Git supports this by making both temporary and permanent forks easy to create and maintain.</p>
<p>Further, it states:</p>
<p>Debian packaging work should be reasonably transparent and public to allow contributors to participate. A maintainer should push their pending changes to Salsa at regular intervals, so that a potential contributor can discover if a particular change has already been made or a bug has been fixed in version control, and thus avoid duplicate work.<br />
Debian maintainers should make reasonable efforts to publish planned changes as Merge Requests on Salsa and solicit feedback and reviews. While pushing changes directly on the main Git branch is the fastest workflow, second only to uploading all changes directly to Debian repositories, it is not an inclusive way to develop software. Even packages that are maintained by a single maintainer should at least occasionally publish Merge Requests to allow new contributors to step up and participate.</p>
<p>I think these are key aspects leading to transparency and true open source collaboration. Even though this talks about Salsa — which is based on GitLab — the concepts are universal and will work also on other forges, like Forgejo or GitHub. The point is that sharing work-in-progress on a real-time platform, with CI and other supporting features, empowers and motivates people to iterate on code collaboratively. As an example of an anti-pattern, Oracle MySQL publishes the source code for all their releases and is license-compliant, but as they don’t publish their Git commits in real-time, it does not feel like a real open source project. Non-Oracle employees are not motivated to participate as second-class developers who are kept in the dark. Debian should embrace git and sharing work in real-time, embodying a true open source spirit.<br />
Recommend, not force<br />
Note that the Debian Enhancement Proposals are not binding. Only the Debian Policy and Technical Committee decisions carry that weight. The nature of collaboration is voluntary anyway, so the DEP does not need to force anything on people who don’t want to use salsa.debian.org.<br />
The DEP-18 is also not a guide for package maintainers. I have my own views and have written detailed guides in blog articles if you want to read more on, for example, how to do code reviews efficiently.<br />
Within DEP-18, there is plenty of room to work in many different ways, and it does not try to force one single workflow. The goal here is to simply have agreed-upon minimal common denominators among those who are keen to collaborate using salsa.debian.org, not to dictate a complete code submission workflow.<br />
Once we reach this, there will hopefully be less friction in the most basic and recurring collaboration tasks, giving DDs more energy to improve other processes or just invest in having more and newer packages for Debian users to enjoy.<br />
Next steps<br />
In addition to lengthy online discussions on mailing lists and DEP reviews, I also presented on this topic at DebConf 2025 in Brest, France. Unfortunately the recording is not yet up on Peertube.<br />
The feedback has been overwhelmingly positive. However, there are a few loud and very negative voices that cannot be ignored. Maintaining a Linux distribution at the scale and complexity of Debian requires extraordinary talent and dedication, and people doing this kind of work often have strong views, most of the time for good reasons. We do not want to alienate existing key contributors with new processes, so maximum consensus is desirable.<br />
We also need more data on what the 1000+ current Debian Developers view as a good process to avoid being skewed by a loud minority. If you are a current or aspiring Debian Developer, please add a thumbs up if you think I should continue with this effort (or a thumbs down if not) on the Merge Request that would make DEP-18 have candidate status.<br />
There is also technical work to do. Increased Git use will obviously lead to growing adoption of the new tag2upload feature, which will need to get full git-buildpackage support so it can integrate into salsa.debian.org without turning off Debian packaging security features. The git-buildpackage tool itself also needs various improvements, such as making contributing to multiple different packages with various levels of diligence in debian/gbp.conf maintenance less error-prone.<br />
Eventually, if it starts looking like all Debian packages might get hosted on salsa.debian.org, I would also start building a review.debian.org website to facilitate code review aspects that are unique to Debian, such as tracking Merge Requests across GitLab projects in ways GitLab can’t do, highlighting which submissions need review most urgently, feeding code reviews and approvals into the contributors.debian.org database for better attribution, and so forth.<br />
Details on this vision will be in a later blog post, so subscribe to updates!</p>
<p><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/">DEP-18: A proposal for Git-based collaboration in Debian</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><img decoding="async" src="https://optimizedbyotto.com/post/debian-collaboration-on-git/debian-git-collaboration.jpg" alt="Featured image of post DEP-18: A proposal for Git-based collaboration in Debian"></p>
<p>I am a huge fan of Git, as I have witnessed how it has made software development so much more productive compared to the pre-2010s era. I wish all Debian source code were in Git to reap the full benefits.</p>
<p>Git is not perfect, as it requires significant effort to learn properly, and the ecosystem is complex with even more things to learn ranging from cryptographic signatures and commit hooks to Git-assisted code review best practices, &lsquo;forge&rsquo; websites, and CI systems.</p>
<p>Sure, there is still room to optimize its use, but Git certainly has proven itself and is now the industry standard. <strong>Thus, some readers might be surprised to learn that Debian development in 2025 is not actually based on Git.</strong> In Debian, the version control is done by the Debian archive itself. Each &lsquo;commit&rsquo; is a new upload to the archive, and the &lsquo;commit message&rsquo; is the <code>debian/changelog</code> entry. The &lsquo;commit log&rsquo; is available at <a class="link" href="https://snapshot.debian.org/" target="_blank" rel="noopener">snapshots.debian.org</a>.</p>
<p>In practice, most Debian Developers (people who have the credentials to upload to the Debian archive) do use Git and host their packaging source code on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a> &ndash; the GitLab instance of Debian. This is, however, based on each DD&rsquo;s personal preferences. <strong>The Debian project does not have any policy requiring that packages be hosted on salsa.debian.org or be in version control at all.</strong></p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#is-collaborative-software-development-possible-without-git-and-version-control-software" class="header-anchor"></a>Is collaborative software development possible without git and version control software?<br>
<a class="anchor-link" id="is-collaborative-software-development-possible-without-git-and-version-control-software"></a></h2>
<p>Debian, however, has some peculiarities that may be surprising to people who have grown accustomed to GitHub, GitLab or various company-internal code review systems.</p>
<p>In Debian:</p>
<ul>
<li>The source code of the next upload is not public but resides only on the developer&rsquo;s laptop.</li>
<li>Code contributions are plain patch files, based on the latest revision released in the Debian archive (where the <code>unstable</code> area is equivalent to the main development branch).</li>
<li>These patches are submitted by email to a bug tracker that does no validation or testing whatsoever.</li>
<li>Developers applying these patches typically have elaborate Mutt or Emacs setups to facilitate fetching patches from email.</li>
<li>There is no public staging area, no concept of rebasing patches or withdrawing a patch and replacing it with a better version.</li>
<li>The submitter won&rsquo;t see any progress information until a notification email arrives after a new version has been uploaded to the Debian archive.</li>
</ul>
<p>This system has served Debian for three decades. It is not broken, but using the package archive just feels&hellip; well, <em>archaic</em>.</p>
<p>There is a more efficient way, and indeed the majority of Debian packages have a metadata field <code>Vcs-Git</code> that advertises which version control repository the maintainer uses. However, newcomers to Debian are surprised to notice that not all packages are hosted on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a> but at various random places with their own account and code submission systems, and there is nothing enforcing or even warning if the code there is <strong>out of sync with what was uploaded to Debian</strong>. Any Debian Developer can at any time upload a new package with whatever changes, bypassing the Git repository, even when the package advertised a Git repository. All PGP signed commits, Git tags and other information in the Git repository are <em>just extras</em> currently, as the Debian archive does not enforce or validate anything about them.</p>
<p>This also makes contributing to multiple packages in parallel hard. One can&rsquo;t just go on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a> and fork a bunch of repositories and submit Merge Requests. Currently, the <strong>only reliable way is to download source packages from Debian unstable</strong>, develop patches on top of them, and send the final version as a plain <strong>patch file by email to the Debian bug tracker</strong>. To my knowledge, no system exists to facilitate working with the patches in the bug tracker, such as rebasing patches 6 months later to detect if they or equivalent changes were applied or if sending refreshed versions is needed.</p>
<p>To newcomers in Debian, it is even more surprising that there are packages that <em>are</em> on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a> but have the Merge Requests feature disabled. This is often because the maintainer does not want to receive notification emails about new Merge Requests, but rather just emails from <a class="link" href="https://bugs.debian.org/" target="_blank" rel="noopener">bugs.debian.org</a>. This may sound arrogant, but keep in mind that these developers put in the effort to set up their Mutt/Emacs workflow for the existing Debian process, and extending it to work with GitLab notifications is not trivial. There are also purists who want to do everything via the command-line (without having to open a browser, run JavaScript and maintain a live Internet connection), and tools like <a class="link" href="https://manpages.debian.org/unstable/glab/glab.1.en.html" target="_blank" rel="noopener">glab</a> are not convenient enough for the full workflow.</p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#inefficient-ways-of-working-prevent-debian-from-flourishing" class="header-anchor"></a>Inefficient ways of working prevent Debian from flourishing<br>
<a class="anchor-link" id="inefficient-ways-of-working-prevent-debian-from-flourishing"></a></h2>
<p>I would claim, based on my personal experiences from the past 10+ years as a Debian Developer, that <strong>the lack of high-quality and productive tooling is seriously harming Debian</strong>. The current methods of collaboration are cumbersome for aspiring contributors to learn and suboptimal to use for both new and seasoned contributors.</p>
<p>There are no exit interviews for contributors who left Debian, no comprehensive data on reasons to contribute or stop contributing, nor are there any metrics tracking how many people tried but failed to contribute to Debian. Some data points to support my concerns do exist:</p>
<ul>
<li>The contributor database shows that the <a class="link" href="https://salsa.debian.org/rafael/debian-contrib-years" target="_blank" rel="noopener">number of contributors is growing slower</a> than Debian&rsquo;s popularity.</li>
<li>Most packages are maintained by one person working alone (just pick any package at random and look at the upload history).</li>
</ul>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#debian-should-embrace-git-but-decision-making-is-slow" class="header-anchor"></a>Debian should embrace git, but decision-making is slow<br>
<a class="anchor-link" id="debian-should-embrace-git-but-decision-making-is-slow"></a></h2>
<p>Debian is all about community and collaboration. One would assume that Debian prioritized above all making collaboration tools and processes simpler, faster and less error-prone, as it would help both current and future package maintainers. Yet, it isn&rsquo;t so, due to some reasons unique to Debian.</p>
<p>There is no single company or entity running Debian, and it has managed to operate as a pure <strong>meritocracy and do-cracy for over 30 years</strong>. This is impressive and admirable. Unfortunately, some of the infrastructure and technical processes are also nearly 30 years old and very difficult to change for the same reason: the nature of Debian&rsquo;s distributed decision-making process.</p>
<p>As a software developer and manager with 25+ years of experience, I strongly feel that developing software collaboratively using Git is a major step forward that Debian needs to take, in one form or another, and I <strong>hope to see other DDs voice their support</strong> if they agree.</p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#debian-enhancement-proposal-18" class="header-anchor"></a>Debian Enhancement Proposal 18<br>
<a class="anchor-link" id="debian-enhancement-proposal-18"></a></h2>
<p>Following how consensus is achieved in Debian, I started drafting <a class="link" href="https://dep-team.pages.debian.net/deps/dep18/" target="_blank" rel="noopener">DEP-18</a> in 2024, and it is currently awaiting enough <em>thumbs up</em> at <a class="link" href="https://salsa.debian.org/dep-team/deps/-/merge_requests/21" target="_blank" rel="noopener">https://salsa.debian.org/dep-team/deps/-/merge_requests/21</a> to get into <em>CANDIDATE</em> status next.</p>
<p>In summary, the DEP-18 proposes that everyone keen on collaborating should:</p>
<ol>
<li>Maintain Debian packaging sources in Git on Salsa.</li>
<li>Use Merge Requests to show your work and to get reviews.</li>
<li>Run Salsa CI before upload.</li>
</ol>
<p>The principles above are not novel. According to stats at e.g. <a class="link" href="https://trends.debian.net/#vcs-hosting" target="_blank" rel="noopener">trends.debian.net</a>, and <a class="link" href="https://udd.debian.org/cgi-bin/dep14stats.cgi" target="_blank" rel="noopener">UDD</a>, ~93% of all Debian source packages are already hosted on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a>. As of June 1st, 2025, only 1640 source packages remain that are not hosted on Salsa. The purpose of DEP-18 is to state in writing what Debian is currently doing for most packages, and thus express what among others new contributors should be learning and doing, so basic collaboration is smooth and free from structural obstacles.</p>
<p>Most packages are also already allowing Merge Requests and using Salsa CI, but there hasn&rsquo;t been any written recommendation anywhere in Debian to do so. The <a class="link" href="https://www.debian.org/doc/debian-policy/" target="_blank" rel="noopener">Debian Policy (v.4.7.2)</a> does not even mention the word &ldquo;Salsa&rdquo; a single time. The current <a class="link" href="https://www.debian.org/doc/manuals/developers-reference/" target="_blank" rel="noopener">process documentation</a> on how to do non-maintainer uploads or salvaging packages are all based on uploading packages to the archive, without any consideration of using git-based collaboration such as posting a Merge Request first. Personally I feel <strong>posting a Merge Request would be a better approach</strong>, as it would invite collaborators to discuss and provide code reviews. If there are no responses, the submitter can proceed to merge, but compared to direct uploads to the Debian archive, the Merge Request practice at least tries to offer a time and place for discussions and reviews to happen.</p>
<p>It could very well be that in the future somebody comes up with a new packaging format that makes upstream source package management easier, or a monorepo with all packages, or some other future structures or processes. Having a DEP to state how to do things <em>now</em> does not prevent people from experimenting and innovating if they intentionally want to do that. The DEP is merely an expression of the minimal common denominators in the packaging workflow that maintainers and contributors should follow, <em>unless they know better</em>.</p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#transparency-and-collaboration" class="header-anchor"></a>Transparency and collaboration<br>
<a class="anchor-link" id="transparency-and-collaboration"></a></h2>
<p>Among the <a class="link" href="https://dep-team.pages.debian.net/deps/dep18/" target="_blank" rel="noopener">DEP-18</a> recommendations is:</p>
<blockquote>
<p>The recommended first step in contributing to a package is to use the built-in &ldquo;Fork&rdquo; feature on Salsa. This serves two purposes. Primarily, it allows any contributor to publish their Git branches and submit them as Merge Requests. Additionally, the mere existence of a list of &ldquo;Forks&rdquo; enables contributors to discover each other, and in rare cases when the original package is not accepting improvements, collaboration could arise among the contributors and potentially lead to permanent forks in the general meaning. Forking is a fundamental part of the dynamics in open source that helps drive quality and agreement. The ability to fork ultimately serves as the last line of defense of users&rsquo; rights. Git supports this by making both temporary and permanent forks easy to create and maintain.</p>
</blockquote>
<p>Further, it states:</p>
<blockquote>
<p>Debian packaging work should be reasonably transparent and public to allow contributors to participate. A maintainer should push their pending changes to Salsa at regular intervals, so that a potential contributor can discover if a particular change has already been made or a bug has been fixed in version control, and thus avoid duplicate work.</p>
<p>Debian maintainers should make reasonable efforts to publish planned changes as Merge Requests on Salsa and solicit feedback and reviews. While pushing changes directly on the main Git branch is the fastest workflow, second only to uploading all changes directly to Debian repositories, it is not an inclusive way to develop software. Even packages that are maintained by a single maintainer should at least occasionally publish Merge Requests to allow new contributors to step up and participate.</p>
</blockquote>
<p><strong>I think these are key aspects leading to transparency and true open source collaboration.</strong> Even though this talks about <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">Salsa</a> &mdash; which is based on <a class="link" href="https://gitlab.com/" target="_blank" rel="noopener">GitLab</a> &mdash; the concepts are universal and will work also on other forges, like <a class="link" href="https://forgejo.org/" target="_blank" rel="noopener">Forgejo</a> or <a class="link" href="https://github.com/" target="_blank" rel="noopener">GitHub</a>. <strong>The point is that sharing work-in-progress on a real-time platform</strong>, with CI and other supporting features, <strong>empowers and motivates people</strong> to iterate on code collaboratively. As an example of an anti-pattern, Oracle MySQL publishes the source code for all their releases and is license-compliant, but as they don&rsquo;t publish their Git commits in real-time, it does not feel like a real open source project. Non-Oracle employees are not motivated to participate as second-class developers who are kept in the dark. Debian should embrace git and sharing work in real-time, embodying a true open source spirit.</p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#recommend-not-force" class="header-anchor"></a>Recommend, not force<br>
<a class="anchor-link" id="recommend-not-force"></a></h2>
<p>Note that the Debian Enhancement Proposals are not binding. Only the Debian Policy and Technical Committee decisions carry that weight. The nature of collaboration is voluntary anyway, so the DEP does not need to force anything on people who don&rsquo;t want to use <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a>.</p>
<p>The DEP-18 is also not a guide for package maintainers. I have my own views and have written detailed guides in blog articles if you want to read more on, for example, how to do <a class="link" href="https://optimizedbyotto.com/post/how-to-code-review/">code reviews</a> efficiently.</p>
<p>Within DEP-18, there is plenty of room to work in many different ways, and it does not try to force one single workflow. <strong>The goal here is to simply have agreed-upon minimal common denominators among those who are keen to collaborate using salsa.debian.org,</strong> not to dictate a complete code submission workflow.</p>
<p>Once we reach this, there will hopefully be less friction in the most basic and recurring collaboration tasks, giving DDs more energy to improve other processes or just invest in having more and newer packages for Debian users to enjoy.</p>
<h2><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/#next-steps" class="header-anchor"></a>Next steps<br>
<a class="anchor-link" id="next-steps"></a></h2>
<p>In addition to lengthy online discussions on mailing lists and DEP reviews, I also <a class="link" href="https://debconf25.debconf.org/talks/135-merge-request-based-collaboration-for-debian-packages/" target="_blank" rel="noopener">presented on this topic at DebConf 2025</a> in Brest, France. Unfortunately the recording is not yet up on <a class="link" href="https://peertube.debian.social/" target="_blank" rel="noopener">Peertube</a>.</p>
<p>The feedback has been overwhelmingly positive. However, there are a few loud and very negative voices that cannot be ignored. Maintaining a Linux distribution at the scale and complexity of Debian requires extraordinary talent and dedication, and people doing this kind of work often have strong views, most of the time for good reasons. We do not want to alienate existing key contributors with new processes, so maximum consensus is desirable.</p>
<p>We also need more data on what the 1000+ current Debian Developers view as a good process to avoid being skewed by a loud minority. <strong>If you are a current or aspiring Debian Developer, <a class="link" href="https://salsa.debian.org/dep-team/deps/-/merge_requests/21" target="_blank" rel="noopener">please add a thumbs up</a> if you think I should continue with this effort (or a thumbs down if not) on the Merge Request that would make DEP-18 have <em>candidate</em> status.</strong></p>
<p>There is also technical work to do. Increased Git use will obviously lead to growing adoption of the new <a class="link" href="https://manpages.debian.org/unstable/git-debpush/tag2upload.5.en.html" target="_blank" rel="noopener">tag2upload</a> feature, which will need to get full <code>git-buildpackage</code> support so it can integrate into <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a> without <a class="link" href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1106071" target="_blank" rel="noopener">turning off</a> Debian packaging security features. The <code>git-buildpackage</code> tool itself also needs various improvements, such as making contributing to multiple different packages with various levels of diligence in <code>debian/gbp.conf</code> maintenance less error-prone.</p>
<p>Eventually, if it starts looking like all Debian packages might get hosted on <a class="link" href="https://salsa.debian.org/" target="_blank" rel="noopener">salsa.debian.org</a>, I would also start building a <em>review.debian.org</em> website to facilitate code review aspects that are unique to Debian, such as tracking Merge Requests across GitLab projects in ways GitLab can&rsquo;t do, highlighting which submissions need review most urgently, feeding code reviews and approvals into the <a class="link" href="https://contributors.debian.org/" target="_blank" rel="noopener">contributors.debian.org</a> database for better attribution, and so forth.</p>
<p>Details on this vision will be in a later blog post, so subscribe to updates!</p>

<p><a href="https://optimizedbyotto.com/post/debian-collaboration-on-git/">DEP-18: A proposal for Git-based collaboration in Debian</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Attribute promotion and demotion in the MariaDB Galera Cluster</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/" />
      <id>https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/</id>
      <updated>2025-11-28T16:26:48+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>In MariaDB master/slave replication there is a feature called attribute promotion/demotion.<br />
Simply put, it is about how the slave behaves or should behave if the master and slave have different column definitions or even a different number of columns or a different sequence of columns.<br />
Use case of the customer<br />
This week we discussed with a customer the case of how he could perform a rolling schema upgrade (RSU) in a Galera cluster.<br />
With previous schema changes he has always had problems, which has led to a total failure of the cluster for several hours.<br />
The customer says that columns are never deleted and new columns are only ever added at the end of a table.<br />
And that it is NOT possible to ensure that there are no more write connections during the rolling schema upgrade.<br />
The PHP ORM framework Doctrine is used.<br />
What does the MariaDB documentation say about this?<br />
The study of the MariaDB documentation did not lead to a conclusive result whether a rolling schema upgrade in the running Galera Cluster operation WITH changes (DML statements) on the schema to be changed (AND the tables to be changed) is supported or not and thus should work or not.<br />
Source: Rolling Schema Upgrade (RSU)<br />
When replicating with different table structures, there is only general information on replication but nothing specific to Galera (whether it works or not):<br />
“Tables on the replica and the primary do not need to have the same definition in order for replication to take place. There can be differing numbers of columns, or differing data definitions and, in certain cases, replication can still proceed.”<br />
Source: Replication When the Primary and Replica Have Different Table Definitions<br />
For the attribute promotion/demotion feature there is a special MariaDB Server configuration parameter that controls the behaviour: slave_type_conversions.<br />
If you read between the lines here, this could also work for Galera Cluster:<br />
“Determines the type conversion mode on the replica when using row-based replication, including replications in MariaDB Galera cluster.”<br />
Source: slave_type_conversions<br />
Test planning<br />
I always make a rough risk assessment for such questions: Frequently used and widely deployed features: Risk of problems is rather low. Rarely used or new features: risk of problems is high! Unfortunately, this assessment is based less on tangible figures and more on experience…<br />
I am not aware of a single MariaDB user who performs rolling schema upgrades during operation. Let alone having a write load on the current schema and the current tables (promotion/demotion attributes).<br />
So: Use of rolling schema upgrade x frequency of use of attribute promotion/demotion is very rare and therefore the risk is very high!<br />
As the situation is not clear and the documentation does not provide any clear information, testing was carried out.<br />
To avoid possible already fixed MariaDB bugs the latest MariaDB 11.8.5 LTS version was used.<br />
Special database configuration parameters were used:<br />
slave_type_conversions = \'ALL_NON_LOSSY,ALL_LOSSY\'</p>
<p>Our test table looks as usual as follows:<br />
CREATE TABLE `test` (<br />
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
 `data` varchar(128) DEFAULT NULL,<br />
 `ts` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),<br />
 PRIMARY KEY (`id`)<br />
);</p>
<p>And test data was generated as follows:<br />
INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
... -- 9 x</p>
<p>Commands for monitoring the tests:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'slave_type_conversions\';<br />
SQL &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
SQL &#62; SHOW CREATE TABLE testG<br />
SQL &#62; CHECKSUM TABLE test;<br />
SQL &#62; SELECT * FROM test;</p>
<p>Testing<br />
Test 1: Attribute promotion with DML remote (on Node C)<br />
This is the simplest case: A column is added and a default value is set. Changes to the table are made on another node:<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>MariaDB error log file:<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) desyncs itself from group<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting SYNCED→DONOR/DESYNCED (TO: 36)<br />
2025-11-28 9:39:11 13 [Note] WSREP: pause<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:36 (43)<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at: 36<br />
2025-11-28 9:39:11 13 [Note] WSREP: resume<br />
2025-11-28 9:39:11 13 [Note] WSREP: resuming provider at 43<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider resumed.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) resyncs itself to group.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting DONOR/DESYNCED→JOINED (TO: 36)<br />
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... -nan% (0/0 events) complete.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) synced with group.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... 100.0% (1/1 events) complete.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting JOINED→SYNCED (TO: 36)<br />
2025-11-28 9:39:11 7 [Note] WSREP: Server Node A synced with group</p>
<p>Then the other commands:<br />
nodeC &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
nodeC &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 10;<br />
nodeC &#62; DELETE FROM test WHERE id = 19;</p>
<p>The ALTER TABLE command was then executed on nodes 2 and 3.<br />
All 3 operations worked perfectly. The cluster is still fully functional. Subsequently:<br />
SQL &#62; ALTER TABLE test DROP COLUMN c1;</p>
<p>to return the system to its initial state for further tests.<br />
Test 2: Attribute promotion with DML remote (on node B)<br />
If you do the same experiment on node B, the cluster will blow up in your face!<br />
nodeB &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>nodeC &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>MariaDB error log file:<br />
[Note] WSREP: Member 1(Node A) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 \"The event was corrupt, leading to illegal data being read\" from storage engine InnoDB, Error_code: 1030;<br />
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:<br />
 dbc9a6ea898b6a29: 1/3<br />
Waiting for more votes.<br />
[Note] WSREP: Member 2(Node C) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 \"The event was corrupt, leading to illegal data being read\" from storage engine InnoDB, Error_code: 1030;<br />
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:<br />
 dbc9a6ea898b6a29: 2/3<br />
Winner: dbc9a6ea898b6a29<br />
[Note] WSREP: Got vote request for seqno 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
[Note] WSREP: Recovering vote result from history: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29<br />
[ERROR] WSREP: Vote 0 (success) on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45 is inconsistent with group. Leaving cluster.<br />
[Note] WSREP: Closing send monitor...<br />
[Note] WSREP: Closed send monitor.<br />
[Note] WSREP: gcomm: terminating thread<br />
[Note] WSREP: gcomm: joining thread<br />
[Note] WSREP: gcomm: closing backend<br />
[Note] WSREP: view(view_id(NON_PRIM,1456a640-aca0,15) memb {<br />
 1456a640-aca0,0<br />
} joined {<br />
} left {<br />
} partitioned {<br />
 97230fdb-b973,0<br />
 a2e10f2c-8929,0<br />
})<br />
[Note] WSREP: PC protocol downgrade 1→0<br />
[Note] WSREP: view((empty))<br />
[Note] WSREP: gcomm: closed<br />
[Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 1<br />
[Note] WSREP: Flow-control interval: [16, 16]<br />
[Note] WSREP: Received NON-PRIMARY.<br />
[Note] WSREP: Shifting SYNCED→OPEN (TO: 45)<br />
[Note] WSREP: New SELF-LEAVE.<br />
[Note] WSREP: Flow-control interval: [0, 0]<br />
[Note] WSREP: Received SELF-LEAVE. Closing connection.<br />
[Note] WSREP: Shifting OPEN→CLOSED (TO: 45)<br />
[Note] WSREP: RECV thread exiting 0: Success<br />
[Note] WSREP: recv_thread() joined.<br />
[Note] WSREP: Closing send queue.<br />
[Note] WSREP: Closing receive queue.<br />
[Note] WSREP: ================================================<br />
View:<br />
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
 status: non-primary<br />
 protocol_version: 4<br />
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO<br />
 final: no<br />
 own_index: 0<br />
 members(1):<br />
 0: 1456a640-cc36-11f0-aca0-e388a5f80ba9, Node B<br />
=================================================<br />
[Note] WSREP: Non-primary view<br />
[Note] WSREP: Server status change synced→connected<br />
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.<br />
[Note] WSREP: ================================================<br />
View:<br />
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
 status: non-primary<br />
 protocol_version: 4<br />
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO<br />
 final: yes<br />
 own_index: -1<br />
 members(0):<br />
=================================================<br />
[Note] WSREP: Non-primary view<br />
[Note] WSREP: Server status change connected→disconnected<br />
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 2<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 9<br />
[Warning] Aborted connection 2 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 5<br />
[Warning] Aborted connection 5 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Warning] Aborted connection 9 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Note] WSREP: Service thread queue flushed.<br />
[Note] WSREP: ####### Assign initial position for certification: 00000000-0000-0000-0000-000000000000:-1, protocol version: 6<br />
[Note] WSREP: Applier thread exiting ret: 0 thd: 6<br />
[Warning] Aborted connection 6 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)</p>
<p>No idea why node B and C behave differently. But this makes the whole rolling schema upgrade (RSU) process completely arbitrary and unplannable.<br />
The whole thing was tested in different variants and the node sometimes becomes inconsistent and sometimes not.<br />
The inconsistent node B is synchronised back into the cluster via a forced SST.<br />
Test 3: Attribute promotion with DML remote (on node C)<br />
This case is somewhat trickier, as no default value is specified.<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT \'\'<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeC &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
nodeC &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 13;<br />
nodeC &#62; DELETE FROM test WHERE id = 22;</p>
<p>The ALTER TABLE command was then executed on nodes 2 and 3.<br />
All 3 operations worked perfectly. The cluster is still fully functional.<br />
Test 4: Attribute promotion with DML remote (on node B)<br />
Same test but the ALTER TABLE ADD COLUMN command is executed on node A and the DML command on node B.<br />
Nodes A and C become “Inconsistent” and node B is still “Synced”.<br />
Test 5: Attribute promotion with DML locally (on node A)<br />
Analogue test but the DML command is executed locally on the same node as the DDL command.<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT \'\'<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeA &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
ERROR 1136 (21S01): Column count doesn\'t match value count at row 1</p>
<p>nodeA &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());<br />
ERROR 1364 (HY000): Field \'c1\' doesn\'t have a default value</p>
<p>nodeA &#62; INSERT INTO test (id, data, ts, c1) VALUES (NULL, \'Some data to fill table up\', NOW(), \'\');</p>
<p>nodeA &#62; SELECT * FROM test;<br />
ERROR 1047 (08S01): WSREP has not yet prepared node for application use</p>
<p>nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>Cluster node became inconsistent. Interestingly enough, the whole thing suddenly worked during further testing! So completely unpredictable…<br />
Test 6: DDL on 2 nodes<br />
New question: What happens after the DDL command has been executed on 2 nodes and then DML commands occur?<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeB &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeB &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeB &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeC &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>works, but:<br />
nodeB &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>root@localhost [test] &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>Test 7: UPDATE and DELETE commands from the same node<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeA &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 16;</p>
<p>nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>nodeA &#62; DELETE FROM test WHERE id = 25;<br />
nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------+<br />
&#124; wsrep_local_state_comment &#124; Synced &#124;<br />
+---------------------------+--------+</p>
<p>...<br />
[Warning] WSREP: Ignoring error \'Can\'t find record in \'test\'\' on Delete_rows_v1 event. Error_code: 1032<br />
[Warning] Slave SQL: Could not execute Delete_rows_v1 event on table test.test; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find re<br />
[ERROR] Slave SQL: Could not read field \'id\' of table \'test.test\', Internal MariaDB error code: 1610<br />
[ERROR] mariadbd: Can\'t find record in \'test\'<br />
...</p>
<p>Process list from node C:<br />
nodeC &#62; SHOW PROCESSLIST;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+<br />
&#124; Id &#124; User &#124; Host &#124; db &#124; Command &#124; Time &#124; State &#124; Info &#124; Progress &#124;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+<br />
&#124; 2 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 1670 &#124; After apply log event &#124; NULL &#124; 0.000 &#124;<br />
&#124; 1 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 4609 &#124; wsrep aborter idle &#124; NULL &#124; 0.000 &#124;<br />
&#124; 7 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 4608 &#124; &#124; NULL &#124; 0.000 &#124;<br />
&#124; 8 &#124; system user &#124; &#124; test &#124; Sleep &#124; 1441 &#124; Executing &#124; DELETE FROM test WHERE id = 25?5jR &#124; 0.000 &#124;<br />
&#124; 10 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 2002 &#124; wsrep applied write set &#124; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW()) 9O? &#124; 0.000 &#124;<br />
&#124; 28 &#124; root &#124; localhost &#124; NULL &#124; Query &#124; 0 &#124; starting &#124; show processlist &#124; 0.000 &#124;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+</p>
<p>Node C is still synchronised:<br />
nodeC &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------+<br />
&#124; wsrep_local_state_comment &#124; Synced &#124;<br />
+---------------------------+--------+</p>
<p>Shutdown of the node for the following error message:<br />
nodeC &#62; SQL &#62; shutdown;<br />
ERROR 1047 (08S01): WSREP has not yet prepared node for application use</p>
<p>The entire cluster was then no longer usable. And had to be restarted (bootstrap).<br />
Summary<br />
We have opened a bug at MariaDB on this topic: Attribute Promotion/Demotion in Galera Cluster.<br />
Further tests were not carried out for the time being, as this feature is too unstable in the tested version and DROP COLUMN is not a use case of our customer.<br />
Conclusion: MariaDB Galera Cluster does not properly handle this situation in the version tested, nor does the cluster prevent this case. Cluster nodes are marked as inconsistent. Our current recommendation: Do NOT do a rolling schema upgrade (RSU) with concurrent DML commands (INSERT, UPDATE, DELETE) on the tables to be changed with the analysed version!<br />
Further sources:</p>
<p>Galera Cluster Inconsistency Voting protocol<br />
Inconsistent Voting in Percona XtraDB Cluster</p>
<p><a href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/">Attribute promotion and demotion in the MariaDB Galera Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In MariaDB master/slave replication there is a feature called <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-when-the-primary-and-replica-have-different-table-definitions" target="_blank">attribute promotion/demotion</a>.</p>
<p>Simply put, it is about how the slave behaves or should behave if the master and slave have different column definitions or even a different number of columns or a different sequence of columns.</p>
<h2>Use case of the customer<a class="anchor-link" id="use-case-of-the-customer"></a></h2>
<p>This week we discussed with a customer the case of how he could perform a rolling schema upgrade (RSU) in a Galera cluster.</p>
<p>With previous schema changes he has always had problems, which has led to a total failure of the cluster for several hours.</p>
<p>The customer says that columns are never deleted and new columns are only ever added at the end of a table.</p>
<p>And that it is NOT possible to ensure that there are no more write connections during the rolling schema upgrade.</p>
<p>The PHP ORM framework <a href="https://www.doctrine-project.org/" target="_blank">Doctrine</a> is used.</p>
<h2>What does the MariaDB documentation say about this?<a class="anchor-link" id="what-does-the-mariadb-documentation-say-about-this"></a></h2>
<p>The study of the MariaDB documentation did not lead to a conclusive result whether a rolling schema upgrade in the running Galera Cluster operation WITH changes (DML statements) on the schema to be changed (AND the tables to be changed) is supported or not and thus should work or not.</p>
<p>Source: <a href="https://mariadb.com/docs/galera-cluster/galera-management/general-operations/performing-schema-upgrades-in-galera-cluster#rolling-schema-upgrade-rsu" target="_blank">Rolling Schema Upgrade (RSU)</a></p>
<p>When replicating with different table structures, there is only general information on replication but nothing specific to Galera (whether it works or not):</p>
<p><em>&ldquo;Tables on the replica and the primary do not need to have the same definition in order for replication to take place. There can be differing numbers of columns, or differing data definitions and, in certain cases, replication can still proceed.&rdquo;</em></p>
<p>Source: <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-when-the-primary-and-replica-have-different-table-definitions" target="_blank">Replication When the Primary and Replica Have Different Table Definitions</a></p>
<p>For the attribute promotion/demotion feature there is a special MariaDB Server configuration parameter that controls the behaviour: <code>slave_type_conversions</code>.</p>
<p>If you read between the lines here, this could also work for Galera Cluster:</p>
<p><em>&ldquo;Determines the type conversion mode on the replica when using row-based replication, including replications in MariaDB Galera cluster.&rdquo;</em></p>
<p>Source: <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-and-binary-log-system-variables#slave_type_conversions" target="_blank"><code>slave_type_conversions</code></a></p>
<h2>Test planning<a class="anchor-link" id="test-planning"></a></h2>
<p>I always make a rough risk assessment for such questions: Frequently used and widely deployed features: Risk of problems is rather low. Rarely used or new features: risk of problems is high! Unfortunately, this assessment is based less on tangible figures and more on experience&hellip;</p>
<p>I am not aware of a single MariaDB user who performs rolling schema upgrades during operation. Let alone having a write load on the current schema and the current tables (promotion/demotion attributes).<br>
So: Use of rolling schema upgrade x frequency of use of attribute promotion/demotion is very rare and therefore the risk is very high!</p>
<p>As the situation is not clear and the documentation does not provide any clear information, testing was carried out.</p>
<p>To avoid possible already fixed MariaDB bugs the latest MariaDB 11.8.5 LTS version was used.</p>
<p>Special database configuration parameters were used:</p>
<pre><code>slave_type_conversions = 'ALL_NON_LOSSY,ALL_LOSSY'
</code></pre>
<p>Our test table looks as usual as follows:</p>
<pre><code>CREATE TABLE `test` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `data` varchar(128) DEFAULT NULL,
 `ts` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
 PRIMARY KEY (`id`)
);
</code></pre>
<p>And test data was generated as follows:</p>
<pre><code>INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
... -- 9 x
</code></pre>
<p>Commands for monitoring the tests:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'slave_type_conversions';
SQL&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
SQL&gt; SHOW CREATE TABLE test<br>G
SQL&gt; CHECKSUM TABLE test;
SQL&gt; SELECT * FROM test;
</code></pre>
<h2>Testing<a class="anchor-link" id="testing"></a></h2>
<h3>Test 1: Attribute promotion with DML remote (on Node C)<a class="anchor-link" id="test-1-attribute-promotion-with-dml-remote-on-node-c"></a></h3>
<p>This is the simplest case: A column is added and a default value is set. Changes to the table are made on another node:</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';
</code></pre>
<p>MariaDB error log file:</p>
<pre><code>2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) desyncs itself from group
2025-11-28 9:39:11 0 [Note] WSREP: Shifting SYNCED&rarr;DONOR/DESYNCED (TO: 36)
2025-11-28 9:39:11 13 [Note] WSREP: pause
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:36 (43)
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at: 36
2025-11-28 9:39:11 13 [Note] WSREP: resume
2025-11-28 9:39:11 13 [Note] WSREP: resuming provider at 43
2025-11-28 9:39:11 13 [Note] WSREP: Provider resumed.
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) resyncs itself to group.
2025-11-28 9:39:11 0 [Note] WSREP: Shifting DONOR/DESYNCED&rarr;JOINED (TO: 36)
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... -nan% (0/0 events) complete.
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) synced with group.
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... 100.0% (1/1 events) complete.
2025-11-28 9:39:11 0 [Note] WSREP: Shifting JOINED&rarr;SYNCED (TO: 36)
2025-11-28 9:39:11 7 [Note] WSREP: Server Node A synced with group
</code></pre>
<p>Then the other commands:</p>
<pre><code>nodeC&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
nodeC&gt; UPDATE test SET data = 'Some data changed' WHERE id = 10;
nodeC&gt; DELETE FROM test WHERE id = 19;
</code></pre>
<p>The <code>ALTER TABLE</code> command was then executed on nodes 2 and 3.</p>
<p>All 3 operations worked perfectly. The cluster is still fully functional. Subsequently:</p>
<pre><code>SQL&gt; ALTER TABLE test DROP COLUMN c1;
</code></pre>
<p>to return the system to its initial state for further tests.</p>
<h3>Test 2: Attribute promotion with DML remote (on node B)<a class="anchor-link" id="test-2-attribute-promotion-with-dml-remote-on-node-b"></a></h3>
<p>If you do the same experiment on node B, the cluster will blow up in your face!</p>
<pre><code>nodeB&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());

nodeC&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<p>MariaDB error log file:</p>
<pre><code>[Note] WSREP: Member 1(Node A) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 "The event was corrupt, leading to illegal data being read" from storage engine InnoDB, Error_code: 1030;
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:
 dbc9a6ea898b6a29: 1/3
Waiting for more votes.
[Note] WSREP: Member 2(Node C) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 "The event was corrupt, leading to illegal data being read" from storage engine InnoDB, Error_code: 1030;
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:
 dbc9a6ea898b6a29: 2/3
Winner: dbc9a6ea898b6a29
[Note] WSREP: Got vote request for seqno 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
[Note] WSREP: Recovering vote result from history: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29
[ERROR] WSREP: Vote 0 (success) on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45 is inconsistent with group. Leaving cluster.
[Note] WSREP: Closing send monitor...
[Note] WSREP: Closed send monitor.
[Note] WSREP: gcomm: terminating thread
[Note] WSREP: gcomm: joining thread
[Note] WSREP: gcomm: closing backend
[Note] WSREP: view(view_id(NON_PRIM,1456a640-aca0,15) memb {
 1456a640-aca0,0
} joined {
} left {
} partitioned {
 97230fdb-b973,0
 a2e10f2c-8929,0
})
[Note] WSREP: PC protocol downgrade 1&rarr;0
[Note] WSREP: view((empty))
[Note] WSREP: gcomm: closed
[Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 1
[Note] WSREP: Flow-control interval: [16, 16]
[Note] WSREP: Received NON-PRIMARY.
[Note] WSREP: Shifting SYNCED&rarr;OPEN (TO: 45)
[Note] WSREP: New SELF-LEAVE.
[Note] WSREP: Flow-control interval: [0, 0]
[Note] WSREP: Received SELF-LEAVE. Closing connection.
[Note] WSREP: Shifting OPEN&rarr;CLOSED (TO: 45)
[Note] WSREP: RECV thread exiting 0: Success
[Note] WSREP: recv_thread() joined.
[Note] WSREP: Closing send queue.
[Note] WSREP: Closing receive queue.
[Note] WSREP: ================================================
View:
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
 status: non-primary
 protocol_version: 4
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO
 final: no
 own_index: 0
 members(1):
 0: 1456a640-cc36-11f0-aca0-e388a5f80ba9, Node B
=================================================
[Note] WSREP: Non-primary view
[Note] WSREP: Server status change synced&rarr;connected
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
[Note] WSREP: ================================================
View:
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
 status: non-primary
 protocol_version: 4
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO
 final: yes
 own_index: -1
 members(0):
=================================================
[Note] WSREP: Non-primary view
[Note] WSREP: Server status change connected&rarr;disconnected
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
[Note] WSREP: Applier thread exiting ret: 6 thd: 2
[Note] WSREP: Applier thread exiting ret: 6 thd: 9
[Warning] Aborted connection 2 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Note] WSREP: Applier thread exiting ret: 6 thd: 5
[Warning] Aborted connection 5 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Warning] Aborted connection 9 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Note] WSREP: Service thread queue flushed.
[Note] WSREP: ####### Assign initial position for certification: 00000000-0000-0000-0000-000000000000:-1, protocol version: 6
[Note] WSREP: Applier thread exiting ret: 0 thd: 6
[Warning] Aborted connection 6 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
</code></pre>
<p>No idea why node B and C behave differently. But this makes the whole rolling schema upgrade (RSU) process completely arbitrary and unplannable.</p>
<p>The whole thing was tested in different variants and the node sometimes becomes inconsistent and sometimes not.</p>
<p>The inconsistent node B is synchronised back into the cluster via a forced SST.</p>
<h3>Test 3: Attribute promotion with DML remote (on node C)<a class="anchor-link" id="test-3-attribute-promotion-with-dml-remote-on-node-c"></a></h3>
<p>This case is somewhat trickier, as no default value is specified.</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT ''
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeC&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
nodeC&gt; UPDATE test SET data = 'Some data changed' WHERE id = 13;
nodeC&gt; DELETE FROM test WHERE id = 22;
</code></pre>
<p>The <code>ALTER TABLE</code> command was then executed on nodes 2 and 3.</p>
<p>All 3 operations worked perfectly. The cluster is still fully functional.</p>
<h3>Test 4: Attribute promotion with DML remote (on node B)<a class="anchor-link" id="test-4-attribute-promotion-with-dml-remote-on-node-b"></a></h3>
<p>Same test but the <code>ALTER TABLE ADD COLUMN</code> command is executed on node A and the DML command on node B.</p>
<p>Nodes A and C become &ldquo;Inconsistent&rdquo; and node B is still &ldquo;Synced&rdquo;.</p>
<h3>Test 5: Attribute promotion with DML locally (on node A)<a class="anchor-link" id="test-5-attribute-promotion-with-dml-locally-on-node-a"></a></h3>
<p>Analogue test but the DML command is executed locally on the same node as the DDL command.</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT ''
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeA&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
ERROR 1136 (21S01): Column count doesn't match value count at row 1

nodeA&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());
ERROR 1364 (HY000): Field 'c1' doesn't have a default value

nodeA&gt; INSERT INTO test (id, data, ts, c1) VALUES (NULL, 'Some data to fill table up', NOW(), '');

nodeA&gt; SELECT * FROM test;
ERROR 1047 (08S01): WSREP has not yet prepared node for application use

nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<p>Cluster node became inconsistent. Interestingly enough, the whole thing suddenly worked during further testing! So completely unpredictable&hellip;</p>
<h3>Test 6: DDL on 2 nodes<a class="anchor-link" id="test-6-ddl-on-2-nodes"></a></h3>
<p>New question: What happens after the DDL command has been executed on 2 nodes and then DML commands occur?</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeB&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeB&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeB&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeC&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());
</code></pre>
<p>works, but:</p>
<pre><code>nodeB&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());

root@localhost [test]&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<h3>Test 7: UPDATE and DELETE commands from the same node<a class="anchor-link" id="test-7-update-and-delete-commands-from-the-same-node"></a></h3>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeA&gt; UPDATE test SET data = 'Some data changed' WHERE id = 16;

nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+

nodeA&gt; DELETE FROM test WHERE id = 25;
nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name | Value |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+

...
[Warning] WSREP: Ignoring error 'Can't find record in 'test'' on Delete_rows_v1 event. Error_code: 1032
[Warning] Slave SQL: Could not execute Delete_rows_v1 event on table test.test; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find re
[ERROR] Slave SQL: Could not read field 'id' of table 'test.test', Internal MariaDB error code: 1610
[ERROR] mariadbd: Can't find record in 'test'
...
</code></pre>
<p>Process list from node C:</p>
<pre><code>nodeC&gt; SHOW PROCESSLIST;
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
| Id | User | Host | db | Command | Time | State | Info | Progress |
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
| 2 | system user | | NULL | Sleep | 1670 | After apply log event | NULL | 0.000 |
| 1 | system user | | NULL | Sleep | 4609 | wsrep aborter idle | NULL | 0.000 |
| 7 | system user | | NULL | Sleep | 4608 | | NULL | 0.000 |
| 8 | system user | | test | Sleep | 1441 | Executing | DELETE FROM test WHERE id = 25?5jR | 0.000 |
| 10 | system user | | NULL | Sleep | 2002 | wsrep applied write set | INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW()) 9O? | 0.000 |
| 28 | root | localhost | NULL | Query | 0 | starting | show processlist | 0.000 |
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
</code></pre>
<p>Node C is still synchronised:</p>
<pre><code>nodeC&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name | Value |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+
</code></pre>
<p>Shutdown of the node for the following error message:</p>
<pre><code>nodeC&gt; SQL&gt; shutdown;
ERROR 1047 (08S01): WSREP has not yet prepared node for application use
</code></pre>
<p>The entire cluster was then no longer usable. And had to be restarted (bootstrap).</p>
<h2>Summary<a class="anchor-link" id="summary"></a></h2>
<p>We have opened a bug at MariaDB on this topic: <a href="https://jira.mariadb.org/browse/MDEV-38215" target="_blank">Attribute Promotion/Demotion in Galera Cluster</a>.</p>
<p>Further tests were not carried out for the time being, as this feature is too unstable in the tested version and <code>DROP COLUMN</code> is not a use case of our customer.</p>
<p>Conclusion: MariaDB Galera Cluster does not properly handle this situation in the version tested, nor does the cluster prevent this case. Cluster nodes are marked as inconsistent. Our current recommendation: Do NOT do a rolling schema upgrade (RSU) with concurrent DML commands (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>) on the tables to be changed with the analysed version!</p>
<p>Further sources:</p>
<ul>
<li><a href="https://galeracluster.com/documentation/html_docs_2023/_sources/documentation/inconsistency-voting.rst.txt" target="_blank">Galera Cluster Inconsistency Voting protocol</a></li>
<li><a href="https://www.percona.com/blog/inconsistent-voting-in-percona-xtradb-cluster/" target="_blank">Inconsistent Voting in Percona XtraDB Cluster</a></li>
</ul>

<p><a href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/">Attribute promotion and demotion in the MariaDB Galera Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Attribute promotion and demotion in the MariaDB Galera Cluster</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/" />
      <id>https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/</id>
      <updated>2025-11-28T16:26:48+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>In MariaDB master/slave replication there is a feature called attribute promotion/demotion.<br />
Simply put, it is about how the slave behaves or should behave if the master and slave have different column definitions or even a different number of columns or a different sequence of columns.<br />
Use case of the customer<br />
This week we discussed with a customer the case of how he could perform a rolling schema upgrade (RSU) in a Galera cluster.<br />
With previous schema changes he has always had problems, which has led to a total failure of the cluster for several hours.<br />
The customer says that columns are never deleted and new columns are only ever added at the end of a table.<br />
And that it is NOT possible to ensure that there are no more write connections during the rolling schema upgrade.<br />
The PHP ORM framework Doctrine is used.<br />
What does the MariaDB documentation say about this?<br />
The study of the MariaDB documentation did not lead to a conclusive result whether a rolling schema upgrade in the running Galera Cluster operation WITH changes (DML statements) on the schema to be changed (AND the tables to be changed) is supported or not and thus should work or not.<br />
Source: Rolling Schema Upgrade (RSU)<br />
When replicating with different table structures, there is only general information on replication but nothing specific to Galera (whether it works or not):<br />
“Tables on the replica and the primary do not need to have the same definition in order for replication to take place. There can be differing numbers of columns, or differing data definitions and, in certain cases, replication can still proceed.”<br />
Source: Replication When the Primary and Replica Have Different Table Definitions<br />
For the attribute promotion/demotion feature there is a special MariaDB Server configuration parameter that controls the behaviour: slave_type_conversions.<br />
If you read between the lines here, this could also work for Galera Cluster:<br />
“Determines the type conversion mode on the replica when using row-based replication, including replications in MariaDB Galera cluster.”<br />
Source: slave_type_conversions<br />
Test planning<br />
I always make a rough risk assessment for such questions: Frequently used and widely deployed features: Risk of problems is rather low. Rarely used or new features: risk of problems is high! Unfortunately, this assessment is based less on tangible figures and more on experience…<br />
I am not aware of a single MariaDB user who performs rolling schema upgrades during operation. Let alone having a write load on the current schema and the current tables (promotion/demotion attributes).<br />
So: Use of rolling schema upgrade x frequency of use of attribute promotion/demotion is very rare and therefore the risk is very high!<br />
As the situation is not clear and the documentation does not provide any clear information, testing was carried out.<br />
To avoid possible already fixed MariaDB bugs the latest MariaDB 11.8.5 LTS version was used.<br />
Special database configuration parameters were used:<br />
slave_type_conversions = \'ALL_NON_LOSSY,ALL_LOSSY\'</p>
<p>Our test table looks as usual as follows:<br />
CREATE TABLE `test` (<br />
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,<br />
 `data` varchar(128) DEFAULT NULL,<br />
 `ts` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),<br />
 PRIMARY KEY (`id`)<br />
);</p>
<p>And test data was generated as follows:<br />
INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
... -- 9 x</p>
<p>Commands for monitoring the tests:<br />
SQL &#62; SHOW GLOBAL VARIABLES LIKE \'slave_type_conversions\';<br />
SQL &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
SQL &#62; SHOW CREATE TABLE testG<br />
SQL &#62; CHECKSUM TABLE test;<br />
SQL &#62; SELECT * FROM test;</p>
<p>Testing<br />
Test 1: Attribute promotion with DML remote (on Node C)<br />
This is the simplest case: A column is added and a default value is set. Changes to the table are made on another node:<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>MariaDB error log file:<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) desyncs itself from group<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting SYNCED→DONOR/DESYNCED (TO: 36)<br />
2025-11-28 9:39:11 13 [Note] WSREP: pause<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:36 (43)<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at: 36<br />
2025-11-28 9:39:11 13 [Note] WSREP: resume<br />
2025-11-28 9:39:11 13 [Note] WSREP: resuming provider at 43<br />
2025-11-28 9:39:11 13 [Note] WSREP: Provider resumed.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) resyncs itself to group.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting DONOR/DESYNCED→JOINED (TO: 36)<br />
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... -nan% (0/0 events) complete.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) synced with group.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... 100.0% (1/1 events) complete.<br />
2025-11-28 9:39:11 0 [Note] WSREP: Shifting JOINED→SYNCED (TO: 36)<br />
2025-11-28 9:39:11 7 [Note] WSREP: Server Node A synced with group</p>
<p>Then the other commands:<br />
nodeC &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
nodeC &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 10;<br />
nodeC &#62; DELETE FROM test WHERE id = 19;</p>
<p>The ALTER TABLE command was then executed on nodes 2 and 3.<br />
All 3 operations worked perfectly. The cluster is still fully functional. Subsequently:<br />
SQL &#62; ALTER TABLE test DROP COLUMN c1;</p>
<p>to return the system to its initial state for further tests.<br />
Test 2: Attribute promotion with DML remote (on node B)<br />
If you do the same experiment on node B, the cluster will blow up in your face!<br />
nodeB &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>nodeC &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>MariaDB error log file:<br />
[Note] WSREP: Member 1(Node A) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 \"The event was corrupt, leading to illegal data being read\" from storage engine InnoDB, Error_code: 1030;<br />
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:<br />
 dbc9a6ea898b6a29: 1/3<br />
Waiting for more votes.<br />
[Note] WSREP: Member 2(Node C) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 \"The event was corrupt, leading to illegal data being read\" from storage engine InnoDB, Error_code: 1030;<br />
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:<br />
 dbc9a6ea898b6a29: 2/3<br />
Winner: dbc9a6ea898b6a29<br />
[Note] WSREP: Got vote request for seqno 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
[Note] WSREP: Recovering vote result from history: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29<br />
[ERROR] WSREP: Vote 0 (success) on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45 is inconsistent with group. Leaving cluster.<br />
[Note] WSREP: Closing send monitor...<br />
[Note] WSREP: Closed send monitor.<br />
[Note] WSREP: gcomm: terminating thread<br />
[Note] WSREP: gcomm: joining thread<br />
[Note] WSREP: gcomm: closing backend<br />
[Note] WSREP: view(view_id(NON_PRIM,1456a640-aca0,15) memb {<br />
 1456a640-aca0,0<br />
} joined {<br />
} left {<br />
} partitioned {<br />
 97230fdb-b973,0<br />
 a2e10f2c-8929,0<br />
})<br />
[Note] WSREP: PC protocol downgrade 1→0<br />
[Note] WSREP: view((empty))<br />
[Note] WSREP: gcomm: closed<br />
[Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 1<br />
[Note] WSREP: Flow-control interval: [16, 16]<br />
[Note] WSREP: Received NON-PRIMARY.<br />
[Note] WSREP: Shifting SYNCED→OPEN (TO: 45)<br />
[Note] WSREP: New SELF-LEAVE.<br />
[Note] WSREP: Flow-control interval: [0, 0]<br />
[Note] WSREP: Received SELF-LEAVE. Closing connection.<br />
[Note] WSREP: Shifting OPEN→CLOSED (TO: 45)<br />
[Note] WSREP: RECV thread exiting 0: Success<br />
[Note] WSREP: recv_thread() joined.<br />
[Note] WSREP: Closing send queue.<br />
[Note] WSREP: Closing receive queue.<br />
[Note] WSREP: ================================================<br />
View:<br />
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
 status: non-primary<br />
 protocol_version: 4<br />
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO<br />
 final: no<br />
 own_index: 0<br />
 members(1):<br />
 0: 1456a640-cc36-11f0-aca0-e388a5f80ba9, Node B<br />
=================================================<br />
[Note] WSREP: Non-primary view<br />
[Note] WSREP: Server status change synced→connected<br />
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.<br />
[Note] WSREP: ================================================<br />
View:<br />
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45<br />
 status: non-primary<br />
 protocol_version: 4<br />
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO<br />
 final: yes<br />
 own_index: -1<br />
 members(0):<br />
=================================================<br />
[Note] WSREP: Non-primary view<br />
[Note] WSREP: Server status change connected→disconnected<br />
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 2<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 9<br />
[Warning] Aborted connection 2 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Note] WSREP: Applier thread exiting ret: 6 thd: 5<br />
[Warning] Aborted connection 5 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Warning] Aborted connection 9 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)<br />
[Note] WSREP: Service thread queue flushed.<br />
[Note] WSREP: ####### Assign initial position for certification: 00000000-0000-0000-0000-000000000000:-1, protocol version: 6<br />
[Note] WSREP: Applier thread exiting ret: 0 thd: 6<br />
[Warning] Aborted connection 6 to db: \'unconnected\' user: \'unauthenticated\' host: \'\' (This connection closed normally without authentication)</p>
<p>No idea why node B and C behave differently. But this makes the whole rolling schema upgrade (RSU) process completely arbitrary and unplannable.<br />
The whole thing was tested in different variants and the node sometimes becomes inconsistent and sometimes not.<br />
The inconsistent node B is synchronised back into the cluster via a forced SST.<br />
Test 3: Attribute promotion with DML remote (on node C)<br />
This case is somewhat trickier, as no default value is specified.<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT \'\'<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeC &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
nodeC &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 13;<br />
nodeC &#62; DELETE FROM test WHERE id = 22;</p>
<p>The ALTER TABLE command was then executed on nodes 2 and 3.<br />
All 3 operations worked perfectly. The cluster is still fully functional.<br />
Test 4: Attribute promotion with DML remote (on node B)<br />
Same test but the ALTER TABLE ADD COLUMN command is executed on node A and the DML command on node B.<br />
Nodes A and C become “Inconsistent” and node B is still “Synced”.<br />
Test 5: Attribute promotion with DML locally (on node A)<br />
Analogue test but the DML command is executed locally on the same node as the DDL command.<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT \'\'<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeA &#62; INSERT INTO test VALUES (NULL, \'Some data to fill table up\', NOW());<br />
ERROR 1136 (21S01): Column count doesn\'t match value count at row 1</p>
<p>nodeA &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());<br />
ERROR 1364 (HY000): Field \'c1\' doesn\'t have a default value</p>
<p>nodeA &#62; INSERT INTO test (id, data, ts, c1) VALUES (NULL, \'Some data to fill table up\', NOW(), \'\');</p>
<p>nodeA &#62; SELECT * FROM test;<br />
ERROR 1047 (08S01): WSREP has not yet prepared node for application use</p>
<p>nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>Cluster node became inconsistent. Interestingly enough, the whole thing suddenly worked during further testing! So completely unpredictable…<br />
Test 6: DDL on 2 nodes<br />
New question: What happens after the DDL command has been executed on 2 nodes and then DML commands occur?<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeB &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeB &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeB &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeC &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>works, but:<br />
nodeB &#62; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW());</p>
<p>root@localhost [test] &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>Test 7: UPDATE and DELETE commands from the same node<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'RSU\';<br />
nodeA &#62; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT \'foo\';<br />
nodeA &#62; SET SESSION wsrep_OSU_method = \'TOI\';</p>
<p>nodeA &#62; UPDATE test SET data = \'Some data changed\' WHERE id = 16;</p>
<p>nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------------+<br />
&#124; wsrep_local_state_comment &#124; Inconsistent &#124;<br />
+---------------------------+--------------+</p>
<p>nodeA &#62; DELETE FROM test WHERE id = 25;<br />
nodeA &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------+<br />
&#124; wsrep_local_state_comment &#124; Synced &#124;<br />
+---------------------------+--------+</p>
<p>...<br />
[Warning] WSREP: Ignoring error \'Can\'t find record in \'test\'\' on Delete_rows_v1 event. Error_code: 1032<br />
[Warning] Slave SQL: Could not execute Delete_rows_v1 event on table test.test; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find record in \'test\', Error_code: 1032; Can\'t find re<br />
[ERROR] Slave SQL: Could not read field \'id\' of table \'test.test\', Internal MariaDB error code: 1610<br />
[ERROR] mariadbd: Can\'t find record in \'test\'<br />
...</p>
<p>Process list from node C:<br />
nodeC &#62; SHOW PROCESSLIST;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+<br />
&#124; Id &#124; User &#124; Host &#124; db &#124; Command &#124; Time &#124; State &#124; Info &#124; Progress &#124;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+<br />
&#124; 2 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 1670 &#124; After apply log event &#124; NULL &#124; 0.000 &#124;<br />
&#124; 1 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 4609 &#124; wsrep aborter idle &#124; NULL &#124; 0.000 &#124;<br />
&#124; 7 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 4608 &#124; &#124; NULL &#124; 0.000 &#124;<br />
&#124; 8 &#124; system user &#124; &#124; test &#124; Sleep &#124; 1441 &#124; Executing &#124; DELETE FROM test WHERE id = 25?5jR &#124; 0.000 &#124;<br />
&#124; 10 &#124; system user &#124; &#124; NULL &#124; Sleep &#124; 2002 &#124; wsrep applied write set &#124; INSERT INTO test (id, data, ts) VALUES (NULL, \'Some data to fill table up\', NOW()) 9O? &#124; 0.000 &#124;<br />
&#124; 28 &#124; root &#124; localhost &#124; NULL &#124; Query &#124; 0 &#124; starting &#124; show processlist &#124; 0.000 &#124;<br />
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+</p>
<p>Node C is still synchronised:<br />
nodeC &#62; SHOW GLOBAL STATUS LIKE \'wsrep_local_state_comment\';<br />
+---------------------------+--------+<br />
&#124; Variable_name &#124; Value &#124;<br />
+---------------------------+--------+<br />
&#124; wsrep_local_state_comment &#124; Synced &#124;<br />
+---------------------------+--------+</p>
<p>Shutdown of the node for the following error message:<br />
nodeC &#62; SQL &#62; shutdown;<br />
ERROR 1047 (08S01): WSREP has not yet prepared node for application use</p>
<p>The entire cluster was then no longer usable. And had to be restarted (bootstrap).<br />
Summary<br />
We have opened a bug at MariaDB on this topic: Attribute Promotion/Demotion in Galera Cluster.<br />
Further tests were not carried out for the time being, as this feature is too unstable in the tested version and DROP COLUMN is not a use case of our customer.<br />
Conclusion: MariaDB Galera Cluster does not properly handle this situation in the version tested, nor does the cluster prevent this case. Cluster nodes are marked as inconsistent. Our current recommendation: Do NOT do a rolling schema upgrade (RSU) with concurrent DML commands (INSERT, UPDATE, DELETE) on the tables to be changed with the analysed version!<br />
Further sources:</p>
<p>Galera Cluster Inconsistency Voting protocol<br />
Inconsistent Voting in Percona XtraDB Cluster</p>
<p><a href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/">Attribute promotion and demotion in the MariaDB Galera Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In MariaDB master/slave replication there is a feature called <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-when-the-primary-and-replica-have-different-table-definitions" target="_blank">attribute promotion/demotion</a>.</p>
<p>Simply put, it is about how the slave behaves or should behave if the master and slave have different column definitions or even a different number of columns or a different sequence of columns.</p>
<h2>Use case of the customer<a class="anchor-link" id="use-case-of-the-customer"></a></h2>
<p>This week we discussed with a customer the case of how he could perform a rolling schema upgrade (RSU) in a Galera cluster.</p>
<p>With previous schema changes he has always had problems, which has led to a total failure of the cluster for several hours.</p>
<p>The customer says that columns are never deleted and new columns are only ever added at the end of a table.</p>
<p>And that it is NOT possible to ensure that there are no more write connections during the rolling schema upgrade.</p>
<p>The PHP ORM framework <a href="https://www.doctrine-project.org/" target="_blank">Doctrine</a> is used.</p>
<h2>What does the MariaDB documentation say about this?<a class="anchor-link" id="what-does-the-mariadb-documentation-say-about-this"></a></h2>
<p>The study of the MariaDB documentation did not lead to a conclusive result whether a rolling schema upgrade in the running Galera Cluster operation WITH changes (DML statements) on the schema to be changed (AND the tables to be changed) is supported or not and thus should work or not.</p>
<p>Source: <a href="https://mariadb.com/docs/galera-cluster/galera-management/general-operations/performing-schema-upgrades-in-galera-cluster#rolling-schema-upgrade-rsu" target="_blank">Rolling Schema Upgrade (RSU)</a></p>
<p>When replicating with different table structures, there is only general information on replication but nothing specific to Galera (whether it works or not):</p>
<p><em>&ldquo;Tables on the replica and the primary do not need to have the same definition in order for replication to take place. There can be differing numbers of columns, or differing data definitions and, in certain cases, replication can still proceed.&rdquo;</em></p>
<p>Source: <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-when-the-primary-and-replica-have-different-table-definitions" target="_blank">Replication When the Primary and Replica Have Different Table Definitions</a></p>
<p>For the attribute promotion/demotion feature there is a special MariaDB Server configuration parameter that controls the behaviour: <code>slave_type_conversions</code>.</p>
<p>If you read between the lines here, this could also work for Galera Cluster:</p>
<p><em>&ldquo;Determines the type conversion mode on the replica when using row-based replication, including replications in MariaDB Galera cluster.&rdquo;</em></p>
<p>Source: <a href="https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-and-binary-log-system-variables#slave_type_conversions" target="_blank"><code>slave_type_conversions</code></a></p>
<h2>Test planning<a class="anchor-link" id="test-planning"></a></h2>
<p>I always make a rough risk assessment for such questions: Frequently used and widely deployed features: Risk of problems is rather low. Rarely used or new features: risk of problems is high! Unfortunately, this assessment is based less on tangible figures and more on experience&hellip;</p>
<p>I am not aware of a single MariaDB user who performs rolling schema upgrades during operation. Let alone having a write load on the current schema and the current tables (promotion/demotion attributes).<br>
So: Use of rolling schema upgrade x frequency of use of attribute promotion/demotion is very rare and therefore the risk is very high!</p>
<p>As the situation is not clear and the documentation does not provide any clear information, testing was carried out.</p>
<p>To avoid possible already fixed MariaDB bugs the latest MariaDB 11.8.5 LTS version was used.</p>
<p>Special database configuration parameters were used:</p>
<pre><code>slave_type_conversions = 'ALL_NON_LOSSY,ALL_LOSSY'
</code></pre>
<p>Our test table looks as usual as follows:</p>
<pre><code>CREATE TABLE `test` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `data` varchar(128) DEFAULT NULL,
 `ts` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
 PRIMARY KEY (`id`)
);
</code></pre>
<p>And test data was generated as follows:</p>
<pre><code>INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
... -- 9 x
</code></pre>
<p>Commands for monitoring the tests:</p>
<pre><code>SQL&gt; SHOW GLOBAL VARIABLES LIKE 'slave_type_conversions';
SQL&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
SQL&gt; SHOW CREATE TABLE test<br>G
SQL&gt; CHECKSUM TABLE test;
SQL&gt; SELECT * FROM test;
</code></pre>
<h2>Testing<a class="anchor-link" id="testing"></a></h2>
<h3>Test 1: Attribute promotion with DML remote (on Node C)<a class="anchor-link" id="test-1-attribute-promotion-with-dml-remote-on-node-c"></a></h3>
<p>This is the simplest case: A column is added and a default value is set. Changes to the table are made on another node:</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';
</code></pre>
<p>MariaDB error log file:</p>
<pre><code>2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) desyncs itself from group
2025-11-28 9:39:11 0 [Note] WSREP: Shifting SYNCED&rarr;DONOR/DESYNCED (TO: 36)
2025-11-28 9:39:11 13 [Note] WSREP: pause
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:36 (43)
2025-11-28 9:39:11 13 [Note] WSREP: Provider paused at: 36
2025-11-28 9:39:11 13 [Note] WSREP: resume
2025-11-28 9:39:11 13 [Note] WSREP: resuming provider at 43
2025-11-28 9:39:11 13 [Note] WSREP: Provider resumed.
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) resyncs itself to group.
2025-11-28 9:39:11 0 [Note] WSREP: Shifting DONOR/DESYNCED&rarr;JOINED (TO: 36)
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... -nan% (0/0 events) complete.
2025-11-28 9:39:11 0 [Note] WSREP: Member 0.0 (Node A) synced with group.
2025-11-28 9:39:11 0 [Note] WSREP: Processing event queue:... 100.0% (1/1 events) complete.
2025-11-28 9:39:11 0 [Note] WSREP: Shifting JOINED&rarr;SYNCED (TO: 36)
2025-11-28 9:39:11 7 [Note] WSREP: Server Node A synced with group
</code></pre>
<p>Then the other commands:</p>
<pre><code>nodeC&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
nodeC&gt; UPDATE test SET data = 'Some data changed' WHERE id = 10;
nodeC&gt; DELETE FROM test WHERE id = 19;
</code></pre>
<p>The <code>ALTER TABLE</code> command was then executed on nodes 2 and 3.</p>
<p>All 3 operations worked perfectly. The cluster is still fully functional. Subsequently:</p>
<pre><code>SQL&gt; ALTER TABLE test DROP COLUMN c1;
</code></pre>
<p>to return the system to its initial state for further tests.</p>
<h3>Test 2: Attribute promotion with DML remote (on node B)<a class="anchor-link" id="test-2-attribute-promotion-with-dml-remote-on-node-b"></a></h3>
<p>If you do the same experiment on node B, the cluster will blow up in your face!</p>
<pre><code>nodeB&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());

nodeC&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<p>MariaDB error log file:</p>
<pre><code>[Note] WSREP: Member 1(Node A) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 "The event was corrupt, leading to illegal data being read" from storage engine InnoDB, Error_code: 1030;
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:
 dbc9a6ea898b6a29: 1/3
Waiting for more votes.
[Note] WSREP: Member 2(Node C) initiates vote on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29: Got error 171 "The event was corrupt, leading to illegal data being read" from storage engine InnoDB, Error_code: 1030;
[Note] WSREP: Votes over 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45:
 dbc9a6ea898b6a29: 2/3
Winner: dbc9a6ea898b6a29
[Note] WSREP: Got vote request for seqno 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
[Note] WSREP: Recovering vote result from history: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45,dbc9a6ea898b6a29
[ERROR] WSREP: Vote 0 (success) on 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45 is inconsistent with group. Leaving cluster.
[Note] WSREP: Closing send monitor...
[Note] WSREP: Closed send monitor.
[Note] WSREP: gcomm: terminating thread
[Note] WSREP: gcomm: joining thread
[Note] WSREP: gcomm: closing backend
[Note] WSREP: view(view_id(NON_PRIM,1456a640-aca0,15) memb {
 1456a640-aca0,0
} joined {
} left {
} partitioned {
 97230fdb-b973,0
 a2e10f2c-8929,0
})
[Note] WSREP: PC protocol downgrade 1&rarr;0
[Note] WSREP: view((empty))
[Note] WSREP: gcomm: closed
[Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 1
[Note] WSREP: Flow-control interval: [16, 16]
[Note] WSREP: Received NON-PRIMARY.
[Note] WSREP: Shifting SYNCED&rarr;OPEN (TO: 45)
[Note] WSREP: New SELF-LEAVE.
[Note] WSREP: Flow-control interval: [0, 0]
[Note] WSREP: Received SELF-LEAVE. Closing connection.
[Note] WSREP: Shifting OPEN&rarr;CLOSED (TO: 45)
[Note] WSREP: RECV thread exiting 0: Success
[Note] WSREP: recv_thread() joined.
[Note] WSREP: Closing send queue.
[Note] WSREP: Closing receive queue.
[Note] WSREP: ================================================
View:
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
 status: non-primary
 protocol_version: 4
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO
 final: no
 own_index: 0
 members(1):
 0: 1456a640-cc36-11f0-aca0-e388a5f80ba9, Node B
=================================================
[Note] WSREP: Non-primary view
[Note] WSREP: Server status change synced&rarr;connected
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
[Note] WSREP: ================================================
View:
 id: 22db9ea1-cb7b-11f0-b26e-6fbce72757f9:45
 status: non-primary
 protocol_version: 4
 capabilities: MULTI-MASTER, CERTIFICATION, PARALLEL_APPLYING, REPLAY, ISOLATION, PAUSE, CAUSAL_READ, INCREMENTAL_WS, UNORDERED, PREORDERED, STREAMING, NBO
 final: yes
 own_index: -1
 members(0):
=================================================
[Note] WSREP: Non-primary view
[Note] WSREP: Server status change connected&rarr;disconnected
[Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
[Note] WSREP: Applier thread exiting ret: 6 thd: 2
[Note] WSREP: Applier thread exiting ret: 6 thd: 9
[Warning] Aborted connection 2 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Note] WSREP: Applier thread exiting ret: 6 thd: 5
[Warning] Aborted connection 5 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Warning] Aborted connection 9 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
[Note] WSREP: Service thread queue flushed.
[Note] WSREP: ####### Assign initial position for certification: 00000000-0000-0000-0000-000000000000:-1, protocol version: 6
[Note] WSREP: Applier thread exiting ret: 0 thd: 6
[Warning] Aborted connection 6 to db: 'unconnected' user: 'unauthenticated' host: '' (This connection closed normally without authentication)
</code></pre>
<p>No idea why node B and C behave differently. But this makes the whole rolling schema upgrade (RSU) process completely arbitrary and unplannable.</p>
<p>The whole thing was tested in different variants and the node sometimes becomes inconsistent and sometimes not.</p>
<p>The inconsistent node B is synchronised back into the cluster via a forced SST.</p>
<h3>Test 3: Attribute promotion with DML remote (on node C)<a class="anchor-link" id="test-3-attribute-promotion-with-dml-remote-on-node-c"></a></h3>
<p>This case is somewhat trickier, as no default value is specified.</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT ''
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeC&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
nodeC&gt; UPDATE test SET data = 'Some data changed' WHERE id = 13;
nodeC&gt; DELETE FROM test WHERE id = 22;
</code></pre>
<p>The <code>ALTER TABLE</code> command was then executed on nodes 2 and 3.</p>
<p>All 3 operations worked perfectly. The cluster is still fully functional.</p>
<h3>Test 4: Attribute promotion with DML remote (on node B)<a class="anchor-link" id="test-4-attribute-promotion-with-dml-remote-on-node-b"></a></h3>
<p>Same test but the <code>ALTER TABLE ADD COLUMN</code> command is executed on node A and the DML command on node B.</p>
<p>Nodes A and C become &ldquo;Inconsistent&rdquo; and node B is still &ldquo;Synced&rdquo;.</p>
<h3>Test 5: Attribute promotion with DML locally (on node A)<a class="anchor-link" id="test-5-attribute-promotion-with-dml-locally-on-node-a"></a></h3>
<p>Analogue test but the DML command is executed locally on the same node as the DDL command.</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL; -- DEFAULT ''
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeA&gt; INSERT INTO test VALUES (NULL, 'Some data to fill table up', NOW());
ERROR 1136 (21S01): Column count doesn't match value count at row 1

nodeA&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());
ERROR 1364 (HY000): Field 'c1' doesn't have a default value

nodeA&gt; INSERT INTO test (id, data, ts, c1) VALUES (NULL, 'Some data to fill table up', NOW(), '');

nodeA&gt; SELECT * FROM test;
ERROR 1047 (08S01): WSREP has not yet prepared node for application use

nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<p>Cluster node became inconsistent. Interestingly enough, the whole thing suddenly worked during further testing! So completely unpredictable&hellip;</p>
<h3>Test 6: DDL on 2 nodes<a class="anchor-link" id="test-6-ddl-on-2-nodes"></a></h3>
<p>New question: What happens after the DDL command has been executed on 2 nodes and then DML commands occur?</p>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeB&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeB&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeB&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeC&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());
</code></pre>
<p>works, but:</p>
<pre><code>nodeB&gt; INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW());

root@localhost [test]&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+
</code></pre>
<h3>Test 7: UPDATE and DELETE commands from the same node<a class="anchor-link" id="test-7-update-and-delete-commands-from-the-same-node"></a></h3>
<pre><code>nodeA&gt; SET SESSION wsrep_OSU_method = 'RSU';
nodeA&gt; ALTER TABLE test ADD COLUMN c1 VARCHAR(64) NOT NULL DEFAULT 'foo';
nodeA&gt; SET SESSION wsrep_OSU_method = 'TOI';

nodeA&gt; UPDATE test SET data = 'Some data changed' WHERE id = 16;

nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------------+
| Variable_name | Value |
+---------------------------+--------------+
| wsrep_local_state_comment | Inconsistent |
+---------------------------+--------------+

nodeA&gt; DELETE FROM test WHERE id = 25;
nodeA&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name | Value |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+

...
[Warning] WSREP: Ignoring error 'Can't find record in 'test'' on Delete_rows_v1 event. Error_code: 1032
[Warning] Slave SQL: Could not execute Delete_rows_v1 event on table test.test; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find record in 'test', Error_code: 1032; Can't find re
[ERROR] Slave SQL: Could not read field 'id' of table 'test.test', Internal MariaDB error code: 1610
[ERROR] mariadbd: Can't find record in 'test'
...
</code></pre>
<p>Process list from node C:</p>
<pre><code>nodeC&gt; SHOW PROCESSLIST;
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
| Id | User | Host | db | Command | Time | State | Info | Progress |
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
| 2 | system user | | NULL | Sleep | 1670 | After apply log event | NULL | 0.000 |
| 1 | system user | | NULL | Sleep | 4609 | wsrep aborter idle | NULL | 0.000 |
| 7 | system user | | NULL | Sleep | 4608 | | NULL | 0.000 |
| 8 | system user | | test | Sleep | 1441 | Executing | DELETE FROM test WHERE id = 25?5jR | 0.000 |
| 10 | system user | | NULL | Sleep | 2002 | wsrep applied write set | INSERT INTO test (id, data, ts) VALUES (NULL, 'Some data to fill table up', NOW()) 9O? | 0.000 |
| 28 | root | localhost | NULL | Query | 0 | starting | show processlist | 0.000 |
+----+-------------+-----------+------+---------+------+-------------------------+----------------------------------------------------------------------------------------+----------+
</code></pre>
<p>Node C is still synchronised:</p>
<pre><code>nodeC&gt; SHOW GLOBAL STATUS LIKE 'wsrep_local_state_comment';
+---------------------------+--------+
| Variable_name | Value |
+---------------------------+--------+
| wsrep_local_state_comment | Synced |
+---------------------------+--------+
</code></pre>
<p>Shutdown of the node for the following error message:</p>
<pre><code>nodeC&gt; SQL&gt; shutdown;
ERROR 1047 (08S01): WSREP has not yet prepared node for application use
</code></pre>
<p>The entire cluster was then no longer usable. And had to be restarted (bootstrap).</p>
<h2>Summary<a class="anchor-link" id="summary"></a></h2>
<p>We have opened a bug at MariaDB on this topic: <a href="https://jira.mariadb.org/browse/MDEV-38215" target="_blank">Attribute Promotion/Demotion in Galera Cluster</a>.</p>
<p>Further tests were not carried out for the time being, as this feature is too unstable in the tested version and <code>DROP COLUMN</code> is not a use case of our customer.</p>
<p>Conclusion: MariaDB Galera Cluster does not properly handle this situation in the version tested, nor does the cluster prevent this case. Cluster nodes are marked as inconsistent. Our current recommendation: Do NOT do a rolling schema upgrade (RSU) with concurrent DML commands (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>) on the tables to be changed with the analysed version!</p>
<p>Further sources:</p>
<ul>
<li><a href="https://galeracluster.com/documentation/html_docs_2023/_sources/documentation/inconsistency-voting.rst.txt" target="_blank">Galera Cluster Inconsistency Voting protocol</a></li>
<li><a href="https://www.percona.com/blog/inconsistent-voting-in-percona-xtradb-cluster/" target="_blank">Inconsistent Voting in Percona XtraDB Cluster</a></li>
</ul>

<p><a href="https://www.fromdual.com/blog/attribute-promotion-and-demotion-in-the-mariadb-galera-cluster/">Attribute promotion and demotion in the MariaDB Galera Cluster</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>TDE is now available for PostgreSQL 18</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/28/tde-is-now-available-for-postgresql-18/" />
      <id>https://percona.community/blog/2025/11/28/tde-is-now-available-for-postgresql-18/</id>
      <updated>2025-11-28T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Back in October, before PGConf.EU, I explained the issues impacting the prolonged wait for TDE in PostgreSQL 18. Explanations were needed as users were buzzing with anticipation, and they deserved to understand what caused the delays and what the roadmap looked like. In that blog post I have shared that due to one of the features newly added in 18.0, the Asynchronous IO (AIO), we have decided to give ourselves time until 18.1 has been released to provide a build with TDE. We wanted to ensure best quality of the solution and that takes time.</p>
<p><a href="https://percona.community/blog/2025/11/28/tde-is-now-available-for-postgresql-18/">TDE is now available for PostgreSQL 18</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Back in October, before <a href="http://pgconf.eu/" target="_blank" rel="noopener noreferrer">PGConf.EU</a>, I <a href="https://percona.community/blog/2025/10/15/keep-calm-tde-for-postgresql-18-is-on-its-way/" target="_blank" rel="noopener noreferrer">explained the issues impacting the prolonged wait for TDE in PostgreSQL 18</a>. Explanations were needed as users were buzzing with anticipation, and they deserved to understand what caused the delays and what the roadmap looked like. In that blog post I have shared that due to one of the features newly added in 18.0, the <a href="https://www.postgresql.org/about/featurematrix/detail/asynchronous-io-aio/" target="_blank" rel="noopener noreferrer">Asynchronous IO (AIO)</a>, we have decided to give ourselves time until 18.1 has been released to provide a build with TDE. We wanted to ensure best quality of the solution and that takes time.</p>
<p>As planned in the <a href="https://www.postgresql.org/developer/roadmap/" target="_blank" rel="noopener noreferrer">PostgreSQL Development Group (PGDG) roadmap</a>, on November 13, the <a href="https://www.postgresql.org/about/news/postgresql-181-177-1611-1515-1420-and-1323-released-3171/" target="_blank" rel="noopener noreferrer">Community released PostgreSQL 18.1</a>, and Percona engineers managed not only to deliver TDE compatible with the PostgreSQL 18 changes, but to fully support them.<br>
We are also skipping PostgreSQL 18.0 entirely in our distribution.<br>
The first TDE enabled release will be <a href="https://docs.percona.com/postgresql/18/release-notes/release-notes-v18.1.1.html" target="_blank" rel="noopener noreferrer">Percona Distribution for PostgreSQL 18.1.1</a>, aligning our builds directly with the first PostgreSQL 18 minor release.<br>
Here are some details on what&rsquo;s new!</p>
<h1>Percona PostgreSQL + TDE + AIO<a class="anchor-link" id="percona-postgresql-tde-aio"></a></h1>
<p>Today, we&rsquo;re proud to announce that Percona now ships PostgreSQL 18.1 with fully supported TDE and AIO from the very beginning.</p>
<figure><img decoding="async" width="3022" height="462" src="https://percona.community/blog/2025/11/Jan-PG-18-luv_hu_3f7cb1c518360001.webp" alt="&nbsp;" loading="lazy"></figure>

<p>No patching, no workarounds, no &ldquo;experimental caveats.&rdquo; Encryption-at-rest is not just compatible with AIO. It&rsquo;s supported, integrated and ready for production deployments across the ecosystem, enabling hardened PostgreSQL environments to meet compliance requirements with confidence as beginning with PostgreSQL 18.1, Percona:</p>
<ul>
<li>fully supports native TDE</li>
<li>ships AIO-enabled builds, aligned with Community PostgreSQL</li>
<li>provides production-ready packages for enterprise deployments</li>
</ul>
<p>TDE matures and pg_tde returns home<br>
In 2026 we are planning significant investment to ensure that TDE continues to evolve for Community PostgreSQL. As part of this renewed focus, Percona is shifting pg_tde back to its dedicated home:</p>
<p>&#10145;&#65039; <a href="https://github.com/percona/pg_tde" target="_blank" rel="noopener noreferrer">https://github.com/percona/pg_tde</a></p>
<p>This reflects pg_tde&rsquo;s new role. With the growing number of pg_tde users, it can no longer be treated as a stopgap solution filling a gap in PostgreSQL. Instead, we want to approach it as a complementary, advanced encryption layer for the PostgreSQL 18+ era:</p>
<ul>
<li>a place to deliver extended capabilities for data-at-rest encryption as soon as they are ready</li>
<li>a hub for integrations requested by the community</li>
<li>a safe space for feedback, comments, and questions to help drive the future of TDE</li>
</ul>
<p>Releasing this new version brings some real improvements, especially on the KMS integration side.</p>
<h1>KMS improvements<a class="anchor-link" id="kms-improvements"></a></h1>
<p>We expanded Key Management Service (KMS) capabilities based directly on user and customer feedback.<br>
Whether it came through GitHub, Percona Community Forums, events, or direct conversations &mdash; thank you! Your input shapes the roadmap.</p>
<h3>pg_tde now works with Akeyless KMS<a class="anchor-link" id="pg_tde-now-works-with-akeyless-kms"></a></h3>
<p><a href="https://www.akeyless.io/" target="_blank" rel="noopener noreferrer">Akeyless</a> is gaining traction among organizations implementing zero-trust security models. pg_tde now integrates cleanly with Akeyless, enabling robust key retrieval and lifecycle management across cloud and on-prem deployments using KMIP.</p>
<h3>HashiCorp Vault &amp; OpenBao Namespace Support<a class="anchor-link" id="hashicorp-vault-openbao-namespace-support"></a></h3>
<p>Vault and OpenBao users can now take advantage of namespaces when storing encryption keys</p>
<p>Why do namespaces matter?</p>
<ul>
<li>Multi-tenancy: isolate key access for teams, environments, or applications</li>
<li>Security boundaries: each namespace can enforce its own authentication and audit policies</li>
<li>Cleaner CI/CD: dev/staging/prod can share a consistent key path structure</li>
<li>Delegation &amp; separation of duties: security teams manage root policies, while application teams manage their own namespaces</li>
</ul>
<p>In large organizations, namespace support isn&rsquo;t just a convenience, it&rsquo;s a requirement.<br>
OpenBao prioritized this early and <a href="https://openbao.org/blog/namespaces-announcement/" target="_blank" rel="noopener noreferrer">released it back in May 2025</a>.<br>
With pg_tde now supporting namespaces natively, PostgreSQL deployments gain enterprise-grade key management flexibility.</p>
<h1>What Comes Next?<a class="anchor-link" id="what-comes-next"></a></h1>
<p>Our commitment remains unchanged: TDE must be a first-class, accessible, community-driven feature in PostgreSQL. Share your feedback and let us achieve this! We can build the future of an open source TDE solution for PostgreSQL together with full openness and transparency!</p>
<p>Winter may be coming, but no weather can stop us! Expect more from TDE soon:</p>
<ul>
<li>Key length configurations are coming &ndash; expect to be able to use 256-bit keys with TDE soon!</li>
<li>extended KMS integrations &ndash; we&rsquo;re already looking into cloud KMS support. Please share which ones you are using and what use cases you want supported!</li>
<li>improvements to pg_tde encryption targets &ndash; temporary files are next to be explored followed by system catalog</li>
<li>compatibility with Community PostgreSQL &ndash; we want to drive inclusion of any changes required by pg_tde in Community PostgreSQL so that the extension can run for users of all PostgreSQL builds!</li>
</ul>
<p>&#10240;At the risk of repeating myself, the deserved highlight goes to the fact that we build based on your feedback, so don&rsquo;t be strangers!</p>
<h1>We Want to Hear from You<a class="anchor-link" id="we-want-to-hear-from-you"></a></h1>
<p>Tell us how you&rsquo;re using PostgreSQL with TDE.<br>
Tell us what your security requirements look like.<br>
Tell us where tooling can be improved.<br>
You can reach us at:</p>
<ul>
<li>Percona Community Forums&#8232;<a href="https://forums.percona.com/c/postgresql/25" target="_blank" rel="noopener noreferrer">https://forums.percona.com/c/postgresql/25</a></li>
<li>pg_tde GitHub Repo&#8232;<a href="https://github.com/percona/pg_tde" target="_blank" rel="noopener noreferrer">https://github.com/percona/pg_tde</a></li>
<li>Issues &amp; Discussions
<ul>
<li><a href="https://github.com/percona/pg_tde/issues" target="_blank" rel="noopener noreferrer">https://github.com/percona/pg_tde/issues</a></li>
<li><a href="https://github.com/percona/pg_tde/discussions" target="_blank" rel="noopener noreferrer">https://github.com/percona/pg_tde/discussions</a></li>
</ul>
</li>
</ul>
<p>Or, if you see us at an event, come chat with us in person!</p>

<p><a href="https://percona.community/blog/2025/11/28/tde-is-now-available-for-postgresql-18/">TDE is now available for PostgreSQL 18</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Slack is a Suboptimal Feed Reader (RSS / Atom)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html" />
      <id>https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html</id>
      <updated>2025-11-25T13:33:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This is a MySQL Blog, why am I posting about Slack, Feed Readers, RSS and Atom ?  Because blog aggregators, which are usually consumed on their RSS or Atom interface via a Feed Reader, are an important knowledge sharing tool in the MySQL Community (and in other communities, see Valkey below).  I know some people are using Slack as their Feed Reader, and I recently realized Slack is</p>
<p><a href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html">Slack is a Suboptimal Feed Reader (RSS / Atom)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This is a MySQL Blog, why am I posting about Slack, Feed Readers, RSS and Atom&nbsp;?&nbsp; Because blog aggregators, which are usually consumed on their RSS or Atom interface via a Feed Reader, are an important knowledge sharing tool in the MySQL Community (and in other communities, see Valkey below).&nbsp; I know some people are using Slack as their Feed Reader, and I recently realized Slack is</p>

<p><a href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html">Slack is a Suboptimal Feed Reader (RSS / Atom)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Slack is a Suboptimal Feed Reader (RSS / Atom)</title>
      <link rel="alternate" type="text/html" href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html" />
      <id>https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html</id>
      <updated>2025-11-25T13:33:00+02:00</updated>
      <author><name>Jean-François Gagné</name></author>
      <summary type="html"><![CDATA[<p>This is a MySQL Blog, why am I posting about Slack, Feed Readers, RSS and Atom ?  Because blog aggregators, which are usually consumed on their RSS or Atom interface via a Feed Reader, are an important knowledge sharing tool in the MySQL Community (and in other communities, see Valkey below).  I know some people are using Slack as their Feed Reader, and I recently realized Slack is</p>
<p><a href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html">Slack is a Suboptimal Feed Reader (RSS / Atom)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>This is a MySQL Blog, why am I posting about Slack, Feed Readers, RSS and Atom&nbsp;?&nbsp; Because blog aggregators, which are usually consumed on their RSS or Atom interface via a Feed Reader, are an important knowledge sharing tool in the MySQL Community (and in other communities, see Valkey below).&nbsp; I know some people are using Slack as their Feed Reader, and I recently realized Slack is</p>

<p><a href="https://jfg-mysql.blogspot.com/2025/11/slack-suboptimal-feed-reader.html">Slack is a Suboptimal Feed Reader (RSS / Atom)</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>The Right Tool for the Job</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/24/the-right-tool-for-the-job/" />
      <id>https://percona.community/blog/2025/11/24/the-right-tool-for-the-job/</id>
      <updated>2025-11-24T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>When I first got into woodworking, my mentor shared a piece of advice that has stuck with me ever since: “Use the right tool for the job.” You wouldn’t reach for a belt sander to flatten a board when a planer can accomplish the task faster, cleaner, and with far better results.</p>
<p><a href="https://percona.community/blog/2025/11/24/the-right-tool-for-the-job/">The Right Tool for the Job</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>When I first got into woodworking, my mentor shared a piece of advice that has stuck with me ever since: &ldquo;Use the right tool for the job.&rdquo; You wouldn&rsquo;t reach for a belt sander to flatten a board when a planer can accomplish the task faster, cleaner, and with far better results.</p>
<p>The same principle applies in the world of database engineering. When working with MySQL or Percona Server, choosing the correct tool can be the difference between efficient diagnostics and unnecessary downtime.</p>
<p>In this post, I&rsquo;ll highlight several of the most practical and commonly used utilities from the Percona Toolkit. While the toolkit includes many powerful commands, I&rsquo;ll focus on the ones that provide the most value in day-to-day operations, troubleshooting, and gathering actionable details for support cases.</p>
<h2>PT Summary<a class="anchor-link" id="pt-summary"></a></h2>
<p>A Percona Toolkit utility that provides a concise, high-level overview of a system&rsquo;s hardware, OS configuration and performance-related metrics. It&rsquo;s designed to quickly capture the essential details needed for diagnostics or support cases&mdash;CPU, memory, disk layout, kernel parameters and more all in a single, easy-to-read report.</p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<p>Run pt-summary with no arguments to generate a full system summary. When possible, run it with sudo to allow the tool to collect additional details that require elevated privileges:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">sudo pt-summary
</span></span><span class="line"><span class="cl"># Percona Toolkit System Summary Report ######################
</span></span><span class="line"><span class="cl"> Date | 2025-11-24 17:15:19 UTC (local TZ: EST -0500)
</span></span><span class="line"><span class="cl"> Hostname | pi16gb
</span></span><span class="line"><span class="cl"> Uptime | 41 days, 2:27, 4 users, load average: 0.00, 0.00, 0.00
</span></span><span class="line"><span class="cl"> Platform | Linux
</span></span><span class="line"><span class="cl"> Release | Debian GNU/Linux 12 (bookworm) (bookworm)
</span></span><span class="line"><span class="cl"> Kernel | 6.12.47+rpt-rpi-2712
</span></span><span class="line"><span class="cl">Architecture | CPU = 32-bit, OS = 64-bit
</span></span><span class="line"><span class="cl"> Threading | NPTL 2.36
</span></span><span class="line"><span class="cl"> SELinux | No SELinux detected
</span></span><span class="line"><span class="cl"> Virtualized | No virtualization detected
</span></span><span class="line"><span class="cl"># Processor ##################################################
</span></span><span class="line"><span class="cl"> Processors | physical = 4, cores = 0, virtual = 4, hyperthreading = no
</span></span><span class="line"><span class="cl"> Speeds |
</span></span><span class="line"><span class="cl"> Models |
</span></span><span class="line"><span class="cl"> Caches |
</span></span><span class="line"><span class="cl"> Designation Configuration Size Associativity
</span></span><span class="line"><span class="cl"> ========================= ============================== ======== ======================
</span></span><span class="line"><span class="cl"># Memory #####################################################
</span></span><span class="line"><span class="cl"> Total | 15.8G
</span></span><span class="line"><span class="cl"> Free | 675.0M
</span></span><span class="line"><span class="cl"> Used | physical = 5.3G, swap allocated = 512.0M, swap used = 0.0, virtual = 5.3G
</span></span><span class="line"><span class="cl"> Shared | 44.7M
</span></span><span class="line"><span class="cl"> Buffers | 10.6G
</span></span><span class="line"><span class="cl"> Caches | 10.5G
</span></span><span class="line"><span class="cl"> Dirty | 128 kB
</span></span><span class="line"><span class="cl"> UsedRSS | 5.1G
</span></span><span class="line"><span class="cl"> Swappiness | 60
</span></span><span class="line"><span class="cl"> DirtyPolicy | 20, 10
</span></span><span class="line"><span class="cl"> DirtyStatus | 0, 0
</span></span><span class="line"><span class="cl"> Locator Size Speed Form Factor Type Type Detail
</span></span><span class="line"><span class="cl"> ========= ======== ================= ============= ============= ===========
</span></span><span class="line"><span class="cl"># Mounted Filesystems ########################################
</span></span><span class="line"><span class="cl"> Filesystem Size Used Type Opts Mountpoint
</span></span><span class="line"><span class="cl"> /dev/nvme0n1p1 510M 14%
</span></span><span class="line"><span class="cl"> /dev/nvme0n1p2 458G 5%
</span></span><span class="line"><span class="cl"> /dev/sda1 117G 16%
</span></span><span class="line"><span class="cl"># Disk Schedulers And Queue Size #############################
</span></span><span class="line"><span class="cl"> nvme0n1 | [none] 255
</span></span><span class="line"><span class="cl"> sda | [mq-deadline] 60
</span></span><span class="line"><span class="cl"># Disk Partitioning ##########################################
</span></span><span class="line"><span class="cl"># Kernel Inode State #########################################
</span></span><span class="line"><span class="cl">dentry-state | 107782 98346 45 0 32304 0
</span></span><span class="line"><span class="cl"> file-nr | 3680 0 9223372036854775807
</span></span><span class="line"><span class="cl"> inode-nr | 99614 20818
</span></span><span class="line"><span class="cl"># LVM Volumes ################################################
</span></span><span class="line"><span class="cl">Unable to collect information
</span></span><span class="line"><span class="cl"># LVM Volume Groups ##########################################
</span></span><span class="line"><span class="cl">Unable to collect information
</span></span><span class="line"><span class="cl"># RAID Controller ############################################
</span></span><span class="line"><span class="cl"> Controller | No RAID controller detected
</span></span><span class="line"><span class="cl"># Network Config #############################################
</span></span><span class="line"><span class="cl"> Controller | 00.0 Ethernet controller
</span></span><span class="line"><span class="cl"> FIN Timeout | 60
</span></span><span class="line"><span class="cl"> Port Range | 60999
</span></span><span class="line"><span class="cl"># Interface Statistics #######################################
</span></span><span class="line"><span class="cl"> interface rx_bytes rx_packets rx_errors tx_bytes tx_packets tx_errors
</span></span><span class="line"><span class="cl"> ========= ========= ========== ========== ========== ========== ==========
</span></span><span class="line"><span class="cl"> lo 6000000000 175000 0 6000000000 175000 0
</span></span><span class="line"><span class="cl"> eth0 0 0 0 0 0 0
</span></span><span class="line"><span class="cl"> wlan0 5000000000 30000000 0 15000000000 22500000 0
</span></span><span class="line"><span class="cl"># Network Devices ############################################
</span></span><span class="line"><span class="cl"> Device Speed Duplex
</span></span><span class="line"><span class="cl"> ========= ========= =========
</span></span><span class="line"><span class="cl"> eth0 Unknown! Unknown!
</span></span><span class="line"><span class="cl"># Network Connections ########################################
</span></span><span class="line"><span class="cl"> Connections from remote IP addresses
</span></span><span class="line"><span class="cl"> 192.168.1.91 1
</span></span><span class="line"><span class="cl"> 192.168.1.251 1
</span></span><span class="line"><span class="cl"> 2603 2
</span></span><span class="line"><span class="cl"> Connections to local IP addresses
</span></span><span class="line"><span class="cl"> 192.168.1.145 2
</span></span><span class="line"><span class="cl"> 2603 2
</span></span><span class="line"><span class="cl"> Connections to top 10 local ports
</span></span><span class="line"><span class="cl"> 3306 2
</span></span><span class="line"><span class="cl"> 6011:ef0:7260:::22 2
</span></span><span class="line"><span class="cl"> States of connections
</span></span><span class="line"><span class="cl"> ESTABLISHED 3
</span></span><span class="line"><span class="cl"> LISTEN 6
</span></span><span class="line"><span class="cl"> TIME_WAIT 1
</span></span><span class="line"><span class="cl"># Top Processes ##############################################
</span></span><span class="line"><span class="cl"> PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
</span></span><span class="line"><span class="cl"> 95842 root 20 0 0 0 0 I 6.7 0.0 0:00.08 kworker+
</span></span><span class="line"><span class="cl"> 1 root 20 0 169520 13088 8672 S 0.0 0.1 0:18.56 systemd
</span></span><span class="line"><span class="cl"> 2 root 20 0 0 0 0 S 0.0 0.0 0:01.70 kthreadd
</span></span><span class="line"><span class="cl"> 3 root 20 0 0 0 0 S 0.0 0.0 0:00.00 pool_wo+
</span></span><span class="line"><span class="cl"> 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker+
</span></span><span class="line"><span class="cl"> 5 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker+
</span></span><span class="line"><span class="cl"> 6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker+
</span></span><span class="line"><span class="cl"> 7 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker+
</span></span><span class="line"><span class="cl"> 8 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker+
</span></span><span class="line"><span class="cl"># Notable Processes ##########################################
</span></span><span class="line"><span class="cl"> PID OOM COMMAND
</span></span><span class="line"><span class="cl"> ? ? sshd doesn't appear to be running
</span></span><span class="line"><span class="cl"># Simplified and fuzzy rounded vmstat (wait please) ##########
</span></span><span class="line"><span class="cl"> procs ---swap-- -----io---- ---system---- --------cpu--------
</span></span><span class="line"><span class="cl"> r b si so bi bo ir cs us sy il wa st
</span></span><span class="line"><span class="cl"> 2 0 0 0 1 6 100 150 0 0 100 0 0
</span></span><span class="line"><span class="cl"> 1 0 0 0 0 0 1750 3000 1 3 97 0 0
</span></span><span class="line"><span class="cl"> 1 0 0 0 0 0 250 400 0 0 100 0 0
</span></span><span class="line"><span class="cl"> 1 0 0 0 0 0 300 450 0 0 100 0 0
</span></span><span class="line"><span class="cl"> 1 0 0 0 0 0 300 450 0 0 100 0 0
</span></span><span class="line"><span class="cl"># Memory management ##########################################
</span></span><span class="line"><span class="cl"># The End ####################################################</span></span></code></pre>
</div>
</div>
</div>
<p>Redirect output to a file.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-summary &gt; server-summary.txt</span></span></code></pre>
</div>
</div>
</div>
<h2>PT MySQL Summary<a class="anchor-link" id="pt-mysql-summary"></a></h2>
<p>A Percona Toolkit utility that collects and displays a concise overview of a MySQL or Percona Server instance, including key configuration settings, performance metrics, storage engine details, replication status, buffer pool usage, and important global variables. It provides a fast, structured snapshot of the database environment, making it ideal for troubleshooting, tuning, and preparing information for support teams.</p>
<h3>Example<a class="anchor-link" id="example"></a></h3>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-mysql-summary
</span></span><span class="line"><span class="cl"># Percona Toolkit MySQL Summary Report #######################
</span></span><span class="line"><span class="cl"> System time | 2025-11-24 17:45:54 UTC (local TZ: EST -0500)
</span></span><span class="line"><span class="cl"># Instances ##################################################
</span></span><span class="line"><span class="cl"> Port Data Directory Nice OOM Socket
</span></span><span class="line"><span class="cl"> ===== ========================== ==== === ======
</span></span><span class="line"><span class="cl"> 3306 /data0/mysql/data/ 0 0 /usr/local/mysql/mysql.sock
</span></span><span class="line"><span class="cl"># MySQL Executable ###########################################
</span></span><span class="line"><span class="cl"> Path to executable | /usr/local/mysql/bin/mysqld
</span></span><span class="line"><span class="cl"> Has symbols | Yes
</span></span><span class="line"><span class="cl"># Report On Port 3306 ########################################
</span></span><span class="line"><span class="cl"> User | wayne@localhost
</span></span><span class="line"><span class="cl"> Time | 2025-11-24 12:45:54 (EST)
</span></span><span class="line"><span class="cl"> Hostname | pi16gb
</span></span><span class="line"><span class="cl"> Version | 8.4.6-6 Source distribution
</span></span><span class="line"><span class="cl"> Built On | Linux aarch64
</span></span><span class="line"><span class="cl"> Started | 2025-10-14 09:48 (up 41+02:57:35)
</span></span><span class="line"><span class="cl"> Databases | 10
</span></span><span class="line"><span class="cl"> Datadir | /data0/mysql/data/
</span></span><span class="line"><span class="cl"> Processes | 2 connected, 2 running
</span></span><span class="line"><span class="cl"> Replication | Is not a replica, has 1 replicas connected
</span></span><span class="line"><span class="cl"> Pidfile | /usr/local/mysql/mysqld.pid (exists)
</span></span><span class="line"><span class="cl"># Processlist ################################################
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> Command COUNT(*) Working SUM(Time) MAX(Time)
</span></span><span class="line"><span class="cl"> ------------------------------ -------- ------- --------- ---------
</span></span><span class="line"><span class="cl"> Binlog Dump GTID 1 1 3000000 3000000
</span></span><span class="line"><span class="cl"> Query 1 1 0 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> User COUNT(*) Working SUM(Time) MAX(Time)
</span></span><span class="line"><span class="cl"> ------------------------------ -------- ------- --------- ---------
</span></span><span class="line"><span class="cl"> replication 1 1 3000000 3000000
</span></span><span class="line"><span class="cl"> wayne 1 1 0 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> Host COUNT(*) Working SUM(Time) MAX(Time)
</span></span><span class="line"><span class="cl"> ------------------------------ -------- ------- --------- ---------
</span></span><span class="line"><span class="cl"> 192.168.1.251 1 1 3000000 3000000
</span></span><span class="line"><span class="cl"> localhost 1 1 0 0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> db COUNT(*) Working SUM(Time) MAX(Time)
</span></span><span class="line"><span class="cl"> ------------------------------ -------- ------- --------- ---------
</span></span><span class="line"><span class="cl"> NULL 2 2 3000000 3000000
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> State COUNT(*) Working SUM(Time) MAX(Time)
</span></span><span class="line"><span class="cl"> ------------------------------ -------- ------- --------- ---------
</span></span><span class="line"><span class="cl"> init 1 1 0 0
</span></span><span class="line"><span class="cl"> Source has sent all binlog to 1 1 3000000 3000000
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Status Counters (Wait 10 Seconds) ##########################
</span></span><span class="line"><span class="cl">Variable Per day Per second 11 secs
</span></span><span class="line"><span class="cl">Aborted_clients 1
</span></span><span class="line"><span class="cl">Binlog_snapshot_position 350000 4
</span></span><span class="line"><span class="cl">Binlog_cache_use 6000
</span></span><span class="line"><span class="cl">Bytes_received 20000000 225 600
</span></span><span class="line"><span class="cl">Bytes_sent 2250000000 25000 4000
</span></span><span class="line"><span class="cl">[...]
</span></span><span class="line"><span class="cl">Table_open_cache_misses 400
</span></span><span class="line"><span class="cl">Table_open_cache_overflows 225
</span></span><span class="line"><span class="cl">Threads_created 9
</span></span><span class="line"><span class="cl">Uptime 90000 1 1
</span></span><span class="line"><span class="cl"># Table cache ################################################
</span></span><span class="line"><span class="cl"> Size | 1000
</span></span><span class="line"><span class="cl"> Usage | 100%
</span></span><span class="line"><span class="cl"># Key Percona Server features ################################
</span></span><span class="line"><span class="cl"> Table &amp; Index Stats | Disabled
</span></span><span class="line"><span class="cl"> Multiple I/O Threads | Enabled
</span></span><span class="line"><span class="cl"> Corruption Resilient | Enabled
</span></span><span class="line"><span class="cl"> Durable Replication | Not Supported
</span></span><span class="line"><span class="cl"> Import InnoDB Tables | Not Supported
</span></span><span class="line"><span class="cl"> Fast Server Restarts | Not Supported
</span></span><span class="line"><span class="cl"> Enhanced Logging | Disabled
</span></span><span class="line"><span class="cl"> Replica Perf Logging | Disabled
</span></span><span class="line"><span class="cl"> Response Time Hist. | Not Supported
</span></span><span class="line"><span class="cl"> Smooth Flushing | Not Supported
</span></span><span class="line"><span class="cl"> HandlerSocket NoSQL | Not Supported
</span></span><span class="line"><span class="cl"> Fast Hash UDFs | Unknown
</span></span><span class="line"><span class="cl"># Percona XtraDB Cluster #####################################
</span></span><span class="line"><span class="cl"># Plugins ####################################################
</span></span><span class="line"><span class="cl"> InnoDB compression | ACTIVE
</span></span><span class="line"><span class="cl"># Schema #####################################################
</span></span><span class="line"><span class="cl">Specify --databases or --all-databases to dump and summarize schemas
</span></span><span class="line"><span class="cl"># Noteworthy Technologies ####################################
</span></span><span class="line"><span class="cl"> SSL | Yes
</span></span><span class="line"><span class="cl"> Explicit LOCK TABLES | No
</span></span><span class="line"><span class="cl"> Delayed Insert | No
</span></span><span class="line"><span class="cl"> XA Transactions | No
</span></span><span class="line"><span class="cl"> NDB Cluster | No
</span></span><span class="line"><span class="cl"> Prepared Statements | Yes
</span></span><span class="line"><span class="cl"> Prepared statement count | 0
</span></span><span class="line"><span class="cl"># InnoDB #####################################################
</span></span><span class="line"><span class="cl"> Version | 8.4.6-6
</span></span><span class="line"><span class="cl"> Buffer Pool Size | 8.0G
</span></span><span class="line"><span class="cl"> Buffer Pool Fill | 30%
</span></span><span class="line"><span class="cl"> Buffer Pool Dirty | 0%
</span></span><span class="line"><span class="cl"> File Per Table | ON
</span></span><span class="line"><span class="cl"> Page Size | 16k
</span></span><span class="line"><span class="cl"> Log File Size | 2 * 48.0M = 96.0M
</span></span><span class="line"><span class="cl"> Log Buffer Size | 64M
</span></span><span class="line"><span class="cl"> Flush Method | O_DIRECT
</span></span><span class="line"><span class="cl"> Flush Log At Commit | 2
</span></span><span class="line"><span class="cl"> XA Support |
</span></span><span class="line"><span class="cl"> Checksums |
</span></span><span class="line"><span class="cl"> Doublewrite | ON
</span></span><span class="line"><span class="cl"> R/W I/O Threads | 4 4
</span></span><span class="line"><span class="cl"> I/O Capacity | 200
</span></span><span class="line"><span class="cl"> Thread Concurrency | 4
</span></span><span class="line"><span class="cl"> Concurrency Tickets | 5000
</span></span><span class="line"><span class="cl"> Commit Concurrency | 0
</span></span><span class="line"><span class="cl"> Txn Isolation Level | REPEATABLE-READ
</span></span><span class="line"><span class="cl"> Adaptive Flushing | ON
</span></span><span class="line"><span class="cl"> Adaptive Checkpoint |
</span></span><span class="line"><span class="cl"> Checkpoint Age | 0
</span></span><span class="line"><span class="cl"> InnoDB Queue | 0 queries inside InnoDB, 0 queries in queue
</span></span><span class="line"><span class="cl"> Oldest Transaction | 0 Seconds
</span></span><span class="line"><span class="cl"> History List Len | 1
</span></span><span class="line"><span class="cl"> Read Views | 0
</span></span><span class="line"><span class="cl"> Undo Log Entries | 0 transactions, 0 total undo, 0 max undo
</span></span><span class="line"><span class="cl"> Pending I/O Reads | 0 buf pool reads, 0 normal AIO, 0 ibuf AIO, 0 preads
</span></span><span class="line"><span class="cl"> Pending I/O Writes | 0 buf pool (0 LRU, 0 flush list, 0 page); 0 AIO, 0 sync, 0 log IO (0 log, 0 chkp); 1 pwrites
</span></span><span class="line"><span class="cl"> Pending I/O Flushes | 0 buf pool, 0 log
</span></span><span class="line"><span class="cl"> Transaction States | 3xnot started
</span></span><span class="line"><span class="cl"># MyISAM #####################################################
</span></span><span class="line"><span class="cl"> Key Cache | 8.0M
</span></span><span class="line"><span class="cl"> Pct Used | 20%
</span></span><span class="line"><span class="cl"> Unflushed | 0%
</span></span><span class="line"><span class="cl"># Security ###################################################
</span></span><span class="line"><span class="cl"> Users | 8 users, 0 anon, 0 w/o pw, 7 old pw
</span></span><span class="line"><span class="cl"> Old Passwords |
</span></span><span class="line"><span class="cl"># Encryption #################################################
</span></span><span class="line"><span class="cl">No keyring plugins found
</span></span><span class="line"><span class="cl"># Binary Logging #############################################
</span></span><span class="line"><span class="cl"> Binlogs | 3
</span></span><span class="line"><span class="cl"> Zero-Sized | 0
</span></span><span class="line"><span class="cl"> Total Size | 437.9M
</span></span><span class="line"><span class="cl"> binlog_format | ROW
</span></span><span class="line"><span class="cl"> expire_logs_days |
</span></span><span class="line"><span class="cl"> sync_binlog | 0
</span></span><span class="line"><span class="cl"> server_id | 10
</span></span><span class="line"><span class="cl"> binlog_do_db |
</span></span><span class="line"><span class="cl"> binlog_ignore_db |
</span></span><span class="line"><span class="cl"># Noteworthy Variables #######################################
</span></span><span class="line"><span class="cl"> Auto-Inc Incr/Offset | 1/1
</span></span><span class="line"><span class="cl"> default_storage_engine | InnoDB
</span></span><span class="line"><span class="cl"> flush_time | 0
</span></span><span class="line"><span class="cl"> init_connect |
</span></span><span class="line"><span class="cl"> init_file |
</span></span><span class="line"><span class="cl"> sql_mode | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
</span></span><span class="line"><span class="cl"> join_buffer_size | 256k
</span></span><span class="line"><span class="cl"> sort_buffer_size | 256k
</span></span><span class="line"><span class="cl"> read_buffer_size | 128k
</span></span><span class="line"><span class="cl"> read_rnd_buffer_size | 256k
</span></span><span class="line"><span class="cl"> bulk_insert_buffer | 0.00
</span></span><span class="line"><span class="cl"> max_heap_table_size | 16M
</span></span><span class="line"><span class="cl"> tmp_table_size | 16M
</span></span><span class="line"><span class="cl"> max_allowed_packet | 64M
</span></span><span class="line"><span class="cl"> thread_stack | 1M
</span></span><span class="line"><span class="cl"> log |
</span></span><span class="line"><span class="cl"> log_error | /var/log/mysql/mysqld.log
</span></span><span class="line"><span class="cl"> log_warnings |
</span></span><span class="line"><span class="cl"> log_slow_queries |
</span></span><span class="line"><span class="cl">log_queries_not_using_indexes | OFF
</span></span><span class="line"><span class="cl"> log_replica_updates | ON
</span></span><span class="line"><span class="cl"># Configuration File #########################################
</span></span><span class="line"><span class="cl"> Config File | /etc/my.cnf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[mysqld]
</span></span><span class="line"><span class="cl">character-set-server = utf8mb4
</span></span><span class="line"><span class="cl">authentication_policy = '*'
</span></span><span class="line"><span class="cl">port = 3306
</span></span><span class="line"><span class="cl">socket = /usr/local/mysql/mysql.sock
</span></span><span class="line"><span class="cl">pid-file = /usr/local/mysql/mysqld.pid
</span></span><span class="line"><span class="cl">basedir = /usr/local/mysql/
</span></span><span class="line"><span class="cl">datadir = /data0/mysql/data/
</span></span><span class="line"><span class="cl">tmpdir = /data0/mysql/tmp/
</span></span><span class="line"><span class="cl">general_log_file = /var/log/mysql/mysql-general.log
</span></span><span class="line"><span class="cl">log-error = /var/log/mysql/mysqld.log
</span></span><span class="line"><span class="cl">slow_query_log_file = /var/log/mysql/slow_query.log
</span></span><span class="line"><span class="cl">[...]
</span></span><span class="line"><span class="cl">innodb_data_home_dir = /data0/mysql/data/
</span></span><span class="line"><span class="cl">innodb_log_group_home_dir = /data0/mysql/data/
</span></span><span class="line"><span class="cl">innodb_temp_data_file_path = ../tmp/ibtmp1:12M:autoextend:max:8G
</span></span><span class="line"><span class="cl">innodb_buffer_pool_size = 8G
</span></span><span class="line"><span class="cl">innodb-redo-log-capacity = 2G
</span></span><span class="line"><span class="cl">innodb_flush_log_at_trx_commit = 2
</span></span><span class="line"><span class="cl">innodb_lock_wait_timeout = 50
</span></span><span class="line"><span class="cl">innodb_flush_method = O_DIRECT
</span></span><span class="line"><span class="cl">innodb_file_per_table = 1
</span></span><span class="line"><span class="cl">innodb_io_capacity = 200
</span></span><span class="line"><span class="cl">innodb_buffer_pool_instances = 8
</span></span><span class="line"><span class="cl">innodb_thread_concurrency = 4
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Memory management library ##################################
</span></span><span class="line"><span class="cl">jemalloc is not enabled in mysql config for process with id 788
</span></span><span class="line"><span class="cl"># The End ####################################################</span></span></code></pre>
</div>
</div>
</div>
<p>Redirect output to file.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-mysql-summary &gt; percona-server.txt</span></span></code></pre>
</div>
</div>
</div>
<p>Capture both pt-summary and pt-mysql-summary into a single file.</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-summary &gt; percona-server-summary.txt
</span></span><span class="line"><span class="cl">pt-mysql-summary &gt;&gt; percona-server-summary.txt</span></span></code></pre>
</div>
</div>
</div>
<h2>PT Online Schema Change<a class="anchor-link" id="pt-online-schema-change"></a></h2>
<p>A Percona Toolkit utility that performs online ALTER TABLE operations by creating a shadow copy of the table, applying the schema change to that copy, and keeping it in sync with the original using triggers until it is ready to swap. This workflow minimizes locking and reduces downtime, allowing large production tables to be altered safely with minimal impact on applications. However, it&rsquo;s important to remind users that long-running queries or transactions holding metadata locks (MDL) on the table will still block the final swap, potentially delaying completion of the schema change.</p>
<h3>Examples<a class="anchor-link" id="examples"></a></h3>
<h4>Adding a New Column</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "ADD COLUMN status TINYINT NOT NULL DEFAULT 0" 
</span></span><span class="line"><span class="cl"> D=mydb,t=orders 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>This safely introduces a new column to a busy table without blocking reads or writes. The tool handles the copy, synchronization, and final table swap automatically.</p>
<h4>Modifying a Column Type</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-6" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "MODIFY COLUMN price DECIMAL(10,2)" 
</span></span><span class="line"><span class="cl"> D=shop,t=products 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>Changing column definitions&mdash;especially on large datasets&mdash;can be disruptive using standard SQL. With pt-osc, the migration happens online, keeping applications responsive throughout the operation.</p>
<h4>Dropping an Unused Column</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-7" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "DROP COLUMN old_flag" 
</span></span><span class="line"><span class="cl"> D=analytics,t=events 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>Column drops can require a full table rebuild, making them great candidates for pt-osc. This example removes a legacy column while avoiding table locks.</p>
<h4>Adding an Index</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-8" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "ADD INDEX idx_user_id (user_id)" 
</span></span><span class="line"><span class="cl"> D=app,t=logins 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>Index creation is another expensive operation for large tables. Here, pt-osc allows the index to be added online, improving performance without interrupting the application.</p>
<h4>Changing a Primary Key</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-9" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "DROP PRIMARY KEY, ADD PRIMARY KEY(id, created_at)" 
</span></span><span class="line"><span class="cl"> D=orders,t=order_items 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>Primary key modifications usually require a full table rewrite. pt-osc makes this process safer and easier on production systems by performing the change on a temporary shadow table.</p>
<h4>Performing a Dry Run</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-10" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "ADD COLUMN test INT" 
</span></span><span class="line"><span class="cl"> D=mydb,t=mytable 
</span></span><span class="line"><span class="cl"> --dry-run</span></span></code></pre>
</div>
</div>
</div>
<p>A dry run allows you to validate the plan and review the process without making any actual changes&mdash;a critical safeguard when preparing for production schema work.</p>
<h4>Printing SQL Changes Before Execution</h4>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-11" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-online-schema-change 
</span></span><span class="line"><span class="cl"> --alter "ADD COLUMN updated_at TIMESTAMP NULL" 
</span></span><span class="line"><span class="cl"> D=crm,t=customers 
</span></span><span class="line"><span class="cl"> --print 
</span></span><span class="line"><span class="cl"> --execute</span></span></code></pre>
</div>
</div>
</div>
<p>Using &ndash;print provides transparency into the SQL operations the tool will perform. This is particularly useful during code reviews or change-control processes.</p>
<h2>PT Show Grants<a class="anchor-link" id="pt-show-grants"></a></h2>
<p>A Percona Toolkit utility that extracts MySQL user accounts and privileges and outputs them as clean, executable CREATE USER and GRANT statements. It normalizes and orders the privileges for readability, making it valuable for auditing security, documenting access, migrating users between servers, or preparing accurate privilege information for support and compliance purposes.</p>
<h3>Examples<a class="anchor-link" id="examples"></a></h3>
<h4>Dump All Grants for All Users</h4>
<p>The simplest and most common use case is generating a complete privilege snapshot:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-12" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-show-grants</span></span></code></pre>
</div>
</div>
</div>
<p>This returns normalized CREATE USER and GRANT statements for every account in the instance. It&rsquo;s ideal for audits, environment comparisons, and creating human-readable privilege reports.</p>
<h4>Show Grants for a Specific User</h4>
<p>If you want to inspect privileges for a single account, you can filter by user/host:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-13" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-show-grants --accounts='user@localhost'</span></span></code></pre>
</div>
</div>
</div>
<p>This makes privilege debugging and user-level audits quick and targeted.</p>
<h4>Export All Grants to a File</h4>
<p>To create a reusable backup of every user account:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-14" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-show-grants &gt; grants.sql</span></span></code></pre>
</div>
</div>
</div>
<p>The resulting file is a set of CREATE USER and GRANT statements that can be restored simply by executing:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-15" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">mysql &lt; grants.sql</span></span></code></pre>
</div>
</div>
</div>
<p>This is an excellent practice before server upgrades, user cleanup, or major permission changes.</p>
<h4>Show Grants for Multiple Accounts</h4>
<p>You can provide a comma-separated list of accounts to extract only what you need:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-16" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-show-grants --accounts='app@%,reporting@localhost,backup@localhost'</span></span></code></pre>
</div>
</div>
</div>
<p>This is ideal for teams that manage groups of service accounts across environments.</p>
<h4>Ignore Specific System Accounts</h4>
<p>For cleanup scripts or custom inventory reports, skip built-in MySQL accounts:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-17" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pt-show-grants --ignore='mysql.sys@localhost,mysql.infoschema@localhost'</span></span></code></pre>
</div>
</div>
</div>
<p>This focuses output on only the accounts relevant to your application.</p>
<h2>Summary<a class="anchor-link" id="summary"></a></h2>
<p>This post highlights the importance of using the right tool for the job&mdash;both in woodworking and in database engineering. For MySQL and Percona Server environments, the Percona Toolkit offers a set of powerful utilities that simplify diagnostics, troubleshooting, schema changes, and security audits.</p>
<p>It introduces four key tools:</p>
<ul>
<li>
<p>pt-summary &ndash; Generates a high-level report of system hardware, OS settings, filesystems, networking, and performance metrics. Useful for support cases and quick environment overviews.</p>
</li>
<li>
<p>pt-mysql-summary &ndash; Produces a structured snapshot of a MySQL instance, including configuration, performance counters, replication status, storage engine details, and important variables. Ideal for tuning and issue analysis.</p>
</li>
<li>
<p>pt-online-schema-change &ndash; Enables online ALTER TABLE operations by copying and syncing the table in the background, minimizing downtime. Several examples show how to add, drop, or modify columns and indexes safely.</p>
</li>
<li>
<p>pt-show-grants &ndash; Extracts all MySQL users and privileges into clean, reproducible CREATE USER and GRANT statements. Helpful for audits, migrations, backups, and security reviews.</p>
</li>
</ul>

<p><a href="https://percona.community/blog/2025/11/24/the-right-tool-for-the-job/">The Right Tool for the Job</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Cold Comfort: When PostgreSQL Protects Your Data by Locking You Out</title>
      <link rel="alternate" type="text/html" href="https://mysql-qa.blogspot.com/2025/11/cold-comfort-when-postgresql-protects.html" />
      <id>https://mysql-qa.blogspot.com/2025/11/cold-comfort-when-postgresql-protects.html</id>
      <updated>2025-11-20T20:25:00+02:00</updated>
      <author><name>jbm</name></author>
      <summary type="html"><![CDATA[<p>Cold Comfort: When PostgreSQL Protects Your Data by Locking You Out</p>
<p>PostgreSQL’s MVCC architecture is designed to preserve data integrity at all costs.</p>
<p>But when autovacuum falls behind, that protection can come at a steep operational price: your data is still there — but you can’t write to it, and in some cases, you can’t even access it.</p>
<p>Over the past three years, PostgreSQL has experienced multiple production-halting outages across major cloud providers and enterprise deployments.</p>
<p>The root cause in every case? Vacuum lag.</p>
<p>Three Confirmed Outages (2024–2025)</p>
<p>  AWS RDS PostgreSQL Wraparound Incident<br />
  Date: December 2024 (updated February 2025)<br />
  Impact: PostgreSQL entered read-only mode<br />
  Cause: Transaction ID wraparound protection triggered due to autovacuum lag<br />
  Recovery: Required emergency vacuuming and downtime<br />
  Source: AWS Blog</p>
<p>  Google Cloud SQL PostgreSQL Lockouts<br />
  Date: Throughout 2024<br />
  Impact: PostgreSQL refused new transactions to prevent wraparound data loss<br />
  Cause: Missed autovacuum cycles; stale replication slots and prepared transactions<br />
  Recovery: Required single-user mode vacuum and cleanup<br />
  Source: Google Cloud Docs</p>
<p>  Metronome Multixact Exhaustion<br />
  Date: May 2025<br />
  Impact: Four outages in one week; blocked write operations across API and UI<br />
  Cause: Multixact member exhaustion from long-lived transactions and insufficient vacuuming<br />
  Recovery: Required emergency tuning and freeze vacuuming<br />
  Source: Metronome Postmortem</p>
<p>Meanwhile: No Documented MariaDB Outages</p>
<p>In the same timeframe, MariaDB has had no confirmed production-halting incidents.</p>
<p>Its architecture avoids transaction ID wraparound and multixact exhaustion entirely. It does not rely on background vacuuming to maintain write availability.</p>
<p>Stewardship Risk Summary (2023–2025)</p>
<p>   System<br />
   Outages<br />
   Business Impact<br />
   Stewardship Risk</p>
<p>   PostgreSQL<br />
   3 confirmed<br />
   High<br />
   Requires freeze monitoring, vacuum tuning, emergency playbooks</p>
<p>   MariaDB<br />
   None<br />
   Low<br />
   Stable, no vacuum dependencies</p>
<p>MariaDB has had no documented outages in the past three years.</p>
<p>It doesn’t rely on autovacuum, doesn’t suffer from transaction ID wraparound, and doesn’t require freeze monitoring to stay writable.</p>
<p>MariaDB\'s architecture avoids the entire class of risks that have repeatedly halted PostgreSQL workloads.</p>
<p>PostgreSQL’s vacuum system is not optional — it’s existential.</p>
<p>In high-write environments, autovacuum can consume 20–40% of CPU and I/O.</p>
<p>When vacuuming falls behind, PostgreSQL will halt writes or enter read-only mode to prevent corruption.</p>
<p>If you’re running PostgreSQL at scale, you need:</p>
<p> Freeze-age monitoring<br />
 Autovacuum tuning<br />
 Emergency vacuum playbooks<br />
 Contributor-safe diagnostics and alerting</p>
<p> “Catastrophic data loss. (Actually the data is still there, but that\'s cold comfort if you cannot get at it.)”<br />
 — PostgreSQL Documentation</p>
<p> Tags:<br />
 #PostgreSQL #Autovacuum #DatabaseOutages #MVCC #WraparoundRisk #MariaDB #Benchmarking #DatabaseStewardship #FreezeMonitoring #CloudReliability #MariaDBFoundation #OpenSource #PerformanceTesting #ContributorDriven #MySQLCompatible #CloudDatabases #LegacyDriven</p>
<p><a href="https://mysql-qa.blogspot.com/2025/11/cold-comfort-when-postgresql-protects.html">Cold Comfort: When PostgreSQL Protects Your Data by Locking You Out</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Cold Comfort: When PostgreSQL Protects Your Data by Locking You Out<a class="anchor-link" id="cold-comfort-when-postgresql-protects-your-data-by-locking-you-out"></a></h2>
<p>PostgreSQL&rsquo;s MVCC architecture is designed to preserve data integrity at all costs.</p>
<p>But when autovacuum falls behind, that protection can come at a steep operational price: your data is still there &mdash; but you can&rsquo;t write to it, and in some cases, you can&rsquo;t even access it.</p>
<p>Over the past three years, PostgreSQL has experienced multiple production-halting outages across major cloud providers and enterprise deployments.</p>
<p><strong>The root cause in every case?</strong> Vacuum lag.</p>
<h3>Three Confirmed Outages (2024&ndash;2025)<a class="anchor-link" id="three-confirmed-outages-2024-2025"></a></h3>
<ol>
<li>
    <strong>AWS RDS PostgreSQL Wraparound Incident</strong><br>
    <strong>Date:</strong> December 2024 (updated February 2025)<br>
    <strong>Impact:</strong> PostgreSQL entered read-only mode<br>
    <strong>Cause:</strong> Transaction ID wraparound protection triggered due to autovacuum lag<br>
    <strong>Recovery:</strong> Required emergency vacuuming and downtime<br>
    <strong>Source:</strong> <a href="https://aws.amazon.com/blogs/database/prevent-transaction-id-wraparound-by-using-postgres_get_av_diag-for-monitoring-autovacuum" target="_blank">AWS Blog</a>
  </li>
<li>
    <strong>Google Cloud SQL PostgreSQL Lockouts</strong><br>
    <strong>Date:</strong> Throughout 2024<br>
    <strong>Impact:</strong> PostgreSQL refused new transactions to prevent wraparound data loss<br>
    <strong>Cause:</strong> Missed autovacuum cycles; stale replication slots and prepared transactions<br>
    <strong>Recovery:</strong> Required single-user mode vacuum and cleanup<br>
    <strong>Source:</strong> <a href="https://cloud.google.com/sql/docs/postgres/txid-wraparound" target="_blank">Google Cloud Docs</a>
  </li>
<li>
    <strong>Metronome Multixact Exhaustion</strong><br>
    <strong>Date:</strong> May 2025<br>
    <strong>Impact:</strong> Four outages in one week; blocked write operations across API and UI<br>
    <strong>Cause:</strong> Multixact member exhaustion from long-lived transactions and insufficient vacuuming<br>
    <strong>Recovery:</strong> Required emergency tuning and freeze vacuuming<br>
    <strong>Source:</strong> <a href="https://metronome.com/blog/multixact-exhaustion-postmortem" target="_blank">Metronome Postmortem</a>
  </li>
</ol>
<h3>Meanwhile: No Documented MariaDB Outages<a class="anchor-link" id="meanwhile-no-documented-mariadb-outages"></a></h3>
<p>In the same timeframe, MariaDB has had no confirmed production-halting incidents.</p>
<p>Its architecture avoids transaction ID wraparound and multixact exhaustion entirely. It does not rely on background vacuuming to maintain write availability.</p>
<h3>Stewardship Risk Summary (2023&ndash;2025)<a class="anchor-link" id="stewardship-risk-summary-2023-2025"></a></h3>
<table border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th>System</th>
<th>Outages</th>
<th>Business Impact</th>
<th>Stewardship Risk</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>PostgreSQL</strong></td>
<td>3 confirmed</td>
<td>High</td>
<td>Requires freeze monitoring, vacuum tuning, emergency playbooks</td>
</tr>
<tr>
<td><strong>MariaDB</strong></td>
<td>None</td>
<td>Low</td>
<td>Stable, no vacuum dependencies</td>
</tr>
</tbody>
</table>
<p>MariaDB has had no documented outages in the past three years.</p>
<p>It doesn&rsquo;t rely on autovacuum, doesn&rsquo;t suffer from transaction ID wraparound, and doesn&rsquo;t require freeze monitoring to stay writable.</p>
<p><strong>MariaDB&rsquo;s architecture avoids the entire class of risks that have repeatedly halted PostgreSQL workloads.</strong></p>
<p><em>PostgreSQL&rsquo;s vacuum system is not optional &mdash; it&rsquo;s existential.</em></p>
<p>In high-write environments, autovacuum can consume 20&ndash;40% of CPU and I/O.</p>
<p>When vacuuming falls behind, PostgreSQL will halt writes or enter read-only mode to prevent corruption.</p>
<h3>If you&rsquo;re running PostgreSQL at scale, you need:<a class="anchor-link" id="if-youre-running-postgresql-at-scale-you-need"></a></h3>
<ul>
<li>Freeze-age monitoring</li>
<li>Autovacuum tuning</li>
<li>Emergency vacuum playbooks</li>
<li>Contributor-safe diagnostics and alerting</li>
</ul>
<blockquote><p>
  &ldquo;Catastrophic data loss. (Actually the data is still there, but that&rsquo;s cold comfort if you cannot get at it.)&rdquo;<br>
  &mdash; <a href="https://www.postgresql.org/docs/current/routine-vacuuming.html" target="_blank">PostgreSQL Documentation</a>
</p></blockquote>
<p>
  <strong>Tags:</strong><br>
  #PostgreSQL #Autovacuum #DatabaseOutages #MVCC #WraparoundRisk #MariaDB #Benchmarking #DatabaseStewardship #FreezeMonitoring #CloudReliability #MariaDBFoundation #OpenSource #PerformanceTesting #ContributorDriven #MySQLCompatible #CloudDatabases #LegacyDriven</p>

<p><a href="https://mysql-qa.blogspot.com/2025/11/cold-comfort-when-postgresql-protects.html">Cold Comfort: When PostgreSQL Protects Your Data by Locking You Out</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Percona Operator for MySQL Is Now GA, More MySQL Options for the Community on Kubernetes</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/19/percona-operator-for-mysql-is-now-ga-more-mysql-options-for-the-community-on-kubernetes/" />
      <id>https://percona.community/blog/2025/11/19/percona-operator-for-mysql-is-now-ga-more-mysql-options-for-the-community-on-kubernetes/</id>
      <updated>2025-11-19T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We’re excited to share that the new Percona Operator for MySQL (based on Percona Server for MySQL) is officially in General Availability (GA)!</p>
<p><a href="https://percona.community/blog/2025/11/19/percona-operator-for-mysql-is-now-ga-more-mysql-options-for-the-community-on-kubernetes/">Percona Operator for MySQL Is Now GA, More MySQL Options for the Community on Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We&rsquo;re excited to share that the new <strong><a href="https://docs.percona.com/percona-operator-for-mysql/ps/index.html" target="_blank" rel="noopener noreferrer">Percona Operator for MySQL (based on Percona Server for MySQL)</a></strong> is officially in General Availability (GA)!</p>
<p>This release introduces native <strong>MySQL Group Replication</strong> support for <strong>Kubernetes</strong>, providing our community with another open-source option for running reliable, consistent MySQL clusters at scale.</p>
<p>This is about more choices for the community. Each MySQL replication technology addresses different real-world needs, and now you can choose the one that best fits your workloads.</p>
<p><figure><img decoding="async" width="536" height="640" src="https://percona.community/blog/2025/11/introm_hu_db44d0f2df34a48a.webp" alt="MySQL Operator for MySQL Intro" loading="lazy"></figure>
</p>
<h2>What This Means for the Community<a class="anchor-link" id="what-this-means-for-the-community"></a></h2>
<p>With this release, Percona now supports two <strong>fully open-source MySQL Operators</strong>:</p>
<h3>1. <a href="https://docs.percona.com/percona-operator-for-mysql/ps/index.html" target="_blank" rel="noopener noreferrer">Percona Operator for MySQL (Percona Server for MySQL)</a>, New and GA<a class="anchor-link" id="1-percona-operator-for-mysql-percona-server-for-mysql-new-and-ga"></a></h3>
<ul>
<li>Group Replication (synchronous)</li>
<li>Asynchronous replication (Technical Preview)</li>
<li>Native MySQL experience</li>
<li>Auto-failover</li>
<li>Kubernetes-native design</li>
</ul>
<h3>2. <a href="https://docs.percona.com/percona-operator-for-mysql/pxc/index.html" target="_blank" rel="noopener noreferrer">Percona XtraDB Cluster Operator (PXC)</a><a class="anchor-link" id="2-percona-xtradb-cluster-operator-pxc"></a></h3>
<ul>
<li>Galera-based synchronous replication</li>
<li>Strong high availability</li>
<li>Auto-failover</li>
<li>Battle-tested for mission-critical workloads</li>
</ul>
<p><strong>These Operators complement each other; they are not replacements</strong>. They give users the freedom to choose the right replication model for their business and technical priorities.</p>
<p><em>This GA release is a step in that direction, and we will continue publishing technical blog posts to explain when to use each Operator, how Group Replication works, and how this all fits into real-world Kubernetes environments</em>.</p>
<p><figure><img decoding="async" width="1790" height="1118" src="https://percona.community/blog/2025/11/two-operators_hu_48f0037e268e5adb.webp" alt="MySQL Operator for MySQL Intro Chart" loading="lazy"></figure>
</p>
<h2>Call for Community Testing and Feedback<a class="anchor-link" id="call-for-community-testing-and-feedback"></a></h2>
<p>Asynchronous replication is now available in Technical Preview, we invite you to:</p>
<ul>
<li>Test it in your clusters</li>
<li>Share your feedback</li>
<li>Open GitHub issues</li>
<li>Contribute docs or examples</li>
</ul>
<p>Your feedback will guide the next features we bring to the Operator.</p>
<h3>Explore Percona Operator for MySQL:<a class="anchor-link" id="explore-percona-operator-for-mysql"></a></h3>
<ul>
<li><a href="https://docs.percona.com/percona-operator-for-mysql/ps/ReleaseNotes/Kubernetes-Operator-for-PS-RN1.0.0.html" target="_blank" rel="noopener noreferrer">Docs Percona Operator for MySQL</a></li>
<li><a href="https://github.com/percona/percona-server-mysql-operator" target="_blank" rel="noopener noreferrer">GitHub: Try it, test it, open issues, or contribute</a></li>
<li><a href="https://www.linkedin.com/posts/percona_the-percona-cloud-native-team-is-happy-activity-7396585512536473600-bFZR/?utm_source=share&amp;utm_medium=member_ios&amp;rcm=ACoAAA_uTn0BQWSwnqQ-mUMcVZ7icaVGYa4mlVs" target="_blank" rel="noopener noreferrer">Announcement Percona Blog</a></li>
</ul>

<p><a href="https://percona.community/blog/2025/11/19/percona-operator-for-mysql-is-now-ga-more-mysql-options-for-the-community-on-kubernetes/">Percona Operator for MySQL Is Now GA, More MySQL Options for the Community on Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>OIDC in PostgreSQL: How It Works and Staying Secure</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/" />
      <id>https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/</id>
      <updated>2025-11-17T09:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>In the previous blog post about the topic, OAuth, OIDC and validators, we discussed basic terminologies to understand the differences between the protocols and how they relate to PostgreSQL.</p>
<p><a href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/">OIDC in PostgreSQL: How It Works and Staying Secure</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In the previous blog post about the topic, <a href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/">OAuth, OIDC and validators</a>, we discussed basic terminologies to understand the differences between the protocols and how they relate to PostgreSQL.</p>
<p>In this second part, we&rsquo;ll go one step further and see how OIDC works exactly in other software and in PostgreSQL, and what OAuthBearer is about. We also focus on the possible attacks and dangers in this flow with some examples to showcase why it&rsquo;s important to use a properly configured secure provider and to teach our users not to just skim through the authorization process.</p>
<h3>Can you keep a secret?<a class="anchor-link" id="can-you-keep-a-secret"></a></h3>
<p>Even the original OAuth RFC was designed to work in many different situations, and later extensions made it even more generic to support more use cases.<br>
However, it also has to acknowledge that different setups have different requirements and limitations and, because of that, might require a different authorization flow.</p>
<p>In one of our previous examples, we used applications such as CloudStorage or EditorApp.<br>
We&rsquo;ll continue to use them for a while for simplicity, but I want to emphasize that this example, even though it uses different names, is relevant and important for understanding how OAuth and OIDC work together with PostgreSQL.</p>
<ul>
<li>For CloudStorage, we can safely assume it&rsquo;s a traditional backend-frontend application, since it has to store the data somewhere on a backend.</li>
<li>For EditorApp, the answer can be different.<br>
Since it doesn&rsquo;t store photos directly but retrieves/saves them on CloudStorage, and also doesn&rsquo;t store users directly but retrieves them from the Provider using OIDC, it has no requirement for a backend.<br>
It is easily imaginable as a single page application, completely written in a modern JS framework, or even a downloadable version of it with Electron or a similar framework.</li>
</ul>
<p>But why is this distinction important for us?<br>
Because we actually have to authenticate and authorize multiple actors here, not just users:</p>
<ul>
<li>The applications, CloudStorage and EditorApp, have to trust the Provider to supply them with proper information about their users and their permissions.</li>
<li>The OAuth Provider has to identify that it&rsquo;s speaking to CloudStorage or EditorApp to tell them what exactly they can do with their users.</li>
</ul>
<p>OAuth or OIDC providers don&rsquo;t just register users &ndash; they also register client applications, and these applications receive either a randomly generated or user-specified identifier (&ldquo;username&rdquo;), depending on the Provider in question.<br>
When we talk about authentication, that usually involves multiple credentials, such as a username and a password, not just a username.<br>
The OAuth standard also recognizes that passwords are good practice and lets client applications use them. This is called a <code>client secret</code>.</p>
<p>But are all applications equal in this sense? Can all of them keep secrets?</p>
<p>The answer to this question is, unfortunately, no.<br>
In our example, CloudStorage has a backend &ndash; this means it can keep its secret on the backend, never sending it to the frontend.<br>
This is how we usually treat our passwords, and this is what OAuth calls a <strong>confidential client</strong>.</p>
<p>EditorApp is, however, in a different situation.<br>
It&rsquo;s an application purely implemented in HTML and JavaScript, without any backend code.<br>
Anybody can download its source, open it in a browser, and start using it.<br>
It&rsquo;s not capable of hiding its secret. Even if it has a client secret &ndash; which is optional in OAuth exactly because of this situation &ndash; a user with sufficient knowledge can extract this secret from its code.<br>
And this is true even if it&rsquo;s not a JavaScript application but rather a traditional desktop application written in a compiled language.<br>
As long as it doesn&rsquo;t have a backend, a part hidden from its users, there&rsquo;s no way to completely hide the secret.<br>
This is called a <strong>public client</strong> in OAuth.</p>
<h3>Are you trying to log in to application &ldquo;X&rdquo;?<a class="anchor-link" id="are-you-trying-to-log-in-to-application-x"></a></h3>
<p>So how can the Provider make sure it&rsquo;s talking to EditorApp and not to something else impersonating EditorApp?<br>
Without a secret, without the client being confidential, it can never be sure about this.</p>
<p>As long as we&rsquo;re talking about web applications, which usually have URLs, it can try to minimize the chance of impersonation.<br>
Providers can make sure that the redirects done during the authorization flow go to URLs configured by the client administrators / owners.<br>
But even this can be circumvented, and in the case of desktop applications, it&rsquo;s not usable.</p>
<p>This is why the Provider uses a different strategy.<br>
Since it can&rsquo;t validate for itself that the thing claiming to be EditorApp is really EditorApp or something else, it informs the user.<br>
When the user clicks on the &ldquo;Login with &rdquo; button, instead of silently authorizing the login, it first displays an information screen to the user.<br>
This is called the <code>consent screen</code>, and it has to display two things:</p>
<ul>
<li>That user  is trying to log in to application </li>
<li>That  is requesting permissions to read and write </li>
</ul>
<p>Along with this information, it has to ask the question:<br>
Is the above information correct? Is  really trying to log in to ? Is  okay with granting these permissions to ?</p>
<p>And only if the user,  agrees can the request proceed.</p>
<h3>Practical limitations<a class="anchor-link" id="practical-limitations"></a></h3>
<p>The above all sounds very nice and secure, but unfortunately we have to discuss some problems with it in practice.</p>
<p>Most importantly, users have a tendency to click &ldquo;next, next, next&rdquo; without properly reading, especially if they&rsquo;re completing the same login flow for the 100th time.<br>
Even if a Provider implements the RFCs perfectly in the most secure way possible, it doesn&rsquo;t help if the user just wants to get through the process quickly and doesn&rsquo;t check the details about the application name and permissions properly.</p>
<p>Another issue is that providers don&rsquo;t always properly follow the RFCs.<br>
For some providers, it&rsquo;s configurable &ndash; administrators can choose when the consent screen is displayed.<br>
They can choose to only display it once when the user first logs into an application, to display it every time the user logs in to the application, or to never display it.<br>
Some providers allow these configuration settings for the permissions, for the application name, or both.<br>
Some providers simply never display permissions and always just say something like &ldquo;be sure to trust this application &rdquo;.</p>
<p>Unfortunately, during our testing we found providers that in some cases completely skipped the consent screen, even when we tried to explicitly configure that it&rsquo;s always required. Be skeptical about the authorization flow used by the Provider, verify that it&rsquo;s secure enough, and don&rsquo;t hesitate to change providers or report bug reports to the developers if something isn&rsquo;t as it should be!</p>
<h3>Limited devices<a class="anchor-link" id="limited-devices"></a></h3>
<p>Everything we discussed above is nice, but PostgreSQL isn&rsquo;t a website.<br>
Something using it might be one, and in that case, everything we discussed above applies &ndash; but to go back to the only currently supported client, <code>psql</code>, that&rsquo;s a console application and can&rsquo;t display a graphical web browser for login.</p>
<p>OAuth also thought about similar clients &ndash; not exactly console applications, but primarily devices that can&rsquo;t display a browser and process a normal login flow to the Provider.<br>
These are called <strong>limited devices</strong> because the primary goal of this extension, <a href="https://www.rfc-editor.org/rfc/rfc8628.html" target="_blank" rel="noopener noreferrer">RFC 8628</a>, was to support specialized hardware where the user either can&rsquo;t or doesn&rsquo;t want to log in.</p>
<p>A typical example is a smart TV.<br>
While it can display a browser and a virtual keyboard, I wouldn&rsquo;t want to type my password and log in to a TV using that.<br>
But these devices can be even more limited. The only requirement for them is that they should be able to display a verification code and instruct the user where (URL) to enter that verification code on another device, which is capable and secure enough to handle the normal password login process or where the user is already logged in.</p>
<p>Other than this indirection, the login process is similar.<br>
The user sees the code, opens the device login webpage on another device, enters the code, and then receives a similar consent screen as before.<br>
This consent screen states that device/application  is trying to log in and requests permissions .<br>
The user then clicks the approve button, while in the background the device periodically checks for approval.<br>
Once approved, the authorization proceeds as normal on the limited device.</p>
<pre class="mermaid">
sequenceDiagram
participant Device as Limited Device<br>(psql)
participant Provider as OIDC Provider
participant Browser as User's Browser<br>(phone/laptop)
Device-&gt;&gt;Provider: Request device code
Provider-&gt;&gt;Device: Device code: ABC123<br>URL: provider.com/device
Note over Device: Display code and URL<br>to user
Browser-&gt;&gt;Provider: Navigate to provider.com/device
Provider-&gt;&gt;Browser: Show code entry form
Browser-&gt;&gt;Provider: Enter code: ABC123
Note over Provider: Verify code is valid
Provider-&gt;&gt;Browser: Show consent screen<br>"psql is requesting access"
Browser-&gt;&gt;Provider: User clicks "Approve"
loop Polling every few seconds
Device-&gt;&gt;Provider: Is code ABC123 approved?
Provider-&gt;&gt;Device: Not yet...
end
Device-&gt;&gt;Provider: Is code ABC123 approved?
Provider-&gt;&gt;Device: Yes! Here's your access token
Note over Device: Token received,<br>authentication complete
</pre>
<p>While <code>psql</code> isn&rsquo;t strictly a limited device, it&rsquo;s in a similar situation.<br>
It&rsquo;s possible that somebody is using it directly on a computer with a UI, with a web browser already logged in to the OIDC provider, but this is an unlikely scenario.<br>
More likely, the user has an SSH session open to another computer, running <code>psql</code> in it.<br>
Even if that remote computer has a graphical user interface and browser installed, displaying a login page on it wouldn&rsquo;t help &ndash; the user wouldn&rsquo;t see it.<br>
But it&rsquo;s much more likely that it&rsquo;s a console-only virtual machine / container somewhere, without a proper way to do a graphical login.<br>
In this sense, <code>psql</code> together with its environment is a limited device.</p>
<h3>Can we mix login processes?<a class="anchor-link" id="can-we-mix-login-processes"></a></h3>
<p>Before we talk about vulnerabilities and staying secure, it&rsquo;s important to clarify something.<br>
OAuth/OIDC supports many different authentication workflows, including but not limited to the possibilities listed above.<br>
Support for specific flows also varies between vendors &ndash; the supported features and nuances vary from Provider to Provider.</p>
<p>But if a vendor supports a specific flow, that doesn&rsquo;t mean it&rsquo;s automatically usable by all clients.<br>
Every provider we tested so far lets the administrators configure which authorization flows they want to support.<br>
If a specific application only works with confidential clients, the best configuration is to disable anything else.</p>
<p>The PostgreSQL wire protocol (and server) doesn&rsquo;t enforce the use of any specific flow, but a generic validator plugin also can&rsquo;t assume that the server uses some specific configuration.<br>
This isn&rsquo;t something the validator is capable of checking, as it gets executed on the server, after the authorization flow already concluded on the client side.</p>
<p>Strictly speaking, the server doesn&rsquo;t even know the ID of the client (application), only the issuer URL.</p>
<p>And if anyone wants to be able to use the <code>psql</code> command, that only supports the limited device flow.</p>
<h3>OAuthBearer<a class="anchor-link" id="oauthbearer"></a></h3>
<p>I already mentioned this in the previous blog post, and a few times in this one.<br>
It&rsquo;s important to understand that the PostgreSQL project has multiple different roles in these authorization flows.<br>
The client and the server/validator are two different actors, and the client can be any client, not just <code>libpq</code>.<br>
It can be either a well-known third-party implementation of the wire protocol or a completely custom implementation created by an attacker with malicious intent.</p>
<p>From a security standpoint, the server/validator can&rsquo;t trust the client or assume anything about the authentication process done by the client.</p>
<p>This design isn&rsquo;t unique to PostgreSQL.<br>
Support for OAuth over non-HTTP protocols, using the SASL mechanism (which was already supported by PostgreSQL previously), is called OAuthBearer, <a href="https://www.rfc-editor.org/rfc/rfc7628.html" target="_blank" rel="noopener noreferrer">RFC 7628</a>, and it&rsquo;s implemented by PostgreSQL similarly to how other software does it.</p>
<p>The idea of OAuthBearer is that the client using the non-HTTP service (in our case, PostgreSQL) creates an <code>access token</code> in some way.<br>
Then this client can use this access token to connect to PostgreSQL and possibly other services.<br>
Even if the client uses multiple services that all authenticate with OIDC, it only has to complete the OAuth flow once.</p>
<p>On the other side, the server doesn&rsquo;t have to do anything else with OAuth other than validating that the token it received is correct and valid.<br>
It doesn&rsquo;t have to deal with multiple flows. On the other hand, it can&rsquo;t assume anything about the flow.</p>
<h3>Can you please enter the code?<a class="anchor-link" id="can-you-please-enter-the-code"></a></h3>
<p>Why are the above details important, and why did I repeat the same description multiple times, worded slightly differently?</p>
<p>Because OIDC with public clients, even if implemented properly without mistakes, is vulnerable to the human factor.<br>
And in practice, most of the time, PostgreSQL and OIDC means using public clients.</p>
<p>There are multiple possible attack vectors that were previously used, and are still being used, to gain access to services, and there&rsquo;s no way to fully secure against them.<br>
Both are enabled by the fact that with OAuthBearer, we have no control over the authentication process &ndash; we have to trust that the access token sent to us by the client was indeed created by the user with the intent to log in to this server.</p>
<p>One very simple &ldquo;attack&rdquo; against the device authentication flow is to ask the user nicely.<br>
Since the two parts of the process &ndash; requesting and using the <code>device code</code>, and verifying the device code &ndash; can happen at two different locations in a completely valid setup, there&rsquo;s not much any software can do programmatically.</p>
<p>In this scenario, the attacker sends an email or calls the victim and comes up with some reason why the user has to go immediately to the device login page and enter the code.<br>
The reason is, of course, usually something completely unrelated, like &ldquo;this is the code to join the meeting,&rdquo; or &ldquo;go to the website and enter this code to verify your account,&rdquo; or anything similar.<br>
This is called <strong>device code phishing</strong>, and it&rsquo;s made worse by the fact that some providers don&rsquo;t display detailed enough consent screens during device code authentication, placing even careful users in vulnerable situations.</p>
<pre class="mermaid">
sequenceDiagram
participant Attacker
participant Provider as OIDC Provider
participant Victim as Victim's Browser
participant PG as PostgreSQL Database
Attacker-&gt;&gt;Provider: Request device code
Provider-&gt;&gt;Attacker: Code: XYZ789<br>URL: provider.com/device
Note over Attacker: Attacker now has<br>device code
Attacker-&gt;&gt;Victim: Email: "Enter code XYZ789<br>at provider.com/device<br>to verify your account"
Victim-&gt;&gt;Provider: Navigate to provider.com/device
Provider-&gt;&gt;Victim: Enter device code
Victim-&gt;&gt;Provider: Enter code: XYZ789
Note over Victim: User thinks they're<br>verifying their account
Provider-&gt;&gt;Victim: Show consent screen<br>"psql requesting database access"
Victim-&gt;&gt;Provider: Click "Approve"<br>(without reading carefully)
Provider-&gt;&gt;Attacker: Access token granted!
Note over Attacker,PG: Attacker now has valid token
Attacker-&gt;&gt;PG: Connect with stolen token
PG-&gt;&gt;Attacker: Connection successful
Note over Attacker,PG: Attacker has full<br>database access
</pre>
<p>Unfortunately, this can&rsquo;t be prevented as long as device code flow is enabled.<br>
It can be mitigated to some degree by educating users and making sure that the consent screen is always displayed and is very clear about the request details.</p>
<h3>Client ID spoofing<a class="anchor-link" id="client-id-spoofing"></a></h3>
<p>Another easy OAuth/OIDC attack vector relies on the fact that we&rsquo;re using a public client &ndash; meaning that either there isn&rsquo;t a client secret, or even if there is one, it&rsquo;s not really a secret.<br>
It&rsquo;s easy to create a legitimate-looking website that uses OIDC for something else and starts an authorization flow&hellip;<br>
but in the background, it uses the same client credentials as the PostgreSQL client.</p>
<p>This again relies on either a non-securely configured consent screen or a careless user who doesn&rsquo;t read the details about the request.<br>
And compared to the previous example, this doesn&rsquo;t require the device flow &ndash; it can work with other flows too.<br>
In the world of LLMs, it&rsquo;s very easy to create a valid-looking website tailor-made just for this purpose.</p>
<pre class="mermaid">
sequenceDiagram
participant Victim as User
participant Fake as Fake Website<br>"Photo Gallery"
participant Provider as OIDC Provider
participant PG as PostgreSQL Database
Victim-&gt;&gt;Fake: Visit fake-photo-gallery.com
Fake-&gt;&gt;Victim: "Login with your account"
Note over Fake: Uses REAL psql client ID!<br>(public, can't be hidden)
Victim-&gt;&gt;Fake: Click "Login"
Fake-&gt;&gt;Provider: OAuth request with<br>psql client ID
Provider-&gt;&gt;Victim: Show consent screen<br>"psql requesting access"
Note over Victim: User thinks:<br>"I'm logging into<br>Photo Gallery"
Victim-&gt;&gt;Provider: Click "Approve"
Provider-&gt;&gt;Fake: Access token
Note over Fake: Fake site now has<br>database token!
Fake-&gt;&gt;PG: Connect with token
PG-&gt;&gt;Fake: Connection successful
Note over Fake,PG: Attacker has database access
</pre>
<p>Similarly to the previous example, there&rsquo;s not much we can do to prevent this in the plugin.<br>
OAuth has extensions that aim to make the process more secure, such as <a href="https://www.rfc-editor.org/rfc/rfc7636.html" target="_blank" rel="noopener noreferrer">PKCE (RFC 7636)</a> or <a href="https://www.rfc-editor.org/rfc/rfc9449.html" target="_blank" rel="noopener noreferrer">DPoP (RFC 9449)</a>, preventing specific situations, but none of those help with OAuthBearer.<br>
As the entire authorization process happens on the client side, the validator on the server can&rsquo;t do anything but assume that if the access token is valid, then the user created it intentionally.</p>
<h3>Educate your users!<a class="anchor-link" id="educate-your-users"></a></h3>
<p>Once more I&rsquo;d like to emphasize that while we can&rsquo;t prevent these attack vectors completely, it is possible to minimize the risk by teaching, both your users and administrators.</p>
<p>Teach your users to:</p>
<ul>
<li>Never enter device codes unless you initiated the login process yourself</li>
<li>Always read the consent screen carefully, even if you&rsquo;ve seen it before</li>
<li>Verify the application name matches what you&rsquo;re trying to access</li>
<li>Be suspicious of unexpected emails or messages asking you to enter codes</li>
<li>When in doubt, don&rsquo;t approve &ndash; contact your administrator instead</li>
</ul>
<p>And your administrators to:</p>
<ul>
<li>Understand that OIDC Provider selection and configuration isn&rsquo;t just a checkbox item</li>
<li>If a provider can&rsquo;t be configured in a secure way, it presents a security vulnerability to any client using it</li>
<li>Always enable and require consent screens for public client flows, for every login, not just for registration</li>
<li>Make sure the consent screen clearly displays the application name and requested permissions</li>
</ul>
<p>These attacks aren&rsquo;t theoretical &ndash; they&rsquo;re actively used in the wild.<br>
The combination of OAuthBearer&rsquo;s trust model and the inherent limitations of public clients means that proper provider configuration and user awareness are your primary defenses.</p>
<h3>Next steps<a class="anchor-link" id="next-steps"></a></h3>
<p>Now that we understand how OIDC works with PostgreSQL and the security considerations involved, it&rsquo;s time to see this in practice.</p>
<p>In the next blog post, we&rsquo;ll walk through setting up Keycloak with PostgreSQL from scratch.<br>
We&rsquo;ll cover both getting Keycloak running and configuring it securely for PostgreSQL authentication, even if you&rsquo;ve never worked with Keycloak before.<br>
You&rsquo;ll see exactly how to configure the settings we discussed here and how to test that everything works correctly.</p>
<p>Stay tuned for practical, hands-on setup instructions!</p>

<p><a href="https://percona.community/blog/2025/11/17/oidc-in-postgresql-how-it-works-and-staying-secure/">OIDC in PostgreSQL: How It Works and Staying Secure</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>PGScorecard &#8211; PostgreSQL Compatibility Index</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/13/pgscorecard-postgresql-compatibility-index/" />
      <id>https://percona.community/blog/2025/11/13/pgscorecard-postgresql-compatibility-index/</id>
      <updated>2025-11-13T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>We’re excited to share that our recent test run using the Postgres Compatibility Index (PCI) achieved 100% compatibility.</p>
<p><a href="https://percona.community/blog/2025/11/13/pgscorecard-postgresql-compatibility-index/">PGScorecard &#8211; PostgreSQL Compatibility Index</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>We&rsquo;re excited to share that our recent test run using the <a href="https://github.com/secp256k1-sha256/postgres-compatibility-index/blob/main/readme.md" target="_blank" rel="noopener noreferrer">Postgres Compatibility Index (PCI)</a> achieved 100% compatibility.</p>
<p>The PCI was created to bring clarity to the often used but loosely defined term &ldquo;PostgreSQL compatible.&rdquo; As Mayur explains in his article <a href="https://drunkdba.medium.com/the-making-of-postgres-is-5034c0dc4639" target="_blank" rel="noopener noreferrer">The Making of &lsquo;Postgres Is&rsquo;</a>, the goal is simple: to ensure that when a system claims to be compatible with PostgreSQL, it truly behaves like upstream PostgreSQL in practice. The PCI accomplishes this by running a comprehensive set of tests across features like data types, procedural functions, constraints, extensions, and more, and producing a measurable, transparent score. This gives users and vendors a reliable benchmark rather than relying on marketing claims.</p>
<p>Compatibility matters because many organizations rely on PostgreSQL variants, repackaged distributions, or vendor supported systems. They want the confidence that their schemas, tools, extensions, ORMs, client libraries, and workflows will continue to work as expected. A system that drifts from upstream PostgreSQL can introduce subtle risks such as, unsupported features, migration challenges, and vendor lock-in.</p>
<p>Achieving a perfect PCI score means our system supports the full baseline feature set as currently defined. It demonstrates that users can rely on the same behavior as community PostgreSQL, whether they are self-hosting, using a vendor-supported version, or integrating with existing tools. Importantly, it also shows that you can have a fully open-source system while still benefiting from vendor support, without compromising compatibility.</p>
<p>In a world where &ldquo;PostgreSQL compatible&rdquo; is often a vague claim, initiatives like the PCI provide the needed help with transparency and comparability. It helps to protect you from marketing claims in the PostgreSQL ecosystem, ensuring that tooling, and workflows continue to function reliably.</p>
<p>Special thanks to Mayur, whose initiative is helping define a clear, standardized framework for PostgreSQL compatibility.</p>
<blockquote>
<p>The test run was done against <a href="https://docs.percona.com/postgresql/17/index.html" target="_blank" rel="noopener noreferrer">Percona Server for PostgreSQL 17.6.1</a>. <a href="https://github.com/secp256k1-sha256/postgres-compatibility-index/blob/main/postgres-compatibility-index/outputs/Percona.json" target="_blank" rel="noopener noreferrer">Click for test result</a>. Percona Server for PostgreSQL is a binary-compatible, open source drop-in replacement for PostgreSQL with the currently needed enhancements, to make <a href="https://docs.percona.com/pg-tde/index.html" target="_blank" rel="noopener noreferrer">Transparent Data Encryption (TDE)</a> work.</p>
</blockquote>

<p><a href="https://percona.community/blog/2025/11/13/pgscorecard-postgresql-compatibility-index/">PGScorecard &#8211; PostgreSQL Compatibility Index</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MariaDB 12 Triggers</title>
      <link rel="alternate" type="text/html" href="https://ocelot.ca/blog/blog/2025/11/11/mariadb-12-triggers/" />
      <id>https://ocelot.ca/blog/blog/2025/11/11/mariadb-12-triggers/</id>
      <updated>2025-11-11T13:51:23+02:00</updated>
      <author><name>pgulutzan</name></author>
      <summary type="html"><![CDATA[<p>There’s new SQL syntax in MariaDB 12. MariaDB’s manual doesn’t document it all, so I will try. First in this series is: the Oracle-style CREATE TRIGGER … event with OR, and the standard-style table information_schema.triggered_update_columns. event with OR CREATE TRIGGER trigger_name BEFORE&#124;AFTER INSERT&#124;UPDATE [OF column-list]&#124;DELETE [OR INSERT&#124;UPDATE [OF column-list]&#124;DELETE ...] ON table_name FOR EACH ROW… Continue Reading MariaDB 12 Triggers</p>
<p><a href="https://ocelot.ca/blog/blog/2025/11/11/mariadb-12-triggers/">MariaDB 12 Triggers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>There&rsquo;s new SQL syntax in MariaDB 12. MariaDB&rsquo;s manual doesn&rsquo;t document it all, so I will try. First in this series is: the Oracle-style CREATE TRIGGER &hellip; event with OR, and the standard-style table information_schema.triggered_update_columns.</p>
<h2 class="wp-block-heading">event with OR<a class="anchor-link" id="event-with-or"></a></h2>
<pre class="wp-block-preformatted">CREATE TRIGGER trigger_name
BEFORE|AFTER
INSERT|UPDATE [OF column-list]|DELETE
[OR INSERT|UPDATE [OF column-list]|DELETE ...]
ON table_name FOR EACH ROW statement_text;</pre>
<p>The OR is new. At the time I&rsquo;m writing this it&rsquo;s not yet in <a href="https://mariadb.com/docs/server/server-usage/triggers-events/triggers/create-trigger">the MariaDB manual</a> but the <a href="https://jira.mariadb.org/browse/MDEV-10164">feature request</a> for it is closed, MariaDB 12.0+ supports it.</p>
<p><a href="https://www.ibm.com/docs/en/db2/11.5.x?topic=statements-create-trigger">DB2</a> and <a href="https://learn.microsoft.com/en-us/sql/t-sql/statements/create-trigger-transact-sql?view=sql-server-ver17">SQL Server</a> and <a href="https://www.postgresql.org/docs/current/sql-createtrigger.html">PostgreSQL</a> support something similar, but what I think is important for MariaDB is that <a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/lnpls/CREATE-TRIGGER-statement.html#LNPLS2183">Oracle</a> supports it. (The feature request is part of MariaDB&rsquo;s &ldquo;Oracle compatibility project&rdquo;.)</p>
<p>So BEFORE|AFTER INSERT OR UPDATE etc. is a non-standard extension that increases Oracle compatibility although sql_mode=&rsquo;oracle&rsquo; is not required.</p>
<p>Gripe: the feature request has the term &ldquo;multiple events&rdquo; but that&rsquo;s wrong, INSERT OR UPDATE is only one trigger event. I&rsquo;m calling it &ldquo;event with OR&rdquo; but there&rsquo;s no good standard term.</p>
<p>Examples:</p>
<pre class="wp-block-preformatted">CREATE TRIGGER t BEFORE INSERT OR UPDATE OR DELETE ON t
                 FOR EACH ROW SET @a = @a + 1;
CREATE TRIGGER t AFTER UPDATE OF s1 OR INSERT ON t
                 FOR EACH ROW SET @a = @a + 1;</pre>
<p>The effect is obvious: if the event happens, the trigger statement should happen.</p>
<p>The advantage is obvious: you don&rsquo;t need to create nearly-duplicate triggers when the table and the statement are the same and the only difference is the event. If there are many triggers, the maintenance &mdash; and the understanding of which ones get activated before other ones &mdash; could become confusing.</p>
<p>Aside: Oracle takes this reduce-trigger-numbers idea to extremes by also supporting <a href="https://asktom.oracle.com/ords/asktom.search?tag=compound-triggers">compound triggers</a>.</p>
<h2 class="wp-block-heading">event with OR, if inserting|updating|deleting<a class="anchor-link" id="event-with-or-if-insertingupdatingdeleting"></a></h2>
<p>The trigger&rsquo;s statement may contain the words INSERTING or UPDATING or DELETING. These words are &ldquo;conditional predicates&rdquo;, that is, they appear whenever a true|false decision may appear, such as in IF INSERTING, CASE WHEN UPDATING, WHILE DELETING. A conditional predicate is true only for the relevant part of a trigger event, for example if the trigger event is INSERT OR DELETE and the statement is INSERT then INSERTING is true, DELETING is false, UPDATING is illegal syntax.</p>
<p>Example:</p>
<pre class="wp-block-preformatted">CREATE TRIGGER tm
BEFORE INSERT OR UPDATE OF s1 OR DELETE ON t
FOR EACH ROW
BEGIN
  CASE WHEN INSERTING THEN SET @a=0;
  ELSE SET @a = 1; END CASE;
  IF UPDATING OR INSERTING THEN
  SET @a = 2; END IF;
  WHILE NOT DELETING AND @a  3 DO
    SET @a = 3;
  END WHILE;
END;</pre>
<p>MariaDB does not support the Oracle-style <a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/plsql-triggers.html#GUID-217E8B13-29EF-45F3-8D0F-2384F9F1D231">conditional predicate</a> UPDATING(&lsquo;column_name&rsquo;).</p>
<p>MariaDB does not support using a conditional predicate as an ordinary operand. For example, &ldquo;SET declared_variable_name = INSERTING;&rdquo; is legal but the result is zero.</p>
<p>Do not assume that UPDATING cannot be legal for an INSERT or DELETE statement, because MariaDB supports INSERT &hellip; ON DUPLICATE KEY UPDATE and supports foreign keys with ON DELETE SET NULL.</p>
<p>There is a new error ER_INCOMPATIBLE_EVENT_FLAG which pops up if a conditional predicate doesn&rsquo;t correspond to anything in the trigger event. For example,</p>
<pre class="wp-block-preformatted">CREATE TRIGGER tx BEFORE UPDATE OR INSERT ON t
                  FOR EACH ROW
                  IF DELETING THEN SET @a = 0; END IF;</pre>
<p>causes Error 4211 (HY000) Event flag &lsquo;DELETING&rsquo; in the condition expression is not compatible with the trigger event type &lsquo;INSERT,UPDATE&rsquo;.</p>
<p>Gripe: I have no idea why this is called an event flag, or why treat DELETING as an error rather than treat it as false, or why the term isn&rsquo;t just &ldquo;trigger event&rdquo;, or why the SQLSTATE class is HY for something that&rsquo;s being treated as a syntax error. I don&rsquo;t see this restriction mentioned in Oracle&rsquo;s <a href="https://docs.oracle.com/cd/E24693_01/appdev.11203/e17126/triggers.htm#autoId3">description of conditional predicates</a>.</p>
<h2 class="wp-block-heading">event with OR, recommendations<a class="anchor-link" id="event-with-or-recommendations"></a></h2>
<p>&ldquo;UPDATE OF column1 OR UPDATE OF column2&rdquo; is unnecessary &mdash; &ldquo;UPDATE OF column1, column2&rdquo; does the same thing and is compatible with MariaDB 11.</p>
<p>The clause order doesn&rsquo;t have to be INSERT before UPDATE before DELETE, but there might as well be some convention. Choose whatever the MariaDB manual uses for an illustration when the MariaDB manual describes this feature.</p>
<p>If you care about compatibility with the standard or with MySQL or with earlier versions of MariaDB, instead of caring about compatibility with Oracle, you should continue to use nearly-duplicate triggers.</p>
<p>MariaDB supports duplication such as INSERT OR INSERT, which it will ignore, so it&rsquo;s your responsibility to avoid such an error.</p>
<p>INSERTING and UPDATING and DELETING are not reserved words. Therefore if the trigger body contains a declared-variable declaration like</p>
<pre class="wp-block-preformatted">DECLARE INSERTING INT;</pre>
<p>then later IF INSERTING will be true or false depending on the variable value, INSERTING is not a conditional predicate in this context. So check that you never declared such variables.</p>
<p>Beware of these bugs:<br><a href="https://jira.mariadb.org/browse/MDEV-37711">MDEV-37711 Multiple-event triggers may fire in wrong order</a><br><a href="https://jira.mariadb.org/browse/MDEV-38009">MDEV-38009 Flaws with CREATE TRIGGER with ORed events including UPDATE OF</a>.</p>
<h2 class="wp-block-heading">event with OR in information_schema<a class="anchor-link" id="event-with-or-in-information_schema"></a></h2>
<p>After you say CREATE TRIGGER &hellip; event with OR, you can see it in information_schema:</p>
<pre class="wp-block-preformatted">SELECT event_object_table, event_manipulation
FROM information_schema.triggers;</pre>
<figure class="wp-block-image size-full"><a href="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger1.png"><img loading="lazy" decoding="async" width="538" height="134" src="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger1.png" alt="" class="wp-image-1066"></a></figure>
<p>The event_manipulation column can now be a comma-separated list such as INSERT,UPDATE,DELETE &mdash; not necessarily in the same order that you used in the CREATE TRIGGER statement.</p>
<p>Gripe: Why comma-delimited? The separator in the CREATE TRIGGER statement was &ldquo;OR&rdquo; not &ldquo;,&rdquo; and I think this isn&rsquo;t Oracle-like, the examples I&rsquo;ve seen for</p>
<pre class="wp-block-preformatted">SELECT triggering_event FROM all_triggers</pre>
<p>look like &lsquo;INSERT OR UPDATE&rsquo; not &lsquo;INSERT,UPDATE&rsquo;. e.g. the &ldquo;Sample result&rdquo; on this <a href="https://dataedo.com/kb/query/oracle/list-triggers#:~:text=triggering_event%20-%20event%20that%20fires%20the,webinars%20straight%20to%20your%20inbox">Dataedo page</a> and the &ldquo;Creating a sample trigger:&rdquo; results on this <a href="https://stackoverflow.com/questions/55848099/how-to-show-stored-trigger-in-sql#:~:text=context%20in%20comments.-,Comments,link%20CC%20BY-SA%204.0">stackoverflow page</a>.</p>
<p>Anyway, whether or not you like it, you must change any of your existing SQL code that has anything like</p>
<pre class="wp-block-preformatted">WHERE event_manipulation = 'INSERT'</pre>
<p>to</p>
<pre class="wp-block-preformatted">WHERE event_manipulation LIKE '%INSERT%'</pre>
<p>This is good enough because even if the event was &ldquo;UPDATE OF inserted_column&rdquo; that won&rsquo;t cause a false positive, for a reason that I&rsquo;ll explain in the next section.</p>
<h2 class="wp-block-heading">information_schema.triggered_update_columns<a class="anchor-link" id="information_schema-triggered_update_columns"></a></h2>
<p>Unlike event with OR, this new feature is documented and standard. The information that&rsquo;s missing in information_schema.triggers.event_manipulation is: what column? Even if you say UPDATE OF, the column name won&rsquo;t be there because the standard says the only possible values are &lsquo;INSERT&rsquo; | &lsquo;UPDATE&rsquo; | &lsquo;DELETE&rsquo;. Luckily you don&rsquo;t often need to know, but it could happen.</p>
<p>You can get something by looking at the SHOW of each trigger. For example (here I use an ocelotgui feature to make it look simple) (but it&rsquo;s not simple):</p>
<pre class="wp-block-preformatted">SELECT `SQL Original Statement`
 FROM (SHOW CREATE TRIGGER t) AS shower
 WHERE `SQL Original Statement`
       regexp '.*\s*before|after\s.*update.*of.\ss2\s|,*\son\.*'
       = 1;
although this fails if there are newlines or the important words are inside comments or strings. Repeat for every trigger.</pre>
<figure class="wp-block-image size-large"><a href="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger2-2.png"><img decoding="async" loading="lazy" width="1024" height="162" src="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger2-2-1024x162.png" alt="" class="wp-image-1069"></a></figure>
<p>So, enter triggered_update_columns. The column names are listed in the <a href="https://mariadb.com/docs/server/reference/system-tables/information-schema/information-schema-tables/information-schema-triggered_update_columns">MariaDB 12.2 manual</a>.</p>
<p>Gripe: Why triggered? The update is &ldquo;triggering&rdquo; not &ldquo;triggered&rdquo;. I claimed this name is silly until I Peter Gulutzan the standards expert got a polite reminder from Sergei Golubchik the MariaDB expert that that&rsquo;s the name in the standard. Oops.</p>
<p>So now, if I want to see what columns are in CREATE TRIGGER UPDATE OF clauses, I SELECT event_object_column FROM information_schema.triggered_update_columns. </p>
<figure class="wp-block-image size-full"><a href="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger3.png"><img decoding="async" loading="lazy" width="830" height="199" src="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger3.png" alt="" class="wp-image-1070"></a></figure>
<p>Big improvement.</p>
<p>The standard says the criterion for triggered_update_columns is: &ldquo;Identify the columns in this catalog that are identified by the explicit UPDATE trigger event columns of a trigger defined in this catalog that are accessible to a given user or role.&rdquo; A column is explicit if it&rsquo;s mentioned in the trigger event. If the trigger was made with</p>
<pre class="wp-block-preformatted">CREATE TRIGGER ... BEFORE|AFTER UPDATE OF s1 ON t ...</pre>
<p>then s1 is explicit, so it gets a row in triggered_update_columns. If the trigger was made with</p>
<pre class="wp-block-preformatted">CREATE TRIGGER ... BEFORE|AFTER UPDATE ON t ...</pre>
<p>then all columns in t are implicit, so do not get rows in triggered_update_columns although of course any changes to them cause trigger activation.</p>
<h2 class="wp-block-heading">information_schema.triggered_update_columns example<a class="anchor-link" id="information_schema-triggered_update_columns-example"></a></h2>
<p>You can look for either explicit or implicit columns with left joins.</p>
<p>Suppose I want to see all the triggers on table t1 column s2. To make it simple I assume there is only one database. This will do it:</p>
<pre class="wp-block-preformatted">SELECT a.trigger_name,
       a.event_manipulation,
       a.event_object_table,
       b.event_object_table,
       b.event_object_column
 FROM information_schema.triggers a
LEFT JOIN information_schema.triggered_update_columns b
ON a.trigger_name = b.trigger_name
WHERE a.event_manipulation LIKE '%UPDATE%'
AND a.event_object_table = 't1'
AND b.event_object_column = 's2'
    OR b.event_object_column IS NULL;
Suppose I get two rows:</pre>
<figure class="wp-block-image size-large"><a href="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger4.png"><img decoding="async" loading="lazy" width="1024" height="246" src="https://ocelot.ca/blog/wp-content/uploads/2025/11/trigger4-1024x246.png" alt="" class="wp-image-1071"></a></figure>
<p><a href="https://ocelot.ca/blog/blog/2025/11/11/mariadb-12-triggers/trigger4.png"></a><br>The first row is a match because there&rsquo;s a TRIGGERED_UPDATE_COLUMNS row with &lsquo;s2&rsquo; (obviously I must have earlier said CREATE TRIGGER t1u BEFORE|AFTER UPDATE OF s2 ON t1 &hellip;). The second row is a match because there&rsquo;s no TRIGGERED_UPDATE_COLUMNS row so I see NULL (obviously I must have earlier said CREATE TRIGGER t1t BEFORE|AFTER INSERT OR UPDATE ON t1 &hellip;) &mdash; thus I know that all columns are affected.</p>
<h2 class="wp-block-heading">ocelotgui 2.6<a class="anchor-link" id="ocelotgui-2-6"></a></h2>
<p>Thinking about incompatibility with MariaDB 11 made me think about this: That is, while you&rsquo;re typing, the GUI shows the possible choices for the next word as a pulldown menu. I suppose every GUI has that. But I don&rsquo;t think every GUI has a tooltip saying what MySQL or MariaDB version first supported that word in that place. This is a tentative feature, it&rsquo;s in the source code but not in the executables of the recently-released ocelotgui 2.6. The same is true for other features described above. As always download of the source code or the release is possible from <a href="https://github.com/ocelot-inc/ocelotgui">github</a>.</p>

<p><a href="https://ocelot.ca/blog/blog/2025/11/11/mariadb-12-triggers/">MariaDB 12 Triggers</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>MySQL Memory Usage: A Guide to Optimization</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/11/mysql-memory-usage-a-guide-to-optimization/" />
      <id>https://percona.community/blog/2025/11/11/mysql-memory-usage-a-guide-to-optimization/</id>
      <updated>2025-11-11T00:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Struggling with MySQL memory spikes? Knowing how and where memory is allocated can make all the difference in maintaining a fast, reliable database. From global buffers to session-specific allocations, understanding the details of MySQL’s memory management can help you optimize performance and avoid slowdowns. Let’s explore the core elements of MySQL memory usage with best practices for trimming excess in demanding environments.</p>
<p><a href="https://percona.community/blog/2025/11/11/mysql-memory-usage-a-guide-to-optimization/">MySQL Memory Usage: A Guide to Optimization</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Struggling with MySQL memory spikes? Knowing how and where memory is allocated can make all the difference in maintaining a fast, reliable database. From global buffers to session-specific allocations, understanding the details of MySQL&rsquo;s memory management can help you optimize performance and avoid slowdowns. Let&rsquo;s explore the core elements of MySQL memory usage with best practices for trimming excess in demanding environments.</p>
<p><figure><img decoding="async" width="1680" height="593" src="https://percona.community/blog/2025/11/mysql_memory_usage_graph_hu_ee0b33309465bcba.webp" alt="Releem Dashboard - RAM usage" loading="lazy"></figure>
</p>
<h2>How MySQL Uses Memory<a class="anchor-link" id="how-mysql-uses-memory"></a></h2>
<p>MySQL dynamically manages memory across several areas to process queries, handle connections, and optimize performance. The two primary areas of memory usage include:</p>
<h3>Global Buffers<a class="anchor-link" id="global-buffers"></a></h3>
<p>These are shared by the entire MySQL server and include components like the InnoDB buffer pool, key buffer, and query cache. The InnoDB buffer pool is particularly memory-intensive, especially in data-heavy applications, as it stores frequently accessed data and indexes to speed up queries.</p>
<h3>Connection (per thread) Buffers<a class="anchor-link" id="connection-per-thread-buffers"></a></h3>
<p>When a client connects, MySQL allocates memory specifically for that session. This includes sort buffers, join buffers, and temporary table memory. The more concurrent connections you have, the more memory is consumed. Session buffers are critical to monitor in high-traffic environments.</p>
<h2>Why MySQL Memory Usage Might Surge<a class="anchor-link" id="why-mysql-memory-usage-might-surge"></a></h2>
<p>Memory spikes in MySQL often result from specific scenarios or misconfigurations. Here are a few examples:</p>
<ul>
<li><strong>High Traffic with Large Connection Buffers</strong>: A surge in concurrent connections can quickly exhaust memory if sort or join buffers are set too large.</li>
<li><strong>Complex Queries</strong>: Queries with large joins, subqueries, or extensive temporary table usage can temporarily allocate significant memory, especially when poorly optimized.</li>
<li><strong>Oversized InnoDB Buffer Pool</strong> : Setting the <a href="https://releem.com/docs/mysql-performance-tuning/innodb_buffer_pool_size" target="_blank" rel="noopener noreferrer">InnoDB buffer pool size</a> too large for the server&rsquo;s available memory can trigger swapping, severely degrading database and server performance.</li>
<li><strong>Large Temporary Tables</strong> : When temporary tables exceed the in-memory limit ( <a href="https://releem.com/docs/mysql-performance-tuning/tmp_table_size" target="_blank" rel="noopener noreferrer">tmp_table_size</a> ), they are written to disk, consuming additional resources and slowing down operations.</li>
<li><strong>Inefficient Indexing</strong> : A lack of proper indexes forces MySQL to perform full table scans, increasing memory and CPU usage for even moderately complex queries.</li>
</ul>
<h2>Best Practices for Controlling MySQL Memory Usage<a class="anchor-link" id="best-practices-for-controlling-mysql-memory-usage"></a></h2>
<p>When you notice MySQL using more memory than expected, consider the following strategies:</p>
<h3>1. Set Limits on Global Buffers<a class="anchor-link" id="1-set-limits-on-global-buffers"></a></h3>
<ul>
<li>Configure <a href="https://releem.com/docs/mysql-performance-tuning/innodb_buffer_pool_size" target="_blank" rel="noopener noreferrer">innodb_buffer_pool_size</a> to 60-70% of available memory for InnoDB-heavy workloads. For smaller workloads, scale it down to avoid overcommitting memory.</li>
<li>Keep <a href="https://releem.com/docs/mysql-performance-tuning/innodb_log_buffer_size" target="_blank" rel="noopener noreferrer">innodb_log_buffer_size</a> at a practical level (e.g., 16MB) unless write-heavy workloads demand more.</li>
<li>Adjust <a href="https://releem.com/docs/mysql-performance-tuning/key_buffer_size" target="_blank" rel="noopener noreferrer">key_buffer_size</a> for MyISAM tables, ensuring it remains proportionate to table usage to avoid unnecessary memory allocation.</li>
</ul>
<h3>2. Adjust Connection Buffer Sizes<a class="anchor-link" id="2-adjust-connection-buffer-sizes"></a></h3>
<ul>
<li>Reduce <a href="https://releem.com/docs/mysql-performance-tuning/sort_buffer_size" target="_blank" rel="noopener noreferrer">sort_buffer_size</a> and <a href="https://releem.com/docs/mysql-performance-tuning/join_buffer_size" target="_blank" rel="noopener noreferrer">join_buffer_size</a> to balance memory usage with query performance, especially in environments with high concurrency.</li>
<li>Optimize <a href="https://releem.com/docs/mysql-performance-tuning/tmp_table_size" target="_blank" rel="noopener noreferrer">tmp_table_size</a> and <a href="https://releem.com/docs/mysql-performance-tuning/max_heap_table_size" target="_blank" rel="noopener noreferrer">max_heap_table_size</a> to control in-memory temporary table allocation and avoid excessive disk usage.</li>
</ul>
<h3>3. Fine-Tune Table Caches<a class="anchor-link" id="3-fine-tune-table-caches"></a></h3>
<ul>
<li>Adjust <a href="https://releem.com/docs/mysql-performance-tuning/table_open_cache" target="_blank" rel="noopener noreferrer">table_open_cache</a> to avoid bottlenecks while considering OS file descriptor limits.</li>
<li>Configure <a href="https://releem.com/docs/mysql-performance-tuning/table_definition_cache" target="_blank" rel="noopener noreferrer">table_definition_cache</a> to manage table metadata efficiently, especially in environments with many tables or foreign key relationships.</li>
</ul>
<h3>4. Control Thread Cache and Connection Limits<a class="anchor-link" id="4-control-thread-cache-and-connection-limits"></a></h3>
<ul>
<li>Use <a href="https://releem.com/docs/mysql-performance-tuning/thread_cache_size" target="_blank" rel="noopener noreferrer">thread_cache_size</a> to reuse threads effectively and reduce overhead from frequent thread creation.</li>
<li>Adjust <a href="https://releem.com/docs/mysql-performance-tuning/thread_stack" target="_blank" rel="noopener noreferrer">thread_stack</a> and <strong>net_buffer_length</strong> to suit your workload while keeping memory usage scalable.</li>
<li>Limit <a href="https://releem.com/docs/mysql-performance-tuning/max_connections" target="_blank" rel="noopener noreferrer">max_connections</a> to a level appropriate for your workload, preventing excessive session buffers from overwhelming server memory.</li>
</ul>
<h3>5. Track Temporary Table Usage<a class="anchor-link" id="5-track-temporary-table-usage"></a></h3>
<p>Monitor temporary table usage and reduce memory pressure by optimizing queries that rely on GROUP BY, ORDER BY, or UNION.</p>
<h3>6. Use MySQL Memory Calculator<a class="anchor-link" id="6-use-mysql-memory-calculator"></a></h3>
<p>Incorporate tools like the <a href="https://releem.com/tools/mysql-memory-calculator" target="_blank" rel="noopener noreferrer">MySQL Memory Calculator by Releem</a> to estimate memory usage. Input your MySQL configuration values, and the calculator will provide real-time insights into maximum memory usage. This prevents overcommitting your server&rsquo;s memory and helps allocate resources effectively.</p>
<p><figure><img decoding="async" width="1680" height="1668" src="https://percona.community/blog/2025/11/mysql_memory_usage_calc_hu_98125a1e303e79e6.webp" alt="MySQL Memory Calculator" loading="lazy"></figure>
</p>
<h3>7. Monitor Query Performance<a class="anchor-link" id="7-monitor-query-performance"></a></h3>
<p>High-memory-consuming queries, such as those with large joins or sorts, queries without indexes, can affect memory usage. Use <a href="https://releem.com/query-analytics" target="_blank" rel="noopener noreferrer">Releem&rsquo;s Query Analytics and Optimization feature</a> to determine inefficient queries and gain insights on further tuning opportunities.</p>
<p><figure><img decoding="async" width="2582" height="974" src="https://percona.community/blog/2025/11/mysql_memory_usage_query_analytics_hu_6ab9fbc34f4c895a.webp" alt="Releem Dashboard - Query Analytics" loading="lazy"></figure>
</p>
<h2>Simplifying MySQL Memory Tuning with Releem<a class="anchor-link" id="simplifying-mysql-memory-tuning-with-releem"></a></h2>
<p>Releem takes the guesswork out of MySQL optimization by automatically analyzing your setup and suggesting configuration changes that align with your memory limits and performance needs. Whether you&rsquo;re dealing with complex workloads or simply don&rsquo;t have time for manual adjustments, Releem makes it easier to keep MySQL running smoothly.</p>

<p><a href="https://percona.community/blog/2025/11/11/mysql-memory-usage-a-guide-to-optimization/">MySQL Memory Usage: A Guide to Optimization</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>A thread through my 2025 Postgres events</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/10/thread-through-2025-pgconfs/" />
      <id>https://percona.community/blog/2025/11/10/thread-through-2025-pgconfs/</id>
      <updated>2025-11-10T07:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>I recently got back from PostgreSQL Conference Europe in Riga, marking the end of my conference activities for 2025. The speakers were great. The audience, for the Extensions Showcase on Community Day on Tuesday and my Kubernetes from the database out talk, were great. The event team was great. The singing at karaoke was terrible, but it’s supposed to be.</p>
<p><a href="https://percona.community/blog/2025/11/10/thread-through-2025-pgconfs/">A thread through my 2025 Postgres events</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>I recently got back from PostgreSQL Conference Europe in Riga, marking the end of my conference activities for 2025. The speakers were great. The audience, for the Extensions Showcase on Community Day on Tuesday and my Kubernetes from the database out talk, were great. The event team was great. The singing at karaoke was terrible, but it&rsquo;s supposed to be.</p>
<p>After attending a good few events this year, starting with CERN PGDay in mid-January, I wanted to write something about more than just the most recent event. I see a common thread across presentations and sessions at a number of events over the year, that is, scale-out Postgres and particularly, its use in non-profit scientific environments.</p>
<h3>The (beginning and) end users<a class="anchor-link" id="the-beginning-and-end-users"></a></h3>
<p>Far fewer data processing challenges require pooling the resources of many physical servers these days, with servers getting bigger and storage faster. Scientific data analysis and managing large, complex scientific facilities still do. I saw three presentations on this: Rafal Kulaga, Antonin Kveton and Martin Zemko&rsquo;s on <a href="https://indico.cern.ch/event/1471762/contributions/6280212/" target="_blank" rel="noopener noreferrer">managing CERN&rsquo;s SCADA data</a>; Daniel Krefl and Krzysztof Nienartowicz at CERN on <a href="https://indico.cern.ch/event/1471762/contributions/6280216/" target="_blank" rel="noopener noreferrer">how Sendai queries variable star data</a>; and Jaoquim Oliveira in Riga on <a href="https://www.postgresql.eu/events/pgconfeu2025/schedule/session/7138-from-stars-to-storage-engines-migrating-big-science-workloads-beyond-greenplum/" target="_blank" rel="noopener noreferrer">managing the European Space Agency&rsquo;s (ESA&rsquo;s) survey mission data</a>.</p>
<p>I admit a fondness for ESA&rsquo;s GAIA catalog dataset. After I was lucky enough to do a proof of concept project on joining it with other catalog data, it has provided significant intellectual interest. Don&rsquo;t let me get started on the possible ways to optimise computationally expensive inequality joins on horribly skewed data, unless you really care about the problem. My interest in a dataset discussed in two of these talks is not why the thread connecting them is worth commenting on. All three presentations had a lot of content on selecting or developing database technologies for the work they were doing. That&rsquo;s worth discussing a bit further.</p>
<h3>Getting the details right<a class="anchor-link" id="getting-the-details-right"></a></h3>
<p>The thread of sharded, scale out, or Massively Parallel Processing (MPP) Postgres connects end user stories at my first event of the year and my last, along with stories of building this software at events in between. At PGConf.dev in Montreal David Wein gave a very condensed explanation of how AWS&rsquo;s Aurora Limitless handles distributed snapshot isolation (<a href="https://www.youtube.com/watch?v=UrRkHSxP2xE&amp;t=378s" target="_blank" rel="noopener noreferrer">watch the lightning talk at on YouTube</a>), there was also an unconference session on handling the issue in core Postgres the next day. For an in-depth explanation of of what the distributed snapshot problem is and how it may be addressed, see <a href="https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5710-high-concurrency-distributed-snapshots/" target="_blank" rel="noopener noreferrer">Ants Aasma&rsquo;s talk from PGConf.EU 2024</a></p>
<p>The organisations with the data are looking for open source software solutions and bumping into issues around open core licensing, project contribution breadth, project activity levels, project governance. The Postgres developer community is working on the knottiest of the problems in this space, trying to get it absolutely right. In the mean-time, various forks and extensions are delivering useful functionality for the owners of these big, complex datasets.<br>
Useful, but could do better</p>
<p>If this were working out for everyone, there wouldn&rsquo;t be a story to tell. Sednai are building Potgres-XZ, which builds on TBase, which built on Postgres-XL. The ESAC Science Data Centre (ESDC) is facing a decision between two single-vendor projects, where one vendor doesn&rsquo;t provide support for on-premises deployments. CERN procurement sought written assurances over license terms for TimescaleDB, since the CERN facilities organisation may be viewed as a service provider to their hosted scientific projects.</p>
<p>This pattern of licenses built specifically to avoid &ldquo;AWS stealing our innovation/lunch/&hellip;&rdquo;, (and it is always AWS set up as the bogeyman in these stories), is particularly unfortunate here, because it just isn&rsquo;t true for Postgres. AWS, and Azure, employ big teams of community contributors to work on open source Postgres. The progress on statistics management, asynchronous IO, and vacuum in Postgres 18 are, among others, thanks to these teams&rsquo; efforts.</p>
<p>No matter how positive the involvement of the hyperscalers may be for Postgres, there are organisations who will prefer to run their own databases. On-premises hosting is a clear choice for organisations with big facilities capabilities, capital-centric budgeting, extreme requirements and predictable, always on workloads. Many of these organisations are publicly funded scientific projects. It would be great if there were broad-based open source solutions to meet their data management needs.</p>
<h3>Doing better, together<a class="anchor-link" id="doing-better-together"></a></h3>
<p>At PGConf in Riga the Percona team took a few, early steps towards building a joint effort to deliver the components of such a solution. I hope that the big, open managers of structured scientific data (or their subcontractors, depending on their engagement model) and a few vendors can come together to build event data compression, columnar storage, and all the other bits which can be implemented as extensions.</p>
<p>The current Postgres extensions and forks for scale out systems were built on older versions of Postgres, so they had to build features which now exist in core Postgres. Their implementation of partitioning, for instance, differs subtly from the capabilities now available in modern Postgres. As feature-specific extensions can take over capabilities which are currently intertwined with sharding (like compression in Timescale or columnar storage in Citus), users will be less locked in to vertical stacks of features, some useful to them and some not. Simple sharding can then become a proxy (like pgDog), an automation on DDL on a gateway server or even a core Postgres feature.</p>
<p>Which leaves those special cases where moving data between shards during execution is key to performance. This is mattering less with ever bigger servers, improving Postgres parallelism and tools like DuckDB &ndash; but when it matters it still really matters. Here the sons of the &lsquo;plum &ndash; CloudberryDB and WarehousePG, forked from Greenplum when it closed source &ndash; work their magic (hat tip to Jimmy Angelakos for the &ldquo;the &lsquo;plum&rdquo; contraction). Managing that particular capability will always be a big, complex code base. If the patches carried to make it happen shrink as Postgres and extensions fill the gap, we&rsquo;ll have a more sustainable route to all good database things being openly available.</p>

<p><a href="https://percona.community/blog/2025/11/10/thread-through-2025-pgconfs/">A thread through my 2025 Postgres events</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>OAuth, OIDC, validators, what is all this about?</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/" />
      <id>https://percona.community/blog/2025/11/07/oauth-oidc-validators/</id>
      <updated>2025-11-07T10:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>Somebody might tell you, “let’s configure PostgreSQL 18 with OIDC, it should be simple, only takes a few minutes!” And that might be the case if you already have an OIDC provider set up and know all the details about the protocols, configurations, and possible issues. Or it might take much longer if you just open your favorite search engine and type “What is this OIDC stuff about?”</p>
<p><a href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/">OAuth, OIDC, validators, what is all this about?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Somebody might tell you, &ldquo;let&rsquo;s configure PostgreSQL 18 with OIDC, it should be simple, only takes a few minutes!&rdquo;<br>
And that might be the case if you already have an OIDC provider set up and know all the details about the protocols, configurations, and possible issues.<br>
Or it might take much longer if you just open your favorite search engine and type &ldquo;What is this OIDC stuff about?&rdquo;</p>
<p>In this series of blog posts, I&rsquo;ll try to help with this task.<br>
First, by clearing up all the terminology and details in this article.<br>
Later, I&rsquo;ll provide vendor-specific setup instructions for some of the popular providers, using our fully open source <code>pg_oidc_validator</code> plugin.</p>
<h3>OAuth 2.0, OIDC, what&rsquo;s even the difference?<a class="anchor-link" id="oauth-2-0-oidc-whats-even-the-difference"></a></h3>
<p>From news and other sources you might have heard that PostgreSQL 18 now has support for OIDC.<br>
But if you look at the <a href="https://www.postgresql.org/docs/current/auth-oauth.html" target="_blank" rel="noopener noreferrer">PostgreSQL documentation about it</a>, or the variables/configuration options PostgreSQL provides, it&rsquo;s clear that is about OAuth everywhere.</p>
<p>People often use them interchangeably, because the two are closely related:<br>
OIDC is built on top of OAuth.<br>
However, they serve different purposes.</p>
<pre class="mermaid">
flowchart TB
OAuth[OAuth 2.0<br>Authorization Protocol]
OIDC[OpenID Connect OIDC<br>Authentication Layer]
OAuth --&gt; OIDC
OAuthQ["Can user X access<br>resource Y?"]
OIDCq["Who is this user?"]
OAuth -.-&gt;|Answers| OAuthQ
OIDC -.-&gt;|Answers| OIDCq
style OAuth fill:#e3f2fd
style OIDC fill:#f3e5f5
style OAuthQ fill:#fff,stroke:#1976d2,stroke-dasharray: 5 5
style OIDCq fill:#fff,stroke:#7b1fa2,stroke-dasharray: 5 5
</pre>
<h4>Authorization is not Authentication</h4>
<p>The &ldquo;Auth&rdquo; in OAuth is about Authorization, not Authentication as people often believe &ndash; and specifically about remote, distributed authorization involving multiple applications.<br>
For example, somebody might store pictures on some cloud storage, and wants to use a photo editing application written and maintained by a different company.<br>
This EditorApp reaches out to the CloudStorage, and says &ldquo;Can you provide me the pictures please?&rdquo;<br>
After this question, CloudStorage has to figure out if it is allowed to do so or not.</p>
<p>OAuth was designed to handle situations like this &ndash; managing permissions in a complex online environment.<br>
However, EditorApp might also ask the question, &ldquo;Hey, I want to display the name of the user I&rsquo;m working with. Can you tell me who I am working with?&rdquo;<br>
And now we&rsquo;ve leaped into the context of Authentication.<br>
OIDC, or OpenID Connect, is a protocol built on top of OAuth to answer questions like this &ndash; to provide information about who the user is.</p>
<h3>Which of them do we need with PostgreSQL?<a class="anchor-link" id="which-of-them-do-we-need-with-postgresql"></a></h3>
<p>The question with PostgreSQL and similar software using OAuth/OIDC is a bit different:<br>
we want to answer, &ldquo;somebody is trying to log in &ndash; can I allow this login to proceed?&rdquo;</p>
<p>Is this the right question, or am I oversimplifying things?<br>
Shouldn&rsquo;t we be asking, &ldquo;who is trying to log in?&rdquo;<br>
Sometimes we do.<br>
The most common setup will likely ask both: &ldquo;can this login proceed, and if yes, who is the user?&rdquo;</p>
<p>But not necessarily always.<br>
It&rsquo;s perfectly valid to configure a server to use this login flow only for administrators, where anybody who is allowed to use it gets associated with an internal admin account.<br>
In this scenario, we don&rsquo;t care about the identity provided by the external provider:<br>
if the user has a valid access token, we treat them as our admin user and proceed accordingly.</p>
<p>To handle this specific workflow, OAuth is enough:<br>
all we have to do is check if the access token is allowed to access the PostgreSQL server, and if yes, the login can proceed as the admin user.<br>
This is however a limited, specific use case, not the generic scenario, where we also want to figure out who is the user on the provider side.</p>
<p>And there are also limitations on the server and client side:</p>
<ul>
<li>When using our validator plugin, it supports the more generic scenario. It has to work with user identities from the provider, so it requires OIDC features.<br>
That&rsquo;s why we called it <code>pg_oidc_validator</code> and not <code>pg_oauth_validator</code>.</li>
<li>While the server and wire protocol can work with any OAuth flow, currently the only client that implements a login mechanism using it is <code>libpq</code> (and with that, the <code>psql</code> command).<br>
And while the <code>psql</code> command also uses parameters with OAuth in their name, internally it relies on a feature called OIDC discovery &ndash; which, as the name suggests, is part of the OIDC standard, not OAuth.</li>
</ul>
<p>To summarize: in practice, right now anyone who wants to log in to a PostgreSQL server using OAuth and <code>psql</code> has to use a provider that also supports the OIDC protocol.<br>
In practice this isn&rsquo;t a restriction, since OIDC is commonly supported.</p>
<h3>Why do we need this validator?<a class="anchor-link" id="why-do-we-need-this-validator"></a></h3>
<blockquote>
<p>Why do we need to use a plugin to validate things?<br>
Many websites and apps implement login with OIDC providers, and they don&rsquo;t need separate validators, so:<br>
Why can&rsquo;t PostgreSQL do everything internally in the core?</p>
</blockquote>
<p>To answer these questions, we have to understand that PostgreSQL in this context isn&rsquo;t an application in itself &ndash; it&rsquo;s a part of a complex infrastructure.<br>
Even <code>libpq</code>, mentioned above, isn&rsquo;t strictly part of the picture.<br>
There are completely independent implementations of the PostgreSQL wire protocol, which can all implement the client-side flow completely differently.</p>
<p>This bigger system that happens to use PostgreSQL might also use other services that need authorization.<br>
The EditorApp in our earlier example might use PostgreSQL as its database.<br>
And that means, after authenticating the user, it has to talk to two different services:</p>
<ul>
<li>CloudStorage, to access the photos on behalf of the user</li>
<li>PostgreSQL, to access the database on behalf of the user</li>
</ul>
<p>Since users like seamless experiences, the developers of EditorApp want to log the user in only once, internally as part of their application, and then forward this information to both CloudStorage and PostgreSQL.</p>
<pre class="mermaid">
sequenceDiagram
participant User
participant EditorApp
participant Provider
participant CloudStorage
participant PostgreSQL
User-&gt;&gt;EditorApp: Login
EditorApp-&gt;&gt;Provider: Request OAuth/OIDC authentication
Provider-&gt;&gt;User: Show login page
User-&gt;&gt;Provider: Enter credentials
Provider-&gt;&gt;EditorApp: Return access token
Note over EditorApp: User authenticated once,<br>token used for multiple services
EditorApp-&gt;&gt;CloudStorage: Access photos (with token)
CloudStorage-&gt;&gt;Provider: Validate token
Provider-&gt;&gt;CloudStorage: Token valid
CloudStorage-&gt;&gt;EditorApp: Return photos
EditorApp-&gt;&gt;PostgreSQL: Connect to database (with token)
PostgreSQL-&gt;&gt;PostgreSQL: Validator checks token
PostgreSQL-&gt;&gt;EditorApp: Connection established
</pre>
<p>This showcases an important difference compared to &ldquo;simple&rdquo; websites and applications:<br>
PostgreSQL doesn&rsquo;t own the entire authentication flow.<br>
It receives information &ndash; what the OAuth standard calls an <code>access token</code> &ndash; and needs to use this to complete its internal authentication/authorization flow.<br>
To do this, it has to validate the access token and answer the question:<br>
&ldquo;was this token really created by the issuer I trust?&rdquo;</p>
<p>There&rsquo;s nothing new about this question.<br>
Big cloud providers like Google, Microsoft, and many others all do SSO (Single sign-on).<br>
If you log in to their webmail service, you can also open their cloud storage, calendar, or other services, and you&rsquo;ll be similarly authenticated and authorized.</p>
<p>However, there is one very important difference in the above example compared to PostgreSQL:<br>
they all only work with their own users and their own tokens.</p>
<p>You might think this is still an easy problem: OAuth and OIDC are standards, so all we have to do is read the related parts about how validation works and implement our validation flow accordingly.<br>
Except that the standards don&rsquo;t say anything about it.</p>
<p>These standards never define what an access token is.<br>
It can be something completely opaque, only interpretable by the original issuer.<br>
Or it can be something completely transparent &ndash; a proper JSON document with a digital signature that guarantees it was issued by the issuer, usually called a <code>JWT</code> (JSON Web Token).</p>
<p>Many OAuth providers implement <code>JWT</code> tokens, but not all of them.<br>
And even with <code>JWT</code> tokens, there are differences between providers.<br>
Since it&rsquo;s not standardized, the content might differ between vendors.<br>
There&rsquo;s also the question of signature validation, where some providers have differences and require special handling.</p>
<p>And this is where a plugin comes into the picture.<br>
The PostgreSQL maintainers decided they don&rsquo;t want to add vendor-specific code into the core.<br>
Implementing <code>JWT</code> access tokens with the most commonly used structures could have been a solution, but that would have meant PostgreSQL had no way to support providers that were completely standards-compliant but implemented access tokens differently.</p>
<p>Even with <code>JWT</code> tokens, we have to think about token revocation.<br>
<code>JWT</code> tokens have a specified lifetime.<br>
When we validate them strictly based on the digital signature, we also have to check if we&rsquo;re still within the allocated time.<br>
The protocols have a mechanism for refreshing these tokens so that users can stay logged in without repeating the process every time the tokens expire.</p>
<p>But sometimes administrators or the security team might discover a breach &ndash; that somebody got hold of an access token that&rsquo;s still valid for hours or days &ndash; and decide to revoke it.<br>
They can&rsquo;t change the tokens already circulating the network, but they can tell their authorization server to start rejecting the token, even if it&rsquo;s still within its allowed lifetime.</p>
<p>If a validator relies solely on validating the signature of the token, it will still authorize users with these revoked tokens as long as their lifetime allows.<br>
If it performs a request to the server to check if the token is still usable, it might provide a more secure experience at the cost of additional HTTP requests.<br>
Alternatively, some providers allow applications to subscribe to broadcasts where they announce revocations &ndash; so instead of actively querying each token, they can keep a list of which tokens they aren&rsquo;t allowed to authorize, as this list is typically short.</p>
<p>Unfortunately, with these questions we&rsquo;ve again arrived at &ldquo;vendor-specific&rdquo; territory, where a validator has to work differently for different services.<br>
It&rsquo;s also a user choice:</p>
<ul>
<li>On a service where logins are rare but security is a top priority, it might make sense to do explicit queries every time.</li>
<li>But on a server processing thousands of login requests every second, these additional requests might slow things down too much. Administrators might decide not to do them, or to cache the results for some period instead.</li>
</ul>
<p>With all these details and choices, it&rsquo;s definitely better to leave the options open so everyone can select a validator suited for their specific needs.</p>
<h3>What does pg_oidc_validator offer?<a class="anchor-link" id="what-does-pg_oidc_validator-offer"></a></h3>
<p>The Percona validator in its current form focuses on providers working with <code>JWT</code> tokens.<br>
We can&rsquo;t guarantee it works with all providers using <code>JWT</code> tokens &ndash; as mentioned above, the exact format of these tokens isn&rsquo;t standardized &ndash; but we implemented logic that can handle all the providers we tested, in some cases allowing customization with configuration variables.</p>
<p>In the future, we might add support for providers using opaque tokens or additional optional checks for token revocation, but these weren&rsquo;t in scope for the first version.</p>
<p>If you try it out, and find any issues, or have any suggestions, miss some features, <a href="https://github.com/Percona-Lab/pg_oidc_validator/issues" target="_blank" rel="noopener noreferrer">please provide us feedback on Github</a>!</p>
<h3>The future<a class="anchor-link" id="the-future"></a></h3>
<p>OAuth support in PostgreSQL is new and only implements the bare minimum.<br>
Client and application support is also in its early days &ndash; adoption will take time.<br>
It can already be useful for some use cases, but it might still be too limited for others.</p>
<p>I can&rsquo;t predict how the code will change in the server itself, but there are certainly many improvements that can be made.<br>
I also want to avoid confusion and clear up some things related to my previous examples &ndash; things that aren&rsquo;t currently supported but might be in the future:</p>
<ul>
<li>Even if a validator plugin checks for revocation, that check is only done during the login process.<br>
If a user is already logged in to PostgreSQL and the token is revoked, the user will stay logged in &ndash; the validator can&rsquo;t evict them from the server.</li>
<li>Similarly, while the protocol has this concept of access token lifetime and how applications using the token can refresh it, this is currently ignored by PostgreSQL.<br>
Tokens are validated during the login process, and we check the validity, including the lifetime of the token, during that time.<br>
We don&rsquo;t try to refresh these tokens later, and we don&rsquo;t evict users when the token expires &ndash; currently there&rsquo;s no infrastructure for that.</li>
<li>In the examples above, CloudStorage was able to decide which photos EditorApp can access.<br>
OAuth is about authorization, and it could be used for authorization within PostgreSQL &ndash; but this isn&rsquo;t the case currently.<br>
Validators have to map access tokens to internal users present in the PostgreSQL database. Other than this, the information provided by the OAuth Provider has no interaction with the internal permissions within PostgreSQL (grants).</li>
<li>In the example above, we described a situation where an application used PostgreSQL together with other services using the same OAuth provider.<br>
While this is a possible use case, it&rsquo;s also possible that an application only needs an OIDC login flow to access PostgreSQL. In that case, it would be possible to provide better security guarantees by integrating the login flow deeply into PostgreSQL, especially when using more recent OAuth/OIDC extensions such as <a href="https://www.rfc-editor.org/rfc/rfc7636.html" target="_blank" rel="noopener noreferrer">PKCE</a> (Proof Key for Code Exchange).</li>
</ul>
<p>Similarly, our <code>pg_oidc_validator</code> is a pre-release prototype.<br>
While it already performs <code>JWT</code>-based authentication and authorization, it currently doesn&rsquo;t support opaque token providers at all, has no internal caches, and doesn&rsquo;t check for revoked tokens.<br>
Also, some of the features mentioned above would be better with integrated core support, but could technically also be implemented as part of a validator plugin. This would provide the additional benefit that users wouldn&rsquo;t have to wait for newer PostgreSQL versions for the new feature &ndash; it would be enough to upgrade the validator plugin to a newer version.</p>
<p>So there are lots of improvement opportunities for both parts. Stay tuned and follow the changelogs!</p>

<p><a href="https://percona.community/blog/2025/11/07/oauth-oidc-validators/">OAuth, OIDC, validators, what is all this about?</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Why PgBouncer Is Essential for Fair PostgreSQL vs MariaDB Benchmarking</title>
      <link rel="alternate" type="text/html" href="https://mysql-qa.blogspot.com/2025/11/why-pgbouncer-is-essential-for-fair.html" />
      <id>https://mysql-qa.blogspot.com/2025/11/why-pgbouncer-is-essential-for-fair.html</id>
      <updated>2025-11-06T12:33:00+02:00</updated>
      <author><name>jbm</name></author>
      <summary type="html"><![CDATA[<p>If you\'re benchmarking PostgreSQL against MariaDB, you\'re not comparing apples to apples unless you introduce PgBouncer. Here\'s why.</p>
<p>Connection Architecture</p>
<p>PostgreSQL spawns a full process per client connection. That means every incoming connection forks a new backend, each with its own memory map, file descriptors, and kernel overhead. On a high-core host, this model hits system limits fast—either process count or scheduler overhead.</p>
<p>To scale efficiently, PostgreSQL requires PgBouncer, a separate connection pooling proxy. It’s an external add-on—you have to install it, configure it, and monitor it independently.</p>
<p>MariaDB, by contrast, uses a thread-per-connection model natively. No add-ons, no proxies. Thread pooling is built into the server and enabled out of the box. Threads are lightweight, share memory space, and scale efficiently. With proper stack tuning, MariaDB can handle tens of thousands of concurrent connections without breaking a sweat.</p>
<p>PgBouncer Levels the Field</p>
<p>PgBouncer sits between clients and PostgreSQL, pooling backend connections and reusing them across sessions, transactions, or statements. This avoids process churn and dramatically reduces memory usage. But again, it’s an external dependency—PostgreSQL needs PgBouncer to match the native scalability that MariaDB provides without any add-ons.</p>
<p>Transaction ID Philosophy</p>
<p>PostgreSQL uses a 32-bit Transaction ID (XID) for MVCC visibility. It’s elegant, but finite—wraparound hits after ~4 billion transactions, requiring vacuuming and freezing to avoid corruption.</p>
<p>MariaDB uses a 64-bit internal ID, primarily for consistency and replication. No wraparound risk. No visibility tracking at the tuple level. Different philosophies, different failure modes.</p>
<p>Bottom Line</p>
<p>If you\'re modeling concurrency, visibility, or long-term reliability, you need PgBouncer to make PostgreSQL behave like MariaDB. But you also need to understand how each system tracks transactions—and what that means for your workload.</p>
<p>Tags: #PostgreSQL #MariaDB #Benchmarking #PgBouncer #MVCC #DatabaseArchitecture #Concurrency #TechLeadership #MySQL</p>
<p><a href="https://mysql-qa.blogspot.com/2025/11/why-pgbouncer-is-essential-for-fair.html">Why PgBouncer Is Essential for Fair PostgreSQL vs MariaDB Benchmarking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>If you&rsquo;re benchmarking PostgreSQL against MariaDB, you&rsquo;re not comparing apples to apples unless you introduce <strong>PgBouncer</strong>. Here&rsquo;s why.</p>
<h3>Connection Architecture<a class="anchor-link" id="connection-architecture"></a></h3>
<p><strong>PostgreSQL</strong> spawns a full process per client connection. That means every incoming connection forks a new backend, each with its own memory map, file descriptors, and kernel overhead. On a high-core host, this model hits system limits fast&mdash;either process count or scheduler overhead.</p>
<p>To scale efficiently, PostgreSQL requires <strong>PgBouncer</strong>, a separate connection pooling proxy. It&rsquo;s an external add-on&mdash;you have to install it, configure it, and monitor it independently.</p>
<p><strong>MariaDB</strong>, by contrast, uses a thread-per-connection model <em>natively</em>. No add-ons, no proxies. Thread pooling is built into the server and enabled out of the box. Threads are lightweight, share memory space, and scale efficiently. With proper stack tuning, MariaDB can handle tens of thousands of concurrent connections without breaking a sweat.</p>
<h3>PgBouncer Levels the Field<a class="anchor-link" id="pgbouncer-levels-the-field"></a></h3>
<p>PgBouncer sits between clients and PostgreSQL, pooling backend connections and reusing them across sessions, transactions, or statements. This avoids process churn and dramatically reduces memory usage. But again, it&rsquo;s an external dependency&mdash;<strong>PostgreSQL needs PgBouncer to match the native scalability that MariaDB provides without any add-ons</strong>.</p>
<h3>Transaction ID Philosophy<a class="anchor-link" id="transaction-id-philosophy"></a></h3>
<p><strong>PostgreSQL</strong> uses a 32-bit Transaction ID (XID) for MVCC visibility. It&rsquo;s elegant, but finite&mdash;wraparound hits after ~4 billion transactions, requiring vacuuming and freezing to avoid corruption.</p>
<p><strong>MariaDB</strong> uses a 64-bit internal ID, primarily for consistency and replication. No wraparound risk. No visibility tracking at the tuple level. Different philosophies, different failure modes.</p>
<h3>Bottom Line<a class="anchor-link" id="bottom-line"></a></h3>
<p>If you&rsquo;re modeling concurrency, visibility, or long-term reliability, you need PgBouncer to make PostgreSQL behave like MariaDB. But you also need to understand how each system tracks transactions&mdash;and what that means for your workload.</p>
<div class="separator">
</div>
<p><strong>Tags:</strong> #PostgreSQL #MariaDB #Benchmarking #PgBouncer #MVCC #DatabaseArchitecture #Concurrency #TechLeadership #MySQL</p>

<p><a href="https://mysql-qa.blogspot.com/2025/11/why-pgbouncer-is-essential-for-fair.html">Why PgBouncer Is Essential for Fair PostgreSQL vs MariaDB Benchmarking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Performance Framework Autobench3&#8217;s CPU Monitor – MySQL Benchmarking</title>
      <link rel="alternate" type="text/html" href="https://mysql-qa.blogspot.com/2025/11/performance-framework-autobench3s-cpu.html" />
      <id>https://mysql-qa.blogspot.com/2025/11/performance-framework-autobench3s-cpu.html</id>
      <updated>2025-11-05T20:54:00+02:00</updated>
      <author><name>jbm</name></author>
      <summary type="html"><![CDATA[<p>Solving the Backlog Problem in MySQL Benchmarking</p>
<p>In database scalability testing, Autobench3 (AB3) steps through thread counts to evaluate performance under increasing load—typically using values like 4, 8, 16, 32, 64, 128, 256...4096 and beyond.</p>
<p>To improve stability and reproducibility, each thread count is executed multiple times (usually 3 iterations, but up to 7 if needed). Final results are averaged across iterations.</p>
<p>The Backlog Problem</p>
<p>One challenge with thread stepping is that MySQL may still be processing work after the client stops sending traffic. This backlog is especially common in write-heavy workloads, but even read tests can leave MySQL active briefly after disconnect.</p>
<p>Initially, AB3 tried to address this by inserting sleep intervals between phases—during MySQLD startup, database load, and between iterations. But sleep was a guessing game. As thread counts increased, so did the time needed to clear the backlog, and fixed sleep durations weren’t reliable.</p>
<p>Enter CpuMonitor</p>
<p>CpuMonitor was created to solve this problem by detecting when MySQL has truly reached a “rest” state before proceeding to the next iteration or thread count. It attaches to the MySQLD process using PID and monitors CPU usage, waiting for sustained low activity before continuing.</p>
<p>CpuMonitor Configuration</p>
<p> CPU_REST_VALUE: CPU usage threshold considered “rest” (typically ≤ 10%)<br />
 MAX_COUNT: Number of consecutive rest samples required to confirm rest<br />
 CPU_REST_RESET_VALUE: CPU usage threshold that resets the rest counter (typically ≥ 70%)<br />
 INTERVAL: How often CPU usage is checked (in seconds)</p>
<p>Impact on Benchmarking</p>
<p> Read tests: Required minimal rest time, reducing overall waits between iterations<br />
 Write tests: Showed longer rest times at higher thread counts, especially for I/O-bound workloads<br />
 Low concurrency: Needed less rest time, but high concurrency demanded more patience</p>
<p>Early versions lacked reset logic, which led to false positives—brief rest followed by CPU spikes (e.g., redo/undo logs flushing). Adding reset logic ensured true rest before moving on (e.g., requiring 10 consecutive rest samples).</p>
<p>Why It Matters</p>
<p>CpuMonitor ensures that no residual work from the previous iteration contaminates the next iteration. This improves stability, reproducibility, and confidence in performance results.</p>
<p>Once stabilized, AB3 began tracking rest duration as a metric—adding another lens for detecting performance changes across versions and workloads.</p>
<p>Rewriting the Tool</p>
<p>The original CpuMonitor was a Java-based utility I built for the Autobench3 framework, owned by Oracle. It relied on the Sigar library for cross-platform CPU monitoring but became increasingly cumbersome due to heavy dependencies and Sigar’s eventual obsolescence.</p>
<p>Over the last few weeks, having some extra time on my hands, I wrote a new Python version from the ground up—not as a direct port, but as a lean, standalone tool. It was a chance to dive deeper into Python’s ecosystem, share something useful, and simplify what had become a bulky Java solution.</p>
<p>https://github.com/JonathanBMiller/process_cpu_monitor</p>
<p>Thanks for following along.</p>
<p>Tags: #MySQL #Benchmarking #Autobench3 #PerformanceTesting #CpuMonitor #AutomationFramework #DatabasePerformance #Scalability #TechLeadership #MariaDB</p>
<p><a href="https://mysql-qa.blogspot.com/2025/11/performance-framework-autobench3s-cpu.html">Performance Framework Autobench3&#8217;s CPU Monitor – MySQL Benchmarking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<h2>Solving the Backlog Problem in MySQL Benchmarking<a class="anchor-link" id="solving-the-backlog-problem-in-mysql-benchmarking"></a></h2>
<p>In database scalability testing, <strong>Autobench3 (AB3)</strong> steps through thread counts to evaluate performance under increasing load&mdash;typically using values like 4, 8, 16, 32, 64, 128, 256&hellip;4096 and beyond.</p>
<p>To improve stability and reproducibility, each thread count is executed multiple times (usually 3 iterations, but up to 7 if needed). Final results are averaged across iterations.</p>
<h3>The Backlog Problem<a class="anchor-link" id="the-backlog-problem"></a></h3>
<p>One challenge with thread stepping is that <strong>MySQL may still be processing work after the client stops sending traffic</strong>. This backlog is especially common in write-heavy workloads, but even read tests can leave MySQL active briefly after disconnect.</p>
<p>Initially, AB3 tried to address this by inserting sleep intervals between phases&mdash;during MySQLD startup, database load, and between iterations. But sleep was a guessing game. As thread counts increased, so did the time needed to clear the backlog, and fixed sleep durations weren&rsquo;t reliable.</p>
<h3>Enter CpuMonitor<a class="anchor-link" id="enter-cpumonitor"></a></h3>
<p><strong>CpuMonitor</strong> was created to solve this problem by detecting when MySQL has truly reached a &ldquo;rest&rdquo; state before proceeding to the next iteration or thread count. It attaches to the MySQLD process using PID and monitors CPU usage, waiting for sustained low activity before continuing.</p>
<h4>CpuMonitor Configuration</h4>
<ul>
<li><strong>CPU_REST_VALUE</strong>: CPU usage threshold considered &ldquo;rest&rdquo; (typically &le; 10%)</li>
<li><strong>MAX_COUNT</strong>: Number of consecutive rest samples required to confirm rest</li>
<li><strong>CPU_REST_RESET_VALUE</strong>: CPU usage threshold that resets the rest counter (typically &ge; 70%)</li>
<li><strong>INTERVAL</strong>: How often CPU usage is checked (in seconds)</li>
</ul>
<h3>Impact on Benchmarking<a class="anchor-link" id="impact-on-benchmarking"></a></h3>
<ul>
<li><strong>Read tests</strong>: Required minimal rest time, reducing overall waits between iterations</li>
<li><strong>Write tests</strong>: Showed longer rest times at higher thread counts, especially for I/O-bound workloads</li>
<li><strong>Low concurrency</strong>: Needed less rest time, but high concurrency demanded more patience</li>
</ul>
<p>Early versions lacked reset logic, which led to false positives&mdash;brief rest followed by CPU spikes (e.g., redo/undo logs flushing). Adding reset logic ensured true rest before moving on (e.g., requiring 10 consecutive rest samples).</p>
<h3>Why It Matters<a class="anchor-link" id="why-it-matters"></a></h3>
<p>CpuMonitor ensures that no residual work from the previous iteration contaminates the next iteration. This improves stability, reproducibility, and confidence in performance results.</p>
<p>Once stabilized, AB3 began tracking rest duration as a metric&mdash;adding another lens for detecting performance changes across versions and workloads.</p>
<h3>Rewriting the Tool<a class="anchor-link" id="rewriting-the-tool"></a></h3>
<p>The original CpuMonitor was a Java-based utility I built for the Autobench3 framework, owned by Oracle. It relied on the Sigar library for cross-platform CPU monitoring but became increasingly cumbersome due to heavy dependencies and Sigar&rsquo;s eventual obsolescence.</p>
<p>Over the last few weeks, having some extra time on my hands, I wrote a new <strong>Python version from the ground up</strong>&mdash;not as a direct port, but as a lean, standalone tool. It was a chance to dive deeper into Python&rsquo;s ecosystem, share something useful, and simplify what had become a bulky Java solution.</p>
<p><a href="https://github.com/JonathanBMiller/process_cpu_monitor" target="_blank">https://github.com/JonathanBMiller/process_cpu_monitor</a></p>
<p></p>
<p>Thanks for following along.</p>
<p></p>
<p><strong>Tags:</strong> #MySQL #Benchmarking #Autobench3 #PerformanceTesting #CpuMonitor #AutomationFramework #DatabasePerformance #Scalability #TechLeadership #MariaDB</p>

<p><a href="https://mysql-qa.blogspot.com/2025/11/performance-framework-autobench3s-cpu.html">Performance Framework Autobench3&#8217;s CPU Monitor – MySQL Benchmarking</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>DNA of PostgreSQL and MariaDB with clarity</title>
      <link rel="alternate" type="text/html" href="https://mysql-qa.blogspot.com/2025/11/dna-of-postgresql-and-mariadb-with.html" />
      <id>https://mysql-qa.blogspot.com/2025/11/dna-of-postgresql-and-mariadb-with.html</id>
      <updated>2025-11-05T15:41:00+02:00</updated>
      <author><name>jbm</name></author>
      <summary type="html"><![CDATA[<p>In the MariaDB Foundation’s deep dive, Manoj Vakeel (who leads database migration at MariaDB) breaks down the architectural DNA of PostgreSQL and MariaDB with clarity and precision.</p>
<p>The discussion touches on MVCC housekeeping, including PostgreSQL’s vacuuming model versus MariaDB’s auto-purge strategy—critical for understanding how each handles transaction visibility and long-term performance.</p>
<p>Key takeaways from the video:</p>
<p> PostgreSQL uses a process-based model, offering strong isolation but requiring external pooling for scale.<br />
 MariaDB uses a threaded architecture, enabling efficient high-concurrency handling out of the box.<br />
 MVCC strategies differ: PostgreSQL relies on VACUUM and freezing, while MariaDB uses auto-purge.<br />
 Replication models diverge: PostgreSQL favors logical decoding and WAL shipping; MariaDB supports GTID and Galera Cluster for synchronous multi-master setups.<br />
 Licensing and ecosystem support vary: PostgreSQL under BSD, MariaDB under GPL.</p>
<p>This video is a solid reference point for anyone benchmarking, migrating, or architecting open-source database systems.</p>
<p>If you’re modeling transaction ID behavior or MVCC visibility, it’s worth watching the full 53-minute session.</p>
<p><a href="https://mysql-qa.blogspot.com/2025/11/dna-of-postgresql-and-mariadb-with.html">DNA of PostgreSQL and MariaDB with clarity</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In the MariaDB Foundation&rsquo;s deep dive, <strong>Manoj Vakeel</strong> (who leads database migration at MariaDB) breaks down the architectural DNA of PostgreSQL and MariaDB with clarity and precision.</p>
<p>The discussion touches on MVCC housekeeping, including PostgreSQL&rsquo;s vacuuming model versus MariaDB&rsquo;s auto-purge strategy&mdash;critical for understanding how each handles transaction visibility and long-term performance.</p>
<div class="separator">
</div>
<p><strong>Key takeaways from the video:</strong></p>
<ul>
<li>PostgreSQL uses a process-based model, offering strong isolation but requiring external pooling for scale.</li>
<li>MariaDB uses a threaded architecture, enabling efficient high-concurrency handling out of the box.</li>
<li>MVCC strategies differ: PostgreSQL relies on VACUUM and freezing, while MariaDB uses auto-purge.</li>
<li>Replication models diverge: PostgreSQL favors logical decoding and WAL shipping; MariaDB supports GTID and Galera Cluster for synchronous multi-master setups.</li>
<li>Licensing and ecosystem support vary: PostgreSQL under BSD, MariaDB under GPL.</li>
</ul>
<p>This video is a solid reference point for anyone benchmarking, migrating, or architecting open-source database systems.</p>
<p>If you&rsquo;re modeling transaction ID behavior or MVCC visibility, it&rsquo;s worth watching the full 53-minute session.</p>

<p><a href="https://mysql-qa.blogspot.com/2025/11/dna-of-postgresql-and-mariadb-with.html">DNA of PostgreSQL and MariaDB with clarity</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Encryption support in PMM Dump</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/10/30/encryption-support-in-pmm-dump/" />
      <id>https://percona.community/blog/2025/10/30/encryption-support-in-pmm-dump/</id>
      <updated>2025-10-30T11:00:00+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The pmm-dump client utility performs a logical backup of the performance metrics collected by the PMM Server and imports them into a different PMM Server instance. PMM Dump allows you to share monitoring data collected by your PMM server with the Percona Support team securely.</p>
<p><a href="https://percona.community/blog/2025/10/30/encryption-support-in-pmm-dump/">Encryption support in PMM Dump</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>The <code>pmm-dump</code> client utility performs a logical backup of the performance metrics collected by the PMM Server and imports them into a different PMM Server instance. PMM Dump allows you to share monitoring data collected by your PMM server with the Percona Support team securely.</p>
<p>Up until now dumps, created by the tool, were not encrypted. It was possible to encrypt them after they are done but this required additional actions from the user.</p>
<p>Starting from the upcoming PMM Dump version 0.8.0-ga released on October 29, 2025, dumps are encrypted by default.</p>
<h2>Key points<a class="anchor-link" id="key-points"></a></h2>
<ul>
<li>Dump files are encrypted by default with AES-256-based encryption.</li>
<li>An auto-generated password is produced for each encrypted dump; it is printed at the end of the export operation or can be written to a file with <code>--pass-filepath</code>.</li>
<li>You can provide a custom password with <code>--pass</code>.</li>
<li>Disable encryption with <code>--no-encryption</code> only when you understand the risks.</li>
<li>By default, for encrypted dumps, export logging to STDOUT is suppressed; use <code>--no-just-key</code> to override.</li>
</ul>
<h2>Why this matters<a class="anchor-link" id="why-this-matters"></a></h2>
<p>Encrypting PMM dumps prevents accidental exposure of monitoring and query data that may contain sensitive information (query text, hostnames, metrics). It brings PMM Dump in line with secure data-handling best practices and simplifies safe sharing with Percona Support.</p>
<h2>Quick examples<a class="anchor-link" id="quick-examples"></a></h2>
<p>Export (encryption enabled by default):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-0" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ pmm-dump export --pmm-url='https://admin:admin@127.0.0.1' --allow-insecure-certs
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Password: ****************
</span></span><span class="line"><span class="cl">$ ls pmm-dump-.tar.gz.enc</span></span></code></pre>
</div>
</div>
</div>
<p>Provide a custom password:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-1" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ pmm-dump export --pmm-url='https://admin:admin@127.0.0.1' --pass='My$trongP@ss'</span></span></code></pre>
</div>
</div>
</div>
<p>Save auto-generated password to file:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-2" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ pmm-dump export --pmm-url='https://admin:admin@127.0.0.1' --pass-filepath=/tmp/pmm-dump.pass</span></span></code></pre>
</div>
</div>
</div>
<p>Disable encryption (not recommended):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-3" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ pmm-dump export --pmm-url='https://admin:admin@127.0.0.1' --no-encryption</span></span></code></pre>
</div>
</div>
</div>
<p>Import an encrypted dump:</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-4" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ pmm-dump import --pmm-url='https://admin:admin@127.0.0.1' --allow-insecure-certs 
</span></span><span class="line"><span class="cl">--dump-path=pmm-dump-1758017090.tar.gz.enc --pass='My$trongP@ss'</span></span></code></pre>
</div>
</div>
</div>
<p>Decrypt an encrypted dump (if needed):</p>
<div class="code-block">
<div class="code-block__header"><button class="code-block__copy" type="button" data-copy-target="codeblock-5" aria-label="Copy code to clipboard"><br>
<span class="code-block__copy-default">Copy</span><br>
<span class="code-block__copy-success" aria-hidden="true">Copied!</span><br>
</button>
</div>
<div class="code-block__content">
<div class="highlight">
<pre class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ openssl enc -d -aes-256-ctr -pbkdf2 -in dump.tar.gz.enc -out dump.tar.gz</span></span></code></pre>
</div>
</div>
</div>
<h2>Recommendations<a class="anchor-link" id="recommendations"></a></h2>
<ul>
<li>Prefer leaving encryption enabled.</li>
<li>Use <code>--pass-filepath</code> to store passwords securely rather than relying on terminal output.</li>
<li>Transfer encrypted archives over secure channels (SCP/SFTP) and share passwords via secure out-of-band channels.</li>
</ul>
<h2>Availability<a class="anchor-link" id="availability"></a></h2>
<p>Encryption support is included starting in the recent PMM Dump 0.8.0-ga release. Check your PMM Dump version (<code>pmm-dump version</code>) and the docs for exact version details.</p>
<h2>Additional information<a class="anchor-link" id="additional-information"></a></h2>
<ul>
<li><a href="https://percona.com/get/pmm-dump" target="_blank" rel="noopener noreferrer">Latest version for x86_64 platforms</a></li>
<li><a href="https://github.com/Percona-Lab/percona-on-arm/releases/tag/v0.12" target="_blank" rel="noopener noreferrer">ARM binaries</a></li>
<li><a href="https://docs.percona.com/pmm-dump-documentation/" target="_blank" rel="noopener noreferrer">PMM Dump Documentation</a></li>
<li><a href="https://github.com/percona/pmm-dump" target="_blank" rel="noopener noreferrer">GitHub repository</a></li>
</ul>

<p><a href="https://percona.community/blog/2025/10/30/encryption-support-in-pmm-dump/">Encryption support in PMM Dump</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>No, you probably dont need Kubernetes</title>
      <link rel="alternate" type="text/html" href="https://medium.com/@arbaudie.it/no-you-probably-dont-need-kubernetes-f70e7d35525b?source=rss-c779d007e7fe------2" />
      <id>https://medium.com/@arbaudie.it/no-you-probably-dont-need-kubernetes-f70e7d35525b?source=rss-c779d007e7fe------2</id>
      <updated>2025-10-29T14:31:30+02:00</updated>
      <author><name>ArBauDie.IT</name></author>
      <summary type="html"><![CDATA[<p>Et ouais, vous ne rêvez pas, je l’affirme en toute confiance. Haut et fort. Sur Linkedin le paradis de la hype et des buzzwords.Comme le souligne fort justement Nicolas Martinez dans son excellent post, K8s ajoute une couche de complexité souvent inutile :Maintenance lourde, coûts cachés, temps perdu, obligation d’avoir une équipe DevOps/SRE : le jeu en vaut-il la chandelle ?Concernant MariaDB (et bien d’autres SGBD, R ou non), les question sont encore plus simples :Avez-vous vraiment besoin de cette complexité pour faire tourner une base de données ? (non)Avez-vous besoin d’élasticité dans vos déploiements ? (non)Voulez-vous faire du CI/CD ? (on peux faire sans)Ne seriez-vous pas plus efficace avec une solution managée, dédiée, comme LayerOps ou même un simple hébergement traditionnel sur VPS accompagné d’un excellent proxy tel que Maxscale ? (oui)Quand vous réalisez que Google ou meta ont scale au niveau planétaire littéralement sans kubernetes il y a déjà de quoi être un peu sceptique sur la pertinence d’un tel produit pour des entreprises ne gérant ni leur traffic ni leur quantité de déploiement. Quand en plus on vous apprends que booking.com gère une flotte de plusieurs centaines de serveurs MySQL sans utiliser k8s, on doit légitimement se poser quelques questions sur la véritable cible de ce produit. Evidemment on peux toujours argumenter que blablacar le fait (et bien en plus !). Il faut toujours une exception pour confirmer la règle paraît-il !Un retour aux basiques s’impose : k8s est un outil parmi tant d’autre et il conviens de l’évaluer comme les autres afin de bien comprendre ses avantages ET ses inconvénients et prendre une décision éclairée parmi les concurrents.Et cote SGBDR on dira ce qu’on veux mais de la concurrence y en a un peu. Des systèmes de haute disponibilité intégrés comme MariaDB Galera cluster , PostgreSQL Citus ou Oracle RAC aux outils tiers tels que LayerOps, Maxscale, F5, haproxy, MHA, ou KeepAlived + VIP il existe de très nombreuses combinaisons moins coûteuses et tout autant automatisées que k8s. A l’époque du finops, il est même assez surprenant de voir que cette notion de coût est encore largement ignorée au moment des prises de décisions technologiques.Donc bon k8s, ca brille, ca fait joli sur Linkedin mais attention à ne pas faire de la tech pour faire de la tech (overengineering anyone ?). D’après le principe KISS, la solution la plus simple est souvent celle qui permet de délivrer le plus rapidement. Et on sait tous que délivrer &#62; &#62; &#62; *. Dont acte.Et vous, quand avez-vous pris la décision de vous passer de k8s car vous n’en avez pas besoin ? Parlons-en et mettons en place un plan de de recentrage de vos équipes sur l’essentiel : l’expérience client.</p>
<p><a href="https://medium.com/@arbaudie.it/no-you-probably-dont-need-kubernetes-f70e7d35525b?source=rss-c779d007e7fe------2">No, you probably dont need Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>Et ouais, vous ne r&ecirc;vez pas, je l&rsquo;affirme en toute confiance. Haut et fort. Sur Linkedin le paradis de la hype et des buzzwords.</p>
<p>Comme le souligne fort justement Nicolas Martinez dans son <a href="https://www.linkedin.com/posts/nicolas-martinez-nimeops_nouveau-cas-client-qui-se-pose-la-question-activity-7369368889698181123-78W0/">excellent post</a>, K8s ajoute une couche de complexit&eacute; souvent inutile&nbsp;:</p>
<p>Maintenance lourde, co&ucirc;ts cach&eacute;s, temps perdu, obligation d&rsquo;avoir une &eacute;quipe DevOps/SRE&nbsp;: le jeu en vaut-il la chandelle&nbsp;?</p>
<p><strong>Concernant MariaDB (et bien d&rsquo;autres SGBD, R ou non), les question sont encore plus simples</strong>&nbsp;:</p>
<ul>
<li>Avez-vous vraiment besoin de cette complexit&eacute; pour faire tourner une base de donn&eacute;es&nbsp;?&nbsp;(non)</li>
<li>Avez-vous besoin d&rsquo;&eacute;lasticit&eacute; dans vos d&eacute;ploiements&nbsp;?&nbsp;(non)</li>
<li>Voulez-vous faire du CI/CD&nbsp;? (on peux faire&nbsp;sans)</li>
<li>Ne seriez-vous pas plus efficace avec une solution manag&eacute;e, d&eacute;di&eacute;e, comme LayerOps ou m&ecirc;me un simple h&eacute;bergement traditionnel sur VPS accompagn&eacute; d&rsquo;un excellent proxy tel que Maxscale&nbsp;?&nbsp;(oui)</li>
</ul>
<p>Quand vous r&eacute;alisez que Google ou meta ont scale au niveau plan&eacute;taire litt&eacute;ralement sans kubernetes il y a d&eacute;j&agrave; de quoi &ecirc;tre un peu sceptique sur la pertinence d&rsquo;un tel produit pour des entreprises ne g&eacute;rant ni leur traffic ni leur quantit&eacute; de d&eacute;ploiement. Quand en plus on vous apprends que booking.com g&egrave;re une flotte de plusieurs centaines de serveurs MySQL sans utiliser k8s, on doit l&eacute;gitimement se poser quelques questions sur la v&eacute;ritable cible de ce produit. Evidemment on peux toujours argumenter que blablacar le fait (et bien en plus&nbsp;!). Il faut toujours une exception pour confirmer la r&egrave;gle para&icirc;t-il&nbsp;!</p>
<p>Un retour aux basiques s&rsquo;impose&nbsp;: k8s est un outil parmi tant d&rsquo;autre et il conviens de l&rsquo;&eacute;valuer comme les autres afin de bien comprendre ses avantages ET ses inconv&eacute;nients et prendre une d&eacute;cision &eacute;clair&eacute;e parmi les concurrents.</p>
<p>Et cote SGBDR on dira ce qu&rsquo;on veux mais de la concurrence y en a un peu. Des syst&egrave;mes de haute disponibilit&eacute; int&eacute;gr&eacute;s comme MariaDB Galera cluster&nbsp;, PostgreSQL Citus ou Oracle RAC aux outils tiers tels que LayerOps, Maxscale, F5, haproxy, MHA, ou KeepAlived + VIP il existe de tr&egrave;s nombreuses combinaisons moins co&ucirc;teuses et tout autant automatis&eacute;es que k8s. A l&rsquo;&eacute;poque du finops, il est m&ecirc;me assez surprenant de voir que cette notion de co&ucirc;t est encore largement ignor&eacute;e au moment des prises de d&eacute;cisions technologiques.</p>
<p>Donc bon k8s, ca brille, ca fait joli sur Linkedin mais attention &agrave; ne pas faire de la tech pour faire de la tech (overengineering anyone&nbsp;?). D&rsquo;apr&egrave;s <a href="https://medium.com/@arbaudie.it/embrace-simplicity-8f3fa62d7167">le principe KISS</a>, la solution la plus simple est souvent celle qui permet de d&eacute;livrer le plus rapidement. Et on sait tous que d&eacute;livrer &gt;&gt;&gt; *. Dont&nbsp;acte.</p>
<p><strong>Et vous, quand avez-vous pris la d&eacute;cision de vous passer de k8s car vous n&rsquo;en avez pas besoin&nbsp;? </strong><a href="https://arbaudie.it/">Parlons-en</a> et mettons en place un plan de de recentrage de vos &eacute;quipes sur l&rsquo;essentiel&nbsp;: l&rsquo;exp&eacute;rience client.</p>
<p><img loading="lazy" decoding="async" src="https://medium.com/_/stat?event=post.clientViewed&amp;referrerSource=full_rss&amp;postId=f70e7d35525b" width="1" height="1" alt=""></p>

<p><a href="https://medium.com/@arbaudie.it/no-you-probably-dont-need-kubernetes-f70e7d35525b?source=rss-c779d007e7fe------2">No, you probably dont need Kubernetes</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Keyword vs. semantic search with AI</title>
      <link rel="alternate" type="text/html" href="https://programmingbrain.com/2025/05/keyword-vs-semantic-search-with-ai.html" />
      <id>https://programmingbrain.com/2025/05/keyword-vs-semantic-search-with-ai.html</id>
      <updated>2025-10-28T15:42:01+02:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>How to build keyword and semantic search in MariaDB using Python, LangChain, and AI embeddings.</p>
<p><a href="https://programmingbrain.com/2025/05/keyword-vs-semantic-search-with-ai.html">Keyword vs. semantic search with AI</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>How to build keyword and semantic search in MariaDB using Python, LangChain, and AI embeddings.</p>

<p><a href="https://programmingbrain.com/2025/05/keyword-vs-semantic-search-with-ai.html">Keyword vs. semantic search with AI</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/" />
      <id>https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/</id>
      <updated>2025-10-22T18:41:15+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.3.2 of its popular Backup and Recovery Manager for MariaDB and MySQL (brman).<br />
The new FromDual Backup and Recovery Manager can be downloaded from here. The FromDual Repositories were updated. How to install and use the Backup and Recovery Manager is described in FromDual Backup and Recovery Manager (brman) installation guide.<br />
In the inconceivable case that you find a bug in the FromDual Backup and Recovery Manager please send us an [email](mailto:contact@fromdual.com?Subject=Bug report for brman).<br />
Any feedback, statements and testimonials are welcome as well! Please send them to feedback@fromdual.com.<br />
Upgrade from 2.x to 2.3.2<br />
$ cd /opt<br />
$ tar xf /download/brman-2.3.2.tar.gz<br />
$ rm -f brman<br />
$ ln -s brman-2.3.2 brman</p>
<p>Changes in FromDual Backup and Recovery Manager 2.3.2<br />
This release is a new minor release. It contains mainly bug fixes. We have tried to maintain backward-compatibility with the 1.2, 2.0, 2.1, 2.2 and 2.3 release series. But you should test the new release seriously!<br />
You can verify your current FromDual Backup Manager version with the following command:<br />
$ bman --version<br />
$ rman --version</p>
<p>General</p>
<p>Tests improved.<br />
New features documented and documentation updated.<br />
Libraries from MyEnv project updated.</p>
<p>FromDual Backup Manager (bman)</p>
<p>SSL/TLS implemented for bman only.<br />
Binary log position should be gathered correctly now also with MySQL 8.4.</p>
<p>FromDual Recovery Manager (rman)</p>
<p>No changes.</p>
<p>Subscriptions for commercial use of FromDual Backup and Recovery Manager you can get from [from us](mailto:contact@fromdual.com?Subject=Commercial use of FromDual brman).</p>
<p><a href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/">FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.3.2 of its popular <a href="https://www.fromdual.com/backup-and-recovery-manager-user-guide" title="FromDual Backup and Recovery Manager for MariaDB and MySQL">Backup and Recovery Manager for MariaDB and MySQL</a> (<code>brman</code>).</p>
<p>The new FromDual Backup and Recovery Manager can be downloaded from <a href="https://support.fromdual.com/admin/public/download.php?operation=select&amp;product_id=12&amp;release_series_id=28&amp;product_version_id=154" title="Download FromDual Backup and Recovery Manager for MariaDB and MySQL">here</a>. The FromDual Repositories were updated. How to install and use the Backup and Recovery Manager is described in <a href="https://www.fromdual.com/fromdual-backup-and-recovery-manager-installation-guide" title="FromDual Backup and Recovery Manager (brman) installation guide">FromDual Backup and Recovery Manager (<code>brman</code>) installation guide</a>.</p>
<p>In the inconceivable case that you find a bug in the FromDual Backup and Recovery Manager please send us an [email](mailto:contact@fromdual.com?Subject=Bug report for brman).</p>
<p>Any feedback, statements and testimonials are welcome as well! Please send them to <a href="mailto:feedback@fromdual.com?Subject=Feedback">feedback@fromdual.com</a>.</p>
<h2>Upgrade from 2.x to 2.3.2<a class="anchor-link" id="upgrade-from-2-x-to-2-3-2"></a></h2>
<pre><code>$ cd /opt
$ tar xf /download/brman-2.3.2.tar.gz
$ rm -f brman
$ ln -s brman-2.3.2 brman
</code></pre>
<h2>Changes in FromDual Backup and Recovery Manager 2.3.2<a class="anchor-link" id="changes-in-fromdual-backup-and-recovery-manager-2-3-2"></a></h2>
<p>This release is a new minor release. It contains mainly bug fixes. We have tried to maintain backward-compatibility with the 1.2, 2.0, 2.1, 2.2 and 2.3 release series. But you should test the new release seriously!</p>
<p>You can verify your current FromDual Backup Manager version with the following command:</p>
<pre><code>$ bman --version
$ rman --version
</code></pre>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Tests improved.</li>
<li>New features documented and documentation updated.</li>
<li>Libraries from MyEnv project updated.</li>
</ul>
<h3>FromDual Backup Manager (bman)<a class="anchor-link" id="fromdual-backup-manager-bman"></a></h3>
<ul>
<li>SSL/TLS implemented for <code>bman</code> only.</li>
<li>Binary log position should be gathered correctly now also with MySQL 8.4.</li>
</ul>
<h3>FromDual Recovery Manager (rman)<a class="anchor-link" id="fromdual-recovery-manager-rman"></a></h3>
<ul>
<li>No changes.</li>
</ul>
<p>Subscriptions for commercial use of FromDual Backup and Recovery Manager you can get from [from us](mailto:contact@fromdual.com?Subject=Commercial use of FromDual brman).</p>

<p><a href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/">FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</title>
      <link rel="alternate" type="text/html" href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/" />
      <id>https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/</id>
      <updated>2025-10-22T18:41:15+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.3.2 of its popular Backup and Recovery Manager for MariaDB and MySQL (brman).<br />
The new FromDual Backup and Recovery Manager can be downloaded from here. The FromDual Repositories were updated. How to install and use the Backup and Recovery Manager is described in FromDual Backup and Recovery Manager (brman) installation guide.<br />
In the inconceivable case that you find a bug in the FromDual Backup and Recovery Manager please send us an [email](mailto:contact@fromdual.com?Subject=Bug report for brman).<br />
Any feedback, statements and testimonials are welcome as well! Please send them to feedback@fromdual.com.<br />
Upgrade from 2.x to 2.3.2<br />
$ cd /opt<br />
$ tar xf /download/brman-2.3.2.tar.gz<br />
$ rm -f brman<br />
$ ln -s brman-2.3.2 brman</p>
<p>Changes in FromDual Backup and Recovery Manager 2.3.2<br />
This release is a new minor release. It contains mainly bug fixes. We have tried to maintain backward-compatibility with the 1.2, 2.0, 2.1, 2.2 and 2.3 release series. But you should test the new release seriously!<br />
You can verify your current FromDual Backup Manager version with the following command:<br />
$ bman --version<br />
$ rman --version</p>
<p>General</p>
<p>Tests improved.<br />
New features documented and documentation updated.<br />
Libraries from MyEnv project updated.</p>
<p>FromDual Backup Manager (bman)</p>
<p>SSL/TLS implemented for bman only.<br />
Binary log position should be gathered correctly now also with MySQL 8.4.</p>
<p>FromDual Recovery Manager (rman)</p>
<p>No changes.</p>
<p>Subscriptions for commercial use of FromDual Backup and Recovery Manager you can get from [from us](mailto:contact@fromdual.com?Subject=Commercial use of FromDual brman).</p>
<p><a href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/">FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>FromDual has the pleasure to announce the release of the new version 2.3.2 of its popular <a href="https://www.fromdual.com/backup-and-recovery-manager-user-guide" title="FromDual Backup and Recovery Manager for MariaDB and MySQL">Backup and Recovery Manager for MariaDB and MySQL</a> (<code>brman</code>).</p>
<p>The new FromDual Backup and Recovery Manager can be downloaded from <a href="https://support.fromdual.com/admin/public/download.php?operation=select&amp;product_id=12&amp;release_series_id=28&amp;product_version_id=154" title="Download FromDual Backup and Recovery Manager for MariaDB and MySQL">here</a>. The FromDual Repositories were updated. How to install and use the Backup and Recovery Manager is described in <a href="https://www.fromdual.com/fromdual-backup-and-recovery-manager-installation-guide" title="FromDual Backup and Recovery Manager (brman) installation guide">FromDual Backup and Recovery Manager (<code>brman</code>) installation guide</a>.</p>
<p>In the inconceivable case that you find a bug in the FromDual Backup and Recovery Manager please send us an [email](mailto:contact@fromdual.com?Subject=Bug report for brman).</p>
<p>Any feedback, statements and testimonials are welcome as well! Please send them to <a href="mailto:feedback@fromdual.com?Subject=Feedback">feedback@fromdual.com</a>.</p>
<h2>Upgrade from 2.x to 2.3.2<a class="anchor-link" id="upgrade-from-2-x-to-2-3-2"></a></h2>
<pre><code>$ cd /opt
$ tar xf /download/brman-2.3.2.tar.gz
$ rm -f brman
$ ln -s brman-2.3.2 brman
</code></pre>
<h2>Changes in FromDual Backup and Recovery Manager 2.3.2<a class="anchor-link" id="changes-in-fromdual-backup-and-recovery-manager-2-3-2"></a></h2>
<p>This release is a new minor release. It contains mainly bug fixes. We have tried to maintain backward-compatibility with the 1.2, 2.0, 2.1, 2.2 and 2.3 release series. But you should test the new release seriously!</p>
<p>You can verify your current FromDual Backup Manager version with the following command:</p>
<pre><code>$ bman --version
$ rman --version
</code></pre>
<h3>General<a class="anchor-link" id="general"></a></h3>
<ul>
<li>Tests improved.</li>
<li>New features documented and documentation updated.</li>
<li>Libraries from MyEnv project updated.</li>
</ul>
<h3>FromDual Backup Manager (bman)<a class="anchor-link" id="fromdual-backup-manager-bman"></a></h3>
<ul>
<li>SSL/TLS implemented for <code>bman</code> only.</li>
<li>Binary log position should be gathered correctly now also with MySQL 8.4.</li>
</ul>
<h3>FromDual Recovery Manager (rman)<a class="anchor-link" id="fromdual-recovery-manager-rman"></a></h3>
<ul>
<li>No changes.</li>
</ul>
<p>Subscriptions for commercial use of FromDual Backup and Recovery Manager you can get from [from us](mailto:contact@fromdual.com?Subject=Commercial use of FromDual brman).</p>

<p><a href="https://www.fromdual.com/blog/brman-release-notes/fromdual-backup-manager-2.3.2-has-been-released/">FromDual Backup and Recovery Manager for MariaDB and MySQL 2.3.2 has been released</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Say Hello to OIDC in PostgreSQL 18!</title>
      <link rel="alternate" type="text/html" href="https://percona.community/blog/2025/10/22/say-hello-to-oidc-in-postgresql-18/" />
      <id>https://percona.community/blog/2025/10/22/say-hello-to-oidc-in-postgresql-18/</id>
      <updated>2025-10-22T11:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>If you’ve ever wondered how to set up OpenID Connect (OIDC) authentication in PostgreSQL, the wait is almost over.</p>
<p><a href="https://percona.community/blog/2025/10/22/say-hello-to-oidc-in-postgresql-18/">Say Hello to OIDC in PostgreSQL 18!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>If you&rsquo;ve ever wondered how to set up OpenID Connect (OIDC) authentication in PostgreSQL, the wait is almost over.</p>
<p>We&rsquo;ve spent some time exploring what it would take to make OIDC easier and more reliable to use with PostgreSQL. And now, we&rsquo;re happy to share the first results of that work.</p>
<h3>Why OIDC, and why now?<a class="anchor-link" id="why-oidc-and-why-now"></a></h3>
<p>We&rsquo;ve spoken to some of our customers and noticed a trend of moving away from LDAP to OIDC. Our MongoDB product is already providing OIDC integration and the team working on PostgreSQL products saw an opportunity coming with PostgreSQL 18.</p>
<p>As some of you may have noticed <strong>PostgreSQL 18</strong> has introduced improvements to authentication that include OAuth 2.0 support. It&rsquo;s a small step from OAuth 2.0 to OIDC, which builds directly on top of it. So we set out to test PostgreSQL 18&rsquo;s integration with one of the most common OIDC providers: Okta.</p>
<p>That&rsquo;s when we discovered a missing piece. While PostgreSQL 18 includes the underlying support for OAuth 2.0, it still lacks a validator library required to successfully complete an OIDC configuration.<br>
So&hellip; we built it.</p>
<h3>Closing the gap for enterprise needs<a class="anchor-link" id="closing-the-gap-for-enterprise-needs"></a></h3>
<p>Percona is known for our Support, Managed, and Consulting services for open source databases. But while our services are commercial, our mission remains deeply open source.</p>
<p>We don&rsquo;t differentiate users by the size of their wallets. We want everyone using open source databases to succeed, grow, and benefit from the same quality foundations that enterprises rely on. Services is what finances our open source investments, what we believe is truly honest and sustainable open source development.</p>
<p>OIDC is a great example of how real-world enterprise needs and community innovation come together. PostgreSQL 18 introduced OAuth thanks to community efforts, but OIDC that helps organizations handle compliance and scale was still not supported. With the work on OIDC validator we want to close the gap between these two while keeping the solution open source.</p>
<h3>The power is in testing<a class="anchor-link" id="the-power-is-in-testing"></a></h3>
<p>It would be great if integrating with one OIDC provider meant it works with all of them. Unfortunately, real world implementations differ, sometimes significantly.<br>
That&rsquo;s why compatibility testing matters. That&rsquo;s also why often particular providers we find require independent approach and some custom handling in the code.</p>
<p>So far, we&rsquo;ve done some preliminary tests of the library. It&rsquo;s not production-ready yet. It&rsquo;s a first release that&rsquo;s shared with users and Community to get feedback while in the meantime our engineers will put it through rigorous testing regimen.<br>
We&rsquo;ve so far tested the validator library compatibility with:</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&#9989; <strong>Okta</strong></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&#9989; <strong>Ping Identity</strong></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&#9989; <strong>Keycloak</strong></p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&#9989; <strong>Microsoft Entra ID (Azure AD)</strong></p>
<p>We&rsquo;re aware it&rsquo;s not yet compatible with Google&rsquo;s OIDC implementation, which has a few unique quirks, but that&rsquo;s on our roadmap for future work.<br>
The broader the testing, the stronger the solution. This is where we hope the Community can join us.</p>
<h3>Try the OIDC validator library now!<a class="anchor-link" id="try-the-oidc-validator-library-now"></a></h3>
<p>We&rsquo;re excited to share that the first release of the OIDC <a href="https://github.com/Percona-Lab/pg_oidc_validator/releases/tag/latest" target="_blank" rel="noopener noreferrer">validator library is now available</a> for your feedback.<br>
<a href="https://github.com/Percona-Lab/pg_oidc_validator/tree/main" target="_blank" rel="noopener noreferrer">The repository includes</a>:</p>
<ul>
<li>Basic setup instructions in the README,</li>
<li>Introductory documentation for getting started, and</li>
<li>Links to examples and test configurations.</li>
</ul>
<p>More detailed guides, including how OIDC works under the hood and how to use it in real PostgreSQL deployments, as well as direct integration guides are coming soon in follow up blog posts and documentation articles.</p>
<h3>We&rsquo;d love your feedback<a class="anchor-link" id="wed-love-your-feedback"></a></h3>
<p>If you&rsquo;re experimenting with PostgreSQL 18 or exploring modern authentication options, give the <a href="https://github.com/Percona-Lab/pg_oidc_validator/releases/tag/latest" target="_blank" rel="noopener noreferrer">OIDC validator library</a> a try and <a href="https://github.com/Percona-Lab/pg_oidc_validator/discussions" target="_blank" rel="noopener noreferrer">let us know what you think</a>!<br>
Your input will help us make this capability more robust, portable, and enterprise-ready while keeping it open source and accessible to everyone.</p>

<p><a href="https://percona.community/blog/2025/10/22/say-hello-to-oidc-in-postgresql-18/">Say Hello to OIDC in PostgreSQL 18!</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Monitoring multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Aurora MySQL</title>
      <link rel="alternate" type="text/html" href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/" />
      <id>https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/</id>
      <updated>2025-10-21T21:09:08+03:00</updated>
      <author><name>Huy Nguyen</name></author>
      <summary type="html"><![CDATA[<p>In this post, we discuss methods to effectively monitor parallel replication performance and tune its related parameters for Amazon Aurora MySQL and Amazon Relational Database Service for MySQL and MariaDB.</p>
<p><a href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/">Monitoring multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Aurora MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p>In our <a href="https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/" target="_blank" rel="noopener">previous post</a>, we discussed how MySQL replication and multithreaded replication (MTR) works and its key configuration options and best practices. In this post, we discuss methods to effectively monitor parallel replication performance and tune its related parameters for <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraMySQL.html" target="_blank" rel="noopener noreferrer">Amazon Aurora MySQL</a> and <a href="https://aws.amazon.com/rds/mysql/" target="_blank" rel="noopener noreferrer">Amazon Relational Database Service for MySQL</a> and MariaDB.</p>
<p>In the following section, we will dive into several methods that MySQL offers to monitor MTR.</p>
<h2>SHOW REPLICA STATUS<a class="anchor-link" id="show-replica-status"></a></h2>
<p>SHOW REPLICA STATUS is a good starting place to monitor and troubleshoot MTR, especially when replication fails and stops. You run this command in replica instance. To understand each field&rsquo;s meaning and interpretation, refer to <a href="https://repost.aws/knowledge-center/rds-mysql-high-replica-lag" target="_blank" rel="noopener noreferrer">How do I troubleshoot high replica lag with Amazon RDS for MySQL?</a> in the AWS Knowledge Center and <a href="https://dev.mysql.com/doc/refman/8.0/en/show-replica-status.html" target="_blank" rel="noopener noreferrer">SHOW REPLICA STATUS Statement</a> in the MySQL documentation:</p>
<div class="hide-language">
<pre><code class="lang-sql">mysql&gt; SHOW REPLICA STATUSG
*************************** 1. row ***************************
             Replica_IO_State: Waiting for source to send event
              Source_Log_File: mysql-bin-changelog.002961
          Read_Source_Log_Pos: 10799580
               Relay_Log_File: relaylog.008193
                Relay_Log_Pos: 26644679
        Relay_Source_Log_File: mysql-bin-changelog.002737
           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Source_Log_Pos: 26644443
              Relay_Log_Space: 29257792087
        Seconds_Behind_Source: 5203
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
    Replica_SQL_Running_State: Waiting for replica workers to process their queues
           Source_Retry_Count: 86400</code></pre>
</div>
<p>Some of the fields have slightly different meanings in MTR compared to single threaded replication.</p>
<ul>
<li><code>Seconds_Behind_Source</code> is still valid, accurate, and useful with multithreaded replication, but you should keep in mind that this value is based on <code>Exec_Source_Log_Pos</code> and might not reflect the position of the most recently committed transaction.
<ul>
<li>When performing operations that will require a cutover to the target database, you should not rely on <code>Seconds_Behind_Source</code> or the <a href="https://aws.amazon.com/cloudwatch/" target="_blank" rel="noopener noreferrer">Amazon CloudWatch</a> metric <code>ReplicaLag</code> in RDS for MySQL and RDS for MariaDB and <code>AuroraBinlogReplicaLag</code> in Aurora MySQL. We recommend using <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html" target="_blank" rel="noopener noreferrer">Amazon RDS Blue/Green Deployments</a>, which not only automates the switchover process with minimal downtime but also provides <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments-switching.html#blue-green-deployments-switching-guardrails" target="_blank" rel="noopener noreferrer">built-in safeguards</a> so that the replica (green) environment is fully synchronized with the source (blue) before cutover occurs.</li>
</ul>
</li>
<li><code>Last_SQL_Errno</code> and <code>Last_SQL_Error</code> only show the error of the coordinator thread, not worker threads. That means there might be more failures in the worker threads that can be found in the <a href="https://dev.mysql.com/doc/refman/8.0/en/performance-schema-replication-applier-status-by-worker-table.html" target="_blank" rel="noopener noreferrer">replication_applier_status_by_worker</a> table that shows each worker thread&rsquo;s status. If that table isn&rsquo;t available, the replica error log can be used. The log or the <code>replication_applier_status_by_worker</code> table should also be used to learn more about the failure shown by <a href="https://dev.mysql.com/doc/refman/8.0/en/show-replica-status.html" target="_blank" rel="noopener noreferrer">SHOW REPLICA STATUS</a> or the coordinator table.</li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/thread-information.html" target="_blank" rel="noopener noreferrer">Common states</a> for the <code>Replica_SQL_Running_State</code> field in MTR include <code>'Waiting for dependent transaction to commit'</code> or <code>'Waiting for preceding transaction to be committed'</code>. These states are normal and indicate that a worker thread is waiting for a dependent transaction to be complete before proceeding. However, if these states appear frequently, consider tuning the workload in the source, such as breaking large transactions to smaller ones, to improve replication parallelism and overall replication performance. Setting <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer">binlog_transaction_dependency_tracking</a> to WRITESET can also significantly reduce these dependencies. Refer to <a href="https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/" target="_blank" rel="noopener noreferrer">Overview and best practices of multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Amazon Aurora MySQL</a> for best practices.</li>
</ul>
<h2>Performance Schema tables<a class="anchor-link" id="performance-schema-tables"></a></h2>
<p>MySQL provides <a href="https://dev.mysql.com/doc/mysql-perfschema-excerpt/8.0/en/performance-schema-replication-tables.html" target="_blank" rel="noopener noreferrer">a set of Performance Schema tables</a> to monitor replication at a deeper level compared to <code>SHOW REPLICA STATUS</code>, hence they are recommended. Three tables that are useful in MTR monitoring are:</p>
<ul>
<li><code>replication_connection_status</code></li>
<li><code>replication_applier_status_by_coordinator</code></li>
<li><code>replication_applier_status_by_worker</code></li>
</ul>
<p>We recommend having <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PerfInsights.EnableMySQL.html#USER_PerfInsights.EnableMySQL.options" target="_blank" rel="noopener noreferrer">Performance Schema enabled</a> so all data is populated accordingly to these tables.</p>
<p>The <a href="https://dev.mysql.com/doc/refman/8.0/en/performance-schema-replication-connection-status-table.html" target="_blank" rel="noopener noreferrer">replication_connection_status</a> table shows the current status of the I/O thread that handles the replica&rsquo;s connection to the source, information on the last transaction queued in the relay log, and information on the transaction currently being queued in the relay log.</p>
<p>The <a href="https://dev.mysql.com/doc/refman/8.0/en/performance-schema-replication-applier-status-by-coordinator-table.html" target="_blank" rel="noopener noreferrer">replication_applier_status_by_coordinator</a> table shows the status of the coordinator thread, specifically the last transaction that was buffered by the coordinator thread to a worker&rsquo;s queue, as well as the transaction it&rsquo;s currently buffering. The start timestamp refers to when the coordinator thread read the first event of the transaction from the relay log to buffer it to a worker&rsquo;s queue, and the end timestamp refers to when the last event finished buffering to the worker&rsquo;s queue.</p>
<p>The <a href="https://dev.mysql.com/doc/refman/8.0/en/performance-schema-replication-applier-status-by-worker-table.html" target="_blank" rel="noopener noreferrer">replication_applier_status_by_worker</a> table is the most important table. It shows the status of the worker threads. When replication fails and stops, <code>LAST_ERROR_NUMBER</code>, <code>LAST_ERROR_MESSAGE</code> columns are important to understand its root cause. An error number of 0 and message of the empty string mean &ldquo;no error.&rdquo; If the <code>LAST_ERROR_MESSAGE</code> value isn&rsquo;t empty, the error values also appear in the replica&rsquo;s error log. The following is an example:</p>
<div class="hide-language">
<pre><code class="lang-sql">LAST_ERROR_NUMBER: 1062
LAST_ERROR_MESSAGE: "Error 'Duplicate entry '123' for key 'PRIMARY''
on query 'INSERT INTO customers(id, name) VALUES (123, 'John')'</code></pre>
</div>
<h2>Custom views<a class="anchor-link" id="custom-views"></a></h2>
<p>Performance Schema tables contain raw data that can be challenging to translate into actionable insights. Currently, there are no industry standards or established best practices for querying these tables to measure MTR lag or evaluate MTR utilization and effectiveness. To make this data more accessible, we recommend creating custom views. The following is an example view that shows detailed statistics about MySQL replication worker threads, including their current activity, timing information, and error status. It can help to identify long-running replica transactions, underutilized workers, and replication errors. Test thoroughly before implementing it in your production environment:</p>
<div class="hide-language">
<pre><code class="lang-sql">CREATE OR REPLACE
  ALGORITHM = MERGE
  SQL SECURITY INVOKER 
VIEW binlog_replication_worker_stats AS
SELECT 
  COALESCE(NULLIF(CHANNEL_NAME, ''), 'default') as channel,
  WORKER_ID as worker_num,
  THREAD_ID as thread_id,
  APPLYING_TRANSACTION_START_APPLY_TIMESTAMP != '0000-00-00 00:00:00.000000' as active,
  CASE 
    WHEN APPLYING_TRANSACTION_START_APPLY_TIMESTAMP != '0000-00-00 00:00:00.000000'
    THEN sys.format_time(GREATEST(0, TIMESTAMPDIFF(MICROSECOND, 
         APPLYING_TRANSACTION_START_APPLY_TIMESTAMP, NOW(6))) * 1000000)
    ELSE NULL
  END as time_applying_current_trx,
  CASE 
    WHEN LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP != '0000-00-00 00:00:00.000000'
    THEN sys.format_time(GREATEST(0, TIMESTAMPDIFF(MICROSECOND, 
         LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP,
         LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP)) * 1000000)
    ELSE NULL
  END as time_applying_last_trx,
  CASE 
    WHEN LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP != '0000-00-00 00:00:00.000000'
    THEN LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP
    ELSE NULL
  END as last_active,
  SERVICE_STATE as worker_state,
  LAST_ERROR_NUMBER as last_error_code,
  LAST_ERROR_MESSAGE as last_error_message
FROM 
  performance_schema.replication_applier_status_by_worker
ORDER BY 
  channel,
  worker_num;</code></pre>
</div>
<p>The following table explains what each column represents in the view:</p>
<table class="styled-table" border="1px" cellpadding="10px">
<tbody>
<tr>
<td><strong>Column name</strong></td>
<td><strong>Description</strong></td>
</tr>
<tr>
<td><code>channel</code></td>
<td>Replication channel name (<code>default</code> for unnamed channel).</td>
</tr>
<tr>
<td><code>worker_num</code></td>
<td>Worker thread number (<code>worker_id</code> from <code>performance_schema</code>).</td>
</tr>
<tr>
<td><code>thread_id</code></td>
<td>MySQL thread ID of the worker.</td>
</tr>
<tr>
<td><code>active</code></td>
<td>Whether the worker is currently applying a transaction (1=yes, 0=no).</td>
</tr>
<tr>
<td><code>time_applying_current_trx</code></td>
<td>How long the current transaction has been applying (if active).</td>
</tr>
<tr>
<td><code>time_applying_last_trx</code></td>
<td>How long the last transaction took to apply.</td>
</tr>
<tr>
<td><code>last_active</code></td>
<td>Timestamp of last transaction ended.</td>
</tr>
<tr>
<td><code>worker_state</code></td>
<td>Current state of the worker thread (<code>ON</code>/<code>OFF</code>)</td>
</tr>
<tr>
<td><code>last_error_code</code></td>
<td>Last error number (0 if no error)</td>
</tr>
<tr>
<td><code>last_error_message</code></td>
<td>Last error message (empty if no error)</td>
</tr>
</tbody>
</table>
<p>The following is the sample output of querying the view:</p>
<div class="hide-language">
<pre><code class="lang-sql">mysql&gt; select * from binlog_replication_worker_stats;
+---------+------------+-----------+--------+---------------------------+------------------------+----------------------------+--------------+-----------------+--------------------+
| channel | worker_num | thread_id | active | time_applying_current_trx | time_applying_last_trx | last_active                | worker_state | last_error_code | last_error_message |
+---------+------------+-----------+--------+---------------------------+------------------------+----------------------------+--------------+-----------------+--------------------+
| default |          1 |        46 |      1 | 930 us                    | 1.62 ms                | 2025-04-09 18:24:21.130941 | ON           |               0 |                    |
| default |          2 |        47 |      1 | 1.78 ms                   | 3.6 ms                 | 2025-04-09 18:24:21.130124 | ON           |               0 |                    |
| default |          3 |        48 |      1 | 1.7 ms                    | 2.47 ms                | 2025-04-09 18:24:21.130132 | ON           |               0 |                    |
| default |          4 |        49 |      1 | 7 us                      | 2.18 ms                | 2025-04-09 18:24:21.131854 | ON           |               0 |                    |
+---------+------------+-----------+--------+---------------------------+------------------------+----------------------------+--------------+-----------------+--------------------+
4 rows in set (0.01 sec</code></pre>
</div>
<p>Regarding the <code>Active</code> column: Ideally you want to confirm all threads are active because this suggests good parallel processing. If you notice significant differences in worker activity, it might indicate these problems:</p>
<ul>
<li>Events are being processed one after another due to dependency conflicts</li>
<li>Transactions are taking too long because of missing indexes</li>
<li>Transactions are delayed because multiple processes are waiting for the same locks</li>
<li>DDL operations being applied</li>
</ul>
<p>The <code>time_applying_current_trx</code> (and <code>time_applying_last_trx</code> ) helps identify long running transactions, which might cause serialization replication in the replica, as explained in <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#sysvar_replica_pending_jobs_size_max" target="_blank" rel="noopener noreferrer">replica_pending_jobs_size_max</a> in <a href="https:///Users/ngyenjh/Library/CloudStorage/WorkDocsDrive-Documents/Blogs/link%20to%20part%201" target="_blank" rel="noopener noreferrer">Overview and best practices of multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Amazon Aurora MySQL</a>. When this happens, you might notice only one active thread with high <code>time_applying_current_trx</code> value.</p>
<p>If, after resolving all issues, you still frequently encounter many worker threads stay inactive for a while, as indicated in <code>last_active</code> column, consider lowering the <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_parallel_workers" target="_blank" rel="noopener noreferrer">replica_parallel_workers</a> parameter. In contrast, if all worker threads are busy or active all the time, increasing the <code>replica_parallel_workers</code> might help.</p>
<h2>Engine error log<a class="anchor-link" id="engine-error-log"></a></h2>
<p>MTR log messages are available when <code>log_error_verbosity</code> is set to 3. Although Aurora MySQL has this setting enabled by default, RDS for MySQL and RDS for MariaDB require you to modify the parameter group value. With this setting enabled, the replica&rsquo;s coordinator thread periodically writes statistical information to the error log, showing how events are distributed among worker threads. The frequency of these log entries depends on the volume of events being processed, with logs appearing no more frequently than one time every 120 seconds. The following is sample output from error logs.</p>
<div class="hide-language">
<pre><code class="lang-code">2025-07-09T12:26:21.017757Z 2892166 [Note] [MY-010559] [Repl] Multi-threaded slave statistics for channel '': seconds elapsed = 120; events assigned = 2276626433; worker queues filled over overrun level = 0; waited due a Worker queue full = 0; waited due the total size = 0; waited at clock conflicts = 171233788281300 waited (count) when Workers occupied = 6460064 waited when Workers occupied = 151556259200 (rpl_replica.cc:4978), 

2025-07-09T12:28:33.805410Z 2892166 [Note] [MY-010559] [Repl] Multi-threaded slave statistics for channel '': seconds elapsed = 132; events assigned = 2276672513; worker queues filled over overrun level = 0; waited due a Worker queue full = 0; waited due the total size = 0; waited at clock conflicts = 171234310046600 waited (count) when Workers occupied = 6460064 waited when Workers occupied = 151556259200 (rpl_replica.cc:4978)
</code></pre>
</div>
<p>You can find the explanation of each field in <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-threads-monitor-worker.html" target="_blank" rel="noopener noreferrer">Monitoring Replication Applier Worker Threads</a> in the MySQL documentation. We recommend that you <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.CloudWatch.html" target="_blank" rel="noopener noreferrer">publish the log to CloudWatch Logs</a> for long-term retention and then use <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AnalyzingLogData.html" target="_blank" rel="noopener noreferrer">CloudWatch Logs Insights </a>to interpret the log and compare changes overtime. Refer to <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html" target="_blank" rel="noopener noreferrer">CloudWatch Logs Insights language query syntax </a>for more details.</p>
<p>Ideally, <code>waited due the total size</code> should be zero. If it&rsquo;s nonzero and increasing between samples especially during replica lag presence, check if you have large transactions. If there are no large transactions, consider increasing the parameter <code>replica_pending_jobs_size_max</code>. <code>Waited (count) when workers occupied</code> should be as low as possible. Consider increasing <code>replica_parallel_threads</code> if you notice significant changes between samples.</p>
<p><code>Waited at clock conflicts</code> indicates transaction/event dependence, meaning a transaction/event had to wait on another transaction before being applied. It is normal to see high and increasing value for this counter in most cases. To help reduce transaction dependence and therefore increase replication parallelism, follow the best practices mentioned in part 1 of this two-part blog post series. These best practices include setting <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer">b<code>inlog_transaction_dependency_tracking</code></a> to WRITESET and <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_parallel_type" target="_blank" rel="noopener noreferrer"><code>replica_parallel_type</code></a> to LOGICAL_CLOCK. We don&rsquo;t recommend turning off <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_preserve_commit_order" target="_blank" rel="noopener noreferrer">replica_preserve_commit_order</a> due to data consistency, especially for applications that depend on commit order.</p>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>In this post, you learned about monitoring and tuning MySQL multithreaded replication using tools such as SHOW REPLICA STATUS, Performance Schema tables, error logs and custom views provided by Amazon. Effective monitoring of multithreaded replication is important for optimal replication performance. Remember that tuning is an iterative process &ndash; start with conservative parameter adjustments and monitor the effects closely. Regular performance audits and proactive optimization ensure robust replication, improving data consistency and minimizing latency.For more information on Aurora MySQL and RDS for MySQL and RDS for MariaDB replication, refer to the following resources:</p>
<ul>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Replication.html" target="_blank" rel="noopener noreferrer">Replication with Amazon Aurora MySQL</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_MySQL.Replication.html" target="_blank" rel="noopener noreferrer">Working with MySQL replication in Amazon RDS</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_MariaDB.Replication.html" target="_blank" rel="noopener noreferrer">Working with MariaDB replication in Amazon RDS</a></li>
</ul>
<p>If you have any additional thoughts, leave a comment.</p>
<hr>
<h3>About the authors<a class="anchor-link" id="about-the-authors"></a></h3>
<footer>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2024/03/01/ngyenjh_image-1.png" alt="Huy Nguyen" width="120" height="160">
  </div>
<h3 class="lb-h4">Huy Nguyen<a class="anchor-link" id="huy-nguyen"></a></h3>
<p><a href="https://www.linkedin.com/in/huy-nguyen-6a0a4960/" target="_blank" rel="noopener">Huy</a> is a Senior Engineer in AWS Support. He specializes in Amazon RDS, Amazon Aurora. He provides guidance and technical assistance to customers, enabling them to build scalable, highly available, and secure solutions in the AWS Cloud</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/10/09/Screenshot-2025-10-09-at-4.48.12%E2%80%AFPM-100x100.png" alt="Arun Gadila" width="120" height="160">
  </div>
<h3 class="lb-h4">Arun Gadila<a class="anchor-link" id="arun-gadila"></a></h3>
<p><a href="https://www.linkedin.com/in/arun-kumar-gadila/" target="_blank" rel="noopener">Arun</a> is a Cloud Support Database Engineer II at AWS with over 3.5 years of expertise specializing in RDS for MySQL, Aurora MySQL, and RDS for SQL Server. Recognized as a Subject Matter Expert (SME) in both RDS MySQL and Aurora MySQL, demonstrating deep technical knowledge in these services. Dedicated to helping customers optimize their database environments and resolve complex challenges across AWS&rsquo;s managed database offerings</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/10/09/reillym-100x132.png" alt="Marc Reilly" width="120" height="160">
  </div>
<h3 class="lb-h4">Marc Reilly<a class="anchor-link" id="marc-reilly"></a></h3>
<p><a href="https://www.linkedin.com/in/marcreillyirl/" target="_blank" rel="noopener">Marc</a> is a Senior Database engineer on the Amazon Aurora MySQL team.</p>
</div>
</footer>

<p><a href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/">Monitoring multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Aurora MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Overview and best practices of multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Amazon Aurora MySQL</title>
      <link rel="alternate" type="text/html" href="https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/" />
      <id>https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/</id>
      <updated>2025-10-21T21:09:00+03:00</updated>
      <author><name>Huy Nguyen</name></author>
      <summary type="html"><![CDATA[<p>In this first post, we dive into the world of MySQL replication, with a special focus on parallel replication techniques. We start with a quick overview of how MySQL replication works, then explore the intricacies of multithreaded replication. We discuss key configuration options and best practices for optimization.</p>
<p><a href="https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/">Overview and best practices of multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Amazon Aurora MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></summary>
      <content type="html"><![CDATA[<p><a href="https://dev.mysql.com/doc/refman/8.0/en/mysql-cluster-replication-mta.html" target="_blank" rel="noopener noreferrer">Multithreaded replication (MTR)</a> is a feature in MySQL that enhances binlog replication performance, particularly for high-throughput databases such as those managed in <a href="https://aws.amazon.com/rds/aurora/features/" target="_blank" rel="noopener noreferrer">Amazon Aurora MySQL-Compatible Edition</a> and <a href="https://aws.amazon.com/rds/mysql/" target="_blank" rel="noopener noreferrer">Amazon Relational Database Service (Amazon RDS) for MySQL</a> and MariaDB. This technology is useful in standard replication scenarios as well as in modern operational practices, including <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/blue-green-deployments.html" target="_blank" rel="noopener noreferrer">Amazon RDS Blue/Green deployments</a>, where it simplifies database upgrades and modifications with minimal downtime.</p>
<p>In this first post, we dive into the world of MySQL replication, with a special focus on parallel replication techniques. We start with a quick overview of how MySQL replication works, then explore the intricacies of multithreaded replication. We discuss key configuration options and best practices for optimization. In the <a href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/" target="_blank" rel="noopener noreferrer">second part</a>, we will cover multithreaded replication monitoring.</p>
<h2>MySQL binlog replication overview<a class="anchor-link" id="mysql-binlog-replication-overview"></a></h2>
<p>The following image shows the high-level architecture of MySQL replication with single threaded replication.</p>
<p><img decoding="async" loading="lazy" class="alignnone wp-image-66340" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/10/10/DB-4628-MTRMySQL.png" alt="MySQL replication arch" width="720" height="377"></p>
<p>Let&rsquo;s dive into each of the components shown in the above diagram on both source and replica.</p>
<h3>In the source<a class="anchor-link" id="in-the-source"></a></h3>
<ul>
<li>When binary logs are enabled and a DML (Data Manipulation Language), DCL (Data Control Language), or DDL (Data Definition Language) is executed in the source and committed, MySQL persists the statement or transaction, as events, to the binary log files. Once persisted, the replicas will fetch the events through the dump thread.
<ul>
<li>Note: Binary logs are enabled by default on Amazon RDS for MySQL if the <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_LogAccess.MySQL.BinaryFormat.html" target="_blank" rel="noopener noreferrer">backup retention period is set to a nonzero value</a>. But in Aurora MySQL, customers must <a href="https://repost.aws/knowledge-center/enable-binary-logging-aurora" target="_blank" rel="noopener noreferrer">enable binary logs</a> explicitly.</li>
</ul>
</li>
<li><strong>Binary log dump thread&nbsp;</strong>&ndash; For each replica, the source database creates a thread to send the binary log contents to a replica when the replica connects. You can identify this thread in the output of <a href="https://dev.mysql.com/doc/refman/8.0/en/show-processlist.html" target="_blank" rel="noopener noreferrer">SHOW PROCESSLIST</a> on the source as the Binlog Dump thread.</li>
</ul>
<h3>In the replica<a class="anchor-link" id="in-the-replica"></a></h3>
<ul>
<li><strong>IO thread</strong> &ndash; the replica creates an IO (receiver) thread, which connects to the source and pull the updates recorded in its binary logs. The IO thread reads the updates that the source&rsquo;s Binlog Dump thread sends and copies them to local files that comprise the replica&rsquo;s <a href="https://dev.mysql.com/doc/refman/8.0/en/replica-logs-relaylog.html" target="_blank" rel="noopener noreferrer">relay log</a>. The state of this thread is shown as Replica_IO_running in the output of SHOW REPLICA STATUS. It is always a single IO Thread regardless of single-threaded replication or MTR. It rarely becomes a bottleneck in replication.</li>
<li><strong>SQL (applier) thread</strong> &ndash; Reads relay logs and applies them to the replica&rsquo;s database (as any other normal client would). There is only one SQL thread in single threaded replication, which is the default before MySQL 8.0.27. There are multiple SQL threads in MTR.</li>
<li>If the replica has binlogs enabled, the SQL thread is also responsible for writing new binary logs to the disk. This can cause replication lag due to binlog overhead.</li>
</ul>
<h2>MTR overview<a class="anchor-link" id="mtr-overview"></a></h2>
<p>When MTR is enabled (by setting <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_slave_parallel_workers" target="_blank" rel="noopener noreferrer">slave_parallel_workers</a> or <code>replica_parallel_workers</code> to a value greater than 1), the SQL thread is divided into two types: a coordinator thread and multiple worker threads. The coordinator thread&rsquo;s job is to read events from the relay log, analyze event dependencies, and then assign them to the worker threads for parallel execution. Event dependencies are determined in the source instance to track which events can be safely executed in parallel on replicas while maintaining data consistency. Worker or applier threads are the workhorses of the replication process, replaying the transactions on the replica. This structure allows for more efficient parallel processing of replication events.</p>
<p>In the following example, the parameter <code>replica_parallel_workers</code> is set to 4. When the query <code>Select * from information_schema.processlist where User='system user';</code> is executed on the read replica, you&rsquo;ll find a total of six threads in the output: one IO thread, one coordinator thread and four worker threads.</p>
<ul>
<li>IO_THREAD: One per replication channel (Id: 419 in the following output)</li>
<li>SQL threads:
<ul>
<li>Coordinator thread: One per replication channel. (Id: 420 in the following output)</li>
<li>Worker threads: <code>replica_parallel_workers</code> per channel. (Id 421-424 in the following output)</li>
</ul>
</li>
</ul>
<div class="hide-language">
<pre><code class="lang-sql">select * from information_schema.processlist where User='system user';
+------+-----------------+--------------------+------+---------+--------+----------------------------------------------------------+-----------------------+
| Id   | User            | Host               | db   | Command | Time   | State                                                    | Info                  |
+------+-----------------+--------------------+------+---------+--------+----------------------------------------------------------+-----------------------+
|  419 | system user     | connecting host    | NULL | Connect | 494497 | Waiting for source to send event                         | NULL                  |
|  420 | system user     |                    | NULL | Query   |      9 | Replica has read all relay log; waiting for more updates | NULL                  |
|  421 | system user     |                    | NULL | Query   |      9 | Waiting for an event from Coordinator                    | NULL                  |
|  422 | system user     |                    | NULL | Query   | 494925 | Waiting for an event from Coordinator                    | NULL                  |
|  423 | system user     |                    | NULL | Query   | 494925 | Waiting for an event from Coordinator                    | NULL                  |
|  424 | system user     |                    | NULL | Query   | 494925 | Waiting for an event from Coordinator                    | NULL                  |
+------+-----------------+--------------------+------+---------+--------+----------------------------------------------------------+-----------------------+</code></pre>
</div>
<h2>How MTR works<a class="anchor-link" id="how-mtr-works"></a></h2>
<p>MTR relies on replication dependency tracking. This system determines how transactions can be safely executed in parallel on replica servers while maintaining data consistency. MySQL provides two primary methods for this dependency tracking: COMMIT ORDER and WRITESET.</p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer">COMMIT_ORDER</a> dependency tracking relies on <a href="https://dev.mysql.com/worklog/task/?id=5223" target="_blank" rel="noopener noreferrer">group commit</a> timing on the source server. The dependency information written by the replication source is represented using logical timestamps and persisted in the binary log events. There are two logical timestamps for each transaction to determine dependencies.</p>
<ul>
<li><code>sequence_number</code> &ndash; This is one for the first transaction in each binary log, two for the second transaction, and so on. The numbering restarts with one in each binary log file.</li>
<li><code>last_committed</code> &ndash; This refers to the <code>sequence_number</code> of the most recently committed transaction found to conflict with the current transaction. This value is always less than the <code>sequence_number</code>.</li>
</ul>
<p>The following code block shows a simplified binlog snippet after being decoded using <a href="https://dev.mysql.com/doc/refman/8.4/en/mysqlbinlog.html" target="_blank" rel="noopener noreferrer">mysqlbinlog</a>. In this example, the transactions with sequence numbers 2347, 2348, 2349, and 2351 can execute in parallel in the replica because their <code>last committed</code> timestamps all point to a transaction before this set. Transaction with sequence number 2350 is dependent on 2348, so it can&rsquo;t be replicated in parallel with 2347, 2348, 2349, and 2351.</p>
<div class="hide-language">
<pre><code class="lang-code">#308741 15:23:45... last committed=2345 sequence number=2346
#308741 15:23:45... last committed=2346 sequence number=2347
#308741 15:23:45... last committed=2346 sequence number=2348
#308741 15:23:45... last committed=2346 sequence number=2349
#308741 15:23:45... last committed=2348 sequence number=2350
#308741 15:23:45... last committed=2345 sequence number=2351</code></pre>
</div>
<p>This approach excels in environments with highly concurrent workloads on the source server or when large group commit sizes are configured by adjusting the <code>binlog_group_commit_sync_delay</code> parameter, which we explore in detail later. However, this method&rsquo;s effectiveness is heavily influenced by timing factors and doesn&rsquo;t consider the actual independence of data access patterns. As a result, in real-world scenarios, the level of parallelism achieved is often more limited than one might initially expect.</p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer">WRITESET</a> represents a more sophisticated approach to dependency tracking in MySQL replication, moving beyond the simple commit time window used in COMMIT ORDER to track actual data modifications at the row level. For each row modified by a transaction, MySQL generates a unique hash. The collection of these hashes, for a transaction, forms its write sets. This tracking enables parallel execution of transactions on the replica if their write sets don&rsquo;t overlap, regardless of their original execution order on the source, session ownership, or commit window timing. By focusing on real data dependencies rather than temporal relationships, this method can substantially enhance replication parallelization capabilities on replica servers. This can result in improved replication performance and throughput compared to the COMMIT ORDER. However, there are cases WRITESET doesn&rsquo;t outperform COMMIT ORDER, which we explore in detail in the best practices section.</p>
<h2>MTR configurations and best practices<a class="anchor-link" id="mtr-configurations-and-best-practices"></a></h2>
<p>MTR is supported in RDS for MySQL 5.7, 8.0, and 8.4, Amazon RDS for MariaDB 10.0.5 and higher, and Aurora MySQL version 3, and in Aurora MySQL version 2.12.1 and higher. However, to fully benefit from MTR, we strongly recommend upgrading both your source and target or replica servers to more recent versions. As of this writing, optimal performance and operation is achieved with source servers running RDS MySQL 5.7.44+, Aurora MySQL 2.12.5+, or RDS MySQL 8.0.35+, Aurora MySQL 3.10.0+. For replica servers, we advise using RDS MySQL 8.0.35+ and Aurora MySQL 3.10.0+. Now, let&rsquo;s dive into the key configuration parameters and best practices for optimizing MTR in your RDS for MySQL, RDS for MariaDB, and Aurora MySQL environment. Note that parameter names might differ between versions due to updates in terminology.</p>
<h3>In the source<a class="anchor-link" id="in-the-source"></a></h3>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>binlog_transaction_dependency_tracking</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_tracking" target="_blank" rel="noopener noreferrer"></a></p>
<p>The valid values for this parameter are <code>COMMIT_ORDER</code>, <code>WRITESET</code>, and <code>WRITESET_SESSION</code>. We already covered the meaning of <code>COMMIT_ORDER</code> and <code>WRITESET</code>. <code>WRITESET_SESSION</code> is the same thing as WRITESET, however there is an additional constraint applied: Two transactions that were committed in the same client session can&rsquo;t be applied in parallel.</p>
<p>From <a href="https://dev.mysql.com/blog-archive/improving-the-parallel-applier-with-writeset-based-dependency-tracking/" target="_blank" rel="noopener noreferrer">Improving the Parallel Applier with Writeset-based Dependency Tracking</a>, you can see WRITESET and COMMIT_ORDER perform similarly under highly concurrent workload on the source, but WRITESET shows better performance in environments with lower concurrency. This is particularly relevant for real-world applications, which typically have lower concurrent DML levels than artificial benchmark tests such as <a href="https://github.com/akopytov/sysbench" target="_blank" rel="noopener noreferrer"><code>sysbench</code></a>, which are designed to at higher thread counts in most cases. We recommend setting this parameter to <code>WRITESET</code> (or <code>WRITESET_SESSION</code> if your application requires).</p>
<p>There are cases WRITESET can&rsquo;t be used and MySQL fallbacks to non-writeset, including:</p>
<ul>
<li><strong>Tables without primary or unique keys</strong> &ndash; The WRITESET dependency tracking method relies on the ability to uniquely identify modified rows. Without primary or unique keys, the system cannot accurately track the full set of changes made by a transaction. It&rsquo;s important to note that regardless of using WRITESET or not, all InnoDB tables should have an explicit primary or unique key to avoid potential performance issues. See <a href="https://dev.mysql.com/doc/refman/8.0/en/primary-key-optimization.html" target="_blank" rel="noopener noreferrer">primary key optimization</a> for more details.</li>
<li><strong>Transactions with DDL statements</strong> &ndash; DDL statements such as CREATE TABLE or ALTER TABLE modify the database schema rather than only the data. These schema changes aren&rsquo;t easily tracked in the same way as regular data modifications, which can impact the effectiveness of replication dependency tracking. As a best practice, it&rsquo;s recommended to minimize or avoid DDL operations if possible, during periods of high DML activity, which not only benefits replication performance but also aligns with general database management best practices.</li>
<li><strong>Transactions accessing parent tables in foreign key relationships</strong> &ndash; When a transaction modifies data in a child table that has a foreign key relationship, the changes to the parent table might not be fully captured in the write sets, potentially leading to incomplete dependency tracking.</li>
</ul>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_history_size" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_history_size" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>binlog_transaction_dependency_history_size</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_transaction_dependency_history_size" target="_blank" rel="noopener noreferrer"></a></p>
<p>This parameter sets an upper limit on the number of row hashes, which are kept in memory and used for looking up the transaction that last modified a given row. When this number of hashes has been reached, the history is purged. You can increase this parameter on large instance classes such as <code>4xlarge</code> or larger. However, setting this value too high can lead to performance issues because it can lead to excessive memory consumption and high CPU usage for tracking dependencies, so test carefully when configuring so you&rsquo;re aware of trade-off.</p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>binlog_format</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"></a></p>
<p>This system variable sets the binary logging format and can be <code>STATEMENT</code>, <code>ROW</code>, or <code>MIXED</code>. Note that <code>binlog_format</code> is deprecated as of MySQL 8.0.34 and is subject to removal in a future version of MySQL. This implies that support for logging formats other than row-based is also subject to removal in a future release. In RDS for MySQL, RDS for MariaDB, and Aurora MySQL, we recommend using <code>binlog_format=Row</code> for performance and compatibility reasons.</p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_group_commit_sync_delay" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_group_commit_sync_delay" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>binlog_group_commit_sync_delay</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_group_commit_sync_delay" target="_blank" rel="noopener noreferrer"></a></p>
<p>The <code>binlog_group_commit_sync_delay</code> parameter determines how many microseconds the binary log commit waits before synchronizing the binary log file to disk. This setting is particularly effective when used with <code>binlog_transaction_dependency_tracking = COMMIT_ORDER</code>. It has no effect on dependency tracking when <code>binlog_transaction_dependency_tracking = WRITESET</code>. By intentionally introducing a slight delay in the commit process on the source, MySQL can batch more writes into each group commit, creating larger commit windows and increasing parallel execution on replicas. However, this optimization comes with a trade-off: It also increases transaction latency on the source server, which might impact client application performance. We strongly recommend thorough testing to find the optimal value that balances improved replication performance against acceptable transaction latency for your specific use case.</p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/optimizing-innodb-transaction-management.html" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/optimizing-innodb-transaction-management.html" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>Transaction size</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/optimizing-innodb-transaction-management.html" target="_blank" rel="noopener noreferrer"></a></p>
<p>In MySQL, maintaining appropriate transaction sizes is important for optimal performance. Large transactions can lead to several issues: They hold locks for extended periods, potentially blocking other operations and reducing overall system throughput, while also increasing <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/proactive-insights.history-list.html" target="_blank" rel="noopener noreferrer">RollbackSegmentHistoryListLength</a>, which can impact overall database performance. In the context of MTR, large transactions might significantly limit parallelism on replica servers, leading to replication lag, which is discussed in detail in the <code>replica_pending_jobs_size_max</code> section. To mitigate these issues, it&rsquo;s recommended to avoid large transactions whenever possible. For operations that must modify substantial amounts of data, consider breaking them into smaller, manageable chunks that can be processed in separate transactions.</p>
<h3>In the replica<a class="anchor-link" id="in-the-replica"></a></h3>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>binlog_format</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-binary-log.html#sysvar_binlog_format" target="_blank" rel="noopener noreferrer"></a></p>
<p>Aurora MySQL doesn&rsquo;t require binlogs enabled for intra-cluster replication and backup and recovery, so if you don&rsquo;t have downstream replication, we recommend setting <code>binlog_format</code> to <code>OFF</code> in the DB cluster parameter group to disable binary logging on the Aurora MySQL DB replica cluster. This helps boost replication performance. Setting <code>binlog_format</code> to <code>OFF</code> resets the <code>binlog_format</code> session variable to the default value of ROW in the database. For RDS for MySQL or RDS for MariaDB replica servers, you can achieve similar performance gains by <a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.Enabling.html" target="_blank" rel="noopener noreferrer">disabling automated backups</a>, which in turn disables binary logging. However, this comes with the trade-off that it removes the ability to perform point-in-time recovery for the replica.</p>
<h4><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_parallel_type" target="_blank" rel="noopener noreferrer">replica_parallel_type</a> or slave_parallel_type</h4>
<p>The possible values are:</p>
<ul>
<li><code>LOGICAL_CLOCK</code> &ndash; Transactions are applied in parallel on the replica, based on timestamps that the replication source writes to the binary log. Dependencies between transactions are tracked based on their logical timestamps to provide additional parallelization where possible.</li>
<li><code>DATABASE</code> &ndash; Transactions that update different databases are applied in parallel. This value is only appropriate if data is partitioned into multiple databases that are being updated independently and concurrently on the source. There must be no cross-database constraints because such constraints might be violated on the replica.</li>
</ul>
<p>We recommend setting <code>LOGICAL_CLOCK</code> because it provides more granular dependency tracking and better parallelism, unless you have a specific use case to use DATABASE. In all RDS for MySQL 8.0 and Aurora MySQL 3 versions, the default value is <code>LOGICAL_CLOCK.</code></p>
<h4><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_parallel_workers" target="_blank" rel="noopener noreferrer">replica_parallel_workers</a> or slave_parallel_workers</h4>
<p>Setting this parameter to a value higher than 1 enables MTR on the replica and sets the number of applier threads for executing replication transactions in parallel. Prior to RDS for MySQL 8.0.27 and Aurora MySQL 3.04.0, the default value of this system variable is 0, so replicas use a single worker thread by default. Beginning with RDS for MySQL 8.0.27 and Aurora MySQL 3.04.0, the default value is 4, which means that replicas are multithreaded by default. The optimal value for <code>replica_parallel_workers</code> depends on your specific hardware and workload characteristics. If your server is larger than <code>2xlarge</code>, you can start with four worker threads, but you need to monitor and observe to tune this parameter, which we will cover in <a href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/" target="_blank" rel="noopener noreferrer">our next post</a>. We don&rsquo;t recommend setting this parameter too high, as beyond a certain point, it can even reduce performance due to concurrency effects such as lock contention.</p>
<p>Setting <code>replica_parallel_workers</code> has no immediate effect. You would need to restart the replication by using <code>mysql.rds_stop_replication</code> and <code>mysql.rds_start_replication</code> statements. Tables without primary keys, which usually harm performance as mentioned previously, might have even greater negative performance impact on replicas having <code>replica_parallel_workers</code> greater than 1.</p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#sysvar_replica_pending_jobs_size_max" target="_blank" rel="noopener noreferrer"></a></p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#sysvar_replica_pending_jobs_size_max" target="_blank" rel="noopener noreferrer"> </a></p>
<h4>replica_pending_jobs_size_max</h4>
<p> </p>
<p><a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#sysvar_replica_pending_jobs_size_max" target="_blank" rel="noopener noreferrer"></a></p>
<p>This variable sets the maximum memory available for queues holding events yet to be applied by worker threads on replica servers. The value of this variable is a soft limit and can be set to match the normal workload. If an unusually large event exceeds this size, the transaction is held until all the worker threads have empty queues and then processed. All subsequent transactions are held until the large transaction has been completed. Although this ensures event processing, it can lead to significantly reduced worker concurrency, hence replication lag. Therefore, it&rsquo;s important to set this parameter high enough to handle your typical event sizes. Additionally, on multithreaded replicas, this value should be at least equal to, if not greater than, the <code>max_allowed_packet</code> setting on the source to prevent replication failures due to large packets, as mentioned in <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-features-max-allowed-packet.html" target="_blank" rel="noopener noreferrer">the MySQL documentation</a>.</p>
<h4><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Reference.ParameterGroups.html#AuroraMySQL.Reference.Parameters.Cluster" target="_blank" rel="noopener noreferrer">aurora_binlog_replication_sec_index_parallel_workers</a> (Aurora MySQL only)</h4>
<p>In Aurora MySQL version 3.06 and higher, you can improve performance for binary log replicas when replicating transactions for large tables with more than one secondary index. This feature introduces a thread pool to apply secondary index changes in parallel on a binlog replica. The feature is controlled by the <code>aurora_binlog_replication_sec_index_parallel_workers</code> DB cluster parameter, which controls the total number of parallel threads available to apply the secondary index changes. The parameter is set to <code>0</code> (disabled) by default. Enabling this feature doesn&rsquo;t require an instance restart. To enable this feature, <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/mysql-stored-proc-replicating.html#mysql_rds_stop_replication" target="_blank" rel="noopener noreferrer">stop ongoing replication</a>, set the desired number of parallel worker threads, and then start replication again.</p>
<h4><a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.3100.html" target="_blank" rel="noopener noreferrer">aurora_in_memory_relaylog</a> (Aurora MySQL only)</h4>
<p>Aurora MySQL version 3.10 extends the in-memory relay log cache support for binary log replicas. This feature, first introduced in <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.Updates.3050.html" target="_blank" rel="noopener noreferrer">version 3.05</a>, can improve binary log replication throughput by up to 40%. The in-memory relay log cache is enabled by default for single-threaded binary log replication, multi-threaded replication with <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-auto-positioning.html" target="_blank" rel="noopener noreferrer">GTID auto-positioning</a> enabled, and starting with version 3.10, it&rsquo;s also enabled for multi-threaded replication with <a href="https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#sysvar_replica_preserve_commit_order" target="_blank" rel="noopener noreferrer">replica_preserve_commit_order = ON</a> (even without GTIDs).</p>
<h4><a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_preserve_commit_order" target="_blank" rel="noopener noreferrer">replica_preserve_commit_order</a> or <strong>slave_preserve_commit_order</strong></h4>
<p>When <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_preserve_commit_order" target="_blank" rel="noopener noreferrer"><code>replica_preserve_commit_order</code></a> (or <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_slave_preserve_commit_order" target="_blank" rel="noopener noreferrer"><code>slave_preserve_commit_order</code></a>) is set to <code>ON</code> (the default in MySQL 8.0.27 and later), transactions are executed and committed on the replica in the same order as they appear in the replica&rsquo;s relay log. This prevents gaps in the sequence of transactions that have been executed from the replica&rsquo;s relay log and preserves the same transaction history on the replica as on the source. When <a href="https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-options-replica.html#sysvar_replica_preserve_commit_order" target="_blank" rel="noopener noreferrer"><code>replica_preserve_commit_order=ON</code></a> is set, the executing worker thread waits until all previous transactions are committed before committing. Although a given thread is waiting for other worker threads to commit their transactions, it reports its status as <code>Waiting for preceding transaction to commit</code>. This might slightly reduce the level of parallelism in the replica, but it&rsquo;s recommended for data consistency, especially for applications that depend on commit order.</p>
<p>Lastly, to increase the resilience of your database against unexpected halts, we recommend that you enable global transaction identifier (GTID) replication on the source and allow GTIDs on the replica. To allow GTID replication, set <code>gtid_mode</code> to <code>ON_PERMISSIVE</code> on both the source and replica. For more information about GTID-based replication, see <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/mysql-replication-gtid.html" target="_blank" rel="noopener noreferrer">Using GTID-based replication</a>.</p>
<h2>Conclusion<a class="anchor-link" id="conclusion"></a></h2>
<p>In this post, we discussed how MySQL replication and MTR works and its key configuration options and best practices. In <a href="https://aws.amazon.com/blogs/database/monitoring-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-aurora-mysql/" rel="noopener" target="_blank">our next post</a>, we will discuss methods to effectively monitor parallel replication performance.</p>
<hr>
<h3>About the authors<a class="anchor-link" id="about-the-authors"></a></h3>
<footer>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2024/03/01/ngyenjh_image-1.png" alt="Huy Nguyen" width="120" height="160">
  </div>
<h3 class="lb-h4">Huy Nguyen<a class="anchor-link" id="huy-nguyen"></a></h3>
<p><a target="_blank" href="https://www.linkedin.com/in/huy-nguyen-6a0a4960/" rel="noopener">Huy</a> is a Senior Engineer in AWS Support. He specializes in Amazon RDS, Amazon Aurora. He provides guidance and technical assistance to customers, enabling them to build scalable, highly available, and secure solutions in the AWS Cloud</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/10/09/Screenshot-2025-10-09-at-4.48.12%E2%80%AFPM-100x100.png" alt="Arun Gadila" width="120" height="160">
  </div>
<h3 class="lb-h4">Arun Gadila<a class="anchor-link" id="arun-gadila"></a></h3>
<p><a target="_blank" href="https://www.linkedin.com/in/arun-kumar-gadila/" rel="noopener">Arun</a> is a Cloud Support Database Engineer II at AWS with over 3.5 years of expertise specializing in RDS for MySQL, Aurora MySQL, and RDS for SQL Server. Recognized as a Subject Matter Expert (SME) in both RDS MySQL and Aurora MySQL, demonstrating deep technical knowledge in these services. Dedicated to helping customers optimize their database environments and resolve complex challenges across AWS&rsquo;s managed database offerings</p>
</div>
<div class="blog-author-box">
<div class="blog-author-image">
   <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-29797" src="https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2025/10/09/reillym-100x132.png" alt="Marc Reilly" width="120" height="160">
  </div>
<h3 class="lb-h4">Marc Reilly<a class="anchor-link" id="marc-reilly"></a></h3>
<p><a target="_blank" href="https://www.linkedin.com/in/marcreillyirl/" rel="noopener">Marc</a> is a Senior Database engineer on the Amazon Aurora MySQL team.</p>
</div>
</footer>

<p><a href="https://aws.amazon.com/blogs/database/overview-and-best-practices-of-multithreaded-replication-in-amazon-rds-for-mysql-amazon-rds-for-mariadb-and-amazon-aurora-mysql/">Overview and best practices of multithreaded replication in Amazon RDS for MySQL, Amazon RDS for MariaDB, and Amazon Aurora MySQL</a> appeared first on <a href="https://mariadb.org">MariaDB.org</a></p>
]]></content>
    </entry>
      <entry>
      <title>Could the XZ backdoor have been detected with better Git and Debian packaging practices?</title>
      <link rel="alternate" type="text/html" href="https://optimizedbyotto.com/post/xz-backdoor-debian-git-detection/" />
      <id>https://optimizedbyotto.com/post/xz-backdoor-debian-git-detection/</id>
      <updated>2025-10-19T00:00:00+03:00</updated>
      <author><name></name></author>
      <summary type="html"><![CDATA[<p>The discovery of a backdoor in XZ Utils in the spring of 2024 shocked the open source community, raising critical questions about software supply chain security. This post explores whether better Debian packaging practices could have detected this threat, offering a guide to auditing packages and suggesting future improvements.<br />
The XZ backdoor in versions 5.6.0/5.6.1 made its way briefly into many major Linux distributions such as Debian and Fedora, but luckily didn’t reach that many actual users, as the backdoored releases were quickly removed thanks to the heroic diligence of Andres Freund. We are all extremely lucky that he detected a half a second performance regression in SSH, cared enough to trace it down, discovered malicious code in the XZ library loaded by SSH, and reported promtly to various security teams for quick coordinated actions.<br />
This episode makes software engineers ponder the following questions:</p>
<p>Why didn’t any Linux distro packagers notice anything odd when importing the new XZ version 5.6.0/5.6.1 from upstream?<br />
Is the current software supply-chain in the most popular Linux distros easy to audit?<br />
Could we have similar backdoors lurking that haven’t been detected yet?</p>
<p>As a Debian Developer, I decided to audit the xz package in Debian, share my methodology and findings in this post, and also suggest some improvements on how the software supply-chain security could be tightened in Debian specifically.<br />
Note that the scope here is only to inspect how Debian imports software from its upstreams, and how they are distributed to Debian’s users. This excludes the whole story of how to assess if an upstream project is following software development security best practices. This post doesn’t discuss how to operate an individual computer running Debian to ensure it remains untampered as there are plenty of guides on that already.<br />
Downloading Debian and upstream source packages<br />
Let’s start by working backwards from what the Debian package repositories offer for download. As auditing binaries is extremely complicated, we skip that, and assume the Debian build hosts are trustworthy and reliably building binaries from the source packages, and the focus should be on auditing the source code packages.<br />
As with everything in Debian, there are multiple tools and ways to do the same thing, but in this post only one (and hopefully the best) way to do something is presented for brevity.<br />
The first step is to download the latest version and some past versions of the package from the Debian archive, which is easiest done with debsnap. The following command will download all Debian source packages of xz-utils from Debian release 5.2.4-1 onwards:</p>
<p>Copy</p>
<p>$ debsnap --verbose --first 5.2.4-1 xz-utils<br />
Getting json https://snapshot.debian.org/mr/package/xz-utils/<br />
...<br />
Getting dsc file xz-utils_5.2.4-1.dsc: https://snapshot.debian.org/file/a98271e4291bed8df795ce04d9dc8e4ce959462d<br />
Getting file xz-utils_5.2.4.orig.tar.xz.asc: https://snapshot.debian.org/file/59ccbfb2405abe510999afef4b374cad30c09275<br />
Getting file xz-utils_5.2.4-1.debian.tar.xz: https://snapshot.debian.org/file/667c14fd9409ca54c397b07d2d70140d6297393f<br />
source-xz-utils/xz-utils_5.2.4-1.dsc:<br />
Good signature found<br />
validating xz-utils_5.2.4.orig.tar.xz<br />
validating xz-utils_5.2.4.orig.tar.xz.asc<br />
validating xz-utils_5.2.4-1.debian.tar.xz<br />
All files validated successfully.$ debsnap --verbose --first 5.2.4-1 xz-utils<br />
Getting json https://snapshot.debian.org/mr/package/xz-utils/<br />
...<br />
Getting dsc file xz-utils_5.2.4-1.dsc: https://snapshot.debian.org/file/a98271e4291bed8df795ce04d9dc8e4ce959462d<br />
Getting file xz-utils_5.2.4.orig.tar.xz.asc: https://snapshot.debian.org/file/59ccbfb2405abe510999afef4b374cad30c09275<br />
Getting file xz-utils_5.2.4-1.debian.tar.xz: https://snapshot.debian.org/file/667c14fd9409ca54c397b07d2d70140d6297393f<br />
source-xz-utils/xz-utils_5.2.4-1.dsc:<br />
Good signature found<br />
validating xz-utils_5.2.4.orig.tar.xz<br />
validating xz-utils_5.2.4.orig.tar.xz.asc<br />
validating xz-utils_5.2.4-1.debian.tar.xz<br />
All files validated successfully.<br />
Once debsnap completes there will be a subfolder source- with the following types of files:</p>
<p>*.orig.tar.xz: source code from upstream<br />
*.orig.tar.xz.asc: detached signature (if upstream signs their releases)<br />
*.debian.tar.xz: Debian packaging source, i.e. the debian/ subdirectory contents<br />
*.dsc: Debian source control file, including signature by Debian Developer/Maintainer</p>
<p>Example:</p>
<p>Copy</p>
<p>$ ls -1 source-xz-utils/<br />
...<br />
xz-utils_5.6.4.orig.tar.xz<br />
xz-utils_5.6.4.orig.tar.xz.asc<br />
xz-utils_5.6.4-1.debian.tar.xz<br />
xz-utils_5.6.4-1.dsc<br />
xz-utils_5.8.0.orig.tar.xz<br />
xz-utils_5.8.0.orig.tar.xz.asc<br />
xz-utils_5.8.0-1.debian.tar.xz<br />
xz-utils_5.8.0-1.dsc<br />
xz-utils_5.8.1.orig.tar.xz<br />
xz-utils_5.8.1.orig.tar.xz.asc<br />
xz-utils_5.8.1-1.1.debian.tar.xz<br />
xz-utils_5.8.1-1.1.dsc<br />
xz-utils_5.8.1-1.debian.tar.xz<br />
xz-utils_5.8.1-1.dsc<br />
xz-utils_5.8.1-2.debian.tar.xz<br />
xz-utils_5.8.1-2.dsc$ ls -1 source-xz-utils/<br />
...<br />
xz-utils_5.6.4.orig.tar.xz<br />
xz-utils_5.6.4.orig.tar.xz.asc<br />
xz-utils_5.6.4-1.debian.tar.xz<br />
xz-utils_5.6.4-1.dsc<br />
xz-utils_5.8.0.orig.tar.xz<br />
xz-utils_5.8.0.orig.tar.xz.asc<br />
xz-utils_5.8.0-1.debian.tar.xz<br />
xz-utils_5.8.0-1.dsc<br />
xz-utils_5.8.1.orig.tar.xz<br />
xz-utils_5.8.1.orig.tar.xz.asc<br />
xz-utils_5.8.1-1.1.debian.tar.xz<br />
xz-utils_5.8.1-1.1.dsc<br />
xz-utils_5.8.1-1.debian.tar.xz<br />
xz-utils_5.8.1-1.dsc<br />
xz-utils_5.8.1-2.debian.tar.xz<br />
xz-utils_5.8.1-2.dsc<br />
Verifying authenticity of upstream and Debian sources using OpenPGP signatures<br />
As seen in the output of debsnap, it already automatically verifies that the downloaded files match the OpenPGP signatures. To have full clarity on what files were authenticated with what keys, we should verify the Debian packagers signature with:</p>
<p>Copy</p>
<p>$ gpg --verify --auto-key-retrieve --keyserver hkps://keyring.debian.org xz-utils_5.8.1-2.dsc<br />
gpg: Signature made Fri Oct 3 22:04:44 2025 UTC<br />
gpg: using RSA key 57892E705233051337F6FDD105641F175712FA5B<br />
gpg: requesting key 05641F175712FA5B from hkps://keyring.debian.org<br />
gpg: key 7B96E8162A8CF5D1: public key \"Sebastian Andrzej Siewior\" imported<br />
gpg: Total number processed: 1<br />
gpg: imported: 1<br />
gpg: Good signature from \"Sebastian Andrzej Siewior\" [unknown]<br />
gpg: aka \"Sebastian Andrzej Siewior \" [unknown]<br />
gpg: aka \"Sebastian Andrzej Siewior \" [unknown]<br />
gpg: WARNING: This key is not certified with a trusted signature!<br />
gpg: There is no indication that the signature belongs to the owner.<br />
Primary key fingerprint: 6425 4695 FFF0 AA44 66CC 19E6 7B96 E816 2A8C F5D1<br />
Subkey fingerprint: 5789 2E70 5233 0513 37F6 FDD1 0564 1F17 5712 FA5B$ gpg --verify --auto-key-retrieve --keyserver hkps://keyring.debian.org xz-utils_5.8.1-2.dsc<br />
gpg: Signature made Fri Oct 3 22:04:44 2025 UTC<br />
gpg: using RSA key 57892E705233051337F6FDD105641F175712FA5B<br />
gpg: requesting key 05641F175712FA5B from hkps://keyring.debian.org<br />
gpg: key 7B96E8162A8CF5D1: public key \"Sebastian Andrzej Siewior\" imported<br />
gpg: Total number processed: 1<br />
gpg: imported: 1<br />
gpg: Good signature from \"Sebastian Andrzej Siewior\" [unknown]<br />
gpg: aka \"Sebastian Andrzej Siewior \" [unknown]<br />
gpg: aka \"Sebastian Andrzej Siewior \" [unknown]<br />
gpg: WARNING: This key is not certified with a trusted signature!<br />
gpg: There is no indication that the signature belongs to the owner.<br />
Primary key fingerprint: 6425 4695 FFF0 AA44 66CC 19E6 7B96 E816 2A8C F5D1<br />
Subkey fingerprint: 5789 2E70 5233 0513 37F6 FDD1 0564 1F17 5712 FA5B<br />
The upstream tarball signature (if available) can be verified with:</p>
<p>Copy</p>
<p>$ gpg --verify --auto-key-retrieve xz-utils_5.8.1.orig.tar.xz.asc<br />
gpg: assuming signed data in \'xz-utils_5.8.1.orig.tar.xz\'<br />
gpg: Signature made Thu Apr 3 11:38:23 2025 UTC<br />
gpg: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpg: key 38EE757D69184620: public key \"Lasse Collin \" imported<br />
gpg: Total number processed: 1<br />
gpg: imported: 1<br />
gpg: Good signature from \"Lasse Collin \" [unknown]<br />
gpg: WARNING: This key is not certified with a trusted signature!<br />
gpg: There is no indication that the signature belongs to the owner.<br />
Primary key fingerprint: 3690 C240 CE51 B467 0D30 AD1C 38EE 757D 6918 4620$ gpg --verify --auto-key-retrieve xz-utils_5.8.1.orig.tar.xz.asc<br />
gpg: assuming signed data in \'xz-utils_5.8.1.orig.tar.xz\'<br />
gpg: Signature made Thu Apr 3 11:38:23 2025 UTC<br />
gpg: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpg: key 38EE757D69184620: public key \"Lasse Collin \" imported<br />
gpg: Total number processed: 1<br />
gpg: imported: 1<br />
gpg: Good signature from \"Lasse Collin \" [unknown]<br />
gpg: WARNING: This key is not certified with a trusted signature!<br />
gpg: There is no indication that the signature belongs to the owner.<br />
Primary key fingerprint: 3690 C240 CE51 B467 0D30 AD1C 38EE 757D 6918 4620<br />
Note that this only proves that there is a key that created a valid signature for this content. The authenticity of the keys themselves need to be validated separately before trusting they in fact are the keys of these people. That can be done by checking e.g. the upstream website for what key fingerprints they published, or the Debian keyring for Debian Developers and Maintainers, or by relying on the OpenPGP “web-of-trust”.<br />
Verifying authenticity of upstream sources by comparing checksums<br />
In case the upstream in question does not publish release signatures, the second best way to verify the authenticity of the sources used in Debian is to download the sources directly from upstream and compare that the sha256 checksums match.<br />
This should be done using the debian/watch file inside the Debian packaging, which defines where the upstream source is downloaded from. Continuing on the example situation above, we can unpack the latest Debian sources, enter and then run uscan to download:</p>
<p>Copy</p>
<p>$ tar xvf xz-utils_5.8.1-2.debian.tar.xz<br />
...<br />
debian/rules<br />
debian/source/format<br />
debian/source.lintian-overrides<br />
debian/symbols<br />
debian/tests/control<br />
debian/tests/testsuite<br />
debian/upstream/signing-key.asc<br />
debian/watch<br />
...<br />
$ uscan --download-current-version --destdir /tmp<br />
Newest version of xz-utils on remote site is 5.8.1, specified download version is 5.8.1<br />
gpgv: Signature made Thu Apr 3 11:38:23 2025 UTC<br />
gpgv: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpgv: Good signature from \"Lasse Collin \"<br />
Successfully symlinked /tmp/xz-5.8.1.tar.xz to /tmp/xz-utils_5.8.1.orig.tar.xz.$ tar xvf xz-utils_5.8.1-2.debian.tar.xz<br />
...<br />
debian/rules<br />
debian/source/format<br />
debian/source.lintian-overrides<br />
debian/symbols<br />
debian/tests/control<br />
debian/tests/testsuite<br />
debian/upstream/signing-key.asc<br />
debian/watch<br />
...<br />
$ uscan --download-current-version --destdir /tmp<br />
Newest version of xz-utils on remote site is 5.8.1, specified download version is 5.8.1<br />
gpgv: Signature made Thu Apr 3 11:38:23 2025 UTC<br />
gpgv: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpgv: Good signature from \"Lasse Collin \"<br />
Successfully symlinked /tmp/xz-5.8.1.tar.xz to /tmp/xz-utils_5.8.1.orig.tar.xz.<br />
The original files downloaded from upstream are now in /tmp along with the files renamed to follow Debian conventions. Using everything downloaded so far the sha256 checksums can be compared across the files and also to what the .dsc file advertised:</p>
<p>Copy</p>
<p>$ ls -1 /tmp/<br />
xz-5.8.1.tar.xz<br />
xz-5.8.1.tar.xz.sig<br />
xz-utils_5.8.1.orig.tar.xz<br />
xz-utils_5.8.1.orig.tar.xz.asc<br />
$ sha256sum xz-utils_5.8.1.orig.tar.xz /tmp/xz-5.8.1.tar.xz<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e xz-utils_5.8.1.orig.tar.xz<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e /tmp/xz-5.8.1.tar.xz<br />
$ grep -A 3 Sha256 xz-utils_5.8.1-2.dsc<br />
Checksums-Sha256:<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e 1461872 xz-utils_5.8.1.orig.tar.xz<br />
4138f4ceca1aa7fd2085fb15a23f6d495d27bca6d3c49c429a8520ea622c27ae 833 xz-utils_5.8.1.orig.tar.xz.asc<br />
3ed458da17e4023ec45b2c398480ed4fe6a7bfc1d108675ec837b5ca9a4b5ccb 24648 xz-utils_5.8.1-2.debian.tar.xz$ ls -1 /tmp/<br />
xz-5.8.1.tar.xz<br />
xz-5.8.1.tar.xz.sig<br />
xz-utils_5.8.1.orig.tar.xz<br />
xz-utils_5.8.1.orig.tar.xz.asc<br />
$ sha256sum xz-utils_5.8.1.orig.tar.xz /tmp/xz-5.8.1.tar.xz<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e xz-utils_5.8.1.orig.tar.xz<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e /tmp/xz-5.8.1.tar.xz<br />
$ grep -A 3 Sha256 xz-utils_5.8.1-2.dsc<br />
Checksums-Sha256:<br />
0b54f79df85912504de0b14aec7971e3f964491af1812d83447005807513cd9e 1461872 xz-utils_5.8.1.orig.tar.xz<br />
4138f4ceca1aa7fd2085fb15a23f6d495d27bca6d3c49c429a8520ea622c27ae 833 xz-utils_5.8.1.orig.tar.xz.asc<br />
3ed458da17e4023ec45b2c398480ed4fe6a7bfc1d108675ec837b5ca9a4b5ccb 24648 xz-utils_5.8.1-2.debian.tar.xz<br />
In the example above the checksum 0b54f79df85... is the same across the files, so it is a match.<br />
Repackaged upstream sources can’t be verified as easily<br />
Note that uscan may in rare cases repackage some upstream sources, for example to exclude files that don’t adhere to Debian’s copyright and licensing requirements. Those files and paths would be listed under the Files-Excluded section in the debian/copyright file. There are also other situations where the file that represents the upstream sources in Debian isn’t bit-by-bit the same as what upstream published. If checksums don’t match, an experienced Debian Developer should review all package settings (e.g. debian/source/options) to see if there was a valid and intentional reason for divergence.<br />
Reviewing changes between two source packages using diffoscope<br />
Diffoscope is an incredibly capable and handy tool to compare arbitrary files. For example, to view a report in HTML format of the differences between two XZ releases, run:</p>
<p>Copy</p>
<p>diffoscope --html-dir xz-utils-5.6.4_vs_5.8.0 xz-utils_5.6.4.orig.tar.xz xz-utils_5.8.0.orig.tar.xz<br />
browse xz-utils-5.6.4_vs_5.8.0/index.htmldiffoscope --html-dir xz-utils-5.6.4_vs_5.8.0 xz-utils_5.6.4.orig.tar.xz xz-utils_5.8.0.orig.tar.xz<br />
browse xz-utils-5.6.4_vs_5.8.0/index.html</p>
<p>If the changes are extensive, and you want to use a LLM to help spot potential security issues, generate the report of both the upstream and Debian packaging differences in Markdown with:</p>
<p>Copy</p>
<p>diffoscope --markdown diffoscope-debian.md xz-utils_5.6.4-1.debian.tar.xz xz-utils_5.8.1-2.debian.tar.xz<br />
diffoscope --markdown diffoscope.md xz-utils_5.6.4.orig.tar.xz xz-utils_5.8.0.orig.tar.xzdiffoscope --markdown diffoscope-debian.md xz-utils_5.6.4-1.debian.tar.xz xz-utils_5.8.1-2.debian.tar.xz<br />
diffoscope --markdown diffoscope.md xz-utils_5.6.4.orig.tar.xz xz-utils_5.8.0.orig.tar.xz<br />
The Markdown files created above can then be passed to your favorite LLM, along with a prompt such as:</p>
<p>Based on the attached diffoscope output for a new Debian package version compared with the previous one, list all suspicious changes that might have introduced a backdoor, followed by other potential security issues. If there are none, list a short summary of changes as the conclusion.</p>
<p>Reviewing Debian source packages in version control<br />
As of today only 93% of all Debian source packages are tracked in git on Debian’s GitLab instance at salsa.debian.org. Some key packages such as Coreutils and Bash are not using version control at all, as their maintainers apparently don’t see value in using git for Debian packaging, and the Debian Policy does not require it. Thus, the only reliable and consistent way to audit changes in Debian packages is to compare the full versions from the archive as shown above.<br />
However, for packages that are hosted on Salsa, one can view the git history to gain additional insight into what exactly changed, when and why. For packages that are using version control, their location can be found in the Git-Vcs header in the debian/control file. For xz-utils the location is salsa.debian.org/debian/xz-utils.<br />
Note that the Debian policy does not state anything about how Salsa should be used, or what git repository layout or development practices to follow. In practice most packages follow the DEP-14 proposal, and use git-buildpackage as the tool for managing changes and pushing and pulling them between upstream and salsa.debian.org.<br />
To get the XZ Utils source, run:</p>
<p>Copy</p>
<p>$ gbp clone https://salsa.debian.org/debian/xz-utils.git<br />
gbp:info: Cloning from \'https://salsa.debian.org/debian/xz-utils.git\'$ gbp clone https://salsa.debian.org/debian/xz-utils.git<br />
gbp:info: Cloning from \'https://salsa.debian.org/debian/xz-utils.git\'<br />
At the time of writing this post the git history shows:</p>
<p>Copy</p>
<p>$ git log --graph --oneline<br />
* bb787585 (HEAD - &#62; debian/unstable, origin/debian/unstable, origin/HEAD) Prepare 5.8.1-2<br />
* 4b769547 d: Remove the symlinks from -dev package.<br />
* a39f3428 Correct the nocheck build profile<br />
* 1b806b8d Import Debian changes 5.8.1-1.1<br />
* b1cad34b Prepare 5.8.1-1<br />
* a8646015 Import 5.8.1<br />
* 2808ec2d Update upstream source from tag \'upstream/5.8.1\'<br />
&#124;<br />
&#124; * fa1e8796 (origin/upstream/v5.8, upstream/v5.8) New upstream version 5.8.1<br />
&#124; * a522a226 Bump version and soname for 5.8.1<br />
&#124; * 1c462c2a Add NEWS for 5.8.1<br />
&#124; * 513cabcf Tests: Call lzma_code() in smaller chunks in fuzz_common.h<br />
&#124; * 48440e24 Tests: Add a fuzzing target for the multithreaded .xz decoder<br />
&#124; * 0c80045a liblzma: mt dec: Fix lack of parallelization in single-shot decoding<br />
&#124; * 81880488 liblzma: mt dec: Don\'t modify thr- &#62;in_size in the worker thread<br />
&#124; * d5a2ffe4 liblzma: mt dec: Don\'t free the input buffer too early (CVE-2025-31115)<br />
&#124; * c0c83596 liblzma: mt dec: Simplify by removing the THR_STOP state<br />
&#124; * 831b55b9 liblzma: mt dec: Fix a comment<br />
&#124; * b9d168ee liblzma: Add assertions to lzma_bufcpy()<br />
&#124; * c8e0a489 DOS: Update Makefile to fix the build<br />
&#124; * 307c02ed sysdefs.h: Avoid even with C11 compilers<br />
&#124; * 7ce38b31 Update THANKS<br />
&#124; * 688e51bd Translations: Update the Croatian translation<br />
* &#124; a6b54dde Prepare 5.8.0-1.<br />
* &#124; 77d9470f Add 5.8 symbols.<br />
* &#124; 9268eb66 Import 5.8.0<br />
* &#124; 6f85ef4f Update upstream source from tag \'upstream/5.8.0\'<br />
&#124;<br />
&#124; * &#124; afba662b New upstream version 5.8.0<br />
&#124; &#124;/<br />
&#124; * 173fb5c6 doc/SHA256SUMS: Add 5.8.0<br />
&#124; * db9258e8 Bump version and soname for 5.8.0<br />
&#124; * bfb752a3 Add NEWS for 5.8.0<br />
&#124; * 6ccbb904 Translations: Run \"make -C po update-po\"<br />
&#124; * 891a5f05 Translations: Run po4a/update-po<br />
&#124; * 4f52e738 Translations: Partially fix overtranslation in Serbian man pages<br />
&#124; * ff5d9447 liblzma: Count the extra bytes in LZMA/LZMA2 decoder memory usage<br />
&#124; * 943b012d liblzma: Use SSE2 intrinsics instead of memcpy() in dict_repeat()$ git log --graph --oneline<br />
* bb787585 (HEAD - &#62; debian/unstable, origin/debian/unstable, origin/HEAD) Prepare 5.8.1-2<br />
* 4b769547 d: Remove the symlinks from -dev package.<br />
* a39f3428 Correct the nocheck build profile<br />
* 1b806b8d Import Debian changes 5.8.1-1.1<br />
* b1cad34b Prepare 5.8.1-1<br />
* a8646015 Import 5.8.1<br />
* 2808ec2d Update upstream source from tag \'upstream/5.8.1\'<br />
&#124;<br />
&#124; * fa1e8796 (origin/upstream/v5.8, upstream/v5.8) New upstream version 5.8.1<br />
&#124; * a522a226 Bump version and soname for 5.8.1<br />
&#124; * 1c462c2a Add NEWS for 5.8.1<br />
&#124; * 513cabcf Tests: Call lzma_code() in smaller chunks in fuzz_common.h<br />
&#124; * 48440e24 Tests: Add a fuzzing target for the multithreaded .xz decoder<br />
&#124; * 0c80045a liblzma: mt dec: Fix lack of parallelization in single-shot decoding<br />
&#124; * 81880488 liblzma: mt dec: Don\'t modify thr- &#62;in_size in the worker thread<br />
&#124; * d5a2ffe4 liblzma: mt dec: Don\'t free the input buffer too early (CVE-2025-31115)<br />
&#124; * c0c83596 liblzma: mt dec: Simplify by removing the THR_STOP state<br />
&#124; * 831b55b9 liblzma: mt dec: Fix a comment<br />
&#124; * b9d168ee liblzma: Add assertions to lzma_bufcpy()<br />
&#124; * c8e0a489 DOS: Update Makefile to fix the build<br />
&#124; * 307c02ed sysdefs.h: Avoid even with C11 compilers<br />
&#124; * 7ce38b31 Update THANKS<br />
&#124; * 688e51bd Translations: Update the Croatian translation<br />
* &#124; a6b54dde Prepare 5.8.0-1.<br />
* &#124; 77d9470f Add 5.8 symbols.<br />
* &#124; 9268eb66 Import 5.8.0<br />
* &#124; 6f85ef4f Update upstream source from tag \'upstream/5.8.0\'<br />
&#124;<br />
&#124; * &#124; afba662b New upstream version 5.8.0<br />
&#124; &#124;/<br />
&#124; * 173fb5c6 doc/SHA256SUMS: Add 5.8.0<br />
&#124; * db9258e8 Bump version and soname for 5.8.0<br />
&#124; * bfb752a3 Add NEWS for 5.8.0<br />
&#124; * 6ccbb904 Translations: Run \"make -C po update-po\"<br />
&#124; * 891a5f05 Translations: Run po4a/update-po<br />
&#124; * 4f52e738 Translations: Partially fix overtranslation in Serbian man pages<br />
&#124; * ff5d9447 liblzma: Count the extra bytes in LZMA/LZMA2 decoder memory usage<br />
&#124; * 943b012d liblzma: Use SSE2 intrinsics instead of memcpy() in dict_repeat()<br />
This shows both the changes on the debian/unstable branch as well as the intermediate upstream import branch, and the actual real upstream development branch. See my Debian source packages in git explainer for details of what these branches are used for.<br />
To only view changes on the Debian branch, run git log --graph --oneline --first-parent or git log --graph --oneline -- debian.<br />
The Debian branch should only have changes inside the debian/ subdirectory, which is easy to check with:</p>
<p>Copy</p>
<p>$ git diff --stat upstream/v5.8<br />
debian/README.source &#124; 16 +++<br />
debian/autogen.sh &#124; 32 +++++<br />
debian/changelog &#124; 949 ++++++++++++++++++++++++++<br />
...<br />
debian/upstream/signing-key.asc &#124; 52 +++++++++<br />
debian/watch &#124; 4 +<br />
debian/xz-utils.README.Debian &#124; 47 ++++++++<br />
debian/xz-utils.docs &#124; 6 +<br />
debian/xz-utils.install &#124; 28 +++++<br />
debian/xz-utils.postinst &#124; 19 +++<br />
debian/xz-utils.prerm &#124; 10 ++<br />
debian/xzdec.docs &#124; 6 +<br />
debian/xzdec.install &#124; 4 +<br />
33 files changed, 2014 insertions(+)$ git diff --stat upstream/v5.8<br />
debian/README.source &#124; 16 +++<br />
debian/autogen.sh &#124; 32 +++++<br />
debian/changelog &#124; 949 ++++++++++++++++++++++++++<br />
...<br />
debian/upstream/signing-key.asc &#124; 52 +++++++++<br />
debian/watch &#124; 4 +<br />
debian/xz-utils.README.Debian &#124; 47 ++++++++<br />
debian/xz-utils.docs &#124; 6 +<br />
debian/xz-utils.install &#124; 28 +++++<br />
debian/xz-utils.postinst &#124; 19 +++<br />
debian/xz-utils.prerm &#124; 10 ++<br />
debian/xzdec.docs &#124; 6 +<br />
debian/xzdec.install &#124; 4 +<br />
33 files changed, 2014 insertions(+)<br />
All the files outside the debian/ directory originate from upstream, and for example running git blame on them should show only upstream commits:</p>
<p>Copy</p>
<p>$ git blame CMakeLists.txt<br />
22af94128 (Lasse Collin 2024-02-12 17:09:10 +0200 1) # SPDX-License-Identifier: 0BSD<br />
22af94128 (Lasse Collin 2024-02-12 17:09:10 +0200 2)<br />
7e3493d40 (Lasse Collin 2020-02-24 23:38:16 +0200 3) ###############<br />
7e3493d40 (Lasse Collin 2020-02-24 23:38:16 +0200 4) #<br />
426bdc709 (Lasse Collin 2024-02-17 21:45:07 +0200 5) # CMake support for building XZ Utils$ git blame CMakeLists.txt<br />
22af94128 (Lasse Collin 2024-02-12 17:09:10 +0200 1) # SPDX-License-Identifier: 0BSD<br />
22af94128 (Lasse Collin 2024-02-12 17:09:10 +0200 2)<br />
7e3493d40 (Lasse Collin 2020-02-24 23:38:16 +0200 3) ###############<br />
7e3493d40 (Lasse Collin 2020-02-24 23:38:16 +0200 4) #<br />
426bdc709 (Lasse Collin 2024-02-17 21:45:07 +0200 5) # CMake support for building XZ Utils<br />
If the upstream in question signs commits or tags, they can be verified with e.g.:</p>
<p>Copy</p>
<p>$ git verify-tag v5.6.2<br />
gpg: Signature made Wed 29 May 2024 09:39:42 AM PDT<br />
gpg: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpg: issuer \"lasse.collin@tukaani.org\"<br />
gpg: Good signature from \"Lasse Collin \" [expired]<br />
gpg: Note: This key has expired!$ git verify-tag v5.6.2<br />
gpg: Signature made Wed 29 May 2024 09:39:42 AM PDT<br />
gpg: using RSA key 3690C240CE51B4670D30AD1C38EE757D69184620<br />
gpg: issuer \"lasse.collin@tukaani.org\"<br />
gpg: Good signature from \"Lasse Collin \" [expired]<br />
gpg: Note: This key has expired!<br />
The main benefit of reviewing changes in git is the ability to see detailed information about each individual change, instead of just staring at a massive list of changes without any explanations. In this example, to view all the upstream commits since the previous import to Debian, one would view the commit range from afba662b New upstream version 5.8.0 to fa1e8796 New upstream version 5.8.1 with git log --reverse -p afba662b...fa1e8796. However, a far superior way to review changes would be to browse this range using a visual git history viewer, such as gitk. Either way, looking at one code change at a time and reading the git commit message makes the review much easier.</p>
<p>Comparing Debian source packages to git contents<br />
As stated in the beginning of the previous section, and worth repeating, there is no guarantee that the contents in the Debian packaging git repository matches what was actually uploaded to Debian. While the tag2upload project in Debian is getting more and more popular, Debian is still far from having any system to enforce that the git repository would be in sync with the Debian archive contents.<br />
To detect such differences we can run diff across the Debian source packages downloaded with debsnap earlier (path source-xz-utils/xz-utils_5.8.1-2.debian) and the git repository cloned in the previous section (path xz-utils):</p>
<p>diff</p>
<p>Copy</p>
<p>$ diff -u source-xz-utils/xz-utils_5.8.1-2.debian/ xz-utils/debian/<br />
diff -u source-xz-utils/xz-utils_5.8.1-2.debian/changelog xz-utils/debian/changelog<br />
--- debsnap/source-xz-utils/xz-utils_5.8.1-2.debian/changelog 2025-10-03 09:32:16.000000000 -0700<br />
+++ xz-utils/debian/changelog 2025-10-12 12:18:04.623054758 -0700<br />
@@ -5,7 +5,7 @@<br />
* Remove the symlinks from -dev, pointing to the lib package.<br />
(Closes: #1109354)<br />
- -- Sebastian Andrzej Siewior Fri, 03 Oct 2025 18:32:16 +0200<br />
+ -- Sebastian Andrzej Siewior Fri, 03 Oct 2025 18:36:59 +0200$ diff -u source-xz-utils/xz-utils_5.8.1-2.debian/ xz-utils/debian/<br />
diff -u source-xz-utils/xz-utils_5.8.1-2.debian/changelog xz-utils/debian/changelog<br />
--- debsnap/source-xz-utils/xz-utils_5.8.1-2.debian/changelog 2025-10-03 09:32:16.000000000 -0700<br />
+++ xz-utils/debian/changelog 2025-10-12 12:18:04.623054758 -0700<br />
@@ -5,7 +5,7 @@<br />
 * Remove the symlinks from -dev, pointing to the lib package.<br />
 (Closes: #1109354)</p>
<p>- -- Sebastian Andrzej Siewior Fri, 03 Oct 2025 18:32:16 +0200<br />
+ -- Sebastian Andrzej Siewior Fri, 03 Oct 2025 18:36:59 +0200</p>
<p>In the case above diff revealed that the timestamp in the changelog in the version uploaded to Debian is different from what was committed to git. This is not malicious, just a mistake by the maintainer who probably didn’t run gbp tag immediately after upload, but instead some dch command and ended up with having a different timestamps in the git compared to what was actually uploaded to Debian.<br />
Creating synthetic Debian packaging git repositories<br />
If n