SQLのSELECT文は、書く順番ではなく「FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT」の順に処理されます。SELECTは先頭に書きますが、処理されるのは後半です。

この順序を知っていると、「WHEREでSELECTの別名が使えない」「WHEREで SUM() が使えない」「LEFT JOINしたのに行が消えた」といった、初心者がつまずきやすいエラーや挙動を、暗記ではなく理由から説明できるようになります。

この記事では、各句の役割と順序、この順序で説明できる7つの挙動、DBごとの違い(別名が使える場所は、PostgreSQL・MySQL・SQL Serverなどで異なります)、そして論理的な順序と実際の実行計画の関係までを解説します。

書く順番と実行される順番

順番 実行される順 書く順 役割
1 FROM(JOIN・ON) SELECT 対象のテーブルを決め、結合する
2 WHERE FROM 行を絞り込む
3 GROUP BY JOIN グループにまとめる
4 HAVING WHERE グループを絞り込む
5 SELECT(ウィンドウ関数を含む) GROUP BY 取り出す列を決め、計算する
6 DISTINCT HAVING 重複を取り除く
7 ORDER BY ORDER BY 並べ替える
8 LIMIT・OFFSET LIMIT 件数を制限する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQLの実行順序|書く順番と違う理由と、エラーになる書き方」

覚え方のコツは、「どこから(FROM)、どれを(WHERE)、どうまとめて(GROUP BY・HAVING)、何を出し(SELECT)、どう並べて(ORDER BY)、何件(LIMIT)」という流れで読むことです。SQLを読むときも、SELECTからではなくFROMから読むと、処理の流れを追いやすくなります。

各句で何が起きているか

次のテーブルを例に、処理の流れを追ってみます。

SELECT   c.name, SUM(o.amount) AS total
FROM     customers c
JOIN     orders o ON o.customer_id = c.id
WHERE    o.status = 'paid'
GROUP BY c.name
HAVING   SUM(o.amount) >= 1000
ORDER BY total DESC
LIMIT    10;
  1. FROM・JOIN — customersとordersを結合し、「顧客×注文」の行の集まりを作る
  2. WHERE — そのうち、支払い済み(paid)の行だけを残す
  3. GROUP BY — 顧客名ごとにグループにまとめる
  4. HAVING — 合計金額が1000以上のグループだけを残す
  5. SELECT — 顧客名と合計金額を計算し、合計に「total」という別名を付ける
  6. ORDER BY — totalの大きい順に並べる
  7. LIMIT — 上位10件だけを返す

各段階は、前の段階の結果だけを受け取って処理する、と考えるのがポイントです。ここから、次のような挙動がすべて説明できます。

実行順序で説明できる7つの挙動

1. WHEREでSELECTの別名が使えない

SELECT amount * 1.1 AS tax_included
FROM   orders
WHERE  tax_included > 1000;   -- エラー:列 tax_included は存在しません

WHERE(2番目)の時点では、SELECT(5番目)がまだ処理されていないため、tax_included という別名は存在しません。式をWHEREにもう一度書くか、サブクエリやWITH句で先に計算します。

-- 式をもう一度書く
WHERE amount * 1.1 > 1000;

-- WITH句(CTE)で先に計算する
WITH t AS (
  SELECT amount * 1.1 AS tax_included FROM orders
)
SELECT * FROM t WHERE tax_included > 1000;

逆に、ORDER BYはSELECTより後なので、別名が使えます。

2. WHEREで集計関数(SUM・COUNTなど)が使えない

SELECT customer_id FROM orders
WHERE  SUM(amount) > 1000        -- エラー:WHERE では集約関数は使えません
GROUP BY customer_id;

WHEREはGROUP BYより前に処理されるため、まだグループができておらず、合計を計算できません。グループの合計で絞り込むときはHAVINGを使います。

SELECT customer_id FROM orders
GROUP BY customer_id
HAVING SUM(amount) > 1000;

WHEREは「行」の絞り込み、HAVINGは「グループ」の絞り込みです。行の条件で絞れるものは、WHEREに書いた方が、集計する行が減って効率的です。

3. WHEREでウィンドウ関数(ROW_NUMBERなど)が使えない

SELECT id, ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn
FROM   orders
WHERE  ROW_NUMBER() OVER (ORDER BY amount DESC) = 1;   -- エラー

ウィンドウ関数は、SELECTの段階で計算されます。WHEREでもHAVINGでも使えないため、サブクエリかWITH句で計算してから、外側で絞り込みます。

WITH ranked AS (
  SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
  FROM orders
)
SELECT * FROM ranked WHERE rn = 1;   -- 顧客ごとに最も高い注文

BigQuery・Snowflake・DuckDBなどでは、ウィンドウ関数の結果で絞り込む QUALIFY 句が使えます(PostgreSQLやMySQLにはありません)。

4. LEFT JOINの条件をWHEREに書くと、行が消える

実務で最も気づきにくいのがこれです。

-- A:ONに書く → 注文がない顧客も残る
SELECT c.name, o.id
FROM   customers c
LEFT JOIN orders o ON o.customer_id = c.id AND o.status = 'paid';

-- B:WHEREに書く → 注文がない顧客が消える
SELECT c.name, o.id
FROM   customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE  o.status = 'paid';

LEFT JOINは、結合相手がない行もNULLで残します。ONの条件は結合の段階(1番目)で使われるので、条件に合う注文がない顧客もNULLで残ります。一方、WHEREは結合が終わった後(2番目)に適用されるため、o.status がNULLの行は条件を満たさず、除外されます。結果として、LEFT JOINがINNER JOINと同じ結果になってしまいます。

「結合相手を絞りたい」ならON、「結合した結果から行を絞りたい」ならWHERE、と使い分けます。

5. DISTINCTとORDER BYで、SELECTにない列で並べ替えられない

SELECT DISTINCT customer_id FROM orders
ORDER BY amount;   -- PostgreSQLではエラー

DISTINCT(6番目)で重複を取り除いた後は、1つの customer_id に対して複数の amount があり得るため、どの値で並べるかが決まりません。並べ替えに使う列をSELECTに含めるか、GROUP BYと集計関数で並べ替える値を決めます。

6. LIMITを付けても、処理が軽くなるとは限らない

LIMITは最後に適用されるため、ORDER BY と組み合わせると、全件を並べ替えてから先頭を取り出す処理が必要になる場合があります。大きなテーブルでは、WHEREで対象を絞るか、ORDER BYの列にインデックスを用意すると、DBが先頭の数件だけを読んで処理を打ち切れるようになります。

OFFSETも同様で、OFFSET 100000 は10万件を読み飛ばす処理が発生します。深いページ送りが遅いときは、「前のページの最後のIDより大きいもの」を条件にする方法(キーセット方式)が有効です。

7. GROUP BYにない列をSELECTに書けない

SELECT customer_id, status, SUM(amount)
FROM   orders
GROUP BY customer_id;   -- エラー:status が GROUP BY にない

GROUP BYで顧客ごとに1行にまとめた後、SELECTの段階では、1つのグループに複数ある status のどれを出すか決まりません。GROUP BYに加えるか、集計関数(MAXなど)で1つに決めます。MySQLは設定(ONLY_FULL_GROUP_BY)によって、エラーにならず任意の値を返す場合があるので注意が必要です。

DBごとの違い:別名が使える場所

「WHEREで別名が使えない」は多くのDBで共通ですが、GROUP BYやHAVINGで別名が使えるかは、DBによって違います。

DB WHERE GROUP BY HAVING ORDER BY
PostgreSQL × ○ × ○
MySQL × ○ ○ ○
SQL Server × × × ○
SQLite ○ ○ ○ ○

これらは、実行順序どおりなら使えないはずの別名を、DBが独自に解釈して使えるようにしている拡張です。あるDBで動いたSQLが、別のDBではエラーになる原因の1つなので、移植性を重視するなら、WHERE・GROUP BY・HAVINGでは別名に頼らず、式を書くのが確実です。

PostgreSQLでは、ORDER BYで別名を使えますが、ORDER BY 別名 + 1 のように別名を式の中で使うとエラーになります。

UNION・サブクエリ・WITH句はどこで処理されるか

  • サブクエリ・WITH句(CTE) — 外側のクエリから見ると、1つのテーブルとして扱われます。FROMに書いたサブクエリは、外側のFROMの段階で結果が使われます
  • UNION・UNION ALL — それぞれのSELECT文を処理してから、結果を縦に結合します。ORDER BYとLIMITは、UNION全体の結果に対して最後に適用されます
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers
ORDER BY name;   -- 2つを結合した結果全体を並べ替える

論理的な順序と、実際の実行順序

ここまでの順序は、「結果がこの順で処理したものと同じになる」という論理的な順序です。実際には、DBのオプティマイザが、統計情報やインデックスをもとにより速い手順(実行計画)を選びます。

  • WHEREの条件を、JOINの前に適用して結合する行を減らす
  • インデックスを使い、ORDER BYの並べ替えを省く
  • LIMITの件数に達した時点で、読み込みを打ち切る

実行計画は、EXPLAIN で確認できます。

EXPLAIN ANALYZE          -- PostgreSQL(実際に実行して時間も表示)
SELECT * FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 10;

EXPLAIN                  -- MySQL
SELECT * FROM orders WHERE customer_id = 1 ORDER BY created_at DESC LIMIT 10;

論理的な順序は「どんな結果になるか」を考えるため、実行計画は「どれくらい速いか」を考えるためのもの、と使い分けると整理しやすくなります。

よくある質問

Q. SELECTで計算した値を、同じSELECTの別の列で使えますか? A. 多くのDBでは使えません(SnowflakeやAmazon Redshiftなど、使えるDBもあります)。同じSELECT内の列は同時に計算される扱いのため、式をもう一度書くか、サブクエリやWITH句で段階を分けます。

Q. WHEREとHAVINGのどちらでも書ける条件は、どちらに書くべきですか? A. WHEREです。グループにまとめる前に行を減らせるため、集計の処理が軽くなります。HAVINGには、集計結果を使う条件だけを書きます。

Q. JOINの順番を変えると結果は変わりますか? A. INNER JOINだけなら、順番を変えても結果は同じです(実行計画はオプティマイザが決めます)。LEFT JOINやRIGHT JOINを含む場合は、どのテーブルを基準に残すかが変わるため、結果が変わります。

Q. 実行順序はSQLの標準で決まっていますか? A. SQLの標準は、各句がどの結果に対して適用されるかという意味を定めており、この記事の順序はその意味を処理の流れとして表したものです。実際の処理手順は、各DBのオプティマイザが決めます。

まとめ

  • SELECT文は FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT の順に処理される
  • WHEREで別名・集計関数・ウィンドウ関数が使えないのは、まだSELECTやGROUP BYが処理されていないから
  • 集計の条件はHAVING、ウィンドウ関数の条件はサブクエリかWITH句で
  • LEFT JOINの相手を絞る条件はONに書く。WHEREに書くと行が消える
  • 別名が使える場所はDBごとに違う。移植性を考えるなら式を書く
  • 実際の処理はオプティマイザが最適化する。速さはEXPLAINで確認する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQLの実行順序|書く順番と違う理由と、エラーになる書き方」

株式会社RJCでは、SQLのような基本を「動いた」で終わらせず、理由とセットで理解することを大切にしています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?