1
0
mirror of https://github.com/django/django.git synced 2025-08-15 06:19:23 +00:00

Fixed #35559 -- Avoided unnecessary query on sliced union of empty queries.

While refs #34125 focused on the SQL correctness of slicing of union of
potentially empty queries it missed an optimization opportunity to avoid
performing a query at all when all queries are empty.

Thanks Lucidiot for the report.
This commit is contained in:
Simon Charette 2024-06-25 23:39:23 -04:00 committed by Sarah Boyce
parent 6b3f55446f
commit 9cb8baa0c4
2 changed files with 44 additions and 34 deletions

View File

@ -583,20 +583,49 @@ class SQLCompiler:
raise DatabaseError( raise DatabaseError(
"ORDER BY not allowed in subqueries of compound statements." "ORDER BY not allowed in subqueries of compound statements."
) )
elif self.query.is_sliced and combinator == "union": parts = []
for compiler in compilers: empty_compiler = None
# A sliced union cannot have its parts elided as some of them
# might be sliced as well and in the event where only a single
# part produces a non-empty resultset it might be impossible to
# generate valid SQL.
compiler.elide_empty = False
parts = ()
selected = self.query.selected
for compiler in compilers: for compiler in compilers:
try: try:
parts.append(self._get_combinator_part_sql(compiler))
except EmptyResultSet:
# Omit the empty queryset with UNION and with DIFFERENCE if the
# first queryset is nonempty.
if combinator == "union" or (combinator == "difference" and parts):
empty_compiler = compiler
continue
raise
if not parts:
raise EmptyResultSet
elif len(parts) == 1 and combinator == "union" and self.query.is_sliced:
# A sliced union cannot be composed of a single component because
# in the event the later is also sliced it might result in invalid
# SQL due to the usage of multiple LIMIT clauses. Prevent that from
# happening by always including an empty resultset query to force
# the creation of an union.
empty_compiler.elide_empty = False
parts.append(self._get_combinator_part_sql(empty_compiler))
combinator_sql = self.connection.ops.set_operators[combinator]
if all and combinator == "union":
combinator_sql += " ALL"
braces = "{}"
if not self.query.subquery and features.supports_slicing_ordering_in_compound:
braces = "({})"
sql_parts, args_parts = zip(
*((braces.format(sql), args) for sql, args in parts)
)
result = [" {} ".format(combinator_sql).join(sql_parts)]
params = []
for part in args_parts:
params.extend(part)
return result, params
def _get_combinator_part_sql(self, compiler):
features = self.connection.features
# If the columns list is limited, then all combined queries # If the columns list is limited, then all combined queries
# must have the same columns list. Set the selects defined on # must have the same columns list. Set the selects defined on
# the query on all combined queries, if not already set. # the query on all combined queries, if not already set.
selected = self.query.selected
if selected is not None and compiler.query.selected is None: if selected is not None and compiler.query.selected is None:
compiler.query = compiler.query.clone() compiler.query = compiler.query.clone()
compiler.query.set_values(selected) compiler.query.set_values(selected)
@ -613,34 +642,9 @@ class SQLCompiler:
or not features.supports_slicing_ordering_in_compound or not features.supports_slicing_ordering_in_compound
): ):
part_sql = "({})".format(part_sql) part_sql = "({})".format(part_sql)
elif ( elif self.query.subquery and features.supports_slicing_ordering_in_compound:
self.query.subquery
and features.supports_slicing_ordering_in_compound
):
part_sql = "({})".format(part_sql) part_sql = "({})".format(part_sql)
parts += ((part_sql, part_args),) return part_sql, part_args
except EmptyResultSet:
# Omit the empty queryset with UNION and with DIFFERENCE if the
# first queryset is nonempty.
if combinator == "union" or (combinator == "difference" and parts):
continue
raise
if not parts:
raise EmptyResultSet
combinator_sql = self.connection.ops.set_operators[combinator]
if all and combinator == "union":
combinator_sql += " ALL"
braces = "{}"
if not self.query.subquery and features.supports_slicing_ordering_in_compound:
braces = "({})"
sql_parts, args_parts = zip(
*((braces.format(sql), args) for sql, args in parts)
)
result = [" {} ".format(combinator_sql).join(sql_parts)]
params = []
for part in args_parts:
params.extend(part)
return result, params
def get_qualify_sql(self): def get_qualify_sql(self):
where_parts = [] where_parts = []

View File

@ -76,6 +76,12 @@ class QuerySetSetOperationTests(TestCase):
qs3 = qs1.union(qs2) qs3 = qs1.union(qs2)
self.assertNumbersEqual(qs3[:1], [0]) self.assertNumbersEqual(qs3[:1], [0])
def test_union_all_none_slice(self):
qs = Number.objects.filter(id__in=[])
with self.assertNumQueries(0):
self.assertSequenceEqual(qs.union(qs), [])
self.assertSequenceEqual(qs.union(qs)[0:0], [])
def test_union_empty_filter_slice(self): def test_union_empty_filter_slice(self):
qs1 = Number.objects.filter(num__lte=0) qs1 = Number.objects.filter(num__lte=0)
qs2 = Number.objects.filter(pk__in=[]) qs2 = Number.objects.filter(pk__in=[])