コマンド道場

スカラー副問い合わせ(値を返す SELECT)の使い方

「全体の平均」と 1 行ずつ比べる

shain社員

namesalary
佐藤720000
加藤610000
中村530000
井上505000

salary > (SELECT avg(salary) FROM shain)平均より上の行

namesalary
佐藤720000
加藤610000

WHERE salary > (SELECT avg(salary) FROM shain)

  • 取り消し線= 条件に合わないので結果に出てこない行
  • 社員 18 行 → 平均より上の行 513 行が消えます)

18 行のうち残るのは 5 行です。副問い合わせは先に 1 回だけ計算され、その値と各行を比べています。

1 行 1 列だけを返す SELECT は、そのまま「値」として使えます。 これをスカラー副問い合わせと呼びます。WHERE に集約関数を直接書けない問題は、これで解決します。ただし返る行数が 1 でなくなった瞬間に、エラーになるか、黙って結果が消えます。

主要 7 製品で通用 そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database / IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

011 行 1 列なら「値」として扱える

結論: 括弧で囲んだ SELECT が 1 行 1 列だけを返すなら、それは値と同じように書ける場所すべてに置けます。

置ける場所は広く、WHERE の右辺・SELECT の列・ORDER BY のキーなど、式が書けるところならどこでも構いません。

条件は 2 つだけです。

  • 列が 1 つであること
  • 行が 1 行であること(0 行と複数行の扱いは後述)

avg()max() のような集約関数は、必ず 1 行 1 列を返すので相性が良く、実際いちばん多い使い道です。

1 行ずつ、何をしているか
  1. SELECT name, salaryどの列を出すか
  2. FROM shainどの表を見るか
  3. WHERE salary >各行の給与と…
  4. (SELECT avg(salary) FROM shain)…全体の平均(1 つの値)を比べる
  5. ORDER BY salary DESC並び順

内側の SELECT は外側の行に関係なく 1 回だけ計算されます。外側の行ごとに計算が変わる書き方は「相関副問い合わせ」と呼び、別のページで扱います。

全体の平均より高い人
SELECT name, salary
FROM shain
WHERE salary > (SELECT avg(salary) FROM shain)
ORDER BY salary DESC;
namesalary
鈴木980000
佐藤720000
山田700000
清水650000
加藤610000

5

5 人です。平均は 542002.77… なので、61 万の加藤までが残ります。

最高給与の人を出す
SELECT name, salary
FROM shain
WHERE salary = (SELECT max(salary) FROM shain);
namesalary
鈴木980000

1

「いちばん高い人」は ORDER BY と LIMIT でも出せますが、この書き方なら同額が複数いれば全員返ります。ここでは 1 人です。

02⚠️ よくある間違い:WHERE に集約関数は直接書けない

結論: WHERE salary > avg(salary) はエラーになります。 これがスカラー副問い合わせを使ういちばんの理由です。

理由は評価の順番です。WHERE1 行ずつ絞り込む段階で働きますが、avg()全部の行を見終わってからでないと計算できません。まだ存在しない値とは比べられないので、SQL 側で禁止されています。

副問い合わせにすると、内側が先に計算されて 1 つの値になり、外側の WHERE はその値と比べるだけになります。順番の問題が消えるわけです。

⚠️ 「集約したあとに絞る」なら HAVING を使う、という別の道もあります。HAVING はまとまりごとの絞り込みなので、全体の平均と各行を比べたい今回の用途には使えません。

わざとエラーになる例
SELECT name, salary
FROM shain
WHERE salary > avg(salary);
ERROR: aggregate functions are not allowed in WHERE

「WHERE に集約関数は書けない」と言われます。書き方の問題ではなく、評価の順番の問題です。

03SELECT 句にも書ける

結論: SELECT の列としてスカラー副問い合わせを置くと、全行に同じ値が並びます。

各行の値と全体の値を横に並べて見比べたいときに使います。「自分の給与」と「全体の平均」が同じ行に出るので、差を計算したり、そのまま表として読ませたりできます。

⚠️ 同じ値が 18 行ぶん並ぶので、大きな表では無駄に見えます。実際には多くの製品が「内側は 1 回だけ計算する」と判断してくれますが、行ごとに計算し直す形(相関副問い合わせ)と見た目が似ているため、読む人が誤解しやすい点には注意してください。

各行の横に全体の平均を並べる
SELECT name,
       salary,
       (SELECT round(avg(salary)) FROM shain) AS zentai_heikin
FROM shain
ORDER BY salary DESC;
namesalaryzentai_heikin
鈴木980000542003
佐藤720000542003
山田700000542003
清水650000542003
加藤610000542003
伊藤530000542003

18 … うち先頭 6 行を表示

18 行すべてに同じ 542003 が入ります。1 つの値を全行に配っている、という形がそのまま見えます。

⚠️ この書き方は SQL Server / IBM Db2 では使えません(round(x)(引数 1 つ))。製品ごとの対応表

04⚠️ よくある間違い:2 行以上返るとエラーになる

結論: スカラー副問い合わせが 2 行以上返すと、実行時にエラーで止まります。

「値」として使うつもりの場所に値が 2 つ来てしまうので、比べようがありません。

これは開発時には親切なエラーです。困るのは、テストデータでは 1 行しか返らなかったが、本番のデータでは 2 行返るという場合です。書いた時点では気づけません。

対処は 2 つ。

  • 本当に 1 行に決まる条件か確かめる(一意な列で絞っているか)
  • 複数返りうるなら INEXISTS を使う。これらは集合を相手にする述語なので、何行返っても構いません

「1 つに決まるはず」という思い込みが崩れる場所なので、副問い合わせの条件が一意かどうかは毎回確かめてください。

わざとエラーになる例(4 部署が返る)
SELECT name
FROM shain
WHERE busho_id = (SELECT busho_id
                  FROM busho
                  WHERE parent_busho_id = 1);
ERROR: more than one row returned by a subquery used as an expression

「副問い合わせが 2 行以上返した」と言われます。経営の直下には部署が 4 つあるためです。

1 行に決まる条件なら通る
SELECT name
FROM shain
WHERE busho_id = (SELECT busho_id
                  FROM busho
                  WHERE busho_name = '開発')
ORDER BY shain_id;
name
佐藤
田中
中村
小林

4

busho_name は一意なので必ず 1 行です。4 人が返ります。

05⚠️ よくある間違い:0 行だと黙って結果が消える

結論: 副問い合わせが 1 行も返さないと、その値は NULL になります。 エラーにはなりません。

そして NULL との比較は unknown になるので、外側の WHERE を通る行が 1 つも無くなります。結果は 0 行です。

2 行返るときはエラーで止まるのに、0 行のときは静かに「該当なし」が返る——ここが非対称で危ないところです。「条件に合うデータが無いんだな」と読んでしまい、副問い合わせ側の条件を間違えていることに気づけません

確かめ方は簡単です。 副問い合わせだけを単体で実行してみてください。何行返るかが一目で分かります。

0 行の副問い合わせは NULL になる
SELECT (SELECT busho_id
        FROM busho
        WHERE busho_name = '存在しない部署') AS kekka;
kekka
NULL

1

エラーではなく NULL が返ります。これが外側の比較に渡ると unknown になります。

結果は 0 行(エラーにはならない)
SELECT name
FROM shain
WHERE busho_id = (SELECT busho_id
                  FROM busho
                  WHERE busho_name = '存在しない部署');
name

0

社員は 18 人いますが 1 人も返りません。「該当者なし」に見えますが、実際は部署名の書き間違いです。

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

ここで出てくる関数を引く

関連するトピック

  • IN / EXISTS複数行が返りうるときは、こちらの述語を使います。
  • 相関副問い合わせ外側の行ごとに計算し直す書き方です。
  • HAVING 句まとまりごとに絞るときはこちらです。
  • 集約関数副問い合わせの中でいちばんよく使う関数です。

根拠(一次情報)