私の経歴は、データベース管理ではなくWebプログラミングにあります。したがって、ここで間違った用語を使用している場合は訂正してください。コーディングするアプリケーションのデータベースを設計する最良の方法を見つけようとしています。
状況:レポートが1つの表にあり、推奨事項が別の表にあります。各レポートには多くの推奨事項があります。キーワードの別のテーブルもあります(タグ付けを実装するため)。ただし、キーワードを検索すると結果としてレポートと推奨事項が表示されるように、レポートと推奨事項の両方に適用されるキーワードのセットを1つだけ用意したいと思います。
ここに私が始めた構造があります:
Reports
----------
ReportID
ReportName
Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)
Keywords
----------
KeywordID
KeywordName
ObjectKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)
RecommendationID (foreign key)
直感的には、これは最適ではなく、タグ付け可能なオブジェクトを共通の親から継承し、そのコメントの親にタグを付けると、次のような構造になります。
BaseObjects
----------
ObjectID (primary key)
ObjectType
Reports
----------
ObjectID_Report (foreign key)
ReportName
Recommendations
----------
ObjectID_Recommendation (foreign key)
RecommendationName
ObjectID_Report (foreign key)
Keywords
----------
KeywordID (primary key)
KeywordName
ObjectKeywords
----------
ObjectID (foreign key)
KeywordID (foreign key)
この2番目の構造を使用する必要がありますか?ここに重要な懸念事項がありませんか?また、2番目を使用する場合、「オブジェクト」を置き換えるために、総称以外の名前として何を使用すればよいですか?
更新:
このプロジェクトではSQL Serverを使用しています。これは、非同時ユーザー数が少ない内部アプリケーションなので、高負荷は予想されません。使用に関しては、キーワードは控えめに使用される可能性があります。これは、ほとんど統計レポートの目的のためだけです。その意味で、私が行った解決策はおそらく、このシステムを将来にわたって維持する必要のある開発者にのみ影響を及ぼします...しかし、可能な場合はいつでも適切な方法を実装することをお勧めします。すべての洞察をありがとう!
最初の例の問題は、トライリンクテーブルです。その場合、レポートまたは推奨のいずれかの外部キーの1つが常にNULLである必要があるため、キーワードはどちらか一方だけにリンクしますか?
2番目の例の場合、ベースから派生テーブルへの結合では、その方法に応じてタイプセレクターまたはLEFT JOINの使用が必要になる場合があります。
それでは、なぜそれを明示的にして、すべてのNULLとLEFT JOINを削除しないのですか?
Reports
----------
ReportID
ReportName
Recommendations
----------
RecommendationID
RecommendationName
ReportID (foreign key)
Keywords
----------
KeywordID
KeywordName
ReportKeywords
----------
KeywordID (foreign key)
ReportID (foreign key)
RecommendationKeywords
----------
KeywordID (foreign key)
RecommendationID (foreign key)
このシナリオでは、タグ付けする必要のある何かを追加する場合、エンティティテーブルとリンケージテーブルを追加するだけです。
その後、検索結果は次のようになります(単一の結果リストが必要な場合は、まだタイプ選択が行われていて、オブジェクト結果レベルでジェネリックに変換されます)。
SELECT CAST('REPORT' AS VARCHAR(15)) AS ResultType
,Reports.ReportID AS ObjectID
,Reports.ReportName AS ObjectName
FROM Keywords
INNER JOIN ReportKeywords
ON ReportKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Reports
ON Reports.ReportID = ReportKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'
UNION ALL
SELECT 'RECOMMENDATION' AS ResultType
,Recommendations.RecommendationID AS ObjectID
,Recommendations.RecommendationName AS ObjectName
FROM Keywords
INNER JOIN RecommendationKeywords
ON RecommendationKeywords.KeywordID = Keywords.KeywordID
INNER JOIN Recommendations
ON Recommendations.RecommendationID = RecommendationKeywords.ReportID
WHERE Keywords.KeywordName LIKE '%' + @SearchCriteria + '%'
とにかく、どこかで型の選択が行われ、何らかの分岐が行われます。
オプション1でこれを行う方法を見ると、これは似ていますが、CASEステートメントまたはLEFT JOINとCOALESCEを使用しています。リンクされるものを増やしてオプション2を拡張する場合、通常は物が見つからない場所にLEFT JOINを追加し続ける必要があります(リンクされるオブジェクトは、有効な派生テーブルを1つだけ持つことができます)。
オプション2に根本的な問題はないと思います。実際に、ビューを使用してこの提案のように見せることもできます。
オプション1では、なぜトライリンクテーブルを選択したのかわかりません。
まず、理想的なソリューションは、使用する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に可能なキーがないことです。その結果、各キーワードWordが各オブジェクトに1回だけ適用されることを保証できないという問題があります。
2番目のソリューションは少し優れています。提供されている他のソリューションが気に入らない場合は、それを使用することをお勧めします。ただし、keyword_idを削除して、キーワードテキストに参加することをお勧めします。これにより、非正規化せずに結合が排除されます。
私は2つの別々の構造を提案します:
report_keywords --------------- レポートID キーワードID recommendation_keywords ----------------------- recommendation_id keyword_id
この方法では、同じテーブルにすべての可能なエンティティIDがなく(非常にスケーラブルではなく、混乱を招く可能性があります)、他の場所を明確にする必要がある汎用の「オブジェクトID」を持つテーブルがありません。 base_object
テーブルは機能しますが、デザインが複雑すぎると思います。