Production Databricks Notes: short technical note
Multi-tenant data platforms make one decision early that is expensive to reverse: where the tenant
boundary sits. Unity Catalog supports several answers. They differ in blast radius, operational
cost, and what they do to features you may be relying on elsewhere.
The four options, strongest isolation first.
Complete separation. Tenants cannot reference each other's objects because they do not share a
namespace.
Cost: a metastore is regional and heavyweight. Cross-tenant analytics becomes a data movement
problem rather than a query. Operationally this is a platform per tenant.
Use when: regulatory or contractual isolation requirements make shared infrastructure
unacceptable, or tenants are large enough that each justifies its own platform.
For most products this is more than the requirement demands, and the cross-tenant reporting cost is
usually what kills it about a year in.
2. Catalog per tenant
One metastore, one catalog per tenant. Grants at the catalog level.
Cost: catalog count grows with tenants. Schema changes must be applied per catalog, so migration
tooling becomes a real piece of software rather than a script.
Use when: tenants are few enough to enumerate and large enough to matter individually. This is
the typical enterprise B2B shape.
The property that makes this attractive is that the boundary is structural. A query cannot leak
across it by mistake, because the catalog name is in every reference. That takes the most common
failure mode off the table entirely, which is worth more than it costs in almost every case where
the tenant count is bounded.
This is my default recommendation when tenant count is in the tens or low hundreds.
3. Schema per tenant
One catalog, one schema per tenant.
Cost: the boundary is one level weaker and a mistaken USE statement crosses it. Grant
management is more fiddly because the useful boundary and the natural organisational boundary are
now the same level.
Use when: tenants share a schema shape and you need shared reference data in the same catalog
without cross-catalog grants.
This one is frequently chosen for convenience and is the one I would push back on hardest, because
it looks like option 2 while providing meaningfully less. If you are choosing it, choose it for the
shared-reference-data reason rather than because it is fewer objects.
4. Row-level security in shared tables
One table, all tenants, a row filter restricting each principal to their rows.
CREATE FUNCTION tenant_filter(tenant_id STRING)
RETURN is_account_group_member(CONCAT('tenant_', tenant_id));
ALTER TABLE orders SET ROW FILTER tenant_filter ON (tenant_id);
Column masks work the same way at column granularity:
ALTER TABLE customers ALTER COLUMN ssn SET MASK ssn_mask;
Cost: the boundary is a predicate. It is correct only if the filter is correct, applied to every
table, and never bypassed. Any new table without the filter is a leak, and nothing structural
prevents that.
Use when: tenant count is high enough that per-tenant objects are unmanageable: thousands of
tenants, self-service signup.
Use with a test that enumerates every table in the catalog and asserts a filter is attached. Not
a review checklist. An automated test that fails the build. The failure mode here is a new table
shipped without a filter, and that is exactly the kind of omission review does not catch reliably.
The limitations that change architecture
Row filters and column masks carry restrictions that reach into decisions made elsewhere. From the
current documentation:
- Databricks Runtime below 12.2 LTS does not support them
- You cannot apply row-level security or column masks to a view
- OpenSharing providers cannot share tables with table-level row filters or column masks
- Time travel does not work with row-level security or column masks
- Deep and shallow clones are not supported on tables that have them
- You cannot create an AI Search index from a table that has them applied
Three of these are load-bearing.
Time travel. Week 3's rollback discussion leaned on Delta time travel as the mechanism for
recovering data that a bad release wrote. Applying row-level security to a table removes that
mechanism. Two features you would reasonably assume compose do not, and you find out during an
incident. If a table needs both tenant isolation and time-travel recovery, the isolation has to be
structural (option 2 or 3) rather than a row filter.
Views. If your access model was "expose views, filter in the view", row filters are not the
implementation. Dynamic views with is_account_group_member() are a separate mechanism and remain
available; they are not the same feature and they have their own trade-offs.
AI Search indexes. If tenant-scoped data feeds a RAG system, a row filter on the source table
blocks index creation. The tenant boundary in that case has to sit at the index level or higher,
which usually means one index per tenant, which pushes you back toward option 2.
flowchart TB
Q{"What does it cost if one tenant<br/>sees another's data?"}
Q -->|"Contractual or regulatory breach"| M["Metastore per tenant<br/>complete separation<br/>cost: a platform per tenant"]
Q -->|"High, and tenants are enumerable"| C["Catalog per tenant<br/>structural: USE CATALOG is required<br/>cost: per-catalog migrations"]
Q -->|"High, but shared reference data needed"| S["Schema per tenant<br/>weaker: a USE statement crosses it<br/>cost: easy to breach by mistake"]
Q -->|"Recoverable, and tenants number thousands"| R["Row filters on shared tables<br/>a predicate, not a boundary<br/>cost: breaks time travel and AI Search"]
Alt text: A decision tree from the question what does it cost if one tenant sees another tenant data. A contractual or regulatory breach leads to metastore per tenant. High cost with enumerable tenants leads to catalog per tenant, structural because USE CATALOG is required. High cost but shared reference data needed leads to schema per tenant, weaker because a USE statement crosses it. Recoverable cost with thousands of tenants leads to row filters on shared tables, a predicate rather than a boundary, which breaks time travel and AI Search.
Choosing
The question that decides it: what is the cost of one tenant seeing another's data?
If the answer is a contract breach, a regulatory report, or a lost customer, the boundary should be
structural: a catalog or a metastore, where crossing it requires an explicit grant that someone had
to write down.
If the answer is embarrassing but recoverable and the tenant count makes per-tenant objects
impractical, row filters are reasonable, with the enumeration test as a non-negotiable.
The thing to avoid is choosing row filters because they are less work to set up. They are less work
to set up and more work to keep correct, and the maintenance falls on whoever adds the next table
rather than on whoever made the decision.
An identity note
None of this works if workloads run as individual humans. Row filters and grants both key on the
executing principal, so tenant isolation depends on the identity model from Week 3 being right
first. A pipeline running as its author has the author's grants, which are broader than the
workload's, and the isolation you designed is not the isolation you have.
Fix it before it is convenient. It is the decision that decided whether your audit log was worth
keeping and whether your deployment identity meant anything, and it decides this too.
Companion to the article on Unity Catalog as a platform contract. Limitations quoted
from Databricks documentation, verified 2026-07-29. This list has changed before; check the current
reference. The recommendations here are engineering judgement rather than a Databricks platform
guarantee, and a genuine compliance requirement deserves review by someone accountable for it.