タイトル: SELECT文
SEOタイトル: SQL SELECT 文完全リファレンス (JOIN/GROUP BY/サブクエリ)
| この記事の要点 |
|
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;
処理順序は書く順とは異なります:
- FROM / JOIN — テーブルを揃える
- WHERE — 行をフィルタ
- GROUP BY — グループ化
- HAVING — グループをフィルタ
- SELECT — 列を確定(エイリアスもここで決まる)
- DISTINCT — 重複排除
- ORDER BY — 並び替え
- 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 NULL | NULL 判定 | 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: WHERE と HAVING、どっちに書く?
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... と振ります。