概要
PostgreSQL にはテーブルの継承という機能があります(PostgreSQL マニュアル:5.10. 継承)。これを用いることで、テーブル間の親子関係を定義できます。子テーブルは親テーブルの列定義を継承し、親テーブルは全ての子テーブルの行を参照できます。
テーブル継承の親テーブル/子テーブルは概念データモデルのスーパータイプ/サブタイプを思い起こさせられます。ですが、スーパータイプ/サブタイプの実装に親テーブル/子テーブルを使うのは難しいです。テーブル継承はパーティショニングのオプションの一つとしてとらえるのが良いでしょう。
PostgreSQL 10登場前のテーブルパーティショニングは、テーブル継承という形でのみ実行可能でした。テーブル継承はPostgreSQL 10以後も、宣言的パーティショニングでは力不足な状況において有用なパーティショニングオプションのひとつです。
PostgreSQL 10: テーブル継承と宣言的パーティショニングでスケーリングする(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
本記事ではテーブル継承をスーパータイプ/サブタイプの実装に用いると、どのような問題が出るかを見ていきます。
動作検証は PostgreSQL 13.2 で行いました。
テーブル継承とスーパータイプ/サブタイプの比較
概念データモデルのスーパータイプ/サブタイプについてはこの記事を参照ください。
概念データモデルのスーパータイプとサブタイプのパターン - ぱと隊長日誌
この記事で基本パターンとして挙げた、「排他的サブタイプ」と「共存的サブタイプ」を例にテーブル継承で実現可能かを見ていきます。
排他的サブタイプ
【概念データモデル】
【関係スキーマ】
【DDL】
CREATE TABLE company ( company_code char(4), company_name varchar(256), PRIMARY KEY (company_code) ); CREATE TABLE customer ( cut_rate int ) INHERITS (company); CREATE TABLE vendor ( payment_class char(1) ) INHERITS (company); CREATE TABLE item ( company_code char(4), item_code char(6), item_name varchar(256), PRIMARY KEY (company_code, item_code), FOREIGN KEY (company_code) REFERENCES company (company_code) );
排他的サブタイプであることを考えると、インスタンスは各サブタイプのいずれかにしか存在できません。ですが、この実装だと異なるサブタイプ間で取引先コードが重複したインスタンスを挿入できてしまいます。
また、親テーブルが外部キーの被参照側に設定されているとき、親テーブルの行の値のみチェックします。子テーブルの行の値はチェックしません。
この制限はマニュアルでも明記されています。
継承機能の重大な制限として、インデックス(一意性制約を含む)、および外部キーは、そのテーブルのみに適用され、それを継承した子テーブルには適用されないことがあります。 これは外部キーの参照側、被参照側の両方について当てはまります。
5.10. 継承
-- 子テーブルに登録する。 =# INSERT INTO customer VALUES ('0001', '株式会社A', 5); -- 他の子テーブルと取引先コードが重複しているのに登録できてしまう。 =# INSERT INTO vendor VALUES ('0001', '株式会社B', 'A'); -- 取引先コードに PRIMARY KEY を宣言した親テーブルでも、子テーブルに存在する取引先コードで登録できてしまう。 =# INSERT INTO company VALUES ('0001', '株式会社C'); =# SELECT tableoid::regclass, * FROM company WHERE company_code = '0001'; tableoid | company_code | company_name ----------+--------------+-------------- company | 0001 | 株式会社C customer | 0001 | 株式会社A vendor | 0001 | 株式会社B (3 rows)
-- 親テーブルに登録する。 =# INSERT INTO company VALUES ('0002', '株式会社D'); -- 子テーブルに登録する。 =# INSERT INTO customer VALUES ('0003', '株式会社E', 5); -- 親テーブルを SELECT すると、子テーブルの行も参照できる。 =# SELECT tableoid::regclass, * FROM company WHERE company_code IN ('0002', '0003'); tableoid | company_code | company_name ----------+--------------+-------------- company | 0002 | 株式会社D customer | 0003 | 株式会社E (2 rows) -- 参照先の親テーブルに値が存在していれば外部キーのチェックは成功する。 =# INSERT INTO item VALUES ('0002', '000001', '株式会社D-000001'); -- 子テーブルにしか値が存在しなければ外部キーのチェックは失敗する。 =# INSERT INTO item VALUES ('0003', '000001', '株式会社E-000001'); ERROR: insert or update on table "item" violates foreign key constraint "item_company_code_fkey" DETAIL: Key (company_code)=(0003) is not present in table "company".
共存的サブタイプ
【概念データモデル】
【関係スキーマ】
契約先(取引先コード, 契約ランク)
出荷先(取引先コード, 出荷先区分)
【DDL】
CREATE TABLE customer ( customer_code char(4), customer_name varchar(256), contractor_flag boolean, shipper_flag boolean, PRIMARY KEY (customer_code) ); CREATE TABLE contractor ( contractor_rank char(1) ) INHERITS (customer); CREATE TABLE shipper ( shipper_class char(1) ) INHERITS (customer);
テーブル継承で子テーブルは親テーブルの全ての列を継承します。共存的サブタイプでは子テーブルの行に共通する値を親テーブルで保持したいのですが、テーブル継承で実装すると子テーブルにて保持することになってしまいます。
-- 親テーブルに登録する。 =# INSERT INTO customer VALUES ('0001', '株式会社A', true, true); -- 親テーブルと子テーブルで矛盾する値を登録できてしまう。 =# INSERT INTO contractor VALUES ('0001', '株式会社B', true, false, 'A'); =# SELECT tableoid::regclass, * FROM customer; tableoid | customer_code | customer_name | contractor_flag | shipper_flag ------------+---------------+---------------+-----------------+-------------- customer | 0001 | 株式会社A | t | t contractor | 0001 | 株式会社B | t | f (2 rows)