mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +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:
		
				
					committed by
					
						 Sarah Boyce
						Sarah Boyce
					
				
			
			
				
	
			
			
			
						parent
						
							6b3f55446f
						
					
				
				
					commit
					9cb8baa0c4
				
			| @@ -583,50 +583,28 @@ 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: | ||||||
|                 # If the columns list is limited, then all combined queries |                 parts.append(self._get_combinator_part_sql(compiler)) | ||||||
|                 # must have the same columns list. Set the selects defined on |  | ||||||
|                 # the query on all combined queries, if not already set. |  | ||||||
|                 if selected is not None and compiler.query.selected is None: |  | ||||||
|                     compiler.query = compiler.query.clone() |  | ||||||
|                     compiler.query.set_values(selected) |  | ||||||
|                 part_sql, part_args = compiler.as_sql(with_col_aliases=True) |  | ||||||
|                 if compiler.query.combinator: |  | ||||||
|                     # Wrap in a subquery if wrapping in parentheses isn't |  | ||||||
|                     # supported. |  | ||||||
|                     if not features.supports_parentheses_in_compound: |  | ||||||
|                         part_sql = "SELECT * FROM ({})".format(part_sql) |  | ||||||
|                     # Add parentheses when combining with compound query if not |  | ||||||
|                     # already added for all compound queries. |  | ||||||
|                     elif ( |  | ||||||
|                         self.query.subquery |  | ||||||
|                         or not features.supports_slicing_ordering_in_compound |  | ||||||
|                     ): |  | ||||||
|                         part_sql = "({})".format(part_sql) |  | ||||||
|                 elif ( |  | ||||||
|                     self.query.subquery |  | ||||||
|                     and features.supports_slicing_ordering_in_compound |  | ||||||
|                 ): |  | ||||||
|                     part_sql = "({})".format(part_sql) |  | ||||||
|                 parts += ((part_sql, part_args),) |  | ||||||
|             except EmptyResultSet: |             except EmptyResultSet: | ||||||
|                 # Omit the empty queryset with UNION and with DIFFERENCE if the |                 # Omit the empty queryset with UNION and with DIFFERENCE if the | ||||||
|                 # first queryset is nonempty. |                 # first queryset is nonempty. | ||||||
|                 if combinator == "union" or (combinator == "difference" and parts): |                 if combinator == "union" or (combinator == "difference" and parts): | ||||||
|  |                     empty_compiler = compiler | ||||||
|                     continue |                     continue | ||||||
|                 raise |                 raise | ||||||
|         if not parts: |         if not parts: | ||||||
|             raise EmptyResultSet |             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] |         combinator_sql = self.connection.ops.set_operators[combinator] | ||||||
|         if all and combinator == "union": |         if all and combinator == "union": | ||||||
|             combinator_sql += " ALL" |             combinator_sql += " ALL" | ||||||
| @@ -642,6 +620,32 @@ class SQLCompiler: | |||||||
|             params.extend(part) |             params.extend(part) | ||||||
|         return result, params |         return result, params | ||||||
|  |  | ||||||
|  |     def _get_combinator_part_sql(self, compiler): | ||||||
|  |         features = self.connection.features | ||||||
|  |         # If the columns list is limited, then all combined queries | ||||||
|  |         # must have the same columns list. Set the selects defined on | ||||||
|  |         # 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: | ||||||
|  |             compiler.query = compiler.query.clone() | ||||||
|  |             compiler.query.set_values(selected) | ||||||
|  |         part_sql, part_args = compiler.as_sql(with_col_aliases=True) | ||||||
|  |         if compiler.query.combinator: | ||||||
|  |             # Wrap in a subquery if wrapping in parentheses isn't | ||||||
|  |             # supported. | ||||||
|  |             if not features.supports_parentheses_in_compound: | ||||||
|  |                 part_sql = "SELECT * FROM ({})".format(part_sql) | ||||||
|  |             # Add parentheses when combining with compound query if not | ||||||
|  |             # already added for all compound queries. | ||||||
|  |             elif ( | ||||||
|  |                 self.query.subquery | ||||||
|  |                 or not features.supports_slicing_ordering_in_compound | ||||||
|  |             ): | ||||||
|  |                 part_sql = "({})".format(part_sql) | ||||||
|  |         elif self.query.subquery and features.supports_slicing_ordering_in_compound: | ||||||
|  |             part_sql = "({})".format(part_sql) | ||||||
|  |         return part_sql, part_args | ||||||
|  |  | ||||||
|     def get_qualify_sql(self): |     def get_qualify_sql(self): | ||||||
|         where_parts = [] |         where_parts = [] | ||||||
|         if self.where: |         if self.where: | ||||||
|   | |||||||
| @@ -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=[]) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user