Optimize subqueries in MariaDB Server for improved performance. This section provides techniques and best practices to ensure your nested queries execute efficiently and enhance overall query speed.
This article describes Condition Pushdown into IN subqueries as implemented in MDEV-12387.
optimizer_switch flag name: condition_pushdown_for_subquery
.
This page is licensed: CC BY-SA / Gnu FDL
Starting from MariaDB 10.3, the optimizer converts certain big IN predicates into IN subqueries.
That is, an IN predicate in the form
COLUMN [NOT] IN (const1, const2, .... )
is converted into an equivalent IN-subquery:
column [NOT] IN (select ... from temporary_table)
which opens new opportunities for the query optimizer.
The conversion happens if the following conditions are met:
the IN list has more than 1000 elements (One can control it through the in_predicate_conversion_threshold parameter).
the [NOT] IN condition is at the top level of the WHERE/ON clause.
The optimization is on by default. MariaDB 10.3.18 (and debug builds prior to that) introduced the in_predicate_conversion_threshold variable. Set to 0
to disable the optimization.
If column
is a key-prefix, MariaDB optimizer will process the condition
COLUMN [NOT] IN (const1, const2, .... )
by trying to construct a range access. If the list is large, the analysis may take a lot of memory and CPU time. The problem gets worse when column
is a part of a multi-column index and the query has conditions on other parts of the index.
Conversion of IN predicates into subqueries bypass the range analysis, which means the query optimization phase will use less CPU and memory.
Possible disadvantages of the conversion are are:
The optimization may convert 'IN LIST elements' key accesses to a table scan (if there is no other usable index for the table)
The estimates for the number of rows matching the IN (...)
are less precise.
This page is licensed: CC BY-SA / Gnu FDL
MySQL (including MySQL 5.6) has only one execution strategy for EXISTS subqueries. The strategy is essentially the straightforward, "naive" execution, without any rewrites.
MariaDB 5.3 introduced a rich set of optimizations for IN subqueries. Since then, it makes sense to convert an EXISTS subquery into an IN so that the new optimizations can be used.
EXISTS
will be converted into IN
in two cases:
Trivially correlated EXISTS subqueries
Semi-join EXISTS
We will now describe these two cases in detail
Often, EXISTS subquery is correlated, but the correlation is trivial. The subquery has form
EXISTS (SELECT ... FROM ... WHERE outer_col= inner_col AND inner_where)
and "outer_col" is the only place where the subquery refers to outside fields. In this case, the subquery can be re-written into uncorrelated IN:
outer_col IN (SELECT inner_col FROM ... WHERE inner_where)
(NULL
values require some special handling, see below). For uncorrelated IN subqueries, MariaDB is able a cost-based choice between two execution strategies:
IN-to-EXISTS (basically, convert back into EXISTS)
That is, converting trivially-correlated EXISTS
into uncorrelated IN
gives query optimizer an option to use Materialization strategy for the subquery.
Currently, EXISTS->IN conversion works only for subqueries that are at top level of the WHERE clause, or are under NOT operation which is directly at top level of the WHERE clause.
If EXISTS
subquery is an AND-part of the WHERE
clause:
SELECT ... FROM outer_tables WHERE EXISTS (SELECT ...) AND ...
then it satisfies the main property of semi-join subqueries:
with semi-join subquery, we're only interested in records of outer_tables that have matches in the subquery
Semi-join optimizer offers a rich set of execution strategies for both correlated and uncorrelated subqueries. The set includes FirstMatch strategy which is an equivalent of how EXISTS suqueries are executed, so we do not lose any opportunities when converting an EXISTS subquery into a semi-join.
In theory, it makes sense to convert all kinds of EXISTS subqueries: convert both correlated and uncorrelated ones, convert irrespectively of whether the subquery has inner=outer equality.
In practice, the subquery will be converted only if it has inner=outer equality. Both correlated and uncorrelated subqueries are converted.
TODO: rephrase this:
IN has complicated NULL-semantics. NOT EXISTS doesn't.
EXISTS-to-IN adds IS NOT NULL before the subquery predicate, when required
The optimization is controlled by the exists_to_in
flag in optimizer_switch. Before MariaDB 10.0.12, the optimization was OFF by default. Since MariaDB 10.0.12, it has been ON by default.
EXISTS-to-IN doesn't handle
subqueries that have GROUP BY, aggregate functions, or HAVING clause
subqueries are UNIONs
a number of degenerate edge cases
This page is licensed: CC BY-SA / Gnu FDL
Certain kinds of IN-subqueries cannot be flattened into semi-joins. These subqueries can be both correlated or non-correlated. In order to provide consistent performance in all cases, MariaDB provides several alternative strategies for these types of subqueries. Whenever several strategies are possible, the optimizer chooses the optimal one based on cost estimates.
The two primary non-semi-join strategies are materialization (also called outside-in materialization), and in-to-exists transformation. Materialization is applicable only for non-correlated subqueries, while in-to-exist can be used both for correlated and non-correlated subqueries.
An IN subquery cannot be flattened into a semi-join in the following cases. The examples below use the World database from the MariaDB regression test suite.
The subquery is located directly or indirectly under an OR operation in the WHERE clause of the outer query.
Query pattern:
SELECT ... FROM ... WHERE (expr1, ..., exprN) [NOT] IN (SELECT ... ) OR expr;
Example:
SELECT Name FROM Country
WHERE (Code IN (SELECT Country FROM City WHERE City.Population > 100000) OR
Name LIKE 'L%') AND
surfacearea > 1000000;
The subquery predicate itself is negated.
Query pattern:
SELECT ... FROM ... WHERE ... (expr1, ..., exprN) NOT IN (SELECT ... ) ...;
Example:
SELECT Country.Name
FROM Country, CountryLanguage
WHERE Code NOT IN (SELECT Country FROM CountryLanguage WHERE Language = 'English')
AND CountryLanguage.Language = 'French'
AND Code = Country;
The subquery is located in the SELECT or HAVING clauses of the outer query.
Query pattern:
SELECT field1, ..., (SELECT ...) WHERE ...;
SELECT ... WHERE ... HAVING (SELECT ...);
Example:
SELECT Name, City.id IN (SELECT capital FROM Country WHERE capital IS NOT NULL) AS is_capital
FROM City
WHERE City.population > 10000000;
The subquery itself is a UNION, while the IN predicate may be anywhere in the query where IN is allowed.
Query pattern:
... [NOT] IN (SELECT ... UNION SELECT ...)
Example:
SELECT * FROM City WHERE (Name, 91) IN
(SELECT Name, round(Population/1000) FROM City WHERE Country = "IND" AND Population > 2500000
UNION
SELECT Name, round(Population/1000) FROM City WHERE Country = "IND" AND Population < 100000);
The basic idea of subquery materialization is to execute the subquery and store its result in an internal temporary table indexed on all its columns. Naturally, this is possible only when the subquery is non-correlated. The IN predicate tests whether its left operand is present in the subquery result. Therefore it is not necessary to store duplicate subquery result rows in the temporary table. Storing only unique subquery rows provides two benefits - the size of the temporary table is smaller, and the index on all its columns can be unique.
If the size of the temporary table is less than the tmp_table_size system variable, the table is a hash-indexed in-memory HEAP table. In the rare cases when the subquery result exceeds this limit, the temporary table is stored on disk in an ARIA or MyISAM B-tree indexed table (ARIA is the default).
Subquery materialization happens on demand during the first execution of the IN predicate. Once the subquery is materialized, the IN predicate is evaluated very efficiently by index lookups of the outer expression into the unique index of the materialized temporary table. If there is a match, IN is TRUE, otherwise IN is FALSE.
An IN predicate may produce a NULL result if there is a NULL value in either of its arguments. Depending on its location in a query, a NULL predicate value is equivalent to FALSE. These are the cases when substituting NULL with FALSE would reject exactly the same result rows. A NULL result of IN is indistinguishable from a FALSE if the IN predicate is:
not negated,
not a function argument,
inside a WHERE or ON clause.
In all these cases the evaluation of IN is performed as described in the previous paragraph via index lookups into the materialized subquery. In all remaining cases when NULL cannot be substituted with FALSE, it is not possible to use index lookups. This is not a limitation in the server, but a consequence of the NULL semantics in the ANSI SQL standard.
Suppose an IN predicate is evaluated as
NULL IN (SELECT
not_null_col FROM t1)
, that is, the left operand of IN is a NULL value, and there are no NULLs in the subquery. In this case the value of IN is neither FALSE, nor TRUE. Instead it is NULL. If we were to perform an index lookup with the NULL as a key, such a value would not be found in not_null_col, and the IN predicate would incorrectly produce a FALSE.
In general, an NULL value on either side of an IN acts as a "wildcard" that matches any value, and if a match exists, the result of IN is NULL. Consider the following example:
If the left argument of IN is the row: (7, NULL, 9)
, and the result of the right subquery operand of IN is the table:
(7, 8, 10)
(6, NULL, NULL)
(7, 11, 9)
The the IN predicate matches the row (7, 11, 9)
, and the result of IN is NULL. Matches where the differing values on either side of the IN arguments are matched by a NULL in the other IN argument, are
called partial matches.
In order to efficiently compute the result of an IN predicate in the presence of NULLs, MariaDB implements two special algorithms forpartial matching, described here in detail.
Rowid-merge partial matching This technique is used when the number of rows in the subquery result is above a certain limit. The technique creates special indexes on some of the columns of the temporary table, and merges them by alternative scanning of each index thus performing an operation similar to set-intersection.
Table scan partial matching This algorithm is used for very small tables when the overhead of the rowid-merge algorithm is not justifiable. Then the server simply scans the materialized subquery, and checks for partial matches. Since this strategy doesn't need any in-memory buffers, it is also used when there is not enough memory to hold the indexes of the rowid-merge strategy.
In principle the subquery materialization strategy is universal, however, due to some technical limitations in the MariaDB server, there are few cases when the server cannot apply this optimization.
BLOB fields Either the left operand of an IN predicate refers to a BLOB field, or the subquery selects one or more BLOBs.
Incomparable fields TODO
In the above cases, the server reverts to theIN-TO-EXISTS transformation.
This optimization is the only subquery execution strategy that existed in older versions of MariaDB and MySQL prior to MariaDB 5.3. We have made various changes and fixed a number of bugs in this code as well, but in essence it remains the same.
Depending on the query and data, either of the two strategies described here may result in orders of magnitude better/worse plan than the other strategy. Older versions of MariaDB and any current MySQL version (including MySQL 5.5, and MySQL 5.6 DMR as of July 2011) implement only the IN-TO-EXISTS transformation. As illustrated below, this strategy is inferior in many common cases to subquery materialization.
Consider the following query over the data of the DBT3 benchmark scale 10. Find customers with top balance in their nations:
SELECT * FROM part
WHERE p_partkey IN
(SELECT l_partkey FROM lineitem
WHERE l_shipdate BETWEEN '1997-01-01' AND '1997-02-01')
ORDER BY p_retailprice DESC LIMIT 10;
The times to run this query is as follows:
Execution time in MariaDB 5.2/MySQL 5.x (any MySQL): > 1 h The query takes more than one hour (we didn't wait longer), which makes it impractical to use subqueries in such cases. The EXPLAIN below shows that the subquery was transformed into a correlated one, which indicates an IN-TO-EXISTS transformation.
+--+------------------+--------+--------------+-------------------+----+------+---------------------------+
|id|select_type |table |type |key |ref |rows |Extra |
+--+------------------+--------+--------------+-------------------+----+------+---------------------------+
| 1|PRIMARY |part |ALL |NULL |NULL|199755|Using where; Using filesort|
| 2|DEPENDENT SUBQUERY|lineitem|index_subquery|i_l_suppkey_partkey|func| 14|Using where |
+--+------------------+--------+--------------+-------------------+----+------+---------------------------+
Execution time in MariaDB 5.3: 43 sec In MariaDB 5.3 it takes less than a minute to run the same query. The EXPLAIN shows that the subquery remains uncorrelated, which is an indication that it is being executed via subquery materialization.
+--+------------+-----------+------+------------------+----+------+-------------------------------+
|id|select_type |table |type |key |ref |rows |Extra |
+--+------------+-----------+------+------------------+----+------+-------------------------------+
| 1|PRIMARY |part |ALL |NULL |NULL|199755|Using temporary; Using filesort|
| 1|PRIMARY |<subquery2>|eq_ref|distinct_key |func| 1| |
| 2|MATERIALIZED|lineitem |range |l_shipdate_partkey|NULL|160060|Using where; Using index |
+--+------------+-----------+------+------------------+----+------+-------------------------------+
The speedup here is practically infinite, because both MySQL and older MariaDB versions cannot complete the query in any reasonable time.
In order to show the benefits of partial matching we extended the customer table from the DBT3 benchmark with two extra columns:
c_pref_nationkey - preferred nation to buy from,
c_pref_brand - preferred brand.
Both columns are prefixed with the percent NULL values in the column, that is, c_pref_nationkey_05 contains 5% NULL values.
Consider the query "Find all customers that didn't buy from a preferred country, and from a preferred brand withing some date ranges":
SELECT count(*)
FROM customer
WHERE (c_custkey, c_pref_nationkey_05, c_pref_brand_05) NOT IN
(SELECT o_custkey, s_nationkey, p_brand
FROM orders, supplier, part, lineitem
WHERE l_orderkey = o_orderkey AND
l_suppkey = s_suppkey AND
l_partkey = p_partkey AND
p_retailprice < 1200 AND
l_shipdate >= '1996-04-01' AND l_shipdate < '1996-04-05' AND
o_orderdate >= '1996-04-01' AND o_orderdate < '1996-04-05');
Execution time in MariaDB 5.2/MySQL 5.x (any MySQL): 40 sec
Execution time in MariaDB 5.3: 2 sec
The speedup for this query is 20 times.
TODO
In certain cases it may be necessary to override the choice of the optimizer. Typically this is needed for benchmarking or testing purposes, or to mimic the behavior of an older version of the server, or if the optimizer made a poor choice.
All the above strategies can be controlled via the following switches inoptimizer_switch system variable.
materialization=on/off In some very special cases, even if materialization was forced, the optimizer may still revert to the IN-TO-EXISTS strategy if materialization is not applicable. In the cases when materialization requres partial matching (because of the presense of NULL values), there are two subordinate switches that control the two partial matching strategies:
partial_match_rowid_merge=on/off This switch controls the Rowid-merge strategy. In addition to this switch, the system variable rowid_merge_buff_size controls the maximum memory available to the Rowid-merge strategy.
partial_match_table_scan=on/off Controls the alternative partial match strategy that performs matching via a table scan.
in_to_exists=on/off This switch controls the IN-TO-EXISTS transformation.
tmp_table_size and max_heap_table_size system variables The tmp_table_size system variable sets the upper limit for internal MEMORY temporary tables. If an internal temporary table exceeds this size, it is converted automatically into a Aria or MyISAM table on disk with a B-tree index. Notice however, that a MEMORY table cannot be larger than max_heap_table_size.
The two main optimizer switches - materialization and in_to_exists cannot be simultaneously off. If both are set to off, the server will issue an error.
This page is licensed: CC BY-SA / Gnu FDL
A DISTINCT clause and a GROUP BY without a corresponding HAVING clause have no meaning in IN/ALL/ANY/SOME/EXISTS subqueries. The reason is that IN/ALL/ANY/SOME/EXISTS only check if an outer row satisfies some condition with respect to all or any row in the subquery result. Therefore is doesn't matter if the subquery has duplicate result rows or not - if some condition is true for some row of the subquery, this condition will be true for all duplicates of this row. Notice that GROUP BY without a corresponding HAVING clause is equivalent to a DISTINCT.
MariaDB 5.3 and later versions automatically remove DISTINCT and GROUP BY without HAVING if these clauses appear in an IN/ALL/ANY/SOME/EXISTS subquery. For instance:
SELECT * FROM t1
WHERE t1.a > ALL(SELECT DISTINCT b FROM t2 WHERE t2.c > 100)
is transformed to:
SELECT * FROM t1
WHERE t1.a > ALL(SELECT b FROM t2 WHERE t2.c > 100)
Removing these unnecessary clauses allows the optimizer to find more efficient query plans because it doesn't need to take care of post-processing the subquery result to satisfy DISTINCT / GROUP BY.
This page is licensed: CC BY-SA / Gnu FDL
MariaDB has a set of optimizations specifically targeted at semi-join subqueries.
A semi-join subquery has a form of
SELECT ... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables ...) AND ...
that is, the subquery is an IN-subquery and it is located in the WHERE clause. The most important part here is
with semi-join subquery, we're only interested in records of outer_tables that have matches in the subquery
Let's see why this is important. Consider a semi-join subquery:
SELECT * FROM Country
WHERE
Continent='Europe' AND
Country.Code IN (SELECT City.country
FROM City
WHERE City.Population>1*1000*1000);
One can execute it "naturally", by starting from countries in Europe and checking if they have populous Cities:
The semi-join property also allows "backwards" execution: we can start from big cities, and check which countries they are in:
To contrast, let's change the subquery to be non-semi-join:
SELECT * FROM Country
WHERE
Country.Continent='Europe' AND
(Country.Code IN (SELECT City.country
FROM City WHERE City.Population>1*1000*1000)
OR Country.SurfaceArea > 100*1000 -- Added this part
);
It is still possible to start from countries, and then check
if a country has any big cities
if it has a large surface area:
The opposite, city-to-country way is not possible. This is not a semi-join.
Semi-join operations are similar to regular relational joins. There is a difference though: with semi-joins, you don't care how many matches an inner table has for an outer row. In the above countries-with-big-cities example, Germany will be returned once, even if it has three cities with populations of more than one million each.
MariaDB uses semi-join optimizations to run IN subqueries.The optimizations are enabled by default. You can disable them by turning off their optimizer_switch like so:
SET optimizer_switch='semijoin=off'
MariaDB has five different semi-join execution strategies:
"Observations about subquery use cases" blog post
This page is licensed: CC BY-SA / Gnu FDL
The goal of the subquery cache is to optimize the evaluation of correlated subqueries by storing results together with correlation parameters in a cache and avoiding re-execution of the subquery in cases where the result is already in the cache.
The cache is on by default. One can switch it off using the optimizer_switch subquery_cache
setting, like so:
SET optimizer_switch='subquery_cache=off';
The efficiency of the subquery cache is visible in 2 statistical variables:
Subquery_cache_hit - Global counter for all subquery cache hits.
Subquery_cache_miss - Global counter for all subquery cache misses.
The session variables tmp_table_size and max_heap_table_size influence the size of in-memory temporary tables in the table used for caching. It cannot grow more than the minimum of the above variables values (see the Implementation section for details).
Your usage of the cache is visible in EXTENDED EXPLAIN
output (warnings) as"<expr_cache><//list of parameters//>(//cached expression//)"
.
For example:
EXPLAIN EXTENDED SELECT * FROM t1 WHERE a IN (SELECT b FROM t2);
+----+--------------------+-------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | PRIMARY | t1 | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where |
| 2 | DEPENDENT SUBQUERY | t2 | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where |
+----+--------------------+-------+------+---------------+------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
SHOW WARNINGS;
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1003 | SELECT `test`.`t1`.`a` AS `a` from `test`.`t1` WHERE <expr_cache><`test`.`t1`.`a`>(<in_optimizer>(`test`.`t1`.`a`,<exists>(SELECT 1 FROM `test`.`t2` WHERE (<cache>(`test`.`t1`.`a`) = `test`.`t2`.`b`)))) |
+-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
In the example above the presence of"<expr_cache><
test.
t1.
a>(...)"
is how you know you are
using the subquery cache.
Every subquery cache creates a temporary table where the results and all parameters are stored. It has a unique index over all parameters. First the cache is created in a MEMORY table (if doing this is impossible the cache becomes disabled for that expression). When the table grows up to the minimum oftmp_table_size
and max_heap_table_size
, the hit rate will be checked:
if the hit rate is really small (<0.2) the cache will be disabled.
if the hit rate is moderate (<0.7) the table will be cleaned (all records deleted) to keep the table in memory
if the hit rate is high the table will be converted to a disk table (for 5.3.0 it can only be converted to a disk table).
hit rate = hit / (hit + miss)
Here are some examples that show the performance impact of the subquery cache (these tests were made on a 2.53 GHz Intel Core 2 Duo MacBook Pro with dbt-3 scale 1 data set).
example
cache on
cache off
gain
hit
miss
hit rate
1
1.01sec
1 hour 31 min 43.33sec
5445x
149975
25
99.98%
2
0.21sec
1.41sec
6.71x
6285
220
96.6%
3
2.54sec
2.55sec
1.00044x
151
461
24.67%
4
1.87sec
1.95sec
0.96x
0
23026
0%
Dataset from DBT-3 benchmark, a query to find customers with balance near top in their nation:
SELECT count(*) FROM customer
WHERE
c_acctbal > 0.8 * (SELECT max(c_acctbal)
FROM customer C
WHERE C.c_nationkey=customer.c_nationkey
GROUP BY c_nationkey);
DBT-3 benchmark, Query #17
SELECT sum(l_extendedprice) / 7.0 AS avg_yearly
FROM lineitem, part
WHERE
p_partkey = l_partkey AND
p_brand = 'Brand#42' AND p_container = 'JUMBO BAG' AND
l_quantity < (SELECT 0.2 * avg(l_quantity) FROM lineitem
WHERE l_partkey = p_partkey);
DBT-3 benchmark, Query #2
SELECT
s_acctbal, s_name, n_name, p_partkey, p_mfgr, s_address, s_phone, s_comment
FROM
part, supplier, partsupp, nation, region
WHERE
p_partkey = ps_partkey AND s_suppkey = ps_suppkey AND p_size = 33
AND p_type LIKE '%STEEL' AND s_nationkey = n_nationkey
AND n_regionkey = r_regionkey AND r_name = 'MIDDLE EAST'
AND ps_supplycost = (
SELECT
min(ps_supplycost)
FROM
partsupp, supplier, nation, region
WHERE
p_partkey = ps_partkey AND s_suppkey = ps_suppkey
AND s_nationkey = n_nationkey AND n_regionkey = r_regionkey
AND r_name = 'MIDDLE EAST'
)
ORDER BY
s_acctbal DESC, n_name, s_name, p_partkey;
DBT-3 benchmark, Query #20
SELECT
s_name, s_address
FROM
supplier, nation
WHERE
s_suppkey IN (
SELECT
DISTINCT (ps_suppkey)
FROM
partsupp, part
WHERE
ps_partkey=p_partkey
AND p_name LIKE 'indian%'
AND ps_availqty > (
SELECT
0.5 * sum(l_quantity)
FROM
lineitem
WHERE
l_partkey = ps_partkey
AND l_suppkey = ps_suppkey
AND l_shipdate >= '1995-01-01'
AND l_shipdate < date_ADD('1995-01-01',INTERVAL 1 year)
)
)
AND s_nationkey = n_nationkey AND n_name = 'JAPAN'
ORDER BY
s_name;
blog post describing impact of subquery cache optimization on queries used by DynamicPageList MediaWiki extension
mariadb-subquery-cache-in-real-use-case.html Another use case from the real world
This page is licensed: CC BY-SA / Gnu FDL
Below is a map showing all types of subqueries allowed in the SQL language, and the optimizer strategies available to handle them.
Uncolored areas represent different kinds of subqueries, for example:
Subqueries that have form x IN (SELECT ...)
Subqueries that are in the FROM
clause
.. and so forth
The size of each uncolored area roughly corresponds to how important (i.e.
frequently used) that kind of subquery is. For
example, x IN (SELECT ...)
queries are the most important,
and EXISTS (SELECT ...)
are relatively unimportant.
Colored areas represent optimizations/execution strategies that are applied to handle various kinds of subqueries.
The color of optimization indicates which version of MySQL/MariaDB it was available in (see legend below)
Some things are not on the map:
MariaDB doesn't evaluate expensive subqueries when doing optimization (this means, EXPLAIN is always fast). MySQL 5.6 has made a progress in this regard but its optimizer will still evaluate certain kinds of subqueries (for example, scalar-context subqueries used in range predicates)
Non-semi-join Materialization (including NULL-aware and partial matching)
Derived table optimizations
This page is licensed: CC BY-SA / Gnu FDL
Table pullout is an optimization for Semi-join subqueries.
Sometimes, a subquery can be re-written as a join. For example:
SELECT *
FROM City
WHERE City.Country IN (SELECT Country.Code
FROM Country
WHERE Country.Population < 100*1000);
If we know that there can be, at most, one country with a given value of Country.Code
(we can tell that if we see that table Country has a primary key or unique index over that column), we can re-write this query as:
SELECT City.*
FROM
City, Country
WHERE
City.Country=Country.Code AND Country.Population < 100*1000;
If one runs EXPLAIN for the above query in MySQL 5.1-5.6 or MariaDB 5.1-5.2, they'll get this plan:
MySQL [world]> EXPLAIN SELECT * FROM City WHERE City.Country IN (SELECT Country.Code FROM Country WHERE Country.Population < 100*1000);
+----+--------------------+---------+-----------------+--------------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+--------------------+---------+-----------------+--------------------+---------+---------+------+------+-------------+
| 1 | PRIMARY | City | ALL | NULL | NULL | NULL | NULL | 4079 | Using where |
| 2 | DEPENDENT SUBQUERY | Country | unique_subquery | PRIMARY,Population | PRIMARY | 3 | func | 1 | Using where |
+----+--------------------+---------+-----------------+--------------------+---------+---------+------+------+-------------+
2 rows in set (0.00 sec)
It shows that the optimizer is going to do a full scan on table City
, and for each city it will do a lookup in table Country
.
If one runs the same query in MariaDB 5.3, they will get this plan:
MariaDB [world]> EXPLAIN SELECT * FROM City WHERE City.Country IN (SELECT Country.Code FROM Country WHERE Country.Population < 100*1000);
+----+-------------+---------+-------+--------------------+------------+---------+--------------------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+-------+--------------------+------------+---------+--------------------+------+-----------------------+
| 1 | PRIMARY | Country | range | PRIMARY,Population | Population | 4 | NULL | 37 | Using index condition |
| 1 | PRIMARY | City | ref | Country | Country | 3 | world.Country.Code | 18 | |
+----+-------------+---------+-------+--------------------+------------+---------+--------------------+------+-----------------------+
2 rows in set (0.00 sec)
The interesting parts are:
Both tables have select_type=PRIMARY
, and id=1
as if they were in one join.
The Country
table is first, followed by the City
table.
Indeed, if one runs EXPLAIN EXTENDED; SHOW WARNINGS, they will see that the subquery is gone and it was replaced with a join:
MariaDB [world]> SHOW warnings\G
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: SELECT `world`.`City`.`ID` AS `ID`,`world`.`City`.`Name` AS
`Name`,`world`.`City`.`Country` AS `Country`,`world`.`City`.`Population` AS
`Population`
FROM `world`.`City` JOIN `world`.`Country` WHERE
((`world`.`City`.`Country` = `world`.`Country`.`Code`) and (`world`.`Country`.
`Population` < (100 * 1000)))
1 row in set (0.00 sec)
Changing the subquery into a join allows feeding the join to the join optimizer, which can make a choice between two possible join orders:
City -> Country
Country -> City
as opposed to the single choice of
City->Country
which we had before the optimization.
In the above example, the choice produces a better query plan. Without pullout, the query plan with a subquery would read (4079 + 1*4079)=8158
table records. With table pullout, the join plan would read (37 + 37 * 18) = 703
rows. Not all row reads are equal, but generally, reading 10
times fewer table records is faster.
Table pullout is possible only in semi-join subqueries.
Table pullout is based on UNIQUE
/PRIMARY
key definitions.
Doing table pullout does not cut off any possible query plans, so MariaDB will always try to pull out as much as possible.
Table pullout is able to pull individual tables out of subqueries to their parent selects. If all tables in a subquery have been pulled out, the subquery (i.e. its semi-join) is removed completely.
One common bit of advice for optimizing MySQL has been "If possible, rewrite your subqueries as joins". Table pullout does exactly that, so manual rewrites are no longer necessary.
There is no separate @@optimizer_switch flag for table pullout. Table pullout can be disabled by switching off all semi-join optimizations withSET @@optimizer_switch='semijoin=off'
command.
This page is licensed: CC BY-SA / Gnu FDL