Performance Optimization on Databricks/AWS: Liquid Clustering, File Layout, Skew and Warehouse Tuning
Tune a Databricks lakehouse on AWS layer by layer: target file sizes (128 MB auto-optimize, 256 MB autotune floor, 1 GiB manual OPTIMIZE ceiling) and the small-file ladder; the partitioning decision (<1 TB never, 1-100 TB clustering, >=100 TB maybe) with the >=1 GB-per-partition guard; liquid clustering deep (CLUSTER BY / CLUSTER BY AUTO, up to four keys, clustering-on-write thresholds, incremental OPTIMIZE vs OPTIMIZE FULL, bin packing idempotence) against the legacy partition+ZORDER path in a worked OPTIMIZE vs CLUSTER BY comparison with a before/after latency plot; data skipping and per-file statistics; AQE defaults (200 shuffle partitions, 30 MB runtime broadcast, 5x skew factor, 64 MB advisory), broadcast/skew/salting ladder with real SQL; Photon by workload; Databricks SQL warehouse resize-vs-scale sizing and the result/disk/Spark cache tiers; query-profile and Spark UI profiling with a worked shuffle-partition calculation; seven worked examples, seven mermaid diagrams, five interactive checks, Comcast, Rivian and Punchh case studies on AWS, an October 2026 update box and twelve practice questions.
Slow lakehouse queries are almost never "Spark is slow". They are a specific layer misbehaving: too many small files on S3, a filter column whose min/max ranges overlap in every file, a single hot join key carrying 38% of the rows, or a warehouse that is the right size and the wrong count. This lesson teaches the stack in the order you should fix it, layout β statistics β planning/runtime β serving, and it teaches the 2026 version of that stack, not the 2019 one.
That distinction matters more than any single knob. Databricks' current documentation is blunt: "Databricks recommends using liquid clustering instead of partitions, ZORDER, or other data layout approaches" (docs .../tables/operations/optimize, last updated 2026-03-31, mirror 2026-09-11), and "Manual tuning recommendations don't apply to Unity Catalog managed tables, which use automatic file size tuning" (docs .../tables/tune-file-size, 2026-09-11). Meanwhile the engine layer keeps self-tuning: AQE has been on by default since DBR 7.3, Photon is the default execution engine in Databricks SQL warehouses, and Databricks reports that "in 2025, Databricks SQL delivered up to 40% faster performance across production workloads, with improvements applied automatically" (blog, 2026-01-28).
So the honest framing for October 2026 is: manual tuning is most valuable on external tables, legacy hive-metastore workloads and non-default clusters, and on understanding which layer is broken before you touch anything. Every fact in this lesson comes from the Databricks AWS documentation pages on optimize, file size, partitions, clustering, data skipping, AQE, hints, predictive optimization, caching, warehouse behavior and the query profile (all accessed or dated October 2026), plus dated Databricks engineering blogs. No DBU rates appear here, cost is lesson 15's subject.
=============================================================
THE PERFORMANCE STACK - fix top-down, measure after each step
-------------------------------------------------------------
1. DATA LAYOUT ....... file sizes, CLUSTER BY, partitions
2. STATISTICS ........ per-file min/max -> data skipping
3. PLANNING + RUNTIME AQE, broadcast, skew, Photon
4. SERVING + CACHING .. warehouse size vs count, caches
Loop closes with: DBSQL query profile (warehouses)
Spark UI (classic / jobs clusters)
Managed-table reality: predictive optimization runs
OPTIMIZE + VACUUM + ANALYZE for you; auto file-size tuning
picks 256 MB / 1 GB bands; layout work is mostly for
EXTERNAL tables and legacy estates.
Defaults verified Oct 2026, no prices in this lesson.
=============================================================In this lesson you will:
- walk the four-layer stack and learn which symptom belongs to which layer before touching a single setting;
- size files against the three official targets, 128 MB auto-optimize, the 256 MB β 1 GB autotune band, and the 1 GiB manual
OPTIMIZEceiling, and hand-compute the file counts; - apply the partitioning ladder (< 1 TB never Β· 1 to 100 TB clustering Β· β₯ 100 TB maybe) and the β₯ 1 GB per partition guard, then watch the high-cardinality anti-pattern fail arithmetic;
- go deep on liquid clustering:
CLUSTER BY,CLUSTER BY AUTO, β€ 4 keys, clustering-on-write thresholds, incrementalOPTIMIZEvsOPTIMIZE FULL, hierarchical keys, bin-packing idempotence and the DBR floors with their dates; - run a worked OPTIMIZE vs CLUSTER BY comparison, the same 8 TB table, the legacy partition+
ZORDERSQL beside the liquid SQL, and read the before/after latency plot; - diagnose join skew from real profile evidence and climb the documented ladder: broadcast hint β AQE skew split β salting, with the wrong-fixes rail beside it;
- configure AQE with its verified defaults (200 shuffle partitions, 30 MB runtime broadcast, 64 MB advisory, 5Γ skew factor);
- judge Photon by workload, where it wins, where Python UDFs defeat it, and size a Databricks SQL warehouse with the resize-vs-scale doctrine plus the cache tiers;
- profile like an operator: the query profile for warehouses, the Spark UI for clusters, and a shuffle-partition calculation you can do on paper;
- study three named AWS customer performance stories, read the 2026 update box, and practise with twelve exam-style questions plus four interactive checks.
1. The four-layer stack, and how to pick a layer
Every query latency in this course is decided in four layers, in this order. Fix them top-down and measure again after each change: never two changes at once, or you cannot attribute the win.
Layer | The question it answers | The evidence you read | The lever |
|---|---|---|---|
1. Data layout | Are the right rows in the same files? | file count, average size, % < 20 MB, DESCRIBE DETAIL | OPTIMIZE, CLUSTER BY, compaction cadence |
2. Statistics | Can the engine prove a file is irrelevant? | "% of data pruned" icon in the query profile | stats columns, clustering keys, sargable predicates |
3. Planning + runtime | Is the plan and the shuffle healthy? | Spark UI stage metrics, SortMergeJoin (isSkew=true) | AQE, hints, salting, Photon, UDF rewrite |
4. Serving + caching | Is the compute the right shape? | "Bytes spilled to disk", Peak Queued Queries | warehouse size, max_num_clusters, caches |
Two 2026 realities reshape that table. First, layers 3 and 4 increasingly self-tune: AQE is on by default (spark.databricks.optimizer.adaptive.enabled = true), Photon runs by default in Databricks SQL warehouses, and managed tables get automatic file-size tuning. Second, layer 2 has no ceiling on managed tables: for Unity Catalog external tables, statistics are collected on the first 32 columns; for UC managed tables, "predictive optimization collects file-skipping statistics on the columns most frequently used in your query filters, instead of a fixed set of columns, so these tables don't have a 32-column limit" (docs .../tables/data-skipping, 2026-06-18).
The rest of this lesson is that stack, layer by layer.
2. Layer 1: file sizing and the small-file problem on S3
Small files hurt because each file costs an S3 LIST/GET round-trip, a Spark task, and a Parquet footer parse. As Databricks' own guide puts it, part of the cost is simply "the time spent for just opening and closing those tiny files" (discover guide, 2026-05-06). On S3 there is no block layer: the file is the unit of parallelism, the unit of statistics, and the unit VACUUM reclaims.
2.1 Three official targets, one healthy band
Three different numbers are all "official", and teaching them as one number is the classic stale-tutorial error:
Mechanism | Target file size | Source (date) |
|---|---|---|
Manual OPTIMIZE output ceiling | 1,073,741,824 B = 1 GiB ( spark.databricks.delta.optimize.maxFileSize) | Delta OPTIMIZE language manual; discover guide (2026-05-06) |
Auto-optimize set to true (autoOptimize.optimizeWrite + autoCompact) | 128 MB | docs .../tables/tune-file-size (2026-09-11) |
Autotuned target (UC managed default behaviour) | < 2.56 TB β 256 MB Β· 2.56 to 10 TB β linear 256 MB β 1 GB Β· > 10 TB β 1 GB | same (2026-09-11) |
Healthy guidance band | "between 16MB and 1GB" | discover guide (2026-05-06) |
Two corollaries that prevent the two most common layout mistakes:
- 1 GiB is a ceiling, not a goal for small tables. Compacting a 300 GB table into 1 GiB files leaves ~300 files and destroys pruned-scan parallelism.
- Raising a target never merges anything by itself. "When the target file size for a table grows, existing files are not re-optimized into larger files by the
OPTIMIZEcommand" (tune-file-size, 2026-09-11); you must runOPTIMIZEafterwards.
2.2 Worked example E1: what 8 TB of Auto Loader output looks like
Worked example E1 - file arithmetic for an 8 TB events table on S3 (as of Oct 2026)
BEFORE (24 Auto Loader streams, one per device fleet)
File count ............... 19,000
Average .................. 8 TB / 19,000 ~= 440 MB <- MISLEADING
Shape .................... 60% of files under 20 MB (streaming micro-batches)
Serving query ............ 12,000 files scanned after min/max pruning
p95 ...................... ~9.5 min on an X-Large warehouse, spill visible in profile
THE TARGET (autotune band 2.56 - 10 TB -> linear 256 MB -> 1 GB)
8 TB interpolates to roughly 768 MB per file
Manual OPTIMIZE ceiling ................ 1 GiB (1,073,741,824 B)
Healthy band .......................... 16 MB - 1 GB
Reference file counts from the docs table:
1 TB @ 256 MB ....... 4,096 files
10 TB @ 1 GB ......... 24,437 files
100 TB @ 1 GB ........ 114,437 files
AFTER (layout fixed - see worked example E4 for the SQL)
File count ............... ~10,000 near-target files
Pruning .................. > 95% of files excluded by customer_id / event_date filters
p95 ...................... ~25-40 s (illustrative band, consistent with Databricks'
reported 2-12x read gains and a 12x point-query report,
blog 2024-05-22 - not a measured benchmark of this table)The lesson of E1 is not the 15 to 25Γ band; it is that file count and file shape are different numbers. A 440 MB average with a 20 MB median means the engine still pays task-per-file overhead on most of the table.
2.3 Three mechanisms fight small files, in increasing order of authority
- Optimized writes (
autoOptimize.optimizeWrite/spark.databricks.delta.optimizeWrite.enabled) shuffle before writing so each partition emits ~128 MB files. They are on by default forMERGE,UPDATE/DELETEwith subqueries, CTAS/INSERT on SQL warehouses, and "In Databricks Runtime 13.3 LTS and above, all tables registered in Unity Catalog have optimized writes enabled for CTAS statements and INSERT operations for partitioned tables". The cost is real: "Writing fewer large files is more efficient β¦ but you might still see an increase in write latency because data is shuffled before being written." - Auto compaction (
autoOptimize.autoCompact) "runs synchronously on the cluster that performs the write, after the write succeeds, and only compacts files that haven't been compacted previously". In table history it appears as anOPTIMIZEoperation withoperationParameters.auto=true. ForMERGE,UPDATEandDELETEit is mandatory: "You can't turn off this functionality." - Scheduled or predictive
OPTIMIZE: "For tables larger than 1 TB, Databricks recommends runningOPTIMIZEon a schedule", classically "daily, preferably at night when spot prices are low" and on a separate job cluster, and for hot liquid-clustered tables, "scheduling anOPTIMIZEjob every one or two hours" (clustering docs, 2026-09-25), because clustering is incremental and most runs are quick.
Auto compaction and optimized writes "are not a full replacement for OPTIMIZE" (tune-file-size, 2026-09-11), and if predictive optimization is enabled, "Databricks recommends disabling any scheduled OPTIMIZE jobs" so you stop paying twice (clustering docs).
How to answer
Fill-in-the-blank exercise with 4 blanks. Type the missing words, then check your answer.
Fill in the file-layout and AQE values exactly as the October 2026 documentation states them:
Auto-optimize set to blank targets 128 MB files; the manual OPTIMIZE output ceiling is blank GiB; tables below 2.56 TB are autotuned to a blank MB target; and the default value of spark.sql.shuffle.partitions is blank.
Word Bank, drag words into the blanks above:
β οΈ WARNING, do not fight the write path with your hands. "If you use optimized writes, Databricks recommends that you don't run
coalesce(n)orrepartition(n)just before a write to control the number of files written" (tune-file-size, 2026-09-11), you pay two shuffles for one job. Equally: never turn auto compaction off at the session level and forget. It cannot be disabled forMERGE/UPDATE/DELETE, but a session-levelfalsedoes silently disable it for appends and CTAS in that workload.
- π Did you know?
OPTIMIZEis safe by construction: "Bin-packing optimization is idempotent, meaning that if it is run twice on the same dataset, the second run has no effect", and "BecauseOPTIMIZEmakes no data changes to the table, a read before and after anOPTIMIZEhas the same results. PerformingOPTIMIZEon a table that is a streaming source does not affect any current or future streams with this table as a source" (docs.../operations/optimize, 2026-03-31). ZORDER, by contrast, was never idempotent; it was incremental and needed re-running.
3. Layer 1, partitioning: the ladder and the β₯ 1 GB guard
The current partitioning guidance is newer than most tutorials and blunter than most teams expect: "Most tables on Databricks with less than 100 TB of data don't need partitioning" (docs .../tables/partitions, 2026-07-01).
The second guardrail is per-partition: "verify that each partition contains at least 1 GB of data. Tables with fewer, larger partitions tend to outperform tables with many smaller partitions" (same page). Partition count equals directory count in your bucket, so every extra level of cardinality multiplies LIST calls, _delta_log entries, OPTIMIZE planning scans and MERGE conflict surface.
Do not partition when: the table is under 1 TB; the filter column is high cardinality (user_id, event_id: cluster instead); access patterns change (partitioning "is static and manifests itself as a filesystem hierarchy. There is no easy way to change partitions", best-practices page); the column is a timestamp that would explode into millions of instants (cluster the raw timestamp instead); or you would partition on a struct field (PARTITIONED BY treats a struct as an expression, not a column reference).
3.1 Worked example E2: the high-cardinality anti-pattern, in arithmetic
Worked example E2 - partitioning the same 8 TB events table by customer_id
PARTITIONED BY (customer_id) with ~200,000 distinct customers
Directories ................ 200,000
Average bytes per partition . 8 TB / 200,000 ~= 40 MB
Guardrail .................. >= 1 GB per partition -> VIOLATED by 25x
Symptom .................... thousands of tiny files per day, compaction never
keeps up, _delta_log listing grows, every OPTIMIZE
and MERGE pays for 200k directory listings
CLUSTER BY (customer_id, event_date) instead
Directories ................ none (hidden partitioning, protocol 3/7)
Rewrite cost ............... metadata-only to declare; OPTIMIZE FULL once
Pruning .................... file-granularity, driven by per-file min/max stats
Change of mind ............. ALTER TABLE ... CLUSTER BY (new cols) - no rewritePartitioning keeps exactly two legitimate roles in 2026: partition-level lifecycle moves (DELETE/DROP PARTITION on a date column at β₯ 100 TB scale) and Delta Sharing partition filtering on shared tables; which is also why converting a partitioned shared table to clustering is blocked.
4. Liquid clustering, deep
"Liquid clustering is a data layout optimization technique that replaces table partitioning and ZORDER" (docs .../tables/clustering, last updated 2026-09-25), and "you can redefine clustering keys without rewriting existing data". That last clause is the whole design: partitioning bakes a filesystem hierarchy you cannot cheaply change; clustering stores a declared key set in table metadata and lets OPTIMIZE physically co-locate ranges incrementally.
4.1 The declaration surface and the DBR floors
-- Declare at CREATE time (metadata only - no data is touched)
CREATE TABLE main.silver.events (
customer_id STRING,
event_date DATE,
event_type STRING,
payload STRING
) USING DELTA
CLUSTER BY (customer_id, event_date);
-- Or attach clustering to an existing table
ALTER TABLE main.silver.events CLUSTER BY (customer_id, event_date);
-- Let the platform pick and evolve keys (15.4 LTS+, UC managed + predictive optimization)
ALTER TABLE main.silver.events CLUSTER BY AUTO;
-- Remove clustering
ALTER TABLE main.silver.events CLUSTER BY NONE;Carry these version numbers with their dates; they are the numbers exams and architecture reviews ask for:
Fact | Value | Source (date) |
|---|---|---|
Trigger / write clustering | DBR 13.3 LTS+ | clustering docs (2026-09-25) |
GA announcement | "generally available in Databricks from DBR 15.2" | blog (2024-05-22) |
Current GA statement | "GA for Delta Lake tables with DBR 15.4 LTS and above"; Iceberg in Public Preview on 16.4 LTS+ | clustering docs (2026-09-25) |
Recommended for large-table OPTIMIZE | DBR 17.3 LTS+ | clustering docs (2026-09-25) |
OPTIMIZE β¦ FULL (force recluster) | DBR 16.4 LTS+ | clustering docs (2026-09-25) |
OPTIMIZE β¦ FULL WHERE β¦ (partial recluster) | DBR 18.1+ | clustering docs (2026-09-25) |
CLUSTER BY AUTO | DBR 15.4 LTS+, UC managed + predictive optimization | clustering docs (2026-09-25) |
Hierarchical clustering | DBR 17.1+ ( delta.liquid.hierarchicalClusteringColumns) | clustering docs (2026-09-25) |
REPLACE PARTITIONED BY WITH CLUSTER BY | DBR 18.1+ (reads from 13.3 LTS+) | clustering docs (2026-09-25) |
Max clustering keys | 4: below 10 TB, more keys "can degrade performance when filtering on a single column" | clustering docs (2026-09-25) |
Stats required on keys | yes: first 32 columns by default | clustering + data-skipping docs (2026) |
Table protocol | reader 3 / writer 7 ("hidden partitioning") | feature-compatibility docs (2026-09-11) |
Compatible with partitioning or ZORDER? | No: "Clustering is not compatible with partitioning or ZORDER" | clustering docs (2026-09-25) |
4.2 Clustering on write: how streaming lands pre-clustered
You do not have to wait for a nightly job. When a write is large enough, clustering runs during the write, per key count (UC managed / other Delta tables):
Clustering keys | UC managed threshold | Other Delta tables |
|---|---|---|
1 | 64 MB | 256 MB |
2 | 256 MB | 1 GB |
3 | 512 MB | 2 GB |
4 | 1 GB | 4 GB |
This is why a CTAS or a streaming micro-batch can land physically clustered without any scheduled maintenance, and why OPTIMIZE on a clustered table is usually fast: it only rewrites what still needs clustering.
4.3 The lifecycle, including the two expensive moments
Two moments in that diagram cost real money and real calendar time:
β οΈ WARNING: declaring keys is free; making old data match them is not. "The default behavior does not apply clustering to previously written data"; you must run
OPTIMIZE <table> FULL, which "might take hours" on a large unclustered table (clustering docs, 2026-09-25). Budget it as a one-off migration job on its own cluster (compute-optimized instances, spot at night), not as a step in a release pipeline. And never claim "enabling clustering rewrote my table":ALTER TABLE β¦ CLUSTER BYis metadata;OPTIMIZE FULLis the rewrite.
β οΈ IMPORTANT:
OPTIMIZErewrites files, so it feedsVACUUM. Every recluster deletes-and-adds files; the old versions survive only fordelta.deletedFileRetentionDuration(default 7 days). Set that property before you enable predictive optimization: "If you setdelta.deletedFileRetentionDurationto less than 7 days, predictive optimization still retains data files for a minimum of 7 days when it runsVACUUM FULL" (predictive-optimization docs, 2026-09-11), and never runVACUUMin the same job step as long-running readers or streams.
4.4 Worked example E3: adopting clustering on a live table
-- Worked example E3 - adopt liquid clustering on main.silver.events (8 TB, external on S3)
-- Step 1 - declare the keys your filters actually use (metadata only)
ALTER TABLE main.silver.events CLUSTER BY (customer_id, event_date);
-- correlated columns? one is enough: "If two columns are highly correlated,
-- you only need to include one of them in the clustering columns"
-- Step 2 - one-off full recluster, on its own job cluster, at night (DBR 16.4 LTS+)
OPTIMIZE main.silver.events FULL;
-- Step 3 - verify what actually ran (history disambiguates the operation type)
DESCRIBE HISTORY main.silver.events LIMIT 5;
-- operationParameters.clusterBy -> liquid clustering
-- operationParameters.zOrderBy -> legacy ZORDER
-- operationParameters.auto -> auto compaction
SHOW TBLPROPERTIES main.silver.events;
-- delta.clusteringColumns -> [customer_id, event_date]
-- Step 4 - incremental maintenance: hot table, every one or two hours
OPTIMIZE main.silver.events WHERE event_date >= current_date() - 2;
-- Step 5 - per-quarter deep maintenance on older data, still incremental
OPTIMIZE main.silver.events WHERE event_date < current_date() - 90;Why a predicate in step 4/5? OPTIMIZE <table> WHERE <predicate> scopes the compaction/recluster to a subset: cheaper, and it shrinks the conflict window with concurrent writers, the same reason best practices push pruning predicates into MERGE... ON.
- π Did you know?
DESCRIBE HISTORYis also your maintenance audit trail: auto compaction, liquid clustering and Z-ordering "all appear in table history asOPTIMIZEoperations", and you tell them apart only throughoperationParameters.{auto, clusterBy, zOrderBy, predicate}(docs.../tables/history, 2026-09-11). A dashboard that counts "OPTIMIZEruns" therefore over-counts auto compaction by a wide margin: filter onauto = 'false'for manual runs.
5. Worked example E4: OPTIMIZE vs CLUSTER BY, side by side
Same table, same 8 TB, two generations of layout doctrine. This is the comparison the course exists to make.
-- ============================================================
-- PATH A - legacy: Hive partitioning + ZORDER (pre-2024)
-- ============================================================
CREATE TABLE main.silver.events (
customer_id STRING, event_date DATE, event_type STRING, payload STRING
) USING DELTA
PARTITIONED BY (event_date); -- date cardinality "seems" safe
-- props you had to remember to set yourself
ALTER TABLE main.silver.events SET TBLPROPERTIES (
'autoOptimize.optimizeWrite' = 'true',
'autoOptimize.autoCompact' = 'true',
'delta.targetFileSize' = '536870912' -- 512 MB, manual guess
);
-- ZORDER the column your dashboards filter on (non-partitioned, stats required)
OPTIMIZE main.silver.events
WHERE event_date >= DATE'2026-10-01'
ZORDER BY (customer_id);
-- and because ZORDER "is not idempotent but is incremental", and because MERGE
-- rewrites blunt it (low shuffle merge helps but does not remove the need),
-- you re-run it on a schedule, forever, inside the date partition.
-- ============================================================
-- PATH B - liquid clustering (Oct 2026 recommendation)
-- ============================================================
CREATE TABLE main.silver.events (
customer_id STRING, event_date DATE, event_type STRING, payload STRING
) USING DELTA
CLUSTER BY (customer_id, event_date); -- no PARTITIONED BY, no ZORDER
-- one-off, after keys are first set or changed (DBR 16.4 LTS+)
OPTIMIZE main.silver.events FULL;
-- incremental maintenance from then on; bin packing happens in the same command
OPTIMIZE main.silver.events WHERE event_date >= current_date() - 2;
-- managed tables: skip the cron job entirely and let predictive optimization
-- run OPTIMIZE/VACUUM/ANALYZE for you (serverless-billed), then delete yours.What changed between the two paths:
Dimension | Path A: partition + ZORDER | Path B: CLUSTER BY |
|---|---|---|
Declared in | filesystem hierarchy + a rewrite command | table metadata (protocol 3/7) |
Change the key | effectively a table rebuild; "no easy way to change partitions" | ALTER TABLE β¦ CLUSTER BY, no rewrite |
Maintenance | re-run ZORDER (not idempotent), inside partitions | incremental OPTIMIZE, idempotent bin packing |
Predictive optimization | "does not run ZORDER β¦ ignores Z-ordered files" | fully supported: CLUSTERING operations appear in history |
High-cardinality column | forbidden as a partition key | the best clustering key candidate |
Benchmark (1 TB warehouse workload) | Hive-style partitioning ~an order of magnitude slower than liquid; Z-order in the middle | 2.5Γ faster clustering than Z-order (blog, 2023-06-29) |
Field results | : | point queries up to 12Γ at a manufacturing firm; read performance 2 to 12Γ reported at GA (blog, 2024-05-22) |
Read that plot as a causal ladder: optimized writes and bin packing (layer 1, file size) buy you a solid but bounded gain because pruning still cannot prove anything; CLUSTER BY (layer 1 + layer 2 together) is what makes the customer_id filter exclude >95% of files. The final bar is labelled illustrative because it is a reference-architecture sketch constrained by Databricks' published 2 to 12Γ read band, not a number from your table: measure yours in the query profile.
5.1 Layer 2: why clustering is what makes skipping work
Delta writes per-file statistics at commit time: "minimum and maximum values, null counts, and total records" (data-skipping docs, 2026-06-18), and at query time the engine skips any file whose stats prove the predicate cannot match. Clustering does not change what can be skipped, "the engine prunes at file granularity. There is no directory-level shortcut to lose" (Databricks blog, 2026-01-06), it changes how much is skipped, by making ranges of the key physically contiguous.
Skipping silently fails when: the predicate is non-sargable (WHERE lower(col) = β¦, WHERE date_add(col, 1) = β¦); the filter column has no statistics; values are uniformly distributed across all files; or you combine columns in a way min/max cannot prove. Stats knobs: delta.dataSkippingNumIndexedCols (positional) and delta.dataSkippingStatsColumns (DBR 13.3 LTS+, explicit list, supersedes the numeric one), and "Updating these properties does not automatically recompute statistics for existing data". Finally: "Databricks recommends that you don't use ZORDER BY on columns that don't have statistics collected because it is ineffective and uses unnecessary compute resources."
6. Layer 3, joins: AQE, broadcast, skew and salting
6.1 The AQE defaults you are actually running with
AQE is on by default and does four things: convert sort-merge join into broadcast at runtime, coalesce post-shuffle partitions, split (and if needed replicate) skewed partitions, and propagate empty relations (AQE docs, page dated 2023-10-12, still current in Oct 2026).
Setting | Default | Meaning |
|---|---|---|
spark.databricks.optimizer.adaptive.enabled | true | AQE master switch |
spark.sql.shuffle.partitions | 200 (value auto = auto-optimized shuffle) | initial shuffle partition count |
spark.databricks.adaptive.autoBroadcastJoinThreshold | 30 MB | runtime switch to broadcast join |
spark.sql.autoBroadcastJoinThreshold | 10 MB | static planning threshold (Spark upstream) |
spark.sql.adaptive.advisoryPartitionSizeInBytes | 64 MB | coalescing target after shuffle |
spark.sql.adaptive.skewJoin.enabled | true | automatic skew split |
spark.sql.adaptive.skewJoin.skewedPartitionFactor | 5 | skewed if > 5Γ median and over the byte threshold |
Skew byte threshold | 256 MB (Spark default: verify on your DBR) | second condition of the skew test |
spark.sql.broadcastTimeout | 300 s | broadcast build timeout |
Rule of thumb for shuffle sizing: "each task should approximately be processing 128MB to 200MB of data" (discover guide, 2026-05-06). And keep AQE on: "It is recommended to keep AQE enabled" (best-practices, 2026-09-11).
6.2 Worked example E5: a 52-minute nightly join, diagnosed
Worked example E5 - join skew, four moves, from evidence to fix
Setup ........ orders 400 GB, customers 2 GB, join on cust_id
one key (cust_id = 'ANON') holds 38% of all rows
Evidence ..... Spark UI longest stage: 200 tasks, median 12 s, Max 41 min
"If the Max duration is 50% more than the 75th percentile duration,
you may be suffering from skew" -> 41 min >> 1.5 x p75 => SKEW
one task reads 180 GB of shuffle; median task reads 0.4 GB
plan: two Exchange nodes + SortMergeJoin (AQE did not broadcast:
filtered customers = 40 MB, above the 30 MB runtime threshold)-- Move 1 - REMOVE the shuffle (always the best outcome if it fits)
SELECT /*+ BROADCAST(customers) */ c.region, SUM(o.amount) AS revenue
FROM main.silver.orders o
JOIN main.gold.customers c ON o.cust_id = c.cust_id
GROUP BY c.region;
-- hint priority: BROADCAST > MERGE > SHUFFLE_HASH > SHUFFLE_REPLICATE_NL
-- aliases: MAPJOIN, BROADCASTJOIN for BROADCAST; result: job 52 min -> ~6 min
-- Move 2 - dimension too big to broadcast (4 GB): let AQE split the hot partition
-- plan marker: SortMergeJoin (isSkew=true) -> job 52 min -> ~18 min
SET spark.sql.adaptive.skewJoin.enabled = true; -- default true
SET spark.sql.adaptive.skewJoin.skewedPartitionFactor = 5; -- default 5
-- Databricks' own recommendation: "rely on AQE skew join handling rather than
-- use the skew join hint ... completely automatic and in general performs better"
-- Move 3 - the documented DataFrame escape hatch when AQE cannot see it
-- df_orders.hint("skew", "cust_id", "ANON")
-- df_orders.hint("skew", ["cust_id", "store_region_id"])
-- Move 4 - LAST RESORT: salting (hand-rolled, maintenance cost, query rewrite)
-- fact side : cust_id || '_' || CAST(FLOOR(RAND() * 16) AS INT) for the hot key
-- dim side : replicate the 'ANON' dimension row 16 times, one per salt value
-- join on the salted key, then GROUP BY partials and collapseThe wrong-fixes rail: all three of these appear in production code and none of them helps:
SET spark.sql.shuffle.partitions = 2000: splits nothing; the hot key still lands in exactly one partition..repartition("cust_id"): same failure, plus an extra shuffle.- A skew hint when AQE already fired: overhead for a fix that already happened; "Skew join optimization has some overhead so it is better to use it only when needed" (retired skew-join docs, page updated 2024-05-13).
- π Did you know? You can read AQE's decisions straight out of the physical plan:
CustomShuffleReaderwith propertyCoalescedmeans partitions were merged after shuffle,SortMergeJoinwithisSkew=truemeans AQE split a hot partition, and aLocalTableScanmeans AQE proved a side of the join empty and short-circuited it (AQE docs, 2023-10-12). Three strings worth grepping in any slow query's plan before you change a single config.
7. Photon: judge it by workload, not by slogan
Photon is the vectorized C++ engine that processes data in columnar batches, and "Photon is used by default in Databricks SQL warehouses" (best-practices, 2026-09-11). The published evidence, with its caveats attached:
Claim | Number | Source (date) |
|---|---|---|
Customer SQL speedups | average 3Γ, maximum over 10Γ | Photon SIGMOD 2022 paper |
TPC-DS / ETL vs non-Photon baseline | up to 5.3Γ analytical, 4.4Γ ETL | blog (2025-10-16): Azure instances, DBR 11.2-era benchmark: evidence of Photon's effect, not an AWS price claim |
Databricks SQL, 5 years of engine work | 5Γ since 2022, +25% in the 2025 release (Photon Vectorized Shuffle = 1.5Γ shuffle throughput) | blog (2025-06-17) |
2025 production workloads | up to 40% faster overall: exploratory ~40%, BI ~20%, ETL ~10%, spatial up to 17Γ | blog (2026-01-28) |
ML feature engineering (ML Runtime 15.2+) | ~2Γ; point-in-time joins 2.0 to 2.4Γ on AWS r6id.xlarge | blog (2026-02-13) |
The per-workload verdicts:
- SQL-heavy / BI / dashboards: biggest wins (vectorized scans, joins, aggregations). Already on by default in SQL warehouses.
- ETL / batch Spark: solid but smaller; MERGE-heavy paths are dominated instead by low shuffle merge (GA since DBR 10.4 LTS, "up to 5x β¦ average on the order of 2-3x").
- Python-heavy code with UDFs: Photon cannot accelerate Python UDF execution: "Serialization is required to transfer data between Python and Spark. This significantly slows down queries" (best-practices). The winning move is rewrite-to-native-SQL (or pandas UDFs if a UDF is unavoidable) first, Photon second. (This is reasoning from the documented UDF guidance, not a published Python benchmark.)
- ML training loops built on RDDs/graph/custom code: Photon is mostly irrelevant there.
8. Layer 4: warehouse sizing and the cache tiers
8.1 Resize vs scale out: two symptoms, two knobs
Databricks' sizing doctrine (warehouse-behavior docs, accessed 2026-10; high-concurrency blog, 2025-02-09): resize when queries need more memory per query (aggregations, multi-table joins: watch disk spill); scale out when queries wait (watch queuing). "If queries are spilling to disk, increase the cluster size. Check for spills in the query profile." Classic and pro warehouses have "a fixed limit of one cluster per 10 concurrent queries", the queue depth is hard-capped at 1,000 queries for every warehouse type, and a practical alert threshold is peak queued queries > 10 that does not resolve when autoscaling fires.
8.2 Worked example E6: sizing a 60-analyst warehouse
Worked example E6 - 60 concurrent analysts on a dashboard warehouse (as of Oct 2026)
Evidence ..... query profile: "Bytes spilled to disk" on the top aggregation
warehouse monitoring: Peak Queued Queries = 27
Step 1 SIZE ......... spill => increase cluster size: X-Large -> 2X-Large
(spill is a per-query memory/disk problem, not a count problem)
Step 2 COUNT ........ 60 concurrent / 10 per cluster = 6 clusters
27 queued >> 10 alert heuristic => max_num_clusters >= 6
hard cap reminder: queue holds at most 1,000 queries
Step 3 IDLE ......... auto-stop: serverless default 10 min (UI min 5, API min 1);
pro/classic default 45 min (min 10) - set it deliberately
Step 4 COLD START ... serverless restarts in 2-6 s; pro/classic ~4 min;
clusters are recycled after 24 h regardless
Step 5 RE-OPEN ...... profile again: spill gone? queue empty? If not, the
bottleneck was never serving - it is layers 1-3For most workloads "Databricks recommends using a serverless SQL warehouse", and its sizing advice is counter-intuitive but correct: "Start with a single larger warehouse and let serverless features manage concurrency and performance. It is usually more efficient to size down if necessary than to start small and scale up."
8.3 Four caches, four lifetimes
Cache | What it stores | Lifetime / invalidation |
|---|---|---|
Local result cache | query results, in memory | cluster lifetime or until full; cleared on stop/restart |
Remote result cache | query results, persisted as workspace system data (serverless-only) | survives warehouse stop/restart, shared across warehouses; invalidated when underlying tables update |
Disk cache (formerly "Delta cache") | copies of remote data files on local SSD | auto-managed, "at most half" of worker SSD; invalidated when files change; lost on worker decommission |
Spark cache ( df.cache()/persist()) | arbitrary DataFrames | manual lifecycle; goes stale across commits |
Rules that prevent the classic cache bugs: result caching needs deterministic queries, a NOW()-style predicate never hits it; SET use_cached_result = false bypasses it (testing/benchmarking only); disk caching "works by default with all instance types that have local SSDs", and on classic compute that means choosing SSD-bearing workers; "In SQL warehouses and Databricks Runtime 14.2 and above, the CACHE SELECT command is ignored", the enhanced disk cache is used instead.
β οΈ WARNING, Spark caching is the trap, not the default. "using incorrect locations in a query can consume all available memory and significantly slow down queries. As a rule of thumb, avoid Spark caching" (best-practices, 2026-09-11), and the Delta best-practices page says flatly "Do not use Spark caching with Delta Lake" (updated 2026-09-11). Cached DataFrames eat executor memory that shuffles and joins then spill to disk, the telltale signature is spill rising while the Storage tab shows large cached blocks. Legitimate uses are narrow: a truly immutable, repeatedly-scanned non-Delta dataset inside one session, always paired with
unpersist(); plus prewarming a cold cluster before the morning rush.
- π Did you know? The disk cache is the renamed Delta cache: "The disk cache (formerly known as 'Delta cache') stores copies of remote data on the local disks (for example, SSD) of the virtual machinesβ¦ The recommended (and easiest) way to use disk caching is to choose a worker type with SSD volumes" (best-practices, 2026-09-11). And because autoscaling decommissions workers, "if autoscaling is enabled, there is some instability with the cache": cache-sensitive job clusters want conservative scale-down or pools.
9. Profiling: the query profile and the Spark UI
Profiling is what makes the stack a loop instead of a checklist. Pick the tool by compute type: query profile for SQL warehouses and serverless, Spark UI for classic and job clusters.
What each signal tells you, and the single documented response:
Signal | Where | Layer | Response |
|---|---|---|---|
Low "% of data pruned" | query profile scan node | 1 to 2 | clustering keys / stats columns / sargable predicate |
"Bytes spilled to disk" | query profile shuffle/sort | 4 | increase warehouse cluster size |
Max task β« 1.5Γ p75 | Spark UI Summary Metrics | 3 | broadcast β AQE skew split β salting |
One task reading 10Γ median shuffle bytes | Spark UI per-task metrics | 3 | hot key: skew ladder |
Peak Queued Queries > 10 | warehouse monitoring | 4 | raise max_num_clusters |
"Query profile is not available" | query profile | : | result-cache hit, not a bug |
Two access details that save a support ticket: profile access requires ownership of the query "or at least CAN MONITOR permission on the SQL warehouse", and "A query profile is not available for queries that run from the query cache. To circumvent the query cache, make a trivial change to the query, such as changing or removing the LIMIT."
9.1 Worked example E7: sizing spark.sql.shuffle.partitions on paper
Worked example E7 - how many shuffle partitions for a 400 GB shuffle on 16 cores?
Rule ......... 128 - 200 MB per task (discover guide, 2026-05-06)
Volume ....... 400 GB = 409,600 MB ; cores = 16
Upper bound .. 409,600 / (128 MB x 16) = 200 partitions <- 128 MB per task
Lower bound .. 409,600 / (200 MB x 16) = 128 partitions <- 200 MB per task
Decision ..... keep 200 (the default), or SET spark.sql.shuffle.partitions = 'auto'
and let AQE coalesce toward the 64 MB advisory target
Bad move ..... 2000 partitions => ~2 MB per task => scheduling overhead wins
Also bad ..... coalesce/repartition right before a write while optimized writes
are on - two shuffles, one job (tune-file-size, 2026-09-11)
Read rate note: Spark reads ~128 MB per task per core from Parquet/Delta, which is
why 128 MB is the floor of the band and not an arbitrary numberOne streaming caveat: spark.sql.shuffle.partitions "cannot be changed between query restarts from the same checkpoint location" (AQE docs): pick it before the first start of a streaming query.
- π Did you know? A query served from the result cache has no query profile at all: "Query profile is not available" in the UI usually means your query was fast because it was cached, not that profiling is broken. During benchmarks,
SET use_cached_result = false(docs.../query-caching, accessed 2026-10), and keepNOW()-style non-determinism out of BI queries you want cached in production.
10. The automation layer: predictive optimization and materialized views
Two 2026 features remove work from your runbook, but only if you know what they replace.
Predictive optimization "automatically runs OPTIMIZE, VACUUM, and ANALYZE on Unity Catalog managed tables (Delta Lake and Iceberg)", is "enabled by default for accounts created on or after November 11, 2024" with the gradual rollout of existing accounts "expected to complete by August 2026", runs on serverless compute billed as a serverless jobs SKU, and, the gotcha, "OPTIMIZE does not run ZORDER when executed by predictive optimization. On tables that use Z-order, predictive optimization ignores Z-ordered files." It applies to managed tables only; external tables keep you on the manual ladder in sections 2 to 4. Observe it with DESCRIBE TABLE EXTENDED β¦ AS JSON β predictive_optimization_evaluations, the Catalog Explorer History "Automatic runs" filter, or system.storage.predictive_optimization_operations_history.
Materialized views are the serving-layer counterpart: "a view where precomputed results are available for query and can be updated to reflect changes in the input. All materialized views are backed by an ETL pipeline", created "using a Pro or Serverless SQL warehouse, or within a pipeline", refreshed by serverless Lakeflow pipelines, with the cached output materialized in cloud storage. Eligibility for incremental refresh is checkable before you commit: EXPLAIN CREATE MATERIALIZED VIEW β¦ (DBR 17.3+) reports "The Materialized View can be incrementally refreshed" or not: a filter/projection/aggregate over Delta qualifies, LIMIT and non-Delta sources (INPUT_NOT_IN_DELTA) do not, and an expectation inside the definition forces a full refresh. Both materialized views and streaming tables accept liquid clustering: "Liquid clustering applies to both streaming tables and materialized views."
11. The traps: sixteen confusions this lesson will not let you keep
- "Target file size is 1 GiB." Manual
OPTIMIZEcaps at 1 GiB; auto-optimize targets 128 MB; autotune picks 256 MB under 2.56 TB. Compacting a 300 GB table to 1 GiB files hurts pruned-scan parallelism. - Assuming managed tables need manual
OPTIMIZErituals. Autotune + predictive optimization own them; a nightly cron job double-pays. - Partitioning a 50 GB table "for performance." Under 1 TB you should not partition at all, and every partition needs β₯ 1 GB.
ZORDERon a column without statistics, or expecting ZORDERed tables to stay maintained under predictive optimization ("ignores Z-ordered files").- Changing clustering keys and expecting instant results. You need
OPTIMIZE FULL, and it "might take hours" on a large unclustered table. - Too many / too-high-cardinality clustering keys. Max 4; below 10 TB extra keys degrade single-column filters; hierarchical keys should be low-cardinality.
VACUUMβ clustering interplay. Every recluster deletes-and-adds; setdelta.deletedFileRetentionDurationbefore enabling predictive optimization (7-day floor), and never vacuum alongside long-running readers.coalesce(n)/repartition(n)right before a write while optimized writes are on: two shuffles, one job.- A session-level
autoOptimize.autoCompact = falseyou forgot about: mandatory forMERGE/UPDATE/DELETE, silently off for your appends. spark.sql.shuffle.partitions = 2000"because the default is too low." The rule is 128 to 200 MB per task; with AQE preferautoor start high and let coalescing merge.- Reaching for a skew hint when AQE already handles it, or salting before trying broadcast. If one side fits in 10/30 MB, a
BROADCASThint deletes the shuffle: strictly better than splitting it. - Spark cache abuse. "As a rule of thumb, avoid Spark caching"; "Do not use Spark caching with Delta Lake".
- Result-cache confusion during debugging. A cached query has no profile; dashboard latency may be cache latency.
- Reading skew from wall-clock alone. The test is Max > 1.5Γ p75 in Summary Metrics, not "the job felt slow".
- Assuming Photon fixes Python UDF pipelines. Serialization dominates; rewrite to native SQL first.
- Running
OPTIMIZE β¦ ZORDER BYon twelve columns, mid-day, on the interactive cluster. Legacy technique, wrong time, wrong cluster: use clustering, a separate spot job cluster, and a schedule.
Comparative Verdict: traditional partitioning + ZORDER vs liquid clustering (AWS, as of Oct 2026)
- Traditional partitioning + ZORDER: the 2019 to 2023 playbook, now legacy. You declare a Hive-style directory hierarchy (
PARTITIONED BY (event_date)) for pruning, then periodicallyOPTIMIZE β¦ ZORDER BY (hot_col)to co-locate a non-partitioned column across files. It works, and at β₯ 100 TB with a genuinely low-cardinality date key, partitioning can still earn its keep for partition-levelDELETE/DROPand Delta Sharing partition filtering. The costs: the hierarchy is "static β¦ no easy way to change partitions"; ZORDER is a rewrite that "is not idempotent but is incremental" and is broken up byMERGE(low shuffle merge blunts but does not remove this); ZORDER needs statistics or it is "ineffective and a waste of resources"; high-cardinality columns cannot be partition keys at all; and: the operational nail: predictive optimization never runsZORDERand "ignores Z-ordered files", so legacy ZORDERed tables silently stop being maintained.- Liquid clustering: the documented default for all new tables.
CLUSTER BY(up to 4 keys, statistics required, protocol 3/7) is metadata-only to declare or change; clustering-on-write lands CTAS and streaming traffic pre-clustered at 64 MB/256 MB/512 MB/1 GB thresholds (managed);OPTIMIZEthen does clustering and bin packing together, incrementally, idempotently, on a 1 to 2 hour cadence for hot tables;CLUSTER BY AUTOlets the platform pick keys cost-aware; and predictive optimization maintains it for you. Evidence: 2.5Γ faster clustering than Z-order on a 1 TB warehouse workload with Hive partitioning "an order of magnitude slower" (blog, 2023-06-29), point queries up to 12Γ and reads 2 to 12Γ at GA (blog, 2024-05-22). The costs: a one-offOPTIMIZE FULLwhen keys are first set or changed (hours on a large table), incompatibility with existing partitioning/ZORDER, and a 4-key ceiling.- Verdict in one line: new table β
CLUSTER BY(orCLUSTER BY AUTOon a managed table) with predictive optimization on; existing partitioned/ZORDERed table β migrate withALTER TABLE β¦ REPLACE PARTITIONED BY WITH CLUSTER BY(18.1+) plus oneOPTIMIZE FULL, keeping partitioning only where a β₯ 100 TB table genuinely needs partition-level lifecycle moves or partition-filtered sharing. Every version number in that sentence is dated in section 4.1; carry the dates, not just the numbers.
2026 Updates (as of October 2026)
What changed in the performance stack this lesson depends on: each line checked against a primary source:
- 2026-09-25: the clustering page is the current rulebook. "Liquid clustering is a data layout optimization technique that replaces table partitioning and
ZORDER"; GA "for Delta Lake tables with Databricks Runtime 15.4 LTS and above"; up to four keys;CLUSTER BY AUTO(15.4 LTS+); hierarchical clustering (17.1+);OPTIMIZE FULL(16.4 LTS+) andOPTIMIZE FULL WHERE(18.1+);REPLACE PARTITIONED BY WITH CLUSTER BY(18.1+); clustering-on-write thresholds 64 MB/256 MB/512 MB/1 GB for managed tables (docs.../aws/en/tables/clustering, updated 2026-09-25). - 2026-09-11: file-size autotune and predictive optimization pages refreshed together. Autotune bands < 2.56 TB β 256 MB, 2.56 to 10 TB β linear 256 MB β 1 GB, > 10 TB β 1 GB; auto compaction and optimized writes "always enabled for
MERGE,UPDATE, andDELETE"; predictive optimization default-on for accounts created on/after 2024-11-11 with rollout "expected to complete by August 2026", serverless-billed, and explicitly not runningZORDER(docs.../tables/tune-file-size and.../optimizations/predictive-optimization, updated 2026-09-11). - 2026-07-01: the partitioning ladder became explicit. "Most tables on Databricks with less than 100 TB β¦ don't need partitioning"; < 1 TB don't partition, 1 to 100 TB use liquid clustering, β₯ 100 TB "partitioning might improve performance, but Databricks recommends using liquid clustering first"; β₯ 1 GB per partition (docs.../tables/partitions, dated 2026-07-01).
- 2026-06-18: data skipping and hints pages carry the current wording. Per-file stats are "minimum and maximum values, null counts, and total records"; UC managed tables have no 32-column stats cap;
delta.dataSkippingStatsColumnssupersedesdataSkippingNumIndexedCols; hint grammar coversBROADCAST/MERGE/SHUFFLE_HASH/SHUFFLE_REPLICATE_NLplusREBALANCE(ignored if AQE is off) (docs.../tables/data-skipping and.../sql-ref-syntax-qry-select-hints, dated 2026-06-18). - 2026-05-06: the tuning guide was re-dated with the per-task rule intact. Healthy file size "between 16MB and 1GB"; shuffle tuning "each task should approximately be processing 128MB to 200MB"; "Always run OPTIMIZE command on a separate job cluster"; compute-optimized instances recommended for
OPTIMIZE(databricks.com/discover/pages/optimize-data-workloads-guide, dated 2026-05-06). - 2026-01-28, engine-side gains landed without tuning. "In 2025, Databricks SQL delivered up to 40% faster performance across production workloads, with improvements applied automatically", exploratory ~40%, BI ~20%, ETL ~10%, spatial up to 17Γ, from Predictive Query Execution and Photon Vectorized Shuffle (blog, 2026-01-28; companion 5Γ/25% blog, 2025-06-17).
Real-World Case Studies
π Case A: Comcast (communications & media, Cloud: AWS) Challenge. Petabytes of telemetry and billions of voice interactions from 20M+ voice remotes, with fragile pipelines, small-file ingestion pain, and hundreds of models to deploy across cloud, on-prem and edge. Stack on AWS. Databricks, Delta Lake for ingest/enrich/optimize small files, managed MLflow, autoscaling + spot instances, notebooks, Tableau downstream:
Cloud: AWSon the story page. Reported outcome. "10x Reduction in overall compute costs to process data Β· 90% Reduction in required DevOps resources"; "Delta Lake has enabled Comcast to optimize data ingestion, replacing 640 machines with 64 while improving performance"; model deployment "weeks to minutes" (https://www.databricks.com/customers/comcast, undated page, accessed 2026-10). Comcast is also named among customers "already leveraging Databricks on AWS" in Databricks' AWS Marketplace press release (2022-05-10), and its RDK telemetry is described as "petabytes of data, collected every 15 minutes from 70 million devices" (databricks.com/blog, 2021-05-10). Maps to this lesson. Sections 2 and 8: the headline is a structural win: Delta's file handling collapsed 640 machines to 64 before any query tuning, which is exactly the layer-1-first ordering this lesson teaches, plus spot instances for the compaction window. β³ Temporal note: story origin 2019, undated page accessed Oct 2026: a reported customer outcome, not a 2026 guarantee.
π Case B: Rivian (electric vehicles, Cloud: AWS) Challenge. More than 70,000 vehicles on the road "generating terabytes of data every day", with legacy tooling that "struggled to scale pipelines cost-effectively", feeding predictive-maintenance and charge-prediction ML. Stack on AWS. Databricks SQL, Delta Lake, Unity Catalog, and, explicitly, "Liquid Clustering for data layout" alongside "Unity Catalog for data governance, Delta Tables for storage" for real-time charge-prediction ML (databricks.com/blog, 2025-07-09);
Cloud: AWSon the story page. Reported outcome. "50% better runtime performance" (https://www.databricks.com/your-ai/customers/rivian, 2024-05-30); "1,000+ platform users, a 75% increase from a year ago" (https://www.databricks.com/customers/rivian, undated, accessed 2026-10). Maps to this lesson. Sections 4 and 5: Rivian is the named AWS example of the exact lever this lesson's worked example E4 exercises: vehicle telemetry is high-cardinality, fast-growing and queried by filter columns, which is the documented best-fit list for clustering ("Queries that filter on high cardinality columns", "Fast growing tables", "Tables with varied or changing access patterns"). β³ Temporal note: the 50% figure is from 2024-05-30; the liquid-clustering stack mention is from 2025-07-09.
π Case C: Punchh (retail loyalty / restaurant tech, Cloud: AWS) Challenge. Outgrown AWS MySQL, a warehouse too costly to keep, and Tableau reporting slowed by joins executed fresh for every report: "When you're joining 12 tables on the fly every time a report is run by a customer, the cost to generate the report can be high." Stack on AWS. Databricks SQL, Delta Lake bronze/silver/gold, notebooks, Tableau on top:
Cloud: AWSon the story page. Reported outcome. "10x Faster time-to-insight Β· 30% Lower operational costs Β· 12% Increase in customer satisfaction" (https://www.databricks.com/customers/punchh, undated page, accessed 2026-10; story origin 2021). Maps to this lesson. Sections 8 and 10: the cheapest join is the one you do not run: a pre-joined gold table (or a materialized view with incremental refresh) beats both a 12-way join at report time and any warehouse resize that would have brute-forced it. β³ Temporal note: 2021-origin story still displayed in 2026: teach the mechanism, date the metrics.
Key Takeaways:
- Fix top-down, measure after every change: layout β statistics β planning/runtime β serving. The query profile tells you which layer: low "% data pruned" = layers 1 to 2, skew/spill in the plan = layer 3, "Bytes spilled to disk" or queueing = layer 4.
- Three file-size numbers, three jobs: auto-optimize
true= 128 MB; autotune = 256 MB below 2.56 TB β 1 GB above 10 TB; manualOPTIMIZEceiling = 1 GiB (1,073,741,824 B). Healthy band 16 MB, 1 GB; 1 GiB is a ceiling, not a small-table goal; growing a target never merges existing files, runOPTIMIZEafter. - Partitioning is now the exception: < 1 TB β never; 1 to 100 TB β liquid clustering ("partitioning likely negatively affects performance more often than it helps"); β₯ 100 TB β maybe, verify; and never below 1 GB per partition.
- Liquid clustering replaces both partitioning and
ZORDER:CLUSTER BYat create orALTER TABLE, β€ 4 keys, statistics required, protocol 3/7, incompatible with partitions/ZORDER, GA on DBR 15.4 LTS+ (trigger 13.3 LTS+,FULL16.4 LTS+,AUTO15.4 LTS+). Declaring keys is metadata;OPTIMIZE FULLis the one-off rewrite: budget hours. OPTIMIZEis incremental and idempotent: bin packing has no second-run effect, reads before/after are identical, streaming sources are unaffected; hot clustered tables want a run every 1 to 2 hours, tables > 1 TB a schedule, and predictive optimization should replace your cron (disable the duplicate).- Skew ladder: detect with Max > 1.5Γ p75, then
BROADCAST(10 MB static / 30 MB runtime), then AQE's automatic split (5Γ median and over the byte threshold, Spark default 256 MB), then, only then, DataFrame.hint("skew")or salting. Never fix skew withshuffle.partitions. - Photon wins by workload: SQL/BI/ETL yes (5Γ since 2022, +25% in 2025, up to 40% faster across 2025 production workloads); Python UDF-heavy code no: rewrite to native SQL first. AQE stays on; shuffle partitions target 128 to 200 MB per task.
- Warehouse sizing is two knobs: spill β grow the size; queuing (peak queued > 10) β grow
max_num_clustersat 1 cluster per 10 concurrent queries (queue cap 1,000). Cache deterministically: a cached query has no profile, disk cache needs local SSDs, and Spark caching on Delta is explicitly discouraged.
Practice Questions
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
An 8 TB table serves a dashboard whose query profile shows 94% of data pruned but 'Bytes spilled to disk' on the top aggregation, and warehouse monitoring shows no queuing. Which layer is the bottleneck and what is the documented first move?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Which set of file-size values is correct as of the October 2026 documentation?
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
A table may declare clustering keys with CLUSTER BY while also keeping its PARTITIONED BY clause and an existing ZORDER: the three techniques stack.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A liquid-clustered table receives heavy concurrent writes. What maintenance cadence do the Databricks docs recommend, and why is it cheap?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
In the Spark UI Summary Metrics for a long stage, the 75th-percentile task duration is 12 s. Which reading triggers the documented skew suspicion?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
AQE converts a sort-merge join to a broadcast join at runtime when a side falls below which documented threshold?
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
Running OPTIMIZE twice in a row on the same dataset doubles the compaction benefit, and OPTIMIZE changes query results because it rewrites files.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A Python-heavy pipeline with several Python UDFs runs on a Photon-enabled cluster and is still slow. What does the October 2026 guidance say to do first?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A dashboard warehouse shows Peak Queued Queries = 27 that does not clear when autoscaling fires, and the query profiles show no disk spill. What is the documented response?
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
A team proposes PARTITIONED BY (user_id) on a 300 GB Delta table in S3 to speed up per-user dashboards. What does the current guidance say?
How to answer
Multiple-choice question with 2 options. Select one option, then choose Check Answer.
On a Unity Catalog managed table, predictive optimization will keep a legacy ZORDERed table maintained by running ZORDER for you.
How to answer
Multiple-choice question with 4 options. Select one option, then choose Check Answer.
Which predicate most reliably defeats Delta data skipping, even though the column has per-file statistics?
How to answer
Matching exercise with 5 pairs. Match each item on the left with its answer on the right.
Match each symptom to the layer it belongs to and the documented lever:
Drag answers from here:
How to answer
Ordering exercise with 7 items. Move them into the correct order, then check your answer.
Order the join-skew debugging sequence exactly as this lesson builds it (evidence first, cheapest fix first, rewrite last):