Fixed #31965 -- Adjusted multi-table fast-deletion on MySQL/MariaDB.
The optimization introduced in7acef095d7
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 in5b83bae031
. 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):
|
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
|
||||||
def as_sql(self):
|
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()
|
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 = [
|
result = [
|
||||||
'DELETE %s FROM' % self.quote_name_unless_alias(
|
'DELETE %s FROM' % self.quote_name_unless_alias(
|
||||||
self.query.get_initial_alias()
|
self.query.get_initial_alias()
|
||||||
|
@ -30,10 +32,10 @@ class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
|
||||||
]
|
]
|
||||||
from_sql, from_params = self.get_from_clause()
|
from_sql, from_params = self.get_from_clause()
|
||||||
result.extend(from_sql)
|
result.extend(from_sql)
|
||||||
where, params = self.compile(self.query.where)
|
where_sql, where_params = self.compile(where)
|
||||||
if where:
|
if where_sql:
|
||||||
result.append('WHERE %s' % where)
|
result.append('WHERE %s' % where_sql)
|
||||||
return ' '.join(result), tuple(from_params) + tuple(params)
|
return ' '.join(result), tuple(from_params) + tuple(where_params)
|
||||||
|
|
||||||
|
|
||||||
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
|
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
|
||||||
|
|
|
@ -1443,6 +1443,11 @@ class SQLDeleteCompiler(SQLCompiler):
|
||||||
]
|
]
|
||||||
outerq = Query(self.query.model)
|
outerq = Query(self.query.model)
|
||||||
outerq.where = self.query.where_class()
|
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))
|
outerq.add_q(Q(pk__in=innerq))
|
||||||
return self._as_sql(outerq)
|
return self._as_sql(outerq)
|
||||||
|
|
||||||
|
|
|
@ -55,3 +55,7 @@ Bugfixes
|
||||||
* Fixed a ``QuerySet.order_by()`` crash on PostgreSQL when ordering and
|
* Fixed a ``QuerySet.order_by()`` crash on PostgreSQL when ordering and
|
||||||
grouping by :class:`~django.db.models.JSONField` with a custom
|
grouping by :class:`~django.db.models.JSONField` with a custom
|
||||||
:attr:`~django.db.models.JSONField.decoder` (:ticket:`31956`).
|
: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):
|
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):
|
class Origin(models.Model):
|
||||||
|
|
|
@ -709,3 +709,16 @@ class FastDeleteTests(TestCase):
|
||||||
referer = Referrer.objects.create(origin=origin, unique_field=42)
|
referer = Referrer.objects.create(origin=origin, unique_field=42)
|
||||||
with self.assertNumQueries(2):
|
with self.assertNumQueries(2):
|
||||||
referer.delete()
|
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…
Reference in New Issue