💾

(理論は知ってますか?👀)集合論とリレーショナルモデルで理解するSQLとRDBMS📚🔍🐬🐘

2024/12/17に公開

Commune Advent Calendar 2024 シリーズ2 17日目の記事です🎄📅

「長年のモヤモヤの原因は、”データの捉え方”にあったのかも。」

ChatGPT o1に概要を述べてもらいました。客観的で簡潔ですね〜。

本記事は、日常の開発現場で漠然とした「モヤモヤ」を感じていた筆者が、その原因を「リレーショナルモデルへの理解の浅さ」にあるのではないかと仮定し、理論的基盤を振り返ろうとするものです。

モヤモヤの背景:
ORMやSQLクエリを書ける、使える状況にはある。しかし、いざ新しい機能の企画や仮説立案になるとデータの捉え方にしっくりこない感じが残る。
→ 実務でRDBMSやSQLに日々触れているが、土台となる「リレーショナルモデル」やそれを支える「集合論」の理解が浅いのではないか、と分析。

振り返りの方向性:
リレーショナルモデル(理論)の上にRDBMS(実装)、そしてその上にSQL(操作言語)がある。さらにリレーショナルモデルの基礎には数学的理論である集合論がある。
→ 今回はその理論(集合論・リレーショナルモデル)を再確認する。

用語整理:
リレーショナルモデル、リレーショナルデータベースの双方で使われる基本用語(関係、属性、タプル、射影、選択、結合など)や、理論固有、実装固有の用語を整理。

集合論の基礎:
テーブルは本質的に「行の集合」である。集合の特徴(重複なし、順序なし、NULLなし(理論上))とテーブル構造の対応関係を説明。また、基本的な集合演算(和集合、積集合、差集合、直積)をおさらいする。

リレーショナルモデルの基礎:
関係代数の演算(選択σ、射影π、結合⋈、和∪、差−、直積×、除算÷、交差∩)とそのSQLでの対応付けを紹介。
制約(主キー、外部キー、ドメイン制約、参照整合性制約)に触れ、一貫性維持のための理論的基盤を確認。

演習問題:
集合演算や関係代数式を使い、理解度を確認できる問題がある。

補足・余談:
SQLは理論そのままではなく、NULLや重複、順序といった集合論にはない拡張を行っているため、その点を意識する必要があることを指摘。
推薦書籍や参考リンクを提示して終了。

1. 何がモヤモヤするの?

※私の気持ちです。読み飛ばしても構いません🙆 もしよければ聞いてやってください。

きっかけは ”違和感”

普段の開発業務では、ORMやSQLを使ったデータ操作は一通りできる。
PrismaのようなORMを使ってCRUD操作を書くとき、なにか調査したくてSQLクエリを書くとき、既にある資料やソースコードを参考にするのがほとんどで、生成AIもあるし 「詰まることが減った」 気がしています。
しかし、いざ新しい機能を設計したり、これからコーディングする内容を頭の中で組み立てたり、調査の進め方を考えたりするとき、 「この捉え方で合っているのか?」 「もっと良い仮説の立て方があるのでは?」 とモヤモヤする場面が多いと気づきました。ソフトウェアエンジニアになって数年経ちますが、ずっとモヤモヤしている、なんとなく言語化できそうだけどできなくて気持ち悪いな〜と感じました。

(少し飛躍するかもですが)原因は、自分の開発しているシステムが生み出す事実(=データ)を、それらのつながりをちゃんと理解できてないからではないかと考えました。

「もう何年もRDBMSの使い方、SQLの構文に触れているのにデータ周りがよくわかってない…。」

率直にこう思いました。でもこれ、よく見るとおかしいですよね。「RDBMS/SQLの使い方を知ること ≠ データ周りを知ること」のはず。データといっても形式/枠組みの話なのか業務知識の話なのかいろんな要素がありそうです。業務知識が最も重要だけど対象業務によってまちまち、対してデータの形式/枠組みはある程度決まっていそうな気がします。実は、この「データの形式/枠組み」には普遍的な理論的基盤があります。業務知識は事業ドメイン依存ですが、データモデルはどんなドメインでも理論的なフレームワークとして共通しています。日々RDBやSQLに触れていながら、この基盤となる理論的なモデルの理解が浅いために、設計時や調査時の切り口が見えづらくなっているのではないか…。

そこで、RDBMSやSQLの基盤となっているリレーショナルモデルをちゃんと理解すれば、データの構造や運用を今よりもしっかり捉えられるようになり、設計や調査を進める際の 「どこから手を付ければ良いか」 「どのように仮説を検証すれば良いか」 という迷いが減るのではないかと考えました👀

そんな仮説のもと、今回はリレーショナルモデルや集合論の基礎を振り返ってみようと思います👇

2. 何をおさらいするの?

私のようなソフトウェアエンジニアが業務で触れるのはRDBMSといったソフトウェアです。

ご存知の通りRDBはリレーショナルデータベースを指します。SQLは「リレーショナルデータベースとやり取りするために設計された専用言語」です。

SQLはリレーショナルデータベースのために設計されていて…
リレーショナルデータベースはリレーショナルモデルという理論に基づいていて…
リレーショナルモデルは集合論という数学の理論に基づいていて…

という関係です。集合論とリレーショナルモデルが理論で、リレーショナルデータベースが実践です。こんな感じでしょうか👇

  1. 集合論(数学): データを集合や演算として数学的に表現する理論的基盤
  2. リレーショナルモデル: データを関係(Relation)として表現し、関係代数による操作を定義したデータモデルで、集合論に基づく理論
  3. リレーショナルデータベース: リレーショナルモデルを基に構築されたソフトウェア🐬🐘

というわけで、今回は理論(①②)からおさらいします。

3. 登場する用語たち / Keywords

本テーマに関係する用語をまとめておきます。記事を読み進めながら気になった用語があれば立ち寄ってもらえれば、と。

3.1 共通の用語

リレーショナルモデルとリレーショナルデータベースの両方で使われる用語です。
  • 関係(Relation):テーブルに相当
  • 属性(Attribute):カラムに相当
  • タプル(Tuple):行に相当
  • キー(Key):データを一意に識別する要素(主キー、外部キーなど)
  • 制約(Constraint):データの整合性を保つためのルール(NOT NULL, UNIQUE, CHECKなど)
  • 結合(Join):複数のリレーションを結びつける操作(内部結合、外部結合)
  • 和集合(Union):2つのリレーションの和集合を取得
  • 射影(Projection):特定の属性(カラム)だけを取得
  • 選択(Selection):条件に基づいて特定のタプル(行)を取得
  • ビュー(View):関係から導出される仮想的な関係

3.2 理論固有の用語(リレーショナルモデル)

リレーショナルモデルや関係代数で登場する概念です。
  • σ(シグマ:選択演算子):条件に基づいてタプルを選択
  • π(パイ:射影演算子):特定の属性を選択
  • ⋈(ナチュラルジョイン演算子):2つのリレーションを共通の属性で結合
  • カーディナリティ(Cardinality):リレーション内のタプル(行)の数
  • 直積(Cartesian Product):2つのリレーションを組み合わせる操作
  • ドメイン(Domain):属性が取り得る値の集合
  • 関係代数(Relational Algebra):リレーショナルモデルの操作を記述する数学的体系
  • 関係計算(Relational Calculus):リレーションに関するクエリを定義する別の形式
  • ビューの分解(View Decomposition):ビューを構成する基盤データへの分解(ビューを定義する際に、そのビューがどのような基礎となるリレーションから構成されているかを理解するプロセスを指す)

3.3 実装固有の用語(リレーショナルデータベース)

リレーショナルデータベースシステム(RDBMS)における概念です。
  • インデックス(Index):データ検索を高速化する仕組み
  • トランザクション(Transaction):データ操作の一連の処理をまとめて管理
  • ストアドプロシージャ(Stored Procedure):データベース内で実行されるプログラム
  • トリガー(Trigger):特定の操作に応じて自動的に実行される処理
  • パーティション(Partition):大規模なテーブルを分割して管理する技術
  • ACID特性:トランザクションの一貫性を保証する原則
  • ビューのマテリアライズ(Materialized View):クエリ結果を物理的に保存したもの
  • シャーディング(Sharding):データを分散して保存する技術
  • 実行計画(Execution Plan):SQLクエリの実行プロセス
  • ER図(Entity-Relationship Diagram):リレーショナルデータベース設計のための図式表現
  • 正規化(Normalization):データの冗長性を減らし整合性を保つ設計プロセス
  • 反正規化(Denormalization):パフォーマンス最適化のためのデータ構造の簡略化

4. 集合論の基礎

ここから先、これらの集合やテーブルを使って説明していきます。

集合:A、B、Students(学生)、Courses(科目)、Enrollments(履修)

- A = {1, 2, 3, 4, 5}
- B = {4, 5, 6, 7, 8}
- Students = {
    (1, '田中', 19, 'active'),
    (2, '山田', 24, 'inactive'),
    (3, '佐藤', 20, 'pending')
}
- Courses = {
    (1, 'データベース', 2),
    (2, 'プログラミング', 3),
    (3, '統計学', 2),
    (4, '機械学習', 3)
}
- Enrollments = {
    (1, 1, 'A'),  # 田中がデータベースでA
    (1, 2, 'B'),  # 田中がプログラミングでB
    (2, 1, 'C'),  # 山田がデータベースでC
    (3, 3, 'B')   # 佐藤が統計学でB
}

テーブル:Students(学生)、Courses(科目)、Enrollments(履修)

  • 学生は、複数の履修登録をする(1対N)
  • 科目は、複数の履修情報にひもづく(1対N)

※急にER図を出しましたが、書式を知らなくても大丈夫です

4.1 集合の概念

集合とは、重複のない要素の集まりです。

記法:

  • A = {1, 2, 3, 4, 5}
  • B = {4, 5, 6, 7, 8}
  • Students = {(1, '田中', 19, 'active'), (2, '山田', 24, 'inactive'), (3, '佐藤', 20, 'pending')}

4.1.1 重要な性質

4.2 集合の概念とテーブルの関係

データベースのテーブルを集合として捉えてみましょう。たとえば、Students テーブルを考えると、各行(レコード)は1人1人の学生を表します。テーブル全体は学生たちの集合になります。そう考えると、集合の重要な性質とピッタリ合います。

  • 重複しない:各学生の情報はユニークなIDで管理されている
  • 順序は関係ない:テーブルの行の並び方が変わっても、各学生の情報は識別できる
  • NULLはない(理想的には):集合の要素には「意味のある値」が必要

同じように、Courses テーブルは「科目の集合」、Enrollments テーブルは「履修情報の集合」として扱えます。
こう考えると、「データの整理整頓の仕方は集合と似ている」と感じませんか?👀

集合として扱うことで、テーブル設計のときに大切なポイントが見えてきます。
たとえば、新しいテーブルを追加するときも「このデータ、本当に重複しない?」とか「どんな属性(カラム)が必要?」と確認できます。結果的に、データの重複や無駄を防げるし、運用中のトラブルも減るはずです。

4.3 集合演算

集合の操作を覚えると、データベース設計やクエリを書くときに「こういうデータの組み合わせを取り出したい」がスムーズになります。ここでは基本的な集合演算を見ていきましょう。

  1. 和集合(Union):A ∪ B
    • 両方の集合の要素をすべて集めたもの
    • 例:A ∪ B = {1, 2, 3, 4, 5, 6, 7, 8}
  2. 積集合(Intersection):A ∩ B
    • 両方の集合に共通する要素のみを抜き出す
    • 例:A ∩ B = {4, 5}
  3. 差集合(Difference):A - B
    • Aのうち、Bに含まれないもの
    • 例:A - B = {1, 2, 3}

次に、少し特殊な演算です。
補集合や対称差、直積は実務で登場する場面が少ないと感じてますが、知っておくと活きる場面があるかもしれません。

  1. 補集合(Complement):A'
    • 全体集合からAを除いたもの
    • 例:全体集合U = {1, 2, 3, 4, 5, 6, 7, 8, 9}, A = {1, 2, 3, 4, 5} のとき、A' = {6, 7, 8, 9}
  2. 対称差(Symmetric Difference):A △ B
    • どちらか一方の集合にのみ含まれる要素
    • 例:A = {1, 2, 3, 4, 5}, B = {4, 5, 6, 7, 8} のとき、A △ B = {1, 2, 3, 6, 7, 8}
  3. べき集合(Power Set):P(A)
    • ある集合の全ての部分集合からなる集合
    • 例:A = {1, 2} のとき、P(A) = {∅, {1}, {2}, {1, 2}}

4.3.1 直積(Cartesian Product):A × B

2つの集合から可能な全ての組み合わせを作る演算です。

例:

  • {1, 2} × {a, b} = {(1,a), (1,b), (2,a), (2,b)}
  • A × B = {(1,4), (1,5), (1,6), (1,7), (1,8), (2,4), (2,5), (2,6), (2,7), (2,8), (3,4), (3,5), (3,6), (3,7), (3,8), (4,4), (4,5), (4,6), (4,7), (4,8), (5,4), (5,5), (5,6), (5,7), (5,8)}

リレーショナルデータベースでの活用

直積はデータベースの 結合(JOIN) でよく使われます。ただし、無条件の直積を使う場面は少なく、ほとんどの場合は条件付きでデータを絞り込みます。

-- 直積(全組み合わせ)
SELECT * FROM Students CROSS JOIN Courses;

-- 条件付きでデータを絞り込む例
SELECT * 
FROM Students s
JOIN Enrollments e ON s.student_id = e.student_id
JOIN Courses c ON e.course_id = c.course_id;

このように、直積に条件を付けて結合すると、実際に使えるデータを効率よく取り出せます。

5. リレーショナルモデルの基礎

関係代数 (関係モデル)

関係代数(かんけいだいすう、リレーショナル代数、: relational algebra)は、関係データベース関係モデル (リレーショナルモデル)において、集合論一階述語論理に基づいて、関係 (リレーション、、テーブル)として表現されたデータを扱う、コンピュータ科学における代数的な演算の体系である。

Oh, カタクルシイ…

語弊を恐れず解釈すると「データベースのテーブル間の関係を数学的な演算(結合・選択・射影など)で扱う方法」ですかね👀

重要な概念

  • 閉包性:関係代数の演算結果は常に新しい関係(テーブル)となります。
    これにより、演算を組み合わせた複雑なクエリが可能になります。
  • 完全性:関係代数は関係データベースに対して完全な操作能力を持ちます。
    つまり、必要なデータ操作は全て関係代数で表現可能です。

5.1 関係代数の基本演算

(SQLとの対応があると理解しやすいでしょうか?👀)

5.1.1 選択(SELECT):σ

条件に合う行(タプル)を選択する演算

σage>=20(Students)  -- 20歳以上の学生を選択

SQLでの表現:

SELECT * FROM Students WHERE age >= 20;

選択演算は、データベースから必要な行を抽出するための最も基本的な操作です。例えば、特定のステータスを持つ顧客を取得するケースです。他にも、特定の地域に住む顧客や、特定の日付に登録したユーザーを取得するなど、さまざまな条件に基づいて行を抽出することができます。

5.1.2 射影(PROJECT):π

指定した列(属性)のみを選択する演算

πname,age(Students)  -- 学生の名前と年齢のみを選択

SQLでの表現:

SELECT name, age FROM Students;

射影演算は、必要な属性(列)だけを抽出する際に使います。これにより、不要なデータを省略して効率的にデータを扱えます。

5.1.3 結合(JOIN):⋈

複数の関係(テーブル)を結合する演算

自然結合:共通の属性で結合

-- 仮にこのようなテーブル定義だった場合
CREATE TABLE Students (
    ...
    grade VARCHAR(2)  -- 学年を表すgrade
);
CREATE TABLE Enrollments (
    ...
    grade VARCHAR(2)  -- 成績を表すgrade
);

-- 自然結合を使用した場合(問題のある例)
SELECT * FROM Students NATURAL JOIN Enrollments;
-- これは両方のテーブルの'grade'カラムで結合してしまい、学年と成績が異なる意味を持つにも関わらず結合されてしまう。

-- 等結合を使用した場合(適切な例)
SELECT * FROM Students s
JOIN Enrollments e ON s.id = e.student_id;
-- 明示的に結合条件を指定することで、意図した結果が得られる

等結合:指定した条件で結合

Students ⋈ Enrollments ⋈ Courses  -- 学生、履修、科目情報を結合

SQLでの表現:

SELECT * FROM Students
JOIN Enrollments ON Students.id = Enrollments.student_id
JOIN Courses ON Enrollments.course_id = Courses.id;

:学生、履修、科目情報を結合することで、どの学生が何を履修してその結果どうであるかを把握できます。自然結合では、共通の属性名を持つ列を自動的に使用して結合しますが、共通の属性が明確でない場合、意図しない結果になることがあります。そのため、自然結合を使用する際には共通の属性が適切であることを確認する必要があります。

5.1.4 和(UNION):∪

2つの関係の和集合を求める演算

ActiveStudents ∪ InactiveStudents  -- すべての学生を取得

SQLでの表現:

SELECT * FROM Students WHERE status = 'active'
UNION
SELECT * FROM Students WHERE status = 'inactive';

UNION 演算は、2つのクエリ結果を統合し、重複を排除します。例えば、すべての学生を取得する際に使用します。重複を許容する場合は UNION ALL を使います。

5.1.5 差(DIFFERENCE):-

2つの関係の差集合を求める演算

Students - (σgrade='F'(π student_id(Enrollments)))  -- F評価のない学生

SQLでの表現:

SELECT * FROM Students
EXCEPT
SELECT * FROM Students
WHERE id IN (
    SELECT student_id
    FROM Enrollments
    WHERE grade = 'F'
);
-- こう書ける↓
SELECT * FROM Students
WHERE id NOT IN (
    SELECT student_id
    FROM Enrollments
    WHERE grade = 'F'
);

差演算は、特定の条件に合わないデータを除外したいときに使います。例えば、全学生からF評価のある学生を除外するケースです。

5.1.6 直積(PRODUCT):×

2つの関係の全ての組み合わせを生成

Students × Enrollments  -- 全ての学生と履修の組み合わせ

SQLでの表現:

SELECT * FROM Students CROSS JOIN Enrollments;

直積は、全ての学生と全ての科目を組み合わせるようなシナリオで使用しますが、実際には結合条件を付けた結合演算に発展させることが一般的です。例えば、学生とその履修を結合する場合、次のようなSQLを使用します:

SELECT *
FROM Students
JOIN Enrollments ON Students.student_id = Enrollments.student_id;

このように結合条件を明示的に指定することで、直積から必要な情報のみを抽出することができます。

5.1.7 除算(DIVISION):÷

ある関係に対して別の関係の全ての値と関連付けられているタプルを抽出

-- 全ての科目を履修している学生を取得
Enrollments ÷ Courses

SQLでの表現:

SELECT s.*
FROM Students s
WHERE s.id IN (
    SELECT e.student_id
    FROM Enrollments e
    GROUP BY e.student_id
    HAVING COUNT(DISTINCT e.course_id) = (SELECT COUNT(*) FROM Courses)
);

除算演算は、「全ての」という条件を扱う場合に使用します。例えば、「ある学生がすべてのコースを履修している」といったケースで活用できます。
SQLでは、除算演算を直接サポートしていないため、NOT EXISTSやHAVING句を使用して同様の結果を得ます。

5.1.8 交差(INTERSECTION):∩

2つの関係の共通部分を抽出する演算

-- 20歳以上でA評価がある学生
(σage>=20(Students)) ∩ (σgrade='A'(π student_id(Enrollments)))

SQLでの表現:

SELECT * FROM Students WHERE age >= 20
INTERSECT
SELECT s.* FROM Students s
JOIN Enrollments e ON s.id = e.student_id
WHERE e.grade = 'A';

-- こう書ける↓
SELECT * FROM Students
WHERE age >= 20
AND id IN (
    SELECT student_id
    FROM Enrollments
    WHERE grade = 'A'
);

交差演算は、両方の条件を満たすデータを抽出する際に使用します。例えば、プレミアム会員でかつアクティブな顧客を特定するケースです。

5.2 制約と整合性

5.2.1 実体整合性制約

各テーブルの主キーに関する制約

  • 必ずユニーク
  • NULL値は許容されない
  • 最小である:スーパーキーの部分集合でない(今回、この説明は省略します)
  • 複合主キーも可能

:学生テーブルの student_id を主キーとして設定することで、各学生が一意に識別されます。

CREATE TABLE Students (
    student_id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

Enrollments テーブルでは、student_idcourse_id の組み合わせを主キーとすることで、特定の学生が特定の科目に一度しか登録できないようにすることができます。

CREATE TABLE Enrollments (
    student_id INT,
    course_id INT,
    PRIMARY KEY (student_id, course_id)
);

5.2.2 参照整合性制約

外部キーに関する制約

  • 参照先のテーブルに存在する値のみ許容
  • 参照先の変更・削除時の動作を定義可能
    • CASCADE:参照先が削除された場合、その関連データも削除(⚠️不意に大量のデータが削除されるリスクがあります。そのため、利用する際にはデータの依存関係を十分に理解しておく必要があります。)
    • SET NULL:参照先が削除された場合、関連する外部キーをNULLに設定(⚠️NULL値を設定することで整合性が崩れる可能性があるため、NULLを許容する列に対してのみ使用することが推奨されます。)
    • RESTRICT:参照先の削除を制限(⚠️データの削除が制限されるため、適切なタイミングでの削除計画が必要です。 )

実装例:

CREATE TABLE Enrollments (
    student_id INT,
    course_id INT,
    grade VARCHAR(2),
    FOREIGN KEY (student_id) REFERENCES Students(id)
        ON DELETE CASCADE,  -- 学生が退学した場合、履修記録も自動的に削除
    FOREIGN KEY (course_id) REFERENCES Courses(id)
        ON DELETE RESTRICT  -- 履修記録があるかぎり、科目の削除は不可
);

5.2.3 ドメイン制約

各列(属性)の値に関する制約

  • CHECK制約:特定の条件を満たす必要がある
  • DEFAULT値:指定しない場合の初期値
  • NOT NULL制約:NULL値を許容しない

実装例:

CREATE TABLE Students (
    id INT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,  -- 名前は必ず存在するよう条件を指定
    age INT CHECK (age >= 0),  -- 年齢が0以上になるよう条件を指定
    status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended'))  -- ステータスが特定の値になるよう条件を指定
    ...
);

CREATE TABLE Courses (
    id INT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,  -- 科目名が必ず存在するよう条件を指定
    credits INT CHECK (credits > 0),  -- 単位が0以上になるよう条件を指定
    ...
);

CREATE TABLE Enrollments (
    student_id INT,
    course_id INT,
    grade VARCHAR(2) CHECK (grade IN ('A+','A','B','C','D','F')),  -- 有効な成績のみ許可
    enrollment_date DATE CHECK (enrollment_date <= CURRENT_DATE),  -- 未来の日付を防止
    ...
);

6. 演習問題

6.1 集合演算

以下の集合について、各演算の結果を求めてください。

A = {1, 2, 3, 4}
B = {3, 4, 5, 6}
  1. A ∪ B = ?(和集合)
  2. A ∩ B = ?(積集合)
  3. A - B = ?(差集合)
  4. A × {x, y} = ?(直積)
解答
  • A ∪ B = {1, 2, 3, 4, 5, 6}
    • 例えば、ユーザーAとユーザーBが購入したすべての商品をリストアップするときに使います。
  • A ∩ B = {3, 4}
    • 例えば、両方のユーザーが購入した共通の商品を特定するときに役立ちます。
  • A - B = {1, 2}
    • 例えば、両ユーザーが似ている購入履歴を持つときに、Bは購入しているけどAがまだ購入していない商品を特定できます。これにより、Aに対する販促キャンペーンの対象商品を絞り込むことができます。
  • A × {x, y} = {(1,x), (1,y), (2,x), (2,y), (3,x), (3,y), (4,x), (4,y)}
    • 例えば、ある商品のリストAと、在庫がある倉庫のリスト{x, y}を直積で組み合わせることで、各商品がどの倉庫に在庫として存在するかの全リストを生成できます。この結果を基に、特定の商品をどの倉庫から発送するかを決めることが可能です。

6.2 関係代数

以下のテーブル構造に対する関係代数式を書いてください。

Students(id, name, age)
Courses(id, title, credits)
Enrollments(student_id, course_id, grade)
  1. 20歳以上の学生の名前を取得
  • 例えば、年齢制限があるキャンペーン対象者を抽出するとき。
  1. 3単位以上の科目に登録している学生の一覧を取得
  • 例えば、卒業要件に関係する履修情報を確認するとき。
解答

20歳以上の学生の名前を取得:

πname(σage≥20(Students))

3単位以上の科目に登録している学生の一覧:

πStudents.name(
  σCredits≥3(
    Students ⋈ Enrollments ⋈ Courses
  )
)

または

πStudents.*(
  Students ⋈
  (Enrollments ⋈
    σcredits≥3(Courses)
  )
)

両方とも正解です。二つ目の方が、結合の順序が明確で実際のクエリ最適化の観点からは好ましいかもしれません。

これらは以下のSQLに相当します:

-- 20歳以上の学生
SELECT name
FROM Students
WHERE age >= 20;

-- 3単位以上の科目登録者
SELECT DISTINCT s.*
FROM Students s
JOIN Enrollments e ON s.id = e.student_id
JOIN Courses c ON e.course_id = c.id
WHERE c.credits >= 3;

7. 補足と余談(と雑感)

特に新しいことは書いてないので、読み飛ばしてもらってOKです🙆‍♂️

7.1 リレーショナルモデルとSQL

リレーショナルモデルとSQLの関係は、「理論と実用」の関係に似ています。

  • 数学の集合論:理論的な基盤
  • リレーショナルモデル:集合論を応用したデータ操作の定義
  • SQL:それを現実に操作するための道具

例えば、選択演算(σ)は SQL では WHERE句に、射影演算(π)は SELECT句に当たります。

7.2 集合の性質とRDBMS/SQL

集合の重要な性質がありました。
DBのレコードを見ると、「あれ、集合と違う?」と思いました👀

  • 重複:集合にはないけど、RDBではDISTINCTやUNION ALLなどで制御。
  • NULL:集合には「空」はあっても「不明」はありません。
  • 順序:集合では無視されるけど、RDBでは ORDER BY があります。

RDBMSは、集合論やリレーショナルモデルを実用性を高めるために拡張したようです。でも、その分、設計時に意識すべき点が増えました。

拡張というのは、一種の「無理をしている」とも言えます。例えば、重複を許すために DISTINCT で重複を排除したり、UNION と UNION ALL のように重複の扱いを切り替える構文が用意されています。これらは集合論では見かけない工夫です。
また、ORDER BY でデータを並び替える処理が重たくなりがちなのも、集合論にはない「順序」という拡張が加わった結果だと考えると納得感があります。
さらに、NULL の存在も DB 設計を複雑にする要因です。この「不明」を避けるだけでも設計がシンプルになりますね。

このあたりは、書籍「失敗から学ぶRDBの正しい歩き方 (Software Design plus)」の6.2節でも取り上げられていました。また、「理論から学ぶデータベース実践入門」も併せて読むと理解が深まる(らしい。私まだです。)ので、ぜひ参考にしてください!

7.3 RDBMSによる集合論の拡張(NULL、重複、順序)とその影響

RDBMSは実用性を高めるために、集合論にはない要素を導入しています。その代表例が NULL重複順序です。これらは便利な機能を提供する一方で、新たな課題も生んでいます。

NULLの影響

NULL は「値が存在しない」だけでなく、「値が不明」を表します。この曖昧さが論理演算を複雑にし、思わぬ挙動を引き起こします。
たとえば:

NULL = NULLfalseになる
NULL は「不明」であるため、どんな値とも等しいとは言えません。これに驚いた経験のある方も多いのではないでしょうか?(私はハマった記憶が💦)

パフォーマンスへの影響
インデックスやJOIN条件の最適化が難しくなる要因です。たとえば、NULL を含むカラムを基準に検索すると、パフォーマンスが低下しやすくなります。

重複の取り扱い

RDBMSは重複データを許容しています。これにより、同じ顧客が複数の購入履歴を持つようなシナリオを自然に表現できますが、一方でデータの整合性を保つための追加の工夫が必要です。

例:DISTINCT を使って重複を排除するクエリは便利ですが、パフォーマンスが低下する場合があります。また、誤って不要な重複を許してしまうと、後からのデータクレンジングが大変です。(と、データエンジニアの人が困っていたのを見たことがあります👀 設計ミスです、すんません。)

順序の取り扱い

集合論には「順序」の概念がありませんが、RDBMSでは ORDER BY を用いてデータの並び替えが可能です。この機能は非常に便利ですが、特に大規模データでは負荷が高くなります。

例:売上データを「金額の降順」で並べ替える場合、適切なインデックスがないとクエリが遅くなり、場合によってはシステム全体に影響を与えることもあります。

8. 参考情報

関係代数、リレーショナルモデルに関する解説

https://e-words.jp/w/関係モデル.html
https://ja.wikipedia.org/wiki/関係代数_(関係モデル)

参考にした書籍

9. さいごに

理論の理解はすぐに目に見える成果にならないかもです。ですが、基盤としての「データとは何か」「リレーショナルモデルとは何か」を把握することで、設計や調査、トラブルシューティングの際に「どこから手を付ければよいか」「どの仮説が正しそうか」をより明確にできると思います。

私のモヤモヤが晴れますように。
そして、みなさんの知識になりますように。

コミューン株式会社

Discussion