У меня есть таблица с этим макетом:
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.
Есть ли какой-нибудь метод, который я пропускаю?
Создать два частичных индекса:
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.
Вы можете создать уникальный индекс с коалесценцией в 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')
Вы можете сохранить избранное без соответствующего меню в отдельной таблице:
CREATE TABLE FavoriteWithoutMenu
(
FavoriteWithoutMenuId uuid NOT NULL, --Primary key
UserId uuid NOT NULL,
RecipeId uuid NOT NULL,
UNIQUE KEY (UserId, RecipeId)
)
FavoriteWithoutMenu
. Если это так, я просто добавляю ссылку в меню - в противном случае я сначала создаю строку FavoriteWithoutMenu
а затем при необходимости связываю ее с меню. Это также затрудняет выбор всех избранных в одном запросе: мне нужно сделать что-то странное, например сначала выбрать все ссылки в меню, а затем выбрать все избранные, идентификаторы которых не существуют в первом запросе. Я не уверен, нравится ли мне это.
NULL MenuId
, вы вставляете в эту таблицу. Если нет, то в таблицу Favorites
. Но запросить, да, это будет сложнее.
Я думаю, что здесь есть семантическая проблема. На мой взгляд, пользователь может иметь любимый рецепт (но только один) для подготовки определенного меню. (У ОП есть меню и рецепт смешаны, если я ошибаюсь: пожалуйста, замените 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)
);
UserId
,RecipeId
), еслиMenuId IS NULL
?