Позволит ли эта (нормализованная) структура базы данных осуществлять поиск по тегам, как я намереваюсь? - PullRequest
8 голосов
/ 07 июля 2010

Я пытаюсь настроить нормализованную базу данных MySQL, содержащую три следующие таблицы. Первая таблица содержит список элементов, которые могут быть описаны различными тегами. Третья таблица содержит различные теги, используемые для описания элементов в первой таблице. Средняя таблица связывает две другие таблицы друг с другом. В случае каждой таблицы идентификатор представляет собой первичный ключ с автоинкрементом (и каждый из них используется в качестве внешнего ключа в средней таблице)

+---------------+---------------------+---------------+
|   Table 1     |      Table 2        |   Table 3     |
+---------------+---------------------+---------------+
|id        item |id   item_id   tag_id|id          tag|
+---------------+---------------------+---------------+
| 1      spaniel| 1         1        4| 1         bird|
| 2        tabby| 2         1       23| 4          pet|
| 3      chicken| 3         1       41|23          dog|
| 4     goldfish| 4         2        4|24          cat|
|               | 5         2       24|25      reptile|
|               | 6         3        1|38         fish|
|               | 7         3       40|40    delicious|
|               | 8         4        4|41        cheap|
|               | 9         4       38|42    expensive|
|               |10         4       41|               |
|               |                     |               |
+---------------+---------------------+---------------+

Я хочу выполнить запрос одного или нескольких тегов к трем таблицам, чтобы получить элементы, соответствующие ВСЕМ тегам.

Так, например, запрос «pet» вернет элементы (1) спаниель, (2) табби и (4) золотая рыбка, потому что все они помечены как «pet». Запрос о «дешевом» и «домашнем животном» вместе вернет (1) спаниеля и (4) золотую рыбку, потому что они оба помечены как «дешевый» и «домашнее животное». Табби не будет возвращен, так как он помечен только как «домашнее животное», но не «дешево» (в моем мире кошки табби стоят дорого: P)

Запросы на "cheap", "pet" и "dog" вернут только (1) Spaniel, поскольку он единственный, соответствующий всем тэгам.

Во всяком случае, это желаемое поведение. У меня два вопроса.

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

  2. При условии, что вышеприведенная настройка работоспособна, как я могу структурировать один запрос MySQL для достижения моего целевое назначение? * (это существо, для ряд тегов, возвращающих ТОЛЬКО элемент (ы), которые соответствуют ВСЕМ указанному теги). Я пытался сделать разнообразие объединений / союзов, но ни один из них не является давая мне желаемый эффект (обычно вернуть ВСЕ предметы, которые соответствуют ЛЮБОМУ из теги). Я провел некоторое время просматривая руководство по MySQL онлайн, но я чувствую, что мне не хватает что-то концептуально.

* Я говорю один запрос, потому что, конечно, я мог бы просто выполнить серию простых запросов WHERE / JOIN, по одному для каждого тега, а затем объединить / отсортировать возвращаемые элементы в PHP или что-то по факту, но это кажется глупым и неэффективным способ сделать это. Я чувствую, что есть способ, которым я должен быть в состоянии сделать это с одним запросом MySQL, учитывая соответствующую настройку.

Ответы [ 6 ]

10 голосов
/ 07 июля 2010

Ваша схема выглядит неплохо. Нет необходимости в столбце идентификатора в вашей соединительной таблице - просто создайте первичный ключ из столбцов идентификаторов других таблиц (хотя см. Комментарий 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'
3 голосов
/ 07 июля 2010

Да. Это называется реляционным делением. Здесь обсуждаются различные методы http://www.simple -talk.com / sql / t-sql-программирование / разделенное-стоящее-sql-из-реляционного разделения /

Один из подходов - использовать двойной минус. то есть. чтобы выбрать все записи из таблицы 1, для которых нет тега в списке 'cheap', 'pet' не имеет связанной записи в table2

SELECT t1.id, t1.item
FROM Table1 t1
WHERE NOT EXISTS
(
    SELECT * FROM  
    table3 t3 WHERE tag IN ('cheap','pet')
    AND NOT EXISTS (
        SELECT * FROM table2 t2
        WHERE t2.tag_id = t3.id
        AND t1.id=t2.item_id
    )
)
0 голосов
/ 09 июля 2010

Спасибо всем за ваши очень подробные и полезные ответы.Бит об использовании «WHERE tag IN ('tag_1' ... 'tag_x')» в сочетании с COUNT для выбора элементов, соответствующих всем тегам, был именно тем, чего мне не хватало раньше.

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

Еще раз спасибо!Вы, ребята, великолепны!

0 голосов
/ 07 июля 2010

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

PRIMARY KEY (item_id, tag_id)

В противном случае это краткая стандартная схема базы данных m: n и она должна работать нормально.

0 голосов
/ 07 июля 2010

Вы можете попробовать что-то вроде этого:

select item, count(*) 'NrMatches'
from #table1 i
inner join #table2 l ON i.id = l.item_id
inner join #table3 t on l.tag_id = t.id
where t.tag IN ('cheap', 'pet', 'dog')
group by item
having count(*) = (select count(*) from #table3 
                   where tag IN ('cheap', 'pet', 'dog'))

Это означает, что ваши поисковые запросы встречаются дважды, но в основном это то, что вам нужно.

0 голосов
/ 07 июля 2010
  1. Эта концепция таблицы отображения довольно стандартна и выглядит хорошо реализованной здесь. Единственное, что я хотел бы изменить, это избавиться от идентификатора в таблице 2; для чего бы вы использовали это? Просто создайте объединенный ключ для Таблицы 2 как для идентификатора элемента, так и для идентификатора тега.

  2. На самом деле, выбрать, где элемент соответствует ВСЕМ тегам, сложно. Попробуйте это:

    SELECT item_id, COUNT (tag_id) FROM Table2 ГДЕ tag_id IN (ваш набор здесь) GROUP BY item_id

Если количество равно количеству идентификаторов тегов в вашем наборе, вы нашли совпадение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...