1. Lighthouse

Level 7 · Boss: the funnel report for budget planning

The request

From Jordan Reyes, Head of Marketing

“Budget planning is next week. I need three tables: how each source converts on each device this year, what each paid channel has cost per conversion over the last three years, and where each source loses its visitors. Make sure spend isn't double counted this time.”

Your goal

Step 1 of 3. For 2025, return one row per source and device with: source, device, sessions, conversions and conversion_rate_pct (conversions * 100 / sessions, rounded to 2). Sort by source, then device, ascending.

  • Read from the web_sessions tablenot done yet
  • Show exactly source device sessions conversions conversion_rate_pctnot done yet
  • 15 rows, sorted as askednot done yet

Step 1 · your query

SQL

Loading the editor…

    ResultPreview of web_sessions · press Run to see yours
    Preview of web_sessions
    session_idcustomer_idstarted_atdevicesource
    1NULL2023-01-01 23:02:24mobilepaid
    2182023-01-01 15:07:49desktoppaid
    3NULL2023-01-01 16:55:46desktopdirect
    4NULL2023-01-01 12:43:28mobileorganic

    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 web_sessions · press Run to see yours
      Preview of web_sessions
      session_idcustomer_idstarted_atdevicesource
      1NULL2023-01-01 23:02:24mobilepaid
      2182023-01-01 15:07:49desktoppaid
      3NULL2023-01-01 16:55:46desktopdirect
      4NULL2023-01-01 12:43:28mobileorganic

      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 web_sessions · press Run to see yours
        Preview of web_sessions
        session_idcustomer_idstarted_atdevicesource
        1NULL2023-01-01 23:02:24mobilepaid
        2182023-01-01 15:07:49desktoppaid
        3NULL2023-01-01 16:55:46desktopdirect
        4NULL2023-01-01 12:43:28mobileorganic

        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.

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

        The lesson

        What the report needs

        Marketing’s budget pack has three tables, and each one uses a different technique from this chapter:

        1. Conversion by source and device (2025): one GROUP BY on two columns.
        2. Cost per conversion by year and paid channel: two tables, each aggregated on its own, then joined.
        3. Funnel by source (2025): two stages per source with step rates.

        Pre-aggregate before you join

        marketing_spend has one row per day and channel. web_sessions has one row per visit. Join them on the channel and year and every spend row repeats once for every matching session: 2025 paid spend turns from $340,778 into $1.2 billion out of nowhere.

        The fix is to bring each table to the grain of the answer first, one row per year and channel, in its own CTE:

        WITH spend AS (SELECT year(spend_date) AS year, channel, SUM(amount) AS spend FROM marketing_spend GROUP BY 1, 2),
             conv  AS (SELECT year(started_at) AS year, source, COUNT(*) FILTER (WHERE converted) AS conversions FROM web_sessions GROUP BY 1, 2)

        Now the join is one-to-one and nothing is counted twice. Organic and direct have no spend, so an inner join from spend drops them, which is what a cost table should do.

        Step rates by source

        For the funnel table, count engaged sessions and conversions per source in one pass, and divide each stage by the stage before it. engaged_pct shows how well a source’s visitors get past the landing page; engaged_conv_pct shows whether those who browse go on to buy. Sorting by the second puts the sources whose engaged visitors buy at the top.

        Caveats to put in the pack

        • Conversions are last-touch converted sessions, not orders. App and marketplace orders are not in the session data.
        • Only known customers can convert here, so sources with many anonymous visitors look weaker.
        • Twelve day/channel rows are missing from marketing_spend because of tracking outages, so a few yearly totals are slightly low.

        Common mistakes

        • Summing spend after a join to sessions.
        • Dividing conversions by all sessions in the step rate, which repeats the overall rate.
        • Filtering 2025 with year() = 2025 on one table and a date range on the other and getting different windows. Use the same rule on both sides.

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