The request

From Brian Mitchell, Finance Analyst
Your goal
Create a table category_sales_2025 from completed orders placed in 2025 (lines with quantity > 0) with one row per category: category, orders (distinct orders), units (sum of quantity) and net_revenue (sum of quantity * unit_price * (1 - discount_pct / 100.0), rounded to 2). Then query that table and return category, orders, units, net_revenue and revenue_per_order (net_revenue / orders, rounded to 2), sorted by net_revenue descending.
- Read from the
productstablenot done yet - Read from the
orderstablenot done yet - Show exactly
categoryordersunitsnet_revenuerevenue_per_ordernot done yet
Your query
Loading the editor…
Reference solution
| product_id | name | category | subcategory | unit_price |
|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Brian gets, and why
Tables
Harbor & Pine store data, ready to query.
products120 rows · 8 columns
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NULL | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
orders12,000 rows · 7 columns
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NULL |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NULL |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
order_items30,338 rows · 6 columns
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
customers2,018 rows · 9 columns
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NULL |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NULL |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NULL |
returns1,673 rows · 5 columns
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 rows · 8 columns
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NULL | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
web_sessions40,000 rows · 7 columns
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NULL | 2023-01-01 13:06:21 | mobile | social | 2 | false |
marketing_spend3,276 rows · 3 columns
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
The lesson
When the same expensive query feeds several reports, save its result once and query the saved copy.
CREATE TABLE ... AS SELECT, often shortened to CTAS, does exactly that.
CREATE TABLE AS SELECT
CREATE TABLE category_sales_2025 AS
SELECT p.category,
COUNT(DISTINCT o.order_id) AS orders,
SUM(i.quantity) AS units,
ROUND(SUM(i.quantity * i.unit_price * (1 - i.discount_pct / 100.0)), 2) AS net_revenue
FROM order_items AS i
JOIN orders AS o ON o.order_id = i.order_id
JOIN products AS p ON p.product_id = i.product_id
WHERE o.status = 'completed' AND i.quantity > 0
AND o.order_date >= '2025-01-01' AND o.order_date < '2026-01-01'
GROUP BY p.category;
There is no column list: the new table takes its column names from the query’s aliases and its types
from the expressions. That is why good aliases matter here. An unnamed SUM(...) would become a column
called something like sum(i.quantity), which is painful to type in every later query.
Query the saved table
The result is an ordinary table with six rows, one per category. Later queries read it like any other:
SELECT category, orders, units, net_revenue,
ROUND(net_revenue / orders, 2) AS revenue_per_order
FROM category_sales_2025
ORDER BY net_revenue DESC;
Furniture tops 2025 with $573,909.05 from 560 orders, so a Furniture order brings in about $1,024.84 of Furniture revenue. Decor appears in the most orders (2,155) but at $134.82 per order. Lighting sits in the middle with $385,144.61 from 1,221 orders.
A saved table is a snapshot. If a refund is recorded tomorrow, category_sales_2025 does not change
until you rebuild it. A view (CREATE VIEW name AS SELECT ...) is the alternative: it stores the
query, not the rows, so it is always current but recomputes every time you read it. Analysts use tables
for daily reporting snapshots and views for logic that must always be live.
Staging tables in practice
Real pipelines chain these: a staging table with the cleaned order lines, a summary table built from it, and a dashboard that reads the summary. Each step is small, named and checkable on its own, the same idea as a chain of CTEs, except the intermediate results persist.
In this game every run is rolled back, so the table exists only for the statements in that run. That is why the solution creates it and queries it in one go.
Common mistakes
- Missing the semicolon between the two statements. The database then sees one long, broken statement.
- Leaving aggregates without aliases, so the new columns get awkward generated names.
- Running
CREATE TABLEtwice on a real database: the second time fails because the table already exists. UseCREATE OR REPLACE TABLE(DuckDB, Snowflake, BigQuery) or drop it first. - Forgetting that the table is a snapshot and presenting stale numbers.
In SQL Server
T-SQL has no CREATE TABLE ... AS SELECT. You write SELECT ... INTO, which creates the table from the
query:
SELECT Category, COUNT(*) AS Orders
INTO dbo.CategorySales2025
FROM ...;
Postgres, DuckDB, Snowflake and BigQuery use CREATE TABLE ... AS; MySQL accepts it too.
Lantern lit!
Practised: create table asstaging tablectasaggregation
- Solved
- No hints
- First run
