ジェフリー・ウルマンの中核的な考え方が現代のデータベースを支える仕組み:関係代数、書き換えルール、結合、コンパイラ風のプランニングがどのようにシステムのスケールを可能にするかを実務寄りに解説。

SQLを書いたりダッシュボードを作ったり遅いクエリをチューニングしたことがある人の多くは、名前を知らなくてもジェフリー・ウルマンの仕事の恩恵を受けています。ウルマンは、データベースがデータをどう記述し、クエリをどう推論し、効率的に実行するかを定義する研究と教科書で知られる計算機科学者・教育者です。
データベースエンジンがあなたのSQLを高速に実行可能な形に変換する際には、厳密かつ適応的でなければならない多くのアイデアが使われています。ウルマンはクエリの「意味」を形式化する手法を整え(システムが安全に書き換えられるように)、データベースの考え方とコンパイラの考え方を結びつけました(クエリをパースし、最適化し、実行手順に翻訳できるようにする)。
その影響はBIツールのボタンやクラウドコンソールの目に見える機能として現れるわけではありません。現れるのは次のような形です:
JOIN を書き換えたりすると速くなるクエリ本稿はウルマンの中核的な考えを案内役にして、実務で最も重要なデータベース内部の要点を説明します:SQLの下にある関係代数、書き換えが意味をどう保つか、コストベースのオプティマイザがなぜその選択をするか、そして結合アルゴリズムがジョブを数秒で終わらせるか数時間かかるかを左右する理由です。
また、パース、書き換え、プランニングといったコンパイラ風の概念も取り入れます。データベースエンジンは多くの人が思うよりずっと洗練されたコンパイラに近い振る舞いをするからです。
お約束:議論は正確に保ちつつ数学的な証明は避けます。目的は、次にパフォーマンスやスケーリング、わかりにくいクエリ挙動が現れたときに職場で使えるメンタルモデルを与えることです。
SQLを書いて「クエリはただ一つの意味を持つはずだ」と期待したことがあるなら、あなたはウルマンが普及・形式化したアイデアに頼っています:データの明確なモデルと、クエリが何を求めているかを正確に記述する方法です。
関係モデルは本質的にデータをテーブル(リレーション)として扱います。各テーブルには行(タプル)と列(属性)があります。今では当たり前に聞こえますが、重要なのはその規律が生む利点です:
この枠組みにより、正しさや性能について根拠を持って考えられるようになります。テーブルが何を表現し、行がどう識別されるかがわかれば、結合が何をすべきか、重複が何を意味するか、特定のフィルタがなぜ結果を変えるのかを予測できます。
ウルマンの教育では、関係代数がクエリの電卓のように扱われます:ごく少数の演算(選択、射影、結合、和、差)を組み合わせて欲しい結果を表現するという考え方です。
実務でSQLとどう関係するかというと:データベースはSQLを代数形式に翻訳し、別の等価な形に書き換えます。見た目が違う2つのクエリが代数的には同じであり得る、というのがオプティマイザが結合順序を入れ替えたりフィルタを押し下げたり冗長な作業を除去したりできる理由です。
SQLは大部分が「何を」寄りですが、エンジンは最適化のために代数的「どうやって」を使います。
Postgres、Snowflake、MySQLといったSQL方言は異なりますが、基礎は不変です。キー、リレーションシップ、代数的等価性を理解していれば、クエリが論理的に間違っているのか単に遅いだけなのか、どの変更が意味を保つのかを見極められます。
関係代数はSQLの“下の数学”です:欲しい結果を記述する少数の演算子群。ウルマンの業績はこの演算子の見方を明確にしやすくし、今でも多くのオプティマイザが使うメンタルモデルとなっています。
データベースのクエリは少数の構成要素のパイプラインとして表現できます:
WHERE に相当)SELECT col1, col2 に相当)JOIN ... ON ...)UNION)EXCEPT のようなもの)集合が小さいため、等価性の議論がしやすくなります:もし2つの代数式が同値なら、どんな有効なデータベース状態でも同じ表を返します。
馴染みのあるクエリを例に取ると:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
概念的には、これは:
customers と orders の結合を始める:customers ⋈ orders
o.total > 100 のみを残すように 選択(σ) を適用:σ(o.total > 100)(...)
取り出したい列だけを 射影(π):π(c.name)(...)
これは全てのエンジンが内部で使う正確な表記ではないかもしれませんが、正しい考え方です:SQLは演算子ツリーになります。
多くの異なるツリーが同じ結果を意味することがあります。例えば、フィルタはしばしばより早い段階に押し下げられ(σ を先に適用)、射影は不要な列を早めに落とすことができます(π を先に適用)。
これらの等価規則があることで、データベースはクエリを書き換えても意味を変えずにより安価なプランに変換できます。クエリを代数として見ると、最適化は魔法ではなくルールに基づく安全な形の変形になります。
SQLを書いたとき、データベースはそれを「書いた通り」に実行するわけではありません。文をクエリプランに翻訳します:実行すべき作業の構造化された表現です。
良いメンタルモデルは演算子の木です。葉はテーブルやインデックスを読み、内部ノードは行を変換・結合します。一般的な演算子には scan、filter(選択)、project(射影)、join、group/aggregate、sort などがあります。
データベースは通常、計画を2層に分けます:
ウルマンの影響は、意味を保つ変換を重視する点に現れます:論理プランを様々な方法で並び替えても答えは変わらないと保証した上で、効率的な物理戦略を選びます。
最終的な実行戦略を選ぶ前に、オプティマイザは代数的な“掃除”ルールを適用します。これらの書き換えは結果を変えませんが、不必要な作業を減らします。
一般的な例:
ユーザがある国にいる注文を取りたい場合:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
素朴に解釈すると 全ユーザ と 全注文 を結合してからカナダのフィルタを適用するかもしれません。意味を保つ書き換えはフィルタを押し下げ、結合する行数を減らします:
country = 'CA' でユーザを先に絞るorder_id と total を射影するプラン的には次のように変えようとします:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
を
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
に近い形にする。答えは同じで、作業量は少なくなります。
これらの書き換えはあなたが明示的にタイプするものではないため見落としがちですが、同じSQLがあるデータベースで速く、別のデータベースで遅い主因はここにあります。
SQLを実行するとき、データベースは同じ答えを返す複数の方法を検討し、最も安そうな方法を選びます。これがコストベース最適化であり、ウルマン風の理論が日常的なパフォーマンスに現れる最も実践的な場所の一つです。
コストモデルは、オプティマイザが代替プランを比較するための採点システムです。ほとんどのエンジンは次のような主要リソースでコストを推定します:
モデルは完璧である必要はなく、十分にしばしば方向性が正しければ良いのです。それで良いプランを選べます。
プランを評価する前に、オプティマイザは各ステップで「どれだけの行が出るか」を推測します。これがカードィナリティ推定です。
WHERE country = 'CA' のようなフィルタならテーブルのどれだけの割合が該当するかを推定します。顧客と注文を結合するなら、結合キーでどれだけペアができるかを推定します。これらの行数予測が、インデックス走査を選ぶか全表走査を選ぶか、ハッシュ結合を選ぶかネストループを選ぶか、ソートが小さいか巨大かを決めます。
オプティマイザの推測は統計に依存します:件数、値の分布、NULL率、列間の相関などです。
統計が古かったり欠如していると、オプティマイザは行数を桁違いに誤推定します。紙上では安そうに見えたプランが実行では高コストになることがあり、典型的な症状はデータ増加後の突然の遅延、突発的なプラン変化、結合が予期せずディスクにスピルすることです。
より良い推定は、より詳しい統計やサンプリング、より多くの候補プランの探索など追加の作業を必要とすることがあります。しかしプラン作成自体にも時間がかかるため、特に複雑なクエリでは計画時間とのトレードオフになります。
したがってオプティマイザは二つの目的のバランスを取ります:
EXPLAIN 出力を解釈するとき、このトレードオフを理解しておくとオプティマイザは「賢く振る舞おうとしている」のではなく「限られた情報で予測可能に正しくあろうとしている」と見なせます。
ウルマンの仕事は、SQLが「実行される」のではなく「実行計画に翻訳される」ことを広める助けになりました。結合ほどそれが明白に現れる部分はありません。同じ行を返す二つのクエリでも、エンジンが選ぶ結合アルゴリズムや結合順序によって実行時間は大きく異なります。
ネストループ結合は概念的に単純です:左の各行について右側のマッチする行を探します。左側が小さく、右側に有効なインデックスがあるときに速くなります。
ハッシュ結合は一方の入力(通常は小さい方)でハッシュ表を作り、もう一方で探します。等価条件(例:A.id = B.id)の大きな未ソート入力に向いていますが、メモリを必要とし、スピルが発生すると利点が失われます。
マージ結合は二つの入力をソート順で順に走査します。両方が既に秩序付けられている場合(インデックスが結合キー順に行を提供できるなど)に非常に適しています。
3つ以上のテーブルがあると可能な結合順序は爆発的に増えます。大きなテーブル同士を先に結合すると巨大な中間結果ができて残りが遅くなります。より良い順序は通常、最も選択度の高いフィルタ(行が少ない)から始め、中間結果を小さく保って外側に広げます。
インデックスは単にルックアップを速くするだけでなく、特定の結合戦略を可能にします。結合キーにインデックスがあれば、高価なネストループを「行ごとのシーク」に変えられることがあります。逆にインデックスがないとエンジンはハッシュ結合や大規模なソートに頼らざるを得ないかもしれません。
データベースは単にSQLを“実行する”のではなく、それをコンパイルします。ウルマンの影響はデータベース理論とコンパイラ的思考の両方に及び、この結びつきがあるためクエリエンジンはプログラム言語のツールチェーンのように振る舞います:翻訳し、書き換え、最適化してから実行するのです。
クエリを送ると最初のステップはコンパイラのフロントエンドのようです。エンジンはキーワードと識別子をトークン化し、文法をチェックして**構文木(parse tree)**を作ります(しばしば抽象構文木に簡略化されます)。ここで基本的なエラーが検出されます:カンマの欠落、曖昧な列名、無効なGROUP BYなど。
有用なメンタルモデル:SQLは「ループの代わりにデータ関係を記述するプログラム」であるプログラミング言語に似ています。
コンパイラが構文を中間表現(IR)に変換するのと同様に、データベースはSQL構文を論理演算子に変換します。例えば:
GROUP BY)その論理形はSQLテキストより関係代数に近く、意味と等価性を議論しやすくなります。
コンパイラ最適化はプログラムの結果を同じに保ちながら実行コストを下げます。データベースオプティマイザも同様で、次のようなルールを使います:
これは「デッドコード削除」と同じ哲学で、手法は異なっても「意味を保持してコストを減らす」という点で一致します。
クエリが遅いときはSQLだけを見つめないでください。実際にエンジンが選んだものを示すクエリプランを見てください。プランは結合順序、インデックス使用、時間を要する箇所を示します。
実務的な結論:EXPLAINの出力を性能の「アセンブリ一覧」として読む習慣をつけましょう。推測ではなく証拠に基づくチューニングが可能になります。詳しい習慣作りは /blog/practical-query-optimization-habits を参照してください。
良いクエリ性能はしばしばSQLを書く前に始まります。ウルマンのスキーマ設計理論(特に正規化)は、データを正しく、予測可能に、効率的に保つための設計法を与えます。
正規化は次を目指します:
これらの正しさの利点は後の性能向上にもつながります:重複フィールドが減り、インデックスが小さくなり、高価な更新が減ります。
証明を覚える必要はありませんが、考え方は簡単です:
次のような場合、デノーマライズは有効な選択です:
重要なのは、デノーマライズを意図的に行い、重複を同期するプロセスを持つことです。
スキーマ設計はオプティマイザのできることを形作ります。明確なキーと外部キーはより良い結合戦略、安全な書き換え、より正確な行数推定を可能にします。一方で過度の重複はインデックスを肥大化させ書き込みを遅くし、複数値列は効率的な述語を阻害します。データ量が増えるにつれて、初期のモデリング決定は単一クエリのマイクロ最適化より重要になることが多いです。
システムが「スケールする」とき、それは単に大きなマシンを追加するだけではありません。同じクエリの意味を保ちながら、非常に異なる物理戦略を選んで実行時間を予測可能にする必要が出てくることが多いです。ウルマンの等価性の重視は、結果を変えずに戦略を変えられることを可能にします。
小さな規模では多くのプランが「動作する」ことがあります。スケールすると、テーブルをスキャンするかインデックスを使うか、事前計算された結果を使うかの違いが秒と数時間の差になります。理論の側面が重要になるのは、オプティマイザが答えを変えずに安全に適用できる書き換えルール群(フィルタ押し下げ、結合の並べ替え等)を持っている必要があるからです。
日付や顧客、リージョンでのパーティショニングは論理的には1つのテーブルを物理的に複数の断片に分けます。これがプランに与える影響は:
SQLテキストは変わらなくても、最適なプランは行がどこにあるかに依存します。
マテリアライズドビューは基本的に「保存された部分式」です。エンジンがクエリを保存済み結果と同値(または書き換えで同値にできる)だと証明できれば、高価な結合や集計を繰り返し計算する代わりに高速なルックアップで置き換えられます。これは関係代数的思考の実践例です:等価性を認識して再利用する。
キャッシュは繰り返し読み取りの速度を上げますが、スキャンすべきデータが多すぎるクエリや巨大な中間結合が必要なクエリの根本的な問題は解決しません。スケールの問題が出たら、多くの場合は:触るデータ量を減らす(配置/パーティショニング)、繰り返し計算を減らす(マテリアライズドビュー)、あるいはプランを変えることが正解で、単なるキャッシュ追加ではないことが多いです。
ウルマンの影響は単純なマインドセットに現れます:遅いクエリを「意図の声明」として扱い、データベースがどう書き換えたかを検証することです。理論家になる必要はありません。繰り返し可能なルーチンがあれば恩恵を受けられます。
まず実行時間を支配することが多い部分を見る:
一つだけやるなら、行数が爆発的に増える最初の演算子を特定してください。多くの場合そこが根本原因です。
書きやすく意外に高コストなもの:
WHERE LOWER(email) = ... はインデックス利用を阻害する。代わりに正規化列や関数インデックスを使う。関係代数は次の実用的な動きを促します:
WHERE を適用して入力を小さくする。良い仮説の例:「この結合が高コストなのは結合入力が多すぎるからだ。orders を直近30日で先に絞れば結合入力が減るはずだ。」
単純な判断ルール:
EXPLAIN が不要な作業(不必要な結合、遅いフィルタ、非SARGableな述語)を示す場合。目標は“賢いSQL”ではなく、予測可能で小さい中間結果を作ることです—これこそウルマンの等価性の考え方が見せてくれる価値です。
これらの概念はDB管理者だけのものではありません。アプリを出荷するなら、スキーマの形状、キーの選択、クエリパターン、データアクセス層といった決定を通して、すでにクエリ計画に影響を与えています。
もしあなたが vibe-coding ワークフローを使っていて(たとえばチャットインターフェースで Koder.ai から React + Go + PostgreSQL アプリを生成するような場合)、ウルマン流のメンタルモデルは実用的な保険になります:生成されたスキーマをキーとリレーションシップの観点でレビューし、アプリが頼るクエリを点検し、本番化前に EXPLAIN で性能を検証できます。"クエリ意図 → プラン → 修正" のサイクルを速く回せるほど、加速された開発から得られる価値は大きくなります。
“理論を別に勉強する”ことを趣味にする必要はありません。ウルマン流の基礎から得られる最速の利点は、クエリプランを自信を持って読めるようになることで、そのために学ぶべき最小限を実地で練習することです。
次の本や講義トピックを探してみてください(いずれも広く引用されている出発点です):
小さく始めて、各ステップを観察可能なことに結びつけましょう:
実際の2〜3件のクエリを取り、次を繰り返してください:
IN と EXISTS の比較、フィルタの前倒し、不要列の削除、結果の比較。プランに基づいた明確な言葉を使って説明します:
これがウルマンの基礎がもたらす実務上の利点です:推測ではなく共有できる語彙で性能を説明できます。
ジェフリー・ウルマンは、データベースが「クエリの意味をどう表現するか」と「クエリを安全に高速化するためにどのように変換できるか」を形式化した研究と教科書で知られる計算機科学者です。その基盤は、エンジンがクエリを書き換えたり結合順序を入れ替えたり、異なる実行計画を選んだりするときに、同じ結果セットを保証する形で現れます。
関係代数は、選択(select)、射影(project)、結合(join)、和(union)、差(difference)といった一組の演算子でクエリ結果を厳密に記述する方法です。データベースは通常、SQLを代数的な演算子ツリーに変換して、フィルタの前倒しなどの同値変換(等価規則)を適用し、効率的な実行戦略を選べるようにします。
書き換えが“意味を保つ”ことを証明できるからこそ最適化が実用的になります。同値規則によりオプティマイザは次のようなことを行えます:
WHERE フィルタを結合の前に押し下げるこれらは意味を変えずに作業量を劇的に削減できます。
論理プランは「何を計算するか」を抽象演算で表したもので、物理プランはそれを実際のストレージやハードウェア上で「どう実行するか」を決めるものです。論理的な書き換えで候補を増やし、その後で物理的な手法(インデックス走査か全表走査か、ハッシュ結合かネストループ結合か等)を選ぶことで性能差が生まれます。
コストベース最適化とは、複数の有効な実行計画を比較して、最も低コストだと推定される計画を選ぶプロセスです。コストは通常、処理する行数、I/O(ディスク/SSD読み取りとキャッシュ効果)、CPU(フィルタやハッシュ、ソートの計算量)、メモリ(メモリ内で完結するかスピルするか)といった要素で評価されます。
カードィナリティ推定(cardinality estimation)は、オプティマイザが各ステップで「どれだけの行が出るか」を推測することです。これらの推定が結合順序や結合方式、インデックスを使うかどうかを左右します。統計が古かったり欠如すると推定が大きく外れ、突然の遅延やディスクへのスピル、大きなプラン変更を招きます。
以下に注目してください:
プランを実行結果の「アセンブリ出力」として読むと、チューニングは推測ではなく証拠に基づく作業になります。詳しくは /blog/practical-query-optimization-habits を参照してください。
正規化は事実を一箇所にまとめて更新異常を減らし、整合性を高め、エンジンにキーや外部キーなどの制約を表現させることを目的とします。結果としてインデックスやテーブルが小さくなり、更新が効率的になります。分析用途などで読み取り重視なら、制御された冗長化(デノーマライズ)を選ぶことも合理的です。
スケール時には論理的なクエリの意味を変えずに、物理的な戦略を大きく変える必要が出てきます。一般的な手法としては:
キャッシュは繰り返しの読み取りを助けますが、触るデータ量や中間結合の大きさを変えられないクエリはキャッシュだけでは解決しません。