自動化無しに生活無し

WEB開発関係を中心に備忘録をまとめています

EXISTSの使い方

thumbnail

DBはOracle。

外部キーを使った存在チェック

SELECT d.department_id, d.department_name
FROM departments d
WHERE EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.department_id
);
Copy

このSQLは、部署内に社員がいる部署だけを返す。

まず、departments内のデータを取り出す。

employeesからdepartment_id で一致するものが1件でもあれば、そのdepartmentsを表示する。

イメージ的には全走をして、行が見つかればTRUEを返してアーリーリターンをしている。(※ただしDB的には集合志向のため、全走をしているとは言えない。)

これは、集約+結合を使えば、ほぼ同じ結果が得られる。

SELECT 
      d.department_id
    , MIN(d.department_name)
FROM employees e
INNER JOIN departments d
    ON e.department_id = d.department_id
GROUP BY d.department_id 
Copy

ただし、こちらは全行スキャンをしているため、パフォーマンスは低い。

department_name に対しても集約関数が必要になるため、完全に先の結果と一致するとは言えない

EXISTSの方は早期終了可能で、departmentを基準に動作しているため、department_name もそのまま使える。

まとめると

  • EXISTSは全走して、行が1件でも見つかればアーリーリターンをしているイメージ
  • 結合+集約は本当に全走をして、アーリーリターンをすることもないイメージ

どちらが高速であるかはあえて言うまでもない。

外部キーを使った存在しないチェック

社員がいない部署を取得している。

SELECT d.department_id, d.department_name
FROM departments d
WHERE NOT EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.department_id
);
Copy

これも、結合+集約でほぼ同じ表現ができる。

SELECT 
      d.department_id
    , MIN(d.department_name)
FROM
    departments d
LEFT JOIN 
    employees e
    ON d.department_id = e.department_id
WHERE
    e.employee_name IS NULL 
GROUP BY 
    d.department_id
Copy

LEFT JOIN で結合先がなかった場合NULLであることを利用している。

しかし、繰り返しになるが、この結合+集約も全走している。

(NOT) EXISTSを使うメリットは、存在する(しない)場合に効果が発動するため、1件でも見つかればTrue(False)でアーリーリターン可能である点にある。

相関サブクエリとの組み合わせ

-- 社員の中で、部長(manager_id を持つ社員)が存在する人だけ取得
SELECT e.employee_id, e.first_name, e.manager_id
FROM employees e
WHERE EXISTS (
    SELECT 1
    FROM employees m
    WHERE m.employee_id = e.manager_id
);
Copy

サブクエリの外で宣言されているeを行ごとに呼び出し、比較をしている。

これはINでも表現はできる。

SELECT 
    e.employee_id
    , e.first_name
    , e.manager_id
FROM 
    employees e
WHERE
    e.manager_id IN (
        SELECT m.employee_id
        FROM employees m
        WHERE m.employee_id IS NOT NULL
    );
Copy

これは相関サブクエリではないが、同様の結果が得られる。

相関サブクエリのINになる場合、パフォーマンスに問題が出る可能性がある。

INとEXISTSのパフォーマンス上の違い

INとEXISTSでは、基本的にパフォーマンス上の違いはない。

しかし相関サブクエリ(サブクエリが呼び出し元の列を行ごとに参照している状況)では、アーリーリターンできる分、EXISTSの方が高速。

NOT IN の場合はリストの中にNULLが含まれると結果が何も得られなくなってしまうため、EXISTSの方が安全。

集計条件をサブクエリで指定する

-- 社員数が5人以上の部署だけ取得
SELECT d.department_id, d.department_name
FROM departments d
WHERE EXISTS (
    SELECT 1
    FROM employees e
    WHERE e.department_id = d.department_id
    GROUP BY e.department_id
    HAVING COUNT(*) >= 5
);
Copy

これも相関サブクエリ。INで表現をした場合、非相関になる。

SELECT d.department_id, d.department_name
FROM departments d
WHERE d.department_id IN (
    SELECT e.department_id
    FROM employees e
    GROUP BY e.department_id
    HAVING COUNT(*) >= 5
);
Copy

この場合、INの方が1回しか評価されないためINのほうが高速になる可能性がある。

先のEXISTSはdepartmentの行ごとに表が作られ、更に集約までしまうため、ほぼ全走である。

たとえEXISTSがアーリーリターンが使えても、集約で全走状態になるため、INに劣る可能性がある。

まとめると。

  • 集約ありの場合、非相関のIN
    • 【補足】 集約で全走しないといけないため、1回の評価で済む非相関サブクエリのほうが有利
  • 集約なしの場合、相関のEXISTS
    • 【補足】 集約なしの場合、アーリーリターンが使える(全走しない)EXISTSが有利

ポイントは全走をしているかしていないか。全走する状態での相関サブクエリは遅い。

存在量化と全称量化

  • 存在量化: 条件に一致する行が1件でもある (EXISTS)
  • 全称量化: 全ての行はある条件に一致する (FORALL)

存在量化の否定は全称量化に値する。つまりNOT EXISTS = FORALL である。

NOT EXISTSを使えば、全ての行はある条件に一致するかを調べることができる。

-- 部署内の全員の給料が5000以上である、部署と社員を取り出す。

SELECT 
	e1.DEPARTMENT_ID
	, e1.FIRST_NAME
	, e1.SALARY
FROM EMPLOYEES e1
WHERE NOT EXISTS (
	SELECT 1
	FROM EMPLOYEES e2
	WHERE e1.DEPARTMENT_ID = e2.DEPARTMENT_ID 
	AND e2.SALARY < 5000
);

-- 部署内の全員の給料が5000以上である
-- = 給料5000未満のリスト には存在していない人を取り出す。
Copy

条件を否定し、さらにEXISTSも否定すれば、二重否定で肯定になる。

が、このSQLは非常に分かりづらい。

一見5000未満の給料を取り出しているように見えるが、NOT EXISTSにより5000以上になる。

しかも自己参照をしており、同一部署の全ての人が5000以上である場合に限定される。

NOT EXISTSを使えば、このような表現は可能ではあるが、可読性だけでなくパフォーマンスの観点から考えても、下記の集約+HAVINGでも成立はする。

-- 集約してHAVINGを使う方法の方がわかりやすい。

SELECT DEPARTMENT_ID
	, FIRST_NAME
	, SALARY
FROM EMPLOYEES
GROUP BY DEPARTMENT_ID
HAVING MIN(SALARY) >= 5000;
Copy

直感的に表現できるこちらのほうが良い。

スポンサーリンク