Dynamic data masking and row-level security are often bought as the same "fine-grained access" feature, yet one hides values and the other hides rows, and each fails in a way the other does not cover. Dynamic data masking (DDM) changes what a column returns while the row stays visible; row-level security (RLS) removes whole rows while every column of visible rows stays in clear. This guide compares threat models, enforcement, inference and bypass paths, service account and BI access, and combining them. For masking techniques in general see the Tokenisation and Data Masking Guide, and for masking production copies see Static vs Dynamic Data Masking.
Platforms named in this guide are listed alphabetically; the order is not a ranking.
Key takeaways
- Masking hides values, RLS hides rows. DDM answers "which fields may this user read in clear"; RLS answers "which records may this user see at all".
- Neither stops a user who can write SQL from inferring data. Microsoft documents both a WHERE-clause inference against masked columns and an error-based inference against filtered rows, and Google documents query timing as a side channel for row policies.
- Who is exempt differs sharply. In SQL Server, sysadmin and db_owner always see unmasked values, but RLS policies apply to dbo and table owners unless the predicate allows them. In PostgreSQL, table owners, superusers and BYPASSRLS roles bypass RLS unless FORCE ROW LEVEL SECURITY is set.
- Service accounts and BI tools are where both controls quietly disappear. Shared logins, pooled connections and BI service principals turn per-user rules into per-application rules.
- Combine them deliberately. Row policies first, then masking, then column-level grants for columns nobody in the role should query at all, with auditing behind all three.
The short answer
Use row-level security when the boundary is the record: a tenant in a shared multi-tenant schema, a sales region, a patient's own care team, an employee's own payslip. Use dynamic data masking when the user legitimately needs the record but not every value in it: a contact centre agent who needs the last four digits of a card number, an analyst who needs a birth year but not a birth date. Use column-level permissions (or object-level security in BI models) when a role should never query a column at all, because a masked column can still be filtered on and a hidden row is not a hidden column. Where users run their own queries, treat both as exposure reduction and keep the real boundary in grants or separate tables; Google's BigQuery documentation rates separate tables as the strongest isolation.
Side-by-side comparison
| Factor | Dynamic data masking | Row-level security |
|---|---|---|
| Unit of protection | Column value in a result set | Row in a table or view |
| What the user sees | Every permitted row, with some values replaced (xxxx, 0, partial strings, NULL or a custom expression) | Only rows the predicate allows, with all columns in clear |
| Evaluated | On read, per column, often after row filtering | On read (filter) and, where supported, on write (block or WITH CHECK) |
| Writes | Not restricted: SQL Server documents that users with write permission can still update masked columns | Can be restricted: SQL Server block predicates, PostgreSQL WITH CHECK |
| Documented inference path | Range predicates in WHERE clauses reveal masked values | Error side channels, referential integrity checks, query timing, change tracking |
| Main exempt principals | Holders of an unmask privilege and administrative roles (platform specific) | Owners, superusers or bypass roles on some platforms; nobody by default on others |
| Failure mode | Silent: value inferred, or copied by a role that sees it in clear | Silent: empty result looks like "no data", or a bypass role reads everything |
What each hides, and what each leaves visible
Dynamic data masking
DDM rewrites column output for users without an unmask right. In SQL Server and Azure SQL the built-in functions are default(), email(), random(), partial() and, from SQL Server 2022, datetime() for masking part of a date. Snowflake and Databricks express it as a policy or SQL function returning either the original or a masked value. What masking leaves visible matters most: the row exists, its other columns are readable, the masked column can still be filtered and joined on, and row counts are exact. Masking reduces casual exposure; it does not stop a determined query writer.
Row-level security
RLS attaches a predicate to a table so that every query behaves as if a WHERE clause had been added. SQL Server binds an inline table-valued function to a table through CREATE SECURITY POLICY; PostgreSQL uses CREATE POLICY with USING and WITH CHECK expressions; Snowflake uses a schema-level row access policy that runs with the policy owner's privileges; BigQuery uses row-level access policies with a grantee list and filter expression; Databricks Unity Catalog uses row filter functions; Oracle Virtual Private Database (VPD) uses a policy function that generates a dynamic WHERE clause, configured with the DBMS_RLS package. What RLS leaves visible is every column of every permitted row.
Column-level controls
Column grants, BigQuery column-level security and Power BI object-level security remove a column from what a role may query. Microsoft states that Power BI RLS cannot hide columns and that object-level security does that job; the same logic holds in the engine. For the wider database control set see the Database Security Guide.
Threat model for each option
DDM addresses over-exposure of identifiers to support, operations and reporting users who need the record, and accidental extracts: in SQL Server, SELECT INTO and INSERT INTO run by a user without UNMASK copy masked values, and exports by such users contain masked data. DDM does not address inference by query (Microsoft's documentation concludes DDM should not be used alone to fully secure sensitive data), privileged users (SQL Server principals with CONTROL SERVER or database-level CONTROL, such as sysadmin and db_owner, always see unmasked data) or writes to the masked column.
RLS addresses cross-tenant and cross-region reads for every client,; AWS describes it as a database-managed WHERE clause, so developers need not add a tenant filter to every query. With SQL Server block predicates or PostgreSQL WITH CHECK it also rejects writes that would put a row outside the user's boundary. RLS does not address side channels, bypass roles (PostgreSQL superusers and BYPASSRLS roles always bypass it, and table owners do unless the table is set to FORCE ROW LEVEL SECURITY) or column exposure within permitted rows. The table below lists the documented paths.
Inference and bypass risks in practice
| Path | Affects | How it works (described, not demonstrated) | Control |
|---|---|---|---|
| Range or equality predicates on masked columns | DDM | Repeated WHERE clauses narrow a masked value until it is known | Restrict ad hoc SQL for masked roles; audit repeated narrow predicates; use column grants for the most sensitive fields |
| Error-based inference | RLS | An expression errors only for a hidden row with a particular value | Limit who can run free-form queries; monitor error spikes from one principal |
| Integrity constraint probing | RLS (PostgreSQL documented) | A unique or foreign key violation reveals that a hidden row exists | Design keys so their existence is not sensitive; avoid natural identifiers as keys in shared tables |
| Timing | RLS (BigQuery documented) | Query duration hints at rows near storage shard boundaries | Audit logging and alerting on unusual query volume |
| Change tracking and CDC | RLS (SQL Server documented) | Change tracking can reveal keys of filtered rows; CDC readers in the gating role see all changes | Review who holds VIEW CHANGE TRACKING and CDC roles |
| Owner or bypass role | Both | Administrative and owner roles are exempt on several platforms | Never run applications or BI refreshes as owner; PAM for exempt roles |
| Policy tampering | Both | A user who can alter masks or policies can switch them off or widen them | Separate policy managers from data readers; alert on policy changes |
| Copy by an unmasked or unfiltered role | Both | A role with full visibility exports or clones the table, and the copy carries no policy | Treat export by exempt roles as a monitored event; static masking for copies |
Two platform details surprise teams. BigQuery removes row-level policies on CREATE OR REPLACE TABLE and WRITE_TRUNCATE (TRUNCATE TABLE keeps them), so a rebuild pipeline can strip protection every run. Databricks returns no data from protected tables on runtimes older than 12.2 LTS, and recommends ANSI mode because silent casts to NULL can make filters and masks return wrong results.
Platform terms at a glance
| Platform | Row control | Value control | Order and notes from vendor docs |
|---|---|---|---|
| Databricks (Unity Catalog) | Row filter (SQL UDF returning boolean) | Column mask (SQL UDF returning original or masked value) | ABAC policies at catalog or schema level apply by governed tag and table owners cannot override them; dynamic views cover views. |
| Google BigQuery | Row-level access policy (grantee list plus filter expression) | Data masking with column-level security | Masking applies on top of row filtering. Principals outside every grantee list see no rows; a TRUE filter preserves full access for listed principals and service jobs. |
| Microsoft SQL Server and Azure SQL | Security policy with filter and block predicates | Dynamic data masking with UNMASK permission (granular from SQL Server 2022) | RLS applies to dbo and owners unless the predicate allows them; sysadmin and db_owner see unmasked data. Microsoft Fabric and Azure Synapse support filter predicates only. |
| Oracle Database | Virtual Private Database policy (DBMS_RLS) | Oracle Data Redaction | The VPD policy function generates a dynamic WHERE clause. Oracle's current guide recommends Oracle Deep Data Security, which extends VPD. |
| PostgreSQL | Row security policy (USING, WITH CHECK; permissive or restrictive) | Views with masking expressions, or extensions | Default deny when RLS is enabled with no policy. Owners bypass unless FORCE ROW LEVEL SECURITY; superusers and BYPASSRLS always bypass. |
| Snowflake | Row access policy | Masking policy | Row access policies are evaluated before masking policies; a column can be in one policy signature or the other, not both. Policy expressions run with the policy owner's privileges. |
Availability: Snowflake documents row access policies as requiring Enterprise Edition or higher. SQL Server granular UNMASK and the datetime() function require SQL Server 2022 or later. Databricks row filters and column masks need Databricks Runtime 12.2 LTS or above, with further rules for dedicated access mode. Check each vendor's current documentation for edition and version limits.
Service account and BI tool access
Both controls are only as precise as the identity the database sees, and most production traffic arrives as an application login, pooled connection, ETL job or BI service principal rather than a named person.
- Shared application logins: SQL Server documents the SESSION_CONTEXT pattern: the application sets the end user's ID with sp_set_session_context using @read_only = 1, a filter predicate checks it and a block predicate stops inserts for other IDs, with DENY UPDATE on the ID column. PostgreSQL's equivalent is a custom setting such as app.current_tenant read by current_setting() in the policy.
- Connection pools: AWS notes that server-side poolers such as pgBouncer may not preserve session variables, so tenant context can leak or be lost. Set context on every checkout and prove a missing context returns nothing.
- Owner logins: a PostgreSQL application connecting as table owner skips RLS by default; use a non-owner role without BYPASSRLS and set FORCE ROW LEVEL SECURITY.
- ETL and service jobs: BigQuery's TRUE filter serves service account jobs and table copies that need all data; those identities sit outside the row boundary and need the tightest credential handling. See the Service Account Security Guide.
- Power BI: RLS applies only to workspace Viewers (including those with Build permission); Admins, Members and Contributors are not filtered. Service principals cannot be added to RLS roles, so embedded apps must pass an EffectiveIdentity in the embed token for per-user RLS. In Import mode the source's own security is ignored and model RLS is the only enforcement.
Where identity fits
Every exempt principal in this guide, from db_owner to a BYPASSRLS role to a BigQuery TRUE-filter grantee, is a privileged identity, and many are non-human. Inventory them alongside other service accounts, keep their credentials in a vault, and review them on the same cycle as database administrators. See the Non-Human Identity (NHI) Guide and the Least Privilege Techniques Guide. An AI agent querying a warehouse as one service identity is seen by the policies as the agent, not the person asking.
Decision criteria
| Situation | Primary control | Add |
|---|---|---|
| Multi-tenant SaaS on a shared schema | RLS on every tenant table, with write checks | Separate databases or schemas for tenants with contractual isolation; see Multi-Tenant SaaS Authorisation and Tenant Isolation Guide |
| Contact centre reading customer records | DDM on identifiers and payment fields | RLS if agents are limited to a region or brand; no ad hoc SQL |
| Analysts with SQL access to a warehouse | Column grants or denying policies for direct identifiers | RLS for business boundaries; masking only for convenience fields; query auditing |
| Self-service BI on a shared model | Model-level RLS and object-level security | Source-level policies if DirectQuery; review workspace roles |
| Data sharing with a partner | Dedicated shared view or table holding only permitted rows and columns | Masking in the shared object; Snowflake documents that CURRENT_ROLE returns NULL in a consumer account |
| Regulated fields with strict need to know | Column-level denial and encryption | DDM is not enough on its own where users can query |
Small organisations with one engine and few query writers: RLS for tenant or customer boundaries and DDM on a short list of identifiers, enforced in the database. Large organisations with a warehouse and several BI tools: tag-driven policies (for example Databricks ABAC policies on governed tags) owned by central data governance, so a newly tagged column is protected everywhere. Classification is the prerequisite; see the Data Classification Guide.
Worked scenarios
Scenario 1, a B2B SaaS reporting feature. A product team adds customer-facing reports over a shared PostgreSQL database. They enable RLS on each reporting table with a policy comparing tenant_id to current_setting('app.current_tenant'), set FORCE ROW LEVEL SECURITY, and connect the reporting service as a role that does not own the tables. Load testing through the pooler shows the tenant setting carrying over between requests, so they set it at the start of every transaction and add a test asserting zero rows when no tenant is set. Masking is not needed: tenants may see all their own columns.
Scenario 2, a retailer's contact centre. Agents look up any customer by order number, so RLS would block the work. The team masks the card number (partial, last four), email and date of birth, grants UNMASK on email only to escalation staff, and removes ad hoc query access so range-predicate inference is out of reach.
Scenario 3, an analytics warehouse. Regional analysts must see only their region and never see national insurance numbers. A row access policy keyed on a region mapping table limits rows; the identifier column is denied to analyst roles rather than masked, because analysts write SQL. The BI service identity gets its own role with the same row policy.
Implementation approach
- Classify and scope: list tables, the row boundary and sensitive columns; decide per column: mask, deny or encrypt.
- Map identities: for every path in (application, pool, ETL, BI, notebooks, agents) record which identity the database sees.
- Design policies centrally: keep predicate functions and policies in a dedicated schema, owned by a policy manager role that does not need SELECT on the data, as Microsoft recommends. Keep expressions simple and deterministic.
- Test in a non-production copy: run each role's typical queries, the inference cases in the table above, and the "context missing" case. For Power BI use Test as role, but validate DirectQuery with SSO by signing in as a real Viewer, since Test as role does not work there.
- Pilot with one consumer: compare row counts and masked outputs against expectations and watch performance (BigQuery notes row filters do not prune partitions or clusters).
- Roll out by data domain: extend to remaining tables, then add write checks (block predicates or WITH CHECK).
- Roll back safely: SQL Server policies can be switched with STATE = OFF and masks dropped per column; PostgreSQL policies can be dropped or RLS disabled by the owner. Version rollback scripts with the policies; disabling a row policy exposes rows.
Detection and measurement
- Policy change events: alert on CREATE, ALTER or DROP of security policies, row access policies, masks and unmask grants. BigQuery audit logs omit the filter expression and grantee list, so keep definitions in version control.
- Exempt principal activity: log reads, exports and clones by owners, administrators, BYPASSRLS roles and TRUE-filter grantees.
- Inference patterns: repeated narrow predicates on masked columns, bursts of errors from one principal, unusually high query counts against a protected table.
- Coverage drift: compare catalogue views (for example sys.masked_columns) with the classification inventory to find unprotected sensitive columns.
Metrics:
- Percentage of classified sensitive columns with a mask, deny or encryption control.
- Percentage of multi-tenant tables with RLS enabled and forced, with write checks.
- Number of exempt principals per database, and the share reviewed in the last quarter.
- Number of application and BI paths that carry the end user's identity versus a shared service identity.
- Policy changes made outside the change process (target zero).
Common failure patterns
- Treating masking as access control for analysts. Users with SQL can infer masked values; deny the column instead.
- Losing tenant context in the pool. A pooled connection reuses the previous request's tenant or none at all.
- Rebuilding tables without policies. CREATE OR REPLACE in BigQuery, or a pipeline that drops and recreates objects, leaves a protected table unprotected.
- BI workspace roles that bypass the model. Power BI Members and Contributors are not subject to RLS, so granting edit roles widely removes the filter.
- Temporal and history tables without policies. SQL Server does not copy policies to temporal history tables, so the history holds every row unfiltered unless predicates are added there too.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev 5 | AC-3 Access Enforcement; AC-3(7) Role-based Access Control | RLS predicates and masking policies are enforcement mechanisms |
| NIST SP 800-53 Rev 5 | AC-6 Least Privilege | Limiting exempt and unmask principals |
| NIST SP 800-53 Rev 5 | AU-2 Event Logging; AU-12 Audit Record Generation; AU-6 Audit Record Review, Analysis, and Reporting | Policy changes, exempt reads and inference patterns |
| NIST CSF 2.0 | PR.AA-05; PR.DS-01; ID.AM-07 | Permissions defined and enforced; data at rest protected; data inventories that drive policy coverage |
| ISO/IEC 27001:2022 Annex A | 8.3 Information access restriction; 8.11 Data masking | RLS maps to access restriction, DDM to data masking |
| CIS Controls v8.1 | 3.3 Configure Data Access Control Lists | Row and column policies as data access control |
| MITRE ATT&CK v20 | T1213 Data from Information Repositories; T1078 Valid Accounts | Collection through legitimate database accounts that policies are meant to constrain |
Practitioner checklist
- Every sensitive column has a decision recorded: mask, deny or encrypt, with the reason.
- Multi-tenant tables have RLS enabled, forced where the platform requires it, and write checks.
- Applications and BI refreshes never connect as table owners or bypass roles.
- Tenant or user context is set on every pooled connection and a missing context returns no rows.
- Users who can write ad hoc SQL are not relying on masking for sensitive identifiers.
- Exempt principals (owners, administrators, unmask holders, TRUE-filter grantees) are inventoried and reviewed quarterly.
- Policy managers are separate from data readers, and policy changes alert.
- Pipelines that rebuild tables reapply row and masking policies, with a test that fails if they are missing.
- BI workspace roles are reviewed so edit roles are not used as a way round model RLS.
Standards and references
- Microsoft Learn: Row-level security
- Microsoft Learn: Dynamic data masking
- PostgreSQL: Row Security Policies
- Google Cloud: Introduction to BigQuery row-level security
- Databricks: Row filters and column masks
- Oracle: Using Oracle Virtual Private Database to Control Data Access
- Snowflake: Understanding row access policies
- Microsoft Learn: Row-level security with Power BI
Related NHI Mgmt Group resources: Static vs Dynamic Data Masking · Database Security Guide · Multi-Tenant SaaS Architecture Security Guide · ABAC vs PBAC · Database Activity Monitoring vs DSPM