Skip to content

Commit 850ec30

Browse files
authored
Dashboard: optimize projects queryset (constant number of queries) (#12385)
This is on top of #12383 as another PR in case we see a regression in performance, it would be easier to just revert this change. Instead of pre-fetching, we annotate, so we can use exist instead of fetching the query. This allows to just make one query. But this does add some extra complexity to the query itself, which is less overhead than doing a prefetch over the queryset itself, but the pagination package we are using calls .count(), and django includes the annotation by default... which makes that query slower as well... but testing in production shows faster results still.
1 parent ac69118 commit 850ec30

File tree

3 files changed

+16
-7
lines changed

3 files changed

+16
-7
lines changed

readthedocs/projects/models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -980,10 +980,10 @@ def conf_dir(self, version=LATEST):
980980

981981
@property
982982
def has_good_build(self):
983-
# Check if there is `_good_build` annotation in the Queryset.
984-
# Used for Database optimization.
985-
if hasattr(self, "_good_build"):
986-
return self._good_build
983+
# Check if there is `_has_good_build` annotation in the queryset.
984+
# Used for database optimization.
985+
if hasattr(self, "_has_good_build"):
986+
return self._has_good_build
987987
return self.builds(manager=INTERNAL).filter(success=True).exists()
988988

989989
def vcs_repo(self, environment, version):

readthedocs/projects/querysets.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from django.conf import settings
44
from django.db import models
55
from django.db.models import Count
6+
from django.db.models import Exists
7+
from django.db.models import OuterRef
68
from django.db.models import Prefetch
79
from django.db.models import Q
810

@@ -146,7 +148,13 @@ def prefetch_latest_build(self):
146148
Build.internal.select_related("version").order_by("-date")[:1],
147149
to_attr=self.model.LATEST_BUILD_CACHE,
148150
)
149-
return self.prefetch_related(latest_build)
151+
query = self.prefetch_related(latest_build)
152+
153+
# Annotate whether the project has a successful build or not,
154+
# to avoid N+1 queries when showing the build status.
155+
return query.annotate(
156+
_has_good_build=Exists(Build.internal.filter(project=OuterRef("pk"), success=True))
157+
)
150158

151159
# Aliases
152160

readthedocs/rtd_tests/tests/test_project_views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ def setUp(self):
379379
self.project = get(Project, slug="pip", users=[self.user])
380380

381381
def test_dashboard_number_of_queries(self):
382-
for i in range(10):
382+
# NOTE: create more than 15 projects, as we paginate by 15.
383+
for i in range(30):
383384
project = get(
384385
Project,
385386
slug=f"project-{i}",
@@ -398,7 +399,7 @@ def test_dashboard_number_of_queries(self):
398399
state=BUILD_STATE_FINISHED,
399400
)
400401

401-
with self.assertNumQueries(23):
402+
with self.assertNumQueries(12):
402403
r = self.client.get(reverse(("projects_dashboard")))
403404
assert r.status_code == 200
404405

0 commit comments

Comments
 (0)