- 8.2.2.1 Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations
- 8.2.2.2 Optimizing Subqueries with Materialization
- 8.2.2.3 Optimizing Subqueries with the EXISTS Strategy
- 8.2.2.4 Optimizing Derived Tables and View References with Merging or Materialization
The MySQL query optimizer has different strategies available to evaluate subqueries:
- For
IN
(or=ANY
) subqueries, the optimizer has these choices:- Semijoin
- Materialization
EXISTS
strategy
- For
NOT IN
(or<>ALL
) subqueries, the optimizer has these choices:- Materialization
EXISTS
strategy
For derived tables, the optimizer has these choices (which also apply to view references):
- Merge the derived table into the outer query block
- Materialize the derived table to an internal temporary table
The following discussion provides more information about the preceding optimization strategies.
Note
A limitation on UPDATE
and DELETE
statements that use a subquery to modify a single table is that the optimizer does not use semijoin or materialization subquery optimizations. As a workaround, try rewriting them as multiple-table UPDATE
and DELETE
statements that use a join rather than a subquery.
A semijoin is a preparation-time transformation that enables multiple execution strategies such as table pullout, duplicate weedout, first match, loose scan, and materialization. The optimizer uses semijoin strategies to improve subquery execution, as described in this section.
For an inner join between two tables, the join returns a row from one table as many times as there are matches in the other table. But for some questions, the only information that matters is whether there is a match, not the number of matches. Suppose that there are tables named class
and roster
that list classes in a course curriculum and class rosters (students enrolled in each class), respectively. To list the classes that actually have students enrolled, you could use this join:
SELECT class.class_num, class.class_name
FROM class INNER JOIN roster
WHERE class.class_num = roster.class_num;
However, the result lists each class once for each enrolled student. For the question being asked, this is unnecessary duplication of information.
Assuming that class_num
is a primary key in the class
table, duplicate suppression is possible by using SELECT DISTINCT
, but it is inefficient to generate all matching rows first only to eliminate duplicates later.
The same duplicate-free result can be obtained by using a subquery:
SELECT class_num, class_name
FROM class
WHERE class_num IN (SELECT class_num FROM roster);
Here, the optimizer can recognize that the IN
clause requires the subquery to return only one instance of each class number from the roster
table. In this case, the query can use a semijoin; that is, an operation that returns only one instance of each row in class
that is matched by rows in roster
.
Outer join and inner join syntax is permitted in the outer query specification, and table references may be base tables, derived tables, or view references.
In MySQL, a subquery must satisfy these criteria to be handled as a semijoin:
-
It must be an
IN
(or=ANY
) subquery that appears at the top level of theWHERE
orON
clause, possibly as a term in anAND
expression. For example:SELECT ... FROM ot1, ... WHERE (oe1, ...) IN (SELECT ie1, ... FROM it1, ... WHERE ...);
Here,
ot_*
i*
andit_*
i*
represent tables in the outer and inner parts of the query, andoe_*
i*
andie_*
i*
represent expressions that refer to columns in the outer and inner tables. -
It must not contain a
GROUP BY
orHAVING
clause. -
It must not be implicitly grouped (it must contain no aggregate functions).
-
It must not have
ORDER BY
withLIMIT
. -
The statement must not use the
STRAIGHT_JOIN
join type in the outer query. -
The
STRAIGHT_JOIN
modifier must not be present. -
The number of outer and inner tables together must be less than the maximum number of tables permitted in a join.
The subquery may be correlated or uncorrelated. DISTINCT
is permitted, as is LIMIT
unless ORDER BY
is also used.
If a subquery meets the preceding criteria, MySQL converts it to a semijoin and makes a cost-based choice from these strategies:
-
Convert the subquery to a join, or use table pullout and run the query as an inner join between subquery tables and outer tables. Table pullout pulls a table out from the subquery to the outer query.
-
Duplicate Weedout: Run the semijoin as if it was a join and remove duplicate records using a temporary table.
-
FirstMatch: When scanning the inner tables for row combinations and there are multiple instances of a given value group, choose one rather than returning them all. This "shortcuts" scanning and eliminates production of unnecessary rows.
-
LooseScan: Scan a subquery table using an index that enables a single value to be chosen from each subquery's value group.
-
Materialize the subquery into an indexed temporary table that is used to perform a join, where the index is used to remove duplicates. The index might also be used later for lookups when joining the temporary table with the outer tables; if not, the table is scanned. For more information about materialization, see Section 8.2.2.2, “Optimizing Subqueries with Materialization”.
Each of these strategies can be enabled or disabled using the following optimizer_switch
system variable flags:
- The
semijoin
flag controls whether semijoins are used. - If
semijoin
is enabled, thefirstmatch
,loosescan
,duplicateweedout
, andmaterialization
flags enable finer control over the permitted semijoin strategies. - If the
duplicateweedout
semijoin strategy is disabled, it is not used unless all other applicable strategies are also disabled. - If
duplicateweedout
is disabled, on occasion the optimizer may generate a query plan that is far from optimal. This occurs due to heuristic pruning during greedy search, which can be avoided by settingoptimizer_prune_level=0
.
These flags are enabled by default. See Section 8.9.2, “Switchable Optimizations”.
The optimizer minimizes differences in handling of views and derived tables. This affects queries that use the STRAIGHT_JOIN
modifier and a view with an IN
subquery that can be converted to a semijoin. The following query illustrates this because the change in processing causes a change in transformation, and thus a different execution strategy:
CREATE VIEW v AS
SELECT *
FROM t1
WHERE a IN (SELECT b
FROM t2);
SELECT STRAIGHT_JOIN *
FROM t3 JOIN v ON t3.x = v.a;
The optimizer first looks at the view and converts the IN
subquery to a semijoin, then checks whether it is possible to merge the view into the outer query. Because the STRAIGHT_JOIN
modifier in the outer query prevents semijoin, the optimizer refuses the merge, causing derived table evaluation using a materialized table.
EXPLAIN
output indicates the use of semijoin strategies as follows:
- Semijoined tables show up in the outer select. For extended
EXPLAIN
output, the text displayed by a followingSHOW WARNINGS
shows the rewritten query, which displays the semijoin structure. (See Section 8.8.3, “Extended EXPLAIN Output Format”.) From this you can get an idea about which tables were pulled out of the semijoin. If a subquery was converted to a semijoin, you will see that the subquery predicate is gone and its tables andWHERE
clause were merged into the outer query join list andWHERE
clause. - Temporary table use for Duplicate Weedout is indicated by
Start temporary
andEnd temporary
in theExtra
column. Tables that were not pulled out and are in the range ofEXPLAIN
output rows covered byStart temporary
andEnd temporary
have theirrowid
in the temporary table. FirstMatch(*
tbl_name*)
in theExtra
column indicates join shortcutting.LooseScan(*
m*..*
n*)
in theExtra
column indicates use of the LooseScan strategy.m
andn
are key part numbers.- Temporary table use for materialization is indicated by rows with a
select_type
value ofMATERIALIZED
and rows with atable
value of<subquery*
N*>
.
The optimizer uses materialization to enable more efficient subquery processing. Materialization speeds up query execution by generating a subquery result as a temporary table, normally in memory. The first time MySQL needs the subquery result, it materializes that result into a temporary table. Any subsequent time the result is needed, MySQL refers again to the temporary table. The optimizer may index the table with a hash index to make lookups fast and inexpensive. The index contains unique values to eliminate duplicates and make the table smaller.
Subquery materialization uses an in-memory temporary table when possible, falling back to on-disk storage if the table becomes too large. See Section 8.4.4, “Internal Temporary Table Use in MySQL”.
If materialization is not used, the optimizer sometimes rewrites a noncorrelated subquery as a correlated subquery. For example, the following IN
subquery is noncorrelated (where_condition
involves only columns from t2
and not t1
):
SELECT * FROM t1
WHERE t1.a IN (SELECT t2.b FROM t2 WHERE where_condition);
The optimizer might rewrite this as an EXISTS
correlated subquery:
SELECT * FROM t1
WHERE EXISTS (SELECT t2.b FROM t2 WHERE where_condition AND t1.a=t2.b);
Subquery materialization using a temporary table avoids such rewrites and makes it possible to execute the subquery only once rather than once per row of the outer query.
For subquery materialization to be used in MySQL, the optimizer_switch
system variable materialization
flag must be enabled. (See Section 8.9.2, “Switchable Optimizations”.) With the materialization
flag enabled, materialization applies to subquery predicates that appear anywhere (in the select list, WHERE
, ON
, GROUP BY
, HAVING
, or ORDER BY
), for predicates that fall into any of these use cases:
-
The predicate has this form, when no outer expression
oe_i
or inner expressionie_i
is nullable.N
is 1 or larger.(oe_1, oe_2, ..., oe_N) [NOT] IN (SELECT ie_1, i_2, ..., ie_N ...)
-
The predicate has this form, when there is a single outer expression
oe
and inner expressionie
. The expressions can be nullable.oe [NOT] IN (SELECT ie ...)
-
The predicate is
IN
orNOT IN
and a result ofUNKNOWN
(NULL
) has the same meaning as a result ofFALSE
.
The following examples illustrate how the requirement for equivalence of UNKNOWN
and FALSE
predicate evaluation affects whether subquery materialization can be used. Assume that where_condition
involves columns only from t2
and not t1
so that the subquery is noncorrelated.
This query is subject to materialization:
SELECT * FROM t1
WHERE t1.a IN (SELECT t2.b FROM t2 WHERE where_condition);
Here, it does not matter whether the IN
predicate returns UNKNOWN
or FALSE
. Either way, the row from t1
is not included in the query result.
An example where subquery materialization is not used is the following query, where t2.b
is a nullable column:
SELECT * FROM t1
WHERE (t1.a,t1.b) NOT IN (SELECT t2.a,t2.b FROM t2
WHERE where_condition);
The following restrictions apply to the use of subquery materialization:
- The types of the inner and outer expressions must match. For example, the optimizer might be able to use materialization if both expressions are integer or both are decimal, but cannot if one expression is integer and the other is decimal.
- The inner expression cannot be a
BLOB
.
Use of EXPLAIN
with a query provides some indication of whether the optimizer uses subquery materialization:
- Compared to query execution that does not use materialization,
select_type
may change fromDEPENDENT SUBQUERY
toSUBQUERY
. This indicates that, for a subquery that would be executed once per outer row, materialization enables the subquery to be executed just once. - For extended
EXPLAIN
output, the text displayed by a followingSHOW WARNINGS
includesmaterialize
andmaterialized-subquery
.
Certain optimizations are applicable to comparisons that use the IN
(or =ANY
) operator to test subquery results. This section discusses these optimizations, particularly with regard to the challenges that NULL
values present. The last part of the discussion suggests how you can help the optimizer.
Consider the following subquery comparison:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
MySQL evaluates queries “from outside to inside.” That is, it first obtains the value of the outer expression outer_expr
, and then runs the subquery and captures the rows that it produces.
A very useful optimization is to “inform” the subquery that the only rows of interest are those where the inner expression inner_expr
is equal to outer_expr
. This is done by pushing down an appropriate equality into the subquery's WHERE
clause to make it more restrictive. The converted comparison looks like this:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
After the conversion, MySQL can use the pushed-down equality to limit the number of rows it must examine to evaluate the subquery.
More generally, a comparison of N
values to a subquery that returns N
-value rows is subject to the same conversion. If oe_i
and ie_i
represent corresponding outer and inner expression values, this subquery comparison:
(oe_1, ..., oe_N) IN
(SELECT ie_1, ..., ie_N FROM ... WHERE subquery_where)
Becomes:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND oe_1 = ie_1
AND ...
AND oe_N = ie_N)
For simplicity, the following discussion assumes a single pair of outer and inner expression values.
The conversion just described has its limitations. It is valid only if we ignore possible NULL
values. That is, the “pushdown” strategy works as long as both of these conditions are true:
-
outer_expr
andinner_expr
cannot beNULL
. -
You need not distinguish
NULL
fromFALSE
subquery results. If the subquery is a part of anOR
orAND
expression in theWHERE
clause, MySQL assumes that you do not care. Another instance where the optimizer notices thatNULL
andFALSE
subquery results need not be distinguished is this construct:... WHERE outer_expr IN (subquery)
In this case, the
WHERE
clause rejects the row whetherIN (*
subquery*)
returnsNULL
orFALSE
.
When either or both of those conditions do not hold, optimization is more complex.
Suppose that outer_expr
is known to be a non-NULL
value but the subquery does not produce a row such that outer_expr
= inner_expr
. Then *
outer_expr* IN (SELECT ...)
evaluates as follows:
NULL
, if theSELECT
produces any row whereinner_expr
isNULL
FALSE
, if theSELECT
produces only non-NULL
values or produces nothing
In this situation, the approach of looking for rows with *
outer_expr* = *
inner_expr*
is no longer valid. It is necessary to look for such rows, but if none are found, also look for rows where inner_expr
is NULL
. Roughly speaking, the subquery can be converted to something like this:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND
(outer_expr=inner_expr OR inner_expr IS NULL))
The need to evaluate the extra IS NULL
condition is why MySQL has the ref_or_null
access method:
mysql> EXPLAIN
SELECT outer_expr IN (SELECT t2.maybe_null_key
FROM t2, t3 WHERE ...)
FROM t1;
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: t1
...
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: t2
type: ref_or_null
possible_keys: maybe_null_key
key: maybe_null_key
key_len: 5
ref: func
rows: 2
Extra: Using where; Using index
...
The unique_subquery
and index_subquery
subquery-specific access methods also have “or NULL
” variants.
The additional OR ... IS NULL
condition makes query execution slightly more complicated (and some optimizations within the subquery become inapplicable), but generally this is tolerable.
The situation is much worse when outer_expr
can be NULL
. According to the SQL interpretation of NULL
as “unknown value,” NULL IN (SELECT *
inner_expr* ...)
should evaluate to:
For proper evaluation, it is necessary to be able to check whether the SELECT
has produced any rows at all, so *
outer_expr* = *
inner_expr*
cannot be pushed down into the subquery. This is a problem because many real world subqueries become very slow unless the equality can be pushed down.
Essentially, there must be different ways to execute the subquery depending on the value of outer_expr
.
The optimizer chooses SQL compliance over speed, so it accounts for the possibility that outer_expr
might be NULL
:
-
If
outer_expr
isNULL
, to evaluate the following expression, it is necessary to execute theSELECT
to determine whether it produces any rows:NULL IN (SELECT inner_expr FROM ... WHERE subquery_where)
It is necessary to execute the original
SELECT
here, without any pushed-down equalities of the kind mentioned previously. -
On the other hand, when
outer_expr
is notNULL
, it is absolutely essential that this comparison:outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
Be converted to this expression that uses a pushed-down condition:
EXISTS (SELECT 1 FROM ... WHERE subquery_where AND outer_expr=inner_expr)
Without this conversion, subqueries will be slow.
To solve the dilemma of whether or not to push down conditions into the subquery, the conditions are wrapped within “trigger” functions. Thus, an expression of the following form:
outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where)
Is converted into:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND trigcond(outer_expr=inner_expr))
More generally, if the subquery comparison is based on several pairs of outer and inner expressions, the conversion takes this comparison:
(oe_1, ..., oe_N) IN (SELECT ie_1, ..., ie_N FROM ... WHERE subquery_where)
And converts it to this expression:
EXISTS (SELECT 1 FROM ... WHERE subquery_where
AND trigcond(oe_1=ie_1)
AND ...
AND trigcond(oe_N=ie_N)
)
Each trigcond(*
X*)
is a special function that evaluates to the following values:
X
when the “linked” outer expressionoe_i
is notNULL
TRUE
when the “linked” outer expressionoe_i
isNULL
Note
Trigger functions are not triggers of the kind that you create with CREATE TRIGGER
.
Equalities that are wrapped within trigcond()
functions are not first class predicates for the query optimizer. Most optimizations cannot deal with predicates that may be turned on and off at query execution time, so they assume any trigcond(*
X*)
to be an unknown function and ignore it. Triggered equalities can be used by those optimizations:
- Reference optimizations:
trigcond(*
X*=*
Y* [OR *
Y* IS NULL])
can be used to constructref
,eq_ref
, orref_or_null
table accesses. - Index lookup-based subquery execution engines:
trigcond(*
X*=*
Y*)
can be used to constructunique_subquery
orindex_subquery
accesses. - Table-condition generator: If the subquery is a join of several tables, the triggered condition is checked as soon as possible.
When the optimizer uses a triggered condition to create some kind of index lookup-based access (as for the first two items of the preceding list), it must have a fallback strategy for the case when the condition is turned off. This fallback strategy is always the same: Do a full table scan. In EXPLAIN
output, the fallback shows up as Full scan on NULL key
in the Extra
column:
mysql> EXPLAIN SELECT t1.col1,
t1.col1 IN (SELECT t2.key1 FROM t2 WHERE t2.col2=t1.col2) FROM t1\G
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: t1
...
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: t2
type: index_subquery
possible_keys: key1
key: key1
key_len: 5
ref: func
rows: 2
Extra: Using where; Full scan on NULL key
If you run EXPLAIN
followed by SHOW WARNINGS
, you can see the triggered condition:
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: select `test`.`t1`.`col1` AS `col1`,
<in_optimizer>(`test`.`t1`.`col1`,
<exists>(<index_lookup>(<cache>(`test`.`t1`.`col1`) in t2
on key1 checking NULL
where (`test`.`t2`.`col2` = `test`.`t1`.`col2`) having
trigcond(<is_not_null_test>(`test`.`t2`.`key1`))))) AS
`t1.col1 IN (select t2.key1 from t2 where t2.col2=t1.col2)`
from `test`.`t1`
The use of triggered conditions has some performance implications. A NULL IN (SELECT ...)
expression now may cause a full table scan (which is slow) when it previously did not. This is the price paid for correct results (the goal of the trigger-condition strategy is to improve compliance, not speed).
For multiple-table subqueries, execution of NULL IN (SELECT ...)
is particularly slow because the join optimizer does not optimize for the case where the outer expression is NULL
. It assumes that subquery evaluations with NULL
on the left side are very rare, even if there are statistics that indicate otherwise. On the other hand, if the outer expression might be NULL
but never actually is, there is no performance penalty.
To help the query optimizer better execute your queries, use these suggestions:
-
Declare a column as
NOT NULL
if it really is. This also helps other aspects of the optimizer by simplifying condition testing for the column. -
If you need not distinguish a
NULL
fromFALSE
subquery result, you can easily avoid the slow execution path. Replace a comparison that looks like this:outer_expr IN (SELECT inner_expr FROM ...)
with this expression:
(outer_expr IS NOT NULL) AND (outer_expr IN (SELECT inner_expr FROM ...))
Then
NULL IN (SELECT ...)
is never evaluated because MySQL stops evaluatingAND
parts as soon as the expression result is clear.Another possible rewrite:
EXISTS (SELECT inner_expr FROM ... WHERE inner_expr=outer_expr)
This would apply when you need not distinguish
NULL
fromFALSE
subquery results, in which case you may actually wantEXISTS
.
The subquery_materialization_cost_based
flag of the optimizer_switch
system variable enables control over the choice between subquery materialization and IN
-to-EXISTS
subquery transformation. See Section 8.9.2, “Switchable Optimizations”.
The optimizer can handle derived table references using two strategies (which also apply to view references):
- Merge the derived table into the outer query block
- Materialize the derived table to an internal temporary table
Example 1:
SELECT * FROM (SELECT * FROM t1) AS derived_t1;
With merging of the derived table derived_t1
, that query is executed similar to:
SELECT * FROM t1;
Example 2:
SELECT *
FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2 ON t1.f2=derived_t2.f1
WHERE t1.f1 > 0;
With merging of the derived table derived_t2
, that query is executed similar to:
SELECT t1.*, t2.f1
FROM t1 JOIN t2 ON t1.f2=t2.f1
WHERE t1.f1 > 0;
With materialization, derived_t1
and derived_t2
are each treated as a separate table within their respective queries.
The optimizer handles derived tables and view references the same way: It avoids unnecessary materialization whenever possible, which enables pushing down conditions from the outer query to derived tables and produces more efficient execution plans. (For an example, see Section 8.2.2.2, “Optimizing Subqueries with Materialization”.)
If merging would result in an outer query block that references more than 61 base tables, the optimizer chooses materialization instead.
The optimizer propagates an ORDER BY
clause in a derived table or view reference to the outer query block if these conditions are all true:
- The outer query is not grouped or aggregated.
- The outer query does not specify
DISTINCT
,HAVING
, orORDER BY
. - The outer query has this derived table or view reference as the only source in the
FROM
clause.
Otherwise, the optimizer ignores the ORDER BY
clause.
The following means are available to influence whether the optimizer attempts to merge derived tables and view references into the outer query block:
-
The
derived_merge
flag of theoptimizer_switch
system variable can be used, assuming that no other rule prevents merging. See Section 8.9.2, “Switchable Optimizations”. By default, the flag is enabled to permit merging. Disabling the flag prevents merging and avoidsER_UPDATE_TABLE_USED
errors.The
derived_merge
flag also applies to views that contain noALGORITHM
clause. Thus, if anER_UPDATE_TABLE_USED
error occurs for a view reference that uses an expression equivalent to the subquery, addingALGORITHM=TEMPTABLE
to the view definition prevents merging and takes precedence over thederived_merge
value. -
It is possible to disable merging by using in the subquery any constructs that prevent merging, although these are not as explicit in their effect on materialization. Constructs that prevent merging are the same for derived tables and view references:
The derived_merge
flag also applies to views that contain no ALGORITHM
clause. Thus, if an ER_UPDATE_TABLE_USED
error occurs for a view reference that uses an expression equivalent to the subquery, adding ALGORITHM=TEMPTABLE
to the view definition prevents merging and takes precedence over the current derived_merge
value.
If the optimizer chooses the materialization strategy rather than merging for a derived table, it handles the query as follows:
- The optimizer postpones derived table materialization until its contents are needed during query execution. This improves performance because delaying materialization may result in not having to do it at all. Consider a query that joins the result of a derived table to another table: If the optimizer processes that other table first and finds that it returns no rows, the join need not be carried out further and the optimizer can completely skip materializing the derived table.
- During query execution, the optimizer may add an index to a derived table to speed up row retrieval from it.
Consider the following EXPLAIN
statement, for a SELECT
query that contains a derived table:
EXPLAIN SELECT * FROM (SELECT * FROM t1) AS derived_t1;
The optimizer avoids materializing the derived table by delaying it until the result is needed during SELECT
execution. In this case, the query is not executed (because it occurs in an EXPLAIN
statement), so the result is never needed.
Even for queries that are executed, delay of derived table materialization may enable the optimizer to avoid materialization entirely. When this happens, query execution is quicker by the time needed to perform materialization. Consider the following query, which joins the result of a derived table to another table:
SELECT *
FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2
ON t1.f2=derived_t2.f1
WHERE t1.f1 > 0;
If the optimization processes t1
first and the WHERE
clause produces an empty result, the join must necessarily be empty and the derived table need not be materialized.
For cases when a derived table requires materialization, the optimizer may add an index to the materialized table to speed up access to it. If such an index enables ref
access to the table, it can greatly reduce amount of data read during query execution. Consider the following query:
SELECT *
FROM t1 JOIN (SELECT DISTINCT f1 FROM t2) AS derived_t2
ON t1.f1=derived_t2.f1;
The optimizer constructs an index over column f1
from derived_t2
if doing so would enable use of ref
access for the lowest cost execution plan. After adding the index, the optimizer can treat the materialized derived table the same as a regular table with an index, and it benefits similarly from the generated index. The overhead of index creation is negligible compared to the cost of query execution without the index. If ref
access would result in higher cost than some other access method, the optimizer creates no index and loses nothing.
For optimizer trace output, a merged derived table or view reference is not shown as a node. Only its underlying tables appear in the top query's plan.
https://dev.mysql.com/doc/refman/5.7/en/subquery-optimization.html