diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py index 7a8f974d29..0bcb05b1d1 100644 --- a/django/db/backends/mysql/features.py +++ b/django/db/backends/mysql/features.py @@ -48,8 +48,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): END; """ db_functions_convert_bytes_to_str = True - # Alias MySQL's TRADITIONAL to TEXT for consistency with other backends. - supported_explain_formats = {'JSON', 'TEXT', 'TRADITIONAL'} # Neither MySQL nor MariaDB support partial indexes. supports_partial_indexes = False @@ -119,6 +117,15 @@ 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 supported_explain_formats(self): + # Alias MySQL's TRADITIONAL to TEXT for consistency with other + # backends. + formats = {'JSON', 'TEXT', 'TRADITIONAL'} + if not self.connection.mysql_is_mariadb and self.connection.mysql_version >= (8, 0, 16): + formats.add('TREE') + return formats + @cached_property def supports_transactions(self): """ diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 49ee2919f2..ef5d23214e 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -296,6 +296,9 @@ class DatabaseOperations(BaseDatabaseOperations): # Alias MySQL's TRADITIONAL to TEXT for consistency with other backends. if format and format.upper() == 'TEXT': format = 'TRADITIONAL' + 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' prefix = super().explain_query_prefix(format, **options) if format: prefix += ' FORMAT=%s' % format diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index d4a922f3dc..136674be2a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2571,8 +2571,10 @@ because an implementation there isn't straightforward. The ``format`` parameter changes the output format from the databases's default, usually text-based. PostgreSQL supports ``'TEXT'``, ``'JSON'``, ``'YAML'``, and -``'XML'``. MySQL supports ``'TEXT'`` (also called ``'TRADITIONAL'``) and -``'JSON'``. +``'XML'`` formats. MariaDB and MySQL support ``'TEXT'`` (also called +``'TRADITIONAL'``) and ``'JSON'`` formats. MySQL 8.0.16+ also supports an +improved ``'TREE'`` format, which is similar to PostgreSQL's ``'TEXT'`` output +and is used by default, if supported. Some databases accept flags that can return more information about the query. Pass these flags as keyword arguments. For example, when using PostgreSQL:: @@ -2589,6 +2591,10 @@ 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. +.. versionchanged:: 3.1 + + Support for the ``'TREE'`` format on MySQL 8.0.16+ was added. + .. _field-lookups: ``Field`` lookups diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 300c7a2c26..cf77bb15db 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -169,6 +169,8 @@ 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+. + Pagination ~~~~~~~~~~ diff --git a/tests/queries/test_explain.py b/tests/queries/test_explain.py index 85e173bf3f..e3872213a0 100644 --- a/tests/queries/test_explain.py +++ b/tests/queries/test_explain.py @@ -71,9 +71,10 @@ class ExplainTests(TestCase): @unittest.skipUnless(connection.vendor == 'mysql', 'MySQL specific') def test_mysql_text_to_traditional(self): - # Initialize the cached property, if needed, to prevent a query for - # the MySQL version during the QuerySet evaluation. + # Ensure these cached properties are initialized to prevent queries for + # the MariaDB or MySQL version during the QuerySet evaluation. connection.features.needs_explain_extended + connection.features.supported_explain_formats with CaptureQueriesContext(connection) as captured_queries: Tag.objects.filter(name='test').explain(format='text') self.assertEqual(len(captured_queries), 1)