1. Lighthouse

Level 6 · Boss: cohort retention

The request

From Catherine Hale, Chief Executive Officer

“I'm presenting to investors next month. I need three things: each category's share of 2025 revenue, how long it takes customers to come back for a second order, and a monthly cohort retention view for the first half of 2025.”

Your goal

Step 1 of 3. For completed orders placed in 2025, return one row per category with: category, net_revenue (rounded to 2) and pct_of_total (the category's share of total 2025 net revenue across all categories, times 100, rounded to 1). Sort by net_revenue descending.

  • Read from the products tablenot done yet
  • Read from the orders tablenot done yet
  • Show exactly category net_revenue pct_of_totalnot 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 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 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.

        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

        Cohort analysis is the classic analyst take-home: it needs CTEs, window functions, date logic and a clear definition, all at once. This boss builds up to it.

        Step 1: share of total

        Aggregate per category, then divide each row by the grand total:

        ROUND(100.0 * net_revenue / SUM(net_revenue) OVER (), 1) AS pct_of_total

        An empty OVER () makes the window the whole result, so every row sees the same total. No subquery, no join. The shares should add up to about 100; if they add to far more, a join fanned out.

        Step 2: time to second order

        ROW_NUMBER numbers each customer’s orders in time order. LAG then gives each order’s gap from the previous one, and keeping order_number = 2 leaves exactly one row per returning customer: the gap between their first and second orders. The order_id tie-breaker guards against two orders with the same timestamp.

        The starter shows one customer’s numbered orders. Inspecting a single entity like this is how you sanity check a window before aggregating it.

        Customers who never placed a second order are not in the average. That is correct for “how long until they come back”, but present it next to how many never came back at all, or the number looks better than reality.

        Step 3: cohort retention

        Three definitions make a cohort table:

        • Cohort: the month of a customer’s first completed order. MIN(order_month) OVER (PARTITION BY customer_id) attaches it to every one of their rows without collapsing them.
        • Activity: a customer is active in a month if they placed at least one completed order. The customer_months CTE uses SELECT DISTINCT on purpose, to get one row per customer per month, the correct grain for “active or not”.
        • Retained in month 1: active in the calendar month right after the cohort month.
        COUNT(DISTINCT CASE WHEN order_month = CAST(cohort_month + INTERVAL '1 month' AS DATE)
                            THEN customer_id END) AS retained_m1

        COUNT(DISTINCT CASE ...) counts customers who meet the condition. The CASE returns NULL otherwise, and COUNT ignores NULLs.

        The cohort filter is applied after the window, in the outer query. Filtering orders to 2025 before computing MIN would assign customers who first bought in 2024 to a 2025 cohort, the same trap as with LAG and moving averages.

        Dialect notes

        Adding a month: DuckDB and Postgres accept date + INTERVAL '1 month' (the result is a timestamp, hence the cast). BigQuery uses DATE_ADD(cohort_month, INTERVAL 1 MONTH), MySQL DATE_ADD(cohort_month, INTERVAL 1 MONTH), Snowflake DATEADD(month, 1, cohort_month).

        Taking it further

        The full cohort table has a column for every month offset (m1, m2, m3, …), usually computed with DATEDIFF in months between order_month and cohort_month, grouped by cohort and offset, then pivoted. In an interview, getting month 1 right, with clear definitions stated out loud, is worth more than a rushed full grid.