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