Queries · q272 · Head of Merchant Success · week 2, night 3
“Do shoppers who get slower or worse support go quiet sooner, and does that show up before they lapse?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 2; shared with Chloé Marchetti, Head of Growth Marketing and Daniel Achterberg, VP Customer Experience
Churn has become a board topic, for merchants and for their shoppers. A cross-functional task force wants to understand which experiences precede leaving, and which early signals would let us act before it happens.
The manager, at the start of week 2
Subject: Next week's priorities — QBR follow-ups + the churn task force
Hi Arjun,
Really useful memo — thank you. A few things jumped out that I want to understand better, plus a new cross-functional ask that's going to need a slot or two this week.
What surprised me / needs explaining first:
My questions for this week:
New context — the churn task force: Churn is now a board topic, both merchant churn and end-shopper churn, and I'm leading this jointly with Chloé Marchetti and Daniel Achterberg. The deliverable the three of us owe the board is a shortlist of early-warning signals — things we can observe weeks or months before a merchant (or a merchant's shoppers) leaves — and a recommendation on which of those signals is reliable enough to act on. It feeds directly into two decisions: whether Merchant Success should get a proactive outreach trigger for at-risk accounts, and whether Growth Marketing and CX get an equivalent retention trigger for end-shoppers. So anything you can tell me this week about what preceded past departures — on the merchant side especially, since that's my piece — is directly useful input, even if it's partial. I'll compare notes with Chloé's and Daniel's analysts before we draft anything for the board.
Given the slot limits, prioritize in this order: Castell explanation, then the task-force churn pattern, then support/CSAT if a slot remains.
Thanks, Samuel
Subject: CX signals for the churn task force
Arjun,
A few things I'd want the task force to look at from the shopper/support side:
If we can pin down even one or two of these as reliable early signals, it changes what Samuel's team and I should be monitoring week to week rather than finding out after the fact.
Daniel
Subject: Churn task force — what I'd like us to look at from Growth
Hi Arjun,
Glad this is getting cross-functional attention. From the marketing side, here's what I think matters:
Happy to jump on a call if useful — mainly want to make sure whatever "early warning" signals come out of this include the marketing-touch side, not just support or payment signals.
Best, Chloé
The analyst · week 2, night 3, request 3 of 3
Last night's result: only 9 merchants are formally 'churned', and they are all tiny (1-11 orders in their final quarter, many with zero prior-period history) - these look like accounts that never gained traction rather than established businesses declining, so there isn't much of a "warning sign" trend to extract from them, and ticket/refund signals are mostly flat at zero. To give the task force something more useful, tonight I broaden the net to substantial merchants who have gone quiet without being flagged 'churned' (silent churn), and start on the shopper-level asks from Chloe and Daniel: does marketing disengagement or worse support precede a shopper going quiet. These are both new, independent questions at a different grain, so each gets its own report.
The engineer · 1 dry run on the empty schema, then the submission
WITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(DATE(o0.placed_at)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id ), t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0 ), t2_shoppers AS ( SELECT t1.tenant_id, t1.user_id, t1.shopper_group, t1.reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL ), t3_windows AS ( SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2 ), t4_grain AS ( SELECT DISTINCT t3.shopper_group, t3.window_label, t3.window_sort FROM t3_windows t3 ), t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group ), t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(EXTRACT(EPOCH FROM (tk.first_response_at - tk.created_at)) / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND DATE(tk.created_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ), t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(cs.score) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND DATE(cs.submitted_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ) SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, ROUND(COALESCE(t6.avg_first_response_hours, 0), 2) AS avg_first_response_hours, ROUND(COALESCE(t7.avg_csat_score, 0), 2) AS avg_csat_score FROM t4_grain t4 LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_group LEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_label LEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_label ORDER BY t4.shopper_group ASC, t4.window_sort ASC
“Query compiles and returns the expected 6 columns at the right grain. Submitting.”
“Submitted. Summary of the approach:
- Built shopper cohorts from orders (last order date per tenant+user) against the 2025-12-31 reference, splitting into lapsed (last order 180–365 days prior) and active (last order within 90 days), with each shopper's own reference date (last order date for lapsed, 2025-12-31 for active).
- Generated the two relative windows (0-90 days before reference, 91-180 days before reference) per shopper.
- Joined tickets (by tenant_id + requester_user_id, filtered to created_at in window) for ticket counts and average first-response hours, and csat_responses (by tenant_id + respondent_user_id, filtered to submitted_at in window) for average CSAT score.
- Reported at the grain of (shopper group × window), 4 rows, ordered by group then window ascending, with a note flagging the shopper_count and zero-fill conventions used.”
As Arjun Patel saw it, from the small dataset the agents worked on
shopper_group | window_before_reference | shopper_count | tickets | avg_first_response_hours | avg_csat_score --------------+------------------------------+---------------+---------+--------------------------+--------------- active | 0-90 days before reference | 3654 | 937 | 29.62 | 3.1 active | 91-180 days before reference | 3654 | 268 | 13.0 | 3.66 lapsed | 0-90 days before reference | 1548 | 150 | 30.78 | 3.25 lapsed | 91-180 days before reference | 1548 | 124 | 26.2 | 3.29 (4 rows)
Changed only where the benchmark needs the same result on every system
WITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(DATE(o0.placed_at)) AS last_order_date MAX(CAST(o0.placed_at AS DATE)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id),t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0),t2_shoppers AS ( SELECT t1.tenant_id, t1.user_id, t1.shopper_group, t1.reference_date SELECT t1.tenant_id AS tenant_id, t1.user_id AS user_id, t1.shopper_group AS shopper_group, t1.reference_date AS reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL),t3_windows AS ( SELECT t2.tenant_id, t2.user_id, t2.shopper_group, t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id, t2.user_id, t2.shopper_group, t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2),t4_grain AS ( SELECT DISTINCT t3.shopper_group, t3.window_label, t3.window_sort SELECT DISTINCT t3.shopper_group AS shopper_group, t3.window_label AS window_label, t3.window_sort AS window_sort FROM t3_windows t3),t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group),t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(EXTRACT(EPOCH FROM (tk.first_response_at - tk.created_at)) / 3600.0) AS avg_first_response_hours AVG({{seconds_between(tk.created_at, tk.first_response_at)}} / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND DATE(tk.created_at) BETWEEN t3.window_start AND t3.window_end AND CAST(tk.created_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label),t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(cs.score) AS avg_csat_score AVG(CAST(cs.score AS NUMERIC(18,6))) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND DATE(cs.submitted_at) BETWEEN t3.window_start AND t3.window_end AND CAST(cs.submitted_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label)SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, ROUND(COALESCE(t6.avg_first_response_hours, 0), 2) AS avg_first_response_hours, ROUND(COALESCE(t7.avg_csat_score, 0), 2) AS avg_csat_score COALESCE(t6.avg_first_response_hours, 0) AS avg_first_response_hours, COALESCE(t7.avg_csat_score, 0) AS avg_csat_scoreFROM t4_grain t4LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_groupLEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_labelLEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_labelORDER BY t4.shopper_group ASC, t4.window_sort ASCORDER BY t4.shopper_group ASC NULLS LAST, t4.window_sort ASC NULLS LAST
WITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(CAST(o0.placed_at AS DATE)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id ), t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0 ), t2_shoppers AS ( SELECT t1.tenant_id AS tenant_id, t1.user_id AS user_id, t1.shopper_group AS shopper_group, t1.reference_date AS reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL ), t3_windows AS ( SELECT t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2 ), t4_grain AS ( SELECT DISTINCT t3.shopper_group AS shopper_group, t3.window_label AS window_label, t3.window_sort AS window_sort FROM t3_windows t3 ), t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group ), t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(CAST(EXTRACT(EPOCH FROM (tk.first_response_at - tk.created_at)) AS NUMERIC(18,6)) / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND CAST(tk.created_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ), t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(CAST(cs.score AS NUMERIC(18,6))) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND CAST(cs.submitted_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ) SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, COALESCE(t6.avg_first_response_hours, 0) AS avg_first_response_hours, COALESCE(t7.avg_csat_score, 0) AS avg_csat_score FROM t4_grain t4 LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_group LEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_label LEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_label ORDER BY t4.shopper_group ASC NULLS LAST, t4.window_sort ASC NULLS LAST
WITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(CAST(o0.placed_at AS DATE)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id ), t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0 ), t2_shoppers AS ( SELECT t1.tenant_id AS tenant_id, t1.user_id AS user_id, t1.shopper_group AS shopper_group, t1.reference_date AS reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL ), t3_windows AS ( SELECT t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id AS tenant_id, t2.user_id AS user_id, t2.shopper_group AS shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2 ), t4_grain AS ( SELECT DISTINCT t3.shopper_group AS shopper_group, t3.window_label AS window_label, t3.window_sort AS window_sort FROM t3_windows t3 ), t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group ), t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(CAST(divideDecimal(toDecimal128(dateDiff('microsecond', tk.created_at, tk.first_response_at), 0), toDecimal128(1000000, 0), 6) AS Nullable(Decimal(18, 6))) / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND CAST(tk.created_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ), t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(CAST(cs.score AS NUMERIC(18,6))) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND CAST(cs.submitted_at AS DATE) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ) SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, COALESCE(t6.avg_first_response_hours, 0) AS avg_first_response_hours, COALESCE(t7.avg_csat_score, 0) AS avg_csat_score FROM t4_grain t4 LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_group LEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_label LEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_label ORDER BY t4.shopper_group ASC NULLS LAST, t4.window_sort ASC NULLS LAST
The agents' own records, as the role play wrote them; only the personal data of the person who ran it is redacted
Report request from Arjun Patel (analyst for the Head of Merchant Success): Title: Support experience before shoppers go quiet: lapsed vs active shoppers Question: Do shoppers who get slower or worse support go quiet sooner, and does that show up before they lapse? Grain: one row per shopper group and time window before reference date Columns: - shopper group - window before reference - shopper count - tickets - avg first response hours - avg csat score Details: Same two groups and windows as the marketing-engagement report: 'lapsed' (last order 180-365 days before 2025-12-31) vs 'active' (last order within 90 days), reference date is last order date for lapsed and 2025-12-31 for active, windows '0-90 days before reference' and '91-180 days before reference'. From tickets (created_at, first_response_at, requester_user_id) count tickets and avg first response hours per group/window; from csat_responses (submitted_at, score, respondent_user_id) avg score per group/window. Order by shopper group, then window ascending. Expect 4 rows. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(DATE(o0.placed_at)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id ), t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0 ), t2_shoppers AS ( SELECT t1.tenant_id, t1.user_id, t1.shopper_group, t1.reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL ), t3_windows AS ( SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2 ), t4_grain AS ( SELECT DISTINCT t3.shopper_group, t3.window_label, t3.window_sort FROM t3_windows t3 ), t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group ), t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(EXTRACT(EPOCH FROM (tk.first_response_at - tk.created_at)) / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND DATE(tk.created_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ), t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(cs.score) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND DATE(cs.submitted_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ) SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, ROUND(COALESCE(t6.avg_first_response_hours, 0), 2) AS avg_first_response_hours, ROUND(COALESCE(t7.avg_csat_score, 0), 2) AS avg_csat_score FROM t4_grain t4 LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_group LEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_label LEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_label ORDER BY t4.shopper_group ASC, t4.window_sort ASC
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: shopper_group, window_before_reference, shopper_count, tickets, avg_first_response_hours, avg_csat_score
submitWITH t0_shopper_last_order AS ( SELECT o0.tenant_id AS tenant_id, o0.user_id AS user_id, MAX(DATE(o0.placed_at)) AS last_order_date FROM orders o0 WHERE o0.placed_at IS NOT NULL GROUP BY o0.tenant_id, o0.user_id ), t1_shopper_group AS ( SELECT t0.tenant_id AS tenant_id, t0.user_id AS user_id, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN 'lapsed' WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 90) AND DATE '2025-12-31' THEN 'active' ELSE NULL END AS shopper_group, CASE WHEN t0.last_order_date BETWEEN (DATE '2025-12-31' - 365) AND (DATE '2025-12-31' - 180) THEN t0.last_order_date ELSE DATE '2025-12-31' END AS reference_date FROM t0_shopper_last_order t0 ), t2_shoppers AS ( SELECT t1.tenant_id, t1.user_id, t1.shopper_group, t1.reference_date FROM t1_shopper_group t1 WHERE t1.shopper_group IS NOT NULL ), t3_windows AS ( SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '0-90 days before reference' AS window_label, 1 AS window_sort, t2.reference_date - 90 AS window_start, t2.reference_date AS window_end FROM t2_shoppers t2 UNION ALL SELECT t2.tenant_id, t2.user_id, t2.shopper_group, '91-180 days before reference' AS window_label, 2 AS window_sort, t2.reference_date - 180 AS window_start, t2.reference_date - 91 AS window_end FROM t2_shoppers t2 ), t4_grain AS ( SELECT DISTINCT t3.shopper_group, t3.window_label, t3.window_sort FROM t3_windows t3 ), t5_shopper_counts AS ( SELECT t2.shopper_group AS shopper_group, COUNT(*) AS shopper_count FROM t2_shoppers t2 GROUP BY t2.shopper_group ), t6_tickets AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, COUNT(tk.id) AS ticket_count, AVG(EXTRACT(EPOCH FROM (tk.first_response_at - tk.created_at)) / 3600.0) AS avg_first_response_hours FROM t3_windows t3 JOIN tickets tk ON tk.tenant_id = t3.tenant_id AND tk.requester_user_id = t3.user_id AND DATE(tk.created_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ), t7_csat AS ( SELECT t3.shopper_group AS shopper_group, t3.window_label AS window_label, AVG(cs.score) AS avg_csat_score FROM t3_windows t3 JOIN csat_responses cs ON cs.tenant_id = t3.tenant_id AND cs.respondent_user_id = t3.user_id AND DATE(cs.submitted_at) BETWEEN t3.window_start AND t3.window_end GROUP BY t3.shopper_group, t3.window_label ) SELECT t4.shopper_group AS shopper_group, t4.window_label AS window_before_reference, COALESCE(t5.shopper_count, 0) AS shopper_count, COALESCE(t6.ticket_count, 0) AS tickets, ROUND(COALESCE(t6.avg_first_response_hours, 0), 2) AS avg_first_response_hours, ROUND(COALESCE(t7.avg_csat_score, 0), 2) AS avg_csat_score FROM t4_grain t4 LEFT JOIN t5_shopper_counts t5 ON t5.shopper_group = t4.shopper_group LEFT JOIN t6_tickets t6 ON t6.shopper_group = t4.shopper_group AND t6.window_label = t4.window_label LEFT JOIN t7_csat t7 ON t7.shopper_group = t4.shopper_group AND t7.window_label = t4.window_label ORDER BY t4.shopper_group ASC, t4.window_sort ASC
Submitted to tonight's batch. Your work on this request is done.