スカラーサブクエリ — SELECT句に全体平均を付与して各行と比較する
サブクエリとは、SQL文の中に書かれた別のSELECT文のことです。スカラーサブクエリは「必ず1行1列(単一値)を返す」サブクエリで、SELECT句・WHERE句など値が使える場所に自由に埋め込めます。
SELECT col, (SELECT AVG(col) FROM tbl) AS avg_val -- 全行に同じ集計値を付与 FROM tbl;
ordersテーブルから、status が 'completed' の注文について order_id, user_id, amount を取得し、さらに全completed注文の平均金額と平均との差分を列として付与してください。amount の高い順に並べてください。
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 6000 | completed |
| 106 | 4 | 2000 | cancelled |
| order_id | user_id | amount | avg_amount | diff_from_avg |
|---|---|---|---|---|
| 102 | 1 | 12000 | 8875 | 3125 |
| 104 | 3 | 9500 | 8875 | 625 |
| 101 | 1 | 8000 | 8875 | -875 |
| 105 | 3 | 6000 | 8875 | -2875 |
SELECT order_id, user_id, amount, (SELECT ROUND(AVG(amount)) -- スカラーSQ: 必ず単一値を返す FROM orders WHERE status = 'completed' ) AS avg_amount, amount - (SELECT ROUND(AVG(amount)) -- 算術式の中でも使用可能 FROM orders WHERE status = 'completed' ) AS diff_from_avg FROM orders WHERE status = 'completed' -- 外側クエリで絞り込む ORDER BY amount DESC; /* 実行順序(SQLの論理的な評価順): 1. スカラーSQ先行評価 → SELECT ROUND(AVG(amount)) FROM orders WHERE status='completed' → 結果: 8875 2. FROM orders → 全行読み込み 3. WHERE status='completed' → 4行に絞り込む 4. SELECT → 各行に avg_amount=8875, diff_from_avg を付与 5. ORDER BY amount DESC → 金額降順に並び替え */
LEGEND
① スカラーSQ実行
SELECT ROUND(AVG(amount)) FROM orders WHERE status='completed'サブクエリが最初に評価され、status='completed' の4件から平均 (8000+12000+9500+6000)÷4 = 8875 を計算し、単一値として返します。この値が外側クエリの全行に付与されます。| order_id | amount | status | SQ対象? |
|---|---|---|---|
| 101 | 8000 | completed | ✓ AVG対象 |
| 102 | 12000 | completed | ✓ AVG対象 |
| 103 | 3500 | pending | ✗ 除外 |
| 104 | 9500 | completed | ✓ AVG対象 |
| 105 | 6000 | completed | ✓ AVG対象 |
| 106 | 2000 | cancelled | ✗ 除外 |
(SELECT AVG(amount)...) はテーブルスキャンを1回だけ行い、全行に同じ値 8875 を渡します。WITH avg_val AS (...) SELECT ..., avg_val.v FROM orders, avg_val とすれば評価1回で済みます(Q9参照)。(SELECT user_id FROM users WHERE plan='premium') は複数行を返すためスカラーSQ には使えず実行時エラーになります。スカラーSQ は必ず集計関数や LIMIT 1 で単一値に絞ること。AVG(amount) OVER () というウィンドウ関数は実質等価です。ただしウィンドウ関数が使えない古い DB 環境(MySQL 5.x 等)ではスカラーSQ が唯一の手段です。PostgreSQL・MySQL 8.0+ などではウィンドウ関数が推奨されますが、スカラーSQ はどの RDBMS でも動くポータブルな書き方です。WHERE IN サブクエリ — サブクエリで動的リストを作り注文を絞り込む
WHERE col IN (サブクエリ) は、サブクエリが返す値のリストを使って行を絞り込みます。固定値 IN (1, 3) の代わりに、テーブルの状態に応じて動的に変わるリストを生成できるのがポイントです。
SELECT * FROM orders WHERE user_id IN ( SELECT user_id FROM users WHERE plan = 'premium' -- 内側が動的リストを生成 );
user_id と照合するため、内側は必ず1列だけ SELECT します。orders テーブルから、plan が 'premium' のユーザーの注文を order_id, user_id, amount, status, ordered_at で取得してください。ordered_at 降順で並べてください。
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | 2024-05-01 |
| 102 | 1 | 12000 | completed | 2024-05-20 |
| 103 | 2 | 3500 | pending | 2024-05-10 |
| 104 | 3 | 9500 | completed | 2024-05-15 |
| 105 | 3 | 6000 | completed | 2024-05-25 |
| 106 | 4 | 2000 | cancelled | 2024-05-08 |
| order_id | user_id | amount | status | ordered_at |
|---|---|---|---|---|
| 105 | 3 | 6000 | completed | 2024-05-25 |
| 102 | 1 | 12000 | completed | 2024-05-20 |
| 104 | 3 | 9500 | completed | 2024-05-15 |
| 101 | 1 | 8000 | completed | 2024-05-01 |
SELECT order_id, user_id, amount, status, ordered_at FROM orders WHERE user_id IN ( -- IN: サブクエリ結果リストに一致する行を残す SELECT user_id -- 必ず単一列だけを返す FROM users WHERE plan = 'premium' -- premium のuser_idリストを動的生成 ) ORDER BY ordered_at DESC; /* 実行順序(SQLの論理的な評価順): 1. サブクエリ評価 → SELECT user_id FROM users WHERE plan='premium' → 結果リスト: [1, 3] 2. FROM orders → 全行読み込み 3. WHERE user_id IN (1, 3) → 4行に絞り込む 4. SELECT ... → 必要列を選択 5. ORDER BY ordered_at DESC → 日付降順に並び替え */
LEGEND
① サブクエリ実行
SELECT user_id FROM users WHERE plan = 'premium'サブクエリが先に実行されます。usersからplan='premium'のuser_idを取り出し、値のリスト [1, 3] を生成します。| user_id | name | plan | リストに含む? |
|---|---|---|---|
| 1 | 田中 太郎 | premium | ✓ [1,3]に追加 |
| 2 | 佐藤 花子 | free | ✗ 除外 |
| 3 | 鈴木 一郎 | premium | ✓ [1,3]に追加 |
| 4 | 山田 次郎 | standard | ✗ 除外 |
| 5 | 伊藤 三郎 | free | ✗ 除外 |
IN (1, 3) と論理的に等価ですが、テーブルの状態に応じて動的に変わる点が重要です。WHERE user_id IN (SELECT user_id FROM users WHERE plan='premium') は INNER JOIN users ON orders.user_id = users.user_id WHERE users.plan='premium' と同じ結果を返します。結合先の列を SELECT に出す必要がなければ IN サブクエリの方が意図が明確です。WHERE col IS NOT NULL を追加しましょう。SELECT user_id, name FROM users のように複数列を返すと構文エラーになります。IN の比較対象と一致する単一列だけを SELECT しましょう。WHERE NOT IN サブクエリ — 一度も注文していないユーザーを検出する
WHERE col NOT IN (サブクエリ) は、サブクエリの結果に含まれない行を返します。「注文したことがないユーザー」「未配信のキャンペーン対象者」など、別テーブルに存在しない行を見つけるパターンです。
SELECT * FROM users WHERE user_id NOT IN ( SELECT DISTINCT user_id FROM orders WHERE user_id IS NOT NULL -- NULL混入防止(重要) );
WHERE col IS NOT NULL を付けるか、NOT EXISTS を使うことを推奨します。users テーブルから、一度も注文がないユーザーを取得してください。user_id, name, plan を user_id 昇順で返してください。
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id |
|---|---|
| 101 | 1 |
| 102 | 1 |
| 103 | 2 |
| 104 | 3 |
| 105 | 3 |
| 106 | 4 |
| user_id | name | plan |
|---|---|---|
| 5 | 伊藤 三郎 | free |
SELECT user_id, name, plan FROM users WHERE user_id NOT IN ( -- リストに含まれない行だけ残す SELECT DISTINCT user_id -- DISTINCT で重複除去(効率化) FROM orders WHERE user_id IS NOT NULL -- NULL混入防止(NOT INの罠を回避) ) ORDER BY user_id; /* 実行順序(SQLの論理的な評価順): 1. サブクエリ評価 → SELECT DISTINCT user_id FROM orders WHERE user_id IS NOT NULL → 結果リスト: [1, 2, 3, 4] 2. FROM users → 全行読み込み 3. WHERE user_id NOT IN (1,2,3,4) → user_id=5 の行だけ残る 4. SELECT user_id, name, plan → 必要列を選択 5. ORDER BY user_id → user_id 昇順に並び替え */
LEGEND
① サブクエリ実行
SELECT DISTINCT user_id FROM orders WHERE user_id IS NOT NULLordersから注文が存在するuser_idを重複なく取得。結果リスト [1,2,3,4] が生成されます。user_id=5 は orders に行が存在しません。| order_id | user_id(DISTINCT後) | リスト登録 |
|---|---|---|
| 101 | 1 | ✓ リストに追加 |
| 102 | 1 | (重複 → 無視) |
| 103 | 2 | ✓ リストに追加 |
| 104 | 3 | ✓ リストに追加 |
| 105 | 3 | (重複 → 無視) |
| 106 | 4 | ✓ リストに追加 |
DISTINCT を付けることでリストを小さく保ち、評価コストを下げられます。WHERE NOT EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.user_id) を使います。NOT EXISTS は NULL があっても期待通り動作します(Q6で詳解)。orders.user_id に NULL 行が1件でもあった場合、NOT IN リストは [1, NULL, 2, 3, 4] になります。SQL の三値論理では 5 NOT IN (1, NULL, 2, 3, 4) が UNKNOWN と評価されるため、user_id=5 も除外されて結果が0件になります。FROM句サブクエリ(派生テーブル)— 集計結果をさらにWHEREで絞り込む
GROUP BY で集計した結果に対して WHERE で条件を付けたい場合、FROM句にサブクエリを書く「派生テーブル(インラインビュー)」を使います。通常の WHERE 句は GROUP BY より先に評価されるため、集計後の列を直接 WHERE で使えません。派生テーブルがこの制約を解消します。
SELECT * FROM ( SELECT col, SUM(amount) AS total -- 内側で集計 FROM table GROUP BY col ) AS sub -- 必ず AS でエイリアスを付ける WHERE total >= 10000; -- 集計後の値で絞り込める
orders テーブルから、status が 'completed' の注文のみを対象にユーザーごとの合計金額を集計し、合計金額が 15,000 以上のユーザーのみを合計金額降順で返してください。取得列は user_id, total_amount とします。
| order_id | user_id | amount | status |
|---|---|---|---|
| 101 | 1 | 8000 | completed |
| 102 | 1 | 12000 | completed |
| 103 | 2 | 3500 | pending |
| 104 | 3 | 9500 | completed |
| 105 | 3 | 6000 | completed |
| 106 | 4 | 2000 | cancelled |
| user_id | total_amount |
|---|---|
| 1 | 20000 |
| 3 | 15500 |
SELECT user_id, total_amount FROM ( -- FROM句にサブクエリ(派生テーブル) SELECT user_id, SUM(amount) AS total_amount -- 集計列に AS で名前を付ける(外側から参照) FROM orders WHERE status = 'completed' -- 内側クエリで絞ってから集計 GROUP BY user_id ) AS user_totals -- 派生テーブルには必ず AS エイリアスを付ける WHERE total_amount >= 15000 -- 集計結果の列名で絞り込める(通常WHEREでは不可) ORDER BY total_amount DESC; /* 実行順序(SQLの論理的な評価順): 1. 内側クエリ(派生テーブル)を評価 → completed を user_id で集計 2. FROM ... AS user_totals → 集計結果を参照 3. WHERE total_amount のしきい値 → 条件で絞る 4. SELECT user_id, total_amount → 列を選択 5. ORDER BY total_amount DESC → 合計額降順 */
LEGEND
① 内側クエリ(GROUP BY)
SELECT user_id, SUM(amount) FROM orders WHERE status='completed' GROUP BY user_id内側のサブクエリが先に実行。completedの4件をuser_idでグループ化し合計を計算。user_id=2(pending)・4(cancelled)は内側WHEREで除外されます。| order_id | user_id | amount | status | グループ |
|---|---|---|---|---|
| 101 | 1 | 8000 | completed | user1グループ |
| 102 | 1 | 12000 | completed | user1グループ |
| 103 | 2 | 3500 | pending | 除外 |
| 104 | 3 | 9500 | completed | user3グループ |
| 105 | 3 | 6000 | completed | user3グループ |
| 106 | 4 | 2000 | cancelled | 除外 |
AS エイリアス名 を付けることが必須です(省略するとエラー)。WHERE SUM(amount) >= 15000 は書けません(エラー)。派生テーブルにして外側 WHERE で絞ることで同じことができ、さらに別テーブルとのJOINなど柔軟な処理に対応できます。WITH user_totals AS (SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE status='completed' GROUP BY user_id) SELECT * FROM user_totals WHERE total_amount >= 15000 とするとネストが浅くなります(Q9参照)。AS user_totals を付けないと多くのDBでエラーになります(MySQL・PostgreSQL とも必須)。派生テーブルには必ずエイリアスを付けましょう。WHERE orders.status = ... のように派生テーブルの内側テーブル名を使おうとするとエラーになります。外側からアクセスできるのは派生テーブルが返した列名(user_totals.total_amount など)のみです。HAVING SUM(amount) >= 15000 の方が短く書けます。一方、集計結果を別テーブルと JOIN する、複数の集計列を組み合わせてフィルタする、集計結果にさらに集計(二重集計)するような複雑なケースでは派生テーブル(またはCTE)が必要になります。実務では「まず HAVING を試し、複雑化したら派生テーブル/CTEに移行する」という判断が自然です。EXISTS サブクエリ — 「存在するか」で絞り込む最も効率的なパターン
WHERE EXISTS (サブクエリ) は、サブクエリが「1行でも結果を返す場合」にその行を残します。何を返すかは関係なく「行が存在するか」だけを確認するため、内側は SELECT 1 で十分です。
SELECT * FROM users u WHERE EXISTS ( SELECT 1 -- 値は何でもよい。行の存在だけを確認 FROM orders o WHERE o.user_id = u.user_id -- 外側列を参照する「相関サブクエリ」 );
users テーブルから、status が 'completed' の注文が1件以上あるユーザーを取得してください。user_id, name, plan を user_id 昇順で返してください。
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
| order_id | user_id | status |
|---|---|---|
| 101 | 1 | completed |
| 102 | 1 | completed |
| 103 | 2 | pending |
| 104 | 3 | completed |
| 105 | 3 | completed |
| 106 | 4 | cancelled |
| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 3 | 鈴木 一郎 | premium |
SELECT user_id, name, plan FROM users u -- 外側クエリに u というエイリアス WHERE EXISTS ( -- SQが1行以上返せば TRUE SELECT 1 -- 値は何でもよい(1, *, 'x' どれも等価) FROM orders o WHERE o.user_id = u.user_id -- 外側 u.user_id を参照する相関SQ AND o.status = 'completed' -- completedが1件でもあれば通過 ) ORDER BY user_id; /* 実行順序(SQLの論理的な評価順): 1. FROM users u → 1行ずつ処理 2. EXISTS(...) → ユーザーごとにサブクエリを実行 3. SELECT user_id, name, plan → 通過行を選択 4. ORDER BY user_id → user_id 昇順 */
LEGEND
① FROM users
FROM users uusersテーブル全5行を1行ずつ処理します。各行に対して EXISTS サブクエリを評価します。| user_id | name | plan |
|---|---|---|
| 1 | 田中 太郎 | premium |
| 2 | 佐藤 花子 | free |
| 3 | 鈴木 一郎 | premium |
| 4 | 山田 次郎 | standard |
| 5 | 伊藤 三郎 | free |
SELECT 1 で書くのが慣例で「値を返すことに意味はない」ことを明示します。o.user_id = u.user_id のように、外側クエリの列(u.user_id)を内側で参照する形式を相関サブクエリといいます。外側クエリが1行処理されるたびに内側が実行されます。SELECT * と書いても動作しますが、EXISTS は値を一切使わないため無駄です。SELECT 1 または SELECT NULL が慣例であり、意図を明確にします。