본문 바로가기
MySQL

optimizer trace

by 타마마임팩트_쫀 2019. 11. 26.

옵티마이저의 판단을 알기 위한 방법으로 explain 명령어를 사용한다. 하지만 explain 옵티마이저가 최종적으로 선택한 실행 계획 뿐이고, 과정에 최적화나 비용 계산, 또는 실행 계획의 비교가 어떻게 일어 났는지는 없다. 옵티마이저 트레이스에는 이러한 내용이 설명 되어있다.

mysql> explain select * from t1 join t3 on (t1.c1=t3.pk and t3.pk < 113);
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | t3    | NULL       | range | PRIMARY       | PRIMARY | 4       | NULL |    2 |   100.00 | Using where                                        |
|  1 | SIMPLE      | t1    | NULL       | ALL   | NULL          | NULL    | NULL    | NULL |   10 |    10.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)


옵티마이저
트레이스의 사용법은 간단하다.

  1. 옵티마이저 트레이스를 활성화한다.
  2. 해석하고 싶은 쿼리를 실행한다.
  3. 트레이스를 출력한다.

 

SQL을 수행하면 아래 그림처럼 진행이 되는데, 옵티마이저 트레이스를 출력하면 옵티마이저가 하는 역할을 그대로 확인 할 수 있다.

 

실행 계획은 information_schema에서 json 형식으로 있다.

select * from inforamtion_schema.optimizer_trace\G;

 

옵티마이저 트레이스를 제어하는 시스템 변수를 확인 하자.

세션 변수이므로 그냥 변경하면 현재 세션에만 적용 된다.

- 옵티마이저 트레이스의 활성화/ 비활성화

set optimizer_trace='enabled=on';
set optimizer_trace='enabled=off';

- 옵티마이저 트레이스 최대 저장 갯수 변경

SET optimizer_trace_offset=-2, optimizer_trace_limit=5; << 마지막 2개 수생된거 부터 5개 저장
SET optimizer_trace_offset=0, optimizer_trace_limit=5; << 마지막 수행된거 부터 5개 저장 (추천, limit 값만 조절 할 것)

※ 최대 출력 갯수는 optimizer_trace_max_mem_size 의 영향을 받는다.

- 옵티마이저 트레이스 다중 출력

select * from information_schema.optimizer_trace ; << 순서대로 모두 출럭 (추천)
select * from information_schema.optimizer_trace limit 1 ; << 1번째 1개 출력
select * from information_schema.optimizer_trace limit 1 offset 1 ; << 2번째 1개 출력
select * from information_schema.optimizer_trace limit 2 offset 0 ; << 1번째 부터 2개 출력
select * from information_schema.optimizer_trace limit 2 offset 1 ; << 2번째 부터 2개 출력
select * from information_schema.optimizer_trace limit 2 offset 2 ; << 3번째 부터 2개 출럭

 

쿼리를 수행하고 트레이스를 확인 보았다.

출력되는 결과가 너무 길어서 잘라서 보자.

mysql> select * from information_schema.optimizer_trace\G;
*************************** 1. row ***************************
                            QUERY: select * from t1 join t3 on (t1.c1=t3.pk and t3.pk < 113)
                            TRACE: {
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `t1`.`c1` AS `c1`,`t1`.`c2` AS `c2`,`t3`.`pk` AS `pk`,`t3`.`i` AS `i` from (`t1` join `t3` on(((`t1`.`c1` = `t3`.`pk`) and (`t3`.`pk` < 113))))"
          },
          {
            "transformations_to_nested_joins": {
              "transformations": [
                "JOIN_condition_to_WHERE",
                "parenthesis_removal"
              ],
              "expanded_query": "/* select#1 */ select `t1`.`c1` AS `c1`,`t1`.`c2` AS `c2`,`t3`.`pk` AS `pk`,`t3`.`i` AS `i` from `t1` join `t3` where ((`t1`.`c1` = `t3`.`pk`) and (`t3`.`pk` < 113))"
            }
          }
        ]
      }
    },

QUERY 컬럼을 통해 실제 입력된 쿼리를 있다.

join_preparation select# explain id 동일한 값이다.

join_preparation transformations_to_nested_joins 에서 쿼리의 실행 계획을 찾기 전에 쿼리를 완전한 형태로 열어 조인문의 on 절에 있는 조건을 where 절로 이동 시킨다.

 

        {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "((`t1`.`c1` = `t3`.`pk`) and (`t3`.`pk` < 113))",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "((`t3`.`pk` < 113) and multiple equal(`t1`.`c1`, `t3`.`pk`))"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "((`t3`.`pk` < 113) and multiple equal(`t1`.`c1`, `t3`.`pk`))"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "((`t3`.`pk` < 113) and multiple equal(`t1`.`c1`, `t3`.`pk`))"
                }
              ]
            }
          },

join_optimization 처음 단계는 where 절의 변환이다.원래 on 절에 지정되어 있던 것들 불필요한 조건을 제거한 옵티마이저가 해석하기 쉬운 형태로 변경 한다. 3개의 변환 알고리즘이 적용 되었다.

equality_propagation(등가 비교에 의한 추이율) 적용한 결과 c1, pk 컬럼이 같은 값이라고 판단하여 같은 그룹(multiple equal)으로 합쳐졌다.

constant_propagation(정수의 등가 비교에 의한 추이) trivial_condition_removal(자명한 조건의 삭제) 조건이 포함 되지 않아 변환은 일어나지 않았다. trivial_condition_removal에서는 만일 where...and 1=1 같은 불필요한 조건이 포함되어 있을 경우 삭제 된다.

위의 과정을 좀 자세히 보면 다음과 같다.

 

           {
            "substitute_generated_columns": {
            }
          },
          {
            "table_dependencies": [
              {
                "table": "`t1`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ]
              },
              {
                "table": "`t3`",
                "row_may_be_null": false,
                "map_bit": 1,
                "depends_on_map_bits": [
                ]
              }
            ]
          },
          {
            "ref_optimizer_key_uses": [
              {
                "table": "`t3`",
                "field": "pk",
                "equals": "`t1`.`c1`",
                "null_rejecting": true
              }
            ]
          },
          {
            "rows_estimation": [
              {
                "table": "`t1`",
                "table_scan": {
                  "rows": 10,
                  "cost": 0.25
                }
              },
              {
                "table": "`t3`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 4,
                    "cost": 2.75
                  },
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": true,
                      "key_parts": [
                        "pk"
                      ]
                    }
                  ],
                  "setup_range_conditions": [
                  ],
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_single_table"
                  },
                  "skip_scan_range": {
                    "chosen": false,
                    "cause": "not_single_table"
                  },
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "PRIMARY",
                        "ranges": [
                          "pk < 113"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": true,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 2,
                        "cost": 0.71,
                        "chosen": true
                      }
                    ],
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    }
                  },
                  "chosen_range_access_summary": {
                    "range_access_plan": {
                      "type": "range_scan",
                      "index": "PRIMARY",
                      "rows": 2,
                      "ranges": [
                        "pk < 113"
                      ]
                    },
                    "rows_for_plan": 2,
                    "cost_for_plan": 0.71,
                    "chosen": true
                  }
                }
              }
            ]
          },

substitute_generated_columns은 컬럼 처리 방식으로 table_dependencies에는 테이블의 의존 관계가 나타나 있다. 이는 outer join에서 의미가 있는 항목이다. outer join에는 depends_on_map_bits에 의존하는 테이블의 map_bit가 표시된다.

ref_optimizer_key_uses에는 이쿼리에서 이용 가능한 인덱스이 목록이 나열된다.

rows_estimation에는 접근한 테이블의 수가 출력된다. 또한 테이블 스캔을 실행할 때의 비용을 있다.

 

          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`t1`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 10,
                      "filtering_effect": [
                      ],
                      "final_filtering_effect": 1,
                      "access_type": "scan",
                      "resulting_rows": 10,
                      "cost": 1.25,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 10,
                "cost_for_plan": 1.25,
                "rest_of_plan": [
                  {
                    "plan_prefix": [
                      "`t1`"
                    ],
                    "table": "`t3`",
                    "best_access_path": {
                      "considered_access_paths": [
                        {
                          "access_type": "eq_ref",
                          "index": "PRIMARY",
                          "rows": 1,
                          "cost": 3.5,
                          "chosen": true,
                          "cause": "clustered_pk_chosen_by_heuristics"
                        },
                        {
                          "access_type": "range",
                          "range_details": {
                            "used_index": "PRIMARY"
                          },
                          "chosen": false,
                          "cause": "heuristic_index_cheaper"
                        }
                      ]
                    },
                    "condition_filtering_pct": 100,
                    "rows_for_plan": 10,
                    "cost_for_plan": 4.75,
                    "chosen": true
                  }
                ]
              },
              {
                "plan_prefix": [
                ],
                "table": "`t3`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "access_type": "ref",
                      "index": "PRIMARY",
                      "usable": false,
                      "chosen": false
                    },
                    {
                      "rows_to_scan": 2,
                      "filtering_effect": [
                      ],
                      "final_filtering_effect": 1,
                      "access_type": "range",
                      "range_details": {
                        "used_index": "PRIMARY"
                      },
                      "resulting_rows": 2,
                      "cost": 0.91,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 2,
                "cost_for_plan": 0.91,
                "rest_of_plan": [
                  {
                    "plan_prefix": [
                      "`t3`"
                    ],
                    "table": "`t1`",
                    "best_access_path": {
                      "considered_access_paths": [
                        {
                          "rows_to_scan": 10,
                          "filtering_effect": [
                          ],
                          "final_filtering_effect": 1,
                          "access_type": "scan",
                          "using_join_cache": true,
                          "buffers_needed": 1,
                          "resulting_rows": 10,
                          "cost": 2.25,
                          "chosen": true
                        }
                      ]
                    },
                    "condition_filtering_pct": 100,
                    "rows_for_plan": 20,
                    "cost_for_plan": 3.16,
                    "chosen": true
                  }
                ]
              }
            ]
          },

considered_execution_plans 옵티마이저가 검토한 실행 계획을 배열로 표시한다.

plan_prefix는 지금까지 접근한 테이블을 나타낸다. 처음에 접근하는 테이블의 plan_prefix는 당연히 비어있다.

table 맴버는 현재 검토하고 있는 테이블을 가리킨다.

best_access_path는 가장 효율적인 접근 방식을 착기 위해 어떤 접근 방식을 검토했는가를 표시하고, 검토한 접근 방식은 considered_access_paths에 정리 되어 있다. 선택된 것은 chosen 맴버가 true 것으로 있다.

condition_filtering_pct는 테이블에서 데이터를 패치한 이후에 적용된 where 절의 조건으로 어느 정도 행의 범위 축소가 일어났는지를 예상한다. 여기서는 100% 되어 있기 대문에 범위 축소가 전혀 일어나지 않았다.

rest_of_plan은 이후의 테이블 접근 정보가 저장된다. 여기서는 조인한 경우를 있다.

두 개의 실행계획을 색으로 분리 하였다. t1->t3 조인 방법은 cost 4.75, t3->t1 조인 방법은 cost 3.16 이 소모된다.

 

          {
            "attaching_conditions_to_tables": {
              "original_condition": "((`t1`.`c1` = `t3`.`pk`) and (`t3`.`pk` < 113))",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`t3`",
                  "attached": "(`t3`.`pk` < 113)"
                },
                {
                  "table": "`t1`",
                  "attached": "(`t1`.`c1` = `t3`.`pk`)"
                }
              ]
            }
          },
          {
            "finalizing_table_conditions": [
              {
                "table": "`t3`",
                "original_table_condition": "(`t3`.`pk` < 113)",
                "final_table_condition   ": "(`t3`.`pk` < 113)"
              },
              {
                "table": "`t1`",
                "original_table_condition": "(`t1`.`c1` = `t3`.`pk`)",
                "final_table_condition   ": "(`t1`.`c1` = `t3`.`pk`)"
              }
            ]
          },

attaching_conditions_to_tables는 추가로 적용될 where 절의 검색 조건을 정리한다.

 

            "refine_plan": [
              {
                "table": "`t3`"
              },
              {
                "table": "`t1`"
              }
            ]
          }
        ]
      }
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0
1 rows in set (0.00 sec)

refine_plan은 join_optimization 마지막 항목이다. 단순히 어떤 테이블에 접근 했는지를 나타낸다.

MISSING_BYTES_BEYOND_MAX_MEM_SIZE는 옵티마이저 트레이스를 잘라낸 부분을 표시한다. 0이므로 잘라낸 부분이 없다. 만일 메모리가 부족하여 일부를 잘라내게 되면 이곳에 바이트를 잘라냈는지 표시한다. 메모리의 크기는 optimizer_trace_max_mem_size 지정한다. 기본값은 16KB (8.0.18 기준 1MB)이다.