Skip to editor content
learningsql.orglesson 9 of 25

Views and CTEs

As your queries grow more complex, you need ways to organize them. Views and Common Table Expressions (CTEs) are two powerful tools for breaking complex logic into manageable, reusable pieces. They make your SQL easier to read, maintain, and debug.

Creating Views

A view is a saved query that acts like a virtual table. It doesn't store data — it runs the underlying query each time you reference it.

-- Create base tables
CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    department_id INTEGER,
    salary INTEGER,
    hire_date TEXT
);

CREATE TABLE departments (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    location TEXT
);

INSERT INTO departments (id, name, location)
VALUES
    (1, 'Engineering', 'Building A'),
    (2, 'Marketing', 'Building B'),
    (3, 'Sales', 'Building C');

INSERT INTO employees (id, name, department_id, salary, hire_date)
VALUES
    (1, 'Alice Chen', 1, 95000, '2020-03-15'),
    (2, 'Bob Martinez', 1, 88000, '2021-06-01'),
    (3, 'Carol White', 2, 72000, '2019-11-20'),
    (4, 'David Kim', 2, 68000, '2022-01-10'),
    (5, 'Eva Brown', 3, 65000, '2023-04-05'),
    (6, 'Frank Lee', 3, 71000, '2020-08-22'),
    (7, 'Grace Patel', 1, 102000, '2018-07-14');

-- Create a view that joins employees with departments
CREATE VIEW employee_details AS
SELECT
    e.id,
    e.name,
    d.name AS department,
    d.location,
    e.salary,
    e.hire_date
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- Now query the view like a table
SELECT * FROM employee_details;

-- Filter and sort the view
SELECT name, department, salary
FROM employee_details
WHERE salary > 80000
ORDER BY salary DESC;

Once a view exists, you can SELECT from it just like any table. You can filter, sort, join, and aggregate view results.

Views for Simplifying Aggregations

Views are especially useful for saving complex aggregations that you query repeatedly.

-- Create tables
CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    category TEXT,
    price REAL
);

CREATE TABLE order_items (
    id INTEGER PRIMARY KEY,
    product_id INTEGER,
    quantity INTEGER,
    order_date TEXT
);

INSERT INTO products (id, name, category, price)
VALUES
    (1, 'Laptop', 'Electronics', 999.99),
    (2, 'Mouse', 'Electronics', 24.99),
    (3, 'Desk', 'Furniture', 299.99),
    (4, 'Chair', 'Furniture', 199.99),
    (5, 'Keyboard', 'Electronics', 79.99);

INSERT INTO order_items (id, product_id, quantity, order_date)
VALUES
    (1, 1, 3, '2025-01-10'),
    (2, 2, 15, '2025-01-11'),
    (3, 3, 5, '2025-01-12'),
    (4, 1, 2, '2025-01-15'),
    (5, 4, 8, '2025-01-16'),
    (6, 2, 10, '2025-01-18'),
    (7, 5, 12, '2025-01-20');

-- Create a view that summarizes product sales
CREATE VIEW product_sales_summary AS
SELECT
    p.name AS product,
    p.category,
    p.price,
    SUM(oi.quantity) AS total_sold,
    ROUND(SUM(oi.quantity * p.price), 2) AS total_revenue
FROM products p
LEFT JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name, p.category, p.price;

-- Query the summary view
SELECT * FROM product_sales_summary
ORDER BY total_revenue DESC;

-- Aggregate the view further
SELECT
    category,
    SUM(total_sold) AS category_units,
    SUM(total_revenue) AS category_revenue
FROM product_sales_summary
GROUP BY category;

-- Drop a view when no longer needed
-- DROP VIEW product_sales_summary;

Common Table Expressions (CTEs)

A CTE uses the WITH keyword to define a temporary named result set within a single query. Think of it as an inline, disposable view.

-- Create a sales table
CREATE TABLE sales (
    id INTEGER PRIMARY KEY,
    salesperson TEXT NOT NULL,
    region TEXT NOT NULL,
    amount REAL,
    sale_date TEXT
);

INSERT INTO sales (id, salesperson, region, amount, sale_date)
VALUES
    (1, 'Alice', 'North', 5000, '2025-01-05'),
    (2, 'Alice', 'North', 3200, '2025-01-12'),
    (3, 'Bob', 'South', 4500, '2025-01-08'),
    (4, 'Bob', 'South', 6100, '2025-01-15'),
    (5, 'Carol', 'North', 2800, '2025-01-10'),
    (6, 'Carol', 'North', 3900, '2025-01-18'),
    (7, 'David', 'South', 7200, '2025-01-06'),
    (8, 'David', 'South', 4800, '2025-01-20');

-- Use a CTE to find top performers per region
WITH salesperson_totals AS (
    SELECT
        salesperson,
        region,
        SUM(amount) AS total_sales,
        COUNT(*) AS num_deals
    FROM sales
    GROUP BY salesperson, region
),
region_averages AS (
    SELECT
        region,
        ROUND(AVG(total_sales), 2) AS avg_sales
    FROM salesperson_totals
    GROUP BY region
)
SELECT
    st.salesperson,
    st.region,
    st.total_sales,
    ra.avg_sales AS region_avg,
    CASE
        WHEN st.total_sales > ra.avg_sales THEN 'Above Average'
        ELSE 'Below Average'
    END AS performance
FROM salesperson_totals st
JOIN region_averages ra ON st.region = ra.region
ORDER BY st.region, st.total_sales DESC;

CTEs are defined with WITH cte_name AS (...). You can chain multiple CTEs separated by commas. Each CTE can reference the ones defined before it.

Recursive CTEs

Recursive CTEs can reference themselves, making them perfect for hierarchical data like org charts, category trees, or sequences.

-- Create an employees table with a manager hierarchy
CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    manager_id INTEGER,
    title TEXT
);

INSERT INTO employees (id, name, manager_id, title)
VALUES
    (1, 'Sarah CEO', NULL, 'CEO'),
    (2, 'Tom VP Eng', 1, 'VP Engineering'),
    (3, 'Lisa VP Sales', 1, 'VP Sales'),
    (4, 'Mike Lead', 2, 'Engineering Lead'),
    (5, 'Anna Dev', 4, 'Senior Developer'),
    (6, 'James Dev', 4, 'Developer'),
    (7, 'Karen Sales', 3, 'Sales Manager'),
    (8, 'Chris Sales', 7, 'Sales Rep');

-- Recursive CTE to build the org chart
WITH RECURSIVE org_chart AS (
    -- Base case: start with the CEO (no manager)
    SELECT
        id,
        name,
        title,
        manager_id,
        0 AS level,
        name AS path
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Recursive case: find employees who report to someone already in the result
    SELECT
        e.id,
        e.name,
        e.title,
        e.manager_id,
        oc.level + 1,
        oc.path || ' > ' || e.name
    FROM employees e
    INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT
    substr('            ', 1, level * 4) || name AS org_tree,
    title,
    level,
    path
FROM org_chart
ORDER BY path;

A recursive CTE has two parts:

  1. Base case: the initial SELECT that doesn't reference the CTE
  2. Recursive case: the SELECT that references the CTE, connected with UNION ALL

The database repeatedly runs the recursive part until it produces no new rows.

Generating Sequences with Recursive CTEs

Recursive CTEs aren't just for hierarchies — they can generate data sequences too.

-- Generate a sequence of dates for the first two weeks of January
WITH RECURSIVE date_series AS (
    SELECT '2025-01-01' AS date
    UNION ALL
    SELECT date(date, '+1 day')
    FROM date_series
    WHERE date < '2025-01-14'
)
SELECT date,
    CASE CAST(strftime('%w', date) AS INTEGER)
        WHEN 0 THEN 'Sunday'
        WHEN 1 THEN 'Monday'
        WHEN 2 THEN 'Tuesday'
        WHEN 3 THEN 'Wednesday'
        WHEN 4 THEN 'Thursday'
        WHEN 5 THEN 'Friday'
        WHEN 6 THEN 'Saturday'
    END AS day_of_week
FROM date_series;

This technique is extremely useful for generating calendar tables, number sequences, or filling gaps in time-series data.

Views vs CTEs

Both simplify complex queries, but they serve different purposes:

FeatureViewsCTEs
PersistenceStored in databaseExists only during query
ReusabilityAny query can reference themOnly within the same query
RecursionNot supportedSupported
Use caseShared logic across queriesSingle complex query

Practice Exercises

Try these on your own:

  1. Create a view that shows each department's headcount and average salary
  2. Write a CTE that finds the highest-paid employee in each department
  3. Use a recursive CTE to generate numbers from 1 to 20
  4. Create a view for product sales and then query it to find the best-selling category
  5. Build an org chart recursive CTE and find all employees under a specific manager

Key Takeaways

  • Views are saved queries that act like virtual tables — use them for frequently needed logic
  • CTEs use WITH ... AS (...) to create temporary result sets within a single query
  • You can chain multiple CTEs, each building on the previous one
  • Recursive CTEs handle hierarchical data and sequence generation
  • A recursive CTE needs a base case and a recursive case joined by UNION ALL
  • Views persist in the database; CTEs exist only during query execution

Pro Tip: Use CTEs to replace deeply nested subqueries. They make your SQL read from top to bottom instead of inside-out, which is much easier for humans to follow.

Next Steps

With views and CTEs, your queries are organized and maintainable. But what happens when multiple operations need to succeed or fail as a unit — like transferring money between two accounts? The next lesson covers transactions, the mechanism databases use to guarantee data consistency. You will learn BEGIN, COMMIT, ROLLBACK, and the ACID properties that make databases reliable even when things go wrong.

Next lesson

Transactions

Learn how to use transactions to ensure data consistency with BEGIN, COMMIT, ROLLBACK, and SAVEPOINT

20 min