SQLのサブクエリ(副問い合わせ)とは ― IN・EXISTS
SQLの
サブクエリとは、SELECT文の中に書く別のSELECT文
SQLのサブクエリ(副問い合わせ)とは、SELECT文の中に丸括弧で囲んで書く、別のSELECT文です。外側のSELECT文はメインクエリと呼びます。
「全注文の平均より高い注文だけ」のように、別の問い合わせで求めた値で絞りたいときに使います。
例には、次の顧客テーブルと注文テーブルを使います。注文5006は会員登録をしていない人の注文で、顧客IDがNULLです。
| 顧客ID | 氏名 |
|---|---|
| 101 | 田中美咲 |
| 102 | 佐藤健太 |
| 103 | 鈴木大輔 |
| 104 | 高橋愛 |
| 注文ID | 顧客ID | 商品 | 金額 |
|---|---|---|---|
| 5001 | 101 | ノートPC | 98,000 |
| 5002 | 101 | マウス | 1,200 |
| 5003 | 102 | キーボード | 3,500 |
| 5004 | 102 | モニター | 15,000 |
| 5005 | 103 | マウス | 1,500 |
| 5006 | NULL | デスク | 32,000 |
全注文の平均より高い注文だけを知りたいとします。GROUP BYの記事の集約関数で平均は求められますが、WHEREには集約関数を直接書けません。そこで、平均を求めるSELECT文を、WHEREの条件の中に入れます。
SELECT 注文ID, 金額
FROM 注文
WHERE 金額 > (
SELECT AVG(金額) FROM 注文
);考え方のうえでは、内側のSELECT文が先に実行され、平均の25200が外側に渡ります。外側は「金額 > 25200」の条件で行を絞り、注文5001と注文5006が残ります。実際の実行手順はDBMSが決めます。
サブクエリは、WHERE句のほか、FROM句とSELECT句にも書けます。SELECT文の基本のWHERE句に、別のSELECT文を入れられると考えてください。
WHERE句では、サブクエリの結果を比較・IN・EXISTSで使う
WHERE句のサブクエリは、内側が返す形によって、書き方が決まります。比較演算子は1つの値、INは1列の複数行を受け取り、EXISTSは行があるかどうかだけを見ます。
| 書き方 | 内側が | 条件が |
|---|---|---|
| 比較演算子(=・>など) | 1行1列(1つの値) | 外側の値と、内側の値の比較が成り立つとき |
| IN | 1列で複数行 | 外側の値が、内側の値のどれかに等しいとき |
| EXISTS | 何でもよい(行があるかだけを見る) | 内側が1行以上を返すとき |
比較演算子では、内側は1つの値を返す
内側が1行1列だけを返すサブクエリを、スカラーサブクエリと呼びます。前の節の平均を求める例が、これに当たります。
内側が2行以上を返すと、PostgreSQLでもMySQLでもエラーになります。内側が0行のときはエラーにならず、結果はNULLになります。NULLとの比較は成り立たないため、外側の行は1行も残りません。
INでは、内側が返した複数の値のどれかに等しいかを調べる
INは、外側の値が内側の結果のどれかに等しいときに真になります。内側は1列を返す必要があります。
SELECT 氏名
FROM 顧客
WHERE 顧客ID IN (
SELECT 顧客ID FROM 注文
);内側は、注文テーブルの顧客IDを6行返します。田中さんの顧客IDは内側に2回出ますが、外側では顧客テーブルの1行として1回だけ残ります。結果は、田中さん、佐藤さん、鈴木さんの3行です。
NOT INは、INの逆に、どの値にも等しくない行を残します。ただし、内側の結果にNULLが1つでも入っていると、NOT INはどの行でも真になりません。注文5006の顧客IDがNULLなので、注文のない顧客を探す次の文は、0行を返します。
SELECT 氏名
FROM 顧客
WHERE 顧客ID NOT IN (
SELECT 顧客ID FROM 注文
);PostgreSQLの公式ドキュメントも、内側にNULLがあるとNOT INの結果が真ではなくNULLになると説明しています。内側に「WHERE 顧客ID IS NOT NULL」を足すか、次に説明するNOT EXISTSを使うと、高橋さんが正しく残ります。値を並べたNOT INの一覧にNULLを書いても同じ結果になることは、INとBETWEENの記事で確かめています。
EXISTSでは、内側が1行でも返すかを調べる
EXISTSは、内側が1行以上を返すときに真になり、0行のときに偽になります。返された値の中身は見ないため、内側のSELECT句は「SELECT 1」のように短く書くのが一般的です。ただし、COUNT(*)などの集約関数を書くと、条件に合う行が無くても内側は1行を返します。そのため、EXISTSはどの行でも真になります。
SELECT 氏名
FROM 顧客 AS c
WHERE EXISTS (
SELECT 1 FROM 注文 AS o
WHERE o.顧客ID = c.顧客ID
);この文の内側は、外側の列c.顧客IDを使っています。このようなサブクエリを、次の節で説明する相関サブクエリと呼びます。NOT EXISTSにすると、注文が1件も無い顧客だけが残ります。
FROM句のサブクエリは、集計した結果を表として使う
FROM句に書いたサブクエリの結果は、1つの表として扱われます。この表を派生表と呼びます。
SELECT t.顧客ID, t.合計
FROM (
SELECT 顧客ID, SUM(金額) AS 合計
FROM 注文
GROUP BY 顧客ID
) AS t
WHERE t.合計 >= 50000;内側が顧客ごとの合計を求め、外側はその結果を表tとして、合計が50000以上の行を残します。この例のように絞るだけなら、HAVINGでも書けます。FROM句のサブクエリを使うと、集計した結果に、別の条件や結合を重ねられます。UNION ALLでつないだ結果を合計するときも、この形で書きます。
FROM句のサブクエリには、別名を付けます。標準SQLでは別名が必須で、MySQLも必須です。PostgreSQLは省略できますが、ほかのDBMSでも動くように付けるのがよいとしています。
SELECT句にサブクエリを書くと、その行の列として値が並びます。内側は1つの値を返す必要があります。
SELECT 氏名,
(SELECT COUNT(*) FROM 注文 AS o
WHERE o.顧客ID = c.顧客ID) AS 注文数
FROM 顧客 AS c;この例も相関サブクエリです。注文がない高橋さんも、COUNT(*)が0を返すため、0件の行として結果に残ります。
JOINとサブクエリは、結果に必要な列と目的で使い分ける
注文のある顧客の氏名を求めるだけなら、JOINでもIN・EXISTSでも書けます。違いは、結果の行数です。
SELECT 顧客.氏名
FROM 顧客
INNER JOIN 注文
ON 顧客.顧客ID = 注文.顧客ID;このJOINは5行を返し、田中さんと佐藤さんが2行ずつ出ます。INやEXISTSで書くと、前の節のとおり3行になります。JOINで1人1行にするには、DISTINCTが必要です。
| やりたい | 向く | 理由 |
|---|---|---|
| 両方の表の列を結果に並べる | JOIN | WHERE句のサブクエリの列は、外側の結果に出せない |
| 相手の行が「ある」ことだけで絞る | IN・EXISTS | 外側の1行が、そのまま1行で結果に残る |
| 集計した値(平均など)と比べる | スカラーサブクエリ | WHEREに集約関数を直接書けないため、内側で集計して渡す |
| 集計した結果を、さらに絞る | FROM句のサブクエリ | 集計結果を1つの表として扱える |
| 相手の行が「ない」ものを探す | NOT EXISTS | NOT INは内側にNULLがあると結果が空になる |
注文のない顧客は、LEFT JOINで注文側がNULLの行を探す書き方でも求められます。どの書き方が速いかは、DBMSとデータの量によって変わります。上の表で比べているのは、結果の違いだけです。
書き方を選んで、SQL文と結果を見る
次の図でサブクエリの書き方を選ぶと、SQL文、内側の結果、全体の結果が変わります。顧客テーブルと注文テーブルは、最初に示したものと同じです。図は実際のデータベースには接続しておらず、結果はあらかじめ確かめた固定の値です。
SELECT 注文ID, 金額
FROM 注文
WHERE 金額 > (
SELECT AVG(金額) FROM 注文
);| AVG(金額) |
|---|
| 25,200 |
| 注文ID | 金額 |
|---|---|
| 5001 | 98,000 |
| 5006 | 32,000 |
SELECT 氏名
FROM 顧客
WHERE 顧客ID IN (
SELECT 顧客ID FROM 注文
);| 顧客ID |
|---|
| 101 |
| 101 |
| 102 |
| 102 |
| 103 |
| NULL |
| 氏名 |
|---|
| 田中美咲 |
| 佐藤健太 |
| 鈴木大輔 |
SELECT 氏名
FROM 顧客
WHERE 顧客ID NOT IN (
SELECT 顧客ID FROM 注文
);| 顧客ID |
|---|
| 101 |
| 101 |
| 102 |
| 102 |
| 103 |
| NULL |
| 氏名 |
|---|
| (該当する行なし) |
SELECT 氏名
FROM 顧客
WHERE 顧客ID NOT IN (
SELECT 顧客ID FROM 注文
WHERE 顧客ID IS NOT NULL
);| 顧客ID |
|---|
| 101 |
| 101 |
| 102 |
| 102 |
| 103 |
| 氏名 |
|---|
| 高橋愛 |
SELECT 氏名
FROM 顧客 AS c
WHERE EXISTS (
SELECT 1 FROM 注文 AS o
WHERE o.顧客ID = c.顧客ID
);| c.顧客ID | 内側が | EXISTS |
|---|---|---|
| 101 | 2行 | 真 |
| 102 | 2行 | 真 |
| 103 | 1行 | 真 |
| 104 | 0行 | 偽 |
| 氏名 |
|---|
| 田中美咲 |
| 佐藤健太 |
| 鈴木大輔 |
SELECT 氏名
FROM 顧客 AS c
WHERE NOT EXISTS (
SELECT 1 FROM 注文 AS o
WHERE o.顧客ID = c.顧客ID
);| c.顧客ID | 内側が | NOT EXISTS |
|---|---|---|
| 101 | 2行 | 偽 |
| 102 | 2行 | 偽 |
| 103 | 1行 | 偽 |
| 104 | 0行 | 真 |
| 氏名 |
|---|
| 高橋愛 |
SELECT t.顧客ID, t.合計
FROM (
SELECT 顧客ID, SUM(金額) AS 合計
FROM 注文
GROUP BY 顧客ID
) AS t
WHERE t.合計 >= 50000;| 顧客ID | 合計 |
|---|---|
| NULL | 32,000 |
| 101 | 99,200 |
| 102 | 18,500 |
| 103 | 1,500 |
| 顧客ID | 合計 |
|---|---|
| 101 | 99,200 |
SELECT 氏名,
(SELECT COUNT(*)
FROM 注文 AS o
WHERE o.顧客ID = c.顧客ID)
AS 注文数
FROM 顧客 AS c;| c.顧客ID | 内側の |
|---|---|
| 101 | 2 |
| 102 | 2 |
| 103 | 1 |
| 104 | 0 |
| 氏名 | 注文数 |
|---|---|
| 田中美咲 | 2 |
| 佐藤健太 | 2 |
| 鈴木大輔 | 1 |
| 高橋愛 | 0 |
SELECT 注文ID, 顧客ID, 金額
FROM 注文 AS o
WHERE 金額 = (
SELECT MAX(金額) FROM 注文
WHERE 顧客ID = o.顧客ID
);| o.注文ID | o.顧客ID | 内側の |
|---|---|---|
| 5001 | 101 | 98,000 |
| 5002 | 101 | 98,000 |
| 5003 | 102 | 15,000 |
| 5004 | 102 | 15,000 |
| 5005 | 103 | 1,500 |
| 5006 | NULL | NULL |
| 注文ID | 顧客ID | 金額 |
|---|---|---|
| 5001 | 101 | 98,000 |
| 5004 | 102 | 15,000 |
| 5005 | 103 | 1,500 |
参考にした資料
- PostgreSQL公式ドキュメント「Value Expressions」(スカラーサブクエリ、外側の列を参照できること)
- PostgreSQL公式ドキュメント「Subquery Expressions」(EXISTS・IN・NOT INとNULL)
- PostgreSQL公式ドキュメント「Table Expressions」(FROM句のサブクエリと別名)
- MySQL 8.4リファレンスマニュアル「Subqueries」(相関サブクエリとその書き換え・派生表・複数行を返すエラー)
練習問題(全6問)
正解:SELECT文の
サブクエリは、
正解:金額の
AVGは
正解:1行も
内側の
正解:外側の
相関サブクエリは、
正解:内側が
EXISTSは、
正解:
FROM句の
選んでから「採点する」を押すと、解説つきで答え合わせができます。
関連記事
おすすめの本 PR
セキュリティとデータベースは、基本情報技術者試験の対策本でまとめて学べます。

