はい、承知いたしました。「SQL実行順序とは?基本を分かりやすく解説」に関する詳細な説明を含む記事を作成します。約5000語を目指し、内容を直接ここに記述します。
SQL実行順序とは?データベースがクエリを処理する仕組みを徹底解説
はじめに:見かけの順序と実際の順序の違い
SQL(Structured Query Language)は、データベースを操作するための強力な言語です。データの検索、挿入、更新、削除など、さまざまな操作をSQL文一つで記述できます。SQL文を書くとき、私たちは特定のキーワード(SELECT
, FROM
, WHERE
, GROUP BY
, ORDER BY
など)を特定の順番で並べます。例えば、データを取得する最も基本的なクエリは次のようになります。
sql
SELECT カラム名
FROM テーブル名
WHERE 条件式;
この見た目の順番(SELECT
-> FROM
-> WHERE
)で、データベースもクエリを処理していると思われがちです。しかし、実際にはSQLの処理順序は、私たちがコードを書く順番とは大きく異なります。データベースエンジンは、効率的かつ正確に要求に応えるために、SQL文の各句を特定の論理的な順序で評価していきます。この実際の処理順序のことをSQL実行順序(あるいは論理的処理順序)と呼びます。
この記事では、このSQL実行順序が具体的にどのようなものなのか、なぜそれが重要なのか、そして各ステップで何が行われるのかを、詳細な例を交えながら分かりやすく解説します。SQLの初心者から、さらに理解を深めたいと考えている方まで、この実行順序を理解することは、より効率的で正確なクエリを書く上で不可欠です。
なぜSQL実行順序の理解が重要なのか?
SQL実行順序を理解することは、単なる知識以上の実用的なメリットをもたらします。主な理由は以下の通りです。
- クエリの動作を正確に予測できる: 各句がどのタイミングで評価されるかを知っていれば、「なぜこの条件が効かないのか?」「なぜこのエイリアスはここでは使えないのか?」といった疑問に対する答えが見つかります。特に複雑なクエリを書く際には、ステップごとのデータの変化を追うことで、意図した通りの結果が得られるかを確認できます。
- パフォーマンスの最適化に役立つ: データベースは大量のデータを扱います。不要なデータをできるだけ早い段階で絞り込むことが、クエリのパフォーマンスを大きく左右します。実行順序を知っていると、どの句(特に
WHERE
句など)が早期に評価されるかを理解し、効率的なフィルタリング戦略を立てられます。 - エラーの原因特定とデバッグが容易になる: クエリが期待通りの結果を返さない場合や、エラーが発生した場合、どのステップで問題が起きているのかを特定しやすくなります。「このエイリアスはまだ定義されていないからエラーになるんだな」「この集計はまだ行われていないから
WHERE
句では使えないんだな」といった判断ができるようになります。 - より高度なクエリを理解・記述できる:
HAVING
句とWHERE
句の違い、ウィンドウ関数の評価タイミング、CTEs(共通テーブル式)の動作など、SQLのより進んだ概念を理解するには、実行順序の知識が不可欠です。 - データベースオプティマイザの挙動を理解する一助となる: データベースシステムには、クエリの実行計画を最適化するオプティマイザが搭載されています。オプティマイザは、論理的な実行順序で定められた最終結果を変えずに、物理的な実行方法を最も効率的なものに調整します。論理的な実行順序を理解していれば、なぜオプティマイザがそのような物理的な計画を選択したのかをある程度推測できるようになります。
このように、SQL実行順序の理解は、SQLスキルを次のレベルへ引き上げるための基盤となります。それでは、具体的な実行順序を見ていきましょう。
SQLの論理的実行順序の概観
一般的なSQLクエリの論理的な実行順序は以下のようになります。各ステップは、前のステップで生成された中間結果セットを受け取り、それを加工して次のステップに渡します。
FROM
句: クエリで使用するテーブルやビューを指定し、結合(JOIN)を行います。ここで最初のデータソースが特定されます。WHERE
句:FROM
句で生成された中間結果セットに対して、行レベルでのフィルタリングを行います。条件に一致しない行はこの時点で破棄されます。GROUP BY
句:WHERE
句でフィルタリングされた結果セットを、指定されたカラムの値に基づいてグループ化します。HAVING
句:GROUP BY
句で生成されたグループに対して、グループレベルでのフィルタリングを行います。集計関数(SUM
,COUNT
,AVG
など)を含む条件を指定できるのはこの句です。SELECT
句:HAVING
句でフィルタリングされた結果セット(またはグループ化されていない場合はWHERE
句の結果セット)から、最終的に出力するカラムを選択し、式(計算、関数呼び出しなど)を評価し、エイリアスを適用します。ここで初めて、SELECT
句で定義されたエイリアスが利用可能になります。DISTINCT
(もしあれば):SELECT
句で生成された結果セットから、重複する行を削除します。ORDER BY
句:DISTINCT
(またはSELECT
句の結果セット)に対して、指定されたカラムに基づいて行の並べ替えを行います。SELECT
句で定義されたエイリアスをここで使用できます。LIMIT
/OFFSET
(またはTOP
など):ORDER BY
句で並べ替えられた結果セットから、指定された数の行だけを返します(結果セットの先頭から指定数だけ、あるいは指定したオフセット以降の指定数だけ)。
このリストは、最も一般的なクエリ構造に基づいたものです。もちろん、全てのクエリがこれらの句をすべて含むわけではありません。含まれていない句はそのステップがスキップされるだけです。また、サブクエリやCTE、ウィンドウ関数といったより高度な要素は、この基本順序の特定のステップの前や、ステップ内で評価されるものとして位置づけられます。
さて、次にこれらの各ステップを詳細に掘り下げていきましょう。
各ステップの詳細解説と中間結果のイメージ
ここでは、上記の論理的実行順序に従って、各ステップで具体的に何が行われ、次のステップにどのような中間結果が渡されるのかを詳しく見ていきます。
ステップ1: FROM
句 – データソースの特定と結合
- 何が行われるか?
クエリが処理を開始する最初のステップです。ここでは、クエリがどのテーブルやビューからデータを取得するかが指定されます。複数のテーブルを指定した場合は、ここでそれらのテーブル間の結合(JOIN)処理が行われます。指定された結合条件(ON
句など)に基づいて、テーブルから行が読み込まれ、結合されて新しい中間的な結果セットが生成されます。 - なぜこのタイミングか?
データを処理するには、まずそのデータがどこにあるかを特定する必要があります。FROM
句は、まさにその「データの所在」を指定する役割を果たします。他のどの操作(フィルタリング、グループ化、選択など)も、データソースが特定され、必要に応じて結合されていなければ実行できません。 - 関連する句:
JOIN
(INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN, CROSS JOIN)、テーブルエイリアス。 - 中間結果:
指定されたテーブルやビューから読み込まれ、必要に応じて結合された未フィルタの全データが生成されます。この結果セットは、まだどの行もフィルタリングされておらず、どの列も選択されていません(ただし、物理的にはオプティマイザが不要な列を早期に削除する場合がありますが、論理的には全列が存在する状態と見なせます)。この中間結果セットは、次のWHERE
句に渡されます。 - ポイント:
テーブルに指定するエイリアス(例:FROM products p JOIN categories c ON p.category_id = c.id
のp
やc
)は、このステップで有効になり、以降のすべてのステップ(WHERE
,GROUP BY
,HAVING
,SELECT
,ORDER BY
)で使用できます。
例:
2つのテーブルがあるとします。
products
テーブル:
product_id | product_name | category_id | price | stock |
---|---|---|---|---|
101 | Laptop | 1 | 1200 | 50 |
102 | Keyboard | 2 | 75 | 150 |
103 | Mouse | 2 | 25 | 200 |
104 | Monitor | 1 | 300 | 80 |
105 | Printer | 3 | 250 | 30 |
categories
テーブル:
category_id | category_name |
---|---|
1 | Electronics |
2 | Accessories |
3 | Peripherals |
クエリ: SELECT ... FROM products p JOIN categories c ON p.category_id = c.category_id ...
ステップ1の結果(中間結果セット): FROM
句とJOIN
句が評価され、結合されたデータセットが生成されます。
product_id | product_name | category_id (p) | price | stock | category_id (c) | category_name |
---|---|---|---|---|---|---|
101 | Laptop | 1 | 1200 | 50 | 1 | Electronics |
102 | Keyboard | 2 | 75 | 150 | 2 | Accessories |
103 | Mouse | 2 | 25 | 200 | 2 | Accessories |
104 | Monitor | 1 | 300 | 80 | 1 | Electronics |
105 | Printer | 3 | 250 | 30 | 3 | Peripherals |
この結合されたデータセット全体が、次のステップであるWHERE
句に渡されます。
ステップ2: WHERE
句 – 行レベルのフィルタリング
- 何が行われるか?
FROM
句で生成された中間結果セットの各行に対して、指定された条件式を評価します。条件式が真 (TRUE
) と評価された行だけが保持され、偽 (FALSE
) または不明 (UNKNOWN
) と評価された行はここで破棄されます。 - なぜこのタイミングか?
不要な行をできるだけ早い段階で削除することで、後続の処理(グループ化、並べ替えなど)のデータ量を減らし、パフォーマンスを向上させることができます。FROM
句でデータソースが確定した直後に行を絞り込むのが最も効率的です。 - 関連する句:
比較演算子 (=
,!=
,<
,>
,<=
,>=
)、論理演算子 (AND
,OR
,NOT
)、LIKE
,IN
,BETWEEN
,IS NULL
,IS NOT NULL
など。 - 使用できない要素:
このステップでは、SELECT
句で定義されたカラムエイリアスや、集計関数を直接使用することはできません。なぜなら、SELECT
句はWHERE
句よりも後に評価されるため、エイリアスはまだ存在せず、集計もまだ行われていないからです。 - 中間結果:
WHERE
句の条件を満たした行だけを含む中間結果セットが生成されます。この結果セットは、次のGROUP BY
句に渡されます。
例(続き):
クエリ: SELECT ... FROM products p JOIN categories c ON p.category_id = c.category_id WHERE p.price > 100 AND c.category_name = 'Electronics' ...
ステップ2の結果(中間結果セット): ステップ1の結果に対して、price > 100
かつ category_name = 'Electronics'
という条件でフィルタリングが行われます。
product_id | product_name | category_id (p) | price | stock | category_id (c) | category_name |
---|---|---|---|---|---|---|
101 | Laptop | 1 | 1200 | 50 | 1 | Electronics |
104 | Monitor | 1 | 300 | 80 | 1 | Electronics |
元の5行から2行に絞り込まれました。この2行を含む中間結果セットが、次のステップであるGROUP BY
句に渡されます。
ステップ3: GROUP BY
句 – グループ化
- 何が行われるか?
WHERE
句でフィルタリングされた中間結果セットの行を、指定された1つ以上のカラムの値に基づいてグループにまとめます。同じグループに属する行は、集計関数(COUNT
,SUM
,AVG
,MIN
,MAX
など)によって1つの値(グループの代表値)に集約できるようになります。 - なぜこのタイミングか?
グループ化は集計を行うための準備です。フィルタリング(WHERE
)が完了した後でなければ、正確なグループ分けや集計はできません。 - 関連する句:
集計関数 (COUNT
,SUM
,AVG
,MIN
,MAX
,GROUP_CONCAT
など。方言によって異なる)。 - ポイント:
GROUP BY
句で指定されたカラムや式は、後続のSELECT
句で集計関数と共に使用する場合、集計の基準となります。SELECT
句で集計関数を使用しないカラムは、原則としてGROUP BY
句に含める必要があります(例外もありますが、これは基本的なルールです)。 - 中間結果:
元の行が指定された基準でグループ化された抽象的なグループの集合が生成されます。厳密な「表形式」というよりは、「各グループに属する行の集まり」というイメージです。このグループの集合が、次のHAVING
句に渡されます。
例(続き):
クエリ: SELECT ... FROM products p JOIN categories c ON p.category_id = c.category_id WHERE p.price > 100 AND c.category_name = 'Electronics' GROUP BY c.category_name ...
ステップ3の結果(中間結果セット): ステップ2の結果に対して、category_name
でグループ化が行われます。
この例の場合、ステップ2の結果は2行とも category_name = 'Electronics'
なので、すべての行が1つのグループにまとめられます。
グループ1: { (Laptop, Electronics, ...), (Monitor, Electronics, ...) }
(条件: category_name = ‘Electronics’)
このグループの集合が、次のステップであるHAVING
句に渡されます。もしステップ2の結果に複数のカテゴリが含まれていれば、カテゴリごとにグループが作成されます。
ステップ4: HAVING
句 – グループレベルのフィルタリング
- 何が行われるか?
GROUP BY
句で生成された各グループに対して、指定された条件式を評価します。条件式が真 (TRUE
) と評価されたグループだけが保持され、偽 (FALSE
) または不明 (UNKNOWN
) と評価されたグループはここで破棄されます。 - なぜこのタイミングか?
HAVING
句はグループに対するフィルタリングを行うためのものです。したがって、グループ化(GROUP BY
)が完了した後でなければ実行できません。特に、HAVING
句の条件では集計関数を使用することが一般的であり、集計値はグループ化されて初めて計算可能になるため、この順序が論理的に必要です。 - 関連する句:
集計関数 (COUNT
,SUM
,AVG
,MIN
,MAX
など)。 - ポイント:
WHERE
句が集計前の個別行をフィルタリングするのに対し、HAVING
句は集計後の各グループをフィルタリングします。集計関数を含む条件でフィルタリングを行いたい場合は、必ずHAVING
句を使用する必要があります。 - 中間結果:
HAVING
句の条件を満たしたグループだけを含む中間結果セットが生成されます。この結果セットは、次のSELECT
句に渡されます。
例(続き):
クエリ: SELECT c.category_name, COUNT(p.product_id) AS product_count, SUM(p.price * p.stock) AS total_inventory_value FROM products p JOIN categories c ON p.category_id = c.category_id WHERE p.price > 100 AND c.category_name = 'Electronics' GROUP BY c.category_name HAVING COUNT(p.product_id) > 1 ...
ステップ4の結果(中間結果セット): ステップ3で生成されたグループに対して、COUNT(p.product_id) > 1
という条件でフィルタリングが行われます。
ステップ3で生成されたグループは1つ(Electronicsカテゴリ)でした。このグループ内の行数は2行です (Laptop
, Monitor
)。
このグループに対して COUNT(p.product_id)
を計算すると 2
になります。
条件 2 > 1
は真 (TRUE
) なので、このグループは保持されます。
保持されるグループの集合: { (Electronicsグループ) }
このグループの集合が、次のステップであるSELECT
句に渡されます。
もし別のカテゴリでグループ化されており、そのグループの製品数が1以下であれば、そのグループはここで破棄されます。
ステップ5: SELECT
句 – カラムの選択と変換
- 何が行われるか?
HAVING
句でフィルタリングされた中間結果セット(グループが存在する場合は各グループ、存在しない場合はWHERE
句の結果セット)から、最終的に出力するカラムを選択します。また、このステップでカラムに対する式(計算、関数呼び出し、集計関数の適用など)が評価され、結果の各行の各カラムの値が決定されます。さらに、カラムエイリアス(AS
を使ってカラムに新しい名前を付ける)もここで適用されます。 - なぜこのタイミングか?
最終的な出力形式を決定するには、必要なデータが全て揃っている必要があります。行レベルのフィルタリング(WHERE
)、グループ化(GROUP BY
)、グループレベルのフィルタリング(HAVING
)が完了した後に、どのカラムをどのように表示するかを決定するのが論理的です。 - 関連する句:
カラム名、式(算術演算、文字列操作、日付関数など)、集計関数、スカラー関数、カラムエイリアス (AS
)。 - ポイント:
SELECT
句で初めてカラムエイリアスが定義されます。このエイリアスは、このステップ以降の句(DISTINCT
,ORDER BY
)で使用可能ですが、このステップより前にある句(FROM
,WHERE
,GROUP BY
,HAVING
)では使用できません。これが「WHERE
句でSELECT
句のエイリアスが使えない」理由です。- 集計関数は、
GROUP BY
句がある場合は各グループに対して、GROUP BY
句がない場合は結果セット全体に対して適用されます。
- 中間結果:
最終的に出力されるカラムと行を持つ中間結果セットが生成されます。各行の各カラムの値は、指定された式に基づいて計算されています。この結果セットが、もしあれば次のDISTINCT
処理、なければORDER BY
句に渡されます。
例(続き):
クエリ: SELECT c.category_name, COUNT(p.product_id) AS product_count, SUM(p.price * p.stock) AS total_inventory_value FROM products p JOIN categories c ON p.category_id = c.category_id WHERE p.price > 100 AND c.category_name = 'Electronics' GROUP BY c.category_name HAVING COUNT(p.product_id) > 1;
ステップ5の結果(中間結果セット): ステップ4で保持されたグループに対して、SELECT
句で指定されたカラムと式が評価されます。
保持されたグループは Electronics のグループ1つでした。
c.category_name
: このグループのカテゴリ名は ‘Electronics’ です。COUNT(p.product_id)
: このグループ内の製品数は 2 です。この値にエイリアスproduct_count
が適用されます。SUM(p.price * p.stock)
: このグループ内の製品の在庫価値の合計を計算します。- Laptop: 1200 * 50 = 60000
- Monitor: 300 * 80 = 24000
- 合計: 60000 + 24000 = 84000
この値にエイリアスtotal_inventory_value
が適用されます。
結果として、次の形式の中間結果セットが生成されます。
category_name | product_count | total_inventory_value |
---|---|---|
Electronics | 2 | 84000 |
この結果セットが、もしあれば次のDISTINCT
処理、なければORDER BY
句に渡されます。ここで初めて product_count
や total_inventory_value
といったカラムエイリアスが使えるようになります。
ステップ6: DISTINCT
– 重複行の削除
- 何が行われるか?
SELECT
句で生成された中間結果セットの中から、完全に同じ内容を持つ重複行をすべて削除し、一意な行だけに絞り込みます。 - なぜこのタイミングか?
重複を判断するには、最終的に出力されるすべてのカラムの値が決まっている必要があります。したがって、SELECT
句でどのカラムをどのような値で表示するかが確定した後に、重複判定を行うのが論理的です。 - 関連する句:
SELECT DISTINCT ...
のようにSELECT
キーワードの直後に指定します。 - ポイント:
DISTINCT
は結果セット全体に対して作用します。指定されたカラムだけでなく、SELECT
リストに記載されているすべてのカラムの組み合わせが重複しているかどうかが判断基準になります。 - 中間結果:
重複する行が削除された、一意な行だけを含む中間結果セットが生成されます。この結果セットが、次のORDER BY
句に渡されます。
例:
クエリ: SELECT DISTINCT category_id FROM products;
ステップ6の結果(中間結果セット): SELECT
句で category_id
が選択され、その結果に対して DISTINCT
が適用されます。
products
テーブルの category_id
は 1, 2, 2, 1, 3
です。
SELECT category_id
の結果(DISTINCT
前):
| category_id |
| :———- |
| 1 |
| 2 |
| 2 |
| 1 |
| 3 |
DISTINCT category_id
の結果:
| category_id |
| :———- |
| 1 |
| 2 |
| 3 |
この重複が削除された結果セットが、次のステップであるORDER BY
句に渡されます。
(注: 先ほどのグループ化の例では GROUP BY
を使用しているため、通常 DISTINCT
を併用することはありません。GROUP BY
は各グループを1つの行に集約するため、結果はデフォルトで一意になります。DISTINCT
は、GROUP BY
を使用せずに単に重複行を排除したい場合に使われます。)
ステップ7: ORDER BY
句 – 結果の並べ替え
- 何が行われるか?
DISTINCT
(またはSELECT
句)で生成された中間結果セットの行を、指定された1つ以上のカラムの値に基づいて並べ替えます。昇順 (ASC
) または降順 (DESC
) を指定できます。 - なぜこのタイミングか?
結果の並べ替えは、最終的にユーザーに提示されるデータの順番を決定するものです。したがって、表示する行(WHERE
,HAVING
)とカラム(SELECT
)がすべて確定した後に行われます。 - 関連する句:
ASC
(昇順、デフォルト),DESC
(降順)。 - ポイント:
ORDER BY
句では、SELECT
句で定義されたカラムエイリアスを使用できます。これは、ORDER BY
句がSELECT
句よりも後に評価されるためです。ORDER BY
句で指定するカラムは、必ずしもSELECT
リストに含まれている必要はありません(ただし、多くのRDBMSで許可されていますが、一部のシステムや標準SQLでは制限がある場合もあります。一般的なRDBMSでは許可されています)。
- 中間結果:
指定された順番に並べ替えられた中間結果セットが生成されます。この結果セットが、もしあれば次のLIMIT
/OFFSET
句に渡されます。
例(続き):
先ほどのSELECT
句の結果を再利用します(DISTINCT
は省略)。
category_name | product_count | total_inventory_value |
---|---|---|
Electronics | 2 | 84000 |
この例では1行しかないので並べ替えの効果は見えにくいですが、別の例で考えます。
products
テーブルに対して、価格で降順に並べ替えるクエリ:
SELECT product_name, price FROM products ORDER BY price DESC;
SELECT product_name, price
の結果(ORDER BY
前、順不同):
| product_name | price |
| :———– | :—- |
| Laptop | 1200 |
| Keyboard | 75 |
| Mouse | 25 |
| Monitor | 300 |
| Printer | 250 |
ステップ7の結果(中間結果セット): ORDER BY price DESC
が適用されます。
product_name | price |
---|---|
Laptop | 1200 |
Monitor | 300 |
Printer | 250 |
Keyboard | 75 |
Mouse | 25 |
この並べ替えられた結果セットが、次のステップであるLIMIT
/OFFSET
句に渡されます。
エイリアスの使用例:
SELECT product_name AS name, price FROM products ORDER BY name ASC;
このクエリでは、SELECT
句で product_name
に name
というエイリアスを付けています。ORDER BY
句はSELECT
句の後に評価されるため、ここでエイリアス name
を使用して並べ替えを行うことができます。
ステップ8: LIMIT
/ OFFSET
(または TOP
) – 結果件数の制限
- 何が行われるか?
ORDER BY
句で並べ替えられた(あるいは、並べ替えが行われていない場合は順不同の)中間結果セットから、返す行の件数を制限します。LIMIT
は返す最大件数を指定し、OFFSET
は結果セットの先頭からスキップする行数を指定します。 - なぜこのタイミングか?
表示する行やカラム、そして行の順番が全て確定した後に、最終的にユーザーに見せる行数だけを切り出すのが論理的です。特にOFFSET
は先頭から数えてスキップするため、並べ替えが完了している必要があります。 - 関連する句:
LIMIT <件数>
,LIMIT <スキップする件数>, <件数>
,OFFSET <スキップする件数> LIMIT <件数>
(方言による)、TOP <件数>
(SQL Serverなど)。 - ポイント:
LIMIT
やOFFSET
を使用する際は、通常ORDER BY
句と組み合わせて使用します。ORDER BY
がない場合、どの行が「最初」や「次」になるかはデータベースの実装に依存し、結果が予測不能になる可能性があります。 - 中間結果:
指定された件数だけを含む最終的な結果セットが生成されます。これがクエリの最終出力となります。
例(続き):
価格で降順に並べ替えられた結果を再利用します。
product_name | price |
---|---|
Laptop | 1200 |
Monitor | 300 |
Printer | 250 |
Keyboard | 75 |
Mouse | 25 |
クエリ: SELECT product_name, price FROM products ORDER BY price DESC LIMIT 2;
ステップ8の結果(最終結果セット): ステップ7の結果から最初の2行だけが選択されます。
product_name | price |
---|---|
Laptop | 1200 |
Monitor | 300 |
これがクエリの最終的な出力となります。
論理的実行順序と物理的実行計画の違い
ここまで説明してきた順序は、「論理的実行順序」と呼ばれるものです。これは、SQL文がどのような手順で評価されれば、指定された論理的な結果(どの行を返すか、どのような値を計算するか、どのような順序で返すかなど)が得られるかを示したものです。
しかし、実際のデータベースシステムでは、この論理的な順序通りに物理的に処理が行われるとは限りません。データベースには「オプティマイザ」と呼ばれる賢いコンポーネントが搭載されており、クエリが要求する最終的な結果を変えない範囲で、最も効率的な物理的実行計画を生成します。
例えば、WHERE
句で非常に限定的な条件が指定されている場合、オプティマイザはFROM
句でテーブルを読み込む際に、インデックスを利用して最初から条件に合う行だけを物理的に読み込むかもしれません。これは、論理的にはFROM
-> WHERE
の順ですが、物理的には読み込みとフィルタリングが同時に、あるいはフィルタリングが読み込みをガイドする形で行われることを意味します。
また、小さなテーブル同士の結合であれば、物理的にはまず一方のテーブル全体を読み込み、次に他方を読み込んで結合するという手順を取るかもしれません。しかし、大きなテーブルと小さなテーブルであれば、小さなテーブルを先に読み込んでメモリに保持し、大きなテーブルをスキャンしながら小さなテーブルとマッチングするという、物理的には全く異なる手順を取る方が効率的な場合もあります。
SELECT
句で指定されたカラムについても、オプティマイザは物理的には必要なカラムだけを読み込むように最適化します。論理的には全カラムが存在する中間結果を想定していますが、物理的には最初から不要なカラムを無視できます。
重要: 物理的な実行計画はデータベースの種類(MySQL, PostgreSQL, SQL Server, Oracleなど)やバージョン、テーブルのデータ量、インデックスの有無、統計情報の鮮度など、様々な要因によって異なります。ユーザーはEXPLAIN
(多くのRDBMSで使用されるコマンド) などを使って、データベースが実際にどのような物理的実行計画を選択したかを確認できます。
しかし、 論理的な実行順序を理解することは依然として非常に重要です。なぜなら、論理的実行順序はSQL文の意味論(semantics)を規定するものであり、どのような最適化が行われようとも、最終的な結果は論理的実行順序に従って得られる結果と一致する必要があるからです。論理順序を理解していれば、物理的な計画を理解し、なぜ特定の最適化が可能なのか、あるいはなぜ非効率な計画が選択されたのかを分析する手助けになります。
例えば、「WHERE
句で集計関数の結果をフィルタリングできないのはなぜか?」という疑問は、WHERE
句が集計(GROUP BY
/HAVING
)よりも論理的に前に評価されるという事実を知っていればすぐに解決します。物理的な最適化がどう行われようとも、この論理的な制約は変わりません。
より高度なSQL要素と実行順序
基本となる実行順序を理解したところで、SQLのより高度な機能がどのようにこの順序に組み込まれるのかを見てみましょう。
サブクエリ (Subqueries)
サブクエリ(別のクエリの内部にネストされたクエリ)の実行タイミングは、その種類と使用される場所によって異なります。
- スカラーサブクエリ (Scalar Subqueries): 単一の行と単一の列を返すサブクエリで、式の代わりとして使用されます(例:
SELECT ..., (SELECT MAX(price) FROM products) AS max_price FROM ...
)。これは通常、そのサブクエリが記述されている句が評価される前に実行されます。なぜなら、外部クエリが評価されるためには、その式(サブクエリの結果)の値が必要だからです。 - 行サブクエリ (Row Subqueries): 単一の行と複数の列を返すサブクエリで、通常は比較演算子と組み合わせて使用されます(例:
WHERE (product_name, price) = (SELECT 'Laptop', 1200 FROM products WHERE product_id = 101)
)。これも、使用される句が評価される前に実行されます。 - テーブルサブクエリ (Table Subqueries): 複数の行と複数の列を返すサブクエリで、
FROM
句の中で使用されます(例:FROM (SELECT ... FROM ...) AS subquery_alias
)。これは、外部クエリのFROM
句が評価される前に、独立したデータソースとして評価され、その結果が外部クエリのFROM
句に渡されます。 - 相関サブクエリ (Correlated Subqueries): 外部クエリの各行に対して繰り返し実行されるサブクエリです。これは、外部クエリの特定の句(
SELECT
,WHERE
,HAVING
など)が評価される最中に実行されます。外部クエリの行が処理されるたびに、サブクエリは外部クエリの現在の行から値を取得して評価されるため、非常にコストが高くなる可能性があります。論理的には、外部クエリの行が処理され、その行データがサブクエリの評価に利用できるようになるタイミングで実行されます。
共通テーブル式 (CTEs – Common Table Expressions)
WITH
句を使用して定義されるCTEsは、クエリの本体が評価される前に評価されます。CTEは、一時的な名前付きの結果セットを作成し、その後のSELECT
, INSERT
, UPDATE
, DELETE
文や他のCTEから参照できます。
論理的には、以下の順序で処理されます。
WITH
句 (CTEsの定義と評価)- メインクエリの
FROM
句 (ここで定義されたCTEを参照) - 以降のメインクエリの標準的な実行順序 (
WHERE
,GROUP BY
, …)
CTEs自体も内部に独自の実行順序を持ちますが、それらはメインクエリが開始される前に完了します。ただし、多くのRDBMSオプティマイザは、CTEを「マテリアライズ」(一時的な結果セットとして物理的に保存)せずに、メインクエリの一部として最適化の中に組み込むことがあります(「CTEインライン化」)。これも物理的な最適化であり、論理的には「先にCTEが評価される」と考えるのが正しい理解です。
ウィンドウ関数 (Window Functions)
ウィンドウ関数(例: ROW_NUMBER() OVER(...)
, RANK() OVER(...)
, SUM() OVER(...)
, AVG() OVER(...)
など)は、標準の集計関数とは異なり、GROUP BY
句で集約された後ではなく、個々の行を保持したまま特定の「ウィンドウ」(関連する行のセット)に対して計算を行います。
ウィンドウ関数の評価タイミングは、論理的にはGROUP BY
句とHAVING
句の後、そしてORDER BY
句とLIMIT
句の前になります。
論理的なステップに追加すると、以下のようになります。
FROM
WHERE
GROUP BY
HAVING
SELECT
(非ウィンドウ関数、カラム選択、エイリアス)- ウィンドウ関数の評価
DISTINCT
ORDER BY
LIMIT
/OFFSET
これは重要なポイントです。ウィンドウ関数は集計関数とは異なり、WHERE
句やHAVING
句で直接フィルタリングすることはできません(ただし、ウィンドウ関数の結果に対してフィルタリングしたい場合は、サブクエリやCTEを使ってウィンドウ関数を先に評価し、その結果を外部クエリでフィルタリングするという方法があります)。また、SELECT
句のエイリアスはウィンドウ関数自体の中では使えませんが、ウィンドウ関数の結果にエイリアスを付けることは可能です。そして、ORDER BY
句ではウィンドウ関数の結果のエイリアスを使用できます。
セット演算子 (UNION
, INTERSECT
, EXCEPT
)
複数のSELECT
文の結果を結合するセット演算子(UNION
, INTERSECT
, EXCEPT
)は、それぞれのSELECT
文が独立して評価された後に処理されます。
各SELECT
文は、それぞれ独自の論理的実行順序に従って評価され、個別の結果セットを生成します。その後、これらの結果セットに対してセット演算子が適用されます。
sql
SELECT column1, column2 FROM table1 WHERE condition1
-- table1に対するFROM -> WHERE -> SELECT -> ... の順序で評価される
UNION -- または INTERSECT, EXCEPT
SELECT columnA, columnB FROM table2 WHERE condition2
-- table2に対するFROM -> WHERE -> SELECT -> ... の順序で評価される
ORDER BY ... -- UNION, INTERSECT, EXCEPTの結果全体に対するORDER BY
LIMIT ... -- その結果全体に対するLIMIT
セット演算子の後にORDER BY
句やLIMIT
句を指定する場合、それはセット演算によって結合された最終的な結果セット全体に対して適用されます。このとき、ORDER BY
句で使用できるカラムは、最初のSELECT
句で定義されたカラム名またはエイリアスに限られるのが一般的です。
まとめ:なぜこの順序なのか?
SQLの論理的実行順序は、データ処理の段階を反映しています。
- データの特定と準備 (
FROM
): まず、どのデータを使うかを決めます。 - 不要な行の早期排除 (
WHERE
): 必要なデータだけを処理対象にするために、最初にできるだけ多くの不要な行を捨てます。 - データの集約準備 (
GROUP BY
): 行をまとめる必要があるなら、ここでグループを作ります。 - 不要なグループの排除 (
HAVING
): グループができた後で、条件に合わないグループを捨てます。 - 表示する内容の決定 (
SELECT
): 最終的に、残ったデータ(行またはグループ)から、どの情報をどのように表示するかを決めます。ここで初めてカラムの名前や形が確定します。 - 重複の排除 (
DISTINCT
): 表示する内容が決まった後で、完全に同じ行があれば一つにまとめます。 - 結果の並べ替え (
ORDER BY
): 最終的に表示するデータの順序を決めます。 - 表示件数の制限 (
LIMIT
/OFFSET
): 最後に、表示する行数を絞ります。
この順序は、パフォーマンスと論理的な正確さの両方を考慮して設計されています。特にフィルタリング(WHERE
, HAVING
)と選択(SELECT
)の順序、そして集計(GROUP BY
, HAVING
)と選択(SELECT
)の関係が重要です。
WHERE
がSELECT
より前なのは、処理データ量を早期に減らすため。HAVING
が集計(GROUP BY
)より後でSELECT
より前なのは、集計結果でグループをフィルタリングし、その後の表示内容を決めるため。SELECT
句で定義されたエイリアスや式がWHERE
やGROUP BY
で使用できないのは、それらの句がSELECT
より前に評価されるため。しかし、ORDER BY
やHAVING
(集計関数の結果のエイリアス)では使用できる場合があるのは、それらがSELECT
より後に評価されるため(ただしHAVING
でのエイリアス使用はRDBMSによる)。標準的にはORDER BY
での使用が認められています。
実践的なアドバイス:実行順序を意識したクエリ記述
SQL実行順序の知識を実際のクエリ記述にどう活かすか、いくつかの実践的なアドバイスです。
- フィルタリングは早く、並べ替えは遅く: 可能であれば、
WHERE
句を使ってできるだけ早い段階で不要な行を絞り込みましょう。これはパフォーマンスに最も大きな影響を与えます。並べ替え(ORDER BY
)は最もコストの高い操作の一つであり、処理する行数を減らした後に実行するのが効率的です。 WHERE
とHAVING
の使い分けを理解する: 行単位のフィルタリングはWHERE
で、集計結果に対するフィルタリングはHAVING
で行うことを徹底しましょう。「合計売上が1000ドル以上の顧客」のように、集計関数を使う条件は必ずHAVING
を使います。- エイリアスの利用ルールを把握する:
SELECT
句のエイリアスはWHERE
,GROUP BY
,HAVING
句では使えません(ただしHAVING
で集計結果のエイリアスが使えるかは方言によります)。FROM
句のテーブルエイリアスは全ての句で使えます。ORDER BY
句ではSELECT
句のエイリアスが使えます。このルールは実行順序から自然に導かれるものです。 LIMIT
/OFFSET
を使う際はORDER BY
を必ず使う: 特定の件数だけを取得する場合、どの行から取るかは並べ替えられていないと保証されません。予測可能な結果を得るためには、LIMIT
/OFFSET
とORDER BY
をセットで使いましょう。- サブクエリやCTEsの評価タイミングを考慮する: 特に相関サブクエリの使用は、パフォーマンス問題を引き起こしやすいです。可能であればJOINやウィンドウ関数で代替できないか検討しましょう。CTEsはクエリを読みやすくしますが、物理的な実行計画にどう影響するか(マテリアライズされるか、インライン化されるか)はRDBMSに依存するため、パフォーマンスチューニング時には
EXPLAIN
で確認することが重要です。 - 実行計画を確認する: 書いたクエリがどのように実行されるかを理解するために、積極的に
EXPLAIN
コマンド(またはそれに類するコマンド)を利用しましょう。論理的な理解と物理的な実行のギャップを埋めるのに役立ちます。
まとめ
この記事では、SQLの論理的実行順序について詳細に解説しました。私たちがSQL文を書く見た目の順序とは異なり、データベースエンジンはFROM
-> WHERE
-> GROUP BY
-> HAVING
-> SELECT
-> DISTINCT
-> ORDER BY
-> LIMIT
/OFFSET
という論理的な順序で各句を評価していきます。
各ステップで何が行われ、どのような中間結果が次のステップに渡されるのかを理解することは、クエリの動作を正確に予測し、意図しない結果やエラーを防ぐために不可欠です。特に、どの句がどのタイミングで評価されるかを知っていれば、WHERE
句でSELECT
句のエイリアスが使えない理由や、HAVING
句が集計関数を扱うために必要な理由などが明確になります。
また、この論理的な理解は、データベースのオプティマイザがどのように物理的な実行計画を生成するかを推測する上での基礎となり、パフォーマンスの最適化(特に早期のフィルタリング)を行うための重要な指針となります。
SQLは、その宣言的な性質から、必ずしも内部の処理手順を意識せずに記述できる強力な言語です。しかし、より複雑な要求に応えたり、大規模なデータを効率的に扱ったりするためには、その裏側にある「データベースがどのようにクエリを解釈し、処理しているか」を理解することが不可欠です。SQL実行順序は、その理解の核心部分と言えるでしょう。
この記事が、あなたのSQLスキル向上の一助となれば幸いです。SQLの学習や業務において、ぜひこの実行順序を意識してみてください。
注記: この記事は一般的なSQLの論理的実行順序に基づいています。具体的なRDBMS製品(MySQL, PostgreSQL, SQL Server, Oracleなど)によっては、構文のバリエーション(例: LIMIT
/OFFSET
と TOP
)や、物理的な最適化の挙動に違いがあります。しかし、根幹となる論理的な処理順序の概念は多くのRDBMSで共通です。