MariaDB/SQL Tuning

[MariaDB][성능] 스칼라 서브쿼리 실행계획

알 수 없는 사용자 2016. 8. 15. 21:59

[테스트 환경] 

OS : CentOS7 

DB : MariaDB 10.1.12


EXPLAIN FORMAT = JSON은 MariaDB 10.1.2 버전이상 부터 지원되며, output에 대한 포맷은 정확하게 나와있지 않기 때문에 아래 파악한 내용이 확실하지 않다. ANALYZE FORMAT = JSON 은 EXPLAIN FORMAT = JSON과 ANALYZE 구문의 혼합형으로 EXPLAIN FORMAT = JSON  포맷형식에 쿼리를 실행시킨 데이터(r_loops, r_total_time_ms, r_filtered)가 추가되어 출력된다.


ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
FROM     EMP A;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.3655,
    "table": {
      "table_name": "A",
      "access_type": "ALL",
      "r_loops": 1,
      "rows": 14,
      "r_rows": 14,
      "r_total_time_ms": 0.0607,
      "filtered": 100,
      "r_filtered": 100
    },
    "subqueries": [
      {
        "expression_cache": {
          "r_loops": 14,
          "r_hit_ratio": 78.571,
          "query_block": {
            "select_id": 2,
            "r_loops": 3,
            "r_total_time_ms": 0.0344,
            "table": {
              "table_name": "S1",
              "access_type": "eq_ref",
              "possible_keys": ["PRIMARY"],
              "key": "PRIMARY",
              "key_length": "4",
              "used_key_parts": ["DEPTNO"],
              "ref": ["test.A.DEPTNO"],
              "r_loops": 3,
              "rows": 1,
              "r_rows": 1,
              "r_total_time_ms": 0.0176,
              "filtered": 100,
              "r_filtered": 100
            }
          }
        }
      }
    ]
  }
}

- query_block은 SELECT 구문이 있는 부분에 발생한다. 여기서는 메인 쿼리와 스칼라 서브쿼리에 SELECT구문이 사용되어 두 개의 query_block이 출력되었다. select_id는 실행된 순서를 의미하지 않으며, 실행계획에 출력된 순서대로 실행된다.[참고 5]


- 첫 번째 query_block은 1번 실행되어 0.3655ms 가 소요되었다,

 - EMP 테이블에 대해서 한 번의 Table Full Scan이 발생했으며 14건의 데이터를 출력했다. 조건절이 따로 없어서 테이블 전체 데이터가 출력되어 r_filtered가 100으로 나타나며, 테이블 액세스에 소요된 시간은 0.0607ms 로 첫 번째 query_block에 소요된 시간에 포함된다. 인덱스 액세스가 발생하지 않는 경우에는 possible_keys, key, key_length, used_key_parts가 출력되지 않는다.


- 스칼라 서브쿼리가 사용되어 subqueries가 나타났고, expression_cache는 스칼라 서브쿼리를 캐싱하여 사용했다는 의미로, 두 번째 query_block 만큼 서브쿼리가 실행되고 결과값을 캐싱해서 사용한 것으로 추측된다.(r_hit_ratio가 캐싱된 서브쿼리를 사용한 비율로 [캐싱된 서브쿼리를 사용한 횟수] / [서브 쿼리를 호출한 횟수] * 100 이며, 서브쿼리는 총 14번이 호출되었지만 실제로 ID가 2인 쿼리는 3번만 호출되었으므로, 11번은 캐싱된 쿼리를 사용한 것으로 확인할 수 있다. 따라서 11/14 * 100 = 78.571로 출력되었다.)실제로 메인 쿼리의 데이터가 여러 건 일때만 해당 문구가 출력되고 [참고 1], 메인 쿼리에서 한 건만 출력되도록 조건절을 추가하면 해당 문구가 사라진다. [참고 2]

 - 메인 쿼리의 결과 값 만큼 서브쿼리가 실행되었다.(r_loops:14) 

 - 두 번째 query_block은 서브쿼리의 SELECT를 의미한다.

  - 메인 쿼리에서 DEPTNO 값을 10, 20, 30만 가지고 있어서 실제로 3번의 쿼리가 실행되었고 총 소요시간은 0.0344ms이다.

  - DEPT는 PK 인덱스로 액세스 되었으며 사용된 컬럼은 DEPTNO이다. DEPTNO의 데이터타입은 int여서 4 bytes를 사용했다. 두 번째 query_block이 실행된 만큼 DEPT에 대한 인덱스 액세스가 실행되어 r_loops가 3이며, 서브쿼리이기 때문에 메인 쿼리의 한 로우당 한 건을 반환한 것을 확인할 수 있다. [참고 3] [참고 4]


[참고 1]

ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
FROM     EMP A
WHERE    A.EMPNO IN (7369, 7380);


{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.2738,
    "table": {
      "table_name": "A",
      "access_type": "range",
      "possible_keys": ["PRIMARY"],
      "key": "PRIMARY",
      "key_length": "4",
      "used_key_parts": ["EMPNO"],
      "r_loops": 1,
      "rows": 2,
      "r_rows": 1,
      "r_total_time_ms": 0.0311,
      "filtered": 100,
      "r_filtered": 100,
      "attached_condition": "(A.EMPNO in (7369,7380))"
    },
    "subqueries": [
      {
        "expression_cache": {
          "r_loops": 1,
          "r_hit_ratio": 0,
          "query_block": {
            "select_id": 2,
            "r_loops": 1,
            "r_total_time_ms": 0.0127,
            "table": {
              "table_name": "S1",
              "access_type": "eq_ref",
              "possible_keys": ["PRIMARY"],
              "key": "PRIMARY",
              "key_length": "4",
              "used_key_parts": ["DEPTNO"],
              "ref": ["test.A.DEPTNO"],
              "r_loops": 1,
              "rows": 1,
              "r_rows": 1,
              "r_total_time_ms": 0.0064,
              "filtered": 100,
              "r_filtered": 100
            }
          }
        }
      }
    ]
  }
}

- 메인 쿼리에 PK 컬럼인 EMP에 대해서 두 건 이상의 로우가 출력되도록 조건절을 추가했을 때, expression_cache가 나타난다.


[참고 2]

ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
FROM     EMP A
WHERE    A.EMPNO = 7369;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.0253,
    "table": {
      "table_name": "A",
      "access_type": "const",
      "possible_keys": ["PRIMARY"],
      "key": "PRIMARY",
      "key_length": "4",
      "used_key_parts": ["EMPNO"],
      "ref": ["const"],
      "r_loops": 0,
      "rows": 1,
      "r_rows": null,
      "filtered": 100,
      "r_filtered": null
    },
    "subqueries": [
      {
        "query_block": {
          "select_id": 2,
          "r_loops": 1,
          "r_total_time_ms": 0.0061,
          "table": {
            "table_name": "S1",
            "access_type": "const",
            "possible_keys": ["PRIMARY"],
            "key": "PRIMARY",
            "key_length": "4",
            "used_key_parts": ["DEPTNO"],
            "ref": ["const"],
            "r_loops": 0,
            "rows": 1,
            "r_rows": null,
            "filtered": 100,
            "r_filtered": null
          }
        }
      }
    ]
  }
}

- 메인 쿼리에 PK 컬럼인 EMP에 대해서 한 건의 로우만 출력되도록 조건절을 추가했을 때, expression_cache가 사라진다.

- 한 건만 출력하도록 했을 때, r_filtered값이 null값으로 출력된다. [참고 6]


[참고 3]

 -- 데이터 변경
UPDATE EMP
SET DEPTNO = NULL
WHERE EMPNO = 7844;

ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
FROM     EMP A;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.4101,
    "table": {
      "table_name": "A",
      "access_type": "ALL",
      "r_loops": 1,
      "rows": 14,
      "r_rows": 14,
      "r_total_time_ms": 0.1192,
      "filtered": 100,
      "r_filtered": 100
    },
    "subqueries": [
      {
        "expression_cache": {
          "r_loops": 14,
          "r_hit_ratio": 71.429,
          "query_block": {
            "select_id": 2,
            "r_loops": 4,
            "r_total_time_ms": 0.0569,
            "table": {
              "table_name": "S1",
              "access_type": "eq_ref",
              "possible_keys": ["PRIMARY"],
              "key": "PRIMARY",
              "key_length": "4",
              "used_key_parts": ["DEPTNO"],
              "ref": ["test.A.DEPTNO"],
              "r_loops": 3,
              "rows": 1,
              "r_rows": 1,
              "r_total_time_ms": 0.0304,
              "filtered": 100,
              "r_filtered": 100
            }
          }
        }
      }
    ]
  }
}

- 메인 쿼리에서 사용되는 DEPTNO를 10, 20, 30 에서 NULL을 추가하여 총 4개의 데이터를 출력하도록 변경했을 때, 두 번째 query_block에서 r_loops가 4번이 발생하지만 DEPT테이블에서 DEPTNO에  NULL 데이터가 없기 때문에 DEPT 인덱스 액세스는 3번 발생했다.


[참고 4]

-- 데이터 변경
UPDATE EMP
SET DEPTNO = 40
WHERE EMPNO = 7844 ;

ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
FROM     EMP A;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.3737,
    "table": {
      "table_name": "A",
      "access_type": "ALL",
      "r_loops": 1,
      "rows": 14,
      "r_rows": 14,
      "r_total_time_ms": 0.0489,
      "filtered": 100,
      "r_filtered": 100
    },
    "subqueries": [
      {
        "expression_cache": {
          "r_loops": 14,
          "r_hit_ratio": 71.429,
          "query_block": {
            "select_id": 2,
            "r_loops": 4,
            "r_total_time_ms": 0.0401,
            "table": {
              "table_name": "S1",
              "access_type": "eq_ref",
              "possible_keys": ["PRIMARY"],
              "key": "PRIMARY",
              "key_length": "4",
              "used_key_parts": ["DEPTNO"],
              "ref": ["test.A.DEPTNO"],
              "r_loops": 4,
              "rows": 1,
              "r_rows": 1,
              "r_total_time_ms": 0.0225,
              "filtered": 100,
              "r_filtered": 100
            }
          }
        }
      }
    ]
  }
}

- 메인 쿼리에서 사용되는 DEPTNO를 10, 20, 30 에서 40을 추가하여 총 4개의 데이터를 출력하도록 변경했을 때, 두 번째 query_block에서 r_loops가 4번이 발생하고 필요한 데이터 만큼 DEPT 인덱스 액세스가 4번 발생했다.


[참고 5]

ANALYZE  FORMAT=JSON
SELECT   A.EMPNO
        ,A.ENAME
        ,(SELECT   S1.DNAME 
          FROM     DEPT S1 
          WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
        ,(SELECT   COUNT(*) AS CNT 
          FROM     EMP S2 
          WHERE    S2.DEPTNO = A.DEPTNO) AS SAME_DEPTNO
FROM     EMP A;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.4719,
    "table": {
      "table_name": "A",
      "access_type": "ALL",
      "r_loops": 1,
      "rows": 14,
      "r_rows": 14,
      "r_total_time_ms": 0.0433,
      "filtered": 100,
      "r_filtered": 100
    },
    "subqueries": [
      {
        "expression_cache": {
          "r_loops": 14,
          "r_hit_ratio": 78.571,
          "query_block": {
            "select_id": 3,
            "r_loops": 3,
            "r_total_time_ms": 0.0982,
            "table": {
              "table_name": "S2",
              "access_type": "ALL",
              "r_loops": 3,
              "rows": 14,
              "r_rows": 14,
              "r_total_time_ms": 0.068,
              "filtered": 100,
              "r_filtered": 33.333,
              "attached_condition": "(S2.DEPTNO = A.DEPTNO)"
            }
          }
        }
      },
      {
        "expression_cache": {
          "r_loops": 14,
          "r_hit_ratio": 78.571,
          "query_block": {
            "select_id": 2,
            "r_loops": 3,
            "r_total_time_ms": 0.0397,
            "table": {
              "table_name": "S1",
              "access_type": "eq_ref",
              "possible_keys": ["PRIMARY"],
              "key": "PRIMARY",
              "key_length": "4",
              "used_key_parts": ["DEPTNO"],
              "ref": ["test.A.DEPTNO"],
              "r_loops": 3,
              "rows": 1,
              "r_rows": 1,
              "r_total_time_ms": 0.0209,
              "filtered": 100,
              "r_filtered": 100
            }
          }
        }
      }
    ]
  }
}

MariaDB [test]> ANALYZE
    -> SELECT   A.EMPNO
    ->         ,A.ENAME
    ->         ,(SELECT   S1.DNAME
    ->           FROM     DEPT S1
    ->           WHERE    S1.DEPTNO = A.DEPTNO) AS DNAME
    ->         ,(SELECT   COUNT(*) AS CNT 
    ->           FROM     EMP S2 
    ->           WHERE    S2.DEPTNO = A.DEPTNO) AS SAME_DEPTNO
    -> FROM     EMP A;
+------+--------------------+-------+--------+---------------+---------+---------+---------------+------+--------+----------+------------+-------------+
| id   | select_type        | table | type   | possible_keys | key     | key_len | ref           | rows | r_rows | filtered | r_filtered | Extra       |
+------+--------------------+-------+--------+---------------+---------+---------+---------------+------+--------+----------+------------+-------------+
|    1 | PRIMARY            | A     | ALL    | NULL          | NULL    | NULL    | NULL          |   14 |  14.00 |   100.00 |     100.00 |             |
|    3 | DEPENDENT SUBQUERY | S2    | ALL    | NULL          | NULL    | NULL    | NULL          |   14 |  14.00 |   100.00 |      33.33 | Using where |
|    2 | DEPENDENT SUBQUERY | S1    | eq_ref | PRIMARY       | PRIMARY | 4       | test.A.DEPTNO |    1 |   1.00 |   100.00 |     100.00 |             |
+------+--------------------+-------+--------+---------------+---------+---------+---------------+------+--------+----------+------------+-------------+
3 rows in set (0.01 sec)

- 스칼라 서브쿼리를 두 개 사용했을 때 출력되는 id를 보면 id 순서대로 실행되지 않는 것을 확인할 수 있다.


[참고 6]
ANALYZE FORMAT = JSON
SELECT    A.* 
FROM      EMP A
WHERE     A.MGR = 7698 ;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.0192,
    "table": {
      "table_name": "A",
      "access_type": "ALL",
      "r_loops": 1,
      "rows": 14,
      "r_rows": 14,
      "r_total_time_ms": 0.0115,
      "filtered": 100,
      "r_filtered": 35.714,
      "attached_condition": "(A.MGR = 7698)"
    }
  }
}
- filetered는 통계정보를 기반으로 출력되기 때문에 실제 값과 맞지 않을 수 있다. 
- r_filtered는 실제 쿼리를 수행 후 출력되는 값이며, MariaDB 엔진에서 필터링하고 남은 데이터에 대한 비율을 의미한다. 따라서 attached_condition 이 없을 경우, 아래와 같이 r_filtered값은 0이나 100이 출력될 수 있다.
-- 인덱스 생성
CREATE INDEX EMP_MGR_IDX01 ON EMP(MGR);

ANALYZE FORMAT = JSON
SELECT    A.* 
FROM      EMP A USE INDEX(EMP_MGR_IDX01)
WHERE     A.MGR = 7698 ;

{
  "query_block": {
    "select_id": 1,
    "r_loops": 1,
    "r_total_time_ms": 0.0263,
    "table": {
      "table_name": "A",
      "access_type": "ref",
      "possible_keys": ["EMP_MGR_IDX01"],
      "key": "EMP_MGR_IDX01",
      "key_length": "5",
      "used_key_parts": ["MGR"],
      "ref": ["const"],
      "r_loops": 1,
      "rows": 5,
      "r_rows": 5,
      "r_total_time_ms": 0.0185,
      "filtered": 100,
      "r_filtered": 100
    }
  }
}