今日はなにの日。

気になったこと勉強になったことのメモ。

今日は、遅いSQLクエリを考えるの日。

目次

とある日

データベースに関するトラブルとして、SQLが遅いというものがある。

自分でも、遅いSQLを見つけチューニングするという作業を行ってみたいが。

遅いクエリを解決する方法は調べるすぐみつかるが、どのようなテーブル構成やSQLなのかを開示しているものが見つからない。

SQLチューニングやってみたい。

なら、自分で環境を作るしかないのか。

ってことで、頑張って遅いSQLを作成する。

そして、解決方法を試してどれぐらい変わるのかを実際にやってみよう。

RDBMS

Oracle 12c

今回は、Oracleを使用してチューニングを行っていく。

理由は特にない。

強いて言うなら、そこにOracleがあったから。

DBMSごとに実行計画取得方法も違うので次やるときは違うものも試してみたい。

問題作成

まず、3つの問題に焦点を当てて行こうと思います。

1.INDEX未使用問題

言わずとしれた遅いSQLの原因といえばINDEXを適切に使用していない問題。

2. ORDER BY問題

ORDER BYでソートを行うと遅くなる問題。

3.IN句EXISTS句問題

IN句でできることEXISTS句にすると早くなる問題。


の以上3つの問題原因を作って解決したいと思います。

テーブル作成

複雑なテーブル構成をするのは大変ので簡単なテーブル作成。

テーブル作成の条件

各問題を同じテーブルで使用したいのですべての条件を満たさないといけない。

問題 条件
INDEX未使用問題 INDEX使用列とINDEX未使用列
ORDER BY問題 ソート使用列とソート未使用列
IN句EXISTS句問題 自己参照するテーブル

INDEX問題とORDER BY問題は、複数列必要となる。

上記の条件を加味すると、適切なテーブルは従業員テーブルかなと。

ということで、ありきたりな従業員テーブルを作っていきます。

テーブル設計

ER図

f:id:Updraft:20200814093241p:plain

SQL

CREATE TABLE employees(
    EMPLOYEE_ID NUMBER(7) PRIMARY KEY,
    FIRST_NAME VARCHAR2(20) NOT NULL,
    LAST_NAME VARCHAR2(20) NOT NULL,
    EMAIL VARCHAR2(20) NOT NULL,
    HIRE_DATE DATE NOT NULL,
    MANAGER_ID NUMBER(7) ,
    FOREIGN KEY(MANAGER_ID) REFERENCES employees(EMPLOYEE_ID)  
);

データ生成

1件1件入れるのめんどくさいので、ランダムで生成したいと思う。

SQL

連番を扱いたいので、シーケンスオブジェクトを使用したいと思います。

CREATE SEQUENCE seq_emp_id;

insert /*+ APPEND */ into employees nologging (
EMPLOYEE_ID, FIRST_NAME, LAST_NAME,EMAIL, HIRE_DATE) 
  (select 
  seq_emp_id.NEXTVAL,
  DBMS_RANDOM.STRING('X', 10),
  DBMS_RANDOM.STRING('X', 10),
  CONCAT(CONCAT(DBMS_RANDOM.STRING('X', 5),'@'),DBMS_RANDOM.STRING('X', 5)),
  TO_CHAR(TO_DATE('20130101','YYYYMMDD')
    + FLOOR(DBMS_RANDOM.VALUE(0, 365)), 'YYYYMMDD')
  from (select 0 from all_catalog where rownum <= 100),
       (select 0 from all_catalog where rownum <= 1000)
  );

10万件作成してみた。

ちなみに、12秒かかった。

INDEX確認

念の為に、適切にINDEXがはられているかを確認する。

INDEX_NAME  |INDEX_TYPE|TABLE_NAME|TABLE_TYPE|UNIQUENESS|
------------|----------|----------|----------|----------|
SYS_C0010475|NORMAL    |EMPLOYEES |TABLE     |UNIQUE    |

遅いSQL作成

1.INDEX未使用問題

INDEXが貼れられる列が、EMPLOYEE_IDなのでそれ以外の列を条件指定して検索する。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES e2  WHERE MANAGER_ID = 10;

SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

PLAN_TABLE_OUTPUT                                                              |
-------------------------------------------------------------------------------|
Plan hash value: 1445457117                                                    |
                                                                               |
-------------------------------------------------------------------------------|
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     ||
-------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT  |           |  1040 | 52000 |   205   (1)| 00:00:01 ||
|*  1 |  TABLE ACCESS FULL| EMPLOYEES |  1040 | 52000 |   205   (1)| 00:00:01 ||
-------------------------------------------------------------------------------|
                                                                               |
Predicate Information (identified by operation id):                            |
---------------------------------------------------                            |
                                                                               |
   1 - filter("MANAGER_ID"=10)                                                 |

結果として、まずOperationでTABLE ACCESS FULLでINDEXが使用されていない。

そして、Costが合わせて410となっている。

2.不用意なソート問題

1番で使用したSQLにORDER BY句を追加する。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES e2  WHERE MANAGER_ID = 10 ORDER BY FIRST_NAME DESC ;
SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

PLAN_TABLE_OUTPUT                                                               |
--------------------------------------------------------------------------------|
Plan hash value: 3447538987                                                     |
                                                                                |
--------------------------------------------------------------------------------|
| Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     ||
--------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT   |           |  1040 | 52000 |   206   (1)| 00:00:01 ||
|   1 |  SORT ORDER BY     |           |  1040 | 52000 |   206   (1)| 00:00:01 ||
|*  2 |   TABLE ACCESS FULL| EMPLOYEES |  1040 | 52000 |   205   (1)| 00:00:01 ||
--------------------------------------------------------------------------------|
                                                                                |
Predicate Information (identified by operation id):                             |
---------------------------------------------------                             |
                                                                                |
   2 - filter("MANAGER_ID"=10)                                                  |

結果は、OperationでSORT ORDER BY が追加されていてソートがされたことがわかる。

Costがソート分増えて617となっている。

3.IN句問題

MANAGER_IDが上司のIDなので、部下がいる従業員を求めるIN句を使ったSQLを使用する。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES e2  WHERE MANAGER_ID IN (SELECT EMPLOYEE_ID FROM EMPLOYEES);
SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

PLAN_TABLE_OUTPUT                                                              |
-------------------------------------------------------------------------------|
Plan hash value: 1445457117                                                    |
                                                                               |
-------------------------------------------------------------------------------|
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     ||
-------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT  |           |   100K|  4882K|   205   (1)| 00:00:01 ||
|*  1 |  TABLE ACCESS FULL| EMPLOYEES |   100K|  4882K|   205   (1)| 00:00:01 ||
-------------------------------------------------------------------------------|
                                                                               |
Predicate Information (identified by operation id):                            |
---------------------------------------------------                            |
                                                                               |
   1 - filter("MANAGER_ID" IS NOT NULL)                                        |

結果は、1番と変わらない物となった。

ただ、Rows は1番と比べて格段に多い。

Costは410となっている。

解決方法

1.INDEX未使用問題

INDEXを使用してる列を条件指定して検索する。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES e2  WHERE EMPLOYEE_ID = 10 ;

SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

PLAN_TABLE_OUTPUT                                                                           |
--------------------------------------------------------------------------------------------|
Plan hash value: 546905373                                                                  |
                                                                                            |
--------------------------------------------------------------------------------------------|
| Id  | Operation                   | Name         | Rows  | Bytes | Cost (%CPU)| Time     ||
--------------------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT            |              |     1 |    50 |     2   (0)| 00:00:01 ||
|   1 |  TABLE ACCESS BY INDEX ROWID| EMPLOYEES    |     1 |    50 |     2   (0)| 00:00:01 ||
|*  2 |   INDEX UNIQUE SCAN         | SYS_C0010475 |     1 |       |     1   (0)| 00:00:01 ||
--------------------------------------------------------------------------------------------|
                                                                                            |
Predicate Information (identified by operation id):                                         |
---------------------------------------------------                                         |
                                                                                            |
   2 - access("EMPLOYEE_ID"=10)                                                             |

Operationで、INDEX UNIQUE SCAN が出ておりきちんとINDEXが使用されていることが確認できた。

Costも、INDEX未使用列と比べて格段に少なく5となっている。

差は、歴然である。

INDEX Cost
なし 410
あり 5

2.不用意なソート問題

ソートを行う列を検索した列を使用する。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES e2  WHERE MANAGER_ID = 10 ORDER BY MANAGER_ID ASC ;
SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());

PLAN_TABLE_OUTPUT                                                              |
-------------------------------------------------------------------------------|
Plan hash value: 1445457117                                                    |
                                                                               |
-------------------------------------------------------------------------------|
| Id  | Operation         | Name      | Rows  | Bytes | Cost (%CPU)| Time     ||
-------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT  |           |  1040 | 52000 |   205   (1)| 00:00:01 ||
|*  1 |  TABLE ACCESS FULL| EMPLOYEES |  1040 | 52000 |   205   (1)| 00:00:01 ||
-------------------------------------------------------------------------------|
                                                                               |
Predicate Information (identified by operation id):                            |
---------------------------------------------------                            |
                                                                               |
   1 - filter("MANAGER_ID"=10)                                                 |

ソート列を検索列と同じ列を使用したことによりOperationからSORT ORDER BYがなくなった。

なので、SORT ORDER BYで使用していたCostの206分がなくなる結果となった。

ORDER BY Cost
なし 617
あり 410

3.IN句問題

IN句をEXISTS句を入れ替えるSQLを組み立てた。

EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES T1  WHERE EXISTS (SELECT 1 FROM EMPLOYEES T2 WHERE T1.EMPLOYEE_ID = T2.MANAGER_ID );

SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY());


PLAN_TABLE_OUTPUT                                                                 |
----------------------------------------------------------------------------------|
Plan hash value: 3439112473                                                       |
                                                                                  |
----------------------------------------------------------------------------------|
| Id  | Operation            | Name      | Rows  | Bytes | Cost (%CPU)| Time     ||
----------------------------------------------------------------------------------|
|   0 | SELECT STATEMENT     |           |   100 |  5300 |   410   (1)| 00:00:01 ||
|*  1 |  HASH JOIN RIGHT SEMI|           |   100 |  5300 |   410   (1)| 00:00:01 ||
|   2 |   TABLE ACCESS FULL  | EMPLOYEES |   100K|   292K|   205   (1)| 00:00:01 ||
|   3 |   TABLE ACCESS FULL  | EMPLOYEES |   100K|  4882K|   205   (1)| 00:00:01 ||
----------------------------------------------------------------------------------|
                                                                                  |
Predicate Information (identified by operation id):                               |
---------------------------------------------------                               |
                                                                                  |
   1 - access("T1"."EMPLOYEE_ID"="T2"."MANAGER_ID")                               |

他のSQLと違ってCostが増加した。

比較方法 Cost
IN 410
EXISTS 1230

副問合せで行っている比較でHASH JOIN RIGHT SEMIが使用されている。

主問い合わせでINDEXを使用しない検索でCost205と副問合せでINDEXを使用しない検索をしたことによってそれらのCost足されたCostがかかっている。

これは、SQLチューニング失敗となった。

この場合のSQLなら、IN句を使用したほうがいいという結果となった。

一概に、EXISTS句を使用すれば良い結果となるわけではないことが判明した。

今回行ったSQLは簡単なものであったため変化量が少なく感じた。

次は、複雑なSQLを書いてそれを早くする作業も行ってみたい。

今回は、チューニングの知識だけでなく、テストデータの自動生成も行えてとてもためになった。

チューニングはもっとやってみたいと思った。