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.
This commit is contained in:
Simon Charette 2020-08-12 23:16:22 -04:00 committed by Mariusz Felisiak
parent 7f4c9222df
commit 51297a9232
4 changed files with 29 additions and 8 deletions

View File

@ -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()

View File

@ -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):

View File

@ -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()

View File

@ -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