2列中的任何一列总是多余的 - 有更好的解决方案吗?

时间:2017-03-03 09:42:11

标签: database postgresql database-design

说,我想创建一个反馈表单。如果注册用户提交反馈,则他的电子邮件地址会自动使用,因为他已经过身份验证。如果匿名用户这样做,他必须手动输入他的电子邮件。我的表看起来像这样:

feedbacks(id, user_id, email, body) 

如您所见,它有一个冗余列:user_id或email。对于那些不熟悉数据库结构的人来说,它会让人感到困惑:为什么要发送电子邮件和user_id?它们都可以为空吗?或者两者同时具有价值?实际上,只有其中一个必须具有一个值,这个值不可能使用约束在数据库级别上实现。另外,如果我错误地在两列中插入值,该怎么办?

因此,我想知道,有没有办法改变其结构,以便它更明智,上述问题已经解决了?使用触发器并不是我正在寻找的东西。

换句话说,问题是" 2列中的任何一列总是多余的"。

4 个答案:

答案 0 :(得分:2)

如果你有几个互斥列,那么你可能会对名为 entity sub-typing 的内容有一个很好的理由。实际上,没有好的设计理由可以添加这种设计模式的所有额外开销。

这些是您拥有的基本选项:

  1. 一个表中的两个互斥列 - 这是您当前的设计。这是一个很好的设计,因为它允许您在user_id上定义正确的外键约束。你提到对于那些不熟悉数据库的人来说可能会让人感到困惑,因为相同类型的信息可能出现在表格的一个或另一个地方。但是,重要的是要记住,即使两列都包含恰好是电子邮件地址形式的字符串,但对于您的系统而言,这些内容语义不同。一个是用户表的外键。另一种是联系(或识别?)非成员的手段。您可以通过以下两种方式之一避免这种明显的混淆:(a)为email列提供更具描述性的名称,例如non_member_email或(b)创建合并user_id的视图和email到一个列中,用于向可能会混淆的人显示此信息。

  2. 实体子类型 - 此方法可让您为逻辑上独立的谓词组(列)创建单独的表。它们通过超类型表连接在一起,它为所有逻辑子类型提供了一个公共主键,并保存了所有其他常见谓词。您可以谷歌四处了解有关此设计模式的更多信息。正如我已经提到的,这对你的情况来说是过度的,因为你只有一对相互排斥的列。如果您认为这样做会让人感到困惑,那么拥有三个表(超类型,成员子类型,非成员子类型)将会让您感到困惑。

  3. 列重载 - 此方法可以将两个列合并为一个列。这是可行的,因为您一次只需要一个电子邮件地址。这是一个糟糕的想法,因为它会阻止您在user_id上创建声明性引用约束,这是维护数据引用完整性的一个非常重要的工具。它还会混淆两个语义不同的信息,这违反了良好的数据库设计原则。

  4. 最佳选择是1号。不要担心有两个相互排斥的专栏,或者如果您认为自己无法评论"你可以通过更具描述性的列名来解决这种混乱,然后使用视图隐藏"复杂性"在两个单独的列中存储两个看起来相似的东西。

答案 1 :(得分:1)

如果必须填写一个:

create table feedbacks (
    id integer,
    user_id text,
    email text,
    body text,
    check ((user_id is null)::int + (email is null)::int = 1)
);

从布尔到整数的转换产生1或0,因此总和必须为1.

答案 2 :(得分:1)

删除电子邮件字段。如果用户已注册,请像现在一样输入他们的user_id。如果用户未注册,请在用户表中搜索具有该电子邮件地址的匿名条目。如果存在,请使用该user_id。否则,在名为' Anonymous'的用户表中创建一个条目,存储该地址并使用新创建的user_id。有两个好处:

  1. 您不需要互斥字段。正如您已经注意到的那样,这些可能是造成大量混乱和额外工作以保持数据清洁的原因。
  2. 如果匿名海报后来注册,现有的"匿名"用户条目可以更新,从而保留user_id值并保留在注册之前输入的所有反馈(以及您为匿名用户跟踪的任何其他活动)。也就是说,如果用户匿名输入一些反馈然后注册,则先前的反馈仍然与现在命名的用户相关联。

答案 3 :(得分:0)

我可能会误解这个问题,但为什么你说这是不可能的约束?...

t=# CREATE TABLE feedbacks (
t(#     id integer,
t(#     user_id text CHECK (case when email is null then user_id is distinct from null else user_id is null end),
t(#     email text CHECK (case when user_id is null then email is distinct from null else email is null end),
t(#     body text
t(# );
CREATE TABLE
t=# insert into feedbacks select 1,null,null,'t';
ERROR:  new row for relation "feedbacks" violates check constraint "feedbacks_check1"
DETAIL:  Failing row contains (1, null, null, t).
t=# insert into feedbacks select 1,'t','t','t';
ERROR:  new row for relation "feedbacks" violates check constraint "feedbacks_check1"
DETAIL:  Failing row contains (1, t, t, t).
t=# insert into feedbacks select 1,'t',null,'t';
INSERT 0 1
t=# insert into feedbacks select 1,null,'t','t';
INSERT 0 1
t=# select * from feedbacks ;
 id | user_id | email | body
----+---------+-------+------
  1 | t       |       | t
  1 |         | t     | t
(2 rows)