1. Lighthouse

Level 6 · Where customers first buy

The request

From Jordan Reyes, Head of Marketing

“I keep hearing that the app is where loyal customers end up, not where they start. Which channel did each customer use for their very first completed order? Give me counts per channel and the share of all customers.”

Your goal

Number each customer's completed orders by order_date (ties broken by order_id) and keep each customer's first one. Return one row per channel of that first order with: first_channel, customers (number of customers whose first completed order used that channel) and pct_of_customers (customers / all customers with a completed order * 100, rounded to 1). Sort by customers descending.

  • Read from the customers tablenot done yet
  • Read from the orders tablenot done yet
  • Show exactly first_channel customers pct_of_customersnot done yet

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.

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

    “First row per group” is one of the most common window function jobs: the first order per customer, the latest price per product, the top seller per category.

    Recap: number, then filter

    SELECT customer_id, order_id, channel,
           ROW_NUMBER() OVER (PARTITION BY customer_id
                              ORDER BY order_date, order_id) AS order_number
    FROM orders
    WHERE status = 'completed';

    PARTITION BY customer_id restarts the numbering for every customer; ORDER BY order_date makes 1 the earliest. The order_id tie-breaker matters: without it, two orders with the same timestamp could swap places between runs, and your counts would wobble.

    Customers who never completed an order have no rows here at all, so the denominator for the share is the 1,730 customers who did, not everyone in the customers table.

    A window function cannot be filtered in the same query’s WHERE, because WHERE runs first. Wrap the numbering in a CTE and filter order_number = 1 outside.

    What the answer shows

    Of the 1,730 customers with a completed order, 893 (51.6%) placed their first one on the web, 593 (34.3%) in the app and 244 (14.1%) on the marketplace. Compare that with all completed orders, where the web has 49.5% and the app 35.8%. The first-order mix leans a little more to the web than the overall mix, which is consistent with the ticket’s hunch that customers move to the app later; the next step would be to count channel switches between order 1 and order 2.

    SUM(COUNT(*)) OVER () looks odd at first: the inner COUNT(*) is the grouped count per channel, and the window sums those counts over the whole result. It gives the total without a second query.

    Common mistakes

    • No tie-breaker in the window’s ORDER BY.
    • RANK() instead of ROW_NUMBER(): ties would give two rows with rank 1 for the same customer.
    • Forgetting the status filter, so a cancelled first order decides the channel.

    In SQL Server

    The window syntax is the same. Neither SQL Server nor most other databases have DuckDB’s QUALIFY, so the CTE-then-filter pattern is the portable one. For a quick look at rows, SQL Server uses SELECT TOP 20 ... instead of LIMIT 20.

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