使用空列创建唯一约束

Mik*_*sen 219 sql postgresql null database-design referential-integrity

我有一个这种布局的表:

CREATE TABLE Favorites
(
  FavoriteId uuid NOT NULL PRIMARY KEY,
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  MenuId uuid
)
Run Code Online (Sandbox Code Playgroud)

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

ALTER TABLE Favorites
ADD CONSTRAINT Favorites_UniqueFavorite UNIQUE(UserId, MenuId, RecipeId);
Run Code Online (Sandbox Code Playgroud)

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

我到目前为止的想法是:

  1. 使用一些硬编码的UUID(例如全零)而不是null.
    但是,MenuId每个用户的菜单都有一个FK约束,所以我必须为每个用户创建一个特殊的"空"菜单,这是一个麻烦.

  2. 使用触发器检查是否存在空条目.
    我认为这是一个麻烦,我喜欢尽可能避免触发器.另外,我不相信他们能保证我的数据永远不会处于不良状态.

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

我正在使用Postgres 9.0.

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

Erw*_*ter 339

创建两个部分索引:

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;
Run Code Online (Sandbox Code Playgroud)

通过这种方式,只能有一个组合(user_id, recipe_id),其中menu_id IS NULL,有效地实现所需的约束.

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

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

旁白:我建议不要在PostgreSQL中使用混合大小写标识符.

  • @a_horse_with_no_name:我猜你知道我知道的.这实际上是我建议**反对**使用的原因之一.不熟悉这些细节的人会感到困惑,因为其他RDBMS标识符(部分)区分大小写.有时人们会迷惑自己.或者他们构建*动态SQL*并使用*quote_ident()*,因为他们应该忘记将标识符作为小写字符串传递!如果可以避免,请不要在PostgreSQL中使用混合大小写标识符.我在这里看到了一些源于这种愚蠢的绝望请求. (10认同)
  • 对于非空情况,我们真的需要在第一个索引中使用 `WHERE menu_id IS NOT NULL;` 吗?不只是`CREATE UNIQUE INDEX favorites_3col_uni_idx ON favorites (user_id, menu_id, recipe_id)` 是一回事吗? (4认同)
  • @Toby1Kenobi:拉丁复数是。但英语复数形式更为常见。 (4认同)
  • @a_horse_with_no_name:是的,这当然是真的.但是如果你可以避免它们:*你不需要混合大小写标识符*.他们没有任何目的.如果你能避免它们:不要使用它们.此外:他们只是丑陋.引用的标识也很难看.带有空格的SQL92标识符是委员会制造的失误.不要使用它们. (3认同)
  • @MarcusJuniusBrutus:这是一个可能的选择,仍然强制执行部分唯一性。不过,这*不*是一回事,因为索引覆盖整个表,因此更大。根据数据分布和要求,这可能是一个好主意,也可能不是。甚至有可能所有三种变体都达到了它们的目的。 (3认同)
  • @Mike:我想你必须和SQL标准委员会谈谈,祝你好运:) (2认同)
  • 是否可以使用多个唯一的部分索引“冲突时插入更新”?据我所知,只能指定一个冲突目标。 (2认同)

mu *_*ort 62

您可以在MenuId上创建一个带有合并的唯一索引:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);
Run Code Online (Sandbox Code Playgroud)

你只需要为COALESCE选择一个永远不会出现在现实生活中的UUID.在现实生活中你可能永远不会看到零UUID,但如果你是偏执狂,你可以添加一个CHECK约束(因为它们真的是为了让你......):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')
Run Code Online (Sandbox Code Playgroud)

  • 这个想法更简单,并且消除了需要 n^2 部分索引的多个可空字段的组合问题。这应该是公认的答案。 (6认同)
  • 这带有(理论上的)缺陷,即 menu_id = '00000000-0000-0000-0000-000000000000' 的条目可能会触发错误的唯一违规 - 但您已经在评论中解决了这个问题。 (2认同)
  • 我认为现有的任何 UUID 生成算法都不会得出该 UUID :) 非常有创意的解决方案。 (2认同)
  • @Erwin:是的,每个基于哨兵的解决方案都会遇到这个问题,UUID 环境是我认为足够安全的少数几个地方之一。如果你想变得偏执(强烈推荐),那么可以添加`CHECK(MenuId为空或MenuId &lt;&gt;'00000000-0000-0000-0000-000000000000')`。 (2认同)
  • @muistooshort:是的,这是一个合适的解决方案.简化为`(MenuId <>'00000000-0000-0000-0000-000000000000')`.默认情况下允许"NULL".顺便说一下,有三种人.偏执狂的人,以及不做数据库的人.第三种是偶尔在困惑中发布有关SO的问题.;) (2认同)
  • @Erwin:你的意思是"偏执狂和数据库损坏的人"吗? (2认同)
  • 这导致了类型 3 的困惑。:) (2认同)
  • 这种出色的解决方案使得在唯一约束中包含更简单类型的空列(例如整数)非常容易. (2认同)
  • 确实,UUID不会提出该特定字符串,这不仅是因为涉及的概率,而且还因为它不是有效的UUID *。UUID生成器不能随意在任何位置使用任何十六进制数字,例如,为UUID的版本号保留了一个位置。 (2认同)