簡體   English   中英

使用 null 列創建唯一約束

[英]Create unique constraint with null columns

我有一個具有這種布局的表:

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)的多行。 我想允許NULL中的MenuId存儲沒有關聯菜單的收藏夾,但我只希望每個用戶/食譜對最多包含這些行中的一個。

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

  1. 使用一些硬編碼的 UUID(例如全零)而不是 null。
    但是, MenuId對每個用戶的菜單都有 FK 約束,因此我必須為每個用戶創建一個特殊的“空”菜單,這很麻煩。

  2. 改用觸發器檢查 null 條目是否存在。
    我認為這很麻煩,我喜歡盡可能避免觸發。 另外,我不相信他們能保證我的數據永遠不會壞 state。

  3. 忘記它並檢查中間件或插入 function 中以前是否存在 null 條目,並且沒有此約束。

我正在使用 Postgres 9.0。 我有什么方法可以忽略嗎?

Postgres 15 或更新版本(目前為測試版)

Postgres 15 添加了子句NULLS NOT DISTINCT 發行說明:

  • 允許唯一約束和索引將 NULL 值視為不同的 (Peter Eisentraut)

    以前 NULL 值總是被索引為不同的值,但現在可以通過使用UNIQUE NULLS NOT DISTINCT創建約束和索引來更改它。

使用此子句, NULL被視為只是另一個值,並且UNIQUE約束不允許多個具有相同NULL值的行。 現在的任務很簡單:

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

手冊章節“唯一約束”中有示例。
該子句切換所有索引鍵的行為。 您不能將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 歲或以上

創建兩個部分索引

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) (您似乎不太可能想要三列寬的 FK 引用 - 請改用 PK 列!)
  • 您不能將CLUSTER基於部分索引。
  • 沒有匹配WHERE條件的查詢不能使用部分索引。

如果您需要一個完整的索引,您也可以從favo_3col_uni_idx中刪除WHERE條件,您的要求仍然得到執行。
現在包含整個表的索引與另一個表重疊並變大。 根據典型查詢和NULL值的百分比,這可能有用也可能沒用。 在極端情況下,它甚至可能有助於維護所有三個索引(兩個部分索引和頂部的總索引)。

對於單個可為空的列,可能是兩個,這是一個很好的解決方案。 但是它很快就會失控,因為您需要為每個可為空列的組合使用單獨的部分索引,因此該數字呈二項式增長。 對於多個可為空的列,請參閱:

另外:我建議不要在 PostgreSQL 中使用混合大小寫標識符

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

您可以將沒有關聯菜單的收藏夾存儲在單獨的表中:

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

我相信有一個選項可以將以前的答案組合成一個更優化的解決方案。

create table unique_with_nulls (
    id serial not null,
    name varchar not null,
    age int2 not null,
    email varchar,
    email_discriminator varchar not null generated always as ( coalesce(email::varchar, 0::varchar) ) stored,

    constraint uwn_pkey primary key (id)
);

create unique index uwn_name_age_email_uidx on unique_with_nulls(name, age, email_discriminator);

這里發生的是列email_discriminator將在“插入或更新時間”生成,作為實際的 email,或者如果前一個是 null 則為“0”。然后,您的唯一索引必須以鑒別器列為目標。
這樣我們就不必創建兩個部分索引,也不會失去僅對nameage選擇使用索引掃描的能力。
此外,您可以保留email列的類型,並且合並 function 沒有任何問題,因為email_discriminator不是外鍵。 而且您不必擔心此列會收到意外值,因為無法寫入生成的列。

我可以看到此解決方案中的三個固執己見的缺點,但它們都可以滿足我的需求:

  • emailemail_discriminator之間的數據重復。
  • 我必須寫一個專欄並從另一個專欄閱讀的事實。
  • 需要找到一個超出可接受值email的值作為后備值(有時這可能很難找到,甚至是主觀的)。

我認為這里存在語義問題。 在我看來,用戶可以有一個(但只有一個)最喜歡的食譜來准備一個特定的菜單。 (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)
);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM