- PK id bigint
- email text
- full_name text
- role text
- created_at timestamptz
Diagram Views
erDiagram
users ||--o{ courses : teaches
courses ||--o{ course_lessons : organizes
lessons ||--o{ course_lessons : reused_in
users ||--o{ enrollments : joins
courses ||--o{ enrollments : has
enrollments ||--o{ lesson_progress : tracks
lessons ||--o{ lesson_progress : measured_by
users {
bigint id PK
text email
text role
}
courses {
bigint id PK
bigint instructor_id FK
text slug
text title
}
lessons {
bigint id PK
text slug
int duration_minutes
}
course_lessons {
bigint course_id PK,FK
bigint lesson_id PK,FK
int position
}
enrollments {
bigint id PK
bigint course_id FK
bigint student_id FK
text status
}
lesson_progress {
bigint enrollment_id PK,FK
bigint lesson_id PK,FK
int progress_percent
}
- PK id bigint
- FK instructor_id bigint
- → users.id
- slug text
- title text
- level text
- is_published boolean
- created_at timestamptz
- PK id bigint
- slug text
- title text
- duration_minutes integer
- FK course_id bigint
- → courses.id
- FK lesson_id bigint
- → lessons.id
- position integer
- PK id bigint
- FK course_id bigint
- → courses.id
- FK student_id bigint
- → users.id
- status text
- enrolled_at timestamptz
- FK enrollment_id bigint
- → enrollments.id
- FK lesson_id bigint
- → lessons.id
- completed_at timestamptz
- progress_percent integer
PK Primary key FK Foreign key Relation Active relation
SQL Snippets
schema.sql
create table users (
id bigint primary key generated always as identity,
email text not null unique,
full_name text not null,
role text not null check (role in ('student','instructor','admin')),
created_at timestamptz not null default now()
);
create table courses (
id bigint primary key generated always as identity,
instructor_id bigint not null references users(id),
slug text not null unique,
title text not null,
level text not null check (level in ('beginner','intermediate','advanced')),
is_published boolean not null default false,
created_at timestamptz not null default now()
);
create table lessons (
id bigint primary key generated always as identity,
slug text not null unique,
title text not null,
duration_minutes integer not null check (duration_minutes > 0)
);
create table course_lessons (
course_id bigint not null references courses(id) on delete cascade,
lesson_id bigint not null references lessons(id),
position integer not null check (position > 0),
primary key (course_id, lesson_id),
unique (course_id, position)
);
create table enrollments (
id bigint primary key generated always as identity,
course_id bigint not null references courses(id) on delete cascade,
student_id bigint not null references users(id) on delete cascade,
status text not null check (status in ('active','completed','cancelled')),
enrolled_at timestamptz not null default now(),
unique (course_id, student_id)
);
create table lesson_progress (
enrollment_id bigint not null references enrollments(id) on delete cascade,
lesson_id bigint not null references lessons(id),
completed_at timestamptz,
progress_percent integer not null default 0 check (progress_percent between 0 and 100),
primary key (enrollment_id, lesson_id)
);
create index idx_courses_instructor on courses(instructor_id);
create index idx_enrollments_student_status on enrollments(student_id, status);
seed.sql
insert into users (email, full_name, role)
values
('teach@academy.dev', 'Instructor Kim', 'instructor'),
('student1@academy.dev', 'Student One', 'student'),
('student2@academy.dev', 'Student Two', 'student');
insert into courses (instructor_id, slug, title, level, is_published)
values
(1, 'sql-for-builders', 'SQL for Builders', 'beginner', true);
insert into lessons (slug, title, duration_minutes)
values
('intro-relational', 'Intro to Relational Modeling', 18),
('joins-ctes', 'Joins and CTE Basics', 24),
('indexing-101', 'Indexing Fundamentals', 21);
insert into course_lessons (course_id, lesson_id, position)
values
(1, 1, 1),
(1, 2, 2),
(1, 3, 3);
insert into enrollments (course_id, student_id, status)
values
(1, 2, 'active'),
(1, 3, 'completed');
insert into lesson_progress (enrollment_id, lesson_id, completed_at, progress_percent)
values
(1, 1, now() - interval '1 day', 100),
(1, 2, null, 45),
(2, 1, now() - interval '7 days', 100),
(2, 2, now() - interval '6 days', 100),
(2, 3, now() - interval '5 days', 100);
queries.sql
-- 1) Enrollment count per course select c.slug, c.title, count(e.id) as enrolled_students from courses c left join enrollments e on e.course_id = c.id and e.status <> 'cancelled' group by c.id order by enrolled_students desc; -- 2) Student completion rate in a course select e.id as enrollment_id, u.email as student_email, round(avg(lp.progress_percent)::numeric, 2) as avg_progress from enrollments e join users u on u.id = e.student_id left join lesson_progress lp on lp.enrollment_id = e.id where e.course_id = 1 group by e.id, u.email order by avg_progress desc; -- 3) Lessons with lowest completion select l.slug, l.title, round(avg(lp.progress_percent)::numeric, 2) as avg_progress from lesson_progress lp join lessons l on l.id = lp.lesson_id group by l.id order by avg_progress asc;