Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 11

Subqueries and CTEs for Complex Queries

Recommended resources

Hello! Welcome to the fifth lesson in our module on Querying Data with Databricks SQL.

In our last lesson, you learned how to use CASE expressions to embed conditional logic directly into your queries, allowing you to create custom categories and perform dynamic calculations. This is a key building block for data transformation.

Today, we'll take a big step forward in organizing complex data logic. As queries grow, they can become difficult to read, debug, and maintain. This lesson tackles that challenge head-on by teaching you how to use subqueries and Common Table Expressions (CTEs). These are two of the most important tools in a data engineer's SQL toolkit for breaking down complex problems into manageable, logical pieces.

This lesson is designed to help you achieve the learning outcome: Write subqueries and common table expressions (CTEs) to structure complex queries. By the end, you'll be able to decide when to use a subquery versus a CTE and write clean, structured queries for multi-step data transformations.

1. Understanding Subqueries

A subquery, also known as an inner query or nested query, is a SELECT statement that is nested inside another SQL statement. The result of the subquery is then used by the outer query. Let's break down the different types.

Non-Correlated Subqueries

A non-correlated subquery is an independent query whose result is calculated once and then passed to the outer query. It can be run on its own without any dependency on the outer query.

Let's watch a video that explains the two main types of non-correlated subqueries: scalar and multi-row.

Subquery in SQL | Correlated Subquery + Complete SQL Subqueries Tutorial

This video from techTFQ provides an excellent, detailed explanation of subqueries. We'll start with the sections on scalar and multi-row subqueries.

Watch from 01:32 to 22:14. As you watch, focus on: Scalar Subqueries (01:32 - 11:32): These return a single value (one row, one column). Pay attention to the example of finding employees with a salary greater than the overall average. Notice how the subquery (SELECT AVG(salary) FROM ...) is executed first. Multi-Row Subqueries (11:32 - 22:14): These can return multiple rows. Understand how operators like IN are used to check if a value from the outer query exists in the list of results returned by the subquery.

To summarize the video's key points:

  • Scalar Subquery: Returns a single value. Often used with comparison operators (=, >, <) in a WHERE clause.
    -- Find products with a price higher than the average price
    SELECT product_name, price
    FROM products
    WHERE price > (SELECT AVG(price) FROM products);
    
  • Multi-Row Subquery: Returns a list of values in a single column. Often used with the IN or NOT IN operators.
    -- Find all customers who live in a country that has a 'Z' in its name
    SELECT first_name, last_name
    FROM customers
    WHERE country IN (SELECT country FROM countries WHERE country LIKE '%Z%');
    

Another very common pattern is using a subquery in the FROM clause. This allows you to treat the result of the subquery as a temporary table, often called a derived table. You must give this derived table an alias.

SQL Subquery Example in Databricks
This Databricks notebook shows a query where a subquery is used in a `LEFT OUTER JOIN`. The subquery calculates the `max_age` per department and is aliased as `max_age_employees`. The outer query then joins the `employee_a` table to this derived table.

Correlated Subqueries

A correlated subquery is different: it depends on the outer query for its values. It cannot be run independently. The subquery is executed once for each row processed by the outer query.

This row-by-row execution makes them powerful but potentially slow on large datasets.

Correlated vs. Non-Correlated Subquery Flow
This diagram visually contrasts the execution flow. A non-correlated subquery runs once, passing its result to the main query. A correlated subquery has a back-and-forth relationship, where the main query passes a value to the subquery for each row it processes.

Let's watch the next segment of the same video to understand this crucial concept.

Subquery in SQL | Correlated Subquery + Complete SQL Subqueries Tutorial

This section of the video explains correlated subqueries and their execution flow. It's a critical concept for advanced SQL.

Watch from 22:14 to 32:00 (stop before the NOT EXISTS example). Focus on the example of finding employees who earn more than the average salary in their specific department. Notice how the inner query references a column from the outer query (e1.department_name). This is the 'correlation' that makes it a correlated subquery.

Here is the classic example of a correlated subquery shown in the video:

-- Find employees who earn more than the average salary in their own department
SELECT employee_name, salary, department_id
FROM employees e1
WHERE salary > (
  SELECT AVG(salary)
  FROM employees e2
  WHERE e2.department_id = e1.department_id -- Correlation: inner query depends on the outer query's current row
);

For each employee (e1) in the outer query, the inner query recalculates the average salary for that specific employee's department.


2. Common Table Expressions (CTEs)

While subqueries are powerful, they can make your code look nested and complex, especially when you have multiple levels. Common Table Expressions (CTEs) solve this by letting you define temporary, named result sets that you can reference within your main query. They drastically improve the readability and structure of complex SQL.

A CTE is defined using the WITH keyword.

Common table expression (CTE) | Databricks on AWS

Before we watch a video, let's quickly review the official Databricks syntax for CTEs. This will provide a solid technical foundation.

Briefly scan this documentation page. Pay attention to the Syntax section to see the WITH ... AS (...) structure. Then look at the 'Basic CTE examples' to see how they are used.

Now, let's see a practical comparison of subqueries and CTEs and explore the advantages of using CTEs.

SQL WITH Clause | Clearly Explained | CTEs vs Subqueries vs Temp Tables | Recursive CTEs

This video from Maven Analytics does an excellent job of comparing subqueries to CTEs and highlighting the key benefits of using CTEs.

Watch the following segments: Rewriting a Subquery as a CTE (01:06 - 03:26): See how a query with a subquery is transformed into a more readable CTE. Multiple References (04:04 - 05:45): This highlights a major advantage: a CTE can be defined once and referenced multiple times in the same query. Multiple CTEs (05:45 - 08:17): Learn how to chain CTEs together to perform a sequence of transformations, where each step is a logical, named block.

The key advantages of CTEs are:

  1. Readability: Complex logic is broken down into sequential, named steps, making the query flow like a story.
  2. Reusability: You can reference the same CTE multiple times within a single SELECT, INSERT, UPDATE, or DELETE statement, avoiding code duplication.
  3. Recursion: CTEs can reference themselves, allowing you to query hierarchical data (like organizational charts). This is an advanced topic but a unique capability of CTEs.

3. Hands-On Practice

Time to put this into practice. Let's write some queries in your Databricks workspace.

Instructions:

  1. Open the SQL Editor in your Databricks workspace.
  2. Ensure you have a SQL Warehouse running and selected.
  3. Execute the following code to create a sales_data temporary view.
CREATE OR REPLACE TEMP VIEW sales_data(product_id, category, region, units_sold, unit_price) AS
VALUES
  (1, 'Electronics', 'North America', 120, 900.00),
  (2, 'Electronics', 'Europe', 80, 950.00),
  (3, 'Apparel', 'North America', 300, 45.00),
  (4, 'Apparel', 'Asia', 450, 40.00),
  (5, 'Home Goods', 'Europe', 150, 120.00),
  (6, 'Home Goods', 'North America', 200, 110.00),
  (7, 'Electronics', 'Asia', 100, 850.00);

-- Query the view to see its contents
-- SELECT * FROM sales_data;

Now, write SQL queries to answer the following questions.

Practice Questions:

  1. Subquery in WHERE: Find all sales records for products in the 'Electronics' category that sold more units than the overall average number of units sold across all categories. (Hint: Use a scalar subquery).

  2. Subquery in FROM (Derived Table): First, calculate the total revenue (units_sold * unit_price) for each sale and the average revenue for its region. Then, join this result back to the original table to show the product_id, category, region, and the calculated total and average revenues for each product.

    • Hint: Your subquery in the FROM clause should calculate AVG(units_sold * unit_price) and group by region.
  3. CTE Transformation: Solve the same problem as in question #2, but this time use two CTEs.

    • The first CTE, sales_revenue, should calculate the total revenue for each sale.
    • The second CTE, region_avg_revenue, should use the sales_revenue CTE to calculate the average revenue per region.
    • Finally, join these CTEs in your main query to get the desired result. Notice how much clearer the logic is.

Conclusion

Excellent work! You have now mastered two essential techniques for structuring complex SQL queries. Understanding when and how to use subqueries and CTEs is a hallmark of a proficient data engineer.

Key Takeaways

  • Subqueries are queries nested inside another query. They are useful for simple, one-off lookups.
    • Non-correlated subqueries run once and are independent of the outer query.
    • Correlated subqueries run once for each row of the outer query and are dependent on it.
  • Common Table Expressions (CTEs) use the WITH clause to create named, temporary result sets. They are the preferred method for complex, multi-step queries because they improve readability, modularity, and allow for reusability within a single statement.
  • For most complex transformations, CTEs provide a cleaner and more maintainable structure than deeply nested subqueries.

Next Steps

In our next lesson, we will explore window functions. These are powerful analytical functions that perform calculations across a set of table rows. You will see that CTEs are the perfect tool for preparing or "staging" your data before applying a window function, allowing you to combine these advanced techniques to solve very complex problems.