PostgreSQLでテーブルの継承

PostgreSQLには、テーブルの継承なんていう、とんがった機能があります。前衛的〜。

スキーマを定義してみる

-- オブジェクトテーブル
CREATE TABLE "object" (
	"object_id" SERIAL PRIMARY KEY,
	"name" VARCHAR(32) NOT NULL
);
--NOTICE:  CREATE TABLE will create implicit sequence "object_object_id_seq" for serial column "object.object_id"
--NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "object_pkey" for table "object"
--CREATE TABLE

-- ヒトテーブル
CREATE TABLE "human" (
	"height" INT,
	"weight" INT,
	PRIMARY KEY ("object_id")
) INHERITS ("object");
--NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "human_pkey" for table "human"
--CREATE TABLE

-- 製品テーブル
CREATE TABLE "product" (
	"price" INT,
	PRIMARY KEY ("object_id")
) INHERITS ("object");
--NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "product_pkey" for table "product"
--CREATE TABLE

継承元のCHECK制約, NOT NULL制約, DEFAULT値は引き継がれますが、UNIQUE制約, FOREIGN KEY制約, PRIMARY KEY制約は継承されないので、継承先であらためて指定する必要があります。

データを投入してみる

INSERT INTO "human" ("name", "height", "weight") VALUES ('山田太郎', 170, 65);
INSERT INTO "human" ("name", "height", "weight") VALUES ('田中次郎', 172, 70);
INSERT INTO "human" ("name", "height", "weight") VALUES ('佐藤花子', 159, 50);
INSERT INTO "product" ("name", "price") VALUES ('ホットコーヒー', 300);
INSERT INTO "product" ("name", "price") VALUES ('アイスコーヒー', 350);
INSERT INTO "product" ("name", "price") VALUES ('カフェオレ', 400);
INSERT INTO "product" ("name", "price") VALUES ('紅茶', 280);
INSERT INTO "product" ("name", "price") VALUES ('アイスティ', 330);
INSERT INTO "product" ("name", "price") VALUES ('ココア', 350);

そしてSELECTしてみる

# SELECT * FROM "human" ORDER BY "object_id";
 object_id |   name   | height | weight
-----------+----------+--------+--------
         1 | 山田太郎 |    170 |     65
         2 | 田中次郎 |    172 |     70
         3 | 佐藤花子 |    159 |     50
(3 rows)

# SELECT * FROM "product" ORDER BY "object_id";
 object_id |      name      | price
-----------+----------------+-------
         4 | ホットコーヒー |   300
         5 | アイスコーヒー |   350
         6 | カフェオレ     |   400
         7 | 紅茶           |   280
         8 | アイスティ     |   330
         9 | ココア         |   350
(6 rows)

# SELECT * FROM "object" ORDER BY "object_id";
 object_id |      name
-----------+----------------
         1 | 山田太郎
         2 | 田中次郎
         3 | 佐藤花子
         4 | ホットコーヒー
         5 | アイスコーヒー
         6 | カフェオレ
         7 | 紅茶
         8 | アイスティ
         9 | ココア
(9 rows)

他のRDBMSとの互換性は皆無ですが、上手く使えばとっても便利ですね。

これはできない?

ここで例えば、各object(ヒトと製品両方)のwebページが有るとして。そのページの閲覧ログをとりたいとします。
そこで、objectテーブルのobject_idを外部キーに指定して、こんな感じのテーブルを作ってみます。

-- 閲覧ログテーブル
CREATE TABLE "view_log" (
	"log_id" SERIAL PRIMARY KEY,
	"view_time" TIMESTAMP DEFAULT NOW(),
	"object_id" INT,
	FOREIGN KEY ("object_id")
		REFERENCES "object" ("object_id")
);
--NOTICE:  CREATE TABLE will create implicit sequence "view_log_log_id_seq" for serial column "view_log.log_id"
--NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "view_log_pkey" for table "view_log"
--CREATE TABLE

そこにデータ投入…。

-- 副問い合わせで、humanからobject_idを拾ってみる。
INSERT INTO "view_log" ("object_id")
	VALUES ((SELECT "object_id" FROM "human" WHERE "name" = '佐藤花子'));
--ERROR:  insert or update on table "view_log" violates foreign key constraint "view_log_object_id_fkey"
--DETAIL:  Key (object_id)=(3) is not present in table "object".

-- 副問い合わせで、objectからobject_idを拾ってみる。
INSERT INTO "view_log" ("object_id")
	VALUES ((SELECT "object_id" FROM "object" WHERE "name" = '佐藤花子'));
--ERROR:  insert or update on table "view_log" violates foreign key constraint "view_log_object_id_fkey"
--DETAIL:  Key (object_id)=(3) is not present in table "object".

-- んじゃ、ダイレクトにobject_idを投入してみる…。
INSERT INTO "view_log" ("object_id")
	VALUES (5); /* アイスコーヒーのobject_id */
--ERROR:  insert or update on table "view_log" violates foreign key constraint "view_log_object_id_fkey"
--DETAIL:  Key (object_id)=(5) is not present in table "object".

んーーー。SELECTしたら有るんだから良いじゃないですか…。objectテーブルからは見えるだけで、実体があるのはhumanテーブルやproductテーブルだから、無いんだ、って理屈なんでしょうけど…。

外部キーを外してしまうしか無いのでしょうか。