SQL-сопоставление «многие ко многим»
-
09-06-2019 - |
Вопрос
Я внедряю систему тегов для веб-сайта.Существует несколько тегов для каждого объекта и несколько объектов для каждого тега.Это достигается за счет ведения таблицы с двумя значениями для каждой записи: одно для идентификаторов объекта и тега.
Я хочу написать запрос для поиска объектов, соответствующих заданному набору тегов.Предположим, у меня были следующие данные (в формате [объект] -> [теги]*)
apple -> fruit red food
banana -> fruit yellow food
cheese -> yellow food
firetruck -> vehicle red
Если я хочу совместить (красный), мне нужно взять яблоко и пожарную машину.Если я хочу совместить (фрукты, еду), я должен получить (яблоко, банан).
Как мне написать SQL-запрос и делать то, что я хочу?
@Джереми Рутен,
Спасибо за Ваш ответ.Используемая нотация использовалась для предоставления некоторых примеров данных: в моей базе данных есть таблица с 1 идентификатором объекта и 1 тегом на запись.
Во-вторых, моя проблема в том, что мне нужно получить все объекты, соответствующие всем тегам.Замените OR на AND следующим образом:
SELECT object WHERE tag = 'fruit' AND tag = 'food';
Никаких результатов при запуске не дает.
Решение
Данный:
- таблица объектов (идентификатор первичного ключа)
- таблица objecttags (внешние ключи objectId, tagid)
таблица тегов (идентификатор первичного ключа)
SELECT distinct o.* from object o join objecttags ot on o.Id = ot.objectid join tags t on ot.tagid = t.id where t.Name = 'fruit' or t.name = 'food';
Это кажется обратным ходом, поскольку вы хотите и, но проблема в том, что 2 тега не находятся в одной строке, и, следовательно, оператор and ничего не дает, поскольку одна строка не может быть одновременно фруктом и едой.Этот запрос обычно выдает дубликаты, поскольку вы получаете по 1 строке каждого объекта для каждого тега.
Если вы действительно хотите сделать и в этом случае вам понадобится group by
, и having count = <number of ors>
в вашем запросе, например.
SELECT distinct o.name, count(*) as count
from object o join objecttags ot on o.Id = ot.objectid
join tags t on ot.tagid = t.id
where t.Name = 'fruit' or t.name = 'food'
group by o.name
having count = 2;
Другие советы
О боже, возможно, я неправильно истолковал ваш первоначальный комментарий.
Самый простой способ сделать это в SQL — создать три таблицы:
1) Tags ( tag_id, name )
2) Objects (whatever that is)
3) Object_Tag( tag_id, object_id )
После этого вы сможете быстро, легко и эффективно задать практически любой вопрос о данных (при условии, что вы правильно их индексируете).Если вы хотите проявить фантазию, вы также можете разрешить теги из нескольких слов (я могу придумать элегантный способ и менее элегантный способ).
Я предполагаю, что это то, что у вас есть, поэтому приведенный ниже SQL будет работать:
Буквальный способ:
SELECT obj
FROM object
WHERE EXISTS( SELECT *
FROM tags
WHERE tag = 'fruit'
AND oid = object_id )
AND EXISTS( SELECT *
FROM tags
WHERE tag = 'Apple'
AND oid = object_id )
Есть и другие способы сделать это, например:
SELECT oid
FROM tags
WHERE tag = 'Apple'
INTERSECT
SELECT oid
FROM tags
WHERE tag = 'Fruit'
@Кайл:Ваш запрос должен быть больше похож на:
SELECT object WHERE tag IN ('fruit', 'food');
Ваш запрос искал строки, в которых тегом были одновременно фрукты И еда, что невозможно, поскольку поле может иметь только одно значение, а не оба одновременно.
Объединив предложение Стива М. с предложением Джереми, вы получите одну запись с тем, что ищете:
select object
from tblTags
where tag = @firstMatch
and (
@secondMatch is null
or
(object in (select object from tblTags where tag = @secondMatch)
)
Это не очень хорошо масштабируется, но вы получите то, что вы ищете.Я думаю, что есть лучший способ сделать это, чтобы вы могли легко иметь N совпадающих элементов без большого влияния на код, но в настоящее время это ускользает от меня.
Рекомендую следующую схему.
Objects: objectID, objectName
Tags: tagID, tagName
ObjectTag: objectID,tagID
Со следующим запросом.
select distinct
objectName
from
ObjectTab ot
join object o
on o.objectID = ot.objectID
join tabs t
on t.tagID = ot.tagID
where
tagName in ('red','fruit')
Я бы предложил сделать в вашей таблице по 1 тегу на запись, например:
apple -> fruit
apple -> red
apple -> food
banana -> fruit
banana -> yellow
banana -> food
Тогда ты мог бы просто
SELECT object WHERE tag = 'fruit' OR tag = 'food';
Если вы действительно хотите сделать это по-своему, вы можете сделать это следующим образом:
SELECT object WHERE tag LIKE 'red' OR tag LIKE '% red' OR tag LIKE 'red %' OR tag LIKE '% red %';