1.

SQL SELECT 文完全リファレンス (JOIN/GROUP BY/サブクエリ)

編集
この記事の要点
  • SELECT 文は SQL の中心。基本形は SELECT 列 FROM テーブル WHERE 条件 ORDER BY 列 LIMIT 件数
  • * で全カラム、AS でエイリアス、DISTINCT で重複排除
  • WHERE 条件: = / <> / LIKE / IN / BETWEEN / IS NULL
  • JOIN は INNER / LEFT / RIGHT / FULL OUTER の 4 種類が基本
  • 集約は GROUP BY + HAVING、応用は サブクエリ / CTE (WITH) / ウィンドウ関数

SELECT 文の基本形

SQL の SELECT 文はテーブルから行を取り出すための文です。すべての RDBMS で共通の基本形は次の通りです:

SELECT  col1, col2, ...
FROM    table_name
WHERE   condition
GROUP BY col1
HAVING  group_condition
ORDER BY col1 ASC, col2 DESC
LIMIT   N OFFSET M;

処理順序は書く順とは異なります:

  1. FROM / JOIN — テーブルを揃える
  2. WHERE — 行をフィルタ
  3. GROUP BY — グループ化
  4. HAVING — グループをフィルタ
  5. SELECT — 列を確定(エイリアスもここで決まる)
  6. DISTINCT — 重複排除
  7. ORDER BY — 並び替え
  8. LIMIT / OFFSET — 件数制限

この順番を意識すると、「WHERE で別名が使えないが ORDER BY では使える」といった挙動が腑に落ちます。

列の指定とエイリアス

-- 全カラム取得(本番では非推奨。明示すること)
SELECT * FROM users;

-- 必要な列だけ
SELECT id, name, email FROM users;

-- AS でエイリアス(AS は省略可)
SELECT id AS user_id, name AS user_name FROM users;
SELECT id user_id, name user_name FROM users;  -- AS 省略

-- 計算列
SELECT id, price, quantity, price * quantity AS subtotal FROM orders;

-- 文字列連結(標準 SQL: ||、MySQL: CONCAT)
SELECT first_name || ' ' || last_name AS full_name FROM users;  -- PostgreSQL / Oracle
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;  -- MySQL

-- DISTINCT で重複排除
SELECT DISTINCT department FROM employees;
SELECT DISTINCT department, position FROM employees;  -- 組み合わせで重複排除

WHERE 条件の書き方

演算子意味
= / <>等しい / 等しくないWHERE status = 'active'
< > <= >=大小比較WHERE age >= 18
LIKEパターンマッチ (% / _)WHERE name LIKE '田%'
INリストに含まれるWHERE id IN (1, 2, 3)
NOT INリストに含まれないWHERE id NOT IN (1, 2, 3)
BETWEEN範囲(両端含む)WHERE age BETWEEN 18 AND 65
IS NULLNULL 判定WHERE deleted_at IS NULL
EXISTSサブクエリに行があれば真WHERE EXISTS (SELECT 1 FROM ...)
AND / OR / NOT論理結合WHERE a = 1 AND (b = 2 OR c = 3)
-- LIKE: % は 0 文字以上、_ は 1 文字
SELECT * FROM users WHERE email LIKE '%@example.com';
SELECT * FROM products WHERE code LIKE 'A__';   -- A + 任意 2 文字

-- 大文字小文字を無視
SELECT * FROM users WHERE LOWER(name) LIKE '%taro%';  -- 標準
SELECT * FROM users WHERE name ILIKE '%taro%';        -- PostgreSQL 拡張

-- NULL に注意。= NULL は NEVER 真
SELECT * FROM users WHERE deleted_at = NULL;  -- 何も返らない(バグ)
SELECT * FROM users WHERE deleted_at IS NULL; -- 正しい

-- IN とサブクエリ
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'active');

JOIN: テーブルを結合する

JOIN 種別意味左に無い / 右に無い行
INNER JOIN両方に存在する行どちらも除外
LEFT [OUTER] JOIN左の全行 + 右で一致する行右が無ければ NULL で補完
RIGHT [OUTER] JOIN右の全行 + 左で一致する行左が無ければ NULL で補完
FULL OUTER JOIN両方の全行無い側は NULL(MySQL は非対応、UNION で代用)
CROSS JOIN直積(全組み合わせ)
-- INNER JOIN: 注文を持つユーザーのみ
SELECT u.id, u.name, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN: 全ユーザーと、あれば注文
SELECT u.id, u.name, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

-- LEFT JOIN + IS NULL で「片方にしか無い行」
-- 注文をしていないユーザー一覧
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

-- 3 テーブル JOIN
SELECT u.name, o.id AS order_id, p.name AS product
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id;

-- 自己結合(上司を取得)
SELECT e.name AS emp, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

GROUP BY と集約関数

-- 部署ごとの人数と平均給与
SELECT
    department,
    COUNT(*)        AS members,
    AVG(salary)     AS avg_salary,
    SUM(salary)     AS total_salary,
    MIN(salary)     AS min_salary,
    MAX(salary)     AS max_salary
FROM employees
GROUP BY department;

-- HAVING: グループに対するフィルタ
-- 平均給与が 50 万以上の部署のみ
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING AVG(salary) >= 500000;

-- WHERE と HAVING の使い分け:
--   WHERE: GROUP BY する前の行に対する条件
--   HAVING: GROUP BY した後のグループに対する条件
SELECT department, COUNT(*) AS cnt
FROM employees
WHERE status = 'active'    -- 行フィルタ(GROUP BY 前)
GROUP BY department
HAVING COUNT(*) >= 5;       -- グループフィルタ(GROUP BY 後)

ORDER BY と LIMIT

-- 昇順 / 降順
SELECT * FROM users ORDER BY created_at DESC;
SELECT * FROM users ORDER BY age ASC, id DESC;  -- 複数キー

-- 列番号で指定(非推奨だが動く)
SELECT id, name, age FROM users ORDER BY 3 DESC;  -- 3 列目 = age

-- NULL の扱い(標準: NULLS FIRST / NULLS LAST)
SELECT * FROM users ORDER BY last_login DESC NULLS LAST;  -- PostgreSQL/Oracle

-- LIMIT / OFFSET(ページング)
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 40;  -- 3 ページ目
-- SQL Server / Oracle 12c+:
SELECT * FROM users ORDER BY id OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY;

サブクエリと CTE (WITH)

-- スカラーサブクエリ(1 行 1 列を返す)
SELECT name, salary,
       (SELECT AVG(salary) FROM employees) AS avg_salary
FROM employees;

-- 相関サブクエリ(外側の行ごとに評価)
SELECT name, salary
FROM employees e
WHERE salary > (
    SELECT AVG(salary) FROM employees
    WHERE department = e.department
);

-- インラインビュー(FROM 句のサブクエリ)
SELECT dept, total
FROM (
    SELECT department AS dept, SUM(salary) AS total
    FROM employees
    GROUP BY department
) t
WHERE total > 5000000;

-- CTE: 読みやすい書き方
WITH dept_totals AS (
    SELECT department, SUM(salary) AS total
    FROM employees
    GROUP BY department
)
SELECT * FROM dept_totals WHERE total > 5000000;

-- 再帰 CTE(階層データ)
WITH RECURSIVE org AS (
    SELECT id, name, manager_id, 1 AS level
    FROM employees WHERE manager_id IS NULL  -- 始点
    UNION ALL
    SELECT e.id, e.name, e.manager_id, o.level + 1
    FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY level, id;

ウィンドウ関数

集計と詳細行を同時に返したいときに便利な構文です。GROUP BY と違い行をまとめずに集計値を付加できます。

-- 部署内の給与順位
SELECT
    name, department, salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
    RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS rk,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS drk
FROM employees;

-- 累計と移動平均
SELECT
    date, amount,
    SUM(amount) OVER (ORDER BY date) AS running_total,
    AVG(amount) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM daily_sales;

-- LAG / LEAD(前後行参照)
SELECT
    date, amount,
    LAG(amount, 1)  OVER (ORDER BY date) AS prev_amount,
    LEAD(amount, 1) OVER (ORDER BY date) AS next_amount,
    amount - LAG(amount, 1) OVER (ORDER BY date) AS diff
FROM daily_sales;

集合演算: UNION / INTERSECT / EXCEPT

-- UNION: 和集合(重複排除)
SELECT id, name FROM customers
UNION
SELECT id, name FROM suppliers;

-- UNION ALL: 和集合(重複そのまま、高速)
SELECT id, name FROM customers
UNION ALL
SELECT id, name FROM suppliers;

-- INTERSECT: 積集合(両方に存在)
SELECT email FROM customers
INTERSECT
SELECT email FROM newsletter_subscribers;

-- EXCEPT (MySQL は MINUS で代用不可、NOT EXISTS で代替)
SELECT email FROM customers
EXCEPT
SELECT email FROM newsletter_subscribers;

パフォーマンスの基本

  • SELECT * はやめて必要な列だけ。インデックスオンリースキャンが効くようになる
  • WHERE 句の左辺に関数を使うとインデックスが効かない(WHERE DATE(created_at) = ...WHERE created_at >= '...' AND created_at < '...'
  • LIKE は前方一致のみインデックスが効く('abc%' はOK、'%abc' はNG)
  • 大きな OFFSET は遅い → Seek 方式(前ページ最終 id を WHERE で渡す)
  • EXPLAIN / EXPLAIN ANALYZE で実行計画を確認する習慣を

FAQ

Q: SELECT * は本当にダメ?
A: 一時的な確認なら OK、本番コードでは列を明示してください。テーブル定義変更時のトラブル予防と、インデックスオンリーアクセスの最適化のためです。

Q: WHEREHAVING、どっちに書く?
A: 集約関数を使う条件は HAVING、それ以外は WHERE。WHERE の方が早く絞り込めるので、可能な限り WHERE 側に書きます。

Q: 並び順を保証したい
A: ORDER BY を必ず書きます。LIMIT だけだと内部実装に依存して順序が不定になります。

Q: ROW_NUMBER と RANK の違いは?
A: ROW_NUMBER は同値でも 1,2,3,4...、RANK は 1,1,3,4...、DENSE_RANK は 1,1,2,3... と振ります。

編集
Post Share
子ページ
  1. GROUP BY 句
  2. HAVING句
  3. ORDER BY 句
同階層のページ
  1. SELECT文
  2. INSERT
  3. UPDATE文
  4. DELETE文
  5. WHERE句
  6. JOIN句
  7. 集合演算子
  8. インラインビュー
  9. 副問い合わせ (サブクエリ)

最近更新/作成されたページ