diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index 0bcb05b1d12..6a7f87ba27a 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -117,6 +117,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): # EXTENDED is deprecated (and not required) in MySQL 5.7. return not self.connection.mysql_is_mariadb and self.connection.mysql_version < (5, 7) + @cached_property + def supports_explain_analyze(self): + return self.connection.mysql_is_mariadb or self.connection.mysql_version >= (8, 0, 18) + @cached_property def supported_explain_formats(self): # Alias MySQL's TRADITIONAL to TEXT for consistency with other diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index ef5d23214e3..b801afef4ad 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -299,11 +299,16 @@ class DatabaseOperations(BaseDatabaseOperations): elif not format and 'TREE' in self.connection.features.supported_explain_formats: # Use TREE by default (if supported) as it's more informative. format = 'TREE' + analyze = options.pop('analyze', False) prefix = super().explain_query_prefix(format, **options) - if format: + if analyze and self.connection.features.supports_explain_analyze: + # MariaDB uses ANALYZE instead of EXPLAIN ANALYZE. + prefix = 'ANALYZE' if self.connection.mysql_is_mariadb else prefix + ' ANALYZE' + if format and not (analyze and not self.connection.mysql_is_mariadb): + # Only MariaDB supports the analyze option with formats. prefix += ' FORMAT=%s' % format - if self.connection.features.needs_explain_extended and format is None: - # EXTENDED and FORMAT are mutually exclusive options. + if self.connection.features.needs_explain_extended and not analyze and format is None: + # ANALYZE, EXTENDED, and FORMAT are mutually exclusive options. prefix += ' EXTENDED' return prefix diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 136674be2a7..766a379a13f 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2587,13 +2587,14 @@ Pass these flags as keyword arguments. For example, when using PostgreSQL:: Execution time: 0.058 ms On some databases, flags may cause the query to be executed which could have -adverse effects on your database. For example, PostgreSQL's ``ANALYZE`` flag -could result in changes to data if there are triggers or if a function is -called, even for a ``SELECT`` query. +adverse effects on your database. For example, the ``ANALYZE`` flag supported +by MariaDB, MySQL 8.0.18+, and PostgreSQL could result in changes to data if +there are triggers or if a function is called, even for a ``SELECT`` query. .. versionchanged:: 3.1 - Support for the ``'TREE'`` format on MySQL 8.0.16+ was added. + Support for the ``'TREE'`` format on MySQL 8.0.16+ and ``analyze`` option + on MariaDB and MySQL 8.0.18+ were added. .. _field-lookups: diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index cf77bb15db5..70297b53111 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -169,7 +169,10 @@ Models :class:`~django.db.models.DateTimeField`, and the new :lookup:`iso_week_day` lookup allows querying by an ISO-8601 day of week. -* :meth:`.QuerySet.explain` now supports ``TREE`` format on MySQL 8.0.16+. +* :meth:`.QuerySet.explain` now supports: + + * ``TREE`` format on MySQL 8.0.16+, + * ``analyze`` option on MySQL 8.0.18+ and MariaDB. Pagination ~~~~~~~~~~ diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py index e3872213a06..481924a2e4d 100644 --- a/tests/queries/test_explain.py +++ b/tests/queries/test_explain.py @@ -80,6 +80,25 @@ class ExplainTests(TestCase): self.assertEqual(len(captured_queries), 1) self.assertIn('FORMAT=TRADITIONAL', captured_queries[0]['sql']) + @unittest.skipUnless(connection.vendor == 'mysql', 'MariaDB and MySQL >= 8.0.18 specific.') + def test_mysql_analyze(self): + # Inner skip to avoid module level query for MySQL version. + if not connection.features.supports_explain_analyze: + raise unittest.SkipTest('MariaDB and MySQL >= 8.0.18 specific.') + qs = Tag.objects.filter(name='test') + with CaptureQueriesContext(connection) as captured_queries: + qs.explain(analyze=True) + self.assertEqual(len(captured_queries), 1) + prefix = 'ANALYZE ' if connection.mysql_is_mariadb else 'EXPLAIN ANALYZE ' + self.assertTrue(captured_queries[0]['sql'].startswith(prefix)) + with CaptureQueriesContext(connection) as captured_queries: + qs.explain(analyze=True, format='JSON') + self.assertEqual(len(captured_queries), 1) + if connection.mysql_is_mariadb: + self.assertIn('FORMAT=JSON', captured_queries[0]['sql']) + else: + self.assertNotIn('FORMAT=JSON', captured_queries[0]['sql']) + @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL < 5.7 specific') def test_mysql_extended(self): # Inner skip to avoid module level query for MySQL version.