mirror of https://github.com/django/django.git
Fixed #29725 -- Removed unnecessary join in QuerySet.count() and exists() on a many to many relation.
Co-Authored-By: Shiwei Chen <april.chen.0615@gmail.com>
This commit is contained in:
parent
0d8fbe2ade
commit
66e47ac69a
|
@ -75,7 +75,7 @@ from django.db import (
|
||||||
router,
|
router,
|
||||||
transaction,
|
transaction,
|
||||||
)
|
)
|
||||||
from django.db.models import Q, Window, signals
|
from django.db.models import Manager, Q, Window, signals
|
||||||
from django.db.models.functions import RowNumber
|
from django.db.models.functions import RowNumber
|
||||||
from django.db.models.lookups import GreaterThan, LessThanOrEqual
|
from django.db.models.lookups import GreaterThan, LessThanOrEqual
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
@ -1121,6 +1121,12 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
queryset._defer_next_filter = True
|
queryset._defer_next_filter = True
|
||||||
return queryset._next_is_sticky().filter(**self.core_filters)
|
return queryset._next_is_sticky().filter(**self.core_filters)
|
||||||
|
|
||||||
|
def get_prefetch_cache(self):
|
||||||
|
try:
|
||||||
|
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
def _remove_prefetched_objects(self):
|
def _remove_prefetched_objects(self):
|
||||||
try:
|
try:
|
||||||
self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
|
self.instance._prefetched_objects_cache.pop(self.prefetch_cache_name)
|
||||||
|
@ -1128,9 +1134,9 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
pass # nothing to clear from cache
|
pass # nothing to clear from cache
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
try:
|
if (cache := self.get_prefetch_cache()) is not None:
|
||||||
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
|
return cache
|
||||||
except (AttributeError, KeyError):
|
else:
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
return self._apply_rel_filters(queryset)
|
return self._apply_rel_filters(queryset)
|
||||||
|
|
||||||
|
@ -1195,6 +1201,45 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def constrained_target(self):
|
||||||
|
# If the through relation's target field's foreign integrity is
|
||||||
|
# enforced, the query can be performed solely against the through
|
||||||
|
# table as the INNER JOIN'ing against target table is unnecessary.
|
||||||
|
if not self.target_field.db_constraint:
|
||||||
|
return None
|
||||||
|
db = router.db_for_read(self.through, instance=self.instance)
|
||||||
|
if not connections[db].features.supports_foreign_keys:
|
||||||
|
return None
|
||||||
|
hints = {"instance": self.instance}
|
||||||
|
manager = self.through._base_manager.db_manager(db, hints=hints)
|
||||||
|
filters = {self.source_field_name: self.instance.pk}
|
||||||
|
# Nullable target rows must be excluded as well as they would have
|
||||||
|
# been filtered out from an INNER JOIN.
|
||||||
|
if self.target_field.null:
|
||||||
|
filters["%s__isnull" % self.target_field_name] = False
|
||||||
|
return manager.filter(**filters)
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
if (
|
||||||
|
superclass is Manager
|
||||||
|
and self.get_prefetch_cache() is None
|
||||||
|
and (constrained_target := self.constrained_target) is not None
|
||||||
|
):
|
||||||
|
return constrained_target.exists()
|
||||||
|
else:
|
||||||
|
return super().exists()
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
if (
|
||||||
|
superclass is Manager
|
||||||
|
and self.get_prefetch_cache() is None
|
||||||
|
and (constrained_target := self.constrained_target) is not None
|
||||||
|
):
|
||||||
|
return constrained_target.count()
|
||||||
|
else:
|
||||||
|
return super().count()
|
||||||
|
|
||||||
def add(self, *objs, through_defaults=None):
|
def add(self, *objs, through_defaults=None):
|
||||||
self._remove_prefetched_objects()
|
self._remove_prefetched_objects()
|
||||||
db = router.db_for_write(self.through, instance=self.instance)
|
db = router.db_for_write(self.through, instance=self.instance)
|
||||||
|
|
|
@ -78,3 +78,15 @@ class InheritedArticleA(AbstractArticle):
|
||||||
|
|
||||||
class InheritedArticleB(AbstractArticle):
|
class InheritedArticleB(AbstractArticle):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NullableTargetArticle(models.Model):
|
||||||
|
headline = models.CharField(max_length=100)
|
||||||
|
publications = models.ManyToManyField(
|
||||||
|
Publication, through="NullablePublicationThrough"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NullablePublicationThrough(models.Model):
|
||||||
|
article = models.ForeignKey(NullableTargetArticle, models.CASCADE)
|
||||||
|
publication = models.ForeignKey(Publication, models.CASCADE, null=True)
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import connection, transaction
|
||||||
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||||
from django.utils.deprecation import RemovedInDjango60Warning
|
from django.utils.deprecation import RemovedInDjango60Warning
|
||||||
|
|
||||||
from .models import Article, InheritedArticleA, InheritedArticleB, Publication, User
|
from .models import (
|
||||||
|
Article,
|
||||||
|
InheritedArticleA,
|
||||||
|
InheritedArticleB,
|
||||||
|
NullablePublicationThrough,
|
||||||
|
NullableTargetArticle,
|
||||||
|
Publication,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManyToManyTests(TestCase):
|
class ManyToManyTests(TestCase):
|
||||||
|
@ -558,10 +566,16 @@ class ManyToManyTests(TestCase):
|
||||||
def test_custom_default_manager_exists_count(self):
|
def test_custom_default_manager_exists_count(self):
|
||||||
a5 = Article.objects.create(headline="deleted")
|
a5 = Article.objects.create(headline="deleted")
|
||||||
a5.publications.add(self.p2)
|
a5.publications.add(self.p2)
|
||||||
self.assertEqual(self.p2.article_set.count(), self.p2.article_set.all().count())
|
with self.assertNumQueries(2) as ctx:
|
||||||
|
self.assertEqual(
|
||||||
|
self.p2.article_set.count(), self.p2.article_set.all().count()
|
||||||
|
)
|
||||||
|
self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
with self.assertNumQueries(2) as ctx:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.p3.article_set.exists(), self.p3.article_set.all().exists()
|
self.p3.article_set.exists(), self.p3.article_set.all().exists()
|
||||||
)
|
)
|
||||||
|
self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
|
||||||
def test_get_prefetch_queryset_warning(self):
|
def test_get_prefetch_queryset_warning(self):
|
||||||
articles = Article.objects.all()
|
articles = Article.objects.all()
|
||||||
|
@ -582,3 +596,73 @@ class ManyToManyTests(TestCase):
|
||||||
instances=articles,
|
instances=articles,
|
||||||
querysets=[Publication.objects.all(), Publication.objects.all()],
|
querysets=[Publication.objects.all(), Publication.objects.all()],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ManyToManyQueryTests(TestCase):
|
||||||
|
"""
|
||||||
|
SQL is optimized to reference the through table without joining against the
|
||||||
|
related table when using count() and exists() functions on a queryset for
|
||||||
|
many to many relations. The optimization applies to the case where there
|
||||||
|
are no filters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.article = Article.objects.create(
|
||||||
|
headline="Django lets you build Web apps easily"
|
||||||
|
)
|
||||||
|
cls.nullable_target_article = NullableTargetArticle.objects.create(
|
||||||
|
headline="The python is good"
|
||||||
|
)
|
||||||
|
NullablePublicationThrough.objects.create(
|
||||||
|
article=cls.nullable_target_article, publication=None
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_foreign_keys")
|
||||||
|
def test_count_join_optimization(self):
|
||||||
|
with self.assertNumQueries(1) as ctx:
|
||||||
|
self.article.publications.count()
|
||||||
|
self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
|
||||||
|
with self.assertNumQueries(1) as ctx:
|
||||||
|
self.article.publications.count()
|
||||||
|
self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
self.assertEqual(self.nullable_target_article.publications.count(), 0)
|
||||||
|
|
||||||
|
def test_count_join_optimization_disabled(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(connection.features, "supports_foreign_keys", False),
|
||||||
|
self.assertNumQueries(1) as ctx,
|
||||||
|
):
|
||||||
|
self.article.publications.count()
|
||||||
|
|
||||||
|
self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_foreign_keys")
|
||||||
|
def test_exists_join_optimization(self):
|
||||||
|
with self.assertNumQueries(1) as ctx:
|
||||||
|
self.article.publications.exists()
|
||||||
|
self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
|
||||||
|
self.article.publications.prefetch_related()
|
||||||
|
with self.assertNumQueries(1) as ctx:
|
||||||
|
self.article.publications.exists()
|
||||||
|
self.assertNotIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
self.assertIs(self.nullable_target_article.publications.exists(), False)
|
||||||
|
|
||||||
|
def test_exists_join_optimization_disabled(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(connection.features, "supports_foreign_keys", False),
|
||||||
|
self.assertNumQueries(1) as ctx,
|
||||||
|
):
|
||||||
|
self.article.publications.exists()
|
||||||
|
|
||||||
|
self.assertIn("JOIN", ctx.captured_queries[0]["sql"])
|
||||||
|
|
||||||
|
def test_prefetch_related_no_queries_optimization_disabled(self):
|
||||||
|
qs = Article.objects.prefetch_related("publications")
|
||||||
|
article = qs.get()
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
article.publications.count()
|
||||||
|
with self.assertNumQueries(0):
|
||||||
|
article.publications.exists()
|
||||||
|
|
Loading…
Reference in New Issue