[英]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
存儲沒有關聯菜單的收藏夾,但我只希望每個用戶/食譜對最多包含這些行中的一個。
到目前為止,我的想法是:
使用一些硬編碼的 UUID(例如全零)而不是 null。
但是, MenuId
對每個用戶的菜單都有 FK 約束,因此我必須為每個用戶創建一個特殊的“空”菜單,這很麻煩。
改用觸發器檢查 null 條目是否存在。
我認為這很麻煩,我喜歡盡可能避免觸發。 另外,我不相信他們能保證我的數據永遠不會壞 state。
忘記它並檢查中間件或插入 function 中以前是否存在 null 條目,並且沒有此約束。
我正在使用 Postgres 9.0。 我有什么方法可以忽略嗎?
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;
注意新子句在關鍵字段之后的位置。
創建兩個部分索引:
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”。然后,您的唯一索引必須以鑒別器列為目標。
這樣我們就不必創建兩個部分索引,也不會失去僅對name
和age
選擇使用索引掃描的能力。
此外,您可以保留email
列的類型,並且合並 function 沒有任何問題,因為email_discriminator
不是外鍵。 而且您不必擔心此列會收到意外值,因為無法寫入生成的列。
我可以看到此解決方案中的三個固執己見的缺點,但它們都可以滿足我的需求:
email
和email_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.