3 weeks ago
I'm currently working with a banking customer migrating from Hive Metastore to Unity Catalog. While planning their catalog layout, one of their platform engineers asked the question that prompted this post: for their schemas and volumes, should they use managed or external storage, and how would they keep each business unit's data in its own storage account?
They were leaning toward external, for two reasons: they believed managed storage meant Databricks would take custody of their data, and they believed managed storage could not be physically segregated by team. I hear both on almost every Hive Metastore migration. Neither holds up. The data stays in your own Azure storage either way, and managed storage can be segregated per business unit right down to the schema. Walking through the differences gave them a clear picture of the trade-offs, and since the same question comes up so often, I figured sharing it more widely would help.
This post is that walkthrough. It covers both storage models on Azure: what they are, their trade-offs, when to pick each, where the two misconceptions break down, and a tested end-to-end example you can run yourself. Everything is backed by the Azure Databricks documentation linked at the end.
Before comparing the two models, it helps to separate three concepts that are often conflated.
| Concept | What it is | Scope |
|---|---|---|
| Storage credential | A securable that holds the authentication to your Azure storage. On Azure this is an Azure Managed Identity attached to a Databricks Access Connector. | Metastore |
| External location | A securable that pairs a storage credential with a specific abfss:// path. It is the governed gateway to a path in ADLS Gen2. |
Metastore |
| Managed location | A path (which must sit inside an external location) where UC stores managed tables and volumes. Can be set at metastore, catalog, or schema level. | Metastore / Catalog / Schema |
The difference comes down to who controls the physical storage layout and lifecycle. Both store data in your own Azure storage account. The table below compares them side by side.
| Dimension | Managed | External |
|---|---|---|
| Storage path | UC chooses and controls the path under a designated managed location | You specify the exact abfss:// path with a LOCATION clause |
| File layout and lifecycle | UC manages layout, directory structure, compaction, and cleanup | You own the file layout and lifecycle |
| How you reference objects | By name only (catalog.schema.table) |
By name, and the data is also reachable by path |
| Access from non-Databricks tools | Through open interfaces (Iceberg REST, Delta Sharing, credential vending) | Direct path read/write at the storage path |
| What happens on DROP | Data is removed (8-day undrop window for tables) | Underlying files are not deleted |
| Automatic optimization | Yes (predictive optimization, auto liquid clustering) | No, you manage OPTIMIZE / VACUUM / statistics |
| Best for | Governed analytical tables and volumes created in Databricks | Fixed paths, register-in-place, sharing with external engines |
| Status in UC | Default and recommended | Use when a concrete path or external-tool requirement exists |
The word "managed" is easy to misread as "Databricks takes custody of your data" or "you can never get it back out." Neither is true.
A more accurate mental label for "managed" is "Databricks-optimized storage in your own account," as opposed to "Databricks-owned storage."
This is the one that pushes teams toward external storage unnecessarily. A managed table or volume does not all land in one big shared bucket by default. UC resolves the managed storage path in this order:
This gives you three levels of granularity to physically separate managed data. You can point different catalogs, or even different schemas, at completely different ADLS Gen2 containers or paths.
Metastore managed location: abfss://metastore@examplestorage.dfs.core.windows.net/
└── Catalog: sales MANAGED LOCATION abfss://sales@examplestorage.dfs.core.windows.net/
└── Schema: orders MANAGED LOCATION abfss://sales-orders@examplestorage.dfs.core.windows.net/
So if the requirement is separate underlying storage per team or per sensitivity tier, managed storage meets it. You get clean physical isolation by assigning each catalog or schema its own managed location. What managed storage does not give you is control over the per-table directory names: UC creates hashed subdirectories (under __unitystorage/...) to guarantee uniqueness. You control the container/path boundary, UC controls the layout inside it.
Volumes work the same way: create separate managed volumes in separate schemas or catalogs, each with its own managed location, to segregate file storage exactly as you would with separate external paths.
OPTIMIZE, VACUUM, and ANALYZE for you. On by default for accounts created after November 11, 2024, rolling out to older accounts.OPTIMIZE, VACUUM, and statistics.MSCK REPAIR TABLE ... SYNC METADATA.| Situation | Recommended model |
|---|---|
| New tables created inside Databricks, no external reader | Managed |
| You want automatic optimization and lowest maintenance | Managed |
| You need physical separation per team or sensitivity tier | Managed, with per-catalog or per-schema managed locations |
| Data must be read/written by a non-Databricks engine at a fixed path | External |
| Registering large existing datasets in place without moving them | External |
| Landing zones, ingestion drop folders, file-based interchange | External volume |
| A vendor or downstream app requires a known storage path | External |
| Highly regulated data where you want zero path-based side doors | Managed (no direct path access by design) |
A typical multi-team environment ends up with managed as the default for governed analytical tables, and external locations reserved for ingestion landing zones, file interchange, and integration with tools that genuinely need direct path access.
MANAGED LOCATION. Use schema-level managed locations for finer isolation. This gives you physical separation while keeping the governance benefits of managed.READ FILES / WRITE FILES broadly on an external location.If you decide you need external storage, here is the end-to-end setup.
Creating external storage is a two-step, two-privilege process.
| To do this | You need |
|---|---|
| Create a storage credential | CREATE STORAGE CREDENTIAL on the metastore. Account admins and metastore admins have it by default. |
| Create an external location | CREATE EXTERNAL LOCATION on both the metastore and the referenced storage credential. Metastore and workspace admins have it by default. |
A non-admin who needs to create external locations must be granted, by a metastore admin: CREATE STORAGE CREDENTIAL on the metastore (if they will also create the credential), CREATE EXTERNAL LOCATION on the metastore, and CREATE EXTERNAL LOCATION (or MANAGE) on the specific storage credential.
On Azure, storage credentials backed by a managed identity are created through Catalog Explorer, the Databricks CLI, the REST API, or Terraform (there is no inline SQL form for Azure managed-identity credentials). The CLI form is:
databricks storage-credentials create --json '{
"name": "adls_cred",
"comment": "Managed identity for ADLS Gen2",
"azure_managed_identity": {
"access_connector_id": "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.Databricks/accessConnectors/<connector-name>"
}
}'
If the Access Connector uses a user-assigned managed identity, add inside azure_managed_identity:
"managed_identity_id": "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<mi-name>"
In Catalog Explorer the equivalent fields are: Credential Type = "Azure Managed Identity", "Access Connector ID", and optionally "Managed Identity ID".
CREATE EXTERNAL LOCATION IF NOT EXISTS sales_landing URL 'abfss://landing@examplestorage.dfs.core.windows.net/sales' WITH (STORAGE CREDENTIAL adls_cred) COMMENT 'Sales landing zone';
Notes: the path is abfss://<container>@<storage-account>.dfs.core.windows.net/<path>. Names containing hyphens must be backtick-quoted. The keyword STORAGE is optional but the canonical form is WITH (STORAGE CREDENTIAL ...).
-- Allow a team to create external tables on this location GRANT CREATE EXTERNAL TABLE ON EXTERNAL LOCATION sales_landing TO `data-engineers`; -- Allow creating external volumes GRANT CREATE EXTERNAL VOLUME ON EXTERNAL LOCATION sales_landing TO `data-engineers`; -- Direct file read/write (WRITE FILES requires READ FILES too) GRANT READ FILES, WRITE FILES ON EXTERNAL LOCATION sales_landing TO `ingestion-svc`; -- Allow this location (or a subpath) to back a managed location for a catalog/schema GRANT CREATE MANAGED STORAGE ON EXTERNAL LOCATION sales_landing TO `platform-admins`; -- Review and transfer ownership SHOW GRANTS ON EXTERNAL LOCATION sales_landing; ALTER EXTERNAL LOCATION sales_landing OWNER TO `platform-admins`;
Because catalog and schema managed locations must sit inside an external location, the pattern for segregated managed storage is: create the external location once, then point each catalog or schema's managed location at a subpath of it.
-- Catalog-level segregation: Sales managed data in its own container CREATE CATALOG IF NOT EXISTS sales MANAGED LOCATION 'abfss://sales@examplestorage.dfs.core.windows.net/managed'; -- Schema-level segregation: Orders data in a separate path CREATE SCHEMA IF NOT EXISTS sales.orders MANAGED LOCATION 'abfss://sales-orders@examplestorage.dfs.core.windows.net/managed'; -- A managed volume inherits the schema's managed location, physically isolated CREATE VOLUME IF NOT EXISTS sales.orders.landing_files;
Reading about resolution order is one thing. Watching a managed table land in the exact container you chose is more convincing. Here is the full sequence run end to end on an Azure workspace, with the actual output. It creates a catalog on one container and a schema on a different container, then shows that the managed table and volume follow the schema.
RG=rg-uc-blog; LOC=eastus; SA=ucblogdemostore az group create -n $RG -l $LOC # ADLS Gen2 = StorageV2 with hierarchical namespace enabled az storage account create -n $SA -g $RG -l $LOC \ --sku Standard_LRS --kind StorageV2 --enable-hierarchical-namespace true for c in sales sales-orders landing; do az storage container create --account-name $SA --name $c --auth-mode login done # Databricks Access Connector with a system-assigned managed identity az databricks access-connector create -n ac-uc-blog -g $RG -l $LOC --identity-type SystemAssigned # grant that managed identity access to the storage account MI=$(az databricks access-connector show -n ac-uc-blog -g $RG --query identity.principalId -o tsv) SAID=$(az storage account show -n $SA -g $RG --query id -o tsv) az role assignment create --assignee-object-id $MI --assignee-principal-type ServicePrincipal \ --role "Storage Blob Data Contributor" --scope "$SAID"
# storage credential wrapping the access connector
databricks storage-credentials create --json '{
"name": "adls_cred",
"azure_managed_identity": { "access_connector_id": "<access-connector-resource-id>" }
}'
# one external location per container
databricks external-locations create sales_el abfss://sales@ucblogdemostore.dfs.core.windows.net/ adls_cred
databricks external-locations create sales_orders_el abfss://sales-orders@ucblogdemostore.dfs.core.windows.net/ adls_cred
databricks external-locations create landing_el abfss://landing@ucblogdemostore.dfs.core.windows.net/ adls_cred
This is the segregation step: the catalog's managed location is on the sales container, but the schema declares its own managed location on sales-orders.
CREATE CATALOG av_sales_demo MANAGED LOCATION 'abfss://sales@ucblogdemostore.dfs.core.windows.net/managed'; CREATE SCHEMA av_sales_demo.orders MANAGED LOCATION 'abfss://sales-orders@ucblogdemostore.dfs.core.windows.net/managed'; CREATE TABLE av_sales_demo.orders.line_items (order_id BIGINT, region STRING, amount DECIMAL(10,2)); INSERT INTO av_sales_demo.orders.line_items VALUES (1,'EMEA',120.50),(2,'AMER',88.00),(3,'APAC',310.75); CREATE VOLUME av_sales_demo.orders.landing_files; -- managed CREATE EXTERNAL VOLUME av_sales_demo.orders.raw_drop LOCATION 'abfss://landing@ucblogdemostore.dfs.core.windows.net/sales/raw_drop'; -- external
DESCRIBE DETAIL av_sales_demo.orders.line_items; DESCRIBE VOLUME av_sales_demo.orders.landing_files; DESCRIBE VOLUME av_sales_demo.orders.raw_drop;
Actual output (trimmed to the relevant fields):
-- line_items (MANAGED table) location: abfss://sales-orders@ucblogdemostore.../managed/__unitystorage/schemas/a412770d-.../tables/5d24725c-... -- landing_files (MANAGED volume) volume_type: MANAGED storage_location: abfss://sales-orders@ucblogdemostore.../managed/__unitystorage/schemas/a412770d-.../volumes/d3235d73-... -- raw_drop (EXTERNAL volume) volume_type: EXTERNAL storage_location: abfss://landing@ucblogdemostore.../sales/raw_drop
The catalog's managed location is on the sales container, yet the managed table and managed volume both landed under sales-orders, because the orders schema declared its own managed location and the schema wins over the catalog. That is physical storage segregation achieved with fully managed objects: no external tables, no loss of automatic optimization. The external volume sits exactly at the path it was given.
CREATE STORAGE CREDENTIAL for the credential, and CREATE EXTERNAL LOCATION on both the metastore and the credential for the location.Official Azure Databricks documentation:
2 weeks ago - last edited 2 weeks ago
Hi Ashwin,
Really clean write-up - the schema override demo with the actual DESCRIBE DETAIL output is what makes it click. Most posts on this topic stop at "here's the theory," this one actually shows it happening.
Something similar came up on a SAP HANA to Databricks migration I worked on, with a slight twist on the landing zone side.
We had two paths feeding into Bronze - Oracle GoldenGate pushing CDC events through Kafka into Structured Streaming, and a separate JDBC job handling the historical backfill. Both needed to write to a fixed path before Databricks touched anything. GoldenGate especially - it was configured to drop files at a specific ADLS location and that path had to just exist, stable, regardless of what we were doing inside Unity Catalog. So, the landing zone ahead of Bronze ended up external, basically your "fixed path, register-in-place" case exactly.
Once Structured Streaming picked those files up and wrote them into Bronze, everything from there on was managed. Honestly that was one of the better decisions - predictive optimization and auto VACUUM just ran on their own, one less job we had to maintain. By Gold, everything was managed, partitioned by company code and fiscal year, Z-ordered on the columns that got hit hardest in joins.
So, for us it landed as: external just for that one pre-bronze hop, managed for everything after. Curious if your banking customer's CDC setup let them skip that step entirely and write straight into a managed volume, or if they hit the same fixed-path constraint we did.