From 51297a92324976a704279b567ec4f80bb92d7b60 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 12 Aug 2020 23:16:22 -0400 Subject: [PATCH] Fixed #31792 -- Made Exists() reuse QuerySet.exists() optimizations. The latter is already optimized to limit the number of results, avoid selecting unnecessary fields, and drop ordering if possible without altering the semantic of the query. --- django/db/models/expressions.py | 4 +--- django/db/models/sql/compiler.py | 3 --- django/db/models/sql/query.py | 8 +++++++- tests/expressions/tests.py | 22 +++++++++++++++++++++- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 5b5a0ae4aa..a9768919a2 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -1145,11 +1145,9 @@ class Exists(Subquery): output_field = fields.BooleanField() def __init__(self, queryset, negated=False, **kwargs): - # As a performance optimization, remove ordering since EXISTS doesn't - # care about it, just whether or not a row matches. - queryset = queryset.order_by() self.negated = negated super().__init__(queryset, **kwargs) + self.query = self.query.exists() def __invert__(self): clone = self.copy() diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index fc7a7aafae..b321762303 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1127,9 +1127,6 @@ class SQLCompiler: Backends (e.g. NoSQL) can override this in order to use optimized versions of "query has any results." """ - # This is always executed on a query clone, so we can modify self.query - self.query.add_extra({'a': 1}, None, None, None, None, None) - self.query.set_extra_mask(['a']) return bool(self.execute_sql(SINGLE)) def execute_sql(self, result_type=MULTI, chunked_fetch=False, chunk_size=GET_ITERATOR_CHUNK_SIZE): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 659fa87314..4648daf395 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -522,7 +522,7 @@ class Query(BaseExpression): def has_filters(self): return self.where - def has_results(self, using): + def exists(self): q = self.clone() if not q.distinct: if q.group_by is True: @@ -533,6 +533,12 @@ class Query(BaseExpression): q.clear_select_clause() q.clear_ordering(True) q.set_limits(high=1) + q.add_extra({'a': 1}, None, None, None, None, None) + q.set_extra_mask(['a']) + return q + + def has_results(self, using): + q = self.exists() compiler = q.get_compiler(using=using) return compiler.has_results() diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index 2f392a51e6..c15204ce33 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -22,7 +22,7 @@ from django.db.models.functions import ( from django.db.models.sql import constants from django.db.models.sql.datastructures import Join from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature -from django.test.utils import Approximate, isolate_apps +from django.test.utils import Approximate, CaptureQueriesContext, isolate_apps from django.utils.functional import SimpleLazyObject from .models import ( @@ -1738,6 +1738,26 @@ class ValueTests(TestCase): Value(object()).output_field +class ExistsTests(TestCase): + def test_optimizations(self): + with CaptureQueriesContext(connection) as context: + list(Experiment.objects.values(exists=Exists( + Experiment.objects.order_by('pk'), + )).order_by()) + captured_queries = context.captured_queries + self.assertEqual(len(captured_queries), 1) + captured_sql = captured_queries[0]['sql'] + self.assertNotIn( + connection.ops.quote_name(Experiment._meta.pk.column), + captured_sql, + ) + self.assertIn( + connection.ops.limit_offset_sql(None, 1), + captured_sql, + ) + self.assertNotIn('ORDER BY', captured_sql) + + class FieldTransformTests(TestCase): @classmethod