1. Lighthouse

Level 7 · Boss: the yearly customer status report

The request

From Catherine Hale, Chief Executive Officer

“For the board pack I want the 2025 customer picture on one slide: how many of 2024's buyers we kept, how many we lost, how many are brand new or came back after a break, and what each group spent with us in 2025.”

Your goal

Step 1 of 3. Return one row with: customers_2024 (number of distinct 2024 buyers), retained (2024 buyers who are also 2025 buyers), lapsed (2024 buyers who are not 2025 buyers) and retention_pct (retained / customers_2024 * 100, rounded to 1).

  • Read from the orders tablenot done yet
  • Show exactly customers_2024 retained lapsed retention_pctnot done yet
  • 1 rownot done yet

Step 1 · your query

SQL

Loading the editor…

    ResultPreview of orders · press Run to see yours
    Preview of orders
    order_idcustomer_idorder_datestatuschannel
    10011252023-01-01 20:51:19completedweb
    10021562023-01-02 07:58:46completedweb
    1003222023-01-02 10:25:27completedmarketplace
    1004482023-01-02 11:21:51completedmarketplace

    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 customers · press Run to see yours
      Preview of customers
      customer_idfirst_namelast_nameemailcity
      1OwenHudsonowen.hudson@icloud.comSan Francisco
      2DouglasMendozadmendoza69@gmail.comAtlanta
      3HenryBrookshbrooks@lakesidecafe.comLos Angeles
      4BrianGrahambgraham@bluecedarhomes.comSeattle

      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 customers · press Run to see yours
        Preview of customers
        customer_idfirst_namelast_nameemailcity
        1OwenHudsonowen.hudson@icloud.comSan Francisco
        2DouglasMendozadmendoza69@gmail.comAtlanta
        3HenryBrookshbrooks@lakesidecafe.comLos Angeles
        4BrianGrahambgraham@bluecedarhomes.comSeattle

        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.

        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
        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
        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
        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
        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

        This boss turns three set operations into the customer bridge every growth review uses: where this year’s customers came from, and where last year’s went.

        Step 1: retained and lapsed

        Two buyer lists, one per year, and two comparisons:

        • retained = 2024 buyers INTERSECT 2025 buyers
        • lapsed = 2024 buyers EXCEPT 2025 buyers

        The buyer CTEs are one row per order, so count DISTINCT customer_id when you want the size of a list. The set operations already return distinct ids. At Harbor & Pine, 997 customers bought in 2024; 752 of them bought again (75.4%) and 245 did not. Retained plus lapsed must equal the 2024 total, which is a free sanity check.

        Step 2: four statuses, one table

        2025 had 1,447 buyers. Two of the groups are easy, new and retained. The third is the one people miss: customers who bought before 2024, skipped 2024 and came back in 2025. Calling them “new” overstates acquisition, so they get their own label, reactivated:

        SELECT customer_id FROM (
          SELECT customer_id FROM buyers_2025
          INTERSECT
          SELECT customer_id FROM buyers_before_2025
        ) AS b
        EXCEPT
        SELECT customer_id FROM buyers_2024

        Wrapping the first operation in a subquery makes the order of operations explicit. (INTERSECT binds more tightly than UNION and EXCEPT in standard SQL, but parentheses are cheaper than remembering.)

        The groups do not overlap, so UNION ALL with a status label stacks them, and a GROUP BY counts each: 752 retained, 635 new, 245 lapsed, 60 reactivated. New + retained + reactivated = 1,447, every 2025 buyer exactly once.

        Step 3: what each group spent

        Now join a set-operation result to real data. Aggregate 2025 revenue to one row per customer first, then join it to the labels. Retained customers bring in about $1.49 million at roughly $1,987 each, new customers about $758,000 at roughly $1,193, and the 60 reactivated customers about $50,000. Lapsed customers have no 2025 revenue, so the inner join drops them, which is what the task asks for.

        Common mistakes

        • Counting orders instead of customers in step 1 (the starter returns 3,673, the number of completed 2024 orders).
        • Putting new and reactivated together, or letting a customer land in two groups. Check that the group totals add up to the yearly buyer counts.
        • Joining labels to order_items and counting rows, which turns customers into order lines.

        In SQL Server

        INTERSECT, EXCEPT and UNION ALL work the same way, and derived tables need an alias as here. The scalar subqueries in step 1 are fine in T-SQL, but 100.0 * matters more there: with two integer counts, / would truncate the percentage to 0.

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