[SQL] EXISTSとINの使い分け

今回はSQLの EXISTSIN の違いについて解説する。 どちらもサブクエリを使って「ある条件を満たす行」を抽出する際によく使われるが、挙動やパフォーマンスに違いがある。

特に 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.
idname
1Aさん
2Bさん
3Cさん
4Dさん
%%sql
SELECT * FROM Orders;
 * sqlite:///data.db
Done.
idcustomer_idamount
11100
21200
33500

課題:注文履歴がある顧客を抽出する

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)

idname
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)

idname
1“Aさん”
3“Cさん”

解説と違い

結果は同じだが、以下の違いがある。

  1. NULLの扱い

    • IN はリスト内に NULL が含まれると、NOT IN の結果がすべて不明(空)になる場合があるなど、直感的でない挙動をすることがある。
    • EXISTS は行の存在有無だけを見るため、NULL の影響を受けにくい。
  2. パフォーマンス

    • IN はサブクエリの結果をすべて取得してから比較を行うことが多い。
    • EXISTS は条件に合致する行が1つでも見つかれば、その時点で検索を打ち切る(Trueを返す)ため、特にサブクエリの結果が大量にある場合に高速になる傾向がある。

まとめ

  • 基本的には EXISTS を使うのが無難である。
  • 特に NOT IN を使う場合は、NULL による予期せぬ挙動を避けるため、NOT EXISTS に書き換えることを強く推奨する。