299

我有一张这样布局的桌子:

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。

有什么我忽略的方法吗?

4

4 回答 4

459

创建两个部分索引

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)。(您似乎不太可能想要三列宽的 FK 引用 - 请改用 PK 列!)
  • 您不能CLUSTER基于部分索引。
  • 没有匹配WHERE条件的查询不能使用部分索引。

如果您需要一个完整的索引,您也可以删除WHERE条件 fromfavo_3col_uni_idx并且仍然强制执行您的要求。
现在包含整个表的索引与另一个表重叠并变大。根据典型的查询和NULL值的百分比,这可能有用也可能没用。在极端情况下,它甚至可能有助于维护所有三个索引(两个部分索引和顶部的总索引)。

对于单个可为空的列,可能是两个,这是一个很好的解决方案。但是它很快就会失控,因为您需要为每个可为空列的组合使用单独的部分索引,因此该数字呈二项式增长。对于多个可为空的列,请参阅:

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

于 2011-11-27T21:34:56.823 回答
98

您可以在 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')
于 2011-11-27T21:45:09.117 回答
2

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

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
于 2011-11-27T21:29:51.800 回答
-2

我认为这里存在语义问题。在我看来,用户可以有一个(但只有一个)最喜欢的食谱来准备一个特定的菜单。(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)
);
于 2011-11-27T23:12:22.703 回答