Внешний ключ с несколькими столбцами из разных таблиц

Возьмем глупый пример: у меня много домашних животных, каждое из которых имеет ИМЯ в качестве идентификатора и тип (КОШКА или СОБАКА), давайте напишем это так (псевдокод):

TABLE ANIMALS (
  NAME char,
  ANIMAL_TYPE char {'DOG', 'CAT'}
  PRIMARY KEY(NAME)
)

(например, у меня есть КОШКА по имени Феликс и собака по имени Плутон)

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

TABLE PREFERED_FOOD (
  ANIMAL_NAME char,
  PREF_FOOD char
  FOREIGN KEY (ANIMAL_NAME) REFERENCES ANIMALS(NAME)
)

(например, Феликс любит молоко, а Плутон любит кости)

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

TABLE FOOD (
  ANIMAL_TYPE char {'DOG', 'CAT'},
  FOOD_TYPE char
)

(например, СОБАКИ едят кости и мясо, КОШКИ едят рыбу и молоко)

Вот мой вопрос: я хотел бы добавить внешнее ограничение в PREFERED_FOOD, так как PREF_FOOD - это FOOD_TYPE из FOOD с FOOD.ANIMAL_TYPE = ANIMALS.TYPE. Как я могу определить этот внешний ключ, не дублируя ANIMAL_TYPE на PREFERED_FOOD?

Я не эксперт в SQL, так что можете называть меня глупым, если это действительно просто ;-)


person user1695584    schedule 24.09.2012    source источник
comment
Первое, что я бы сделал, это использовать числовые клавиши вместо клавиш char.   -  person Abe Miessler    schedule 25.09.2012
comment
Я согласен, цифровые клавиши действительно лучше, они упрощают работу, хотя многие новички думают, что они действительно не нужны.   -  person geekman    schedule 25.09.2012
comment
Можно ли завести собаку по имени Феликс, а также кота по имени Феликс? Можно ли завести кошку по имени Плутон и собаку по имени Плутон? Это ведет к вопросу «Что такое первичный ключ таблицы Animals?» - всегда полезно быть откровенным, потому что (как вы можете понять из моего вопроса) то, что очевидно для вас, может не быть очевидным для других людей. FK в таблице предпочтительной еды предполагает, что у вас не может быть двух разных домашних животных с одним и тем же именем одновременно, но это косвенный, но, вероятно, надежный вывод.   -  person Jonathan Leffler    schedule 25.09.2012
comment
Для обеспечить соблюдение ограничений целостности данных. Также откажитесь от внешних ключей char, просто создайте таблицу тегов для хранения описаний и используйте идентификаторы для поиска   -  person kolossus    schedule 25.09.2012
comment
Да, конечно, имя питомца в качестве первичного ключа совсем нехорошо. Я просто написал это так, чтобы проиллюстрировать вопрос. Вопрос был только по последнему пункту, и эти таблицы - просто глупый пример ;-).   -  person user1695584    schedule 25.09.2012
comment
(и я помню о предложении триггера, вы правы, я просто хотел сохранить внешние ключи, но если я не могу, триггеры почти справятся с этой задачей)   -  person user1695584    schedule 25.09.2012


Ответы (5)


Вы не можете использовать SQL. Я думаю, вы могли бы, если бы SQL поддерживал утверждения. (Утверждения определены стандартом SQL-92. Насколько мне известно, их пока никто не поддерживает.)

Чтобы обойти эту проблему, используйте перекрывающиеся ограничения.

-- Nothing special here.
create table animal_types (
  animal_type varchar(15) primary key
);

create table animals (
  name varchar(15) primary key,
  animal_type varchar(15) not null references animal_types (animal_type),
  -- This constraint lets us work around SQL's lack of assertions in this case.
  unique (name, animal_type)
);

-- Nothing special here.
create table animal_food_types (
  animal_type varchar(15) not null references animal_types (animal_type),
  food_type varchar(15) not null,
  primary key (animal_type, food_type)
);

-- Overlapping foreign key constraints.
create table animals_preferred_food (
  animal_name varchar(15) not null,
  -- This column is necessary to implement your requirement. 
  animal_type varchar(15) not null,
  pref_food varchar(10) not null,
  primary key (animal_name, pref_food),
  -- This foreign key constraint requires a unique constraint on these
  -- two columns in "animals".
  foreign key (animal_name, animal_type) 
    references animals (animal_name, animal_type),
  -- Since the animal_type column is now in this table, this constraint
  -- is simple.
  foreign key (animal_type, pref_food) 
    references animal_food_types (animal_type, food_type)
);
person Mike Sherrill 'Cat Recall'    schedule 25.09.2012
comment
хорошо, поэтому я должен добавить animal_type во вторую таблицу :-( (ну, здесь вы указываете это как часть ключа, так что это звучит логично, но если вы представите, что единственный ключ - это имя питомца, это дублирование :-() Спасибо за ответ, посмотрю утверждения, не знаю по этому поводу! - person user1695584; 25.09.2012
comment
@ user1695584 Вам действительно нужно несколько любимых блюд на одно животное? - person Branko Dimitrijevic; 25.09.2012
comment
@BrankoDimitrijevic: Я полностью упустил из виду этот момент. Я буду винить в этом свою собаку. Первичный ключ для этой таблицы, вероятно, должен быть просто animal_name. Я подожду, пока OP подтвердит. - person Mike Sherrill 'Cat Recall'; 25.09.2012
comment
@Catcall Кроме того, в этом случае таблица animals_preferred_food может быть полностью удалена. Единственная любимая еда для каждого животного может быть представлена ​​полем в таблице animals. - person Branko Dimitrijevic; 25.09.2012
comment
@BrankoDimitrijevic: Частично суть вопроса заключалась в сохранении подходящих предпочтений для каждого типа животных. Для этого вам необходимо хранить Еда [food_type] - подходящий корм для животных [animal_type]. - person Mike Sherrill 'Cat Recall'; 25.09.2012
comment
@Catcall И для этого animal_food_types стол, верно? Я говорил об устранении animals_preferred_food. - person Branko Dimitrijevic; 25.09.2012

FOREIGN KEY (PREF_FOOD) REFERENCES FOOD (FOOD_TYPE)

в таблице PREFERRED_FOOD это гарантирует, что каждый PREFFOOD в таблице PREFERRED_FOOD уже присутствует в таблице FOOD_TYPE таблицы FOOD.

и в использовании таблицы FOOD теперь это говорит само за себя.

FOREIGN KEY (ANIMAL_TYPE) REFERENCES ANIMALS (ANIMAL_TYPE)
person geekman    schedule 24.09.2012
comment
animals.animal_type не уникален. Если он не уникален, вы не можете использовать его в качестве цели для ограничения внешнего ключа. (За исключением MySQL, где они предупреждают не делать этого в документации, но не мешают делать это, как это делают все нормальные СУБД.) - person Mike Sherrill 'Cat Recall'; 25.09.2012
comment
Что разумного в этом ограничении? Курсы могут быть организованы только в городах, где у нас есть кафедра. Город не является ключевым для DEPT. Тем не менее, правило само по себе остается не чем иным, как просто «CRS (CITY) REFERENCES DEPT (CITY)» ... - person Erwin Smout; 27.09.2012

В зависимости от того, какую СУБД вы используете (отредактируйте свой вопрос, включив это), вы, вероятно, захотите создать уникальное ограничение для столбцов ANIMAL_TYPE и PREFERED_FOOD.

Что-то вроде этого:

ALTER TABLE PREFERED_FOOD
ADD CONSTRAINT uc_FoodAnimal UNIQUE (ANIMAL_TYPE,PREFERED_FOOD)
person Abe Miessler    schedule 24.09.2012
comment
Да, конечно ;-). Но вопрос, который у меня был, заключался только в добавлении внешнего ключа с несколькими столбцами и ограничениями из разных таблиц ;-) (и это был просто общий вопрос SQL, я не имею в виду конкретную СУБД) - person user1695584; 25.09.2012

Честно говоря, у меня были некоторые проблемы с соблюдением ваших требований, но простая модель для представления животных и их еды, вероятно, будет выглядеть так:

введите описание изображения здесь

SPECIES_FOOD перечисляет все продукты, которые может съесть данный вид, а затем INDIVIDUAL просто выбирает одну из них в поле PREFERRED_FOOD_NAME.

Поскольку INDIVIDUAL.SPECIES_NAME относится к как SPECIES, так и SPECIES_FOOD, человек никогда не может предпочесть пищу, несъедобную для его вида.

Это, конечно, предполагает, что у отдельного животного не может быть более одной предпочтительной пищи. 1 Также предполагается, что у него не может быть ни одного - если это не так, просто сделайте INDIVIDUAL.PREFERRED_FOOD_NAME NOT NULL.

INDIVIDUAL_NAME был намеренно не сделан ключом, поэтому у вас могут быть, скажем, две кошки с именем "Феликс". Если это нежелательно, вы легко добавите соответствующий ключ.

Если все, что вам нужно знать о продукте - это его название, и вам не нужно представлять продукт независимо от какого-либо вида, таблицу FOOD можно вообще опустить.


1 В случае, если для одного животного может быть несколько предпочтительных продуктов, вам понадобится еще одна таблица "между" INDIVIDUAL и SPECIES_FOOD, и будьте осторожны, чтобы продолжать использовать идентифицирующие отношения, поэтому SPECIES_NAME переносится полностью пух (чтобы не отдавать предпочтение еде, несъедобной для данного вида животных).

person Branko Dimitrijevic    schedule 25.09.2012

Если вы возьмете (естественное) СОЕДИНЕНИЕ ЖИВОТНЫХ и PREFERRED_FOOD, вы получите таблицу, в которой для каждого животного указаны его тип и предпочтительная пища.

Вы хотите, чтобы эта комбинация была «действительной» для каждого отдельного животного, где «действительный» означает «появиться в перечне допустимых комбинаций типа животного / типа корма, перечисленных в FOOD.

Итак, у вас есть ограничение, которое несколько похоже на FK, но на этот раз «внешний ключ» появляется не в базовой таблице, а в соединении двух таблиц. Для этого типа ограничения в языке SQL есть ограничения CHECK и ASSERTIONS.

Версия ASSERTION самая простая. Это ограничение вроде (я был несколько либерален с именами атрибутов, чтобы избежать простого переименования атрибутов, которое запутывает точку)

CREATE ASSERTION <name for your constraint here>
 CHECK NOT EXISTS (SELECT ANIMAL_TYPE, FOOD_TYPE
                     FROM ANIMALS NATURAL JOIN PREF_FOOD
                    WHERE (ANIMAL_TYPE, FOOD_TYPE) NOT IN
                          SELECT ANIMAL_TYPE, FOOD_TYPE FROM FOOD_TYPE);

Но ваш средний движок SQL не поддерживает утверждения. Поэтому вам нужно использовать ограничения CHECK. Например, для таблицы PREF_FOOD необходимое ограничение CHECK может выглядеть примерно так:

CHECK EXISTS (SELECT 1
                FROM FOOD NATURAL JOIN ANIMAL
               WHERE ANIMAL_TYPE = <animal type of inserted row> AND
                     FOOD_TYPE = <food type of inserted row>);

Теоретически этого должно быть достаточно для обеспечения соблюдения вашего ограничения, но опять же, ваш средний механизм SQL снова не будет поддерживать такого рода ограничение CHECK из-за ссылок на таблицы, отличные от той, для которой определено ограничение.

Таким образом, у вас есть варианты: прибегнуть к довольно сложным (*) настройкам, таким как catcall, или принудить ограничение с помощью триггеров (и вам придется написать довольно много из них (по крайней мере, три или шесть, не продумали это подробно), и ваш следующий лучший вариант - обеспечить это в коде приложения, и снова будет три или шесть (более или менее) отдельных мест, где необходимо реализовать одинаковое количество отдельных проверок.

Во всех этих трех сценариях желательно задокументировать существование ограничения и его суть в другом месте. Ни один из трех не сделает очевидным для стороннего лица, читающего этот дизайн, о чем, черт возьми, все это идет.

(*) «сложный» может быть не совсем правильным словом, но обратите внимание, что такие решения основаны на преднамеренной избыточности, поэтому намеренно ниже 3NF в дизайне. А это означает, что ваш дизайн подвержен аномалиям обновления, а это означает, что пользователю будет сложнее обновлять базу данных И поддерживать ее согласованность (именно из-за преднамеренного дублирования).

person Erwin Smout    schedule 26.09.2012