mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[3.1.x] Fixed #31965 -- Adjusted multi-table fast-deletion on MySQL/MariaDB.
The optimization introduced in7acef095d7did not properly handle deletion involving filters against aggregate annotations. It initially was surfaced by a MariaDB test failure but misattributed to an undocumented change in behavior that resulted in the systemic generation of poorly performing database queries in5b83bae031. Thanks Anton Plotkin for the report. Refs #23576. Backport off6405c0b8efrom master
This commit is contained in:
		
				
					committed by
					
						 Mariusz Felisiak
						Mariusz Felisiak
					
				
			
			
				
	
			
			
			
						parent
						
							655e1ce6b1
						
					
				
				
					commit
					2986ec031d
				
			| @@ -15,13 +15,15 @@ class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler): | ||||
|  | ||||
| class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): | ||||
|     def as_sql(self): | ||||
|         if self.connection.features.update_can_self_select or self.single_alias: | ||||
|         # Prefer the non-standard DELETE FROM syntax over the SQL generated by | ||||
|         # the SQLDeleteCompiler's default implementation when multiple tables | ||||
|         # are involved since MySQL/MariaDB will generate a more efficient query | ||||
|         # plan than when using a subquery. | ||||
|         where, having = self.query.where.split_having() | ||||
|         if self.single_alias or having: | ||||
|             # DELETE FROM cannot be used when filtering against aggregates | ||||
|             # since it doesn't allow for GROUP BY and HAVING clauses. | ||||
|             return super().as_sql() | ||||
|         # MySQL and MariaDB < 10.3.2 doesn't support deletion with a subquery | ||||
|         # which is what the default implementation of SQLDeleteCompiler uses | ||||
|         # when multiple tables are involved. Use the MySQL/MariaDB specific | ||||
|         # DELETE table FROM table syntax instead to avoid performing the | ||||
|         # operation in two queries. | ||||
|         result = [ | ||||
|             'DELETE %s FROM' % self.quote_name_unless_alias( | ||||
|                 self.query.get_initial_alias() | ||||
| @@ -29,10 +31,10 @@ class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler): | ||||
|         ] | ||||
|         from_sql, from_params = self.get_from_clause() | ||||
|         result.extend(from_sql) | ||||
|         where, params = self.compile(self.query.where) | ||||
|         if where: | ||||
|             result.append('WHERE %s' % where) | ||||
|         return ' '.join(result), tuple(from_params) + tuple(params) | ||||
|         where_sql, where_params = self.compile(where) | ||||
|         if where_sql: | ||||
|             result.append('WHERE %s' % where_sql) | ||||
|         return ' '.join(result), tuple(from_params) + tuple(where_params) | ||||
|  | ||||
|  | ||||
| class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler): | ||||
|   | ||||
| @@ -1439,6 +1439,11 @@ class SQLDeleteCompiler(SQLCompiler): | ||||
|         ] | ||||
|         outerq = Query(self.query.model) | ||||
|         outerq.where = self.query.where_class() | ||||
|         if not self.connection.features.update_can_self_select: | ||||
|             # Force the materialization of the inner query to allow reference | ||||
|             # to the target table on MySQL. | ||||
|             sql, params = innerq.get_compiler(connection=self.connection).as_sql() | ||||
|             innerq = RawSQL('SELECT * FROM (%s) subquery' % sql, params) | ||||
|         outerq.add_q(Q(pk__in=innerq)) | ||||
|         return self._as_sql(outerq) | ||||
|  | ||||
|   | ||||
| @@ -55,3 +55,7 @@ Bugfixes | ||||
| * Fixed a ``QuerySet.order_by()`` crash on PostgreSQL when ordering and | ||||
|   grouping by :class:`~django.db.models.JSONField` with a custom | ||||
|   :attr:`~django.db.models.JSONField.decoder` (:ticket:`31956`). | ||||
|  | ||||
| * Fixed a ``QuerySet.delete()`` crash on MySQL, following a performance | ||||
|   regression in Django 3.1 on MariaDB 10.3.2+, when filtering against an | ||||
|   aggregate function (:ticket:`31965`). | ||||
|   | ||||
| @@ -141,7 +141,7 @@ class Base(models.Model): | ||||
|  | ||||
|  | ||||
| class RelToBase(models.Model): | ||||
|     base = models.ForeignKey(Base, models.DO_NOTHING) | ||||
|     base = models.ForeignKey(Base, models.DO_NOTHING, related_name='rels') | ||||
|  | ||||
|  | ||||
| class Origin(models.Model): | ||||
|   | ||||
| @@ -709,3 +709,16 @@ class FastDeleteTests(TestCase): | ||||
|         referer = Referrer.objects.create(origin=origin, unique_field=42) | ||||
|         with self.assertNumQueries(2): | ||||
|             referer.delete() | ||||
|  | ||||
|     def test_fast_delete_aggregation(self): | ||||
|         # Fast-deleting when filtering against an aggregation result in | ||||
|         # a single query containing a subquery. | ||||
|         Base.objects.create() | ||||
|         with self.assertNumQueries(1): | ||||
|             self.assertEqual( | ||||
|                 Base.objects.annotate( | ||||
|                     rels_count=models.Count('rels'), | ||||
|                 ).filter(rels_count=0).delete(), | ||||
|                 (1, {'delete.Base': 1}), | ||||
|             ) | ||||
|         self.assertIs(Base.objects.exists(), False) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user