KPI Reference¶
These are the most commonly requested business metrics. Getting them right requires careful filtering — the most frequent mistakes involve forgetting to exclude reversed contracts, counting modules as members, or assuming ACTIVE status alone means a contract is currently running. The SQL example below shows the correct pattern for active member counts. For ready-to-run queries answering everyday operator questions, see the Use Cases.
| Metric | Definition | Key Tables | How to Calculate |
|---|---|---|---|
| Active Members | Customers with at least one currently active main membership. | fct_contract, dim_rate, dim_contract_property |
contract_type = 'CONTRACT' AND contract_status = 'ACTIVE' AND is_reversed = FALSE AND date range check. |
| New Sign-ups | Contracts created in a given period. | fct_contract |
Use created_date_id + contract_type = 'CONTRACT'. |
| Churn Rate | Cancellations ÷ active members over a period. | fct_contract |
Use cancellation_date_id for timing. |
| ARPM | Average Revenue Per Member = membership revenue ÷ avg active members. | fct_revenue, dim_revenue_group |
Filter sub_group = 'MEMBERSHIP_FEE'. |
| Check-in Frequency | Average gym visits per active member per period. | fct_checkin |
Combine check-in counts with active member count. |
| Class Utilization | Actual participants ÷ max capacity. | fct_class_appointment, dim_appointment |
num_participants / max_participants. Filter status = 'PLANNED'. |
| Lead Conversion Rate | Won leads ÷ total resolved leads. | fct_lead_lifetime |
COUNT(WIN) / COUNT(WIN + LOSS). Exclude PENDING. |
| Payment Collection Rate | Successful bookings ÷ total bookings. | fct_payment_run |
COUNT(REGULAR_BOOKING) / COUNT(*). Filter archived = FALSE. |
Active Members SQL Example¶
SELECT
ou.name AS gym_name,
COUNT(DISTINCT c.customer_id) AS active_members
FROM fct_contract c
JOIN dim_organization_unit ou ON c.organization_unit_id = ou.organization_unit_id
JOIN dim_contract_property cp ON c.contract_property_id = cp.contract_property_id
JOIN dim_rate r ON c.rate_id = r.rate_id
WHERE ou.archived = FALSE
AND c.is_reversed = FALSE
AND cp.contract_status = 'ACTIVE'
AND r.contract_type = 'CONTRACT'
AND c.start_date_id <= CAST(TO_CHAR(CURRENT_DATE, 'YYYYMMDD') AS INTEGER)
AND (c.end_date_id IS NULL OR c.end_date_id >= CAST(TO_CHAR(CURRENT_DATE, 'YYYYMMDD') AS INTEGER))
GROUP BY ou.name;
Common mistakes: forgetting contract_type = 'CONTRACT' (inflates with modules/flat fees), forgetting is_reversed = FALSE, assuming ACTIVE alone means "currently active".