Ваша схема выглядит неплохо. Нет необходимости в столбце идентификатора в вашей соединительной таблице - просто создайте первичный ключ из столбцов идентификаторов других таблиц (хотя см. Комментарий Marjan Venema и Стоит ли использовать составные первичные ключи или нет? для альтернативных видов на этом). В следующих примерах показано, как можно создавать таблицы, добавлять некоторые данные и выполнять запрошенные вами запросы.
Создание таблиц с ограничениями внешнего ключа . Короче говоря, ограничения внешнего ключа помогают обеспечить целостность базы данных. В этом примере они запрещают вставку элементов в таблицу соединений (item_tag
), если в таблицах item
и tag
нет соответствующих элементов:
CREATE TABLE IF NOT EXISTS `item` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`item` VARCHAR(255) NOT NULL ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `tag` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`tag` VARCHAR(255) NOT NULL ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `item_tag` (
`item_id` INT UNSIGNED NOT NULL ,
`tag_id` INT UNSIGNED NOT NULL ,
PRIMARY KEY (`item_id`, `tag_id`) ,
INDEX `fk_item_tag_item` (`item_id` ASC) ,
INDEX `fk_item_tag_tag` (`tag_id` ASC) ,
CONSTRAINT `fk_item_tag_item`
FOREIGN KEY (`item_id` )
REFERENCES `item` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_item_tag_tag`
FOREIGN KEY (`tag_id` )
REFERENCES `tag` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
Вставьте некоторые тестовые данные:
INSERT INTO item (item) VALUES
('spaniel'),
('tabby'),
('chicken'),
('goldfish');
INSERT INTO tag (tag) VALUES
('bird'),
('pet'),
('dog'),
('cat'),
('reptile'),
('fish'),
('delicious'),
('cheap'),
('expensive');
INSERT INTO item_tag (item_id, tag_id) VALUES
(1,2),
(1,3),
(1,8),
(2,2),
(2,4),
(3,1),
(3,7),
(4,2),
(4,6),
(4,8);
Выбрать все элементы и все теги:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id;
+----+----------+-----------+
| id | item | tag |
+----+----------+-----------+
| 1 | spaniel | pet |
| 1 | spaniel | dog |
| 1 | spaniel | cheap |
| 2 | tabby | pet |
| 2 | tabby | cat |
| 3 | chicken | bird |
| 3 | chicken | delicious |
| 4 | goldfish | pet |
| 4 | goldfish | fish |
| 4 | goldfish | cheap |
+----+----------+-----------+
Выберите элементы с определенным тегом:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag = 'pet';
+----+----------+-----+
| id | item | tag |
+----+----------+-----+
| 1 | spaniel | pet |
| 2 | tabby | pet |
| 4 | goldfish | pet |
+----+----------+-----+
Выберите элементы с одним или несколькими тегами. Обратите внимание, что при этом будут возвращены элементы с тегами cheap OR pet :
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet');
+----+----------+-------+
| id | item | tag |
+----+----------+-------+
| 1 | spaniel | pet |
| 1 | spaniel | cheap |
| 2 | tabby | pet |
| 4 | goldfish | pet |
| 4 | goldfish | cheap |
+----+----------+-------+
Приведенный выше запрос дает ответ, который вам может не понадобиться, как это выделено в следующем запросе. В этом случае нет элементов с тегом house , но этот запрос по-прежнему возвращает несколько строк:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'house');
+----+----------+-------+
| id | item | tag |
+----+----------+-------+
| 1 | spaniel | cheap |
| 4 | goldfish | cheap |
+----+----------+-------+
Это можно исправить, добавив GROUP BY
и HAVING
:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'house')
GROUP BY item.id HAVING COUNT(*) = 2;
Empty set (0.00 sec)
GROUP BY
приводит к тому, что все элементы с одинаковым идентификатором (или любым указанным вами столбцом) группируются в одну строку, эффективно удаляя дубликаты. HAVING COUNT
ограничивает результаты теми, где количество совпадающих сгруппированных строк равно двум. Это гарантирует, что будут возвращены только элементы с двумя тегами - обратите внимание, что это значение должно соответствовать количеству тегов, указанному в предложении IN
. Вот пример, который производит что-то:
SELECT item.id, item.item, tag.tag
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet')
GROUP BY item.id HAVING COUNT(*) = 2;
+----+----------+-----+
| id | item | tag |
+----+----------+-----+
| 1 | spaniel | pet |
| 4 | goldfish | pet |
+----+----------+-----+
Обратите внимание, что в предыдущем примере элементы были сгруппированы, так что вы не получите дубликаты. В этом случае нет необходимости в столбце tag
, так как это просто смешивает результаты - вы уже знаете, какие есть теги, поскольку вы запрашивали элементы с этими тегами. Поэтому вы можете немного упростить задачу, удалив столбец tag
из запроса:
SELECT item.id, item.item
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet')
GROUP BY item.id HAVING COUNT(*) = 2;
+----+----------+
| id | item |
+----+----------+
| 1 | spaniel |
| 4 | goldfish |
+----+----------+
Вы можете пойти дальше и использовать GROUP_CONCAT
, чтобы получить список подходящих тегов. Это может быть удобно, когда вам нужен список элементов, имеющих один или несколько указанных тегов, но не обязательно все из них:
SELECT item.id, item.item, GROUP_CONCAT(tag.tag) AS tags
FROM item
JOIN item_tag ON item_tag.item_id = item.id
JOIN tag ON item_tag.tag_id = tag.id
WHERE tag IN ('cheap', 'pet', 'bird', 'cat')
GROUP BY id;
+----+----------+-----------+
| id | item | tags |
+----+----------+-----------+
| 1 | spaniel | pet,cheap |
| 2 | tabby | pet,cat |
| 3 | chicken | bird |
| 4 | goldfish | pet,cheap |
+----+----------+-----------+
Одна проблема с вышеупомянутой схемой состоит в том, что можно вводить дубликаты элементов и теги. То есть вы можете вставить bird в таблицу tag
столько раз, сколько захотите, и это не хорошо. Один из способов исправить это - добавить UNIQUE INDEX
в столбцы item
и tag
. Это дает дополнительное преимущество, помогая ускорить запросы, основанные на этих столбцах. Обновленные команды CREATE TABLE
теперь выглядят так:
CREATE TABLE IF NOT EXISTS `item` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`item` VARCHAR(255) NOT NULL ,
UNIQUE INDEX `item` (`item`) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `tag` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`tag` VARCHAR(255) NOT NULL ,
UNIQUE INDEX `tag` (`tag`) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
Теперь, если вы попытаетесь вставить повторяющееся значение, MySQL помешает вам сделать это:
INSERT INTO tag (tag) VALUES ('bird');
ERROR 1062 (23000): Duplicate entry 'bird' for key 'tag'