The request

From Daniel Foster, Mobile Product Manager
Your goal
From web_sessions started in 2025 (started_at from 2025-01-01 up to, not including, 2026-01-01), return one row per device with: device, sessions (all sessions), engaged_sessions (sessions with pages_viewed >= 3), converted_sessions (sessions where converted is true), engaged_pct (engaged_sessions / sessions * 100, rounded to 1), engaged_to_converted_pct (converted_sessions / engaged_sessions * 100, rounded to 1) and overall_conversion_pct (converted_sessions / sessions * 100, rounded to 2). Sort by sessions descending.
- Read from the
web_sessionstablenot done yet - Show exactly
devicesessionsengaged_sessionsconverted_sessionsengaged_pctengaged_to_converted_pctoverall_conversion_pctnot done yet - 3 rows, sorted as askednot done yet
Your query
Loading the editor…
Reference solution
| session_id | customer_id | started_at | device | source |
|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic |
Mote checks your result
Press Run. Each row that matches the goal becomes a firefly in your lantern.
What Daniel gets, and why
Tables
Harbor & Pine store data, ready to query.
web_sessions40,000 rows · 7 columns
session_idintegercustomer_idintegerstarted_attimestampdevicevarcharsourcevarcharpages_viewedintegerconvertedboolean
| session_id | customer_id | started_at | device | source | pages_viewed | converted |
|---|---|---|---|---|---|---|
| 1 | NULL | 2023-01-01 23:02:24 | mobile | paid | 1 | false |
| 2 | 18 | 2023-01-01 15:07:49 | desktop | paid | 1 | false |
| 3 | NULL | 2023-01-01 16:55:46 | desktop | direct | 7 | false |
| 4 | NULL | 2023-01-01 12:43:28 | mobile | organic | 3 | false |
| 5 | NULL | 2023-01-01 13:06:21 | mobile | social | 2 | false |
customers2,018 rows · 9 columns
customer_idintegerfirst_namevarcharlast_namevarcharemailvarcharcityvarcharstatevarcharsignup_datedatesegmentvarcharreferral_sourcevarchar
| customer_id | first_name | last_name | city | state | signup_date | segment | referral_source | |
|---|---|---|---|---|---|---|---|---|
| 1 | Owen | Hudson | owen.hudson@icloud.com | San Francisco | CA | 2022-06-02 | consumer | search |
| 2 | Douglas | Mendoza | dmendoza69@gmail.com | Atlanta | GA | 2022-06-02 | consumer | NULL |
| 3 | Henry | Brooks | hbrooks@lakesidecafe.com | Los Angeles | CA | 2022-06-04 | business | NULL |
| 4 | Brian | Graham | bgraham@bluecedarhomes.com | Seattle | WA | 2022-06-06 | business | |
| 5 | Kyle | Jimenez | kjimenez27@yahoo.com | Indianapolis | IN | 2022-06-07 | consumer | NULL |
products120 rows · 8 columns
product_idintegernamevarcharcategoryvarcharsubcategoryvarcharunit_pricedecimalunit_costdecimallaunched_ondateis_activeboolean
| product_id | name | category | subcategory | unit_price | unit_cost | launched_on | is_active |
|---|---|---|---|---|---|---|---|
| 101 | Vale Velvet Loveseat | Furniture | Sofas | 1659.00 | NULL | 2023-09-11 | true |
| 102 | Alder Oak Coffee Table | Furniture | Tables | 919.00 | 503.50 | 2024-05-16 | true |
| 103 | Orchard Rattan Dining Chair | Furniture | Chairs | 789.00 | 429.87 | 2023-05-27 | true |
| 104 | Upland Oak Bookcase | Furniture | Storage | 429.00 | 214.70 | 2022-12-20 | true |
| 105 | Avery Leather Bed Frame | Furniture | Beds | 449.00 | 232.70 | 2021-08-31 | true |
orders12,000 rows · 7 columns
order_idintegercustomer_idintegerorder_datetimestampstatusvarcharchannelvarcharshipping_feedecimalcoupon_codevarchar
| order_id | customer_id | order_date | status | channel | shipping_fee | coupon_code |
|---|---|---|---|---|---|---|
| 1001 | 125 | 2023-01-01 20:51:19 | completed | web | 0.00 | NULL |
| 1002 | 156 | 2023-01-02 07:58:46 | completed | web | 0.00 | NULL |
| 1003 | 22 | 2023-01-02 10:25:27 | completed | marketplace | 4.99 | WELCOME10 |
| 1004 | 48 | 2023-01-02 11:21:51 | completed | marketplace | 4.99 | WELCOME10 |
| 1005 | 82 | 2023-01-02 11:43:36 | completed | web | 0.00 | WELCOME10 |
order_items30,338 rows · 6 columns
order_item_idintegerorder_idintegerproduct_idintegerquantityintegerunit_pricedecimaldiscount_pctdecimal
| order_item_id | order_id | product_id | quantity | unit_price | discount_pct |
|---|---|---|---|---|---|
| 1 | 1001 | 121 | 9 | 31.99 | 0.00 |
| 2 | 1001 | 154 | 3 | 11.99 | 0.00 |
| 3 | 1002 | 154 | 4 | 11.99 | 0.00 |
| 4 | 1002 | 194 | 1 | 149.00 | 0.00 |
| 5 | 1002 | 193 | 2 | 19.99 | 0.00 |
returns1,673 rows · 5 columns
return_idintegerorder_item_idintegerreturned_ondatereasonvarcharrefund_amountdecimal
| return_id | order_item_id | returned_on | reason | refund_amount |
|---|---|---|---|---|
| 1 | 14 | 2023-01-10 | damaged | 83.69 |
| 2 | 42 | 2023-01-10 | wrong_item | 19.99 |
| 3 | 105 | 2023-01-14 | wrong_item | 10.99 |
| 4 | 24 | 2023-01-16 | changed_mind | 26.99 |
| 5 | 126 | 2023-01-16 | late | 20.99 |
employees60 rows · 8 columns
employee_idintegerfirst_namevarcharlast_namevarchardepartmentvarchartitlevarcharmanager_idintegerhire_datedatesalaryinteger
| employee_id | first_name | last_name | department | title | manager_id | hire_date | salary |
|---|---|---|---|---|---|---|---|
| 1 | Catherine | Hale | Executive | Chief Executive Officer | NULL | 2016-03-01 | 285000 |
| 2 | Robert | Kim | Executive | Chief Operating Officer | 1 | 2017-01-09 | 235000 |
| 3 | Megan | Collins | Sales | Head of Sales | 1 | 2018-04-16 | 172000 |
| 4 | Chloe | Gutierrez | Sales | Account Executive | 3 | 2020-01-21 | 72000 |
| 5 | Frank | Bell | Sales | Account Executive | 3 | 2023-05-21 | 73000 |
marketing_spend3,276 rows · 3 columns
spend_datedatechannelvarcharamountdecimal
| spend_date | channel | amount |
|---|---|---|
| 2023-01-01 | paid | 492.99 |
| 2023-01-01 | 37.38 | |
| 2023-01-01 | social | 200.53 |
| 2023-01-02 | paid | 709.13 |
| 2023-01-02 | 64.13 |
The lesson
A funnel follows people through a sequence of steps and counts how many survive each one. Product teams use it to find the step that loses the most people, because that is where a fix pays off.
Stages as conditional counts
When every stage is a condition on the same row, a funnel is one GROUP BY with a count per stage:
SELECT
device,
COUNT(*) AS sessions,
COUNT(*) FILTER (WHERE pages_viewed >= 3) AS engaged_sessions,
COUNT(*) FILTER (WHERE converted) AS converted_sessions
FROM web_sessions
GROUP BY device;
FILTER (WHERE ...) counts only the rows that match. It works in DuckDB and Postgres;
SUM(CASE WHEN pages_viewed >= 3 THEN 1 ELSE 0 END) does the same everywhere.
Check that the stages nest
A funnel only makes sense if each stage is a subset of the one before. At Harbor & Pine every
converted session viewed at least 3 pages, which you can confirm with
COUNT(*) FILTER (WHERE converted AND pages_viewed < 3): it returns 0. If it did not, a “step rate”
could exceed 100% and the funnel would be lying.
Worked example: 2025 by device
| device | sessions | engaged | converted | engaged % | engaged → converted % | overall % |
|---|---|---|---|---|---|---|
| mobile | 9,407 | 4,195 | 237 | 44.6 | 5.6 | 2.52 |
| desktop | 5,252 | 2,311 | 217 | 44.0 | 9.4 | 4.13 |
| tablet | 1,566 | 707 | 46 | 45.1 | 6.5 | 2.94 |
The overall rate alone says “mobile converts worse”. The steps say where: mobile visitors engage as often as desktop visitors, then fall off at the last step. That points the product team at checkout on mobile, not at the landing pages.
Two kinds of rate
- Step conversion: this stage divided by the previous stage. It locates the leak.
- Overall conversion: the last stage divided by the first. It is the headline number.
Name which one you report. “Conversion is 9.4%” and “conversion is 4.13%” are both true for desktop.
Common mistakes
- Integer division. In Postgres and SQL Server,
237 / 9407is 0. Multiply by100.0first. - Counting the wrong unit. These are session rates. A customer-level funnel counts distinct customers per stage and gives different numbers.
- Stages that do not nest, as described above.
In SQL Server
There is no FILTER clause and converted would be a BIT, so use CASE:
SUM(CASE WHEN converted = 1 THEN 1 ELSE 0 END) AS converted_sessions,
ROUND(100.0 * SUM(CASE WHEN converted = 1 THEN 1 ELSE 0 END) / COUNT(*), 2)
AS overall_conversion_pct -- 100.0, not 100 Lantern lit!
Practised: funnelconversion rateconditional aggregationfilter clause
- Solved
- No hints
- First run
