まず、理想的なソリューションは、使用するRDBMSにある程度依存することに注意してください。次に、標準的な回答とPostgreSQL固有の回答の両方を示します。
正規化された標準回答
標準的な答えは、2つの結合テーブルを持つことです。
テーブルがあるとします。
CREATE TABLE keywords (
kword text
);
CREATE TABLE reports (
id serial not null unique,
...
);
CREATE TABLE recommendations (
id serial not null unique,
...
);
CREATE TABLE report_keywords (
report_id int not null references reports(id),
keyword text not null references keyword(kword),
primary key (report_id, keyword)
);
CREATE TABLE recommendation_keywords (
recommendation_id int not null references recommendation(id),
keyword text not null references keyword(kword),
primary key (recommendation_id, keyword)
);
このアプローチは、すべての標準正規化ルールに従い、従来のデータベース正規化の原則に違反しません。どのRDBMSでも機能するはずです。
PostgreSQL固有の回答、N1NF設計
最初に、なぜPostgreSQLが異なるのかについて一言。PostgreSQLは、配列に対してインデックスを使用する非常に便利な方法をいくつかサポートしています。特に、GINインデックスと呼ばれるものを使用しています。これらをここで適切に使用すると、パフォーマンスが大幅に向上します。PostgreSQLはこの方法でデータ型に「到達」できるため、原子性と正規化の基本的な仮定は、ここに厳密に適用するにはやや問題があります。したがって、この理由から、私の推奨は、最初の正規形の原子性規則を破り、パフォーマンスを向上させるためにGINインデックスに依存することです。
ここで2番目の注意点は、これによりパフォーマンスは向上しますが、参照整合性を正しく機能させるために手動で行う必要があるため、いくつかの問題が追加されます。したがって、ここでのトレードオフは、手動作業のパフォーマンスです。
CREATE TABLE keyword (
kword text primary key
);
CREATE FUNCTION check_keywords(in_kwords text[]) RETURNS BOOL LANGUAGE SQL AS $$
WITH kwords AS ( SELECT array_agg(kword) as kwords FROM keyword),
empty AS (SELECT count(*) = 0 AS test FROM unnest($1))
SELECT bool_and(val = ANY(kwords.kwords))
FROM unnest($1) val
UNION
SELECT test FROM empty WHERE test;
$$;
CREATE TABLE reports (
id serial not null unique,
...
keywords text[]
);
CREATE TABLE recommendations (
id serial not null unique,
...
keywords text[]
);
次に、キーワードを適切に管理するためにトリガーを追加する必要があります。
CREATE OR REPLACE FUNCTION trigger_keyword_check() RETURNS TRIGGER
LANGUAGE PLPGSQL AS
$$
BEGIN
IF check_keywords(new.keywords) THEN RETURN NEW
ELSE RAISE EXCEPTION 'unknown keyword entered'
END IF;
END;
$$;
CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE TO reports
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();
CREATE CONSTRAINT TRIGGER check_keywords AFTER INSERT OR UPDATE
TO recommendations
WHEN (old.keywords <> new.keywords)
FOR EACH ROW EXECUTE PROCEDURE trigger_keyword_check();
次に、キーワードが削除されたときに何をするかを決定する必要があります。現在のところ、キーワードテーブルから削除されたキーワードは、キーワードフィールドにカスケードされません。多分これは望ましいことかもしれませんし、そうでないかもしれません。最も簡単なことは、削除を常に制限することであり、削除が発生した場合は手動でこのケースを処理することを期待します(ここでは安全のためにトリガーを使用します)。別のオプションは、キーワードが存在するすべてのキーワード値を書き換えて、それを削除することです。繰り返しになりますが、トリガーもその方法です。
このソリューションの大きな利点は、キーワードによる非常に高速な検索のためにインデックスを作成できることと、結合なしですべてのタグをプルできることです。欠点は、キーワードを削除するのは面倒であり、良い日でもうまく機能しないことです。これはまれなイベントであり、バックグラウンドプロセスに委託される可能性がありますが、理解する価値のあるトレードオフであるため、これは許容できる場合があります。
最初のソリューションを批評する
最初のソリューションの本当の問題は、ObjectKeywordsにキーがない可能性があることです。その結果、各キーワードが各オブジェクトに一度だけ適用されることを保証できないという問題があります。
2番目のソリューションは少し優れています。提供されている他のソリューションが気に入らない場合は、それを使用することをお勧めします。ただし、keyword_idを削除して、キーワードテキストに参加することをお勧めします。これにより、非正規化せずに結合が排除されます。