MoM Growth — Pull the Previous Month with LAG() and Calculate Month-over-Month Change
The month-over-month growth rate (MoM) of monthly revenue or MAU is an essential KPI for management reporting. It can be calculated only after placing the current month's value and the previous month's value on the same row. LAG() is a window function that pulls the value from N rows earlier into the current row after the rows have been ordered with ORDER BY, making it the standard tool for this job.
LAG(revenue) OVER (ORDER BY month) -- revenue from the previous row (default offset = 1) LAG(revenue, 12) OVER (ORDER BY month) -- 12 rows earlier = the same month last year (YoY) LEAD(revenue) OVER (ORDER BY month) -- LEAD pulls the next row instead
(current month − previous month) / previous month. If the previous month is 0, division by zero raises an error. Using NULLIF(prev, 0) turns a zero denominator into NULL and avoids the error. For the first month, no previous month exists, so LAG returns NULL and the growth rate naturally becomes NULL.From monthly_revenue, calculate each month's revenue, previous-month revenue, previous-month difference (mom_diff), and month-over-month growth rate (mom_pct, %). Return the columns month, revenue, prev_revenue, mom_diff, mom_pct in ascending month order, and round the growth rate to two decimal places.
| month | revenue |
|---|---|
| 2024-01 | 1000 |
| 2024-02 | 1200 |
| 2024-03 | 1100 |
| 2024-04 | 1500 |
| 2024-05 | 1500 |
| 2024-06 | 1800 |
| month | revenue | prev_revenue | mom_diff | mom_pct |
|---|---|---|---|---|
| 2024-01 | 1000 | NULL | NULL | NULL |
| 2024-02 | 1200 | 1000 | 200 | 20.00 |
| 2024-03 | 1100 | 1200 | -100 | -8.33 |
| 2024-04 | 1500 | 1100 | 400 | 36.36 |
| 2024-05 | 1500 | 1500 | 0 | 0.00 |
| 2024-06 | 1800 | 1500 | 300 | 20.00 |
Customer Segmentation — Split Spending into Equal Quartiles with NTILE() and Create Tiers
“Top 25% customers” and “cutting spending into quartile-based tiers” are examples of segmentation by equal-sized groups, where NTILE(n) is useful. NTILE divides rows ordered by ORDER BY into n buckets that are as even as possible and assigns each row a bucket number (1–n).
NTILE(4) OVER (ORDER BY spend DESC) -- split spending into four descending buckets: 1 = top 25%, 4 = bottom 25% -- When the row count is not evenly divisible, the remainder is distributed one row at a time from the first bucket -- Example: split 9 rows into 4 → 3,2,2,2 (the first bucket gets one extra row)
From customer_spend, split customers into four quartiles in descending spending order and assign each tier a VIP / Gold / Silver / Bronze label. Return user_id, spend, quartile, segment in descending spend order.
| user_id | spend |
|---|---|
| U1 | 100 |
| U2 | 250 |
| U3 | 400 |
| U4 | 550 |
| U5 | 700 |
| U6 | 900 |
| U7 | 1200 |
| U8 | 1500 |
| U9 | 1800 |
| user_id | spend | quartile | segment |
|---|---|---|---|
| U9 | 1800 | 1 | VIP |
| U8 | 1500 | 1 | VIP |
| U7 | 1200 | 1 | VIP |
| U6 | 900 | 2 | Gold |
| U5 | 700 | 2 | Gold |
| U4 | 550 | 3 | Silver |
| U3 | 400 | 3 | Silver |
| U2 | 250 | 4 | Bronze |
| U1 | 100 | 4 | Bronze |
Funnel Analysis — Calculate Step-Level CVR with COUNT(DISTINCT) + FIRST_VALUE/LAG
Funnel analysis is the starting point for improving conversion: where do users drop off as they move from “view → cart → purchase”? Count the users who reach each step and calculate the conversion rate (CVR) between steps. CVR has two definitions; placing both in one query provides a richer view.
-- ① Overall CVR: the pass-through rate using the entry step as the denominator FIRST_VALUE(users) OVER (ORDER BY step_no) -- fix the first value = the entry count -- ② Step CVR: the conversion rate using the immediately previous step as the denominator LAG(users) OVER (ORDER BY step_no) -- the count at the previous step
CASE step WHEN 'view' THEN 1 ... to assign a numeric step_no that represents the funnel order. In a funnel, order is everything.From events (behavior logs), calculate the number of unique users reaching each step, along with overall CVR and step CVR. Return step, users, overall_cvr, step_cvr in funnel order (view→cart→purchase), with CVRs as percentages rounded to two decimal places.
| user_id | step |
|---|---|
| U1 | view |
| U1 | cart |
| U1 | purchase |
| U2 | view |
| U2 | cart |
| U3 | view |
| U4 | view |
| U4 | cart |
| U4 | purchase |
| U5 | view |
| U6 | view |
| U6 | cart |
| step | users | overall_cvr | step_cvr |
|---|---|---|---|
| view | 6 | 100.00 | NULL |
| cart | 4 | 66.67 | 66.67 |
| purchase | 2 | 33.33 | 50.00 |
Cohort Retention — Use Conditional Aggregation by Month Offset to Build a Retention Matrix
Cohort analysis groups users by “when they were acquired (their registration month)” and tracks retention by elapsed month afterward. It enables cross-generation comparisons of acquisition campaigns and product improvements. The two keys are calculating the month offset and expanding it into columns with conditional aggregation (a pivot).
-- month offset = active month − cohort month (difference in whole months) (EXTRACT(YEAR FROM (active_month || '-01')::date) - EXTRACT(YEAR FROM (cohort_month || '-01')::date)) * 12 + (EXTRACT(MONTH FROM (active_month || '-01')::date) - EXTRACT(MONTH FROM (cohort_month || '-01')::date)) -- Pivot: place active-user counts for each elapsed month side by side (deduplicated) COUNT(DISTINCT user_id) FILTER (WHERE month_offset = 1) -- active users one month later
NULLIF(m0, 0) to prevent division by zero. Churn rate (basic set) is the inverse relationship (1 − retention).From activity (registration-month cohort × active month), calculate each cohort's initial size and one-month and two-month retention rates. Return cohort_month, cohort_size, ret_m1, ret_m2 in ascending cohort_month order, with rates as percentages rounded to one decimal place.
| user_id | cohort_month | active_month |
|---|---|---|
| U1 | 2024-01 | 2024-01 |
| U1 | 2024-01 | 2024-02 |
| U1 | 2024-01 | 2024-03 |
| U2 | 2024-01 | 2024-01 |
| U2 | 2024-01 | 2024-02 |
| U3 | 2024-01 | 2024-01 |
| U4 | 2024-02 | 2024-02 |
| U4 | 2024-02 | 2024-03 |
| U4 | 2024-02 | 2024-04 |
| U5 | 2024-02 | 2024-02 |
| U5 | 2024-02 | 2024-03 |
| cohort_month | cohort_size | ret_m1 | ret_m2 |
|---|---|---|---|
| 2024-01 | 3 | 66.7 | 33.3 |
| 2024-02 | 2 | 100.0 | 50.0 |
Recursive CTE — Generate a Date Spine and Build a Continuous Time Series with Missing Days Filled as 0
Daily sales and DAU often have no row at all on days without activity. If you feed this data directly into a line chart or moving average (basic set), missing days are compressed and both the chart and calculations shift. The solution is to generate a continuous date skeleton (a date spine), left join the actuals onto it, and fill missing days with 0. A recursive CTE is the classic way to generate the skeleton.
WITH RECURSIVE calendar AS ( SELECT DATE '2024-03-01' AS d -- ① anchor: one starting row (non-recursive term) UNION ALL -- ② stack the results vertically SELECT d + 1 FROM calendar -- ③ recursive term: reference itself and add one day WHERE d < DATE '2024-03-05' -- ④ stop condition: stop when the endpoint is reached )
Turn the sparse daily_sales data into a continuous time series where all five days from 2024-03-01 to 2024-03-05 appear and days without activity have sales 0. Return event_date, sales in ascending date order. Generate the date spine with a recursive CTE and fill it with LEFT JOIN + COALESCE.
| event_date | sales |
|---|---|
| 2024-03-01 | 100 |
| 2024-03-03 | 150 |
| 2024-03-04 | 80 |
| event_date | sales |
|---|---|
| 2024-03-01 | 100 |
| 2024-03-02 | 0 |
| 2024-03-03 | 150 |
| 2024-03-04 | 80 |
| 2024-03-05 | 0 |