mirror of
https://github.com/django/django.git
synced 2025-01-23 08:39:17 +00:00
Fixed #31965 -- Adjusted multi-table fast-deletion on MySQL/MariaDB.
The optimization introduced in 7acef095d73 did 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 in 5b83bae031. Thanks Anton Plotkin for the report. Refs #23576.
This commit is contained in:
parent
38fce49c82
commit
f6405c0b8e
@ -16,13 +16,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()
|
||||
@ -30,10 +32,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):
|
||||
|
@ -1443,6 +1443,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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user