Store denormalized review statistics (#982) - #1266
Conversation
Course review averages (rating components, difficulty, hours breakdowns, recommendability, enjoyability, instructor rating) were re-aggregated from every Review on each request. Store these aggregates and maintain them incrementally instead. - Add CourseStats (per-course rollup) and CourseInstructorStats (per course+instructor) models, each holding review_count plus the average of every Review field the read paths aggregate. Composite "rating" is derived from the three stored rating components (single source of truth). - Maintain stats via post_save/post_delete signals on Review, recomputing from scratch for the affected (course, instructor) pair and course rollup. A pre_save hook also refreshes the old target when a review is re-pointed or hidden. Empty aggregates delete the row so nothing stale lingers. - Add recompute_review_stats management command to backfill from existing reviews. - Update read paths (Course.average_rating/difficulty, with_stats, Instructor.average_*_for_course, get_instructors_and_data, and the course_instructor view chart data) to read stored values, each with a SAFE FALLBACK to live aggregation when a stats row is missing (pre-backfill). Public method names/return types are unchanged so templates keep working. - GPA/grade averages (CourseGrade/CourseInstructorGrade via load_grades) are intentionally untouched — out of scope for #982. Adds tests for signal maintenance (create/edit/delete/move), hidden-review exclusion, stored-vs-live equivalence, and the backfill command.
|
Warning Review limit reached
Next review available in: 59 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Closes #982
The performance win only lands after
recompute_review_statsruns (see Deploy steps). Until then everything falls back to live computation (correct, just not faster).Problem
Course/instructor review averages (rating, difficulty, enjoyability, recommendability, hours, reading/writing/group/homework) were aggregated from all
Reviewrows on every request —Course.average_*,Course.with_stats,Instructor.average_*_for_course, and the course-instructor view.Fix
Denormalized storage, mirroring the existing
CourseGrade/CourseInstructorGradesplit:CourseStats(OneToOne →Course, course-level rollup) andCourseInstructorStats(per course+instructor), sharing an abstract base withreview_count+ a stored average for every aggregatedReviewfield. Composite rating is derived from its 3 stored components (single source of truth, matching the historic formula). Only non-hidden reviews are counted.post_save/post_delete/pre_savesignals onReviewrecompute the affected(course, instructor)pair + course rollup on every create / edit / delete / re-point / hide; rows with zero remaining reviews are deleted.python manage.py recompute_review_statsbackfills / rebuilds all rows from existing reviews.Migration:
0029_coursestats_courseinstructorstats_and_more.py.Deploy steps
0029.python manage.py recompute_review_statsonce.Verification (run against a disposable Postgres)
makemigrations --check --dry-run— no changes detected (models match migration 0029).manage.py checkclean;ruff check/ruff format --checkclean.manage.py test tcf_website— 267 OK, incl. 10 new tests (signal maintenance on create/edit/delete/move, hidden-review exclusion, stored-vs-live numeric equivalence, backfill command).Risks for review
with_statsnow LEFT JOINs thestatsOneToOne withCoalesceto correlated subqueries (fallback dormant post-backfill) — worth anEXPLAINon prod-sized data.save()/signals (e.g.load_review_drive) won't update stats incrementally — re-run the backfill afterward.Instructor.average_rating()/average_difficulty()(all-courses; no current consumers) intentionally left as live computation.🤖 Implemented with Claude Code.