HardSQL challenge #268SUBQUERYDATEANTI JOIN

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.

Solve This SQL Challenge

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

First 3 rows of the products table
idnamecategorypricecoststockskustatusratingreview_count
1Play Kitchen DeluxeToys1786.38589.51257SKU-000001active457
2Micro-Cut Paper Shredder 8-SheetOffice Supplies907.93281.46131SKU-000002active2.5447
34K Dual Dash CameraAutomotive568.89204.8176SKU-000003active4.129

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

First 3 rows of the orders table
iduser_idtotaldiscountitem_countstatusorder_dateshipped_dateshipping_cityshipping_country
1432638.110.253cancelled2024-04-24NULLLos AngelesUSA
2829470.195delivered2024-03-152023-04-09ChicagoUSA
3136683.240.082delivered2023-02-152023-07-10Los AngelesUSA

order_items 496 rows

id INTEGER (PK) · order_id INTEGER · product_id INTEGER · quantity INTEGER · unit_price REAL · subtotal REAL

First 3 rows of the order_items table
idorder_idproduct_idquantityunit_pricesubtotal
11765192.21961.05
21823107.1321.3
316515.395.39

The first rows of each table are shown. See how all the tables relate on the database schema page.

Expected output

Expected output of the challenge
idnamecategorypricecoststockskustatusratingreview_count
1Play Kitchen DeluxeToys1786.38589.51257SKU-000001active457
2Micro-Cut Paper Shredder 8-SheetOffice Supplies907.93281.46131SKU-000002active2.5447
4Ergonomic Desk Mat XLOffice Supplies1618.64663.64164SKU-000004active4262
5Premium Beard Oil 60mlHealth & Beauty1428.31428.49183SKU-000005active5305
6Retinol Eye Cream 30mlHealth & Beauty235.1182.29165SKU-000006active3.7463

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:

  1. Read from the products table.
  2. Filter in WHERE: keep the rows where the subquery finds no matching row (NOT EXISTS).
  3. The subquery is correlated: it refers to the current outer row, so it is evaluated for each row. It works like this:
    1. Read from the order_items table.
    2. Join orders on order_items.order_id = orders.id.
    3. Filter in WHERE: keep the rows where order_items.product_id equals the outer row's id, and orders.order_date is at least DATE((SELECT MAX(order_date) FROM orders), '-1 month').
    4. The subquery runs once and returns a single value. It works like this:
      1. Read from the orders table.
      2. Aggregate over all the rows that are left: compute MAX(order_date). With no GROUP BY, the result is a single row.
    5. Select a constant: the enclosing query only needs to know whether a row exists, not what is in it.
  4. 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.
Solve This SQL Challenge

Keep learning