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

129

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

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.

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

  • 0
    Почему допускается несколько строк с одинаковыми ( UserId , RecipeId ), если MenuId IS NULL ?
Теги:
database-design
null
referential-integrity

4 ответа

210
Лучший ответ

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

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.

  • 1
    @Erwin Brandsetter: в отношении примечания « идентификаторы смешанного регистра »: пока двойные кавычки не используются, использование идентификаторов в смешанном регистре абсолютно нормально. Нет разницы в использовании всех строчных идентификаторов (опять же: только если не используются кавычки)
  • 8
    @a_horse_with_no_name: я полагаю, вы знаете, что я это знаю. Это на самом деле одна из причин , почему я советую против его использования. Люди, которые не знают специфику так хорошо, запутываются, как и в других идентификаторах СУБД (частично) с учетом регистра. Иногда люди путают себя. Или они создают динамический SQL и используют quote_ident (), как должны, и забывают передавать идентификаторы в виде строчных букв ! Не используйте смешанные идентификаторы в PostgreSQL, если вы можете избежать этого. Я видел ряд отчаянных просьб, проистекающих из этой глупости.
Показать ещё 13 комментариев
34

Вы можете создать уникальный индекс с коалесценцией в 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')
  • 1
    В этом есть (теоретический) недостаток: записи с menu_id = '00000000-0000-0000-0000-000000000000' могут вызывать ложные уникальные нарушения, но вы уже исправили это в своем комментарии.
  • 0
    Я не думаю, что какой-либо существующий алгоритм генерации UUID в любом случае придумал бы этот UUID :) Очень креативное решение.
Показать ещё 6 комментариев
2

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

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
  • 0
    Интересная идея Это делает вставку немного сложнее. Мне нужно сначала проверить, существует ли уже строка в FavoriteWithoutMenu . Если это так, я просто добавляю ссылку в меню - в противном случае я сначала создаю строку FavoriteWithoutMenu а затем при необходимости связываю ее с меню. Это также затрудняет выбор всех избранных в одном запросе: мне нужно сделать что-то странное, например сначала выбрать все ссылки в меню, а затем выбрать все избранные, идентификаторы которых не существуют в первом запросе. Я не уверен, нравится ли мне это.
  • 0
    Я не думаю, что вставка как более сложная. Если вы хотите вставить запись с NULL MenuId , вы вставляете в эту таблицу. Если нет, то в таблицу Favorites . Но запросить, да, это будет сложнее.
Показать ещё 5 комментариев
1

Я думаю, что здесь есть семантическая проблема. На мой взгляд, пользователь может иметь любимый рецепт (но только один) для подготовки определенного меню. (У ОП есть меню и рецепт смешаны, если я ошибаюсь: пожалуйста, замените 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)
);
  • 2
    Да, это правильно. За исключением того, что в моем случае я хочу поддержать избранное, которое не принадлежит ни одному меню. Представьте это как ваши закладки в вашем браузере. Вы можете просто «добавить в закладки» страницу. Или вы можете создавать подпапки закладок и называть их разными вещами. Я хочу разрешить пользователям добавлять рецепты в избранное или создавать подпапки избранного, называемые меню.
  • 1
    Как я уже сказал: все дело в семантике. (Я думал о еде, очевидно). Любить «не принадлежащее ни к одному меню» для меня не имеет смысла. Вы не можете отдать предпочтение тому, чего не существует, ИМХО.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню