使用空列创建唯一约束

问题描述 投票:201回答:4

我有一个这种布局的表:

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),如果MenuId IS NULL。我想允许NULL中的MenuId存储没有相关菜单的收藏夹,但我最多只想要每个用户/配方对中的一行。

我到目前为止的想法是:

  1. 使用一些硬编码的UUID(例如全零)而不是null。 但是,MenuId对每个用户的菜单都有一个FK约束,所以我必须为每个用户创建一个特殊的“空”菜单,这是一个麻烦。
  2. 使用触发器检查是否存在空条目。 我认为这是一个麻烦,我喜欢尽可能避免触发器。另外,我不相信他们能保证我的数据永远不会处于不良状态。
  3. 只需忘记它并检查中间件或插入函数中是否存在空条目,并且没有此约束。

我正在使用Postgres 9.0。

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

sql postgresql database-design null referential-integrity
4个回答
329
投票

创建two partial indexes

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)只能有一个组合menu_id IS NULL,有效地实现了所需的约束。

可能的缺点:你不能有引用(user_id, menu_id, recipe_id)的外键,你不能将CLUSTER基于部分索引,而没有匹配的WHERE条件的查询不能使用部分索引。 (你似乎不太可能想要一个宽三列的FK参考 - 改用PK列)。

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

旁白:我建议不要使用mixed case identifiers in PostgreSQL


60
投票

您可以在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')

2
投票

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

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

0
投票

我认为这里存在语义问题。在我看来,用户可以拥有(但只有一个)最喜欢的食谱来准备特定的菜单。 (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)
);
© www.soinside.com 2019 - 2024. All rights reserved.