【MariaDB】サブクエリとEXISTSのA to Z:エラー解決とパフォーマンス最適化
MariaDB(MySQLと高い互換性を持つRDBMS)において、サブクエリとEXISTS
は、複雑な条件でのデータ抽出や、複数のテーブル間の関連性をチェックする際に非常に強力なツールです。
サブクエリ(Subquery)とは
サブクエリとは、別のSQL文(外部クエリ)の中にネストされた(入れ子になった)SQLクエリのことです。サブクエリは、外部クエリが必要とする値や行セットを提供するために使用されます。
基本的な考え方
- サブクエリが返す結果によって、様々な使い方が可能です。
- 単一の値(スカラサブクエリ)
- 単一の列の複数の値(
IN
句やANY
/ALL
句と組み合わせて使用) - 複数の列の複数の行(導出テーブルとして
FROM
句で使用) - 行の存在チェック(
EXISTS
句と組み合わせて使用)
- サブクエリはまず単独で実行され、その結果が外部クエリに渡されます。
- サブクエリは括弧
()
で囲まれます。
例
「orders
テーブルに注文が存在する顧客の情報をcustomers
テーブルから取得する」という場合、以下のように書けます。
SELECT customer_name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
この例では、SELECT customer_id FROM orders
がサブクエリであり、orders
テーブルに存在する顧客IDのリストを返します。外部クエリはそのリストに含まれる顧客IDを持つ顧客の名前を抽出します。
EXISTSオペレータとは
EXISTS
オペレータは、サブクエリが1つでも行を返す場合にTRUE
(真)を返し、行を全く返さない場合にFALSE
(偽)を返す論理オペレータです。WHERE
句で条件として使用されます。
EXISTS
の最も重要な特徴は、サブクエリが実際に返すデータの内容(カラムの値)には関心がなく、単に行が返されるかどうかにのみ関心がある点です。そのため、SELECT *
や SELECT 1
のように指定しても結果は同じになります。
構文
SELECT カラム1, カラム2, ...
FROM テーブル名
WHERE EXISTS (サブクエリ);
EXISTSの挙動と利点
- パフォーマンス
大量のデータが存在する場合、IN
句よりもEXISTS
の方が高速になることがあります。IN
句はサブクエリの結果セットを完全に生成してから外部クエリと比較するのに対し、EXISTS
はサブクエリが1行でも見つかった時点で処理を中断できるためです。ただし、これはインデックスの有無やクエリオプティマイザの判断によって異なります。 - 相関サブクエリとの相性
EXISTS
は「相関サブクエリ」と組み合わせて使用されることが非常に多いです。相関サブクエリとは、外部クエリの各行に対してサブクエリが実行され、サブクエリの条件に外部クエリのカラムが使用されるものです。 - 存在チェックに特化
特定の条件を満たすデータが存在するかどうかを効率的にチェックします。
例
先ほどの例をEXISTS
を使って書き直してみます。
「orders
テーブルに注文が存在する顧客の情報をcustomers
テーブルから取得する」
SELECT c.customer_name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
解説
- 外部クエリが
customers
テーブルから1行ずつ顧客データを取り出します(例えば、c.customer_id = 1
の顧客)。 - その顧客ID (
c.customer_id
) を使って、サブクエリ(SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id)
が実行されます。 - もし
orders
テーブルにc.customer_id
と同じcustomer_id
を持つ注文が1つでも存在すれば、サブクエリは1行以上の結果を返します。 - この場合、
EXISTS
条件はTRUE
となり、外部クエリはその顧客 (c.customer_name
) を結果セットに含めます。 - もし一致する注文が1つもなければ、サブクエリは何も行を返さず、
EXISTS
条件はFALSE
となり、その顧客は結果セットに含まれません。
NOT EXISTSオペレータ
EXISTS
の逆の条件をチェックするのがNOT EXISTS
です。サブクエリが行を全く返さない場合にTRUE
を返し、1つでも行を返す場合にFALSE
を返します。
例
「orders
テーブルにまだ注文が存在しない顧客の情報をcustomers
テーブルから取得する」
SELECT c.customer_name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
- NOT EXISTS
EXISTS
の逆で、サブクエリが全く結果を返さない場合にTRUE
を返す。 - EXISTS
サブクエリが1行でも結果を返せばTRUE
、そうでなければFALSE
を返す論理オペレータ。主に相関サブクエリと組み合わせて、関連するデータの存在チェックに用いられる。データ内容ではなく「存在するかどうか」のみを判断するため、効率的なクエリが可能。 - サブクエリ
SQL文の中にネストされた別のSQL文。外部クエリに必要なデータを提供する。
エラー:サブクエリが複数の行を返す場合 (Subquery returns more than 1 row)
エラーコード
ERROR 1242 (21000): Subquery returns more than 1 row
原因
スカラサブクエリ(単一の値を返すことが期待されるサブクエリ)が、実際には複数の行を返してしまった場合に発生します。例えば、=
(イコール)演算子や、単一の行を期待するコンテキストでサブクエリを使用した場合などです。
例
-- customersテーブルにcustomer_idが重複している場合に発生する可能性がある
SELECT customer_name
FROM customers
WHERE customer_id = (SELECT customer_id FROM orders WHERE order_total > 100);
もしorders
テーブルでorder_total > 100
となるcustomer_id
が複数存在する場合、このクエリはエラーになります。
トラブルシューティング
-
サブクエリを制限する
LIMIT 1
などを使って、サブクエリが返す行数を意図的に1つに制限する方法もありますが、結果が複数ある場合にどの1行が選ばれるかは保証されないため、慎重に検討する必要があります。-- 意図的に単一の行を返すように制限する場合 SELECT customer_name FROM customers WHERE customer_id = (SELECT customer_id FROM orders WHERE order_total > 100 ORDER BY order_date DESC LIMIT 1);
-
EXISTSを使用する
単に存在チェックをしたいだけで、サブクエリが返す具体的な値には興味がない場合は、EXISTS
を使用します。SELECT c.customer_name FROM customers c WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id AND o.order_total > 100);
-
INまたはANY/ALLを使用する
サブクエリが複数の値を返す可能性がある場合は、=
の代わりにIN
、ANY
、ALL
を使用します。IN
: サブクエリが返す値のいずれかに一致する場合ANY
/SOME
: サブクエリが返す値のいずれかが条件を満たす場合ALL
: サブクエリが返す全ての値が条件を満たす場合
SELECT customer_name FROM customers WHERE customer_id IN (SELECT customer_id FROM orders WHERE order_total > 100);
エラー:不正なカラム数 (Operand should contain 1 column(s))
エラーコード
ERROR 1241 (21000): Operand should contain 1 column(s)
原因
サブクエリが単一のカラムを返すことが期待されるコンテキストで、複数のカラムを返してしまった場合に発生します。
例
SELECT customer_name
FROM customers
WHERE (customer_id, customer_status) = (SELECT customer_id, status FROM orders WHERE order_id = 123);
トラブルシューティング
-
行コンパリゾンを正しく使用する
複数のカラムを比較したい場合は、行コンパリゾンの構文が適用されるか確認します。-- 正しい行コンパリゾン(MariaDB/MySQLではサポートされているが、すべてのRDBMSで一般的ではない) SELECT customer_name FROM customers WHERE (customer_id, customer_status) IN ((SELECT customer_id, status FROM orders WHERE order_id = 123));
または、個々のカラムで比較するように分解します。
SELECT c.customer_name FROM customers c WHERE c.customer_id = (SELECT o.customer_id FROM orders o WHERE o.order_id = 123) AND c.customer_status = (SELECT o.status FROM orders o WHERE o.order_id = 123);
-
単一のカラムのみを選択する
サブクエリが返すカラムが必要なカラムのみであることを確認します。
エラー:同じテーブルの更新と参照 (You can't specify target table 'x' for update in FROM clause)
エラーコード
ERROR 1093 (HY000): Table 'x' is specified twice, both as a target for 'UPDATE' and as a separate source for data
原因
UPDATE
またはDELETE
ステートメントのサブクエリ内で、更新/削除対象のテーブルと同じテーブルを参照している場合に発生します。これは、データの整合性を保つためのMariaDB/MySQLの制限です。
例
-- 同じテーブルを更新しながら参照しようとしている
UPDATE products
SET price = price * 1.1
WHERE product_id IN (SELECT product_id FROM products WHERE stock_quantity < 10);
トラブルシューティング
-
JOINを使用する
多くのサブクエリはJOIN
句に書き換えることができ、その方がパフォーマンスが良い場合が多いです。UPDATE products p JOIN (SELECT product_id FROM products WHERE stock_quantity < 10) AS low_stock ON p.product_id = low_stock.product_id SET p.price = p.price * 1.1;
-
導出テーブル(Derived Table)を使用する
サブクエリをFROM
句で導出テーブルとして使用することで、この制限を回避できます。UPDATE products p SET p.price = p.price * 1.1 WHERE p.product_id IN ( SELECT temp.product_id FROM (SELECT product_id FROM products WHERE stock_quantity < 10) AS temp );
パフォーマンスに関する問題
サブクエリやEXISTS
が遅いと感じる場合、以下の点を確認し、最適化を検討してください。
原因
まず、例で使用するテーブルを作成し、データを投入します。
-- 顧客テーブル
CREATE TABLE customers (
customer_id INT PRIMARY KEY AUTO_INCREMENT,
customer_name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
registration_date DATE
);
-- 商品テーブル
CREATE TABLE products (
product_id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INT NOT NULL
);
-- 注文テーブル
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
-- 注文詳細テーブル (どの商品が注文されたか)
CREATE TABLE order_items (
order_item_id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
item_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- データ挿入
INSERT INTO customers (customer_name, email, registration_date) VALUES
('佐藤 太郎', '[email protected]', '2023-01-10'),
('鈴木 花子', '[email protected]', '2023-02-15'),
('田中 次郎', '[email protected]', '2023-03-20'),
('山本 明美', '[email protected]', '2023-04-01'),
('中村 健太', '[email protected]', '2023-05-05');
INSERT INTO products (product_name, price, stock_quantity) VALUES
('ノートPC', 120000.00, 50),
('スマートフォン', 80000.00, 120),
('ワイヤレスイヤホン', 15000.00, 200),
('モバイルバッテリー', 3000.00, 300),
('キーボード', 8000.00, 80);
INSERT INTO orders (customer_id, order_date, total_amount) VALUES
(1, '2024-01-20', 120000.00), -- 佐藤太郎: ノートPC
(2, '2024-02-25', 80000.00), -- 鈴木花子: スマートフォン
(1, '2024-03-01', 15000.00), -- 佐藤太郎: ワイヤレスイヤホン
(3, '2024-04-10', 3000.00), -- 田中次郎: モバイルバッテリー
(2, '2024-05-15', 8000.00); -- 鈴木花子: キーボード
INSERT INTO order_items (order_id, product_id, quantity, item_price) VALUES
(1, 1, 1, 120000.00),
(2, 2, 1, 80000.00),
(3, 3, 1, 15000.00),
(4, 4, 1, 3000.00),
(5, 5, 1, 8000.00);
-- 注文がない顧客を確認するためのNULLデータ
-- (山本明美と中村健太はまだ注文がない状態)
スカラサブクエリ (Scalar Subquery)
単一の値を返すサブクエリで、主にWHERE
句やSELECT
句で使用されます。
例1-1: 平均価格より高い商品を探す
SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
解説
SELECT AVG(price) FROM products
が最初に実行され、商品の平均価格(単一の値)を返します。外部クエリはその平均価格より高い商品を選択します。
IN句とサブクエリ
サブクエリが複数の値を返す場合に、外部クエリの条件にそのリストを使用します。
例2-1: 少なくとも1つ注文したことがある顧客の名前を取得する
SELECT customer_name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
解説
SELECT customer_id FROM orders
が実行され、注文したことのある顧客IDのリストを返します。外部クエリはそのリストに含まれるcustomer_id
を持つ顧客の名前を取得します。
例2-2: 在庫が100個以下の商品を含む注文の詳細を取得する
SELECT oi.order_id, p.product_name, oi.quantity
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
WHERE oi.product_id IN (SELECT product_id FROM products WHERE stock_quantity < 100);
解説
まず、stock_quantity < 100
の条件を満たすproduct_id
のリストがサブクエリで取得されます。次に、order_items
とproducts
を結合し、そのリストに含まれるproduct_id
を持つ注文アイテムをフィルタリングします。
EXISTSと相関サブクエリ
EXISTS
は、サブクエリが1つでも行を返すかどうかの真偽値に基づいて外部クエリの行をフィルタリングします。通常、外部クエリの各行に対してサブクエリが実行される「相関サブクエリ」として使用されます。
例3-1: 少なくとも1つ注文したことがある顧客の名前を取得する(EXISTS
版)
SELECT c.customer_name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
解説
customers
テーブルの各行(例えば、c.customer_id = 1
の顧客)に対して、サブクエリSELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
が実行されます。もしorders
テーブルにその顧客IDの注文が1つでもあれば、EXISTS
はTRUE
を返し、その顧客のcustomer_name
が結果に含まれます。
例3-2: 特定の商品(例: 'ノートPC')を購入した顧客の名前を取得する
SELECT c.customer_name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.customer_id = c.customer_id
AND p.product_name = 'ノートPC'
);
解説
これはより複雑なEXISTS
の例です。各顧客に対して、その顧客が'ノートPC'を含む注文をしたことがあるかどうかをチェックします。サブクエリ内で複数のテーブルを結合して条件を絞り込んでいます。
NOT EXISTSと相関サブクエリ
NOT EXISTS
はEXISTS
の逆で、サブクエリが全く行を返さない場合にTRUE
を返します。
例4-1: まだ一度も注文したことがない顧客の名前を取得する
SELECT c.customer_name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
解説
customers
テーブルの各行に対して、サブクエリが実行されます。もしorders
テーブルにその顧客IDの注文が一つもなければ(サブクエリが何も返さない)、NOT EXISTS
はTRUE
を返し、その顧客の名前が結果に含まれます。この例では、山本明美
と中村健太
が表示されるはずです。
例4-2: 在庫が全くない商品に関連する注文がないことを確認する(例: 注文はあったが、現在は在庫0の商品がない)
SELECT p.product_name, p.stock_quantity
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.product_id
AND p.stock_quantity = 0
);
解説
このクエリは、「在庫が0ではない商品」をリストアップします。もし、ある商品のproduct_id
を持つorder_items
が存在し、かつその商品のstock_quantity
が0
である場合、NOT EXISTS
はFALSE
となり、その商品は結果に含まれません。
FROM句におけるサブクエリ(導出テーブル)
サブクエリの結果を一時的なテーブルとして扱い、外部クエリでその一時テーブルを結合したり選択したりします。
例5-1: 各顧客の総注文金額と顧客情報を結合する
SELECT c.customer_name, IFNULL(order_summary.total_orders_amount, 0) AS total_spent
FROM customers c
LEFT JOIN (
SELECT customer_id, SUM(total_amount) AS total_orders_amount
FROM orders
GROUP BY customer_id
) AS order_summary ON c.customer_id = order_summary.customer_id;
解説
FROM
句内のサブクエリは、各顧客の総注文金額を計算した一時的なテーブルorder_summary
を作成します。その後、customers
テーブルとorder_summary
をLEFT JOIN
で結合し、すべての顧客と、存在すればその総注文金額を表示します。IFNULL
は、まだ注文がない顧客のtotal_spent
を0
として表示するために使用しています。
主に、JOIN
句を使用する方法と、より現代的なCTE
(共通テーブル式)を使用する方法が挙げられます。
JOIN句 (JOIN Clauses)
ほとんどのサブクエリ、特にIN
やEXISTS
を使用するクエリは、JOIN
句に書き換えることが可能です。JOIN
は、データベースオプティマイザがより効率的な実行計画を立てやすいため、パフォーマンスが改善されることがよくあります。
例1-1: IN
の代替 - INNER JOIN
元となるサブクエリ (IN
を使用): 少なくとも1つ注文したことがある顧客の名前を取得
SELECT customer_name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
INNER JOINへの代替
SELECT c.customer_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name;
解説
INNER JOIN
は、両方のテーブルに一致する行がある場合にのみ結果を返します。GROUP BY
を使用することで、同じ顧客が複数の注文を持っていても、顧客名が重複して表示されるのを防ぎます。通常、このJOIN
形式はIN
サブクエリよりもパフォーマンスが良いことが多いです。
例1-2: EXISTS
の代替 - INNER JOIN
(または EXISTS
が適している場合)
元となるサブクエリ (EXISTS
を使用): 少なくとも1つ注文したことがある顧客の名前を取得
SELECT c.customer_name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
INNER JOINへの代替
SELECT DISTINCT c.customer_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id;
解説
この場合もINNER JOIN
で代用できます。DISTINCT
キーワードは、customer_name
が重複して表示されるのを防ぎます。
EXISTS vs JOIN の使い分けについて
- JOIN
関連するテーブルからデータを取得したり、集計したりする必要がある場合に適しています。また、オプティマイザがJOIN
句の最適化に優れていることが多いため、インデックスが適切であれば高速に実行されます。 - EXISTS
サブクエリが1行でも見つかった時点で処理を中断するため、**「存在するかどうか」**だけをチェックする場合には非常に効率的です。特に、外部クエリの行数が少なく、サブクエリの結果セットが非常に大きい場合に有利になることがあります。
どちらが高速かはデータ量やインデックス、MariaDBのバージョンやオプティマイザの挙動に依存するため、EXPLAIN
で実行計画を確認し、両方の方法でベンチマークを行うことが推奨されます。
例1-3: NOT EXISTS
の代替 - LEFT JOIN
+ IS NULL
元となるサブクエリ (NOT EXISTS
を使用): まだ一度も注文したことがない顧客の名前を取得
SELECT c.customer_name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
LEFT JOIN + IS NULLへの代替
SELECT c.customer_name
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL; -- ordersテーブルに一致する行がなかった場合、その行はNULLになる
解説
LEFT JOIN
は、左側のテーブル(customers
)のすべての行を保持し、右側のテーブル(orders
)から一致する行があればそれを結合します。一致する行がなければ、右側のテーブルのカラムはNULL
になります。この性質を利用して、o.order_id IS NULL
でフィルタリングすることで、NOT EXISTS
と同じ結果を得られます。このパターンは「アンチジョイン」とも呼ばれ、非常に一般的で効率的な代替手段です。
導出テーブル (Derived Tables)
FROM
句の中にサブクエリを記述し、そのサブクエリの結果を一時的なテーブル(導出テーブル)として扱います。
例2-1: 各顧客の総注文金額と顧客情報を結合する (FROM
句サブクエリ)`
SELECT c.customer_name, IFNULL(order_summary.total_orders_amount, 0) AS total_spent
FROM customers c
LEFT JOIN (
SELECT customer_id, SUM(total_amount) AS total_orders_amount
FROM orders
GROUP BY customer_id
) AS order_summary ON c.customer_id = order_summary.customer_id;
解説
この例は、前回の「プログラミング例」で既に紹介したものですが、これもサブクエリの代替手段として非常に有効です。JOIN
句を使用する代わりに、FROM
句で複雑な集計やフィルタリングを行った結果を一時的なテーブルとして定義し、それに対して外部クエリを実行します。これにより、クエリの論理構造が明確になり、複雑な計算を段階的に行うことができます。
共通テーブル式 (CTE - Common Table Expressions) / WITH句
MariaDB 10.2以降で利用可能なCTE
は、より複雑なクエリをより読みやすく、管理しやすくするための強力な機能です。再帰クエリにも対応しています。
例3-1: 各顧客の総注文金額と顧客情報を結合する (CTE
を使用)
WITH CustomerOrderSummary AS (
SELECT customer_id, SUM(total_amount) AS total_orders_amount
FROM orders
GROUP BY customer_id
)
SELECT c.customer_name, IFNULL(cos.total_orders_amount, 0) AS total_spent
FROM customers c
LEFT JOIN CustomerOrderSummary cos ON c.customer_id = cos.customer_id;
解説
WITH
句でCustomerOrderSummary
という名前の一時的な結果セットを定義しています。このCustomerOrderSummary
は、その後のSELECT
文で通常のテーブルのように参照できます。導出テーブルと似ていますが、CTEは複数のCTEを連鎖させたり、再帰的に使用したりできる点で、より柔軟で可読性の高いクエリを記述できます。特にクエリが非常に複雑になる場合や、同じサブクエリの結果を複数回使用する場合に有効です。
NOT INとLEFT JOIN + IS NULL / NOT EXISTS の比較
NOT IN
は直感的ですが、サブクエリがNULL
を含む場合に予期せぬ結果(全ての行が返されない)になるという落とし穴があります。そのため、NOT EXISTS
またはLEFT JOIN
+ IS NULL
の方が安全で、一般的に推奨されます。
-
NOT EXISTS
(前述の例4-1参照)LEFT JOIN
+IS NULL
(前述の例1-3参照)
-
NOT INの注意点
-- もしorders.customer_idにNULLが存在する可能性があれば、このクエリは空の結果を返す可能性がある SELECT customer_name FROM customers WHERE customer_id NOT IN (SELECT customer_id FROM orders);
サブクエリの結果に
NULL
が含まれる場合、NOT IN
は常にUNKNOWN
となり、結果的にどの行も選択されません。
- 複雑なサブクエリの可読性向上
- 導出テーブル (
FROM
句内のサブクエリ): 一時的な結果セットを生成し、それをメインクエリで利用します。 - 共通テーブル式 (CTE /
WITH
句): MariaDB 10.2以降で利用可能。クエリの論理構造を明確にし、複雑なクエリの可読性と保守性を向上させます。
- 導出テーブル (
- NOT EXISTSサブクエリの代替
LEFT JOIN
+WHERE ... IS NULL
: 関連するデータが存在しない行を効率的に特定するアンチジョインの典型的なパターンです。NOT IN
よりも安全で推奨されます。
- EXISTSサブクエリの代替
INNER JOIN
+DISTINCT
:EXISTS
と似た結果を得られます。どちらが高速かはケースバイケースでEXPLAIN
で比較推奨。
- INサブクエリの代替
INNER JOIN
: 関連するデータが存在する場合にデータを結合し、GROUP BY
やDISTINCT
で重複を処理します。ほとんどの場合、IN
より効率的です。