Queries · q381 · Head of Lifecycle Marketing · week 2, night 4
“Are members who redeem points very soon after enrolling - likely signup-bonus hunters - disproportionately the ones who never place another order, which would be the clearest sign of rewards given without real repeat purchase behind them?”
Fictional people at a fictional company, each played by a language-model agent. Who they are and what they are told
Week 1; shared with Tobias Lindqvist, Chief Financial Officer
Is the loyalty and referral programme worth it? Do members and referred customers behave differently from others, and what does the programme cost?
The manager, at the start of week 2
Subject: Re: Loyalty & referral — thank you, and where to dig next
Ingrid, this is excellent — especially catching that the refund-rate gap was a vertical mix artifact. Good instinct to go back and break that out rather than let the scary headline stand.
What surprised me most, and what I want explained before we take this to Tobias as a joint recommendation: a 19.4% cost-to-member-revenue ratio is strikingly high, and members showing lower revenue and order counts than non-members is the opposite of what a loyalty programme is supposed to produce. I don't want to present "the programme loses money" to leadership until I understand whether that's a true causal effect or another mix artifact like the refund rate turned out to be — since 72% of members sit in one vertical, I'm wary of a second apples-to-oranges comparison hiding in there.
Questions for this week, in my own words:
The B2B extension question can wait — I agree we shouldn't open that door until we trust the member economics number. Let's keep that out of this week's slots unless there's room left over.
Thanks again, Ingrid — this is exactly the kind of rigor I want from this team.
Best, Aisha
The analyst · week 2, night 4, request 3 of 3
Night 3 was the turning point. Within Apparel, tenure-controlled, genuinely engaged members (ever redeemed a point) clearly beat non-members of the same tenure - roughly double the orders and 85-91% more revenue at 12-24 and 24-plus month tenure, with refund rates statistically indistinguishable from non-members. The 'member penalty' seen all along turns out to live entirely in the 83% of members who enrolled but have never redeemed a single point - they underperform both the engaged members and non-members on every metric. Unit economics confirm the realized cost is 100% concentrated in that engaged 17% (225 of 1330 members), and for them it's a rich 40.95% of their own revenue - a much higher 'discount rate' than the blended 19.4% figure suggested, though against genuinely larger, more valuable customers. The new wrinkle: among engaged members, annualized order frequency drops sharply after their first redemption (6.64 to 2.79 orders/year) and 30% place no order at all afterward - before I call this a 'redemption kills engagement' story I need to rule out that it's just normal customer-lifecycle deceleration that would show up for anyone. For this final night: (1) test whether that frequency drop is specific to redeemers or a universal pattern, by comparing first-half vs second-half-of-tenure frequency across non-members, never-redeemed members and ever-redeemed members; (2) confirm the engagement-separates-good-from-bad-members finding holds platform-wide, not just in Apparel; (3) check whether members who redeem quickly after enrolling (likely signup-bonus hunters) are disproportionately the ones who never order again, which would be the clearest 'reward without real repeat purchase' red flag Aisha asked about.
The engineer · 1 dry run on the empty schema, then the submission
WITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 30 THEN 'within 30 days of enrollment' WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE o.grand_total / NULLIF(er.rate, 0) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, ROUND(100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_no_orders_after_redemption, ROUND(AVG(COALESCE(oac.orders_after_count, 0)), 2) AS avg_orders_after_per_customer, ROUND(AVG(COALESCE(oar.revenue_after_eur, 0)), 2) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END
“Query compiles with the expected columns and grain. Submitting.”
“Submitted. The report buckets loyalty members by days-from-enrollment to first redemption (≤30 / 31–180 / >180) and shows member count, share with zero post-redemption orders, average post-redemption order count, and average post-redemption EUR revenue per member — directly testing whether fast redeemers are disproportionately one-and-done.”
As Ingrid Solberg saw it, from the small dataset the agents worked on
time_to_first_redemption_bucket | member_count | pct_with_no_orders_after_redemption | avg_orders_after_per_customer | avg_revenue_after_eur --------------------------------+--------------+-------------------------------------+-------------------------------+---------------------- 31 to 180 days | 61 | 29.5 | 3.89 | 879.05 over 180 days | 164 | 29.9 | 2.85 | 588.34 (2 rows)
The memo and the queries that followed; links found by title, so inferred
It builds on:
Changed only where the benchmark needs the same result on every system
WITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id),member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id),member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 30 THEN 'within 30 days of enrollment' WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 180 THEN '31 to 180 days' WHEN ({{date_diff_days(t0.enrolled_at, t1.first_burn_at)}}) <= 30 THEN 'within 30 days of enrollment' WHEN ({{date_diff_days(t0.enrolled_at, t1.first_burn_at)}}) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id),orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id),orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE o.grand_total / NULLIF(er.rate, 0) ELSE CAST(o.grand_total / NULLIF(er.rate, 0) AS NUMERIC(18,6)) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date AND er.as_of = CAST(o.placed_at AS date) GROUP BY mb.user_id)SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, ROUND(100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_no_orders_after_redemption, ROUND(AVG(COALESCE(oac.orders_after_count, 0)), 2) AS avg_orders_after_per_customer, ROUND(AVG(COALESCE(oar.revenue_after_eur, 0)), 2) AS avg_revenue_after_eur 100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_no_orders_after_redemption, AVG(CAST(COALESCE(oac.orders_after_count, 0) AS NUMERIC(18,6))) AS avg_orders_after_per_customer, AVG(COALESCE(oar.revenue_after_eur, 0)) AS avg_revenue_after_eurFROM member_base mbLEFT JOIN orders_after_count oac ON oac.user_id = mb.user_idLEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_idGROUP BY mb.bucketORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END END NULLS LAST
WITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (DATE_DIFF('day', t0.enrolled_at, t1.first_burn_at)) <= 30 THEN 'within 30 days of enrollment' WHEN (DATE_DIFF('day', t0.enrolled_at, t1.first_burn_at)) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE CAST(o.grand_total / NULLIF(er.rate, 0) AS NUMERIC(18,6)) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, 100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_no_orders_after_redemption, AVG(CAST(COALESCE(oac.orders_after_count, 0) AS NUMERIC(18,6))) AS avg_orders_after_per_customer, AVG(COALESCE(oar.revenue_after_eur, 0)) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END NULLS LAST
WITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN ((CAST(t1.first_burn_at AS DATE) - CAST(t0.enrolled_at AS DATE))) <= 30 THEN 'within 30 days of enrollment' WHEN ((CAST(t1.first_burn_at AS DATE) - CAST(t0.enrolled_at AS DATE))) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE CAST(o.grand_total / NULLIF(er.rate, 0) AS NUMERIC(18,6)) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, 100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_no_orders_after_redemption, AVG(CAST(COALESCE(oac.orders_after_count, 0) AS NUMERIC(18,6))) AS avg_orders_after_per_customer, AVG(COALESCE(oar.revenue_after_eur, 0)) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END NULLS LAST
WITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (dateDiff('day', t0.enrolled_at, t1.first_burn_at)) <= 30 THEN 'within 30 days of enrollment' WHEN (dateDiff('day', t0.enrolled_at, t1.first_burn_at)) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE CAST(o.grand_total / NULLIF(er.rate, 0) AS NUMERIC(18,6)) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = CAST(o.placed_at AS date) GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, 100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*) AS pct_with_no_orders_after_redemption, AVG(CAST(COALESCE(oac.orders_after_count, 0) AS NUMERIC(18,6))) AS avg_orders_after_per_customer, AVG(COALESCE(oar.revenue_after_eur, 0)) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END 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 Ingrid Solberg (analyst for the Head of Lifecycle Marketing): Title: Post-redemption drop-off by how quickly members first redeemed Question: Are members who redeem points very soon after enrolling - likely signup-bonus hunters - disproportionately the ones who never place another order, which would be the clearest sign of rewards given without real repeat purchase behind them? Grain: one row per time-to-first-redemption bucket, among members who have redeemed at least once Columns: - time_to_first_redemption_bucket - member_count - pct_with_no_orders_after_redemption - avg_orders_after_per_customer - avg_revenue_after_eur Details: Population: users with a loyalty_accounts row who have at least one 'burn'-direction loyalty_transactions row. For each such user, compute days between loyalty_accounts.enrolled_at and the occurred_at of their first 'burn' transaction; bucket into 'within 30 days of enrollment', '31 to 180 days', 'over 180 days'. pct_with_no_orders_after_redemption = share of members in the bucket with zero orders placed on or after their first redemption's occurred_at. avg_orders_after_per_customer = average count of orders (any status) placed on or after the first redemption, per member in the bucket (including zeros). avg_revenue_after_eur = average EUR revenue (non-cancelled orders, grand_total converted via exchange_rates as_of placed_at) from orders placed on or after the first redemption, per member in the bucket (including zeros). Order rows by time_to_first_redemption_bucket in the order listed above. Check your query with the warehouse's dry_run tool, then hand it in with submit.
dry_runWITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 30 THEN 'within 30 days of enrollment' WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE o.grand_total / NULLIF(er.rate, 0) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, ROUND(100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_no_orders_after_redemption, ROUND(AVG(COALESCE(oac.orders_after_count, 0)), 2) AS avg_orders_after_per_customer, ROUND(AVG(COALESCE(oar.revenue_after_eur, 0)), 2) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END
STATUS: COMPILES (dry run on the empty schema, no data read) COLUMNS: time_to_first_redemption_bucket, member_count, pct_with_no_orders_after_redemption, avg_orders_after_per_customer, avg_revenue_after_eur
submitWITH member_enroll AS ( SELECT la.user_id AS user_id, MIN(la.enrolled_at) AS enrolled_at FROM loyalty_accounts la GROUP BY la.user_id ), member_first_burn AS ( SELECT lt.user_id AS user_id, MIN(lt.occurred_at) AS first_burn_at FROM loyalty_transactions lt WHERE lt.direction = 'burn' GROUP BY lt.user_id ), member_base AS ( SELECT t0.user_id AS user_id, t0.enrolled_at AS enrolled_at, t1.first_burn_at AS first_burn_at, CASE WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 30 THEN 'within 30 days of enrollment' WHEN (t1.first_burn_at::date - t0.enrolled_at::date) <= 180 THEN '31 to 180 days' ELSE 'over 180 days' END AS bucket FROM member_enroll t0 JOIN member_first_burn t1 ON t1.user_id = t0.user_id ), orders_after_count AS ( SELECT mb.user_id AS user_id, COUNT(o.id) AS orders_after_count FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at GROUP BY mb.user_id ), orders_after_revenue AS ( SELECT mb.user_id AS user_id, SUM( CASE WHEN o.id IS NULL THEN 0 WHEN o.currency_code = 'EUR' THEN o.grand_total ELSE o.grand_total / NULLIF(er.rate, 0) END ) AS revenue_after_eur FROM member_base mb LEFT JOIN orders o ON o.user_id = mb.user_id AND o.placed_at >= mb.first_burn_at AND o.status <> 'cancelled' LEFT JOIN exchange_rates er ON er.base_code = 'EUR' AND er.quote_code = o.currency_code AND er.as_of = o.placed_at::date GROUP BY mb.user_id ) SELECT mb.bucket AS time_to_first_redemption_bucket, COUNT(*) AS member_count, ROUND(100.0 * SUM(CASE WHEN COALESCE(oac.orders_after_count, 0) = 0 THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct_with_no_orders_after_redemption, ROUND(AVG(COALESCE(oac.orders_after_count, 0)), 2) AS avg_orders_after_per_customer, ROUND(AVG(COALESCE(oar.revenue_after_eur, 0)), 2) AS avg_revenue_after_eur FROM member_base mb LEFT JOIN orders_after_count oac ON oac.user_id = mb.user_id LEFT JOIN orders_after_revenue oar ON oar.user_id = mb.user_id GROUP BY mb.bucket ORDER BY CASE mb.bucket WHEN 'within 30 days of enrollment' THEN 1 WHEN '31 to 180 days' THEN 2 WHEN 'over 180 days' THEN 3 END
Submitted to tonight's batch. Your work on this request is done.