介绍

对于 MySQL 5.6 以及之前的版本来说,查询优化器就像是一个黑盒子一样,
只能通过 explain 语句查看最后优化器决定使用的执行计划,却无法知道查询优化器为什么做这个决策。
在 MySQL 5.6 以及之后的版本中,设计 MySQL 的人提出了一个 optimizer trace 的功能,
这个功能可以方便的查看查询优化器生成执行计划的整个过程。
这个功能的开启与关闭由系统变量 optimizer_trace 决定:show variables like ‘optimizer_trace’;
图片.png
enabled 的值为 off,表明这个功能默认是关闭的。
one_line 的值是控制输出格式的,如果值为 on,那么所有输出都将在一行中展示。
如果想打开这个功能,必须首先把 enabled 的值改为 on:set optimizer_trace=”enabled=on”;
图片.png
然后就可以输入要查看优化过程的查询语句,当该查询语句执行完成后,就可以到 information_schema 数据库下的 optimizer_trace 表中查看完整的优化过程。
这个 optimizer_trace 表共有 4 个列,分别是:

  • query:表示我们的查询语句。
  • trace:表示优化过程的 json 格式文本。
  • missing_bytes_beyound_max_mem_size:由于优化过程可能会输出很多,如果超过某个限制,多余的文本将不会被显示,这个字段展示了被忽略文本的字节数。
  • insufficient_privileges:表示是否没有权限查看优化过程,默认值是 0,只有某些特殊情况下才会是 1。0 代表有权限,1 代表无权限。(单词的意思为:权限不够)

完整的使用 optimizer trace 功能的步骤如下:

json 文本

  1. # 1. 打开 optimizer trace 功能 (默认是关闭的):
  2. set optimizer_trace="enabled=on";
  3. # 2. 这里输入你自己的查询语句
  4. select ...;
  5. # 3. 从 optimizer_trace 表中查看上一个查询的优化过程
  6. select * from information_schema.OPTIMIZER_TRACE;
  7. # 4. 如果需要观察其他语句执行的优化过程,重复上边的第 2、3 步
  8. # 5. 停止查看语句的优化过程,把 optimizer trace 功能关闭
  9. set optimizer_trace="enabled=off";

比如:explain select * from s1 where key1 > ‘a’ and key2 < 1000000 and key3 in ( ‘a’, ‘b’, ‘c’ ) and common_field = ‘abc’;
执行计划如下表:

id select_type table type possible_keys key rows filtered
1 simple s1 range idx_key2,idx_key1,idx_key3 idx_key2 12 0.42

可以看到该查询可能使用到的索引有 3 个,
那么为什么查询优化器最终选择了 idx_key2 而不选择其他的索引或者全表扫描呢?
这时候就可以通过 otpimzer trace 功能来查看查询优化器的具体工作过程来寻找答案:

  1. // 分析的查询语句是什么
  2. QUERY: SELECT * FROM s1 WHERE
  3. key1 > 'z' AND
  4. key2 < 1000000 AND
  5. key3 IN ('a', 'b', 'c') AND
  6. common_field = 'abc'
  7. // 优化的具体过程
  8. TRACE: {
  9. "steps": [
  10. {
  11. // prepare 阶段
  12. "join_preparation": {
  13. "select#": 1,
  14. "steps": [
  15. {
  16. "IN_uses_bisection": true
  17. },
  18. {
  19. "expanded_query": "/* select#1 */ select `s1`.`id` AS `id`,`s1`.`key1` AS `key1`,`s1`.`key2` AS `key2`,`s1`.`key3` AS `key3`,`s1`.`key_part1` AS `key_part1`,`s1`.`key_part2` AS `key_part2`,`s1`.`key_part3` AS `key_part3`,`s1`.`common_field` AS `common_field` from `s1` where ((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  20. }
  21. ]
  22. }
  23. },
  24. {
  25. // optimize 阶段
  26. "join_optimization": {
  27. "select#": 1,
  28. "steps": [
  29. {
  30. // 处理搜索条件
  31. "condition_processing": {
  32. "condition": "WHERE",
  33. // 原始的搜索条件
  34. "original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
  35. "steps": [
  36. {
  37. // 等值传递转换
  38. "transformation": "equality_propagation",
  39. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  40. },
  41. {
  42. // 常量传递转换
  43. "transformation": "constant_propagation",
  44. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  45. },
  46. {
  47. // 去除没用的条件
  48. "transformation": "trivial_condition_removal",
  49. "resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  50. }
  51. ]
  52. }
  53. },
  54. {
  55. // 替换虚拟生成列
  56. "substitute_generated_columns": {
  57. }
  58. },
  59. {
  60. // 表的依赖信息
  61. "table_dependencies": [
  62. {
  63. "table": "`s1`",
  64. "row_may_be_null": false,
  65. "map_bit": 0,
  66. "depends_on_map_bits": [
  67. ]
  68. }
  69. ]
  70. },
  71. {
  72. "ref_optimizer_key_uses": [
  73. ]
  74. },
  75. {
  76. // 预估不同单表访问方法的访问成本
  77. "rows_estimation": [
  78. {
  79. "table": "`s1`",
  80. "range_analysis": {
  81. // 全表扫描的行数以及成本
  82. "table_scan": {
  83. "rows": 9688,
  84. "cost": 2036.7
  85. },
  86. // 分析可能使用的索引
  87. "potential_range_indexes": [
  88. {
  89. // 主键索引不可用
  90. "index": "PRIMARY",
  91. "usable": false,
  92. "cause": "not_applicable"
  93. },
  94. {
  95. // idx_key2 索引可能被使用
  96. "index": "idx_key2",
  97. "usable": true,
  98. "key_parts": [
  99. "key2"
  100. ]
  101. },
  102. {
  103. // idx_key1 索引可能被使用
  104. "index": "idx_key1",
  105. "usable": true,
  106. "key_parts": [
  107. "key1",
  108. "id"
  109. ]
  110. },
  111. {
  112. // idx_key3 索引可能被使用
  113. "index": "idx_key3",
  114. "usable": true,
  115. "key_parts": [
  116. "key3",
  117. "id"
  118. ]
  119. },
  120. {
  121. // idx_key_part 联合索引不可用
  122. "index": "idx_key_part",
  123. "usable": false,
  124. "cause": "not_applicable"
  125. }
  126. ]
  127. "setup_range_conditions": [
  128. ]
  129. "group_index_range": {
  130. "chosen": false,
  131. "cause": "not_group_by_or_distinct"
  132. }
  133. // 分析各种可能使用的索引的成本
  134. "analyzing_range_alternatives": {
  135. "range_scan_alternatives": [
  136. {
  137. // 使用 idx_key2 索引的成本分析
  138. "index": "idx_key2",
  139. // 使用 idx_key2 索引的范围区间
  140. "ranges": [
  141. "NULL < key2 < 1000000"
  142. ],
  143. // 是否使用 index dive
  144. "index_dives_for_eq_ranges": true,
  145. // 使用该索引获取的记录是否按照主键排序
  146. "rowid_ordered": false,
  147. // 是否使用 mrr
  148. "using_mrr": false,
  149. // 是否是索引覆盖访问
  150. "index_only": false,
  151. // 使用该索引获取的记录条数
  152. "rows": 12,
  153. // 使用该索引的成本
  154. "cost": 15.41,
  155. // 是否选择该索引
  156. "chosen": true
  157. },
  158. {
  159. // 使用 idx_key1 索引的成本分析
  160. "index": "idx_key1",
  161. // 使用 idx_key1 索引的范围区间
  162. "ranges": [
  163. "z < key1"
  164. ],
  165. "index_dives_for_eq_ranges": true,
  166. "rowid_ordered": false,
  167. "using_mrr": false,
  168. "index_only": false,
  169. "rows": 266,
  170. "cost": 320.21,
  171. "chosen": false,
  172. // 不选择该索引的原因:成本太大
  173. "cause": "cost"
  174. },
  175. {
  176. // 使用 idx_key3 索引的成本分析
  177. "index": "idx_key3",
  178. // 使用 idx_key3 索引的范围区间
  179. "ranges": [
  180. "a <= key3 <= a",
  181. "b <= key3 <= b",
  182. "c <= key3 <= c"
  183. ],
  184. "index_dives_for_eq_ranges": true,
  185. "rowid_ordered": false,
  186. "using_mrr": false,
  187. "index_only": false,
  188. "rows": 21,
  189. "cost": 28.21,
  190. "chosen": false,
  191. "cause": "cost"
  192. }
  193. ]
  194. // 分析使用索引合并的成本
  195. "analyzing_roworder_intersect": {
  196. "usable": false,
  197. "cause": "too_few_roworder_scans"
  198. }
  199. },
  200. // 对于上述 s1 单表查询最优的访问方法
  201. "chosen_range_access_summary": {
  202. "range_access_plan": {
  203. "type": "range_scan",
  204. "index": "idx_key2",
  205. "rows": 12,
  206. "ranges": [
  207. "NULL < key2 < 1000000"
  208. ]
  209. },
  210. "rows_for_plan": 12,
  211. "cost_for_plan": 15.41,
  212. "chosen": true
  213. }
  214. }
  215. }
  216. ]
  217. },
  218. {
  219. // 分析各种可能的执行计划
  220. // 对多表查询可能有很多种不同的方案
  221. // 单表查询的方案上边已经分析过了,直接选取 idx_key2 索引
  222. "considered_execution_plans": [
  223. {
  224. "plan_prefix": [
  225. ],
  226. "table": "`s1`",
  227. "best_access_path": {
  228. "considered_access_paths": [
  229. {
  230. "rows_to_scan": 12,
  231. "access_type": "range",
  232. "range_details": {
  233. "used_index": "idx_key2"
  234. },
  235. "resulting_rows": 12,
  236. "cost": 17.81,
  237. "chosen": true
  238. }
  239. ]
  240. },
  241. "condition_filtering_pct": 100,
  242. "rows_for_plan": 12,
  243. "cost_for_plan": 17.81,
  244. "chosen": true
  245. }
  246. ]
  247. },
  248. {
  249. // 尝试给查询添加一些其他的查询条件
  250. "attaching_conditions_to_tables": {
  251. "original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
  252. "attached_conditions_computation": [
  253. ],
  254. "attached_conditions_summary": [
  255. {
  256. "table": "`s1`",
  257. "attached": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  258. }
  259. ]
  260. }
  261. },
  262. {
  263. // 再稍微改进执行计划
  264. "refine_plan": [
  265. {
  266. "table": "`s1`",
  267. "pushed_index_condition": "(`s1`.`key2` < 1000000)",
  268. "table_condition_attached": "((`s1`.`key1` > 'z') and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
  269. }
  270. ]
  271. }
  272. ]
  273. }
  274. },
  275. {
  276. // execute 阶段
  277. "join_execution": {
  278. "select#": 1,
  279. "steps": [
  280. ]
  281. }
  282. }
  283. ]
  284. }
  285. // 因优化过程文本太多而丢弃的文本字节大小,值为0时表示并没有丢弃
  286. MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
  287. // 权限字段(权限不足)
  288. INSUFFICIENT_PRIVILEGES: 0

总结

这只是优化器执行过程中的一小部分,
设计 MySQL 的人可能会在之后的版本中添加更多的优化过程信息。
虽然复杂但是有规律,优化过程大致分了三个阶段:

  • prepare 阶段
  • optimize 阶段
  • execute 阶段

我们所说的基于成本的优化主要集中在 optimize 阶段。
对于单表查询来说,我们主要关注 optimize 阶段的 “rows_estimation” 这个过程,
“rows_estimation” 深入分析了对单表查询的各种执行方案的成本。

对于多表连接查询来说,我们更多需要关注 “considered_execution_plans” 这个过程,
“considered_execution_plans” 里会写明各种不同的连接方式所对应的成本。
查询优化器最终会选择成本最低的那种方案来作为最终的执行计划,
也就是使用 explain 语句所展现出的那种方案。