Набор дополнений MySQL «многие ко многим» - PullRequest
4 голосов
/ 03 октября 2011

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

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

  • entries: id (int), заголовок (varchar [255]), контент (текст)
  • tags: id (int), имя (varchar [255]), слизень (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

ОК, у нас есть данные испытаний. Я хочу знать, как получить все записи с тегом One, но без тега Three (это будут записи 1 и 4).

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

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

edit2 Ответ правильный, но он слишком медленный для практического использования. Я думаю, что единственный способ заставить это работать - использовать стороннюю поисковую систему, такую ​​как Lucene или ElasticSearch.

Ответы [ 3 ]

3 голосов
/ 03 октября 2011

Следующий скрипт выбирает записи, которые имеют теги 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 голос
/ 03 октября 2011

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

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

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 

СЛЕВА, присоединившись к таблице records_tags et2 (данные, которые вам не нужны), затем можно выбрать только те записи, для которых et2.name IS NULL (где запись et2 не существует).

0 голосов
/ 03 октября 2011

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

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 - тег, который вам не нужен)

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

...