Metric Formulas
Common metric formulas and SQL patterns
Last updated: 1/17/2025
Ready-to-use SQL formulas for common business metrics.
Revenue Metrics
Monthly Recurring Revenue (MRR)
SELECT SUM(amount) as value
FROM subscriptions
WHERE status = 'active'
AND billing_period = 'monthly'
Annual Recurring Revenue (ARR)
SELECT SUM(
CASE
WHEN billing_period = 'monthly' THEN amount * 12
WHEN billing_period = 'annual' THEN amount
ELSE 0
END
) as value
FROM subscriptions
WHERE status = 'active'
Average Revenue Per User (ARPU)
SELECT
SUM(amount) / NULLIF(COUNT(DISTINCT user_id), 0) as value
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days'
Growth Metrics
User Growth Rate
WITH current_month AS (
SELECT COUNT(*) as count
FROM users
WHERE created_at >= DATE_TRUNC('month', NOW())
),
last_month AS (
SELECT COUNT(*) as count
FROM users
WHERE created_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month')
AND created_at < DATE_TRUNC('month', NOW())
)
SELECT
((c.count - l.count) * 100.0 / NULLIF(l.count, 0)) as value
FROM current_month c, last_month l
Churn Rate
SELECT
(COUNT(*) FILTER (WHERE churned_at IS NOT NULL) * 100.0 /
NULLIF(COUNT(*), 0)) as value
FROM subscriptions
WHERE created_at < NOW() - INTERVAL '30 days'
Net Revenue Retention (NRR)
WITH cohort AS (
SELECT SUM(amount) as start_mrr
FROM subscriptions
WHERE created_at >= NOW() - INTERVAL '1 year'
AND created_at < NOW() - INTERVAL '11 months'
),
current AS (
SELECT SUM(amount) as current_mrr
FROM subscriptions
WHERE user_id IN (
SELECT user_id FROM subscriptions
WHERE created_at >= NOW() - INTERVAL '1 year'
AND created_at < NOW() - INTERVAL '11 months'
)
AND status = 'active'
)
SELECT
(c.current_mrr * 100.0 / NULLIF(co.start_mrr, 0)) as value
FROM cohort co, current c
Engagement Metrics
Daily Active Users (DAU)
SELECT COUNT(DISTINCT user_id) as value
FROM events
WHERE created_at >= NOW() - INTERVAL '24 hours'
Monthly Active Users (MAU)
SELECT COUNT(DISTINCT user_id) as value
FROM events
WHERE created_at >= NOW() - INTERVAL '30 days'
DAU/MAU Ratio (Stickiness)
WITH dau AS (
SELECT COUNT(DISTINCT user_id) as count
FROM events
WHERE created_at >= NOW() - INTERVAL '24 hours'
),
mau AS (
SELECT COUNT(DISTINCT user_id) as count
FROM events
WHERE created_at >= NOW() - INTERVAL '30 days'
)
SELECT
(d.count * 100.0 / NULLIF(m.count, 0)) as value
FROM dau d, mau m
Average Session Duration
SELECT AVG(duration_seconds) / 60.0 as value -- Convert to minutes
FROM sessions
WHERE created_at >= NOW() - INTERVAL '7 days'
Sales Metrics
Conversion Rate
WITH visitors AS (
SELECT COUNT(DISTINCT user_id) as count
FROM page_views
WHERE created_at >= NOW() - INTERVAL '30 days'
),
conversions AS (
SELECT COUNT(DISTINCT user_id) as count
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days'
)
SELECT
(c.count * 100.0 / NULLIF(v.count, 0)) as value
FROM visitors v, conversions c
Average Deal Size
SELECT AVG(amount) as value
FROM deals
WHERE status = 'closed_won'
AND closed_at >= NOW() - INTERVAL '90 days'
Sales Cycle Length
SELECT AVG(
EXTRACT(EPOCH FROM (closed_at - created_at)) / 86400
) as value -- Days
FROM deals
WHERE status = 'closed_won'
AND closed_at >= NOW() - INTERVAL '90 days'
Product Metrics
Feature Adoption Rate
WITH total_users AS (
SELECT COUNT(DISTINCT user_id) as count
FROM users
WHERE created_at < NOW() - INTERVAL '7 days' -- Active users
),
feature_users AS (
SELECT COUNT(DISTINCT user_id) as count
FROM events
WHERE event_name = 'feature_used'
AND created_at >= NOW() - INTERVAL '30 days'
)
SELECT
(f.count * 100.0 / NULLIF(t.count, 0)) as value
FROM total_users t, feature_users f
Time to Value
SELECT AVG(
EXTRACT(EPOCH FROM (first_value_at - signed_up_at)) / 3600
) as value -- Hours
FROM users
WHERE signed_up_at >= NOW() - INTERVAL '30 days'
AND first_value_at IS NOT NULL
Customer Metrics
Customer Lifetime Value (LTV)
WITH customer_revenue AS (
SELECT
user_id,
SUM(amount) as total_revenue,
MIN(created_at) as first_purchase,
MAX(created_at) as last_purchase
FROM orders
WHERE status = 'completed'
GROUP BY user_id
),
lifetime AS (
SELECT
AVG(EXTRACT(EPOCH FROM (last_purchase - first_purchase)) / 86400) as avg_lifetime_days,
AVG(total_revenue) as avg_revenue
FROM customer_revenue
)
SELECT avg_revenue as value
FROM lifetime
Customer Acquisition Cost (CAC)
WITH marketing_spend AS (
SELECT SUM(amount) as total_spend
FROM expenses
WHERE category = 'marketing'
AND created_at >= NOW() - INTERVAL '30 days'
),
new_customers AS (
SELECT COUNT(*) as count
FROM users
WHERE created_at >= NOW() - INTERVAL '30 days'
)
SELECT
(m.total_spend / NULLIF(n.count, 0)) as value
FROM marketing_spend m, new_customers n
System Metrics
API Error Rate
SELECT
(COUNT(*) FILTER (WHERE status >= 400) * 100.0 /
NULLIF(COUNT(*), 0)) as value
FROM api_requests
WHERE created_at >= NOW() - INTERVAL '1 hour'
Average Response Time
SELECT AVG(response_time_ms) as value
FROM api_requests
WHERE created_at >= NOW() - INTERVAL '1 hour'
System Uptime
WITH total_time AS (
SELECT EXTRACT(EPOCH FROM (NOW() - NOW() + INTERVAL '24 hours')) as seconds
),
downtime AS (
SELECT SUM(EXTRACT(EPOCH FROM (ended_at - started_at))) as seconds
FROM incidents
WHERE started_at >= NOW() - INTERVAL '24 hours'
)
SELECT
((t.seconds - COALESCE(d.seconds, 0)) * 100.0 / t.seconds) as value
FROM total_time t, downtime d
Advanced Patterns
Cohort Analysis
WITH cohort AS (
SELECT
DATE_TRUNC('month', created_at) as cohort_month,
user_id
FROM users
),
retention AS (
SELECT
c.cohort_month,
COUNT(DISTINCT CASE
WHEN e.created_at >= c.cohort_month + INTERVAL '1 month'
AND e.created_at < c.cohort_month + INTERVAL '2 months'
THEN c.user_id
END) * 100.0 / NULLIF(COUNT(DISTINCT c.user_id), 0) as month_1_retention
FROM cohort c
LEFT JOIN events e ON c.user_id = e.user_id
GROUP BY c.cohort_month
)
SELECT AVG(month_1_retention) as value
FROM retention
WHERE cohort_month >= NOW() - INTERVAL '6 months'
Percentile Calculation
SELECT
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY response_time_ms) as value
FROM api_requests
WHERE created_at >= NOW() - INTERVAL '1 hour'
Tips for Writing Formulas
1. Always Handle NULL
-- Good
SELECT COUNT(*) / NULLIF(total, 0) as value
-- Bad
SELECT COUNT(*) / total as value -- Can cause division by zero
2. Use Time Ranges
-- Good
WHERE created_at >= NOW() - INTERVAL '30 days'
-- Bad
-- No time filter (scans entire table)
3. Filter Early
-- Good
SELECT COUNT(*)
FROM users
WHERE status = 'active'
AND created_at >= NOW() - INTERVAL '30 days'
-- Bad
SELECT COUNT(*)
FROM (SELECT * FROM users) u -- Fetches all rows first
WHERE status = 'active'