テーブルの継承について学ぶ
はじめに
データベース設計において、テーブルの継承は取り扱いが難しい内容です。とくに、異なる種類のデータを効率的に管理するためには、適切な継承戦略を選択することが求められます。
本記事では、単一テーブル継承、具象テーブル継承、クラステーブル継承の3つの主要な継承戦略について、それぞれのメリットとデメリットを比較しながら学びます。
また、実際の実装例を通じて、どのようにこれらの戦略を適用できるかをsqlc を使い、具体的に示します。
成果物
https://github.com/kntks/blog-code/tree/main/2024/12/database-polymorphism
環境
バージョン
バージョン | |
---|---|
Mac | Ventura 13.2.1 |
MySQL | 8.0.32 |
Docker | 26.0.0 |
Docker Compose | v2.24.5 |
Golang | 1.23.4 |
環境構築
はじめに、以下のコマンドで MySQL を起動します。
sqlc をインストールします。
以下のコマンドで sqlc.yaml
を作成します。
Getting started with MySQL を参考にして、sqlc.yaml
を編集します。
それぞれのディレクトリ(class_table_inheritance、concrete_table_inheritance、single_table_inheritance)を作成し、その配下に query.sql
と schema.sql
を作成します。
それぞれの query.sql
と schema.sql
の詳細は以下のURLを参照してください。
https://github.com/kntks/blog-code/tree/main/2024/12/database-polymorphism
課題
チケット管理システムを作るために、Tickets テーブルをデータベースに定義したと仮定します。チケットには種類があり、それぞれの種類に応じてカラムが異なります。たとえば、バグレポートの場合はバグの内容を記録するカラム(bug_report
)を必須にしたいと思いますが、その他の種類のチケットには不要です。このとき、bug_report
に NOT NULL
制約をつけたいです。しかし、他の種類のチケットには bug_report
カラムが必要ないため、NOT NULL
にする必要がありません。
このようなカラムが数個ならいいのですが、チケットの種類が増えたり、カラムが増えたりすると、テーブルのカラムが膨大になり管理が難しくなります。
このような状況でのテーブル設計には、以下の3つの戦略が知られています。
-
Single Table Inheritance(単一テーブル継承)
すべてのチケットタイプを1つのテーブルに格納し、タイプを識別するためのカラムを追加します。この方法はテーブル数が少なく、クエリが簡単ですが、NULL 値が増え、制約の適用が難しくなります。 -
Concrete Table Inheritance(具象テーブル継承)
各チケットタイプごとに独立したテーブルを作成します。この方法は NULL 値が減り、制約の適用が容易ですが、テーブル数が増え、設計が複雑になります。 -
Class Table Inheritance(クラステーブル継承)
共通カラムを持つ親テーブルと、各チケットタイプごとのサブテーブルを作成します。この方法は NULL 値が減り、スキーマの変更が容易ですが、JOINが増え、パフォーマンスに影響が出る可能性があります。
Single Table Inheritance (単一テーブル継承)
単一テーブル継承は、全タイプの属性を1つの個別の列に格納し、関連するすべてのサブタイプを1つのテーブルに格納します。
その列の中にどのサブタイプであるかを判定する列(type
など)を使用します。
先ほど課題にあげたテーブル設計は単一テーブル継承です。
メリット
単一テーブル継承のメリットは以下が挙げられます。
- テーブル数の削減:すべてのクラスを1つのテーブルで管理するため、テーブル数が少なくなり、データベースの構造がシンプルになる。
- クエリの簡素化:データ取得の際にジョイン操作が不要になるため、クエリが簡潔になり、パフォーマンスが向上する可能性がある。
- スキーマの統一性:1つのテーブルに統一されているため、スキーマの管理が容易になる。
デメリット
単一テーブル継承には、どの列がどのサブタイプに属するのかを定義するメタデータが存在しません。
そのため以下のようなデメリットが考えられます。
- NULL値の増加:サブタイプごとに特有のカラムが存在するため、他のサブクラスには不要なカラムとなり、NULL値が多く発生する
- 制約の適用が困難:サブタイプ特有の必須項目に対して、NOT NULL 制約などを個別に設定することが難しくなる
- テーブルの肥大化:すべての属性を含めるため、カラム数が増加し、テーブルが肥大化する
- データの整合性リスク:不適切なデータ入力を防ぐための制約やバリデーションが困難になり、データの整合性が損なわれる可能性がある
ちなみに GitLab では 単一テーブル継承 の設計は避けるようにしているそうです。
以下の引用(翻訳)でその理由を説明しています。
STIテーブルの新規作成は認めない。なぜなら、以下のような理由からである
- テーブルに非常に多くの行が含まれる原因となり、テーブルは小さく保つべきです。
- 追加のインデックスが必要となり、軽量ロックの使用が増加し、それが飽和するとインシデントを引き起こす可能性があります。
- データを特定の値でフィルタリングする必要があり、それが読み取り時のページアクセスを増やし、オーバーヘッドを発生させます。
- オブジェクトの正しいクラスを読み込むために class_name を使用しますが、クラス名を保存することはコストが高く、不必要です。
引用:https://docs.gitlab.com/ee/development/database/single_table_inheritance.html
そして、以下のような代替案を提案しています。
単一テーブル継承を使用する代わりに、以下の代替案を検討してください
- 各タイプごとに別々のテーブルを使用する。
- *_type カラムを追加するのは避ける。これはコードの悪臭(code smell)とみなされ、将来的に新しいタイプが追加される可能性が示唆されます。将来リファクタリングすることが非常に困難になるかもしれません。
- すでに *_type カラムで事実上STIを使用しているテーブルがある場合、以下を検討してください
- 既存のデータを複数のテーブルに分割する。
- 新しいタイプを新しいテーブルとして追加しつつ、既存のテーブルを維持できるようにリファクタリングする(例えば、基底クラスのロジックを共通モジュールや関心事(concern)に移動する)。
引用::https://docs.gitlab.com/ee/development/database/single_table_inheritance.html
実装例
単一テーブル継承は、全タイプの属性を1つの個別の列に格納するため、tickets
テーブルを以下のように定義します。
bug_report
と feature_request
はそれぞれのサブクラスに属するカラムです。
bug チケットまたは、featureRequest チケットをそれぞれ取得するには以下のように JOIN 句と WHERE 句を使います。
sqlc を使って、クエリをコンパイルし、コードを実行します。
詳しい実装については、こちら を参照してください。
Concrete Table Inheritance (具象テーブル継承)
具象テーブル継承は、サブタイプごとに独立したテーブルを作成し、それぞれのテーブルで専用のカラムや制約を管理する手法です。
すべての共通項目を基底クラス相当のテーブルに集約せず、必要に応じて重複したカラムをサブタイプ側に持たせます。
主に、すべてのサブタイプを跨いだ検索を行うことが少ない場合に適しています。
メリット
具象テーブル継承のメリットは以下が挙げられます。
- タイプカラムの削除:サブタイプごとにテーブルを分割するため、タイプカラムが不要になる
- NULLの削減:サブタイプごとにテーブルを分割するため、不要な NULL カラムが減る
- 制約の適用:個別のテーブルに特化した制約やバリデーションを適用しやすい
- 可読性の向上:サブタイプ独自の実装を分離でき、可読性が高まる
デメリット
具象テーブル継承のデメリットは以下が挙げられます。
- 設計が複雑になる:テーブル数が増え、スキーマ設計やマイグレーションが複雑になる
- 結合が増える:データをまとめて扱う場合は結合が増え、クエリが煩雑になる
- 共通カラム管理が難しくなる:すべてのタイプに共通するカラムを追加するとき、すべてのテーブルに影響が出る
実装例
具象テーブル継承は、サブタイプごとに独立したテーブルを作成するため、bug_tickets
テーブルと feature_tickets
テーブルを以下のように定義します。単一テーブル継承とは異なり、bug_report
と feature_request
はそれぞれのテーブルに属するカラムであり、タイプカラムを使用しません。
メリットにもあるように、具象テーブル継承は NULL 値が減り、制約の適用が容易です。
しかし、2つのチケットに共通するカラム(例:created_at
など)を追加する場合、すべてのテーブルに影響が出るため、管理が難しくなります。
bug チケット、featureRequest チケットはそれぞれ別のテーブルなので、データの取得はシンプルになります。
しかし、すべてのチケットを取得する際には、UNION
を使用して、複数のテーブルを結合する必要があります。
sqlc を使って、クエリをコンパイルし、コードを実行します。
詳しい実装については、こちら を参照してください。
Class Table Inheritance (クラステーブル継承)
クラステーブル継承は、データベース設計の一種で、オブジェクト指向プログラミングの継承概念をデータベースに適用したものです。この設計では、親クラスとサブクラスの関係をそれぞれ別々のテーブルに分割して管理します。
メリット
- NULLの削減:サブタイプごとにテーブルを分割するため、不要な NULL カラムが減る
- タイプカラムの削除:サブタイプごとにテーブルを分割するため、タイプカラムが不要になる
- 冗長カラムの削減:共通カラムを親テーブルに集約でき、冗長なカラムを減らせる
- スキーマの変更容易性:サブクラスを追加しやすく、スキーマの変更が比較的容易
- 変換が容易:データベースからアプリケーションのクラスにマッピングしやすい
デメリット
- JOINの増加:派生クラスごとに別のテーブルが増え、JOIN が多くなる
- パフォーマンス:パフォーマンスを考慮した最適なインデックス設計が難しくなる
- 保守性:テーブル分割により保守やマイグレーションが複雑化しやすい
実装例
具象テーブル継承と同じように、bug_report
と feature_request
はそれぞれのサブクラスに属するカラムで NOT NULL
制約をつけます。
チケットを取得する際には、INNER JOIN
を使用して、サブクラスのカラムを取得します。
sqlc を使って、クエリをコンパイルし、コードを実行します。
詳しい実装については、こちら を参照してください。