summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2019-10-17 01:57:39 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2019-10-24 12:24:53 +0200
commit7acef095d73322f45dcceb99afa1a4e50b520479 (patch)
tree44390915d053c6789d7263c8c454581909d42fd3
parente645f27907da0098ee08455d9f1acec14b285f98 (diff)
Fixed #23576 -- Implemented multi-alias fast-path deletion in MySQL backend.
This required moving the entirety of DELETE SQL generation to the compiler where it should have been in the first place and implementing a specialized compiler on MySQL/MariaDB. The MySQL compiler relies on the "DELETE table FROM table JOIN" syntax for queries spanning over multiple tables.
-rw-r--r--django/db/backends/mysql/compiler.py20
-rw-r--r--django/db/models/query.py5
-rw-r--r--django/db/models/sql/compiler.py39
-rw-r--r--django/db/models/sql/subqueries.py35
-rw-r--r--tests/delete/tests.py4
5 files changed, 54 insertions, 49 deletions
diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py
index fc74ac1991..96232aff51 100644
--- a/django/db/backends/mysql/compiler.py
+++ b/django/db/backends/mysql/compiler.py
@@ -14,7 +14,25 @@ class SQLInsertCompiler(compiler.SQLInsertCompiler, SQLCompiler):
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, SQLCompiler):
- pass
+ def as_sql(self):
+ if self.single_alias:
+ 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()
+ )
+ ]
+ 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)
class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
diff --git a/django/db/models/query.py b/django/db/models/query.py
index b61c76f80f..bb0bc4da63 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -744,7 +744,10 @@ class QuerySet:
Delete objects found from the given queryset in single direct SQL
query. No signals are sent and there is no protection for cascades.
"""
- return sql.DeleteQuery(self.model).delete_qs(self, using)
+ query = self.query.clone()
+ query.__class__ = sql.DeleteQuery
+ cursor = query.get_compiler(using).execute_sql(CURSOR)
+ return cursor.rowcount if cursor else 0
_raw_delete.alters_data = True
def update(self, **kwargs):
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 9493d0dc3c..638813333d 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -7,13 +7,16 @@ from django.core.exceptions import EmptyResultSet, FieldError
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import OrderBy, Random, RawSQL, Ref, Value
from django.db.models.functions import Cast
-from django.db.models.query_utils import QueryWrapper, select_related_descend
+from django.db.models.query_utils import (
+ Q, QueryWrapper, select_related_descend,
+)
from django.db.models.sql.constants import (
CURSOR, GET_ITERATOR_CHUNK_SIZE, MULTI, NO_RESULTS, ORDER_DIR, SINGLE,
)
from django.db.models.sql.query import Query, get_order_dir
from django.db.transaction import TransactionManagementError
from django.db.utils import DatabaseError, NotSupportedError
+from django.utils.functional import cached_property
from django.utils.hashable import make_hashable
@@ -1344,19 +1347,37 @@ class SQLInsertCompiler(SQLCompiler):
class SQLDeleteCompiler(SQLCompiler):
+ @cached_property
+ def single_alias(self):
+ return sum(self.query.alias_refcount[t] > 0 for t in self.query.alias_map) == 1
+
+ def _as_sql(self, query):
+ result = [
+ 'DELETE FROM %s' % self.quote_name_unless_alias(query.base_table)
+ ]
+ where, params = self.compile(query.where)
+ if where:
+ result.append('WHERE %s' % where)
+ return ' '.join(result), tuple(params)
+
def as_sql(self):
"""
Create the SQL for this query. Return the SQL string and list of
parameters.
"""
- assert len([t for t in self.query.alias_map if self.query.alias_refcount[t] > 0]) == 1, \
- "Can only delete from one table at a time."
- qn = self.quote_name_unless_alias
- result = ['DELETE FROM %s' % qn(self.query.base_table)]
- where, params = self.compile(self.query.where)
- if where:
- result.append('WHERE %s' % where)
- return ' '.join(result), tuple(params)
+ if self.single_alias:
+ return self._as_sql(self.query)
+ innerq = self.query.clone()
+ innerq.__class__ = Query
+ innerq.clear_select_clause()
+ pk = self.query.model._meta.pk
+ innerq.select = [
+ pk.get_col(self.query.get_initial_alias())
+ ]
+ outerq = Query(self.query.model)
+ outerq.where = self.query.where_class()
+ outerq.add_q(Q(pk__in=innerq))
+ return self._as_sql(outerq)
class SQLUpdateCompiler(SQLCompiler):
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index fbc265d113..af48239cdc 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -3,7 +3,6 @@ Query subclasses which provide extra functionality beyond simple data retrieval.
"""
from django.core.exceptions import FieldError
-from django.db import connections
from django.db.models.query_utils import Q
from django.db.models.sql.constants import (
CURSOR, GET_ITERATOR_CHUNK_SIZE, NO_RESULTS,
@@ -41,40 +40,6 @@ class DeleteQuery(Query):
num_deleted += self.do_query(self.get_meta().db_table, self.where, using=using)
return num_deleted
- def delete_qs(self, query, using):
- """
- Delete the queryset in one SQL query (if possible). For simple queries
- this is done by copying the query.query.where to self.query, for
- complex queries by using subquery.
- """
- innerq = query.query
- # Make sure the inner query has at least one table in use.
- innerq.get_initial_alias()
- # The same for our new query.
- self.get_initial_alias()
- innerq_used_tables = tuple([t for t in innerq.alias_map if innerq.alias_refcount[t]])
- if not innerq_used_tables or innerq_used_tables == tuple(self.alias_map):
- # There is only the base table in use in the query.
- self.where = innerq.where
- else:
- pk = query.model._meta.pk
- if not connections[using].features.update_can_self_select:
- # We can't do the delete using subquery.
- values = list(query.values_list('pk', flat=True))
- if not values:
- return 0
- return self.delete_batch(values, using)
- else:
- innerq.clear_select_clause()
- innerq.select = [
- pk.get_col(self.get_initial_alias())
- ]
- values = innerq
- self.where = self.where_class()
- self.add_q(Q(pk__in=values))
- cursor = self.get_compiler(using).execute_sql(CURSOR)
- return cursor.rowcount if cursor else 0
-
class UpdateQuery(Query):
"""An UPDATE SQL query."""
diff --git a/tests/delete/tests.py b/tests/delete/tests.py
index 8cb028344b..d61fdb40c1 100644
--- a/tests/delete/tests.py
+++ b/tests/delete/tests.py
@@ -535,9 +535,7 @@ class FastDeleteTests(TestCase):
a = Avatar.objects.create(desc='a')
User.objects.create(avatar=a)
u2 = User.objects.create()
- expected_queries = 1 if connection.features.update_can_self_select else 2
- self.assertNumQueries(expected_queries,
- User.objects.filter(avatar__desc='a').delete)
+ self.assertNumQueries(1, User.objects.filter(avatar__desc='a').delete)
self.assertEqual(User.objects.count(), 1)
self.assertTrue(User.objects.filter(pk=u2.pk).exists())