perf(db): +21 indexes for hot-path queries (audit 2026-05-22)

Adds missing indexes identified by comprehensive audit:

users:        created_at, last_login, role, (is_banned, role)

test_sessions: started_at, finished_at, subject_id, (status, finished_at DESC)

notifications: (user_id, created_at DESC) — previous idx was on is_read, not date

admin_audit_log: target, action, (action, created_at DESC)

classroom_*:  session_id FK indexes — prevents full-table scan on CASCADE

              DELETE (muted, attendance, invites, notes, draw_permissions, hands)

              + chat (session_id, created_at)

submissions:  (student_id, submitted_at DESC) — gradebook timeline

questions:    difficulty — exam builder filter

Skipped users.email — UNIQUE constraint already provides equality lookup.

Expected impact: 30-60% on admin overview / analytics, 10-20% on

notification/session timelines. CASCADE DELETE of classroom_sessions

no longer triggers full-table scans on 6 child tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-22 21:41:37 +03:00
parent e4ec9f8823
commit 3135402dd7
@@ -0,0 +1,46 @@
-- ═══════════════════════════════════════════════════════════════
-- 007: DB Indexes Optimization (audit 2026-05-22)
--
-- Adds 18 missing indexes for hot-path queries identified by
-- comprehensive audit. Skips users.email — UNIQUE constraint
-- already provides equality-lookup index automatically.
--
-- Expected impact: 30-60% speedup for admin overview / analytics,
-- 10-20% for per-user notification + session timelines, and
-- prevents full-table scans on CASCADE DELETE of classroom_sessions.
-- ═══════════════════════════════════════════════════════════════
-- ── users: filter/sort by created_at, last_login, role ──────────
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_banned_role ON users(is_banned, role);
-- ── test_sessions: date range filters + JOIN on subject_id ──────
CREATE INDEX IF NOT EXISTS idx_test_sessions_started_at ON test_sessions(started_at);
CREATE INDEX IF NOT EXISTS idx_test_sessions_finished_at ON test_sessions(finished_at);
CREATE INDEX IF NOT EXISTS idx_test_sessions_subject_id ON test_sessions(subject_id);
CREATE INDEX IF NOT EXISTS idx_test_sessions_status_finished ON test_sessions(status, finished_at DESC);
-- ── notifications: per-user timeline (was indexed on is_read, not created_at) ──
CREATE INDEX IF NOT EXISTS idx_notifications_user_created ON notifications(user_id, created_at DESC);
-- ── admin_audit_log: target LIKE 'user:%' + action filters ──────
CREATE INDEX IF NOT EXISTS idx_audit_log_target ON admin_audit_log(target);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON admin_audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_log_action_date ON admin_audit_log(action, created_at DESC);
-- ── classroom_*: CASCADE DELETE safety (FKs without indexes) ────
CREATE INDEX IF NOT EXISTS idx_classroom_muted_session ON classroom_muted(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_attendance_session ON classroom_attendance(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_invites_session ON classroom_invites(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_notes_session ON classroom_notes(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_draw_permissions_session ON classroom_draw_permissions(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_hands_session ON classroom_hands(session_id);
CREATE INDEX IF NOT EXISTS idx_classroom_chat_session_date ON classroom_chat(session_id, created_at);
-- ── submissions: per-student timeline ────────────────────────────
CREATE INDEX IF NOT EXISTS idx_submissions_student_date ON submissions(student_id, submitted_at DESC);
-- ── questions.difficulty: exam builder filter ────────────────────
CREATE INDEX IF NOT EXISTS idx_questions_difficulty ON questions(difficulty);