数据库设计-具有共享标签的不同对象


8

我的背景更多是网络编程而不是数据库管理,所以如果我在这里使用错误的术语,请指正。我正在尝试找出为我要编写的应用程序设计数据库的最佳方法。

情况:我在一个表中有“报告”,在另一表中有“建议”。每个报告可以有许多建议。我还有一个单独的关键字表(用于实施标记)。但是,我只希望将一组关键字同时应用于“报告”和“建议”,以便搜索关键字可以将“报告”和“建议”作为结果。

这是我开始的结构:

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)

我应该采用第二种结构吗?我在这里错过任何重要的问题吗?另外,如果我确实使用第二种,那么应该使用什么作为非通用名称来替换“对象”?

更新:

我正在为此项目使用SQL Server。这是一个内部应用程序,具有少量非并发用户,因此我预计不会有很高的负载。在用法上,关键字可能会很少使用。它几乎仅用于统计报告目的。从这个意义上讲,无论我采用哪种解决方案,都可能只会影响需要维护该系统的所有开发人员……但是我认为,只要有可能,实施良好实践是一件好事。感谢您提供的所有见解!


看来您没有回答最重要的问题-如何访问数据?-您想针对哪些查询/陈述“调整”模型?-您打算如何扩展功能?我认为没有通用的最佳做法-解决方案取决于这些问题的答案。即使在像这样的简单模型中,它也开始变得重要。或者,您最终可能会遵循一些更高的原则,但实际上会吸纳最重要的场景(系统用户看到的那些场景)。
斯特凡Oravec

好点子!我得花点时间考虑一下!
matikin9 2013年

Answers:


6

第一个示例的问题是三链接表。是否需要报表或建议中的外键之一始终为NULL,以便关键字只能以一种或另一种方式链接?

在第二个示例中,根据您的操作方式,现在可能需要使用类型选择器或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 JOINs和COALESCE。当扩展选项2链接更多的东西时,您必须继续添加更多的LEFT JOIN,这些地方通常找不到东西(链接的对象只能有一个有效的派生表)。

我不认为您的选择2根本上没有错,您实际上可以通过使用视图使它看起来像此提案。

在您的选项1中,我很难理解您为什么选择了三链接表。


您提到的三链接表可能是我精神上懒惰的结果...:P在阅读了各种答案之后,我认为我的最初选择都没有道理。具有单独的独立ReportKeywords和RecommendationKeywords表更加实用。我正在考虑可伸缩性,因为可能会应用更多需要关键字的对象,但实际上,可能只有一种可能需要关键字的对象类型。
matikin9 2013年

4

首先,请注意,理想的解决方案在某种程度上取决于您使用的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并仅加入关键字文本。这样就消除了联接,而无需进行非规范化。


我正在为此项目使用MS SQL Server,但感谢PostgreSQL上的信息。关于删除和确保对象-关键字对每个仅出现一次的其他要点。即使我有每个对象-关键字对的关键字,在插入之前我是否仍需要检查?至于有一个单独的关键字id ...对于SQL Server,我读过它,字符串较长会降低性能,而且我可能不得不允许用户输入“关键短语”,而不仅仅是“关键字” ”。
matikin9 2013年

0

我建议两个单独的结构:

report_keywords
---------------
  报告编号
  关键字编号

Recommendation_keywords
-----------------------
  Recommendation_id
  keyword_id

这样一来,您就不会在同一张表中拥有所有可能的实体ID(这不是很容易扩展,并且可能会造成混淆),并且您也没有具有通用“对象ID”的表,因此您必须在其他地方消除歧义使用base_object表格,这将起作用,但我认为设计过于复杂。


我不同意您的建议是可行的选择,但是为什么OP的设计B无法强制执行RI?(我想这就是你的意思)。
ypercubeᵀᴹ

@ypercube:我想我BaseObjects在第一次通读时就错过了该表,并且以为我看到了一个表的描述,该描述object_id可以指向任何表中的ID 。
FrustratedWithFormsDesigner

-1

以我的经验,这是您可以做的。

Reports
----------
Report_id (primary_key)
Report_name

Recommendations
----------------
Recommendation_id (primary key)
Recommendation_name
Report_id (foreign key)

Keywords
----------
Keyword_id (primary key)
Keyword

对于关键字,报告和建议之间的关系,您可以执行以下两种选择之一:选项A:

Recommendation_keywords
------------------------
Recommendation_id(foreign_key)
keyword_id (foreign_key)

这样可以实现从报告到建议书,再到关键词,再到关键词的直接关系。选项B:

object_keywords
---------------
Object_id
Object_type
Keyword_id(foreign_key)

选项A更易于应用和管理,因为它将具有数据库约束来处理数据完整性,并且不允许插入无效数据。

选项B尽管需要做更多的工作,因为您将需要编码关系的标识。从长远来看,它更具灵活性,如果将来某个时候您需要将关键字添加到报表或建议以外的其他项目中,则只需添加标识并直接使用表格即可。


让我解释一下为什么我投票失败:1.目前尚不清楚您是否赞成选项A,B或第三种方法。(对我而言)您似乎都说两者都差不多(我不同意,因为A有其他人用他们的答案概述的几个问题。2.您是否建议对A(或B)的设计进行改进? ?还是不清楚。明确定义FK也将是一件好事,您的建议也不是很明显。总的来说,我喜欢答案,可以为以后的访客弄清楚事情和选择。请尝试编辑答案并我会扭转我的票。
ypercubeᵀᴹ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.