Find Products with No Sales in the Last Month of Data
Return the products that were not sold in the final month of the order history.
The challenge runs on the SQLabHub practice database (SQLite) and uses the products, orders and order_items tables. You can solve it in the browser, with nothing to install.
The task
Find products with no sales in the last month of the data, counting back from the most recent order date.
A correct answer returns 103 rows with 10 columns: id, name, category, price, cost, stock, sku, status, rating, review_count.
Tables used in this challenge
products 120 rows
id INTEGER (PK) · name TEXT · category TEXT · price REAL · cost REAL · stock INTEGER · sku TEXT · status TEXT · rating REAL · review_count INTEGER
| id | name | category | price | cost | stock | sku | status | rating | review_count |
|---|---|---|---|---|---|---|---|---|---|
orders 200 rows
id INTEGER (PK) · user_id INTEGER · total REAL · discount REAL · item_count INTEGER · status TEXT · order_date TEXT · shipped_date TEXT · shipping_city TEXT · shipping_country TEXT
| id | user_id | total | discount | item_count | status | order_date | shipped_date | shipping_city | shipping_country |
|---|---|---|---|---|---|---|---|---|---|
order_items 496 rows
id INTEGER (PK) · order_id INTEGER · product_id INTEGER · quantity INTEGER · unit_price REAL · subtotal REAL
| id | order_id | product_id | quantity | unit_price | subtotal |
|---|---|---|---|---|---|
The first rows of each table are shown. See how all the tables relate on the database schema page.
Expected output
| id | name | category | price | cost | stock | sku | status | rating | review_count |
|---|---|---|---|---|---|---|---|---|---|
First 5 of 103 rows. This is the output of the model solution on the practice database. The task does not fix a sort order, so your rows may come back in a different order.
What you practise
- Filtering rows with a WHERE condition
- Testing whether a related row exists with EXISTS or NOT EXISTS
- Summarising many rows into one value with COUNT, SUM, AVG, MIN or MAX
- Combining rows from two tables on a matching key with an inner join
- Writing a subquery that refers to the current row of the outer query
- Using the result of one query inside another
- Working with dates in SQLite: strftime, julianday and DATE
Hint and approach
Try the challenge first. Open these only when you need them.
Show the hint
NOT EXISTS with the product and the date condition inside the subquery
Show the step-by-step approach
How the model solution is built, in the order the database evaluates it:
- Read from the
productstable. - Filter in
WHERE: keep the rows where the subquery finds no matching row (NOT EXISTS). - The subquery is correlated: it refers to the current outer row, so it is evaluated for each row. It works like this:
- Read from the
order_itemstable. - Join
ordersonorder_items.order_id=orders.id. - Filter in
WHERE: keep the rows whereorder_items.product_idequals the outer row'sid, andorders.order_dateis at leastDATE((SELECT MAX(order_date) FROM orders), '-1 month'). - The subquery runs once and returns a single value. It works like this:
- Read from the
orderstable. - Aggregate over all the rows that are left: compute
MAX(order_date). With no GROUP BY, the result is a single row.
- Read from the
- Select a constant: the enclosing query only needs to know whether a row exists, not what is in it.
- Read from the
- Return every column of
products.
Model solution
Checking your session…
What to watch for
The date condition has to sit inside the subquery. A LEFT JOIN with the date in the ON clause and a NULL test outside does not work here: it leaves one row per older order line rather than one row per product. NOT EXISTS answers the question once per product and returns each at most once.
- EXISTS. The subquery must refer to the outer row. Without that link, EXISTS is simply true or false for the whole table and filters nothing.
- Correlated subquery. A correlated subquery is evaluated per outer row, which gets slow on large tables. A join to a grouped subquery or a window function usually does the same work in one pass.
- INNER JOIN. An inner join drops rows that have no partner on the other side, and it repeats a row once for every match, which inflates SUM and COUNT.
Related SQL challenges
- Count Orders in Each Customer's First 7 DaysSimilar levelHard
- Monthly Revenue Running Total with a CTE and Correlated SubquerySimilar levelHard
- Find Orders from the Last 30 Days of DataEasierMedium
- Count Orders per User in the Latest Year of DataEasierMedium
- Find the Amount of Each Customer's Most Recent OrderEasierMedium
Keep learning
- Lesson: EXISTS
- Lesson: Correlated Subqueries
- Lesson: INNER JOIN
- Lesson: Date Functions
- All SQL practice challenges, by difficulty and topic
- SQL interview questions with answers
- SQL Playground: run any query on the same database