Advanced Queries
Overview
In the previous lesson, you learned how to retrieve and filter data using SELECT and WHERE. Those queries answer straightforward questions: "Show me all books under $20." But real data questions are rarely that simple. In this lesson, you'll learn how to write more expressive queries using subqueries, column aliases, CASE expressions, COALESCE, and SQL's built-in functions to transform and classify data directly in the database.
Learning Objective
By the end of this lesson, you will have learned how to:
- Use subqueries and aliases to structure complex queries.
- Perform transformations and conditional logic using
CASEandCOALESCE.
Key terms
Subquery: A SELECT statement nested inside another SQL statement. The inner query runs first and its result is used by the outer query.
Alias (AS): A temporary name given to a table or column in a query to make results more readable or to reference a subquery.
CASE WHEN: SQL's conditional expression — equivalent to an if-elif-else block. Evaluates conditions in order and returns the first matching result.
COALESCE(): A function that returns the first non-NULL value from a list of arguments. Used to replace NULL with a default value.
ROUND(): A function that rounds a numeric value to a specified number of decimal places.
LENGTH(): A function that returns the number of characters in a text string.
UPPER() / LOWER(): Functions that convert text to uppercase or lowercase.
strftime(): SQLite's function for formatting and extracting parts of dates stored as text.
Starter Code
This lesson encourages experimentation. Use the included Colaboratory notebook to run the code as you learn, or read the lesson and then freely experiment with the notebook's code.
Introduction
The SELECT and WHERE clauses you learned in the previous lesson are powerful, but they only retrieve data as-is. Advanced queries let you transform data while querying it — classifying values into categories, replacing missing data with defaults, computing derived values, and filtering based on aggregated results. These techniques reduce the amount of post-processing you need to do in Python, keeping your data pipelines clean and efficient.
Column Aliases
You've already seen AS used to rename a column in results. Aliases also make complex expressions readable:
SELECT
title,
price,
price * 0.9 AS discounted_price,
stock * price AS inventory_value
FROM books;
Output:
title | price | discounted_price | inventory_value
---------------------------+-------+------------------+----------------
Python Crash Course | 35.99 | 32.391 | 431.88
The Data Detective | 18.50 | 16.650 | 462.50
...
Aliases only exist within the query — they are not stored in the table.
CASE WHEN — Conditional Logic
CASE WHEN is SQL's if-elif-else. It evaluates conditions in order and returns the value of the first matching branch:
SELECT
title,
price,
CASE
WHEN price < 15 THEN 'Budget'
WHEN price BETWEEN 15 AND 30 THEN 'Mid-range'
ELSE 'Premium'
END AS price_tier
FROM books;
Output:
title | price | price_tier
----------------------------+-------+-----------
Python Crash Course | 35.99 | Premium
The Data Detective | 18.50 | Mid-range
Naked Statistics | 16.99 | Mid-range
Storytelling with Data | 29.99 | Mid-range
How to Lie with Statistics | 12.99 | Budget
Deep Learning | 74.99 | Premium
Simple CASE (Equality Check)
When all branches check equality against one column, a simpler syntax is available:
SELECT
name,
city,
CASE city
WHEN 'Chicago' THEN 'Midwest'
WHEN 'New York' THEN 'Northeast'
WHEN 'Austin' THEN 'South'
WHEN 'Denver' THEN 'West'
ELSE 'Other'
END AS region
FROM customers;
COALESCE — Handling NULL Values
COALESCE(val1, val2, ...) returns the first non-NULL value from its arguments. It is the clean way to replace NULL with a default:
-- Replace NULL city with 'Unknown'
SELECT name, COALESCE(city, 'Unknown') AS city
FROM customers;
-- Use discount if available, otherwise use standard price
SELECT
title,
price,
COALESCE(discount_price, price) AS effective_price
FROM books;
COALESCE is especially useful when joining tables where some rows have no match — the joined columns will be NULL and you may want a sensible default.
Built-In Functions
Numeric Functions
SELECT
title,
price,
ROUND(price * 1.08, 2) AS price_with_tax, -- Round to 2 decimal places
ABS(stock - 10) AS distance_from_10
FROM books;
Text Functions
SELECT
UPPER(name) AS name_upper,
LOWER(email) AS email_lower,
LENGTH(title) AS title_length,
SUBSTR(title, 1, 10) AS title_preview -- First 10 characters
FROM customers
JOIN books ON 1=1
LIMIT 5;
-- Find books with long titles
SELECT title, LENGTH(title) AS char_count
FROM books
WHERE LENGTH(title) > 20
ORDER BY char_count DESC;
Date Functions
SQLite stores dates as text. Use strftime() to extract parts or format them:
-- Extract the year and month from order_date
SELECT
order_id,
order_date,
strftime('%Y', order_date) AS order_year,
strftime('%m', order_date) AS order_month,
strftime('%Y-%m', order_date) AS year_month
FROM orders;
-- Orders placed in February 2024
SELECT * FROM orders
WHERE strftime('%Y-%m', order_date) = '2024-02';
Subqueries
A subquery is a SELECT embedded inside another statement. The inner query executes first and its result is passed to the outer query.
Subquery in WHERE
-- Find customers who have placed at least one order
SELECT name, city
FROM customers
WHERE customer_id IN (
SELECT DISTINCT customer_id FROM orders
);
The inner query returns the list of customer IDs that appear in orders. The outer query then returns customers whose ID is in that list.
-- Find books that cost more than the average price
SELECT title, price
FROM books
WHERE price > (SELECT AVG(price) FROM books);
Output:
title | price
--------------------+-------
Python Crash Course | 35.99
Deep Learning | 74.99
Subquery in FROM (Derived Table)
A subquery in the FROM clause acts as a temporary table. It must be given an alias:
-- Average order value per customer, then filter for high-value customers
SELECT customer_id, avg_spent
FROM (
SELECT customer_id, AVG(quantity * price) AS avg_spent
FROM orders
JOIN books USING (book_id)
GROUP BY customer_id
) AS customer_summary
WHERE avg_spent > 30;
Subquery in SELECT (Scalar Subquery)
A subquery that returns a single value can appear in the SELECT list:
-- Show each book's price alongside the overall average
SELECT
title,
price,
ROUND((SELECT AVG(price) FROM books), 2) AS avg_price,
ROUND(price - (SELECT AVG(price) FROM books), 2) AS diff_from_avg
FROM books
ORDER BY diff_from_avg DESC;
Combining Techniques
Here's a realistic query that uses aliases, CASE, COALESCE, and a subquery together:
import sqlite3
import pandas as pd
conn = sqlite3.connect("bookstore.db")
query = """
SELECT
b.title,
b.genre,
b.price,
CASE
WHEN b.price < 15 THEN 'Budget'
WHEN b.price BETWEEN 15 AND 30 THEN 'Mid-range'
ELSE 'Premium'
END AS price_tier,
COALESCE(b.genre, 'Uncategorized') AS genre_clean,
ROUND(b.price - avg.mean_price, 2) AS vs_average
FROM books AS b,
(SELECT ROUND(AVG(price), 2) AS mean_price FROM books) AS avg
ORDER BY b.price DESC;
"""
df = pd.read_sql_query(query, conn)
print(df)
conn.close()
Conclusion
In this lesson, you learned how to write expressive SQL using column aliases for readability, CASE WHEN for conditional classification, COALESCE for handling NULL values, SQL's built-in numeric and text functions, and subqueries for filtering against derived results or computed aggregates. These tools let you do significant data transformation directly in SQL before data even reaches Python. In the next lesson, you'll learn how to summarize data across groups using GROUP BY and ORDER BY.
Practice
Coding Assessment
Practice the concepts from this lesson using this notebook. After completing the exercises, save your notebook to GitHub and submit the link for grading.
Knowledge Check
SELECT title, price
FROM books
WHERE price > (SELECT AVG(price) FROM books);