Brightlane's merchandising team is ranking top-performing products for a promotional campaign.
Write a query to return the product ID and total revenue for every product whose combined order-line revenue exceeds $2,000.
Assumptions:
- The
order_itemstable contains one row per product per order. - Per-line revenue is
quantity * unit_price; per-product revenue is the sum of that calculation across all of the product's lines. - The threshold (
> $2,000) applies to the per-product total, not to any individual line item.
Output:
- One row per qualifying product, with columns
product_idandtotal_revenue.
Schema · ecommerce 5 tables
Run previews · Check grades
Write a query, then run it to see results here.
Worked solution Try it yourself first
SELECT
product_id,
SUM(quantity * unit_price) AS total_revenue
FROM
order_items
GROUP BY
product_id
HAVING
SUM(quantity * unit_price) > 2000 The shape
GROUP BY product_id builds per-product sums of quantity * unit_price; HAVING SUM(quantity * unit_price) > 2000 keeps only the products that clear $2,000. Product 6 lands at 11994, product 9 at 5996, down to product 30 at 2093.
Clause by clause
SELECT product_id, SUM(quantity * unit_price) AS total_revenuereturns the grouping column with the per-product revenue alongside it.SUMwalks the rows that belong to each product and adds thequantity * unit_priceexpression on each line.FROM order_itemsis the source set: one row per (order, product) line.GROUP BY product_idcollapses those lines into one row per product. Each surviving row carries the aggregated revenue for the product.HAVING SUM(quantity * unit_price) > 2000filters those per-product rows by aggregate. The expression is repeated rather than referenced by itstotal_revenuealias.
Why this and not HAVING total_revenue > 2000
Referencing the alias feels natural. PostgreSQL doesn't allow it. The evaluation order is FROM → WHERE → GROUP BY → HAVING → SELECT, which means the alias total_revenue hasn't been created yet at the moment HAVING runs. The fix is to repeat the aggregate expression. It feels redundant, but the rule is fixed: anything HAVING filters on has to be expressed as an aggregate, not the name SELECT will give it.
You practiced filtering on a computed aggregate with HAVING. The same expression that built the total_revenue column has to be repeated inside HAVING — PostgreSQL doesn't allow HAVING to reference SELECT-list aliases, because the alias doesn't exist yet when HAVING runs.