[SQL] EXISTSとINの使い分け
今回はSQLの EXISTS と IN の違いについて解説する。
どちらもサブクエリを使って「ある条件を満たす行」を抽出する際によく使われるが、挙動やパフォーマンスに違いがある。
特に NULLの扱い と パフォーマンス の観点から、どのように使い分けるべきかを見ていく。
準備
本サイトではpythonのライブラリであるsqlite3とipython-sqlを利用して、Jupyter Notebook上でSQLを実行・解説する。 また、データフレームの表示には Polars を利用する。
github
- githubのjupyter notebook形式のファイルはこちら
google colaboratory
- google colaboratory で実行する場合はこちら
筆者の環境
筆者の環境である。
!sw_vers
ProductName: macOS
ProductVersion: 15.5
BuildVersion: 24F74
!python -V
Python 3.12.12
必要なライブラリを読み込む。
import sqlite3
import polars as pl
%load_ext sql
%config SqlMagic.feedback = True
%config SqlMagic.style = '_DEPRECATED_DEFAULT'
データの準備
サンプルとして、顧客テーブル Customers と 注文テーブル Orders を作成する。
import os
if os.path.exists("data.db"):
os.remove("data.db")
os.system("touch data.db")
os.system("chmod 664 data.db")
# データベース接続
%sql sqlite:///data.db
'Connected: @data.db'
%%sql
CREATE TABLE Customers (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE Orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount INTEGER
);
INSERT INTO Customers (name) VALUES
('Aさん'),
('Bさん'),
('Cさん'),
('Dさん');
INSERT INTO Orders (customer_id, amount) VALUES
(1, 100),
(1, 200),
(3, 500);
* sqlite:///data.db
Done.
Done.
4 rows affected.
3 rows affected.
[]
%%sql
SELECT * FROM Customers;
* sqlite:///data.db
Done.
| id | name |
|---|---|
| 1 | Aさん |
| 2 | Bさん |
| 3 | Cさん |
| 4 | Dさん |
%%sql
SELECT * FROM Orders;
* sqlite:///data.db
Done.
| id | customer_id | amount |
|---|---|---|
| 1 | 1 | 100 |
| 2 | 1 | 200 |
| 3 | 3 | 500 |
課題:注文履歴がある顧客を抽出する
Aさん、Cさんは注文履歴があるが、Bさん、Dさんは注文履歴がない。 注文履歴がある顧客だけを抽出してみる。
方法1: IN を使用する場合
IN 句を使って、Orders テーブルにある customer_id のリストに含まれる顧客を抽出する。
query = """
SELECT
*
FROM
Customers
WHERE
id IN (SELECT customer_id FROM Orders);
"""
with sqlite3.connect("data.db") as conn:
df = pl.read_database(query, connection=conn)
display(df)
shape: (2, 2)
| id | name |
|---|---|
| 1 | “Aさん” |
| 3 | “Cさん” |
方法2: EXISTS を使用する場合
EXISTS 句を使って、Orders テーブルに関連する行が存在するかどうかをチェックする。
query = """
SELECT
*
FROM
Customers c
WHERE
EXISTS (
SELECT
1
FROM
Orders o
WHERE
c.id = o.customer_id
);
"""
with sqlite3.connect("data.db") as conn:
df = pl.read_database(query, connection=conn)
display(df)
shape: (2, 2)
| id | name |
|---|---|
| 1 | “Aさん” |
| 3 | “Cさん” |
解説と違い
結果は同じだが、以下の違いがある。
NULLの扱い
INはリスト内にNULLが含まれると、NOT INの結果がすべて不明(空)になる場合があるなど、直感的でない挙動をすることがある。EXISTSは行の存在有無だけを見るため、NULLの影響を受けにくい。
パフォーマンス
INはサブクエリの結果をすべて取得してから比較を行うことが多い。EXISTSは条件に合致する行が1つでも見つかれば、その時点で検索を打ち切る(Trueを返す)ため、特にサブクエリの結果が大量にある場合に高速になる傾向がある。
まとめ
- 基本的には EXISTS を使うのが無難である。
- 特に
NOT INを使う場合は、NULLによる予期せぬ挙動を避けるため、NOT EXISTS に書き換えることを強く推奨する。