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

252

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

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.

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

Майк Кристенсен
источник
Почему разрешено несколько строк с одинаковыми ( UserId, RecipeId), если MenuId IS NULL?
Друкс

Ответы:

382

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

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

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

Эрвин Брандштеттер
источник
1
@Erwin Brandsetter: в отношении примечания « идентификаторы смешанного регистра »: пока двойные кавычки не используются, использование идентификаторов в смешанном регистре абсолютно нормально. Нет разницы в использовании всех строчных идентификаторов (опять же: только если не используются кавычки)
a_horse_with_no_name
14
@a_horse_with_no_name: Полагаю, вы знаете, что я это знаю. Это на самом деле одна из причин , почему я советую против его использования. Люди, которые не знают специфику так хорошо, запутываются, как и в других идентификаторах СУБД (частично) с учетом регистра. Иногда люди путают себя. Или они создают динамический SQL и используют quote_ident () так, как должны, и забывают передавать идентификаторы в виде строчных букв ! Не используйте смешанные идентификаторы в PostgreSQL, если вы можете избежать этого. Я видел ряд отчаянных просьб, проистекающих из этой глупости.
Эрвин Брандштеттер
3
@a_horse_with_no_name: Да, это, конечно, правда. Но если вы можете избежать их: вам не нужны смешанные идентификаторы . Они не служат цели. Если вы можете избежать их: не используйте их. Кроме того: они просто безобразны. Цитируемые идентификаторы тоже безобразны. Идентификаторы SQL92 с пробелами в них - ошибка, допущенная комитетом. Не используйте их.
wildplasser
2
@Mike: я думаю, тебе нужно поговорить об этом с комитетом по стандартам SQL, удачи :)
мю слишком мало
1
@buffer: затраты на обслуживание и общий объем хранилища в основном одинаковы (за исключением незначительных фиксированных накладных расходов на индекс). Каждая строка представлена ​​только в одном индексе. Производительность: если ваши результаты охватывают оба случая, может заплатить дополнительный общий простой индекс. Если нет, частичный индекс обычно быстрее полного индекса, в основном из-за меньшего размера. Добавьте условие индекса к запросам (избыточно), если Postgres не выяснит, что он может использовать частичный индекс сам по себе. Пример.
Эрвин Брандштеттер,
75

Вы можете создать уникальный индекс с объединением на 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' могут вызывать ложные уникальные нарушения, но вы уже исправили это в своем комментарии.
Эрвин Брандштеттер
2
@muistooshort: Да, это правильное решение. Упростите, (MenuId <> '00000000-0000-0000-0000-000000000000')хотя. NULLпо умолчанию разрешено Кстати, есть три типа людей. Параноидальные, и люди, которые не делают базы данных. Третий вид время от времени отправляет вопросы о ТА в недоумении. ;)
Эрвин Брандштеттер
2
@ Эрвин: Разве ты не имеешь в виду "параноидальных и тех, кто с поврежденными базами данных"?
мю слишком коротка
2
Это превосходное решение позволяет очень просто включить пустой столбец более простого типа, такого как целое число, в уникальное ограничение.
Маркус Пшеидт
2
Это правда, что UUID не придет с этой конкретной строкой, не только из-за вероятностей, но и потому, что это не правильный UUID . Генератор UUID не может свободно использовать любую шестнадцатеричную цифру в любой позиции, например, одна позиция зарезервирована для номера версии UUID.
Тоби 1 Кеноби
1

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

CREATE TABLE FavoriteWithoutMenu
(
  FavoriteWithoutMenuId uuid NOT NULL, --Primary key
  UserId uuid NOT NULL,
  RecipeId uuid NOT NULL,
  UNIQUE KEY (UserId, RecipeId)
)
ypercubeᵀᴹ
источник
Интересная идея Это делает вставку немного сложнее. Мне нужно проверить, если строка уже существует в FavoriteWithoutMenuпервую очередь. Если это так, я просто добавляю ссылку в меню - в противном случае я FavoriteWithoutMenuсначала создаю строку, а затем при необходимости связываю ее с меню. Это также затрудняет выбор всех избранных в одном запросе: мне нужно сделать что-то странное, например сначала выбрать все ссылки в меню, а затем выбрать все избранные, идентификаторы которых не существуют в первом запросе. Я не уверен, нравится ли мне это.
Майк Кристенсен
Я не думаю, что вставка как более сложная. Если вы хотите вставить запись с помощью NULL MenuId, вы вставляете в эту таблицу. Если нет, к Favoritesстолу. Но запросить, да, это будет сложнее.
ypercubeᵀᴹ
На самом деле, если вы выберете все фавориты, то просто получите левое соединение, чтобы получить меню. Хм, да, это может быть путь ...
Майк Кристенсен
INSERT становится более сложным, если вы хотите добавить один и тот же рецепт в несколько меню, поскольку у вас есть уникальное ограничение на UserId / RecipeId на FavoriteWithoutMenu. Мне нужно создать эту строку, только если она еще не существует.
Майк Кристенсен
1
Спасибо! Этот ответ заслуживает +1, так как это скорее кросс-база данных с чисто SQL-кодом. Однако в этом случае я собираюсь пойти по пути частичного индекса, потому что он не требует никаких изменений в моей схеме, и мне это нравится :)
Майк Кристенсен
-1

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