首先,请注意,理想的解决方案在某种程度上取决于您使用的RDBMS。然后,我将给出标准答案和PostgreSQL特定答案。
标准化,标准答案
标准答案是有两个联接表。
假设我们有表格:
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索引获得更好的性能。
这里的第二个注意事项是,尽管这样做可以提供更好的性能,但也增加了一些麻烦,因为您将需要做一些手工工作才能使参照完整性正常工作。因此,这里要权衡的是手动工作的性能。
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上没有可能的键。因此,您将遇到一个问题,即您不能保证每个关键字只能应用于每个对象一次。
您的第二个解决方案要好一些。如果您不喜欢所提供的其他解决方案,建议您使用它。但是,我建议您删除keyword_id并仅加入关键字文本。这样就消除了联接,而无需进行非规范化。