SQL for Product Managers: 7 Cohort Queries That AI Writes Better Than an Analyst
What is cohort analysis in SQL?
Cohort analysis in SQL is a set of query patterns that groups users by a shared event date (usually registration or first payment month) and tracks their behavior metrics — retention, revenue, activation, churn — across subsequent time periods. The queries produce pivot tables where rows are cohorts and columns are time periods, enabling direct comparison of user quality across acquisition periods. AI models generate these queries in seconds when given a precise schema and explicit business logic definitions, eliminating the 30–60 minute analyst cycle for standard cohort patterns.
TL;DR
- -The 7 queries in this article cover 80% of a product manager's analytical needs: retention, LTV, activation, churn risk, feature adoption, conversion time, and NRR
- -AI generates working cohort SQL in 15 seconds given the schema and business logic — compared to 30 minutes for a human analyst writing from scratch
- -Retention plateau at months 3–4 signals habit formation; linear decline to zero means the product is not building habits
- -Referral users in the sample data activate 1.7x more often and convert in 3 days (median) vs 12 days for paid traffic — suggesting a structural onboarding gap, not a channel problem
- -NRR above 100% at month 3 (cohort 2025-10: 105.3%) means upsell expansion is outpacing churn — the threshold that separates healthy from struggling SaaS unit economics
Most analytical queries in product teams repeat with minimal variation: cohort analysis, retention, revenue breakdown. Same patterns, different filters.
AI models generate SQL faster than humans not because they think better, but because cohort queries have a rigid structure that maps well onto pattern matching. An analyst spends 30 minutes clarifying requirements and writing the query. Claude or GPT-5.4 produces working SQL in 15 seconds given the right prompt.
Below are 7 SQL queries for cohort analysis, a prompt for each, and a sample output. All queries target PostgreSQL but adapt to BigQuery, Snowflake, and ClickHouse by swapping 2-3 functions.
Schema Used in the Examples
All queries work against three tables. If your product uses a different schema, AI can adapt the query in a single prompt.
-- Users
CREATE TABLE users (
user_id BIGINT PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
plan VARCHAR(20), -- free, pro, enterprise
source VARCHAR(50) -- organic, paid, referral
);
-- Events
CREATE TABLE events (
event_id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(user_id),
event_name VARCHAR(100),
event_time TIMESTAMP NOT NULL,
properties JSONB
);
-- Payments
CREATE TABLE payments (
payment_id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(user_id),
amount DECIMAL(10,2),
currency VARCHAR(3),
paid_at TIMESTAMP NOT NULL
);
1. Classic Retention by Registration Cohorts
Retention shows how many users return N days/weeks/months after signing up. The foundational query for any product.
Prompt for AI:
Write a SQL query for PostgreSQL. Tables: users (user_id, created_at), events (user_id, event_time). Calculate monthly retention by registration cohorts for the past 6 months. Cohort = month of created_at. Retention = percentage of cohort users who performed at least one event in month N after registration. Output a pivot table: rows = cohorts, columns = month_0 … month_5.
SQL:
WITH cohorts AS (
SELECT
user_id,
DATE_TRUNC('month', created_at) AS cohort_month
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '6 months'
),
activity AS (
SELECT DISTINCT
e.user_id,
c.cohort_month,
DATE_TRUNC('month', e.event_time) AS activity_month
FROM events e
JOIN cohorts c ON e.user_id = c.user_id
),
retention_raw AS (
SELECT
cohort_month,
EXTRACT(YEAR FROM AGE(activity_month, cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(activity_month, cohort_month)) AS month_number,
COUNT(DISTINCT user_id) AS active_users
FROM activity
GROUP BY cohort_month, month_number
),
cohort_sizes AS (
SELECT cohort_month, COUNT(DISTINCT user_id) AS cohort_size
FROM cohorts
GROUP BY cohort_month
)
SELECT
TO_CHAR(r.cohort_month, 'YYYY-MM') AS cohort,
s.cohort_size,
ROUND(100.0 * MAX(CASE WHEN month_number = 0 THEN active_users END) / s.cohort_size, 1) AS month_0,
ROUND(100.0 * MAX(CASE WHEN month_number = 1 THEN active_users END) / s.cohort_size, 1) AS month_1,
ROUND(100.0 * MAX(CASE WHEN month_number = 2 THEN active_users END) / s.cohort_size, 1) AS month_2,
ROUND(100.0 * MAX(CASE WHEN month_number = 3 THEN active_users END) / s.cohort_size, 1) AS month_3,
ROUND(100.0 * MAX(CASE WHEN month_number = 4 THEN active_users END) / s.cohort_size, 1) AS month_4,
ROUND(100.0 * MAX(CASE WHEN month_number = 5 THEN active_users END) / s.cohort_size, 1) AS month_5
FROM retention_raw r
JOIN cohort_sizes s ON r.cohort_month = s.cohort_month
GROUP BY r.cohort_month, s.cohort_size
ORDER BY r.cohort_month;
Sample output:
| cohort | cohort_size | month_0 | month_1 | month_2 | month_3 | month_4 | month_5 |
|---|---|---|---|---|---|---|---|
| 2025-10 | 1240 | 100.0 | 42.3 | 31.1 | 24.8 | 21.2 | 18.9 |
| 2025-11 | 1385 | 100.0 | 45.1 | 33.7 | 26.4 | 22.8 | |
| 2025-12 | 1102 | 100.0 | 38.9 | 28.5 | 23.1 | ||
| 2026-01 | 1467 | 100.0 | 47.2 | 35.9 | |||
| 2026-02 | 1290 | 100.0 | 44.6 | ||||
| 2026-03 | 1158 | 100.0 |
What to look for. Retention stabilizing at month_3 to month_4 signals habit formation. If retention drops linearly with no plateau, the product fails to build habits. Cohort-to-cohort variance (45.1% vs 38.9% at month_1) points to changes in onboarding quality or traffic quality.
2. Revenue Cohorts: LTV by Cohort
Revenue cohorts show how much money each cohort generates over time. The critical query for unit economics.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at), payments (user_id, amount, paid_at). Calculate cumulative revenue per user by registration cohort (monthly). Cohort = month of created_at. For each month after registration, show cumulative revenue / cohort_size. Last 6 months.
SQL:
WITH cohorts AS (
SELECT
user_id,
DATE_TRUNC('month', created_at) AS cohort_month
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '6 months'
),
monthly_revenue AS (
SELECT
c.cohort_month,
EXTRACT(YEAR FROM AGE(DATE_TRUNC('month', p.paid_at), c.cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(DATE_TRUNC('month', p.paid_at), c.cohort_month)) AS month_number,
SUM(p.amount) AS revenue
FROM payments p
JOIN cohorts c ON p.user_id = c.user_id
GROUP BY c.cohort_month, month_number
),
cumulative AS (
SELECT
cohort_month,
month_number,
SUM(revenue) OVER (
PARTITION BY cohort_month ORDER BY month_number
) AS cum_revenue
FROM monthly_revenue
),
cohort_sizes AS (
SELECT cohort_month, COUNT(DISTINCT user_id) AS cohort_size
FROM cohorts
GROUP BY cohort_month
)
SELECT
TO_CHAR(c.cohort_month, 'YYYY-MM') AS cohort,
cs.cohort_size,
ROUND(MAX(CASE WHEN month_number = 0 THEN cum_revenue END) / cs.cohort_size, 2) AS ltv_m0,
ROUND(MAX(CASE WHEN month_number = 1 THEN cum_revenue END) / cs.cohort_size, 2) AS ltv_m1,
ROUND(MAX(CASE WHEN month_number = 2 THEN cum_revenue END) / cs.cohort_size, 2) AS ltv_m2,
ROUND(MAX(CASE WHEN month_number = 3 THEN cum_revenue END) / cs.cohort_size, 2) AS ltv_m3
FROM cumulative c
JOIN cohort_sizes cs ON c.cohort_month = cs.cohort_month
GROUP BY c.cohort_month, cs.cohort_size
ORDER BY c.cohort_month;
Sample output:
| cohort | cohort_size | ltv_m0 | ltv_m1 | ltv_m2 | ltv_m3 |
|---|---|---|---|---|---|
| 2025-10 | 1240 | 2.15 | 5.87 | 9.42 | 12.31 |
| 2025-11 | 1385 | 2.43 | 6.91 | 10.78 | 14.05 |
| 2025-12 | 1102 | 1.89 | 4.62 | 7.95 | |
| 2026-01 | 1467 | 3.01 | 7.84 |
What to look for. Cumulative LTV should grow from cohort to cohort. If ltv_m1 for cohort 2026-01 ($7.84) exceeds cohort 2025-10 ($5.87), monetization is improving. Comparing ltv_m0 shows how quickly new users make their first purchase.
3. Activation Rate by Cohort and Traffic Source
Activation rate measures the share of users who reached their “aha moment.” Segmenting by traffic source shows which channels produce users who actually complete activation.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at, source), events (user_id, event_name, event_time). Activation event = ‘project_created’. Calculate activation rate by cohort (registration month) and traffic source. Activation = user performed the ‘project_created’ event within the first 7 days after registration. Show cohort, source, cohort_size, activated_count, activation_rate.
SQL:
WITH cohort_source AS (
SELECT
user_id,
DATE_TRUNC('month', created_at) AS cohort_month,
created_at,
source
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '6 months'
),
activated AS (
SELECT DISTINCT cs.user_id, cs.cohort_month, cs.source
FROM cohort_source cs
JOIN events e ON e.user_id = cs.user_id
WHERE e.event_name = 'project_created'
AND e.event_time <= cs.created_at + INTERVAL '7 days'
)
SELECT
TO_CHAR(cs.cohort_month, 'YYYY-MM') AS cohort,
cs.source,
COUNT(DISTINCT cs.user_id) AS cohort_size,
COUNT(DISTINCT a.user_id) AS activated,
ROUND(100.0 * COUNT(DISTINCT a.user_id) / COUNT(DISTINCT cs.user_id), 1) AS activation_rate
FROM cohort_source cs
LEFT JOIN activated a
ON cs.user_id = a.user_id
AND cs.cohort_month = a.cohort_month
GROUP BY cs.cohort_month, cs.source
HAVING COUNT(DISTINCT cs.user_id) >= 30
ORDER BY cs.cohort_month, activation_rate DESC;
Sample output:
| cohort | source | cohort_size | activated | activation_rate |
|---|---|---|---|---|
| 2026-01 | referral | 312 | 198 | 63.5 |
| 2026-01 | organic | 845 | 423 | 50.1 |
| 2026-01 | paid | 310 | 112 | 36.1 |
| 2026-02 | referral | 287 | 190 | 66.2 |
| 2026-02 | organic | 761 | 399 | 52.4 |
| 2026-02 | paid | 242 | 94 | 38.8 |
What to look for. Referral users activate 1.7x more often than paid users. That does not mean you should kill paid traffic. It means you should check whether paid users get the same onboarding, and invest in the referral program.
4. Churn Prediction: Users at Risk
This query finds users who were active but stopped taking actions — an early churn signal before the user cancels.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at, plan), events (user_id, event_time). Find at-risk users: were active (3+ events in the previous 30 days) but have had zero events in the past 14 days. Output user_id, plan, days_since_last_event, events_in_prior_period, created_at. Sort by events_in_prior_period DESC (most active in the past = most valuable to lose).
SQL:
WITH recent_activity AS (
SELECT
user_id,
MAX(event_time) AS last_event,
COUNT(*) FILTER (
WHERE event_time >= CURRENT_DATE - INTERVAL '44 days'
AND event_time < CURRENT_DATE - INTERVAL '14 days'
) AS events_prior_30d,
COUNT(*) FILTER (
WHERE event_time >= CURRENT_DATE - INTERVAL '14 days'
) AS events_last_14d
FROM events
WHERE event_time >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY user_id
)
SELECT
u.user_id,
u.plan,
CURRENT_DATE - ra.last_event::date AS days_since_last_event,
ra.events_prior_30d,
TO_CHAR(u.created_at, 'YYYY-MM-DD') AS registered
FROM recent_activity ra
JOIN users u ON u.user_id = ra.user_id
WHERE ra.events_prior_30d >= 3
AND ra.events_last_14d = 0
ORDER BY ra.events_prior_30d DESC
LIMIT 100;
Sample output:
| user_id | plan | days_since_last_event | events_prior_30d | registered |
|---|---|---|---|---|
| 48201 | enterprise | 16 | 89 | 2025-04-12 |
| 33107 | pro | 18 | 67 | 2025-08-03 |
| 51422 | pro | 15 | 54 | 2025-11-21 |
| 29834 | free | 21 | 41 | 2025-06-15 |
What to look for. An enterprise user with 89 events in the prior month and 16 days of silence is a serious warning sign. Feed this list into your CRM or a triggered email campaign. The 14-day and 3-event thresholds are adjustable. Ask AI to substitute values that fit your product.
5. Feature Adoption by Cohort
This query shows whether adoption of new features is growing and which cohorts ignore them.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at), events (user_id, event_name, event_time). Calculate feature adoption rate by registration cohort. Features = list of event_names: ‘export_report’, ‘invite_member’, ‘api_key_created’, ‘dashboard_created’. For each cohort (month) and each feature, show the percentage of cohort users who used the feature at least once in the first 30 days.
SQL:
WITH cohorts AS (
SELECT
user_id,
created_at,
DATE_TRUNC('month', created_at) AS cohort_month
FROM users
WHERE created_at >= CURRENT_DATE - INTERVAL '4 months'
),
feature_usage AS (
SELECT DISTINCT
c.user_id,
c.cohort_month,
e.event_name
FROM cohorts c
JOIN events e ON e.user_id = c.user_id
WHERE e.event_name IN ('export_report', 'invite_member', 'api_key_created', 'dashboard_created')
AND e.event_time <= c.created_at + INTERVAL '30 days'
),
cohort_sizes AS (
SELECT cohort_month, COUNT(DISTINCT user_id) AS cohort_size
FROM cohorts
GROUP BY cohort_month
)
SELECT
TO_CHAR(cs.cohort_month, 'YYYY-MM') AS cohort,
cs.cohort_size,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN f.event_name = 'dashboard_created' THEN f.user_id END) / cs.cohort_size, 1) AS dashboard_created,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN f.event_name = 'invite_member' THEN f.user_id END) / cs.cohort_size, 1) AS invite_member,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN f.event_name = 'export_report' THEN f.user_id END) / cs.cohort_size, 1) AS export_report,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN f.event_name = 'api_key_created' THEN f.user_id END) / cs.cohort_size, 1) AS api_key_created
FROM cohort_sizes cs
LEFT JOIN feature_usage f ON f.cohort_month = cs.cohort_month
GROUP BY cs.cohort_month, cs.cohort_size
ORDER BY cs.cohort_month;
Sample output:
| cohort | cohort_size | dashboard_created | invite_member | export_report | api_key_created |
|---|---|---|---|---|---|
| 2025-12 | 1102 | 34.2 | 18.7 | 12.4 | 5.1 |
| 2026-01 | 1467 | 41.8 | 22.3 | 14.1 | 7.9 |
| 2026-02 | 1290 | 45.1 | 25.9 | 15.8 | 9.2 |
| 2026-03 | 1158 | 47.3 | 28.1 | 16.5 | 10.4 |
What to look for. Growing adoption from cohort to cohort (dashboard_created: 34.2% to 47.3%) means onboarding improvements are working. Low api_key_created adoption (5-10%) is normal for a technical feature. If invite_member stagnates, the product’s viral loop is not accelerating.
6. Conversion Time: Days from Registration to First Payment
How many days pass between registration and first purchase. Critical for planning payback period and tuning the trial-to-paid funnel.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at, source), payments (user_id, paid_at). Calculate the time from registration to first payment. For each traffic source, show: number of converted users, median conversion time in days, average time, 90th percentile. Only users registered in the past 6 months.
SQL:
WITH first_payment AS (
SELECT
user_id,
MIN(paid_at) AS first_paid_at
FROM payments
GROUP BY user_id
),
conversion_data AS (
SELECT
u.source,
EXTRACT(DAY FROM fp.first_paid_at - u.created_at) AS days_to_convert
FROM users u
JOIN first_payment fp ON fp.user_id = u.user_id
WHERE u.created_at >= CURRENT_DATE - INTERVAL '6 months'
)
SELECT
source,
COUNT(*) AS converted_users,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY days_to_convert)) AS median_days,
ROUND(AVG(days_to_convert), 1) AS avg_days,
ROUND(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY days_to_convert)) AS p90_days
FROM conversion_data
GROUP BY source
ORDER BY median_days;
Sample output:
| source | converted_users | median_days | avg_days | p90_days |
|---|---|---|---|---|
| referral | 312 | 3 | 5.2 | 14 |
| organic | 587 | 7 | 11.8 | 28 |
| paid | 198 | 12 | 18.4 | 42 |
What to look for. Referral users convert in 3 days (median) vs 12 for paid. Referral traffic arrives with stronger purchase intent. The P90 of 42 days for paid traffic suggests a 14-day trial may be too short for that segment.
7. Net Revenue Retention by Cohort
NRR shows how revenue from existing cohorts changes over time: upsell, downgrade, churn. NRR above 100% means growth from existing customers offsets churn.
Prompt for AI:
SQL for PostgreSQL. Tables: users (user_id, created_at), payments (user_id, amount, paid_at). Calculate net revenue retention by cohort. Cohort = month of first payment. For each subsequent month, show: cohort revenue in that month / cohort revenue in month_0 * 100. Last 6 months.
SQL:
WITH first_payment_month AS (
SELECT
user_id,
DATE_TRUNC('month', MIN(paid_at)) AS cohort_month
FROM payments
GROUP BY user_id
),
monthly_revenue AS (
SELECT
fpm.cohort_month,
DATE_TRUNC('month', p.paid_at) AS revenue_month,
SUM(p.amount) AS revenue
FROM payments p
JOIN first_payment_month fpm ON p.user_id = fpm.user_id
WHERE fpm.cohort_month >= CURRENT_DATE - INTERVAL '6 months'
GROUP BY fpm.cohort_month, DATE_TRUNC('month', p.paid_at)
),
base_revenue AS (
SELECT cohort_month, revenue AS base
FROM monthly_revenue
WHERE revenue_month = cohort_month
)
SELECT
TO_CHAR(mr.cohort_month, 'YYYY-MM') AS cohort,
ROUND(br.base, 0) AS base_revenue,
ROUND(100.0 * MAX(CASE
WHEN EXTRACT(YEAR FROM AGE(revenue_month, mr.cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(revenue_month, mr.cohort_month)) = 0
THEN revenue END) / br.base, 1) AS nrr_m0,
ROUND(100.0 * MAX(CASE
WHEN EXTRACT(YEAR FROM AGE(revenue_month, mr.cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(revenue_month, mr.cohort_month)) = 1
THEN revenue END) / br.base, 1) AS nrr_m1,
ROUND(100.0 * MAX(CASE
WHEN EXTRACT(YEAR FROM AGE(revenue_month, mr.cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(revenue_month, mr.cohort_month)) = 2
THEN revenue END) / br.base, 1) AS nrr_m2,
ROUND(100.0 * MAX(CASE
WHEN EXTRACT(YEAR FROM AGE(revenue_month, mr.cohort_month)) * 12
+ EXTRACT(MONTH FROM AGE(revenue_month, mr.cohort_month)) = 3
THEN revenue END) / br.base, 1) AS nrr_m3
FROM monthly_revenue mr
JOIN base_revenue br ON mr.cohort_month = br.cohort_month
GROUP BY mr.cohort_month, br.base
ORDER BY mr.cohort_month;
Sample output:
| cohort | base_revenue | nrr_m0 | nrr_m1 | nrr_m2 | nrr_m3 |
|---|---|---|---|---|---|
| 2025-10 | 18500 | 100.0 | 94.2 | 91.8 | 105.3 |
| 2025-11 | 22100 | 100.0 | 97.1 | 103.6 | |
| 2025-12 | 16800 | 100.0 | 88.4 | ||
| 2026-01 | 27300 | 100.0 | 101.2 |
What to look for. The 2025-10 cohort shows NRR of 105.3% at month_3 — upsell has more than offset churn. The 2025-12 cohort loses 11.6% of revenue by month_1 — a possible issue with user quality or holiday churn. NRR above 100% in early months (2026-01 cohort: 101.2%) points to a successful expansion revenue strategy.
How to Write Prompts for SQL Generation
1. Provide the schema. AI does not know your database structure. List tables, columns, and types. The more precise the schema, the more accurate the query.
2. Describe business logic explicitly. “Active user” might mean “logged in” or “performed a key action.” AI will not guess. Define your terms: “activation = performed event ‘project_created’ within the first 7 days.”
3. Specify the database engine. PostgreSQL, BigQuery, and ClickHouse use different syntax for dates, window functions, and percentile functions. One word in the prompt saves a correction cycle.
4. Ask for pivot format. Cohort data in long format (cohort, month_number, value) is inconvenient for analysis. A pivot table (rows = cohorts, columns = months) reads much faster.
5. Add constraints. “Last 6 months,” “minimum 30 users per cohort,” “paying users only.” Without constraints, AI returns a query that scans the entire table or includes irrelevant data.
Connection to Observability
Cohort analysis shows what happens with users. LLM observability shows what happens with your AI system. If the product uses LLM features, cohort metrics without LLM call tracing give an incomplete picture. A churn spike in a cohort might correlate with model quality degradation, rising latency, or increased cost per query. Pairing cohort SQL with Langfuse tracing closes both sides.
Summary
Seven queries cover 80% of a product manager’s analytical needs: retention, LTV, activation, churn risk, feature adoption, conversion time, NRR. Each query takes AI 15 to 30 seconds to generate with the right prompt. The critical requirement is an accurate schema and explicit business logic definitions.
An analyst is still needed for interpreting results, building data pipelines, and handling edge cases. But writing standard cohort queries no longer requires waiting in a ticket queue.
Need help with product analytics and data infrastructure? I help startups build AI products and automate processes — belov.works.
FAQ
How do cohort queries need to change when migrating from PostgreSQL to BigQuery?
The logic stays identical; only a handful of syntax differences need attention. Replace DATE_TRUNC('month', ts) with DATE_TRUNC(ts, MONTH), EXTRACT(YEAR FROM AGE(...)) with DATE_DIFF(d1, d2, YEAR), and PERCENTILE_CONT with APPROX_QUANTILES. Ask AI directly: paste the PostgreSQL query and specify “adapt for BigQuery” — the model handles the syntax translation in one step and flags any aggregate function differences.
What minimum cohort size is statistically meaningful for retention analysis?
30 users per cohort is the practical floor for making any directional decision. Below 30, a single outlier user can swing retention by 3–5 percentage points. The query in section 3 already includes HAVING COUNT(DISTINCT cs.user_id) >= 30 — extend this to the retention query if your volume is low. For A/B test-level confidence on cohort comparisons, you need 100+ per cohort to detect a 5-percentage-point difference with 80% power.
Why does NRR sometimes exceed 100% in early months if there’s been no explicit upsell?
Expansion revenue can come from usage-based billing components, plan upgrades triggered by hitting limits, add-on activations, or seat additions — all of which appear as positive revenue events in the payments table. If your product has any variable billing element, early NRR above 100% is expected and healthy. If you have flat-rate plans only and still see NRR > 100%, check your payment data for duplicate entries or refund reversals being counted as revenue.