Создать уникальное ограничение с пустыми столбцами - PullRequest
206 голосов
/ 28 ноября 2011

У меня есть таблица с таким макетом:

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 (например, все нули) вместо нуля.
    Тем не менее, MenuId имеет ограничение FK для меню каждого пользователя, поэтому я должен был бы создать специальное «нулевое» меню для каждого пользователя, что создает трудности.

  2. Проверьте наличие нулевой записи, используя вместо этого триггер.
    Я думаю, что это хлопот, и мне нравится избегать триггеров, где это возможно. Кроме того, я не доверяю им, чтобы гарантировать, что мои данные никогда не будут в плохом состоянии.

  3. Просто забудьте об этом и проверьте наличие ранее нулевой записи в промежуточном программном обеспечении или в функции вставки, и у вас нет этого ограничения.

Я использую Postgres 9.0.

Есть ли какой-то метод, который я пропускаю?

Ответы [ 4 ]

324 голосов
/ 28 ноября 2011

Создать два частичных индекса :

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

Если вам нужен индекс complete , вы также можете удалить условие WHERE с favo_3col_uni_idx, и ваши требования по-прежнему выполняются.
Индекс, теперь включающий всю таблицу, перекрывается с другим и становится больше. В зависимости от типичных запросов и процента значений NULL это может быть или не быть полезным. В экстремальных ситуациях это может даже помочь сохранить все три индекса (два частичных и общее число сверху).

Помимо: я советую не использовать идентификаторы смешанного регистра в PostgreSQL .

59 голосов
/ 28 ноября 2011

Вы можете создать уникальный индекс с объединением в MenuId:

CREATE UNIQUE INDEX
Favorites_UniqueFavorite ON Favorites
(UserId, COALESCE(MenuId, '00000000-0000-0000-0000-000000000000'), RecipeId);

Вам просто нужно выбрать UUID для COALESCE, который никогда не произойдет в «реальной жизни».Вы, вероятно, никогда бы не увидели нулевой UUID в реальной жизни, но вы можете добавить ограничение CHECK, если вы параноик (а поскольку они действительно хотят вас ...):

alter table Favorites
add constraint check
(MenuId <> '00000000-0000-0000-0000-000000000000')
2 голосов
/ 28 ноября 2011

Вы можете хранить избранное без связанного меню в отдельной таблице:

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
0 голосов
/ 28 ноября 2011

Я думаю, что здесь есть семантическая проблема. На мой взгляд, пользователь может иметь (но только один ) любимый рецепт для приготовления определенного меню. (У OP есть меню и рецепт, перепутанные; если я не прав: пожалуйста, поменяйте местами MenuId и RecipeId ниже) Это означает, что {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)
);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...