From e5f8092025637d5a730b63b91d52c075bd4ef660 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 25 Sep 2024 03:25:24 -0700 Subject: [PATCH] Prefetch build and project on version list (#11616) This optimizes the version listing view and removes redundant queries. - Requires readthedocs/ext-theme#493 - Fixes readthedocs/ext-theme#463 --- readthedocs/builds/models.py | 16 ++++++++++++++++ readthedocs/builds/querysets.py | 26 +++++++++++++++++++++++++- readthedocs/projects/views/public.py | 6 +++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/readthedocs/builds/models.py b/readthedocs/builds/models.py index 25940a8bb99..98ad201105c 100644 --- a/readthedocs/builds/models.py +++ b/readthedocs/builds/models.py @@ -207,6 +207,9 @@ class Meta: unique_together = [("project", "slug")] ordering = ["-verbose_name"] + # Property used for prefetching version related fields + LATEST_BUILD_CACHE = "_latest_build" + def __str__(self): return self.verbose_name @@ -291,6 +294,19 @@ def vcs_url(self): @property def last_build(self): + # TODO deprecated in favor of `latest_build`, which matches naming on + # the Project model + return self.latest_build + + @property + def latest_build(self): + # Check if there is `_latest_build` prefetch in the Queryset. + # Used for database optimization. + if hasattr(self, self.LATEST_BUILD_CACHE): + if latest_build := getattr(self, self.LATEST_BUILD_CACHE): + return latest_build[0] + return None + return self.builds.order_by("-date").first() @property diff --git a/readthedocs/builds/querysets.py b/readthedocs/builds/querysets.py index 006185cca76..8c304419986 100644 --- a/readthedocs/builds/querysets.py +++ b/readthedocs/builds/querysets.py @@ -3,7 +3,7 @@ import structlog from django.db import models -from django.db.models import Q +from django.db.models import OuterRef, Prefetch, Q, Subquery from django.utils import timezone from readthedocs.builds.constants import ( @@ -141,6 +141,30 @@ def for_reindex(self): .distinct() ) + def prefetch_subquery(self): + """ + Prefetch related objects via subquery for each version. + + .. note:: + + This should come after any filtering. + """ + from readthedocs.builds.models import Build + + # Prefetch the latest build for each project. + subquery_builds = Subquery( + Build.internal.filter(version=OuterRef("version_id")) + .order_by("-date") + .values_list("id", flat=True)[:1] + ) + prefetch_builds = Prefetch( + "builds", + Build.internal.filter(pk__in=subquery_builds), + to_attr=self.model.LATEST_BUILD_CACHE, + ) + + return self.prefetch_related(prefetch_builds) + class VersionQuerySet(SettingsOverrideObject): _default_class = VersionQuerySetBase diff --git a/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py index c9fb0d3d808..b8481e72954 100644 --- a/readthedocs/projects/views/public.py +++ b/readthedocs/projects/views/public.py @@ -123,7 +123,11 @@ def get_context_data(self, **kwargs): queryset=versions, project=project, ) - versions = self.get_filtered_queryset() + versions = ( + self.get_filtered_queryset() + .prefetch_related("project") + .prefetch_subquery() + ) context["versions"] = versions protocol = "http"