Набор дополнений MySQL «многие ко многим»

#mysql #sql #join #many-to-many

#mysql #sql #Присоединиться #многие ко многим

Вопрос:

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

У меня есть три таблицы, классическое решение «многие ко многим»:

  • entries : идентификатор (int), заголовок (varchar[255]), содержимое (текст)
  • tags : id (int), name (varchar[255]), slug (varchar[255])
  • entries_tags : id (int), entry_id (int), tag_id (int)

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

 ID | name
1. | one 
2. | two 
3. | three
4. | four
5. | five 
  

У меня также есть три записи:

 ID | title
1. | Something
2. | Blah blah blah
3. | Yay!
  

И отношения:

 ID | entry_id | tag_id
1. | 1        | 1
2. | 1        | 2
3. | 2        | 1
4. | 2        | 3
5. | 3        | 1
6. | 3        | 2
7. | 3        | 3
8. | 4        | 1
9. | 4        | 4
  

Хорошо, у нас есть наши тестовые данные. Я хочу знать, как получить все записи, которые имеют тег Один, но не имеют тега Три (это были бы записи 1 и 4).

Я знаю, как это сделать с помощью подзапроса, проблема в том, что это занимает много времени (при 100 тыс. записей это заняло около 10-15 секунд). Есть ли какой-нибудь способ сделать это с помощью объединений? Или я что-то упускаю?

редактировать Полагаю, я должен был упомянуть, что мне нужно решение, которое работает с наборами данных, а не с отдельными тегами, поэтому замените «Один» в моем вопросе на «Один», «Два» и «Два» на «Три», «Четыре»

edit2 Предоставленный ответ правильный, но он слишком медленный, чтобы его можно было использовать практически. Я думаю, единственный способ заставить его работать — использовать стороннюю поисковую систему, такую как Lucene или ElasticSearch.

Ответ №1:

Следующий скрипт выбирает записи, которые имеют теги One Two и не имеют тегов Three и Four :

 SELECT DISTINCT
  et.entry_id
FROM entries_tags et
  INNER JOIN tags t1 ON et.tag_id = t1.id AND t1.name IN ('One', 'Two')
  LEFT JOIN  tags t2 ON et.tag_id = t2.id AND t2.name IN ('Three', 'Four')
WHERE t2.id IS NULL
  

Альтернативное решение: INNER JOIN заменяется на WHERE EXISTS , что позволяет нам избавиться от (довольно дорогого) DISTINCT :

 SELECT
  et.entry_id
FROM entries_tags et
  LEFT JOIN  tags t2 ON et.tag_id = t2.id AND t2.name IN ('Three', 'Four')
WHERE t2.id IS NULL
  AND EXISTS (
    SELECT *
    FROM tags t1
    WHERE t1.id = et.tag_id
      AND t1.name IN ('One', 'Two')
  )
  

Комментарии:

1. Это сделало это за 1/3 предыдущего времени (~ 5 секунд), и я думаю, это максимум, что вы можете получить, не кэшируя результат и не выполняя какие-то магические трюки вуду. Большое спасибо!

2. Всегда приветствуется! На самом деле, есть еще одна идея, и я уже обновил свой ответ ее реализацией. Не могли бы вы попробовать?

Ответ №2:

Это должно делать то, что вы хотите.

(Это может быть или не быть быстрее, чем решение с вложенным запросом, я предлагаю вам сравнить планы запросов)

 SELECT DISTINCT e.* 
FROM tags t1 
INNER JOIN entries_tags et1 ON t1.id=et1.tag_id 
INNER JOIN entries e ON e.entry_id=et1.entry_id 
INNER JOIN tags t2 on t2.name='three'
INNER JOIN tags t3 on t3.name='four'
LEFT JOIN entries_tags et2 ON (et1.entryid=et2.entryid AND t2.id = et2.tag_id ) 
       OR (et1.entryid=et2.entryid AND t3.id = et2.tag_id )
WHERE t1.name IN ('one','two') AND et2.name is NULL 
  

Присоединив СЛЕВА к таблице et2 entries_tags (данные, которые вам не нужны), вы можете выбрать только те записи, в которых et2.name ИМЕЕТ значение NULL (если запись et2 не существует).

Комментарии:

1. Я не уверен, что вы пытались сделать здесь, но этот запрос полностью нарушен. Или, может быть, я исправляю это неправильно (ваши имена таблиц и полей кажутся немного случайными)

2. Я немного изменил свой вопрос, имейте это в виду 🙂

3. SELECT DISTINCT e.* FROM tags t1 INNER JOIN entries_tags et1 ON t1.id=et1.tag_id INNER JOIN entries e ON e.id=et1.entry_id INNER JOIN tags t2 on t2.id=3 INNER JOIN tags t3 on t3.name=4 LEFT JOIN entries_tags et2 ON (et1.entry_id=et2.entry_id AND t2.id = et2.tag_id ) OR (et1.entry_id=et2.entry_id AND t3.id = et2.tag_id ) WHERE t1.id IN (1,2) AND et2.id is NULL; — я делаю это неправильно? по-прежнему не работает (0 результатов)

4. извиняюсь, у меня здесь сейчас нет mysql. ВНУТРЕННИЕ теги соединения t3 на t3.name=4 выглядят для меня немного подозрительно

5. О, вы правы. Жаль, что в любом случае это занимает ~ 13 секунд. Я думаю, что просто нет способа эффективно делать подобные вещи…

Ответ №3:

Вы упомянули попытку выполнения подзапроса. Это то, что вы пробовали?

 SELECT entries.id, entries.content
FROM entries
  LEFT JOIN entries_tags ON entries.id=entries_tags.entries_id
  LEFT JOIN tags ON entries_tags.tag_id=tags.id
WHERE tag.id=XX
  and entries.id NOT IN (
    SELECT entries.id
    FROM entries
      LEFT JOIN entries_tags ON entries.id=entries_tags.entries_id
      LEFT JOIN tags ON entries_tags.tag_id=tags.id
    WHERE tag.id=YY
  )
  

(Где XX — это тег, который вам нужен, а YY — тег, который вам не нужен)

С индексами в полях идентификаторов это не должно быть так медленно, как вы говорите. Это будет зависеть от набора данных, но с индексами все должно быть в порядке (и без сравнения строк).

Комментарии:

1. После изменений (был нарушен в нескольких местах): SELECT count(entries.id) FROM entries LEFT JOIN entries_tags ON entries.id=entries_tags.entry_id LEFT JOIN tags ON entries_tags.tag_id=tags.id WHERE tags.id IN (1,2) and entries.id NOT IN (SELECT entries.id FROM entries LEFT JOIN entries_tags ON entries.id=entries_tags.entry_id LEFT JOIN tags ON entries_tags.tag_id=tags.id WHERE tags.id IN (3,4)); . Потребовалось 13 секунд для моей тестовой базы данных.

2. У вас есть индексы для идентификаторов? Вы можете проверить с помощью SHOW CREATE TABLE [Таблица] . Кроме того, сколько элементов имеют теги 3 или 4?

3. Дамп структуры таблицы . И мы говорим о 1 млн записей и 1,172 млн отношений entries_tags.