Створіть унікальне обмеження з нульовими стовпцями


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 (таких як усі нулі) замість null.
    Однак MenuIdу меню кожного користувача є обмеження FK, тому мені доведеться створити спеціальне меню "null" для кожного користувача, який є клопотом.

  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 посилалось на три колонки - замість цього використовуйте стовпець ПК).

Якщо вам потрібен повний індекс, ви можете відмовитись від WHEREумови, favo_3col_uni_idxі ваші вимоги все ще виконуються.
Індекс, який зараз складається з усієї таблиці, перекривається з іншою і збільшується. Залежно від типових запитів і відсотківNULL значень, це може бути або не корисно. В екстремальних ситуаціях це може навіть допомогти зберегти всі три індекси (два часткові та загальний верх).

Убік: Я раджу не використовувати змішані ідентифікатори регістрів у PostgreSQL .


1
@Erwin Brandsetter: щодо " змішаних ідентифікаторів випадків " зауваження: Поки не використовуються подвійні лапки, використання змішаних кесованих ідентифікаторів абсолютно нормально. Немає різниці у використанні всіх малих ідентифікаторів (знову ж таки: лише якщо не використовуються лапки)
a_horse_with_no_name

14
@a_horse_with_no_name: Я припускаю, що ви знаєте, що я це знаю. Це насправді одна з причин , чому я раджу проти його використання. Люди, які так добре не знають специфіки, плутаються, як і в інших ідентифікаторах RDBMS (частково) залежно від регістру. Іноді люди плутають себе. Або вони будують динамічний SQL і використовують quo_ident () як слід і забудьте зараз передавати ідентифікатори як рядки нижнього регістру! Не використовуйте змішані ідентифікатори випадків у PostgreSQL, якщо ви можете цього уникнути. Я бачив низку відчайдушних прохань, що випливають із цієї глупоти.
Ервін Брандстеттер

3
@a_horse_with_no_name: Так, звичайно, це правда. Але якщо ви можете їх уникнути: вам не потрібно змішаних ідентифікаторів випадків . Вони не служать меті. Якщо ви можете їх уникнути: не використовуйте їх. Крім того: вони просто некрасиві. Цитовані ідентифікатори теж некрасиві. Ідентифікатори SQL92 з пробілами в них - це помилка, зроблена комітетом. Не використовуйте їх.
wildplasser

2
@Mike: Я думаю, що вам доведеться про це поговорити з комітетом стандартів SQL, удача :)
mu занадто короткий

1
@buffer: Вартість технічного обслуговування та загальний обсяг зберігання в основному однакові (за винятком незначних фіксованих накладних витрат на індекс). Кожен рядок представлений лише одним індексом. Продуктивність: Якщо ваші результати охоплюють обидва випадки, додатковий загальний звичайний індекс може платити. Якщо ні, то частковий індекс, як правило, швидший, ніж повний індекс, головним чином через менший розмір. Додайте умову індексу до запитів (надлишково), якщо Postgres не з'ясує, він може використовувати частковий індекс самостійно. Приклад.
Ервін Брандштеттер

75

Ви можете створити унікальний індекс з коалесценцією в MenuId:

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

Вам просто потрібно вибрати UUID для COALESCE, який ніколи не відбудеться в "реальному житті". Ви, мабуть, ніколи не бачите нульового UUID у реальному житті, але ви можете додати обмеження ПЕРЕВІРИТИ, якщо ви параноїчні (а оскільки вони насправді виходять, щоб отримати вас ...):

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
@Erwin: Ви не маєте на увазі "параноїків та тих, у кого зламані бази даних"?
mu занадто короткий

2
Це відмінне рішення дозволяє дуже легко включити нульовий стовпець більш простого типу, такого як ціле число, в унікальне обмеження.
Маркус Пшейдт

2
Це правда, що UUID не зможе придумати саме цей рядок не тільки через пов'язані ймовірності, але й тому, що це не дійсний UUID . Генератор UUID не може використовувати будь-яку шістнадцяткову цифру в будь-якій позиції, наприклад, одна позиція зарезервована для номера версії UUID.
Toby 1 Kenobi

1

Ви можете зберігати вибране без пов’язаного меню в окремій таблиці:

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столу. Але запити, так, це буде складніше.
ypercubeᵀᴹ

Насправді подряпини про те, що вибір усіх обраних буде лише одним лівим приєднанням для отримання меню. Хм, так, це може бути шлях ..
Майк Крістенсен,

INSERT стає складнішим, якщо ви хочете додати один і той же рецепт до більш ніж одного меню, оскільки у вас є UNIQUE обмеження щодо UserId / RecipeId у FavoriteWithoutMenu. Мені потрібно створити цей рядок, лише якщо він ще не існував.
Майк Крістенсен

1
Дякую! Ця відповідь заслуговує +1, оскільки це більше є простою базою даних SQL, яка є міжбазовою базою даних. Однак у цьому випадку я збираюся піти на частковий індексний маршрут, оскільки він не потребує змін у моїй схемі, і мені це подобається :)
Майк Крістенсен

-1

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

2
Так, це правильно. Крім того, в моєму випадку я хочу підтримати наявність улюбленого, який не належить до жодного меню. Уявіть, що це як Ваші закладки у вашому браузері. Ви можете просто "зробити закладку" на сторінці. Або ви можете створити підпапки закладок і назвати їх різними речами. Я хочу дозволити користувачам вподобати рецепт або створити підпапки улюблених під назвою меню.
Майк Крістенсен

1
Як я вже сказав: мова йде про семантику. (Я думав про їжу, очевидно) Вподобання "яке не належить до жодного меню" для мене не має сенсу. Ви не можете підтримувати щось, чого не існує, ІМХО.
wildplasser

Схоже, деяка нормалізація ДБ могла б допомогти. Створіть другу таблицю, яка стосується рецептів меню (чи ні). Хоча це узагальнює проблему і передбачає не одне меню, до якого може входити рецепт. Незважаючи на це, питання стосувалося унікальних індексів у PostgreSQL. Дякую.
Кріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.