Hints¶
As well as adjusting the cost constants in the mysql
system database, MySQL also provides a method to override how plans are selected. Using query hints is useful for two distinct reasons:
- In debugging.
EXPLAIN
shows a decision that was made based on the metadata that was available. Adding a hint allows you to force some other execution plans and compare the actual execution time. - In production. If while debugging you notice that an alternative was sufficiently faster, you may choose to add a hint to speed up execution.
I would say that while I frequently use hints in debugging, I am always careful when introducing a hint in production. These can create a maintenance burden because as data distribution and indexes change over time, hints can lock you into a specific plan chosen at one point in time.
Best practice suggests that you should always revisit hints after a major MySQL version upgrade (for example, when upgrading to MySQL 9.0) [1]. You may find that as new optimizations are introduced, you are able to retire a number of your hints.
Old Style Hints¶
Earlier versions of MySQL supported only a series of hints that were an extension of SQL syntax directly. For example:
# Join tables in the order specified
SELECT STRAIGHT_JOIN Country.Name as CountryName, City.Name AS City
FROM Country INNER JOIN City ON City.CountryCode=Country.Code;
# Force usage of an index
SELECT * FROM Country FORCE INDEX (p)
WHERE continent='Asia' and population > 5000000;
# Ignore an index
SELECT * FROM Country IGNORE INDEX (p)
WHERE continent='Asia' and population > 5000000;
# Suggest an index over other indexes
SELECT * FROM Country USE INDEX (p)
WHERE continent='Asia' and population > 5000000;
While these hints continue to be supported in MySQL 8.0, they are partially replaced by comment-style hints. In Example 5 we can see that the p (population)
index avoided in Example 2 is being selected due to the use of a FORCE INDEX
hint. Despite the FORCE INDEX
, EXPLAIN
still shows its true cost as 152.21 versus the table scan cost of approximately 53.
Example 5: Forcing the index to be used despite its cost
EXPLAIN FORMAT=JSON
SELECT * FROM Country FORCE INDEX (p) WHERE continent='Asia' and population > 5000000;
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "152.21" # Cost is greater than table scan
},
"table": {
"table_name": "Country",
"access_type": "range",
"possible_keys": [
"p"
],
"key": "p",
"used_key_parts": [
"Population"
],
"key_length": "4",
"rows_examined_per_scan": 108,
"rows_produced_per_join": 15,
"filtered": "14.29",
"index_condition": "(`world`.`Country`.`Population` > 5000000)",
"cost_info": {
"read_cost": "149.12",
"eval_cost": "3.09",
"prefix_cost": "152.21",
"data_read_per_join": "3K"
},
"used_columns": [
"Code",
"Name",
"Continent",
"Region",
"SurfaceArea",
"IndepYear",
"Population",
"LifeExpectancy",
"GNP",
"GNPOld",
"LocalName",
"GovernmentForm",
"HeadOfState",
"Capital",
"Code2"
],
"attached_condition": "(`world`.`Country`.`Continent` = 'Asia')"
}
}
}
New Comment-Style Hints¶
MySQL 8.0 extends the new style of comment-style hints introduced in MySQL 5.7, with the ability to control the join order of tables (similar to STRAIGHT_JOIN
). I prefer these hints over the old SQL-syntax-extension hints for three reasons:
- They are easy to read and write by separating SQL’s declarative nature from something that is implying how to execute.
- They have clear semantics as hints and not directives. This means that if a hint is unable to operate it will result in a statement warning and not an error. This contrasts with hints like
FORCE INDEX
, where a missing index will result in an error. This behavior is similar to hints in the Oracle database, for example. - They are more fine grained in control. This grants the DBA more flexibility in hint usage.
Hint Name | Description |
---|---|
BKA , NO_BKA |
Enable or disable the Batched Key Access optimization for specific tables (this optimization is off by default, but can be controlled by the optimizer_switch). |
BNL , NO_BNL |
Enable or disable the Block Nested Loop optimization for specified tables. |
MAX_EXECUTION_TIME |
Set a maximum execution time for the query to run in milliseconds.
This hint currently only affects SELECT statements. |
MRR , NO_MRR |
Effects Multi-Range Read optimization |
NO_ICP |
Effects Index Condition Pushdown optimization |
NO_RANGE_OPTIMIZATION |
Disables range optimization for the specified tables or indexes. |
QB_NAME |
Assign a name to query block |
SEMIJOIN ,
NO_SEMIJOIN |
Control semi-join strategy for a subquery (options include
DUPSWEEDOUT , FIRSTMATCH , LOOSESCAN , MATERIALIZATION ). |
SUBQUERY |
Similar to SEMIJOIN /NO_SEMIJOIN . Allows control of subquery strategy,
including IN-to-EXISTS optimizations. |
In Example 6, range optimization has been disabled for a specific table. This results in the p (Population)
index being ignored, despite being extremely selective. There are only two rows in the table with a population >1 Billion.
Example 6: Disabling range optimization means the index cannot be used
EXPLAIN FORMAT=JSON
SELECT /*+NO_RANGE_OPTIMIZATION(Country) */ * FROM Country
WHERE Population > 1000000000 AND Continent='Asia';
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "56.80"
},
"table": {
"table_name": "Country",
"access_type": "ALL", # The access method is a table scan
"possible_keys": [ # The possible key can only be used as a
"p" # range scan which has been disabled
],
"rows_examined_per_scan": 239,
"rows_produced_per_join": 11,
"filtered": "4.76",
"cost_info": {
"read_cost": "54.52",
"eval_cost": "2.28",
"prefix_cost": "56.80",
"data_read_per_join": "2K"
},
"used_columns": [
"Code",
"Name",
"Continent",
"Region",
"SurfaceArea",
"IndepYear",
"Population",
"LifeExpectancy",
"GNP",
"GNPOld",
"LocalName",
"GovernmentForm",
"HeadOfState",
"Capital",
"Code2"
],
"attached_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 1000000000))"
}
}
}
[1] | http://mysqlserverteam.com/what-to-do-with-optimizer-hints-after-an-upgrade/ |