Q1Which of these is required when you write a subquery (derived table) in the FROM clause?
Subqueries ② — Derived Tables, SELECT Subqueries, CREATE / INSERT SELECT
Learn SQL derived tables, SELECT-clause subqueries, pairing with CASE, and CREATE / INSERT SELECT — hands-on with employee and sales data in your browser.
Data used in this article — employee / department / sales
Last time you used a subquery in WHERE.
In this article you'll work through four applications in order: putting a subquery in the `FROM` clause (a derived table), writing one in a `SELECT` column, combining it with `CASE`, and building a new table from a subquery's result.
The material is three employee-data tables.
Using employee (30 employees), department (6 departments), and sales (50 sales line items), you'll build a per-department aggregate table and line up each employee's total sales as a column.
Derived tables in FROM — treat a subquery's result as a single table
A derived table is a way of writing a subquery in the FROM clause so that its result is treated as a single virtual table.
You can build "the average salary per department" first as an aggregate table, then apply further JOINs or WHEREs to that table.
It's handy when you want to narrow down an aggregated result one more step.
The key rule for derived tables is that you must give it an alias.
Write AS alias like FROM (SELECT ...) AS ds (SQLite also lets you drop AS) — without it you get an error.
Through that alias you reference the derived table's columns, like ds.avg_salary.
-- Derived table in FROM: aggregate headcount and avg salary per department, then join with department
SELECT d.dept_name, ds.headcount, ds.avg_salary
FROM (
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employee
WHERE dept_id IS NOT NULL
GROUP BY dept_id
) AS ds
JOIN department d ON d.dept_id = ds.dept_id
ORDER BY ds.avg_salary DESC;
SELECT-clause subqueries — embed a single value as a column
When you write a scalar subquery in the SELECT column list, you can add a column whose value is computed one per row.
Put (SELECT SUM(sale.amount) FROM sales sale WHERE sale.emp_id = emp.emp_id) in a column, and you can line up "that employee's total sales" as one column per employee.
This is a correlated subquery that references the outer emp.emp_id.
-- SELECT-clause subquery: add each employee's total sales as one column
SELECT emp.name,
COALESCE(
(SELECT SUM(sale.amount) FROM sales sale WHERE sale.emp_id = emp.emp_id),
0
) AS total_amount
FROM employee emp
ORDER BY emp.emp_id
LIMIT 6;
An employee with no sales makes the subquery return NULL, so replacing it with 0 using COALESCE(..., 0) makes the table easier to read.
A SELECT-clause subquery also has to fit into one row and one column.
Combining with CASE — attach a branching label based on a subquery's value
A SELECT-clause subquery works just as well as a condition in CASE.
You can build a rank column like "if total sales is 1,500,000 or more then High, if greater than 0 then Mid, otherwise None" by judging the subquery's value with CASE WHEN.
Note that you have to repeat the same subquery in each CASE WHEN (if readability comes first, there's also a way to write it just once with WITH (Common Table Expressions), which you'll learn in a later chapter).
In this article you'll cover the basic form of combining a subquery and CASE directly.
-- Classify employees by their number of sales
SELECT emp.name,
(SELECT COUNT(*) FROM sales sale WHERE sale.emp_id = emp.emp_id) AS sale_count,
CASE
WHEN (SELECT COUNT(*) FROM sales sale WHERE sale.emp_id = emp.emp_id) >= 4 THEN 'Frequent'
WHEN (SELECT COUNT(*) FROM sales sale WHERE sale.emp_id = emp.emp_id) >= 1 THEN 'Occasional'
ELSE 'None'
END AS activity
FROM employee emp
ORDER BY sale_count DESC, emp.emp_id
LIMIT 8;
CREATE / INSERT SELECT — turn a subquery's result into a table
Writing CREATE TABLE new_table AS SELECT ... lets you save the SELECT result directly as a new table (CTAS: Create Table As Select).
It's often used when you want to keep an aggregated result as a snapshot.
When you want to add rows to a table that already exists, use INSERT INTO existing_table SELECT ... to bulk-add the SELECT result.
These are write operations that create a table or add rows.
In this article you'll use a disposable working table top_seller for the aggregation.
So that you get the same result no matter how many times you run it, you'll clean up the working table with DROP TABLE IF EXISTS before recreating it.
Bulk INSERT is faster
INSERT INTO table SELECT ... inserts the target rows all at once in a single statement.
Compared with repeating INSERT INTO table VALUES (...) for each row, the SQL parsing, index updates, and transaction handling happen just once, so the more rows you insert, the faster it gets.
When copying or moving another table or an aggregated result, prefer the bulk INSERT ... SELECT over repeating INSERT one row at a time.
-- Save a per-department aggregate into a snapshot table
DROP TABLE IF EXISTS dept_summary;
CREATE TABLE dept_summary AS
SELECT dept_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employee
WHERE dept_id IS NOT NULL
GROUP BY dept_id;
SELECT * FROM dept_summary ORDER BY avg_salary DESC;
Knowledge Check
Answer each question one by one.
Q2In SELECT emp.name, (SELECT SUM(sale.amount) FROM sales sale WHERE sale.emp_id = emp.emp_id) AS total FROM employee emp, what happens to the total column for an employee with no sales at all?
Q3Which is a correct description of CREATE TABLE top_seller AS SELECT ...?