📘

Oracle OptimizerによるUNION句のPredicate Pushdown最適化

に公開

はじめに

私は SQL の UNION といえば「最適化が難しく、利用を避けるべきもの」として記憶していました。しかし、これは誤解でした。適切に利用すれば DB によって最適化され、効率的に動作するケースが存在します。Oracle 23.5 で実験した結果を紹介します。

実験

全く同じカラムで構成された2つのテーブルを UNION し、それを検索した時に実行計画がどうなるか観察してみましょう。

データベースの作成

sqlplus SYS/${ORACLE_PWD}@FREEPDB1 AS SYSDBA
CREATE USER exp_user IDENTIFIED BY hoge;
GRANT CREATE SESSION TO exp_user;
GRANT CREATE TABLE TO exp_user;
GRANT UNLIMITED TABLESPACE TO exp_user;
GRANT CREATE VIEW TO exp_user;

テーブルの作成

社員テーブルと、退職済み社員テーブルという2つのテーブルを作成します。

sqlplus exp_user/hoge@FREEPDB1
CREATE TABLE employees (
    employee_id   NUMBER(6)    PRIMARY KEY,
    employee_name VARCHAR2(100) NOT NULL,
    salary        NUMBER(8)
);

CREATE TABLE archived_employees (
    employee_id   NUMBER(6)    PRIMARY KEY,
    employee_name VARCHAR2(100) NOT NULL,
    salary        NUMBER(8)
);

データの作成

INSERT INTO employees (employee_id, employee_name, salary) VALUES (101, '山田 太郎', 6000000);
INSERT INTO employees (employee_id, employee_name, salary) VALUES (102, '鈴木 花子', 7500000);
INSERT INTO employees (employee_id, employee_name, salary) VALUES (103, '佐藤 次郎', 5500000);
INSERT INTO employees (employee_id, employee_name, salary) VALUES (104, '高橋 三郎', 8000000);
INSERT INTO employees (employee_id, employee_name, salary) VALUES (105, '田中 良子', 5000000);

INSERT INTO archived_employees (employee_id, employee_name, salary) VALUES (201, '伊藤 博文', 7000000);
INSERT INTO archived_employees (employee_id, employee_name, salary) VALUES (202, '渡辺 直美', 6500000);
INSERT INTO archived_employees (employee_id, employee_name, salary) VALUES (104, '高橋 三郎', 8000000);

COMMIT;

インデックスの定義

CREATE INDEX name_idx1 ON employees(employee_name);
CREATE INDEX name_idx2 ON archived_employees(employee_name);

ビューの定義

CREATE OR REPLACE VIEW all_employees AS
SELECT
    employee_id,
    employee_name,
    salary
FROM
    employees
UNION ALL
SELECT
    employee_id,
    employee_name,
    salary
FROM
    archived_employees;

実行計画の確認

EXPLAIN PLAN FOR SELECT * FROM all_employees WHERE employee_name = '高橋 三郎'

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

出力結果は下記の通りでした。

------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |                    |     2 |   190 |     4   (0)| 00:00:01 |
|   1 |  VIEW                                 | ALL_EMPLOYEES      |     2 |   190 |     4   (0)| 00:00:01 |
|   2 |   UNION-ALL                           |                    |     2 |   174 |     4   (0)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID BATCHED| EMPLOYEES          |     1 |    87 |     2   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN                  | NAME_IDX1          |     1 |       |     1   (0)| 00:00:01 |
|   5 |    TABLE ACCESS BY INDEX ROWID BATCHED| ARCHIVED_EMPLOYEES |     1 |    87 |     2   (0)| 00:00:01 |
|*  6 |     INDEX RANGE SCAN                  | NAME_IDX2          |     1 |       |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   4 - access("EMPLOYEE_NAME"='高橋 三郎')
   6 - access("EMPLOYEE_NAME"='高橋 三郎')

ポイントは 4行目、6行目と Predicate Information です。これらの行は Oracle のオプティマイザが UNION 結果に対して WHERE 句をかけるのではなく、UNION 前の2つのテーブルに対して個別に WHERE 句をかけ、その結果を返していることを示しています。インデックスもしっかりと適用されているので、高速に動作するはずです。

ちなみに、インデックスがない場合の実行計画は下記の通りです。

------------------------------------------------------------------------------------------
| Id  | Operation           | Name               | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                    |     2 |   190 |     6   (0)| 00:00:01 |
|   1 |  VIEW               | ALL_EMPLOYEES      |     2 |   190 |     6   (0)| 00:00:01 |
|   2 |   UNION-ALL         |                    |     2 |   174 |     6   (0)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL| EMPLOYEES          |     1 |    87 |     3   (0)| 00:00:01 |
|*  4 |    TABLE ACCESS FULL| ARCHIVED_EMPLOYEES |     1 |    87 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   3 - filter("EMPLOYEE_NAME"='高橋 三郎')
   4 - filter("EMPLOYEE_NAME"='高橋 三郎')

この場合でも Predicate Information を読むと、UNION 前の2つのテーブルに対して個別に WHERE 句をかけ、その結果を返していることがわかります。

おわりに

実際にテーブルを作成して UNION 句を使ってもインデックスが利用されることを確認しました。今回紹介した最適化は、一般には Predicate Pushdown と呼ばれているようです。これで、安心して UNION を使うことができますね。アプリケーションの複雑さをデータベースで吸収するのに役立つはずです。

今回は前置きなしに UNION ALL を利用しましたが、 ALL を与えない場合にはパフォーマンスの差があります。今回のテストデータではほとんど差がなかったため省略しましたが、実際に利用するときは注意してください。

また、今回実験したテーブルはうまくいく前提のシンプルな設計でした。そのため、より複雑なテーブルを取り扱う場合には、期待通り動かない可能性もあります。実行計画を確認した方が良いでしょう。

GitHubで編集を提案

Discussion