この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:4
ページ更新者:T
更新日時:2026-06-11 07:07:02

タイトル: SELECT
SEOタイトル: SQL SELECT 文の基本構文と性能 - WHERE / GROUP BY / JOIN / EXPLAIN まで

この記事の要点
  • SELECT はテーブルから行を取得する SQL の中核。基本順序は SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT …
  • 論理処理順は構文と異なり FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
  • WHERE はグループ化前の絞り込み、HAVING は集約後の絞り込み
  • JOININNER / LEFT / RIGHT / FULL / CROSS。多くの実務は INNER と LEFT で足りる
  • 遅い SELECT は EXPLAIN で実行計画を見て、適切なインデックス・JOIN 順を確認

SELECT 文の基本構文

SELECT は SQL で最も多用される命令で、テーブルから条件にあう行を取り出します。基本形は以下:

SELECT 列1, 列2, ...
  FROM テーブル
 WHERE 行レベルの条件
 GROUP BY グループ化キー
HAVING グループレベルの条件
 ORDER BY ソートキー [ASC|DESC]
 LIMIT 取得数 OFFSET 開始位置;

これらの句は構文上の順序であり、後述するとおり実際の処理順とは異なります

論理処理順(とても重要)

順序役割
1FROM / JOIN元データセットを作る
2WHERE行レベルで絞る
3GROUP BYグルーピング
4HAVINGグループレベルで絞る
5SELECT列の射影、集約関数評価
6DISTINCT重複除外
7ORDER BY並べ替え
8LIMIT / 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 クエリループ内で SELECTJOIN や 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 の三値論理の代表的な落とし穴です。