Sensitive columns — SSNs, credit cards, emails, addresses — must stay queryable for support, analytics, and development. Broad cleartext access is not the answer. Data masking is.
For years, MySQL offered two paths: a function library in Enterprise, an OSS clone from Percona — both wired through views. MySQL 9.7 LTS (April 21, 2026) adds a third: a first-class masking policy object inside the server. MariaDB takes a different route entirely — masking runs outside the server, in MaxScale, the MariaDB connection proxy. Five options in total. This post compares them.
MySQL 9.7 Enterprise Dynamic Data Masking
Dynamic Data Masking (DDM) is GA in MySQL 9.7 Enterprise Edition and OCI MySQL HeatWave. Define a policy. Attach it to a column. The server enforces it.
-- 1. Define the policy. The predicate decides who sees cleartext.
CREATE MASKING POLICY mask_ssn_policy(ssn_col)
CASE WHEN CURRENT_USER_IN('seeall')
THEN ssn_col
ELSE mask_ssn(ssn_col)
END;
-- 2. Attach the policy to the column.
ALTER TABLE protected.user_profiles
ALTER COLUMN ssn
SET MASKING POLICY mask_ssn_policy;
-- 3. Every read goes through the policy.
SELECT id, first_name, last_name, ssn FROM protected.user_profiles;
-- noseeall → XXX-XX-0001 (masked)
-- seeall → 900-01-0001 (cleartext)Two access predicates ship in 9.7: CURRENT_USER_IN(...) and CURRENT_ROLE_IN(...). The policy body uses the existing MySQL masking function library — mask_inner, mask_outer, mask_pan, mask_ssn, gen_rnd_email, gen_rnd_us_phone. The functions are not new. The policy object is. Before 9.7, binding a masking function to a column meant a view or an in-query call. In 9.7, the server does the binding.
What the policy object adds:
- Server-side enforcement. Interactive SQL, application traffic,
mysqldump, downstream extracts — every read path returns the masked value. Direct connections to the base table cannot bypass it. - No view inventory. One policy replaces a per-variation view catalog.
- Predicate safety. Masking applies inside
WHEREandJOIN. TheWHERE ssn LIKE '900-%'side-channel is closed.
What it does not do:
- Row-level filtering. DDM gates columns, not rows. For row-level control, pair with row-level filtering.
- Cover Community or Percona. Enterprise and HeatWave only.
MySQL Enterprise Data Masking Plugin (legacy)
Before 9.7, the only Enterprise path was the data masking plugin, shipped since 5.7. The plugin exposes masking functions (mask_inner, mask_outer, mask_ssn, …). It does not expose a policy object. Protect raw data by wrapping the base table in a view that calls those functions, then grant access to the view.

The pattern still works on 8.0 and 8.4. Documented limits:
- Account model first. Enforcement keys off MySQL roles. Most instances have a handful of users. Adopting the plugin means redesigning accounts before policies.
- One view per variation. The view inventory grows with every variation. Schema changes ripple through it.
- No management surface. It is plain SQL. Version, review, and audit it yourself.
- Base-table bypass. Anyone with
SELECTon the base table sees cleartext.
On 9.7 Enterprise, the policy object replaces this pattern for new work. The functions remain for direct calls.
Percona Data Masking Plugin
Percona Data Masking is the open-source clone of the legacy Enterprise plugin, shipped with Percona Server for MySQL.

Same function library. Same view-based pattern. Same limits. Free, but only on Percona Server. As of 9.7, Percona has not implemented CREATE MASKING POLICY. The OSS path is still views over functions.
MariaDB MaxScale Masking Filter
MariaDB Server has no native masking. No MASKING POLICY object. No mask_inner / mask_ssn plugin like MySQL Enterprise. Masking runs outside the server — in MaxScale, the MariaDB connection proxy, whose Masking filter rewrites result rows on the wire.
9.7 DDM, the legacy plugin, and Percona all run inside the server. MaxScale runs in front of it — the first of two outside-the-engine options compared here. Bytebase is the other.
Rules are JSON, not DDL:
{
"rules": [
{
"replace": { "database": "hr", "table": "person", "column": "ssn" },
"with": { "value": "XXX-XX-0000", "fill": "X" },
"applies_to": [ "'analyst'@'%'" ],
"exempted": [ "'security_officer'@'%'" ]
}
]
}Two operations. replace substitutes the column with a fixed value or a repeated fill character. obfuscate runs the value through a non-reversible hash. Both accept a match field for PCRE2 partial replacement.
MaxScale ships behaviour flags to close the obvious bypasses — prevent_function_usage rejects CONCAT(ssn, ''), treat_string_arg_as_field blocks ANSI_QUOTES tricks, check_subqueries rejects subqueries that read the masked column.
What MaxScale documents about its own filter:
- Best-effort, not malicious-defense. The filter is "intended for protecting against accidental misuse rather than malicious attacks."
- Direct connections bypass it. Anyone with credentials to the MariaDB instance behind MaxScale can connect on the native port and read cleartext. The deployment must remove direct routes.
- Narrow type coverage. Binary, text, and enumeration columns only. Numeric columns are not maskable by the filter.
- Known bypass vectors. Intermediate tables built from
SELECT INTO, expressions that wrap the masked column, user variables,UNIONagainst unmasked sources. The hardening flags reduce these, not eliminate them. - Licensing. MaxScale ships under Business Source License 1.1. Source-available. Free for limited production use. Converts to GPLv2 four years after release. Not OSS in the OSI sense.
Bytebase Dynamic Data Masking

Like MaxScale, Bytebase lives outside the engine. Unlike MaxScale, Bytebase is not a wire-protocol proxy. MaxScale is a transparent proxy: apps, BI tools, and scheduled jobs point at its port and see masked data unchanged. Bytebase is a workflow surface: humans log into the SQL Editor; queries route through approval and audit; service traffic continues straight to the database. Same family. Different surfaces.
Native MySQL masking is fragmented. 9.7 DDM: Enterprise + HeatWave only. Legacy plugin: Enterprise only. Percona's clone: Percona Server only. Community: nothing. Real fleets span several of these — plus RDS, Aurora, and managed forks that ship none. Three of the four native paths also require views, multiplied per masking variation.
Bytebase Dynamic Data Masking applies one policy model across every MySQL distribution. No view inventory. No edition gate. No per-engine plugin. Policy changes and exemption requests run through a built-in workflow — Request. Review. Approve. — every step audited.
Policies compose from three layers, evaluated in fixed precedence: Masking Exemption > Global Masking Rule > Column Masking.
- Global Masking Rule. Workspace-level. Rules evaluate top-down. First match wins. Match conditions span environment, project, database, and data classification. Each match applies a Semantic Type, which selects a masking algorithm — full, partial, MD5, range, or custom. One rule covers the Enterprise 9.7 cluster, the Percona shard, and the Community staging instance.

- Column Masking. Project-level override on a specific column when the global rule does not apply.

- Masking Exemption. Named users receive time-bound
QueryorExportexemptions to specific databases or tables. Service accounts are not eligible. Every grant logged. Every access logged.

Masking is infectious. When a column is masked, the policy propagates to every view and derived structure that depends on it. The view-inventory problem all three native MySQL options share disappears.

Policies can also be codified via GitOps.
Masking decisions are recorded in the audit log. Every SQL execution entry carries per-column masking metadata — masked columns, Semantic Type, matching rule — alongside user, source IP, statement, and row count. Granted exemptions, used exemptions, and policy edits are first-class audit events.
Enforcement boundary: Bytebase masks queries routed through the SQL Editor. Direct connections bypass it. Route human access through Bytebase and one policy applies across MySQL Community, Enterprise, Percona, MariaDB, RDS, Aurora, and managed forks — including the variants where neither the plugin, 9.7 DDM, nor MaxScale reaches.
Comparison
| MySQL 9.7 Enterprise DDM | Enterprise Plugin (legacy) | Percona Plugin | MariaDB MaxScale Masking | Bytebase Dynamic Data Masking | |
|---|---|---|---|---|---|
| Compatibility | MySQL 9.7+ Enterprise, HeatWave | MySQL Enterprise | Percona Server for MySQL | MariaDB behind MaxScale | All MySQL/MariaDB distributions ⭐️ |
| Mechanism | Policy object on column ⭐️ | Functions + views | Functions + views | JSON rules in proxy | Policy in Bytebase, applied at SQL Editor |
| Enforced at | Database, every read path ⭐️ | View boundary | View boundary | Proxy boundary | SQL Editor |
| Policy mgmt | First-class server objects | DIY views | DIY views | JSON config files | Centralized UI, grants, audit log ⭐️ |
| Workflow | DDL only | DDL only | DDL only | Config-file edits | Request. Review. Approve. ⭐️ |
| Row-level filter | No (pair with row-level filtering) | No | No | No | No (pair with access policy) |
| Price | Paid | Paid | Free ⭐️ | BSL (source-available) | Paid |
Picking one
- On 9.7 Enterprise or OCI HeatWave. Use the policy object. Only option that enforces uniformly across
SELECT *,mysqldump, and direct client sessions. - On 8.0 or 8.4 Enterprise. Use the legacy plugin with views. Plan the migration to policies on the next LTS upgrade.
- On Percona Server, no Enterprise budget. Use the Percona plugin. Same constraints as the legacy Enterprise plugin.
- On MariaDB. Use the MaxScale masking filter — best-effort, by its own docs. Network-segment the MariaDB port so direct connections are not reachable. For stronger guarantees, route human access through Bytebase.
- Mixed MySQL/MariaDB fleet, or alongside Postgres, Snowflake, and managed forks. Use Bytebase. One policy model. Every engine. Audited grants for every unmask.
Try Bytebase Dynamic Data Masking with this tutorial.