FFastero

Connect any database. Ask in plain English.

Try free
Back to blog

Blog article

How to Track Feature Adoption with SQL

Most teams track feature adoption as a binary — used it or didn't. Five SQL queries that go deeper: adoption rate, time-to-first-use, stickiness, usage depth, and the "aha moment" that predicts retention.

Fastero Dev TeamFastero Dev Team
2026-08-04
product analyticsSQLfeature adoptionretentionactivation
How to Track Feature Adoption with SQL

How to Track Feature Adoption with SQL

Every product team I've worked with tracks feature adoption the same way: count the unique users who triggered event X. Used it or didn't. Binary. That number is almost useless on its own.

A feature with 40% adoption sounds healthy until you realize most of those users tried it once and never came back. A feature at 8% sounds dead until you notice those users renew every year. The rate is table stakes. What actually matters is depth, timing, and whether the feature predicts retention — all of it queryable from the same event table.

These queries assume two tables: events(user_id, event_name, created_at) and users(id, created_at, status, plan). We run them against our own product data at Fastero, and they work on any event table with the same shape. If you've already got product analytics running from Postgres, you have everything you need.

Adoption rate per feature

What percentage of your active users touched each feature in the last 30 days?

WITH active_users AS (
  SELECT count(DISTINCT user_id) AS total
  FROM events
  WHERE created_at >= now() - interval '30 days'
),
feature_usage AS (
  SELECT
    event_name,
    count(DISTINCT user_id) AS users
  FROM events
  WHERE created_at >= now() - interval '30 days'
  GROUP BY event_name
)
SELECT
  f.event_name,
  f.users,
  round(100.0 * f.users / a.total, 1) AS adoption_pct
FROM feature_usage f
CROSS JOIN active_users a
ORDER BY adoption_pct DESC;

The leaderboard. Which features have traction, which ones shipped to silence. The bottom of the list is always revealing — but a low number doesn't tell you whether the feature is bad or just hard to discover. That's where time-to-first-use comes in.

Time-to-first-use

How long after signup does someone first try each feature? This separates onboarding failures from product failures.

WITH first_use AS (
  SELECT
    user_id,
    event_name,
    min(created_at) AS first_at
  FROM events
  GROUP BY user_id, event_name
)
SELECT
  f.event_name,
  count(*) AS users_who_tried,
  percentile_cont(0.5) WITHIN GROUP (
    ORDER BY extract(day FROM f.first_at - u.created_at)
  ) AS median_days_to_first_use
FROM first_use f
JOIN users u ON u.id = f.user_id
WHERE u.created_at >= now() - interval '90 days'
GROUP BY f.event_name
HAVING count(*) >= 10
ORDER BY median_days_to_first_use;

If a feature's median is 45 days, your feature isn't failing — your onboarding is. Features under 3 days are well-positioned in the UI or naturally part of the first session. Features at 20+ days are the ones worth putting into onboarding nudges or tooltips.

The HAVING count(*) >= 10 matters. Three users will give you a meaningless median. And percentile_cont is Postgres/DuckDB syntax — on MySQL you'd need a window function workaround or SUBSTRING_INDEX(GROUP_CONCAT(...), ',', count/2), which is as ugly as it sounds.

Fastero

Connect your database. Ask questions. Get dashboards.

Postgres, BigQuery, Snowflake, and 10+ sources — live-connected, AI-powered, no dashboard builder learning curve.

Try free →

Stickiness: DAU/MAU per feature

Adoption tells you who tried it. Stickiness tells you who keeps coming back. The DAU/MAU ratio — daily active users of a feature divided by monthly active users of that same feature — is the cleanest measure of habit:

WITH daily_users AS (
  SELECT
    event_name,
    created_at::date AS day,
    count(DISTINCT user_id) AS dau
  FROM events
  WHERE created_at >= now() - interval '30 days'
  GROUP BY event_name, created_at::date
),
monthly_users AS (
  SELECT
    event_name,
    count(DISTINCT user_id) AS mau
  FROM events
  WHERE created_at >= now() - interval '30 days'
  GROUP BY event_name
)
SELECT
  m.event_name,
  m.mau,
  round(avg(d.dau), 1) AS avg_dau,
  round(avg(d.dau) / NULLIF(m.mau, 0), 3) AS stickiness
FROM monthly_users m
JOIN daily_users d ON d.event_name = m.event_name
GROUP BY m.event_name, m.mau
HAVING m.mau >= 20
ORDER BY stickiness DESC;

Above 0.3 is strong — the average user comes back roughly every third day. Below 0.05 and it's basically a one-shot tool. Most SaaS features land between 0.08 and 0.15, the "weekly-ish" zone.

Don't interpret stickiness without adoption next to it. A feature with 0.5 stickiness and 2% adoption is a niche power tool — great for the two people who use it, irrelevant to your product strategy.

Usage depth and breadth

Here's where binary adoption tracking falls apart completely. A user who created one dashboard isn't the same as one who created twenty. A user who only ever touches one feature isn't getting the same value as one who uses five.

This query buckets users by how many distinct features they've used (breadth) and how intensely they use their most-used feature (depth):

WITH user_features AS (
  SELECT
    user_id,
    event_name,
    count(*) AS times_used
  FROM events
  WHERE created_at >= now() - interval '30 days'
  GROUP BY user_id, event_name
),
user_summary AS (
  SELECT
    user_id,
    count(DISTINCT event_name) AS features_used,
    sum(times_used) AS total_actions,
    max(times_used) AS deepest_feature_usage
  FROM user_features
  GROUP BY user_id
)
SELECT
  CASE
    WHEN features_used = 1 THEN '1 feature'
    WHEN features_used BETWEEN 2 AND 3 THEN '2-3 features'
    WHEN features_used BETWEEN 4 AND 6 THEN '4-6 features'
    ELSE '7+ features'
  END AS breadth_bucket,
  count(*) AS users,
  round(avg(total_actions), 1) AS avg_total_actions,
  round(avg(deepest_feature_usage), 1) AS avg_deepest_usage
FROM user_summary
GROUP BY
  CASE
    WHEN features_used = 1 THEN '1 feature'
    WHEN features_used BETWEEN 2 AND 3 THEN '2-3 features'
    WHEN features_used BETWEEN 4 AND 6 THEN '4-6 features'
    ELSE '7+ features'
  END
ORDER BY min(features_used);

features_used is your feature diversity score. deepest_feature_usage shows whether someone went deep on at least one thing. In our data, users in the 4-6 features bucket retain at dramatically higher rates than single-feature users, regardless of which specific features they touched.

Two users can both "adopt" a feature where one used it twice and the other used it 80 times. They don't churn at the same rate. Treating them identically in your adoption metric is the kind of laziness that makes product decisions worse, not better.

The aha moment: which features predict retention

The highest-value query on this list. For each feature, compare the 90-day retention rate of users who tried it within their first week against the overall baseline. Features with the biggest lift are your aha moments — the things that, once discovered early, make people stick around:

WITH eligible AS (
  SELECT id AS user_id, created_at AS signup_at
  FROM users
  WHERE created_at <= now() - interval '90 days'
),
first_week AS (
  SELECT DISTINCT e.user_id, e.event_name
  FROM events e
  JOIN eligible el ON el.user_id = e.user_id
  WHERE e.created_at BETWEEN el.signup_at
    AND el.signup_at + interval '7 days'
),
still_active AS (
  SELECT DISTINCT user_id
  FROM events
  WHERE created_at >= now() - interval '30 days'
),
baseline AS (
  SELECT
    count(*) AS total,
    count(sa.user_id) AS retained
  FROM eligible el
  LEFT JOIN still_active sa ON sa.user_id = el.user_id
)
SELECT
  fw.event_name,
  count(DISTINCT fw.user_id) AS tried_in_week_1,
  round(100.0 * count(DISTINCT fw.user_id)
    FILTER (WHERE sa.user_id IS NOT NULL)
    / NULLIF(count(DISTINCT fw.user_id), 0), 1) AS pct_retained,
  round(100.0 * b.retained
    / NULLIF(b.total, 0), 1) AS baseline_retention,
  round(100.0 * count(DISTINCT fw.user_id)
    FILTER (WHERE sa.user_id IS NOT NULL)
    / NULLIF(count(DISTINCT fw.user_id), 0), 1)
    - round(100.0 * b.retained
    / NULLIF(b.total, 0), 1) AS retention_lift
FROM first_week fw
LEFT JOIN still_active sa ON sa.user_id = fw.user_id
CROSS JOIN baseline b
GROUP BY fw.event_name, b.retained, b.total
HAVING count(DISTINCT fw.user_id) >= 20
ORDER BY retention_lift DESC;

The retention_lift column is the one worth staring at. A feature showing +25% over baseline is telling you something your roadmap should listen to: if users who create a dashboard in their first week retain 40% better than average, your onboarding should put dashboard creation front and center. If you're also tracking where users drop out of your funnel, this query tells you which drop-offs matter most.

Correlation isn't causation — users who try more features might just be more motivated in general. But as a ranking signal it's directionally correct and far better than guessing. One practical note: the WHERE created_at <= now() - interval '90 days' filter means you need at least 90 days of user data before this returns anything useful. Don't run it on a two-week-old product.

Beyond the leaderboard

Run all five queries and you'll have something most product teams never build: a multi-dimensional view of adoption. Not "40% adopted feature X" but "40% tried it, half used it more than five times, the sticky ones come back daily, and early adopters retain 30% better than baseline." That's a different conversation with your team than a flat percentage.

We schedule these queries in Fastero against our own Postgres and pipe the results into dashboards our team checks without asking anyone to run SQL. The same setup works for any product — connect your database, save the queries, schedule the refresh, share the results.


Try Fastero free — connect your product database and track which features actually drive retention, not just which ones got clicked. No credit card required.

Ready to try it yourself?

Connect your database, ask questions in plain English, and get live dashboards — in under 2 minutes. No credit card required.