使用空列创建唯一约束


251

我有一个具有这种布局的表:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)

我想创建一个类似于以下的唯一约束:

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);

但是,这将允许多行具有相同的(UserId, RecipeId)if MenuId IS NULL。我想允许NULLMenuId存储不具有关联菜单中的最爱,但我只希望每个用户/食谱对这些行中最多只有一个。

到目前为止,我的想法是:

  1. 使用一些硬编码的UUID(例如全零)而不是null。
    但是,MenuId每个用户的菜单都有FK约束,因此我不得不为每个用户创建一个特殊的“空”菜单,这很麻烦。

  2. 而是使用触发器检查是否存在空条目。
    我认为这很麻烦,我喜欢尽可能避免触发事件。另外,我不信任他们以确保我的数据永远不会处于错误状态。

  3. 只需忘记它,然后检查中间件或插入函数中以前是否存在空条目,并且没有此约束。

我正在使用Postgres 9.0。

我有什么方法可以忽略吗?


为什么要允许多行具有相同的(UserIdRecipeId),如果MenuId IS NULL
Drux

Answers:


382

创建两个部分索引

CREATE UNIQUE INDEX favo_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)
WHERE menu_id IS NOT NULL;

CREATE UNIQUE INDEX favo_2col_uni_idx ON favorites (user_id, recipe_id)
WHERE menu_id IS NULL;

这样,只能存在(user_id, recipe_id)where的一种组合menu_id IS NULL,从而有效地实现所需的约束。

可能的缺点:您不能有外键引用(user_id, menu_id, recipe_id),不能CLUSTER基于部分索引,并且没有匹配WHERE条件的查询不能使用部分索引。(似乎不太可能希望FK引用宽三列,而应使用PK列)。

如果您需要一个完整的索引,则可以从中删除WHERE条件,favo_3col_uni_idx并且仍然可以执行您的要求。
现在组成整个表的索引与另一个表重叠,并且变得更大。根据典型的查询和NULL值的百分比,这可能有用也可能没有用。在极端情况下,甚至可以帮助维护所有三个索引(两个部分索引,总和在最前面)。

另外:我建议不要在PostgreSQL中使用大小写混合的标识符


1
@Erwin Brandsetter:关于“ 大小写混合的标识符 ”备注:只要不使用双引号,使用大小写混合的标识符绝对可以。使用所有小写标识符没有区别(再次:在不使用引号的情况下)
a_horse_with_no_name 2011年

14
@a_horse_with_no_name:我想你知道我知道。这实际上是我建议不要使用它的原因之一。不太了解具体细节的人会感到困惑,因为在其他RDBMS标识符中,(部分)区分大小写。有时人们会混淆自己。或者他们构建动态SQL并按原样使用quote_ident(),而现在忘记将标识符作为小写字符串传递!如果可以避免,请勿在PostgreSQL中使用大小写混合的标识符。我在这里看到了一些愚蠢的迫切要求。
Erwin Brandstetter

3
@a_horse_with_no_name:是的,当然是这样。但是,如果可以避免使用它们:您不需要混合大小写的标识符。他们没有任何目的。如果可以避免,请不要使用它们。此外:它们只是丑陋的。带引号的标识也很难看。其中带有空格的SQL92标识符是委员会的错误选择。不要使用它们。
wildplasser

2
@Mike:我认为您必须与SQL标准委员会谈谈,祝您好运:)
mu太短了,

1
@buffer:维护成本和总存储量基本相同(每个索引的固定开销较小)。每行仅在一个索引中表示。效果:如果您的结果涵盖了两种情况,则可能需要支付额外的总普通指数。如果不是,则部分索引通常比完整索引快,这主要是由于较小的大小。如果Postgres不知道它可以单独使用部分索引,则将索引条件添加到查询中(冗余)。例。
Erwin Brandstetter 2014年

75

您可以在MenuId上合并一个唯一索引:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

您只需要为COALESCE选择一个UUID,就不会在“现实生活”中发生这种情况。您可能在现实生活中可能永远不会看到UUID为零,但是如果您偏执,则可以添加CHECK约束(并且因为它们确实可以帮助您...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')

1
这带有(理论上的)缺陷,即menu_id ='00000000-0000-0000-0000-0000-000000000000'的条目会触发错误的唯一违规-但您已经在注释中解决了这一问题。
Erwin Brandstetter

2
@muistooshort:是的,这是一个适当的解决方案。简化为(MenuId <> '00000000-0000-0000-0000-000000000000')NULL默认是允许的。顺便说一句,有三种人。偏执狂的人,以及那些不做数据库的人。第三种偶尔会在困惑中提出关于SO的问题。;)
Erwin Brandstetter

2
@Erwin:您不是说“偏执狂的人和数据库破损的人”吗?
亩太短了

2
这种出色的解决方案非常容易在唯一约束中包含更简单类型(例如整数)的空列。
Markus Pscheidt

2
确实,UUID不会提出该特定字符串,这不仅是因为所涉及的概率,而且还因为它不是有效的UUID。UUID生成器不能随意在任何位置使用任何十六进制数字,例如,为UUID的版本号保留了一个位置。
Toby 1 Kenobi

1

您可以将没有关联菜单的收藏夹存储在单独的表中:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)

一个有趣的想法。这使插入变得更加复杂。我首先需要检查行是否已经存在FavoriteWithoutMenu。如果是这样,我只添加菜单链接-否则,我FavoriteWithoutMenu首先创建该行,然后在必要时将其链接到菜单。这也使得在一个查询中选择所有收藏夹变得非常困难:我不得不做一些奇怪的事情,例如先选择所有菜单链接,然后选择ID在第一个查询中不存在的所有收藏夹。我不确定我是否喜欢那样。
Mike Christensen

我认为插入没有那么复杂。如果要使用插入记录NULL MenuId,请插入到此表中。如果没有,到Favorites桌子上。但是查询,是的,它将更加复杂。
ypercubeᵀᴹ

实际上,选择所有收藏夹只是一次向左联接即可获得菜单。嗯,是啊,这可能是要走的路..
迈克·克里斯滕森

如果要将同一配方添加到多个菜单中,则INSERT变得更加复杂,因为您对FavoriteWithoutMenu的UserId / RecipeId具有UNIQUE约束。仅当该行尚不存在时,才需要创建该行。
Mike Christensen

1
谢谢!这个答案值得+1,因为它更像是跨数据库的纯SQL内容。.但是,在这种情况下,我将使用部分索引路由,因为它不需要更改我的架构,而且我喜欢它:)
Mike Christensen

-1

我认为这里存在语义问题。在我看来,用户可以使用(但只有一个)喜欢的食谱来准备特定菜单。(OP中混合了菜单和配方;如果我错了:请在下面互换MenuId和RecipeId)这意味着{user,menu}应该是此表中的唯一键。它应该指向一个配方。如果用户没有此特定菜单的收藏夹食谱,则此{user,menu}键对将不存在任何行。另外:代理键(FaVouRiteId)是多余的:复合主键对于关系映射表完全有效。

这将导致表定义减少:

CREATE TABLE Favorites
( UserId uuid NOT NULL REFERENCES users(id)
, MenuId uuid NOT NULL REFERENCES menus(id)
, RecipeId uuid NOT NULL REFERENCES recipes(id)
, PRIMARY KEY (UserId, MenuId)
);

2
是的,这是正确的。除此以外,在我的情况下,我想支持拥有不属于任何菜单的收藏夹。想象它像您在浏览器中的书签。您可能只是在页面上添加“书签”。或者,您可以创建书签的子文件夹并为它们命名不同的名称。我想允许用户收藏食谱,或创建收藏夹的子文件夹,即菜单。
Mike Christensen

1
正如我所说:这全都与语义有关。(显然,我当时在考虑食物)拥有“不属于任何菜单”的最爱对我来说毫无意义。您不能偏爱不存在的事物,恕我直言。
wildplasser 2011年

似乎某些数据库规范化可能会有所帮助。创建另一个将配方与菜单相关(或不与菜单相关)的表。尽管它使问题泛化,并允许一个食谱包含多个菜单。无论如何,问题是关于PostgreSQL中的唯一索引。谢谢。
克里斯(Chris)
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.