DEV Community

Cover image for Hands-On Performance: Diagnosing and Fixing Databricks SQL Bottlenecks
Mayank Gupta
Mayank Gupta

Posted on

Hands-On Performance: Diagnosing and Fixing Databricks SQL Bottlenecks

Once you know how to monitor your queries, the next step is taking action. In Databricks SQL, performance tuning isn't a "set it and forget it" task—it’s a hands-on process of reducing data scans, optimizing joins, and leveraging intelligent caching.

This guide moves from theory to execution, showing you exactly how to identify bottlenecks and apply the right fixes to make your queries run faster and cheaper.


The Problem: Inefficient "Brute Force" Queries

A common mistake for SQL developers is writing "brute force" queries that scan entire tables to find a single row. While modern engines are fast, this approach is unsustainable at the petabyte scale.

Inefficient queries lead to:

  • High Latency: Users waiting minutes for simple dashboard refreshes.
  • Wasted Spend: Paying for compute resources to read data that is immediately discarded.
  • Resource Contention: One "heavy" query slowing down the entire warehouse for everyone else.

Core Concept: The Golden Rule of Big Data

The fastest way to speed up a query is to read less data.

To achieve this, Databricks uses three primary scan-reduction techniques:

  1. Partition Pruning: Skipping entire directories of files based on a filter (e.g., WHERE region = 'North').
  2. Predicate Pushdown: Using metadata (Min/Max statistics) within files to skip specific blocks of data before they are even read into memory.
  3. Dynamic File Pruning: Eliminating files at runtime based on values discovered from the other side of a join.

Deep Dive: Join Strategies & Optimization

Joins are often the most expensive part of a query execution. Understanding how Databricks handles them is key to fixing a slow DAG.

1. BroadcastHashJoin (The Winner)

The engine takes the smaller table and sends a full copy to every worker node.

  • Why it's fast: No data shuffle is required.
  • Best for: Joining a massive fact table with a smaller dimension table.

2. SortMergeJoin (The Workhorse)

Both tables are shuffled across the network, sorted by the join key, and then merged.

  • Why it's used: It is the standard for joining two very large tables that don't fit in memory.
  • The downside: Heavy network and I/O overhead.

3. Adaptive Query Execution (AQE)

Databricks isn't static. AQE can look at a query during execution and say, "Wait, this table I thought was big is actually small. Let's switch from a SortMergeJoin to a BroadcastJoin on the fly."


Technical Implementation: Writing Cache-Friendly SQL

Caching can make a query go from 30 seconds to 0.5 seconds, but only if you write your SQL correctly.

The "Cache-Busters" to Avoid

Certain functions make your query "non-deterministic," meaning the engine can't be sure the result will be the same next time, so it refuses to cache it.

❌ Always Misses Cache ✅ Always Hits Cache (Recommended)
WHERE date = CURRENT_DATE() WHERE date = '2024-05-20'
WHERE ts > NOW() - INTERVAL 1 DAY WHERE ts > '2024-05-19 08:00:00'
Adding/Removing random whitespace Consistent, formatted SQL blocks

Pre-Warming the Cache

If you have a high-priority dashboard, you can "pre-warm" the SSDs of your warehouse using the CACHE SELECT command. This ensures the data is sitting on local fast storage before the first user even logs in.

-- Pre-warm the local Delta cache for high-priority tables
CACHE SELECT * FROM gold.sales_summary;
Enter fullscreen mode Exit fullscreen mode

System Design: The Performance Playbook

To build a high-performance environment, follow this four-step cycle:

Step 1: Identify the "Heavy Hitters"

Query the system tables to find the top 10 most expensive queries by total_task_duration_ms.

SELECT 
    statement_text, 
    total_task_duration_ms, 
    read_bytes / 1024 / 1024 AS mb_read
FROM system.query.history
WHERE start_time > date_add(now(), -1)
ORDER BY total_task_duration_ms DESC
LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

Step 2: Analyze the DAG

Open the Query Profile. Look for the "Scan Table" node. If the Pruning Percentage is low, you are reading too much data.

Step 3: Apply the Fix

  • Missing Pruning? Add a filter on a partitioned column.
  • Massive Shuffles? Use a /*+ BROADCAST(small_table) */ hint.
  • Slow Scans? Run OPTIMIZE table_name ZORDER BY (frequent_filter_column).

Step 4: Verify & Alert

Compare the "Before" and "After" metrics in system.query.history. If the read_bytes dropped significantly, your fix worked.


Best Practices & Pitfalls

  • Avoid Functions on Filter Columns: Writing WHERE YEAR(my_date) = 2023 prevents the engine from using partition pruning. Use WHERE my_date BETWEEN '2023-01-01' AND '2023-12-31' instead.
  • Right-Size the Warehouse: Don't use a Large warehouse for 2X-Small tasks. Use the smallest tier that meets your SLA to save money.
  • Monitor Parallelism: A low Parallelism Ratio in your history logs means your query is running sequentially and not taking advantage of your cluster's power.

Summary / Key Takeaways

  • Read Less: Partition pruning and predicate pushdown are your best friends.
  • Filter Early: The closer a filter is to the source scan, the less work every downstream join and aggregate has to do.
  • Be Deterministic: Replace NOW() and CURRENT_DATE() with static parameters to unlock the Result Cache.
  • Use Serverless: For bursty workloads, serverless warehouses prevent you from paying for idle compute time.

Interview Questions

  1. What is the difference between Partition Pruning and Predicate Pushdown?
  2. How does wrapping a column in a function (like TO_DATE()) affect query performance?
  3. When would you choose a ShuffleHashJoin over a BroadcastHashJoin?
  4. What metrics in the Query Profile indicate that a table needs Z-Ordering or better partitioning?

Top comments (0)