Home SQL About
CASE STUDY • OCTOBER 2026

Unity Catalog Is Not a Web App Database: Why Our Databricks App Moved to PostgreSQL

Our Databricks App kept its records in Unity Catalog Delta tables behind a SQL warehouse, and one save took about 7 seconds. We moved the same app to PostgreSQL and it became drastically faster. Here is what we measured, what tuning could not fix, and why Lakebase was not an option for us.

Before the move (Estimate)
~7 s
One save on the SQL warehouse, after tuning
After the move (Estimate)
< 1 s
The same save on PostgreSQL
Delta Lake
No row locks
Optimistic concurrency, conflicts found at commit
Unity Catalog
Keys: info only
Primary and foreign keys are not enforced
Infographic: Unity Catalog is not a web app database. One save on a Databricks SQL warehouse over Delta tables took about 7 seconds across 13 statements; the same app on PostgreSQL takes under 1 second.
The post in one picture: where the seven seconds went, why, what the app had to build around Delta, and what changed on PostgreSQL. Both totals are estimates.

01 The short answer

We run a web application that analyzes incoming email and performs AI analysis on it: how confident we are about the sender's intent, and which evidence supports it. It runs as a Databricks App. Until recently, every page and every save went through a SQL warehouse to Delta tables in Unity Catalog.

Unity Catalog itself is a governance layer for data and AI assets. In this post, "Unity Catalog as a database" means what our app actually used: Delta tables governed by Unity Catalog and queried through a SQL warehouse. That combination is built for analytics. It is not built for a web application that saves records all day.

The app was written for PostgreSQL first. On the warehouse, saving one record took about 7 seconds, even after several rounds of tuning (Estimate). We then moved the app's data to a PostgreSQL server. Nothing else changed: the same code, the same Databricks App, the same sign-in. The app became drastically faster (Observed). Only the database changed, and that is the whole lesson of this post.

How to read our numbers Observed: seen on our running app, or timed on a local PostgreSQL setup where we say "local". Emulator: exact statement counts from our offline stand-in for the warehouse, not timings. Estimate: simple arithmetic on those. Target: a threshold from our own diagnostics page, not a measurement. We have not yet timed individual requests on the live PostgreSQL server, so no PostgreSQL figure here is a live timing.

02 The basics, in plain words

Databricks is a cloud platform for analytics and AI, and the pieces it is made of explain everything that follows. This section covers what the platform is, what it is normally used for, and the seven pieces our app touched on every save. If you already work with Databricks, skip to the next section.

What Databricks is

In its own words, Databricks is "a unified, open analytics platform for building, deploying, sharing, and maintaining enterprise-grade data, analytics, and AI solutions at scale". In plain words, it is a service where a company brings its data together, cleans it, and uses it for reports, dashboards and machine learning.

It runs on AWS, Microsoft Azure and Google Cloud. The platform "integrates with cloud storage and security in your cloud account", so in the usual setup the data stays as files in the company's own cloud storage, and Databricks supplies the engines that work on those files. Azure Databricks is the same platform offered on Microsoft Azure, and it is where our app runs.

Two ideas carry most of the weight:

Workspace
"A workspace is an environment for accessing all of your Databricks assets." It is where a team signs in and finds its notebooks, dashboards, data and compute.
Storage and compute are separate
The data rests in storage. Compute is the machines that read and change it, and Databricks' lakehouse page describes an engine "that runs on compute resources decoupled from storage". A SQL warehouse is one kind of compute: "a computation resource on which you run SQL queries".

Where things are in a workspace

This is what a workspace looks like when you sign in. The screenshots in this section come from Databricks' own documentation as published in October 2026. The orange boxes and numbers are ours.

A Databricks workspace with seven highlights: the workspace name in the top bar, the sidebar entries Workspace, Catalog, Jobs and Pipelines, Compute and SQL Warehouses, and the Type column in the list of job and pipeline runs.
  1. Workspace name. The top bar shows which workspace you are in.
  2. Workspace. The workspace browser, where you "create, browse, and organize Databricks objects" such as notebooks and dashboards.
  3. Catalog. Opens Catalog Explorer, the screen for browsing what Unity Catalog governs: catalogs, schemas and tables.
  4. Jobs & Pipelines. The jobs and pipelines that load and reshape data. The page on the right is the list of their runs.
  5. Compute. The clusters that run notebooks and jobs.
  6. SQL Warehouses. The compute that runs SQL. Our app sent every statement to one of these.
  7. Type. Every run in the list is a job or a pipeline. This is the work the platform is built around.
Screenshot: Databricks documentation, Monitor Lakeflow Jobs. Cropped, highlights added.

What it is normally used for

Most work on Databricks moves data in one direction: from raw to ready for a report. Databricks calls the usual pattern the medallion architecture, "a series of data layers that denote the quality of data stored in the lakehouse".

The bronze layer "contains raw, unvalidated data". The silver layer "represents validated, cleaned, and enriched versions of the data". The gold layer "represents highly refined views of the data that drive downstream analytics, dashboards, ML, and applications". A pipeline is the code that moves data from one layer to the next. When this post says "pipelines and gold tables", this is what it means.

Notice the shape of that work: a few large jobs, each reading and writing many rows at a time, with nobody waiting on a single row. A web application has the opposite shape: many tiny reads and writes of single records, each with a person waiting for it. Section 04 explains why one engine is not good at both.

The seven pieces behind one save

Seven pieces sit between a click on Save and the files that hold the data: the browser, the Apps proxy, the Databricks App, a SQL warehouse, Unity Catalog, a Delta table and cloud object storage.

Two of those pieces are easy to picture once you have seen them. The first is the page Databricks shows for an app.

The App status panel on a Databricks App's overview page, with three highlights: the Running status, the app's own web address ending in databricksapps.com, and the workspace folder that holds its source code.
  1. Status. The app is running. It is a web server hosted by Databricks.
  2. The app's web address. People open this in a browser. The Apps proxy checks their sign-in before the request reaches the app.
  3. Source. The app's code, in a folder of the workspace.
Screenshot: Databricks documentation, Deploy a Databricks app. Cropped, highlights added.

The second is a SQL warehouse at work. A warehouse's Monitoring tab lists the statements it ran.

Query history on a SQL warehouse's Monitoring tab, with the Duration column highlighted: select 1 LIMIT 1000 took 214 ms, select now() took 228 ms and select 3 LIMIT 1000 took 200 ms.
  1. Duration. In Databricks' own example, select 1 LIMIT 1000 takes 214 ms and select now() takes 228 ms. Neither reads a table. This is one screenshot, not a benchmark, but it is the per-statement cost that section 04 is about.
Screenshot: Databricks documentation, Monitor a SQL warehouse. Cropped, highlight added. The red 6 is Databricks' own callout.

The terms

Lakehouse
Databricks describes a data lakehouse as "a data management system that combines the benefits of data lakes and data warehouses". The data sits as files in cloud object storage, and the platform is built for work such as business intelligence and machine learning.
Unity Catalog
Unity Catalog is "the unified governance layer for data and AI built into Databricks". It is the list of what exists and the rules for who may use it. It is not a storage engine.
Delta table
Delta Lake "extends Parquet data files with a file-based transaction log for ACID transactions". It is the default table format on Databricks.
SQL warehouse
A compute resource that runs SQL against those tables. The app never reads a Delta file directly; it asks the warehouse.
Databricks App
Databricks Apps "enables developers to build and deploy secure data and AI applications directly on the Databricks platform".
Volume
Volumes are "Unity Catalog objects that enable governance over non-tabular datasets". Our app keeps its uploaded files in one.
PostgreSQL and Lakebase
PostgreSQL is an open source relational database built for transactional work. Lakebase is Databricks' own managed Postgres service.

Every table has a three-part name

In Unity Catalog, tables and other objects "follow a three-level namespace": a catalog holds schemas, and a schema holds the objects themselves, such as tables, volumes and functions. The names below are made up.

Here are the same three levels in Catalog Explorer, which you open by clicking Catalog in the sidebar.

Catalog Explorer with four highlights: the catalog todos, the schema productivity_insights inside it, the table todo_priority_insights inside that, and the SQL warehouse selected at the top.
  1. Catalog: todos.
  2. Schema: productivity_insights.
  3. Table: todo_priority_insights. Its full name is todos.productivity_insights.todo_priority_insights.
  4. SQL warehouse. The table is storage. A warehouse is the compute that reads it.
Screenshot: Databricks documentation, Serve lakehouse data with synced tables. Highlights added.

What a Delta table is on disk, and what changing one row does

A Delta table is a folder. The rows are in Parquet files, a column-oriented file format. Next to them, a _delta_log folder holds one numbered JSON file for each committed change, as the Delta protocol describes. Databricks keeps that table history for 30 days by default.

The folder
invoices/
_delta_log/the log
00000000000000000000.jsonfirst commit
00000000000000000001.jsonsecond commit
part-0001.parquetrows
part-0002.parquetrows
Changing one row

plus one new entry in the log

Databricks states it plainly: "Without deletion vectors, modifying a single row requires rewriting the entire Parquet file containing that record." With deletion vectors, the row is marked as modified in metadata instead and readers apply that mark at query time. Either way, the design favours reading many rows at once over changing one. The rest of this post is what that looked like in a real application.

03 One save, two databases

Here is one save drawn to scale. Both lanes start the same way: every request first passes through the Databricks Apps proxy, which checks that the user is signed in; that takes about 0.5 s (Observed). On the warehouse the save then sends 13 statements, each waiting for the one before it (Emulator), and each takes about 0.5 s, anywhere from 0.3 to 1 s (Observed). On PostgreSQL the database work is too small to draw as steps. Whole requests take 0.06 to 0.17 s in our local measurements (Observed, local), and the second lane draws the larger figure.

One save, drawn to scale
Apps proxy sign-in, about 0.5 s (Observed) One warehouse statement, about 0.5 s (Observed) A whole request on PostgreSQL, up to 0.17 s (Observed, local)
What we looked atSQL warehouse over DeltaPostgreSQL
One small statementAbout 0.5 s. In two requests we timed end to end it came to 0.51 and 0.64 s per statement (Observed)Not timed on the live server yet. Our diagnostics page calls a round trip good at 20 ms or less (Target)
A whole requestOpening the list views: 7.7 s for 15 statements. A records query: 15.9 s for 25 statements (Observed, before tuning)A page of records from a 10,000-row table: 55 ms at the 95th percentile. Metadata requests: 100 to 170 ms (Observed, local, without the proxy)
One saveAbout 60 round trips before tuning (Observed). 13 statements in sequence after it (Emulator): about 7 s with the proxy (Estimate)Drastically faster on the running app (Observed, not yet timed). Under 1 s with the proxy (Estimate)
A good save, by our diagnostics page3 s or less (Target)0.5 s or less (Target)
Answers kept in memory to save round tripsConfiguration and permission reads; sign-in and access answers for 30 secondsNone. The app reads everything on every request

The two Target rows are the thresholds our diagnostics page uses on each database. We had to give the warehouse its own, looser set, or a healthy warehouse would always have read as slow.

None of these statements is a slow query. Each one reads or writes a handful of rows. What differs between the two lanes is the price of a single statement, multiplied by the number of statements the user waits for.

The last row deserves a second look. On the warehouse we kept configuration and permission answers in memory, because every read we avoided saved about half a second. On PostgreSQL we switched all of that off. The app now reads everything from the database on every request, so it sends more statements than it did on the tuned warehouse, and it is still drastically faster.

04 Why an analytics engine is slow at app work

Databases are built for one of two jobs. In Databricks' own explanation of what OLTP is, OLTP focuses on "efficiently and reliably storing and updating transactional data in real time for live operations", while OLAP "combines and groups data so it can be analyzed from different perspectives". A web app that saves records is OLTP work. A SQL warehouse is an OLAP engine.

1. A fixed cost for every statement

A SQL warehouse is a compute resource to query and explore data. In our app, every statement sent to it carried a fixed overhead before it ran, even one that touched a single row: 0.3 to 1 s per statement (Observed). Next to a report that runs for a minute, that overhead is nothing. Next to a save that touches five rows, it is almost all of the time.

The data layout points the same way. AWS explains it in its columnar storage documentation: transactional work reads and writes whole records, so row-wise storage suits it, while analytical queries read a few columns across many rows, so columnar storage suits them.

Row storage (OLTP)
idsenderintentscore
101a@example.cominvoice0.91
102b@example.netquery0.64
103c@example.orgsupport0.87
104d@example.commeeting0.72
Open email 101: one block holds the whole record.
Column storage (OLAP)
id
101
102
103
104
sender
a@example.com
b@example.net
c@example.org
d@example.com
intent
invoice
query
support
meeting
score
0.91
0.64
0.87
0.72
Average score: one column block is enough.

2. File skipping instead of a row index, and no row locks

PostgreSQL finds a row through an index and can lock it while a transaction changes it: FOR UPDATE locks the selected rows until the transaction ends. Databricks finds a row differently: it uses per-file statistics such as minimum and maximum values to skip files that cannot match, then reads the files that remain.

Concurrency is optimistic. Databricks' ACID guarantees page says "there are no locks on reading or writing against a table"; when two saves change overlapping data, the conflict is found at commit and one of them must retry. The transactions a web app's database driver opens are the interactive kind, and Databricks notes that interactive transactions "can conflict at the table level", apart from plain appends. Primary and foreign keys are informational, so the app has to check them itself.

3. Many statements, one after another

A save is not one statement. It checks who the user is and what they may change, reads the field definitions and rules, writes the record, its history and its audit trail, then commits. Microsoft calls this the chatty I/O antipattern: "The cumulative effect of a large number of I/O requests can have a significant impact on performance and responsiveness."

Before any tuning, a save needed about 60 round trips on the running app (Observed). We followed the antipattern's advice: we cached configuration and permission reads, wrote audit rows in one statement at commit, moved derived work to the background and skipped reads we did not need. In the last round, a save went from 32 statements, 29 of them one after another, to 18 statements, 13 one after another (Emulator).

Statements in one save, before and after the last round of tuning
Before the last round of tuning: 32 statements, 29 one after another (Emulator)
After: 18 statements, 13 one after another (Emulator). Lighter tiles run side by side
Tuning cuts the number of trips, not the cost of each trip Thirteen trips at about half a second is still about 6.5 s. Sizing the warehouse up or adding clusters helps when requests queue; it does not make one small statement cheap. For reads, Databricks has a Lakehouse Real-Time warehouse in Beta that targets sub-second SELECT queries, but it is read-only. A save still needs a different kind of database.

05 What the app had to build around Delta

Speed was the problem people could see. Underneath it, the app kept needing things a transactional database gives for free. Each row below is something we built, limited or accepted while the app ran on Delta (Observed in our project), next to what PostgreSQL does.

What the app needsOn Delta through a SQL warehouseOn PostgreSQL
Keys that holdKeys are informational, so the app checks them itself. Deleting a parent row leaves its child rows behind unless the app removes themThe database enforces primary and foreign keys
Two people saving at onceNo row locks. The save that loses the race at commit is run again, or the user is told the app is busyRow locks: the second save waits its turn
A commit that is refusedThe transaction stays open and aborted until it is rolled back. A pooled connection left that way broke the next request until our database layer learned to roll it back or replace itAfter a failed commit the connection was ready for the next request (Observed)
Undoing part of a batchDatabricks' ROLLBACK undoes the whole transaction and we had no savepoints, so a bulk save handles one record at a timeSavepoints: one record in a batch can be rolled back on its own
Many writes in one transactionDatabricks allows up to 100 intermediate commits per table in one transaction, and every copied record writes to the same shared tables. Copying a record with 120 related records failed, so that feature is limited to 40The same feature copies up to 1,000
Reading configurationKept in memory and carefully invalidated, because each read was a round tripNo cache: read on every request
The app's SQLWritten for PostgreSQL and translated, statement by statement, into the warehouse's SQLRuns as it was written
Schema changesIn our deployment the app could not alter tables, so each release's table changes ran from notebooks and each new table needed its own grantOne schema file, applied in one transaction before the release

None of this is a flaw in Delta. It is what an analytics table format looks like when a web application leans on it. Every row above was code we wrote, tested and maintained only because of where the data lived.

06 The move to PostgreSQL

The fix was to give each kind of work the engine built for it. The app's records now live in PostgreSQL, where a small statement is cheap. Delta stays the home of analytics: pipelines and gold tables remain there. The app is still a Databricks App behind the same proxy, and its files are still in a Unity Catalog Volume.

Before: every page and every save went to the warehouse
Reviewers
web browser
Apps proxy
sign-in, about 0.5 s
Databricks App
pages, AI analysis
SQL warehouse
0.3 to 1 s per statement
Delta tables
app data and analytics
Now: the app talks to PostgreSQL, and Delta keeps the analytics
Reviewers
web browser
Apps proxy
sign-in, about 0.5 s
Databricks App
the same app and code
PostgreSQL
app records, indexes, row locks

How the move went

1
A PostgreSQL server next to the workspace

A managed server in the same region as the Databricks workspace. The app connects over verified TLS and reads its password from a Databricks secret.

2
A trial copy while the app kept running

The copy only reads the Delta tables, so we could rehearse it at any time. The Delta tables were never changed: they were the way back.

3
Check every relationship

Delta enforces no foreign keys, so the copy loads every table first, then restores and checks each relationship and reports any row it could not place. In rehearsal it moved 3,382 of 3,382 rows and checked all 395 relationships.

4
Stop, copy again, switch

Stop the app and the intake pipeline, copy once more to pick up the last rows, point the app's configuration at PostgreSQL instead of the warehouse, and deploy.

5
Know the way back

Going back means restoring the earlier configuration and release. Rows saved on PostgreSQL after the switch are not carried back, so check the app before people return to it.

The email intake pipeline used to write new rows straight into Delta. It now sends them to the app's PostgreSQL data instead.

Ask which extensions your server allows Our search used two PostgreSQL extensions: pg_trgm, which matches text by trigram similarity, and btree_gin, which lets common data types share a GIN index. On Azure Database for PostgreSQL an extension has to be added to the allow list before it can be created, and on our server these two were not. The app now checks once and runs without them: search still finds whole words and parts of words, but it no longer forgives small typos.

Why PostgreSQL and not Lakebase

On Databricks, the native choice for this is Lakebase, a fully managed Postgres database integrated into the platform and offered for low-latency apps. It has been generally available on AWS since January 2026, and Databricks announced general availability on Azure Databricks in March 2026.

Generally available does not mean available in every workspace. Lakebase runs in a list of supported regions, and a project is always created in the workspace's own region. Compliance settings matter too: in March 2026 Lakebase covered workspaces with the compliance security profile only for some compliance standards, and others, such as PCI-DSS and HITRUST, were added in August 2026. In the Azure Databricks workspace this app runs in, Lakebase was not available to us when we needed it. So we moved the app's data to Azure Database for PostgreSQL, Microsoft's fully managed PostgreSQL service. Because the app was written for PostgreSQL first, its SQL runs there as it was written.

What changed for the people using it

Nothing about the app changed for its users: the same address, the same sign-in, the same screens. What changed is the wait. On the warehouse a save took about 7 s after tuning (Estimate). On PostgreSQL the app is drastically faster (Observed). We have not yet timed individual requests on the live server, so the "under 1 s" in this post stays an Estimate until we do.

07 Choose the database before you build

Pick the database by the work the app does, before any code depends on it: count the statements a click sends, price one statement, and keep app records and analytics on separate engines.

1
Write down a typical click

For your three busiest actions, list every statement they send. If a save sends more than a handful, the app needs a transactional database.

2
Price one statement

Time one small read and one small write on each candidate database, then multiply by the number of statements a user waits for.

3
Split the work

App records in a transactional database; analytics in Delta. If reports need the app's records, copy them into Delta rather than pointing the app at Delta.

4
Confirm Lakebase is available to you

Lakebase is the native choice on Databricks, and a Lakebase database can be added to an app as a resource. Before you design around it, check that your workspace's region is on the supported list and that you can create a project in your own workspace.

5
Keep the database close

Put it in the same region as the app and pool connections. Azure Database for PostgreSQL has a built-in PgBouncer connection pooler on its General Purpose and Memory Optimized tiers (not on Burstable).

6
Tune anyway

Batching writes helped on the warehouse and still helps on PostgreSQL. The read caches we built for the warehouse turned out to be unnecessary there.

08 Frequently Asked Questions

Yes. An app can read and write Unity Catalog Delta tables through a SQL warehouse, and that suits read-mostly apps such as reporting portals and dashboards. For an app that saves records all day, each statement is a comparatively expensive round trip, so give the app's records a transactional database.
Because the cost was per statement, not per row. Each statement took about half a second on the warehouse, and a save waited for 13 of them one after another, even after tuning cut the ones in sequence from 29 to 13 (Emulator).
On the warehouse one small statement cost about 0.5 s and a save about 7 s. On PostgreSQL whole requests take 0.06 to 0.17 s in our local measurements, and the running app became drastically faster after the move. We have not yet timed individual requests on the live server.
Databricks announced general availability of Lakebase on Azure Databricks in March 2026, and it runs in a list of supported Azure regions. Whether you can use it still depends on your own workspace, so confirm that before you design around it. If it is not available to you, a managed PostgreSQL server in the same region gives the app the same kind of engine.
No. Unity Catalog and Delta are the right home for analytics, pipelines and governed data, and ours stay there. They are the wrong place for the records a web application reads and writes one at a time.

09 Abbreviations & Glossary

ACID
Atomicity, consistency, isolation, durability: the guarantees of a database transaction.
AI
Artificial intelligence.
Apps proxy
The reverse proxy in front of every Databricks App. It passes each signed-in user's request, with their identity in HTTP headers, on to the app.
AWS
Amazon Web Services.
Azure Databricks
The Databricks platform offered as a service on Microsoft Azure.
Compute
The machines that run queries and jobs. On Databricks it is separate from the storage that holds the data.
Delta
Delta Lake, the table format behind Databricks tables.
GIN
Generalized Inverted Index: a PostgreSQL index type. Our search used one through the pg_trgm and btree_gin extensions.
HITRUST
A certifiable security and privacy framework that brings several compliance standards together. One of the standards a Databricks compliance security profile can be set to.
HTTP
Hypertext Transfer Protocol: the protocol a browser and a web application use to exchange requests and responses.
I/O
Input/output: here, a request sent over the network to a database or service.
JSON
JavaScript Object Notation: a text format for structured data. Each entry in a Delta table's log is a JSON file.
Lakebase
Databricks' fully managed Postgres database for transactional work.
Medallion architecture
Databricks' pattern of data layers by quality: bronze (raw), silver (cleaned) and gold (refined for reports).
ML
Machine learning.
OLAP
Online analytical processing: scans and aggregates over many rows.
OLTP
Online transaction processing: many small reads and writes of single records.
Parquet
Apache Parquet: a column-oriented file format. A Delta table keeps its rows in Parquet files.
PCI-DSS
Payment Card Industry Data Security Standard: a global standard for organizations that store, process or transmit card data.
Round trip
One statement sent to the database and its answer.
SQL
Structured Query Language: the language used to read and write data in a database.
SQL warehouse
Databricks compute for SQL queries over Delta tables.
TLS
Transport Layer Security: the encryption on the connection between the app and its database.
Unity Catalog
Databricks' governance layer for data and AI assets.
Volume
A Unity Catalog object for governing files that are not tables.
Workspace
The environment where a team signs in to Databricks and reaches its notebooks, dashboards, data and compute.
Link copied to clipboard!
@
Previous Post
Salesforce Agentforce Credits & Cost Model: Complete Guide 2026
Archives by Year
2026 17 2025 16 2024 2 2023 9 2022 8 2021 4 2020 18 2019 16 2018 21 2017 34 2016 44 2015 54 2014 30 2013 31 2012 46 2011 114 2010 162
Search Blog

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from Jitendra Zaa

Subscribe now to keep reading and get access to the full archive.

Continue Reading