What is course completion rate, and how do you measure it in Metabase?
What does a course completion rate chart look like in Metabase?
Plot completion rate by monthly enrollment cohort, counting only cohorts that have passed the maturity window, and read the slope rather than any single month. The steady climb tracks onboarding and curriculum fixes, while a one-cohort dip like Dec's usually means a promo-heavy month of free enrollments that never started lesson one.
Definition
Course completion rate is the share of enrolled students who finish a course. Two decisions make or break the number: what 'complete' means — every lesson marked done, a certificate issued, or a final assessment passed — and which enrollments belong in the denominator. A student who enrolled yesterday has not failed to complete; they have not had time to.
What data do you need?
- Enrollment records with enrolled_at, access type (paid, free, bundled), and refund status
- Lesson-level progress events with a completion timestamp per lesson
- The required-lesson list per course, and whether optional lessons count
- Certificate or final-assessment results if those define completion
- A maturity window per course — the elapsed time a typical finisher needs
SQL pattern
WITH cohort AS (
-- Denominator: only enrollments old enough to plausibly finish.
-- Widen the window for long courses; state it on the dashboard.
SELECT
enrollment_id,
course_id,
student_id,
access_type
FROM creator_enrollments
WHERE enrolled_at < CURRENT_DATE - INTERVAL '90 days'
AND status <> 'refunded'
), required AS (
-- Required lessons per course. If a course adds lessons later,
-- students who already "finished" quietly drop below 100%.
SELECT
course_id,
COUNT(DISTINCT lesson_id) FILTER (WHERE is_required) AS lessons_required
FROM creator_lesson_progress
GROUP BY 1
), done AS (
SELECT
enrollment_id,
COUNT(DISTINCT lesson_id) FILTER (
WHERE is_required AND completed_at IS NOT NULL
) AS lessons_done
FROM creator_lesson_progress
GROUP BY 1
)
SELECT
c.course_id,
c.access_type,
COUNT(*) AS matured_enrollments,
-- LEFT JOIN leaves lessons_done NULL for never-started enrollments;
-- the comparison is NULL, so FILTER correctly counts them as not done.
COUNT(*) FILTER (WHERE d.lessons_done >= r.lessons_required)
AS completed,
ROUND(
100.0 * COUNT(*) FILTER (WHERE d.lessons_done >= r.lessons_required)
/ NULLIF(COUNT(*), 0), 1
) AS completion_rate_pct,
ROUND(
100.0 * COUNT(*) FILTER (WHERE COALESCE(d.lessons_done, 0) = 0)
/ NULLIF(COUNT(*), 0), 1
) AS never_started_pct
FROM cohort c
JOIN required r USING (course_id)
LEFT JOIN done d USING (enrollment_id)
GROUP BY 1, 2
ORDER BY matured_enrollments DESC; Common pitfalls
Where does this metric apply?
This metric commonly uses data from Kajabi, Thinkific, Teachable, Podia, Circle, plus any warehouse models built at the same grain.