Use CasesAIPostgreSQLSQL

How to Build a Customer Health Score Directly From Your Database

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 r...

Priya Sharma· Product LeadApril 3, 20267 min read

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

  • Login frequency (daily, weekly, monthly active users per account)
  • Feature adoption depth (how many core features they've used)
  • API call volume or session time
  • Last active date
  • Account signals

  • Billing status (current, overdue, on downgrade plan)
  • Contract renewal date proximity
  • Number of seats used vs. seats purchased
  • Support signals

  • Open support tickets
  • Ticket volume trend (increasing = bad)
  • Time to first response / resolution
  • Engagement signals

  • Email open rates (if you store these)
  • In-app survey responses (NPS, CSAT)
  • 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:

  • Stale data your CS team is acting on a two-week-old picture
  • No alerting accounts cross the risk threshold without anyone noticing
  • 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.

    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 query

    Now 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.

    Ready to try AI for Database?

    Query your database in plain English. No SQL required. Start free today.