Создать уникальное ограничение с нулевыми столбцами

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

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 хранить избранное, у которого нет связанного с ним меню, но я хочу, чтобы не больше одной из этих строк на пару пользователя/рецепта.

Идеи, которые я имею до сих пор:

  • Используйте некоторые жестко запрограммированные UUID (такие как все нули) вместо null.
    Тем не менее, MenuId имеет ограничение FK для каждого пользовательского меню, поэтому мне пришлось бы создать специальное "нулевое" меню для каждого пользователя, который является проблемой.

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

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

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

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

Ответ 1

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

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). Если вам нужен полный индекс, вы также можете отказаться от условия WHERE от favo_3col_uni_idx, и ваши требования по-прежнему соблюдаются.
Индекс, который теперь содержит всю таблицу, перекрывается с другой и становится больше. В зависимости от типичных запросов и процента значений NULL это может быть или не быть полезным. В экстремальных ситуациях это может даже помочь поддерживать обе версии favo_3col_uni_idx.

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

Ответ 2

Вы можете создать уникальный индекс с коалесценцией в 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')

Ответ 3

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

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

Ответ 4

Я думаю, что здесь есть семантическая проблема. На мой взгляд, пользователь может иметь любимый рецепт (но только один) для подготовки определенного меню. (У ОП есть меню и рецепт смешаны, если я ошибаюсь: пожалуйста, замените 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)
);