ChatGPT解决这个技术问题 Extra ChatGPT

使用空列创建唯一约束

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

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);

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

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

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

我正在使用 Postgres 9.0。有什么我忽略的方法吗?

如果 MenuId IS NULL,为什么允许多行具有相同的(UserIdRecipeId)?
@Drux 我相信自 Null != Null 以来,它遵循 (userid, recipieid, null) != (userid, recipieid, null)。因此,将允许看起来与我们相同但不等于 postgresql 的重复项。

E
Erwin Brandstetter

Postgres 15 或更新版本(目前为测试版)

Postgres 15 添加了子句 NULLS NOT DISTINCTThe release notes:

允许唯一约束和索引将 NULL 值视为不明确的 (Peter Eisentraut) 以前,NULL 值始终被索引为不同的值,但现在可以通过使用 UNIQUE NULLS NOT DISTINCT 创建约束和索引来更改这一点。

在此子句中,NULL 被视为只是另一个值,并且 UNIQUE constraint 不允许多行具有相同的 NULL 值。现在的任务很简单:

ALTER TABLE favorites
ADD CONSTRAINT favo_uni UNIQUE NULLS NOT DISTINCT (user_id, menu_id, recipe_id);

手册第 "Unique Constraints" 章中有示例。
该子句切换 所有 索引键的行为。您不能将 NULL 视为一个键相等,而另一个键则不相等。
NULLS DISTINCT 仍然是默认值(与标准 SQL 一致)并且不必拼写出来。

相同的子句也适用于 UNIQUE index

CREATE UNIQUE INDEX favo_uni_idx
ON favorites (user_id, menu_id, recipe_id) NULLS NOT DISTINCT;

注意新子句在关键字段之后的位置。

Postgres 14 岁或以上

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

可能的缺点:

您不能有外键引用(user_id、menu_id、recipe_id)。 (您似乎不太可能想要三列宽的 FK 引用 - 请改用 PK 列!)

您不能将 CLUSTER 基于部分索引。

没有匹配 WHERE 条件的查询不能使用部分索引。

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

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

为什么我的 UNIQUE 约束没有触发?

另外:我建议不要使用 mixed case identifiers in PostgreSQL


@a_horse_with_no_name:我假设你知道我知道这一点。这实际上是我建议不要使用它的原因之一。不太了解细节的人会感到困惑,因为在其他 RDBMS 标识符中(部分)区分大小写。有时人们会混淆自己。或者他们构建动态 SQL 并按照他们应该的方式使用 quote_ident() 并且现在忘记将标识符作为小写字符串传递!如果可以避免的话,不要在 PostgreSQL 中使用混合大小写的标识符。我在这里看到了许多来自这种愚蠢行为的绝望请求。
@a_horse_with_no_name:是的,这当然是真的。但是如果你能避免它们:你不想要混合大小写的标识符。他们没有任何目的。如果你可以避免它们:不要使用它们。此外:它们简直丑陋。引用的标识也很丑陋。带有空格的 SQL92 标识符是委员会的失误。不要使用它们。
@Mike:我想你必须和 SQL 标准委员会谈谈,祝你好运:)
对于非空情况,我们真的需要第一个索引中的 WHERE menu_id IS NOT NULL; 吗? CREATE UNIQUE INDEX favorites_3col_uni_idx ON favorites (user_id, menu_id, recipe_id) 不是一回事吗?
@Toby1Kenobi:拉丁复数是。但英语复数形式更为常见。
m
mu is too short

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

@muistooshort:是的,这是一个合适的解决方案。不过简化为 (MenuId <> '00000000-0000-0000-0000-000000000000')NULL 默认是允许的。顺便说一句,有三种人。偏执狂和不做数据库的人。第三类偶尔会困惑地发帖提问。 ;)
@Erwin:您不是说“偏执狂和数据库损坏的人”吗?
这个出色的解决方案使得在唯一约束中包含更简单类型的空列(例如整数)变得非常容易。
确实,UUID 不会出现那个特定的字符串,不仅因为涉及的概率,还因为它不是有效的 UUID。 UUID 生成器不能随意在任何位置使用任何十六进制数字,例如为 UUID 的版本号保留一个位置。
这个想法更简单,并且消除了需要 n^2 部分索引的多个可空字段的组合问题。这应该是公认的答案。
y
ypercubeᵀᴹ

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

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

一个有趣的想法。它使插入更加复杂。我需要先检查 FavoriteWithoutMenu 中是否已经存在一行。如果是这样,我只需添加一个菜单链接 - 否则我首先创建 FavoriteWithoutMenu 行,然后在必要时将其链接到菜单。这也使得在一个查询中选择所有收藏夹非常困难:我必须做一些奇怪的事情,比如先选择所有菜单链接,然后选择第一个查询中不存在 ID 的所有收藏夹。我不确定我是否喜欢这样。
我不认为插入更复杂。如果要插入带有 NULL MenuId 的记录,请插入到此表中。如果没有,则到 Favorites 表。但是查询,是的,它会更复杂。
实际上从头开始,选择所有收藏夹只需一个左键即可获取菜单。嗯,是的,这可能是要走的路..
如果您想将相同的菜谱添加到多个菜单中,则 INSERT 会变得更加复杂,因为您对 FavoriteWithoutMenu 上的 UserId/RecipeId 有一个唯一约束。只有当它不存在时,我才需要创建该行。
谢谢!这个答案值得+1,因为它更像是一个跨数据库的纯SQL。但是,在这种情况下,我将采用部分索引路线,因为它不需要更改我的架构,我喜欢它:)
w
wildplasser

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

是的,这是正确的。除了,就我而言,我想支持拥有一个不属于任何菜单的收藏夹。想象它就像浏览器中的书签一样。您可能只是“书签”一个页面。或者,您可以创建书签的子文件夹并为它们命名不同的东西。我想让用户收藏一个食谱,或者创建收藏夹的子文件夹,称为菜单。
正如我所说:这都是关于语义的。 (显然,我在考虑食物)拥有最喜欢的“不属于任何菜单”对我来说毫无意义。恕我直言,您不能偏爱不存在的东西。
似乎一些数据库规范化可能会有所帮助。创建第二个表,将食谱与菜单(或不)相关联。尽管它概括了问题并允许食谱可能包含的多个菜单。无论如何,问题是关于 PostgreSQL 中的唯一索引。谢谢。