タイトル: SELECT
SEOタイトル: SQL SELECT 文の基本構文と性能 - WHERE / GROUP BY / JOIN / EXPLAIN まで
| この記事の要点 |
|
SELECT 文の基本構文
SELECT は SQL で最も多用される命令で、テーブルから条件にあう行を取り出します。基本形は以下:
SELECT 列1, 列2, ...
FROM テーブル
WHERE 行レベルの条件
GROUP BY グループ化キー
HAVING グループレベルの条件
ORDER BY ソートキー [ASC|DESC]
LIMIT 取得数 OFFSET 開始位置;
これらの句は構文上の順序であり、後述するとおり実際の処理順とは異なります。
論理処理順(とても重要)
| 順序 | 句 | 役割 |
|---|---|---|
| 1 | FROM / JOIN | 元データセットを作る |
| 2 | WHERE | 行レベルで絞る |
| 3 | GROUP BY | グルーピング |
| 4 | HAVING | グループレベルで絞る |
| 5 | SELECT | 列の射影、集約関数評価 |
| 6 | DISTINCT | 重複除外 |
| 7 | ORDER BY | 並べ替え |
| 8 | LIMIT / OFFSET | 件数制限 |
この順序を理解すると「WHERE で COUNT(*) > 5 が使えない」「SELECT のエイリアスを WHERE で使えない(多くの DB で)」といったエラーが腑に落ちます。
WHERE: 行の絞り込み
-- 比較
SELECT * FROM users WHERE age >= 20 AND age < 30;
SELECT * FROM users WHERE age BETWEEN 20 AND 29; -- 同義
-- IN / NOT IN
SELECT * FROM users WHERE country IN ('JP', 'US', 'GB');
-- パターンマッチ
SELECT * FROM users WHERE email LIKE '%@example.com';
SELECT * FROM users WHERE name LIKE 'A%'; -- A で始まる
-- NULL 判定(= NULL は使えない!)
SELECT * FROM users WHERE deleted_at IS NULL;
SELECT * FROM users WHERE deleted_at IS NOT NULL;
-- EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
GROUP BY と HAVING
-- 国別ユーザー数
SELECT country, COUNT(*) AS cnt
FROM users
GROUP BY country
HAVING COUNT(*) >= 100 -- ★ 集約後の絞り込み
ORDER BY cnt DESC;
-- 月別売上集計
SELECT DATE_FORMAT(created_at, '%Y-%m') AS ym,
COUNT(*) AS order_count,
SUM(total) AS total_sum,
AVG(total) AS total_avg
FROM orders
WHERE created_at >= '2026-01-01'
GROUP BY ym
ORDER BY ym;
JOIN
複数テーブルを結合します。代表的な 5 種類:
| JOIN | 意味 | 典型用途 |
|---|---|---|
| INNER JOIN | 両側にマッチする行のみ | 顧客と注文の対応取得 |
| LEFT JOIN | 左側全行 + 右側マッチ行(無ければ NULL) | 注文有無に関わらず全顧客出力 |
| RIGHT JOIN | 右側全行 + 左側マッチ行 | LEFT で書き換え可能(実務では稀) |
| FULL OUTER JOIN | 両側どちらかにある行 | 差分検出 |
| CROSS JOIN | 直積 | 全組合せ生成(カレンダー作成等) |
-- INNER JOIN: 注文を持つ顧客のみ
SELECT u.name, o.total
FROM users u
INNER JOIN orders o ON o.user_id = u.id;
-- LEFT JOIN: 注文有無に関わらず全顧客
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
-- 注文の無い顧客を抽出(典型 LEFT JOIN パターン)
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
-- 自己結合: 上司と部下
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
ORDER BY と LIMIT
-- 単一キー降順
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- 複合キー
SELECT * FROM users ORDER BY country ASC, age DESC;
-- ページング
SELECT * FROM users
ORDER BY id
LIMIT 20 OFFSET 40; -- 3 ページ目(21〜40 件目飛ばして 21 件目から 20 件)
-- NULL の扱い(PostgreSQL)
SELECT * FROM users ORDER BY last_login DESC NULLS LAST;
サブクエリと CTE (WITH)
-- スカラーサブクエリ
SELECT name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;
-- WITH 句(CTE)で可読性アップ
WITH active_users AS (
SELECT id, name FROM users WHERE last_login > NOW() - INTERVAL 30 DAY
), recent_orders AS (
SELECT user_id, SUM(total) AS total
FROM orders
WHERE created_at > NOW() - INTERVAL 30 DAY
GROUP BY user_id
)
SELECT u.name, COALESCE(o.total, 0) AS recent_total
FROM active_users u
LEFT JOIN recent_orders o ON o.user_id = u.id
ORDER BY recent_total DESC
LIMIT 100;
性能改善: EXPLAIN で実行計画を見る
-- MySQL / PostgreSQL 共通
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
-- MySQL: 詳細
EXPLAIN FORMAT=JSON SELECT ...;
EXPLAIN ANALYZE SELECT ...; -- MySQL 8.0+, PostgreSQL
-- 確認ポイント:
-- - type が ALL (full scan) になっていないか
-- - rows がテーブルサイズに対して大きすぎないか
-- - key (使用インデックス) が NULL ではないか
-- - Extra に Using filesort / Using temporary が出ていないか
遅い SELECT の典型パターン
| パターン | 原因 | 対処 |
|---|---|---|
| WHERE の列にインデックス無し | full scan | 該当列に INDEX |
WHERE func(col) = ? | 関数適用でインデックス使えず | 関数インデックス or 列側で計算 |
LIKE '%abc%' | 前方一致でないと INDEX 使えない | 全文検索 (FTS) / 別 DB |
| N+1 クエリ | ループ内で SELECT | JOIN や eager loading |
| 大量 OFFSET ページング | OFFSET 値分スキャン | カーソル方式 (id > ?) |
FAQ
Q: SELECT * は使わない方がいい?
A: アプリケーションコードでは必要な列だけ明示するのが推奨。インデックスのみで完結する Covering Index 化や、不要列の転送削減が効きます。ad-hoc クエリでの利用は OK。
Q: なぜ WHERE で集約関数(COUNT 等)が使えない?
A: WHERE は GROUP BY より論理処理順で先に実行されるため、まだ集約値が存在しないからです。集約後の絞り込みは HAVING を使います。
Q: NULL = NULL は true?
A: false(厳密には UNKNOWN)。NULL 判定は IS NULL / IS NOT NULL を使います。SQL の三値論理の代表的な落とし穴です。