Every SaaS company has the same problem: customer success managers need to know which accounts are at risk before it's too late. But the data that actually reveals customer health login frequency, feature usage, support ticket volume, billing status lives in your database, not in a tidy dashboard.
Most teams respond to this by stitching together spreadsheets, writing one-off SQL reports, or waiting for a data engineer to build something. By the time the report lands, the customer has already decided to leave.
This article walks through how to build a meaningful customer health score using data that's already in your database and how to automate it so your team always has a current picture, without anyone writing a query.
What Goes Into a Customer Health Score
A health score is a composite number that summarises how well a customer is doing. Done right, it predicts churn risk weeks before the customer actually churns.
The signals that matter most vary by product, but most B2B SaaS companies track some version of these:
Usage signals
Account signals
Support signals
Engagement signals
Each signal gets a weight. The final score is typically a number from 0–100, where below 40 means "at risk," 40–70 is "neutral," and above 70 is "healthy."
The SQL Behind It
Let's make this concrete. Assume you have a PostgreSQL database with tables like users, sessions, subscriptions, support_tickets, and accounts.
Here's a query that calculates a simplified health score per account:
WITH usage AS (
SELECT
account_id,
COUNT(DISTINCT DATE(created_at)) AS active_days_last_30,
MAX(created_at) AS last_session
FROM sessions
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY account_id
),
support AS (
SELECT
account_id,
COUNT(*) FILTER (WHERE status = 'open') AS open_tickets
FROM support_tickets
GROUP BY account_id
),
billing AS (
SELECT
account_id,
status AS billing_status,
CASE WHEN status = 'active' THEN 20 ELSE 0 END AS billing_score
FROM subscriptions
WHERE is_current = true
)
SELECT
a.id AS account_id,
a.name,
COALESCE(u.active_days_last_30, 0) AS active_days,
COALESCE(s.open_tickets, 0) AS open_tickets,
b.billing_status,
(
-- Usage score: up to 50 points
LEAST(COALESCE(u.active_days_last_30, 0) * 2, 50) +
-- Billing score: 20 points for active
COALESCE(b.billing_score, 0) +
-- Support penalty: -10 per open ticket, floor at 0
GREATEST(-10 * COALESCE(s.open_tickets, 0), -30)
) AS health_score
FROM accounts a
LEFT JOIN usage u ON u.account_id = a.id
LEFT JOIN support s ON s.account_id = a.id
LEFT JOIN billing b ON b.account_id = a.id
ORDER BY health_score ASC;This gives you a ranked list of accounts, worst-first. Your customer success team can start making calls from the top.
The weights are illustrative you'll tune them based on what actually predicts churn in your data. But the structure is reusable.
Adding Feature Adoption Depth
Usage frequency alone doesn't tell the whole story. A customer might log in every day but only use one feature, which is a warning sign for expansion and retention.
Feature adoption depth requires tracking what features each account has actually used. If you store feature events in a table:
SELECT
account_id,
COUNT(DISTINCT feature_name) AS features_used,
-- assume 10 core features total
ROUND(COUNT(DISTINCT feature_name)::numeric / 10 * 100, 0) AS adoption_pct
FROM feature_events
WHERE created_at >= NOW() - INTERVAL '60 days'
GROUP BY account_id;Layer this into your health score as another weighted component. Accounts using fewer than 30% of core features get a lower score; accounts using 70%+ get a bonus.
The Problem With Manual Queries
If you run this query once, you have a snapshot. But customer health is a moving target. An account that was healthy last week might have two support tickets open today after a failed deployment.
The two failure modes:
Most teams handle this with scheduled Tableau/Metabase reports, which means a data analyst has to own the query, maintain it, and make sure it runs. When they leave or get busy, the report breaks.
Automating Health Scores With AI for Database
This is where AI for Database changes the workflow entirely. Instead of maintaining a scheduled query someone has to own, you connect your database and set up two things:
A live dashboard a plain-English query like "Show me all accounts with fewer than 5 active days in the last 30 days, sorted by open support tickets" returns a live table. No SQL required. Non-technical CS managers can check it themselves.
An action workflow define the condition once: "When an account's health score drops below 40 (based on active_days < 5 AND open_tickets > 2)..." and connect an action: send a Slack alert, trigger a webhook to your CRM, or fire an email to the account owner.
Condition: active_days_last_30 < 5 AND open_tickets > 1
Action: POST to Slack webhook → "#cs-alerts: Account {name} may need attention"Your customer success team gets alerted in Slack the moment an account crosses into risk territory before anyone has to remember to run a query.
Segmenting Your Health Score by Plan or Segment
Enterprise accounts and small business accounts need different thresholds. An SMB using the product 5 days a month might be perfectly healthy; an enterprise account doing the same is underperforming relative to the contract value.
Segment your scoring:
SELECT
a.id,
a.name,
a.plan_tier,
CASE
WHEN a.plan_tier = 'enterprise' THEN
-- stricter thresholds
LEAST(COALESCE(u.active_days_last_30, 0) * 3, 60) + ...
ELSE
LEAST(COALESCE(u.active_days_last_30, 0) * 2, 50) + ...
END AS health_score
FROM accounts a
LEFT JOIN usage u ON u.account_id = a.id;Once you've built this segmentation, you can expose it to your CS team as a simple question: "Which enterprise accounts have a health score below 50?" They get the answer without touching SQL.
Tracking Health Score Trends Over Time
A single health score is useful. A trend is more useful. An account at 45 today that was at 70 last month is declining fast. An account at 45 that was at 30 last month is recovering.
To track trends, you need to snapshot scores periodically. The simplest approach: write a scheduled job that inserts daily health scores into a account_health_snapshots table.
INSERT INTO account_health_snapshots (account_id, score, snapshot_date)
SELECT id, calculated_score, CURRENT_DATE
FROM v_account_health_scores; -- the view from your composite queryNow you can ask "Show me accounts whose health score has dropped by more than 15 points in the last 14 days" exactly the kind of question AI for Database can answer in plain English against your live data.
Start Simple, Iterate Fast
The teams that build the best health scores don't start with a perfect model. They start with two signals, see which accounts it identifies, talk to customers in that cohort, and refine the weights based on what they learn.
If you have a database and want to start querying your customer health data today without writing SQL, try AI for Database free. Connect your database, ask "Which customers haven't logged in for 30 days?" and you'll have your first at-risk list in minutes.