ITの教科書検索
入門読了 約10分

SQLのサブクエリ(副問い合わせ)とは ― IN・EXISTS

SQLのサブクエリ(副問い合わせ)は、SELECT文の中に書く別のSELECT文で、平均より高い注文を探すときなどに使います。IN・EXISTS・相関サブクエリの違いを図で確かめられます。

目次
  1. サブクエリとは
  2. WHERE句での使い方
  3. 相関サブクエリ
  4. FROM句・SELECT句での使い方
  5. JOINとの使い分け
  6. 書き方を選んで結果を見る
  7. 練習問題

サブクエリとは、SELECT文の中に書く別のSELECT文

SQLのサブクエリ(副問い合わせ)とは、SELECT文の中に丸括弧で囲んで書く、別のSELECT文です。外側のSELECT文はメインクエリと呼びます。

「全注文の平均より高い注文だけ」のように、別の問い合わせで求めた値で絞りたいときに使います。

例には、次の顧客テーブルと注文テーブルを使います。注文5006は会員登録をしていない人の注文で、顧客IDがNULLです。

顧客テーブル
顧客ID氏名
101田中美咲
102佐藤健太
103鈴木大輔
104高橋愛
注文テーブル
注文ID顧客ID商品金額
5001101ノートPC98,000
5002101マウス1,200
5003102キーボード3,500
5004102モニター15,000
5005103マウス1,500
5006NULLデスク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は行があるかどうかだけを見ます。

WHERE句のサブクエリの3つの書き方
書き方内側が返すもの条件が真になるとき
比較演算子(=・>など)1行1列(1つの値)外側の値と、内側の値の比較が成り立つとき
IN1列で複数行外側の値が、内側の値のどれかに等しいとき
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件も無い顧客だけが残ります。

相関サブクエリは、外側の1行ごとに内側を実行する

相関サブクエリとは、外側のSELECT文の列を使うサブクエリです。外側の1行を取り出すたびに、その行の値を使って内側が実行されます。

外側の列を使わない場合内側1回だけ実行外側渡された値で絞る結果を渡す外側の列を使う場合(相関)外側の1行1行ずつ取り出す内側その行の値で実行外側その行を残すか決める列の値を渡す結果を返す次の行へ繰り返す
外側の列を使わないサブクエリは、内側が1回だけ実行されます。相関サブクエリは、外側の1行ごとに、その行の値を使って内側が実行されます。

外側の列は、「表名.列名」や「別名.列名」の形で指します。先ほどの例のcとoが、表に付けた別名です。内側と外側に同じ列名があっても、c.顧客ID、o.顧客IDと書き分けられます。

先ほどのEXISTSの例で、顧客101の行を取り出したとします。内側は「o.顧客ID = 101」の注文を探して2行を返すので、EXISTSは真になります。顧客104では内側が0行になるため、EXISTSは偽です。

この1行ごとの手順は、考え方のうえでの流れです。MySQLのように、相関サブクエリを結合を使う形に書き換えて実行するDBMSもあります。

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が必要です。

やりたいことごとの、向く書き方
やりたいこと向く書き方理由
両方の表の列を結果に並べるJOINWHERE句のサブクエリの列は、外側の結果に出せない
相手の行が「ある」ことだけで絞るIN・EXISTS外側の1行が、そのまま1行で結果に残る
集計した値(平均など)と比べるスカラーサブクエリWHEREに集約関数を直接書けないため、内側で集計して渡す
集計した結果を、さらに絞るFROM句のサブクエリ集計結果を1つの表として扱える
相手の行が「ない」ものを探すNOT EXISTSNOT INは内側にNULLがあると結果が空になる

注文のない顧客は、LEFT JOINで注文側がNULLの行を探す書き方でも求められます。どの書き方が速いかは、DBMSとデータの量によって変わります。上の表で比べているのは、結果の違いだけです。

書き方を選んで、SQL文と結果を見る

次の図でサブクエリの書き方を選ぶと、SQL文、内側の結果、全体の結果が変わります。顧客テーブルと注文テーブルは、最初に示したものと同じです。図は実際のデータベースには接続しておらず、結果はあらかじめ確かめた固定の値です。

サブクエリの書き方
SELECT 注文ID, 金額
FROM 注文
WHERE 金額 > (
  SELECT AVG(金額) FROM 注文
);
1. 内側が先に実行され、1つの値を返す
AVG(金額)
25,200
2. 外側は「金額 > 25200」として、行を絞ります。
実行結果(2行)
注文ID金額
500198,000
500632,000

参考にした資料

練習問題(全6問)

Q1サブクエリ(副問い合わせ)の説明として最も適切なものはどれ?
Q2WHERE 金額 > (SELECT AVG(金額) FROM 注文) の内側のSELECT文は、何を返しているか。
Q3顧客ID NOT IN (SELECT 顧客ID FROM 注文) で、注文の顧客IDにNULLが1件含まれているとき、結果はどうなるか。
Q4相関サブクエリの説明として正しいものはどれ?
Q5EXISTSが真になるのは、どのときか。
Q6FROM句に書いたサブクエリの結果は、どう扱われるか。

選んでから「採点する」を押すと、解説つきで答え合わせができます。

関連記事

おすすめの本 PR

セキュリティとデータベースは、基本情報技術者試験の対策本でまとめて学べます。

目的別の本の選び方を見る

データベースの記事を続けて読む

データベース 全36本中23本目