Row-level security (RLS) is a database access control that limits which rows of a table a user can read or change based on their identity, role or session context.
Instead of restricting access to whole tables or specific columns, RLS filters data row by row. The database engine applies the filter automatically at query time, so the same rule holds no matter which tool the user uses to access the data.
RLS is part of fine-grained access control, alongside:
For example, a salesperson may query the company’s orders table but only see orders for their assigned region, even though the table contains every region’s data. The user writes a normal SELECT statement, and the engine returns only the rows they’re allowed to see.
RLS is now a core building block for multi-tenant SaaS, regional data segregation and compliance use cases. This article covers how RLS works, where it helps, where it falls short and how it works on the Databricks Platform.
Row-level security works by applying a filter rule, often called a policy or predicate, to a table. When a user runs a query, the database engine automatically applies that filter and returns only the rows the user is allowed to see.
In practice, RLS usually works in three steps:
CURRENT_USER, a session variable set by the application or a mapping table that connects users and groups to permitted data.TRUE for rows the user can see and FALSE for everything else. Only rows that pass the predicate are returned.Because enforcement happens at the database layer, the same rule applies consistently across every access path, including BI dashboards, notebooks, ad-hoc SQL, APIs and third-party tools. That consistency is what makes RLS powerful: one rule, applied everywhere, enforced by the engine.
Most engines also distinguish between read-side and write-side enforcement. A read predicate controls what a SELECT query returns. A write predicate, often defined separately with a WITH CHECK clause, controls which rows a user can insert, update or delete.
The two predicates can be the same, but they do not have to be. For example, a user might be allowed to read rows for every region but only insert rows for their own region. Defining both sides matters when a table accepts writes, because skipping the write check is one of the most common ways teams misconfigure RLS in production.
RLS is one of several fine-grained access controls, and in production it’s almost always paired with others. The table below shows how each control fits.
| Control | What it restricts | Typical use case |
|---|---|---|
| Row-level security (RLS) | Specific rows in a table | Limit users to their region, tenant or department |
| Column-level security (CLS) | Specific columns in a table | Hide salary, SSN or PII columns from analysts |
| Object-level security (OLS) | Entire tables, views or measures | Block access to a sensitive dataset altogether |
| Data masking | Visible values within a column | Show only the last 4 digits of a card number |
| GRANT / REVOKE | Table-level read/write permissions | Allow or deny access to the table as a whole |
These controls are designed to layer. A typical setup uses table-level grants to control who can access a table, RLS to scope which rows are visible, and column-level security or data masking to protect sensitive fields within those rows. Treating them as a stack rather than a menu of alternatives makes governance both auditable and resilient. A misconfiguration in one layer doesn't compromise the others.
RLS is the standard way to enforce who can see what inside a shared table, filtering rows based on a user's attributes against a keying column like region, tenant, or classification. Most teams reach for it when one dataset has to serve multiple audiences with different visibility rules.
The general pattern is consistent across platforms, with vendor-specific syntax filled in where needed.
tenant_id, region or owner_id. If no such column exists yet, plan a backfill before the policy goes live, and consider indexing the column to keep the predicate cheap.Moving access logic into the data layer pays off in several practical ways. The shorthand is that the database becomes the source of truth for access, rather than every application that touches the data.
RLS is powerful, but it has well-known gotchas teams should plan for. Most of these surface only in production or during an audit, which makes them worth knowing upfront.
In many databases, table owners and high-privilege admins bypass RLS by default. PostgreSQL, for example, requires the FORCE ROW LEVEL SECURITY setting to apply policies to the table owner, and similar settings exist in other engines. This is a common audit finding: assume privileged users see every row unless your configuration explicitly forces the policy to apply to them. Test the policy from a privileged session, not just a regular one, before signing off on it.
RLS filters rows, but it does not hide columns or block aggregate results. An analyst blocked from seeing individual EU records can still run SELECT COUNT(*) over the unfiltered table if RLS isn’t paired with column or aggregate restrictions. Pair RLS with column-level security or data masking to close that gap, and consider whether aggregate queries themselves need to be governed for the most sensitive tables.
Every query gets the RLS predicate applied, which can slow performance if the filter logic is complex or the keying column isn’t indexed. Index the columns the policy references and keep the predicate as simple as possible. Prefer straightforward CASE expressions over subqueries or mapping-table lookups inside the filter. If the engine supports it, materialize the user-to-rows mapping into a small, well-indexed table rather than computing it on the fly.
Empty result sets caused by RLS look identical to “no matching data.” Developers chasing a missing row often spend hours before they realize the policy filtered it out. Log the effective user identity and policy version during development, give engineers a way to confirm whether RLS is active when results look wrong and document the policy in the same place as the table schema so it’s discoverable.
RLS policies often have two sides: a USING clause that filters what users can read and a WITH CHECK clause that controls what they can insert or update. Defining one without the other is a classic mistake: read filtering with no write check lets users insert or update rows they shouldn’t own. Always define both sides when the table accepts writes, and run a write-side test as part of the policy review.
On the Databricks Platform, row-level security is handled through row filters in Unity Catalog, Databricks’ unified governance layer for data and AI. The pattern is straightforward: define a SQL user-defined function that returns true for the rows a given user is allowed to see, then attach it to the target table. The filter runs automatically at query time, using the current user’s identity or session context to determine which rows to return.
Row filters are enforced consistently across Databricks SQL, notebooks, jobs and connected BI tools, with no custom logic required per surface. They work alongside column masks for full fine-grained access control, and every query that touches a filtered table is captured in Unity Catalog’s lineage and audit logs, so governance and security teams can see exactly which policies apply to which tables and which users have queried what.
What is dynamic row-level security? Dynamic RLS evaluates the access rule at query time using the current user's identity or session context, so the same policy returns different results for different users. All modern RLS implementations work this way, including Databricks' ABAC policies, row filters and dynamic views.
What is the difference between row-level security and column-level security? RLS restricts which rows a user can see; column-level security restricts which columns, typically to hide sensitive fields like salary or Social Security numbers. Most production deployments use both together.
Is row-level security enough on its own to secure sensitive data? No. RLS handles row visibility but doesn't mask column values, block aggregate queries or replace identity and access management. Pair it with column-level security, table-level grants and audit logging as part of a defense-in-depth strategy.
How does Databricks implement row-level security? Through Unity Catalog, with three options: ABAC policies, table-level row filters and dynamic views. ABAC is recommended for governance at scale; row filters and dynamic views are available for more bespoke needs.
Does row-level security affect query performance? Yes, but the impact is usually manageable. Keep policy logic simple, index the columns the policy references and prefer SQL UDFs over Python UDFs. Profile queries before and after policy changes to catch regressions early.
Row-level security is most effective as part of a broader governance model that also covers columns, masking, lineage and audit. See how Unity Catalog brings row-level security, column masking and unified governance together on the Databricks Platform.
Subscribe to our blog and get the latest posts delivered to your inbox.