1. Lighthouse

Level 2 · Find the slow movers in SQL

The request

From Thomas Whitaker, Head of Merchandising

“We agreed it: active products on sale all of 2025, each one against its own category, the weakest three per category. First show me which products you're judging and which you're leaving out, and whether anything didn't sell at all.”

Your goal

Step 1 of 3. Check which products can be judged. Label every product product_group: 'retired' if is_active is false, else 'new in 2025' if launched_on >= '2025-01-01', else 'established'. Return product_group, products (the number of products), sold_in_2025 (how many had any 2025 sales) and missing_cost (how many have a NULL unit_cost). Sort by product_group.

  • Read from the products tablenot done yet
  • Read from the orders tablenot done yet
  • Show exactly product_group products sold_in_2025 missing_costnot done yet

Step 1 · your query

SQL

Loading the editor…

    ResultPreview of products · press Run to see yours
    Preview of products
    product_idnamecategorysubcategoryunit_price
    101Vale Velvet LoveseatFurnitureSofas1659.00
    102Alder Oak Coffee TableFurnitureTables919.00
    103Orchard Rattan Dining ChairFurnitureChairs789.00
    104Upland Oak BookcaseFurnitureStorage429.00

    Mote checks your result

    Press Run. Each row that matches the goal becomes a firefly in your lantern.

    Step 2 · your query

    SQL

    Loading the editor…

      ResultPreview of products · press Run to see yours
      Preview of products
      product_idnamecategorysubcategoryunit_price
      101Vale Velvet LoveseatFurnitureSofas1659.00
      102Alder Oak Coffee TableFurnitureTables919.00
      103Orchard Rattan Dining ChairFurnitureChairs789.00
      104Upland Oak BookcaseFurnitureStorage429.00

      Mote checks your result

      Press Run. Each row that matches the goal becomes a firefly in your lantern.

      Step 3 · your query

      SQL

      Loading the editor…

        ResultPreview of products · press Run to see yours
        Preview of products
        product_idnamecategorysubcategoryunit_price
        101Vale Velvet LoveseatFurnitureSofas1659.00
        102Alder Oak Coffee TableFurnitureTables919.00
        103Orchard Rattan Dining ChairFurnitureChairs789.00
        104Upland Oak BookcaseFurnitureStorage429.00

        Mote checks your result

        Press Run. Each row that matches the goal becomes a firefly in your lantern.

        Tables

        Harbor & Pine store data, ready to query.

        products120 rows · 8 columns
        • product_idinteger
        • namevarchar
        • categoryvarchar
        • subcategoryvarchar
        • unit_pricedecimal
        • unit_costdecimal
        • launched_ondate
        • is_activeboolean
        First 5 rows of products
        product_idnamecategorysubcategoryunit_priceunit_costlaunched_onis_active
        101Vale Velvet LoveseatFurnitureSofas1659.00NULL2023-09-11true
        102Alder Oak Coffee TableFurnitureTables919.00503.502024-05-16true
        103Orchard Rattan Dining ChairFurnitureChairs789.00429.872023-05-27true
        104Upland Oak BookcaseFurnitureStorage429.00214.702022-12-20true
        105Avery Leather Bed FrameFurnitureBeds449.00232.702021-08-31true
        orders12,000 rows · 7 columns
        • order_idinteger
        • customer_idinteger
        • order_datetimestamp
        • statusvarchar
        • channelvarchar
        • shipping_feedecimal
        • coupon_codevarchar
        First 5 rows of orders
        order_idcustomer_idorder_datestatuschannelshipping_feecoupon_code
        10011252023-01-01 20:51:19completedweb0.00NULL
        10021562023-01-02 07:58:46completedweb0.00NULL
        1003222023-01-02 10:25:27completedmarketplace4.99WELCOME10
        1004482023-01-02 11:21:51completedmarketplace4.99WELCOME10
        1005822023-01-02 11:43:36completedweb0.00WELCOME10
        order_items30,338 rows · 6 columns
        • order_item_idinteger
        • order_idinteger
        • product_idinteger
        • quantityinteger
        • unit_pricedecimal
        • discount_pctdecimal
        First 5 rows of order_items
        order_item_idorder_idproduct_idquantityunit_pricediscount_pct
        11001121931.990.00
        21001154311.990.00
        31002154411.990.00
        410021941149.000.00
        51002193219.990.00
        customers2,018 rows · 9 columns
        • customer_idinteger
        • first_namevarchar
        • last_namevarchar
        • emailvarchar
        • cityvarchar
        • statevarchar
        • signup_datedate
        • segmentvarchar
        • referral_sourcevarchar
        First 5 rows of customers
        customer_idfirst_namelast_nameemailcitystatesignup_datesegmentreferral_source
        1OwenHudsonowen.hudson@icloud.comSan FranciscoCA2022-06-02consumersearch
        2DouglasMendozadmendoza69@gmail.comAtlantaGA2022-06-02consumerNULL
        3HenryBrookshbrooks@lakesidecafe.comLos AngelesCA2022-06-04businessNULL
        4BrianGrahambgraham@bluecedarhomes.comSeattleWA2022-06-06businesspinterest
        5KyleJimenezkjimenez27@yahoo.comIndianapolisIN2022-06-07consumerNULL
        returns1,673 rows · 5 columns
        • return_idinteger
        • order_item_idinteger
        • returned_ondate
        • reasonvarchar
        • refund_amountdecimal
        First 5 rows of returns
        return_idorder_item_idreturned_onreasonrefund_amount
        1142023-01-10damaged83.69
        2422023-01-10wrong_item19.99
        31052023-01-14wrong_item10.99
        4242023-01-16changed_mind26.99
        51262023-01-16late20.99
        employees60 rows · 8 columns
        • employee_idinteger
        • first_namevarchar
        • last_namevarchar
        • departmentvarchar
        • titlevarchar
        • manager_idinteger
        • hire_datedate
        • salaryinteger
        First 5 rows of employees
        employee_idfirst_namelast_namedepartmenttitlemanager_idhire_datesalary
        1CatherineHaleExecutiveChief Executive OfficerNULL2016-03-01285000
        2RobertKimExecutiveChief Operating Officer12017-01-09235000
        3MeganCollinsSalesHead of Sales12018-04-16172000
        4ChloeGutierrezSalesAccount Executive32020-01-2172000
        5FrankBellSalesAccount Executive32023-05-2173000
        web_sessions40,000 rows · 7 columns
        • session_idinteger
        • customer_idinteger
        • started_attimestamp
        • devicevarchar
        • sourcevarchar
        • pages_viewedinteger
        • convertedboolean
        First 5 rows of web_sessions
        session_idcustomer_idstarted_atdevicesourcepages_viewedconverted
        1NULL2023-01-01 23:02:24mobilepaid1false
        2182023-01-01 15:07:49desktoppaid1false
        3NULL2023-01-01 16:55:46desktopdirect7false
        4NULL2023-01-01 12:43:28mobileorganic3false
        5NULL2023-01-01 13:06:21mobilesocial2false
        marketing_spend3,276 rows · 3 columns
        • spend_datedate
        • channelvarchar
        • amountdecimal
        First 5 rows of marketing_spend
        spend_datechannelamount
        2023-01-01paid492.99
        2023-01-01email37.38
        2023-01-01social200.53
        2023-01-02paid709.13
        2023-01-02email64.13

        The lesson

        What you are checking, and why

        Before ranking anything, decide which products can fairly be judged. Three groups behave differently:

        • Retired products are already leaving the range; they belong to a different review.
        • New in 2025 products had only part of the year on sale. A lantern launched in August cannot be compared with one that sold all summer.
        • Established products were on sale for all twelve months: these are the ones to rank.

        Step 1 labels every product with a CASE and counts. It starts from products with a LEFT JOIN to the 2025 sales, so a product that sold nothing would still appear, with NULL sales:

        FROM products AS p
        LEFT JOIN sales AS s ON s.product_id = p.product_id

        COUNT(s.product_id) then counts only the products that sold. The same step counts missing unit costs, because a slow product with an unknown cost is one whose margin nobody can judge.

        Ranking inside each category

        Step 2 needs two numbers for every product that depend on its category: the category’s total and the product’s rank. Window functions give both without collapsing the rows:

        s.revenue * 100.0 / SUM(s.revenue) OVER (PARTITION BY p.category) AS share,
        ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY s.revenue) AS rn

        PARTITION BY restarts the calculation for each category. Because window functions run after WHERE, the filter rn <= 3 goes in the outer query.

        Step 3 ranks again and then aggregates: SUM(CASE WHEN rn <= 3 THEN revenue ELSE 0 END) keeps the weakest three, SUM(revenue) the whole category.

        Reading the result

        category weakest product units_2025 category_share_pct
        Furniture Upland Oak Bookcase 9 0.7
        Textiles Spruce Wool Duvet Cover 25 0.7
        Outdoor Orchard Aluminum Adirondack Chair 93 1.0
        Lighting Fern Rattan Desk Lamp 13 1.2

        Revenue share and units together tell the story. The bookcase and the desk lamp sold almost nothing; the Adirondack chair sold 93 units but at $51.99 each, so it barely registers in a category of patio sets and grills.

        Pitfalls

        • A product with NULL sales falls out of an inner join; start from products when you need every product.
        • ROW_NUMBER breaks ties arbitrarily; with revenue to the cent ties are rare, but say which rule you used.
        • A share of revenue says nothing about profit. Seven established products have no unit cost, so their margin is unknown.

        Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.